Start work on allowing half-stars for custom rating columns

Still have to implement support in the bulk metadata editors and the
content server.
This commit is contained in:
Kovid Goyal 2016-09-04 16:47:35 +05:30
parent a8f7c27e40
commit baddd6c298
12 changed files with 238 additions and 83 deletions

View File

@ -11,17 +11,18 @@ UnderlinePosition: -100
UnderlineWidth: 50 UnderlineWidth: 50
Ascent: 800 Ascent: 800
Descent: 200 Descent: 200
InvalidEm: 0
LayerCount: 2 LayerCount: 2
Layer: 0 0 "Back" 1 Layer: 0 0 "Back" 1
Layer: 1 0 "Fore" 0 Layer: 1 0 "Fore" 0
NeedsXUIDChange: 1
XUID: [1021 913 325894820 11538708] XUID: [1021 913 325894820 11538708]
StyleMap: 0x0000
FSType: 0 FSType: 0
OS2Version: 0 OS2Version: 0
OS2_WeightWidthSlopeOnly: 0 OS2_WeightWidthSlopeOnly: 0
OS2_UseTypoMetrics: 1 OS2_UseTypoMetrics: 1
CreationTime: 1330331997 CreationTime: 1330331997
ModificationTime: 1330487767 ModificationTime: 1472969125
OS2TypoAscent: 0 OS2TypoAscent: 0
OS2TypoAOffset: 1 OS2TypoAOffset: 1
OS2TypoDescent: 0 OS2TypoDescent: 0
@ -44,10 +45,10 @@ DisplaySize: -24
AntiAlias: 1 AntiAlias: 1
FitToEm: 1 FitToEm: 1
WidthSeparation: 150 WidthSeparation: 150
WinInfo: 9600 75 22 WinInfo: 0 152 34
BeginPrivate: 0 BeginPrivate: 0
EndPrivate EndPrivate
BeginChars: 1114112 3 BeginChars: 1114112 4
StartChar: uni2605 StartChar: uni2605
Encoding: 9733 9733 0 Encoding: 9733 9733 0
@ -91,7 +92,7 @@ SplineSet
485.545 635.457 493.518 604.173 506.689 547.357 c 2 485.545 635.457 493.518 604.173 506.689 547.357 c 2
551.923 352.862 l 1 551.923 352.862 l 1
EndSplineSet EndSplineSet
Validated: 524289 Validated: 1
EndChar EndChar
StartChar: zero StartChar: zero
@ -148,5 +149,35 @@ SplineSet
EndSplineSet EndSplineSet
Validated: 1 Validated: 1
EndChar 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 EndChars
EndSplineFont EndSplineFont

Binary file not shown.

View File

@ -1,4 +1,5 @@
#!/usr/bin/env python2 #!/usr/bin/env python2
# vim:fileencoding=utf-8
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
@ -393,3 +394,14 @@ def check_doi(doi):
return doi_check.group() return doi_check.group()
return None 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

View File

@ -106,11 +106,16 @@ def mi_to_html(mi, field_list=None, default_author_link=None, use_roman_numbers=
elif metadata['datatype'] == 'rating': elif metadata['datatype'] == 'rating':
val = getattr(mi, field) val = getattr(mi, field)
if val: 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, ans.append((field,
u'<td class="title">%s</td><td class="rating value" ' u'<td class="title">%s</td><td class="rating value" '
'style=\'font-family:"%s"\'>%s</td>'%( 'style=\'font-family:"%s"\'>%s</td>'%(
name, rating_font, u'\u2605'*int(val)))) name, rating_font, star_string)))
elif metadata['datatype'] == 'composite': elif metadata['datatype'] == 'composite':
val = getattr(mi, field) val = getattr(mi, field)
if val: if val:

View File

@ -21,6 +21,7 @@ from calibre.utils.config import tweaks
from calibre.utils.icu import sort_key from calibre.utils.icu import sort_key
from calibre.library.comments import comments_to_html from calibre.library.comments import comments_to_html
from calibre.gui2.library.delegates import ClearingDoubleSpinBox, ClearingSpinBox from calibre.gui2.library.delegates import ClearingDoubleSpinBox, ClearingSpinBox
from calibre.gui2.widgets2 import RatingEditor
class Base(object): class Base(object):
@ -158,27 +159,18 @@ class Float(Int):
val = self.widgets[1].minimum() val = self.widgets[1].minimum()
self.widgets[1].setValue(val) self.widgets[1].setValue(val)
class Rating(Int): class Rating(Base):
def setup_ui(self, parent): def setup_ui(self, parent):
Int.setup_ui(self, parent) allow_half_stars = self.col_metadata['display'].get('allow_half_stars', False)
w = self.widgets[1] self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent), RatingEditor(parent=parent, is_half_star=allow_half_stars)]
w.setRange(0, 5)
w.setSuffix(' '+_('star(s)'))
w.setSpecialValueText(_('Not rated'))
def setter(self, val): def setter(self, val):
if val is None: val = max(0, min(int(val or 0), 10))
val = 0 self.widgets[1].rating_value = val
self.widgets[1].setValue(int(round(val/2.)))
def getter(self): def getter(self):
val = self.widgets[1].value() return self.widgets[1].rating_value or None
if val == 0:
val = None
else:
val *= 2
return val
class DateTimeEdit(QDateTimeEdit): class DateTimeEdit(QDateTimeEdit):

View File

@ -12,10 +12,11 @@ from PyQt5.Qt import (Qt, QApplication, QStyle, QIcon, QDoubleSpinBox, QStyleOp
QAbstractTextDocumentLayout, QFont, QFontInfo, QDate, QDateTimeEdit, QDateTime, QAbstractTextDocumentLayout, QFont, QFontInfo, QDate, QDateTimeEdit, QDateTime,
QStyleOptionComboBox, QStyleOptionSpinBox, QLocale, QSize, QLineEdit) QStyleOptionComboBox, QStyleOptionSpinBox, QLocale, QSize, QLineEdit)
from calibre.ebooks.metadata import rating_to_stars
from calibre.gui2 import UNDEFINED_QDATETIME, rating_font from calibre.gui2 import UNDEFINED_QDATETIME, rating_font
from calibre.constants import iswindows from calibre.constants import iswindows
from calibre.gui2.widgets import EnLineEdit 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.gui2.complete2 import EditWithComplete
from calibre.utils.date import now, format_date, qt_to_dt, is_date_undefined from calibre.utils.date import now, format_date, qt_to_dt, is_date_undefined
from calibre.utils.config import tweaks from calibre.utils.config import tweaks
@ -176,6 +177,7 @@ class RatingDelegate(QStyledItemDelegate, UpdateEditorGeometry): # {{{
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
QStyledItemDelegate.__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.table_widget = args[0]
self.rf = QFont(rating_font()) self.rf = QFont(rating_font())
self.em = Qt.ElideMiddle self.em = Qt.ElideMiddle
@ -184,33 +186,25 @@ class RatingDelegate(QStyledItemDelegate, UpdateEditorGeometry): # {{{
delta = 2 delta = 2
self.rf.setPointSize(QFontInfo(QApplication.font()).pointSize()+delta) 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): def get_required_width(self, editor, style, fm):
val = editor.maximum() return editor.sizeHint().width()
text = editor.textFromValue(val) + editor.suffix()
srect = style.itemTextRect(fm, editor.geometry(), Qt.AlignLeft, False,
text + u'M')
return srect.width()
def displayText(self, value, locale): def displayText(self, value, locale):
r = int(value) return rating_to_stars(value, self.is_half_star)
if r < 0 or r > 5:
r = 0 def createEditor(self, parent, option, index):
return u'\u2605'*r return RatingEditor(parent, is_half_star=self.is_half_star)
def setEditorData(self, editor, index): def setEditorData(self, editor, index):
if check_key_modifier(Qt.ControlModifier): if check_key_modifier(Qt.ControlModifier):
val = 0 val = 0
else: else:
val = index.data(Qt.EditRole) 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): def sizeHint(self, option, index):
option.font = self.rf option.font = self.rf
@ -224,6 +218,7 @@ class RatingDelegate(QStyledItemDelegate, UpdateEditorGeometry): # {{{
# }}} # }}}
class DateDelegate(QStyledItemDelegate, UpdateEditorGeometry): # {{{ class DateDelegate(QStyledItemDelegate, UpdateEditorGeometry): # {{{
def __init__(self, parent, tweak_name='gui_timestamp_display_format', def __init__(self, parent, tweak_name='gui_timestamp_display_format',

View File

@ -709,6 +709,7 @@ class BooksModel(QAbstractTableModel): # {{{
return img return img
def build_data_convertors(self): def build_data_convertors(self):
rating_fields = {}
def renderer(field, decorator=False): def renderer(field, decorator=False):
idfunc = self.db.id idfunc = self.db.id
@ -776,8 +777,9 @@ class BooksModel(QAbstractTableModel): # {{{
def func(idx): def func(idx):
return (QDateTime(as_local_time(fffunc(field_obj, idfunc(idx), default_value=UNDEFINED_DATE)))) return (QDateTime(as_local_time(fffunc(field_obj, idfunc(idx), default_value=UNDEFINED_DATE))))
elif dt == 'rating': elif dt == 'rating':
rating_fields[field] = m['display'].get('allow_half_stars', False)
def func(idx): 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': elif dt == 'series':
sidx_field = self.db.new_api.fields[field + '_index'] sidx_field = self.db.new_api.fields[field + '_index']
def func(idx): def func(idx):
@ -817,8 +819,20 @@ class BooksModel(QAbstractTableModel): # {{{
elif dt == 'bool': elif dt == 'bool':
self.dc_decorator[col] = renderer(col, '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 # 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_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] self.column_to_dc_decorator_map = [self.dc_decorator.get(col, None) for col in self.column_map]
def data(self, index, role): def data(self, index, role):
@ -850,7 +864,9 @@ class BooksModel(QAbstractTableModel): # {{{
return None return None
self.icon_cache[id_][cache_index] = None self.icon_cache[id_][cache_index] = None
return self.column_to_dc_map[col](index.row()) 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()) return self.column_to_dc_map[col](index.row())
elif role == Qt.BackgroundRole: elif role == Qt.BackgroundRole:
if self.id(index) in self.ids_to_highlight_set: if self.id(index) in self.ids_to_highlight_set:
@ -996,9 +1012,7 @@ class BooksModel(QAbstractTableModel): # {{{
elif typ == 'bool': elif typ == 'bool':
val = value if value is None else bool(value) val = value if value is None else bool(value)
elif typ == 'rating': elif typ == 'rating':
val = int(value) val = max(0, min(int(value or 0), 10))
val = 0 if val < 0 else 5 if val > 5 else val
val *= 2
elif typ in ('int', 'float'): elif typ in ('int', 'float'):
if value == 0: if value == 0:
val = '0' val = '0'
@ -1089,8 +1103,7 @@ class BooksModel(QAbstractTableModel): # {{{
id = self.db.id(row) id = self.db.id(row)
books_to_refresh = set([id]) books_to_refresh = set([id])
if column == 'rating': if column == 'rating':
val = 0 if val < 0 else 5 if val > 5 else val val = max(0, min(int(val or 0), 10))
val *= 2
self.db.set_rating(id, val) self.db.set_rating(id, val)
elif column == 'series': elif column == 'series':
val = val.strip() val = val.strip()

View File

@ -224,6 +224,7 @@ class BooksView(QTableView): # {{{
self.setWordWrap(False) self.setWordWrap(False)
self.rating_delegate = RatingDelegate(self) self.rating_delegate = RatingDelegate(self)
self.half_rating_delegate = RatingDelegate(self, is_half_star=True)
self.timestamp_delegate = DateDelegate(self) self.timestamp_delegate = DateDelegate(self)
self.pubdate_delegate = PubDateDelegate(self) self.pubdate_delegate = PubDateDelegate(self)
self.last_modified_delegate = DateDelegate(self, self.last_modified_delegate = DateDelegate(self,
@ -760,9 +761,9 @@ class BooksView(QTableView): # {{{
def database_changed(self, db): def database_changed(self, db):
db.data.add_marked_listener(self.marked_changed_listener) db.data.add_marked_listener(self.marked_changed_listener)
for i in range(self.model().columnCount(None)): for i in range(self.model().columnCount(None)):
if self.itemDelegateForColumn(i) in (self.rating_delegate, if self.itemDelegateForColumn(i) in (
self.timestamp_delegate, self.pubdate_delegate, self.rating_delegate, self.timestamp_delegate, self.pubdate_delegate,
self.last_modified_delegate, self.languages_delegate): self.last_modified_delegate, self.languages_delegate, self.half_rating_delegate):
self.setItemDelegateForColumn(i, self.itemDelegate()) self.setItemDelegateForColumn(i, self.itemDelegate())
cm = self.column_map cm = self.column_map
@ -799,7 +800,8 @@ class BooksView(QTableView): # {{{
elif cc['datatype'] == 'bool': elif cc['datatype'] == 'bool':
self.setItemDelegateForColumn(cm.index(colhead), self.cc_bool_delegate) self.setItemDelegateForColumn(cm.index(colhead), self.cc_bool_delegate)
elif cc['datatype'] == 'rating': 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': elif cc['datatype'] == 'composite':
self.setItemDelegateForColumn(cm.index(colhead), self.cc_template_delegate) self.setItemDelegateForColumn(cm.index(colhead), self.cc_template_delegate)
elif cc['datatype'] == 'enumeration': elif cc['datatype'] == 'enumeration':
@ -1126,6 +1128,7 @@ class DeviceBooksView(BooksView): # {{{
self.can_add_columns = False self.can_add_columns = False
self.resize_on_select = False self.resize_on_select = False
self.rating_delegate = None self.rating_delegate = None
self.half_rating_delegate = None
for i in range(10): for i in range(10):
self.setItemDelegateForColumn(i, TextDelegate(self)) self.setItemDelegateForColumn(i, TextDelegate(self))
self.setDragDropMode(self.NoDragDrop) self.setDragDropMode(self.NoDragDrop)

View File

@ -13,12 +13,12 @@ from datetime import date, datetime
from PyQt5.Qt import ( from PyQt5.Qt import (
Qt, QDateTimeEdit, pyqtSignal, QMessageBox, QIcon, QToolButton, QWidget, Qt, QDateTimeEdit, pyqtSignal, QMessageBox, QIcon, QToolButton, QWidget,
QLabel, QGridLayout, QApplication, QDoubleSpinBox, QListWidgetItem, QSize, 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, QDialogButtonBox, QAction, QCalendarWidget, QDate, QDateTime, QUndoCommand,
QUndoStack, QVBoxLayout, QPlainTextEdit) QUndoStack, QVBoxLayout, QPlainTextEdit)
from calibre.gui2.widgets import EnLineEdit, FormatList as _FormatList, ImageView 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.icu import sort_key
from calibre.utils.config import tweaks, prefs from calibre.utils.config import tweaks, prefs
from calibre.ebooks.metadata import ( from calibre.ebooks.metadata import (
@ -1231,7 +1231,7 @@ class CommentsEdit(Editor, ToMetadataMixin): # {{{
db.set_comment(id_, self.current_val, notify=False, commit=False) db.set_comment(id_, self.current_val, notify=False, commit=False)
# }}} # }}}
class RatingEdit(make_undoable(QSpinBox), ToMetadataMixin): # {{{ class RatingEdit(RatingEditor, ToMetadataMixin): # {{{
LABEL = _('&Rating:') LABEL = _('&Rating:')
TOOLTIP = _('Rating of this book. 0-5 stars') TOOLTIP = _('Rating of this book. 0-5 stars')
FIELD_NAME = 'rating' FIELD_NAME = 'rating'
@ -1240,40 +1240,26 @@ class RatingEdit(make_undoable(QSpinBox), ToMetadataMixin): # {{{
super(RatingEdit, self).__init__(parent) super(RatingEdit, self).__init__(parent)
self.setToolTip(self.TOOLTIP) self.setToolTip(self.TOOLTIP)
self.setWhatsThis(self.TOOLTIP) self.setWhatsThis(self.TOOLTIP)
self.setMaximum(5)
self.setSuffix(' ' + _('stars'))
self.setSpecialValueText(_('Not rated'))
@dynamic_property @dynamic_property
def current_val(self): def current_val(self):
def fget(self): def fget(self):
return self.value() return self.rating_value
def fset(self, val): def fset(self, val):
if val is None: self.rating_value = val
val = 0
val = int(val)
if val < 0:
val = 0
if val > 5:
val = 5
self.set_spinbox_value(val)
return property(fget=fget, fset=fset) return property(fget=fget, fset=fset)
def initialize(self, db, id_): def initialize(self, db, id_):
val = db.rating(id_, index_is_id=True) val = db.rating(id_, index_is_id=True)
if val > 0:
val = int(val/2.)
else:
val = 0
self.current_val = val self.current_val = val
self.original_val = self.current_val self.original_val = self.current_val
def commit(self, db, id_): 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 return True
def zero(self): def zero(self):
self.setValue(0) self.setCurrentIndex(0)
# }}} # }}}

View File

@ -436,10 +436,7 @@ class MetadataSingleDialogBase(QDialog):
elif update_sorts and not mi.is_null('authors'): elif update_sorts and not mi.is_null('authors'):
self.author_sort.auto_generate() self.author_sort.auto_generate()
if not mi.is_null('rating'): if not mi.is_null('rating'):
try: self.rating.set_value(mi.rating * 2)
self.rating.set_value(mi.rating)
except:
pass
if not mi.is_null('publisher'): if not mi.is_null('publisher'):
self.publisher.set_value(mi.publisher) self.publisher.set_value(mi.publisher)
if not mi.is_null('tags'): if not mi.is_null('tags'):

View File

@ -1,3 +1,6 @@
#!/usr/bin/env python2
# vim:fileencoding=UTF-8
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2010, Kovid Goyal <kovid at kovidgoyal.net>'
@ -168,6 +171,8 @@ class CreateCustomColumn(QDialog):
self.comments_heading_position.setCurrentIndex(idx) self.comments_heading_position.setCurrentIndex(idx)
idx = max(0, self.comments_type.findData(c['display'].get('interpret_as', 'html'))) idx = max(0, self.comments_type.findData(c['display'].get('interpret_as', 'html')))
self.comments_type.setCurrentIndex(idx) self.comments_type.setCurrentIndex(idx)
elif ct == 'rating':
self.allow_half_stars.setChecked(bool(c['display'].get('allow_half_stars', False)))
self.datatype_changed() self.datatype_changed()
if ct in ['text', 'composite', 'enumeration']: if ct in ['text', 'composite', 'enumeration']:
self.use_decorations.setChecked(c['display'].get('use_decorations', False)) 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) l.addWidget(ec), l.addWidget(la, 1, 1)
self.enum_label = add_row(_('&Values'), l) 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: ') + '<span style="font-family:calibre Symbols">★★★½</span>')
add_row(None, ahs)
# Composite display properties # Composite display properties
l = QHBoxLayout() l = QHBoxLayout()
self.composite_sort_by_label = la = QLabel(_("&Sort/search column by")) 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_heading_position_label.setVisible(is_comments)
self.comments_type.setVisible(is_comments) self.comments_type.setVisible(is_comments)
self.comments_type_label.setVisible(is_comments) self.comments_type_label.setVisible(is_comments)
self.allow_half_stars.setVisible(col_type == 'rating')
def accept(self): def accept(self):
col = unicode(self.column_name_box.text()).strip() col = unicode(self.column_name_box.text()).strip()
@ -524,6 +535,8 @@ class CreateCustomColumn(QDialog):
elif col_type == 'comments': elif col_type == 'comments':
display_dict['heading_position'] = type(u'')(self.comments_heading_position.currentData()) display_dict['heading_position'] = type(u'')(self.comments_heading_position.currentData())
display_dict['interpret_as'] = type(u'')(self.comments_type.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: if col_type in ['text', 'composite', 'enumeration'] and not is_multiple:
display_dict['use_decorations'] = self.use_decorations.checkState() display_dict['use_decorations'] = self.use_decorations.checkState()

View File

@ -6,11 +6,16 @@ from __future__ import (unicode_literals, division, absolute_import,
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
import weakref
from PyQt5.Qt import ( from PyQt5.Qt import (
QPushButton, QPixmap, QIcon, QColor, Qt, QColorDialog, pyqtSignal, 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.complete2 import LineEdit, EditWithComplete
from calibre.gui2.widgets import history from calibre.gui2.widgets import history
@ -180,3 +185,106 @@ class Dialog(QDialog):
def setup_ui(self): def setup_ui(self):
raise NotImplementedError('You must implement this method in Dialog subclasses') 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_()