%PDF- %PDF-
| Direktori : /lib/calibre/calibre/gui2/ |
| Current File : //lib/calibre/calibre/gui2/bars.py |
#!/usr/bin/env python3
__license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from functools import partial
from qt.core import (
Qt, QAction, QMenu, QObject, QToolBar, QToolButton, QSize, pyqtSignal, QKeySequence, QMenuBar,
QTimer, QPropertyAnimation, QEasingCurve, pyqtProperty, QPainter, QWidget, QPalette, sip)
from calibre.constants import ismacos
from calibre.gui2 import gprefs, native_menubar_defaults, config
from calibre.gui2.throbber import ThrobbingButton
from polyglot.builtins import itervalues
class RevealBar(QWidget): # {{{
def __init__(self, parent):
QWidget.__init__(self, parent)
self.setVisible(False)
self._animated_size = 1.0
self.animation = QPropertyAnimation(self, b'animated_size', self)
self.animation.setEasingCurve(QEasingCurve.Type.Linear)
self.animation.setDuration(1000), self.animation.setStartValue(0.0), self.animation.setEndValue(1.0)
self.animation.valueChanged.connect(self.animation_value_changed)
self.animation.finished.connect(self.animation_done)
@pyqtProperty(float)
def animated_size(self):
return self._animated_size
@animated_size.setter
def animated_size(self, val):
self._animated_size = val
def animation_value_changed(self, *args):
self.update()
def animation_done(self):
self.setVisible(False)
self.update()
def start(self, bar):
self.setGeometry(bar.geometry())
self.setVisible(True)
self.animation.start()
def paintEvent(self, ev):
if self._animated_size < 1.0:
rect = self.rect()
painter = QPainter(self)
pal = self.palette()
col = pal.color(QPalette.ColorRole.Button)
rect.setLeft(rect.left() + int(rect.width() * self._animated_size))
painter.setClipRect(rect)
painter.fillRect(self.rect(), col)
# }}}
MAX_TEXT_LENGTH = 10
connected_pairs = set()
def wrap_button_text(text, max_len=MAX_TEXT_LENGTH):
parts = text.split()
ans = ''
broken = False
for word in parts:
if broken:
ans += ' ' + word
else:
if len(ans) + len(word) < max_len:
if ans:
ans += ' ' + word
else:
ans = word
else:
if ans:
ans += '\n' + word
broken = True
else:
ans = word
if not broken:
if ' ' in ans:
ans = '\n'.join(ans.split(' ', 1))
elif '/' in ans:
ans = '/\n'.join(ans.split('/', 1))
else:
ans += '\n\xa0'
return ans
def rewrap_button(w):
if not sip.isdeleted(w) and w.defaultAction() is not None:
w.setText(wrap_button_text(w.defaultAction().text()))
def wrap_all_button_texts(all_buttons):
if not all_buttons:
return
for w in all_buttons:
if hasattr(w, 'defaultAction'):
ac = w.defaultAction()
text = ac.text()
key = id(w), id(ac)
if key not in connected_pairs:
ac.changed.connect(partial(rewrap_button, w))
connected_pairs.add(key)
else:
text = w.text()
w.setText(wrap_button_text(text))
def create_donate_button(action):
ans = ThrobbingButton()
ans.setAutoRaise(True)
ans.setCursor(Qt.CursorShape.PointingHandCursor)
ans.clicked.connect(action.trigger)
ans.setToolTip(action.text().replace('&', ''))
ans.setIcon(action.icon())
ans.setStatusTip(ans.toolTip())
return ans
class ToolBar(QToolBar): # {{{
def __init__(self, donate_action, location_manager, parent):
QToolBar.__init__(self, parent)
self.setMovable(False)
self.setFloatable(False)
self.setOrientation(Qt.Orientation.Horizontal)
self.setAllowedAreas(Qt.ToolBarArea.TopToolBarArea|Qt.ToolBarArea.BottomToolBarArea)
self.setStyleSheet('QToolButton:checked { font-weight: bold }')
self.preferred_width = self.sizeHint().width()
self.gui = parent
self.donate_action = donate_action
self.donate_button = None
self.added_actions = []
self.location_manager = location_manager
self.setAcceptDrops(True)
self.showing_donate = False
def resizeEvent(self, ev):
QToolBar.resizeEvent(self, ev)
style = self.get_text_style()
self.setToolButtonStyle(style)
if self.showing_donate:
self.donate_button.setToolButtonStyle(style)
def get_text_style(self):
style = Qt.ToolButtonStyle.ToolButtonTextUnderIcon
s = gprefs['toolbar_icon_size']
if s != 'off':
p = gprefs['toolbar_text']
if p == 'never':
style = Qt.ToolButtonStyle.ToolButtonIconOnly
elif p == 'auto' and self.preferred_width > self.width()+15:
style = Qt.ToolButtonStyle.ToolButtonIconOnly
return style
def contextMenuEvent(self, ev):
ac = self.actionAt(ev.pos())
if ac is None:
return
ch = self.widgetForAction(ac)
sm = getattr(ch, 'showMenu', None)
if callable(sm):
ev.accept()
sm()
def update_lm_actions(self):
for ac in self.added_actions:
if ac in self.location_manager.all_actions:
ac.setVisible(ac in self.location_manager.available_actions)
def init_bar(self, actions):
self.showing_donate = False
for ac in self.added_actions:
m = ac.menu()
if m is not None:
m.setVisible(False)
self.clear()
self.added_actions = []
self.donate_button = None
self.all_widgets = []
for what in actions:
if what is None:
self.addSeparator()
elif what == 'Location Manager':
for ac in self.location_manager.all_actions:
self.addAction(ac)
self.added_actions.append(ac)
self.setup_tool_button(self, ac, QToolButton.ToolButtonPopupMode.MenuButtonPopup)
ac.setVisible(False)
elif what == 'Donate':
self.donate_button = create_donate_button(self.donate_action)
self.addWidget(self.donate_button)
self.donate_button.setIconSize(self.iconSize())
self.donate_button.setToolButtonStyle(self.toolButtonStyle())
self.showing_donate = True
elif what in self.gui.iactions:
action = self.gui.iactions[what]
self.addAction(action.qaction)
self.added_actions.append(action.qaction)
self.setup_tool_button(self, action.qaction, action.popup_type)
if gprefs['wrap_toolbar_text']:
wrap_all_button_texts(self.all_widgets)
self.preferred_width = self.sizeHint().width()
self.all_widgets = []
def setup_tool_button(self, bar, ac, menu_mode=None):
ch = bar.widgetForAction(ac)
if ch is None:
ch = self.child_bar.widgetForAction(ac)
ch.setCursor(Qt.CursorShape.PointingHandCursor)
if hasattr(ch, 'setText') and hasattr(ch, 'text'):
self.all_widgets.append(ch)
if hasattr(ch, 'setAutoRaise'): # is a QToolButton or similar
ch.setAutoRaise(True)
m = ac.menu()
if m is not None:
if menu_mode is not None:
ch.setPopupMode(menu_mode)
return ch
# support drag&drop from/to library, from/to reader/card, enabled plugins
def check_iactions_for_drag(self, event, md, func):
if self.added_actions:
pos = event.pos()
for iac in itervalues(self.gui.iactions):
if iac.accepts_drops:
aa = iac.qaction
w = self.widgetForAction(aa)
m = aa.menu()
if (((w is not None and w.geometry().contains(pos)) or (
m is not None and m.isVisible() and m.geometry().contains(pos))) and getattr(
iac, func)(event, md)):
return True
return False
def dragEnterEvent(self, event):
md = event.mimeData()
if md.hasFormat("application/calibre+from_library") or \
md.hasFormat("application/calibre+from_device"):
event.setDropAction(Qt.DropAction.CopyAction)
event.accept()
return
if self.check_iactions_for_drag(event, md, 'accept_enter_event'):
event.accept()
else:
event.ignore()
def dragMoveEvent(self, event):
allowed = False
md = event.mimeData()
# Drop is only allowed in the location manager widget's different from the selected one
for ac in self.location_manager.available_actions:
w = self.widgetForAction(ac)
if w is not None:
if (md.hasFormat(
"application/calibre+from_library") or md.hasFormat(
"application/calibre+from_device")) and \
w.geometry().contains(event.pos()) and \
isinstance(w, QToolButton) and not w.isChecked():
allowed = True
break
if allowed:
event.acceptProposedAction()
return
if self.check_iactions_for_drag(event, md, 'accept_drag_move_event'):
event.acceptProposedAction()
else:
event.ignore()
def dropEvent(self, event):
md = event.mimeData()
mime = 'application/calibre+from_library'
if md.hasFormat(mime):
ids = list(map(int, md.data(mime).data().split()))
tgt = None
for ac in self.location_manager.available_actions:
w = self.widgetForAction(ac)
if w is not None and w.geometry().contains(event.pos()):
tgt = ac.calibre_name
if tgt is not None:
if tgt == 'main':
tgt = None
self.gui.sync_to_device(tgt, False, send_ids=ids)
event.accept()
return
mime = 'application/calibre+from_device'
if md.hasFormat(mime):
paths = [str(u.toLocalFile()) for u in md.urls()]
if paths:
self.gui.iactions['Add Books'].add_books_from_device(
self.gui.current_view(), paths=paths)
event.accept()
return
# Give added_actions an opportunity to process the drag&drop event
if self.check_iactions_for_drag(event, md, 'drop_event'):
event.accept()
else:
event.ignore()
# }}}
class MenuAction(QAction): # {{{
def __init__(self, clone, parent):
QAction.__init__(self, clone.text(), parent)
self.clone = clone
clone.changed.connect(self.clone_changed)
def clone_changed(self):
self.setText(self.clone.text())
# }}}
# MenuBar {{{
if ismacos:
# On OS X we need special handling for the application global menu bar and
# the context menus, since Qt does not handle dynamic menus or menus in
# which the same action occurs in more than one place.
class CloneAction(QAction):
text_changed = pyqtSignal()
visibility_changed = pyqtSignal()
def __init__(self, clone, parent, is_top_level=False, clone_shortcuts=True):
QAction.__init__(self, clone.text().replace('&&', '&'), parent)
self.setMenuRole(QAction.MenuRole.NoRole) # ensure this action is not moved around by Qt
self.is_top_level = is_top_level
self.clone_shortcuts = clone_shortcuts
self.clone = clone
clone.changed.connect(self.clone_changed)
self.clone_changed()
self.triggered.connect(self.do_trigger)
def clone_menu(self):
m = self.menu()
m.clear()
for ac in QMenu.actions(self.clone.menu()):
if ac.isSeparator():
m.addSeparator()
else:
m.addAction(CloneAction(ac, self.parent(), clone_shortcuts=self.clone_shortcuts))
def clone_changed(self):
otext = self.text()
self.setText(self.clone.text())
if otext != self.text:
self.text_changed.emit()
ov = self.isVisible()
self.setVisible(self.clone.isVisible())
if ov != self.isVisible():
self.visibility_changed.emit()
self.setEnabled(self.clone.isEnabled())
self.setCheckable(self.clone.isCheckable())
self.setChecked(self.clone.isChecked())
self.setIcon(self.clone.icon())
if self.clone_shortcuts:
sc = self.clone.shortcut()
if sc and not sc.isEmpty():
self.setText(self.text() + '\t' + sc.toString(QKeySequence.SequenceFormat.NativeText))
if self.clone.menu() is None:
if not self.is_top_level:
self.setMenu(None)
else:
m = QMenu(self.text(), self.parent())
m.aboutToShow.connect(self.about_to_show)
self.setMenu(m)
self.clone_menu()
def about_to_show(self):
if sip.isdeleted(self.clone):
return
cm = self.clone.menu()
if cm is None:
return
before = list(QMenu.actions(cm))
cm.aboutToShow.emit()
after = list(QMenu.actions(cm))
if before != after:
self.clone_menu()
def do_trigger(self, checked=False):
if not sip.isdeleted(self.clone):
self.clone.trigger()
def populate_menu(m, items, iactions):
for what in items:
if what is None:
m.addSeparator()
elif what in iactions:
ia = iactions[what]
ac = ia.qaction
if not ac.menu() and hasattr(ia, 'shortcut_action_for_context_menu'):
ia.shortcut_action_for_context_menu.setIcon(ac.icon())
ac = ia.shortcut_action_for_context_menu
m.addAction(CloneAction(ac, m))
class MenuBar(QObject):
is_native_menubar = True
@property
def native_menubar(self):
mb = self.gui.native_menubar
if mb.parent() is None:
# Without this the menubar does not update correctly with Qt >=
# 5.6. See the last couple of lines in updateMenuBarImmediately
# in qcocoamenubar.mm
mb.setParent(self.gui)
return mb
def __init__(self, location_manager, parent):
QObject.__init__(self, parent)
self.gui = parent
self.location_manager = location_manager
self.added_actions = []
self.last_actions = []
self.donate_action = QAction(_('Donate'), self)
self.donate_menu = QMenu()
self.donate_menu.addAction(self.gui.donate_action)
self.donate_action.setMenu(self.donate_menu)
self.refresh_timer = t = QTimer(self)
t.setInterval(200), t.setSingleShot(True), t.timeout.connect(self.refresh_bar)
def adapt_for_dialog(self, enter):
def ac(text, key, role=QAction.MenuRole.TextHeuristicRole):
ans = QAction(text, self)
ans.setMenuRole(role)
ans.setShortcut(QKeySequence(key))
self.edit_menu.addAction(ans)
return ans
mb = self.native_menubar
if enter:
self.clear_bar(mb)
self.edit_menu = QMenu()
self.edit_action = QAction(_('Edit'), self)
self.edit_action.setMenu(self.edit_menu)
ac(_('Copy'), QKeySequence.StandardKey.Copy),
ac(_('Paste'), QKeySequence.StandardKey.Paste),
ac(_('Select all'), QKeySequence.StandardKey.SelectAll),
mb.addAction(self.edit_action)
self.added_actions = [self.edit_action]
else:
self.refresh_bar()
def clear_bar(self, mb):
for ac in self.added_actions:
m = ac.menu()
if m is not None:
m.setVisible(False)
for ac in self.added_actions:
mb.removeAction(ac)
if ac is not self.donate_action:
ac.setMenu(None)
ac.deleteLater()
self.added_actions = []
def init_bar(self, actions):
mb = self.native_menubar
self.last_actions = actions
self.clear_bar(mb)
for what in actions:
if what is None:
continue
elif what == 'Location Manager':
for ac in self.location_manager.available_actions:
self.build_menu(ac)
elif what == 'Donate':
mb.addAction(self.donate_action)
elif what in self.gui.iactions:
action = self.gui.iactions[what]
self.build_menu(action.qaction)
def build_menu(self, ac):
ans = CloneAction(ac, self.native_menubar, is_top_level=True)
if ans.menu() is None:
m = QMenu()
m.addAction(CloneAction(ac, self.native_menubar))
ans.setMenu(m)
# Qt (as of 5.3.0) does not update global menubar entries
# correctly, so we have to rebuild the global menubar.
# Without this the Choose Library action shows the text
# 'Untitled' and the Location Manager items do not work.
ans.text_changed.connect(self.refresh_timer.start)
ans.visibility_changed.connect(self.refresh_timer.start)
self.native_menubar.addAction(ans)
self.added_actions.append(ans)
return ans
def setVisible(self, yes):
pass # no-op on OS X since menu bar is always visible
def update_lm_actions(self):
pass # no-op as this is taken care of by init_bar()
def refresh_bar(self):
self.init_bar(self.last_actions)
else:
def populate_menu(m, items, iactions):
for what in items:
if what is None:
m.addSeparator()
elif what in iactions:
ia = iactions[what]
ac = ia.qaction
if not ac.menu() and hasattr(ia, 'shortcut_action_for_context_menu'):
ia.shortcut_action_for_context_menu.setIcon(ac.icon())
ac = ia.shortcut_action_for_context_menu
m.addAction(ac)
class MenuBar(QObject):
is_native_menubar = False
def __init__(self, location_manager, parent):
QObject.__init__(self, parent)
self.menu_bar = QMenuBar(parent)
self.menu_bar.is_native_menubar = False
parent.setMenuBar(self.menu_bar)
self.gui = parent
self.location_manager = location_manager
self.added_actions = []
self.donate_action = QAction(_('Donate'), self)
self.donate_menu = QMenu()
self.donate_menu.addAction(self.gui.donate_action)
self.donate_action.setMenu(self.donate_menu)
def addAction(self, *args):
self.menu_bar.addAction(*args)
def setVisible(self, visible):
self.menu_bar.setVisible(visible)
def clear(self):
self.menu_bar.clear()
def init_bar(self, actions):
for ac in self.added_actions:
m = ac.menu()
if m is not None:
m.setVisible(False)
self.clear()
self.added_actions = []
for what in actions:
if what is None:
continue
elif what == 'Location Manager':
for ac in self.location_manager.all_actions:
ac = self.build_menu(ac)
self.addAction(ac)
self.added_actions.append(ac)
ac.setVisible(False)
elif what == 'Donate':
self.addAction(self.donate_action)
elif what in self.gui.iactions:
action = self.gui.iactions[what]
ac = self.build_menu(action.qaction)
self.addAction(ac)
self.added_actions.append(ac)
def build_menu(self, action):
m = action.menu()
ac = MenuAction(action, self)
if m is None:
m = QMenu()
m.addAction(action)
ac.setMenu(m)
return ac
def update_lm_actions(self):
for ac in self.added_actions:
clone = getattr(ac, 'clone', None)
if clone is not None and clone in self.location_manager.all_actions:
ac.setVisible(clone in self.location_manager.available_actions)
# }}}
class AdaptMenuBarForDialog:
def __init__(self, menu_bar):
self.menu_bar = menu_bar
def __enter__(self):
if ismacos and self.menu_bar.is_native_menubar:
self.menu_bar.adapt_for_dialog(True)
def __exit__(self, *a):
if ismacos and self.menu_bar.is_native_menubar:
self.menu_bar.adapt_for_dialog(False)
class BarsManager(QObject):
def __init__(self, donate_action, location_manager, parent):
QObject.__init__(self, parent)
self.location_manager = location_manager
bars = [ToolBar(donate_action, location_manager, parent) for i in range(3)]
self.main_bars = tuple(bars[:2])
self.child_bars = tuple(bars[2:])
self.reveal_bar = RevealBar(parent)
self.menu_bar = MenuBar(self.location_manager, self.parent())
is_native_menubar = self.menu_bar.is_native_menubar
self.adapt_menu_bar_for_dialog = AdaptMenuBarForDialog(self.menu_bar)
self.menubar_fallback = native_menubar_defaults['action-layout-menubar'] if is_native_menubar else ()
self.menubar_device_fallback = native_menubar_defaults['action-layout-menubar-device'] if is_native_menubar else ()
self.apply_settings()
self.init_bars()
def database_changed(self, db):
pass
@property
def bars(self):
yield from self.main_bars + self.child_bars
@property
def showing_donate(self):
for b in self.bars:
if b.isVisible() and b.showing_donate:
return True
return False
def start_animation(self):
for b in self.bars:
if b.isVisible() and b.showing_donate:
b.donate_button.start_animation()
return True
def init_bars(self):
self.bar_actions = tuple([
gprefs['action-layout-toolbar'+x] for x in ('', '-device')] + [
gprefs['action-layout-toolbar-child']] + [
gprefs['action-layout-menubar'] or self.menubar_fallback] + [
gprefs['action-layout-menubar-device'] or self.menubar_device_fallback
])
for bar, actions in zip(self.bars, self.bar_actions[:3]):
bar.init_bar(actions)
def update_bars(self, reveal_bar=False):
'''
This shows the correct main toolbar and rebuilds the menubar based on
whether a device is connected or not. Note that the toolbars are
explicitly not rebuilt, this is to workaround a Qt limitation with
QToolButton's popup menus and modal dialogs. If you want the toolbars
rebuilt, call init_bars().
'''
showing_device = self.location_manager.has_device
main_bar = self.main_bars[1 if showing_device else 0]
child_bar = self.child_bars[0]
for bar in self.bars:
bar.setVisible(False)
bar.update_lm_actions()
if main_bar.added_actions:
main_bar.setVisible(True)
if reveal_bar and not config['disable_animations']:
self.reveal_bar.start(main_bar)
if child_bar.added_actions:
child_bar.setVisible(True)
self.menu_bar.init_bar(self.bar_actions[4 if showing_device else 3])
self.menu_bar.update_lm_actions()
self.menu_bar.setVisible(bool(self.menu_bar.added_actions))
def apply_settings(self):
sz = gprefs['toolbar_icon_size']
sz = {'off':0, 'small':24, 'medium':48, 'large':64}[sz]
style = Qt.ToolButtonStyle.ToolButtonTextUnderIcon
if sz > 0 and gprefs['toolbar_text'] == 'never':
style = Qt.ToolButtonStyle.ToolButtonIconOnly
for bar in self.bars:
bar.setIconSize(QSize(sz, sz))
bar.setToolButtonStyle(style)
if bar.showing_donate:
bar.donate_button.setIconSize(bar.iconSize())
bar.donate_button.setToolButtonStyle(style)