Centralize management of keyboard shortcuts

This commit is contained in:
Kovid Goyal 2011-08-05 21:33:02 -06:00
parent 902a50c711
commit 292cca0507
17 changed files with 260 additions and 110 deletions

View File

@ -106,6 +106,10 @@ class InterfaceAction(QObject):
self.gui.addAction(self.menuless_qaction) self.gui.addAction(self.menuless_qaction)
self.genesis() self.genesis()
@property
def unique_name(self):
return u'%s(%s)'%(self.__class__.__name__, self.name)
def create_action(self, spec=None, attr='qaction'): def create_action(self, spec=None, attr='qaction'):
if spec is None: if spec is None:
spec = self.action_spec spec = self.action_spec
@ -125,13 +129,20 @@ class InterfaceAction(QObject):
a.setToolTip(text) a.setToolTip(text)
a.setStatusTip(text) a.setStatusTip(text)
a.setWhatsThis(text) a.setWhatsThis(text)
if shortcut: keys = ()
a = ma if attr == 'qaction' else action shortcut_action = action
if isinstance(shortcut, list): desc = tooltip if tooltip else None
a.setShortcuts(shortcut) if attr == 'qaction':
else: shortcut_action = ma
a.setShortcut(shortcut) if shortcut is not None:
setattr(self, attr, action) keys = ((shortcut,) if isinstance(shortcut, basestring) else
tuple(shortcut))
self.gui.keyboard.register_shortcut(self.unique_name + ' - ' + attr,
unicode(shortcut_action.text()), default_keys=keys,
action=shortcut_action, description=desc)
if attr is not None:
setattr(self, attr, action)
if attr == 'qaction' and self.action_add_menu: if attr == 'qaction' and self.action_add_menu:
menu = QMenu() menu = QMenu()
action.setMenu(menu) action.setMenu(menu)
@ -139,6 +150,30 @@ class InterfaceAction(QObject):
menu.addAction(self.menuless_qaction) menu.addAction(self.menuless_qaction)
return action return action
def create_menu_action(self, menu, unique_name, text, icon=None, shortcut=None,
description=None, triggered=None):
ac = menu.addAction(text)
if icon is not None:
if not isinstance(icon, QIcon):
icon = QIcon(I(icon))
ac.setIcon(icon)
keys = ()
if shortcut is not None and shortcut is not False:
keys = ((shortcut,) if isinstance(shortcut, basestring) else
tuple(shortcut))
unique_name = '%s : menu action : %s'%(self.unique_name, unique_name)
if description is not None:
ac.setToolTip(description)
ac.setStatusTip(description)
ac.setWhatsThis(description)
if shortcut is not False:
self.gui.keyboard.register_shortcut(unique_name,
unicode(text), default_keys=keys,
action=ac, description=description)
if triggered is not None:
ac.triggered.connect(triggered)
return ac
def load_resources(self, names): def load_resources(self, names):
''' '''
If this plugin comes in a ZIP file (user added plugin), this method If this plugin comes in a ZIP file (user added plugin), this method

View File

@ -54,20 +54,22 @@ class AddAction(InterfaceAction):
def genesis(self): def genesis(self):
self._add_filesystem_book = self.Dispatcher(self.__add_filesystem_book) self._add_filesystem_book = self.Dispatcher(self.__add_filesystem_book)
self.add_menu = self.qaction.menu() self.add_menu = self.qaction.menu()
self.add_menu.addAction(_('Add books from directories, including ' ma = partial(self.create_menu_action, self.add_menu)
ma('recursive-single', _('Add books from directories, including '
'sub-directories (One book per directory, assumes every ebook ' 'sub-directories (One book per directory, assumes every ebook '
'file is the same book in a different format)'), 'file is the same book in a different format)')).triggered.connect(
self.add_recursive_single) self.add_recursive_single)
self.add_menu.addAction(_('Add books from directories, including ' ma('recursive-multiple', _('Add books from directories, including '
'sub directories (Multiple books per directory, assumes every ' 'sub directories (Multiple books per directory, assumes every '
'ebook file is a different book)'), self.add_recursive_multiple) 'ebook file is a different book)')).triggered.connect(
self.add_recursive_multiple)
self.add_menu.addSeparator() self.add_menu.addSeparator()
self.add_menu.addAction(_('Add Empty book. (Book entry with no ' ma('add-empty', _('Add Empty book. (Book entry with no formats)'),
'formats)'), self.add_empty, _('Shift+Ctrl+E')) shortcut=_('Shift+Ctrl+E')).triggered.connect(self.add_empty)
self.add_menu.addAction(_('Add from ISBN'), self.add_from_isbn) ma('add-isbn', _('Add from ISBN')).triggered.connect(self.add_from_isbn)
self.add_menu.addSeparator() self.add_menu.addSeparator()
self.add_menu.addAction(_('Add files to selected book records'), ma('add-formats', _('Add files to selected book records'),
self.add_formats, _('Shift+A')) triggered=self.add_formats, shortcut=_('Shift+A'))
self.qaction.triggered.connect(self.add_books) self.qaction.triggered.connect(self.add_books)
@ -82,7 +84,8 @@ class AddAction(InterfaceAction):
view = self.gui.library_view view = self.gui.library_view
rows = view.selectionModel().selectedRows() rows = view.selectionModel().selectedRows()
if not rows: if not rows:
return return error_dialog(self.gui, _('No books selected'),
_('Cannot add files as no books are selected'), show=True)
ids = [view.model().id(r) for r in rows] ids = [view.model().id(r) for r in rows]
if len(ids) > 1 and not question_dialog(self.gui, if len(ids) > 1 and not question_dialog(self.gui,

View File

@ -8,7 +8,7 @@ __docformat__ = 'restructuredtext en'
import os import os
from functools import partial from functools import partial
from PyQt4.Qt import QModelIndex, QIcon from PyQt4.Qt import QModelIndex
from calibre.gui2 import error_dialog, Dispatcher from calibre.gui2 import error_dialog, Dispatcher
from calibre.gui2.tools import convert_single_ebook, convert_bulk_ebook from calibre.gui2.tools import convert_single_ebook, convert_bulk_ebook
@ -25,17 +25,19 @@ class ConvertAction(InterfaceAction):
action_add_menu = True action_add_menu = True
def genesis(self): def genesis(self):
cm = self.qaction.menu() m = self.convert_menu = self.qaction.menu()
cm.addAction(self.qaction.icon(), _('Convert individually'), partial(self.convert_ebook, cm = partial(self.create_menu_action, self.convert_menu)
cm('convert-individual', _('Convert individually'),
icon=self.qaction.icon(), triggered=partial(self.convert_ebook,
False, bulk=False)) False, bulk=False))
cm.addAction(_('Bulk convert'), cm('convert-bulk', _('Bulk convert'),
partial(self.convert_ebook, False, bulk=True)) triggered=partial(self.convert_ebook, False, bulk=True))
cm.addSeparator() m.addSeparator()
ac = cm.addAction(QIcon(I('catalog.png')), cm('create-catalog',
_('Create a catalog of the books in your calibre library')) _('Create a catalog of the books in your calibre library'),
ac.triggered.connect(self.gui.iactions['Generate Catalog'].generate_catalog) icon='catalog.png', shortcut=False,
triggered=self.gui.iactions['Generate Catalog'].generate_catalog)
self.qaction.triggered.connect(self.convert_ebook) self.qaction.triggered.connect(self.convert_ebook)
self.convert_menu = cm
self.conversion_jobs = {} self.conversion_jobs = {}
def location_selected(self, loc): def location_selected(self, loc):

View File

@ -90,21 +90,23 @@ class DeleteAction(InterfaceAction):
def genesis(self): def genesis(self):
self.qaction.triggered.connect(self.delete_books) self.qaction.triggered.connect(self.delete_books)
self.delete_menu = self.qaction.menu() self.delete_menu = self.qaction.menu()
self.delete_menu.addAction( m = partial(self.create_menu_action, self.delete_menu)
m('delete-specific',
_('Remove files of a specific format from selected books..'), _('Remove files of a specific format from selected books..'),
self.delete_selected_formats) triggered=self.delete_selected_formats)
self.delete_menu.addAction( m('delete-except',
_('Remove all formats from selected books, except...'), _('Remove all formats from selected books, except...'),
self.delete_all_but_selected_formats) triggered=self.delete_all_but_selected_formats)
self.delete_menu.addAction( m('delete-all',
_('Remove all formats from selected books'), _('Remove all formats from selected books'),
self.delete_all_formats) triggered=self.delete_all_formats)
self.delete_menu.addAction( m('delete-covers',
_('Remove covers from selected books'), self.delete_covers) _('Remove covers from selected books'),
triggered=self.delete_covers)
self.delete_menu.addSeparator() self.delete_menu.addSeparator()
self.delete_menu.addAction( m('delete-matching',
_('Remove matching books from device'), _('Remove matching books from device'),
self.remove_matching_books_from_device) triggered=self.remove_matching_books_from_device)
self.qaction.setMenu(self.delete_menu) self.qaction.setMenu(self.delete_menu)
self.delete_memory = {} self.delete_memory = {}

View File

@ -60,6 +60,15 @@ class ShareConnMenu(QMenu): # {{{
self.email_actions = [] self.email_actions = []
if hasattr(parent, 'keyboard'):
r = parent.keyboard.register_shortcut
prefix = 'Share/Connect Menu '
for attr in ('folder', 'bambook', 'itunes'):
if not (iswindows or isosx) and attr == 'itunes':
continue
ac = getattr(self, 'connect_to_%s_action'%attr)
r(prefix + attr, unicode(ac.text()), action=ac)
def server_state_changed(self, running): def server_state_changed(self, running):
text = _('Start Content Server') text = _('Start Content Server')
if running: if running:

View File

@ -8,7 +8,7 @@ __docformat__ = 'restructuredtext en'
import os import os
from functools import partial from functools import partial
from PyQt4.Qt import Qt, QMenu, QModelIndex, QTimer from PyQt4.Qt import QMenu, QModelIndex, QTimer
from calibre.gui2 import error_dialog, Dispatcher, question_dialog from calibre.gui2 import error_dialog, Dispatcher, question_dialog
from calibre.gui2.dialogs.metadata_bulk import MetadataBulkDialog from calibre.gui2.dialogs.metadata_bulk import MetadataBulkDialog
@ -27,37 +27,38 @@ class EditMetadataAction(InterfaceAction):
action_add_menu = True action_add_menu = True
def genesis(self): def genesis(self):
self.create_action(spec=(_('Merge book records'), 'merge_books.png',
None, _('M')), attr='action_merge')
md = self.qaction.menu() md = self.qaction.menu()
md.addAction(self.qaction.icon(), _('Edit metadata individually'), cm = partial(self.create_menu_action, md)
partial(self.edit_metadata, False, bulk=False)) cm('individual', _('Edit metadata individually'), icon=self.qaction.icon(),
triggered=partial(self.edit_metadata, False, bulk=False))
md.addSeparator() md.addSeparator()
md.addAction(_('Edit metadata in bulk'), cm('bulk', _('Edit metadata in bulk'),
partial(self.edit_metadata, False, bulk=True)) triggered=partial(self.edit_metadata, False, bulk=True))
md.addSeparator() md.addSeparator()
md.addAction(_('Download metadata and covers'), self.download_metadata, cm('download', _('Download metadata and covers'),
Qt.ControlModifier+Qt.Key_D) triggered=partial(self.download_metadata, ids=None),
shortcut='Ctrl+D')
self.metadata_menu = md self.metadata_menu = md
mb = QMenu() mb = QMenu()
mb.addAction(_('Merge into first selected book - delete others'), cm2 = partial(self.create_menu_action, mb)
self.merge_books) cm2('merge delete', _('Merge into first selected book - delete others'),
triggered=self.merge_books)
mb.addSeparator() mb.addSeparator()
mb.addAction(_('Merge into first selected book - keep others'), cm2('merge keep', _('Merge into first selected book - keep others'),
partial(self.merge_books, safe_merge=True), triggered=partial(self.merge_books, safe_merge=True),
Qt.AltModifier+Qt.Key_M) shortcut='Alt+M')
mb.addSeparator() mb.addSeparator()
mb.addAction(_('Merge only formats into first selected book - delete others'), cm2('merge formats', _('Merge only formats into first selected book - delete others'),
partial(self.merge_books, merge_only_formats=True), triggered=partial(self.merge_books, merge_only_formats=True),
Qt.AltModifier+Qt.ShiftModifier+Qt.Key_M) shortcut='Alt+Shift+M')
self.merge_menu = mb self.merge_menu = mb
self.action_merge.setMenu(mb)
md.addSeparator() md.addSeparator()
md.addAction(self.action_merge) self.action_merge = cm('merge', _('Merge book records'), icon='merge_books.png',
shortcut=_('M'), triggered=self.merge_books)
self.action_merge.setMenu(mb)
self.qaction.triggered.connect(self.edit_metadata) self.qaction.triggered.connect(self.edit_metadata)
self.action_merge.triggered.connect(self.merge_books)
def location_selected(self, loc): def location_selected(self, loc):
enabled = loc == 'library' enabled = loc == 'library'

View File

@ -5,6 +5,8 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
from functools import partial
from PyQt4.Qt import QIcon, Qt from PyQt4.Qt import QIcon, Qt
from calibre.gui2.actions import InterfaceAction from calibre.gui2.actions import InterfaceAction
@ -21,18 +23,17 @@ class PreferencesAction(InterfaceAction):
def genesis(self): def genesis(self):
pm = self.qaction.menu() pm = self.qaction.menu()
cm = partial(self.create_menu_action, pm)
if isosx: if isosx:
pm.addAction(QIcon(I('config.png')), _('Preferences'), self.do_config) pm.addAction(QIcon(I('config.png')), _('Preferences'), self.do_config)
pm.addAction(QIcon(I('wizard.png')), _('Run welcome wizard'), cm('welcome wizard', _('Run welcome wizard'),
self.gui.run_wizard) icon='wizard.png', triggered=self.gui.run_wizard)
pm.addAction(QIcon(I('plugins/plugin_updater.png')), cm('plugin updater', _('Get plugins to enhance calibre'),
_('Get plugins to enhance calibre'), self.get_plugins) icon='plugins/plugin_updater.png', triggered=self.get_plugins)
if not DEBUG: if not DEBUG:
pm.addSeparator() pm.addSeparator()
ac = pm.addAction(QIcon(I('debug.png')), _('Restart in debug mode'), cm('restart', _('Restart in debug mode'), icon='debug.png',
self.debug_restart) triggered=self.debug_restart, shortcut='Ctrl+Shift+R')
ac.setShortcut('Ctrl+Shift+R')
self.gui.addAction(ac)
self.preferences_menu = pm self.preferences_menu = pm
for x in (self.gui.preferences_action, self.qaction): for x in (self.gui.preferences_action, self.qaction):

View File

@ -44,15 +44,16 @@ class SaveToDiskAction(InterfaceAction):
def genesis(self): def genesis(self):
self.qaction.triggered.connect(self.save_to_disk) self.qaction.triggered.connect(self.save_to_disk)
self.save_menu = self.qaction.menu() self.save_menu = self.qaction.menu()
self.save_menu.addAction(_('Save to disk in a single directory'), cm = partial(self.create_menu_action, self.save_menu)
partial(self.save_to_single_dir, False)) cm('single dir', _('Save to disk in a single directory'),
self.save_menu.addAction(_('Save only %s format to disk')% triggered=partial(self.save_to_single_dir, False))
cm('single format', _('Save only %s format to disk')%
prefs['output_format'].upper(), prefs['output_format'].upper(),
partial(self.save_single_format_to_disk, False)) triggered=partial(self.save_single_format_to_disk, False))
self.save_menu.addAction( cm('fingle dir and format',
_('Save only %s format to disk in a single directory')% _('Save only %s format to disk in a single directory')%
prefs['output_format'].upper(), prefs['output_format'].upper(),
partial(self.save_single_fmt_to_single_dir, False)) triggered=partial(self.save_single_fmt_to_single_dir, False))
self.save_sub_menu = SaveMenu(self.gui) self.save_sub_menu = SaveMenu(self.gui)
self.save_sub_menu_action = self.save_menu.addMenu(self.save_sub_menu) self.save_sub_menu_action = self.save_menu.addMenu(self.save_sub_menu)
self.save_sub_menu.save_fmt.connect(self.save_specific_format_disk) self.save_sub_menu.save_fmt.connect(self.save_specific_format_disk)

View File

@ -24,16 +24,21 @@ class StoreAction(InterfaceAction):
def genesis(self): def genesis(self):
self.qaction.triggered.connect(self.do_search) self.qaction.triggered.connect(self.do_search)
self.store_menu = self.qaction.menu() self.store_menu = self.qaction.menu()
self.load_menu() cm = partial(self.create_menu_action, self.store_menu)
for x, t in [('author', _('author')), ('title', _('title')),
def load_menu(self): ('book', _('book'))]:
self.store_menu.clear() func = getattr(self, 'search_%s'%('author_title' if x == 'book'
self.store_menu.addAction(self.menuless_qaction) else x))
self.store_menu.addAction(_('Search for this author'), self.search_author) ac = cm(x, _('Search for this %s'%t), triggered=func)
self.store_menu.addAction(_('Search for this title'), self.search_title) setattr(self, 'action_search_by_'+x, ac)
self.store_menu.addAction(_('Search for this book'), self.search_author_title)
self.store_menu.addSeparator() self.store_menu.addSeparator()
self.store_list_menu = self.store_menu.addMenu(_('Stores')) self.store_list_menu = self.store_menu.addMenu(_('Stores'))
self.load_menu()
self.store_menu.addSeparator()
cm('choose stores', _('Choose stores'), triggered=self.choose)
def load_menu(self):
self.store_list_menu.clear()
icon = QIcon() icon = QIcon()
icon.addFile(I('donate.png'), QSize(16, 16)) icon.addFile(I('donate.png'), QSize(16, 16))
for n, p in sorted(self.gui.istores.items(), key=lambda x: x[0].lower()): for n, p in sorted(self.gui.istores.items(), key=lambda x: x[0].lower()):
@ -41,8 +46,6 @@ class StoreAction(InterfaceAction):
self.store_list_menu.addAction(icon, n, partial(self.open_store, p)) self.store_list_menu.addAction(icon, n, partial(self.open_store, p))
else: else:
self.store_list_menu.addAction(n, partial(self.open_store, p)) self.store_list_menu.addAction(n, partial(self.open_store, p))
self.store_menu.addSeparator()
self.store_menu.addAction(_('Choose stores'), self.choose)
def do_search(self): def do_search(self):
return self.search() return self.search()

View File

@ -6,6 +6,7 @@ __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import os, time import os, time
from functools import partial
from PyQt4.Qt import Qt, QAction, pyqtSignal from PyQt4.Qt import Qt, QAction, pyqtSignal
@ -43,36 +44,33 @@ class ViewAction(InterfaceAction):
self.qaction.triggered.connect(self.view_book) self.qaction.triggered.connect(self.view_book)
self.view_action = self.menuless_qaction self.view_action = self.menuless_qaction
self.view_menu = self.qaction.menu() self.view_menu = self.qaction.menu()
ac = self.view_specific_action = QAction(_('View specific format'), cm = partial(self.create_menu_action, self.view_menu)
self.gui) self.view_specific_action = cm('specific', _('View specific format'),
ac.setShortcut(Qt.AltModifier+Qt.Key_V) shortcut='Alt+V', triggered=self.view_specific_format)
ac.triggered.connect(self.view_specific_format, type=Qt.QueuedConnection) self.action_pick_random = cm('pick random', _('Read a random book'),
ac = self.create_action(spec=(_('Read a random book'), 'random.png', icon='random.png', triggered=self.view_random)
None, None), attr='action_pick_random') self.clear_sep1 = self.view_menu.addSeparator()
ac.triggered.connect(self.view_random) self.clear_sep2 = self.view_menu.addSeparator()
ac = self.clear_history_action = QAction( self.clear_history_action = cm('clear history',
_('Clear recently viewed list'), self.gui) _('Clear recently viewed list'), triggered=self.clear_history)
ac.triggered.connect(self.clear_history) self.history_actions = [self.clear_sep1]
def initialization_complete(self): def initialization_complete(self):
self.build_menus(self.gui.current_db) self.build_menus(self.gui.current_db)
def build_menus(self, db): def build_menus(self, db):
self.view_menu.clear() for ac in self.history_actions:
self.view_menu.addAction(self.view_action) self.view_menu.removeAction(ac)
self.view_menu.addAction(self.view_specific_action)
self.view_menu.addSeparator()
self.view_menu.addAction(self.action_pick_random)
self.history_actions = [] self.history_actions = []
history = db.prefs.get('gui_view_history', []) history = db.prefs.get('gui_view_history', [])
if history: if history:
self.view_menu.addSeparator() self.view_menu.insertAction(self.clear_sep2, self.clear_sep1)
self.history_actions.append(self.clear_sep1)
for id_, title in history: for id_, title in history:
ac = HistoryAction(id_, title, self.view_menu) ac = HistoryAction(id_, title, self.view_menu)
self.view_menu.addAction(ac) self.view_menu.insertAction(self.clear_sep2, ac)
ac.view_historical.connect(self.view_historical) ac.view_historical.connect(self.view_historical)
self.view_menu.addSeparator() self.history_actions.append(ac)
self.view_menu.addAction(self.clear_history_action)
def clear_history(self): def clear_history(self):
db = self.gui.current_db db = self.gui.current_db

View File

@ -218,7 +218,7 @@ class LayoutMixin(object): # {{{
self.bd_splitter = Splitter('book_details_splitter', self.bd_splitter = Splitter('book_details_splitter',
_('Book Details'), I('book.png'), _('Book Details'), I('book.png'),
orientation=Qt.Vertical, parent=self, side_index=1, orientation=Qt.Vertical, parent=self, side_index=1,
shortcut=_('Alt+D')) shortcut=_('Shift+Alt+D'))
self.bd_splitter.addWidget(self.stack) self.bd_splitter.addWidget(self.stack)
self.bd_splitter.addWidget(self.book_details) self.bd_splitter.addWidget(self.book_details)
self.bd_splitter.setCollapsible(self.bd_splitter.other_index, False) self.bd_splitter.setCollapsible(self.bd_splitter.other_index, False)

View File

@ -0,0 +1,71 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from collections import OrderedDict
from PyQt4.Qt import (QObject, QKeySequence)
from calibre.utils.config import JSONConfig
from calibre.constants import DEBUG
from calibre import prints
class NameConflict(ValueError):
pass
class Manager(QObject):
def __init__(self, parent=None):
QObject.__init__(self, parent)
self.config = JSONConfig('shortcuts/main')
self.custom_keys_map = {}
self.shortcuts = OrderedDict()
self.keys_map = {}
for unique_name, keys in self.config.get(
'map', {}).iteritems():
self.custom_keys_map[unique_name] = tuple(keys)
def register_shortcut(self, unique_name, name, default_keys=(),
description=None, action=None):
if unique_name in self.shortcuts:
name = self.shortcuts[unique_name]['name']
raise NameConflict('Shortcut for %r already registered by %s'%(
unique_name, name))
shortcut = {'name':name, 'desc':description, 'action': action,
'default_keys':tuple(default_keys)}
self.shortcuts[unique_name] = shortcut
def finalize(self):
seen = {}
for unique_name, shortcut in self.shortcuts.iteritems():
custom_keys = self.custom_keys_map.get(unique_name, None)
if custom_keys is None:
candidates = shortcut['default_keys']
else:
candidates = custom_keys
keys = []
for x in candidates:
ks = QKeySequence(x, QKeySequence.PortableText)
x = unicode(ks.toString(QKeySequence.PortableText))
if x in seen:
if DEBUG:
prints('Key %r for shortcut %s is already used by'
' %s, ignoring'%(x, shortcut['name'], seen[x]['name']))
continue
seen[x] = shortcut
keys.append(ks)
keys = tuple(keys)
#print (111111, unique_name, candidates, keys)
self.keys_map[unique_name] = keys
ac = shortcut['action']
if ac is not None:
ac.setShortcuts(list(keys))

View File

@ -8,7 +8,7 @@ __docformat__ = 'restructuredtext en'
from functools import partial from functools import partial
from PyQt4.Qt import (QIcon, Qt, QWidget, QSize, from PyQt4.Qt import (QIcon, Qt, QWidget, QSize,
pyqtSignal, QToolButton, QMenu, pyqtSignal, QToolButton, QMenu, QAction,
QObject, QVBoxLayout, QSizePolicy, QLabel, QHBoxLayout, QActionGroup) QObject, QVBoxLayout, QSizePolicy, QLabel, QHBoxLayout, QActionGroup)
@ -178,7 +178,12 @@ class SearchBar(QWidget): # {{{
x.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) x.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
parent.advanced_search_button = x = QToolButton(self) parent.advanced_search_button = x = QToolButton(self)
parent.advanced_search_button.setShortcut(_("Shift+Ctrl+F")) parent.advanced_search_toggle_action = ac = QAction(parent)
parent.addAction(ac)
parent.keyboard.register_shortcut('advanced search toggle',
_('Advanced search'), default_keys=(_("Shift+Ctrl+F"),),
action=ac)
ac.triggered.connect(x.click)
x.setIcon(QIcon(I('search.png'))) x.setIcon(QIcon(I('search.png')))
l.addWidget(x) l.addWidget(x)
x.setToolTip(_("Advanced search")) x.setToolTip(_("Advanced search"))

View File

@ -376,9 +376,12 @@ class SearchBoxMixin(object): # {{{
self.search.clear() self.search.clear()
self.search.setMaximumWidth(self.width()-150) self.search.setMaximumWidth(self.width()-150)
self.action_focus_search = QAction(self) self.action_focus_search = QAction(self)
shortcuts = QKeySequence.keyBindings(QKeySequence.Find) shortcuts = list(
shortcuts = list(shortcuts) + [QKeySequence('/'), QKeySequence('Alt+S')] map(lambda x:unicode(x.toString()),
self.action_focus_search.setShortcuts(shortcuts) QKeySequence.keyBindings(QKeySequence.Find)))
shortcuts += ['/', 'Alt+S']
self.keyboard.register_shortcut('start search', _('Start search'),
default_keys=shortcuts, action=self.action_focus_search)
self.action_focus_search.triggered.connect(self.focus_search_box) self.action_focus_search.triggered.connect(self.focus_search_box)
self.addAction(self.action_focus_search) self.addAction(self.action_focus_search)
self.search.setStatusTip(re.sub(r'<\w+>', ' ', self.search.setStatusTip(re.sub(r'<\w+>', ' ',

View File

@ -16,7 +16,7 @@ from collections import OrderedDict
from PyQt4.Qt import (Qt, SIGNAL, QTimer, QHelpEvent, QAction, from PyQt4.Qt import (Qt, SIGNAL, QTimer, QHelpEvent, QAction,
QMenu, QIcon, pyqtSignal, QUrl, QMenu, QIcon, pyqtSignal, QUrl,
QDialog, QSystemTrayIcon, QApplication, QKeySequence) QDialog, QSystemTrayIcon, QApplication)
from calibre import prints from calibre import prints
from calibre.constants import __appname__, isosx from calibre.constants import __appname__, isosx
@ -40,6 +40,7 @@ from calibre.gui2.init import LibraryViewMixin, LayoutMixin
from calibre.gui2.search_box import SearchBoxMixin, SavedSearchBoxMixin from calibre.gui2.search_box import SearchBoxMixin, SavedSearchBoxMixin
from calibre.gui2.search_restriction_mixin import SearchRestrictionMixin from calibre.gui2.search_restriction_mixin import SearchRestrictionMixin
from calibre.gui2.tag_browser.ui import TagBrowserMixin from calibre.gui2.tag_browser.ui import TagBrowserMixin
from calibre.gui2.keyboard import Manager
class Listener(Thread): # {{{ class Listener(Thread): # {{{
@ -104,6 +105,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
def __init__(self, opts, parent=None, gui_debug=None): def __init__(self, opts, parent=None, gui_debug=None):
global _gui global _gui
MainWindow.__init__(self, opts, parent=parent, disable_automatic_gc=True) MainWindow.__init__(self, opts, parent=parent, disable_automatic_gc=True)
self.keyboard = Manager(self)
_gui = self _gui = self
self.opts = opts self.opts = opts
self.device_connected = None self.device_connected = None
@ -238,7 +240,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
self.eject_action.setEnabled(False) self.eject_action.setEnabled(False)
self.addAction(self.quit_action) self.addAction(self.quit_action)
self.system_tray_menu.addAction(self.quit_action) self.system_tray_menu.addAction(self.quit_action)
self.quit_action.setShortcut(QKeySequence(Qt.CTRL + Qt.Key_Q)) self.keyboard.register_shortcut('quit calibre', _('Quit calibre'),
default_keys=('Ctrl+Q',), action=self.quit_action)
self.system_tray_icon.setContextMenu(self.system_tray_menu) self.system_tray_icon.setContextMenu(self.system_tray_menu)
self.connect(self.quit_action, SIGNAL('triggered(bool)'), self.quit) self.connect(self.quit_action, SIGNAL('triggered(bool)'), self.quit)
self.connect(self.donate_action, SIGNAL('triggered(bool)'), self.donate) self.connect(self.donate_action, SIGNAL('triggered(bool)'), self.donate)
@ -249,7 +252,9 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
self.esc_action = QAction(self) self.esc_action = QAction(self)
self.addAction(self.esc_action) self.addAction(self.esc_action)
self.esc_action.setShortcut(QKeySequence(Qt.Key_Escape)) self.keyboard.register_shortcut('clear current search',
_('Clear the current search'), default_keys=('Esc',),
action=self.esc_action)
self.esc_action.triggered.connect(self.esc) self.esc_action.triggered.connect(self.esc)
####################### Start spare job server ######################## ####################### Start spare job server ########################
@ -340,6 +345,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
raise raise
self.device_manager.set_current_library_uuid(db.library_id) self.device_manager.set_current_library_uuid(db.library_id)
self.keyboard.finalize()
# Collect cycles now # Collect cycles now
gc.collect() gc.collect()

View File

@ -1063,9 +1063,16 @@ class Splitter(QSplitter):
self.action_toggle = QAction(QIcon(icon), _('Toggle') + ' ' + label, self.action_toggle = QAction(QIcon(icon), _('Toggle') + ' ' + label,
self) self)
self.action_toggle.triggered.connect(self.toggle_triggered) self.action_toggle.triggered.connect(self.toggle_triggered)
self.action_toggle.setShortcut(shortcut)
if parent is not None: if parent is not None:
parent.addAction(self.action_toggle) parent.addAction(self.action_toggle)
if hasattr(parent, 'keyboard'):
parent.keyboard.register_shortcut('splitter %s %s'%(name,
label), unicode(self.action_toggle.text()),
default_keys=(shortcut,), action=self.action_toggle)
else:
self.action_toggle.setShortcut(shortcut)
else:
self.action_toggle.setShortcut(shortcut)
def toggle_triggered(self, *args): def toggle_triggered(self, *args):
self.toggle_side_pane() self.toggle_side_pane()

View File

@ -547,6 +547,8 @@ Calibre has several keyboard shortcuts to save you time and mouse movement. Thes
- Toggle jobs list - Toggle jobs list
* - :kbd:`Alt+Shift+B` * - :kbd:`Alt+Shift+B`
- Toggle Cover Browser - Toggle Cover Browser
* - :kbd:`Alt+Shift+B`
- Toggle Book Details panel
* - :kbd:`Alt+Shift+T` * - :kbd:`Alt+Shift+T`
- Toggle Tag Browser - Toggle Tag Browser
* - :kbd:`Alt+A` * - :kbd:`Alt+A`