From 690b84ce1271b69ad0c92a09e336c4c2f4b23d57 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 25 Nov 2014 10:14:02 +0530 Subject: [PATCH] Edit Book: Implement auto-indent, now when pressing the Enter key to start a new line, the new line's indentation is automatically created --- .../gui2/tweak_book/editor/smarts/css.py | 39 +++++++- .../gui2/tweak_book/editor/smarts/html.py | 90 ++++++++++++++++++- .../gui2/tweak_book/editor/smarts/python.py | 4 +- .../gui2/tweak_book/editor/smarts/utils.py | 6 +- src/calibre/gui2/tweak_book/editor/text.py | 8 +- 5 files changed, 137 insertions(+), 10 deletions(-) diff --git a/src/calibre/gui2/tweak_book/editor/smarts/css.py b/src/calibre/gui2/tweak_book/editor/smarts/css.py index 051f31c749..e7cd826d2c 100644 --- a/src/calibre/gui2/tweak_book/editor/smarts/css.py +++ b/src/calibre/gui2/tweak_book/editor/smarts/css.py @@ -6,7 +6,12 @@ from __future__ import (unicode_literals, division, absolute_import, __license__ = 'GPL v3' __copyright__ = '2014, Kovid Goyal ' +from PyQt5.Qt import Qt + from calibre.gui2.tweak_book.editor.smarts import NullSmarts +from calibre.gui2.tweak_book.editor.smarts.utils import ( + no_modifiers, get_leading_whitespace_on_block, get_text_before_cursor, + smart_home, smart_backspace, smart_tab, expand_tabs) def find_rule(raw, rule_address): import tinycss @@ -27,5 +32,37 @@ def find_rule(raw, rule_address): return ans class Smarts(NullSmarts): - pass + + def handle_key_press(self, ev, editor): + key = ev.key() + + if key in (Qt.Key_Enter, Qt.Key_Return) and no_modifiers(ev, Qt.ControlModifier, Qt.AltModifier): + ls = get_leading_whitespace_on_block(editor) + cursor, text = get_text_before_cursor(editor) + if text.rstrip().endswith('{'): + ls += ' ' * editor.tw + editor.textCursor().insertText('\n' + ls) + return True + + if key == Qt.Key_BraceRight: + ls = get_leading_whitespace_on_block(editor) + pls = get_leading_whitespace_on_block(editor, previous=True) + cursor, text = get_text_before_cursor(editor) + if not text.rstrip() and ls >= pls and len(text) > 1: + text = expand_tabs(text, editor.tw)[:-editor.tw] + cursor.insertText(text + '}') + editor.setTextCursor(cursor) + return True + + if key == Qt.Key_Home and smart_home(editor, ev): + return True + + if key == Qt.Key_Tab and smart_tab(editor, ev): + return True + + if key == Qt.Key_Backspace and smart_backspace(editor, ev): + return True + + return False + diff --git a/src/calibre/gui2/tweak_book/editor/smarts/html.py b/src/calibre/gui2/tweak_book/editor/smarts/html.py index 61e873b235..d90b908881 100644 --- a/src/calibre/gui2/tweak_book/editor/smarts/html.py +++ b/src/calibre/gui2/tweak_book/editor/smarts/html.py @@ -18,6 +18,9 @@ from calibre.gui2.tweak_book.editor.syntax.html import ATTR_NAME, ATTR_END, ATTR from calibre.utils.icu import utf16_length from calibre.gui2.tweak_book import tprefs from calibre.gui2.tweak_book.editor.smarts import NullSmarts +from calibre.gui2.tweak_book.editor.smarts.utils import ( + no_modifiers, get_leading_whitespace_on_block, get_text_before_cursor, + get_text_after_cursor, smart_home, smart_backspace, smart_tab, expand_tabs) get_offset = itemgetter(0) PARAGRAPH_SEPARATOR = '\u2029' @@ -274,6 +277,12 @@ entity_pat = re.compile(r'&(#{0,1}[a-zA-Z0-9]{1,8});$') class Smarts(NullSmarts): def __init__(self, *args, **kwargs): + if not hasattr(Smarts, 'regexps_compiled'): + Smarts.regexps_compiled = True + Smarts.tag_pat = re.compile(r'<[^>]+>') + Smarts.closing_tag_pat = re.compile(r'<\s*/[^>]+>') + Smarts.closing_pat = re.compile(r'<\s*/') + Smarts.self_closing_pat = re.compile(r'/\s*>') NullSmarts.__init__(self, *args, **kwargs) self.last_matched_tag = None @@ -533,11 +542,55 @@ class Smarts(NullSmarts): set_style_property(tag, 'text-align', value, editor) def handle_key_press(self, ev, editor): - text = ev.text() + ev_text = ev.text() key = ev.key() - if tprefs['replace_entities_as_typed'] and (key == Qt.Key_Semicolon or ';' in text): + is_xml = editor.syntax == 'xml' + + if tprefs['replace_entities_as_typed'] and (key == Qt.Key_Semicolon or ';' in ev_text): self.replace_possible_entity(editor) return True + + if key in (Qt.Key_Enter, Qt.Key_Return) and no_modifiers(ev, Qt.ControlModifier, Qt.AltModifier): + ls = get_leading_whitespace_on_block(editor) + if ls == ' ': + ls = '' # Do not consider a single leading space as indentation + if is_xml: + count = 0 + for m in self.tag_pat.finditer(get_text_before_cursor(editor)[1]): + text = m.group() + if self.closing_pat.search(text) is not None: + count -= 1 + elif self.self_closing_pat.search(text) is None: + count += 1 + if self.closing_tag_pat.match(get_text_after_cursor(editor)[1].lstrip()): + count -= 1 + if count > 0: + ls += editor.tw * ' ' + editor.textCursor().insertText('\n' + ls) + return True + + if key == Qt.Key_Slash: + cursor, text = get_text_before_cursor(editor) + if not text.rstrip().endswith('<'): + return False + text = expand_tabs(text.rstrip()[:-1], editor.tw) + pls = get_leading_whitespace_on_block(editor, previous=True) + if is_xml and not text.lstrip() and len(text) > 1 and len(text) >= len(pls): + # Auto-dedent + text = text[:-editor.tw] + ' + + + + + A title with a tag <span> in it, the tag is treated as normal text + + + + +

A heading that should appear in bold, with an italic word

+

Some text with inline formatting, that is syntax highlighted. A bold word, and an italic word. \ +Some italic text with a bold-italic word in the middle.

+ + + + +

Some\xa0words\xa0separated\xa0by\xa0non\u2011breaking\xa0spaces and non\u2011breaking hyphens.

+

Some non-BMP unicode text:\U0001f431\U0001f431\U0001f431

+ + +''', path_is_raw=True, syntax='xml') diff --git a/src/calibre/gui2/tweak_book/editor/smarts/python.py b/src/calibre/gui2/tweak_book/editor/smarts/python.py index a5e5648906..f726e16d38 100644 --- a/src/calibre/gui2/tweak_book/editor/smarts/python.py +++ b/src/calibre/gui2/tweak_book/editor/smarts/python.py @@ -35,10 +35,10 @@ class Smarts(NullSmarts): def handle_key_press(self, ev, editor): key = ev.key() - if key == Qt.Key_Tab and smart_tab(editor, ev, tw): + if key == Qt.Key_Tab and smart_tab(editor, ev): return True - elif key == Qt.Key_Backspace and smart_backspace(editor, ev, tw): + elif key == Qt.Key_Backspace and smart_backspace(editor, ev): return True elif key in (Qt.Key_Enter, Qt.Key_Return): diff --git a/src/calibre/gui2/tweak_book/editor/smarts/utils.py b/src/calibre/gui2/tweak_book/editor/smarts/utils.py index 8a5c6d3763..0632d77943 100644 --- a/src/calibre/gui2/tweak_book/editor/smarts/utils.py +++ b/src/calibre/gui2/tweak_book/editor/smarts/utils.py @@ -65,10 +65,11 @@ def smart_home(editor, ev): def expand_tabs(text, tw): return text.replace('\t', ' ' * tw) -def smart_tab(editor, ev, tw): +def smart_tab(editor, ev): cursor, text = get_text_before_cursor(editor) if not text.lstrip(): # cursor is preceded by only whitespace + tw = editor.tw text = expand_tabs(text, tw) spclen = len(text) - (len(text) % tw) + tw cursor.insertText(' ' * spclen) @@ -76,10 +77,11 @@ def smart_tab(editor, ev, tw): return True return False -def smart_backspace(editor, ev, tw): +def smart_backspace(editor, ev): cursor, text = get_text_before_cursor(editor) if text and not text.lstrip(): # cursor is preceded by only whitespace + tw = editor.tw text = expand_tabs(text, tw) spclen = max(0, len(text) - (len(text) % tw) - tw) cursor.insertText(' ' * spclen) diff --git a/src/calibre/gui2/tweak_book/editor/text.py b/src/calibre/gui2/tweak_book/editor/text.py index 5af765338f..d762d67140 100644 --- a/src/calibre/gui2/tweak_book/editor/text.py +++ b/src/calibre/gui2/tweak_book/editor/text.py @@ -139,6 +139,7 @@ class TextEdit(PlainTextEdit): def __init__(self, parent=None, expected_geometry=(100, 50)): PlainTextEdit.__init__(self, parent) self.gutter_width = 0 + self.tw = 2 self.expected_geometry = expected_geometry self.saved_matches = {} self.syntax = None @@ -175,8 +176,8 @@ class TextEdit(PlainTextEdit): self.apply_theme(theme) w = self.fontMetrics() self.space_width = w.width(' ') - tw = self.smarts.override_tab_stop_width if self.smarts.override_tab_stop_width is not None else prefs['editor_tab_stop_width'] - self.setTabStopWidth(tw * self.space_width) + self.tw = self.smarts.override_tab_stop_width if self.smarts.override_tab_stop_width is not None else prefs['editor_tab_stop_width'] + self.setTabStopWidth(self.tw * self.space_width) if dictionaries_changed: self.highlighter.rehighlight() @@ -223,7 +224,8 @@ class TextEdit(PlainTextEdit): if sclass is not None: self.smarts = sclass(self) if self.smarts.override_tab_stop_width is not None: - self.setTabStopWidth(self.smarts.override_tab_stop_width * self.space_width) + self.tw = self.smarts.override_tab_stop_width + self.setTabStopWidth(self.tw * self.space_width) self.setPlainText(unicodedata.normalize('NFC', unicode(text))) if process_template and QPlainTextEdit.find(self, '%CURSOR%'): c = self.textCursor()