mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
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:
parent
97111f70a0
commit
8ff2f2f865
@ -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'
|
||||||
|
@ -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()
|
||||||
|
@ -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())
|
||||||
|
@ -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 &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 &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 &news to ebook reader</string>
|
<string>Automatically send downloaded &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>&Delete news from library when it is automatically sent to reader</string>
|
<string>&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">
|
||||||
|
@ -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()
|
||||||
|
@ -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'''
|
||||||
|
@ -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)
|
Loading…
x
Reference in New Issue
Block a user