diff --git a/src/calibre/gui2/comments_editor.py b/src/calibre/gui2/comments_editor.py index 8546bc6c0a..024b268241 100644 --- a/src/calibre/gui2/comments_editor.py +++ b/src/calibre/gui2/comments_editor.py @@ -12,12 +12,11 @@ from html5_parser import parse from lxml import html from qt.core import ( QAction, QApplication, QBrush, QByteArray, QCheckBox, QColor, QColorDialog, - QDialog, QDialogButtonBox, QFont, QFontInfo, QFontMetrics, QFormLayout, - QHBoxLayout, QIcon, QKeySequence, QLabel, QLineEdit, QMenu, QPalette, - QPlainTextEdit, QPushButton, QSize, QSyntaxHighlighter, Qt, QTabWidget, - QTextBlockFormat, QTextCharFormat, QTextCursor, QTextEdit, QTextFormat, - QTextListFormat, QToolBar, QToolButton, QUrl, QVBoxLayout, QWidget, pyqtSignal, - pyqtSlot + QDialog, QDialogButtonBox, QFont, QFontInfo, QFontMetrics, QFormLayout, QIcon, + QKeySequence, QLabel, QLineEdit, QMenu, QPalette, QPlainTextEdit, QPushButton, + QSize, QSyntaxHighlighter, Qt, QTabWidget, QTextBlockFormat, QTextCharFormat, + QTextCursor, QTextEdit, QTextFormat, QTextListFormat, QToolButton, QUrl, + QVBoxLayout, QWidget, pyqtSignal, pyqtSlot ) from calibre import xml_replace_entities @@ -26,8 +25,9 @@ from calibre.gui2 import ( NO_URL_FORMATTING, choose_files, error_dialog, gprefs, is_dark_theme ) from calibre.gui2.book_details import css +from calibre.gui2.flow_toolbar import create_flow_toolbar from calibre.gui2.widgets import LineEditECM -from calibre.gui2.widgets2 import to_plain_text, FlowLayout +from calibre.gui2.widgets2 import to_plain_text from calibre.utils.cleantext import clean_xml_chars from calibre.utils.config import tweaks from calibre.utils.imghdr import what @@ -1069,12 +1069,7 @@ class Editor(QWidget): # {{{ def __init__(self, parent=None, one_line_toolbar=False, toolbar_prefs_name=None): QWidget.__init__(self, parent) self.toolbar_prefs_name = toolbar_prefs_name or self.toolbar_prefs_name - self.toolbar1 = QToolBar(self) - self.toolbar2 = QToolBar(self) - self.toolbar3 = QToolBar(self) - for i in range(1, 4): - t = getattr(self, 'toolbar%d'%i) - t.setIconSize(QSize(18, 18)) + self.toolbar = create_flow_toolbar(self, restrict_to_single_line=one_line_toolbar, icon_size=18) self.editor = EditorWidget(self) self.editor.data_changed.connect(self.data_changed) self.set_base_url = self.editor.set_base_url @@ -1090,15 +1085,8 @@ class Editor(QWidget): # {{{ self.wyswyg.layout = l = QVBoxLayout(self.wyswyg) self.setLayout(self._layout) l.setContentsMargins(0, 0, 0, 0) - if one_line_toolbar: - tb = QHBoxLayout() - else: - tb = FlowLayout() - tb.setContentsMargins(0, 0, 0, 0) - l.addLayout(tb) - tb.addWidget(self.toolbar1) - tb.addWidget(self.toolbar2) - tb.addWidget(self.toolbar3) + + l.addWidget(self.toolbar) l.addWidget(self.editor) self._layout.addWidget(self.tabs) self.tabs.addTab(self.wyswyg, _('&Normal view')) @@ -1111,53 +1099,46 @@ class Editor(QWidget): # {{{ if hidden: self.hide_toolbars() - # toolbar1 {{{ - self.toolbar1.addAction(self.editor.action_undo) - self.toolbar1.addAction(self.editor.action_redo) - self.toolbar1.addAction(self.editor.action_select_all) - self.toolbar1.addAction(self.editor.action_remove_format) - self.toolbar1.addAction(self.editor.action_clear) - self.toolbar1.addSeparator() + self.toolbar.add_action(self.editor.action_undo) + self.toolbar.add_action(self.editor.action_redo) + self.toolbar.add_action(self.editor.action_select_all) + self.toolbar.add_action(self.editor.action_remove_format) + self.toolbar.add_action(self.editor.action_clear) + self.toolbar.add_separator() for x in ('copy', 'cut', 'paste'): ac = getattr(self.editor, 'action_'+x) - self.toolbar1.addAction(ac) + self.toolbar.add_action(ac) - self.toolbar1.addSeparator() - self.toolbar1.addAction(self.editor.action_background) - # }}} + self.toolbar.add_separator() + self.toolbar.add_action(self.editor.action_background) + self.toolbar.add_separator() - # toolbar2 {{{ for x in ('', 'un'): ac = getattr(self.editor, 'action_%sordered_list'%x) - self.toolbar2.addAction(ac) - self.toolbar2.addSeparator() + self.toolbar.add_action(ac) + self.toolbar.add_separator() for x in ('superscript', 'subscript', 'indent', 'outdent'): - self.toolbar2.addAction(getattr(self.editor, 'action_' + x)) + self.toolbar.add_action(getattr(self.editor, 'action_' + x)) if x in ('subscript', 'outdent'): - self.toolbar2.addSeparator() + self.toolbar.add_separator() - self.toolbar2.addAction(self.editor.action_block_style) - w = self.toolbar2.widgetForAction(self.editor.action_block_style) - if hasattr(w, 'setPopupMode'): - w.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup) - self.toolbar2.addAction(self.editor.action_insert_link) - self.toolbar2.addAction(self.editor.action_insert_hr) - # }}} + self.toolbar.add_action(self.editor.action_block_style, popup_mode=QToolButton.ToolButtonPopupMode.InstantPopup) + self.toolbar.add_action(self.editor.action_insert_link) + self.toolbar.add_action(self.editor.action_insert_hr) + self.toolbar.add_separator() - # toolbar3 {{{ for x in ('bold', 'italic', 'underline', 'strikethrough'): ac = getattr(self.editor, 'action_'+x) - self.toolbar3.addAction(ac) + self.toolbar.add_action(ac) self.addAction(ac) - self.toolbar3.addSeparator() + self.toolbar.add_separator() for x in ('left', 'center', 'right', 'justified'): ac = getattr(self.editor, 'action_align_'+x) - self.toolbar3.addAction(ac) - self.toolbar3.addSeparator() - self.toolbar3.addAction(self.editor.action_color) - # }}} + self.toolbar.add_action(ac) + self.toolbar.add_separator() + self.toolbar.add_action(self.editor.action_color) self.code_edit.textChanged.connect(self.code_dirtied) self.editor.data_changed.connect(self.wyswyg_dirtied) @@ -1201,14 +1182,10 @@ class Editor(QWidget): # {{{ self.source_dirty = True def hide_toolbars(self): - self.toolbar1.setVisible(False) - self.toolbar2.setVisible(False) - self.toolbar3.setVisible(False) + self.toolbar.setVisible(False) def show_toolbars(self): - self.toolbar1.setVisible(True) - self.toolbar2.setVisible(True) - self.toolbar3.setVisible(True) + self.toolbar.setVisible(True) def toggle_toolbars(self): visible = self.toolbars_visible @@ -1218,7 +1195,7 @@ class Editor(QWidget): # {{{ @property def toolbars_visible(self): - return self.toolbar1.isVisible() or self.toolbar2.isVisible() or self.toolbar3.isVisible() + return self.toolbar.isVisible() @toolbars_visible.setter def toolbars_visible(self, val): @@ -1243,8 +1220,9 @@ class Editor(QWidget): # {{{ if __name__ == '__main__': from calibre.gui2 import Application app = Application([]) - w = Editor() + w = Editor(one_line_toolbar=False) w.resize(800, 600) + w.setWindowFlag(Qt.WindowType.Dialog) w.show() w.html = '''

Test Heading

Test blockquote

He hadn't set out to have an affair, diff --git a/src/calibre/gui2/flow_toolbar.py b/src/calibre/gui2/flow_toolbar.py new file mode 100644 index 0000000000..20b518f021 --- /dev/null +++ b/src/calibre/gui2/flow_toolbar.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8 +# License: GPL v3 Copyright: 2021, Kovid Goyal + + +from qt.core import ( + QPainter, QPoint, QRect, QSize, QSizePolicy, QStyle, QStyleOption, Qt, QToolBar, + QToolButton, QWidget, pyqtSignal +) + + +class Separator(QWidget): + + def __init__(self, icon_size, parent=None): + super().__init__(parent) + self.desired_height = icon_size.height() * 0.85 + + def style_option(self): + opt = QStyleOption() + opt.initFrom(self) + opt.state |= QStyle.StateFlag.State_Horizontal + return opt + + def sizeHint(self): + width = self.style().pixelMetric(QStyle.PixelMetric.PM_ToolBarSeparatorExtent, self.style_option(), self) + return QSize(width, int(self.devicePixelRatioF() * self.desired_height)) + + def paintEvent(self, ev): + p = QPainter(self) + self.style().drawPrimitive(QStyle.PrimitiveElement.PE_IndicatorToolBarSeparator, self.style_option(), p, self) + + +class Button(QToolButton): + + layout_needed = pyqtSignal() + + def __init__(self, action, parent=None): + super().__init__(parent) + self.action = action + self.setAutoRaise(True) + action.changed.connect(self.update_state) + self.update_state() + self.clicked.connect(self.action.trigger) + + def update_state(self): + ac = self.action + self.setIcon(ac.icon()) + self.setToolTip(ac.toolTip() or self.action.text()) + self.setEnabled(ac.isEnabled()) + self.setCheckable(ac.isCheckable()) + self.setChecked(ac.isChecked()) + self.setMenu(ac.menu()) + old = self.isVisible() + self.setVisible(ac.isVisible()) + if self.isVisible() != old: + self.layout_needed.emit() + + +class SingleLineToolBar(QToolBar): + + def __init__(self, parent=None, icon_size=18): + super().__init__(parent) + self.setIconSize(QSize(icon_size, icon_size)) + + def add_action(self, ac, popup_mode=QToolButton.ToolButtonPopupMode.DelayedPopup): + self.addAction(ac) + w = self.widgetForAction(ac) + w.setPopupMode(popup_mode) + + def add_separator(self): + self.addSeparator() + + +class FlowToolBar(QWidget): + + def __init__(self, parent=None, icon_size=18): + super().__init__(parent) + self.icon_size = QSize(icon_size, icon_size) + self.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Preferred) + self.items = [] + self.button_map = {} + self.applied_geometry = QRect(0, 0, 0, 0) + + def add_action(self, ac, popup_mode=QToolButton.ToolButtonPopupMode.DelayedPopup): + w = Button(ac, self) + w.setPopupMode(popup_mode) + w.setIconSize(self.icon_size) + self.button_map[ac] = w + self.items.append(w) + w.layout_needed.connect(self.updateGeometry) + self.updateGeometry() + + def add_separator(self): + self.items.append(Separator(self.icon_size, self)) + self.updateGeometry() + + def smart_spacing(self, horizontal=True): + p = self.parent() + if p is None: + return -1 + if p.isWidgetType(): + which = QStyle.PixelMetric.PM_LayoutHorizontalSpacing if horizontal else QStyle.PixelMetric.PM_LayoutVerticalSpacing + return p.style().pixelMetric(which, None, p) + return p.spacing() + + def hasHeightForWidth(self): + return True + + def heightForWidth(self, width): + return self.do_layout(QRect(0, 0, width, 0), apply_geometry=False) + + def minimumSize(self): + size = QSize() + for item in self.items: + size = size.expandedTo(item.minimumSize()) + return size + sizeHint = minimumSize + + def paintEvent(self, ev): + if self.applied_geometry != self.rect(): + self.do_layout(self.rect(), apply_geometry=True) + super().paintEvent(ev) + + def do_layout(self, rect, apply_geometry=False): + x, y = rect.x(), rect.y() + + line_height = 0 + + def layout_spacing(wid, horizontal=True): + ans = self.smart_spacing(horizontal) + if ans != -1: + return ans + return wid.style().layoutSpacing( + QSizePolicy.ControlType.ToolButton, + QSizePolicy.ControlType.ToolButton, + Qt.Orientation.Horizontal if horizontal else Qt.Orientation.Vertical) + + lines, current_line = [], [] + gmap = {} + for i, wid in enumerate(self.items): + prev_wid = self.items[i - 1] if i else None + if isinstance(wid, Button) and not wid.isVisible(): + continue + isz = wid.sizeHint() + hs, vs = layout_spacing(wid), layout_spacing(wid, False) + + next_x = x + isz.width() + hs + wid.setVisible(True) + if isinstance(wid, Separator) and isinstance(prev_wid, Separator): + wid.setVisible(False) + if next_x - hs > rect.right() and line_height > 0: + if isinstance(prev_wid, Separator): + prev_wid.setVisible(False) + if isinstance(wid, Separator): + wid.setVisible(False) + continue + x = rect.x() + y = y + line_height + vs + next_x = x + isz.width() + hs + lines.append((line_height, current_line)) + current_line = [] + line_height = 0 + if apply_geometry: + gmap[wid] = x, y, isz + x = next_x + line_height = max(line_height, isz.height()) + current_line.append((wid, isz.height())) + + lines.append((line_height, current_line)) + + if apply_geometry: + self.applied_geometry = rect + for line_height, items in lines: + for wid, item_height in items: + x, wy, isz = gmap[wid] + if item_height < line_height: + wy += (line_height - item_height) // 2 + wid.setGeometry(QRect(QPoint(x, wy), isz)) + + return y + line_height - rect.y() + + +def create_flow_toolbar(parent=None, icon_size=18, restrict_to_single_line=False): + if restrict_to_single_line: + return SingleLineToolBar(parent, icon_size) + return FlowToolBar(parent, icon_size)