19
19
from calibre import prepare_string_for_xml, xml_entity_to_unicode
20
20
from calibre.gui2.tweak_book import tprefs, TOP
21
21
from calibre.gui2.tweak_book.editor import SYNTAX_PROPERTY
22
from calibre.gui2.tweak_book.editor.themes import THEMES, default_theme, theme_color
22
from calibre.gui2.tweak_book.editor.themes import THEMES, default_theme, theme_color, theme_format
23
23
from calibre.gui2.tweak_book.editor.syntax.base import SyntaxHighlighter
24
24
from calibre.gui2.tweak_book.editor.syntax.html import HTMLHighlighter, XMLHighlighter
25
25
from calibre.gui2.tweak_book.editor.syntax.css import CSSHighlighter
26
from calibre.gui2.tweak_book.editor.smart import NullSmarts
27
from calibre.gui2.tweak_book.editor.smart.html import HTMLSmarts
27
29
PARAGRAPH_SEPARATOR = '\u2029'
28
30
entity_pat = re.compile(r'&(#{0,1}[a-zA-Z0-9]{1,8});')
73
75
c.movePosition(c.Start)
74
76
c.movePosition(c.End, c.KeepAnchor)
75
return c.selectedText().replace(PARAGRAPH_SEPARATOR, '\n')
77
ans = c.selectedText().replace(PARAGRAPH_SEPARATOR, '\n')
78
# QTextCursor pads the return value of selectedText with null bytes if
79
# non BMP characters such as 0x1f431 are present.
80
if hasattr(ans, 'rstrip'):
81
ans = ans.rstrip('\0')
83
while ans[-1] == '\0':
94
104
def selected_text(self):
95
return unicodedata.normalize('NFC', unicode(self.textCursor().selectedText()).replace(PARAGRAPH_SEPARATOR, '\n'))
105
return unicodedata.normalize('NFC', unicode(self.textCursor().selectedText()).replace(PARAGRAPH_SEPARATOR, '\n').rstrip('\0'))
97
107
def selection_changed(self):
98
108
# Workaround Qt replacing nbsp with normal spaces on copy
114
124
def __init__(self, parent=None):
115
125
PlainTextEdit.__init__(self, parent)
126
self.saved_matches = {}
127
self.smarts = NullSmarts(self)
116
128
self.current_cursor_line = None
117
129
self.current_search_mark = None
118
130
self.highlighter = SyntaxHighlighter(self)
164
176
pal.setColor(pal.Base, theme_color(theme, 'LineNr', 'bg'))
165
177
pal.setColor(pal.Text, theme_color(theme, 'LineNr', 'fg'))
166
178
pal.setColor(pal.BrightText, theme_color(theme, 'LineNrC', 'fg'))
179
self.match_paren_format = theme_format(theme, 'MatchParen')
167
180
font = self.font()
168
181
ff = tprefs['editor_font_family']
186
199
self.highlighter = get_highlighter(syntax)(self)
187
200
self.highlighter.apply_theme(self.theme)
188
201
self.highlighter.setDocument(self.document())
202
sclass = {'html':HTMLSmarts, 'xml':HTMLSmarts}.get(syntax, None)
203
if sclass is not None:
204
self.smarts = sclass(self)
189
205
self.setPlainText(unicodedata.normalize('NFC', text))
190
206
if process_template and QPlainTextEdit.find(self, '%CURSOR%'):
191
207
c = self.textCursor()
211
227
c.movePosition(c.NextBlock, n=lnum - 1)
212
228
c.movePosition(c.StartOfLine)
213
229
c.movePosition(c.EndOfLine, c.KeepAnchor)
214
text = unicode(c.selectedText())
230
text = unicode(c.selectedText()).rstrip('\0')
216
232
c.movePosition(c.StartOfLine)
217
233
lt = text.lstrip()
232
248
sel.append(self.current_cursor_line)
233
249
if self.current_search_mark is not None:
234
250
sel.append(self.current_search_mark)
251
sel.extend(self.smarts.get_extra_selections(self))
235
252
self.setExtraSelections(sel)
237
254
# Search and replace {{{
248
265
self.current_search_mark = None
249
266
self.update_extra_selections()
251
def find_in_marked(self, pat, wrap=False):
268
def find_in_marked(self, pat, wrap=False, save_match=None):
252
269
if self.current_search_mark is None:
254
271
csm = self.current_search_mark.cursor
266
283
pos = m_end if reverse else m_start
267
284
c.setPosition(pos, c.KeepAnchor)
268
raw = unicode(c.selectedText()).replace(PARAGRAPH_SEPARATOR, '\n')
285
raw = unicode(c.selectedText()).replace(PARAGRAPH_SEPARATOR, '\n').rstrip('\0')
269
286
m = pat.search(raw)
290
307
self.setTextCursor(c)
291
308
# Center search result on screen
292
309
self.centerCursor()
310
if save_match is not None:
311
self.saved_matches[save_match] = m
295
314
def all_in_marked(self, pat, template=None):
296
315
if self.current_search_mark is None:
298
317
c = self.current_search_mark.cursor
299
raw = unicode(c.selectedText()).replace(PARAGRAPH_SEPARATOR, '\n')
318
raw = unicode(c.selectedText()).replace(PARAGRAPH_SEPARATOR, '\n').rstrip('\0')
300
319
if template is None:
301
320
count = len(pat.findall(raw))
308
327
self.update_extra_selections()
311
def find(self, pat, wrap=False, marked=False, complete=False):
330
def find(self, pat, wrap=False, marked=False, complete=False, save_match=None):
313
return self.find_in_marked(pat, wrap=wrap)
332
return self.find_in_marked(pat, wrap=wrap, save_match=save_match)
314
333
reverse = pat.flags & regex.REVERSE
315
334
c = self.textCursor()
316
335
c.clearSelection()
321
340
if wrap and not complete:
322
341
pos = c.End if reverse else c.Start
323
342
c.movePosition(pos, c.KeepAnchor)
324
raw = unicode(c.selectedText()).replace(PARAGRAPH_SEPARATOR, '\n')
343
raw = unicode(c.selectedText()).replace(PARAGRAPH_SEPARATOR, '\n').rstrip('\0')
325
344
m = pat.search(raw)
345
364
self.setTextCursor(c)
346
365
# Center search result on screen
347
366
self.centerCursor()
367
if save_match is not None:
368
self.saved_matches[save_match] = m
350
def replace(self, pat, template):
371
def replace(self, pat, template, saved_match='gui'):
351
372
c = self.textCursor()
352
raw = unicode(c.selectedText()).replace(PARAGRAPH_SEPARATOR, '\n')
373
raw = unicode(c.selectedText()).replace(PARAGRAPH_SEPARATOR, '\n').rstrip('\0')
353
374
m = pat.fullmatch(raw)
376
# This can happen if either the user changed the selected text or
377
# the search expression uses lookahead/lookbehind operators. See if
378
# the saved match matches the currently selected text and
380
if saved_match is not None and saved_match in self.saved_matches:
381
saved = self.saved_matches.pop(saved_match)
382
if saved.group() == raw:
356
386
text = m.expand(template)
357
387
c.insertText(text)
455
485
if ev.type() == ev.ToolTip:
456
486
self.show_tooltip(ev)
458
if ev.type() == ev.ShortcutOverride and ev in (
459
QKeySequence.Copy, QKeySequence.Cut, QKeySequence.Paste):
460
# Let the global cut/copy/paste shortcuts work,this avoids the nbsp
461
# problem as well, since they use the overridden copy() method
462
# instead of the one from Qt
488
if ev.type() == ev.ShortcutOverride:
490
# Let the global cut/copy/paste shortcuts work,this avoids the nbsp
491
# problem as well, since they use the overridden copy() method
492
# instead of the one from Qt
493
QKeySequence.Copy, QKeySequence.Cut, QKeySequence.Paste,
495
# This is used to convert typed hex codes into unicode
497
ev.key() == Qt.Key_X and ev.modifiers() == Qt.AltModifier
465
501
return QPlainTextEdit.event(self, ev)
534
570
c = self.textCursor()
535
571
c.setPosition(left)
536
572
c.setPosition(right, c.KeepAnchor)
537
prev_text = unicode(c.selectedText())
573
prev_text = unicode(c.selectedText()).rstrip('\0')
538
574
c.insertText(prefix + prev_text + suffix)
540
576
right = c.position()
566
602
self.setTextCursor(c)
568
604
def keyPressEvent(self, ev):
605
if ev.key() == Qt.Key_X and ev.modifiers() == Qt.AltModifier:
606
if self.replace_possible_unicode_sequence():
569
609
QPlainTextEdit.keyPressEvent(self, ev)
570
610
if (ev.key() == Qt.Key_Semicolon or ';' in unicode(ev.text())) and tprefs['replace_entities_as_typed'] and self.syntax == 'html':
571
611
self.replace_possible_entity()
613
def replace_possible_unicode_sequence(self):
614
c = self.textCursor()
615
has_selection = c.hasSelection()
617
text = unicode(c.selectedText()).rstrip('\0')
619
c.setPosition(c.position() - min(c.positionInBlock(), 6), c.KeepAnchor)
620
text = unicode(c.selectedText()).rstrip('\0')
621
m = re.search(r'[a-fA-F0-9]{2,6}$', text)
629
if num > 0x10ffff or num < 1:
631
from calibre.gui2.tweak_book.char_select import chr
632
end_pos = max(c.anchor(), c.position())
633
c.setPosition(end_pos - len(text)), c.setPosition(end_pos, c.KeepAnchor)
634
c.insertText(chr(num))
573
637
def replace_possible_entity(self):
574
638
c = self.textCursor()
575
639
c.setPosition(c.position() - min(c.positionInBlock(), 10), c.KeepAnchor)
576
text = unicode(c.selectedText())
640
text = unicode(c.selectedText()).rstrip('\0')
577
641
m = entity_pat.search(text)
590
654
c.movePosition(c.End, c.KeepAnchor)
591
655
self.setTextCursor(c)
657
def rename_block_tag(self, new_name):
658
if hasattr(self.smarts, 'rename_block_tag'):
659
self.smarts.rename_block_tag(self, new_name)