diff --git a/src/calibre/gui2/actions.py b/src/calibre/gui2/actions.py index 4b2e367080..6da04a41be 100644 --- a/src/calibre/gui2/actions.py +++ b/src/calibre/gui2/actions.py @@ -1064,6 +1064,12 @@ class ViewAction(object): # {{{ if fmt_path: self._view_file(fmt_path) + def view_format_by_id(self, id_, format): + fmt_path = self.library_view.model().db.format_abspath(id_, format, + index_is_id=True) + if fmt_path: + self._view_file(fmt_path) + def metadata_view_format(self, fmt): fmt_path = self.library_view.model().db.\ format_abspath(self._metadata_view_id, @@ -1146,6 +1152,9 @@ class ViewAction(object): # {{{ path = self.library_view.model().db.abspath(row.row()) QDesktopServices.openUrl(QUrl.fromLocalFile(path)) + def view_folder_for_id(self, id_): + path = self.library_view.model().db.abspath(id_, index_is_id=True) + QDesktopServices.openUrl(QUrl.fromLocalFile(path)) def view_book(self, triggered): rows = self.current_view().selectionModel().selectedRows() diff --git a/src/calibre/gui2/book_details.py b/src/calibre/gui2/book_details.py new file mode 100644 index 0000000000..485c1b2d19 --- /dev/null +++ b/src/calibre/gui2/book_details.py @@ -0,0 +1,171 @@ +#!/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' + +import os, collections + +from PyQt4.Qt import QLabel, QPixmap, QSize, QWidget, Qt, pyqtSignal, \ + QVBoxLayout, QScrollArea + +from calibre import fit_image, prepare_string_for_xml +from calibre.gui2.widgets import IMAGE_EXTENSIONS +from calibre.ebooks import BOOK_EXTENSIONS +from calibre.constants import preferred_encoding + +class CoverView(QLabel): + + def __init__(self, parent=None): + QLabel.__init__(self, parent) + self.default_pixmap = QPixmap(I('book.svg')) + self.max_width, self.max_height = 120, 120 + self.setScaledContents(True) + self.setPixmap(self.default_pixmap) + + def do_layout(self): + pixmap = self.pixmap() + pwidth, pheight = pixmap.width(), pixmap.height() + width, height = fit_image(pwidth, pheight, + self.max_width, self.max_height)[1:] + self.setMaximumWidth(width) + try: + aspect_ratio = pwidth/float(pheight) + except ZeroDivisionError: + aspect_ratio = 1 + mh = min(self.max_height, int(width/aspect_ratio)) + self.setMaximumHeight(mh) + + def setPixmap(self, pixmap): + QLabel.setPixmap(self, pixmap) + self.do_layout() + + + def sizeHint(self): + return QSize(self.maximumWidth(), self.maximumHeight()) + + def relayout(self, parent_size): + self.max_height = int(parent_size.height()/3.) + self.max_width = parent_size.width() + self.do_layout() + + def show_data(self, data): + if data.has_key('cover'): + self.setPixmap(QPixmap.fromImage(data.pop('cover'))) + else: + self.setPixmap(self.default_pixmap) + +class BookInfo(QScrollArea): + + def __init__(self, parent=None): + QScrollArea.__init__(self, parent) + self.setWidgetResizable(True) + self.label = QLabel() + self.label.setWordWrap(True) + self.setWidget(self.label) + + def show_data(self, data): + self.label.setText('') + self.data = data.copy() + rows = render_rows(self.data) + rows = u'\n'.join([u'%s:%s'%(k,t) for + k, t in rows]) + self.label.setText(u'%s
'%rows) + +WEIGHTS = collections.defaultdict(lambda : 100) +WEIGHTS[_('Path')] = 0 +WEIGHTS[_('Formats')] = 1 +WEIGHTS[_('Collections')] = 2 +WEIGHTS[_('Series')] = 3 +WEIGHTS[_('Tags')] = 4 + +def render_rows(data): + keys = data.keys() + keys.sort(cmp=lambda x, y: cmp(WEIGHTS[x], WEIGHTS[y])) + rows = [] + for key in keys: + txt = data[key] + if key in ('id', _('Comments')) or not txt or not txt.strip() or \ + txt == 'None': + continue + if isinstance(key, str): + key = key.decode(preferred_encoding, 'replace') + if isinstance(txt, str): + txt = txt.decode(preferred_encoding, 'replace') + if '' not in txt: + txt = prepare_string_for_xml(txt) + if 'id' in data: + if key == _('Path'): + txt = '...'+os.sep+os.sep.join(txt.split(os.sep)[-2:]) + txt = u'%s'%(data['id'], txt) + 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) + rows.append((key, txt)) + return rows + +class BookDetails(QWidget): + + resized = pyqtSignal(object) + show_book_info = pyqtSignal() + + # Drag 'n drop {{{ + DROPABBLE_EXTENSIONS = IMAGE_EXTENSIONS+BOOK_EXTENSIONS + files_dropped = pyqtSignal(object, object) + + @classmethod + def paths_from_event(cls, event): + ''' + Accept a drop event and return a list of paths that can be read from + and represent files with extensions. + ''' + if event.mimeData().hasFormat('text/uri-list'): + urls = [unicode(u.toLocalFile()) for u in event.mimeData().urls()] + urls = [u for u in urls if os.path.splitext(u)[1] and os.access(u, os.R_OK)] + return [u for u in urls if os.path.splitext(u)[1][1:].lower() in cls.DROPABBLE_EXTENSIONS] + + def dragEnterEvent(self, event): + if int(event.possibleActions() & Qt.CopyAction) + \ + int(event.possibleActions() & Qt.MoveAction) == 0: + return + paths = self.paths_from_event(event) + if paths: + event.acceptProposedAction() + + def dropEvent(self, event): + paths = self.paths_from_event(event) + event.setDropAction(Qt.CopyAction) + self.files_dropped.emit(event, paths) + + def dragMoveEvent(self, event): + event.acceptProposedAction() + + # }}} + + def __init__(self, parent=None): + QWidget.__init__(self, parent) + self._layout = QVBoxLayout() + + self.setLayout(self._layout) + self.cover_view = CoverView(self) + self.cover_view.relayout() + self.resized.connect(self.cover_view.relayout, type=Qt.QueuedConnection) + self._layout.addWidget(self.cover_view) + + + def resizeEvent(self, ev): + self.resized.emit(self.size()) + + def show_data(self, data): + self.cover_view.show_data(data) + + def reset_info(self): + self.show_data({}) + + def mouseReleaseEvent(self, ev): + self.show_book_info.emit() + + diff --git a/src/calibre/gui2/dialogs/book_info.py b/src/calibre/gui2/dialogs/book_info.py index 0cd7c1d419..20ddfae0b4 100644 --- a/src/calibre/gui2/dialogs/book_info.py +++ b/src/calibre/gui2/dialogs/book_info.py @@ -121,6 +121,7 @@ class BookInfo(QDialog, Ui_BookInfo): f = f.strip() info[_('Formats')] += '%s, '%(f,f) for key in info.keys(): + if key == 'id': continue txt = info[key] txt = u'
\n'.join(textwrap.wrap(txt, 120)) rows += u'%s:%s'%(key, txt) diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index 293a522a9e..ff4b2b6ee9 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -21,7 +21,7 @@ from calibre.utils.date import dt_factory, qt_to_dt, isoformat from calibre.ebooks.metadata.meta import set_metadata as _set_metadata from calibre.utils.search_query_parser import SearchQueryParser from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, REGEXP_MATCH -from calibre import strftime, isbytestring +from calibre import strftime, isbytestring, prepare_string_for_xml from calibre.constants import filesystem_encoding from calibre.gui2.library import DEFAULT_SORT @@ -300,6 +300,7 @@ class BooksModel(QAbstractTableModel): # {{{ 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') @@ -308,7 +309,9 @@ class BooksModel(QAbstractTableModel): # {{{ 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, series) + data[_('Series')] = \ + _('Book %s of %s.')%\ + (sidx, prepare_string_for_xml(series)) return data diff --git a/src/calibre/gui2/status.py b/src/calibre/gui2/status.py index 06c3e9c85f..377410cf86 100644 --- a/src/calibre/gui2/status.py +++ b/src/calibre/gui2/status.py @@ -1,6 +1,7 @@ __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal ' -import os, collections + +import os from PyQt4.Qt import QStatusBar, QLabel, QWidget, QHBoxLayout, QPixmap, \ QSizePolicy, QScrollArea, Qt, QSize, pyqtSignal, \ @@ -13,6 +14,7 @@ from calibre.gui2.widgets import IMAGE_EXTENSIONS from calibre.gui2.notify import get_notifier from calibre.ebooks import BOOK_EXTENSIONS from calibre.library.comments import comments_to_html +from calibre.gui2.book_details import render_rows class BookInfoDisplay(QWidget): @@ -91,24 +93,27 @@ class BookInfoDisplay(QWidget): class BookDataDisplay(QLabel): - mr = pyqtSignal(int) + mr = pyqtSignal(object) + link_clicked = pyqtSignal(object) def __init__(self): QLabel.__init__(self) self.setText('') self.setWordWrap(True) self.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)) + self.linkActivated.connect(self.link_activated) + self._link_clicked = False def mouseReleaseEvent(self, ev): - self.mr.emit(1) + QLabel.mouseReleaseEvent(self, ev) + if not self._link_clicked: + self.mr.emit(ev) + self._link_clicked = False - WEIGHTS = collections.defaultdict(lambda : 100) - WEIGHTS[_('Path')] = 0 - WEIGHTS[_('Formats')] = 1 - WEIGHTS[_('Collections')] = 2 - WEIGHTS[_('Series')] = 3 - WEIGHTS[_('Tags')] = 4 - WEIGHTS[_('Comments')] = 5 + def link_activated(self, link): + self._link_clicked = True + link = unicode(link) + self.link_clicked.emit(link) show_book_info = pyqtSignal() @@ -129,6 +134,7 @@ class BookInfoDisplay(QWidget): self._layout.setAlignment(self.cover_display, Qt.AlignTop|Qt.AlignLeft) def mouseReleaseEvent(self, ev): + ev.accept() self.show_book_info.emit() def show_data(self, data): @@ -140,23 +146,11 @@ class BookInfoDisplay(QWidget): rows, comments = [], '' self.book_data.setText('') self.data = data.copy() - keys = data.keys() - keys.sort(cmp=lambda x, y: cmp(self.WEIGHTS[x], self.WEIGHTS[y])) - for key in keys: - txt = data[key] - if not txt or not txt.strip() or txt == 'None': - continue - if isinstance(key, str): - key = key.decode(preferred_encoding, 'replace') - if isinstance(txt, str): - txt = txt.decode(preferred_encoding, 'replace') - if key == _('Comments'): - comments = comments_to_html(txt) - else: - rows.append((key, txt)) + rows = render_rows(self.data) rows = '\n'.join([u'%s:%s'%(k,t) for k, t in rows]) - if comments: + if _('Comments') in self.data: + comments = comments_to_html(self.data[_('Comments')]) comments = 'Comments:'+comments left_pane = u'%s
'%rows right_pane = u'
%s
'%comments @@ -193,6 +187,8 @@ class BookDetailsInterface(object): # These signals must be defined in the class implementing this interface files_dropped = None show_book_info = None + open_containing_folder = None + view_specific_format = None def reset_info(self): raise NotImplementedError() @@ -204,7 +200,8 @@ class StatusBar(QStatusBar, StatusBarInterface, BookDetailsInterface): files_dropped = pyqtSignal(object, object) show_book_info = pyqtSignal() - + open_containing_folder = pyqtSignal(int) + view_specific_format = pyqtSignal(int, object) resized = pyqtSignal(object) @@ -219,11 +216,21 @@ class StatusBar(QStatusBar, StatusBarInterface, BookDetailsInterface): type=Qt.QueuedConnection) self.book_info.files_dropped.connect(self.files_dropped.emit, type=Qt.QueuedConnection) + self.book_info.book_data.link_clicked.connect(self._link_clicked) self.addWidget(self.scroll_area, 100) self.setMinimumHeight(120) self.resized.connect(self.book_info.cover_display.relayout) self.book_info.cover_display.relayout(self.size()) + + def _link_clicked(self, link): + typ, _, val = link.partition(':') + if typ == 'path': + self.open_containing_folder.emit(int(val)) + if typ == 'format': + id_, fmt = val.split(':') + self.view_specific_format.emit(int(id_), fmt) + def resizeEvent(self, ev): self.resized.emit(self.size()) diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 682ede1978..99c7856478 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -219,6 +219,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, # {{{ self.status_bar.initialize(self.system_tray_icon) self.book_details.show_book_info.connect(self.show_book_info) self.book_details.files_dropped.connect(self.files_dropped_on_book) + self.book_details.open_containing_folder.connect(self.view_folder_for_id) + self.book_details.view_specific_format.connect(self.view_format_by_id) ####################### Setup Toolbar ##################### ToolbarMixin.__init__(self) diff --git a/src/calibre/library/server/content.py b/src/calibre/library/server/content.py index 4dd32eb284..0e04fdfdb5 100644 --- a/src/calibre/library/server/content.py +++ b/src/calibre/library/server/content.py @@ -60,6 +60,7 @@ class ContentServer(object): items.sort(cmp=self.seriescmp, reverse=not order) else: lookup = 'sort' if field == 'title' else field + lookup = 'author_sort' if field == 'authors' else field field = self.db.FIELD_MAP[lookup] getter = operator.itemgetter(field) items.sort(cmp=lambda x, y: cmpf(getter(x), getter(y)), reverse=not order)