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 @@ def get_device_information(self, end_session=True):
"""
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 @@ def configure_for_generic_epub_app(self):
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 @@ def _update_drive_info(self, storage, location_code, name=None):
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 @@ def set_driveinfo_name(self, location_code, name):
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 @@ def iterebooks(self, storage_id):
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 @@ def filesystem_cache(self):
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 @@ def filesystem_cache(self):
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 @@ def __init__(self):
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 @@
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 @@ def __init__(self, parent, db, preferred_output_format=None,
'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 @@ def accept(self):
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 @@
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 @@ def __init__(self, parent, db, book_id,
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 @@ def reject(self):
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 @@ def get_device_information(self, done, add_as_step_to_job=None):
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 @@ def metadata_downloaded(self, job):
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.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 @@ def validate(self):
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 @@ def __init__(self, rule=None):
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 @@ def __init__(self, rule=None):
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 @@ def rule(self):
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 @@ def __init__(self, rules):
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 @@ def __init__(self, rules):
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 @@ def __init__(self, device, parent=None):
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 @@ def __init__(self, shortcuts, parent=None):
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.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 @@ def set_font_settings(self):
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 @@ def __init__(self, shortcuts, parent=None, debug_javascript=False):
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 @@ def __init__(self, shortcuts, parent=None, debug_javascript=False):
# 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 @@ def turn_off_internal_scrollbars(self):
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 @@ def set_user_stylesheet(self):
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 @@ def config(self, parent=None):
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 @@ def __init__(self, pathtoebook=None, debug_javascript=False, open_at=None):
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 @@ def __init__(self, pathtoebook=None, debug_javascript=False, open_at=None):
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 @@ def open_progress_indicator(self, msg=''):
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>