diff --git a/resources/default_tweaks.py b/resources/default_tweaks.py index bda839b28f..2075391da4 100644 --- a/resources/default_tweaks.py +++ b/resources/default_tweaks.py @@ -71,3 +71,10 @@ gui_pubdate_display_format = 'MMM yyyy' # order until the title is edited. Double-clicking on a title and hitting return # without changing anything is sufficient to change the sort. 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' diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index 3063ef252d..e4d98c1e4b 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -101,6 +101,8 @@ def _config(): help=_('tag browser categories not to display')) c.add_opt('gui_layout', choices=['wide', '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) config = _config() diff --git a/src/calibre/gui2/dialogs/config/__init__.py b/src/calibre/gui2/dialogs/config/__init__.py index b9c57b27ab..fa7bd8c2c5 100644 --- a/src/calibre/gui2/dialogs/config/__init__.py +++ b/src/calibre/gui2/dialogs/config/__init__.py @@ -481,6 +481,8 @@ class ConfigDialog(ResizableDialog, Ui_Dialog): self.opt_enforce_cpu_limit.setChecked(config['enforce_cpu_limit']) self.device_detection_button.clicked.connect(self.debug_device_detection) 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', True)) @@ -854,6 +856,7 @@ class ConfigDialog(ResizableDialog, Ui_Dialog): config['delete_news_from_library_on_upload'] = self.delete_news.isChecked() config['upload_news_to_device'] = self.sync_news.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['overwrite_author_title_metadata'] = self.opt_overwrite_author_title_metadata.isChecked() config['enforce_cpu_limit'] = bool(self.opt_enforce_cpu_limit.isChecked()) diff --git a/src/calibre/gui2/dialogs/config/config.ui b/src/calibre/gui2/dialogs/config/config.ui index 917333a989..781b53f941 100644 --- a/src/calibre/gui2/dialogs/config/config.ui +++ b/src/calibre/gui2/dialogs/config/config.ui @@ -373,28 +373,38 @@ - Search as you type + Search as &you type true - + + + + Show &average ratings in the tags browser + + + true + + + + Automatically send downloaded &news to ebook reader - + &Delete news from library when it is automatically sent to reader - + @@ -411,7 +421,7 @@ - + Toolbar @@ -459,7 +469,7 @@ - + diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index 6ada763f80..9919ef97a2 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -9,17 +9,15 @@ Browsing book collection by tags. from itertools import izip from functools import partial -from math import cos, sin, pi from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, QCheckBox, \ QFont, QSize, QIcon, QPoint, QVBoxLayout, QComboBox, \ QAbstractItemModel, QVariant, QModelIndex, QMenu, \ QPushButton, QWidget -from PyQt4.Qt import QItemDelegate, QString, QPainterPath, QPen, QColor, \ - QLinearGradient, QBrush +from PyQt4.Qt import QItemDelegate, QString, QPen, QColor, QLinearGradient, QBrush 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.utils.search_query_parser import saved_searches from calibre.gui2 import error_dialog @@ -31,84 +29,59 @@ class TagDelegate(QItemDelegate): def __init__(self, parent): QItemDelegate.__init__(self, parent) self._parent = parent + self.icon = QIcon(I('star.png')) def paint(self, painter, option, index): - - def draw_rating(rect, rating): - COLOR = QColor("blue") - if rating is None: - return 0 - painter.save() - painter.translate(r.left(), r.top()) - 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 - height = 80 - left_offset = 5 - top_offset = 10 - if rating > 0.0: - pen = QPen(COLOR, 5, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin) - painter.setPen(pen) - painter.scale(factor, factor) - painter.drawRect(left_offset, top_offset, width, height) - fill_height = height*(rating/5.0) - gradient = QLinearGradient(0, 0, 0, 100) - gradient.setColorAt(0.0, COLOR) - gradient.setColorAt(1.0, COLOR) - painter.setBrush(QBrush(gradient)) - painter.drawRect(left_offset, top_offset+(height-fill_height), - width, fill_height) - 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 - r.setLeft(r.left() + text_start+5) - painter.drawText(r, Qt.AlignLeft|Qt.AlignVCenter, - QString('[%d] %s'%(item.tag.count, item.tag.name))) - else: + 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) + + # Paint the rating, if any. The decoration icon is assumed to be square, + # filling the row top to bottom. The three is arbitrary, there to + # provide a little space between the icon and what follows + 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() + 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()) + # Compute factor so sizes can be expressed in percentages of the + # box defined by the row height + factor = r.height()/100. + width = 20 + height = 80 + left_offset = 5 + top_offset = 10 + if r > 0.0: + color = QColor(100, 100, 255) #medium blue, less glare + pen = QPen(color, 5, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin) + painter.setPen(pen) + painter.scale(factor, factor) + painter.drawRect(left_offset, top_offset, width, height) + fill_height = height*(rating/5.0) + gradient = QLinearGradient(0, 0, 0, 100) + gradient.setColorAt(0.0, color) + gradient.setColorAt(1.0, color) + painter.setBrush(QBrush(gradient)) + painter.drawRect(left_offset, top_offset+(height-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() + # Paint the text + painter.drawText(r, Qt.AlignLeft|Qt.AlignVCenter, + QString('[%d] %s'%(item.tag.count, item.tag.name))) class TagsView(QTreeView): # {{{ @@ -386,10 +359,11 @@ class TagTreeItem(object): # {{{ if self.tag.count == 0: return QVariant('%s'%(self.tag.name)) else: - if self.tag.avg is None: + if self.tag.avg_rating is None: return QVariant('[%d] %s'%(self.tag.count, self.tag.name)) 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: return QVariant(self.tag.name) if role == Qt.DecorationRole: @@ -453,7 +427,7 @@ class TagsModel(QAbstractItemModel): # {{{ if r not in self.categories_with_ratings and \ not self.db.field_metadata[r]['is_custom'] and \ 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) def set_search_restriction(self, s): @@ -519,7 +493,7 @@ class TagsModel(QAbstractItemModel): # {{{ if r not in self.categories_with_ratings and \ not self.db.field_metadata[r]['is_custom'] and \ not self.db.field_metadata[r]['kind'] == 'user': - tag.avg = None + tag.avg_rating = None tag.state = state_map.get(tag.name, 0) t = TagTreeItem(parent=category, data=tag, icon_map=self.icon_state_map) self.endInsertRows() diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 04acca913c..939ab92f38 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -61,7 +61,7 @@ class Tag(object): self.id = id self.count = count 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.icon = icon @@ -126,8 +126,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): self.connect() self.is_case_sensitive = not iswindows and not isosx and \ not os.path.exists(self.dbpath.replace('metadata.db', 'MeTAdAtA.dB')) - self.initialize_dynamic() SchemaUpgrade.__init__(self) + self.initialize_dynamic() def initialize_dynamic(self): self.conn.executescript(u''' diff --git a/src/calibre/library/schema_upgrades.py b/src/calibre/library/schema_upgrades.py index 0660a8b136..870254f999 100644 --- a/src/calibre/library/schema_upgrades.py +++ b/src/calibre/library/schema_upgrades.py @@ -355,13 +355,24 @@ class SchemaUpgrade(object): '''.format(lt=link_table_name, table=table_name) self.conn.executescript(script) - for field in self.field_metadata.itervalues(): - if field['is_category'] and not field['is_custom'] and 'link_column' in field: - create_std_tag_browser_view(field['table'], field['link_column'], - field['column']) + STANDARD_TAG_BROWSER_TABLES = [ + ('authors', 'author', 'name'), + ('publishers', 'publisher', 'name'), + ('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(): - if field['is_category'] and field['is_custom']: - link_table_name = 'books_custom_column_%d_link'%field['colnum'] - print 'try to upgrade cust col', field['table'], link_table_name - create_cust_tag_browser_view(field['table'], link_table_name) + db_tables = self.conn.get('''SELECT name FROM sqlite_master + WHERE type='table' + ORDER BY name'''); + tables = [] + 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) \ No newline at end of file