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_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

View File

@ -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)

View File

@ -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)

View File

@ -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: