diff --git a/src/calibre/gui2/shortcuts.py b/src/calibre/gui2/shortcuts.py new file mode 100644 index 0000000000..a617b6897f --- /dev/null +++ b/src/calibre/gui2/shortcuts.py @@ -0,0 +1,266 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +from __future__ import with_statement + +__license__ = 'GPL v3' +__copyright__ = '2009, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +from functools import partial + +from PyQt4.Qt import QAbstractListModel, Qt, QKeySequence, QListView, \ + QHBoxLayout, QWidget, QApplication, QStyledItemDelegate, QStyle, \ + QVariant, QTextDocument, QRectF, QFrame, QSize, QFont, QKeyEvent + +from calibre.gui2 import NONE, error_dialog +from calibre.utils.config import XMLConfig +from calibre.gui2.shortcuts_ui import Ui_Frame + +DEFAULTS = Qt.UserRole +DESCRIPTION = Qt.UserRole + 1 +CUSTOM = Qt.UserRole + 2 +KEY = Qt.UserRole + 3 + +class Customize(QFrame, Ui_Frame): + + def __init__(self, dup_check, parent=None): + QFrame.__init__(self, parent) + self.setupUi(self) + self.setFocusPolicy(Qt.StrongFocus) + self.setAutoFillBackground(True) + self.custom.toggled.connect(self.custom_toggled) + self.custom_toggled(False) + self.capture = 0 + self.key = None + self.shorcut1 = self.shortcut2 = None + self.dup_check = dup_check + for x in (1, 2): + button = getattr(self, 'button%d'%x) + button.clicked.connect(partial(self.capture_clicked, which=x)) + button.keyPressEvent = partial(self.key_press_event, which=x) + clear = getattr(self, 'clear%d'%x) + clear.clicked.connect(partial(self.clear_clicked, which=x)) + + def clear_clicked(self, which=0): + button = getattr(self, 'button%d'%which) + button.setText(_('None')) + setattr(self, 'shortcut%d'%which, None) + + def custom_toggled(self, checked): + for w in ('1', '2'): + for o in ('label', 'button', 'clear'): + getattr(self, o+w).setEnabled(checked) + + def capture_clicked(self, which=1): + self.capture = which + button = getattr(self, 'button%d'%which) + button.setText(_('Press a key...')) + button.setFocus(Qt.OtherFocusReason) + font = QFont() + font.setBold(True) + button.setFont(font) + + def key_press_event(self, ev, which=0): + code = ev.key() + if self.capture == 0 or code in (0, Qt.Key_unknown, + Qt.Key_Shift, Qt.Key_Control, Qt.Key_Alt, Qt.Key_Meta, + Qt.Key_AltGr, Qt.Key_CapsLock, Qt.Key_NumLock, Qt.Key_ScrollLock): + return QWidget.keyPressEvent(self, ev) + button = getattr(self, 'button%d'%which) + font = QFont() + button.setFont(font) + sequence = QKeySequence(code|int(ev.modifiers())) + button.setText(sequence.toString()) + self.capture = 0 + setattr(self, 'shortcut%d'%which, sequence) + dup_desc = self.dup_check(sequence, self.key) + if dup_desc is not None: + error_dialog(self, _('Already assigned'), + unicode(sequence.toString()) + ' ' + + _('already assigned to') + ' ' + dup_desc, show=True) + self.clear_clicked(which=which) + + +class Delegate(QStyledItemDelegate): + + def __init__(self, parent=None): + QStyledItemDelegate.__init__(self, parent) + self.editing_indices = {} + + def to_doc(self, index): + doc = QTextDocument() + doc.setHtml(index.data().toString()) + 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() + + def createEditor(self, parent, option, index): + w = Customize(index.model().duplicate_check, parent=parent) + self.editing_indices[index.row()] = w + self.sizeHintChanged.emit(index) + return w + + def setEditorData(self, editor, index): + defs = index.data(DEFAULTS).toPyObject() + defs = _(' or ').join([unicode(x.toString(x.NativeText)) for x in defs]) + editor.key = unicode(index.data(KEY).toString()) + editor.default_shortcuts.setText(_('&Default') + ': %s' % defs) + editor.default_shortcuts.setChecked(True) + editor.header.setText('%s: %s'%(_('Customize shortcuts for'), + unicode(index.data(DESCRIPTION).toString()))) + custom = index.data(CUSTOM).toPyObject() + if custom: + editor.custom.setChecked(True) + for x in (0, 1): + button = getattr(editor, 'button%d'%(x+1)) + if len(custom) > x: + seq = QKeySequence(custom[x]) + button.setText(seq.toString(seq.NativeText)) + setattr(editor, 'shortcut%d'%(x+1), seq) + + def setModelData(self, editor, model, index): + self.editing_indices.pop(index.row()) + self.sizeHintChanged.emit(index) + self.closeEditor.emit(editor, self.NoHint) + custom = [] + if editor.custom.isChecked(): + for x in ('1', '2'): + sc = getattr(editor, 'shortcut'+x) + if sc is not None: + custom.append(sc) + + model.set_data(index, custom) + + def updateEditorGeometry(self, editor, option, index): + editor.setGeometry(option.rect) + +class Shortcuts(QAbstractListModel): + + TEMPLATE = ''' +

{0}
+ Keys: {1}

+ ''' + + def __init__(self, shortcuts, config_file_base_name, parent=None): + QAbstractListModel.__init__(self, parent) + + self.descriptions = {} + for k, v in shortcuts.items(): + self.descriptions[k] = v[-1] + self.keys = {} + for k, v in shortcuts.items(): + self.keys[k] = v[0] + self.order = list(shortcuts) + self.order.sort(cmp=lambda x,y : cmp(self.descriptions[x], + self.descriptions[y])) + self.sequences = {} + for k, v in self.keys.items(): + self.sequences[k] = [QKeySequence(x) for x in v] + + self.custom = XMLConfig(config_file_base_name) + + def rowCount(self, parent): + return len(self.order) + + def get_sequences(self, key): + custom = self.custom.get(key, []) + if custom: + return [QKeySequence(x) for x in custom] + return self.sequences[key] + + def get_match(self, event_or_sequence, ignore=tuple()): + q = event_or_sequence + if isinstance(q, QKeyEvent): + q = QKeySequence(q.key()|int(q.modifiers())) + for key in self.order: + if key not in ignore: + for seq in self.get_sequences(key): + if seq.matches(q) == QKeySequence.ExactMatch: + return key + return None + + def duplicate_check(self, seq, ignore): + key = self.get_match(seq, ignore=[ignore]) + if key is not None: + return self.descriptions[key] + + def get_shortcuts(self, key): + return [unicode(x.toString(x.NativeText)) for x in + self.get_sequences(key)] + + + def data(self, index, role): + row = index.row() + if row < 0 or row >= len(self.order): + return NONE + key = self.order[row] + if role == Qt.DisplayRole: + return QVariant(self.TEMPLATE.format(self.descriptions[key], + _(' or ').join(self.get_shortcuts(key)))) + if role == Qt.ToolTipRole: + return QVariant(_('Double click to change')) + if role == DEFAULTS: + return QVariant(self.sequences[key]) + if role == DESCRIPTION: + return QVariant(self.descriptions[key]) + if role == CUSTOM: + if key in self.custom: + return QVariant(self.custom[key]) + else: + return QVariant([]) + if role == KEY: + return QVariant(key) + return NONE + + def set_data(self, index, custom): + key = self.order[index.row()] + if custom: + self.custom[key] = [unicode(x.toString()) for x in custom] + elif key in self.custom: + del self.custom[key] + + + def flags(self, index): + if not index.isValid(): + return Qt.ItemIsEnabled + return QAbstractListModel.flags(self, index) | Qt.ItemIsEditable + +class ShortcutConfig(QWidget): + + def __init__(self, model, parent=None): + QWidget.__init__(self, parent) + self._layout = QHBoxLayout() + self.setLayout(self._layout) + self.view = QListView(self) + self._layout.addWidget(self.view) + self.view.setModel(model) + self.delegate = Delegate() + self.view.setItemDelegate(self.delegate) + self.delegate.sizeHintChanged.connect(self.view.scrollTo) + + +if __name__ == '__main__': + from calibre.gui2 import is_ok_to_use_qt + from calibre.gui2.viewer.keys import SHORTCUTS + is_ok_to_use_qt() + model = Shortcuts(SHORTCUTS, 'shortcuts/viewer') + conf = ShortcutConfig(model) + conf.resize(400, 500) + conf.show() + QApplication.instance().exec_() diff --git a/src/calibre/gui2/shortcuts.ui b/src/calibre/gui2/shortcuts.ui new file mode 100644 index 0000000000..4afe564640 --- /dev/null +++ b/src/calibre/gui2/shortcuts.ui @@ -0,0 +1,135 @@ + + + Frame + + + + 0 + 0 + 400 + 170 + + + + Frame + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + &Default + + + + + + + &Custom + + + + + + + 25 + + + + + &Shortcut: + + + button1 + + + + + + + Click to change + + + None + + + + + + + Clear + + + ... + + + + :/images/clear_left.svg:/images/clear_left.svg + + + + + + + + + 25 + + + + + &Alternate shortcut: + + + button1 + + + + + + + Click to change + + + None + + + + + + + Clear + + + ... + + + + :/images/clear_left.svg:/images/clear_left.svg + + + + + + + + + + + + true + + + + + + + + + + diff --git a/src/calibre/gui2/viewer/config.ui b/src/calibre/gui2/viewer/config.ui index 45a6f539c2..bf4e390bf9 100644 --- a/src/calibre/gui2/viewer/config.ui +++ b/src/calibre/gui2/viewer/config.ui @@ -18,203 +18,6 @@ :/images/config.svg:/images/config.svg - - - - &Font options - - - - - - - - Se&rif family: - - - serif_family - - - - - - - - - - &Sans family: - - - sans_family - - - - - - - - - - &Monospace family: - - - mono_family - - - - - - - - - - - - - - &Default font size: - - - default_font_size - - - - - - - px - - - 8 - - - 40 - - - - - - - Monospace &font size: - - - mono_font_size - - - - - - - px - - - 8 - - - 50 - - - - - - - S&tandard font: - - - standard_font - - - - - - - - Serif - - - - - Sans-serif - - - - - Monospace - - - - - - - - Remember last used &window size - - - - - - - px - - - 100 - - - 10000 - - - - - - - Maximum &view width: - - - max_view_width - - - - - - - H&yphenate (break line in the middle of large words) - - - - - - - The default language to use for hyphenation rules. If the book does not specify a language, this will be used. - - - - - - - Default &language for hyphenation: - - - hyphenate_default_lang - - - - - - - - - &User stylesheet - - - - - - - - - - - @@ -225,6 +28,234 @@ + + + + 0 + + + + &General + + + + + + &Font options + + + + + + + + Se&rif family: + + + serif_family + + + + + + + + + + &Sans family: + + + sans_family + + + + + + + + + + &Monospace family: + + + mono_family + + + + + + + + + + + + + + &Default font size: + + + default_font_size + + + + + + + px + + + 8 + + + 40 + + + + + + + Monospace &font size: + + + mono_font_size + + + + + + + px + + + 8 + + + 50 + + + + + + + S&tandard font: + + + standard_font + + + + + + + + Serif + + + + + Sans-serif + + + + + Monospace + + + + + + + + Remember last used &window size + + + + + + + px + + + 100 + + + 10000 + + + + + + + Maximum &view width: + + + max_view_width + + + + + + + H&yphenate (break line in the middle of large words) + + + + + + + The default language to use for hyphenation rules. If the book does not specify a language, this will be used. + + + + + + + Default &language for hyphenation: + + + hyphenate_default_lang + + + + + + + + + &User stylesheet + + + + + + + + + + + + + + + + &Keyboard shortcuts + + + + + + Double click to change a keyborad shortcut + + + true + + + + + + + diff --git a/src/calibre/gui2/viewer/documentview.py b/src/calibre/gui2/viewer/documentview.py index 348b1232ee..d4ee68f25c 100644 --- a/src/calibre/gui2/viewer/documentview.py +++ b/src/calibre/gui2/viewer/documentview.py @@ -15,9 +15,11 @@ from PyQt4.QtWebKit import QWebPage, QWebView, QWebSettings from calibre.utils.config import Config, StringConfig from calibre.utils.localization import get_language from calibre.gui2.viewer.config_ui import Ui_Dialog +from calibre.gui2.shortcuts import Shortcuts, ShortcutConfig from calibre.ptempfile import PersistentTemporaryFile from calibre.constants import iswindows from calibre import prints, guess_type +from calibre.gui2.viewer.keys import SHORTCUTS bookmarks = referencing = hyphenation = jquery = jquery_scrollTo = hyphenator = None @@ -73,8 +75,8 @@ class PythonJS(QObject): class ConfigDialog(QDialog, Ui_Dialog): - def __init__(self, *args): - QDialog.__init__(self, *args) + def __init__(self, shortcuts, parent=None): + QDialog.__init__(self, parent) self.setupUi(self) opts = config().parse() @@ -104,6 +106,10 @@ class ConfigDialog(QDialog, Ui_Dialog): self.hyphenate_default_lang.setCurrentIndex(idx) self.hyphenate.setChecked(opts.hyphenate) self.hyphenate_default_lang.setEnabled(opts.hyphenate) + self.shortcuts = shortcuts + self.shortcut_config = ShortcutConfig(shortcuts, parent=self) + p = self.tabs.widget(1) + p.layout().addWidget(self.shortcut_config) def accept(self, *args): @@ -139,15 +145,15 @@ class Document(QWebPage): settings.setFontFamily(QWebSettings.FixedFont, opts.mono_family) def do_config(self, parent=None): - d = ConfigDialog(parent) + d = ConfigDialog(self.shortcuts, parent) if d.exec_() == QDialog.Accepted: self.set_font_settings() self.set_user_stylesheet() self.misc_config() self.triggerAction(QWebPage.Reload) - def __init__(self, *args): - QWebPage.__init__(self, *args) + def __init__(self, shortcuts, parent=None): + QWebPage.__init__(self, parent) self.setObjectName("py_bridge") self.debug_javascript = False self.current_language = None @@ -155,6 +161,7 @@ class Document(QWebPage): self.setLinkDelegationPolicy(self.DelegateAllLinks) self.scroll_marks = [] + self.shortcuts = shortcuts pal = self.palette() pal.setBrush(QPalette.Background, QColor(0xee, 0xee, 0xee)) self.setPalette(pal) @@ -366,13 +373,14 @@ class DocumentView(QWebView): def __init__(self, *args): QWebView.__init__(self, *args) self.debug_javascript = False + self.shortcuts = Shortcuts(SHORTCUTS, 'shortcuts/viewer') self.self_closing_pat = re.compile(r'<([a-z]+)\s+([^>]+)/>', re.IGNORECASE) self.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)) self._size_hint = QSize(510, 680) self.initial_pos = 0.0 self.to_bottom = False - self.document = Document(self) + self.document = Document(self.shortcuts, parent=self) self.setPage(self.document) self.manager = None self._reference_mode = False @@ -407,6 +415,7 @@ class DocumentView(QWebView): self.document.do_config(parent) if self.manager is not None: self.manager.set_max_width() + self.setFocus(Qt.OtherFocusReason) def bookmark(self): return self.document.bookmark() @@ -663,30 +672,30 @@ class DocumentView(QWebView): return ret def keyPressEvent(self, event): - key = event.key() - if key in [Qt.Key_PageDown, Qt.Key_Space, Qt.Key_Down]: + key = self.shortcuts.get_match(event) + if key == 'Next Page': self.next_page() - elif key in [Qt.Key_PageUp, Qt.Key_Backspace, Qt.Key_Up]: + elif key == 'Previous Page': self.previous_page() - elif key in [Qt.Key_Home]: + elif key == 'Section Top': if event.modifiers() & Qt.ControlModifier: if self.manager is not None: self.manager.goto_start() else: self.scroll_to(0) - elif key in [Qt.Key_End]: + elif key == 'Section Bottom': if event.modifiers() & Qt.ControlModifier: if self.manager is not None: self.manager.goto_end() else: self.scroll_to(1) - elif key in [Qt.Key_J]: + elif key == 'Down': self.scroll_by(y=15) - elif key in [Qt.Key_K]: + elif key == 'Up': self.scroll_by(y=-15) - elif key in [Qt.Key_H]: + elif key == 'Left': self.scroll_by(x=-15) - elif key in [Qt.Key_L]: + elif key == 'Right': self.scroll_by(x=15) else: return QWebView.keyPressEvent(self, event) diff --git a/src/calibre/gui2/viewer/keys.py b/src/calibre/gui2/viewer/keys.py new file mode 100644 index 0000000000..5ca1802092 --- /dev/null +++ b/src/calibre/gui2/viewer/keys.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +from __future__ import with_statement + +__license__ = 'GPL v3' +__copyright__ = '2009, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + + +SHORTCUTS = { + 'Next Page' : (['PgDown', 'Space'], + _('Scroll to the next page')), + + 'Previous Page' : (['PgUp', 'Backspace'], + _('Scroll to the previous page')), + + 'Next Section' : (['Ctrl+PgDown', 'Ctrl+Down'], + _('Scroll to the next section')), + + 'Previous Section' : (['Ctrl+PgUp', 'Ctrl+Up'], + _('Scroll to the previous section')), + + 'Section Bottom' : (['End'], + _('Scroll to the bottom of the section')), + + 'Section Top' : (['Home'], + _('Scroll to the top of the section')), + + 'Document Bottom' : (['Ctrl+End'], + _('Scroll to the end of the document')), + + 'Document Top' : (['Ctrl+Home'], + _('Scroll to the start of the document')), + + 'Down' : (['J', 'Down'], + _('Scroll down')), + + 'Up' : (['K', 'Up'], + _('Scroll up')), + + 'Left' : (['H', 'Left'], + _('Scroll left')), + + 'Right' : (['L', 'Right'], + _('Scroll right')), + +} diff --git a/src/calibre/manual/faq.rst b/src/calibre/manual/faq.rst index 4f3155b8c8..9c03488cc7 100644 --- a/src/calibre/manual/faq.rst +++ b/src/calibre/manual/faq.rst @@ -264,7 +264,7 @@ Why does |app| show only some of my fonts on OS X? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |app| embeds fonts in ebook files it creates. E-book files support embedding only TrueType (.ttf) fonts. Most fonts on OS X systems are in .dfont format, thus they cannot be embedded. |app| shows only TrueType fonts founf on your system. You can obtain many TrueType fonts on the web. Simply download the .ttf files and add them to the Library/Fonts directory in your home directory. -The graphical user interface of |app| is not starting on Windows? +|app| is not starting on Windows? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ There can be several causes for this: @@ -279,8 +279,20 @@ If it still wont launch, start a command prompt (press the windows key and R; th calibre-debug -g Post any output you see in a help message on the `Forum `_. - -My antivirus programs claims |app| is a virus/trojan? + +|app| is not starting on OS X? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can obtain debug output about why |app| is not starting by running `Console.app`. Debug output will +be printed to it. If the debug output contains a line that looks like:: + + Qt: internal: -108: Error ATSUMeasureTextImage text/qfontengine_mac.mm + +then the problem is a corrupted font cache. You can clear the cache by following these +`instructions `_. + + +My antivirus program claims |app| is a virus/trojan? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Your antivirus program is wrong. |app| is a completely open source product. You can actually browse the source code yourself (or hire someone to do it for you) to verify that it is not a virus. Please report the false identification to whatever company you buy your antivirus software from. If the antivirus program is preventing you from downloading/installing |app|, disable it temporarily, install |app| and then re-enable it. diff --git a/src/calibre/utils/config.py b/src/calibre/utils/config.py index cf829621e3..3bfba86cb2 100644 --- a/src/calibre/utils/config.py +++ b/src/calibre/utils/config.py @@ -608,6 +608,10 @@ class XMLConfig(dict): def set(self, key, val): self.__setitem__(key, val) + def __delitem__(self, key): + dict.__delitem__(self, key) + self.commit() + def commit(self): if hasattr(self, 'file_path') and self.file_path: dpath = os.path.dirname(self.file_path)