diff --git a/src/calibre/debug.py b/src/calibre/debug.py index e88cc63a37..9ad0f2d8b7 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, 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): diff --git a/src/calibre/devices/kobo/driver.py b/src/calibre/devices/kobo/driver.py index c20b9fb037..34bde73b82 100644 --- a/src/calibre/devices/kobo/driver.py +++ b/src/calibre/devices/kobo/driver.py @@ -145,6 +145,11 @@ 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 + + def initialize(self): USBMS.initialize(self) self.dbversion = 7 @@ -311,7 +316,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 +714,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 +903,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 +1053,50 @@ 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 + + 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,83 +1338,9 @@ 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 @@ -1415,6 +1393,10 @@ 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 + def initialize(self): super(KOBOTOUCH, self).initialize() self.bookshelvelist = [] @@ -1426,6 +1408,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 +1450,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 +1641,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 +1700,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' @@ -1884,6 +1865,7 @@ class KOBOTOUCH(KOBO): def get_extra_css(self): extra_sheet = None + from cssutils.css import CSSRule if self.modifying_css(): extra_css_path = os.path.join(self._main_prefix, self.KOBO_EXTRA_CSSFILE) @@ -1898,8 +1880,27 @@ 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): + from cssutils.css import CSSRule + 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))) @@ -1943,7 +1944,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() @@ -1954,7 +1955,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 @@ -1962,58 +1963,96 @@ 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): + from cssutils.css import CSSRule + + #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 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) @@ -2148,7 +2187,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 +2201,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 +2221,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 +2268,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 +2285,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 +2313,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 +2326,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 +2361,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 +2370,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 +2603,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 +2705,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 +2776,72 @@ 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 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:", key) + 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 +2858,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 +2907,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 +2998,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 @@ -2901,21 +3029,77 @@ class KOBOTOUCH(KOBO): 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 +3136,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() diff --git a/src/calibre/devices/kobo/kobotouch_config.py b/src/calibre/devices/kobo/kobotouch_config.py new file mode 100644 index 0000000000..56dd6fde94 --- /dev/null +++ b/src/calibre/devices/kobo/kobotouch_config.py @@ -0,0 +1,450 @@ +#!/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.tab1 = Tab1Config(self, self.device) + self.tab2 = Tab2Config(self, self.device) + + self.addDeviceTab(self.tab1, _("Collections, Covers && Uploads")) + self.addDeviceTab(self.tab2, _('Metadata, On Device && 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 + + 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.addDeviceWidget(self.collections_options) + + self.covers_options = CoversGroupBox(self, device) + self.l.addWidget(self.covers_options) + self.addDeviceWidget(self.covers_options) + + self.book_uploads_options = BookUploadsGroupBox(self, device) + self.l.addWidget(self.book_uploads_options) + self.addDeviceWidget(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.addDeviceWidget(self.metadata_options) + + self.device_list_options = DeviceListGroupBox(self, device) + self.l.addWidget(self.device_list_options) + self.addDeviceWidget(self.device_list_options) + + self.advanced_options = AdvancedGroupBox(self, device) + self.l.addWidget(self.advanced_options) + self.addDeviceWidget(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.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.')) + 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.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(4, 1) + + @property + def manage_collections(self): + return self.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() + + +class CoversGroupBox(DeviceOptionsGroupBox): + + def __init__(self, parent, device): + super(CoversGroupBox, self).__init__(parent, device) + self.setTitle(_("Upload covers")) + + self.options_layout = QGridLayout() + self.options_layout.setObjectName("options_layout") + self.setLayout(self.options_layout) + + 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'), + _('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.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): + return self.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() + + +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, 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): + 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(2, 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(_("Update metadata on the device")) + + self.options_layout = QGridLayout() + self.options_layout.setObjectName("options_layout") + self.setLayout(self.options_layout) + + 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. ' + '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, 0, 0, 1, 1) + 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.isChecked() + + +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..4506631160 --- /dev/null +++ b/src/calibre/gui2/device_drivers/tabbed_device_config.py @@ -0,0 +1,390 @@ +#!/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, QLabel, QTabWidget, QGridLayout, QLineEdit, QVBoxLayout, + QGroupBox, QComboBox, QSizePolicy, QDialog, QDialogButtonBox, 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 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: + 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 + + @property + def device(self): + return self._device() + + def format_map(self): + 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() + + 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 + + +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.device_widgets = [] + + 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.device_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() + +