mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-08-11 09:13:57 -04:00
Edit Book: Add buttons to change the text alignment of the current paragraph. If you do not see them, go to Preferences->Toolbars and add them to the Text formatting actions toolbar.
This commit is contained in:
parent
c952bb5888
commit
251aa384a7
@ -55,8 +55,10 @@ d['global_tools_toolbar'] = ['check-book', 'spell-check-book', 'edit-toc', 'inse
|
|||||||
d['editor_css_toolbar'] = ['pretty-current', 'insert-image']
|
d['editor_css_toolbar'] = ['pretty-current', 'insert-image']
|
||||||
d['editor_xml_toolbar'] = ['pretty-current', 'insert-tag']
|
d['editor_xml_toolbar'] = ['pretty-current', 'insert-tag']
|
||||||
d['editor_html_toolbar'] = ['fix-html-current', 'pretty-current', 'insert-image', 'insert-hyperlink', 'insert-tag', 'change-paragraph']
|
d['editor_html_toolbar'] = ['fix-html-current', 'pretty-current', 'insert-image', 'insert-hyperlink', 'insert-tag', 'change-paragraph']
|
||||||
d['editor_format_toolbar'] = [('format-text-' + x) for x in (
|
d['editor_format_toolbar'] = [('format-text-' + x) if x else x for x in (
|
||||||
'bold', 'italic', 'underline', 'strikethrough', 'subscript', 'superscript', 'color', 'background-color')]
|
'bold', 'italic', 'underline', 'strikethrough', 'subscript', 'superscript',
|
||||||
|
None, 'color', 'background-color', None, 'justify-left', 'justify-center',
|
||||||
|
'justify-right', 'justify-fill')]
|
||||||
d['spell_check_case_sensitive_search'] = False
|
d['spell_check_case_sensitive_search'] = False
|
||||||
d['add_cover_preserve_aspect_ratio'] = False
|
d['add_cover_preserve_aspect_ratio'] = False
|
||||||
del d
|
del d
|
||||||
|
@ -8,14 +8,15 @@ __copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
|
|||||||
|
|
||||||
import sys, re
|
import sys, re
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
from . import NullSmarts
|
|
||||||
|
|
||||||
|
from cssutils import parseStyle
|
||||||
from PyQt4.Qt import QTextEdit
|
from PyQt4.Qt import QTextEdit
|
||||||
|
|
||||||
from calibre import prepare_string_for_xml
|
from calibre import prepare_string_for_xml
|
||||||
from calibre.gui2 import error_dialog
|
from calibre.gui2 import error_dialog
|
||||||
from calibre.gui2.tweak_book.editor.syntax.html import ATTR_NAME, ATTR_END, ATTR_START, ATTR_VALUE
|
from calibre.gui2.tweak_book.editor.syntax.html import ATTR_NAME, ATTR_END, ATTR_START, ATTR_VALUE
|
||||||
from calibre.utils.icu import utf16_length
|
from calibre.utils.icu import utf16_length
|
||||||
|
from calibre.gui2.tweak_book.editor.smart import NullSmarts
|
||||||
|
|
||||||
get_offset = itemgetter(0)
|
get_offset = itemgetter(0)
|
||||||
PARAGRAPH_SEPARATOR = '\u2029'
|
PARAGRAPH_SEPARATOR = '\u2029'
|
||||||
@ -119,6 +120,7 @@ def find_containing_attribute(block, offset):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def find_attribute_in_tag(block, offset, attr_name):
|
def find_attribute_in_tag(block, offset, attr_name):
|
||||||
|
' Return the start of the attribute value as block, offset or None, None if attribute not found '
|
||||||
end_block, boundary = next_tag_boundary(block, offset)
|
end_block, boundary = next_tag_boundary(block, offset)
|
||||||
if boundary.is_start:
|
if boundary.is_start:
|
||||||
return None, None
|
return None, None
|
||||||
@ -140,6 +142,15 @@ def find_attribute_in_tag(block, offset, attr_name):
|
|||||||
found_attr = True
|
found_attr = True
|
||||||
current_offset += 1
|
current_offset += 1
|
||||||
|
|
||||||
|
def find_end_of_attribute(block, offset):
|
||||||
|
' Find the end of an attribute that occurs somewhere after the position specified by (block, offset) '
|
||||||
|
block, boundary = next_attr_boundary(block, offset)
|
||||||
|
if block is None or boundary is None:
|
||||||
|
return None, None
|
||||||
|
if boundary.type is not ATTR_VALUE or boundary.data is not ATTR_END:
|
||||||
|
return None, None
|
||||||
|
return block, boundary.offset
|
||||||
|
|
||||||
def find_closing_tag(tag, max_tags=sys.maxint):
|
def find_closing_tag(tag, max_tags=sys.maxint):
|
||||||
''' Find the closing tag corresponding to the specified tag. To find it we
|
''' Find the closing tag corresponding to the specified tag. To find it we
|
||||||
search for the first closing tag after the specified tag that does not
|
search for the first closing tag after the specified tag that does not
|
||||||
@ -209,11 +220,13 @@ def ensure_not_within_tag_definition(cursor, forward=True):
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def find_closest_containing_block_tag(block, offset, block_tag_names=frozenset((
|
BLOCK_TAG_NAMES = frozenset((
|
||||||
'address', 'article', 'aside', 'blockquote', 'center', 'dir',
|
'address', 'article', 'aside', 'blockquote', 'center', 'dir', 'fieldset',
|
||||||
'fieldset', 'isindex', 'menu', 'noframes', 'hgroup', 'noscript', 'pre',
|
'isindex', 'menu', 'noframes', 'hgroup', 'noscript', 'pre', 'section',
|
||||||
'section', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'p', 'div', 'dd',
|
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'p', 'div', 'dd', 'dl', 'ul',
|
||||||
'dl', 'ul', 'ol', 'li', 'body', 'td', 'th'))):
|
'ol', 'li', 'body', 'td', 'th'))
|
||||||
|
|
||||||
|
def find_closest_containing_block_tag(block, offset, block_tag_names=BLOCK_TAG_NAMES):
|
||||||
while True:
|
while True:
|
||||||
tag = find_closest_containing_tag(block, offset)
|
tag = find_closest_containing_tag(block, offset)
|
||||||
if tag is None:
|
if tag is None:
|
||||||
@ -222,6 +235,31 @@ def find_closest_containing_block_tag(block, offset, block_tag_names=frozenset((
|
|||||||
return tag
|
return tag
|
||||||
block, offset = tag.start_block, tag.start_offset
|
block, offset = tag.start_block, tag.start_offset
|
||||||
|
|
||||||
|
def set_style_property(tag, property_name, value, editor):
|
||||||
|
'''
|
||||||
|
Set a style property, i.e. a CSS property inside the style attribute of the tag.
|
||||||
|
Any existing style attribute is updated or a new attribute is inserted.
|
||||||
|
'''
|
||||||
|
block, offset = find_attribute_in_tag(tag.start_block, tag.start_offset + 1, 'style')
|
||||||
|
c = editor.textCursor()
|
||||||
|
def css(d):
|
||||||
|
return d.cssText.replace('\n', ' ')
|
||||||
|
if block is None or offset is None:
|
||||||
|
d = parseStyle('')
|
||||||
|
d.setProperty(property_name, value)
|
||||||
|
c.setPosition(tag.end_block.position() + tag.end_offset)
|
||||||
|
c.insertText(' style="%s"' % css(d))
|
||||||
|
else:
|
||||||
|
c.setPosition(block.position() + offset - 1)
|
||||||
|
end_block, end_offset = find_end_of_attribute(block, offset + 1)
|
||||||
|
if end_block is None:
|
||||||
|
return error_dialog(editor, _('Invalid markup'), _(
|
||||||
|
'The current block tag has an existing unclosed style attribute. Run the Fix HTML'
|
||||||
|
' tool first.'), show=True)
|
||||||
|
c.setPosition(end_block.position() + end_offset, c.KeepAnchor)
|
||||||
|
d = parseStyle(editor.selected_text_from_cursor(c)[1:-1])
|
||||||
|
d.setProperty(property_name, value)
|
||||||
|
c.insertText('"%s"' % css(d))
|
||||||
|
|
||||||
class HTMLSmarts(NullSmarts):
|
class HTMLSmarts(NullSmarts):
|
||||||
|
|
||||||
@ -427,3 +465,33 @@ class HTMLSmarts(NullSmarts):
|
|||||||
c.setPosition(ctag.start_block.position() + ctag.start_offset, c.KeepAnchor)
|
c.setPosition(ctag.start_block.position() + ctag.start_offset, c.KeepAnchor)
|
||||||
return c
|
return c
|
||||||
|
|
||||||
|
def set_text_alignment(self, editor, value):
|
||||||
|
''' Set the text-align property on the current block tag(s) '''
|
||||||
|
editor.highlighter.join()
|
||||||
|
block_tag_names = BLOCK_TAG_NAMES - {'body'} # ignore body since setting text-align globally on body is almost never what is wanted
|
||||||
|
tags = []
|
||||||
|
c = editor.textCursor()
|
||||||
|
if c.hasSelection():
|
||||||
|
start, end = min(c.anchor(), c.position()), max(c.anchor(), c.position())
|
||||||
|
c.setPosition(start)
|
||||||
|
block = c.block()
|
||||||
|
while block.isValid() and block.position() < end:
|
||||||
|
ud = block.userData()
|
||||||
|
if ud is not None:
|
||||||
|
for tb in ud.tags:
|
||||||
|
if tb.is_start and not tb.closing and tb.name.lower() in block_tag_names:
|
||||||
|
nblock, boundary = next_tag_boundary(block, tb.offset)
|
||||||
|
if boundary is not None and not boundary.is_start and not boundary.self_closing:
|
||||||
|
tags.append(Tag(block, tb, nblock, boundary))
|
||||||
|
block = block.next()
|
||||||
|
if not tags:
|
||||||
|
c = editor.textCursor()
|
||||||
|
block, offset = c.block(), c.positionInBlock()
|
||||||
|
tag = find_closest_containing_block_tag(block, offset, block_tag_names)
|
||||||
|
if tag is None:
|
||||||
|
return error_dialog(editor, _('Not in a block tag'), _(
|
||||||
|
'Cannot change text alignment as the cursor is not inside a block level tag, such as a <p> or <div> tag.'), show=True)
|
||||||
|
tags = [tag]
|
||||||
|
for tag in reversed(tags):
|
||||||
|
set_style_property(tag, 'text-align', value, editor)
|
||||||
|
|
||||||
|
@ -642,6 +642,8 @@ class TextEdit(PlainTextEdit):
|
|||||||
def format_text(self, formatting):
|
def format_text(self, formatting):
|
||||||
if self.syntax != 'html':
|
if self.syntax != 'html':
|
||||||
return
|
return
|
||||||
|
if formatting.startswith('justify_'):
|
||||||
|
return self.smarts.set_text_alignment(self, formatting.partition('_')[-1])
|
||||||
color = 'currentColor'
|
color = 'currentColor'
|
||||||
if formatting in {'color', 'background-color'}:
|
if formatting in {'color', 'background-color'}:
|
||||||
color = QColorDialog.getColor(QColor(Qt.black if formatting == 'color' else Qt.white), self, _('Choose color'), QColorDialog.ShowAlphaChannel)
|
color = QColorDialog.getColor(QColor(Qt.black if formatting == 'color' else Qt.white), self, _('Choose color'), QColorDialog.ShowAlphaChannel)
|
||||||
|
@ -64,6 +64,14 @@ def register_text_editor_actions(_reg, palette):
|
|||||||
ac = reg('format-fill-color', _('&Background Color'), ('format_text', 'background-color'),
|
ac = reg('format-fill-color', _('&Background Color'), ('format_text', 'background-color'),
|
||||||
'format-text-background-color', (), _('Change background color of text'))
|
'format-text-background-color', (), _('Change background color of text'))
|
||||||
ac.setToolTip(_('<h3>Background Color</h3>Change the background color of the selected text'))
|
ac.setToolTip(_('<h3>Background Color</h3>Change the background color of the selected text'))
|
||||||
|
ac = reg('format-justify-left', _('Align &left'), ('format_text', 'justify_left'), 'format-text-justify-left', (), _('Align left'))
|
||||||
|
ac.setToolTip(_('<h3>Align left</h3>Align the paragraph to the left'))
|
||||||
|
ac = reg('format-justify-center', _('&Center'), ('format_text', 'justify_center'), 'format-text-justify-center', (), _('Center'))
|
||||||
|
ac.setToolTip(_('<h3>Center</h3>Center the paragraph'))
|
||||||
|
ac = reg('format-justify-right', _('Align &right'), ('format_text', 'justify_right'), 'format-text-justify-right', (), _('Align right'))
|
||||||
|
ac.setToolTip(_('<h3>Align right</h3>Align the paragraph to the right'))
|
||||||
|
ac = reg('format-justify-fill', _('&Justify'), ('format_text', 'justify_justify'), 'format-text-justify-fill', (), _('Justify'))
|
||||||
|
ac.setToolTip(_('<h3>Justify</h3>Align the paragraph to both the left and right margins'))
|
||||||
|
|
||||||
ac = reg('view-image', _('&Insert image'), ('insert_resource', 'image'), 'insert-image', (), _('Insert an image into the text'), syntaxes=('html', 'css'))
|
ac = reg('view-image', _('&Insert image'), ('insert_resource', 'image'), 'insert-image', (), _('Insert an image into the text'), syntaxes=('html', 'css'))
|
||||||
ac.setToolTip(_('<h3>Insert image</h3>Insert an image into the text'))
|
ac.setToolTip(_('<h3>Insert image</h3>Insert an image into the text'))
|
||||||
@ -86,6 +94,7 @@ def register_text_editor_actions(_reg, palette):
|
|||||||
editor_toolbar_actions['html']['change-paragraph'] = actions['change-paragraph'] = QAction(
|
editor_toolbar_actions['html']['change-paragraph'] = actions['change-paragraph'] = QAction(
|
||||||
QIcon(I('format-text-heading.png')), _('Change paragraph to heading'), ac.parent())
|
QIcon(I('format-text-heading.png')), _('Change paragraph to heading'), ac.parent())
|
||||||
|
|
||||||
|
|
||||||
class Editor(QMainWindow):
|
class Editor(QMainWindow):
|
||||||
|
|
||||||
has_line_numbers = True
|
has_line_numbers = True
|
||||||
@ -258,6 +267,9 @@ class Editor(QMainWindow):
|
|||||||
def populate_toolbars(self):
|
def populate_toolbars(self):
|
||||||
self.tools_bar.clear()
|
self.tools_bar.clear()
|
||||||
def add_action(name, bar):
|
def add_action(name, bar):
|
||||||
|
if name is None:
|
||||||
|
bar.addSeparator()
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
ac = actions[name]
|
ac = actions[name]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user