From 978e1f826b50bce4cfd23981cf678a564081bd00 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 24 Apr 2011 19:44:17 -0600 Subject: [PATCH] Make the book details panel completely user configurable --- resources/default_tweaks.py | 20 - resources/templates/book_details.css | 26 + src/calibre/ebooks/metadata/book/base.py | 8 +- src/calibre/gui2/__init__.py | 12 +- src/calibre/gui2/actions/show_book_details.py | 2 +- src/calibre/gui2/book_details.py | 280 ++++---- src/calibre/gui2/dialogs/book_info.py | 91 +-- src/calibre/gui2/dialogs/book_info.ui | 86 +-- src/calibre/gui2/library/models.py | 149 ++-- src/calibre/gui2/preferences/look_feel.py | 12 +- src/calibre/gui2/preferences/look_feel.ui | 669 +++++++++++------- src/calibre/library/db/__init__.py | 9 - src/calibre/library/db/base.py | 37 - src/calibre/library/field_metadata.py | 20 +- 14 files changed, 717 insertions(+), 704 deletions(-) create mode 100644 resources/templates/book_details.css delete mode 100644 src/calibre/library/db/__init__.py delete mode 100644 src/calibre/library/db/base.py diff --git a/resources/default_tweaks.py b/resources/default_tweaks.py index 091aa9a34d..08017b5c98 100644 --- a/resources/default_tweaks.py +++ b/resources/default_tweaks.py @@ -266,26 +266,6 @@ max_content_server_tags_shown=5 content_server_will_display = ['*'] content_server_wont_display = [] -#: Set custom metadata fields that the book details panel will or will not display. -# book_details_will_display is a list of custom fields to be displayed. -# book_details_wont_display is a list of custom fields not to be displayed. -# wont_display has priority over will_display. -# The special value '*' means all custom fields. The value [] means no entries. -# Defaults: -# book_details_will_display = ['*'] -# book_details_wont_display = [] -# Examples: -# To display only the custom fields #mytags and #genre: -# book_details_will_display = ['#mytags', '#genre'] -# book_details_wont_display = [] -# To display all fields except #mycomments: -# book_details_will_display = ['*'] -# book_details_wont_display['#mycomments'] -# As above, this tweak affects only display of custom fields. The standard -# fields are not affected -book_details_will_display = ['*'] -book_details_wont_display = [] - #: Set the maximum number of sort 'levels' # Set the maximum number of sort 'levels' that calibre will use to resort the # library after certain operations such as searches or device insertion. Each diff --git a/resources/templates/book_details.css b/resources/templates/book_details.css new file mode 100644 index 0000000000..9671ad77ee --- /dev/null +++ b/resources/templates/book_details.css @@ -0,0 +1,26 @@ +a { + text-decoration: none; + color: blue +} +.comments { + margin-top: 0; + padding-top: 0; + text-indent: 0 +} + +table.fields { + margin-bottom: 0; + padding-bottom: 0; +} + +table.fields td { + vertical-align: top +} + +table.fields td.title { + font-weight: bold +} + +.series_name { + font-style: italic +} diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py index faac8e98b1..5f1841d518 100644 --- a/src/calibre/ebooks/metadata/book/base.py +++ b/src/calibre/ebooks/metadata/book/base.py @@ -19,6 +19,9 @@ from calibre.utils.date import isoformat, format_date from calibre.utils.icu import sort_key from calibre.utils.formatter import TemplateFormatter +def human_readable(size, precision=2): + """ Convert a size in bytes into megabytes """ + return ('%.'+str(precision)+'f'+ 'MB') % ((size/(1024.*1024.)),) NULL_VALUES = { 'user_metadata': {}, @@ -551,7 +554,8 @@ class Metadata(object): def format_field_extended(self, key, series_with_index=True): from calibre.ebooks.metadata import authors_to_string ''' - returns the tuple (field_name, formatted_value) + returns the tuple (field_name, formatted_value, original_value, + field_metadata) ''' # Handle custom series index @@ -627,6 +631,8 @@ class Metadata(object): res = format_date(res, fmeta['display'].get('date_format','dd MMM yyyy')) elif datatype == 'rating': res = res/2.0 + elif key in ('book_size', 'size'): + res = human_readable(res) return (name, unicode(res), orig_res, fmeta) return (None, None, None, None) diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index de066359ed..f1357728ec 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -80,6 +80,14 @@ gprefs.defaults['font'] = None gprefs.defaults['tags_browser_partition_method'] = 'first letter' gprefs.defaults['tags_browser_collapse_at'] = 100 gprefs.defaults['edit_metadata_single_layout'] = 'default' +gprefs.defaults['book_display_fields'] = [ + ('title', False), ('authors', False), ('formats', True), + ('series', True), ('identifiers', True), ('tags', True), + ('path', True), ('publisher', False), ('rating', False), + ('author_sort', False), ('sort', False), ('timestamp', False), + ('uuid', False), ('comments', True), ('id', False), ('pubdate', False), + ('last_modified', False), ('size', False), + ] # }}} @@ -89,7 +97,7 @@ UNDEFINED_QDATE = QDate(UNDEFINED_DATE) ALL_COLUMNS = ['title', 'ondevice', 'authors', 'size', 'timestamp', 'rating', 'publisher', 'tags', 'series', 'pubdate'] -def _config(): +def _config(): # {{{ c = Config('gui', 'preferences for the calibre GUI') c.add_opt('send_to_storage_card_by_default', default=False, help=_('Send file to storage card instead of main memory by default')) @@ -181,6 +189,8 @@ def _config(): return ConfigProxy(c) config = _config() +# }}} + # Turn off DeprecationWarnings in windows GUI if iswindows: import warnings diff --git a/src/calibre/gui2/actions/show_book_details.py b/src/calibre/gui2/actions/show_book_details.py index 11064f2f39..1c28a08a79 100644 --- a/src/calibre/gui2/actions/show_book_details.py +++ b/src/calibre/gui2/actions/show_book_details.py @@ -30,5 +30,5 @@ class ShowBookDetailsAction(InterfaceAction): index = self.gui.library_view.currentIndex() if index.isValid(): BookInfo(self.gui, self.gui.library_view, index, - self.gui.iactions['View'].view_format_by_id).show() + self.gui.book_details.handle_click).show() diff --git a/src/calibre/gui2/book_details.py b/src/calibre/gui2/book_details.py index 4e75a42e89..f2f048a5d5 100644 --- a/src/calibre/gui2/book_details.py +++ b/src/calibre/gui2/book_details.py @@ -5,67 +5,151 @@ __license__ = 'GPL v3' __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import collections, sys -from Queue import Queue -from PyQt4.Qt import QPixmap, QSize, QWidget, Qt, pyqtSignal, QUrl, \ - QPropertyAnimation, QEasingCurve, QThread, QApplication, QFontInfo, \ - QSizePolicy, QPainter, QRect, pyqtProperty, QLayout, QPalette, QMenu +from PyQt4.Qt import (QPixmap, QSize, QWidget, Qt, pyqtSignal, QUrl, + QPropertyAnimation, QEasingCurve, QApplication, QFontInfo, + QSizePolicy, QPainter, QRect, pyqtProperty, QLayout, QPalette, QMenu) from PyQt4.QtWebKit import QWebView -from calibre import fit_image, prepare_string_for_xml -from calibre.gui2.dnd import dnd_has_image, dnd_get_image, dnd_get_files, \ - IMAGE_EXTENSIONS, dnd_has_extension +from calibre import fit_image, force_unicode, prepare_string_for_xml +from calibre.gui2.dnd import (dnd_has_image, dnd_get_image, dnd_get_files, + IMAGE_EXTENSIONS, dnd_has_extension) from calibre.ebooks import BOOK_EXTENSIONS -from calibre.constants import preferred_encoding +from calibre.ebooks.metadata.book.base import (field_metadata, Metadata) +from calibre.ebooks.metadata import fmt_sidx +from calibre.constants import filesystem_encoding from calibre.library.comments import comments_to_html -from calibre.gui2 import config, open_local_file, open_url, pixmap_to_data +from calibre.gui2 import (config, open_local_file, open_url, pixmap_to_data, + gprefs) from calibre.utils.icu import sort_key -# render_rows(data) {{{ -WEIGHTS = collections.defaultdict(lambda : 100) -WEIGHTS[_('Path')] = 5 -WEIGHTS[_('Formats')] = 1 -WEIGHTS[_('Collections')] = 2 -WEIGHTS[_('Series')] = 3 -WEIGHTS[_('Tags')] = 4 -def render_rows(data): - keys = data.keys() - # First sort by name. The WEIGHTS sort will preserve this sub-order - keys.sort(key=sort_key) - keys.sort(key=lambda x: WEIGHTS[x]) - rows = [] - for key in keys: - txt = data[key] - if key in ('id', _('Comments')) or not hasattr(txt, 'strip') or not txt.strip() or \ - txt == 'None': +def render_html(mi, css, vertical, widget, all_fields=False): # {{{ + table = render_data(mi, all_fields=all_fields, + use_roman_numbers=config['use_roman_numerals_for_series_number']) + + def color_to_string(col): + ans = '#000000' + if col.isValid(): + col = col.toRgb() + if col.isValid(): + ans = unicode(col.name()) + return ans + + f = QFontInfo(QApplication.font(widget)).pixelSize() + c = color_to_string(QApplication.palette().color(QPalette.Normal, + QPalette.WindowText)) + templ = u'''\ + + + + + + + %%s + + + '''%(f, c, css) + comments = u'' + if mi.comments: + comments = comments_to_html(force_unicode(mi.comments)) + right_pane = u'
%s
'%comments + + if vertical: + ans = templ%(table+right_pane) + else: + ans = templ%(u'
%s%s
' + % (table, right_pane)) + return ans + +def get_field_list(fm): + fieldlist = list(gprefs['book_display_fields']) + names = frozenset([x[0] for x in fieldlist]) + for field in fm.displayable_field_keys(): + if field not in names: + fieldlist.append((field, True)) + return fieldlist + +def render_data(mi, use_roman_numbers=True, all_fields=False): + ans = [] + isdevice = not hasattr(mi, 'id') + fm = getattr(mi, 'field_metadata', field_metadata) + + for field, display in get_field_list(fm): + metadata = fm.get(field, None) + if all_fields: + display = True + if (not display or not metadata or mi.is_null(field) or + field == 'comments'): continue - if isinstance(key, str): - key = key.decode(preferred_encoding, 'replace') - if isinstance(txt, str): - txt = txt.decode(preferred_encoding, 'replace') - if key.endswith(u':html'): - key = key[:-5] - txt = comments_to_html(txt) - elif '' not in txt: - txt = prepare_string_for_xml(txt) - if 'id' in data: - if key == _('Path'): - txt = u'%s'%(data['id'], - txt, _('Click to open')) - if key == _('Formats') and txt and txt != _('None'): - fmts = [x.strip() for x in txt.split(',')] - fmts = [u'%s' % (data['id'], x, x) for x - in fmts] - txt = ', '.join(fmts) + name = metadata['name'] + if not name: + name = field + name += ':' + if metadata['datatype'] == 'comments': + val = getattr(mi, field) + if val: + val = force_unicode(val) + ans.append((field, + u'%s'%comments_to_html(val))) + elif field == 'path': + if mi.path: + path = force_unicode(mi.path, filesystem_encoding) + scheme = u'devpath' if isdevice else u'path' + url = prepare_string_for_xml(path if isdevice else + unicode(mi.id), True) + link = u'%s' % (scheme, url, + prepare_string_for_xml(path, True), _('Click to open')) + ans.append((field, u'%s%s'%(name, link))) + elif field == 'formats': + if isdevice: continue + fmts = [u'%s' % (mi.id, x, x) for x + in mi.formats] + ans.append((field, u'%s%s'%(name, + u', '.join(fmts)))) + elif field == 'identifiers': + pass # TODO else: - if key == _('Path'): - txt = u'%s'%(txt, - _('Click to open')) + val = mi.format_field(field)[-1] + if val is None: + continue + val = prepare_string_for_xml(val) + if metadata['datatype'] == 'series': + if metadata['is_custom']: + sidx = mi.get_extra(field) + else: + sidx = getattr(mi, field+'_index') + if sidx is None: + sidx = 1.0 + val = _('Book %s of %s')%(fmt_sidx(sidx, + use_roman=use_roman_numbers), + prepare_string_for_xml(getattr(mi, field))) - rows.append((key, txt)) - return rows + ans.append((field, u'%s%s'%(name, val))) + + dc = getattr(mi, 'device_collections', []) + if dc: + dc = u', '.join(sorted(dc, key=sort_key)) + ans.append(('device_collections', + u'%s%s'%( + _('Collections')+':', dc))) + + def classname(field): + try: + dt = fm[field]['datatype'] + except: + dt = 'text' + return 'datatype_%s'%dt + + ans = [u'%s'%(field.replace('#', '_'), + classname(field), html) for field, html in ans] + # print '\n'.join(ans) + return u'%s
'%(u'\n'.join(ans)) # }}} @@ -117,10 +201,10 @@ class CoverView(QWidget): # {{{ def show_data(self, data): self.animation.stop() - same_item = data.get('id', True) == self.data.get('id', False) + same_item = getattr(data, 'id', True) == self.data.get('id', False) self.data = {'id':data.get('id', None)} - if data.has_key('cover'): - self.pixmap = QPixmap.fromImage(data.pop('cover')) + if data.cover_data[1]: + self.pixmap = QPixmap.fromImage(data.cover_data[1]) if self.pixmap.isNull() or self.pixmap.width() < 5 or \ self.pixmap.height() < 5: self.pixmap = self.default_pixmap @@ -188,32 +272,6 @@ class CoverView(QWidget): # {{{ # Book Info {{{ -class RenderComments(QThread): - - rdone = pyqtSignal(object, object) - - def __init__(self, parent): - QThread.__init__(self, parent) - self.queue = Queue() - self.start() - - def run(self): - while True: - try: - rows, comments = self.queue.get() - except: - break - import time - time.sleep(0.001) - oint = sys.getcheckinterval() - sys.setcheckinterval(5) - try: - self.rdone.emit(rows, comments_to_html(comments)) - except: - pass - sys.setcheckinterval(oint) - - class BookInfo(QWebView): link_clicked = pyqtSignal(object) @@ -221,8 +279,6 @@ class BookInfo(QWebView): def __init__(self, vertical, parent=None): QWebView.__init__(self, parent) self.vertical = vertical - self.renderer = RenderComments(self) - self.renderer.rdone.connect(self._show_data, type=Qt.QueuedConnection) self.page().setLinkDelegationPolicy(self.page().DelegateAllLinks) self.linkClicked.connect(self.link_activated) self._link_clicked = False @@ -231,6 +287,7 @@ class BookInfo(QWebView): self.setAcceptDrops(False) palette.setBrush(QPalette.Base, Qt.transparent) self.page().setPalette(palette) + self.css = P('templates/book_details.css', data=True).decode('utf-8') def link_activated(self, link): self._link_clicked = True @@ -240,56 +297,9 @@ class BookInfo(QWebView): def turnoff_scrollbar(self, *args): self.page().mainFrame().setScrollBarPolicy(Qt.Horizontal, Qt.ScrollBarAlwaysOff) - def show_data(self, data): - rows = render_rows(data) - rows = u'\n'.join([u'%s:%s'%(k,t) for - k, t in rows]) - comments = data.get(_('Comments'), '') - if not comments or comments == u'None': - comments = '' - self.renderer.queue.put((rows, comments)) - self._show_data(rows, '') - - - def _show_data(self, rows, comments): - - def color_to_string(col): - ans = '#000000' - if col.isValid(): - col = col.toRgb() - if col.isValid(): - ans = unicode(col.name()) - return ans - - f = QFontInfo(QApplication.font(self.parent())).pixelSize() - c = color_to_string(QApplication.palette().color(QPalette.Normal, - QPalette.WindowText)) - templ = u'''\ - - - - - - %%s - - - '''%(f, c) - if self.vertical: - extra = '' - if comments: - extra = u'
%s
'%comments - self.setHtml(templ%(u'%s
%s'%(rows, extra))) - else: - left_pane = u'%s
'%rows - right_pane = u'
%s
'%comments - self.setHtml(templ%(u'
%s%s
' - % (left_pane, right_pane))) + def show_data(self, mi): + html = render_html(mi, self.css, self.vertical, self.parent()) + self.setHtml(html) def mouseDoubleClickEvent(self, ev): swidth = self.page().mainFrame().scrollBarGeometry(Qt.Vertical).width() @@ -457,10 +467,10 @@ class BookDetails(QWidget): # {{{ self._layout.addWidget(self.cover_view) self.book_info = BookInfo(vertical, self) self._layout.addWidget(self.book_info) - self.book_info.link_clicked.connect(self._link_clicked) + self.book_info.link_clicked.connect(self.handle_click) self.setCursor(Qt.PointingHandCursor) - def _link_clicked(self, link): + def handle_click(self, link): typ, _, val = link.partition(':') if typ == 'path': self.open_containing_folder.emit(int(val)) @@ -484,7 +494,7 @@ class BookDetails(QWidget): # {{{ def show_data(self, data): self.book_info.show_data(data) self.cover_view.show_data(data) - self.current_path = data.get(_('Path'), '') + self.current_path = getattr(data, u'path', u'') self.update_layout() def update_layout(self): @@ -500,7 +510,7 @@ class BookDetails(QWidget): # {{{ ) def reset_info(self): - self.show_data({}) + self.show_data(Metadata(_('Unknown'))) # }}} diff --git a/src/calibre/gui2/dialogs/book_info.py b/src/calibre/gui2/dialogs/book_info.py index 46d26c2f4a..4036e71a38 100644 --- a/src/calibre/gui2/dialogs/book_info.py +++ b/src/calibre/gui2/dialogs/book_info.py @@ -3,30 +3,33 @@ __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' __docformat__ = 'restructuredtext en' -import textwrap, os, re -from PyQt4.Qt import QCoreApplication, SIGNAL, QModelIndex, QTimer, Qt, \ - QDialog, QPixmap, QIcon, QSize +from PyQt4.Qt import (QCoreApplication, SIGNAL, QModelIndex, QTimer, Qt, + QDialog, QPixmap, QIcon, QSize, QPalette) from calibre.gui2.dialogs.book_info_ui import Ui_BookInfo -from calibre.gui2 import dynamic, open_local_file, open_url +from calibre.gui2 import dynamic from calibre import fit_image -from calibre.library.comments import comments_to_html -from calibre.utils.icu import sort_key - +from calibre.gui2.book_details import render_html class BookInfo(QDialog, Ui_BookInfo): - def __init__(self, parent, view, row, view_func): + def __init__(self, parent, view, row, link_delegate): QDialog.__init__(self, parent) Ui_BookInfo.__init__(self) self.setupUi(self) self.gui = parent self.cover_pixmap = None - self.comments.sizeHint = self.comments_size_hint - self.comments.page().setLinkDelegationPolicy(self.comments.page().DelegateAllLinks) - self.comments.linkClicked.connect(self.link_clicked) - self.view_func = view_func + self.details.sizeHint = self.details_size_hint + self.details.page().setLinkDelegationPolicy(self.details.page().DelegateAllLinks) + self.details.linkClicked.connect(self.link_clicked) + self.css = P('templates/book_details.css', data=True).decode('utf-8') + self.link_delegate = link_delegate + self.details.setAttribute(Qt.WA_OpaquePaintEvent, False) + palette = self.details.palette() + self.details.setAcceptDrops(False) + palette.setBrush(QPalette.Base, Qt.transparent) + self.details.page().setPalette(palette) self.view = view @@ -37,7 +40,6 @@ class BookInfo(QDialog, Ui_BookInfo): self.connect(self.view.selectionModel(), SIGNAL('currentChanged(QModelIndex,QModelIndex)'), self.slave) self.connect(self.next_button, SIGNAL('clicked()'), self.next) self.connect(self.previous_button, SIGNAL('clicked()'), self.previous) - self.connect(self.text, SIGNAL('linkActivated(QString)'), self.open_book_path) self.fit_cover.stateChanged.connect(self.toggle_cover_fit) self.cover.resizeEvent = self.cover_view_resized self.cover.cover_changed.connect(self.cover_changed) @@ -46,6 +48,10 @@ class BookInfo(QDialog, Ui_BookInfo): screen_height = desktop.availableGeometry().height() - 100 self.resize(self.size().width(), screen_height) + def link_clicked(self, qurl): + link = unicode(qurl.toString()) + self.link_delegate(link) + def cover_changed(self, data): if self.current_row is not None: id_ = self.view.model().id(self.current_row) @@ -60,11 +66,8 @@ class BookInfo(QDialog, Ui_BookInfo): if self.fit_cover.isChecked(): self.resize_cover() - def link_clicked(self, url): - open_url(url) - - def comments_size_hint(self): - return QSize(350, 250) + def details_size_hint(self): + return QSize(350, 550) def toggle_cover_fit(self, state): dynamic.set('book_info_dialog_fit_cover', self.fit_cover.isChecked()) @@ -77,13 +80,6 @@ class BookInfo(QDialog, Ui_BookInfo): row = current.row() self.refresh(row) - def open_book_path(self, path): - path = unicode(path) - if os.sep in path: - open_local_file(path) - else: - self.view_func(self.view.model().id(self.current_row), path) - def next(self): row = self.view.currentIndex().row() ni = self.view.model().index(row+1, 0) @@ -117,8 +113,8 @@ class BookInfo(QDialog, Ui_BookInfo): row = row.row() if row == self.current_row: return - info = self.view.model().get_book_info(row) - if info is None: + mi = self.view.model().get_book_display_info(row) + if mi is None: # Indicates books was deleted from library, or row numbers have # changed return @@ -126,40 +122,11 @@ class BookInfo(QDialog, Ui_BookInfo): self.previous_button.setEnabled(False if row == 0 else True) self.next_button.setEnabled(False if row == self.view.model().rowCount(QModelIndex())-1 else True) self.current_row = row - self.setWindowTitle(info[_('Title')]) - self.title.setText(''+info.pop(_('Title'))) - comments = info.pop(_('Comments'), '') - if comments: - comments = comments_to_html(comments) - if re.search(r'<[a-zA-Z]+>', comments) is None: - lines = comments.splitlines() - lines = [x if x.strip() else '

' for x in lines] - comments = '\n'.join(lines) - self.comments.setHtml('
%s
' % comments) - self.comments.page().setLinkDelegationPolicy(self.comments.page().DelegateAllLinks) - cdata = info.pop('cover', '') - self.cover_pixmap = QPixmap.fromImage(cdata) + self.setWindowTitle(mi.title) + self.title.setText(''+mi.title) + mi.title = _('Unknown') + self.cover_pixmap = QPixmap.fromImage(mi.cover_data[1]) self.resize_cover() + html = render_html(mi, self.css, True, self, all_fields=True) + self.details.setHtml(html) - rows = u'' - self.text.setText('') - self.data = info - if _('Path') in info.keys(): - p = info[_('Path')] - info[_('Path')] = '%s'%(p, p) - if _('Formats') in info.keys(): - formats = info[_('Formats')].split(',') - info[_('Formats')] = '' - for f in formats: - f = f.strip() - info[_('Formats')] += '%s, '%(f,f) - for key in sorted(info.keys(), key=sort_key): - if key == 'id': continue - txt = info[key] - if key.endswith(':html'): - key = key[:-5] - txt = comments_to_html(txt) - if key != _('Path'): - txt = u'
\n'.join(textwrap.wrap(txt, 120)) - rows += u'%s:%s'%(key, txt) - self.text.setText(u''+rows+'
') diff --git a/src/calibre/gui2/dialogs/book_info.ui b/src/calibre/gui2/dialogs/book_info.ui index 9e9e71eda0..44fd1adf22 100644 --- a/src/calibre/gui2/dialogs/book_info.ui +++ b/src/calibre/gui2/dialogs/book_info.ui @@ -20,6 +20,12 @@ + + + 0 + 0 + + 75 @@ -34,82 +40,17 @@ - + - - - - QFrame::NoFrame - - - true - - - - - 0 - 0 - 435 - 670 - - - - - - - TextLabel - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - true - - - - - - - Comments - - - - - - - 0 - 0 - - - - - 350 - 16777215 - - - - - about:blank - - - - - - - - - - - - + Fit &cover within view - + @@ -135,6 +76,15 @@ + + + + + about:blank + + + + diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index 0bd3f2133a..8b830d2ec2 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -8,20 +8,20 @@ __docformat__ = 'restructuredtext en' import shutil, functools, re, os, traceback from contextlib import closing -from PyQt4.Qt import QAbstractTableModel, Qt, pyqtSignal, QIcon, QImage, \ - QModelIndex, QVariant, QDate, QColor +from PyQt4.Qt import (QAbstractTableModel, Qt, pyqtSignal, QIcon, QImage, + QModelIndex, QVariant, QDate, QColor) -from calibre.gui2 import NONE, config, UNDEFINED_QDATE +from calibre.gui2 import NONE, UNDEFINED_QDATE from calibre.utils.pyparsing import ParseException from calibre.ebooks.metadata import fmt_sidx, authors_to_string, string_to_authors from calibre.ptempfile import PersistentTemporaryFile from calibre.utils.config import tweaks, prefs -from calibre.utils.date import dt_factory, qt_to_dt, isoformat +from calibre.utils.date import dt_factory, qt_to_dt from calibre.utils.icu import sort_key from calibre.utils.search_query_parser import SearchQueryParser -from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, \ - REGEXP_MATCH, MetadataBackup, force_to_bool -from calibre import strftime, isbytestring, prepare_string_for_xml +from calibre.library.caches import (_match, CONTAINS_MATCH, EQUALS_MATCH, + REGEXP_MATCH, MetadataBackup, force_to_bool) +from calibre import strftime, isbytestring from calibre.constants import filesystem_encoding, DEBUG from calibre.gui2.library import DEFAULT_SORT @@ -114,7 +114,7 @@ class BooksModel(QAbstractTableModel): # {{{ return cc_label in self.custom_columns def read_config(self): - self.use_roman_numbers = config['use_roman_numerals_for_series_number'] + pass def set_device_connected(self, is_connected): self.device_connected = is_connected @@ -355,63 +355,13 @@ class BooksModel(QAbstractTableModel): # {{{ return self.rowCount(None) def get_book_display_info(self, idx): - def custom_keys_to_display(): - ans = getattr(self, '_custom_fields_in_book_info', None) - if ans is None: - cfkeys = set(self.db.custom_field_keys()) - yes_fields = set(tweaks['book_details_will_display']) - no_fields = set(tweaks['book_details_wont_display']) - if '*' in yes_fields: - yes_fields = cfkeys - if '*' in no_fields: - no_fields = cfkeys - ans = frozenset(yes_fields - no_fields) - setattr(self, '_custom_fields_in_book_info', ans) - return ans - - data = {} - cdata = self.cover(idx) - if cdata: - data['cover'] = cdata - tags = list(self.db.get_tags(self.db.id(idx))) - if tags: - tags.sort(key=sort_key) - tags = ', '.join(tags) - else: - tags = _('None') - data[_('Tags')] = tags - formats = self.db.formats(idx) - if formats: - formats = formats.replace(',', ', ') - else: - formats = _('None') - data[_('Formats')] = formats - data[_('Path')] = self.db.abspath(idx) - data['id'] = self.id(idx) - comments = self.db.comments(idx) - if not comments: - comments = _('None') - data[_('Comments')] = comments - series = self.db.series(idx) - if series: - sidx = self.db.series_index(idx) - sidx = fmt_sidx(sidx, use_roman = self.use_roman_numbers) - data[_('Series')] = \ - _('Book %s of %s.')%\ - (sidx, prepare_string_for_xml(series)) mi = self.db.get_metadata(idx) - cf_to_display = custom_keys_to_display() - for key in mi.custom_field_keys(): - if key not in cf_to_display: - continue - name, val = mi.format_field(key) - if mi.metadata_for_field(key)['datatype'] == 'comments': - name += ':html' - if val and name not in data: - data[name] = val - - return data - + mi.size = mi.book_size + mi.cover_data = ('jpg', self.cover(idx)) + mi.id = self.db.id(idx) + mi.field_metadata = self.db.field_metadata + mi.path = self.db.abspath(idx, create_dirs=False) + return mi def current_changed(self, current, previous, emit_signal=True): if current.isValid(): @@ -425,16 +375,8 @@ class BooksModel(QAbstractTableModel): # {{{ def get_book_info(self, index): if isinstance(index, int): index = self.index(index, 0) + # If index is not valid returns None data = self.current_changed(index, None, False) - if data is None: - return data - row = index.row() - data[_('Title')] = self.db.title(row) - au = self.db.authors(row) - if not au: - au = _('Unknown') - au = authors_to_string([a.strip().replace('|', ',') for a in au.split(',')]) - data[_('Author(s)')] = au return data def metadata_for(self, ids): @@ -1189,39 +1131,46 @@ class DeviceBooksModel(BooksModel): # {{{ img = self.default_image return img - def current_changed(self, current, previous): - data = {} - item = self.db[self.map[current.row()]] - cover = self.cover(current.row()) - if cover is not self.default_image: - data['cover'] = cover - type = _('Unknown') + def get_book_display_info(self, idx): + from calibre.ebooks.metadata.book.base import Metadata + item = self.db[self.map[idx]] + cover = self.cover(idx) + if cover is self.default_image: + cover = None + title = item.title + if not title: + title = _('Unknown') + au = item.authors + if not au: + au = [_('Unknown')] + mi = Metadata(title, au) + mi.cover_data = ('jpg', cover) + fmt = _('Unknown') ext = os.path.splitext(item.path)[1] if ext: - type = ext[1:].lower() - data[_('Format')] = type - data[_('Path')] = item.path + fmt = ext[1:].lower() + mi.formats = [fmt] + mi.path = (item.path if item.path else None) dt = dt_factory(item.datetime, assume_utc=True) - data[_('Timestamp')] = isoformat(dt, sep=' ', as_utc=False) - data[_('Collections')] = ', '.join(item.device_collections) - - tags = getattr(item, 'tags', None) - if tags: - tags = u', '.join(tags) - else: - tags = _('None') - data[_('Tags')] = tags - comments = getattr(item, 'comments', None) - if not comments: - comments = _('None') - data[_('Comments')] = comments + mi.timestamp = dt + mi.device_collections = list(item.device_collections) + mi.tags = list(getattr(item, 'tags', [])) + mi.comments = getattr(item, 'comments', None) series = getattr(item, 'series', None) if series: sidx = getattr(item, 'series_index', 0) - sidx = fmt_sidx(sidx, use_roman = self.use_roman_numbers) - data[_('Series')] = _('Book %s of %s.')%(sidx, series) + mi.series = series + mi.series_index = sidx + return mi - self.new_bookdisplay_data.emit(data) + def current_changed(self, current, previous, emit_signal=True): + if current.isValid(): + idx = current.row() + data = self.get_book_display_info(idx) + if emit_signal: + self.new_bookdisplay_data.emit(data) + else: + return data def paths(self, rows): return [self.db[self.map[r.row()]].path for r in rows ] @@ -1281,7 +1230,7 @@ class DeviceBooksModel(BooksModel): # {{{ elif cname == 'authors': au = self.db[self.map[row]].authors if not au: - au = self.unknown + au = [_('Unknown')] return QVariant(authors_to_string(au)) elif cname == 'size': size = self.db[self.map[row]].size diff --git a/src/calibre/gui2/preferences/look_feel.py b/src/calibre/gui2/preferences/look_feel.py index 9f06d9a6ab..ed4312ad86 100644 --- a/src/calibre/gui2/preferences/look_feel.py +++ b/src/calibre/gui2/preferences/look_feel.py @@ -5,16 +5,22 @@ __license__ = 'GPL v3' __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' -from PyQt4.Qt import QApplication, QFont, QFontInfo, QFontDialog +from PyQt4.Qt import (QApplication, QFont, QFontInfo, QFontDialog, + QAbstractListModel) from calibre.gui2.preferences import ConfigWidgetBase, test_widget, CommaSeparatedList from calibre.gui2.preferences.look_feel_ui import Ui_Form from calibre.gui2 import config, gprefs, qt_app -from calibre.utils.localization import available_translations, \ - get_language, get_lang +from calibre.utils.localization import (available_translations, + get_language, get_lang) from calibre.utils.config import prefs from calibre.utils.icu import sort_key +class DisplayedFields(QAbstractListModel): + + def __init__(self, parent=None): + QAbstractListModel.__init__(self, parent) + class ConfigWidget(ConfigWidgetBase, Ui_Form): def genesis(self, gui): diff --git a/src/calibre/gui2/preferences/look_feel.ui b/src/calibre/gui2/preferences/look_feel.ui index 996caeb653..2d5409271c 100644 --- a/src/calibre/gui2/preferences/look_feel.ui +++ b/src/calibre/gui2/preferences/look_feel.ui @@ -14,280 +14,421 @@ Form - - - - User Interface &layout (needs restart): - - - opt_gui_layout - - - - - - - - 250 - 16777215 - - - - QComboBox::AdjustToMinimumContentsLengthWithIcon - - - 20 - - - - - - - &Number of covers to show in browse mode (needs restart): - - - opt_cover_flow_queue_length - - - - - - - - - - Choose &language (requires restart): - - - opt_language - - - - - - - QComboBox::AdjustToMinimumContentsLengthWithIcon - - - 20 - - - - - - - Show &average ratings in the tags browser - - - true - - - - - - - Disable all animations. Useful if you have a slow/old computer. - - - Disable &animations - - - - - - - Enable system &tray icon (needs restart) - - - - - - - Show &splash screen at startup - - - - - - - Disable &notifications in system tray - - - - - - - Use &Roman numerals for series - - - true - - - - - - - Show cover &browser in a separate window (needs restart) - - - - - - - - - Tags browser category &partitioning method: - - - opt_tags_browser_partition_method - - - - - - - Choose how tag browser subcategories are displayed when + + + + + + 0 + 0 + 682 + 254 + + + + Main interface + + + + + + User Interface &layout (needs restart): + + + opt_gui_layout + + + + + + + + 250 + 16777215 + + + + QComboBox::AdjustToMinimumContentsLengthWithIcon + + + 20 + + + + + + + Choose &language (requires restart): + + + opt_language + + + + + + + QComboBox::AdjustToMinimumContentsLengthWithIcon + + + 20 + + + + + + + Disable all animations. Useful if you have a slow/old computer. + + + Disable &animations + + + + + + + Show &splash screen at startup + + + + + + + &Toolbar + + + + + + + + + &Icon size: + + + opt_toolbar_icon_size + + + + + + + + + + Show &text under icons: + + + opt_toolbar_text + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + Interface font: + + + font_display + + + + + + + true + + + + + + + + + Change &font (needs restart) + + + + + + + Enable system &tray icon (needs restart) + + + + + + + Disable &notifications in system tray + + + + + + + + + 0 + 0 + 699 + 151 + + + + Tag Browser + + + + + + Show &average ratings in the tags browser + + + true + + + + + + + + + Tags browser category &partitioning method: + + + opt_tags_browser_partition_method + + + + + + + Choose how tag browser subcategories are displayed when there are more items than the limit. Select by first letter to see an A, B, C list. Choose partitioned to have a list of fixed-sized groups. Set to disabled if you never want subcategories - - - - - - - &Collapse when more items than: - - - opt_tags_browser_collapse_at - - - - - - - If a Tag Browser category has more than this number of items, it is divided + + + + + + + &Collapse when more items than: + + + opt_tags_browser_collapse_at + + + + + + + If a Tag Browser category has more than this number of items, it is divided up into sub-categories. If the partition method is set to disable, this value is ignored. - - - 10000 - - - - - - - Qt::Horizontal - - - - 20 - 5 - - - - - - - - - - Categories with &hierarchical items: - - - opt_categories_using_hierarchy - - - - - - - A comma-separated list of columns in which items containing + + + 10000 + + + + + + + Qt::Horizontal + + + + 20 + 5 + + + + + + + + + + Categories with &hierarchical items: + + + opt_categories_using_hierarchy + + + + + + + A comma-separated list of columns in which items containing periods are displayed in the tag browser trees. For example, if this box contains 'tags' then tags of the form 'Mystery.English' and 'Mystery.Thriller' will be displayed with English and Thriller both under 'Mystery'. If 'tags' is not in this box, then the tags will be displayed each on their own line. - + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + 0 + 0 + 699 + 306 + + + + Cover Browser + + + + + + Show cover &browser in a separate window (needs restart) + + + + + + + &Number of covers to show in browse mode (needs restart): + + + opt_cover_flow_queue_length + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + 0 + 0 + 699 + 306 + + + + Book Details + + + + + + Use &Roman numerals for series + + + true + + + + + + + Select displayed metadata + + + + + + true + + + + + + + Move up + + + + :/images/arrow-up.png:/images/arrow-up.png + + + + + + + Move down + + + + :/images/arrow-down.png:/images/arrow-down.png + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + - - - - &Toolbar - - - - - - - - - &Icon size: - - - opt_toolbar_icon_size - - - - - - - - - - Show &text under icons: - - - opt_toolbar_text - - - - - - - - - - - - Interface font: - - - font_display - - - - - - - true - - - - - - - - - Change &font (needs restart) - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - @@ -297,6 +438,8 @@ then the tags will be displayed each on their own line.
calibre/gui2/complete.h
- + + + diff --git a/src/calibre/library/db/__init__.py b/src/calibre/library/db/__init__.py deleted file mode 100644 index 0080175bfa..0000000000 --- a/src/calibre/library/db/__init__.py +++ /dev/null @@ -1,9 +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 ' -__docformat__ = 'restructuredtext en' - - - diff --git a/src/calibre/library/db/base.py b/src/calibre/library/db/base.py deleted file mode 100644 index a2374583eb..0000000000 --- a/src/calibre/library/db/base.py +++ /dev/null @@ -1,37 +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 ' -__docformat__ = 'restructuredtext en' - - -''' Design documentation {{{ - - Storage paradigm {{{ - * Agnostic to storage paradigm (i.e. no book per folder assumptions) - * Two separate concepts: A store and collection - A store is a backend, like a sqlite database associated with a path on - the local filesystem, or a cloud based storage solution. - A collection is a user defined group of stores. Most of the logic for - data manipulation sorting/searching/restrictions should be in the collection - class. The collection class should transparently handle the - conversion from store name + id to row number in the collection. - * Not sure how feasible it is to allow many-many maps between stores - and collections. - }}} - - Event system {{{ - * Comprehensive event system that other components can subscribe to - * Subscribers should be able to temporarily block receiving events - * Should event dispatch be asynchronous? - * Track last modified time for metadata and each format - }}} -}}}''' - -# Imports {{{ -# }}} - - - - diff --git a/src/calibre/library/field_metadata.py b/src/calibre/library/field_metadata.py index 374505c467..0ae4d74242 100644 --- a/src/calibre/library/field_metadata.py +++ b/src/calibre/library/field_metadata.py @@ -188,7 +188,7 @@ class FieldMetadata(dict): 'datatype':'text', 'is_multiple':None, 'kind':'field', - 'name':None, + 'name':_('Author Sort'), 'search_terms':['author_sort'], 'is_custom':False, 'is_category':False, @@ -238,7 +238,7 @@ class FieldMetadata(dict): 'datatype':'datetime', 'is_multiple':None, 'kind':'field', - 'name':_('Date'), + 'name':_('Modified'), 'search_terms':['last_modified'], 'is_custom':False, 'is_category':False, @@ -258,7 +258,7 @@ class FieldMetadata(dict): 'datatype':'text', 'is_multiple':None, 'kind':'field', - 'name':None, + 'name':_('Path'), 'search_terms':[], 'is_custom':False, 'is_category':False, @@ -308,7 +308,7 @@ class FieldMetadata(dict): 'datatype':'float', 'is_multiple':None, 'kind':'field', - 'name':_('Size (MB)'), + 'name':_('Size'), 'search_terms':['size'], 'is_custom':False, 'is_category':False, @@ -399,6 +399,13 @@ class FieldMetadata(dict): if self._tb_cats[k]['kind']=='field' and self._tb_cats[k]['datatype'] is not None] + def displayable_field_keys(self): + return [k for k in self._tb_cats.keys() + if self._tb_cats[k]['kind']=='field' and + self._tb_cats[k]['datatype'] is not None and + k not in ('au_map', 'marked', 'ondevice', 'cover') and + not self.is_series_index(k)] + def standard_field_keys(self): return [k for k in self._tb_cats.keys() if self._tb_cats[k]['kind']=='field' and @@ -442,6 +449,11 @@ class FieldMetadata(dict): def is_custom_field(self, key): return key.startswith(self.custom_field_prefix) + def is_series_index(self, key): + m = self[key] + return (m['datatype'] == 'float' and key.endswith('_index') and + key[:-6] in self) + def key_to_label(self, key): if 'label' not in self._tb_cats[key]: return key