This commit is contained in:
GRiker 2012-09-13 06:26:06 -06:00
commit 1cc0a6861d
16 changed files with 302 additions and 41 deletions

View File

@ -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 <templatelangcalibre>` for more information templates and the subitem function.
See :ref:`The template language <templatelangcalibre>` for more information templates and the :func:`subitems` function.

View File

@ -95,6 +95,10 @@ class DevicePlugin(Plugin):
#: call post_yank_cleanup().
MANAGES_DEVICE_PRESENCE = False
#: 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
@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.

View File

@ -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:

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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])

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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('<p>'+_('''A <b>list of &folders</b> 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('<p>'+_(
@ -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)

View File

@ -0,0 +1,119 @@
#!/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 <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from operator import attrgetter
from PyQt4.Qt import (QTabWidget, QTreeWidget, QTreeWidgetItem, Qt, QDialog,
QDialogButtonBox, QVBoxLayout, QSize, pyqtSignal, QIcon)
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)
@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):
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))
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):
return self.folders.current_item
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)
d.exec_()
dev.shutdown()
return d.current_item
if __name__ == '__main__':
print (browse())

View File

@ -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)

View File

@ -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()

View File

@ -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

View File

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>653</width>
<height>672</height>
<height>746</height>
</rect>
</property>
<property name="windowTitle">
@ -141,6 +141,7 @@
<addaction name="separator"/>
<addaction name="action_preferences"/>
<addaction name="action_metadata"/>
<addaction name="action_load_theme"/>
<addaction name="separator"/>
<addaction name="action_print"/>
</widget>
@ -332,6 +333,18 @@
<string>Toggle Paged mode</string>
</property>
</action>
<action name="action_load_theme">
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/wizard.png</normaloff>:/images/wizard.png</iconset>
</property>
<property name="text">
<string>Load theme</string>
</property>
<property name="toolTip">
<string>Load a theme</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>