From 5008cc5d92b561c849417d510d4bca3a6970fb0a Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 12 Sep 2012 15:26:08 +0530 Subject: [PATCH 1/8] MTP: only populate driveinfo during the fetching of the book lists so that the get_device_information() job is not slow --- src/calibre/devices/interface.py | 16 ++++++++++++++++ src/calibre/devices/mtp/driver.py | 17 ++++++++++++----- src/calibre/devices/mtp/filesystem_cache.py | 3 +++ src/calibre/devices/mtp/unix/driver.py | 3 ++- src/calibre/devices/mtp/windows/driver.py | 3 ++- src/calibre/gui2/device.py | 10 ++++++++++ 6 files changed, 45 insertions(+), 7 deletions(-) diff --git a/src/calibre/devices/interface.py b/src/calibre/devices/interface.py index 58b097f951..c345045e7e 100644 --- a/src/calibre/devices/interface.py +++ b/src/calibre/devices/interface.py @@ -95,6 +95,10 @@ class DevicePlugin(Plugin): #: call post_yank_cleanup(). MANAGES_DEVICE_PRESENCE = False + #: If set the True, calibre will call the :method:`get_driveinfo()` method + #: after the books lists have been loaded to get the driveinfo. + SLOW_DRIVEINFO = False + @classmethod def get_gui_name(cls): if hasattr(cls, 'gui_name'): @@ -352,6 +356,18 @@ class DevicePlugin(Plugin): """ raise NotImplementedError() + def get_driveinfo(self): + ''' + Return the driveinfo dictionary. Usually called from + get_device_information(), but if loading the driveinfo is slow for this + driver, then it should set SLOW_DRIVEINFO. In this case, this method + will be called by calibre after the book lists have been loaded. Note + that it is not called on the device thread, so the driver should cache + the drive info in the books() method and this function should return + the cached data. + ''' + return {} + def card_prefix(self, end_session=True): ''' Return a 2 element list of the prefix to paths on the cards. diff --git a/src/calibre/devices/mtp/driver.py b/src/calibre/devices/mtp/driver.py index caf3174a11..de06955bfe 100644 --- a/src/calibre/devices/mtp/driver.py +++ b/src/calibre/devices/mtp/driver.py @@ -35,6 +35,7 @@ class MTP_DEVICE(BASE): MANAGES_DEVICE_PRESENCE = True FORMATS = ['epub', 'azw3', 'mobi', 'pdf'] DEVICE_PLUGBOARD_NAME = 'MTP_DEVICE' + SLOW_DRIVEINFO = True def __init__(self, *args, **kwargs): BASE.__init__(self, *args, **kwargs) @@ -76,6 +77,7 @@ class MTP_DEVICE(BASE): def open(self, devices, library_uuid): self.current_library_uuid = library_uuid self.location_paths = None + self.driveinfo = {} BASE.open(self, devices, library_uuid) h = self.prefs['history'] if self.current_serial_num: @@ -109,13 +111,17 @@ class MTP_DEVICE(BASE): self.put_file(storage, self.DRIVEINFO, BytesIO(raw), len(raw)) self.driveinfo[location_code] = dinfo + def get_driveinfo(self): + if not self.driveinfo: + self.driveinfo = {} + for sid, location_code in ( (self._main_id, 'main'), (self._carda_id, + 'A'), (self._cardb_id, 'B')): + if sid is None: continue + self._update_drive_info(self.filesystem_cache.storage(sid), location_code) + return self.driveinfo + def get_device_information(self, end_session=True): self.report_progress(1.0, _('Get device information...')) - self.driveinfo = {} - for sid, location_code in ( (self._main_id, 'main'), (self._carda_id, - 'A'), (self._cardb_id, 'B')): - if sid is None: continue - self._update_drive_info(self.filesystem_cache.storage(sid), location_code) dinfo = self.get_basic_device_information() return tuple( list(dinfo) + [self.driveinfo] ) @@ -135,6 +141,7 @@ class MTP_DEVICE(BASE): def books(self, oncard=None, end_session=True): from calibre.devices.mtp.books import JSONCodec from calibre.devices.mtp.books import BookList, Book + self.get_driveinfo() # Ensure driveinfo is loaded sid = {'carda':self._carda_id, 'cardb':self._cardb_id}.get(oncard, self._main_id) if sid is None: diff --git a/src/calibre/devices/mtp/filesystem_cache.py b/src/calibre/devices/mtp/filesystem_cache.py index 8f4d20ae18..b1c2828b8c 100644 --- a/src/calibre/devices/mtp/filesystem_cache.py +++ b/src/calibre/devices/mtp/filesystem_cache.py @@ -230,6 +230,9 @@ class FilesystemCache(object): continue # Ignore .txt files in the root yield x + def __len__(self): + return len(self.id_map) + def resolve_mtp_id_path(self, path): if not path.startswith('mtp:::'): raise ValueError('%s is not a valid MTP path'%path) diff --git a/src/calibre/devices/mtp/unix/driver.py b/src/calibre/devices/mtp/unix/driver.py index 6e5d91c0a0..4b9ed9e928 100644 --- a/src/calibre/devices/mtp/unix/driver.py +++ b/src/calibre/devices/mtp/unix/driver.py @@ -222,7 +222,8 @@ class MTP_DEVICE(MTPDeviceBase): self.current_friendly_name, self.format_errorstack(all_errs))) self._filesystem_cache = FilesystemCache(storage, all_items) - debug('Filesystem metadata loaded in %g seconds'%(time.time()-st)) + debug('Filesystem metadata loaded in %g seconds (%d objects)'%( + time.time()-st, len(self._filesystem_cache))) return self._filesystem_cache @synchronous diff --git a/src/calibre/devices/mtp/windows/driver.py b/src/calibre/devices/mtp/windows/driver.py index 3290115028..3f79e7d991 100644 --- a/src/calibre/devices/mtp/windows/driver.py +++ b/src/calibre/devices/mtp/windows/driver.py @@ -220,7 +220,8 @@ class MTP_DEVICE(MTPDeviceBase): all_storage.append(storage) items.append(id_map.itervalues()) self._filesystem_cache = FilesystemCache(all_storage, chain(*items)) - debug('Filesystem metadata loaded in %g seconds'%(time.time()-st)) + debug('Filesystem metadata loaded in %g seconds (%d objects)'%( + time.time()-st, len(self._filesystem_cache))) return self._filesystem_cache @same_thread diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 8466fe9320..553532e95d 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -433,6 +433,15 @@ class DeviceManager(Thread): # {{{ return self.create_job_step(self._get_device_information, done, description=_('Get device information'), to_job=add_as_step_to_job) + def slow_driveinfo(self): + ''' Update the stored device information with the driveinfo if the + device indicates that getting driveinfo is slow ''' + info = self._device_information['info'] + if (not info[4] and self.device.SLOW_DRIVEINFO): + info = list(info) + info[4] = self.device.get_driveinfo() + self._device_information['info'] = tuple(info) + def get_current_device_information(self): return self._device_information @@ -1023,6 +1032,7 @@ class DeviceMixin(object): # {{{ if job.failed: self.device_job_exception(job) return + self.device_manager.slow_driveinfo() # set_books_in_library might schedule a sync_booklists job self.set_books_in_library(job.result, reset=True, add_as_step_to_job=job) mainlist, cardalist, cardblist = job.result From e35622a98b9fa95eb8af921eed6033ed235baf37 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 12 Sep 2012 17:15:22 +0530 Subject: [PATCH 2/8] MTP: Filesystem browser widget --- .../gui2/device_drivers/mtp_folder_browser.py | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 src/calibre/gui2/device_drivers/mtp_folder_browser.py diff --git a/src/calibre/gui2/device_drivers/mtp_folder_browser.py b/src/calibre/gui2/device_drivers/mtp_folder_browser.py new file mode 100644 index 0000000000..97b75c9de2 --- /dev/null +++ b/src/calibre/gui2/device_drivers/mtp_folder_browser.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2012, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +from operator import attrgetter + +from PyQt4.Qt import (QTabWidget, QTreeWidget, QTreeWidgetItem, Qt, QDialog, + QDialogButtonBox, QVBoxLayout, QSize) + +from calibre.gui2 import file_icon_provider + +def item(f, parent): + name = f.name + if not f.is_folder: + name += ' [%s]'%f.last_mod_string + ans = QTreeWidgetItem(parent, [name]) + ans.setData(0, Qt.UserRole, f.full_path) + if f.is_folder: + ext = 'dir' + else: + ext = f.name.rpartition('.')[-1] + ans.setData(0, Qt.DecorationRole, file_icon_provider().icon_from_ext(ext)) + + return ans + +class Storage(QTreeWidget): + + def __init__(self, storage, show_files): + QTreeWidget.__init__(self) + self.show_files = show_files + self.create_children(storage, self) + self.name = storage.name + self.object_id = storage.persistent_id + self.setMinimumHeight(350) + self.setHeaderHidden(True) + + def create_children(self, f, parent): + for child in sorted(f.folders, key=attrgetter('name')): + i = item(child, parent) + self.create_children(child, i) + if self.show_files: + for child in sorted(f.files, key=attrgetter('name')): + i = item(child, parent) + +class Folders(QTabWidget): + + def __init__(self, filesystem_cache, show_files=True): + QTabWidget.__init__(self) + self.fs = filesystem_cache + for storage in self.fs.entries: + w = Storage(storage, show_files) + self.addTab(w, w.name) + + self.setCurrentIndex(0) + +class Browser(QDialog): + + def __init__(self, filesystem_cache, show_files=True, parent=None): + QDialog.__init__(self, parent) + self.l = l = QVBoxLayout() + self.setLayout(l) + self.folders = cw = Folders(filesystem_cache, show_files=show_files) + l.addWidget(cw) + bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel) + l.addWidget(bb) + bb.accepted.connect(self.accept) + bb.rejected.connect(self.reject) + self.setMinimumSize(QSize(500, 500)) + +def browse(): + from calibre.gui2 import Application + from calibre.devices.mtp.driver import MTP_DEVICE + from calibre.devices.scanner import DeviceScanner + s = DeviceScanner() + s.scan() + app = Application([]) + app + dev = MTP_DEVICE(None) + dev.startup() + cd = dev.detect_managed_devices(s.devices) + if cd is None: + raise ValueError('No MTP device found') + dev.open(cd, 'test') + d = Browser(dev.filesystem_cache) + if d.exec_() == d.Accepted: + pass + dev.shutdown() + +if __name__ == '__main__': + browse() + From 903af8f4e545c0c9c7db5ae18cc296deb3c1ab50 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 12 Sep 2012 21:18:16 +0530 Subject: [PATCH 3/8] Allow the user to graphically select folders when configuring the MTP device --- src/calibre/gui2/device_drivers/mtp_config.py | 60 +++++++++++++++---- .../gui2/device_drivers/mtp_folder_browser.py | 29 +++++++-- 2 files changed, 75 insertions(+), 14 deletions(-) diff --git a/src/calibre/gui2/device_drivers/mtp_config.py b/src/calibre/gui2/device_drivers/mtp_config.py index 7481cbf19c..0187915a4a 100644 --- a/src/calibre/gui2/device_drivers/mtp_config.py +++ b/src/calibre/gui2/device_drivers/mtp_config.py @@ -18,6 +18,7 @@ from calibre.ebooks import BOOK_EXTENSIONS from calibre.gui2 import error_dialog from calibre.gui2.dialogs.template_dialog import TemplateDialog from calibre.utils.date import parse_date +from calibre.gui2.device_drivers.mtp_folder_browser import Browser class FormatsConfig(QWidget): # {{{ @@ -117,19 +118,36 @@ class TemplateConfig(QWidget): # {{{ class SendToConfig(QWidget): # {{{ - def __init__(self, val): + def __init__(self, val, device): QWidget.__init__(self) self.t = t = QLineEdit(self) t.setText(', '.join(val or [])) t.setCursorPosition(0) - self.l = l = QVBoxLayout(self) + self.l = l = QGridLayout(self) self.setLayout(l) self.m = m = QLabel('

'+_('''A list of &folders on the device to which to send ebooks. The first one that exists will be used:''')) m.setWordWrap(True) m.setBuddy(t) - l.addWidget(m) - l.addWidget(t) + l.addWidget(m, 0, 0, 1, 2) + l.addWidget(t, 1, 0) + self.b = b = QToolButton() + l.addWidget(b, 1, 1) + b.setIcon(QIcon(I('document_open.png'))) + b.clicked.connect(self.browse) + b.setToolTip(_('Browse for a folder on the device')) + self._device = weakref.ref(device) + + @property + def device(self): + return self._device() + + def browse(self): + b = Browser(self.device.filesystem_cache, show_files=False, + parent=self) + if b.exec_() == b.Accepted: + sid, path = b.current_item + self.t.setText('/'.join(path[1:])) @property def value(self): @@ -183,8 +201,9 @@ class Rule(QWidget): remove = pyqtSignal(object) - def __init__(self, rule=None): + def __init__(self, device, rule=None): QWidget.__init__(self) + self._device = weakref.ref(device) self.l = l = QHBoxLayout() self.setLayout(l) @@ -198,6 +217,11 @@ class Rule(QWidget): self.folder = f = QLineEdit(self) f.setPlaceholderText(_('Folder on the device')) l.addWidget(f) + self.b = b = QToolButton() + l.addWidget(b) + b.setIcon(QIcon(I('document_open.png'))) + b.clicked.connect(self.browse) + b.setToolTip(_('Browse for a folder on the device')) self.rb = rb = QPushButton(QIcon(I('list_remove.png')), _('&Remove rule'), self) l.addWidget(rb) @@ -217,6 +241,17 @@ class Rule(QWidget): self.ignore = False + @property + def device(self): + return self._device() + + def browse(self): + b = Browser(self.device.filesystem_cache, show_files=False, + parent=self) + if b.exec_() == b.Accepted: + sid, path = b.current_item + self.folder.setText('/'.join(path[1:])) + def removed(self): self.remove.emit(self) @@ -232,8 +267,9 @@ class Rule(QWidget): class FormatRules(QGroupBox): - def __init__(self, rules): + def __init__(self, device, rules): QGroupBox.__init__(self, _('Format specific sending')) + self._device = weakref.ref(device) self.l = l = QVBoxLayout() self.setLayout(l) self.la = la = QLabel('

'+_( @@ -251,7 +287,7 @@ class FormatRules(QGroupBox): l.addWidget(sa) self.widgets = [] for rule in rules: - r = Rule(rule) + r = Rule(device, rule) self.widgets.append(r) w.l.addWidget(r) r.remove.connect(self.remove_rule) @@ -264,8 +300,12 @@ class FormatRules(QGroupBox): b.clicked.connect(self.add_rule) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored) + @property + def device(self): + return self._device() + def add_rule(self): - r = Rule() + r = Rule(self.device) self.widgets.append(r) self.w.l.addWidget(r) r.remove.connect(self.remove_rule) @@ -319,10 +359,10 @@ class MTPConfig(QTabWidget): l = self.base.l = QGridLayout(self.base) self.base.setLayout(l) - self.rules = r = FormatRules(self.get_pref('rules')) + self.rules = r = FormatRules(self.device, self.get_pref('rules')) self.formats = FormatsConfig(set(BOOK_EXTENSIONS), self.get_pref('format_map')) - self.send_to = SendToConfig(self.get_pref('send_to')) + self.send_to = SendToConfig(self.get_pref('send_to'), self.device) self.template = TemplateConfig(self.get_pref('send_template')) self.base.la = la = QLabel(_( 'Choose the formats to send to the %s')%self.device.current_friendly_name) diff --git a/src/calibre/gui2/device_drivers/mtp_folder_browser.py b/src/calibre/gui2/device_drivers/mtp_folder_browser.py index 97b75c9de2..de9562fee7 100644 --- a/src/calibre/gui2/device_drivers/mtp_folder_browser.py +++ b/src/calibre/gui2/device_drivers/mtp_folder_browser.py @@ -10,7 +10,7 @@ __docformat__ = 'restructuredtext en' from operator import attrgetter from PyQt4.Qt import (QTabWidget, QTreeWidget, QTreeWidgetItem, Qt, QDialog, - QDialogButtonBox, QVBoxLayout, QSize) + QDialogButtonBox, QVBoxLayout, QSize, pyqtSignal) from calibre.gui2 import file_icon_provider @@ -47,17 +47,33 @@ class Storage(QTreeWidget): for child in sorted(f.files, key=attrgetter('name')): i = item(child, parent) + @property + def current_item(self): + item = self.currentItem() + if item is not None: + return (self.object_id, item.data(0, Qt.UserRole).toPyObject()) + return None + class Folders(QTabWidget): + selected = pyqtSignal() + def __init__(self, filesystem_cache, show_files=True): QTabWidget.__init__(self) self.fs = filesystem_cache for storage in self.fs.entries: w = Storage(storage, show_files) self.addTab(w, w.name) + w.doubleClicked.connect(self.selected) self.setCurrentIndex(0) + @property + def current_item(self): + w = self.currentWidget() + if w is not None: + return w.current_item + class Browser(QDialog): def __init__(self, filesystem_cache, show_files=True, parent=None): @@ -71,6 +87,11 @@ class Browser(QDialog): bb.accepted.connect(self.accept) bb.rejected.connect(self.reject) self.setMinimumSize(QSize(500, 500)) + self.folders.selected.connect(self.accept) + + @property + def current_item(self): + return self.folders.current_item def browse(): from calibre.gui2 import Application @@ -87,10 +108,10 @@ def browse(): raise ValueError('No MTP device found') dev.open(cd, 'test') d = Browser(dev.filesystem_cache) - if d.exec_() == d.Accepted: - pass + d.exec_() dev.shutdown() + return d.current_item if __name__ == '__main__': - browse() + print (browse()) From 23999f9b423aea5fea80367782086bfce6608730 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 12 Sep 2012 23:18:11 +0530 Subject: [PATCH 4/8] ... --- src/calibre/gui2/__init__.py | 1 + src/calibre/gui2/device_drivers/mtp_folder_browser.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index da254a7b3f..dc4646f208 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -499,6 +499,7 @@ class FileIconProvider(QFileIconProvider): self.icons = {} for key in self.__class__.ICONS.keys(): self.icons[key] = I('mimetypes/')+self.__class__.ICONS[key]+'.png' + self.icons['calibre'] = I('lt.png') for i in ('dir', 'default', 'zero'): self.icons[i] = QIcon(self.icons[i]) diff --git a/src/calibre/gui2/device_drivers/mtp_folder_browser.py b/src/calibre/gui2/device_drivers/mtp_folder_browser.py index de9562fee7..960f821b57 100644 --- a/src/calibre/gui2/device_drivers/mtp_folder_browser.py +++ b/src/calibre/gui2/device_drivers/mtp_folder_browser.py @@ -10,7 +10,7 @@ __docformat__ = 'restructuredtext en' from operator import attrgetter from PyQt4.Qt import (QTabWidget, QTreeWidget, QTreeWidgetItem, Qt, QDialog, - QDialogButtonBox, QVBoxLayout, QSize, pyqtSignal) + QDialogButtonBox, QVBoxLayout, QSize, pyqtSignal, QIcon) from calibre.gui2 import file_icon_provider @@ -88,6 +88,8 @@ class Browser(QDialog): bb.rejected.connect(self.reject) self.setMinimumSize(QSize(500, 500)) self.folders.selected.connect(self.accept) + self.setWindowTitle(_('Choose folder on device')) + self.setWindowIcon(QIcon(I('devices/galaxy_s3.png'))) @property def current_item(self): From 427a88fcb3ded2ed340bfce34ba2be2b540dd856 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 13 Sep 2012 09:39:43 +0530 Subject: [PATCH 5/8] Remember last used window size of the conversion dialogs. Fixes #1049265 ([Enhancement] Save last used window-size) --- src/calibre/gui2/convert/bulk.py | 13 +++++++++++-- src/calibre/gui2/convert/single.py | 11 ++++++++++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/calibre/gui2/convert/bulk.py b/src/calibre/gui2/convert/bulk.py index 3a65a4617e..213f23ced2 100644 --- a/src/calibre/gui2/convert/bulk.py +++ b/src/calibre/gui2/convert/bulk.py @@ -8,8 +8,8 @@ import shutil from PyQt4.Qt import QString, SIGNAL -from calibre.gui2.convert.single import Config, sort_formats_by_preference, \ - GroupModel +from calibre.gui2.convert.single import (Config, sort_formats_by_preference, + GroupModel, gprefs) from calibre.customize.ui import available_output_formats from calibre.gui2 import ResizableDialog from calibre.gui2.convert.look_and_feel import LookAndFeelWidget @@ -62,6 +62,9 @@ class BulkConfig(Config): 'settings.')) o.setChecked(False) + geom = gprefs.get('convert_bulk_dialog_geom', None) + if geom: + self.restoreGeometry(geom) def setup_pipeline(self, *args): oidx = self.groups.currentIndex().row() @@ -139,3 +142,9 @@ class BulkConfig(Config): self._recommendations = recs ResizableDialog.accept(self) + def done(self, r): + if self.isVisible(): + gprefs['convert_bulk_dialog_geom'] = \ + bytearray(self.saveGeometry()) + return ResizableDialog.done(self, r) + diff --git a/src/calibre/gui2/convert/single.py b/src/calibre/gui2/convert/single.py index 4d13ce371b..332dc4ae92 100644 --- a/src/calibre/gui2/convert/single.py +++ b/src/calibre/gui2/convert/single.py @@ -10,7 +10,7 @@ import cPickle, shutil from PyQt4.Qt import QString, SIGNAL, QAbstractListModel, Qt, QVariant, QFont -from calibre.gui2 import ResizableDialog, NONE +from calibre.gui2 import ResizableDialog, NONE, gprefs from calibre.ebooks.conversion.config import (GuiRecommendations, save_specifics, load_specifics) from calibre.gui2.convert.single_ui import Ui_Dialog @@ -146,6 +146,9 @@ class Config(ResizableDialog, Ui_Dialog): rb = self.buttonBox.button(self.buttonBox.RestoreDefaults) self.connect(rb, SIGNAL('clicked()'), self.restore_defaults) self.groups.setMouseTracking(True) + geom = gprefs.get('convert_single_dialog_geom', None) + if geom: + self.restoreGeometry(geom) def restore_defaults(self): delete_specifics(self.db, self.book_id) @@ -263,6 +266,12 @@ class Config(ResizableDialog, Ui_Dialog): self.break_cycles() ResizableDialog.reject(self) + def done(self, r): + if self.isVisible(): + gprefs['convert_single_dialog_geom'] = \ + bytearray(self.saveGeometry()) + return ResizableDialog.done(self, r) + def break_cycles(self): for i in range(self.stack.count()): w = self.stack.widget(i) From 167800975b1a006de2eb8afd96037ab2efb4475a Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 13 Sep 2012 10:50:53 +0530 Subject: [PATCH 6/8] ... --- manual/sub_groups.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/manual/sub_groups.rst b/manual/sub_groups.rst index 709daa7256..2b5c3b3856 100644 --- a/manual/sub_groups.rst +++ b/manual/sub_groups.rst @@ -108,10 +108,10 @@ After creating the saved search, you can use it as a restriction. Useful Template Functions ------------------------- - You might want to use the genre information in a template, such as with save to disk or send to device. The question might then be "How do I get the outermost genre name or names?" An |app| template function, subitems, is provided to make doing this easier. + You might want to use the genre information in a template, such as with save to disk or send to device. The question might then be "How do I get the outermost genre name or names?" A |app| template function, subitems, is provided to make doing this easier. For example, assume you want to add the outermost genre level to the save-to-disk template to make genre folders, as in "History/The Gathering Storm - Churchill, Winston". To do this, you must extract the first level of the hierarchy and add it to the front along with a slash to indicate that it should make a folder. The template below accomplishes this:: {#genre:subitems(0,1)||/}{title} - {authors} -See :ref:`The |app| template language ` for more information templates and the subitem function. +See :ref:`The template language ` for more information templates and the :func:`subitems` function. From 5446deb69a1d9417421130d07d5ce951bfdabcba Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 13 Sep 2012 10:51:40 +0530 Subject: [PATCH 7/8] ... --- src/calibre/devices/interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/devices/interface.py b/src/calibre/devices/interface.py index c345045e7e..74236609ee 100644 --- a/src/calibre/devices/interface.py +++ b/src/calibre/devices/interface.py @@ -95,7 +95,7 @@ class DevicePlugin(Plugin): #: call post_yank_cleanup(). MANAGES_DEVICE_PRESENCE = False - #: If set the True, calibre will call the :method:`get_driveinfo()` method + #: If set the True, calibre will call the :meth:`get_driveinfo()` method #: after the books lists have been loaded to get the driveinfo. SLOW_DRIVEINFO = False From 797013153d02d31a5b387d07dfef229a533c4539 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 13 Sep 2012 11:13:25 +0530 Subject: [PATCH 8/8] Fix #1035182 ([Enhancement] 'Switch themes' button for ebook viewer) --- src/calibre/gui2/viewer/config.py | 5 ++- src/calibre/gui2/viewer/documentview.py | 41 ++++++++++++++++--------- src/calibre/gui2/viewer/main.py | 22 +++++++++++-- src/calibre/gui2/viewer/main.ui | 15 ++++++++- 4 files changed, 64 insertions(+), 19 deletions(-) diff --git a/src/calibre/gui2/viewer/config.py b/src/calibre/gui2/viewer/config.py index 8631d9e6a4..db2d67e0e3 100644 --- a/src/calibre/gui2/viewer/config.py +++ b/src/calibre/gui2/viewer/config.py @@ -76,6 +76,9 @@ def config(defaults=None): return c +def load_themes(): + return JSONConfig('viewer_themes') + class ConfigDialog(QDialog, Ui_Dialog): def __init__(self, shortcuts, parent=None): @@ -113,7 +116,7 @@ class ConfigDialog(QDialog, Ui_Dialog): self.hyphenate_default_lang.setVisible(False) self.hyphenate_label.setVisible(False) - self.themes = JSONConfig('viewer_themes') + self.themes = load_themes() self.save_theme_button.clicked.connect(self.save_theme) self.load_theme_button.m = m = QMenu() self.load_theme_button.setMenu(m) diff --git a/src/calibre/gui2/viewer/documentview.py b/src/calibre/gui2/viewer/documentview.py index 55791afd4a..80dba06afd 100644 --- a/src/calibre/gui2/viewer/documentview.py +++ b/src/calibre/gui2/viewer/documentview.py @@ -21,7 +21,7 @@ from calibre.customize.ui import all_viewer_plugins from calibre.gui2.viewer.keys import SHORTCUTS from calibre.gui2.viewer.javascript import JavaScriptLoader from calibre.gui2.viewer.position import PagePosition -from calibre.gui2.viewer.config import config, ConfigDialog +from calibre.gui2.viewer.config import config, ConfigDialog, load_themes from calibre.gui2.viewer.image_popup import ImagePopup from calibre.ebooks.oeb.display.webview import load_html from calibre.constants import isxp, iswindows @@ -31,8 +31,7 @@ class Document(QWebPage): # {{{ page_turn = pyqtSignal(object) - def set_font_settings(self): - opts = config().parse() + def set_font_settings(self, opts): settings = self.settings() settings.setFontSize(QWebSettings.DefaultFontSize, opts.default_font_size) settings.setFontSize(QWebSettings.DefaultFixedFontSize, opts.mono_font_size) @@ -47,11 +46,15 @@ class Document(QWebPage): # {{{ def do_config(self, parent=None): d = ConfigDialog(self.shortcuts, parent) if d.exec_() == QDialog.Accepted: - with self.page_position: - self.set_font_settings() - self.set_user_stylesheet() - self.misc_config() - self.after_load() + opts = config().parse() + self.apply_settings(opts) + + def apply_settings(self, opts): + with self.page_position: + self.set_font_settings(opts) + self.set_user_stylesheet(opts) + self.misc_config(opts) + self.after_load() def __init__(self, shortcuts, parent=None, debug_javascript=False): QWebPage.__init__(self, parent) @@ -87,7 +90,8 @@ class Document(QWebPage): # {{{ self.all_viewer_plugins = tuple(all_viewer_plugins()) for pl in self.all_viewer_plugins: pl.load_fonts() - self.set_font_settings() + opts = config().parse() + self.set_font_settings(opts) # Security settings.setAttribute(QWebSettings.JavaEnabled, False) @@ -98,8 +102,8 @@ class Document(QWebPage): # {{{ # Miscellaneous settings.setAttribute(QWebSettings.LinksIncludedInFocusChain, True) settings.setAttribute(QWebSettings.DeveloperExtrasEnabled, True) - self.set_user_stylesheet() - self.misc_config() + self.set_user_stylesheet(opts) + self.misc_config(opts) # Load javascript self.mainFrame().javaScriptWindowObjectCleared.connect( @@ -112,8 +116,7 @@ class Document(QWebPage): # {{{ mf.setScrollBarPolicy(Qt.Vertical, Qt.ScrollBarAlwaysOff) mf.setScrollBarPolicy(Qt.Horizontal, Qt.ScrollBarAlwaysOff) - def set_user_stylesheet(self): - opts = config().parse() + def set_user_stylesheet(self, opts): bg = opts.background_color or 'white' brules = ['background-color: %s !important'%bg] prefix = ''' @@ -127,8 +130,7 @@ class Document(QWebPage): # {{{ data += b64encode(raw.encode('utf-8')) self.settings().setUserStyleSheetUrl(QUrl(data)) - def misc_config(self): - opts = config().parse() + def misc_config(self, opts): self.hyphenate = opts.hyphenate self.hyphenate_default_lang = opts.hyphenate_default_lang self.do_fit_images = opts.fit_images @@ -560,6 +562,15 @@ class DocumentView(QWebView): # {{{ self.document.switch_to_fullscreen_mode() self.setFocus(Qt.OtherFocusReason) + def load_theme(self, theme_id): + themes = load_themes() + theme = themes[theme_id] + opts = config(theme).parse() + self.document.apply_settings(opts) + if self.document.in_fullscreen_mode: + self.document.switch_to_fullscreen_mode() + self.setFocus(Qt.OtherFocusReason) + def bookmark(self): return self.document.bookmark() diff --git a/src/calibre/gui2/viewer/main.py b/src/calibre/gui2/viewer/main.py index a63fb2c8c6..ffc5ae2ac7 100644 --- a/src/calibre/gui2/viewer/main.py +++ b/src/calibre/gui2/viewer/main.py @@ -245,8 +245,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer): self.action_back.triggered[bool].connect(self.back) self.action_forward.triggered[bool].connect(self.forward) self.action_bookmark.triggered[bool].connect(self.bookmark) - self.action_preferences.triggered.connect(lambda : - self.view.config(self)) + self.action_preferences.triggered.connect(self.do_config) self.pos.editingFinished.connect(self.goto_page_num) self.vertical_scrollbar.valueChanged[int].connect(lambda x:self.goto_page(x/100.)) @@ -259,6 +258,10 @@ class EbookViewer(MainWindow, Ui_EbookViewer): self.action_bookmark.setMenu(self.bookmarks_menu) self.set_bookmarks([]) + self.themes_menu = QMenu() + self.action_load_theme.setMenu(self.themes_menu) + self.tool_bar.widgetForAction(self.action_load_theme).setPopupMode(QToolButton.InstantPopup) + self.load_theme_menu() if pathtoebook is not None: f = functools.partial(self.load_ebook, pathtoebook, open_at=open_at) @@ -845,6 +848,21 @@ class EbookViewer(MainWindow, Ui_EbookViewer): getattr(self, o).setEnabled(False) self.setCursor(Qt.BusyCursor) + def load_theme_menu(self): + from calibre.gui2.viewer.config import load_themes + self.themes_menu.clear() + for key in load_themes(): + title = key[len('theme_'):] + self.themes_menu.addAction(title, partial(self.load_theme, + key)) + + def load_theme(self, theme_id): + self.view.load_theme(theme_id) + + def do_config(self): + self.view.config(self) + self.load_theme_menu() + def bookmark(self, *args): num = 1 bm = None diff --git a/src/calibre/gui2/viewer/main.ui b/src/calibre/gui2/viewer/main.ui index ddc4cfb776..d6deb315b2 100644 --- a/src/calibre/gui2/viewer/main.ui +++ b/src/calibre/gui2/viewer/main.ui @@ -7,7 +7,7 @@ 0 0 653 - 672 + 746 @@ -141,6 +141,7 @@ + @@ -332,6 +333,18 @@ Toggle Paged mode + + + + :/images/wizard.png:/images/wizard.png + + + Load theme + + + Load a theme + +