Merge from trunk

This commit is contained in:
Charles Haley 2012-09-12 22:44:00 +02:00
commit 5ad16d52c1
12 changed files with 396 additions and 44 deletions

View File

@ -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 :method:`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.

View File

@ -288,7 +288,7 @@ class KINDLE2(KINDLE):
name = 'Kindle 2/3/4/Touch Device Interface' name = 'Kindle 2/3/4/Touch Device Interface'
description = _('Communicate with the Kindle 2/3/4/Touch eBook reader.') description = _('Communicate with the Kindle 2/3/4/Touch eBook reader.')
FORMATS = KINDLE.FORMATS + ['pdf', 'azw4', 'pobi'] FORMATS = ['azw3'] + KINDLE.FORMATS + ['pdf', 'azw4', 'pobi']
DELETE_EXTS = KINDLE.DELETE_EXTS + ['.mbp1', '.mbs', '.sdr'] DELETE_EXTS = KINDLE.DELETE_EXTS + ['.mbp1', '.mbs', '.sdr']
PRODUCT_ID = [0x0002, 0x0004] PRODUCT_ID = [0x0002, 0x0004]
@ -449,7 +449,7 @@ class KINDLE_DX(KINDLE2):
name = 'Kindle DX Device Interface' name = 'Kindle DX Device Interface'
description = _('Communicate with the Kindle DX eBook reader.') description = _('Communicate with the Kindle DX eBook reader.')
FORMATS = KINDLE2.FORMATS[1:]
PRODUCT_ID = [0x0003] PRODUCT_ID = [0x0003]
BCD = [0x0100] BCD = [0x0100]
@ -462,7 +462,6 @@ class KINDLE_FIRE(KINDLE2):
description = _('Communicate with the Kindle Fire') description = _('Communicate with the Kindle Fire')
gui_name = 'Fire' gui_name = 'Fire'
FORMATS = list(KINDLE2.FORMATS) FORMATS = list(KINDLE2.FORMATS)
FORMATS.insert(0, 'azw3')
PRODUCT_ID = [0x0006] PRODUCT_ID = [0x0006]
BCD = [0x216, 0x100] BCD = [0x216, 0x100]

View File

@ -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)
@ -47,12 +48,13 @@ class MTP_DEVICE(BASE):
from calibre.library.save_to_disk import config from calibre.library.save_to_disk import config
self._prefs = p = JSONConfig('mtp_devices') self._prefs = p = JSONConfig('mtp_devices')
p.defaults['format_map'] = self.FORMATS p.defaults['format_map'] = self.FORMATS
p.defaults['send_to'] = ['Books', 'eBooks/import', 'eBooks', p.defaults['send_to'] = ['Calibre_Companion', 'Books',
'wordplayer/calibretransfer', 'sdcard/ebooks', 'eBooks/import', 'eBooks', 'wordplayer/calibretransfer',
'kindle'] 'sdcard/ebooks', 'kindle']
p.defaults['send_template'] = config().parse().send_template p.defaults['send_template'] = config().parse().send_template
p.defaults['blacklist'] = [] p.defaults['blacklist'] = []
p.defaults['history'] = {} p.defaults['history'] = {}
p.defaults['rules'] = []
return self._prefs return self._prefs
@ -75,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:
@ -106,15 +109,19 @@ class MTP_DEVICE(BASE):
dinfo['mtp_prefix'] = storage.storage_prefix dinfo['mtp_prefix'] = storage.storage_prefix
raw = json.dumps(dinfo, default=to_json) raw = json.dumps(dinfo, default=to_json)
self.put_file(storage, self.DRIVEINFO, BytesIO(raw), len(raw)) self.put_file(storage, self.DRIVEINFO, BytesIO(raw), len(raw))
self.driveinfo = 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] )
@ -134,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:
@ -273,15 +281,18 @@ class MTP_DEVICE(BASE):
self.plugboards = plugboards self.plugboards = plugboards
self.plugboard_func = pb_func self.plugboard_func = pb_func
def create_upload_path(self, path, mdata, fname): def create_upload_path(self, path, mdata, fname, routing):
from calibre.devices.utils import create_upload_path from calibre.devices.utils import create_upload_path
from calibre.utils.filenames import ascii_filename as sanitize from calibre.utils.filenames import ascii_filename as sanitize
ext = fname.rpartition('.')[-1].lower()
path = routing.get(ext, path)
filepath = create_upload_path(mdata, fname, self.save_template, sanitize, filepath = create_upload_path(mdata, fname, self.save_template, sanitize,
prefix_path=path, prefix_path=path,
path_type=posixpath, path_type=posixpath,
maxlen=self.MAX_PATH_LEN, maxlen=self.MAX_PATH_LEN,
use_subdirs = True, use_subdirs=True,
news_in_folder = self.NEWS_IN_FOLDER, news_in_folder=self.NEWS_IN_FOLDER,
) )
return tuple(x for x in filepath.split('/')) return tuple(x for x in filepath.split('/'))
@ -329,8 +340,10 @@ class MTP_DEVICE(BASE):
self.report_progress(0, _('Transferring books to device...')) self.report_progress(0, _('Transferring books to device...'))
i, total = 0, len(files) i, total = 0, len(files)
routing = {fmt:dest for fmt,dest in self.get_pref('rules')}
for infile, fname, mi in izip(files, names, metadata): for infile, fname, mi in izip(files, names, metadata):
path = self.create_upload_path(prefix, mi, fname) path = self.create_upload_path(prefix, mi, fname, routing)
parent = self.ensure_parent(storage, path) parent = self.ensure_parent(storage, path)
if hasattr(infile, 'read'): if hasattr(infile, 'read'):
pos = infile.tell() pos = infile.tell()

View File

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

View File

@ -7,7 +7,7 @@ __license__ = 'GPL v3'
__copyright__ = '2012, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2012, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import operator, traceback, pprint, sys import operator, traceback, pprint, sys, time
from threading import RLock from threading import RLock
from collections import namedtuple from collections import namedtuple
from functools import partial from functools import partial
@ -16,7 +16,7 @@ from calibre import prints, as_unicode
from calibre.constants import plugins from calibre.constants import plugins
from calibre.ptempfile import SpooledTemporaryFile from calibre.ptempfile import SpooledTemporaryFile
from calibre.devices.errors import OpenFailed, DeviceError, BlacklistedDevice from calibre.devices.errors import OpenFailed, DeviceError, BlacklistedDevice
from calibre.devices.mtp.base import MTPDeviceBase, synchronous from calibre.devices.mtp.base import MTPDeviceBase, synchronous, debug
MTPDevice = namedtuple('MTPDevice', 'busnum devnum vendor_id product_id ' MTPDevice = namedtuple('MTPDevice', 'busnum devnum vendor_id product_id '
'bcd serial manufacturer product') 'bcd serial manufacturer product')
@ -193,6 +193,8 @@ class MTP_DEVICE(MTPDeviceBase):
@property @property
def filesystem_cache(self): def filesystem_cache(self):
if self._filesystem_cache is None: if self._filesystem_cache is None:
st = time.time()
debug('Loading filesystem metadata...')
from calibre.devices.mtp.filesystem_cache import FilesystemCache from calibre.devices.mtp.filesystem_cache import FilesystemCache
with self.lock: with self.lock:
storage, all_items, all_errs = [], [], [] storage, all_items, all_errs = [], [], []
@ -220,6 +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 (%d objects)'%(
time.time()-st, len(self._filesystem_cache)))
return self._filesystem_cache return self._filesystem_cache
@synchronous @synchronous

View File

@ -16,7 +16,7 @@ from calibre import as_unicode, prints
from calibre.constants import plugins, __appname__, numeric_version from calibre.constants import plugins, __appname__, numeric_version
from calibre.ptempfile import SpooledTemporaryFile from calibre.ptempfile import SpooledTemporaryFile
from calibre.devices.errors import OpenFailed, DeviceError, BlacklistedDevice from calibre.devices.errors import OpenFailed, DeviceError, BlacklistedDevice
from calibre.devices.mtp.base import MTPDeviceBase from calibre.devices.mtp.base import MTPDeviceBase, debug
class ThreadingViolation(Exception): class ThreadingViolation(Exception):
@ -199,6 +199,8 @@ class MTP_DEVICE(MTPDeviceBase):
@property @property
def filesystem_cache(self): def filesystem_cache(self):
if self._filesystem_cache is None: if self._filesystem_cache is None:
debug('Loading filesystem metadata...')
st = time.time()
from calibre.devices.mtp.filesystem_cache import FilesystemCache from calibre.devices.mtp.filesystem_cache import FilesystemCache
ts = self.total_space() ts = self.total_space()
all_storage = [] all_storage = []
@ -218,6 +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 (%d objects)'%(
time.time()-st, len(self._filesystem_cache)))
return self._filesystem_cache return self._filesystem_cache
@same_thread @same_thread

View File

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

View File

@ -152,8 +152,16 @@ def render_data(mi, use_roman_numbers=True, all_fields=False):
scheme = u'devpath' if isdevice else u'path' scheme = u'devpath' if isdevice else u'path'
url = prepare_string_for_xml(path if isdevice else url = prepare_string_for_xml(path if isdevice else
unicode(mi.id), True) unicode(mi.id), True)
link = u'<a href="%s:%s" title="%s">%s</a>' % (scheme, url, pathstr = _('Click to open')
prepare_string_for_xml(path, True), _('Click to open')) extra = ''
if isdevice:
durl = url
if durl.startswith('mtp:::'):
durl = ':::'.join( (durl.split(':::'))[2:] )
extra = '<br><span style="font-size:smaller">%s</span>'%(
prepare_string_for_xml(durl))
link = u'<a href="%s:%s" title="%s">%s</a>%s' % (scheme, url,
prepare_string_for_xml(path, True), pathstr, extra)
ans.append((field, u'<td class="title">%s</td><td>%s</td>'%(name, link))) ans.append((field, u'<td class="title">%s</td><td>%s</td>'%(name, link)))
elif field == 'formats': elif field == 'formats':
if isdevice: continue if isdevice: continue

View File

@ -7,7 +7,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>588</width> <width>588</width>
<height>342</height> <height>416</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
@ -91,23 +91,33 @@
<item row="0" column="1"> <item row="0" column="1">
<widget class="QComboBox" name="opt_mobi_file_type"/> <widget class="QComboBox" name="opt_mobi_file_type"/>
</item> </item>
<item row="1" column="0"> <item row="3" column="0">
<widget class="QLabel" name="label_3"> <widget class="QLabel" name="label_3">
<property name="text"> <property name="text">
<string>Personal Doc tag:</string> <string>Personal Doc tag:</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="1"> <item row="3" column="1">
<widget class="QLineEdit" name="opt_personal_doc"/> <widget class="QLineEdit" name="opt_personal_doc"/>
</item> </item>
<item row="2" column="0" colspan="2"> <item row="4" column="0" colspan="2">
<widget class="QCheckBox" name="opt_share_not_sync"> <widget class="QCheckBox" name="opt_share_not_sync">
<property name="text"> <property name="text">
<string>Enable sharing of book content via Facebook, etc. WARNING: Disables last read syncing</string> <string>Enable sharing of book content via Facebook, etc. WARNING: Disables last read syncing</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="0" colspan="2">
<widget class="QLabel" name="label_4">
<property name="text">
<string>&lt;b&gt;WARNING:&lt;/b&gt; Various Kindle devices have trouble displaying the new or both MOBI filetypes. If you wish to use the new format on your device, convert to AZW3 instead of MOBI.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
</item> </item>

View File

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

View File

@ -11,12 +11,14 @@ import weakref
from PyQt4.Qt import (QWidget, QListWidgetItem, Qt, QToolButton, QLabel, from PyQt4.Qt import (QWidget, QListWidgetItem, Qt, QToolButton, QLabel,
QTabWidget, QGridLayout, QListWidget, QIcon, QLineEdit, QVBoxLayout, QTabWidget, QGridLayout, QListWidget, QIcon, QLineEdit, QVBoxLayout,
QPushButton) QPushButton, QGroupBox, QScrollArea, QHBoxLayout, QComboBox,
pyqtSignal, QSizePolicy, QDialog, QDialogButtonBox)
from calibre.ebooks import BOOK_EXTENSIONS 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): # {{{
@ -86,7 +88,7 @@ class TemplateConfig(QWidget): # {{{
m.setBuddy(t) m.setBuddy(t)
l.addWidget(m, 0, 0, 1, 2) l.addWidget(m, 0, 0, 1, 2)
l.addWidget(t, 1, 0, 1, 1) l.addWidget(t, 1, 0, 1, 1)
b = self.b = QPushButton(_('Template editor')) b = self.b = QPushButton(_('&Template editor'))
l.addWidget(b, 1, 1, 1, 1) l.addWidget(b, 1, 1, 1, 1)
b.clicked.connect(self.edit_template) b.clicked.connect(self.edit_template)
@ -116,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):
@ -176,6 +195,135 @@ class IgnoredDevices(QWidget): # {{{
# }}} # }}}
# Rules {{{
class Rule(QWidget):
remove = pyqtSignal(object)
def __init__(self, device, rule=None):
QWidget.__init__(self)
self._device = weakref.ref(device)
self.l = l = QHBoxLayout()
self.setLayout(l)
self.l1 = l1 = QLabel(_('Send the '))
l.addWidget(l1)
self.fmt = f = QComboBox(self)
l.addWidget(f)
self.l2 = l2 = QLabel(_(' format to the folder: '))
l.addWidget(l2)
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)
rb.clicked.connect(self.removed)
for fmt in sorted(BOOK_EXTENSIONS):
self.fmt.addItem(fmt.upper(), fmt.lower())
self.fmt.setCurrentIndex(0)
if rule is not None:
fmt, folder = rule
idx = self.fmt.findText(fmt.upper())
if idx > -1:
self.fmt.setCurrentIndex(idx)
self.folder.setText(folder)
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)
@property
def rule(self):
folder = unicode(self.folder.text()).strip()
if folder:
return (
unicode(self.fmt.itemData(self.fmt.currentIndex()).toString()),
folder
)
return None
class FormatRules(QGroupBox):
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>'+_(
'''You can create rules that control where ebooks of a specific
format are sent to on the device. These will take precedence over
the folders specified above.'''))
la.setWordWrap(True)
l.addWidget(la)
self.sa = sa = QScrollArea(self)
sa.setWidgetResizable(True)
self.w = w = QWidget(self)
w.l = QVBoxLayout()
w.setLayout(w.l)
sa.setWidget(w)
l.addWidget(sa)
self.widgets = []
for rule in rules:
r = Rule(device, rule)
self.widgets.append(r)
w.l.addWidget(r)
r.remove.connect(self.remove_rule)
if not self.widgets:
self.add_rule()
self.b = b = QPushButton(QIcon(I('plus.png')), _('Add a &new rule'))
l.addWidget(b)
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(self.device)
self.widgets.append(r)
self.w.l.addWidget(r)
r.remove.connect(self.remove_rule)
self.sa.verticalScrollBar().setValue(self.sa.verticalScrollBar().maximum())
def remove_rule(self, rule):
rule.setVisible(False)
rule.ignore = True
@property
def rules(self):
for w in self.widgets:
if not w.ignore:
r = w.rule
if r is not None:
yield r
# }}}
class MTPConfig(QTabWidget): class MTPConfig(QTabWidget):
def __init__(self, device, parent=None): def __init__(self, device, parent=None):
@ -185,8 +333,8 @@ class MTPConfig(QTabWidget):
cd = msg = None cd = msg = None
if device.current_friendly_name is not None: if device.current_friendly_name is not None:
if device.current_serial_num is None: if device.current_serial_num is None:
msg = '<p>' + _('The <b>%s</b> device has no serial number, ' msg = '<p>' + (_('The <b>%s</b> device has no serial number, '
'it cannot be configured'%device.current_friendly_name) 'it cannot be configured')%device.current_friendly_name)
else: else:
cd = 'device-'+device.current_serial_num cd = 'device-'+device.current_serial_num
else: else:
@ -211,24 +359,28 @@ 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.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)
la.setWordWrap(True) la.setWordWrap(True)
l.addWidget(la, 0, 0, 1, 1) self.base.b = b = QPushButton(QIcon(I('list_remove.png')),
l.addWidget(self.formats, 1, 0, 2, 1) _('&Ignore the %s in calibre')%device.current_friendly_name,
l.addWidget(self.send_to, 1, 1, 1, 1)
l.addWidget(self.template, 2, 1, 1, 1)
l.setRowStretch(2, 10)
self.base.b = b = QPushButton(QIcon(I('minus.png')),
_('Ignore the %s in calibre')%device.current_friendly_name,
self.base) self.base)
l.addWidget(b, 3, 0, 1, 2)
b.clicked.connect(self.ignore_device) b.clicked.connect(self.ignore_device)
l.addWidget(b, 0, 0, 1, 2)
l.addWidget(la, 1, 0, 1, 1)
l.addWidget(self.formats, 2, 0, 3, 1)
l.addWidget(self.send_to, 2, 1, 1, 1)
l.addWidget(self.template, 3, 1, 1, 1)
l.setRowStretch(4, 10)
l.addWidget(r, 5, 0, 1, 2)
l.setRowStretch(5, 100)
self.igntab = IgnoredDevices(self.device.prefs['history'], self.igntab = IgnoredDevices(self.device.prefs['history'],
self.device.prefs['blacklist']) self.device.prefs['blacklist'])
self.addTab(self.igntab, _('Ignored devices')) self.addTab(self.igntab, _('Ignored devices'))
@ -280,6 +432,11 @@ class MTPConfig(QTabWidget):
if s and s != self.device.prefs['send_to']: if s and s != self.device.prefs['send_to']:
p['send_to'] = s p['send_to'] = s
p.pop('rules', None)
r = list(self.rules.rules)
if r and r != self.device.prefs['rules']:
p['rules'] = r
self.device.prefs[self.current_device_key] = p self.device.prefs[self.current_device_key] = p
self.device.prefs['blacklist'] = self.igntab.blacklist self.device.prefs['blacklist'] = self.igntab.blacklist
@ -296,8 +453,16 @@ if __name__ == '__main__':
cd = dev.detect_managed_devices(s.devices) cd = dev.detect_managed_devices(s.devices)
dev.open(cd, 'test') dev.open(cd, 'test')
cw = dev.config_widget() cw = dev.config_widget()
cw.show() d = QDialog()
app.exec_() d.l = QVBoxLayout()
d.setLayout(d.l)
d.l.addWidget(cw)
bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel)
d.l.addWidget(bb)
bb.accepted.connect(d.accept)
bb.rejected.connect(d.reject)
if d.exec_() == d.Accepted:
cw.commit()
dev.shutdown() dev.shutdown()

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