Finish average rating display.

1) change schema upgrade to not use field_metadata.
2) add option to preferences to show (or not) the average rating
3) add a tweak to choose between the clipped star and the rectangle
4) fix bug -- missing set of the search_as_you_type checkbox
This commit is contained in:
Charles Haley 2010-06-13 07:46:31 +01:00
parent 97111f70a0
commit 8ff2f2f865
7 changed files with 106 additions and 99 deletions

View File

@ -71,3 +71,10 @@ gui_pubdate_display_format = 'MMM yyyy'
# order until the title is edited. Double-clicking on a title and hitting return # order until the title is edited. Double-clicking on a title and hitting return
# without changing anything is sufficient to change the sort. # without changing anything is sufficient to change the sort.
title_series_sorting = 'library_order' title_series_sorting = 'library_order'
# How to render average rating in the tag browser.
# There are two rendering methods available. The first is to show a partial
# star, and the second is to show a partially filled rectangle. The first is
# better looking, but uses more screen space than the second.
# Values are 'star' or 'rectangle'
render_avg_rating_using='star'

View File

@ -101,6 +101,8 @@ def _config():
help=_('tag browser categories not to display')) help=_('tag browser categories not to display'))
c.add_opt('gui_layout', choices=['wide', 'narrow'], c.add_opt('gui_layout', choices=['wide', 'narrow'],
help=_('The layout of the user interface'), default='narrow') help=_('The layout of the user interface'), default='narrow')
c.add_opt('show_avg_rating', default=True,
help=_('Show the average rating per item indication in the tag browser'))
return ConfigProxy(c) return ConfigProxy(c)
config = _config() config = _config()

View File

@ -481,6 +481,8 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
self.opt_enforce_cpu_limit.setChecked(config['enforce_cpu_limit']) self.opt_enforce_cpu_limit.setChecked(config['enforce_cpu_limit'])
self.device_detection_button.clicked.connect(self.debug_device_detection) self.device_detection_button.clicked.connect(self.debug_device_detection)
self.port.editingFinished.connect(self.check_port_value) self.port.editingFinished.connect(self.check_port_value)
self.search_as_you_type.setChecked(config['search_as_you_type'])
self.show_avg_rating.setChecked(config['show_avg_rating'])
self.show_splash_screen.setChecked(gprefs.get('show_splash_screen', self.show_splash_screen.setChecked(gprefs.get('show_splash_screen',
True)) True))
@ -854,6 +856,7 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
config['delete_news_from_library_on_upload'] = self.delete_news.isChecked() config['delete_news_from_library_on_upload'] = self.delete_news.isChecked()
config['upload_news_to_device'] = self.sync_news.isChecked() config['upload_news_to_device'] = self.sync_news.isChecked()
config['search_as_you_type'] = self.search_as_you_type.isChecked() config['search_as_you_type'] = self.search_as_you_type.isChecked()
config['show_avg_rating'] = self.show_avg_rating.isChecked()
config['get_social_metadata'] = self.opt_get_social_metadata.isChecked() config['get_social_metadata'] = self.opt_get_social_metadata.isChecked()
config['overwrite_author_title_metadata'] = self.opt_overwrite_author_title_metadata.isChecked() config['overwrite_author_title_metadata'] = self.opt_overwrite_author_title_metadata.isChecked()
config['enforce_cpu_limit'] = bool(self.opt_enforce_cpu_limit.isChecked()) config['enforce_cpu_limit'] = bool(self.opt_enforce_cpu_limit.isChecked())

View File

@ -373,28 +373,38 @@
<item row="4" column="0"> <item row="4" column="0">
<widget class="QCheckBox" name="search_as_you_type"> <widget class="QCheckBox" name="search_as_you_type">
<property name="text"> <property name="text">
<string>Search as you type</string> <string>Search as &amp;you type</string>
</property> </property>
<property name="checked"> <property name="checked">
<bool>true</bool> <bool>true</bool>
</property> </property>
</widget> </widget>
</item> </item>
<item row="5" column="0" colspan="2"> <item row="5" column="0">
<widget class="QCheckBox" name="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="6" column="0" colspan="2">
<widget class="QCheckBox" name="sync_news"> <widget class="QCheckBox" name="sync_news">
<property name="text"> <property name="text">
<string>Automatically send downloaded &amp;news to ebook reader</string> <string>Automatically send downloaded &amp;news to ebook reader</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="6" column="0" colspan="2"> <item row="7" column="0" colspan="2">
<widget class="QCheckBox" name="delete_news"> <widget class="QCheckBox" name="delete_news">
<property name="text"> <property name="text">
<string>&amp;Delete news from library when it is automatically sent to reader</string> <string>&amp;Delete news from library when it is automatically sent to reader</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="7" column="0" colspan="2"> <item row="8" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout"> <layout class="QHBoxLayout" name="horizontalLayout">
<item> <item>
<widget class="QLabel" name="label_6"> <widget class="QLabel" name="label_6">
@ -411,7 +421,7 @@
</item> </item>
</layout> </layout>
</item> </item>
<item row="8" column="0" colspan="2"> <item row="9" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox_2"> <widget class="QGroupBox" name="groupBox_2">
<property name="title"> <property name="title">
<string>Toolbar</string> <string>Toolbar</string>
@ -459,7 +469,7 @@
</layout> </layout>
</widget> </widget>
</item> </item>
<item row="9" column="0" colspan="2"> <item row="10" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_7"> <layout class="QHBoxLayout" name="horizontalLayout_7">
<item> <item>
<widget class="QGroupBox" name="groupBox"> <widget class="QGroupBox" name="groupBox">

View File

@ -9,17 +9,15 @@ Browsing book collection by tags.
from itertools import izip from itertools import izip
from functools import partial from functools import partial
from math import cos, sin, pi
from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, QCheckBox, \ from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, QCheckBox, \
QFont, QSize, QIcon, QPoint, QVBoxLayout, QComboBox, \ QFont, QSize, QIcon, QPoint, QVBoxLayout, QComboBox, \
QAbstractItemModel, QVariant, QModelIndex, QMenu, \ QAbstractItemModel, QVariant, QModelIndex, QMenu, \
QPushButton, QWidget QPushButton, QWidget
from PyQt4.Qt import QItemDelegate, QString, QPainterPath, QPen, QColor, \ from PyQt4.Qt import QItemDelegate, QString, QPen, QColor, QLinearGradient, QBrush
QLinearGradient, QBrush
from calibre.gui2 import config, NONE from calibre.gui2 import config, NONE
from calibre.utils.config import prefs from calibre.utils.config import prefs, tweaks
from calibre.library.field_metadata import TagsIcons from calibre.library.field_metadata import TagsIcons
from calibre.utils.search_query_parser import saved_searches from calibre.utils.search_query_parser import saved_searches
from calibre.gui2 import error_dialog from calibre.gui2 import error_dialog
@ -31,84 +29,59 @@ class TagDelegate(QItemDelegate):
def __init__(self, parent): def __init__(self, parent):
QItemDelegate.__init__(self, parent) QItemDelegate.__init__(self, parent)
self._parent = parent self._parent = parent
self.icon = QIcon(I('star.png'))
def paint(self, painter, option, index): def paint(self, painter, option, index):
item = index.internalPointer()
if item.type != TagTreeItem.TAG:
QItemDelegate.paint(self, painter, option, index)
return
r = option.rect
# Paint the decoration icon
icon = self._parent.model().data(index, Qt.DecorationRole).toPyObject()
icon.paint(painter, r, Qt.AlignLeft)
def draw_rating(rect, rating): # Paint the rating, if any. The decoration icon is assumed to be square,
COLOR = QColor("blue") # filling the row top to bottom. The three is arbitrary, there to
if rating is None: # provide a little space between the icon and what follows
return 0 r.setLeft(r.left()+r.height()+3)
rating = item.tag.avg_rating
if config['show_avg_rating'] and item.tag.avg_rating is not None:
painter.save() painter.save()
if tweaks['render_avg_rating_using'] == 'star':
painter.setClipRect(r.left(), r.top(),
int(r.height()*(rating/5.0)), r.height())
self.icon.paint(painter, r, Qt.AlignLeft | Qt.AlignVCenter)
r.setLeft(r.left() + r.height())
else:
painter.translate(r.left(), r.top()) painter.translate(r.left(), r.top())
# Compute factor so sizes can be expressed in percentages of the
# box defined by the row height
factor = r.height()/100. factor = r.height()/100.
# Try the star
# star_path = QPainterPath()
# star_path.moveTo(90, 50)
# for i in range(1, 5):
# star_path.lineTo(50 + 40 * cos(0.8 * i * pi), \
# 50 + 40 * sin(0.8 * i * pi))
# star_path.closeSubpath()
# star_path.setFillRule(Qt.WindingFill)
# pen = QPen(COLOR, 1, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)
# gradient = QLinearGradient(0, 0, 0, 100)
# gradient.setColorAt(0.0, COLOR)
# gradient.setColorAt(1.0, COLOR)
# painter.setBrush(QBrush(gradient))
# painter.setClipRect(0, 0, int(r.height() * (rating/5.0)), r.height())
# painter.scale(factor, factor)
# painter.translate(50.0, 50.0)
# painter.rotate(-20)
# painter.translate(-50.0, -50.0)
# painter.drawPath(star_path)
# painter.restore()
# return r.height()
# Try a circle
# gradient = QLinearGradient(0, 0, 0, 100)
# gradient.setColorAt(0.0, COLOR)
# gradient.setColorAt(1.0, COLOR)
# painter.setBrush(QBrush(gradient))
# painter.setClipRect(0, 0, int(r.height() * (rating/5.0)), r.height())
# painter.scale(factor, factor)
# painter.drawEllipse(0, 0, 100, 100)
# painter.restore()
# return r.height()
# Try a rectangle
width = 20 width = 20
height = 80 height = 80
left_offset = 5 left_offset = 5
top_offset = 10 top_offset = 10
if rating > 0.0: if r > 0.0:
pen = QPen(COLOR, 5, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin) color = QColor(100, 100, 255) #medium blue, less glare
pen = QPen(color, 5, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)
painter.setPen(pen) painter.setPen(pen)
painter.scale(factor, factor) painter.scale(factor, factor)
painter.drawRect(left_offset, top_offset, width, height) painter.drawRect(left_offset, top_offset, width, height)
fill_height = height*(rating/5.0) fill_height = height*(rating/5.0)
gradient = QLinearGradient(0, 0, 0, 100) gradient = QLinearGradient(0, 0, 0, 100)
gradient.setColorAt(0.0, COLOR) gradient.setColorAt(0.0, color)
gradient.setColorAt(1.0, COLOR) gradient.setColorAt(1.0, color)
painter.setBrush(QBrush(gradient)) painter.setBrush(QBrush(gradient))
painter.drawRect(left_offset, top_offset+(height-fill_height), painter.drawRect(left_offset, top_offset+(height-fill_height),
width, fill_height) width, fill_height)
# The '3' is arbitrary, there because we need a little space
# between the rectangle and the text.
r.setLeft(r.left() + ((width+left_offset*2)*factor) + 3)
painter.restore() painter.restore()
return int ((width+left_offset*2) * factor)
item = index.internalPointer()
if item.type == TagTreeItem.TAG:
r = option.rect
# Paint the decoration icon
icon = self._parent.model().data(index, Qt.DecorationRole).toPyObject()
icon.paint(painter, r, Qt.AlignLeft)
# Paint the rating, if any
r.setLeft(r.left()+r.height()+5)
text_start = draw_rating(r, item.tag.avg)
# Paint the text # Paint the text
r.setLeft(r.left() + text_start+5)
painter.drawText(r, Qt.AlignLeft|Qt.AlignVCenter, painter.drawText(r, Qt.AlignLeft|Qt.AlignVCenter,
QString('[%d] %s'%(item.tag.count, item.tag.name))) QString('[%d] %s'%(item.tag.count, item.tag.name)))
else:
QItemDelegate.paint(self, painter, option, index)
class TagsView(QTreeView): # {{{ class TagsView(QTreeView): # {{{
@ -386,10 +359,11 @@ class TagTreeItem(object): # {{{
if self.tag.count == 0: if self.tag.count == 0:
return QVariant('%s'%(self.tag.name)) return QVariant('%s'%(self.tag.name))
else: else:
if self.tag.avg is None: if self.tag.avg_rating is None:
return QVariant('[%d] %s'%(self.tag.count, self.tag.name)) return QVariant('[%d] %s'%(self.tag.count, self.tag.name))
else: else:
return QVariant('[%d][%3.1f] %s'%(self.tag.count, self.tag.avg, self.tag.name)) return QVariant('[%d][%3.1f] %s'%(self.tag.count,
self.tag.avg_rating, self.tag.name))
if role == Qt.EditRole: if role == Qt.EditRole:
return QVariant(self.tag.name) return QVariant(self.tag.name)
if role == Qt.DecorationRole: if role == Qt.DecorationRole:
@ -453,7 +427,7 @@ class TagsModel(QAbstractItemModel): # {{{
if r not in self.categories_with_ratings and \ if r not in self.categories_with_ratings and \
not self.db.field_metadata[r]['is_custom'] and \ not self.db.field_metadata[r]['is_custom'] and \
not self.db.field_metadata[r]['kind'] == 'user': not self.db.field_metadata[r]['kind'] == 'user':
tag.avg = None tag.avg_rating = None
TagTreeItem(parent=c, data=tag, icon_map=self.icon_state_map) TagTreeItem(parent=c, data=tag, icon_map=self.icon_state_map)
def set_search_restriction(self, s): def set_search_restriction(self, s):
@ -519,7 +493,7 @@ class TagsModel(QAbstractItemModel): # {{{
if r not in self.categories_with_ratings and \ if r not in self.categories_with_ratings and \
not self.db.field_metadata[r]['is_custom'] and \ not self.db.field_metadata[r]['is_custom'] and \
not self.db.field_metadata[r]['kind'] == 'user': not self.db.field_metadata[r]['kind'] == 'user':
tag.avg = None tag.avg_rating = None
tag.state = state_map.get(tag.name, 0) tag.state = state_map.get(tag.name, 0)
t = TagTreeItem(parent=category, data=tag, icon_map=self.icon_state_map) t = TagTreeItem(parent=category, data=tag, icon_map=self.icon_state_map)
self.endInsertRows() self.endInsertRows()

View File

@ -61,7 +61,7 @@ class Tag(object):
self.id = id self.id = id
self.count = count self.count = count
self.state = state self.state = state
self.avg = avg/2.0 if avg is not None else 0 self.avg_rating = avg/2.0 if avg is not None else 0
self.tooltip = tooltip self.tooltip = tooltip
self.icon = icon self.icon = icon
@ -126,8 +126,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.connect() self.connect()
self.is_case_sensitive = not iswindows and not isosx and \ self.is_case_sensitive = not iswindows and not isosx and \
not os.path.exists(self.dbpath.replace('metadata.db', 'MeTAdAtA.dB')) not os.path.exists(self.dbpath.replace('metadata.db', 'MeTAdAtA.dB'))
self.initialize_dynamic()
SchemaUpgrade.__init__(self) SchemaUpgrade.__init__(self)
self.initialize_dynamic()
def initialize_dynamic(self): def initialize_dynamic(self):
self.conn.executescript(u''' self.conn.executescript(u'''

View File

@ -355,13 +355,24 @@ class SchemaUpgrade(object):
'''.format(lt=link_table_name, table=table_name) '''.format(lt=link_table_name, table=table_name)
self.conn.executescript(script) self.conn.executescript(script)
for field in self.field_metadata.itervalues(): STANDARD_TAG_BROWSER_TABLES = [
if field['is_category'] and not field['is_custom'] and 'link_column' in field: ('authors', 'author', 'name'),
create_std_tag_browser_view(field['table'], field['link_column'], ('publishers', 'publisher', 'name'),
field['column']) ('ratings', 'rating', 'rating'),
('series', 'series', 'name'),
('tags', 'tag', 'name'),
]
for table, column, view_column in STANDARD_TAG_BROWSER_TABLES:
create_std_tag_browser_view(table, column, view_column)
for field in self.field_metadata.itervalues(): db_tables = self.conn.get('''SELECT name FROM sqlite_master
if field['is_category'] and field['is_custom']: WHERE type='table'
link_table_name = 'books_custom_column_%d_link'%field['colnum'] ORDER BY name''');
print 'try to upgrade cust col', field['table'], link_table_name tables = []
create_cust_tag_browser_view(field['table'], link_table_name) for (table,) in db_tables:
tables.append(table)
for table in tables:
link_table = 'books_%s_link'%table
if table.startswith('custom_column_') and link_table in tables:
print table
create_cust_tag_browser_view(table, link_table)