diff --git a/imgsrc/keyboard-prefs.svg b/imgsrc/keyboard-prefs.svg new file mode 100644 index 0000000000..bcdc07f7b8 --- /dev/null +++ b/imgsrc/keyboard-prefs.svg @@ -0,0 +1,912 @@ + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/images/keyboard-prefs.png b/resources/images/keyboard-prefs.png new file mode 100644 index 0000000000..68b8d7fb88 Binary files /dev/null and b/resources/images/keyboard-prefs.png differ diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index ecf92195d5..95f6bdbf76 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -1029,7 +1029,7 @@ class TemplateFunctions(PreferencesPlugin): category = 'Advanced' gui_category = _('Advanced') category_order = 5 - name_order = 4 + name_order = 5 config_widget = 'calibre.gui2.preferences.template_functions' description = _('Create your own template functions') @@ -1092,6 +1092,17 @@ class Tweaks(PreferencesPlugin): config_widget = 'calibre.gui2.preferences.tweaks' description = _('Fine tune how calibre behaves in various contexts') +class Keyboard(PreferencesPlugin): + name = 'Keyboard' + icon = I('keyboard-prefs.png') + gui_name = _('Keyboard') + category = 'Advanced' + gui_category = _('Advanced') + category_order = 5 + name_order = 4 + config_widget = 'calibre.gui2.preferences.keyboard' + description = _('Customize the keyboard shortcuts used by calibre') + class Misc(PreferencesPlugin): name = 'Misc' icon = I('exec.png') @@ -1106,7 +1117,7 @@ class Misc(PreferencesPlugin): plugins += [LookAndFeel, Behavior, Columns, Toolbar, Search, InputOptions, CommonOptions, OutputOptions, Adding, Saving, Sending, Plugboard, Email, Server, Plugins, Tweaks, Misc, TemplateFunctions, - MetadataSources] + MetadataSources, Keyboard] #}}} diff --git a/src/calibre/gui2/actions/__init__.py b/src/calibre/gui2/actions/__init__.py index dbeaa61ea1..aedb10c4b3 100644 --- a/src/calibre/gui2/actions/__init__.py +++ b/src/calibre/gui2/actions/__init__.py @@ -8,9 +8,13 @@ __docformat__ = 'restructuredtext en' from functools import partial from zipfile import ZipFile -from PyQt4.Qt import QToolButton, QAction, QIcon, QObject, QMenu +from PyQt4.Qt import (QToolButton, QAction, QIcon, QObject, QMenu, + QKeySequence) +from calibre import prints from calibre.gui2 import Dispatcher +from calibre.gui2.keyboard import NameConflict + class InterfaceAction(QObject): @@ -108,7 +112,10 @@ class InterfaceAction(QObject): @property def unique_name(self): - return u'%s(%s)'%(self.__class__.__name__, self.name) + bn = self.__class__.__name__ + if hasattr(self.interface_action_base_plugin, 'name'): + bn = self.interface_action_base_plugin.name + return u'%s (%s)'%(bn, self.name) def create_action(self, spec=None, attr='qaction'): if spec is None: @@ -129,7 +136,6 @@ class InterfaceAction(QObject): a.setToolTip(text) a.setStatusTip(text) a.setWhatsThis(text) - keys = () shortcut_action = action desc = tooltip if tooltip else None if attr == 'qaction': @@ -138,9 +144,22 @@ class InterfaceAction(QObject): 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 spec[0] and not (attr=='qaction' and self.popup_type == + QToolButton.InstantPopup): + try: + self.gui.keyboard.register_shortcut(self.unique_name + ' - ' + attr, + unicode(spec[0]), default_keys=keys, + action=shortcut_action, description=desc, + group=self.action_spec[0]) + except NameConflict as e: + try: + prints(unicode(e)) + except: + pass + shortcut_action.setShortcuts([QKeySequence(key, + QKeySequence.PortableText) for key in keys]) + + if attr is not None: setattr(self, attr, action) if attr == 'qaction' and self.action_add_menu: @@ -166,10 +185,11 @@ class InterfaceAction(QObject): 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) + action=ac, description=description, group=self.action_spec[0]) if triggered is not None: ac.triggered.connect(triggered) return ac diff --git a/src/calibre/gui2/actions/choose_library.py b/src/calibre/gui2/actions/choose_library.py index 579aa681b2..1b369a2f0b 100644 --- a/src/calibre/gui2/actions/choose_library.py +++ b/src/calibre/gui2/actions/choose_library.py @@ -79,13 +79,14 @@ class LibraryUsageStats(object): # {{{ class ChooseLibraryAction(InterfaceAction): name = 'Choose Library' - action_spec = (_('%d books'), 'lt.png', + action_spec = (_('Choose Library'), 'lt.png', _('Choose calibre library to work with'), None) dont_add_to = frozenset(['menubar-device', 'toolbar-device', 'context-menu-device']) action_add_menu = True action_menu_clone_qaction = _('Switch/create library...') def genesis(self): + self.base_text = _('%d books') self.count_changed(0) self.qaction.triggered.connect(self.choose_library, type=Qt.QueuedConnection) @@ -376,7 +377,7 @@ class ChooseLibraryAction(InterfaceAction): self.switch_requested(self.qs_locations[idx]) def count_changed(self, new_count): - text = self.action_spec[0]%new_count + text = self.base_text%new_count a = self.qaction a.setText(text) tooltip = self.action_spec[2] + '\n\n' + text diff --git a/src/calibre/gui2/actions/device.py b/src/calibre/gui2/actions/device.py index d4ed26ba8a..483b398943 100644 --- a/src/calibre/gui2/actions/device.py +++ b/src/calibre/gui2/actions/device.py @@ -63,11 +63,15 @@ class ShareConnMenu(QMenu): # {{{ if hasattr(parent, 'keyboard'): r = parent.keyboard.register_shortcut prefix = 'Share/Connect Menu ' + gr = ConnectShareAction.action_spec[0] 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) + r(prefix + attr, unicode(ac.text()), action=ac, + group=gr) + r(prefix+' content server', _('Start/stop content server'), + action=self.toggle_server_action, group=gr) def server_state_changed(self, running): text = _('Start Content Server') diff --git a/src/calibre/gui2/actions/restart.py b/src/calibre/gui2/actions/restart.py index be940fa32e..9bbabedbbf 100644 --- a/src/calibre/gui2/actions/restart.py +++ b/src/calibre/gui2/actions/restart.py @@ -11,7 +11,7 @@ from calibre.gui2.actions import InterfaceAction class RestartAction(InterfaceAction): name = 'Restart' - action_spec = (_('&Restart'), None, None, _('Ctrl+R')) + action_spec = (_('Restart'), None, None, _('Ctrl+R')) def genesis(self): self.qaction.triggered.connect(self.restart) diff --git a/src/calibre/gui2/keyboard.py b/src/calibre/gui2/keyboard.py index ee936f5f4f..e3563867e3 100644 --- a/src/calibre/gui2/keyboard.py +++ b/src/calibre/gui2/keyboard.py @@ -9,16 +9,22 @@ __docformat__ = 'restructuredtext en' from collections import OrderedDict -from PyQt4.Qt import (QObject, QKeySequence) +from PyQt4.Qt import (QObject, QKeySequence, QAbstractItemModel, QModelIndex, + Qt, QStyledItemDelegate, QTextDocument, QStyle, pyqtSignal, + QApplication, QSize, QRectF, QWidget, QHBoxLayout, QTreeView) from calibre.utils.config import JSONConfig from calibre.constants import DEBUG from calibre import prints +from calibre.utils.icu import sort_key +from calibre.gui2 import NONE + +ROOT = QModelIndex() class NameConflict(ValueError): pass -class Manager(QObject): +class Manager(QObject): # {{{ def __init__(self, parent=None): QObject.__init__(self, parent) @@ -27,13 +33,10 @@ class Manager(QObject): 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) + self.groups = {} def register_shortcut(self, unique_name, name, default_keys=(), - description=None, action=None): + description=None, action=None, group=None): if unique_name in self.shortcuts: name = self.shortcuts[unique_name]['name'] raise NameConflict('Shortcut for %r already registered by %s'%( @@ -41,8 +44,13 @@ class Manager(QObject): shortcut = {'name':name, 'desc':description, 'action': action, 'default_keys':tuple(default_keys)} self.shortcuts[unique_name] = shortcut + group = group if group else _('Miscellaneous') + self.groups[group] = self.groups.get(group, []) + [unique_name] def finalize(self): + self.custom_keys_map = {un:tuple(keys) for un, keys in self.config.get( + 'map', {}).iteritems()} + seen = {} for unique_name, shortcut in self.shortcuts.iteritems(): custom_keys = self.custom_keys_map.get(unique_name, None) @@ -69,3 +77,155 @@ class Manager(QObject): if ac is not None: ac.setShortcuts(list(keys)) + self.groups = {g:frozenset(v) for g, v in self.groups.iteritems()} +# }}} + +# Model {{{ + +class Node(object): + + def __init__(self, group_map, shortcut_map, name=None, shortcut=None): + self.data = name if name is not None else shortcut + self.is_shortcut = shortcut is not None + self.children = [] + if name is not None: + self.children = [Node(None, None, shortcut=shortcut_map[uname]) + for uname in group_map[name]] + + def __len__(self): + return len(self.children) + + def __getitem__(self, row): + return self.children[row] + +class ConfigModel(QAbstractItemModel): + + def __init__(self, keyboard, parent=None): + QAbstractItemModel.__init__(self, parent) + + self.keyboard = keyboard + groups = sorted(keyboard.groups, key=sort_key) + shortcut_map = {k:dict(v) for k, v in + self.keyboard.shortcuts.iteritems()} + for un, s in shortcut_map.iteritems(): + s['keys'] = tuple(self.keyboard.keys_map[un]) + s['unique_name'] = un + s['group'] = [g for g, names in self.keyboard.groups.iteritems() if un in + names][0] + + group_map = {group:sorted(names, key=lambda x: + sort_key(shortcut_map[x]['name'])) for group, names in + self.keyboard.groups.iteritems()} + + self.data = [Node(group_map, shortcut_map, group) for group in groups] + + def rowCount(self, parent=ROOT): + ip = parent.internalPointer() + if ip is None: + return len(self.data) + return len(ip) + + def columnCount(self, parent=ROOT): + return 1 + + def index(self, row, column, parent=ROOT): + ip = parent.internalPointer() + if ip is None: + ip = self.data + try: + return self.createIndex(row, column, ip[row]) + except: + pass + return ROOT + + def parent(self, index): + ip = index.internalPointer() + if ip is None or not ip.is_shortcut: + return ROOT + group = ip.data['group'] + for i, g in enumerate(self.data): + if g.data == group: + return self.index(i, 0) + return ROOT + + def data(self, index, role=Qt.DisplayRole): + ip = index.internalPointer() + if ip is not None and role == Qt.UserRole: + return ip + return NONE + +# }}} + +class Delegate(QStyledItemDelegate): # {{{ + + def __init__(self, parent=None): + QStyledItemDelegate.__init__(self, parent) + self.editing_indices = {} + + def to_doc(self, index): + data = index.data(Qt.UserRole).toPyObject() + if data.is_shortcut: + shortcut = data.data + # Shortcut + keys = [unicode(k.toString(k.NativeText)) for k in shortcut['keys']] + if not keys: + keys = _('None') + else: + keys = ', '.join(keys) + html = '

%s

%s: %s'%(shortcut['name'], _('Shortcuts'), keys) + else: + # Group + html = '

%s

'%data.data + doc = QTextDocument() + doc.setHtml(html) + return doc + + def sizeHint(self, option, index): + if index.row() in self.editing_indices: + return QSize(200, 200) + ans = self.to_doc(index).size().toSize() + #ans.setHeight(ans.height()+10) + return ans + + def paint(self, painter, option, index): + painter.save() + painter.setClipRect(QRectF(option.rect)) + if hasattr(QStyle, 'CE_ItemViewItem'): + QApplication.style().drawControl(QStyle.CE_ItemViewItem, option, painter) + elif option.state & QStyle.State_Selected: + painter.fillRect(option.rect, option.palette.highlight()) + painter.translate(option.rect.topLeft()) + self.to_doc(index).drawContents(painter) + painter.restore() + +# }}} + +class ShortcutConfig(QWidget): + + changed_signal = pyqtSignal() + + def __init__(self, parent=None): + QWidget.__init__(self, parent) + self._layout = QHBoxLayout() + self.setLayout(self._layout) + self.view = QTreeView(self) + self.view.setAlternatingRowColors(True) + self.view.setHeaderHidden(True) + self.view.setAnimated(True) + self._layout.addWidget(self.view) + self.delegate = Delegate() + self.view.setItemDelegate(self.delegate) + self.delegate.sizeHintChanged.connect(self.scrollTo) + + def initialize(self, keyboard): + self._model = ConfigModel(keyboard, parent=self) + self.view.setModel(self._model) + + def scrollTo(self, index): + self.view.scrollTo(index) + + @property + def is_editing(self): + return self.view.state() == self.view.EditingState + + diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index 4d11784aea..a0c103a33b 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -694,7 +694,7 @@ class BooksModel(QAbstractTableModel): # {{{ # we will get asked to display columns we don't know about. Must test for this. if col >= len(self.column_to_dc_map): return NONE - if role in (Qt.DisplayRole, Qt.EditRole): + if role in (Qt.DisplayRole, Qt.EditRole, Qt.ToolTipRole): return self.column_to_dc_map[col](index.row()) elif role == Qt.BackgroundRole: if self.id(index) in self.ids_to_highlight_set: diff --git a/src/calibre/gui2/preferences/keyboard.py b/src/calibre/gui2/preferences/keyboard.py new file mode 100644 index 0000000000..ccc3a390d3 --- /dev/null +++ b/src/calibre/gui2/preferences/keyboard.py @@ -0,0 +1,44 @@ +#!/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__ = '2010, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +from PyQt4.Qt import QVBoxLayout + +from calibre.gui2.preferences import (ConfigWidgetBase, test_widget) +from calibre.gui2.keyboard import ShortcutConfig + +class ConfigWidget(ConfigWidgetBase): + + def genesis(self, gui): + self.gui = gui + self.conf_widget = ShortcutConfig(self) + self.conf_widget.changed_signal.connect(self.changed_signal) + self._layout = l = QVBoxLayout() + self.setLayout(l) + l.addWidget(self.conf_widget) + + def initialize(self): + ConfigWidgetBase.initialize(self) + self.conf_widget.initialize(self.gui.keyboard) + + def restore_defaults(self): + ConfigWidgetBase.restore_defaults(self) + self.conf_widget.restore_defaults() + + def commit(self): + self.conf_widget.commit() + return ConfigWidgetBase.commit(self) + + def refresh_gui(self, gui): + gui.keyboard.finalize() + +if __name__ == '__main__': + from PyQt4.Qt import QApplication + app = QApplication([]) + test_widget('Advanced', 'Keyboard') +