%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)