From baddd6c29814dfc34fffac71e720a70415bff6d8 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 4 Sep 2016 16:47:35 +0530 Subject: [PATCH] Start work on allowing half-stars for custom rating columns Still have to implement support in the bulk metadata editors and the content server. --- imgsrc/calibreSymbols.spd | 47 ++++++-- resources/fonts/calibreSymbols.otf | Bin 3216 -> 3600 bytes src/calibre/ebooks/metadata/__init__.py | 14 ++- src/calibre/ebooks/metadata/book/render.py | 9 +- src/calibre/gui2/custom_column_widgets.py | 22 ++-- src/calibre/gui2/library/delegates.py | 33 +++--- src/calibre/gui2/library/models.py | 27 +++-- src/calibre/gui2/library/views.py | 11 +- src/calibre/gui2/metadata/basic_widgets.py | 28 ++--- src/calibre/gui2/metadata/single.py | 5 +- .../gui2/preferences/create_custom_column.py | 13 ++ src/calibre/gui2/widgets2.py | 112 +++++++++++++++++- 12 files changed, 238 insertions(+), 83 deletions(-) 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 d80fcfec9c1830439328f631f2434eda75e0fdfd..9cb688b783b92835d89b367bde82d395884003f3 100644 GIT binary patch literal 3600 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 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_()