1#!/usr/local/bin/python3.8 2# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai 3 4 5__license__ = 'GPL v3' 6__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>' 7__docformat__ = 'restructuredtext en' 8 9from functools import partial 10from zipfile import ZipFile 11 12from qt.core import (QToolButton, QAction, QIcon, QObject, QMenu, 13 QKeySequence) 14 15from calibre import prints 16from calibre.constants import ismacos 17from calibre.gui2 import Dispatcher 18from calibre.gui2.keyboard import NameConflict 19from polyglot.builtins import string_or_bytes 20 21 22def menu_action_unique_name(plugin, unique_name): 23 return '%s : menu action : %s'%(plugin.unique_name, unique_name) 24 25 26class InterfaceAction(QObject): 27 28 ''' 29 A plugin representing an "action" that can be taken in the graphical user 30 interface. All the items in the toolbar and context menus are implemented 31 by these plugins. 32 33 Note that this class is the base class for these plugins, however, to 34 integrate the plugin with calibre's plugin system, you have to make a 35 wrapper class that references the actual plugin. See the 36 :mod:`calibre.customize.builtins` module for examples. 37 38 If two :class:`InterfaceAction` objects have the same name, the one with higher 39 priority takes precedence. 40 41 Sub-classes should implement the :meth:`genesis`, :meth:`library_changed`, 42 :meth:`location_selected`, :meth:`shutting_down`, 43 :meth:`initialization_complete` and :meth:`tag_browser_context_action` methods. 44 45 Once initialized, this plugin has access to the main calibre GUI via the 46 :attr:`gui` member. You can access other plugins by name, for example:: 47 48 self.gui.iactions['Save To Disk'] 49 50 To access the actual plugin, use the :attr:`interface_action_base_plugin` 51 attribute, this attribute only becomes available after the plugin has been 52 initialized. Useful if you want to use methods from the plugin class like 53 do_user_config(). 54 55 The QAction specified by :attr:`action_spec` is automatically create and 56 made available as ``self.qaction``. 57 58 ''' 59 60 #: The plugin name. If two plugins with the same name are present, the one 61 #: with higher priority takes precedence. 62 name = 'Implement me' 63 64 #: The plugin priority. If two plugins with the same name are present, the one 65 #: with higher priority takes precedence. 66 priority = 1 67 68 #: The menu popup type for when this plugin is added to a toolbar 69 popup_type = QToolButton.ToolButtonPopupMode.MenuButtonPopup 70 71 #: Whether this action should be auto repeated when its shortcut 72 #: key is held down. 73 auto_repeat = False 74 75 #: Of the form: (text, icon_path, tooltip, keyboard shortcut) 76 #: icon, tooltip and keyboard shortcut can be None 77 #: shortcut must be a string, None or tuple of shortcuts. 78 #: If None, a keyboard shortcut corresponding to the action is not 79 #: registered. If you pass an empty tuple, then the shortcut is registered 80 #: with no default key binding. 81 action_spec = ('text', 'icon', None, None) 82 83 #: If True, a menu is automatically created and added to self.qaction 84 action_add_menu = False 85 86 #: If True, a clone of self.qaction is added to the menu of self.qaction 87 #: If you want the text of this action to be different from that of 88 #: self.qaction, set this variable to the new text 89 action_menu_clone_qaction = False 90 91 #: Set of locations to which this action must not be added. 92 #: See :attr:`all_locations` for a list of possible locations 93 dont_add_to = frozenset() 94 95 #: Set of locations from which this action must not be removed. 96 #: See :attr:`all_locations` for a list of possible locations 97 dont_remove_from = frozenset() 98 99 all_locations = frozenset(['toolbar', 'toolbar-device', 'context-menu', 100 'context-menu-device', 'toolbar-child', 'menubar', 'menubar-device', 101 'context-menu-cover-browser', 'context-menu-split']) 102 103 #: Type of action 104 #: 'current' means acts on the current view 105 #: 'global' means an action that does not act on the current view, but rather 106 #: on calibre as a whole 107 action_type = 'global' 108 109 #: If True, then this InterfaceAction will have the opportunity to interact 110 #: with drag and drop events. See the methods, :meth:`accept_enter_event`, 111 #: :meth`:accept_drag_move_event`, :meth:`drop_event` for details. 112 accepts_drops = False 113 114 def __init__(self, parent, site_customization): 115 QObject.__init__(self, parent) 116 self.setObjectName(self.name) 117 self.gui = parent 118 self.site_customization = site_customization 119 self.interface_action_base_plugin = None 120 121 def accept_enter_event(self, event, mime_data): 122 ''' This method should return True iff this interface action is capable 123 of handling the drag event. Do not call accept/ignore on the event, 124 that will be taken care of by the calibre UI.''' 125 return False 126 127 def accept_drag_move_event(self, event, mime_data): 128 ''' This method should return True iff this interface action is capable 129 of handling the drag event. Do not call accept/ignore on the event, 130 that will be taken care of by the calibre UI.''' 131 return False 132 133 def drop_event(self, event, mime_data): 134 ''' This method should perform some useful action and return True 135 iff this interface action is capable of handling the drop event. Do not 136 call accept/ignore on the event, that will be taken care of by the 137 calibre UI. You should not perform blocking/long operations in this 138 function. Instead emit a signal or use QTimer.singleShot and return 139 quickly. See the builtin actions for examples.''' 140 return False 141 142 def do_genesis(self): 143 self.Dispatcher = partial(Dispatcher, parent=self) 144 self.create_action() 145 self.gui.addAction(self.qaction) 146 self.gui.addAction(self.menuless_qaction) 147 self.genesis() 148 self.location_selected('library') 149 150 @property 151 def unique_name(self): 152 bn = self.__class__.__name__ 153 if getattr(self.interface_action_base_plugin, 'name'): 154 bn = self.interface_action_base_plugin.name 155 return 'Interface Action: %s (%s)'%(bn, self.name) 156 157 def create_action(self, spec=None, attr='qaction', shortcut_name=None, persist_shortcut=False): 158 if spec is None: 159 spec = self.action_spec 160 text, icon, tooltip, shortcut = spec 161 if icon is not None: 162 action = QAction(QIcon(I(icon)), text, self.gui) 163 else: 164 action = QAction(text, self.gui) 165 if attr == 'qaction': 166 if hasattr(self.action_menu_clone_qaction, 'rstrip'): 167 mt = str(self.action_menu_clone_qaction) 168 else: 169 mt = action.text() 170 self.menuless_qaction = ma = QAction(action.icon(), mt, self.gui) 171 ma.triggered.connect(action.trigger) 172 for a in ((action, ma) if attr == 'qaction' else (action,)): 173 a.setAutoRepeat(self.auto_repeat) 174 text = tooltip if tooltip else text 175 a.setToolTip(text) 176 a.setStatusTip(text) 177 a.setWhatsThis(text) 178 shortcut_action = action 179 desc = tooltip if tooltip else None 180 if attr == 'qaction': 181 shortcut_action = ma 182 if shortcut is not None: 183 keys = ((shortcut,) if isinstance(shortcut, string_or_bytes) else 184 tuple(shortcut)) 185 if shortcut_name is None and spec[0]: 186 shortcut_name = str(spec[0]) 187 188 if shortcut_name and self.action_spec[0] and not ( 189 attr == 'qaction' and self.popup_type == QToolButton.ToolButtonPopupMode.InstantPopup): 190 try: 191 self.gui.keyboard.register_shortcut(self.unique_name + ' - ' + attr, 192 shortcut_name, default_keys=keys, 193 action=shortcut_action, description=desc, 194 group=self.action_spec[0], 195 persist_shortcut=persist_shortcut) 196 except NameConflict as e: 197 try: 198 prints(str(e)) 199 except: 200 pass 201 shortcut_action.setShortcuts([QKeySequence(key, 202 QKeySequence.SequenceFormat.PortableText) for key in keys]) 203 else: 204 self.shortcut_action_for_context_menu = shortcut_action 205 if ismacos: 206 # In Qt 5 keyboard shortcuts dont work unless the 207 # action is explicitly added to the main window 208 self.gui.addAction(shortcut_action) 209 210 if attr is not None: 211 setattr(self, attr, action) 212 if attr == 'qaction' and self.action_add_menu: 213 menu = QMenu() 214 action.setMenu(menu) 215 if self.action_menu_clone_qaction: 216 menu.addAction(self.menuless_qaction) 217 return action 218 219 def create_menu_action(self, menu, unique_name, text, icon=None, shortcut=None, 220 description=None, triggered=None, shortcut_name=None, persist_shortcut=False): 221 ''' 222 Convenience method to easily add actions to a QMenu. 223 Returns the created QAction. This action has one extra attribute 224 calibre_shortcut_unique_name which if not None refers to the unique 225 name under which this action is registered with the keyboard manager. 226 227 :param menu: The QMenu the newly created action will be added to 228 :param unique_name: A unique name for this action, this must be 229 globally unique, so make it as descriptive as possible. If in doubt, add 230 an UUID to it. 231 :param text: The text of the action. 232 :param icon: Either a QIcon or a file name. The file name is passed to 233 the I() builtin, so you do not need to pass the full path to the images 234 folder. 235 :param shortcut: A string, a list of strings, None or False. If False, 236 no keyboard shortcut is registered for this action. If None, a keyboard 237 shortcut with no default keybinding is registered. String and list of 238 strings register a shortcut with default keybinding as specified. 239 :param description: A description for this action. Used to set 240 tooltips. 241 :param triggered: A callable which is connected to the triggered signal 242 of the created action. 243 :param shortcut_name: The text displayed to the user when customizing 244 the keyboard shortcuts for this action. By default it is set to the 245 value of ``text``. 246 :param persist_shortcut: Shortcuts for actions that don't 247 always appear, or are library dependent, may disappear 248 when other keyboard shortcuts are edited unless 249 ```persist_shortcut``` is set True. 250 251 ''' 252 if shortcut_name is None: 253 shortcut_name = str(text) 254 ac = menu.addAction(text) 255 if icon is not None: 256 if not isinstance(icon, QIcon): 257 icon = QIcon(I(icon)) 258 ac.setIcon(icon) 259 keys = () 260 if shortcut is not None and shortcut is not False: 261 keys = ((shortcut,) if isinstance(shortcut, string_or_bytes) else 262 tuple(shortcut)) 263 unique_name = menu_action_unique_name(self, unique_name) 264 if description is not None: 265 ac.setToolTip(description) 266 ac.setStatusTip(description) 267 ac.setWhatsThis(description) 268 269 ac.calibre_shortcut_unique_name = unique_name 270 if shortcut is not False: 271 self.gui.keyboard.register_shortcut(unique_name, 272 shortcut_name, default_keys=keys, 273 action=ac, description=description, group=self.action_spec[0], 274 persist_shortcut=persist_shortcut) 275 # In Qt 5 keyboard shortcuts dont work unless the 276 # action is explicitly added to the main window and on OSX and 277 # Unity since the menu might be exported, the shortcuts won't work 278 self.gui.addAction(ac) 279 if triggered is not None: 280 ac.triggered.connect(triggered) 281 return ac 282 283 def load_resources(self, names): 284 ''' 285 If this plugin comes in a ZIP file (user added plugin), this method 286 will allow you to load resources from the ZIP file. 287 288 For example to load an image:: 289 290 pixmap = QPixmap() 291 pixmap.loadFromData(tuple(self.load_resources(['images/icon.png']).values())[0]) 292 icon = QIcon(pixmap) 293 294 :param names: List of paths to resources in the ZIP file using / as separator 295 296 :return: A dictionary of the form ``{name : file_contents}``. Any names 297 that were not found in the ZIP file will not be present in the 298 dictionary. 299 300 ''' 301 if self.plugin_path is None: 302 raise ValueError('This plugin was not loaded from a ZIP file') 303 ans = {} 304 with ZipFile(self.plugin_path, 'r') as zf: 305 for candidate in zf.namelist(): 306 if candidate in names: 307 ans[candidate] = zf.read(candidate) 308 return ans 309 310 def genesis(self): 311 ''' 312 Setup this plugin. Only called once during initialization. self.gui is 313 available. The action specified by :attr:`action_spec` is available as 314 ``self.qaction``. 315 ''' 316 pass 317 318 def location_selected(self, loc): 319 ''' 320 Called whenever the book list being displayed in calibre changes. 321 Currently values for loc are: ``library, main, card and cardb``. 322 323 This method should enable/disable this action and its sub actions as 324 appropriate for the location. 325 ''' 326 pass 327 328 def library_changed(self, db): 329 ''' 330 Called whenever the current library is changed. 331 332 :param db: The LibraryDatabase corresponding to the current library. 333 334 ''' 335 pass 336 337 def gui_layout_complete(self): 338 ''' 339 Called once per action when the layout of the main GUI is 340 completed. If your action needs to make changes to the layout, they 341 should be done here, rather than in :meth:`initialization_complete`. 342 ''' 343 pass 344 345 def initialization_complete(self): 346 ''' 347 Called once per action when the initialization of the main GUI is 348 completed. 349 ''' 350 pass 351 352 def tag_browser_context_action(self, index): 353 ''' 354 Called when displaying the context menu in the Tag browser. ``index`` is 355 the QModelIndex that points to the Tag browser item that was right clicked. 356 Test it for validity with index.valid() and get the underlying TagTreeItem 357 object with index.data(Qt.ItemDataRole.UserRole). Any action objects 358 yielded by this method will be added to the context menu. 359 ''' 360 if False: 361 yield QAction() 362 363 def shutting_down(self): 364 ''' 365 Called once per plugin when the main GUI is in the process of shutting 366 down. Release any used resources, but try not to block the shutdown for 367 long periods of time. 368 ''' 369 pass 370