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:
Kovid Goyal 2014-07-07 17:35:55 +05:30
parent c952bb5888
commit 251aa384a7
4 changed files with 92 additions and 8 deletions

View File

@ -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_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_format_toolbar'] = [('format-text-' + x) for x in (
'bold', 'italic', 'underline', 'strikethrough', 'subscript', 'superscript', 'color', 'background-color')]
d['editor_format_toolbar'] = [('format-text-' + x) if x else x for x in (
'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['add_cover_preserve_aspect_ratio'] = False
del d

View File

@ -8,14 +8,15 @@ __copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
import sys, re
from operator import itemgetter
from . import NullSmarts
from cssutils import parseStyle
from PyQt4.Qt import QTextEdit
from calibre import prepare_string_for_xml
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.utils.icu import utf16_length
from calibre.gui2.tweak_book.editor.smart import NullSmarts
get_offset = itemgetter(0)
PARAGRAPH_SEPARATOR = '\u2029'
@ -119,6 +120,7 @@ def find_containing_attribute(block, offset):
return None
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)
if boundary.is_start:
return None, None
@ -140,6 +142,15 @@ def find_attribute_in_tag(block, offset, attr_name):
found_attr = True
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):
''' 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
@ -209,11 +220,13 @@ def ensure_not_within_tag_definition(cursor, forward=True):
return False
def find_closest_containing_block_tag(block, offset, block_tag_names=frozenset((
'address', 'article', 'aside', 'blockquote', 'center', 'dir',
'fieldset', 'isindex', 'menu', 'noframes', 'hgroup', 'noscript', 'pre',
'section', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'p', 'div', 'dd',
'dl', 'ul', 'ol', 'li', 'body', 'td', 'th'))):
BLOCK_TAG_NAMES = frozenset((
'address', 'article', 'aside', 'blockquote', 'center', 'dir', 'fieldset',
'isindex', 'menu', 'noframes', 'hgroup', 'noscript', 'pre', 'section',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'p', 'div', 'dd', 'dl', 'ul',
'ol', 'li', 'body', 'td', 'th'))
def find_closest_containing_block_tag(block, offset, block_tag_names=BLOCK_TAG_NAMES):
while True:
tag = find_closest_containing_tag(block, offset)
if tag is None:
@ -222,6 +235,31 @@ def find_closest_containing_block_tag(block, offset, block_tag_names=frozenset((
return tag
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):
@ -427,3 +465,33 @@ class HTMLSmarts(NullSmarts):
c.setPosition(ctag.start_block.position() + ctag.start_offset, c.KeepAnchor)
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)

View File

@ -642,6 +642,8 @@ class TextEdit(PlainTextEdit):
def format_text(self, formatting):
if self.syntax != 'html':
return
if formatting.startswith('justify_'):
return self.smarts.set_text_alignment(self, formatting.partition('_')[-1])
color = 'currentColor'
if formatting in {'color', 'background-color'}:
color = QColorDialog.getColor(QColor(Qt.black if formatting == 'color' else Qt.white), self, _('Choose color'), QColorDialog.ShowAlphaChannel)

View File

@ -64,6 +64,14 @@ def register_text_editor_actions(_reg, palette):
ac = reg('format-fill-color', _('&Background Color'), ('format_text', 'background-color'),
'format-text-background-color', (), _('Change background color of 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.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(
QIcon(I('format-text-heading.png')), _('Change paragraph to heading'), ac.parent())
class Editor(QMainWindow):
has_line_numbers = True
@ -258,6 +267,9 @@ class Editor(QMainWindow):
def populate_toolbars(self):
self.tools_bar.clear()
def add_action(name, bar):
if name is None:
bar.addSeparator()
return
try:
ac = actions[name]
except KeyError: