From b7577601803bd38f46ea39de64657b7471bdef0a Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Wed, 6 Oct 2010 20:29:42 +0100 Subject: [PATCH 01/12] Add set_plugboards to interface.py --- src/calibre/devices/interface.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/calibre/devices/interface.py b/src/calibre/devices/interface.py index 2307bf94d6..29ba5020c7 100644 --- a/src/calibre/devices/interface.py +++ b/src/calibre/devices/interface.py @@ -411,6 +411,22 @@ class DevicePlugin(Plugin): ''' raise NotImplementedError() + def set_plugboards(self, plugboards, pb_func): + ''' + provide the driver the current set of plugboards and a function to + select a specific plugboard. This method is called immediately before + add_books and sync_booklists. + + pb_func is a callable with the following signature: + def pb_func(device_name, format, plugboards) + You give it the current device name (either the class name or + DEVICE_PLUGBOARD_NAME), the format you are interested in (a 'real' + format or 'device_db'), and the plugboards (you were given those by + set_plugboards, the same place you got this method). + + Return value: None or a single plugboard instance. + ''' + pass class BookList(list): ''' From 2dab246d4df0ddf6f28c0f2d9dedab3b0fb607ea Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 6 Oct 2010 16:21:29 -0600 Subject: [PATCH 02/12] Implement #7075 (possibility to move a book to another library) --- src/calibre/gui2/actions/copy_to_library.py | 19 +++++++++++++++++-- src/calibre/gui2/actions/delete.py | 21 +++++++++++++-------- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/src/calibre/gui2/actions/copy_to_library.py b/src/calibre/gui2/actions/copy_to_library.py index 6b7654f644..513026f757 100644 --- a/src/calibre/gui2/actions/copy_to_library.py +++ b/src/calibre/gui2/actions/copy_to_library.py @@ -21,6 +21,7 @@ class Worker(Thread): def __init__(self, ids, db, loc, progress, done): Thread.__init__(self) self.ids = ids + self.processed = set([]) self.db = db self.loc = loc self.error = None @@ -71,6 +72,7 @@ class Worker(Thread): co = self.db.conversion_options(x, 'PIPE') if co is not None: newdb.set_conversion_options(x, 'PIPE', co) + self.processed.add(x) class CopyToLibraryAction(InterfaceAction): @@ -107,9 +109,13 @@ class CopyToLibraryAction(InterfaceAction): for name, loc in locations: self.menu.addAction(name, partial(self.copy_to_library, loc)) + self.menu.addAction(name + ' ' + _('(delete after copy)'), + partial(self.copy_to_library, loc, delete_after=True)) + self.menu.addSeparator() + self.qaction.setVisible(bool(locations)) - def copy_to_library(self, loc): + def copy_to_library(self, loc, delete_after=False): rows = self.gui.library_view.selectionModel().selectedRows() if not rows or len(rows) == 0: return error_dialog(self.gui, _('Cannot copy'), @@ -140,7 +146,16 @@ class CopyToLibraryAction(InterfaceAction): else: self.gui.status_bar.show_message(_('Copied %d books to %s') % (len(ids), loc), 2000) - + if delete_after and self.worker.processed: + v = self.gui.library_view + ci = v.currentIndex() + row = None + if ci.isValid(): + row = ci.row() + + v.model().delete_books_by_id(self.worker.processed) + self.gui.iactions['Remove Books'].library_ids_deleted( + self.worker.processed, row) diff --git a/src/calibre/gui2/actions/delete.py b/src/calibre/gui2/actions/delete.py index 406860e4ec..a541590fd1 100644 --- a/src/calibre/gui2/actions/delete.py +++ b/src/calibre/gui2/actions/delete.py @@ -149,6 +149,18 @@ class DeleteAction(InterfaceAction): self.gui.library_view.model().current_changed(self.gui.library_view.currentIndex(), self.gui.library_view.currentIndex()) + + def library_ids_deleted(self, ids_deleted, current_row=None): + view = self.gui.library_view + for v in (self.gui.memory_view, self.gui.card_a_view, self.gui.card_b_view): + if v is None: + continue + v.model().clear_ondevice(ids_deleted) + if current_row is not None: + ci = view.model().index(current_row, 0) + if ci.isValid(): + view.set_current_row(current_row) + def delete_books(self, *args): ''' Delete selected books from device or library. @@ -168,14 +180,7 @@ class DeleteAction(InterfaceAction): if ci.isValid(): row = ci.row() ids_deleted = view.model().delete_books(rows) - for v in (self.gui.memory_view, self.gui.card_a_view, self.gui.card_b_view): - if v is None: - continue - v.model().clear_ondevice(ids_deleted) - if row is not None: - ci = view.model().index(row, 0) - if ci.isValid(): - view.set_current_row(row) + self.library_ids_deleted(ids_deleted, row) else: if not confirm('

'+_('The selected books will be ' 'permanently deleted ' From e3f6049cf65d261cdcb9d5a0895374707f7082f5 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 6 Oct 2010 17:33:26 -0600 Subject: [PATCH 03/12] Fix another regression in the isbndb plugin --- src/calibre/ebooks/metadata/isbndb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/ebooks/metadata/isbndb.py b/src/calibre/ebooks/metadata/isbndb.py index 6416dcdc39..07a054eeaa 100644 --- a/src/calibre/ebooks/metadata/isbndb.py +++ b/src/calibre/ebooks/metadata/isbndb.py @@ -83,7 +83,7 @@ class ISBNDBMetadata(Metadata): summ = tostring(book.find('summary')) if summ: - self.comments = 'SUMMARY:\n'+summ.string + self.comments = 'SUMMARY:\n'+summ def build_isbn(base_url, opts): From 217ea7a9a22ec243b1d19d332b478f357a77417d Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 6 Oct 2010 20:31:40 -0600 Subject: [PATCH 04/12] Implement #7082 (Enhance viewer "Remember Last Used Window Size") --- src/calibre/gui2/viewer/main.py | 38 ++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/src/calibre/gui2/viewer/main.py b/src/calibre/gui2/viewer/main.py index 79f4c29998..26fd2cadc9 100644 --- a/src/calibre/gui2/viewer/main.py +++ b/src/calibre/gui2/viewer/main.py @@ -166,6 +166,8 @@ class EbookViewer(MainWindow, Ui_EbookViewer): def __init__(self, pathtoebook=None, debug_javascript=False): MainWindow.__init__(self, None) self.setupUi(self) + self.show_toc_on_open = False + self.current_book_has_toc = False self.base_window_title = unicode(self.windowTitle()) self.iterator = None self.current_page = None @@ -214,11 +216,12 @@ class EbookViewer(MainWindow, Ui_EbookViewer): self.action_metadata.setCheckable(True) self.action_metadata.setShortcut(Qt.CTRL+Qt.Key_I) self.action_table_of_contents.setCheckable(True) + self.toc.setMinimumWidth(80) self.action_reference_mode.setCheckable(True) self.connect(self.action_reference_mode, SIGNAL('triggered(bool)'), lambda x: self.view.reference_mode(x)) self.connect(self.action_metadata, SIGNAL('triggered(bool)'), lambda x:self.metadata.setVisible(x)) - self.connect(self.action_table_of_contents, SIGNAL('triggered(bool)'), lambda x:self.toc.setVisible(x)) + self.connect(self.action_table_of_contents, SIGNAL('toggled(bool)'), lambda x:self.toc.setVisible(x)) self.connect(self.action_copy, SIGNAL('triggered(bool)'), self.copy) self.connect(self.action_font_size_larger, SIGNAL('triggered(bool)'), self.font_size_larger) @@ -259,7 +262,6 @@ class EbookViewer(MainWindow, Ui_EbookViewer): f = functools.partial(self.load_ebook, pathtoebook) QTimer.singleShot(50, f) self.view.setMinimumSize(100, 100) - self.splitter.setSizes([1, 300]) self.toc.setCursor(Qt.PointingHandCursor) self.tool_bar.setContextMenuPolicy(Qt.PreventContextMenu) self.tool_bar2.setContextMenuPolicy(Qt.PreventContextMenu) @@ -285,6 +287,12 @@ class EbookViewer(MainWindow, Ui_EbookViewer): def save_state(self): state = str(self.saveState(self.STATE_VERSION)) dynamic['viewer_toolbar_state'] = state + dynamic.set('viewer_window_geometry', self.saveGeometry()) + if self.current_book_has_toc: + dynamic.set('viewer_toc_isvisible', bool(self.toc.isVisible())) + if self.toc.isVisible(): + dynamic.set('viewer_splitter_state', + bytearray(self.splitter.saveState())) def restore_state(self): state = dynamic.get('viewer_toolbar_state', None) @@ -609,10 +617,15 @@ class EbookViewer(MainWindow, Ui_EbookViewer): title = self.iterator.opf.title if not title: title = os.path.splitext(os.path.basename(pathtoebook))[0] - self.action_table_of_contents.setDisabled(not self.iterator.toc) if self.iterator.toc: self.toc_model = TOC(self.iterator.toc) self.toc.setModel(self.toc_model) + if self.show_toc_on_open: + self.action_table_of_contents.setChecked(True) + else: + self.action_table_of_contents.setChecked(False) + self.action_table_of_contents.setDisabled(not self.iterator.toc) + self.current_book_has_toc = bool(self.iterator.toc) self.current_title = title self.setWindowTitle(self.base_window_title+' - '+title) self.pos.setMaximum(sum(self.iterator.pages)) @@ -656,22 +669,21 @@ class EbookViewer(MainWindow, Ui_EbookViewer): return self def __exit__(self, *args): - self.write_settings() if self.iterator is not None: self.save_current_position() self.iterator.__exit__(*args) - def write_settings(self): - dynamic.set('viewer_window_geometry', self.saveGeometry()) - def read_settings(self): c = config().parse() - wg = dynamic['viewer_window_geometry'] - if wg is not None and c.remember_window_size: - self.restoreGeometry(wg) - - - + self.splitter.setSizes([1, 300]) + if c.remember_window_size: + wg = dynamic.get('viewer_window_geometry', None) + if wg is not None: + self.restoreGeometry(wg) + ss = dynamic.get('viewer_splitter_state', None) + if ss is not None: + self.splitter.restoreState(ss) + self.show_toc_on_open = dynamic.get('viewer_toc_isvisible', False) def config(defaults=None): desc = _('Options to control the ebook viewer') From 00b25125b11957e1f48eff649d472ef23cb40f71 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Thu, 7 Oct 2010 11:58:09 +0100 Subject: [PATCH 05/12] Fix clearing custom series columns. They should be cleared to None, not the empty string --- src/calibre/gui2/custom_column_widgets.py | 6 ++++-- src/calibre/gui2/library/models.py | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/custom_column_widgets.py b/src/calibre/gui2/custom_column_widgets.py index 1758116e7a..3be4c19d17 100644 --- a/src/calibre/gui2/custom_column_widgets.py +++ b/src/calibre/gui2/custom_column_widgets.py @@ -299,7 +299,9 @@ class Series(Base): val, s_index = self.gui_val val = self.normalize_ui_val(val) if val != self.initial_val or s_index != self.initial_index: - if s_index == 0.0: + if val == '': + val = s_index = None + elif s_index == 0.0: if tweaks['series_index_auto_increment'] == 'next': s_index = self.db.get_next_cc_series_num_for(val, num=self.col_id) @@ -488,7 +490,7 @@ class BulkSeries(BulkBase): def commit(self, book_ids, notify=False): val, update_indices, force_start, at_value, clear = self.gui_val - val = '' if clear else self.normalize_ui_val(val) + val = None if clear else self.normalize_ui_val(val) if clear or val != '': extras = [] next_index = self.db.get_next_cc_series_num_for(val, num=self.col_id) diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index 848c93f485..53e0982211 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -743,6 +743,8 @@ class BooksModel(QAbstractTableModel): # {{{ val = qt_to_dt(val, as_utc=False) elif typ == 'series': val, s_index = parse_series_string(self.db, label, value.toString()) + if not val: + val = s_index = None elif typ == 'composite': tmpl = unicode(value.toString()).strip() disp = cc['display'] From 108163392876f27af3557d03c869ddf661579753 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Thu, 7 Oct 2010 13:18:13 +0100 Subject: [PATCH 06/12] Add calibredb list_categories, a command that dumps the data shown in the tags pane. --- src/calibre/library/cli.py | 134 +++++++++++++++++++++++++++++++++++-- 1 file changed, 128 insertions(+), 6 deletions(-) diff --git a/src/calibre/library/cli.py b/src/calibre/library/cli.py index cd8305b059..8ceb63a368 100644 --- a/src/calibre/library/cli.py +++ b/src/calibre/library/cli.py @@ -956,12 +956,6 @@ def command_check_library(args, dbpath): print_one(checker, check) -COMMANDS = ('list', 'add', 'remove', 'add_format', 'remove_format', - 'show_metadata', 'set_metadata', 'export', 'catalog', - 'saved_searches', 'add_custom_column', 'custom_columns', - 'remove_custom_column', 'set_custom', 'restore_database', - 'check_library') - def restore_database_option_parser(): parser = get_parser(_( ''' @@ -1015,6 +1009,134 @@ def command_restore_database(args, dbpath): prints('Some errors occurred. A detailed report was ' 'saved to', name) +def list_categories_option_parser(): + from calibre.library.check_library import CHECKS + parser = get_parser(_('''\ +%prog list_categories [options] + +Produce a report of the category information in the database. The +information is the equivalent of what is shown in the tags pane. +''')) + + parser.add_option('-i', '--item_count', default=False, action='store_true', + help=_('Output only the number of items in a category instead of the ' + 'counts per item within the category')) + parser.add_option('-c', '--csv', default=False, action='store_true', + help=_('Output in CSV')) + parser.add_option('-q', '--quote', default='"', + help=_('The character to put around the category value in CSV mode. ' + 'Default is quotes (").')) + parser.add_option('-r', '--categories', default=None, dest='report', + help=_("Comma-separated list of category lookup names.\n" + "Default: all")) + parser.add_option('-w', '--line-width', default=-1, type=int, + help=_('The maximum width of a single line in the output. ' + 'Defaults to detecting screen size.')) + parser.add_option('-s', '--separator', default=',', + help=_('The string used to separate fields in CSV mode. ' + 'Default is a comma.')) + return parser + +def command_list_categories(args, dbpath): + parser = list_categories_option_parser() + opts, args = parser.parse_args(args) + if len(args) != 0: + parser.print_help() + return 1 + + if opts.library_path is not None: + dbpath = opts.library_path + + if isbytestring(dbpath): + dbpath = dbpath.decode(preferred_encoding) + + db = LibraryDatabase2(dbpath) + category_data = db.get_categories() + data = [] + categories = [k for k in category_data.keys() + if db.metadata_for_field(k)['kind'] != 'user'] + + categories.sort(cmp=lambda x,y: cmp(x if x[0] != '#' else x[1:], + y if y[0] != '#' else y[1:])) + if not opts.item_count: + for category in categories: + is_rating = db.metadata_for_field(category)['datatype'] == 'rating' + for tag in category_data[category]: + if is_rating: + tag.name = unicode(len(tag.name)) + data.append({'category':category, 'tag_name':tag.name, + 'count':unicode(tag.count), 'rating':unicode(tag.avg_rating)}) + else: + for category in categories: + data.append({'category':category, + 'tag_name':_('CATEGORY ITEMS'), + 'count': len(category_data[category]), 'rating': 0.0}) + + fields = ['category', 'tag_name', 'count', 'rating'] + + def do_list(): + separator = ' ' + widths = list(map(lambda x : 0, fields)) + for i in data: + for j, field in enumerate(fields): + widths[j] = max(widths[j], max(len(field), len(unicode(i[field])))) + + screen_width = terminal_controller.COLS if opts.line_width < 0 else opts.line_width + if not screen_width: + screen_width = 80 + field_width = screen_width//len(fields) + base_widths = map(lambda x: min(x+1, field_width), widths) + + while sum(base_widths) < screen_width: + adjusted = False + for i in range(len(widths)): + if base_widths[i] < widths[i]: + base_widths[i] += min(screen_width-sum(base_widths), widths[i]-base_widths[i]) + adjusted = True + break + if not adjusted: + break + + widths = list(base_widths) + titles = map(lambda x, y: '%-*s%s'%(x-len(separator), y, separator), + widths, fields) + print terminal_controller.GREEN + ''.join(titles)+terminal_controller.NORMAL + + wrappers = map(lambda x: TextWrapper(x-1), widths) + o = cStringIO.StringIO() + + for record in data: + text = [wrappers[i].wrap(unicode(record[field]).encode('utf-8')) for i, field in enumerate(fields)] + lines = max(map(len, text)) + for l in range(lines): + for i, field in enumerate(text): + ft = text[i][l] if l < len(text[i]) else '' + filler = '%*s'%(widths[i]-len(ft)-1, '') + o.write(ft) + o.write(filler+separator) + print >>o + print o.getvalue() + + def do_csv(): + lf = '{category},"{tag_name}",{count},{rating}' + lf = lf.replace(',', opts.separator).replace(r'\t','\t').replace(r'\n','\n') + lf = lf.replace('"', opts.quote) + for d in data: + print lf.format(**d) + + if opts.csv: + do_csv() + else: + do_list() + + +COMMANDS = ('list', 'add', 'remove', 'add_format', 'remove_format', + 'show_metadata', 'set_metadata', 'export', 'catalog', + 'saved_searches', 'add_custom_column', 'custom_columns', + 'remove_custom_column', 'set_custom', 'restore_database', + 'check_library', 'list_categories') + + def option_parser(): parser = OptionParser(_( '''\ From 1942e843782c8c4f60555dc90614f04d0ec46a63 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Thu, 7 Oct 2010 14:50:57 +0100 Subject: [PATCH 07/12] BUg #7090 - Fix ISBN in save templates --- src/calibre/ebooks/metadata/book/base.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py index fa5a20283e..c18ad7dcd4 100644 --- a/src/calibre/ebooks/metadata/book/base.py +++ b/src/calibre/ebooks/metadata/book/base.py @@ -221,6 +221,11 @@ class Metadata(object): v = _data.get(attr, None) if v is not None: result[attr] = v + # separate these because it uses the self.get(), not _data.get() + for attr in TOP_LEVEL_CLASSIFIERS: + v = self.get(attr, None) + if v is not None: + result[attr] = v for attr in _data['user_metadata'].iterkeys(): v = self.get(attr, None) if v is not None: From 93c00c7bebc83e1710d1b13292441a977623de7e Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Thu, 7 Oct 2010 15:27:24 +0100 Subject: [PATCH 08/12] Allow the common field mispellings in composite columns --- src/calibre/ebooks/metadata/book/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py index c18ad7dcd4..0d08218790 100644 --- a/src/calibre/ebooks/metadata/book/base.py +++ b/src/calibre/ebooks/metadata/book/base.py @@ -38,6 +38,7 @@ class SafeFormat(TemplateFormatter): def get_value(self, key, args, kwargs): try: + key = field_metadata.search_term_to_field_key(key.lower()) b = self.book.get_user_metadata(key, False) if b and b['datatype'] == 'int' and self.book.get(key, 0) == 0: v = '' From 25648fe100f28882e2004d30408e2c8146d430c4 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 7 Oct 2010 08:54:48 -0600 Subject: [PATCH 09/12] Fix #7067 (New Yorker downloads not including images) --- resources/recipes/new_yorker.recipe | 59 ++++++++++++++++------------- 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/resources/recipes/new_yorker.recipe b/resources/recipes/new_yorker.recipe index d19c5fb6b0..87dea4534b 100644 --- a/resources/recipes/new_yorker.recipe +++ b/resources/recipes/new_yorker.recipe @@ -1,50 +1,57 @@ -#!/usr/bin/env python - __license__ = 'GPL v3' -__copyright__ = '2008-2009, Darko Miletic ' +__copyright__ = '2008-2010, Darko Miletic ' ''' newyorker.com ''' from calibre.web.feeds.news import BasicNewsRecipe -from calibre.ebooks.BeautifulSoup import Tag class NewYorker(BasicNewsRecipe): title = 'The New Yorker' __author__ = 'Darko Miletic' description = 'The best of US journalism' oldest_article = 15 - language = 'en' - + language = 'en' max_articles_per_feed = 100 no_stylesheets = True use_embedded_content = False publisher = 'Conde Nast Publications' category = 'news, politics, USA' encoding = 'cp1252' + publication_type = 'magazine' + masthead_url = 'http://www.newyorker.com/css/i/hed/logo.gif' + extra_css = """ + body {font-family: "Times New Roman",Times,serif} + .articleauthor{color: #9F9F9F; font-family: Arial, sans-serif; font-size: small; text-transform: uppercase} + .rubric{color: #CD0021; font-family: Arial, sans-serif; font-size: small; text-transform: uppercase} + """ - keep_only_tags = [dict(name='div', attrs={'id':'printbody'})] - remove_tags_after = dict(name='div',attrs={'id':'articlebody'}) - remove_tags = [ - dict(name='div', attrs={'class':['utils','articleRailLinks','icons'] }) - ,dict(name='link') - ] - - feeds = [(u'The New Yorker', u'http://feeds.newyorker.com/services/rss/feeds/everything.xml')] + conversion_options = { + 'comment' : description + , 'tags' : category + , 'publisher' : publisher + , 'language' : language + } + + keep_only_tags = [dict(name='div', attrs={'id':['articleheads','articleRail','articletext','photocredits']})] + remove_tags = [ + dict(name=['meta','iframe','base','link','embed','object']) + ,dict(name='div', attrs={'class':['utils','articleRailLinks','icons'] }) + ] + remove_attributes = ['lang'] + feeds = [(u'The New Yorker', u'http://feeds.newyorker.com/services/rss/feeds/everything.xml')] def print_version(self, url): return url + '?printable=true' - def get_article_url(self, article): - return article.get('guid', None) + def image_url_processor(self, baseurl, url): + return url.strip() - def postprocess_html(self, soup, x): - body = soup.find('body') - if body: - html = soup.find('html') - if html: - body.extract() - html.insert(2, body) - mcharset = Tag(soup,'meta',[("http-equiv","Content-Type"),("content","text/html; charset=utf-8")]) - soup.head.insert(1,mcharset) - return soup + def get_cover_url(self): + cover_url = None + soup = self.index_to_soup('http://www.newyorker.com/magazine/toc/') + cover_item = soup.find('img',attrs={'id':'inThisIssuePhoto'}) + if cover_item: + cover_url = 'http://www.newyorker.com' + cover_item['src'].strip() + return cover_url + \ No newline at end of file From a9e01673fd7cc01ddf322436a21b87cba52e56ad Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Thu, 7 Oct 2010 16:01:33 +0100 Subject: [PATCH 10/12] Fix template 'shorten' function to permit 0-length trailers. --- src/calibre/utils/formatter.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/calibre/utils/formatter.py b/src/calibre/utils/formatter.py index 50c08977cd..5e2cb6535a 100644 --- a/src/calibre/utils/formatter.py +++ b/src/calibre/utils/formatter.py @@ -59,10 +59,10 @@ class TemplateFormatter(string.Formatter): return value_if_empty def _shorten(self, val, leading, center_string, trailing): - l = int(leading) - t = int(trailing) + l = max(0, int(leading)) + t = max(0, int(trailing)) if len(val) > l + len(center_string) + t: - return val[0:l] + center_string + val[-t:] + return val[0:l] + center_string + ('' if t == 0 else val[-t:]) else: return val From c30059200cd6e95e0b48625feaf2704464042216 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Thu, 7 Oct 2010 16:34:27 +0100 Subject: [PATCH 11/12] Take saved searches out of the command line list_categories --- src/calibre/library/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/library/cli.py b/src/calibre/library/cli.py index 8ceb63a368..c1a1109cf8 100644 --- a/src/calibre/library/cli.py +++ b/src/calibre/library/cli.py @@ -1054,7 +1054,7 @@ def command_list_categories(args, dbpath): category_data = db.get_categories() data = [] categories = [k for k in category_data.keys() - if db.metadata_for_field(k)['kind'] != 'user'] + if db.metadata_for_field(k)['kind'] not in ['user', 'search']] categories.sort(cmp=lambda x,y: cmp(x if x[0] != '#' else x[1:], y if y[0] != '#' else y[1:])) From bfc753f794fc0bae9ed5b1ef748da8b277e06137 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 7 Oct 2010 11:49:43 -0600 Subject: [PATCH 12/12] Allow switching of library when device is connected --- src/calibre/gui2/actions/choose_library.py | 14 ++++++-------- src/calibre/gui2/layout.py | 14 +++++++++++--- src/calibre/gui2/ui.py | 7 +++++++ 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/src/calibre/gui2/actions/choose_library.py b/src/calibre/gui2/actions/choose_library.py index 2f8beab976..39f7159989 100644 --- a/src/calibre/gui2/actions/choose_library.py +++ b/src/calibre/gui2/actions/choose_library.py @@ -17,7 +17,7 @@ from calibre.gui2 import gprefs, warning_dialog, Dispatcher, error_dialog, \ question_dialog, info_dialog from calibre.gui2.actions import InterfaceAction -class LibraryUsageStats(object): +class LibraryUsageStats(object): # {{{ def __init__(self): self.stats = {} @@ -73,7 +73,7 @@ class LibraryUsageStats(object): if stats is not None: self.stats[newloc] = stats self.write_stats() - +# }}} class ChooseLibraryAction(InterfaceAction): @@ -147,9 +147,11 @@ class ChooseLibraryAction(InterfaceAction): self.qs_locations = [i[1] for i in locations] self.rename_menu.clear() self.delete_menu.clear() + quick_actions = [] for name, loc in locations: - self.quick_menu.addAction(name, Dispatcher(partial(self.switch_requested, + ac = self.quick_menu.addAction(name, Dispatcher(partial(self.switch_requested, loc))) + quick_actions.append(ac) self.rename_menu.addAction(name, Dispatcher(partial(self.rename_requested, name, loc))) self.delete_menu.addAction(name, Dispatcher(partial(self.delete_requested, @@ -164,6 +166,7 @@ class ChooseLibraryAction(InterfaceAction): self.quick_menu_action.setVisible(bool(locations)) self.rename_menu_action.setVisible(bool(locations)) self.delete_menu_action.setVisible(bool(locations)) + self.gui.location_manager.set_switch_actions(quick_actions) def location_selected(self, loc): @@ -263,11 +266,6 @@ class ChooseLibraryAction(InterfaceAction): c.exec_() def change_library_allowed(self): - if self.gui.device_connected: - warning_dialog(self.gui, _('Not allowed'), - _('You cannot change libraries when a device is' - ' connected.'), show=True) - return False if self.gui.job_manager.has_jobs(): warning_dialog(self.gui, _('Not allowed'), _('You cannot change libraries while jobs' diff --git a/src/calibre/gui2/layout.py b/src/calibre/gui2/layout.py index 6c237bd67b..0cd93f388c 100644 --- a/src/calibre/gui2/layout.py +++ b/src/calibre/gui2/layout.py @@ -24,6 +24,7 @@ class LocationManager(QObject): # {{{ locations_changed = pyqtSignal() unmount_device = pyqtSignal() location_selected = pyqtSignal(object) + switch_actions_set = pyqtSignal(object) def __init__(self, parent=None): QObject.__init__(self, parent) @@ -60,7 +61,7 @@ class LocationManager(QObject): # {{{ return ac - ac('library', _('Library'), 'lt.png', + self.library_action = ac('library', _('Library'), 'lt.png', _('Show books in calibre library')) ac('main', _('Device'), 'reader.png', _('Show books in the main memory of the device')) @@ -69,6 +70,13 @@ class LocationManager(QObject): # {{{ ac('cardb', _('Card B'), 'sd.png', _('Show books in storage card B')) + def set_switch_actions(self, actions): + self.switch_menu = QMenu() + for ac in actions: + self.switch_menu.addAction(ac) + self.library_action.setMenu(self.switch_menu) + self.switch_actions_set.emit(bool(actions)) + def _location_selected(self, location, *args): if location != self.current_location and hasattr(self, 'location_'+location): @@ -197,14 +205,14 @@ class SearchBar(QWidget): # {{{ # }}} -class Spacer(QWidget): +class Spacer(QWidget): # {{{ def __init__(self, parent): QWidget.__init__(self, parent) self.l = QHBoxLayout() self.setLayout(self.l) self.l.addStretch(10) - +# }}} class ToolBar(QToolBar): # {{{ diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 937b23b113..6ada31418a 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -390,6 +390,13 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{ except: import traceback traceback.print_exc() + if self.device_connected: + self.set_books_in_library(self.booklists(), reset=True) + self.refresh_ondevice() + self.memory_view.reset() + self.card_a_view.reset() + self.card_b_view.reset() + def set_window_title(self): self.setWindowTitle(__appname__ + u' - ||%s||'%self.iactions['Choose Library'].library_name())