| Server IP : 35.80.110.71 / Your IP : 216.73.216.221 Web Server : Apache/2.4.58 (Ubuntu) System : Linux ip-172-31-21-44 6.17.0-1019-aws #19~24.04.1-Ubuntu SMP Tue Jun 23 18:53:06 UTC 2026 x86_64 User : ubuntu ( 1000) PHP Version : 8.3.31 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /lib/calibre/calibre/gui2/ |
Upload File : |
#!/usr/bin/env python3
# License: GPLv3 Copyright: 2023, Kovid Goyal <kovid at kovidgoyal.net>
from contextlib import suppress
from copy import copy
from dataclasses import asdict, dataclass, fields
from enum import Enum, auto
from qt.core import (
QAction, QDialog, QHBoxLayout, QIcon, QKeySequence, QLabel, QMainWindow, QPalette,
QPointF, QSize, QSizePolicy, QStyle, QStyleOption, QStylePainter, Qt, QToolButton,
QVBoxLayout, QWidget, pyqtSignal,
)
from calibre.gui2 import Application, config, gprefs
from calibre.gui2.cover_flow import MIN_SIZE
HIDE_THRESHOLD = 10
SHOW_THRESHOLD = 50
def migrate_settings(width=-1, height=-1):
if width < 100:
width = 1200
if height < 100:
height = 600
wd, nd = WideDesires(), NarrowDesires()
ans = {
'layout': config['gui_layout'],
'wide_visibility': Visibility().serialize(),
'narrow_visibility': Visibility().serialize(),
'wide_desires': wd.serialize(),
'narrow_desires': nd.serialize(),
}
sizes = {
'wide': {
'tag_browser': int(width * wd.tag_browser_width),
'book_details': int(width * wd.book_details_width),
'cover_browser': int(height * wd.cover_browser_height),
'quick_view': int(height * wd.quick_view_height),
},
'narrow': {
'tag_browser': int(width * nd.tag_browser_width),
'book_details': int(height * nd.book_details_height),
'cover_browser': int(width * nd.cover_browser_width),
'quick_view': int(height * nd.quick_view_height),
}
}
for which, hor_is_wide in {
'tag_browser': True,
'book_details': True,
'cover_browser': False,
}.items():
key = 'wide' if hor_is_wide else 'narrow'
val = gprefs.get(f'{which}_splitter_horizontal_state')
if val:
with suppress(Exception):
ans[f'{key}_visibility'][which] = bool(val[0])
sizes[key][which] = val[1]
key = 'narrow' if hor_is_wide else 'wide'
val = gprefs.get(f'{which}_splitter_vertical_state')
if val:
with suppress(Exception):
ans[f'{key}_visibility'][which] = bool(val[0])
sizes[key][which] = val[1]
if gprefs.get('quickview visible'):
ans['wide_visibility']['quick_view'] = ans['narrow_visibility']['quick_view'] = True
qdh = gprefs.get('quickview_dialog_heights') or (int(2*height/3), int(height/3))
# Migrate wide sizes
s, a = sizes['wide'], ans['wide_desires']
a['tag_browser_width'] = min(0.45, s['tag_browser'] / width)
a['book_details_width'] = min(0.45, s['book_details'] / width)
theight = s['cover_browser'] + qdh[0] + qdh[1]
if theight == s['cover_browser']:
theight *= 3
a['cover_browser_height'] = s['cover_browser'] / theight
a['quick_view_height'] = qdh[1] / theight
# Migrate narrow sizes
s, a = sizes['narrow'], ans['narrow_desires']
a['tag_browser_width'] = min(0.45, s['tag_browser'] / width)
a['cover_browser_width'] = min(0.45, s['cover_browser'] / width)
theight = s['book_details'] + qdh[0] + qdh[1]
if theight == s['book_details']:
theight *= 3
a['book_details_height'] = s['book_details'] / theight
a['quick_view_height'] = qdh[1] / theight
return ans
class Placeholder(QLabel):
backgrounds = 'yellow', 'lightgreen', 'grey', 'cyan', 'magenta'
bgcount = 0
def __init__(self, text, parent=None):
super().__init__(text, parent)
bg = self.backgrounds[Placeholder.bgcount]
Placeholder.bgcount = (Placeholder.bgcount + 1) % len(self.backgrounds)
self.setStyleSheet(f'QLabel {{ background: {bg};\nborder: 1px solid red; }}')
class LayoutButton(QToolButton):
on_action_trigger = pyqtSignal(bool)
def __init__(self, name: str, icon: str, label: str, central: 'CentralContainer', shortcut=None):
super().__init__(central)
self.central = central
self.label = label
self.name = name
self.shortcut = shortcut
self.setIcon(QIcon.ic(icon))
self.setCheckable(True)
self.setChecked(self.is_visible)
self.setCursor(Qt.CursorShape.PointingHandCursor)
if isinstance(central, CentralContainer):
self.toggled.connect(central.layout_button_toggled)
def initialize_with_gui(self, gui):
if self.shortcut is not None:
self.action_toggle = QAction(self.icon(), _('Toggle') + ' ' + self.label, self)
self.action_toggle.changed.connect(self.update_shortcut)
self.action_toggle.triggered.connect(self.toggle)
gui.addAction(self.action_toggle)
gui.keyboard.register_shortcut(
f'toggle_central_panel_{self.name}', self.action_toggle.text(), group=_('Main window layout'),
default_keys=(self.shortcut,), action=self.action_toggle)
@property
def is_visible(self):
return getattr(self.central.is_visible, self.name)
def update_shortcut(self, action_toggle=None):
if not isinstance(action_toggle, QAction):
action_toggle = getattr(self, 'action_toggle', None)
if action_toggle:
sc = ', '.join(sc.toString(QKeySequence.SequenceFormat.NativeText)
for sc in action_toggle.shortcuts())
self.shortcut = sc or ''
self.update_text()
def update_text(self):
t = _('Hide {}') if self.isChecked() else _('Show {}')
t = t.format(self.label)
if self.shortcut:
t += f' [{self.shortcut}]'
self.setText(t), self.setToolTip(t), self.setStatusTip(t)
def set_state_to_show(self, *args):
self.setChecked(False)
self.update_text()
def set_state_to_hide(self, *args):
self.setChecked(True)
self.update_text()
def update_state(self, *args):
self.set_state_to_show() if self.is_visible else self.set_state_to_hide()
def mouseReleaseEvent(self, ev):
if ev.button() == Qt.MouseButton.RightButton:
from calibre.gui2.ui import get_gui
gui = get_gui()
if self.name == 'search':
gui.iactions['Preferences'].do_config(initial_plugin=('Interface', 'Search'), close_after_initial=True)
ev.accept()
return
tab_name = {'book_details':'book_details', 'cover_grid':'cover_grid', 'cover_browser':'cover_browser',
'tag_browser':'tag_browser', 'quick_view':'quickview'}.get(self.name)
if tab_name:
if gui is not None:
gui.iactions['Preferences'].do_config(initial_plugin=('Interface', 'Look & Feel', tab_name+'_tab'), close_after_initial=True)
ev.accept()
return
return QToolButton.mouseReleaseEvent(self, ev)
class HandleState(Enum):
both_visible = auto()
only_main_visible = auto()
only_side_visible = auto()
class SplitterHandle(QWidget):
dragged_to = pyqtSignal(QPointF)
toggle_requested = pyqtSignal()
drag_start = None
COLLAPSED_SIZE = 2 # pixels
def __init__(self, parent: QWidget=None, orientation: Qt.Orientation = Qt.Orientation.Vertical):
super().__init__(parent)
self.set_orientation(orientation)
def set_orientation(self, orientation):
self.orientation = orientation
if orientation is Qt.Orientation.Vertical:
self.setCursor(Qt.CursorShape.SplitHCursor)
else:
self.setCursor(Qt.CursorShape.SplitVCursor)
@property
def state(self) -> HandleState:
p = self.parent()
if p is not None:
try:
return p.handle_state(self)
except AttributeError as err:
raise Exception(str(err)) from err
return HandleState.both_visible
def mousePressEvent(self, ev):
super().mousePressEvent(ev)
if ev.button() is Qt.MouseButton.LeftButton:
self.drag_start = ev.position()
def mouseReleaseEvent(self, ev):
super().mouseReleaseEvent(ev)
if ev.button() is Qt.MouseButton.LeftButton:
self.drag_start = None
def mouseDoubleClickEvent(self, ev):
if ev.button() == Qt.MouseButton.LeftButton:
self.toggle_requested.emit()
ev.accept()
return
return super().mouseDoubleClickEvent(ev)
def mouseMoveEvent(self, ev):
super().mouseMoveEvent(ev)
if self.drag_start is not None:
pos = ev.position() - self.drag_start
self.dragged_to.emit(self.mapToParent(pos))
def paintEvent(self, ev):
p = QStylePainter(self)
opt = QStyleOption()
opt.initFrom(self)
if self.orientation is Qt.Orientation.Vertical:
opt.state |= QStyle.StateFlag.State_Horizontal
if min(opt.rect.width(), opt.rect.height()) == self.COLLAPSED_SIZE:
p.fillRect(opt.rect, opt.palette.color(QPalette.ColorRole.Window))
else:
p.drawControl(QStyle.ControlElement.CE_Splitter, opt)
p.end()
class Layout(Enum):
wide = auto()
narrow = auto()
@dataclass
class WideDesires:
tag_browser_width: float = 0.3
book_details_width: float = 0.3
cover_browser_height: float = 0.4
quick_view_height: float = 0.2
def serialize(self):
return {k: v for k, v in asdict(self).items() if v > 0}
def unserialize(self, x):
for field in fields(self):
v = x.get(field.name, 0)
if isinstance(v, (int, float)) and 0 < v <= 1:
setattr(self, field.name, v)
def reset_to_defaults(self):
c = type(self)
for f in fields(self):
setattr(self, f.name, getattr(c, f.name))
@dataclass
class NarrowDesires:
book_details_height: int = 0.3
quick_view_height: int = 0.2
tag_browser_width: int = 0.25
cover_browser_width: int = 0.35
def serialize(self):
return {k: v for k, v in asdict(self).items() if v > 0}
def unserialize(self, x):
for field in fields(self):
v = x.get(field.name, 0)
if isinstance(v, (int, float)) and 0 < v <= 1:
setattr(self, field.name, v)
def reset_to_defaults(self):
c = type(self)
for f in fields(self):
setattr(self, f.name, getattr(c, f.name))
@dataclass
class Visibility:
tag_browser: bool = True
book_details: bool = True
book_list: bool = True
cover_browser: bool = False
quick_view: bool = False
def serialize(self):
return asdict(self)
def unserialize(self, x):
for f in fields(self):
v = x.get(f.name)
if isinstance(v, bool):
setattr(self, f.name, v)
def reset_to_defaults(self):
c = type(self)
for f in fields(self):
setattr(self, f.name, getattr(c, f.name))
class CentralContainer(QWidget):
layout: Layout = Layout.wide
def __init__(self, parent=None, prefs_name='main_window_central_widget_state', separate_cover_browser=None, for_develop=False):
self.separate_cover_browser = config['separate_cover_flow'] if separate_cover_browser is None else separate_cover_browser
self.prefs_name = prefs_name
self.wide_desires = WideDesires()
self.narrow_desires = NarrowDesires()
self.wide_is_visible = Visibility()
self.narrow_is_visible = Visibility()
self._last_cb_position = self.gui = None
super().__init__(parent)
self.action_toggle_layout = QAction(self)
self.action_toggle_layout.triggered.connect(self.toggle_layout)
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
if for_develop:
self.tag_browser = Placeholder('tag browser', self)
self.book_list = Placeholder('book list', self)
self.cover_browser = Placeholder('cover browser', self)
self.book_details = Placeholder('book details', self)
self.quick_view = Placeholder('quick view', self)
else:
self.tag_browser = QWidget(self)
self.book_list = QWidget(self)
self.cover_browser = QWidget(self)
self.book_details = QWidget(self)
self.quick_view = QWidget(self)
self.cover_browser.setMinimumSize(MIN_SIZE)
self.ignore_button_toggles = False
self.tag_browser_button = LayoutButton('tag_browser', 'tags.png', _('Tag browser'), self, 'Shift+Alt+T')
self.book_details_button = LayoutButton('book_details', 'book.png', _('Book details'), self, 'Shift+Alt+D')
self.cover_browser_button = LayoutButton('cover_browser', 'cover_flow.png', _('Cover browser'), self, 'Shift+Alt+B')
self.quick_view_button = LayoutButton('quick_view', 'quickview.png', _('Quickview'), self)
self.setMinimumSize(MIN_SIZE + QSize(200, 100))
def h(orientation: Qt.Orientation = Qt.Orientation.Vertical):
ans = SplitterHandle(self, orientation)
ans.dragged_to.connect(self.splitter_handle_dragged)
ans.toggle_requested.connect(self.toggle_handle)
return ans
self.left_handle = h()
self.right_handle = h()
self.top_handle = h(Qt.Orientation.Horizontal)
self.bottom_handle = h(Qt.Orientation.Horizontal)
@property
def narrow_cb_on_top(self):
p = self._last_cb_position = gprefs['cover_browser_narrow_view_position']
if p == 'automatic':
gui = self.gui or self
ratio = gui.width() / gui.height()
return ratio <= 1.4
return p == 'on_top'
@property
def cb_on_top_changed(self):
return (self._last_cb_position is None or
gprefs['cover_browser_narrow_view_position'] != self._last_cb_position)
@property
def is_visible(self):
return self.wide_is_visible if self.layout is Layout.wide else self.narrow_is_visible
def set_widget(self, which, w):
existing = getattr(self, which)
existing.setVisible(False)
existing.setParent(None)
setattr(self, which, w)
w.setParent(self)
def initialize_with_gui(self, gui, book_list_widget):
self.gui = gui
self.tag_browser_button.initialize_with_gui(gui)
self.book_details_button.initialize_with_gui(gui)
self.cover_browser_button.initialize_with_gui(gui)
self.quick_view_button.initialize_with_gui(gui)
self.set_widget('book_details', gui.book_details)
self.set_widget('tag_browser', gui.tb_widget)
self.set_widget('book_list', book_list_widget)
gui.keyboard.register_shortcut(
'toggle_layout_type', _('Toggle layout between wide and narrow'), group=_('Main window layout'),
default_keys=('Alt+Shift+L',), action=self.action_toggle_layout)
gui.addAction(self.action_toggle_layout)
# cover browser is set in CoverFlowMixin
# Quickview is set in quickview.py code
@property
def is_wide(self):
return self.layout is Layout.wide
def change_layout(self, gui, is_wide):
layout = Layout.wide if is_wide else Layout.narrow
if layout is self.layout and not self.cb_on_top_changed:
return False
ss = self.serialized_settings()
before = ss[self.layout.name + '_visibility']
after = ss[layout.name + '_visibility']
gui.book_details.change_layout(is_wide)
self.layout = layout
self.write_settings()
# apply visibility changes by clicking buttons to ensure button
# state is correct and also deals with the case of the QV widget not
# being initialised
changes = set()
if before != after:
for k in before:
if before[k] != after[k]:
changes.add(k)
setattr(self.is_visible, k, before[k])
if changes:
for k in changes:
button = getattr(self, k + '_button')
button.click()
else:
self.relayout()
return True
def serialized_settings(self):
return {
'layout': self.layout.name,
'wide_visibility': self.wide_is_visible.serialize(),
'narrow_visibility': self.narrow_is_visible.serialize(),
'wide_desires': self.wide_desires.serialize(),
'narrow_desires': self.narrow_desires.serialize()
}
def layout_button_toggled(self):
if not self.ignore_button_toggles:
b = self.sender()
if b.name == 'quick_view':
return
self.set_visibility_of(b.name, b.isChecked())
self.relayout()
def unserialize_settings(self, s):
l = s.get('layout')
self.layout = Layout.narrow if l == 'narrow' else Layout.wide
self.wide_is_visible.unserialize(s.get('wide_visibility') or {})
self.narrow_is_visible.unserialize(s.get('narrow_visibility') or {})
self.wide_desires.unserialize(s.get('wide_desires') or {})
self.narrow_desires.unserialize(s.get('narrow_desires') or {})
def write_settings(self):
gprefs.set(self.prefs_name, self.serialized_settings())
def read_settings(self):
before = self.serialized_settings()
# sadly self.size() doesnt always return sensible values so look at
# the size of the main window which works perfectly for width, not so
# perfectly for height
sz = self.size()
p = self.parent()
while p is not None and not isinstance(p, QMainWindow):
p = p.parent()
if p is not None:
psz = p.size()
sz = QSize(max(sz.width(), psz.width()), max(sz.height(), psz.height() - 50))
settings = gprefs.get(self.prefs_name) or migrate_settings(sz.width(), sz.height())
self.unserialize_settings(settings)
if self.serialized_settings() != before:
self.update_button_states_from_visibility()
self.relayout()
return True
return False
def reset_to_defaults(self):
before = self.serialized_settings()
self.layout = Layout.wide
self.is_visible.reset_to_defaults()
self.wide_desires.reset_to_defaults()
self.narrow_desires.reset_to_defaults()
if self.serialized_settings() != before:
self.update_button_states_from_visibility()
self.relayout()
def toggle_panel(self, which):
was_visible = getattr(self.is_visible, which)
self.set_visibility_of(which, was_visible ^ True)
self.relayout()
def set_visibility_of(self, which, visible):
was_visible = getattr(self.is_visible, which)
setattr(self.is_visible, which, visible)
if not was_visible:
if self.layout is Layout.wide:
self.size_panel_on_initial_show_wide(which)
else:
self.size_panel_on_initial_show_narrow(which)
self.update_button_states_from_visibility()
def show_panel(self, which):
if not getattr(self.is_visible, which):
self.set_visibility_of(which, True)
self.relayout()
def hide_panel(self, which):
if getattr(self.is_visible, which):
self.set_visibility_of(which, False)
self.relayout()
def panel_name_for_handle(self, handle):
return self.panel_name_for_handle_wide(handle) if self.layout is Layout.wide else self.panel_name_for_handle_narrow(handle)
def toggle_handle(self):
panel = self.panel_name_for_handle(self.sender())
self.set_visibility_of(panel, getattr(self.is_visible, panel) ^ True)
self.relayout()
def update_button_states_from_visibility(self):
orig = self.ignore_button_toggles
self.ignore_button_toggles = True
try:
self.tag_browser_button.setChecked(self.is_visible.tag_browser)
self.book_details_button.setChecked(self.is_visible.book_details)
self.cover_browser_button.setChecked(self.is_visible.cover_browser)
self.quick_view_button.setChecked(self.is_visible.quick_view)
finally:
self.ignore_button_toggles = orig
def toggle_tag_browser(self):
self.toggle_panel('tag_browser')
def toggle_book_details(self):
self.toggle_panel('book_details')
def toggle_cover_browser(self):
self.toggle_panel('cover_browser')
def toggle_quick_view(self):
self.toggle_panel('quick_view')
def handle_state(self, handle):
if self.layout is Layout.wide:
return self.wide_handle_state(handle)
return self.narrow_handle_state(handle)
def splitter_handle_dragged(self, pos):
handle = self.sender()
bv = copy(self.is_visible)
if self.layout is Layout.wide:
bd = copy(self.wide_desires)
self.wide_move_splitter_handle_to(handle, pos)
ad = self.wide_desires
else:
bd = copy(self.narrow_desires)
self.narrow_move_splitter_handle_to(handle, pos)
ad = self.narrow_desires
if (bv, bd) != (self.is_visible, ad):
if bv != self.is_visible:
self.update_button_states_from_visibility()
self.relayout()
def resizeEvent(self, ev):
super().resizeEvent(ev)
self.relayout()
def relayout(self):
self.tag_browser.setVisible(self.is_visible.tag_browser)
self.book_details.setVisible(self.is_visible.book_details)
self.cover_browser.setVisible(self.is_visible.cover_browser and not self.separate_cover_browser)
self.book_list.setVisible(self.is_visible.book_list)
self.quick_view.setVisible(self.is_visible.quick_view)
if self.layout is Layout.wide:
self.right_handle.set_orientation(Qt.Orientation.Vertical)
self.do_wide_layout()
else:
self.right_handle.set_orientation(Qt.Orientation.Horizontal if self.narrow_cb_on_top else Qt.Orientation.Vertical)
self.do_narrow_layout()
self.update()
def toggle_layout(self):
if self.gui:
self.change_layout(self.gui, self.layout is Layout.narrow)
else:
self.layout = Layout.narrow if self.layout is Layout.wide else Layout.wide
self.relayout()
def button_for(self, which):
return getattr(self, which + '_button')
def sizeHint(self):
return QSize(800, 600)
# Wide {{{
def wide_handle_state(self, handle):
if handle is self.left_handle:
return HandleState.both_visible if self.is_visible.tag_browser else HandleState.only_main_visible
if handle is self.right_handle:
return HandleState.both_visible if self.is_visible.book_details else HandleState.only_main_visible
if handle is self.top_handle:
if self.is_visible.cover_browser and not self.separate_cover_browser:
return HandleState.both_visible if self.is_visible.book_list else HandleState.only_side_visible
return HandleState.only_main_visible
if handle is self.bottom_handle:
return HandleState.both_visible if self.is_visible.quick_view else HandleState.only_main_visible
def min_central_width_wide(self):
return max(200, self.cover_browser.minimumWidth())
def do_wide_layout(self):
s = self.style()
normal_handle_width = int(s.pixelMetric(QStyle.PixelMetric.PM_SplitterWidth, widget=self))
available_width = self.width()
for h in (self.left_handle, self.right_handle):
width = h.COLLAPSED_SIZE
hs = h.state
if hs is HandleState.both_visible or hs is HandleState.only_side_visible:
width = normal_handle_width
h.resize(int(width), int(self.height()))
available_width -= width
tb = int(self.width() * self.wide_desires.tag_browser_width) if self.is_visible.tag_browser else 0
bd = int(self.width() * self.wide_desires.book_details_width) if self.is_visible.book_details else 0
min_central_width = self.min_central_width_wide()
if tb + bd > available_width - min_central_width:
width_to_share = max(0, available_width - min_central_width)
tb = int(tb * width_to_share / (tb + bd))
bd = max(0, width_to_share - tb)
central_width = available_width - (tb + bd)
if self.is_visible.tag_browser:
self.tag_browser.setGeometry(0, 0, int(tb), int(self.height()))
self.left_handle.move(tb, 0)
central_x = self.left_handle.x() + self.left_handle.width()
self.right_handle.move(tb + central_width + self.left_handle.width(), 0)
if self.is_visible.book_details:
self.book_details.setGeometry(int(self.right_handle.x() + self.right_handle.width()), 0, int(bd), int(self.height()))
available_height = self.height()
for h in (self.top_handle, self.bottom_handle):
height = h.COLLAPSED_SIZE
hs = h.state
if hs is HandleState.both_visible or hs is HandleState.only_side_visible:
height = normal_handle_width
if hs is HandleState.only_main_visible and h is self.bottom_handle or (h is self.top_handle and self.separate_cover_browser):
height = 0
h.resize(int(central_width), int(height))
available_height -= height
cb = max(self.cover_browser.minimumHeight(), int(self.wide_desires.cover_browser_height * self.height()))
if not self.is_visible.cover_browser or self.separate_cover_browser:
cb = 0
qv = bl = 0
if cb >= available_height:
cb = available_height
else:
available_height -= cb
min_bl_height = 50
if available_height <= min_bl_height:
bl = available_height
elif self.is_visible.quick_view:
qv = min(available_height - min_bl_height, int(self.wide_desires.quick_view_height * self.height()))
if qv < HIDE_THRESHOLD:
qv = 0
bl = available_height - qv
else:
bl = available_height
if self.is_visible.cover_browser and not self.separate_cover_browser:
self.cover_browser.setGeometry(int(central_x), 0, int(central_width), int(cb))
self.top_handle.move(central_x, cb)
if self.is_visible.book_list:
self.book_list.setGeometry(int(central_x), int(self.top_handle.y() + self.top_handle.height()), int(central_width), int(bl))
self.bottom_handle.move(central_x, self.book_list.y() + self.book_list.height())
if self.is_visible.quick_view:
self.quick_view.setGeometry(int(central_x), int(self.bottom_handle.y() + self.bottom_handle.height()), int(central_width), int(qv))
def wide_move_splitter_handle_to(self, handle: SplitterHandle, pos: QPointF):
if handle is self.left_handle:
x = int(pos.x())
self.is_visible.tag_browser = True
if x < HIDE_THRESHOLD:
self.is_visible.tag_browser = False
self.wide_desires.tag_browser_width = 0
else:
self.wide_desires.tag_browser_width = x / self.width()
elif handle is self.right_handle:
x = int(pos.x())
self.is_visible.book_details = True
w = self.width() - x
if w < HIDE_THRESHOLD:
self.is_visible.book_details = False
self.wide_desires.book_details_width = 0
else:
self.wide_desires.book_details_width = w / self.width()
elif handle is self.top_handle:
y = int(pos.y())
self.is_visible.cover_browser = True
if y < max(self.cover_browser.minimumHeight(), HIDE_THRESHOLD):
self.is_visible.cover_browser = False
self.wide_desires.cover_browser_height = 0
else:
self.wide_desires.cover_browser_height = max(y, self.cover_browser.minimumHeight()) / self.height()
elif handle is self.bottom_handle:
y = int(pos.y())
h = self.height() - y - self.bottom_handle.height()
if h < HIDE_THRESHOLD:
self.is_visible.quick_view = False
self.wide_desires.quick_view_height = 0
else:
available_height = max(0, self.height() - self.top_handle.height() - self.bottom_handle.height() - self.cover_browser.minimumHeight() - 50)
self.wide_desires.quick_view_height = max(HIDE_THRESHOLD, min(h, available_height)) / self.height()
def size_panel_on_initial_show_wide(self, which):
if which in ('tag_browser', 'book_details'):
which += '_width'
current = int(getattr(self.wide_desires, which) * self.width())
if current < SHOW_THRESHOLD:
setattr(self.wide_desires, which, getattr(WideDesires, which))
elif which == 'cover_browser':
self.wide_desires.cover_browser_height = max(int(self.height() * self.wide_desires.cover_browser_height),
self.cover_browser.minimumHeight()) / self.height()
else:
self.wide_desires.quick_view_height = max(int(self.height() * self.wide_desires.quick_view_height), 150) / self.height()
def panel_name_for_handle_wide(self, handle):
return {self.left_handle: 'tag_browser', self.right_handle: 'book_details', self.top_handle: 'cover_browser', self.bottom_handle: 'quick_view'}[handle]
# }}}
# Narrow {{{
def narrow_handle_state(self, handle):
if handle is self.left_handle:
return HandleState.both_visible if self.is_visible.tag_browser else HandleState.only_main_visible
if handle is self.right_handle:
if self.is_visible.cover_browser and not self.separate_cover_browser:
return HandleState.both_visible if self.is_visible.book_list else HandleState.only_side_visible
return HandleState.only_main_visible
if handle is self.top_handle:
return HandleState.both_visible if self.is_visible.quick_view else HandleState.only_main_visible
if handle is self.bottom_handle:
return HandleState.both_visible if self.is_visible.book_details else HandleState.only_main_visible
def min_central_width_narrow(self):
return 200
def min_central_height_narrow(self):
return max(150, self.cover_browser.minimumHeight())
def do_narrow_layout_with_cb_on_top(self):
s = self.style()
normal_handle_width = int(s.pixelMetric(QStyle.PixelMetric.PM_SplitterWidth, widget=self))
available_height = self.height()
for handle in (self.bottom_handle, self.right_handle):
hs = handle.state
height = self.bottom_handle.COLLAPSED_SIZE
if hs is HandleState.both_visible or hs is HandleState.only_side_visible:
height = normal_handle_width
handle.resize(self.width(), height)
available_height -= height
bd = int(self.narrow_desires.book_details_height * self.height()) if self.is_visible.book_details else 0
bd = max(0, min(bd, max(0, available_height - self.min_central_height_narrow() - 40)))
central_height = available_height - bd + self.right_handle.height()
self.bottom_handle.move(0, central_height - self.bottom_handle.height())
if self.is_visible.book_details:
self.book_details.setGeometry(0, self.bottom_handle.y() + self.bottom_handle.height(), self.width(), bd)
available_width = self.width()
hs = self.left_handle.state
width = self.left_handle.COLLAPSED_SIZE
if hs is HandleState.both_visible or hs is HandleState.only_side_visible:
width = normal_handle_width
self.left_handle.resize(width, central_height)
available_width -= width
tb = int(self.narrow_desires.tag_browser_width * self.width()) if self.is_visible.tag_browser else 0
self.left_handle.move(tb, 0)
if self.is_visible.tag_browser:
self.tag_browser.setGeometry(0, 0, tb, central_height)
central_x = self.left_handle.x() + self.left_handle.width()
central_width = self.width() - central_x
central_height -= self.right_handle.height()
cb = min(max(0, central_height - 80), int(self.height() * self.narrow_desires.cover_browser_width)) if self.is_visible.cover_browser else 0
if cb and cb < self.cover_browser.minimumHeight():
cb = min(self.cover_browser.minimumHeight(), central_height)
if self.is_visible.cover_browser:
self.cover_browser.setGeometry(central_x, 0, central_width, cb)
self.right_handle.resize(central_width, self.right_handle.height())
self.right_handle.move(central_x, cb)
central_top = self.right_handle.y() + self.right_handle.height()
central_height = self.bottom_handle.y() - central_top
self.top_handle.resize(central_width, normal_handle_width if self.is_visible.quick_view else 0)
available_height = central_height - self.top_handle.height()
qv = int(self.height() * self.narrow_desires.quick_view_height) if self.is_visible.quick_view else 0
qv = min(qv, max(0, available_height - 80))
bl = max(0, available_height - qv)
self.book_list.setGeometry(central_x, central_top, central_width, bl)
self.top_handle.move(central_x, self.book_list.y() + self.book_list.height())
if self.is_visible.quick_view:
self.quick_view.setGeometry(central_x, self.top_handle.y() + self.top_handle.height(), central_width, qv)
def do_narrow_layout(self):
if self.narrow_cb_on_top:
return self.do_narrow_layout_with_cb_on_top()
s = self.style()
normal_handle_width = int(s.pixelMetric(QStyle.PixelMetric.PM_SplitterWidth, widget=self))
available_height = self.height()
hs = self.bottom_handle.state
height = self.bottom_handle.COLLAPSED_SIZE
if hs is HandleState.both_visible or hs is HandleState.only_side_visible:
height = normal_handle_width
self.bottom_handle.resize(int(self.width()), int(height))
available_height -= height
bd = int(self.narrow_desires.book_details_height * self.height()) if self.is_visible.book_details else 0
central_height = max(self.min_central_height_narrow(), available_height - bd)
bd = available_height - central_height
self.bottom_handle.move(0, central_height)
if self.is_visible.book_details:
self.book_details.setGeometry(0, int(central_height + self.bottom_handle.height()), int(self.width()), int(bd))
available_width = self.width()
for h in (self.left_handle, self.right_handle):
width = h.COLLAPSED_SIZE
hs = h.state
if hs is HandleState.both_visible or hs is HandleState.only_side_visible:
width = normal_handle_width
if h is self.right_handle and self.separate_cover_browser:
width = 0
h.resize(int(width), int(central_height))
available_width -= width
tb = int(self.narrow_desires.tag_browser_width * self.width()) if self.is_visible.tag_browser else 0
cb = max(self.cover_browser.minimumWidth(),
int(self.narrow_desires.cover_browser_width * self.width())) if self.is_visible.cover_browser and not self.separate_cover_browser else 0
min_central_width = self.min_central_width_narrow()
if tb + cb > max(0, available_width - min_central_width):
width_to_share = max(0, available_width - min_central_width)
cb = int(cb * width_to_share / (tb + cb))
cb = max(self.cover_browser.minimumWidth(), cb)
if cb > width_to_share:
cb = 0
tb = max(0, width_to_share - cb)
central_width = available_width - (tb + cb)
if self.is_visible.tag_browser:
self.tag_browser.setGeometry(0, 0, int(tb), int(central_height))
self.left_handle.move(tb, 0)
central_x = self.left_handle.x() + self.left_handle.width()
self.right_handle.move(tb + central_width + self.left_handle.width(), 0)
if self.is_visible.cover_browser and not self.separate_cover_browser:
self.cover_browser.setGeometry(int(self.right_handle.x() + self.right_handle.width()), 0, int(cb), int(central_height))
self.top_handle.resize(int(central_width), int(normal_handle_width if self.is_visible.quick_view else 0))
central_height -= self.top_handle.height()
qv = 0
if self.is_visible.quick_view:
qv = int(self.narrow_desires.quick_view_height * self.height())
qv = max(0, min(qv, central_height - 50))
self.book_list.setGeometry(int(central_x), 0, int(central_width), int(max(0, central_height - qv)))
self.top_handle.move(central_x, self.book_list.y() + self.book_list.height())
if self.is_visible.quick_view:
self.quick_view.setGeometry(int(central_x), int(self.top_handle.y() + self.top_handle.height()), int(central_width), int(qv))
def narrow_move_splitter_handle_to(self, handle: SplitterHandle, pos: QPointF):
if handle is self.left_handle:
x = int(pos.x())
available_width = self.width() - self.left_handle.width() - self.right_handle.width() - self.min_central_width_narrow()
if self.narrow_cb_on_top:
available_width += self.right_handle.width()
self.is_visible.tag_browser = True
if x < HIDE_THRESHOLD:
self.is_visible.tag_browser = False
self.narrow_desires.tag_browser_width = 0
else:
self.narrow_desires.tag_browser_width = min(available_width, x) / self.width()
elif handle is self.right_handle:
if self.narrow_cb_on_top:
y = int(pos.y())
self.is_visible.cover_browser = True
if y < max(self.cover_browser.minimumHeight(), HIDE_THRESHOLD):
self.is_visible.cover_browser = False
self.narrow_desires.cover_browser_width = 0
else:
self.narrow_desires.cover_browser_width = max(y, self.cover_browser.minimumHeight()) / self.height()
else:
x = int(pos.x())
available_width = self.width() - self.left_handle.width() - self.right_handle.width() - self.min_central_width_narrow()
self.is_visible.cover_browser = True
w = min(available_width, self.width() - x - self.right_handle.width())
if w < HIDE_THRESHOLD:
self.is_visible.cover_browser = False
self.narrow_desires.book_details_width = 0
else:
self.narrow_desires.cover_browser_width = max(self.cover_browser.minimumWidth(), w) / self.width()
elif handle is self.bottom_handle:
y = int(pos.y())
h = self.height() - y - self.bottom_handle.height()
if h < HIDE_THRESHOLD:
self.is_visible.book_details = False
self.narrow_desires.book_details_height = 0
else:
self.is_visible.book_details = True
available_height = max(0, self.height() - self.bottom_handle.height() - self.min_central_height_narrow())
self.narrow_desires.book_details_height = max(HIDE_THRESHOLD, min(h, available_height)) / self.height()
elif handle is self.top_handle:
y = int(pos.y())
available_height = self.bottom_handle.y() if self.is_visible.book_details else self.height()
available_height -= self.top_handle.height()
h = available_height - y
if h < HIDE_THRESHOLD or available_height < 5:
self.is_visible.quick_view = False
self.narrow_desires.quick_view_height = 0
else:
self.is_visible.quick_view = True
self.narrow_desires.quick_view_height = max(0, available_height - y) / self.height()
def size_panel_on_initial_show_narrow(self, which):
if which == 'tag_browser':
current = self.narrow_desires.tag_browser_width * self.width()
if current < SHOW_THRESHOLD:
self.narrow_desires.tag_browser_width = NarrowDesires.tag_browser_width
elif which == 'cover_browser':
d = self.height() if self.narrow_cb_on_top else self.width()
current = self.narrow_desires.cover_browser_width * d
if current < SHOW_THRESHOLD:
self.narrow_desires.cover_browser_width = NarrowDesires.cover_browser_width
elif which == 'book_details':
current = self.height() * self.narrow_desires.book_details_height
if current < SHOW_THRESHOLD:
self.narrow_desires.book_details_height = NarrowDesires.book_details_height
else:
current = self.height() * self.narrow_desires.quick_view_height
if current < SHOW_THRESHOLD:
self.narrow_desires.quick_view_height = NarrowDesires.quick_view_height
def panel_name_for_handle_narrow(self, handle):
return {self.left_handle: 'tag_browser', self.right_handle: 'cover_browser', self.top_handle: 'quick_view', self.bottom_handle: 'book_details'}[handle]
# }}}
# develop {{{
def develop():
app = Application([])
class d(QDialog):
def __init__(self):
super().__init__()
l = QVBoxLayout(self)
l.setContentsMargins(0, 0, 0, 0)
h = QHBoxLayout()
l.addLayout(h)
self.central = CentralContainer(self, for_develop=True, prefs_name='develop_central_layout_widget_state', separate_cover_browser=False)
h.addWidget(self.central.tag_browser_button)
h.addWidget(self.central.book_details_button)
h.addWidget(self.central.cover_browser_button)
h.addWidget(self.central.quick_view_button)
l.addWidget(self.central)
self.resize(self.sizeHint())
def keyPressEvent(self, ev):
if ev.key() == Qt.Key.Key_Q:
self.central.toggle_quick_view()
elif ev.key() == Qt.Key.Key_T:
self.central.toggle_tag_browser()
elif ev.key() == Qt.Key.Key_C:
self.central.toggle_cover_browser()
elif ev.key() == Qt.Key.Key_D:
self.central.toggle_book_details()
elif ev.key() == Qt.Key.Key_L:
self.central.toggle_layout()
elif ev.key() == Qt.Key.Key_R:
self.central.reset_to_defaults()
elif ev.key() == Qt.Key.Key_Escape:
self.reject()
d = d()
d.central.read_settings()
d.show()
app.exec()
d.central.write_settings()
if __name__ == '__main__':
develop()
# }}}