diff --git a/src/calibre/gui2/book_details.py b/src/calibre/gui2/book_details.py index f9214fbd09..6824db280a 100644 --- a/src/calibre/gui2/book_details.py +++ b/src/calibre/gui2/book_details.py @@ -8,9 +8,9 @@ from collections import namedtuple from functools import partial from qt.core import ( QAction, QApplication, QClipboard, QColor, QDialog, QEasingCurve, QIcon, - QKeySequence, QLayout, QMenu, QMimeData, QPainter, QPen, QPixmap, + QKeySequence, QMenu, QMimeData, QPainter, QPen, QPixmap, QPropertyAnimation, QRect, QSize, QSizePolicy, Qt, QUrl, QWidget, pyqtProperty, - pyqtSignal + QTimer, pyqtSignal ) from calibre import fit_image, sanitize_file_name @@ -30,6 +30,7 @@ from calibre.gui2.dialogs.confirm_delete import confirm, confirm as confirm_dele from calibre.gui2.dnd import ( dnd_get_files, dnd_get_image, dnd_has_extension, dnd_has_image, image_extensions ) +from calibre.gui2.widgets import BasicSplitter from calibre.gui2.widgets2 import HTMLDisplay from calibre.utils.config import tweaks from calibre.utils.img import blend_image, image_from_x @@ -548,6 +549,9 @@ class CoverView(QWidget): # {{{ def setCurrentPixmapSize(self, val): self._current_pixmap_size = val + def minimumSizeHint(self): + return QSize(100, 100) + def do_layout(self): if self.rect().width() == 0 or self.rect().height() == 0: return @@ -652,7 +656,7 @@ class CoverView(QWidget): # {{{ book_id = self.data.get('id') if not book_id: return - from calibre.utils.img import image_from_x, remove_borders_from_image + from calibre.utils.img import remove_borders_from_image img = image_from_x(self.pixmap) nimg = remove_borders_from_image(img) if nimg is not img: @@ -910,54 +914,56 @@ class BookInfo(HTMLDisplay): # }}} -class DetailsLayout(QLayout): # {{{ +class DetailsLayout(BasicSplitter): # {{{ def __init__(self, vertical, parent): - QLayout.__init__(self, parent) + orientation = Qt.Orientation.Vertical if vertical else Qt.Orientation.Horizontal + BasicSplitter.__init__(self, orientation, parent) self.vertical = vertical + self.setCollapsible(0, True) + self._children = [] self.min_size = QSize(190, 200) if vertical else QSize(120, 120) self.setContentsMargins(0, 0, 0, 0) + self.restore_geometry(gprefs, 'book_details_splitter') + self.splitterMoved.connect(self.do_splitter_moved) def minimumSize(self): return QSize(self.min_size) - def addItem(self, child): + def addWidget(self, child): if len(self._children) > 2: raise ValueError('This layout can only manage two children') self._children.append(child) - def itemAt(self, i): - try: - return self._children[i] - except: - pass - return None - - def takeAt(self, i): - try: - self._children.pop(i) - except: - pass - return None - def count(self): return len(self._children) def sizeHint(self): return QSize(self.min_size) + def restore_splitter_state(self): + s = gprefs.get('book_details_widget_splitter_state') + if s is not None: + self.restoreState(s) + self.setOrientation(Qt.Orientation.Vertical if self.vertical else Qt.Orientation.Horizontal) + def setGeometry(self, r): - QLayout.setGeometry(self, r) + BasicSplitter.setGeometry(self, r) self.do_layout(r) + self.restore_splitter_state() + + def do_splitter_moved(self, *args): + gprefs['book_details_widget_splitter_state'] = bytearray(self.saveState()) + self._children[0].do_layout() def cover_height(self, r): - if not self._children[0].widget().isVisible(): + if not self._children[0].isVisible(): return 0 mh = min(int(r.height()//2), int(4/3 * r.width())+1) try: - ph = self._children[0].widget().pixmap.height() + ph = self._children[0].pixmap.height() except: ph = 0 if ph > 0: @@ -965,11 +971,11 @@ class DetailsLayout(QLayout): # {{{ return mh def cover_width(self, r): - if not self._children[0].widget().isVisible(): + if not self._children[0].isVisible(): return 0 mw = 1 + int(3/4 * r.height()) try: - pw = self._children[0].widget().pixmap.width() + pw = self._children[0].pixmap.width() except: pw = 0 if pw > 0: @@ -979,7 +985,11 @@ class DetailsLayout(QLayout): # {{{ def do_layout(self, rect): if len(self._children) != 2: return - left, top, right, bottom = self.getContentsMargins() + cm = self.contentsMargins() + left = cm.left() + top = cm.top() + right = cm.right() + bottom = cm.top() r = rect.adjusted(+left, +top, -right, -bottom) x = r.x() y = r.y() @@ -987,20 +997,20 @@ class DetailsLayout(QLayout): # {{{ if self.vertical: ch = self.cover_height(r) cover.setGeometry(QRect(x, y, r.width(), ch)) - cover.widget().do_layout() y += ch + 5 details.setGeometry(QRect(x, y, r.width(), r.height()-ch-5)) else: cw = self.cover_width(r) cover.setGeometry(QRect(x, y, cw, r.height())) - cover.widget().do_layout() x += cw + 5 details.setGeometry(QRect(x, y, r.width() - cw - 5, r.height())) - + self.restore_splitter_state() # only required on first call to do_layout, but ... + self.save_geometry(gprefs, 'book_details_splitter') + cover.do_layout() # }}} -class BookDetails(QWidget): # {{{ +class BookDetails(DetailsLayout): # {{{ show_book_info = pyqtSignal() open_containing_folder = pyqtSignal(int) @@ -1072,11 +1082,10 @@ class BookDetails(QWidget): # {{{ # }}} def __init__(self, vertical, parent=None): - QWidget.__init__(self, parent) + DetailsLayout.__init__(self, vertical, parent) self.last_data = {} self.setAcceptDrops(True) - self._layout = DetailsLayout(vertical, self) - self.setLayout(self._layout) + self._layout = self self.current_path = '' self.cover_view = CoverView(vertical, self) diff --git a/src/calibre/gui2/dialogs/book_info.py b/src/calibre/gui2/dialogs/book_info.py index d2963e4993..6d2d1954b1 100644 --- a/src/calibre/gui2/dialogs/book_info.py +++ b/src/calibre/gui2/dialogs/book_info.py @@ -6,7 +6,7 @@ import textwrap from qt.core import ( QAction, QApplication, QBrush, QCheckBox, QDialog, QGridLayout, QHBoxLayout, QIcon, QKeySequence, QLabel, QListView, QModelIndex, QPalette, - QPixmap, QPushButton, QShortcut, QSize, QSplitter, Qt, QTimer, QToolButton, + QPixmap, QPushButton, QShortcut, QSize, Qt, QTimer, QToolButton, QVBoxLayout, QWidget, pyqtSignal, QDialogButtonBox ) @@ -17,7 +17,7 @@ from calibre.gui2.book_details import ( set_html ) from calibre.gui2.ui import get_gui -from calibre.gui2.widgets import CoverView +from calibre.gui2.widgets import CoverView, BasicSplitter from calibre.gui2.widgets2 import Dialog, HTMLDisplay @@ -136,7 +136,7 @@ class BookInfo(QDialog): QDialog.__init__(self, parent) self.marked = None self.gui = parent - self.splitter = QSplitter(self) + self.splitter = BasicSplitter(self) self._l = l = QVBoxLayout(self) self.setLayout(l) l.addWidget(self.splitter) diff --git a/src/calibre/gui2/dialogs/check_library.py b/src/calibre/gui2/dialogs/check_library.py index e39e889d2b..a5d0607ec9 100644 --- a/src/calibre/gui2/dialogs/check_library.py +++ b/src/calibre/gui2/dialogs/check_library.py @@ -9,7 +9,7 @@ import os import weakref from qt.core import ( QApplication, QCheckBox, QCursor, QDialog, QDialogButtonBox, QGridLayout, - QHBoxLayout, QIcon, QLabel, QLineEdit, QProgressBar, QPushButton, QSplitter, + QHBoxLayout, QIcon, QLabel, QLineEdit, QProgressBar, QPushButton, QStackedLayout, Qt, QTextEdit, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget, pyqtSignal ) @@ -17,6 +17,7 @@ from threading import Thread from calibre import as_unicode, prints from calibre.gui2.dialogs.confirm_delete import confirm +from calibre.gui2.widgets import BasicSplitter from calibre.library.check_library import CHECKS, CheckLibrary from calibre.utils.recycle_bin import delete_file, delete_tree @@ -125,13 +126,14 @@ class CheckLibraryDialog(QDialog): self._tl = QHBoxLayout() self.setLayout(self._tl) - self.splitter = QSplitter(self) + self.splitter = BasicSplitter(self) self.left = QWidget(self) self.splitter.addWidget(self.left) self.helpw = QTextEdit(self) self.splitter.addWidget(self.helpw) self._tl.addWidget(self.splitter) self._layout = QVBoxLayout() + self._layout.setContentsMargins(0, 0, 0, 0) self.left.setLayout(self._layout) self.helpw.setReadOnly(True) self.helpw.setText(_('''\ diff --git a/src/calibre/gui2/init.py b/src/calibre/gui2/init.py index 9f78df508c..900ef01b6b 100644 --- a/src/calibre/gui2/init.py +++ b/src/calibre/gui2/init.py @@ -23,7 +23,7 @@ from calibre.gui2.library.alternate_views import GridView from calibre.gui2.library.views import BooksView, DeviceBooksView from calibre.gui2.notify import get_notifier from calibre.gui2.tag_browser.ui import TagBrowserWidget -from calibre.gui2.widgets import LayoutButton, Splitter +from calibre.gui2.widgets import LayoutButton, Splitter, BasicSplitter from calibre.utils.config import prefs from calibre.utils.icu import sort_key from calibre.utils.localization import localize_website_link @@ -112,10 +112,10 @@ class LibraryViewMixin: # {{{ # }}} -class QuickviewSplitter(QSplitter): # {{{ +class QuickviewSplitter(BasicSplitter): # {{{ def __init__(self, parent=None, orientation=Qt.Orientation.Vertical, qv_widget=None): - QSplitter.__init__(self, parent=parent, orientation=orientation) + BasicSplitter.__init__(self, parent=parent, orientation=orientation) self.splitterMoved.connect(self.splitter_moved) self.setChildrenCollapsible(False) self.qv_widget = qv_widget @@ -124,7 +124,7 @@ class QuickviewSplitter(QSplitter): # {{{ gprefs['quickview_dialog_heights'] = self.sizes() def resizeEvent(self, *args): - QSplitter.resizeEvent(self, *args) + BasicSplitter.resizeEvent(self, *args) if self.sizes()[1] != 0: gprefs['quickview_dialog_heights'] = self.sizes() diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py index b449234017..0525eff188 100644 --- a/src/calibre/gui2/metadata/single.py +++ b/src/calibre/gui2/metadata/single.py @@ -12,7 +12,7 @@ from functools import partial from qt.core import ( QDialog, QDialogButtonBox, QFrame, QGridLayout, QGroupBox, QHBoxLayout, QIcon, QInputDialog, QKeySequence, QMenu, QPushButton, QScrollArea, QShortcut, QSize, - QSizePolicy, QSpacerItem, QSplitter, Qt, QTabWidget, QToolButton, QVBoxLayout, + QSizePolicy, QSpacerItem, Qt, QTabWidget, QToolButton, QVBoxLayout, QWidget, pyqtSignal ) @@ -29,6 +29,7 @@ from calibre.gui2.metadata.basic_widgets import ( TitleSortEdit, show_locked_file_error ) from calibre.gui2.metadata.single_download import FullFetch +from calibre.gui2.widgets import BasicSplitter from calibre.gui2.widgets2 import CenteredToolButton from calibre.library.comments import merge_comments as merge_two_comments from calibre.utils.date import local_tz @@ -731,13 +732,13 @@ class MetadataSingleDialogBase(QDialog): # }}} -class Splitter(QSplitter): +class Splitter(BasicSplitter): frame_resized = pyqtSignal(object) def resizeEvent(self, ev): self.frame_resized.emit(ev) - return QSplitter.resizeEvent(self, ev) + return BasicSplitter.resizeEvent(self, ev) class MetadataSingleDialog(MetadataSingleDialogBase): # {{{ @@ -1039,7 +1040,7 @@ class MetadataSingleDialogAlt1(MetadataSingleDialogBase): # {{{ QSizePolicy.Policy.Expanding)) wgl.addWidget(self.formats_manager) - self.splitter = QSplitter(Qt.Orientation.Horizontal, tab1) + self.splitter = BasicSplitter(Qt.Orientation.Horizontal, tab1) tab1.l.addWidget(self.splitter) self.splitter.addWidget(self.cover) self.splitter.addWidget(wsp) @@ -1207,7 +1208,7 @@ class MetadataSingleDialogAlt2(MetadataSingleDialogBase): # {{{ cover_layout.addLayout(hl) sto(self.cover.buttons[-2], self.cover.buttons[-1]) # Splitter for both cover & formats boxes - self.cover_and_formats = cover_and_formats = QSplitter(Qt.Orientation.Vertical) + self.cover_and_formats = cover_and_formats = BasicSplitter(Qt.Orientation.Vertical) # Put a very small margin on the left so that the word "Cover" doesn't # touch the splitter cover_and_formats.setContentsMargins(1, 0, 0, 0) diff --git a/src/calibre/gui2/metadata/single_download.py b/src/calibre/gui2/metadata/single_download.py index ab07afd0b2..29abe1d871 100644 --- a/src/calibre/gui2/metadata/single_download.py +++ b/src/calibre/gui2/metadata/single_download.py @@ -19,7 +19,7 @@ from qt.core import ( QWidget, QTableView, QGridLayout, QPalette, QTimer, pyqtSignal, QAbstractTableModel, QSize, QListView, QPixmap, QModelIndex, QAbstractListModel, QRect, QTextBrowser, QStringListModel, QMenu, QItemSelectionModel, - QCursor, QHBoxLayout, QPushButton, QSizePolicy, QSplitter, QAbstractItemView) + QCursor, QHBoxLayout, QPushButton, QSizePolicy, QAbstractItemView) from calibre.customize.ui import metadata_plugins from calibre.ebooks.metadata import authors_to_string, rating_to_stars @@ -29,6 +29,7 @@ from calibre.ebooks.metadata.book.base import Metadata from calibre.ebooks.metadata.opf2 import OPF from calibre.gui2 import error_dialog, rating_font, gprefs from calibre.gui2.progress_indicator import SpinAnimator +from calibre.gui2.widgets import BasicSplitter from calibre.gui2.widgets2 import HTMLDisplay from calibre.utils.date import (utcnow, fromordinal, format_date, UNDEFINED_DATE, as_utc) @@ -445,7 +446,7 @@ class IdentifyWidget(QWidget): # {{{ self.top.setWordWrap(True) l.addWidget(self.top) - self.splitter = s = QSplitter(self) + self.splitter = s = BasicSplitter(self) s.setChildrenCollapsible(False) l.addWidget(s, 100) self.results_view = ResultsView(self) diff --git a/src/calibre/gui2/pin_columns.py b/src/calibre/gui2/pin_columns.py index 239830896c..d2f2a11bc8 100644 --- a/src/calibre/gui2/pin_columns.py +++ b/src/calibre/gui2/pin_columns.py @@ -2,10 +2,11 @@ # License: GPLv3 Copyright: 2018, Kovid Goyal -from qt.core import QSplitter, QTableView +from qt.core import QTableView from calibre.gui2.library import DEFAULT_SORT from calibre.gui2 import gprefs +from calibre.gui2.widgets import BasicSplitter class PinTableView(QTableView): @@ -124,10 +125,10 @@ class PinTableView(QTableView): self.apply_state(state) -class PinContainer(QSplitter): +class PinContainer(BasicSplitter): def __init__(self, books_view, parent=None): - QSplitter.__init__(self, parent) + BasicSplitter.__init__(self, parent) self.setChildrenCollapsible(False) self.books_view = books_view self.addWidget(books_view) diff --git a/src/calibre/gui2/preferences/tweaks.py b/src/calibre/gui2/preferences/tweaks.py index 28d16fc808..8770e334b6 100644 --- a/src/calibre/gui2/preferences/tweaks.py +++ b/src/calibre/gui2/preferences/tweaks.py @@ -11,7 +11,7 @@ from calibre import isbytestring, prepare_string_for_xml from calibre.gui2 import error_dialog, info_dialog from calibre.gui2.preferences import AbortCommit, ConfigWidgetBase, test_widget from calibre.gui2.search_box import SearchBox2 -from calibre.gui2.widgets import PythonHighlighter +from calibre.gui2.widgets import PythonHighlighter, BasicSplitter from calibre.utils.config_base import (default_tweaks_raw, exec_tweaks, normalize_tweak, read_custom_tweaks, write_custom_tweaks) @@ -22,7 +22,7 @@ from qt.core import (QAbstractItemView, QAbstractListModel, QApplication, QComboBox, QDialog, QDialogButtonBox, QFont, QGridLayout, QGroupBox, QIcon, QItemSelectionModel, QLabel, QListView, QMenu, QModelIndex, QPlainTextEdit, QPushButton, - QSizePolicy, QSplitter, Qt, QVBoxLayout, QWidget, + QSizePolicy, Qt, QVBoxLayout, QWidget, pyqtSignal) ROOT = QModelIndex() @@ -371,7 +371,7 @@ class ConfigWidget(ConfigWidgetBase): _("Values for the tweaks are shown below. Edit them to change the behavior of calibre." " Your changes will only take effect after a restart of calibre.")) l.addWidget(la), la.setWordWrap(True) - self.splitter = s = QSplitter(self) + self.splitter = s = BasicSplitter(self) s.setChildrenCollapsible(False) l.addWidget(s, 10) diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index d54955df13..afdcdfa8e7 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -434,6 +434,9 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ # layout button. We need to let a book be selected in the book list # before initializing quickview, so run it after an event loop tick QTimer.singleShot(0, self.start_quickview) + # Force repaint of the book details splitter because it otherwise ends + # up with the wrong size. I don't know why. + QTimer.singleShot(0, self.bd_splitter.repaint) def start_quickview(self): from calibre.gui2.actions.show_quickview import get_quickview_action_plugin diff --git a/src/calibre/gui2/widgets.py b/src/calibre/gui2/widgets.py index 562501a085..25fdce5771 100644 --- a/src/calibre/gui2/widgets.py +++ b/src/calibre/gui2/widgets.py @@ -980,13 +980,48 @@ class PythonHighlighter(QSyntaxHighlighter): # {{{ # Splitter {{{ +class BasicSplitterHandle(QSplitterHandle): -class SplitterHandle(QSplitterHandle): + def __init__(self, orientation, splitter): + QSplitterHandle.__init__(self, orientation, splitter) + self.handle_width = splitter.handleWidth() + + def paintEvent(self, event): + rect = event.rect() + painter = QPainter(self) + # draw the separator bar. + painter.setPen(Qt.NoPen) + palette = QApplication.palette() + painter.setBrush(palette.color(QPalette.ColorGroup.Normal, QPalette.ColorRole.AlternateBase)) + painter.drawRect(rect) + # draw the dots + painter.setBrush(palette.color(QPalette.ColorGroup.Normal, QPalette.ColorRole.Shadow)) + horizontal = self.orientation() == Qt.Orientation.Horizontal + dot_count = 6 + dot_size = int(max(1, self.handle_width/2)) + start_point = max(0, int((rect.height()/2 if horizontal else rect.width()/2) - (dot_count*dot_size/2))) + for i in range(dot_count): + # Move the rect to leave 2 dot spaces between the dots + if horizontal: + dot_rect = QRect(1, start_point + i*dot_size*3, dot_size, dot_size) + else: + dot_rect = QRect(start_point + i*dot_size*3, 1, dot_size, dot_size) + painter.drawRect(dot_rect) + painter.end() + + +class BasicSplitter(QSplitter): + + def createHandle(self): + return BasicSplitterHandle(self.orientation(), self) + + +class SplitterHandle(BasicSplitterHandle): double_clicked = pyqtSignal(object) def __init__(self, orientation, splitter): - QSplitterHandle.__init__(self, orientation, splitter) + BasicSplitterHandle.__init__(self, orientation, splitter) splitter.splitterMoved.connect(self.splitter_moved, type=Qt.ConnectionType.QueuedConnection) self.double_clicked.connect(splitter.double_clicked, @@ -1066,7 +1101,7 @@ class LayoutButton(QToolButton): return QToolButton.mouseReleaseEvent(self, ev) -class Splitter(QSplitter): +class Splitter(BasicSplitter): state_changed = pyqtSignal(object) reapply_sizes = pyqtSignal(object) @@ -1075,7 +1110,7 @@ class Splitter(QSplitter): initial_side_size=120, connect_button=True, orientation=Qt.Orientation.Horizontal, side_index=0, parent=None, shortcut=None, hide_handle_on_single_panel=True): - QSplitter.__init__(self, parent) + BasicSplitter.__init__(self, parent) self.reapply_sizes.connect(self.setSizes, type=Qt.ConnectionType.QueuedConnection) self.hide_handle_on_single_panel = hide_handle_on_single_panel if hide_handle_on_single_panel: @@ -1189,7 +1224,7 @@ class Splitter(QSplitter): def do_resize(self, *args): orig = self.desired_side_size - QSplitter.resizeEvent(self, self._resize_ev) + BasicSplitter.resizeEvent(self, self._resize_ev) if orig > 20 and self.desired_show: c = 0 while abs(self.side_index_size - orig) > 10 and c < 5: