Make the book details panel completely user configurable

This commit is contained in:
Kovid Goyal 2011-04-24 19:44:17 -06:00
parent 4dbd7840d6
commit 978e1f826b
14 changed files with 717 additions and 704 deletions

View File

@ -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

View File

@ -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
}

View File

@ -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)

View File

@ -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

View File

@ -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()

View File

@ -5,67 +5,151 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__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':
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 '</font>' not in txt:
txt = prepare_string_for_xml(txt)
if 'id' in data:
if key == _('Path'):
txt = u'<a href="path:%s" title="%s">%s</a>'%(data['id'],
txt, _('Click to open'))
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)
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'''\
<html>
<head>
<style type="text/css">
body, td {background-color: transparent; font-size: %dpx; color: %s }
</style>
<style type="text/css">
%s
</style>
</head>
<body>
%%s
</body>
<html>
'''%(f, c, css)
comments = u''
if mi.comments:
comments = comments_to_html(force_unicode(mi.comments))
right_pane = u'<div id="comments" class="comments">%s</div>'%comments
if vertical:
ans = templ%(table+right_pane)
else:
if key == _('Path'):
txt = u'<a href="devpath:%s">%s</a>'%(txt,
_('Click to open'))
ans = templ%(u'<table><tr><td valign="top" '
'style="padding-right:2em; width:40%%">%s</td><td valign="top">%s</td></tr></table>'
% (table, right_pane))
return ans
rows.append((key, txt))
return rows
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
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'<td class="comments" colspan="2">%s</td>'%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'<a href="%s:%s" title="%s">%s</a>' % (scheme, url,
prepare_string_for_xml(path, True), _('Click to open'))
ans.append((field, u'<td class="title">%s</td><td>%s</td>'%(name, link)))
elif field == 'formats':
if isdevice: continue
fmts = [u'<a href="format:%s:%s">%s</a>' % (mi.id, x, x) for x
in mi.formats]
ans.append((field, u'<td class="title">%s</td><td>%s</td>'%(name,
u', '.join(fmts))))
elif field == 'identifiers':
pass # TODO
else:
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 <span class="series_name">%s</span>')%(fmt_sidx(sidx,
use_roman=use_roman_numbers),
prepare_string_for_xml(getattr(mi, field)))
ans.append((field, u'<td class="title">%s</td><td>%s</td>'%(name, val)))
dc = getattr(mi, 'device_collections', [])
if dc:
dc = u', '.join(sorted(dc, key=sort_key))
ans.append(('device_collections',
u'<td class="title">%s</td><td>%s</td>'%(
_('Collections')+':', dc)))
def classname(field):
try:
dt = fm[field]['datatype']
except:
dt = 'text'
return 'datatype_%s'%dt
ans = [u'<tr id="%s" class="%s">%s</tr>'%(field.replace('#', '_'),
classname(field), html) for field, html in ans]
# print '\n'.join(ans)
return u'<table class="fields">%s</table>'%(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'<tr><td valign="top"><b>%s:</b></td><td valign="top">%s</td></tr>'%(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'''\
<html>
<head>
<style type="text/css">
body, td {background-color: transparent; font-size: %dpx; color: %s }
a { text-decoration: none; color: blue }
div.description { margin-top: 0; padding-top: 0; text-indent: 0 }
table { margin-bottom: 0; padding-bottom: 0; }
</style>
</head>
<body>
%%s
</body>
<html>
'''%(f, c)
if self.vertical:
extra = ''
if comments:
extra = u'<div class="description">%s</div>'%comments
self.setHtml(templ%(u'<table>%s</table>%s'%(rows, extra)))
else:
left_pane = u'<table>%s</table>'%rows
right_pane = u'<div>%s</div>'%comments
self.setHtml(templ%(u'<table><tr><td valign="top" '
'style="padding-right:2em; width:40%%">%s</td><td valign="top">%s</td></tr></table>'
% (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')))
# }}}

View File

@ -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('<b>'+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 '<br><br>' for x in lines]
comments = '\n'.join(lines)
self.comments.setHtml('<div>%s</div>' % 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('<b>'+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')] = '<a href="%s">%s</a>'%(p, p)
if _('Formats') in info.keys():
formats = info[_('Formats')].split(',')
info[_('Formats')] = ''
for f in formats:
f = f.strip()
info[_('Formats')] += '<a href="%s">%s</a>, '%(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'<br />\n'.join(textwrap.wrap(txt, 120))
rows += u'<tr><td><b>%s:</b></td><td>%s</td></tr>'%(key, txt)
self.text.setText(u'<table>'+rows+'</table>')

View File

@ -20,6 +20,12 @@
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0" colspan="2">
<widget class="QLabel" name="title">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<weight>75</weight>
@ -34,82 +40,17 @@
</property>
</widget>
</item>
<item row="1" column="0" rowspan="3">
<item row="2" column="0" rowspan="3">
<widget class="CoverView" name="cover"/>
</item>
<item row="1" column="1">
<widget class="QScrollArea" name="scrollArea">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="scrollAreaWidgetContents">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>435</width>
<height>670</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="text">
<property name="text">
<string>TextLabel</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Comments</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QWebView" name="comments">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>350</width>
<height>16777215</height>
</size>
</property>
<property name="url">
<url>
<string>about:blank</string>
</url>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item row="2" column="1">
<item row="3" column="1">
<widget class="QCheckBox" name="fit_cover">
<property name="text">
<string>Fit &amp;cover within view</string>
</property>
</widget>
</item>
<item row="3" column="1">
<item row="4" column="1">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="previous_button">
@ -135,6 +76,15 @@
</item>
</layout>
</item>
<item row="2" column="1">
<widget class="QWebView" name="details">
<property name="url">
<url>
<string>about:blank</string>
</url>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>

View File

@ -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 <font face="serif">%s</font> of %s.')%(sidx, series)
mi.series = series
mi.series_index = sidx
return mi
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

View File

@ -5,16 +5,22 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__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):

View File

@ -14,6 +14,21 @@
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0" colspan="2">
<widget class="QToolBox" name="toolBox">
<widget class="QWidget" name="page">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>682</width>
<height>254</height>
</rect>
</property>
<attribute name="label">
<string>Main interface</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_4">
<item row="0" column="0">
<widget class="QLabel" name="label_17">
<property name="text">
@ -41,19 +56,6 @@
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>&amp;Number of covers to show in browse mode (needs restart):</string>
</property>
<property name="buddy">
<cstring>opt_cover_flow_queue_length</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="opt_cover_flow_queue_length"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Choose &amp;language (requires restart):</string>
@ -63,7 +65,7 @@
</property>
</widget>
</item>
<item row="2" column="1">
<item row="1" column="1">
<widget class="QComboBox" name="opt_language">
<property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum>
@ -73,17 +75,7 @@
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="opt_show_avg_rating">
<property name="text">
<string>Show &amp;average ratings in the tags browser</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="1">
<item row="2" column="1">
<widget class="QCheckBox" name="opt_disable_animations">
<property name="toolTip">
<string>Disable all animations. Useful if you have a slow/old computer.</string>
@ -93,45 +85,129 @@
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QCheckBox" name="opt_systray_icon">
<property name="text">
<string>Enable system &amp;tray icon (needs restart)</string>
</property>
</widget>
</item>
<item row="4" column="1">
<item row="3" column="1">
<widget class="QCheckBox" name="opt_show_splash_screen">
<property name="text">
<string>Show &amp;splash screen at startup</string>
</property>
</widget>
</item>
<item row="5" column="0">
<item row="5" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>&amp;Toolbar</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="1">
<widget class="QComboBox" name="opt_toolbar_icon_size"/>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>&amp;Icon size:</string>
</property>
<property name="buddy">
<cstring>opt_toolbar_icon_size</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="opt_toolbar_text"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Show &amp;text under icons:</string>
</property>
<property name="buddy">
<cstring>opt_toolbar_text</cstring>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="8" column="0">
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="6" column="0">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Interface font:</string>
</property>
<property name="buddy">
<cstring>font_display</cstring>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="font_display">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item row="6" column="1">
<widget class="QPushButton" name="change_font_button">
<property name="text">
<string>Change &amp;font (needs restart)</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="opt_systray_icon">
<property name="text">
<string>Enable system &amp;tray icon (needs restart)</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="opt_disable_tray_notification">
<property name="text">
<string>Disable &amp;notifications in system tray</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QCheckBox" name="opt_use_roman_numerals_for_series_number">
</layout>
</widget>
<widget class="QWidget" name="page_2">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>699</width>
<height>151</height>
</rect>
</property>
<attribute name="label">
<string>Tag Browser</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_5">
<item row="0" column="0" colspan="2">
<widget class="QCheckBox" name="opt_show_avg_rating">
<property name="text">
<string>Use &amp;Roman numerals for series</string>
<string>Show &amp;average ratings in the tags browser</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="6" column="0" colspan="2">
<widget class="QCheckBox" name="opt_separate_cover_flow">
<property name="text">
<string>Show cover &amp;browser in a separate window (needs restart)</string>
</property>
</widget>
</item>
<item row="7" column="0" colspan="2">
<item row="1" column="0" colspan="2">
<layout class="QHBoxLayout">
<item>
<widget class="QLabel" name="label_6">
@ -190,7 +266,7 @@ up into sub-categories. If the partition method is set to disable, this value is
</item>
</layout>
</item>
<item row="8" column="0">
<item row="2" column="0">
<widget class="QLabel" name="label_81">
<property name="text">
<string>Categories with &amp;hierarchical items:</string>
@ -200,7 +276,7 @@ up into sub-categories. If the partition method is set to disable, this value is
</property>
</widget>
</item>
<item row="8" column="1">
<item row="2" column="1">
<widget class="MultiCompleteLineEdit" name="opt_categories_using_hierarchy">
<property name="toolTip">
<string>A comma-separated list of columns in which items containing
@ -212,71 +288,8 @@ then the tags will be displayed each on their own line.</string>
</property>
</widget>
</item>
<item row="15" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>&amp;Toolbar</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="1">
<widget class="QComboBox" name="opt_toolbar_icon_size"/>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>&amp;Icon size:</string>
</property>
<property name="buddy">
<cstring>opt_toolbar_icon_size</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="opt_toolbar_text"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Show &amp;text under icons:</string>
</property>
<property name="buddy">
<cstring>opt_toolbar_text</cstring>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="16" column="0">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Interface font:</string>
</property>
<property name="buddy">
<cstring>font_display</cstring>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="font_display">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item row="16" column="1">
<widget class="QPushButton" name="change_font_button">
<property name="text">
<string>Change &amp;font (needs restart)</string>
</property>
</widget>
</item>
<item row="17" column="0" colspan="2">
<spacer name="verticalSpacer">
<item row="3" column="0" colspan="2">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
@ -290,6 +303,134 @@ then the tags will be displayed each on their own line.</string>
</item>
</layout>
</widget>
<widget class="QWidget" name="page_3">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>699</width>
<height>306</height>
</rect>
</property>
<attribute name="label">
<string>Cover Browser</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_6">
<item row="0" column="0" colspan="2">
<widget class="QCheckBox" name="opt_separate_cover_flow">
<property name="text">
<string>Show cover &amp;browser in a separate window (needs restart)</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>&amp;Number of covers to show in browse mode (needs restart):</string>
</property>
<property name="buddy">
<cstring>opt_cover_flow_queue_length</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="opt_cover_flow_queue_length"/>
</item>
<item row="2" column="0" colspan="2">
<spacer name="verticalSpacer_4">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="page_4">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>699</width>
<height>306</height>
</rect>
</property>
<attribute name="label">
<string>Book Details</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_7">
<item row="0" column="1">
<widget class="QCheckBox" name="opt_use_roman_numerals_for_series_number">
<property name="text">
<string>Use &amp;Roman numerals for series</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Select displayed metadata</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0" rowspan="3">
<widget class="QListView" name="field_display_order">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QToolButton" name="toolButton">
<property name="toolTip">
<string>Move up</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/arrow-up.png</normaloff>:/images/arrow-up.png</iconset>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QToolButton" name="toolButton_2">
<property name="toolTip">
<string>Move down</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/arrow-down.png</normaloff>:/images/arrow-down.png</iconset>
</property>
</widget>
</item>
<item row="1" column="1">
<spacer name="verticalSpacer_5">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>MultiCompleteLineEdit</class>
@ -297,6 +438,8 @@ then the tags will be displayed each on their own line.</string>
<header>calibre/gui2/complete.h</header>
</customwidget>
</customwidgets>
<resources/>
<resources>
<include location="../../../../resources/images.qrc"/>
</resources>
<connections/>
</ui>

View File

@ -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 <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

View File

@ -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 <kovid@kovidgoyal.net>'
__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 {{{
# }}}

View File

@ -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