A better multiline toolbar for the comments editor

This commit is contained in:
Kovid Goyal 2021-11-13 10:08:11 +05:30
parent 796034a8ab
commit b604611a3a
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
2 changed files with 224 additions and 60 deletions

View File

@ -12,12 +12,11 @@ from html5_parser import parse
from lxml import html from lxml import html
from qt.core import ( from qt.core import (
QAction, QApplication, QBrush, QByteArray, QCheckBox, QColor, QColorDialog, QAction, QApplication, QBrush, QByteArray, QCheckBox, QColor, QColorDialog,
QDialog, QDialogButtonBox, QFont, QFontInfo, QFontMetrics, QFormLayout, QDialog, QDialogButtonBox, QFont, QFontInfo, QFontMetrics, QFormLayout, QIcon,
QHBoxLayout, QIcon, QKeySequence, QLabel, QLineEdit, QMenu, QPalette, QKeySequence, QLabel, QLineEdit, QMenu, QPalette, QPlainTextEdit, QPushButton,
QPlainTextEdit, QPushButton, QSize, QSyntaxHighlighter, Qt, QTabWidget, QSize, QSyntaxHighlighter, Qt, QTabWidget, QTextBlockFormat, QTextCharFormat,
QTextBlockFormat, QTextCharFormat, QTextCursor, QTextEdit, QTextFormat, QTextCursor, QTextEdit, QTextFormat, QTextListFormat, QToolButton, QUrl,
QTextListFormat, QToolBar, QToolButton, QUrl, QVBoxLayout, QWidget, pyqtSignal, QVBoxLayout, QWidget, pyqtSignal, pyqtSlot
pyqtSlot
) )
from calibre import xml_replace_entities 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 NO_URL_FORMATTING, choose_files, error_dialog, gprefs, is_dark_theme
) )
from calibre.gui2.book_details import css 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.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.cleantext import clean_xml_chars
from calibre.utils.config import tweaks from calibre.utils.config import tweaks
from calibre.utils.imghdr import what 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): def __init__(self, parent=None, one_line_toolbar=False, toolbar_prefs_name=None):
QWidget.__init__(self, parent) QWidget.__init__(self, parent)
self.toolbar_prefs_name = toolbar_prefs_name or self.toolbar_prefs_name self.toolbar_prefs_name = toolbar_prefs_name or self.toolbar_prefs_name
self.toolbar1 = QToolBar(self) self.toolbar = create_flow_toolbar(self, restrict_to_single_line=one_line_toolbar, icon_size=18)
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.editor = EditorWidget(self) self.editor = EditorWidget(self)
self.editor.data_changed.connect(self.data_changed) self.editor.data_changed.connect(self.data_changed)
self.set_base_url = self.editor.set_base_url self.set_base_url = self.editor.set_base_url
@ -1090,15 +1085,8 @@ class Editor(QWidget): # {{{
self.wyswyg.layout = l = QVBoxLayout(self.wyswyg) self.wyswyg.layout = l = QVBoxLayout(self.wyswyg)
self.setLayout(self._layout) self.setLayout(self._layout)
l.setContentsMargins(0, 0, 0, 0) l.setContentsMargins(0, 0, 0, 0)
if one_line_toolbar:
tb = QHBoxLayout() l.addWidget(self.toolbar)
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.editor) l.addWidget(self.editor)
self._layout.addWidget(self.tabs) self._layout.addWidget(self.tabs)
self.tabs.addTab(self.wyswyg, _('&Normal view')) self.tabs.addTab(self.wyswyg, _('&Normal view'))
@ -1111,53 +1099,46 @@ class Editor(QWidget): # {{{
if hidden: if hidden:
self.hide_toolbars() self.hide_toolbars()
# toolbar1 {{{ self.toolbar.add_action(self.editor.action_undo)
self.toolbar1.addAction(self.editor.action_undo) self.toolbar.add_action(self.editor.action_redo)
self.toolbar1.addAction(self.editor.action_redo) self.toolbar.add_action(self.editor.action_select_all)
self.toolbar1.addAction(self.editor.action_select_all) self.toolbar.add_action(self.editor.action_remove_format)
self.toolbar1.addAction(self.editor.action_remove_format) self.toolbar.add_action(self.editor.action_clear)
self.toolbar1.addAction(self.editor.action_clear) self.toolbar.add_separator()
self.toolbar1.addSeparator()
for x in ('copy', 'cut', 'paste'): for x in ('copy', 'cut', 'paste'):
ac = getattr(self.editor, 'action_'+x) ac = getattr(self.editor, 'action_'+x)
self.toolbar1.addAction(ac) self.toolbar.add_action(ac)
self.toolbar1.addSeparator() self.toolbar.add_separator()
self.toolbar1.addAction(self.editor.action_background) self.toolbar.add_action(self.editor.action_background)
# }}} self.toolbar.add_separator()
# toolbar2 {{{
for x in ('', 'un'): for x in ('', 'un'):
ac = getattr(self.editor, 'action_%sordered_list'%x) ac = getattr(self.editor, 'action_%sordered_list'%x)
self.toolbar2.addAction(ac) self.toolbar.add_action(ac)
self.toolbar2.addSeparator() self.toolbar.add_separator()
for x in ('superscript', 'subscript', 'indent', 'outdent'): 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'): if x in ('subscript', 'outdent'):
self.toolbar2.addSeparator() self.toolbar.add_separator()
self.toolbar2.addAction(self.editor.action_block_style) self.toolbar.add_action(self.editor.action_block_style, popup_mode=QToolButton.ToolButtonPopupMode.InstantPopup)
w = self.toolbar2.widgetForAction(self.editor.action_block_style) self.toolbar.add_action(self.editor.action_insert_link)
if hasattr(w, 'setPopupMode'): self.toolbar.add_action(self.editor.action_insert_hr)
w.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup) self.toolbar.add_separator()
self.toolbar2.addAction(self.editor.action_insert_link)
self.toolbar2.addAction(self.editor.action_insert_hr)
# }}}
# toolbar3 {{{
for x in ('bold', 'italic', 'underline', 'strikethrough'): for x in ('bold', 'italic', 'underline', 'strikethrough'):
ac = getattr(self.editor, 'action_'+x) ac = getattr(self.editor, 'action_'+x)
self.toolbar3.addAction(ac) self.toolbar.add_action(ac)
self.addAction(ac) self.addAction(ac)
self.toolbar3.addSeparator() self.toolbar.add_separator()
for x in ('left', 'center', 'right', 'justified'): for x in ('left', 'center', 'right', 'justified'):
ac = getattr(self.editor, 'action_align_'+x) ac = getattr(self.editor, 'action_align_'+x)
self.toolbar3.addAction(ac) self.toolbar.add_action(ac)
self.toolbar3.addSeparator() self.toolbar.add_separator()
self.toolbar3.addAction(self.editor.action_color) self.toolbar.add_action(self.editor.action_color)
# }}}
self.code_edit.textChanged.connect(self.code_dirtied) self.code_edit.textChanged.connect(self.code_dirtied)
self.editor.data_changed.connect(self.wyswyg_dirtied) self.editor.data_changed.connect(self.wyswyg_dirtied)
@ -1201,14 +1182,10 @@ class Editor(QWidget): # {{{
self.source_dirty = True self.source_dirty = True
def hide_toolbars(self): def hide_toolbars(self):
self.toolbar1.setVisible(False) self.toolbar.setVisible(False)
self.toolbar2.setVisible(False)
self.toolbar3.setVisible(False)
def show_toolbars(self): def show_toolbars(self):
self.toolbar1.setVisible(True) self.toolbar.setVisible(True)
self.toolbar2.setVisible(True)
self.toolbar3.setVisible(True)
def toggle_toolbars(self): def toggle_toolbars(self):
visible = self.toolbars_visible visible = self.toolbars_visible
@ -1218,7 +1195,7 @@ class Editor(QWidget): # {{{
@property @property
def toolbars_visible(self): def toolbars_visible(self):
return self.toolbar1.isVisible() or self.toolbar2.isVisible() or self.toolbar3.isVisible() return self.toolbar.isVisible()
@toolbars_visible.setter @toolbars_visible.setter
def toolbars_visible(self, val): def toolbars_visible(self, val):
@ -1243,8 +1220,9 @@ class Editor(QWidget): # {{{
if __name__ == '__main__': if __name__ == '__main__':
from calibre.gui2 import Application from calibre.gui2 import Application
app = Application([]) app = Application([])
w = Editor() w = Editor(one_line_toolbar=False)
w.resize(800, 600) w.resize(800, 600)
w.setWindowFlag(Qt.WindowType.Dialog)
w.show() w.show()
w.html = '''<h1>Test Heading</h1><blockquote>Test blockquote</blockquote><p><span style="background-color: rgb(0, 255, 255); ">He hadn't w.html = '''<h1>Test Heading</h1><blockquote>Test blockquote</blockquote><p><span style="background-color: rgb(0, 255, 255); ">He hadn't
set <u>out</u> to have an <em>affair</em>, <span style="font-style:italic; background-color:red"> set <u>out</u> to have an <em>affair</em>, <span style="font-style:italic; background-color:red">

View File

@ -0,0 +1,186 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net>
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)