diff --git a/src/calibre/gui2/tweak_book/__init__.py b/src/calibre/gui2/tweak_book/__init__.py index c286bed43a..eba535372f 100644 --- a/src/calibre/gui2/tweak_book/__init__.py +++ b/src/calibre/gui2/tweak_book/__init__.py @@ -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 diff --git a/src/calibre/gui2/tweak_book/editor/smart/html.py b/src/calibre/gui2/tweak_book/editor/smart/html.py index 63e0400a48..8af9dba27f 100644 --- a/src/calibre/gui2/tweak_book/editor/smart/html.py +++ b/src/calibre/gui2/tweak_book/editor/smart/html.py @@ -8,14 +8,15 @@ __copyright__ = '2014, Kovid Goyal ' 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

or

tag.'), show=True) + tags = [tag] + for tag in reversed(tags): + set_style_property(tag, 'text-align', value, editor) + diff --git a/src/calibre/gui2/tweak_book/editor/text.py b/src/calibre/gui2/tweak_book/editor/text.py index 465423aa73..28327cabd4 100644 --- a/src/calibre/gui2/tweak_book/editor/text.py +++ b/src/calibre/gui2/tweak_book/editor/text.py @@ -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) diff --git a/src/calibre/gui2/tweak_book/editor/widget.py b/src/calibre/gui2/tweak_book/editor/widget.py index d07b7027f7..4f736ee260 100644 --- a/src/calibre/gui2/tweak_book/editor/widget.py +++ b/src/calibre/gui2/tweak_book/editor/widget.py @@ -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(_('

Background Color

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

Align left

Align the paragraph to the left')) + ac = reg('format-justify-center', _('&Center'), ('format_text', 'justify_center'), 'format-text-justify-center', (), _('Center')) + ac.setToolTip(_('

Center

Center the paragraph')) + ac = reg('format-justify-right', _('Align &right'), ('format_text', 'justify_right'), 'format-text-justify-right', (), _('Align right')) + ac.setToolTip(_('

Align right

Align the paragraph to the right')) + ac = reg('format-justify-fill', _('&Justify'), ('format_text', 'justify_justify'), 'format-text-justify-fill', (), _('Justify')) + ac.setToolTip(_('

Justify

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

Insert image

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: