mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Merge from trunk
This commit is contained in:
commit
dfa53c4cf8
@ -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()
|
||||
|
171
src/calibre/gui2/book_details.py
Normal file
171
src/calibre/gui2/book_details.py
Normal file
@ -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 <kovid@kovidgoyal.net>'
|
||||
__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'<tr><td valign="top"><b>%s:</b></td><td valign="top">%s</td></tr>'%(k,t) for
|
||||
k, t in rows])
|
||||
self.label.setText(u'<table>%s</table>'%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 '</font>' 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'<a href="path:%s">%s</a>'%(data['id'], txt)
|
||||
if key == _('Formats') and txt and txt != _('None'):
|
||||
fmts = [x.strip() for x in txt.split(',')]
|
||||
fmts = [u'<a href="format:%s:%s">%s</a>' % (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()
|
||||
|
||||
|
@ -121,6 +121,7 @@ class BookInfo(QDialog, Ui_BookInfo):
|
||||
f = f.strip()
|
||||
info[_('Formats')] += '<a href="%s">%s</a>, '%(f,f)
|
||||
for key in info.keys():
|
||||
if key == 'id': continue
|
||||
txt = info[key]
|
||||
txt = u'<br />\n'.join(textwrap.wrap(txt, 120))
|
||||
rows += u'<tr><td><b>%s:</b></td><td>%s</td></tr>'%(key, txt)
|
||||
|
@ -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 <font face="serif">%s</font> of %s.')%(sidx, series)
|
||||
data[_('Series')] = \
|
||||
_('Book <font face="serif">%s</font> of %s.')%\
|
||||
(sidx, prepare_string_for_xml(series))
|
||||
|
||||
return data
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
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'<tr><td valign="top"><b>%s:</b></td><td valign="top">%s</td></tr>'%(k,t) for
|
||||
k, t in rows])
|
||||
if comments:
|
||||
if _('Comments') in self.data:
|
||||
comments = comments_to_html(self.data[_('Comments')])
|
||||
comments = '<b>Comments:</b>'+comments
|
||||
left_pane = u'<table>%s</table>'%rows
|
||||
right_pane = u'<div>%s</div>'%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())
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user