mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-08-11 09:13:57 -04:00
0.8.68+
This commit is contained in:
commit
1cc0a6861d
@ -108,10 +108,10 @@ After creating the saved search, you can use it as a restriction.
|
|||||||
Useful Template Functions
|
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::
|
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}
|
{#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.
|
||||||
|
@ -95,6 +95,10 @@ class DevicePlugin(Plugin):
|
|||||||
#: call post_yank_cleanup().
|
#: call post_yank_cleanup().
|
||||||
MANAGES_DEVICE_PRESENCE = False
|
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
|
@classmethod
|
||||||
def get_gui_name(cls):
|
def get_gui_name(cls):
|
||||||
if hasattr(cls, 'gui_name'):
|
if hasattr(cls, 'gui_name'):
|
||||||
@ -352,6 +356,18 @@ class DevicePlugin(Plugin):
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
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):
|
def card_prefix(self, end_session=True):
|
||||||
'''
|
'''
|
||||||
Return a 2 element list of the prefix to paths on the cards.
|
Return a 2 element list of the prefix to paths on the cards.
|
||||||
|
@ -35,6 +35,7 @@ class MTP_DEVICE(BASE):
|
|||||||
MANAGES_DEVICE_PRESENCE = True
|
MANAGES_DEVICE_PRESENCE = True
|
||||||
FORMATS = ['epub', 'azw3', 'mobi', 'pdf']
|
FORMATS = ['epub', 'azw3', 'mobi', 'pdf']
|
||||||
DEVICE_PLUGBOARD_NAME = 'MTP_DEVICE'
|
DEVICE_PLUGBOARD_NAME = 'MTP_DEVICE'
|
||||||
|
SLOW_DRIVEINFO = True
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
BASE.__init__(self, *args, **kwargs)
|
BASE.__init__(self, *args, **kwargs)
|
||||||
@ -76,6 +77,7 @@ class MTP_DEVICE(BASE):
|
|||||||
def open(self, devices, library_uuid):
|
def open(self, devices, library_uuid):
|
||||||
self.current_library_uuid = library_uuid
|
self.current_library_uuid = library_uuid
|
||||||
self.location_paths = None
|
self.location_paths = None
|
||||||
|
self.driveinfo = {}
|
||||||
BASE.open(self, devices, library_uuid)
|
BASE.open(self, devices, library_uuid)
|
||||||
h = self.prefs['history']
|
h = self.prefs['history']
|
||||||
if self.current_serial_num:
|
if self.current_serial_num:
|
||||||
@ -109,13 +111,17 @@ class MTP_DEVICE(BASE):
|
|||||||
self.put_file(storage, self.DRIVEINFO, BytesIO(raw), len(raw))
|
self.put_file(storage, self.DRIVEINFO, BytesIO(raw), len(raw))
|
||||||
self.driveinfo[location_code] = dinfo
|
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):
|
def get_device_information(self, end_session=True):
|
||||||
self.report_progress(1.0, _('Get device information...'))
|
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()
|
dinfo = self.get_basic_device_information()
|
||||||
return tuple( list(dinfo) + [self.driveinfo] )
|
return tuple( list(dinfo) + [self.driveinfo] )
|
||||||
|
|
||||||
@ -135,6 +141,7 @@ class MTP_DEVICE(BASE):
|
|||||||
def books(self, oncard=None, end_session=True):
|
def books(self, oncard=None, end_session=True):
|
||||||
from calibre.devices.mtp.books import JSONCodec
|
from calibre.devices.mtp.books import JSONCodec
|
||||||
from calibre.devices.mtp.books import BookList, Book
|
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,
|
sid = {'carda':self._carda_id, 'cardb':self._cardb_id}.get(oncard,
|
||||||
self._main_id)
|
self._main_id)
|
||||||
if sid is None:
|
if sid is None:
|
||||||
|
@ -230,6 +230,9 @@ class FilesystemCache(object):
|
|||||||
continue # Ignore .txt files in the root
|
continue # Ignore .txt files in the root
|
||||||
yield x
|
yield x
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self.id_map)
|
||||||
|
|
||||||
def resolve_mtp_id_path(self, path):
|
def resolve_mtp_id_path(self, path):
|
||||||
if not path.startswith('mtp:::'):
|
if not path.startswith('mtp:::'):
|
||||||
raise ValueError('%s is not a valid MTP path'%path)
|
raise ValueError('%s is not a valid MTP path'%path)
|
||||||
|
@ -222,7 +222,8 @@ class MTP_DEVICE(MTPDeviceBase):
|
|||||||
self.current_friendly_name,
|
self.current_friendly_name,
|
||||||
self.format_errorstack(all_errs)))
|
self.format_errorstack(all_errs)))
|
||||||
self._filesystem_cache = FilesystemCache(storage, all_items)
|
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
|
return self._filesystem_cache
|
||||||
|
|
||||||
@synchronous
|
@synchronous
|
||||||
|
@ -220,7 +220,8 @@ class MTP_DEVICE(MTPDeviceBase):
|
|||||||
all_storage.append(storage)
|
all_storage.append(storage)
|
||||||
items.append(id_map.itervalues())
|
items.append(id_map.itervalues())
|
||||||
self._filesystem_cache = FilesystemCache(all_storage, chain(*items))
|
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
|
return self._filesystem_cache
|
||||||
|
|
||||||
@same_thread
|
@same_thread
|
||||||
|
@ -499,6 +499,7 @@ class FileIconProvider(QFileIconProvider):
|
|||||||
self.icons = {}
|
self.icons = {}
|
||||||
for key in self.__class__.ICONS.keys():
|
for key in self.__class__.ICONS.keys():
|
||||||
self.icons[key] = I('mimetypes/')+self.__class__.ICONS[key]+'.png'
|
self.icons[key] = I('mimetypes/')+self.__class__.ICONS[key]+'.png'
|
||||||
|
self.icons['calibre'] = I('lt.png')
|
||||||
for i in ('dir', 'default', 'zero'):
|
for i in ('dir', 'default', 'zero'):
|
||||||
self.icons[i] = QIcon(self.icons[i])
|
self.icons[i] = QIcon(self.icons[i])
|
||||||
|
|
||||||
|
@ -8,8 +8,8 @@ import shutil
|
|||||||
|
|
||||||
from PyQt4.Qt import QString, SIGNAL
|
from PyQt4.Qt import QString, SIGNAL
|
||||||
|
|
||||||
from calibre.gui2.convert.single import Config, sort_formats_by_preference, \
|
from calibre.gui2.convert.single import (Config, sort_formats_by_preference,
|
||||||
GroupModel
|
GroupModel, gprefs)
|
||||||
from calibre.customize.ui import available_output_formats
|
from calibre.customize.ui import available_output_formats
|
||||||
from calibre.gui2 import ResizableDialog
|
from calibre.gui2 import ResizableDialog
|
||||||
from calibre.gui2.convert.look_and_feel import LookAndFeelWidget
|
from calibre.gui2.convert.look_and_feel import LookAndFeelWidget
|
||||||
@ -62,6 +62,9 @@ class BulkConfig(Config):
|
|||||||
'settings.'))
|
'settings.'))
|
||||||
o.setChecked(False)
|
o.setChecked(False)
|
||||||
|
|
||||||
|
geom = gprefs.get('convert_bulk_dialog_geom', None)
|
||||||
|
if geom:
|
||||||
|
self.restoreGeometry(geom)
|
||||||
|
|
||||||
def setup_pipeline(self, *args):
|
def setup_pipeline(self, *args):
|
||||||
oidx = self.groups.currentIndex().row()
|
oidx = self.groups.currentIndex().row()
|
||||||
@ -139,3 +142,9 @@ class BulkConfig(Config):
|
|||||||
self._recommendations = recs
|
self._recommendations = recs
|
||||||
ResizableDialog.accept(self)
|
ResizableDialog.accept(self)
|
||||||
|
|
||||||
|
def done(self, r):
|
||||||
|
if self.isVisible():
|
||||||
|
gprefs['convert_bulk_dialog_geom'] = \
|
||||||
|
bytearray(self.saveGeometry())
|
||||||
|
return ResizableDialog.done(self, r)
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ import cPickle, shutil
|
|||||||
|
|
||||||
from PyQt4.Qt import QString, SIGNAL, QAbstractListModel, Qt, QVariant, QFont
|
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,
|
from calibre.ebooks.conversion.config import (GuiRecommendations, save_specifics,
|
||||||
load_specifics)
|
load_specifics)
|
||||||
from calibre.gui2.convert.single_ui import Ui_Dialog
|
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)
|
rb = self.buttonBox.button(self.buttonBox.RestoreDefaults)
|
||||||
self.connect(rb, SIGNAL('clicked()'), self.restore_defaults)
|
self.connect(rb, SIGNAL('clicked()'), self.restore_defaults)
|
||||||
self.groups.setMouseTracking(True)
|
self.groups.setMouseTracking(True)
|
||||||
|
geom = gprefs.get('convert_single_dialog_geom', None)
|
||||||
|
if geom:
|
||||||
|
self.restoreGeometry(geom)
|
||||||
|
|
||||||
def restore_defaults(self):
|
def restore_defaults(self):
|
||||||
delete_specifics(self.db, self.book_id)
|
delete_specifics(self.db, self.book_id)
|
||||||
@ -263,6 +266,12 @@ class Config(ResizableDialog, Ui_Dialog):
|
|||||||
self.break_cycles()
|
self.break_cycles()
|
||||||
ResizableDialog.reject(self)
|
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):
|
def break_cycles(self):
|
||||||
for i in range(self.stack.count()):
|
for i in range(self.stack.count()):
|
||||||
w = self.stack.widget(i)
|
w = self.stack.widget(i)
|
||||||
|
@ -433,6 +433,15 @@ class DeviceManager(Thread): # {{{
|
|||||||
return self.create_job_step(self._get_device_information, done,
|
return self.create_job_step(self._get_device_information, done,
|
||||||
description=_('Get device information'), to_job=add_as_step_to_job)
|
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):
|
def get_current_device_information(self):
|
||||||
return self._device_information
|
return self._device_information
|
||||||
|
|
||||||
@ -1023,6 +1032,7 @@ class DeviceMixin(object): # {{{
|
|||||||
if job.failed:
|
if job.failed:
|
||||||
self.device_job_exception(job)
|
self.device_job_exception(job)
|
||||||
return
|
return
|
||||||
|
self.device_manager.slow_driveinfo()
|
||||||
# set_books_in_library might schedule a sync_booklists job
|
# 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)
|
self.set_books_in_library(job.result, reset=True, add_as_step_to_job=job)
|
||||||
mainlist, cardalist, cardblist = job.result
|
mainlist, cardalist, cardblist = job.result
|
||||||
|
@ -18,6 +18,7 @@ from calibre.ebooks import BOOK_EXTENSIONS
|
|||||||
from calibre.gui2 import error_dialog
|
from calibre.gui2 import error_dialog
|
||||||
from calibre.gui2.dialogs.template_dialog import TemplateDialog
|
from calibre.gui2.dialogs.template_dialog import TemplateDialog
|
||||||
from calibre.utils.date import parse_date
|
from calibre.utils.date import parse_date
|
||||||
|
from calibre.gui2.device_drivers.mtp_folder_browser import Browser
|
||||||
|
|
||||||
class FormatsConfig(QWidget): # {{{
|
class FormatsConfig(QWidget): # {{{
|
||||||
|
|
||||||
@ -117,19 +118,36 @@ class TemplateConfig(QWidget): # {{{
|
|||||||
|
|
||||||
class SendToConfig(QWidget): # {{{
|
class SendToConfig(QWidget): # {{{
|
||||||
|
|
||||||
def __init__(self, val):
|
def __init__(self, val, device):
|
||||||
QWidget.__init__(self)
|
QWidget.__init__(self)
|
||||||
self.t = t = QLineEdit(self)
|
self.t = t = QLineEdit(self)
|
||||||
t.setText(', '.join(val or []))
|
t.setText(', '.join(val or []))
|
||||||
t.setCursorPosition(0)
|
t.setCursorPosition(0)
|
||||||
self.l = l = QVBoxLayout(self)
|
self.l = l = QGridLayout(self)
|
||||||
self.setLayout(l)
|
self.setLayout(l)
|
||||||
self.m = m = QLabel('<p>'+_('''A <b>list of &folders</b> on the device to
|
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:'''))
|
which to send ebooks. The first one that exists will be used:'''))
|
||||||
m.setWordWrap(True)
|
m.setWordWrap(True)
|
||||||
m.setBuddy(t)
|
m.setBuddy(t)
|
||||||
l.addWidget(m)
|
l.addWidget(m, 0, 0, 1, 2)
|
||||||
l.addWidget(t)
|
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
|
@property
|
||||||
def value(self):
|
def value(self):
|
||||||
@ -183,8 +201,9 @@ class Rule(QWidget):
|
|||||||
|
|
||||||
remove = pyqtSignal(object)
|
remove = pyqtSignal(object)
|
||||||
|
|
||||||
def __init__(self, rule=None):
|
def __init__(self, device, rule=None):
|
||||||
QWidget.__init__(self)
|
QWidget.__init__(self)
|
||||||
|
self._device = weakref.ref(device)
|
||||||
|
|
||||||
self.l = l = QHBoxLayout()
|
self.l = l = QHBoxLayout()
|
||||||
self.setLayout(l)
|
self.setLayout(l)
|
||||||
@ -198,6 +217,11 @@ class Rule(QWidget):
|
|||||||
self.folder = f = QLineEdit(self)
|
self.folder = f = QLineEdit(self)
|
||||||
f.setPlaceholderText(_('Folder on the device'))
|
f.setPlaceholderText(_('Folder on the device'))
|
||||||
l.addWidget(f)
|
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')),
|
self.rb = rb = QPushButton(QIcon(I('list_remove.png')),
|
||||||
_('&Remove rule'), self)
|
_('&Remove rule'), self)
|
||||||
l.addWidget(rb)
|
l.addWidget(rb)
|
||||||
@ -217,6 +241,17 @@ class Rule(QWidget):
|
|||||||
|
|
||||||
self.ignore = False
|
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):
|
def removed(self):
|
||||||
self.remove.emit(self)
|
self.remove.emit(self)
|
||||||
|
|
||||||
@ -232,8 +267,9 @@ class Rule(QWidget):
|
|||||||
|
|
||||||
class FormatRules(QGroupBox):
|
class FormatRules(QGroupBox):
|
||||||
|
|
||||||
def __init__(self, rules):
|
def __init__(self, device, rules):
|
||||||
QGroupBox.__init__(self, _('Format specific sending'))
|
QGroupBox.__init__(self, _('Format specific sending'))
|
||||||
|
self._device = weakref.ref(device)
|
||||||
self.l = l = QVBoxLayout()
|
self.l = l = QVBoxLayout()
|
||||||
self.setLayout(l)
|
self.setLayout(l)
|
||||||
self.la = la = QLabel('<p>'+_(
|
self.la = la = QLabel('<p>'+_(
|
||||||
@ -251,7 +287,7 @@ class FormatRules(QGroupBox):
|
|||||||
l.addWidget(sa)
|
l.addWidget(sa)
|
||||||
self.widgets = []
|
self.widgets = []
|
||||||
for rule in rules:
|
for rule in rules:
|
||||||
r = Rule(rule)
|
r = Rule(device, rule)
|
||||||
self.widgets.append(r)
|
self.widgets.append(r)
|
||||||
w.l.addWidget(r)
|
w.l.addWidget(r)
|
||||||
r.remove.connect(self.remove_rule)
|
r.remove.connect(self.remove_rule)
|
||||||
@ -264,8 +300,12 @@ class FormatRules(QGroupBox):
|
|||||||
b.clicked.connect(self.add_rule)
|
b.clicked.connect(self.add_rule)
|
||||||
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored)
|
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device(self):
|
||||||
|
return self._device()
|
||||||
|
|
||||||
def add_rule(self):
|
def add_rule(self):
|
||||||
r = Rule()
|
r = Rule(self.device)
|
||||||
self.widgets.append(r)
|
self.widgets.append(r)
|
||||||
self.w.l.addWidget(r)
|
self.w.l.addWidget(r)
|
||||||
r.remove.connect(self.remove_rule)
|
r.remove.connect(self.remove_rule)
|
||||||
@ -319,10 +359,10 @@ class MTPConfig(QTabWidget):
|
|||||||
l = self.base.l = QGridLayout(self.base)
|
l = self.base.l = QGridLayout(self.base)
|
||||||
self.base.setLayout(l)
|
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.formats = FormatsConfig(set(BOOK_EXTENSIONS),
|
||||||
self.get_pref('format_map'))
|
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.template = TemplateConfig(self.get_pref('send_template'))
|
||||||
self.base.la = la = QLabel(_(
|
self.base.la = la = QLabel(_(
|
||||||
'Choose the formats to send to the %s')%self.device.current_friendly_name)
|
'Choose the formats to send to the %s')%self.device.current_friendly_name)
|
||||||
|
119
src/calibre/gui2/device_drivers/mtp_folder_browser.py
Normal file
119
src/calibre/gui2/device_drivers/mtp_folder_browser.py
Normal 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())
|
||||||
|
|
@ -76,6 +76,9 @@ def config(defaults=None):
|
|||||||
|
|
||||||
return c
|
return c
|
||||||
|
|
||||||
|
def load_themes():
|
||||||
|
return JSONConfig('viewer_themes')
|
||||||
|
|
||||||
class ConfigDialog(QDialog, Ui_Dialog):
|
class ConfigDialog(QDialog, Ui_Dialog):
|
||||||
|
|
||||||
def __init__(self, shortcuts, parent=None):
|
def __init__(self, shortcuts, parent=None):
|
||||||
@ -113,7 +116,7 @@ class ConfigDialog(QDialog, Ui_Dialog):
|
|||||||
self.hyphenate_default_lang.setVisible(False)
|
self.hyphenate_default_lang.setVisible(False)
|
||||||
self.hyphenate_label.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.save_theme_button.clicked.connect(self.save_theme)
|
||||||
self.load_theme_button.m = m = QMenu()
|
self.load_theme_button.m = m = QMenu()
|
||||||
self.load_theme_button.setMenu(m)
|
self.load_theme_button.setMenu(m)
|
||||||
|
@ -21,7 +21,7 @@ from calibre.customize.ui import all_viewer_plugins
|
|||||||
from calibre.gui2.viewer.keys import SHORTCUTS
|
from calibre.gui2.viewer.keys import SHORTCUTS
|
||||||
from calibre.gui2.viewer.javascript import JavaScriptLoader
|
from calibre.gui2.viewer.javascript import JavaScriptLoader
|
||||||
from calibre.gui2.viewer.position import PagePosition
|
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.gui2.viewer.image_popup import ImagePopup
|
||||||
from calibre.ebooks.oeb.display.webview import load_html
|
from calibre.ebooks.oeb.display.webview import load_html
|
||||||
from calibre.constants import isxp, iswindows
|
from calibre.constants import isxp, iswindows
|
||||||
@ -31,8 +31,7 @@ class Document(QWebPage): # {{{
|
|||||||
|
|
||||||
page_turn = pyqtSignal(object)
|
page_turn = pyqtSignal(object)
|
||||||
|
|
||||||
def set_font_settings(self):
|
def set_font_settings(self, opts):
|
||||||
opts = config().parse()
|
|
||||||
settings = self.settings()
|
settings = self.settings()
|
||||||
settings.setFontSize(QWebSettings.DefaultFontSize, opts.default_font_size)
|
settings.setFontSize(QWebSettings.DefaultFontSize, opts.default_font_size)
|
||||||
settings.setFontSize(QWebSettings.DefaultFixedFontSize, opts.mono_font_size)
|
settings.setFontSize(QWebSettings.DefaultFixedFontSize, opts.mono_font_size)
|
||||||
@ -47,11 +46,15 @@ class Document(QWebPage): # {{{
|
|||||||
def do_config(self, parent=None):
|
def do_config(self, parent=None):
|
||||||
d = ConfigDialog(self.shortcuts, parent)
|
d = ConfigDialog(self.shortcuts, parent)
|
||||||
if d.exec_() == QDialog.Accepted:
|
if d.exec_() == QDialog.Accepted:
|
||||||
with self.page_position:
|
opts = config().parse()
|
||||||
self.set_font_settings()
|
self.apply_settings(opts)
|
||||||
self.set_user_stylesheet()
|
|
||||||
self.misc_config()
|
def apply_settings(self, opts):
|
||||||
self.after_load()
|
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):
|
def __init__(self, shortcuts, parent=None, debug_javascript=False):
|
||||||
QWebPage.__init__(self, parent)
|
QWebPage.__init__(self, parent)
|
||||||
@ -87,7 +90,8 @@ class Document(QWebPage): # {{{
|
|||||||
self.all_viewer_plugins = tuple(all_viewer_plugins())
|
self.all_viewer_plugins = tuple(all_viewer_plugins())
|
||||||
for pl in self.all_viewer_plugins:
|
for pl in self.all_viewer_plugins:
|
||||||
pl.load_fonts()
|
pl.load_fonts()
|
||||||
self.set_font_settings()
|
opts = config().parse()
|
||||||
|
self.set_font_settings(opts)
|
||||||
|
|
||||||
# Security
|
# Security
|
||||||
settings.setAttribute(QWebSettings.JavaEnabled, False)
|
settings.setAttribute(QWebSettings.JavaEnabled, False)
|
||||||
@ -98,8 +102,8 @@ class Document(QWebPage): # {{{
|
|||||||
# Miscellaneous
|
# Miscellaneous
|
||||||
settings.setAttribute(QWebSettings.LinksIncludedInFocusChain, True)
|
settings.setAttribute(QWebSettings.LinksIncludedInFocusChain, True)
|
||||||
settings.setAttribute(QWebSettings.DeveloperExtrasEnabled, True)
|
settings.setAttribute(QWebSettings.DeveloperExtrasEnabled, True)
|
||||||
self.set_user_stylesheet()
|
self.set_user_stylesheet(opts)
|
||||||
self.misc_config()
|
self.misc_config(opts)
|
||||||
|
|
||||||
# Load javascript
|
# Load javascript
|
||||||
self.mainFrame().javaScriptWindowObjectCleared.connect(
|
self.mainFrame().javaScriptWindowObjectCleared.connect(
|
||||||
@ -112,8 +116,7 @@ class Document(QWebPage): # {{{
|
|||||||
mf.setScrollBarPolicy(Qt.Vertical, Qt.ScrollBarAlwaysOff)
|
mf.setScrollBarPolicy(Qt.Vertical, Qt.ScrollBarAlwaysOff)
|
||||||
mf.setScrollBarPolicy(Qt.Horizontal, Qt.ScrollBarAlwaysOff)
|
mf.setScrollBarPolicy(Qt.Horizontal, Qt.ScrollBarAlwaysOff)
|
||||||
|
|
||||||
def set_user_stylesheet(self):
|
def set_user_stylesheet(self, opts):
|
||||||
opts = config().parse()
|
|
||||||
bg = opts.background_color or 'white'
|
bg = opts.background_color or 'white'
|
||||||
brules = ['background-color: %s !important'%bg]
|
brules = ['background-color: %s !important'%bg]
|
||||||
prefix = '''
|
prefix = '''
|
||||||
@ -127,8 +130,7 @@ class Document(QWebPage): # {{{
|
|||||||
data += b64encode(raw.encode('utf-8'))
|
data += b64encode(raw.encode('utf-8'))
|
||||||
self.settings().setUserStyleSheetUrl(QUrl(data))
|
self.settings().setUserStyleSheetUrl(QUrl(data))
|
||||||
|
|
||||||
def misc_config(self):
|
def misc_config(self, opts):
|
||||||
opts = config().parse()
|
|
||||||
self.hyphenate = opts.hyphenate
|
self.hyphenate = opts.hyphenate
|
||||||
self.hyphenate_default_lang = opts.hyphenate_default_lang
|
self.hyphenate_default_lang = opts.hyphenate_default_lang
|
||||||
self.do_fit_images = opts.fit_images
|
self.do_fit_images = opts.fit_images
|
||||||
@ -560,6 +562,15 @@ class DocumentView(QWebView): # {{{
|
|||||||
self.document.switch_to_fullscreen_mode()
|
self.document.switch_to_fullscreen_mode()
|
||||||
self.setFocus(Qt.OtherFocusReason)
|
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):
|
def bookmark(self):
|
||||||
return self.document.bookmark()
|
return self.document.bookmark()
|
||||||
|
|
||||||
|
@ -245,8 +245,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
|
|||||||
self.action_back.triggered[bool].connect(self.back)
|
self.action_back.triggered[bool].connect(self.back)
|
||||||
self.action_forward.triggered[bool].connect(self.forward)
|
self.action_forward.triggered[bool].connect(self.forward)
|
||||||
self.action_bookmark.triggered[bool].connect(self.bookmark)
|
self.action_bookmark.triggered[bool].connect(self.bookmark)
|
||||||
self.action_preferences.triggered.connect(lambda :
|
self.action_preferences.triggered.connect(self.do_config)
|
||||||
self.view.config(self))
|
|
||||||
self.pos.editingFinished.connect(self.goto_page_num)
|
self.pos.editingFinished.connect(self.goto_page_num)
|
||||||
self.vertical_scrollbar.valueChanged[int].connect(lambda
|
self.vertical_scrollbar.valueChanged[int].connect(lambda
|
||||||
x:self.goto_page(x/100.))
|
x:self.goto_page(x/100.))
|
||||||
@ -259,6 +258,10 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
|
|||||||
self.action_bookmark.setMenu(self.bookmarks_menu)
|
self.action_bookmark.setMenu(self.bookmarks_menu)
|
||||||
self.set_bookmarks([])
|
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:
|
if pathtoebook is not None:
|
||||||
f = functools.partial(self.load_ebook, pathtoebook, open_at=open_at)
|
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)
|
getattr(self, o).setEnabled(False)
|
||||||
self.setCursor(Qt.BusyCursor)
|
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):
|
def bookmark(self, *args):
|
||||||
num = 1
|
num = 1
|
||||||
bm = None
|
bm = None
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>653</width>
|
<width>653</width>
|
||||||
<height>672</height>
|
<height>746</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowTitle">
|
<property name="windowTitle">
|
||||||
@ -141,6 +141,7 @@
|
|||||||
<addaction name="separator"/>
|
<addaction name="separator"/>
|
||||||
<addaction name="action_preferences"/>
|
<addaction name="action_preferences"/>
|
||||||
<addaction name="action_metadata"/>
|
<addaction name="action_metadata"/>
|
||||||
|
<addaction name="action_load_theme"/>
|
||||||
<addaction name="separator"/>
|
<addaction name="separator"/>
|
||||||
<addaction name="action_print"/>
|
<addaction name="action_print"/>
|
||||||
</widget>
|
</widget>
|
||||||
@ -332,6 +333,18 @@
|
|||||||
<string>Toggle Paged mode</string>
|
<string>Toggle Paged mode</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</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>
|
</widget>
|
||||||
<customwidgets>
|
<customwidgets>
|
||||||
<customwidget>
|
<customwidget>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user