diff --git a/imgsrc/calibreSymbols.spd b/imgsrc/calibreSymbols.spd
index 1ef6f532c5..391d95e509 100644
--- a/imgsrc/calibreSymbols.spd
+++ b/imgsrc/calibreSymbols.spd
@@ -4,24 +4,25 @@ FullName: calibre Symbols
FamilyName: calibre Symbols
Weight: Medium
Copyright: Created by Kovid Goyal with FontForge 2.0 (http://fontforge.sf.net)
-UComments: "2012-2-27: Created."
+UComments: "2012-2-27: Created."
Version: 001.000
ItalicAngle: 0
UnderlinePosition: -100
UnderlineWidth: 50
Ascent: 800
Descent: 200
+InvalidEm: 0
LayerCount: 2
-Layer: 0 0 "Back" 1
-Layer: 1 0 "Fore" 0
-NeedsXUIDChange: 1
+Layer: 0 0 "Back" 1
+Layer: 1 0 "Fore" 0
XUID: [1021 913 325894820 11538708]
+StyleMap: 0x0000
FSType: 0
OS2Version: 0
OS2_WeightWidthSlopeOnly: 0
OS2_UseTypoMetrics: 1
CreationTime: 1330331997
-ModificationTime: 1330487767
+ModificationTime: 1472969125
OS2TypoAscent: 0
OS2TypoAOffset: 1
OS2TypoDescent: 0
@@ -44,10 +45,10 @@ DisplaySize: -24
AntiAlias: 1
FitToEm: 1
WidthSeparation: 150
-WinInfo: 9600 75 22
+WinInfo: 0 152 34
BeginPrivate: 0
EndPrivate
-BeginChars: 1114112 3
+BeginChars: 1114112 4
StartChar: uni2605
Encoding: 9733 9733 0
@@ -91,7 +92,7 @@ SplineSet
485.545 635.457 493.518 604.173 506.689 547.357 c 2
551.923 352.862 l 1
EndSplineSet
-Validated: 524289
+Validated: 1
EndChar
StartChar: zero
@@ -148,5 +149,35 @@ SplineSet
EndSplineSet
Validated: 1
EndChar
+
+StartChar: onehalf
+Encoding: 189 189 3
+Width: 979
+VWidth: -26
+Flags: WO
+LayerCount: 2
+Fore
+SplineSet
+466.134 74.71 m 1
+ 320.554 -51.8184 l 2
+ 274.802 -91.5547 249.758 -112.902 245.426 -115.866 c 0
+ 241.092 -118.828 236.846 -120.31 232.688 -120.31 c 0
+ 227.835 -120.31 223.415 -118.306 219.429 -114.297 c 0
+ 215.442 -110.289 213.449 -105.844 213.449 -100.965 c 0
+ 213.449 -97.8281 223.329 -71.3379 243.087 -21.4932 c 2
+ 322.115 180.323 l 1
+ 152.618 289.598 l 2
+ 104.783 320.271 79.2217 337.176 75.9297 340.313 c 0
+ 72.6357 343.45 70.9893 347.981 70.9893 353.907 c 0
+ 70.9893 369.243 79.8291 376.912 97.5059 376.912 c 0
+ 98.8926 376.912 123.155 374.82 170.296 370.638 c 2
+ 379.825 352.862 l 1
+ 427.14 555.201 l 2
+ 439.271 607.834 446.811 636.764 449.757 641.992 c 0
+ 452.702 647.221 458.162 649.834 466.134 649.834 c 4
+ 474.454 649.834 466.134 74.71 466.134 74.71 c 1
+EndSplineSet
+Validated: 524321
+EndChar
EndChars
EndSplineFont
diff --git a/resources/fonts/calibreSymbols.otf b/resources/fonts/calibreSymbols.otf
index d80fcfec9c..9cb688b783 100644
Binary files a/resources/fonts/calibreSymbols.otf and b/resources/fonts/calibreSymbols.otf differ
diff --git a/src/calibre/ebooks/metadata/__init__.py b/src/calibre/ebooks/metadata/__init__.py
index 409b1c3c1f..fe8ad7fbb5 100644
--- a/src/calibre/ebooks/metadata/__init__.py
+++ b/src/calibre/ebooks/metadata/__init__.py
@@ -1,4 +1,5 @@
-#!/usr/bin/env python2
+#!/usr/bin/env python2
+# vim:fileencoding=utf-8
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
@@ -393,3 +394,14 @@ def check_doi(doi):
return doi_check.group()
return None
+def rating_to_stars(value, allow_half_star=False, star=u'★', half=u'½'):
+ r = max(0, min(int(value), 10))
+ if allow_half_star:
+ ans = u'★' * (r // 2)
+ if r % 2:
+ ans += u'½'
+ else:
+ ans = u'★' * int(r/2.0)
+ return ans
+
+
diff --git a/src/calibre/ebooks/metadata/book/render.py b/src/calibre/ebooks/metadata/book/render.py
index 14eb674ea3..b0f07ec24c 100644
--- a/src/calibre/ebooks/metadata/book/render.py
+++ b/src/calibre/ebooks/metadata/book/render.py
@@ -106,11 +106,16 @@ def mi_to_html(mi, field_list=None, default_author_link=None, use_roman_numbers=
elif metadata['datatype'] == 'rating':
val = getattr(mi, field)
if val:
- val = val/2.0
+ if disp.get('allow_half_stars'):
+ val = max(0, min(int(val), 10))
+ star_string = u'\u2605' * (val // 2) + (u'\u00bd' if val % 2 else '')
+ else:
+ val = max(0, min(int(val/2.0), 5))
+ star_string = u'\u2605' * val
ans.append((field,
u'
%s | %s | '%(
- name, rating_font, u'\u2605'*int(val))))
+ name, rating_font, star_string)))
elif metadata['datatype'] == 'composite':
val = getattr(mi, field)
if val:
diff --git a/src/calibre/gui2/custom_column_widgets.py b/src/calibre/gui2/custom_column_widgets.py
index cffd7f33cc..951c00694c 100644
--- a/src/calibre/gui2/custom_column_widgets.py
+++ b/src/calibre/gui2/custom_column_widgets.py
@@ -21,6 +21,7 @@ from calibre.utils.config import tweaks
from calibre.utils.icu import sort_key
from calibre.library.comments import comments_to_html
from calibre.gui2.library.delegates import ClearingDoubleSpinBox, ClearingSpinBox
+from calibre.gui2.widgets2 import RatingEditor
class Base(object):
@@ -158,27 +159,18 @@ class Float(Int):
val = self.widgets[1].minimum()
self.widgets[1].setValue(val)
-class Rating(Int):
+class Rating(Base):
def setup_ui(self, parent):
- Int.setup_ui(self, parent)
- w = self.widgets[1]
- w.setRange(0, 5)
- w.setSuffix(' '+_('star(s)'))
- w.setSpecialValueText(_('Not rated'))
+ allow_half_stars = self.col_metadata['display'].get('allow_half_stars', False)
+ self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent), RatingEditor(parent=parent, is_half_star=allow_half_stars)]
def setter(self, val):
- if val is None:
- val = 0
- self.widgets[1].setValue(int(round(val/2.)))
+ val = max(0, min(int(val or 0), 10))
+ self.widgets[1].rating_value = val
def getter(self):
- val = self.widgets[1].value()
- if val == 0:
- val = None
- else:
- val *= 2
- return val
+ return self.widgets[1].rating_value or None
class DateTimeEdit(QDateTimeEdit):
diff --git a/src/calibre/gui2/library/delegates.py b/src/calibre/gui2/library/delegates.py
index a4c32cc28f..d741d6bb99 100644
--- a/src/calibre/gui2/library/delegates.py
+++ b/src/calibre/gui2/library/delegates.py
@@ -12,10 +12,11 @@ from PyQt5.Qt import (Qt, QApplication, QStyle, QIcon, QDoubleSpinBox, QStyleOp
QAbstractTextDocumentLayout, QFont, QFontInfo, QDate, QDateTimeEdit, QDateTime,
QStyleOptionComboBox, QStyleOptionSpinBox, QLocale, QSize, QLineEdit)
+from calibre.ebooks.metadata import rating_to_stars
from calibre.gui2 import UNDEFINED_QDATETIME, rating_font
from calibre.constants import iswindows
from calibre.gui2.widgets import EnLineEdit
-from calibre.gui2.widgets2 import populate_standard_spinbox_context_menu
+from calibre.gui2.widgets2 import populate_standard_spinbox_context_menu, RatingEditor
from calibre.gui2.complete2 import EditWithComplete
from calibre.utils.date import now, format_date, qt_to_dt, is_date_undefined
from calibre.utils.config import tweaks
@@ -176,6 +177,7 @@ class RatingDelegate(QStyledItemDelegate, UpdateEditorGeometry): # {{{
def __init__(self, *args, **kwargs):
QStyledItemDelegate.__init__(self, *args, **kwargs)
+ self.is_half_star = kwargs.get('is_half_star', False)
self.table_widget = args[0]
self.rf = QFont(rating_font())
self.em = Qt.ElideMiddle
@@ -184,33 +186,25 @@ class RatingDelegate(QStyledItemDelegate, UpdateEditorGeometry): # {{{
delta = 2
self.rf.setPointSize(QFontInfo(QApplication.font()).pointSize()+delta)
- def createEditor(self, parent, option, index):
- sb = QSpinBox(parent)
- sb.setMinimum(0)
- sb.setMaximum(5)
- sb.setSuffix(' ' + _('stars'))
- sb.setSpecialValueText(_('Not rated'))
- return sb
-
def get_required_width(self, editor, style, fm):
- val = editor.maximum()
- text = editor.textFromValue(val) + editor.suffix()
- srect = style.itemTextRect(fm, editor.geometry(), Qt.AlignLeft, False,
- text + u'M')
- return srect.width()
+ return editor.sizeHint().width()
def displayText(self, value, locale):
- r = int(value)
- if r < 0 or r > 5:
- r = 0
- return u'\u2605'*r
+ return rating_to_stars(value, self.is_half_star)
+
+ def createEditor(self, parent, option, index):
+ return RatingEditor(parent, is_half_star=self.is_half_star)
def setEditorData(self, editor, index):
if check_key_modifier(Qt.ControlModifier):
val = 0
else:
val = index.data(Qt.EditRole)
- editor.setValue(val)
+ editor.rating_value = val
+
+ def setModelData(self, editor, model, index):
+ val = editor.rating_value
+ model.setData(index, val, Qt.EditRole)
def sizeHint(self, option, index):
option.font = self.rf
@@ -224,6 +218,7 @@ class RatingDelegate(QStyledItemDelegate, UpdateEditorGeometry): # {{{
# }}}
+
class DateDelegate(QStyledItemDelegate, UpdateEditorGeometry): # {{{
def __init__(self, parent, tweak_name='gui_timestamp_display_format',
diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py
index 316f8af580..2bcac9851f 100644
--- a/src/calibre/gui2/library/models.py
+++ b/src/calibre/gui2/library/models.py
@@ -709,6 +709,7 @@ class BooksModel(QAbstractTableModel): # {{{
return img
def build_data_convertors(self):
+ rating_fields = {}
def renderer(field, decorator=False):
idfunc = self.db.id
@@ -776,8 +777,9 @@ class BooksModel(QAbstractTableModel): # {{{
def func(idx):
return (QDateTime(as_local_time(fffunc(field_obj, idfunc(idx), default_value=UNDEFINED_DATE))))
elif dt == 'rating':
+ rating_fields[field] = m['display'].get('allow_half_stars', False)
def func(idx):
- return (int(fffunc(field_obj, idfunc(idx), default_value=0)/2.0))
+ return int(fffunc(field_obj, idfunc(idx), default_value=0))
elif dt == 'series':
sidx_field = self.db.new_api.fields[field + '_index']
def func(idx):
@@ -817,8 +819,20 @@ class BooksModel(QAbstractTableModel): # {{{
elif dt == 'bool':
self.dc_decorator[col] = renderer(col, 'bool')
+ tc = self.dc.copy()
+ def stars_tooltip(func, allow_half=True):
+ def f(idx):
+ ans = val = int(func(idx))
+ ans = str(val // 2)
+ if allow_half and val % 2:
+ ans += '.5'
+ return _('%s stars') % ans
+ return f
+ for f, allow_half in rating_fields.iteritems():
+ tc[f] = stars_tooltip(self.dc[f], allow_half)
# build a index column to data converter map, to remove the string lookup in the data loop
self.column_to_dc_map = [self.dc[col] for col in self.column_map]
+ self.column_to_tc_map = [tc[col] for col in self.column_map]
self.column_to_dc_decorator_map = [self.dc_decorator.get(col, None) for col in self.column_map]
def data(self, index, role):
@@ -850,7 +864,9 @@ class BooksModel(QAbstractTableModel): # {{{
return None
self.icon_cache[id_][cache_index] = None
return self.column_to_dc_map[col](index.row())
- elif role in (Qt.EditRole, Qt.ToolTipRole):
+ elif role == Qt.ToolTipRole:
+ return self.column_to_tc_map[col](index.row())
+ elif role == Qt.EditRole:
return self.column_to_dc_map[col](index.row())
elif role == Qt.BackgroundRole:
if self.id(index) in self.ids_to_highlight_set:
@@ -996,9 +1012,7 @@ class BooksModel(QAbstractTableModel): # {{{
elif typ == 'bool':
val = value if value is None else bool(value)
elif typ == 'rating':
- val = int(value)
- val = 0 if val < 0 else 5 if val > 5 else val
- val *= 2
+ val = max(0, min(int(value or 0), 10))
elif typ in ('int', 'float'):
if value == 0:
val = '0'
@@ -1089,8 +1103,7 @@ class BooksModel(QAbstractTableModel): # {{{
id = self.db.id(row)
books_to_refresh = set([id])
if column == 'rating':
- val = 0 if val < 0 else 5 if val > 5 else val
- val *= 2
+ val = max(0, min(int(val or 0), 10))
self.db.set_rating(id, val)
elif column == 'series':
val = val.strip()
diff --git a/src/calibre/gui2/library/views.py b/src/calibre/gui2/library/views.py
index b6f644e497..06c7c1562d 100644
--- a/src/calibre/gui2/library/views.py
+++ b/src/calibre/gui2/library/views.py
@@ -224,6 +224,7 @@ class BooksView(QTableView): # {{{
self.setWordWrap(False)
self.rating_delegate = RatingDelegate(self)
+ self.half_rating_delegate = RatingDelegate(self, is_half_star=True)
self.timestamp_delegate = DateDelegate(self)
self.pubdate_delegate = PubDateDelegate(self)
self.last_modified_delegate = DateDelegate(self,
@@ -760,9 +761,9 @@ class BooksView(QTableView): # {{{
def database_changed(self, db):
db.data.add_marked_listener(self.marked_changed_listener)
for i in range(self.model().columnCount(None)):
- if self.itemDelegateForColumn(i) in (self.rating_delegate,
- self.timestamp_delegate, self.pubdate_delegate,
- self.last_modified_delegate, self.languages_delegate):
+ if self.itemDelegateForColumn(i) in (
+ self.rating_delegate, self.timestamp_delegate, self.pubdate_delegate,
+ self.last_modified_delegate, self.languages_delegate, self.half_rating_delegate):
self.setItemDelegateForColumn(i, self.itemDelegate())
cm = self.column_map
@@ -799,7 +800,8 @@ class BooksView(QTableView): # {{{
elif cc['datatype'] == 'bool':
self.setItemDelegateForColumn(cm.index(colhead), self.cc_bool_delegate)
elif cc['datatype'] == 'rating':
- self.setItemDelegateForColumn(cm.index(colhead), self.rating_delegate)
+ d = self.half_rating_delegate if cc['display'].get('allow_half_stars', False) else self.rating_delegate
+ self.setItemDelegateForColumn(cm.index(colhead), d)
elif cc['datatype'] == 'composite':
self.setItemDelegateForColumn(cm.index(colhead), self.cc_template_delegate)
elif cc['datatype'] == 'enumeration':
@@ -1126,6 +1128,7 @@ class DeviceBooksView(BooksView): # {{{
self.can_add_columns = False
self.resize_on_select = False
self.rating_delegate = None
+ self.half_rating_delegate = None
for i in range(10):
self.setItemDelegateForColumn(i, TextDelegate(self))
self.setDragDropMode(self.NoDragDrop)
diff --git a/src/calibre/gui2/metadata/basic_widgets.py b/src/calibre/gui2/metadata/basic_widgets.py
index 117c316925..4c15363dfe 100644
--- a/src/calibre/gui2/metadata/basic_widgets.py
+++ b/src/calibre/gui2/metadata/basic_widgets.py
@@ -13,12 +13,12 @@ from datetime import date, datetime
from PyQt5.Qt import (
Qt, QDateTimeEdit, pyqtSignal, QMessageBox, QIcon, QToolButton, QWidget,
QLabel, QGridLayout, QApplication, QDoubleSpinBox, QListWidgetItem, QSize,
- QPixmap, QDialog, QMenu, QSpinBox, QLineEdit, QSizePolicy, QKeySequence,
+ QPixmap, QDialog, QMenu, QLineEdit, QSizePolicy, QKeySequence,
QDialogButtonBox, QAction, QCalendarWidget, QDate, QDateTime, QUndoCommand,
QUndoStack, QVBoxLayout, QPlainTextEdit)
from calibre.gui2.widgets import EnLineEdit, FormatList as _FormatList, ImageView
-from calibre.gui2.widgets2 import access_key, populate_standard_spinbox_context_menu, RightClickButton, Dialog
+from calibre.gui2.widgets2 import access_key, populate_standard_spinbox_context_menu, RightClickButton, Dialog, RatingEditor
from calibre.utils.icu import sort_key
from calibre.utils.config import tweaks, prefs
from calibre.ebooks.metadata import (
@@ -1231,7 +1231,7 @@ class CommentsEdit(Editor, ToMetadataMixin): # {{{
db.set_comment(id_, self.current_val, notify=False, commit=False)
# }}}
-class RatingEdit(make_undoable(QSpinBox), ToMetadataMixin): # {{{
+class RatingEdit(RatingEditor, ToMetadataMixin): # {{{
LABEL = _('&Rating:')
TOOLTIP = _('Rating of this book. 0-5 stars')
FIELD_NAME = 'rating'
@@ -1240,40 +1240,26 @@ class RatingEdit(make_undoable(QSpinBox), ToMetadataMixin): # {{{
super(RatingEdit, self).__init__(parent)
self.setToolTip(self.TOOLTIP)
self.setWhatsThis(self.TOOLTIP)
- self.setMaximum(5)
- self.setSuffix(' ' + _('stars'))
- self.setSpecialValueText(_('Not rated'))
@dynamic_property
def current_val(self):
def fget(self):
- return self.value()
+ return self.rating_value
def fset(self, val):
- if val is None:
- val = 0
- val = int(val)
- if val < 0:
- val = 0
- if val > 5:
- val = 5
- self.set_spinbox_value(val)
+ self.rating_value = val
return property(fget=fget, fset=fset)
def initialize(self, db, id_):
val = db.rating(id_, index_is_id=True)
- if val > 0:
- val = int(val/2.)
- else:
- val = 0
self.current_val = val
self.original_val = self.current_val
def commit(self, db, id_):
- db.set_rating(id_, 2*self.current_val, notify=False, commit=False)
+ db.set_rating(id_, self.current_val, notify=False, commit=False)
return True
def zero(self):
- self.setValue(0)
+ self.setCurrentIndex(0)
# }}}
diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py
index ce8ed50784..86a86b9366 100644
--- a/src/calibre/gui2/metadata/single.py
+++ b/src/calibre/gui2/metadata/single.py
@@ -436,10 +436,7 @@ class MetadataSingleDialogBase(QDialog):
elif update_sorts and not mi.is_null('authors'):
self.author_sort.auto_generate()
if not mi.is_null('rating'):
- try:
- self.rating.set_value(mi.rating)
- except:
- pass
+ self.rating.set_value(mi.rating * 2)
if not mi.is_null('publisher'):
self.publisher.set_value(mi.publisher)
if not mi.is_null('tags'):
diff --git a/src/calibre/gui2/preferences/create_custom_column.py b/src/calibre/gui2/preferences/create_custom_column.py
index a7ef2c41d1..ad8807f66a 100644
--- a/src/calibre/gui2/preferences/create_custom_column.py
+++ b/src/calibre/gui2/preferences/create_custom_column.py
@@ -1,3 +1,6 @@
+#!/usr/bin/env python2
+# vim:fileencoding=UTF-8
+
__license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal '
@@ -168,6 +171,8 @@ class CreateCustomColumn(QDialog):
self.comments_heading_position.setCurrentIndex(idx)
idx = max(0, self.comments_type.findData(c['display'].get('interpret_as', 'html')))
self.comments_type.setCurrentIndex(idx)
+ elif ct == 'rating':
+ self.allow_half_stars.setChecked(bool(c['display'].get('allow_half_stars', False)))
self.datatype_changed()
if ct in ['text', 'composite', 'enumeration']:
self.use_decorations.setChecked(c['display'].get('use_decorations', False))
@@ -350,6 +355,11 @@ class CreateCustomColumn(QDialog):
l.addWidget(ec), l.addWidget(la, 1, 1)
self.enum_label = add_row(_('&Values'), l)
+ # Rating allow half stars
+ self.allow_half_stars = ahs = QCheckBox(_('Allow half stars'))
+ ahs.setToolTip(_('Allow half star ratings, for example: ') + '★★★½')
+ add_row(None, ahs)
+
# Composite display properties
l = QHBoxLayout()
self.composite_sort_by_label = la = QLabel(_("&Sort/search column by"))
@@ -427,6 +437,7 @@ class CreateCustomColumn(QDialog):
self.comments_heading_position_label.setVisible(is_comments)
self.comments_type.setVisible(is_comments)
self.comments_type_label.setVisible(is_comments)
+ self.allow_half_stars.setVisible(col_type == 'rating')
def accept(self):
col = unicode(self.column_name_box.text()).strip()
@@ -524,6 +535,8 @@ class CreateCustomColumn(QDialog):
elif col_type == 'comments':
display_dict['heading_position'] = type(u'')(self.comments_heading_position.currentData())
display_dict['interpret_as'] = type(u'')(self.comments_type.currentData())
+ elif col_type == 'rating':
+ display_dict['allow_half_stars'] = bool(self.allow_half_stars.isChecked())
if col_type in ['text', 'composite', 'enumeration'] and not is_multiple:
display_dict['use_decorations'] = self.use_decorations.checkState()
diff --git a/src/calibre/gui2/widgets2.py b/src/calibre/gui2/widgets2.py
index a9d8c60584..c3ad2c01cc 100644
--- a/src/calibre/gui2/widgets2.py
+++ b/src/calibre/gui2/widgets2.py
@@ -6,11 +6,16 @@ from __future__ import (unicode_literals, division, absolute_import,
__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal '
+import weakref
+
from PyQt5.Qt import (
QPushButton, QPixmap, QIcon, QColor, Qt, QColorDialog, pyqtSignal,
- QKeySequence, QToolButton, QDialog, QDialogButtonBox)
+ QKeySequence, QToolButton, QDialog, QDialogButtonBox, QComboBox, QFont,
+ QAbstractListModel, QModelIndex, QApplication, QStyledItemDelegate,
+ QUndoCommand, QUndoStack)
-from calibre.gui2 import gprefs
+from calibre.ebooks.metadata import rating_to_stars
+from calibre.gui2 import gprefs, rating_font
from calibre.gui2.complete2 import LineEdit, EditWithComplete
from calibre.gui2.widgets import history
@@ -180,3 +185,106 @@ class Dialog(QDialog):
def setup_ui(self):
raise NotImplementedError('You must implement this method in Dialog subclasses')
+
+class RatingModel(QAbstractListModel):
+
+ def __init__(self, parent=None, is_half_star=False):
+ QAbstractListModel.__init__(self, parent)
+ self.is_half_star = is_half_star
+ self.rating_font = QFont(rating_font())
+
+ def rowCount(self, parent=QModelIndex()):
+ return 11 if self.is_half_star else 6
+
+ def data(self, index, role=Qt.DisplayRole):
+ if role == Qt.DisplayRole:
+ val = index.row() * (1 if self.is_half_star else 2)
+ return rating_to_stars(val, self.is_half_star) or _('Not rated')
+ if role == Qt.FontRole:
+ return QApplication.instance().font() if index.row() == 0 else self.rating_font
+
+class UndoCommand(QUndoCommand):
+
+ def __init__(self, widget, val):
+ QUndoCommand.__init__(self)
+ self.widget = weakref.ref(widget)
+ self.undo_val = widget.rating_value
+ self.redo_val = val
+
+ def undo(self):
+ w = self.widget()
+ w.setCurrentIndex(self.undo_val)
+
+ def redo(self):
+ w = self.widget()
+ w.setCurrentIndex(self.redo_val)
+
+class RatingEditor(QComboBox):
+
+ def __init__(self, parent=None, is_half_star=False):
+ QComboBox.__init__(self, parent)
+ self.undo_stack = QUndoStack(self)
+ self.undo, self.redo = self.undo_stack.undo, self.undo_stack.redo
+ self.allow_undo = False
+ self.is_half_star = is_half_star
+ self._model = RatingModel(is_half_star=is_half_star, parent=self)
+ self.setModel(self._model)
+ self.delegate = QStyledItemDelegate(self)
+ self.view().setItemDelegate(self.delegate)
+ self.view().setStyleSheet('QListView { background: palette(window) }\nQListView::item { padding: 6px }')
+ self.setMaxVisibleItems(self.count())
+ self.currentIndexChanged.connect(self.update_font)
+
+ def update_font(self):
+ if self.currentIndex() == 0:
+ self.setFont(QApplication.instance().font())
+ else:
+ self.setFont(self._model.rating_font)
+
+ def clear_to_undefined(self):
+ self.setCurrentIndex(0)
+
+ @property
+ def rating_value(self):
+ ' An integer from 0 to 10 '
+ ans = self.currentIndex()
+ if not self.is_half_star:
+ ans *= 2
+ return ans
+
+ @rating_value.setter
+ def rating_value(self, val):
+ val = max(0, min(int(val or 0), 10))
+ if self.allow_undo:
+ cmd = UndoCommand(self, val)
+ self.undo_stack.push(cmd)
+ else:
+ self.undo_stack.clear()
+ if not self.is_half_star:
+ val //= 2
+ self.setCurrentIndex(val)
+
+ def keyPressEvent(self, ev):
+ if ev == QKeySequence.Undo:
+ self.undo()
+ return ev.accept()
+ if ev == QKeySequence.Redo:
+ self.redo()
+ return ev.accept()
+ k = ev.key()
+ num = {getattr(Qt, 'Key_%d'%i):i for i in range(6)}.get(k)
+ if num is None:
+ return QComboBox.keyPressEvent(self, ev)
+ ev.accept()
+ if self.is_half_star:
+ num *= 2
+ self.setCurrentIndex(num)
+
+if __name__ == '__main__':
+ from calibre.gui2 import Application
+ app = Application([])
+ app.load_builtin_fonts()
+ q = RatingEditor(is_half_star=True)
+ q.rating_value = 7
+ q.show()
+ app.exec_()