~ubuntu-branches/debian/sid/calibre/sid

« back to all changes in this revision

Viewing changes to src/calibre/gui2/tweak_book/editor/text.py

  • Committer: Package Import Robot
  • Author(s): Martin Pitt
  • Date: 2014-02-27 07:48:06 UTC
  • mto: This revision was merged to the branch mainline in revision 74.
  • Revision ID: package-import@ubuntu.com-20140227074806-64wdebb3ptosxhhx
Tags: upstream-1.25.0+dfsg
ImportĀ upstreamĀ versionĀ 1.25.0+dfsg

Show diffs side-by-side

added added

removed removed

Lines of Context:
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
26
28
 
27
29
PARAGRAPH_SEPARATOR = '\u2029'
28
30
entity_pat = re.compile(r'&(#{0,1}[a-zA-Z0-9]{1,8});')
72
74
        c.clearSelection()
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')
 
82
        else:  # QString
 
83
            while ans[-1] == '\0':
 
84
                ans.chop(1)
 
85
        return ans
76
86
 
77
87
    @pyqtSlot()
78
88
    def copy(self):
92
102
 
93
103
    @property
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'))
96
106
 
97
107
    def selection_changed(self):
98
108
        # Workaround Qt replacing nbsp with normal spaces on copy
113
123
 
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']
169
182
        if ff is None:
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')
215
231
        if col is None:
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)
236
253
 
237
254
    # Search and replace {{{
248
265
            self.current_search_mark = None
249
266
        self.update_extra_selections()
250
267
 
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:
253
270
            return False
254
271
        csm = self.current_search_mark.cursor
265
282
        if wrap:
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)
270
287
        if m is None:
271
288
            return False
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
293
312
        return True
294
313
 
295
314
    def all_in_marked(self, pat, template=None):
296
315
        if self.current_search_mark is None:
297
316
            return 0
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))
302
321
        else:
308
327
                self.update_extra_selections()
309
328
        return count
310
329
 
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):
312
331
        if marked:
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)
326
345
        if m is None:
327
346
            return False
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
348
369
        return True
349
370
 
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)
354
375
        if m is None:
 
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
 
379
            # use it, if so.
 
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:
 
383
                    m = saved
 
384
        if m is None:
355
385
            return False
356
386
        text = m.expand(template)
357
387
        c.insertText(text)
455
485
        if ev.type() == ev.ToolTip:
456
486
            self.show_tooltip(ev)
457
487
            return True
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
463
 
            ev.ignore()
464
 
            return False
 
488
        if ev.type() == ev.ShortcutOverride:
 
489
            if ev in (
 
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,
 
494
            ) or (
 
495
                # This is used to convert typed hex codes into unicode
 
496
                # characters
 
497
                ev.key() == Qt.Key_X and ev.modifiers() == Qt.AltModifier
 
498
            ):
 
499
                ev.ignore()
 
500
                return False
465
501
        return QPlainTextEdit.event(self, ev)
466
502
 
467
503
    # Tooltips {{{
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)
539
575
        if prev_text:
540
576
            right = c.position()
566
602
        self.setTextCursor(c)
567
603
 
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():
 
607
                ev.accept()
 
608
                return
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()
572
612
 
 
613
    def replace_possible_unicode_sequence(self):
 
614
        c = self.textCursor()
 
615
        has_selection = c.hasSelection()
 
616
        if has_selection:
 
617
            text = unicode(c.selectedText()).rstrip('\0')
 
618
        else:
 
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)
 
622
        if m is None:
 
623
            return False
 
624
        text = m.group()
 
625
        try:
 
626
            num = int(text, 16)
 
627
        except ValueError:
 
628
            return False
 
629
        if num > 0x10ffff or num < 1:
 
630
            return False
 
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))
 
635
        return True
 
636
 
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)
578
642
        if m is None:
579
643
            return
590
654
        c.movePosition(c.End, c.KeepAnchor)
591
655
        self.setTextCursor(c)
592
656
 
 
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)
 
660