mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Edit Book: Implement auto-indent, now when pressing the Enter key to start a new line, the new line's indentation is automatically created
This commit is contained in:
parent
9575a482a7
commit
690b84ce12
@ -6,7 +6,12 @@ from __future__ import (unicode_literals, division, absolute_import,
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
@ -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] + '</'
|
||||
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
|
||||
|
||||
def replace_possible_entity(self, editor):
|
||||
@ -553,3 +606,36 @@ class Smarts(NullSmarts):
|
||||
c.setPosition(c.position() + m.start(), c.KeepAnchor)
|
||||
c.insertText(repl)
|
||||
editor.setTextCursor(c)
|
||||
|
||||
if __name__ == '__main__':
|
||||
from calibre.gui2.tweak_book.editor.widget import launch_editor
|
||||
launch_editor('''\
|
||||
<!DOCTYPE html>
|
||||
<html xml:lang="en" lang="en">
|
||||
<!--
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>A title with a tag <span> in it, the tag is treated as normal text</title>
|
||||
<style type="text/css">
|
||||
body {
|
||||
color: green;
|
||||
font-size: 12pt;
|
||||
}
|
||||
</style>
|
||||
<style type="text/css">p.small { font-size: x-small; color:gray }</style>
|
||||
</head id="invalid attribute on closing tag">
|
||||
<body lang="en_IN"><p:
|
||||
<!-- The start of the actual body text -->
|
||||
<h1 lang="en_US">A heading that should appear in bold, with an <i>italic</i> word</h1>
|
||||
<p>Some text with inline formatting, that is syntax highlighted. A <b>bold</b> word, and an <em>italic</em> word. \
|
||||
<i>Some italic text with a <b>bold-italic</b> word in </i>the middle.</p>
|
||||
<!-- Let's see what exotic constructs like namespace prefixes and empty attributes look like -->
|
||||
<svg:svg xmlns:svg="http://whatever" />
|
||||
<input disabled><input disabled /><span attr=<></span>
|
||||
<!-- Non-breaking spaces are rendered differently from normal spaces, so that they stand out -->
|
||||
<p>Some\xa0words\xa0separated\xa0by\xa0non\u2011breaking\xa0spaces and non\u2011breaking hyphens.</p>
|
||||
<p>Some non-BMP unicode text:\U0001f431\U0001f431\U0001f431</p>
|
||||
</body>
|
||||
</html>
|
||||
''', path_is_raw=True, syntax='xml')
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user