From 5665f5e57e68534f5a60123483a5c2ea8721bdfe Mon Sep 17 00:00:00 2001 From: David Date: Mon, 11 Apr 2016 20:34:05 +1000 Subject: [PATCH 1/8] New tabbed configuration for KoboTouch driver. --- src/calibre/devices/kobo/driver.py | 518 ++++++++++++++++++++--------- 1 file changed, 356 insertions(+), 162 deletions(-) diff --git a/src/calibre/devices/kobo/driver.py b/src/calibre/devices/kobo/driver.py index c20b9fb037..82bf8ff0c5 100644 --- a/src/calibre/devices/kobo/driver.py +++ b/src/calibre/devices/kobo/driver.py @@ -145,6 +145,49 @@ class KOBO(USBMS): OPT_SHOW_RECOMMENDATIONS = 5 OPT_SUPPORT_NEWER_FIRMWARE = 6 + def __init__(self, *args, **kwargs): + USBMS.__init__(self, *args, **kwargs) + self.plugboards = self.plugboard_func = None + self._prefs = None +# self.device_defaults = DeviceDefaults() + self.current_device_defaults = {} + self.device_defaults_key = self.name + + @property + def prefs(self): + from calibre.utils.config import JSONConfig +# debug_print("KOBO:prefs - start") + if self._prefs is None: + self._prefs = p = JSONConfig(self.device_defaults_key + '_devices') +# debug_print("KOBO:prefs - self._prefs=", self._prefs) + if p == {}: + old_settings = super(KOBO, self).settings() + p[self.device_defaults_key] = self.migrate_old_settings(old_settings) +# debug_print("KOBO:prefs - Old settings self._prefs=", self._prefs) + p.defaults['format_map'] = self.FORMATS + p.defaults['save_template'] = KOBO._default_save_template() + p.defaults['use_subdirs'] = True + p.defaults['read_metadata'] = True + p.defaults['use_author_sort'] = False + + p.defaults['collections_columns'] = '' + p.defaults['create_collections'] = False + p.defaults['delete_empty_collections'] = False + + p.defaults['upload_covers'] = False + p.defaults['upload_grayscale'] = False + + p.defaults['show_expired_books'] = False + p.defaults['show_previews'] = False + p.defaults['show_recommendations'] = False + p.defaults['support_newer_firmware'] = False + + p.defaults['extra_customization'] = self.EXTRA_CUSTOMIZATION_DEFAULT + debug_print("KOBO:prefs - finish self._prefs=", self._prefs) + + return self._prefs + + def initialize(self): USBMS.initialize(self) self.dbversion = 7 @@ -311,7 +354,7 @@ class KOBO(USBMS): 'BookID is Null %(previews)s %(recomendations)s and not ((___ExpirationStatus=3 or ___ExpirationStatus is Null) %(expiry)s') % dict(expiry=' and ContentType = 6)' if opts.extra_customization[self.OPT_SHOW_EXPIRED_BOOK_RECORDS] else ')', previews=' and Accessibility <> 6' - if opts.extra_customization[self.OPT_SHOW_PREVIEWS] == False else '', + if not self.show_previews else '', recomendations=' and IsDownloaded in (\'true\', 1)' if opts.extra_customization[self.OPT_SHOW_RECOMMENDATIONS] == False else '') elif self.dbversion >= 16 and self.dbversion < 33: @@ -709,7 +752,7 @@ class KOBO(USBMS): # debug_print("KOBO:book_from_path - title=%s"%title) from calibre.ebooks.metadata import MetaInformation - if cls.settings().read_metadata or cls.MUST_READ_METADATA: + if cls.read_metadata or cls.MUST_READ_METADATA: mi = cls.metadata_from_path(cls.normalize_path(os.path.join(prefix, lpath))) else: from calibre.ebooks.metadata.meta import metadata_from_filename @@ -898,12 +941,15 @@ class KOBO(USBMS): # debug_print('Finished update_device_database_collections', collections_attributes) def get_collections_attributes(self): - collections = [] - opts = self.settings() - if opts.extra_customization and len(opts.extra_customization[self.OPT_COLLECTIONS]) > 0: - collections = [x.lower().strip() for x in opts.extra_customization[self.OPT_COLLECTIONS].split(',')] + collections = [x.lower().strip() for x in self.collections_columns.split(',')] return collections + @property + def collections_columns(self): + opts = self.settings() + return opts.extra_customization[self.OPT_COLLECTIONS] + + def sync_booklists(self, booklists, end_session=True): debug_print('KOBO:sync_booklists - start') paths = self.get_device_paths() @@ -1045,6 +1091,50 @@ class KOBO(USBMS): paths[idx] = tf.name return paths + def config_widget(self): + # TODO: Cleanup the following + self.current_friendly_name = self.gui_name + + from calibre.gui2.device_drivers.tabbed_device_config import TabbedDeviceConfig + return TabbedDeviceConfig(self.settings(), self.FORMATS, self.SUPPORTS_SUB_DIRS, + self.MUST_READ_METADATA, self.SUPPORTS_USE_AUTHOR_SORT, + self.EXTRA_CUSTOMIZATION_MESSAGE, self, + extra_customization_choices=self.EXTRA_CUSTOMIZATION_CHOICES) + + def migrate_old_settings(self, old_settings): + + OPT_COLLECTIONS = 0 + OPT_UPLOAD_COVERS = 1 + OPT_UPLOAD_GRAYSCALE_COVERS = 2 + OPT_SHOW_EXPIRED_BOOK_RECORDS = 3 + OPT_SHOW_PREVIEWS = 4 + OPT_SHOW_RECOMMENDATIONS = 5 + OPT_SUPPORT_NEWER_FIRMWARE = 6 + + + p = {} + p['format_map'] = old_settings.format_map + p['save_template'] = old_settings.save_template + p['use_subdirs'] = old_settings.use_subdirs + p['read_metadata'] = old_settings.read_metadata + p['use_author_sort'] = old_settings.use_author_sort + p['extra_customization'] = old_settings.extra_customization + + p['collections_columns'] = old_settings.extra_customization[OPT_COLLECTIONS] + + p['upload_covers'] = old_settings.extra_customization[OPT_UPLOAD_COVERS] + p['upload_grayscale'] = old_settings.extra_customization[OPT_UPLOAD_GRAYSCALE_COVERS] + + p['show_expired_books'] = old_settings.extra_customization[OPT_SHOW_EXPIRED_BOOK_RECORDS] + p['show_previews'] = old_settings.extra_customization[OPT_SHOW_PREVIEWS] + p['show_recommendations'] = old_settings.extra_customization[OPT_SHOW_RECOMMENDATIONS] + + p['support_newer_firmware'] = old_settings.extra_customization[OPT_SUPPORT_NEWER_FIRMWARE] + + return p + + + def create_annotations_path(self, mdata, device_path=None): if device_path: return device_path @@ -1286,84 +1376,10 @@ class KOBOTOUCH(KOBO): KOBO_EXTRA_CSSFILE = 'kobo_extra.css' EXTRA_CUSTOMIZATION_MESSAGE = [ - _('The Kobo from firmware V2.0.0 supports bookshelves.' - ' These are created on the Kobo. ' + - 'Specify a tags type column for automatic management.'), - _('Create Bookshelves') + - ':::'+_('Create new bookshelves on the Kobo if they do not exist. This is only for firmware V2.0.0 or later.'), - _('Delete Empty Bookshelves') + - ':::'+_('Delete any empty bookshelves from the Kobo when syncing is finished. This is only for firmware V2.0.0 or later.'), - _('Upload covers for books') + - ':::'+_('Upload cover images from the calibre library when sending books to the device.'), - _('Upload Black and White Covers'), - _('Keep cover aspect ratio') + - ':::'+_('When uploading covers, do not change the aspect ratio when resizing for the device.' - ' This is for firmware versions 2.3.1 and later.'), - _('Show archived books') + - ':::'+_('Archived books are listed on the device but need to be downloaded to read.' - ' Use this option to show these books and match them with books in the calibre library.'), - _('Show Previews') + - ':::'+_('Kobo previews are included on the Touch and some other versions' - ' by default they are no longer displayed as there is no good reason to ' - 'see them. Enable if you wish to see/delete them.'), - _('Show Recommendations') + - ':::'+_('Kobo shows recommendations on the device. In some cases these have ' - 'files but in other cases they are just pointers to the web site to buy. ' - 'Enable if you wish to see/delete them.'), - _('Set Series information') + - ':::'+_('The book lists on the Kobo devices can display series information. ' - 'This is not read by the device from the sideloaded books. ' - 'Series information can only be added to the device after the book has been processed by the device. ' - 'Enable if you wish to set series information.'), - _('Modify CSS') + - ':::'+_('This allows addition of user CSS rules and removal of some CSS. ' - 'When sending a book, the driver adds the contents of {0} to all stylesheets in the ePub. ' - 'This file is searched for in the root directory of the main memory of the device. ' - 'As well as this, if the file contains settings for the "orphans" or "widows", ' - 'these are removed for all styles in the original stylesheet.').format(KOBO_EXTRA_CSSFILE), - _('Attempt to support newer firmware') + - ':::'+_('Kobo routinely updates the firmware and the ' - 'database version. With this option Calibre will attempt ' - 'to perform full read-write functionality - Here be Dragons!! ' - 'Enable only if you are comfortable with restoring your kobo ' - 'to factory defaults and testing software. ' - 'This driver supports firmware V2.x.x and DBVersion up to ') + unicode(supported_dbversion), - _('Title to test when debugging') + - ':::'+_('Part of title of a book that can be used when doing some tests for debugging. ' - 'The test is to see if the string is contained in the title of a book. ' - 'The better the match, the less extraneous output.'), - ] - + ] EXTRA_CUSTOMIZATION_DEFAULT = [ - u'', - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - u'' - ] - - OPT_COLLECTIONS = 0 - OPT_CREATE_BOOKSHELVES = 1 - OPT_DELETE_BOOKSHELVES = 2 - OPT_UPLOAD_COVERS = 3 - OPT_UPLOAD_GRAYSCALE_COVERS = 4 - OPT_KEEP_COVER_ASPECT_RATIO = 5 - OPT_SHOW_ARCHIVED_BOOK_RECORDS = 6 - OPT_SHOW_PREVIEWS = 7 - OPT_SHOW_RECOMMENDATIONS = 8 - OPT_UPDATE_SERIES_DETAILS = 9 - OPT_MODIFY_CSS = 10 - OPT_SUPPORT_NEWER_FIRMWARE = 11 - OPT_DEBUGGING_TITLE = 12 - + ] + opts = None TIMESTAMP_STRING = "%Y-%m-%dT%H:%M:%SZ" @@ -1415,6 +1431,15 @@ class KOBOTOUCH(KOBO): # ' - N3_FULL.parsed':[(600,800),0, 99,], # Used for screensaver if "Full screen" is checked. # } + def __init__(self, *args, **kwargs): + KOBO.__init__(self, *args, **kwargs) + self.plugboards = self.plugboard_func = None + self._prefs = None +# self.device_defaults = DeviceDefaults() + self.current_device_defaults = {} + self.device_defaults_key = self.name + + def initialize(self): super(KOBOTOUCH, self).initialize() self.bookshelvelist = [] @@ -1426,6 +1451,7 @@ class KOBOTOUCH(KOBO): def books(self, oncard=None, end_session=True): debug_print("KoboTouch:books - oncard='%s'"%oncard) from calibre.ebooks.metadata.meta import path_to_ext + self.debugging_title = self.get_debugging_title() dummy_bl = self.booklist_class(None, None, None) @@ -1467,11 +1493,11 @@ class KOBOTOUCH(KOBO): opts = self.settings() debug_print("KoboTouch:books - opts.extra_customization=", opts.extra_customization) + debug_print("KoboTouch:books - driver options=", self) debug_print("KoboTouch:books - prefs['manage_device_metadata']=", prefs['manage_device_metadata']) - if opts.extra_customization: - debugging_title = opts.extra_customization[self.OPT_DEBUGGING_TITLE] - debug_print("KoboTouch:books - set_debugging_title to '%s'" % debugging_title) - bl.set_debugging_title(debugging_title) + debugging_title = self.debugging_title + debug_print("KoboTouch:books - set_debugging_title to '%s'" % debugging_title) + bl.set_debugging_title(debugging_title) debug_print("KoboTouch:books - length bl=%d"%len(bl)) need_sync = self.parse_metadata_cache(bl, prefix, self.METADATA_CACHE) debug_print("KoboTouch:books - length bl after sync=%d"%len(bl)) @@ -1658,7 +1684,7 @@ class KOBOTOUCH(KOBO): def get_bookshelvesforbook(connection, ContentID): # debug_print("KoboTouch:get_bookshelvesforbook - " + ContentID) bookshelves = [] - if not self.supports_bookshelves(): + if not self.supports_bookshelves: return bookshelves cursor = connection.cursor() @@ -1717,32 +1743,30 @@ class KOBOTOUCH(KOBO): " %(previews)s %(recomendations)s )" " and not ((___ExpirationStatus=3 or ___ExpirationStatus is Null) and ContentType = 6)") % \ dict( - expiry="" if opts.extra_customization[self.OPT_SHOW_ARCHIVED_BOOK_RECORDS] else "and IsDownloaded in ('true', 1)", - previews=" or (Accessibility in (6) and ___UserID <> '')" if opts.extra_customization[self.OPT_SHOW_PREVIEWS] else "", - recomendations=" or (Accessibility in (-1, 4, 6) and ___UserId = '')" if opts.extra_customization[ - self.OPT_SHOW_RECOMMENDATIONS] else "" + expiry="" if self.show_archived_books else "and IsDownloaded in ('true', 1)", + previews=" or (Accessibility in (6) and ___UserID <> '')" if self.show_previews else "", + recomendations=" or (Accessibility in (-1, 4, 6) and ___UserId = '')" if self.show_recommendations else "" ) elif self.supports_series(): where_clause = (" where BookID is Null " " and ((Accessibility = -1 and IsDownloaded in ('true', 1)) or (Accessibility in (1,2)) %(previews)s %(recomendations)s )" " and not ((___ExpirationStatus=3 or ___ExpirationStatus is Null) %(expiry)s)") % \ dict( - expiry=" and ContentType = 6" if opts.extra_customization[self.OPT_SHOW_ARCHIVED_BOOK_RECORDS] else "", - previews=" or (Accessibility in (6) and ___UserID <> '')" if opts.extra_customization[self.OPT_SHOW_PREVIEWS] else "", - recomendations=" or (Accessibility in (-1, 4, 6) and ___UserId = '')" if opts.extra_customization[ - self.OPT_SHOW_RECOMMENDATIONS] else "" + expiry=" and ContentType = 6" if self.show_archived_books else "", + previews=" or (Accessibility in (6) and ___UserID <> '')" if self.show_previews else "", + recomendations=" or (Accessibility in (-1, 4, 6) and ___UserId = '')" if self.show_recommendations else "" ) elif self.dbversion >= 33: where_clause = (' where BookID is Null %(previews)s %(recomendations)s and not ((___ExpirationStatus=3 or ___ExpirationStatus is Null) %(expiry)s)') % \ dict( - expiry=' and ContentType = 6' if opts.extra_customization[self.OPT_SHOW_ARCHIVED_BOOK_RECORDS] else '', - previews=' and Accessibility <> 6' if opts.extra_customization[self.OPT_SHOW_PREVIEWS] == False else '', - recomendations=' and IsDownloaded in (\'true\', 1)' if opts.extra_customization[self.OPT_SHOW_RECOMMENDATIONS] == False else '' + expiry=' and ContentType = 6' if self.show_archived_books else '', + previews=' and Accessibility <> 6' if not self.show_previews else '', + recomendations=' and IsDownloaded in (\'true\', 1)' if not self.show_recommendations else '' ) elif self.dbversion >= 16: where_clause = (' where BookID is Null ' 'and not ((___ExpirationStatus=3 or ___ExpirationStatus is Null) %(expiry)s)') % \ - dict(expiry=' and ContentType = 6' if opts.extra_customization[self.OPT_SHOW_ARCHIVED_BOOK_RECORDS] else '') + dict(expiry=' and ContentType = 6' if self.show_archived_books else '') else: where_clause = ' where BookID is Null' @@ -1943,7 +1967,7 @@ class KOBOTOUCH(KOBO): self.set_filesize_in_device_database(connection, contentID, fname) - if not self.copying_covers(): + if not self.upload_covers: imageID = self.imageid_from_contentid(contentID) self.delete_images(imageID, fname) connection.commit() @@ -2148,7 +2172,6 @@ class KOBOTOUCH(KOBO): "Closed": 3, "Shortlist": 4, "Archived": 5, - # "Preview":99, # Unsupported as we don't want to change it } # Define lists for the ReadStatus @@ -2163,33 +2186,16 @@ class KOBOTOUCH(KOBO): "Recommendation":4, "Deleted":1, } - - # specialshelveslist = { - # "Shortlist":1, - # "Wishlist":2, - # } # debug_print('KoboTouch:update_device_database_collections - collections_attributes=', collections_attributes) - opts = self.settings() - if opts.extra_customization: - create_bookshelves = opts.extra_customization[self.OPT_CREATE_BOOKSHELVES] and self.supports_bookshelves() - delete_empty_shelves = opts.extra_customization[self.OPT_DELETE_BOOKSHELVES] and self.supports_bookshelves() - update_series_details = opts.extra_customization[self.OPT_UPDATE_SERIES_DETAILS] and self.supports_series() - debugging_title = opts.extra_customization[self.OPT_DEBUGGING_TITLE] - debug_print("KoboTouch:update_device_database_collections - set_debugging_title to '%s'" % debugging_title) - booklists.set_debugging_title(debugging_title) - else: - delete_empty_shelves = False - create_bookshelves = False - update_series_details = False + create_bookshelves = self.create_bookshelves + delete_empty_shelves = self.delete_empty_shelves + update_series_details = self.update_series_details + debugging_title = self.get_debugging_title() + debug_print("KoboTouch:update_device_database_collections - set_debugging_title to '%s'" % debugging_title) + booklists.set_debugging_title(debugging_title) - opts = self.settings() - if opts.extra_customization: - create_bookshelves = opts.extra_customization[self.OPT_CREATE_BOOKSHELVES] and self.supports_bookshelves() - delete_empty_shelves = opts.extra_customization[self.OPT_DELETE_BOOKSHELVES] and self.supports_bookshelves() - else: - delete_empty_shelves = False - bookshelf_attribute = len(collections_attributes) + bookshelf_attribute = len(collections_attributes) > 0 collections = booklists.get_collections(collections_attributes) if bookshelf_attribute else None # debug_print('KoboTouch:update_device_database_collections - Collections:', collections) @@ -2200,13 +2206,12 @@ class KOBOTOUCH(KOBO): # and the removal of the last book would not occur import sqlite3 as sqlite - with closing(sqlite.connect(self.normalize_path(self._main_prefix + - '.kobo/KoboReader.sqlite'))) as connection: + with closing(sqlite.connect(self.device_database_path())) as connection: # return bytestrings if the content cannot the decoded as unicode connection.text_factory = lambda x: unicode(x, "utf-8", "ignore") - if collections: + if self.manage_collections and collections: # debug_print("KoboTouch:update_device_database_collections - length collections=" + unicode(len(collections))) # Need to reset the collections outside the particular loops @@ -2248,7 +2253,7 @@ class KOBOTOUCH(KOBO): ContentType = self.get_content_type_from_extension(extension) if extension != '' else self.get_content_type_from_path(book.path) book.contentID = self.contentid_from_path(book.path, ContentType) - if category in self.bookshelvelist and self.supports_bookshelves(): + if category in self.bookshelvelist and self.supports_bookshelves: if show_debug: debug_print(' length book.device_collections=%d'%len(book.device_collections)) if category not in book.device_collections: @@ -2265,7 +2270,7 @@ class KOBOTOUCH(KOBO): if show_debug: debug_print(' Have an older version shortlist - %s'%book.title) # Manage FavouritesIndex/Shortlist - if not self.supports_bookshelves(): + if not self.supports_bookshelves: if show_debug: debug_print(' and about to set it - %s'%book.title) self.set_favouritesindex(connection, book.contentID) @@ -2293,7 +2298,7 @@ class KOBOTOUCH(KOBO): self.reset_favouritesindex(connection, oncard) # Set the series info and cleanup the bookshelves only if the firmware supports them and the user has set the options. - if (self.supports_bookshelves() or self.supports_series()) and (bookshelf_attribute or update_series_details): + if (self.supports_bookshelves and self.manage_collections or self.supports_series()) and (bookshelf_attribute or update_series_details): debug_print("KoboTouch:update_device_database_collections - managing bookshelves and series.") self.series_set = 0 @@ -2306,7 +2311,7 @@ class KOBOTOUCH(KOBO): debug_print("KoboTouch:update_device_database_collections - book.title=%s" % book.title) if update_series_details: self.set_series(connection, book) - if bookshelf_attribute: + if self.manage_collections and bookshelf_attribute: if show_debug: debug_print("KoboTouch:update_device_database_collections - about to remove a book from shelves book.title=%s" % book.title) self.remove_book_from_device_bookshelves(connection, book) @@ -2341,8 +2346,7 @@ class KOBOTOUCH(KOBO): debug_print("KoboTouch:upload_cover - path='%s' filename='%s' "%(path, filename)) debug_print(" filepath='%s' "%(filepath)) - opts = self.settings() - if not self.copying_covers(): + if not self.upload_covers: # Building thumbnails disabled # debug_print('KoboTouch: not uploading cover') return @@ -2351,14 +2355,9 @@ class KOBOTOUCH(KOBO): if self._card_a_prefix and os.path.abspath(path).startswith(os.path.abspath(self._card_a_prefix)) and not self.supports_covers_on_sdcard(): return - if not opts.extra_customization[self.OPT_UPLOAD_GRAYSCALE_COVERS]: - uploadgrayscale = False - else: - uploadgrayscale = True - # debug_print('KoboTouch: uploading cover') try: - self._upload_cover(path, filename, metadata, filepath, uploadgrayscale, self.keep_cover_aspect()) + self._upload_cover(path, filename, metadata, filepath, self.upload_grayscale, self.keep_cover_aspect) except Exception as e: debug_print('KoboTouch: FAILED to upload cover=%s Exception=%s'%(filepath, str(e))) @@ -2589,7 +2588,7 @@ class KOBOTOUCH(KOBO): # debug_print('KoboTouch:get_bookshelflist') bookshelves = [] - if not self.supports_bookshelves(): + if not self.supports_bookshelves: return bookshelves query = 'SELECT Name FROM Shelf WHERE _IsDeleted = "false"' @@ -2691,7 +2690,7 @@ class KOBOTOUCH(KOBO): def remove_from_bookshelves(self, connection, oncard, ContentID=None, bookshelves=None): debug_print('KoboTouch:remove_from_bookshelf ContentID=', ContentID) - if not self.supports_bookshelves(): + if not self.supports_bookshelves: return query = 'DELETE FROM ShelfContent' @@ -2762,10 +2761,71 @@ class KOBOTOUCH(KOBO): if show_debug: debug_print("KoboTouch:set_series - end") + def config_widget(self): + # TODO: Cleanup the following + self.current_friendly_name = self.gui_name + + from calibre.devices.kobo.kobotouch_config import KOBOTOUCHConfig + return KOBOTOUCHConfig(self.settings(), self.FORMATS, + self.SUPPORTS_SUB_DIRS, self.MUST_READ_METADATA, + self.SUPPORTS_USE_AUTHOR_SORT, self.EXTRA_CUSTOMIZATION_MESSAGE, + self, extra_customization_choices=self.EXTRA_CUSTOMIZATION_CHOICES + ) + + @classmethod + def get_pref(cls, key): + ''' Get the setting named key. First looks for a device specific setting. + If that is not found looks for a device default and if that is not + found uses the global default.''' +# debug_print("KoboTouch::get_prefs - key=", key, "cls=", cls) + opts = cls.settings() + try: + return getattr(opts, key) + except: + debug_print("KoboTouch::get_prefs - probably an extra_customization") + return None + + @classmethod + def save_settings(cls, config_widget): + config_widget.commit() + + + @classmethod + def save_template(cls): + return cls.settings().save_template + + @classmethod + def _config(cls): + c = super(KOBOTOUCH, cls)._config() + + c.add_opt('manage_collections', default=True) + c.add_opt('collections_columns', default='') + c.add_opt('create_collections', default=False) + c.add_opt('delete_empty_collections', default=False) + + c.add_opt('upload_covers', default=False) + c.add_opt('keep_cover_aspect', default=False) + c.add_opt('upload_grayscale', default=False) + + c.add_opt('show_archived_books', default=False) + c.add_opt('show_previews', default=False) + c.add_opt('show_recommendations', default=False) + + c.add_opt('update_series', default=True) + c.add_opt('update_device_metadata', default=True) + + c.add_opt('modify_css', default=False) + + c.add_opt('support_newer_firmware', default=False) + c.add_opt('debugging_title', default='') + + return c + + @classmethod def settings(cls): opts = cls._config().parse() - if isinstance(cls.EXTRA_CUSTOMIZATION_DEFAULT, list): + if isinstance(cls.EXTRA_CUSTOMIZATION_DEFAULT, list) and len(cls.EXTRA_CUSTOMIZATION_DEFAULT) > 0: if opts.extra_customization is None: opts.extra_customization = [] if not isinstance(opts.extra_customization, list): @@ -2782,6 +2842,10 @@ class KOBOTOUCH(KOBO): else: extra_customization.append(opts.extra_customization[i - extra_options_offset]) opts.extra_customization = extra_customization + if opts.extra_customization: + opts = cls.migrate_old_settings(opts) + + cls.opts = opts return opts def isAura(self): @@ -2827,24 +2891,73 @@ class KOBOTOUCH(KOBO): self.__class__.gui_name = device_name return device_name - def copying_covers(self): - opts = self.settings() - return opts.extra_customization[self.OPT_UPLOAD_COVERS] or opts.extra_customization[self.OPT_KEEP_COVER_ASPECT_RATIO] + @property + def manage_collections(self): + return self.get_pref('manage_collections') + @property + def create_collections(self): + return self.get_pref('create_collections') + @property + def collections_columns(self): + return self.get_pref('collections_columns') + @property + def delete_empty_collections(self): + return self.get_pref('delete_empty_collections') + @property + def upload_covers(self): + return self.get_pref('upload_covers') + @property def keep_cover_aspect(self): - opts = self.settings() - return opts.extra_customization[self.OPT_KEEP_COVER_ASPECT_RATIO] + return self.upload_covers and self.get_pref('keep_cover_aspect') + @property + def upload_grayscale(self): + return self.upload_covers and self.get_pref('upload_grayscale') def modifying_epub(self): return self.modifying_css() def modifying_css(self): - opts = self.settings() - return opts.extra_customization[self.OPT_MODIFY_CSS] + return self.get_pref('modify_css') + @property + def create_bookshelves(self): + return self.get_pref('create_collections') and self.supports_bookshelves + @property + def delete_empty_shelves(self): + return self.get_pref('delete_empty_collections') and self.supports_bookshelves + @property + def update_device_metadata(self): + return self.get_pref('update_device_metadata') + @property + def update_series_details(self): + return self.update_device_metadata and self.get_pref('update_series') and self.supports_series() + + @classmethod + def get_debugging_title(cls): + debugging_title = cls.get_pref('debugging_title') + if not debugging_title: # Make sure the value is set to prevent rereading the settings. + debugging_title = '' + return debugging_title + + @property def supports_bookshelves(self): return self.dbversion >= self.min_supported_dbversion + @property + def show_archived_books(self): + return self.get_pref('show_archived_books') + @property + def show_previews(self): + return self.get_pref('show_previews') + @property + def show_recommendations(self): + return self.get_pref('show_recommendations') + + @property + def read_metadata(self): + return self.get_pref('read_metadata') + def supports_series(self): return self.dbversion >= self.min_dbversion_series @@ -2869,8 +2982,7 @@ class KOBOTOUCH(KOBO): # debug_print("KoboTouch:modify_database_check - self.fwversion > self.max_supported_fwversion=", self.fwversion > self.max_supported_fwversion) if self.dbversion > self.supported_dbversion or self.fwversion > self.max_supported_fwversion: # Unsupported database - opts = self.settings() - if not opts.extra_customization[self.OPT_SUPPORT_NEWER_FIRMWARE]: + if not self.get_pref('support_newer_firmware'): debug_print('The database has been upgraded past supported version') self.report_progress(1.0, _('Removing books from device...')) from calibre.devices.errors import UserFeedback @@ -2899,23 +3011,79 @@ class KOBOTOUCH(KOBO): else: # Supported database version return True - + @classmethod - def is_debugging_title(cls, title): + def migrate_old_settings(cls, settings): + debug_print("KoboTouch::migrate_old_settings - start") + + count_options = 0 + OPT_COLLECTIONS = count_options + count_options += 1 + OPT_CREATE_BOOKSHELVES = count_options + count_options += 1 + OPT_DELETE_BOOKSHELVES = count_options + count_options += 1 + OPT_UPLOAD_COVERS = count_options + count_options += 1 + OPT_UPLOAD_GRAYSCALE_COVERS = count_options + count_options += 1 + OPT_KEEP_COVER_ASPECT_RATIO = count_options + count_options += 1 + OPT_SHOW_ARCHIVED_BOOK_RECORDS = count_options + count_options += 1 + OPT_SHOW_PREVIEWS = count_options + count_options += 1 + OPT_SHOW_RECOMMENDATIONS = count_options + count_options += 1 + OPT_UPDATE_SERIES_DETAILS = count_options + count_options += 1 + OPT_MODIFY_CSS = count_options + count_options += 1 + OPT_SUPPORT_NEWER_FIRMWARE = count_options + count_options += 1 + OPT_DEBUGGING_TITLE = count_options + + if len(settings.extra_customization) >= count_options: + debug_print("KoboTouch::migrate_old_settings - settings need to be migrated") + settings.manage_collections = True + settings.collections_columns = settings.extra_customization[OPT_COLLECTIONS] + debug_print("KoboTouch::migrate_old_settings - settings.collections_columns=", settings.collections_columns) + settings.create_collections = settings.extra_customization[OPT_CREATE_BOOKSHELVES] + settings.delete_empty_collections = settings.extra_customization[OPT_DELETE_BOOKSHELVES] + + settings.upload_covers = settings.extra_customization[OPT_UPLOAD_COVERS] + settings.keep_cover_aspect = settings.extra_customization[OPT_KEEP_COVER_ASPECT_RATIO] + settings.upload_grayscale = settings.extra_customization[OPT_UPLOAD_GRAYSCALE_COVERS] + + settings.show_archived_books = settings.extra_customization[OPT_SHOW_ARCHIVED_BOOK_RECORDS] + settings.show_previews = settings.extra_customization[OPT_SHOW_PREVIEWS] + settings.show_recommendations = settings.extra_customization[OPT_SHOW_RECOMMENDATIONS] + + settings.update_series = settings.extra_customization[OPT_UPDATE_SERIES_DETAILS] + settings.update_metadata = settings.update_series + + settings.modify_css = settings.extra_customization[OPT_MODIFY_CSS] + + settings.support_newer_firmware = settings.extra_customization[OPT_SUPPORT_NEWER_FIRMWARE] + settings.debugging_title = settings.extra_customization[OPT_DEBUGGING_TITLE] + settings.extra_customization = settings.extra_customization[count_options + 1:] + + return settings + + + def is_debugging_title(self, title): if not DEBUG: return False -# debug_print("KoboTouch:is_debugging - title=", title) - is_debugging = False - opts = cls.settings() +# debug_print("KoboTouch:is_debugging - title=", title) - if opts.extra_customization: - debugging_title = opts.extra_customization[cls.OPT_DEBUGGING_TITLE] - is_debugging = len(debugging_title) > 0 and title.lower().find(debugging_title.lower()) >= 0 or len(title) == 0 + if not self.debugging_title and not self.debugging_title == '': + self.debugging_title = self.get_debugging_title() + is_debugging = len(self.debugging_title) > 0 and title.lower().find(self.debugging_title.lower()) >= 0 or len(title) == 0 return is_debugging def dump_bookshelves(self, connection): - if not (DEBUG and self.supports_bookshelves() and False): + if not (DEBUG and self.supports_bookshelves and False): return debug_print('KoboTouch:dump_bookshelves - start') @@ -2952,3 +3120,29 @@ class KOBOTOUCH(KOBO): cursor.close() debug_print('KoboTouch:dump_bookshelves - end') + def __str__(self, *args, **kwargs): + options = ', '.join(['%s: %s' % (x.name, self.get_pref(x.name)) for x in self._config().preferences]) + return u"Driver:%s, Options - %s" % (self.name, options) + +if __name__ == '__main__': + dev = KOBOTOUCH(None) + dev.startup() + try: + dev.initialize() + from calibre.devices.scanner import DeviceScanner + scanner = DeviceScanner() + scanner.scan() + devs = scanner.devices +# debug_print("unit test: devs.__class__=", devs.__class__) +# debug_print("unit test: devs.__class__=", devs.__class__.__name__) + debug_print("unit test: devs=", devs) + debug_print("unit test: dev=", dev) + # cd = dev.detect_managed_devices(devs) + # if cd is None: + # raise ValueError('Failed to detect KOBOTOUCH device') + dev.set_progress_reporter(prints) +# dev.open(cd, None) +# dev.filesystem_cache.dump() + print ('Prefix for main memory:', dev.dbversion) + finally: + dev.shutdown() From adc9535714843aae1eaaa992ac29b9d8ca2bc54d Mon Sep 17 00:00:00 2001 From: David Date: Mon, 11 Apr 2016 20:36:34 +1000 Subject: [PATCH 2/8] New tabbed configuration for KoboTouch driver. With the new files. --- src/calibre/devices/kobo/kobotouch_config.py | 486 ++++++++++++++++++ .../device_drivers/tabbed_device_config.py | 408 +++++++++++++++ 2 files changed, 894 insertions(+) create mode 100644 src/calibre/devices/kobo/kobotouch_config.py create mode 100644 src/calibre/gui2/device_drivers/tabbed_device_config.py diff --git a/src/calibre/devices/kobo/kobotouch_config.py b/src/calibre/devices/kobo/kobotouch_config.py new file mode 100644 index 0000000000..e413eabc84 --- /dev/null +++ b/src/calibre/devices/kobo/kobotouch_config.py @@ -0,0 +1,486 @@ +#!/usr/bin/env python2 +# 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__ = '2015, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +import textwrap + +from PyQt5.Qt import (QLabel, QGridLayout, QLineEdit, QVBoxLayout, + QDialog, QDialogButtonBox, QCheckBox) + +from calibre.gui2.device_drivers.tabbed_device_config import TabbedDeviceConfig, DeviceConfigTab, DeviceOptionsGroupBox +from calibre.devices.usbms.driver import debug_print + +def wrap_msg(msg): + return textwrap.fill(msg.strip(), 100) + +def setToolTipFor(widget, tt): + widget.setToolTip(wrap_msg(tt)) + +def create_checkbox(title, tt, state): + cb = QCheckBox(title) + cb.setToolTip(wrap_msg(tt)) + cb.setChecked(bool(state)) + return cb + + +class KOBOTOUCHConfig(TabbedDeviceConfig): + + def __init__(self, device_settings, all_formats, supports_subdirs, + must_read_metadata, supports_use_author_sort, + extra_customization_message, device, extra_customization_choices=None, parent=None): + + super(KOBOTOUCHConfig, self).__init__(device_settings, all_formats, supports_subdirs, + must_read_metadata, supports_use_author_sort, + extra_customization_message, device, extra_customization_choices, parent) + + self.device_settings = device_settings + self.all_formats = all_formats + self.supports_subdirs = supports_subdirs + self.must_read_metadata = must_read_metadata + self.supports_use_author_sort = supports_use_author_sort + self.extra_customization_message = extra_customization_message + self.extra_customization_choices = extra_customization_choices + + self.current_device_key = device.device_defaults_key + + self.tab1 = Tab1Config(self, self.device) + self.tab2 = Tab2Config(self, self.device) + + extra_tab_pos = self.indexOf(self.extra_tab) + last_tab_pos = self.insertTab(extra_tab_pos, self.tab1, _("Collections, Covers && Uploads")) + last_tab_pos = self.insertTab(last_tab_pos + 1, self.tab2, _('Metadata && Advanced')) + + + def get_pref(self, key): + return self.device.get_pref(key) + + @property + def device(self): + return self._device() + + def validate(self): + if hasattr(self, 'formats'): + if not self.formats.validate(): + return False + if not self.template.validate(): + return False + return True + + @property + def book_uploads_options(self): + return self.tab1.book_uploads_options + + @property + def collections_options(self): + return self.tab1.collections_options + + @property + def cover_options(self): + return self.tab1.covers_options + + @property + def device_list_options(self): + return self.tab2.device_list_options + + @property + def advanced_options(self): + return self.tab2.advanced_options + + @property + def metadata_options(self): + return self.tab2.metadata_options + + def commit(self): + debug_print("KOBOTOUCHConfig::commit: start") + p = super(KOBOTOUCHConfig, self).commit() + + p['manage_collections'] = self.manage_collections + p['create_collections'] = self.create_collections + p['collections_columns'] = self.collections_columns + p['delete_empty_collections'] = self.delete_empty_collections + + p['upload_covers'] = self.upload_covers + p['keep_cover_aspect'] = self.keep_cover_aspect + p['upload_grayscale'] = self.upload_grayscale + + p['show_recommendations'] = self.show_recommendations + p['show_previews'] = self.show_previews + p['show_archived_books'] = self.show_archived_books + + p['update_series'] = self.update_series + p['modify_css'] = self.modify_css + + p['support_newer_firmware'] = self.support_newer_firmware + p['debugging_title'] = self.debugging_title + + p['extra_customization'] = self.extra_tab.extra_customization() + + return p + + +class Tab1Config(DeviceConfigTab): # {{{ + + def __init__(self, parent, device): + super(Tab1Config, self).__init__(parent) + + self.l = QVBoxLayout(self) + self.setLayout(self.l) + + self.collections_options = CollectionsGroupBox(self, device) + self.l.addWidget(self.collections_options) + self.add_widget(self.collections_options) + + self.covers_options = CoversGroupBox(self, device) + self.l.addWidget(self.covers_options) + self.add_widget(self.covers_options) + + self.book_uploads_options = BookUploadsGroupBox(self, device) + self.l.addWidget(self.book_uploads_options) + self.add_widget(self.book_uploads_options) +# }}} + +class Tab2Config(DeviceConfigTab): # {{{ + + def __init__(self, parent, device): + super(Tab2Config, self).__init__(parent) + + self.l = QVBoxLayout(self) + self.setLayout(self.l) + + self.metadata_options = MetadataGroupBox(self, device) + self.l.addWidget(self.metadata_options) + self.add_widget(self.metadata_options) + + self.device_list_options = DeviceListGroupBox(self, device) + self.l.addWidget(self.device_list_options) + self.add_widget(self.device_list_options) + + self.advanced_options = AdvancedGroupBox(self, device) + self.l.addWidget(self.advanced_options) + self.add_widget(self.advanced_options) +# }}} + + +class BookUploadsGroupBox(DeviceOptionsGroupBox): + + def __init__(self, parent, device): + super(BookUploadsGroupBox, self).__init__(parent, device) + self.setTitle(_("Book Uploading")) + + self.options_layout = QGridLayout() + self.options_layout.setObjectName("options_layout") + self.setLayout(self.options_layout) + + self.modify_css_checkbox = create_checkbox( + _("Modify CSS"), + _('This allows addition of user CSS rules and removal of some CSS. ' + 'When sending a book, the driver adds the contents of {0} to all stylesheets in the ePub. ' + 'This file is searched for in the root directory of the main memory of the device. ' + 'As well as this, if the file contains settings for the "orphans" or "widows", ' + 'these are removed for all styles in the original stylesheet.').format(device.KOBO_EXTRA_CSSFILE), + device.get_pref('modify_css') + ) + + self.options_layout.addWidget(self.modify_css_checkbox, 0, 0, 1, 2) + self.options_layout.setRowStretch(1, 1) + + @property + def modify_css(self): + return self.modify_css_checkbox.isChecked() + + +class CollectionsGroupBox(DeviceOptionsGroupBox): + + def __init__(self, parent, device): + super(CollectionsGroupBox, self).__init__(parent, device) + self.setTitle(_("Collections")) + + self.options_layout = QGridLayout() + self.options_layout.setObjectName("options_layout") + self.setLayout(self.options_layout) + + self.manage_collections_checkbox = create_checkbox( + _("Manage Collections"), + _('Create new bookshelves on the Kobo if they do not exist. This is only for firmware V2.0.0 or later.'), + device.get_pref('manage_collections') + ) + self.manage_collections_checkbox.clicked.connect(self.manage_collections_checkbox_clicked) + + self.collections_columns_label = QLabel(_('Collections Columns')) + self.collections_columns_edit = QLineEdit(self) + self.collections_columns_edit.setToolTip(_('The Kobo from firmware V2.0.0 supports bookshelves.' + ' These are created on the Kobo. ' + + 'Specify a tags type column for automatic management.')) + debug_print("CollectionsGroupBox::__init__ - device.settings()=", device.settings()) + self.collections_columns_edit.setText(device.get_pref('collections_columns')) + + self.create_collections_checkbox = create_checkbox( + _("Create Collections"), + _('Create new bookshelves on the Kobo if they do not exist. This is only for firmware V2.0.0 or later.'), + device.get_pref('create_collections') + ) + self.delete_empty_collections_checkbox = create_checkbox( + _('Delete Empty Bookshelves'), + _('Delete any empty bookshelves from the Kobo when syncing is finished. This is only for firmware V2.0.0 or later.'), + device.get_pref('delete_empty_collections') + ) + + self.options_layout.addWidget(self.manage_collections_checkbox, 0, 0, 1, 1) + self.options_layout.addWidget(self.collections_columns_label, 1, 0, 1, 1) + self.options_layout.addWidget(self.collections_columns_edit, 1, 1, 1, 1) + self.options_layout.addWidget(self.create_collections_checkbox, 2, 0, 1, 1) + self.options_layout.addWidget(self.delete_empty_collections_checkbox, 2, 1, 1, 1) + self.options_layout.setRowStretch(5, 1) + + self.manage_collections_checkbox_clicked(self.manage_collections) + + @property + def manage_collections(self): + return self.manage_collections_checkbox.isChecked() + + @property + def collections_columns(self): + return self.collections_columns_edit.text().strip() + + @property + def create_collections(self): + return self.create_collections_checkbox.isChecked() + + @property + def delete_empty_collections(self): + return self.delete_empty_collections_checkbox.isChecked() + + def manage_collections_checkbox_clicked(self, checked): + self.collections_columns_label.setEnabled(checked) + self.collections_columns_edit.setEnabled(checked) + self.create_collections_checkbox.setEnabled(checked) + self.delete_empty_collections_checkbox.setEnabled(checked) + + +class CoversGroupBox(DeviceOptionsGroupBox): + + def __init__(self, parent, device): + super(CoversGroupBox, self).__init__(parent, device) + self.setTitle(_("Covers")) + + self.options_layout = QGridLayout() + self.options_layout.setObjectName("options_layout") + self.setLayout(self.options_layout) + + self.upload_covers_checkbox = create_checkbox( + _("Upload covers for books"), + _('Upload cover images from the calibre library when sending books to the device.'), + device.get_pref('upload_covers') + ) + self.upload_covers_checkbox.clicked.connect(self.upload_covers_checkbox_clicked) + + self.upload_grayscale_checkbox = create_checkbox( + _('Upload Black and White Covers'), + _('Convert covers to Black and White when uploading'), + device.get_pref('upload_grayscale') + ) + + self.keep_cover_aspect_checkbox = create_checkbox( + _('Keep cover aspect ratio'), + _('When uploading covers, do not change the aspect ratio when resizing for the device.' + ' This is for firmware versions 2.3.1 and later.'), + device.get_pref('keep_cover_aspect')) + + self.options_layout.addWidget(self.upload_covers_checkbox, 0, 0, 1, 2) + self.options_layout.addWidget(self.keep_cover_aspect_checkbox, 1, 0, 1, 1) + self.options_layout.addWidget(self.upload_grayscale_checkbox, 1, 1, 1, 1) + self.options_layout.setRowStretch(2, 1) + + self.upload_covers_checkbox_clicked(self.upload_covers) + + @property + def upload_covers(self): + return self.upload_covers_checkbox.isChecked() + + @property + def upload_grayscale(self): + return self.upload_grayscale_checkbox.isChecked() + + @property + def keep_cover_aspect(self): + return self.keep_cover_aspect_checkbox.isChecked() + + def upload_covers_checkbox_clicked(self, checked): + self.upload_grayscale_checkbox.setEnabled(checked) + self.keep_cover_aspect_checkbox.setEnabled(checked) + + +class DeviceListGroupBox(DeviceOptionsGroupBox): + + def __init__(self, parent, device): + super(DeviceListGroupBox, self).__init__(parent, device) + self.setTitle(_("Show as on device")) + + self.options_layout = QGridLayout() + self.options_layout.setObjectName("options_layout") + self.setLayout(self.options_layout) + + self.show_recommendations_checkbox = create_checkbox( + _("Show Recommendations"), + _('Kobo shows recommendations on the device. In some cases these have ' + 'files but in other cases they are just pointers to the web site to buy. ' + 'Enable if you wish to see/delete them.'), + device.get_pref('show_recommendations') + ) + + self.show_archived_books_checkbox = create_checkbox( + _("Show archived books"), + _('Archived books are listed on the device but need to be downloaded to read.' + ' Use this option to show these books and match them with books in the calibre library.'), + device.get_pref('show_archived_books') + ) + + self.show_previews_checkbox = create_checkbox( + _('Show Previews'), + _('Kobo previews are included on the Touch and some other versions' + ' by default they are no longer displayed as there is no good reason to ' + 'see them. Enable if you wish to see/delete them.'), + device.get_pref('show_previews') + ) + + self.options_layout.addWidget(self.show_recommendations_checkbox, 0, 0, 1, 1) + self.options_layout.addWidget(self.show_archived_books_checkbox, 0, 1, 1, 1) + self.options_layout.addWidget(self.show_previews_checkbox, 1, 0, 1, 1) + self.options_layout.setRowStretch(1, 1) + + @property + def show_recommendations(self): + return self.show_recommendations_checkbox.isChecked() + + @property + def show_archived_books(self): + return self.show_archived_books_checkbox.isChecked() + + @property + def show_previews(self): + return self.show_previews_checkbox.isChecked() + + +class AdvancedGroupBox(DeviceOptionsGroupBox): + + def __init__(self, parent, device): + super(AdvancedGroupBox, self).__init__(parent, device, _("Advanced Options")) +# self.setTitle(_("Advanced Options")) + + self.options_layout = QGridLayout() + self.options_layout.setObjectName("options_layout") + self.setLayout(self.options_layout) + + self.support_newer_firmware_checkbox = create_checkbox( + _("Attempt to support newer firmware"), + _('Kobo routinely updates the firmware and the ' + 'database version. With this option Calibre will attempt ' + 'to perform full read-write functionality - Here be Dragons!! ' + 'Enable only if you are comfortable with restoring your kobo ' + 'to factory defaults and testing software. ' + 'This driver supports firmware V2.x.x and DBVersion up to ') + unicode(device.supported_dbversion), + device.get_pref('support_newer_firmware') + ) + + self.debugging_title_checkbox = create_checkbox( + _("Title to test when debugging"), + _('Part of title of a book that can be used when doing some tests for debugging. ' + 'The test is to see if the string is contained in the title of a book. ' + 'The better the match, the less extraneous output.'), + device.get_pref('debugging_title') + ) + self.debugging_title_label = QLabel(_('Title to test when debugging')) + self.debugging_title_edit = QLineEdit(self) + self.debugging_title_edit.setToolTip(_('Part of title of a book that can be used when doing some tests for debugging. ' + 'The test is to see if the string is contained in the title of a book. ' + 'The better the match, the less extraneous output.')) + self.debugging_title_edit.setText(device.get_pref('debugging_title')) + self.debugging_title_label.setBuddy(self.debugging_title_edit) + + self.options_layout.addWidget(self.support_newer_firmware_checkbox, 0, 0, 1, 2) + self.options_layout.addWidget(self.debugging_title_label, 1, 0, 1, 1) + self.options_layout.addWidget(self.debugging_title_edit, 1, 1, 1, 1) + self.options_layout.setRowStretch(1, 2) + + @property + def support_newer_firmware(self): + return self.support_newer_firmware_checkbox.isChecked() + + @property + def debugging_title(self): + return self.debugging_title_edit.text().strip() + + +class MetadataGroupBox(DeviceOptionsGroupBox): + + def __init__(self, parent, device): + super(MetadataGroupBox, self).__init__(parent, device) + self.setTitle(_("Metadata Options")) + + self.options_layout = QGridLayout() + self.options_layout.setObjectName("options_layout") + self.setLayout(self.options_layout) + + self.update_device_metadata_checkbox = create_checkbox( + _("Update metadata on the device"), + _('Update the metadata on the device when it is connected. ' + 'Be careful when doing this as it will take time and could make the initial connection take a long time.'), + device.get_pref('update_device_metadata') + ) + self.options_layout.addWidget(self.update_device_metadata_checkbox, 0, 0, 1, 2) + self.update_device_metadata_checkbox.clicked.connect(self.update_device_metadata_checkbox_clicked) + + self.update_series_checkbox = create_checkbox( + _("Set Series information"), + _('The book lists on the Kobo devices can display series information. ' + 'This is not read by the device from the sideloaded books. ' + 'Series information can only be added to the device after the book has been processed by the device. ' + 'Enable if you wish to set series information.'), + device.get_pref('update_series') + ) + self.options_layout.addWidget(self.update_series_checkbox, 1, 0, 1, 2) + self.options_layout.setRowStretch(1, 1) + + @property + def update_series(self): + return self.update_series_checkbox.isChecked() + + @property + def update_device_metadata(self): + return self.update_series_checkbox.isChecked() + + def update_device_metadata_checkbox_clicked(self, checked): + self.update_series_checkbox.setEnabled(checked) + + +if __name__ == '__main__': + from calibre.gui2 import Application + from calibre.devices.kobo.driver import KOBOTOUCH + from calibre.devices.scanner import DeviceScanner + s = DeviceScanner() + s.scan() + app = Application([]) + debug_print("KOBOTOUCH:", KOBOTOUCH) + dev = KOBOTOUCH(None) +# dev.startup() +# cd = dev.detect_managed_devices(s.devices) +# dev.open(cd, 'test') + cw = dev.config_widget() + d = QDialog() + 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() + + diff --git a/src/calibre/gui2/device_drivers/tabbed_device_config.py b/src/calibre/gui2/device_drivers/tabbed_device_config.py new file mode 100644 index 0000000000..0b20c168d8 --- /dev/null +++ b/src/calibre/gui2/device_drivers/tabbed_device_config.py @@ -0,0 +1,408 @@ +#!/usr/bin/env python2 +# 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__ = '2015, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +import weakref, textwrap + +from PyQt5.Qt import (QWidget, QListWidgetItem, Qt, QToolButton, QLabel, + QTabWidget, QGridLayout, QListWidget, QIcon, QLineEdit, QVBoxLayout, + QPushButton, QGroupBox, QScrollArea, QHBoxLayout, QComboBox, + pyqtSignal, QSizePolicy, QDialog, QDialogButtonBox, QPlainTextEdit, + QApplication, QSize, QCheckBox, QSpacerItem) + +from calibre.ebooks import BOOK_EXTENSIONS +from calibre.gui2.device_drivers.mtp_config import (FormatsConfig, TemplateConfig) +from calibre.devices.usbms.driver import debug_print + +def wrap_msg(msg): + return textwrap.fill(msg.strip(), 100) + +def setToolTipFor(widget, tt): + widget.setToolTip(wrap_msg(tt)) + +def create_checkbox(title, tt, state): + cb = QCheckBox(title) + cb.setToolTip(wrap_msg(tt)) + cb.setChecked(bool(state)) + return cb + + +class TabbedDeviceConfig(QTabWidget): + """ + This is a generic Tabbed Device config widget. It designed for devices with more + complex configuration. But, it is backwards compatible to the standard device + configuration widget. + + The configuration made up of two default tabs plus extra tabs as needed for the + device. The extra tabs are defined as part of the subclass of this widget for + the device. + + The two default tabs are the "File Formats" and "Extra Customization". These + tabs are the same as the two sections of the standard device configuration + widget. The second of these tabs will only be created if the device driver has + extra configuration options. All options on these tabs work the same way as for + the standard device configuration widget. + + When implementing a subclass for a device driver, create tabs, subclassed from + DeviceConfigTab, for each set of options. Within the tabs, group boxes, subclassed + from DeviceOptionsGroupBox, are created to further group the options. The group + boxes can be coded to support any control type and dependencies between them. + """ + def __init__(self, device_settings, all_formats, supports_subdirs, + must_read_metadata, supports_use_author_sort, + extra_customization_message, device, + extra_customization_choices=None, parent=None): + QTabWidget.__init__(self, parent) + self._device = weakref.ref(device) + + self.device_settings = device_settings + self.all_formats = set(all_formats) + self.supports_subdirs = supports_subdirs + self.must_read_metadata = must_read_metadata + self.supports_use_author_sort = supports_use_author_sort + self.extra_customization_message = extra_customization_message + self.extra_customization_choices = extra_customization_choices + + try: + self.device_name = device.get_gui_name() + except TypeError: + self.device_name = getattr(device, 'gui_name', None) or _('Device') + + if device.USER_CAN_ADD_NEW_FORMATS: + self.all_formats = set(self.all_formats) | set(BOOK_EXTENSIONS) + + self.base = QWidget(self) +# self.insertTab(0, self.base, _('Configure %s') % self.device.current_friendly_name) + self.insertTab(0, self.base, _("File Formats")) + l = self.base.l = QGridLayout(self.base) + self.base.setLayout(l) + + self.formats = FormatsConfig(self.all_formats, device_settings.format_map) + if device.HIDE_FORMATS_CONFIG_BOX: + self.formats.hide() + + self.opt_use_subdirs = create_checkbox( + _("Use sub-directories"), + _('Place files in sub-directories if the device supports them'), + device_settings.use_subdirs + ) + self.opt_read_metadata = create_checkbox( + _("Read metadata from files on device"), + _('Read metadata from files on device'), + device_settings.read_metadata + ) + + self.template = TemplateConfig(device_settings.save_template) + self.opt_use_author_sort = create_checkbox( + _("Use author sort for author"), + _("Use author sort for author"), + device_settings.read_metadata + ) + self.opt_use_author_sort.setObjectName("opt_use_author_sort") + self.base.la = la = QLabel(_( + 'Choose the formats to send to the %s')%self.device_name) + la.setWordWrap(True) + + l.addWidget(la, 1, 0, 1, 1) + l.addWidget(self.formats, 2, 0, 1, 1) + l.addWidget(self.opt_read_metadata, 3, 0, 1, 1) + l.addWidget(self.opt_use_subdirs, 4, 0, 1, 1) + l.addWidget(self.opt_use_author_sort, 5, 0, 1, 1) + l.addWidget(self.template, 6, 0, 1, 1) + l.setRowStretch(2, 10) + + if device.HIDE_FORMATS_CONFIG_BOX: + self.formats.hide() + + if supports_subdirs: + self.opt_use_subdirs.setChecked(device_settings.use_subdirs) + else: + self.opt_use_subdirs.hide() + if not must_read_metadata: + self.opt_read_metadata.setChecked(device_settings.read_metadata) + else: + self.opt_read_metadata.hide() + if supports_use_author_sort: + self.opt_use_author_sort.setChecked(device_settings.use_author_sort) + else: + self.opt_use_author_sort.hide() + + + self.extra_tab = ExtraCustomization(self.extra_customization_message, + self.extra_customization_choices, + self.device_settings) + # Only display the extra customization tab if there are options on it. + if self.extra_tab.has_extra_customizations: + self.addTab(self.extra_tab, _('Extra Customization')) + + self.setCurrentIndex(0) + + def __getattr__(self, attr_name): + "If the object doesn't have an attribute, then check each tab." + try: + return super(TabbedDeviceConfig, self).__getattr__(attr_name) + except AttributeError as ae: + for i in range(0, self.count()): + atab = self.widget(i) + try: + return getattr(atab, attr_name) + except AttributeError: + pass + raise ae + + def get_pref(self, key): + debug_print("get_pref - self.device.prefs", self.device.prefs) + p = self.device.prefs.get(self.current_device_key, {}) + if not p: + self.device.prefs[self.current_device_key] = p + debug_print("get_pref - self.device.get_pref(key)", self.device.get_pref(key)) + return self.device.get_pref(key) + + @property + def device(self): + return self._device() + + def format_map(self): +# formats = [unicode(self.columns.item(i).data(Qt.UserRole) or '') for i in range(self.columns.count()) if self.columns.item(i).checkState()==Qt.Checked] + return self.formats.format_map + + def use_subdirs(self): + return self.opt_use_subdirs.isChecked() + + def read_metadata(self): + return self.opt_read_metadata.isChecked() + + def use_author_sort(self): + return self.opt_use_author_sort.isChecked() + + @property + def opt_save_template(self): + # Really shouldn't be accessing the template this way + return self.template.t + + def text(self): + # Really shouldn't be accessing the template this way + return self.template.t.text() + + @property + def opt_extra_customization(self): + return self.extra_tab.opt_extra_customization + + @property + def label(self): + return self.opt_save_template + + def validate(self): + if hasattr(self, 'formats'): + if not self.formats.validate(): + return False + if not self.template.validate(): + return False + return True + + def commit(self): + debug_print("TabbedDeviceConfig::commit: start") + p = self.device._configProxy() + debug_print("commit: starting setting=%s" % (p, )) + + f = self.formats.format_map + debug_print("commit: self.formats.format_map=", self.formats.format_map) + debug_print("commit: self.device.prefs['format_map']=", self.device.prefs['format_map']) + if f and f != self.device.prefs['format_map']: + p['format_map'] = f + + f = self.use_subdirs() + if f != self.get_pref('use_subdirs'): + p['use_subdirs'] = f + + f = self.read_metadata() + if f != self.get_pref('read_metadata'): + p['read_metadata'] = f + + t = self.template.template + if t and t != self.device.prefs['save_template']: + p['save_template'] = t + + p['extra_customization'] = self.extra_tab.extra_customization() + + return p + + +class DeviceConfigTab(QWidget): # {{{ + ''' + This is an abstraction for a tab in the configuration. The main reason for it is to + abstract the properties of the configuration tab. When a property is accessed, it + will iterate over all known widgets looking for the property. + ''' + def __init__(self, parent=None): + QWidget.__init__(self) + self.parent = parent + + self.widgets = [] + + def add_widget(self, widget): + self.widgets.append(widget) + + def __getattr__(self, attr_name): + try: + return super(DeviceConfigTab, self).__getattr__(attr_name) + except AttributeError as ae: + for awidget in self.widgets: + try: + return getattr(awidget, attr_name) + except AttributeError: + pass + raise ae + + + +class ExtraCustomization(DeviceConfigTab): # {{{ + def __init__(self, extra_customization_message, extra_customization_choices, device_settings): + super(ExtraCustomization, self).__init__() + + debug_print("ExtraCustomization.__init__ - extra_customization_message=", extra_customization_message) + debug_print("ExtraCustomization.__init__ - extra_customization_choices=", extra_customization_choices) + debug_print("ExtraCustomization.__init__ - device_settings.extra_customization=", device_settings.extra_customization) + debug_print("ExtraCustomization.__init__ - device_settings=", device_settings) + self.extra_customization_message = extra_customization_message + + self.l = QVBoxLayout(self) + self.setLayout(self.l) + + options_group = QGroupBox(_("Extra driver customization options"), self) + self.l.addWidget(options_group) + self.extra_layout = QGridLayout() + self.extra_layout.setObjectName("extra_layout") + options_group.setLayout(self.extra_layout) + + if extra_customization_message: + extra_customization_choices = extra_customization_choices or {} + def parse_msg(m): + msg, _, tt = m.partition(':::') if m else ('', '', '') + return msg.strip(), textwrap.fill(tt.strip(), 100) + + if isinstance(extra_customization_message, list): + self.opt_extra_customization = [] + if len(extra_customization_message) > 6: + row_func = lambda x, y: ((x/2) * 2) + y + col_func = lambda x: x%2 + else: + row_func = lambda x, y: x*2 + y + col_func = lambda x: 0 + + for i, m in enumerate(extra_customization_message): + label_text, tt = parse_msg(m) + if not label_text: + self.opt_extra_customization.append(None) + continue + if isinstance(device_settings.extra_customization[i], bool): + self.opt_extra_customization.append(QCheckBox(label_text)) + self.opt_extra_customization[-1].setToolTip(tt) + self.opt_extra_customization[i].setChecked(bool(device_settings.extra_customization[i])) + elif i in extra_customization_choices: + cb = QComboBox(self) + self.opt_extra_customization.append(cb) + l = QLabel(label_text) + l.setToolTip(tt), cb.setToolTip(tt), l.setBuddy(cb), cb.setToolTip(tt) + for li in sorted(extra_customization_choices[i]): + self.opt_extra_customization[i].addItem(li) + cb.setCurrentIndex(max(0, cb.findText(device_settings.extra_customization[i]))) + else: + self.opt_extra_customization.append(QLineEdit(self)) + l = QLabel(label_text) + l.setToolTip(tt) + self.opt_extra_customization[i].setToolTip(tt) + l.setBuddy(self.opt_extra_customization[i]) + l.setWordWrap(True) + self.opt_extra_customization[i].setText(device_settings.extra_customization[i]) + self.opt_extra_customization[i].setCursorPosition(0) + self.extra_layout.addWidget(l, row_func(i + 2, 0), col_func(i)) + self.extra_layout.addWidget(self.opt_extra_customization[i], + row_func(i + 2, 1), col_func(i)) + spacerItem1 = QSpacerItem(10, 10, QSizePolicy.Minimum, QSizePolicy.Expanding) + self.extra_layout.addItem(spacerItem1, row_func(i + 2 + 2, 1), 0, 1, 2) + self.extra_layout.setRowStretch(row_func(i + 2 + 2, 1), 2) + else: + self.opt_extra_customization = QLineEdit() + label_text, tt = parse_msg(extra_customization_message) + l = QLabel(label_text) + l.setToolTip(tt) + l.setBuddy(self.opt_extra_customization) + l.setWordWrap(True) + if device_settings.extra_customization: + self.opt_extra_customization.setText(device_settings.extra_customization) + self.opt_extra_customization.setCursorPosition(0) + self.opt_extra_customization.setCursorPosition(0) + self.extra_layout.addWidget(l, 0, 0) + self.extra_layout.addWidget(self.opt_extra_customization, 1, 0) + + def extra_customization(self): + ec = [] + if self.extra_customization_message: + if isinstance(self.extra_customization_message, list): + for i in range(0, len(self.extra_customization_message)): + if self.opt_extra_customization[i] is None: + ec.append(None) + continue + if hasattr(self.opt_extra_customization[i], 'isChecked'): + ec.append(self.opt_extra_customization[i].isChecked()) + elif hasattr(self.opt_extra_customization[i], 'currentText'): + ec.append(unicode(self.opt_extra_customization[i].currentText()).strip()) + else: + ec.append(unicode(self.opt_extra_customization[i].text()).strip()) + else: + ec = unicode(self.opt_extra_customization.text()).strip() + if not ec: + ec = None + + return ec + + @property + def has_extra_customizations(self): + debug_print("ExtraCustomization::has_extra_customizations - self.extra_customization_message", self.extra_customization_message) + return self.extra_customization_message and len(self.extra_customization_message) > 0 + +# }}} + +class DeviceOptionsGroupBox(QGroupBox): + """ + This is a container for the individual options for a device driver. + """ + def __init__(self, parent, device=None, title=_("Unknown")): + QGroupBox.__init__(self, parent) + + self.device = device + self.setTitle(title) + + +if __name__ == '__main__': + from calibre.gui2 import Application + from calibre.devices.kobo.driver import KOBO + from calibre.devices.scanner import DeviceScanner + s = DeviceScanner() + s.scan() + app = Application([]) + dev = KOBO(None) + debug_print("KOBO:", KOBO) +# dev.startup() +# cd = dev.detect_managed_devices(s.devices) +# dev.open(cd, 'test') + cw = dev.config_widget() + d = QDialog() + 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() + + From b468713b5cf75e8531497b9233e9e0faa9fbcb3f Mon Sep 17 00:00:00 2001 From: David Date: Mon, 23 May 2016 00:02:10 +1000 Subject: [PATCH 3/8] Make groupboxes checkable and refactory tab adding Instead of checkboxes to enable a set of options, make the groupbox checkable. Changes also to how tabs and widgets are added. This will make subclassing the driver easier --- src/calibre/devices/kobo/kobotouch_config.py | 96 +++++++------------ .../device_drivers/tabbed_device_config.py | 17 +++- 2 files changed, 46 insertions(+), 67 deletions(-) diff --git a/src/calibre/devices/kobo/kobotouch_config.py b/src/calibre/devices/kobo/kobotouch_config.py index e413eabc84..1b9df07608 100644 --- a/src/calibre/devices/kobo/kobotouch_config.py +++ b/src/calibre/devices/kobo/kobotouch_config.py @@ -51,9 +51,8 @@ class KOBOTOUCHConfig(TabbedDeviceConfig): self.tab1 = Tab1Config(self, self.device) self.tab2 = Tab2Config(self, self.device) - extra_tab_pos = self.indexOf(self.extra_tab) - last_tab_pos = self.insertTab(extra_tab_pos, self.tab1, _("Collections, Covers && Uploads")) - last_tab_pos = self.insertTab(last_tab_pos + 1, self.tab2, _('Metadata && Advanced')) + self.addDeviceTab(self.tab1, _("Collections, Covers && Uploads")) + self.addDeviceTab(self.tab2, _('Metadata, On Device && Advanced')) def get_pref(self, key): @@ -133,15 +132,15 @@ class Tab1Config(DeviceConfigTab): # {{{ self.collections_options = CollectionsGroupBox(self, device) self.l.addWidget(self.collections_options) - self.add_widget(self.collections_options) + self.addDeviceWidget(self.collections_options) self.covers_options = CoversGroupBox(self, device) self.l.addWidget(self.covers_options) - self.add_widget(self.covers_options) + self.addDeviceWidget(self.covers_options) self.book_uploads_options = BookUploadsGroupBox(self, device) self.l.addWidget(self.book_uploads_options) - self.add_widget(self.book_uploads_options) + self.addDeviceWidget(self.book_uploads_options) # }}} class Tab2Config(DeviceConfigTab): # {{{ @@ -154,15 +153,15 @@ class Tab2Config(DeviceConfigTab): # {{{ self.metadata_options = MetadataGroupBox(self, device) self.l.addWidget(self.metadata_options) - self.add_widget(self.metadata_options) + self.addDeviceWidget(self.metadata_options) self.device_list_options = DeviceListGroupBox(self, device) self.l.addWidget(self.device_list_options) - self.add_widget(self.device_list_options) + self.addDeviceWidget(self.device_list_options) self.advanced_options = AdvancedGroupBox(self, device) self.l.addWidget(self.advanced_options) - self.add_widget(self.advanced_options) + self.addDeviceWidget(self.advanced_options) # }}} @@ -204,19 +203,15 @@ class CollectionsGroupBox(DeviceOptionsGroupBox): self.options_layout.setObjectName("options_layout") self.setLayout(self.options_layout) - self.manage_collections_checkbox = create_checkbox( - _("Manage Collections"), - _('Create new bookshelves on the Kobo if they do not exist. This is only for firmware V2.0.0 or later.'), - device.get_pref('manage_collections') - ) - self.manage_collections_checkbox.clicked.connect(self.manage_collections_checkbox_clicked) + self.setCheckable(True) + self.setChecked(device.get_pref('manage_collections')) + self.setToolTip(wrap_msg(_('Create new bookshelves on the Kobo if they do not exist. This is only for firmware V2.0.0 or later.'))) self.collections_columns_label = QLabel(_('Collections Columns')) self.collections_columns_edit = QLineEdit(self) self.collections_columns_edit.setToolTip(_('The Kobo from firmware V2.0.0 supports bookshelves.' ' These are created on the Kobo. ' + 'Specify a tags type column for automatic management.')) - debug_print("CollectionsGroupBox::__init__ - device.settings()=", device.settings()) self.collections_columns_edit.setText(device.get_pref('collections_columns')) self.create_collections_checkbox = create_checkbox( @@ -230,18 +225,16 @@ class CollectionsGroupBox(DeviceOptionsGroupBox): device.get_pref('delete_empty_collections') ) - self.options_layout.addWidget(self.manage_collections_checkbox, 0, 0, 1, 1) self.options_layout.addWidget(self.collections_columns_label, 1, 0, 1, 1) self.options_layout.addWidget(self.collections_columns_edit, 1, 1, 1, 1) - self.options_layout.addWidget(self.create_collections_checkbox, 2, 0, 1, 1) - self.options_layout.addWidget(self.delete_empty_collections_checkbox, 2, 1, 1, 1) + self.options_layout.addWidget(self.create_collections_checkbox, 2, 0, 1, 2) + self.options_layout.addWidget(self.delete_empty_collections_checkbox, 3, 0)#, 1, 2) self.options_layout.setRowStretch(5, 1) - self.manage_collections_checkbox_clicked(self.manage_collections) @property def manage_collections(self): - return self.manage_collections_checkbox.isChecked() + return self.isChecked() @property def collections_columns(self): @@ -255,29 +248,20 @@ class CollectionsGroupBox(DeviceOptionsGroupBox): def delete_empty_collections(self): return self.delete_empty_collections_checkbox.isChecked() - def manage_collections_checkbox_clicked(self, checked): - self.collections_columns_label.setEnabled(checked) - self.collections_columns_edit.setEnabled(checked) - self.create_collections_checkbox.setEnabled(checked) - self.delete_empty_collections_checkbox.setEnabled(checked) - class CoversGroupBox(DeviceOptionsGroupBox): def __init__(self, parent, device): super(CoversGroupBox, self).__init__(parent, device) - self.setTitle(_("Covers")) + self.setTitle(_("Upload covers")) self.options_layout = QGridLayout() self.options_layout.setObjectName("options_layout") self.setLayout(self.options_layout) - self.upload_covers_checkbox = create_checkbox( - _("Upload covers for books"), - _('Upload cover images from the calibre library when sending books to the device.'), - device.get_pref('upload_covers') - ) - self.upload_covers_checkbox.clicked.connect(self.upload_covers_checkbox_clicked) + self.setCheckable(True) + self.setChecked(device.get_pref('upload_covers')) + self.setToolTip(wrap_msg(_('Upload cover images from the calibre library when sending books to the device.'))) self.upload_grayscale_checkbox = create_checkbox( _('Upload Black and White Covers'), @@ -291,16 +275,13 @@ class CoversGroupBox(DeviceOptionsGroupBox): ' This is for firmware versions 2.3.1 and later.'), device.get_pref('keep_cover_aspect')) - self.options_layout.addWidget(self.upload_covers_checkbox, 0, 0, 1, 2) self.options_layout.addWidget(self.keep_cover_aspect_checkbox, 1, 0, 1, 1) - self.options_layout.addWidget(self.upload_grayscale_checkbox, 1, 1, 1, 1) - self.options_layout.setRowStretch(2, 1) + self.options_layout.addWidget(self.upload_grayscale_checkbox, 2, 0, 1, 1) + self.options_layout.setRowStretch(3, 1) - self.upload_covers_checkbox_clicked(self.upload_covers) - @property def upload_covers(self): - return self.upload_covers_checkbox.isChecked() + return self.isChecked() @property def upload_grayscale(self): @@ -310,10 +291,6 @@ class CoversGroupBox(DeviceOptionsGroupBox): def keep_cover_aspect(self): return self.keep_cover_aspect_checkbox.isChecked() - def upload_covers_checkbox_clicked(self, checked): - self.upload_grayscale_checkbox.setEnabled(checked) - self.keep_cover_aspect_checkbox.setEnabled(checked) - class DeviceListGroupBox(DeviceOptionsGroupBox): @@ -349,9 +326,9 @@ class DeviceListGroupBox(DeviceOptionsGroupBox): ) self.options_layout.addWidget(self.show_recommendations_checkbox, 0, 0, 1, 1) - self.options_layout.addWidget(self.show_archived_books_checkbox, 0, 1, 1, 1) - self.options_layout.addWidget(self.show_previews_checkbox, 1, 0, 1, 1) - self.options_layout.setRowStretch(1, 1) + self.options_layout.addWidget(self.show_archived_books_checkbox, 1, 0, 1, 1) + self.options_layout.addWidget(self.show_previews_checkbox, 2, 0, 1, 1) + self.options_layout.setRowStretch(3, 1) @property def show_recommendations(self): @@ -405,7 +382,7 @@ class AdvancedGroupBox(DeviceOptionsGroupBox): self.options_layout.addWidget(self.support_newer_firmware_checkbox, 0, 0, 1, 2) self.options_layout.addWidget(self.debugging_title_label, 1, 0, 1, 1) self.options_layout.addWidget(self.debugging_title_edit, 1, 1, 1, 1) - self.options_layout.setRowStretch(1, 2) + self.options_layout.setRowStretch(2, 2) @property def support_newer_firmware(self): @@ -420,21 +397,17 @@ class MetadataGroupBox(DeviceOptionsGroupBox): def __init__(self, parent, device): super(MetadataGroupBox, self).__init__(parent, device) - self.setTitle(_("Metadata Options")) + self.setTitle(_("Update metadata on the device")) self.options_layout = QGridLayout() self.options_layout.setObjectName("options_layout") self.setLayout(self.options_layout) - self.update_device_metadata_checkbox = create_checkbox( - _("Update metadata on the device"), - _('Update the metadata on the device when it is connected. ' - 'Be careful when doing this as it will take time and could make the initial connection take a long time.'), - device.get_pref('update_device_metadata') - ) - self.options_layout.addWidget(self.update_device_metadata_checkbox, 0, 0, 1, 2) - self.update_device_metadata_checkbox.clicked.connect(self.update_device_metadata_checkbox_clicked) - + self.setCheckable(True) + self.setChecked(device.get_pref('update_device_metadata')) + self.setToolTip(wrap_msg(_('Update the metadata on the device when it is connected. ' + 'Be careful when doing this as it will take time and could make the initial connection take a long time.'))) + self.update_series_checkbox = create_checkbox( _("Set Series information"), _('The book lists on the Kobo devices can display series information. ' @@ -443,7 +416,7 @@ class MetadataGroupBox(DeviceOptionsGroupBox): 'Enable if you wish to set series information.'), device.get_pref('update_series') ) - self.options_layout.addWidget(self.update_series_checkbox, 1, 0, 1, 2) + self.options_layout.addWidget(self.update_series_checkbox, 0, 0, 1, 1) self.options_layout.setRowStretch(1, 1) @property @@ -452,11 +425,8 @@ class MetadataGroupBox(DeviceOptionsGroupBox): @property def update_device_metadata(self): - return self.update_series_checkbox.isChecked() + return self.isChecked() - def update_device_metadata_checkbox_clicked(self, checked): - self.update_series_checkbox.setEnabled(checked) - if __name__ == '__main__': from calibre.gui2 import Application diff --git a/src/calibre/gui2/device_drivers/tabbed_device_config.py b/src/calibre/gui2/device_drivers/tabbed_device_config.py index 0b20c168d8..e8b0c8d016 100644 --- a/src/calibre/gui2/device_drivers/tabbed_device_config.py +++ b/src/calibre/gui2/device_drivers/tabbed_device_config.py @@ -142,6 +142,15 @@ class TabbedDeviceConfig(QTabWidget): self.setCurrentIndex(0) + def addDeviceTab(self, tab, label): + ''' + This is used to add a new tab for the device config. The new tab will always be added + as immediately before the "Extra Customization" tab. + ''' + extra_tab_pos = self.indexOf(self.extra_tab) + self.insertTab(extra_tab_pos, tab, label) + + def __getattr__(self, attr_name): "If the object doesn't have an attribute, then check each tab." try: @@ -243,16 +252,16 @@ class DeviceConfigTab(QWidget): # {{{ QWidget.__init__(self) self.parent = parent - self.widgets = [] + self.device_widgets = [] - def add_widget(self, widget): - self.widgets.append(widget) + def addDeviceWidget(self, widget): + self.device_widgets.append(widget) def __getattr__(self, attr_name): try: return super(DeviceConfigTab, self).__getattr__(attr_name) except AttributeError as ae: - for awidget in self.widgets: + for awidget in self.device_widgets: try: return getattr(awidget, attr_name) except AttributeError: From 77f6664a4f898299ffa048b8fa13d765746727f2 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 25 May 2016 22:33:03 +1000 Subject: [PATCH 4/8] Fix configuration handling Removed some experimental stuff that I had forgotten about. --- src/calibre/devices/kobo/driver.py | 44 ------------------- src/calibre/devices/kobo/kobotouch_config.py | 14 +++--- .../device_drivers/tabbed_device_config.py | 31 ++----------- 3 files changed, 9 insertions(+), 80 deletions(-) diff --git a/src/calibre/devices/kobo/driver.py b/src/calibre/devices/kobo/driver.py index 82bf8ff0c5..806820ff7c 100644 --- a/src/calibre/devices/kobo/driver.py +++ b/src/calibre/devices/kobo/driver.py @@ -148,44 +148,6 @@ class KOBO(USBMS): def __init__(self, *args, **kwargs): USBMS.__init__(self, *args, **kwargs) self.plugboards = self.plugboard_func = None - self._prefs = None -# self.device_defaults = DeviceDefaults() - self.current_device_defaults = {} - self.device_defaults_key = self.name - - @property - def prefs(self): - from calibre.utils.config import JSONConfig -# debug_print("KOBO:prefs - start") - if self._prefs is None: - self._prefs = p = JSONConfig(self.device_defaults_key + '_devices') -# debug_print("KOBO:prefs - self._prefs=", self._prefs) - if p == {}: - old_settings = super(KOBO, self).settings() - p[self.device_defaults_key] = self.migrate_old_settings(old_settings) -# debug_print("KOBO:prefs - Old settings self._prefs=", self._prefs) - p.defaults['format_map'] = self.FORMATS - p.defaults['save_template'] = KOBO._default_save_template() - p.defaults['use_subdirs'] = True - p.defaults['read_metadata'] = True - p.defaults['use_author_sort'] = False - - p.defaults['collections_columns'] = '' - p.defaults['create_collections'] = False - p.defaults['delete_empty_collections'] = False - - p.defaults['upload_covers'] = False - p.defaults['upload_grayscale'] = False - - p.defaults['show_expired_books'] = False - p.defaults['show_previews'] = False - p.defaults['show_recommendations'] = False - p.defaults['support_newer_firmware'] = False - - p.defaults['extra_customization'] = self.EXTRA_CUSTOMIZATION_DEFAULT - debug_print("KOBO:prefs - finish self._prefs=", self._prefs) - - return self._prefs def initialize(self): @@ -1111,7 +1073,6 @@ class KOBO(USBMS): OPT_SHOW_RECOMMENDATIONS = 5 OPT_SUPPORT_NEWER_FIRMWARE = 6 - p = {} p['format_map'] = old_settings.format_map p['save_template'] = old_settings.save_template @@ -1434,11 +1395,6 @@ class KOBOTOUCH(KOBO): def __init__(self, *args, **kwargs): KOBO.__init__(self, *args, **kwargs) self.plugboards = self.plugboard_func = None - self._prefs = None -# self.device_defaults = DeviceDefaults() - self.current_device_defaults = {} - self.device_defaults_key = self.name - def initialize(self): super(KOBOTOUCH, self).initialize() diff --git a/src/calibre/devices/kobo/kobotouch_config.py b/src/calibre/devices/kobo/kobotouch_config.py index 1b9df07608..68887ccec7 100644 --- a/src/calibre/devices/kobo/kobotouch_config.py +++ b/src/calibre/devices/kobo/kobotouch_config.py @@ -46,8 +46,6 @@ class KOBOTOUCHConfig(TabbedDeviceConfig): self.extra_customization_message = extra_customization_message self.extra_customization_choices = extra_customization_choices - self.current_device_key = device.device_defaults_key - self.tab1 = Tab1Config(self, self.device) self.tab2 = Tab2Config(self, self.device) @@ -117,8 +115,6 @@ class KOBOTOUCHConfig(TabbedDeviceConfig): p['support_newer_firmware'] = self.support_newer_firmware p['debugging_title'] = self.debugging_title - p['extra_customization'] = self.extra_tab.extra_customization() - return p @@ -228,8 +224,8 @@ class CollectionsGroupBox(DeviceOptionsGroupBox): self.options_layout.addWidget(self.collections_columns_label, 1, 0, 1, 1) self.options_layout.addWidget(self.collections_columns_edit, 1, 1, 1, 1) self.options_layout.addWidget(self.create_collections_checkbox, 2, 0, 1, 2) - self.options_layout.addWidget(self.delete_empty_collections_checkbox, 3, 0)#, 1, 2) - self.options_layout.setRowStretch(5, 1) + self.options_layout.addWidget(self.delete_empty_collections_checkbox, 3, 0, 1, 2) + self.options_layout.setRowStretch(4, 1) @property @@ -275,9 +271,9 @@ class CoversGroupBox(DeviceOptionsGroupBox): ' This is for firmware versions 2.3.1 and later.'), device.get_pref('keep_cover_aspect')) - self.options_layout.addWidget(self.keep_cover_aspect_checkbox, 1, 0, 1, 1) - self.options_layout.addWidget(self.upload_grayscale_checkbox, 2, 0, 1, 1) - self.options_layout.setRowStretch(3, 1) + self.options_layout.addWidget(self.keep_cover_aspect_checkbox, 0, 0, 1, 1) + self.options_layout.addWidget(self.upload_grayscale_checkbox, 1, 0, 1, 1) + self.options_layout.setRowStretch(2, 1) @property def upload_covers(self): diff --git a/src/calibre/gui2/device_drivers/tabbed_device_config.py b/src/calibre/gui2/device_drivers/tabbed_device_config.py index e8b0c8d016..74d378da45 100644 --- a/src/calibre/gui2/device_drivers/tabbed_device_config.py +++ b/src/calibre/gui2/device_drivers/tabbed_device_config.py @@ -164,14 +164,6 @@ class TabbedDeviceConfig(QTabWidget): pass raise ae - def get_pref(self, key): - debug_print("get_pref - self.device.prefs", self.device.prefs) - p = self.device.prefs.get(self.current_device_key, {}) - if not p: - self.device.prefs[self.current_device_key] = p - debug_print("get_pref - self.device.get_pref(key)", self.device.get_pref(key)) - return self.device.get_pref(key) - @property def device(self): return self._device() @@ -217,26 +209,11 @@ class TabbedDeviceConfig(QTabWidget): def commit(self): debug_print("TabbedDeviceConfig::commit: start") p = self.device._configProxy() - debug_print("commit: starting setting=%s" % (p, )) - - f = self.formats.format_map - debug_print("commit: self.formats.format_map=", self.formats.format_map) - debug_print("commit: self.device.prefs['format_map']=", self.device.prefs['format_map']) - if f and f != self.device.prefs['format_map']: - p['format_map'] = f - - f = self.use_subdirs() - if f != self.get_pref('use_subdirs'): - p['use_subdirs'] = f - - f = self.read_metadata() - if f != self.get_pref('read_metadata'): - p['read_metadata'] = f - - t = self.template.template - if t and t != self.device.prefs['save_template']: - p['save_template'] = t + p['format_map'] = self.formats.format_map + p['use_subdirs'] = self.use_subdirs() + p['read_metadata'] = self.read_metadata() + p['save_template'] = self.template.template p['extra_customization'] = self.extra_tab.extra_customization() return p From 4aee79ae59253b1ec1268fcfd8342b8a536383f0 Mon Sep 17 00:00:00 2001 From: David Date: Sat, 28 May 2016 16:38:11 +1000 Subject: [PATCH 5/8] Refactor _modify_epub to make subclassing easier Based on suggestions from jackie_w, refactor _modify_epub and related methods. This is to make subclassing to add more CSS editing rules easier. --- src/calibre/devices/kobo/driver.py | 139 ++++++++++++++++++++--------- 1 file changed, 98 insertions(+), 41 deletions(-) diff --git a/src/calibre/devices/kobo/driver.py b/src/calibre/devices/kobo/driver.py index 806820ff7c..a67567831a 100644 --- a/src/calibre/devices/kobo/driver.py +++ b/src/calibre/devices/kobo/driver.py @@ -28,6 +28,8 @@ from calibre.ptempfile import PersistentTemporaryFile from calibre.constants import DEBUG from calibre.utils.config_base import prefs +from cssutils.css import CSSRule + EPUB_EXT = '.epub' KEPUB_EXT = '.kepub' @@ -1878,8 +1880,26 @@ class KOBOTOUCH(KOBO): except Exception as e: debug_print("KoboTouch:get_extra_css: Problem parsing extra CSS file {0}".format(extra_css_path)) debug_print("KoboTouch:get_extra_css: Exception {0}".format(e)) + + # create dictionary of features enabled in kobo extra css + self.extra_css_options = {} + if extra_sheet: + # search extra_css for @page rule + self.extra_css_options['has_atpage'] = len(self.get_extra_css_rules(extra_sheet, CSSRule.PAGE_RULE)) > 0 + + # search extra_css for style rule(s) containing widows or orphans + self.extra_css_options['has_widows_orphans'] = len(self.get_extra_css_rules_widow_orphan(extra_sheet)) > 0 + debug_print('KoboTouch:get_extra_css - CSS options:', self.extra_css_options) + return extra_sheet + def get_extra_css_rules(self, sheet, css_rule): + return [r for r in sheet.cssRules.rulesOfType(css_rule)] + + def get_extra_css_rules_widow_orphan(self, sheet): + return [r for r in self.get_extra_css_rules(sheet, CSSRule.STYLE_RULE) + if (r.style['widows'] or r.style['orphans'])] + def upload_books(self, files, names, on_card=None, end_session=True, metadata=None): debug_print('KoboTouch:upload_books - %d books'%(len(files))) @@ -1934,7 +1954,7 @@ class KOBOTOUCH(KOBO): return result - def _modify_epub(self, file, metadata, container=None): + def _modify_epub(self, book_file, metadata, container=None): debug_print("KoboTouch:_modify_epub:Processing {0} - {1}".format(metadata.author_sort, metadata.title)) # Currently only modifying CSS, so if no stylesheet, don't do anything @@ -1942,58 +1962,95 @@ class KOBOTOUCH(KOBO): debug_print("KoboTouch:_modify_epub: no CSS file") return True - commit_container = False + container, commit_container = self.create_container(book_file, metadata, container) if not container: - commit_container = True - try: - from calibre.ebooks.oeb.polish.container import get_container - debug_print("KoboTouch:_modify_epub: creating container") - container = get_container(file) - container.css_preprocessor = DummyCSSPreProcessor() - except Exception as e: - debug_print("KoboTouch:_modify_epub: exception from get_container {0} - {1}".format(metadata.author_sort, metadata.title)) - debug_print("KoboTouch:_modify_epub: exception is: {0}".format(e)) - return False - else: - debug_print("KoboTouch:_modify_epub: received container") - + return False + from calibre.ebooks.oeb.base import OEB_STYLES + + is_dirty = False for cssname, mt in container.mime_map.iteritems(): if mt in OEB_STYLES: newsheet = container.parsed(cssname) oldrules = len(newsheet.cssRules) - # remove any existing @page rules in epub css - # if css to be appended contains an @page rule - if self.extra_sheet and len([r for r in self.extra_sheet if r.type == r.PAGE_RULE]): - page_rules = [r for r in newsheet if r.type == r.PAGE_RULE] - if len(page_rules) > 0: - debug_print("KoboTouch:_modify_epub:Removing existing @page rules") - for rule in page_rules: - rule.style = '' - # remove any existing widow/orphan settings in epub css - # if css to be appended contains a widow/orphan rule or we there is no extra CSS file - if (len([r for r in self.extra_sheet if r.type == r.STYLE_RULE - and (r.style['widows'] or r.style['orphans'])]) > 0): - widow_orphan_rules = [r for r in newsheet if r.type == r.STYLE_RULE - and (r.style['widows'] or r.style['orphans'])] - if len(widow_orphan_rules) > 0: - debug_print("KoboTouch:_modify_epub:Removing existing widows/orphans attribs") - for rule in widow_orphan_rules: - rule.style.removeProperty('widows') - rule.style.removeProperty('orphans') - # append all rules from kobo extra css stylesheet - for addrule in [r for r in self.extra_sheet.cssRules]: - newsheet.insertRule(addrule, len(newsheet.cssRules)) - debug_print("KoboTouch:_modify_epub:CSS rules {0} -> {1} ({2})".format(oldrules, len(newsheet.cssRules), cssname)) - container.dirty(cssname) + + # future css mods may be epub/kepub specific, so pass file extension arg + fileext = os.path.splitext(book_file)[-1].lower() + debug_print("KoboTouch:_modify_epub: Modifying {0}".format(cssname)) + if self._modify_stylesheet(newsheet, fileext): + debug_print("KoboTouch:_modify_epub:CSS rules {0} -> {1} ({2})".format(oldrules, len(newsheet.cssRules), cssname)) + container.dirty(cssname) + is_dirty = True if commit_container: debug_print("KoboTouch:_modify_epub: committing container.") - os.unlink(file) - container.commit(file) + self.commit_container(container, is_dirty) return True + def _modify_stylesheet(self, sheet, fileext, is_dirty=False): + + #if fileext in (EPUB_EXT, KEPUB_EXT): + + # if kobo extra css contains a @page rule + # remove any existing @page rules in epub css + if self.extra_css_options.get('has_atpage', False): + page_rules = self.get_extra_css_rules(sheet, CSSRule.PAGE_RULE) + if len(page_rules) > 0: + debug_print("KoboTouch:_modify_stylesheet: Removing existing @page rules") + for rule in page_rules: + rule.style = '' + is_dirty = True + + # if kobo extra css contains any widow/orphan style rules + # remove any existing widow/orphan settings in epub css + if self.extra_css_options.get('has_widows_orphans', False): + widow_orphan_rules = self.get_extra_css_rules_widow_orphan(sheet) + if len(widow_orphan_rules) > 0: + debug_print("KoboTouch:_modify_stylesheet: Removing existing widows/orphans attribs") + for rule in widow_orphan_rules: + rule.style.removeProperty('widows') + rule.style.removeProperty('orphans') + is_dirty = True + + # append all rules from kobo extra css + debug_print("KoboTouch:_modify_stylesheet: Append all kobo extra css rules") + for extra_rule in [r for r in self.extra_sheet.cssRules]: + sheet.insertRule(extra_rule) + is_dirty = True + + return is_dirty + + def create_container(self, book_file, metadata, container=None): + # create new container if not received, else pass through + if not container: + commit_container = True + try: + from calibre.ebooks.oeb.polish.container import get_container + debug_print("KoboTouch:create_container: try to create new container") + container = get_container(book_file) + container.css_preprocessor = DummyCSSPreProcessor() + except Exception as e: + debug_print("KoboTouch:create_container: exception from get_container {0} - {1}".format(metadata.author_sort, metadata.title)) + debug_print("KoboTouch:create_container: exception is: {0}".format(e)) + else: + commit_container = False + debug_print("KoboTouch:create_container: received container") + return container, commit_container + + def commit_container(self, container, is_dirty=True): + # commit container if changes have been made + if is_dirty: + debug_print("KoboTouch:commit_container: commit container.") + container.commit() + + # Clean-up-AYGO prevents build-up of TEMP exploded epub/kepub files + debug_print("KoboTouch:commit_container: removing container temp files.") + try: + shutil.rmtree(container.root) + except: + pass + def delete_via_sql(self, ContentID, ContentType): imageId = super(KOBOTOUCH, self).delete_via_sql(ContentID, ContentType) From 9c97808328d2145604db6279ae887434ddc3c4c9 Mon Sep 17 00:00:00 2001 From: David Date: Sat, 28 May 2016 19:39:50 +1000 Subject: [PATCH 6/8] Make config_widget a class method Missed the @classmethod for the config_widget methods. This is needed to not break the extended driver. --- src/calibre/devices/kobo/driver.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/calibre/devices/kobo/driver.py b/src/calibre/devices/kobo/driver.py index a67567831a..57849a9394 100644 --- a/src/calibre/devices/kobo/driver.py +++ b/src/calibre/devices/kobo/driver.py @@ -1055,6 +1055,7 @@ class KOBO(USBMS): paths[idx] = tf.name return paths + @classmethod def config_widget(self): # TODO: Cleanup the following self.current_friendly_name = self.gui_name @@ -2774,6 +2775,7 @@ class KOBOTOUCH(KOBO): if show_debug: debug_print("KoboTouch:set_series - end") + @classmethod def config_widget(self): # TODO: Cleanup the following self.current_friendly_name = self.gui_name From 8e5dc1d23fb93b433eb38e41654df7d41293e0fa Mon Sep 17 00:00:00 2001 From: David Date: Wed, 1 Jun 2016 21:49:34 +1000 Subject: [PATCH 7/8] Small code optimization in CSS modifying Remove a stupid double loop when adding the extra CSS to the existing stylesheets. --- src/calibre/devices/kobo/driver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/devices/kobo/driver.py b/src/calibre/devices/kobo/driver.py index 57849a9394..4a70f42d68 100644 --- a/src/calibre/devices/kobo/driver.py +++ b/src/calibre/devices/kobo/driver.py @@ -2016,7 +2016,7 @@ class KOBOTOUCH(KOBO): # append all rules from kobo extra css debug_print("KoboTouch:_modify_stylesheet: Append all kobo extra css rules") - for extra_rule in [r for r in self.extra_sheet.cssRules]: + for extra_rule in self.extra_sheet.cssRules: sheet.insertRule(extra_rule) is_dirty = True @@ -2797,7 +2797,7 @@ class KOBOTOUCH(KOBO): try: return getattr(opts, key) except: - debug_print("KoboTouch::get_prefs - probably an extra_customization") + debug_print("KoboTouch::get_prefs - probably an extra_customization:", key) return None @classmethod From 0abfcb43b7c6b3416e86a0778ada3ad80058211b Mon Sep 17 00:00:00 2001 From: David Date: Thu, 2 Jun 2016 23:27:59 +1000 Subject: [PATCH 8/8] Add versions in plugin list in error output We have a nice list of plugins with the errors, so add the plugin version numbers to help the plugin developers. --- src/calibre/debug.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/debug.py b/src/calibre/debug.py index e88cc63a37..8815c594f4 100644 --- a/src/calibre/debug.py +++ b/src/calibre/debug.py @@ -182,7 +182,7 @@ def print_basic_debug_info(out=None): pass from calibre.customize.ui import has_external_plugins, initialized_plugins if has_external_plugins(): - names = (p.name for p in initialized_plugins() if getattr(p, 'plugin_path', None) is not None) + names = ('{0} ({1})'.format(p.name, '.'.join(map(unicode, p.version))) for p in initialized_plugins() if getattr(p, 'plugin_path', None) is not None) out('Successfully initialized third party plugins:', ' && '.join(names)) def run_debug_gui(logpath):