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'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
|
__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 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):
|
def find_rule(raw, rule_address):
|
||||||
import tinycss
|
import tinycss
|
||||||
@ -27,5 +32,37 @@ def find_rule(raw, rule_address):
|
|||||||
return ans
|
return ans
|
||||||
|
|
||||||
class Smarts(NullSmarts):
|
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.utils.icu import utf16_length
|
||||||
from calibre.gui2.tweak_book import tprefs
|
from calibre.gui2.tweak_book import tprefs
|
||||||
from calibre.gui2.tweak_book.editor.smarts import NullSmarts
|
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)
|
get_offset = itemgetter(0)
|
||||||
PARAGRAPH_SEPARATOR = '\u2029'
|
PARAGRAPH_SEPARATOR = '\u2029'
|
||||||
@ -274,6 +277,12 @@ entity_pat = re.compile(r'&(#{0,1}[a-zA-Z0-9]{1,8});$')
|
|||||||
class Smarts(NullSmarts):
|
class Smarts(NullSmarts):
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
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)
|
NullSmarts.__init__(self, *args, **kwargs)
|
||||||
self.last_matched_tag = None
|
self.last_matched_tag = None
|
||||||
|
|
||||||
@ -533,11 +542,55 @@ class Smarts(NullSmarts):
|
|||||||
set_style_property(tag, 'text-align', value, editor)
|
set_style_property(tag, 'text-align', value, editor)
|
||||||
|
|
||||||
def handle_key_press(self, ev, editor):
|
def handle_key_press(self, ev, editor):
|
||||||
text = ev.text()
|
ev_text = ev.text()
|
||||||
key = ev.key()
|
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)
|
self.replace_possible_entity(editor)
|
||||||
return True
|
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
|
return False
|
||||||
|
|
||||||
def replace_possible_entity(self, editor):
|
def replace_possible_entity(self, editor):
|
||||||
@ -553,3 +606,36 @@ class Smarts(NullSmarts):
|
|||||||
c.setPosition(c.position() + m.start(), c.KeepAnchor)
|
c.setPosition(c.position() + m.start(), c.KeepAnchor)
|
||||||
c.insertText(repl)
|
c.insertText(repl)
|
||||||
editor.setTextCursor(c)
|
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):
|
def handle_key_press(self, ev, editor):
|
||||||
key = ev.key()
|
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
|
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
|
return True
|
||||||
|
|
||||||
elif key in (Qt.Key_Enter, Qt.Key_Return):
|
elif key in (Qt.Key_Enter, Qt.Key_Return):
|
||||||
|
@ -65,10 +65,11 @@ def smart_home(editor, ev):
|
|||||||
def expand_tabs(text, tw):
|
def expand_tabs(text, tw):
|
||||||
return text.replace('\t', ' ' * tw)
|
return text.replace('\t', ' ' * tw)
|
||||||
|
|
||||||
def smart_tab(editor, ev, tw):
|
def smart_tab(editor, ev):
|
||||||
cursor, text = get_text_before_cursor(editor)
|
cursor, text = get_text_before_cursor(editor)
|
||||||
if not text.lstrip():
|
if not text.lstrip():
|
||||||
# cursor is preceded by only whitespace
|
# cursor is preceded by only whitespace
|
||||||
|
tw = editor.tw
|
||||||
text = expand_tabs(text, tw)
|
text = expand_tabs(text, tw)
|
||||||
spclen = len(text) - (len(text) % tw) + tw
|
spclen = len(text) - (len(text) % tw) + tw
|
||||||
cursor.insertText(' ' * spclen)
|
cursor.insertText(' ' * spclen)
|
||||||
@ -76,10 +77,11 @@ def smart_tab(editor, ev, tw):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def smart_backspace(editor, ev, tw):
|
def smart_backspace(editor, ev):
|
||||||
cursor, text = get_text_before_cursor(editor)
|
cursor, text = get_text_before_cursor(editor)
|
||||||
if text and not text.lstrip():
|
if text and not text.lstrip():
|
||||||
# cursor is preceded by only whitespace
|
# cursor is preceded by only whitespace
|
||||||
|
tw = editor.tw
|
||||||
text = expand_tabs(text, tw)
|
text = expand_tabs(text, tw)
|
||||||
spclen = max(0, len(text) - (len(text) % tw) - tw)
|
spclen = max(0, len(text) - (len(text) % tw) - tw)
|
||||||
cursor.insertText(' ' * spclen)
|
cursor.insertText(' ' * spclen)
|
||||||
|
@ -139,6 +139,7 @@ class TextEdit(PlainTextEdit):
|
|||||||
def __init__(self, parent=None, expected_geometry=(100, 50)):
|
def __init__(self, parent=None, expected_geometry=(100, 50)):
|
||||||
PlainTextEdit.__init__(self, parent)
|
PlainTextEdit.__init__(self, parent)
|
||||||
self.gutter_width = 0
|
self.gutter_width = 0
|
||||||
|
self.tw = 2
|
||||||
self.expected_geometry = expected_geometry
|
self.expected_geometry = expected_geometry
|
||||||
self.saved_matches = {}
|
self.saved_matches = {}
|
||||||
self.syntax = None
|
self.syntax = None
|
||||||
@ -175,8 +176,8 @@ class TextEdit(PlainTextEdit):
|
|||||||
self.apply_theme(theme)
|
self.apply_theme(theme)
|
||||||
w = self.fontMetrics()
|
w = self.fontMetrics()
|
||||||
self.space_width = w.width(' ')
|
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.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.setTabStopWidth(self.tw * self.space_width)
|
||||||
if dictionaries_changed:
|
if dictionaries_changed:
|
||||||
self.highlighter.rehighlight()
|
self.highlighter.rehighlight()
|
||||||
|
|
||||||
@ -223,7 +224,8 @@ class TextEdit(PlainTextEdit):
|
|||||||
if sclass is not None:
|
if sclass is not None:
|
||||||
self.smarts = sclass(self)
|
self.smarts = sclass(self)
|
||||||
if self.smarts.override_tab_stop_width is not None:
|
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)))
|
self.setPlainText(unicodedata.normalize('NFC', unicode(text)))
|
||||||
if process_template and QPlainTextEdit.find(self, '%CURSOR%'):
|
if process_template and QPlainTextEdit.find(self, '%CURSOR%'):
|
||||||
c = self.textCursor()
|
c = self.textCursor()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user