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