diff --git a/resources/default_tweaks.py b/resources/default_tweaks.py index e9ad64cee2..bda839b28f 100644 --- a/resources/default_tweaks.py +++ b/resources/default_tweaks.py @@ -61,3 +61,13 @@ sort_columns_at_startup = None # default if not set: MMM yyyy gui_pubdate_display_format = 'MMM yyyy' +# Control title and series sorting in the library view. +# If set to 'library_order', Leading articles such as The and A will be ignored. +# If set to 'strictly_alphabetic', the titles will be sorted without processing +# For example, with library_order, The Client will sort under 'C'. With +# strictly_alphabetic, the book will sort under 'T'. +# This flag affects Calibre's library display. It has no effect on devices. In +# addition, titles for books added before changing the flag will retain their +# order until the title is edited. Double-clicking on a title and hitting return +# without changing anything is sufficient to change the sort. +title_series_sorting = 'library_order' diff --git a/resources/recipes/economist.recipe b/resources/recipes/economist.recipe index 4ae0bb8b05..a6d0e08eea 100644 --- a/resources/recipes/economist.recipe +++ b/resources/recipes/economist.recipe @@ -24,9 +24,10 @@ class Economist(BasicNewsRecipe): oldest_article = 7.0 cover_url = 'http://www.economist.com/images/covers/currentcoverus_large.jpg' remove_tags = [dict(name=['script', 'noscript', 'title', 'iframe', 'cf_floatingcontent']), - dict(attrs={'class':['dblClkTrk']})] - remove_tags_before = dict(name=lambda tag: tag.name=='title' and tag.parent.name=='body') + dict(attrs={'class':['dblClkTrk', 'ec-article-info']})] + keep_only_tags = [dict(id='ec-article-body')] needs_subscription = True + no_stylesheets = True preprocess_regexps = [(re.compile('.*', re.DOTALL), lambda x:'')] @@ -87,7 +88,7 @@ class Economist(BasicNewsRecipe): continue a = tag.find('a', href=True) if a is not None: - url=a['href'].replace('displaystory', 'PrinterFriendly').strip() + url=a['href'].split('?')[0]+'/print' if url.startswith('Printer'): url = '/'+url if url.startswith('/'): diff --git a/resources/recipes/economist_free.recipe b/resources/recipes/economist_free.recipe index cdcd457501..1a783521f6 100644 --- a/resources/recipes/economist_free.recipe +++ b/resources/recipes/economist_free.recipe @@ -17,8 +17,9 @@ class Economist(BasicNewsRecipe): oldest_article = 7.0 cover_url = 'http://www.economist.com/images/covers/currentcoverus_large.jpg' remove_tags = [dict(name=['script', 'noscript', 'title', 'iframe', 'cf_floatingcontent']), - dict(attrs={'class':['dblClkTrk']})] - remove_tags_before = dict(name=lambda tag: tag.name=='title' and tag.parent.name=='body') + dict(attrs={'class':['dblClkTrk', 'ec-article-info']})] + keep_only_tags = [dict(id='ec-article-body')] + no_stylesheets = True preprocess_regexps = [(re.compile('.*', re.DOTALL), lambda x:'')] @@ -88,19 +89,20 @@ class Economist(BasicNewsRecipe): br = browser() ret = br.open(url) raw = ret.read() - url = br.geturl().replace('displaystory', 'PrinterFriendly').strip() + url = br.geturl().split('?')[0]+'/print' root = html.fromstring(raw) - matches = root.xpath('//*[@class = "article-section"]') + matches = root.xpath('//*[@class = "ec-article-info"]') feedtitle = 'Miscellaneous' if matches: - feedtitle = string.capwords(html.tostring(matches[0], method='text', - encoding=unicode)) + feedtitle = string.capwords(html.tostring(matches[-1], method='text', + encoding=unicode).split('|')[-1].strip()) return (i, feedtitle, url, title, description, author, published) def eco_article_found(self, req, result): from calibre.web.feeds import Article i, feedtitle, link, title, description, author, published = result - self.log('Found print version for article:', title) + self.log('Found print version for article:', title, 'in', feedtitle, + 'at', link) a = Article(i, title, link, author, description, published, '') diff --git a/resources/recipes/smh.recipe b/resources/recipes/smh.recipe index 21643b9611..b5c7f4d54e 100644 --- a/resources/recipes/smh.recipe +++ b/resources/recipes/smh.recipe @@ -21,7 +21,7 @@ class Smh_au(BasicNewsRecipe): language = 'en_AU' remove_empty_feeds = True masthead_url = 'http://images.smh.com.au/2010/02/02/1087188/smh-620.jpg' - publication_type = 'newspaper' + publication_type = 'newspaper' extra_css = ' h1{font-family: Georgia,"Times New Roman",Times,serif } body{font-family: Arial,Helvetica,sans-serif} .cT-imageLandscape{font-size: x-small} ' conversion_options = { @@ -47,7 +47,7 @@ class Smh_au(BasicNewsRecipe): for itimg in soup.findAll('img',src=True): if itimg['src'].endswith('frontpage.jpg'): self.cover_url = itimg['src'] - + for item in soup.findAll(attrs={'class':'cN-storyHeadlineLead cfix'}): description = '' title_prefix = '' @@ -65,4 +65,4 @@ class Smh_au(BasicNewsRecipe): ,'url' :url ,'description':description }) - return [(soup.head.title.string, articles)] + return [(self.tag_to_string(soup.find('title')), articles)] diff --git a/src/calibre/devices/prs505/sony_cache.py b/src/calibre/devices/prs505/sony_cache.py index 5542e28d90..413d6959a6 100644 --- a/src/calibre/devices/prs505/sony_cache.py +++ b/src/calibre/devices/prs505/sony_cache.py @@ -12,7 +12,7 @@ from uuid import uuid4 from lxml import etree -from calibre import prints, guess_type +from calibre import prints, guess_type, iswindows from calibre.devices.errors import DeviceError from calibre.devices.usbms.driver import debug_print from calibre.constants import DEBUG @@ -423,7 +423,10 @@ class XMLCache(object): return ans def update_text_record(self, record, book, path, bl_index): - timestamp = os.path.getctime(path) + timestamp = os.path.getmtime(path) + # Correct for MS DST time 'adjustment' + if iswindows and time.daylight: + timestamp -= time.altzone - time.timezone date = strftime(timestamp) if date != record.get('date', None): record.set('date', date) diff --git a/src/calibre/devices/usbms/driver.py b/src/calibre/devices/usbms/driver.py index 92e57e7447..6f558b9b34 100644 --- a/src/calibre/devices/usbms/driver.py +++ b/src/calibre/devices/usbms/driver.py @@ -294,6 +294,18 @@ class USBMS(CLI, Device): self.report_progress(1.0, _('Sending metadata to device...')) debug_print('USBMS: finished sync_booklists') + @classmethod + def build_template_regexp(cls): + def replfunc(match): + if match.group(1) in ['title', 'series', 'series_index', 'isbn']: + return '(?P<' + match.group(1) + '>.+?)' + elif match.group(1) == 'authors': + return '(?P.+?)' + else: + return '(.+?)' + template = cls.save_template().rpartition('/')[2] + return re.compile(re.sub('{([^}]*)}', replfunc, template) + '([_\d]*$)') + @classmethod def path_to_unicode(cls, path): if isbytestring(path): @@ -355,22 +367,22 @@ class USBMS(CLI, Device): from calibre.ebooks.metadata.meta import metadata_from_formats from calibre.customize.ui import quick_metadata with quick_metadata: - return metadata_from_formats(fmts) + return metadata_from_formats(fmts, force_read_metadata=True, + pattern=cls.build_template_regexp()) @classmethod - def book_from_path(cls, prefix, path): + def book_from_path(cls, prefix, lpath): from calibre.ebooks.metadata import MetaInformation if cls.settings().read_metadata or cls.MUST_READ_METADATA: - mi = cls.metadata_from_path(cls.normalize_path(os.path.join(prefix, path))) + mi = cls.metadata_from_path(cls.normalize_path(os.path.join(prefix, lpath))) else: from calibre.ebooks.metadata.meta import metadata_from_filename - mi = metadata_from_filename(cls.normalize_path(os.path.basename(path)), - re.compile(r'^(?P[ \S]+?)[ _]-[ _](?P<author>[ \S]+?)_+\d+')) - + mi = metadata_from_filename(cls.normalize_path(os.path.basename(lpath)), + cls.build_template_regexp()) if mi is None: - mi = MetaInformation(os.path.splitext(os.path.basename(path))[0], + mi = MetaInformation(os.path.splitext(os.path.basename(lpath))[0], [_('Unknown')]) - size = os.stat(cls.normalize_path(os.path.join(prefix, path))).st_size - book = cls.book_class(prefix, path, other=mi, size=size) + size = os.stat(cls.normalize_path(os.path.join(prefix, lpath))).st_size + book = cls.book_class(prefix, lpath, other=mi, size=size) return book diff --git a/src/calibre/ebooks/metadata/meta.py b/src/calibre/ebooks/metadata/meta.py index f5a327a0d6..eae8171362 100644 --- a/src/calibre/ebooks/metadata/meta.py +++ b/src/calibre/ebooks/metadata/meta.py @@ -27,16 +27,16 @@ for i, ext in enumerate(_METADATA_PRIORITIES): def path_to_ext(path): return os.path.splitext(path)[1][1:].lower() -def metadata_from_formats(formats): +def metadata_from_formats(formats, force_read_metadata=False, pattern=None): try: - return _metadata_from_formats(formats) + return _metadata_from_formats(formats, force_read_metadata, pattern) except: - mi = metadata_from_filename(list(iter(formats))[0]) + mi = metadata_from_filename(list(iter(formats), pattern)[0]) if not mi.authors: mi.authors = [_('Unknown')] return mi -def _metadata_from_formats(formats): +def _metadata_from_formats(formats, force_read_metadata=False, pattern=None): mi = MetaInformation(None, None) formats.sort(cmp=lambda x,y: cmp(METADATA_PRIORITIES[path_to_ext(x)], METADATA_PRIORITIES[path_to_ext(y)])) @@ -51,7 +51,9 @@ def _metadata_from_formats(formats): with open(path, 'rb') as stream: try: newmi = get_metadata(stream, stream_type=ext, - use_libprs_metadata=True) + use_libprs_metadata=True, + force_read_metadata=force_read_metadata, + pattern=pattern) mi.smart_update(newmi) except: continue @@ -69,18 +71,21 @@ def is_recipe(filename): return filename.startswith('calibre') and \ filename.rpartition('.')[0].endswith('_recipe_out') -def get_metadata(stream, stream_type='lrf', use_libprs_metadata=False): +def get_metadata(stream, stream_type='lrf', use_libprs_metadata=False, + force_read_metadata=False, pattern=None): pos = 0 if hasattr(stream, 'tell'): pos = stream.tell() try: - return _get_metadata(stream, stream_type, use_libprs_metadata) + return _get_metadata(stream, stream_type, use_libprs_metadata, + force_read_metadata, pattern) finally: if hasattr(stream, 'seek'): stream.seek(pos) -def _get_metadata(stream, stream_type, use_libprs_metadata): +def _get_metadata(stream, stream_type, use_libprs_metadata, + force_read_metadata=False, pattern=None): if stream_type: stream_type = stream_type.lower() if stream_type in ('html', 'html', 'xhtml', 'xhtm', 'xml'): stream_type = 'html' @@ -100,8 +105,8 @@ def _get_metadata(stream, stream_type, use_libprs_metadata): mi = MetaInformation(None, None) name = os.path.basename(getattr(stream, 'name', '')) - base = metadata_from_filename(name) - if is_recipe(name) or prefs['read_file_metadata']: + base = metadata_from_filename(name, pat=pattern) + if force_read_metadata or is_recipe(name) or prefs['read_file_metadata']: mi = get_file_type_metadata(stream, stream_type) if base.title == os.path.splitext(name)[0] and base.authors is None: # Assume that there was no metadata in the file and the user set pattern @@ -139,7 +144,7 @@ def metadata_from_filename(name, pat=None): pat = re.compile(prefs.get('filename_pattern')) name = name.replace('_', ' ') match = pat.search(name) - if match: + if match is not None: try: mi.title = match.group('title') except IndexError: diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index bddefe97f8..cbe9449f1f 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -99,6 +99,8 @@ def _config(): help=_('Limit max simultaneous jobs to number of CPUs')) c.add_opt('tag_browser_hidden_categories', default=set(), help=_('tag browser categories not to display')) + c.add_opt('gui_layout', choices=['wide', 'narrow'], + help=_('The layout of the user interface'), default='narrow') return ConfigProxy(c) config = _config() @@ -125,6 +127,11 @@ def available_width(): desktop = QCoreApplication.instance().desktop() return desktop.availableGeometry().width() +try: + is_widescreen = float(available_width())/available_height() > 1.4 +except: + is_widescreen = True + def extension(path): return os.path.splitext(path)[1][1:].lower() diff --git a/src/calibre/gui2/cover_flow.py b/src/calibre/gui2/cover_flow.py index 98a36532da..1ac53be132 100644 --- a/src/calibre/gui2/cover_flow.py +++ b/src/calibre/gui2/cover_flow.py @@ -10,10 +10,11 @@ Module to implement the Cover Flow feature import sys, os, time from PyQt4.Qt import QImage, QSizePolicy, QTimer, QDialog, Qt, QSize, \ - QStackedLayout + QStackedLayout, QLabel from calibre import plugins from calibre.gui2 import config, available_height, available_width + pictureflow, pictureflowerror = plugins['pictureflow'] if pictureflow is not None: @@ -79,12 +80,15 @@ if pictureflow is not None: def __init__(self, parent=None): pictureflow.PictureFlow.__init__(self, parent, config['cover_flow_queue_length']+1) - self.setMinimumSize(QSize(10, 10)) + self.setMinimumSize(QSize(300, 150)) self.setFocusPolicy(Qt.WheelFocus) self.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)) self.setZoomFactor(150) + def sizeHint(self): + return self.minimumSize() + def wheelEvent(self, ev): ev.accept() if ev.delta() < 0: @@ -108,56 +112,49 @@ class CoverFlowMixin(object): self.cover_flow_sync_timer.timeout.connect(self.cover_flow_do_sync) self.cover_flow_sync_flag = True self.cover_flow = CoverFlow(parent=self) - self.cover_flow.setVisible(False) - if not config['separate_cover_flow']: - self.cb_layout.addWidget(self.cover_flow) self.cover_flow.currentChanged.connect(self.sync_listview_to_cf) self.library_view.selectionModel().currentRowChanged.connect( self.sync_cf_to_listview) self.db_images = DatabaseImages(self.library_view.model()) self.cover_flow.setImages(self.db_images) - ah, aw = available_height(), available_width() - self._cb_layout_is_horizontal = float(aw)/ah >= 1.4 - self.cb_layout.setDirection(self.cb_layout.LeftToRight if - self._cb_layout_is_horizontal else - self.cb_layout.TopToBottom) - - def toggle_cover_flow_visibility(self, show): + else: + self.cover_flow = QLabel('<p>'+_('Cover browser could not be loaded') + +'<br>'+pictureflowerror) + self.cover_flow.setWordWrap(True) if config['separate_cover_flow']: - if show: - d = QDialog(self) - ah, aw = available_height(), available_width() - d.resize(int(aw/1.5), ah-60) - d._layout = QStackedLayout() - d.setLayout(d._layout) - d.setWindowTitle(_('Browse by covers')) - d.layout().addWidget(self.cover_flow) - self.cover_flow.setVisible(True) - self.cover_flow.setFocus(Qt.OtherFocusReason) - d.show() - d.finished.connect(self.sidebar.external_cover_flow_finished) - self.cf_dialog = d - else: - cfd = getattr(self, 'cf_dialog', None) - if cfd is not None: - self.cover_flow.setVisible(False) - cfd.hide() - self.cf_dialog = None + self.cb_splitter.button.clicked.connect(self.toggle_cover_browser) + if CoverFlow is not None: + self.cover_flow.stop.connect(self.hide_cover_browser) else: - if show: - self.cover_flow.setVisible(True) - self.cover_flow.setFocus(Qt.OtherFocusReason) - else: - self.cover_flow.setVisible(False) + self.cb_splitter.insertWidget(self.cb_splitter.side_index, self.cover_flow) + if CoverFlow is not None: + self.cover_flow.stop.connect(self.cb_splitter.hide_side_pane) + self.cb_splitter.button.toggled.connect(self.cover_browser_toggled) - def toggle_cover_flow(self, show): - if show: - self.cover_flow.setCurrentSlide(self.library_view.currentIndex().row()) - self.library_view.setCurrentIndex( - self.library_view.currentIndex()) - self.cover_flow_sync_timer.start(500) - self.library_view.scroll_to_row(self.library_view.currentIndex().row()) + def toggle_cover_browser(self): + cbd = getattr(self, 'cb_dialog', None) + if cbd is not None: + self.hide_cover_browser() else: + self.show_cover_browser() + + def cover_browser_toggled(self, *args): + if self.cb_splitter.button.isChecked(): + self.cover_browser_shown() + else: + self.cover_browser_hidden() + + def cover_browser_shown(self): + self.cover_flow.setFocus(Qt.OtherFocusReason) + if CoverFlow is not None: + self.cover_flow.setCurrentSlide(self.library_view.currentIndex().row()) + self.cover_flow_sync_timer.start(500) + self.library_view.setCurrentIndex( + self.library_view.currentIndex()) + self.library_view.scroll_to_row(self.library_view.currentIndex().row()) + + def cover_browser_hidden(self): + if CoverFlow is not None: self.cover_flow_sync_timer.stop() idx = self.library_view.model().index(self.cover_flow.currentSlide(), 0) if idx.isValid(): @@ -165,7 +162,28 @@ class CoverFlowMixin(object): sm.select(idx, sm.ClearAndSelect|sm.Rows) self.library_view.setCurrentIndex(idx) self.library_view.scroll_to_row(idx.row()) - self.toggle_cover_flow_visibility(show) + + + def show_cover_browser(self): + d = QDialog(self) + ah, aw = available_height(), available_width() + d.resize(int(aw/1.5), ah-60) + d._layout = QStackedLayout() + d.setLayout(d._layout) + d.setWindowTitle(_('Browse by covers')) + d.layout().addWidget(self.cover_flow) + self.cover_flow.setVisible(True) + self.cover_flow.setFocus(Qt.OtherFocusReason) + d.show() + self.cb_splitter.button.set_state_to_hide() + d.finished.connect(self.cb_splitter.button.set_state_to_show) + self.cb_dialog = d + + def hide_cover_browser(self): + cbd = getattr(self, 'cb_dialog', None) + if cbd is not None: + cbd.accept() + self.cb_dialog = None def sync_cf_to_listview(self, current, previous): if self.cover_flow_sync_flag and self.cover_flow.isVisible() and \ diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 1445b0f36e..d806890807 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -391,16 +391,14 @@ class DeviceMenu(QMenu): # {{{ default_account = (dest, False, False, I('mail.svg'), _('Email to')+' '+account) action1 = DeviceAction(dest, False, False, I('mail.svg'), - _('Email to')+' '+account, self) + _('Email to')+' '+account) action2 = DeviceAction(dest, True, False, I('mail.svg'), - _('Email to')+' '+account, self) + _('Email to')+' '+account+ _(' and delete from library')) map(self.email_to_menu.addAction, (action1, action2)) map(self._memory.append, (action1, action2)) self.email_to_menu.addSeparator() - self.connect(action1, SIGNAL('a_s(QAction)'), - self.action_triggered) - self.connect(action2, SIGNAL('a_s(QAction)'), - self.action_triggered) + action1.a_s.connect(self.action_triggered) + action2.a_s.connect(self.action_triggered) basic_actions = [ ('main:', False, False, I('reader.svg'), @@ -1140,6 +1138,13 @@ class DeviceMixin(object): in cache['authors']: loc[i] = True continue + # Also check author sort, because it can be used as author in + # some formats + if mi.author_sort and \ + re.sub('(?u)\W|[_]', '', mi.author_sort.lower()) \ + in cache['authors']: + loc[i] = True + continue return loc def set_books_in_library(self, booklists, reset=False): @@ -1152,10 +1157,16 @@ class DeviceMixin(object): mi = db.get_metadata(id, index_is_id=True) title = re.sub('(?u)\W|[_]', '', mi.title.lower()) if title not in self.db_book_title_cache: - self.db_book_title_cache[title] = {'authors':{}, 'db_ids':{}} - authors = authors_to_string(mi.authors).lower() if mi.authors else '' - authors = re.sub('(?u)\W|[_]', '', authors) - self.db_book_title_cache[title]['authors'][authors] = mi + self.db_book_title_cache[title] = \ + {'authors':{}, 'author_sort':{}, 'db_ids':{}} + if mi.authors: + authors = authors_to_string(mi.authors).lower() + authors = re.sub('(?u)\W|[_]', '', authors) + self.db_book_title_cache[title]['authors'][authors] = mi + if mi.author_sort: + aus = mi.author_sort.lower() + aus = re.sub('(?u)\W|[_]', '', aus) + self.db_book_title_cache[title]['author_sort'][aus] = mi self.db_book_title_cache[title]['db_ids'][mi.application_id] = mi self.db_book_uuid_cache.add(mi.uuid) @@ -1186,12 +1197,19 @@ class DeviceMixin(object): book.smart_update(d['db_ids'][book.db_id]) resend_metadata = True continue - book_authors = authors_to_string(book.authors).lower() if book.authors else '' - book_authors = re.sub('(?u)\W|[_]', '', book_authors) - if book_authors in d['authors']: - book.in_library = True - book.smart_update(d['authors'][book_authors]) - resend_metadata = True + if book.authors: + # Compare against both author and author sort, because + # either can appear as the author + book_authors = authors_to_string(book.authors).lower() + book_authors = re.sub('(?u)\W|[_]', '', book_authors) + if book_authors in d['authors']: + book.in_library = True + book.smart_update(d['authors'][book_authors]) + resend_metadata = True + elif book_authors in d['author_sort']: + book.in_library = True + book.smart_update(d['author_sort'][book_authors]) + resend_metadata = True # Set author_sort if it isn't already asort = getattr(book, 'author_sort', None) if not asort and book.authors: diff --git a/src/calibre/gui2/init.py b/src/calibre/gui2/init.py index a991c4d1f8..93f3a7c623 100644 --- a/src/calibre/gui2/init.py +++ b/src/calibre/gui2/init.py @@ -7,12 +7,16 @@ __docformat__ = 'restructuredtext en' import functools -from PyQt4.Qt import QMenu, Qt, pyqtSignal, QToolButton, QIcon +from PyQt4.Qt import QMenu, Qt, pyqtSignal, QToolButton, QIcon, QStackedWidget, \ + QWidget, QHBoxLayout, QToolBar, QSize, QSizePolicy from calibre.utils.config import prefs from calibre.ebooks import BOOK_EXTENSIONS -from calibre.constants import isosx -from calibre.gui2 import config +from calibre.constants import isosx, __appname__ +from calibre.gui2 import config, is_widescreen +from calibre.gui2.library.views import BooksView, DeviceBooksView +from calibre.gui2.widgets import Splitter +from calibre.gui2.tag_view import TagBrowserWidget _keep_refs = [] @@ -275,7 +279,123 @@ class LibraryViewMixin(object): # {{{ if search: self.search.set_search_string(join.join(search)) - + def search_done(self, view, ok): + if view is self.current_view(): + self.search.search_done(ok) + self.set_number_of_books_shown() # }}} +class LibraryWidget(Splitter): # {{{ + + def __init__(self, parent): + orientation = Qt.Vertical if config['gui_layout'] == 'narrow' and \ + not is_widescreen else Qt.Horizontal + #orientation = Qt.Vertical + idx = 0 if orientation == Qt.Vertical else 1 + Splitter.__init__(self, 'cover_browser_splitter', _('Cover Browser'), + I('cover_flow.svg'), + orientation=orientation, parent=parent, + connect_button=not config['separate_cover_flow'], + side_index=idx, initial_side_size=400, initial_show=False) + parent.library_view = BooksView(parent) + parent.library_view.setObjectName('library_view') + self.addWidget(parent.library_view) +# }}} + +class Stack(QStackedWidget): # {{{ + + def __init__(self, parent): + QStackedWidget.__init__(self, parent) + + parent.cb_splitter = LibraryWidget(parent) + self.tb_widget = TagBrowserWidget(parent) + parent.tb_splitter = Splitter('tag_browser_splitter', + _('Tag Browser'), I('tags.svg'), + parent=parent, side_index=0, initial_side_size=200) + parent.tb_splitter.addWidget(self.tb_widget) + parent.tb_splitter.addWidget(parent.cb_splitter) + parent.tb_splitter.setCollapsible(parent.tb_splitter.other_index, False) + + self.addWidget(parent.tb_splitter) + for x in ('memory', 'card_a', 'card_b'): + name = x+'_view' + w = DeviceBooksView(parent) + setattr(parent, name, w) + self.addWidget(w) + w.setObjectName(name) + + +# }}} + +class SideBar(QToolBar): # {{{ + + + def __init__(self, splitters, jobs_button, parent=None): + QToolBar.__init__(self, _('Side bar'), parent) + self.setOrientation(Qt.Vertical) + self.setMovable(False) + self.setFloatable(False) + self.setToolButtonStyle(Qt.ToolButtonIconOnly) + self.setIconSize(QSize(48, 48)) + self.spacer = QWidget(self) + self.spacer.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding) + for s in splitters: + self.addWidget(s.button) + self.addWidget(self.spacer) + self.addWidget(jobs_button) + + for ch in self.children(): + if isinstance(ch, QToolButton): + ch.setCursor(Qt.PointingHandCursor) + +# }}} + +class LayoutMixin(object): # {{{ + + def __init__(self): + self.setupUi(self) + self.setWindowTitle(__appname__) + + if config['gui_layout'] == 'narrow': + from calibre.gui2.status import StatusBar + self.status_bar = StatusBar(self) + self.stack = Stack(self) + self.bd_splitter = Splitter('book_details_splitter', + _('Book Details'), I('book.svg'), + orientation=Qt.Vertical, parent=self, side_index=1) + self._layout_mem = [QWidget(self), QHBoxLayout()] + self._layout_mem[0].setLayout(self._layout_mem[1]) + l = self._layout_mem[1] + l.addWidget(self.stack) + self.sidebar = SideBar([getattr(self, x+'_splitter') + for x in ('bd', 'tb', 'cb')], self.jobs_button, parent=self) + l.addWidget(self.sidebar) + self.bd_splitter.addWidget(self._layout_mem[0]) + self.bd_splitter.addWidget(self.status_bar) + self.bd_splitter.setCollapsible((self.bd_splitter.side_index+1)%2, False) + self.centralwidget.layout().addWidget(self.bd_splitter) + + def finalize_layout(self): + m = self.library_view.model() + if m.rowCount(None) > 0: + self.library_view.set_current_row(0) + m.current_changed(self.library_view.currentIndex(), + self.library_view.currentIndex()) + + + def save_layout_state(self): + for x in ('library', 'memory', 'card_a', 'card_b'): + getattr(self, x+'_view').save_state() + + for x in ('cb', 'tb', 'bd'): + getattr(self, x+'_splitter').save_state() + + def read_layout_settings(self): + # View states are restored automatically when set_database is called + + for x in ('cb', 'tb', 'bd'): + getattr(self, x+'_splitter').restore_state() + +# }}} + diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index 71f41eab45..293a522a9e 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -200,7 +200,7 @@ class BooksModel(QAbstractTableModel): # {{{ self.count_changed() self.clear_caches() self.reset() - + return ids def delete_books_by_id(self, ids): for id in ids: @@ -882,6 +882,15 @@ class DeviceBooksModel(BooksModel): # {{{ ans.extend(v) return ans + def clear_ondevice(self, db_ids): + for data in self.db: + if data is None: + continue + app_id = getattr(data, 'application_id', None) + if app_id is not None and app_id in db_ids: + data.in_library = False + self.reset() + def flags(self, index): if self.map[index.row()] in self.indices_to_be_deleted(): return Qt.ItemIsUserCheckable # Can't figure out how to get the disabled flag in python diff --git a/src/calibre/gui2/library/views.py b/src/calibre/gui2/library/views.py index 109b001925..6d8efd68fa 100644 --- a/src/calibre/gui2/library/views.py +++ b/src/calibre/gui2/library/views.py @@ -26,6 +26,15 @@ class BooksView(QTableView): # {{{ def __init__(self, parent, modelcls=BooksModel): QTableView.__init__(self, parent) + + self.setDragEnabled(True) + self.setDragDropOverwriteMode(False) + self.setDragDropMode(self.DragDrop) + self.setAlternatingRowColors(True) + self.setSelectionBehavior(self.SelectRows) + self.setShowGrid(False) + self.setWordWrap(False) + self.rating_delegate = RatingDelegate(self) self.timestamp_delegate = DateDelegate(self) self.pubdate_delegate = PubDateDelegate(self) @@ -434,6 +443,18 @@ class BooksView(QTableView): # {{{ self.scrollTo(self.model().index(row, i)) break + def set_current_row(self, row, select=True): + if row > -1: + h = self.horizontalHeader() + for i in range(h.count()): + if not h.isSectionHidden(i): + index = self.model().index(row, i) + self.setCurrentIndex(index) + if select: + sm = self.selectionModel() + sm.select(index, sm.ClearAndSelect|sm.Rows) + break + def close(self): self._model.close() diff --git a/src/calibre/gui2/main.ui b/src/calibre/gui2/main.ui index b4bafc3b79..c863ff28f5 100644 --- a/src/calibre/gui2/main.ui +++ b/src/calibre/gui2/main.ui @@ -169,6 +169,12 @@ </item> <item> <widget class="QComboBox" name="search_restriction"> + <property name="maximumSize"> + <size> + <width>150</width> + <height>16777215</height> + </size> + </property> <property name="toolTip"> <string>Books display will be restricted to those matching the selected saved search</string> </property> @@ -304,270 +310,6 @@ </item> </layout> </item> - <item> - <widget class="Splitter" name="vertical_splitter"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Preferred" vsizetype="Expanding"> - <horstretch>0</horstretch> - <verstretch>100</verstretch> - </sizepolicy> - </property> - <property name="orientation"> - <enum>Qt::Vertical</enum> - </property> - <widget class="QWidget" name="layoutWidget"> - <layout class="QHBoxLayout" name="horizontalLayout"> - <item> - <widget class="QStackedWidget" name="stack"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> - <horstretch>100</horstretch> - <verstretch>100</verstretch> - </sizepolicy> - </property> - <property name="currentIndex"> - <number>0</number> - </property> - <widget class="QWidget" name="library"> - <layout class="QVBoxLayout" name="verticalLayout_4"> - <item> - <widget class="Splitter" name="horizontal_splitter"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <widget class="QWidget" name="layoutWidget"> - <layout class="QVBoxLayout" name="verticalLayout"> - <item> - <widget class="TagsView" name="tags_view"> - <property name="tabKeyNavigation"> - <bool>true</bool> - </property> - <property name="alternatingRowColors"> - <bool>true</bool> - </property> - <property name="animated"> - <bool>true</bool> - </property> - <property name="headerHidden"> - <bool>true</bool> - </property> - </widget> - </item> - <item> - <widget class="QCheckBox" name="popularity"> - <property name="text"> - <string>Sort by &popularity</string> - </property> - </widget> - </item> - <item> - <widget class="QComboBox" name="tag_match"> - <property name="currentIndex"> - <number>0</number> - </property> - <item> - <property name="text"> - <string>Match any</string> - </property> - </item> - <item> - <property name="text"> - <string>Match all</string> - </property> - </item> - </widget> - </item> - <item> - <widget class="QPushButton" name="edit_categories"> - <property name="toolTip"> - <string>Create, edit, and delete user categories</string> - </property> - <property name="text"> - <string>Manage &user categories</string> - </property> - </widget> - </item> - </layout> - </widget> - <widget class="QWidget" name=""> - <layout class="QVBoxLayout" name="cb_layout"> - <item> - <widget class="BooksView" name="library_view"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> - <horstretch>100</horstretch> - <verstretch>10</verstretch> - </sizepolicy> - </property> - <property name="acceptDrops"> - <bool>true</bool> - </property> - <property name="dragEnabled"> - <bool>true</bool> - </property> - <property name="dragDropOverwriteMode"> - <bool>false</bool> - </property> - <property name="dragDropMode"> - <enum>QAbstractItemView::DragDrop</enum> - </property> - <property name="alternatingRowColors"> - <bool>true</bool> - </property> - <property name="selectionBehavior"> - <enum>QAbstractItemView::SelectRows</enum> - </property> - <property name="showGrid"> - <bool>false</bool> - </property> - <property name="wordWrap"> - <bool>false</bool> - </property> - </widget> - </item> - </layout> - </widget> - </widget> - </item> - </layout> - </widget> - <widget class="QWidget" name="main_memory"> - <layout class="QGridLayout"> - <item row="0" column="0"> - <widget class="DeviceBooksView" name="memory_view"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> - <horstretch>100</horstretch> - <verstretch>10</verstretch> - </sizepolicy> - </property> - <property name="acceptDrops"> - <bool>true</bool> - </property> - <property name="dragEnabled"> - <bool>true</bool> - </property> - <property name="dragDropOverwriteMode"> - <bool>false</bool> - </property> - <property name="dragDropMode"> - <enum>QAbstractItemView::DragDrop</enum> - </property> - <property name="alternatingRowColors"> - <bool>true</bool> - </property> - <property name="selectionBehavior"> - <enum>QAbstractItemView::SelectRows</enum> - </property> - <property name="showGrid"> - <bool>false</bool> - </property> - <property name="wordWrap"> - <bool>false</bool> - </property> - </widget> - </item> - </layout> - </widget> - <widget class="QWidget" name="card_a_memory"> - <layout class="QGridLayout"> - <item row="0" column="0"> - <widget class="DeviceBooksView" name="card_a_view"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Preferred" vsizetype="Expanding"> - <horstretch>10</horstretch> - <verstretch>10</verstretch> - </sizepolicy> - </property> - <property name="acceptDrops"> - <bool>true</bool> - </property> - <property name="dragEnabled"> - <bool>true</bool> - </property> - <property name="dragDropOverwriteMode"> - <bool>false</bool> - </property> - <property name="dragDropMode"> - <enum>QAbstractItemView::DragDrop</enum> - </property> - <property name="alternatingRowColors"> - <bool>true</bool> - </property> - <property name="selectionBehavior"> - <enum>QAbstractItemView::SelectRows</enum> - </property> - <property name="showGrid"> - <bool>false</bool> - </property> - <property name="wordWrap"> - <bool>false</bool> - </property> - </widget> - </item> - </layout> - </widget> - <widget class="QWidget" name="card_b_memory"> - <layout class="QGridLayout"> - <item row="0" column="0"> - <widget class="DeviceBooksView" name="card_b_view"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Preferred" vsizetype="Expanding"> - <horstretch>10</horstretch> - <verstretch>10</verstretch> - </sizepolicy> - </property> - <property name="acceptDrops"> - <bool>true</bool> - </property> - <property name="dragEnabled"> - <bool>true</bool> - </property> - <property name="dragDropOverwriteMode"> - <bool>false</bool> - </property> - <property name="dragDropMode"> - <enum>QAbstractItemView::DragDrop</enum> - </property> - <property name="alternatingRowColors"> - <bool>true</bool> - </property> - <property name="selectionBehavior"> - <enum>QAbstractItemView::SelectRows</enum> - </property> - <property name="showGrid"> - <bool>false</bool> - </property> - <property name="wordWrap"> - <bool>false</bool> - </property> - </widget> - </item> - </layout> - </widget> - </widget> - </item> - <item> - <widget class="SideBar" name="sidebar" native="true"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Minimum" vsizetype="Preferred"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="minimumSize"> - <size> - <width>30</width> - <height>0</height> - </size> - </property> - </widget> - </item> - </layout> - </widget> - <widget class="StatusBar" name="status_bar" native="true"/> - </widget> - </item> </layout> </widget> <widget class="QToolBar" name="tool_bar"> @@ -804,26 +546,11 @@ </action> </widget> <customwidgets> - <customwidget> - <class>BooksView</class> - <extends>QTableView</extends> - <header>calibre/gui2/library/views.h</header> - </customwidget> <customwidget> <class>LocationView</class> <extends>QListView</extends> <header>widgets.h</header> </customwidget> - <customwidget> - <class>DeviceBooksView</class> - <extends>QTableView</extends> - <header>calibre/gui2/library/views.h</header> - </customwidget> - <customwidget> - <class>TagsView</class> - <extends>QTreeView</extends> - <header>calibre/gui2/tag_view.h</header> - </customwidget> <customwidget> <class>SearchBox2</class> <extends>QComboBox</extends> @@ -834,24 +561,6 @@ <extends>QComboBox</extends> <header>calibre.gui2.search_box</header> </customwidget> - <customwidget> - <class>StatusBar</class> - <extends>QWidget</extends> - <header>calibre/gui2/status.h</header> - <container>1</container> - </customwidget> - <customwidget> - <class>Splitter</class> - <extends>QSplitter</extends> - <header>calibre/gui2/widgets.h</header> - <container>1</container> - </customwidget> - <customwidget> - <class>SideBar</class> - <extends>QWidget</extends> - <header>calibre/gui2/sidebar.h</header> - <container>1</container> - </customwidget> </customwidgets> <resources> <include location="../../../resources/images.qrc"/> diff --git a/src/calibre/gui2/pictureflow/pictureflow.cpp b/src/calibre/gui2/pictureflow/pictureflow.cpp index 9bb9a0954c..60985a1a12 100644 --- a/src/calibre/gui2/pictureflow/pictureflow.cpp +++ b/src/calibre/gui2/pictureflow/pictureflow.cpp @@ -713,8 +713,9 @@ void PictureFlowPrivate::render() QPainter painter; painter.begin(&buffer); - QFont font("Arial", FONT_SIZE); + QFont font = QFont(); font.setBold(true); + font.setPointSize(FONT_SIZE); painter.setFont(font); painter.setPen(Qt::white); //painter.setPen(QColor(255,255,255,127)); @@ -763,8 +764,9 @@ void PictureFlowPrivate::render() QPainter painter; painter.begin(&buffer); - QFont font("Arial", FONT_SIZE); + QFont font = QFont(); font.setBold(true); + font.setPointSize(FONT_SIZE); painter.setFont(font); int leftTextIndex = (step>0) ? centerIndex : centerIndex-1; diff --git a/src/calibre/gui2/search_box.py b/src/calibre/gui2/search_box.py index 4e9ccc2900..17a815c4ce 100644 --- a/src/calibre/gui2/search_box.py +++ b/src/calibre/gui2/search_box.py @@ -7,11 +7,15 @@ __copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>' __docformat__ = 'restructuredtext en' from PyQt4.Qt import QComboBox, Qt, QLineEdit, QStringList, pyqtSlot, \ - pyqtSignal, SIGNAL + pyqtSignal, SIGNAL, QObject, QDialog from PyQt4.QtGui import QCompleter from calibre.gui2 import config from calibre.gui2.dialogs.confirm_delete import confirm +from calibre.gui2.dialogs.saved_search_editor import SavedSearchEditor +from calibre.gui2.dialogs.search import SearchDialog +from calibre.utils.config import prefs +from calibre.utils.search_query_parser import saved_searches class SearchLineEdit(QLineEdit): @@ -79,8 +83,7 @@ class SearchBox2(QComboBox): self.setMinimumContentsLength(25) self._in_a_search = False - def initialize(self, opt_name, colorize=False, - help_text=_('Search')): + def initialize(self, opt_name, colorize=False, help_text=_('Search')): self.as_you_type = config['search_as_you_type'] self.opt_name = opt_name self.addItems(QStringList(list(set(config[opt_name])))) @@ -239,9 +242,9 @@ class SavedSearchBox(QComboBox): self.setInsertPolicy(self.NoInsert) self.setSizeAdjustPolicy(self.AdjustToMinimumContentsLengthWithIcon) self.setMinimumContentsLength(10) + self.tool_tip_text = self.toolTip() def initialize(self, _saved_searches, _search_box, colorize=False, help_text=_('Search')): - self.tool_tip_text = self.toolTip() self.saved_searches = _saved_searches self.search_box = _search_box self.help_text = help_text @@ -331,3 +334,69 @@ class SavedSearchBox(QComboBox): if idx < 0: return self.search_box.set_search_string(self.saved_searches.lookup(unicode(self.currentText()))) + +class SearchBoxMixin(object): + + def __init__(self): + self.search.initialize('main_search_history', colorize=True, + help_text=_('Search (For Advanced Search click the button to the left)')) + self.connect(self.search, SIGNAL('cleared()'), self.search_box_cleared) + self.connect(self.clear_button, SIGNAL('clicked()'), self.search.clear) + QObject.connect(self.advanced_search_button, SIGNAL('clicked(bool)'), + self.do_advanced_search) + + self.search.clear() + self.search.setFocus(Qt.OtherFocusReason) + self.search.setMaximumWidth(self.width()-150) + + def search_box_cleared(self): + self.tags_view.clear() + self.saved_search.clear_to_help() + self.set_number_of_books_shown() + + def do_advanced_search(self, *args): + d = SearchDialog(self) + if d.exec_() == QDialog.Accepted: + self.search.set_search_string(d.search_string()) + +class SavedSearchBoxMixin(object): + + def __init__(self): + self.connect(self.saved_search, SIGNAL('changed()'), self.saved_searches_changed) + self.saved_searches_changed() + self.connect(self.clear_button, SIGNAL('clicked()'), self.saved_search.clear_to_help) + self.saved_search.initialize(saved_searches, self.search, colorize=True, + help_text=_('Saved Searches')) + self.connect(self.save_search_button, SIGNAL('clicked()'), + self.saved_search.save_search_button_clicked) + self.connect(self.delete_search_button, SIGNAL('clicked()'), + self.saved_search.delete_search_button_clicked) + self.connect(self.copy_search_button, SIGNAL('clicked()'), + self.saved_search.copy_search_button_clicked) + + + def saved_searches_changed(self): + p = prefs['saved_searches'].keys() + p.sort() + t = unicode(self.search_restriction.currentText()) + self.search_restriction.clear() # rebuild the restrictions combobox using current saved searches + self.search_restriction.addItem('') + self.tags_view.recount() + for s in p: + self.search_restriction.addItem(s) + if t: + if t in p: # redo the current restriction, if there was one + self.search_restriction.setCurrentIndex(self.search_restriction.findText(t)) + # self.tags_view.set_search_restriction(t) + else: + self.search_restriction.setCurrentIndex(0) + self.apply_search_restriction('') + + def do_saved_search_edit(self, search): + d = SavedSearchEditor(self, search) + d.exec_() + if d.result() == d.Accepted: + self.saved_searches_changed() + self.saved_search.clear_to_help() + + diff --git a/src/calibre/gui2/search_restriction_mixin.py b/src/calibre/gui2/search_restriction_mixin.py new file mode 100644 index 0000000000..287c3b2fc2 --- /dev/null +++ b/src/calibre/gui2/search_restriction_mixin.py @@ -0,0 +1,57 @@ +''' +Created on 10 Jun 2010 + +@author: charles +''' + +class SearchRestrictionMixin(object): + + def __init__(self): + self.search_restriction.activated[str].connect(self.apply_search_restriction) + self.library_view.model().count_changed_signal.connect(self.restriction_count_changed) + self.search_restriction.setSizeAdjustPolicy(self.search_restriction.AdjustToMinimumContentsLengthWithIcon) + self.search_restriction.setMinimumContentsLength(10) + + ''' + Adding and deleting books while restricted creates a complexity. When added, + they are displayed regardless of whether they match a search restriction. + However, if they do not, they are removed at the next search. The counts + must take this behavior into effect. + ''' + + def restriction_count_changed(self, c): + self.restriction_count_of_books_in_view += \ + c - self.restriction_count_of_books_in_library + self.restriction_count_of_books_in_library = c + if self.restriction_in_effect: + self.set_number_of_books_shown() + + def apply_search_restriction(self, r): + r = unicode(r) + if r is not None and r != '': + self.restriction_in_effect = True + restriction = 'search:"%s"'%(r) + else: + self.restriction_in_effect = False + restriction = '' + self.restriction_count_of_books_in_view = \ + self.library_view.model().set_search_restriction(restriction) + self.search.clear_to_help() + self.saved_search.clear_to_help() + self.tags_view.set_search_restriction(restriction) + self.set_number_of_books_shown() + + def set_number_of_books_shown(self): + if self.current_view() == self.library_view and self.restriction_in_effect: + t = _("({0} of {1})").format(self.current_view().row_count(), + self.restriction_count_of_books_in_view) + self.search_count.setStyleSheet \ + ('QLabel { border-radius: 8px; background-color: yellow; }') + else: # No restriction or not library view + if not self.search.in_a_search(): + t = _("(all books)") + else: + t = _("({0} of all)").format(self.current_view().row_count()) + self.search_count.setStyleSheet( + 'QLabel { background-color: transparent; }') + self.search_count.setText(t) diff --git a/src/calibre/gui2/sidebar.py b/src/calibre/gui2/sidebar.py deleted file mode 100644 index 6710b5d471..0000000000 --- a/src/calibre/gui2/sidebar.py +++ /dev/null @@ -1,147 +0,0 @@ -#!/usr/bin/env python -# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai - -__license__ = 'GPL v3' -__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>' -__docformat__ = 'restructuredtext en' - -from functools import partial - -from PyQt4.Qt import QToolBar, Qt, QIcon, QSizePolicy, QWidget, \ - QSize, QToolButton - -from calibre.gui2 import dynamic - -class SideBar(QToolBar): - - toggle_texts = { - 'book_info' : (_('Show Book Details'), _('Hide Book Details')), - 'tag_browser' : (_('Show Tag Browser'), _('Hide Tag Browser')), - 'cover_browser': (_('Show Cover Browser'), _('Hide Cover Browser')), - } - toggle_icons = { - 'book_info' : 'book.svg', - 'tag_browser' : 'tags.svg', - 'cover_browser': 'cover_flow.svg', - } - - - def __init__(self, parent=None): - QToolBar.__init__(self, _('Side bar'), parent) - self.setOrientation(Qt.Vertical) - self.setMovable(False) - self.setFloatable(False) - self.setToolButtonStyle(Qt.ToolButtonIconOnly) - self.setIconSize(QSize(48, 48)) - - for ac in ('book_info', 'tag_browser', 'cover_browser'): - action = self.addAction(QIcon(I(self.toggle_icons[ac])), - self.toggle_texts[ac][1], getattr(self, '_toggle_'+ac)) - setattr(self, 'action_toggle_'+ac, action) - w = self.widgetForAction(action) - w.setCheckable(True) - setattr(self, 'show_'+ac, partial(getattr(self, '_toggle_'+ac), - show=True)) - setattr(self, 'hide_'+ac, partial(getattr(self, '_toggle_'+ac), - show=False)) - - - self.spacer = QWidget(self) - self.spacer.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding) - self.addWidget(self.spacer) - - self.show_cover_browser = partial(self._toggle_cover_browser, show=True) - self.hide_cover_browser = partial(self._toggle_cover_browser, - show=False) - for ch in self.children(): - if isinstance(ch, QToolButton): - ch.setCursor(Qt.PointingHandCursor) - - def initialize(self, jobs_button, cover_browser, toggle_cover_browser, - cover_browser_error, vertical_splitter, horizontal_splitter): - self.cover_browser, self.do_toggle_cover_browser = cover_browser, \ - toggle_cover_browser - if self.cover_browser is None: - self.action_toggle_cover_browser.setEnabled(False) - self.action_toggle_cover_browser.setText( - _('Cover browser could not be loaded: ') + cover_browser_error) - else: - self.cover_browser.stop.connect(self.hide_cover_browser) - self._toggle_cover_browser(dynamic.get('cover_flow_visible', False)) - - self.horizontal_splitter = horizontal_splitter - self.vertical_splitter = vertical_splitter - - tb_state = dynamic.get('tag_browser_state', None) - if tb_state is not None: - self.horizontal_splitter.restoreState(tb_state) - tb_last_open_state = dynamic.get('tag_browser_last_open_state', None) - if tb_last_open_state is not None and \ - not self.horizontal_splitter.is_side_index_hidden: - self.horizontal_splitter.restoreState(tb_last_open_state) - - bi_state = dynamic.get('book_info_state', None) - if bi_state is not None: - self.vertical_splitter.restoreState(bi_state) - bi_last_open_state = dynamic.get('book_info_last_open_state', None) - if bi_last_open_state is not None and \ - not self.vertical_splitter.is_side_index_hidden: - self.vertical_splitter.restoreState(bi_last_open_state) - - self.horizontal_splitter.initialize(name='tag_browser') - self.vertical_splitter.initialize(name='book_info') - self.view_status_changed('book_info', not - self.vertical_splitter.is_side_index_hidden) - self.view_status_changed('tag_browser', not - self.horizontal_splitter.is_side_index_hidden) - self.vertical_splitter.state_changed.connect(partial(self.view_status_changed, - 'book_info'), type=Qt.QueuedConnection) - self.horizontal_splitter.state_changed.connect(partial(self.view_status_changed, - 'tag_browser'), type=Qt.QueuedConnection) - self.addWidget(jobs_button) - - - - def view_status_changed(self, name, visible): - action = getattr(self, 'action_toggle_'+name) - texts = self.toggle_texts[name] - action.setText(texts[int(visible)]) - w = self.widgetForAction(action) - w.setCheckable(True) - w.setChecked(visible) - - def location_changed(self, location): - is_lib = location == 'library' - for ac in ('cover_browser', 'tag_browser'): - ac = getattr(self, 'action_toggle_'+ac) - ac.setEnabled(is_lib) - self.widgetForAction(ac).setVisible(is_lib) - - def save_state(self): - dynamic.set('cover_flow_visible', self.is_cover_browser_visible) - dynamic.set('tag_browser_state', - str(self.horizontal_splitter.saveState())) - dynamic.set('book_info_state', - str(self.vertical_splitter.saveState())) - - - @property - def is_cover_browser_visible(self): - return self.cover_browser is not None and self.cover_browser.isVisible() - - def _toggle_cover_browser(self, show=None): - if show is None: - show = not self.is_cover_browser_visible - self.do_toggle_cover_browser(show) - self.view_status_changed('cover_browser', show) - - def external_cover_flow_finished(self, *args): - self.view_status_changed('cover_browser', False) - - def _toggle_tag_browser(self, show=None): - self.horizontal_splitter.toggle_side_index() - - def _toggle_book_info(self, show=None): - self.vertical_splitter.toggle_side_index() - - diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index b5ced8d626..bc698a3502 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -10,9 +10,11 @@ Browsing book collection by tags. from itertools import izip from functools import partial -from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, \ - QFont, QSize, QIcon, QPoint, \ - QAbstractItemModel, QVariant, QModelIndex, QMenu +from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, QCheckBox, \ + QFont, QSize, QIcon, QPoint, QVBoxLayout, QComboBox, \ + QAbstractItemModel, QVariant, QModelIndex, QMenu, \ + QPushButton, QWidget + from calibre.gui2 import config, NONE from calibre.utils.config import prefs from calibre.library.field_metadata import TagsIcons @@ -597,6 +599,7 @@ class TagsModel(QAbstractItemModel): # {{{ class TagBrowserMixin(object): # {{{ def __init__(self, db): + self.library_view.model().count_changed_signal.connect(self.tags_view.recount) self.tags_view.set_database(self.library_view.model().db, self.tag_match, self.popularity) self.tags_view.tags_marked.connect(self.search.search_from_tags) @@ -606,6 +609,8 @@ class TagBrowserMixin(object): # {{{ self.tags_view.saved_search_edit.connect(self.do_saved_search_edit) self.tags_view.tag_item_renamed.connect(self.do_tag_item_renamed) self.tags_view.search_item_renamed.connect(self.saved_search.clear_to_help) + self.edit_categories.clicked.connect(lambda x: + self.do_user_categories_edit()) def do_user_categories_edit(self, on_category=None): d = TagCategories(self, self.library_view.model().db, on_category) @@ -633,3 +638,29 @@ class TagBrowserMixin(object): # {{{ # }}} +class TagBrowserWidget(QWidget): # {{{ + + def __init__(self, parent): + QWidget.__init__(self, parent) + self._layout = QVBoxLayout() + self.setLayout(self._layout) + + parent.tags_view = TagsView(parent) + self._layout.addWidget(parent.tags_view) + + parent.popularity = QCheckBox(parent) + parent.popularity.setText(_('Sort by &popularity')) + self._layout.addWidget(parent.popularity) + + parent.tag_match = QComboBox(parent) + for x in (_('Match any'), _('Match all')): + parent.tag_match.addItem(x) + parent.tag_match.setCurrentIndex(0) + self._layout.addWidget(parent.tag_match) + + parent.edit_categories = QPushButton(_('Manage &user categories'), parent) + self._layout.addWidget(parent.edit_categories) + + +# }}} + diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 9ff30aa768..6c9d0b3d52 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -29,7 +29,6 @@ from calibre.utils.filenames import ascii_filename from calibre.ptempfile import PersistentTemporaryFile from calibre.utils.config import prefs, dynamic from calibre.utils.ipc.server import Server -from calibre.utils.search_query_parser import saved_searches from calibre.devices.errors import UserFeedback from calibre.gui2 import warning_dialog, choose_files, error_dialog, \ question_dialog,\ @@ -37,7 +36,7 @@ from calibre.gui2 import warning_dialog, choose_files, error_dialog, \ Dispatcher, gprefs, \ max_available_height, config, info_dialog, \ GetMetadata -from calibre.gui2.cover_flow import pictureflowerror, CoverFlowMixin +from calibre.gui2.cover_flow import CoverFlowMixin from calibre.gui2.widgets import ProgressIndicator, IMAGE_EXTENSIONS from calibre.gui2.wizard import move_library from calibre.gui2.dialogs.scheduler import Scheduler @@ -51,7 +50,6 @@ from calibre.gui2.dialogs.metadata_bulk import MetadataBulkDialog from calibre.gui2.tools import convert_single_ebook, convert_bulk_ebook, \ fetch_scheduled_recipe, generate_catalog from calibre.gui2.dialogs.config import ConfigDialog -from calibre.gui2.dialogs.search import SearchDialog from calibre.gui2.dialogs.choose_format import ChooseFormatDialog from calibre.gui2.dialogs.book_info import BookInfo from calibre.ebooks import BOOK_EXTENSIONS @@ -59,9 +57,10 @@ from calibre.ebooks.BeautifulSoup import BeautifulSoup, Tag, NavigableString from calibre.library.database2 import LibraryDatabase2 from calibre.library.caches import CoverCache from calibre.gui2.dialogs.confirm_delete import confirm -from calibre.gui2.dialogs.saved_search_editor import SavedSearchEditor +from calibre.gui2.init import ToolbarMixin, LibraryViewMixin, LayoutMixin +from calibre.gui2.search_box import SearchBoxMixin, SavedSearchBoxMixin +from calibre.gui2.search_restriction_mixin import SearchRestrictionMixin from calibre.gui2.tag_view import TagBrowserMixin -from calibre.gui2.init import ToolbarMixin, LibraryViewMixin class Listener(Thread): # {{{ @@ -106,7 +105,8 @@ class SystemTrayIcon(QSystemTrayIcon): # {{{ # }}} class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, - TagBrowserMixin, CoverFlowMixin, LibraryViewMixin): + TagBrowserMixin, CoverFlowMixin, LibraryViewMixin, SearchBoxMixin, + SavedSearchBoxMixin, SearchRestrictionMixin, LayoutMixin): 'The main GUI' def set_default_thumbnail(self, height): @@ -143,35 +143,25 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, self.check_messages_timer.start(1000) Ui_MainWindow.__init__(self) - self.setupUi(self) - self.setWindowTitle(__appname__) + + # Jobs Button {{{ + self.job_manager = JobManager() + self.jobs_dialog = JobsDialog(self, self.job_manager) + self.jobs_button = JobsButton() + self.jobs_button.initialize(self.jobs_dialog, self.job_manager) + # }}} + + LayoutMixin.__init__(self) self.restriction_count_of_books_in_view = 0 self.restriction_count_of_books_in_library = 0 self.restriction_in_effect = False - self.search.initialize('main_search_history', colorize=True, - help_text=_('Search (For Advanced Search click the button to the left)')) - self.connect(self.clear_button, SIGNAL('clicked()'), self.search.clear) - self.connect(self.clear_button, SIGNAL('clicked()'), self.saved_search.clear_to_help) - self.search.clear() - - self.saved_search.initialize(saved_searches, self.search, colorize=True, - help_text=_('Saved Searches')) - self.connect(self.save_search_button, SIGNAL('clicked()'), - self.saved_search.save_search_button_clicked) - self.connect(self.delete_search_button, SIGNAL('clicked()'), - self.saved_search.delete_search_button_clicked) - self.connect(self.copy_search_button, SIGNAL('clicked()'), - self.saved_search.copy_search_button_clicked) self.progress_indicator = ProgressIndicator(self) self.verbose = opts.verbose self.get_metadata = GetMetadata() - self.read_settings() - self.job_manager = JobManager() self.emailer = Emailer() self.emailer.start() - self.jobs_dialog = JobsDialog(self, self.job_manager) self.upload_memory = {} self.delete_memory = {} self.conversion_jobs = {} @@ -225,8 +215,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, self.connect(self.system_tray_icon, SIGNAL('activated(QSystemTrayIcon::ActivationReason)'), self.system_tray_icon_activated) - QObject.connect(self.advanced_search_button, SIGNAL('clicked(bool)'), - self.do_advanced_search) DeviceMixin.__init__(self) @@ -265,6 +253,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, self.update_checker.update_found.connect(self.update_found, type=Qt.QueuedConnection) self.update_checker.start() + ####################### Status Bar ##################### self.status_bar.initialize(self.system_tray_icon) self.status_bar.show_book_info.connect(self.show_book_info) @@ -273,6 +262,10 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, ####################### Setup Toolbar ##################### ToolbarMixin.__init__(self) + ####################### Search boxes ######################## + SavedSearchBoxMixin.__init__(self) + SearchBoxMixin.__init__(self) + ####################### Library view ######################## LibraryViewMixin.__init__(self, db) @@ -280,20 +273,11 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, if self.system_tray_icon.isVisible() and opts.start_in_tray: self.hide_windows() - self.stack.setCurrentIndex(0) - self.search.setFocus(Qt.OtherFocusReason) self.cover_cache = CoverCache(self.library_path) self.cover_cache.start() self.library_view.model().cover_cache = self.cover_cache - self.connect(self.edit_categories, SIGNAL('clicked()'), self.do_user_categories_edit) - self.search_restriction.activated[str].connect(self.apply_search_restriction) - for x in (self.location_view.count_changed, self.tags_view.recount, - self.restriction_count_changed): - self.library_view.model().count_changed_signal.connect(x) - - self.connect(self.search, SIGNAL('cleared()'), self.search_box_cleared) - self.connect(self.saved_search, SIGNAL('changed()'), self.saved_searches_changed) - self.saved_searches_changed() + self.library_view.model().count_changed_signal.connect \ + (self.location_view.count_changed) if not gprefs.get('quick_start_guide_added', False): from calibre.ebooks.metadata import MetaInformation mi = MetaInformation(_('Calibre Quick Start Guide'), ['John Schember']) @@ -314,8 +298,9 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, ########################### Tags Browser ############################## TagBrowserMixin.__init__(self, db) - self.search_restriction.setSizeAdjustPolicy(self.search_restriction.AdjustToMinimumContentsLengthWithIcon) - self.search_restriction.setMinimumContentsLength(10) + + ######################### Search Restriction ########################## + SearchRestrictionMixin.__init__(self) ########################### Cover Flow ################################ @@ -324,18 +309,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, self._calculated_available_height = min(max_available_height()-15, self.height()) self.resize(self.width(), self._calculated_available_height) - self.search.setMaximumWidth(self.width()-150) - - # Jobs Button {{{ - self.jobs_button = JobsButton() - self.jobs_button.initialize(self.jobs_dialog, self.job_manager) - # }}} - - ####################### Side Bar ############################### - - self.sidebar.initialize(self.jobs_button, self.cover_flow, - self.toggle_cover_flow, pictureflowerror, - self.vertical_splitter, self.horizontal_splitter) if config['autolaunch_server']: @@ -362,13 +335,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, self._add_filesystem_book = Dispatcher(self.__add_filesystem_book) self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection) - - def do_saved_search_edit(self, search): - d = SavedSearchEditor(self, search) - d.exec_() - if d.result() == d.Accepted: - self.saved_searches_changed() - self.saved_search.clear_to_help() + self.read_settings() + self.finalize_layout() def resizeEvent(self, ev): MainWindow.resizeEvent(self, ev) @@ -451,76 +419,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, error_dialog(self, _('Failed to start content server'), unicode(self.content_server.exception)).exec_() - ''' - Restrictions. - Adding and deleting books creates a complexity. When added, they are - displayed regardless of whether they match a search restriction. However, if - they do not, they are removed at the next search. The counts must take this - behavior into effect. - ''' - - def restriction_count_changed(self, c): - self.restriction_count_of_books_in_view += c - self.restriction_count_of_books_in_library - self.restriction_count_of_books_in_library = c - if self.restriction_in_effect: - self.set_number_of_books_shown() - - def apply_search_restriction(self, r): - r = unicode(r) - if r is not None and r != '': - self.restriction_in_effect = True - restriction = 'search:"%s"'%(r) - else: - self.restriction_in_effect = False - restriction = '' - self.restriction_count_of_books_in_view = \ - self.library_view.model().set_search_restriction(restriction) - self.search.clear_to_help() - self.saved_search.clear_to_help() - self.tags_view.set_search_restriction(restriction) - self.set_number_of_books_shown() - - def set_number_of_books_shown(self): - if self.current_view() == self.library_view and self.restriction_in_effect: - t = _("({0} of {1})").format(self.current_view().row_count(), - self.restriction_count_of_books_in_view) - self.search_count.setStyleSheet('QLabel { border-radius: 8px; background-color: yellow; }') - else: # No restriction or not library view - if not self.search.in_a_search(): - t = _("(all books)") - else: - t = _("({0} of all)").format(self.current_view().row_count()) - self.search_count.setStyleSheet( - 'QLabel { background-color: transparent; }') - self.search_count.setText(t) - - def search_box_cleared(self): - self.tags_view.clear() - self.saved_search.clear_to_help() - self.set_number_of_books_shown() - - def search_done(self, view, ok): - if view is self.current_view(): - self.search.search_done(ok) - self.set_number_of_books_shown() - - def saved_searches_changed(self): - p = prefs['saved_searches'].keys() - p.sort() - t = unicode(self.search_restriction.currentText()) - self.search_restriction.clear() # rebuild the restrictions combobox using current saved searches - self.search_restriction.addItem('') - self.tags_view.recount() - for s in p: - self.search_restriction.addItem(s) - if t: - if t in p: # redo the current restriction, if there was one - self.search_restriction.setCurrentIndex(self.search_restriction.findText(t)) - # self.tags_view.set_search_restriction(t) - else: - self.search_restriction.setCurrentIndex(0) - self.apply_search_restriction('') - def another_instance_wants_to_talk(self): try: @@ -559,8 +457,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, def booklists(self): return self.memory_view.model().db, self.card_a_view.model().db, self.card_b_view.model().db - - ########################## Connect to device ############################## def save_device_view_settings(self): @@ -1135,7 +1031,11 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, row = None if ci.isValid(): row = ci.row() - view.model().delete_books(rows) + ids_deleted = view.model().delete_books(rows) + for v in (self.memory_view, self.card_a_view, self.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(): @@ -1180,6 +1080,11 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, self.booklists()) model.paths_deleted(paths) self.upload_booklists() + # Clear the ondevice info so it will be recomputed + self.book_on_device(None, None, reset=True) + # We want to reset all the ondevice flags in the library. Use a big + # hammer, so we don't need to worry about whether some succeeded or not + self.library_view.model().refresh() ############################################################################ @@ -1850,13 +1755,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, ############################################################################ - ########################### Do advanced search ############################# - - def do_advanced_search(self, *args): - d = SearchDialog(self) - if d.exec_() == QDialog.Accepted: - self.search.set_search_string(d.search_string()) - ############################################################################ ############################### Do config ################################## @@ -1934,7 +1832,9 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, page = 0 if location == 'library' else 1 if location == 'main' else 2 if location == 'carda' else 3 self.stack.setCurrentIndex(page) self.status_bar.reset_info() - self.sidebar.location_changed(location) + for x in ('tb', 'cb'): + splitter = getattr(self, x+'_splitter') + splitter.button.setEnabled(location == 'library') if location == 'library': self.action_edit.setEnabled(True) self.action_merge.setEnabled(True) @@ -2037,14 +1937,12 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, if geometry is not None: self.restoreGeometry(geometry) self.read_toolbar_settings() + self.read_layout_settings() def write_settings(self): config.set('main_window_geometry', self.saveGeometry()) dynamic.set('sort_history', self.library_view.model().sort_history) - self.sidebar.save_state() - for view in ('library_view', 'memory_view', 'card_a_view', - 'card_b_view'): - getattr(self, view).save_state() + self.save_layout_state() def restart(self): self.quit(restart=True) diff --git a/src/calibre/gui2/widgets.py b/src/calibre/gui2/widgets.py index 093fa3fc5c..52bd8eda9a 100644 --- a/src/calibre/gui2/widgets.py +++ b/src/calibre/gui2/widgets.py @@ -4,16 +4,18 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' Miscellaneous widgets used in the GUI ''' import re, os, traceback + from PyQt4.Qt import QListView, QIcon, QFont, QLabel, QListWidget, \ QListWidgetItem, QTextCharFormat, QApplication, \ QSyntaxHighlighter, QCursor, QColor, QWidget, \ - QPixmap, QPalette, QSplitterHandle, \ + QPixmap, QPalette, QSplitterHandle, QToolButton, \ QAbstractListModel, QVariant, Qt, SIGNAL, pyqtSignal, \ QRegExp, QSettings, QSize, QModelIndex, QSplitter, \ QAbstractButton, QPainter, QLineEdit, QComboBox, \ - QMenu, QStringListModel, QCompleter, QStringList + QMenu, QStringListModel, QCompleter, QStringList, \ + QTimer -from calibre.gui2 import NONE, error_dialog, pixmap_to_data, dynamic +from calibre.gui2 import NONE, error_dialog, pixmap_to_data, gprefs from calibre.gui2.filename_pattern_ui import Ui_Form from calibre import fit_image, human_readable @@ -927,6 +929,7 @@ class SplitterHandle(QSplitterHandle): self.double_clicked.connect(splitter.double_clicked, type=Qt.QueuedConnection) self.highlight = False + self.setToolTip(_('Drag to resize')+' '+splitter.label) def splitter_moved(self, *args): oh = self.highlight @@ -944,20 +947,62 @@ class SplitterHandle(QSplitterHandle): def mouseDoubleClickEvent(self, ev): self.double_clicked.emit(self) +class LayoutButton(QToolButton): + + def __init__(self, icon, text, splitter, parent=None): + QToolButton.__init__(self, parent) + self.label = text + self.setIcon(QIcon(icon)) + self.setCheckable(True) + + self.splitter = splitter + splitter.state_changed.connect(self.update_state) + + def set_state_to_show(self, *args): + self.setChecked(False) + label =_('Show') + self.setText(label + ' ' + self.label) + + def set_state_to_hide(self, *args): + self.setChecked(True) + label = _('Hide') + self.setText(label + ' ' + self.label) + + def update_state(self, *args): + if self.splitter.is_side_index_hidden: + self.set_state_to_show() + else: + self.set_state_to_hide() + class Splitter(QSplitter): state_changed = pyqtSignal(object) - def __init__(self, *args): - QSplitter.__init__(self, *args) + def __init__(self, name, label, icon, initial_show=True, + initial_side_size=120, connect_button=True, + orientation=Qt.Horizontal, side_index=0, parent=None): + QSplitter.__init__(self, parent) + self.resize_timer = QTimer(self) + self.resize_timer.setSingleShot(True) + self.desired_side_size = initial_side_size + self.desired_show = initial_show + self.resize_timer.setInterval(5) + self.resize_timer.timeout.connect(self.do_resize) + self.setOrientation(orientation) + self.side_index = side_index + self._name = name + self.label = label + self.initial_side_size = initial_side_size + self.initial_show = initial_show self.splitterMoved.connect(self.splitter_moved, type=Qt.QueuedConnection) + self.button = LayoutButton(icon, label, self) + if connect_button: + self.button.clicked.connect(self.double_clicked) def createHandle(self): return SplitterHandle(self.orientation(), self) - def initialize(self, name=None): - if name is not None: - self._name = name + def initialize(self): for i in range(self.count()): h = self.handle(i) if h is not None: @@ -965,40 +1010,115 @@ class Splitter(QSplitter): self.state_changed.emit(not self.is_side_index_hidden) def splitter_moved(self, *args): + self.desired_side_size = self.side_index_size self.state_changed.emit(not self.is_side_index_hidden) - @property - def side_index(self): - return 0 if self.orientation() == Qt.Horizontal else 1 - @property def is_side_index_hidden(self): sizes = list(self.sizes()) return sizes[self.side_index] == 0 - def toggle_side_index(self): - self.double_clicked(None) + @property + def save_name(self): + ori = 'horizontal' if self.orientation() == Qt.Horizontal \ + else 'vertical' + return self._name + '_' + ori - def double_clicked(self, handle): - visible = not self.is_side_index_hidden - sizes = list(self.sizes()) - if 0 in sizes: - idx = sizes.index(0) - sizes[idx] = 80 - else: - sizes[self.side_index] = 0 + def print_sizes(self): + if self.count() > 1: + print self.save_name, 'side:', self.side_index_size, 'other:', + print list(self.sizes())[self.other_index] - if visible: - dynamic.set(self._name + '_last_open_state', str(self.saveState())) + @dynamic_property + def side_index_size(self): + def fget(self): + if self.count() < 2: return 0 + return self.sizes()[self.side_index] + + def fset(self, val): + if self.count() < 2: return + if val == 0 and not self.is_side_index_hidden: + self.save_state() + sizes = list(self.sizes()) + for i in range(len(sizes)): + sizes[i] = val if i == self.side_index else 10 self.setSizes(sizes) + total = sum(self.sizes()) + sizes = list(self.sizes()) + for i in range(len(sizes)): + sizes[i] = val if i == self.side_index else total-val + self.setSizes(sizes) + self.initialize() + + return property(fget=fget, fset=fset) + + def do_resize(self, *args): + orig = self.desired_side_size + QSplitter.resizeEvent(self, self._resize_ev) + if orig > 20 and self.desired_show: + c = 0 + while abs(self.side_index_size - orig) > 10 and c < 5: + self.apply_state(self.get_state(), save_desired=False) + c += 1 + + def resizeEvent(self, ev): + if self.resize_timer.isActive(): + self.resize_timer.stop() + self._resize_ev = ev + self.resize_timer.start() + + def get_state(self): + if self.count() < 2: return (False, 200) + return (self.desired_show, self.desired_side_size) + + def apply_state(self, state, save_desired=True): + if state[0]: + self.side_index_size = state[1] + if save_desired: + self.desired_side_size = self.side_index_size else: - state = dynamic.get(self._name+ '_last_open_state', None) - if state is not None: - self.restoreState(state) - else: - self.setSizes(sizes) - self.initialize() + self.side_index_size = 0 + self.desired_show = state[0] + def default_state(self): + return (self.initial_show, self.initial_side_size) + # Public API {{{ + def save_state(self): + if self.count() > 1: + gprefs[self.save_name+'_state'] = self.get_state() + + @property + def other_index(self): + return (self.side_index+1)%2 + + def restore_state(self): + if self.count() > 1: + state = gprefs.get(self.save_name+'_state', + self.default_state()) + self.apply_state(state, save_desired=False) + self.desired_side_size = state[1] + + def toggle_side_pane(self, hide=None): + if hide is None: + action = 'show' if self.is_side_index_hidden else 'hide' + else: + action = 'hide' if hide else 'show' + getattr(self, action+'_side_pane')() + + def show_side_pane(self): + if self.count() < 2 or not self.is_side_index_hidden: + return + self.apply_state((True, self.desired_side_size)) + + def hide_side_pane(self): + if self.count() < 2 or self.is_side_index_hidden: + return + self.apply_state((False, self.desired_side_size)) + + def double_clicked(self, *args): + self.toggle_side_pane() + + # }}} diff --git a/src/calibre/gui2/wizard/__init__.py b/src/calibre/gui2/wizard/__init__.py index 1fc57e8f4e..b831201f2d 100644 --- a/src/calibre/gui2/wizard/__init__.py +++ b/src/calibre/gui2/wizard/__init__.py @@ -96,14 +96,14 @@ class Kobo(Device): class Booq(Device): name = 'Booq Reader' manufacturer = 'Booq' - output_profile = 'prs505' + output_profile = 'sony' output_format = 'EPUB' id = 'booq' class TheBook(Device): name = 'The Book' manufacturer = 'Augen' - output_profile = 'prs505' + output_profile = 'sony' output_format = 'EPUB' id = 'thebook' diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py index e2ecdd9f55..bb6001794a 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -619,9 +619,12 @@ class ResultCache(SearchQueryParser): if self.first_sort: subsort = True self.first_sort = False - fcmp = self.seriescmp if field == 'series' else \ - functools.partial(self.cmp, self.FIELD_MAP[field], subsort=subsort, - asstr=as_string) + fcmp = self.seriescmp \ + if field == 'series' and \ + tweaks['title_series_sorting'] == 'library_order' \ + else \ + functools.partial(self.cmp, self.FIELD_MAP[field], + subsort=subsort, asstr=as_string) self._map.sort(cmp=fcmp, reverse=not ascending) self._map_filtered = [id for id in self._map if id in self._map_filtered] diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 5868a782ad..7b98dc4537 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -28,7 +28,7 @@ from calibre.customize.ui import run_plugins_on_import from calibre.utils.filenames import ascii_filename from calibre.utils.date import utcnow, now as nowf, utcfromtimestamp -from calibre.utils.config import prefs +from calibre.utils.config import prefs, tweaks from calibre.utils.search_query_parser import saved_searches from calibre.ebooks import BOOK_EXTENSIONS, check_ebook_format from calibre.utils.magick_draw import save_cover_data_to @@ -736,8 +736,12 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): icon=icon, tooltip = tooltip) for r in data if item_not_zero_func(r)] if category == 'series' and not sort_on_count: - categories[category].sort(cmp=lambda x,y:cmp(title_sort(x.name).lower(), - title_sort(y.name).lower())) + if tweaks['title_series_sorting'] == 'library_order': + ts = lambda x: title_sort(x) + else: + ts = lambda x:x + categories[category].sort(cmp=lambda x,y:cmp(ts(x.name).lower(), + ts(y.name).lower())) # We delayed computing the standard formats category because it does not # use a view, but is computed dynamically @@ -950,7 +954,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): title = title.decode(preferred_encoding, 'replace') self.conn.execute('UPDATE books SET title=? WHERE id=?', (title, id)) self.data.set(id, self.FIELD_MAP['title'], title, row_is_id=True) - self.data.set(id, self.FIELD_MAP['sort'], title_sort(title), row_is_id=True) + if tweaks['title_series_sorting'] == 'library_order': + self.data.set(id, self.FIELD_MAP['sort'], title_sort(title), row_is_id=True) + else: + self.data.set(id, self.FIELD_MAP['sort'], title, row_is_id=True) self.set_path(id, True) self.conn.commit() if notify: @@ -1835,6 +1842,8 @@ books_series_link feeds os.remove(self.dbpath) shutil.copyfile(dest, self.dbpath) self.connect() + self.field_metadata.remove_dynamic_categories() + self.field_metadata.remove_custom_fields() self.initialize_dynamic() self.refresh() if os.path.exists(dest): diff --git a/src/calibre/library/field_metadata.py b/src/calibre/library/field_metadata.py index 243e3646da..82e4edfdf2 100644 --- a/src/calibre/library/field_metadata.py +++ b/src/calibre/library/field_metadata.py @@ -379,6 +379,17 @@ class FieldMetadata(dict): self._add_search_terms_to_map(key, [key]) self.custom_label_to_key_map[label] = key + def remove_custom_fields(self): + for key in self.get_custom_fields(): + del self._tb_cats[key] + + def remove_dynamic_categories(self): + for key in list(self._tb_cats.keys()): + val = self._tb_cats[key] + if val['is_category'] and val['kind'] in ('user', 'search'): + del self._tb_cats[key] + + def add_user_category(self, label, name): if label in self._tb_cats: raise ValueError('Duplicate user field [%s]'%(label)) diff --git a/src/calibre/library/sqlite.py b/src/calibre/library/sqlite.py index d95eae9226..adf6691671 100644 --- a/src/calibre/library/sqlite.py +++ b/src/calibre/library/sqlite.py @@ -15,6 +15,7 @@ from threading import RLock from datetime import datetime from calibre.ebooks.metadata import title_sort +from calibre.utils.config import tweaks from calibre.utils.date import parse_date, isoformat global_lock = RLock() @@ -115,7 +116,10 @@ class DBThread(Thread): self.conn.create_aggregate('concat', 1, Concatenate) self.conn.create_aggregate('sortconcat', 2, SortedConcatenate) self.conn.create_aggregate('sort_concat', 2, SafeSortedConcatenate) - self.conn.create_function('title_sort', 1, title_sort) + if tweaks['title_series_sorting'] == 'library_order': + self.conn.create_function('title_sort', 1, title_sort) + else: + self.conn.create_function('title_sort', 1, lambda x:x) self.conn.create_function('uuid4', 0, lambda : str(uuid.uuid4())) # Dummy functions for dynamically created filters self.conn.create_function('books_list_filter', 1, lambda x: 1) diff --git a/src/calibre/web/feeds/__init__.py b/src/calibre/web/feeds/__init__.py index da7122c491..c34334ee09 100644 --- a/src/calibre/web/feeds/__init__.py +++ b/src/calibre/web/feeds/__init__.py @@ -137,7 +137,7 @@ class Feed(object): def populate_from_preparsed_feed(self, title, articles, oldest_article=7, max_articles_per_feed=100): - self.title = title if title else _('Unknown feed') + self.title = unicode(title if title else _('Unknown feed')) self.description = '' self.image_url = None self.articles = [] diff --git a/src/calibre/web/feeds/templates.py b/src/calibre/web/feeds/templates.py index e98edd0e95..b64795b816 100644 --- a/src/calibre/web/feeds/templates.py +++ b/src/calibre/web/feeds/templates.py @@ -65,6 +65,7 @@ class NavBarTemplate(Template): text = 'This article was downloaded by ' p = PT(text, STRONG(__appname__), A(url, href=url), style='text-align:left') p[0].tail = ' from ' + navbar.append(p) navbar.append(BR()) navbar.append(BR()) else: @@ -111,6 +112,7 @@ class TouchscreenNavBarTemplate(Template): text = 'This article was downloaded by ' p = PT(text, STRONG(__appname__), A(url, href=url), style='text-align:left') p[0].tail = ' from ' + navbar.append(p) navbar.append(BR()) navbar.append(BR()) else: