1#!/usr/bin/env python
2
3
4#############################################################################
5##
6## Copyright (C) 2013 Riverbank Computing Limited
7## Copyright (C) 2010 Hans-Peter Jansen <hpj@urpla.net>.
8## Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies).
9## All rights reserved.
10##
11## This file is part of the examples of PyQt.
12##
13## $QT_BEGIN_LICENSE:BSD$
14## You may use this file under the terms of the BSD license as follows:
15##
16## "Redistribution and use in source and binary forms, with or without
17## modification, are permitted provided that the following conditions are
18## met:
19##   * Redistributions of source code must retain the above copyright
20##     notice, this list of conditions and the following disclaimer.
21##   * Redistributions in binary form must reproduce the above copyright
22##     notice, this list of conditions and the following disclaimer in
23##     the documentation and/or other materials provided with the
24##     distribution.
25##   * Neither the name of Nokia Corporation and its Subsidiary(-ies) nor
26##     the names of its contributors may be used to endorse or promote
27##     products derived from this software without specific prior written
28##     permission.
29##
30## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
31## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
32## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
33## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
34## OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
35## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
36## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
37## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
38## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
39## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
40## OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
41## $QT_END_LICENSE$
42##
43#############################################################################
44
45
46import math
47
48from PyQt5.QtCore import pyqtSignal, QPointF, QSize, Qt
49from PyQt5.QtGui import QPainter, QPolygonF
50from PyQt5.QtWidgets import (QAbstractItemView, QApplication, QStyle,
51        QStyledItemDelegate, QTableWidget, QTableWidgetItem, QWidget)
52
53
54class StarRating(object):
55    # enum EditMode
56    Editable, ReadOnly = range(2)
57
58    PaintingScaleFactor = 20
59
60    def __init__(self, starCount=1, maxStarCount=5):
61        self._starCount = starCount
62        self._maxStarCount = maxStarCount
63
64        self.starPolygon = QPolygonF([QPointF(1.0, 0.5)])
65        for i in range(5):
66            self.starPolygon << QPointF(0.5 + 0.5 * math.cos(0.8 * i * math.pi),
67                                        0.5 + 0.5 * math.sin(0.8 * i * math.pi))
68
69        self.diamondPolygon = QPolygonF()
70        self.diamondPolygon << QPointF(0.4, 0.5) \
71                            << QPointF(0.5, 0.4) \
72                            << QPointF(0.6, 0.5) \
73                            << QPointF(0.5, 0.6) \
74                            << QPointF(0.4, 0.5)
75
76    def starCount(self):
77        return self._starCount
78
79    def maxStarCount(self):
80        return self._maxStarCount
81
82    def setStarCount(self, starCount):
83        self._starCount = starCount
84
85    def setMaxStarCount(self, maxStarCount):
86        self._maxStarCount = maxStarCount
87
88    def sizeHint(self):
89        return self.PaintingScaleFactor * QSize(self._maxStarCount, 1)
90
91    def paint(self, painter, rect, palette, editMode):
92        painter.save()
93
94        painter.setRenderHint(QPainter.Antialiasing, True)
95        painter.setPen(Qt.NoPen)
96
97        if editMode == StarRating.Editable:
98            painter.setBrush(palette.highlight())
99        else:
100            painter.setBrush(palette.windowText())
101
102        yOffset = (rect.height() - self.PaintingScaleFactor) / 2
103        painter.translate(rect.x(), rect.y() + yOffset)
104        painter.scale(self.PaintingScaleFactor, self.PaintingScaleFactor)
105
106        for i in range(self._maxStarCount):
107            if i < self._starCount:
108                painter.drawPolygon(self.starPolygon, Qt.WindingFill)
109            elif editMode == StarRating.Editable:
110                painter.drawPolygon(self.diamondPolygon, Qt.WindingFill)
111
112            painter.translate(1.0, 0.0)
113
114        painter.restore()
115
116
117class StarEditor(QWidget):
118
119    editingFinished = pyqtSignal()
120
121    def __init__(self, parent = None):
122        super(StarEditor, self).__init__(parent)
123
124        self._starRating = StarRating()
125
126        self.setMouseTracking(True)
127        self.setAutoFillBackground(True)
128
129    def setStarRating(self, starRating):
130        self._starRating = starRating
131
132    def starRating(self):
133        return self._starRating
134
135    def sizeHint(self):
136        return self._starRating.sizeHint()
137
138    def paintEvent(self, event):
139        painter = QPainter(self)
140        self._starRating.paint(painter, self.rect(), self.palette(),
141                StarRating.Editable)
142
143    def mouseMoveEvent(self, event):
144        star = self.starAtPosition(event.x())
145
146        if star != self._starRating.starCount() and star != -1:
147            self._starRating.setStarCount(star)
148            self.update()
149
150    def mouseReleaseEvent(self, event):
151        self.editingFinished.emit()
152
153    def starAtPosition(self, x):
154        # Enable a star, if pointer crosses the center horizontally.
155        starwidth = self._starRating.sizeHint().width() // self._starRating.maxStarCount()
156        star = (x + starwidth / 2) // starwidth
157        if 0 <= star <= self._starRating.maxStarCount():
158            return star
159
160        return -1
161
162
163class StarDelegate(QStyledItemDelegate):
164    def paint(self, painter, option, index):
165        starRating = index.data()
166        if isinstance(starRating, StarRating):
167            if option.state & QStyle.State_Selected:
168                painter.fillRect(option.rect, option.palette.highlight())
169
170            starRating.paint(painter, option.rect, option.palette,
171                    StarRating.ReadOnly)
172        else:
173            super(StarDelegate, self).paint(painter, option, index)
174
175    def sizeHint(self, option, index):
176        starRating = index.data()
177        if isinstance(starRating, StarRating):
178            return starRating.sizeHint()
179        else:
180            return super(StarDelegate, self).sizeHint(option, index)
181
182    def createEditor(self, parent, option, index):
183        starRating = index.data()
184        if isinstance(starRating, StarRating):
185            editor = StarEditor(parent)
186            editor.editingFinished.connect(self.commitAndCloseEditor)
187            return editor
188        else:
189            return super(StarDelegate, self).createEditor(parent, option, index)
190
191    def setEditorData(self, editor, index):
192        starRating = index.data()
193        if isinstance(starRating, StarRating):
194            editor.setStarRating(starRating)
195        else:
196            super(StarDelegate, self).setEditorData(editor, index)
197
198    def setModelData(self, editor, model, index):
199        starRating = index.data()
200        if isinstance(starRating, StarRating):
201            model.setData(index, editor.starRating())
202        else:
203            super(StarDelegate, self).setModelData(editor, model, index)
204
205    def commitAndCloseEditor(self):
206        editor = self.sender()
207        self.commitData.emit(editor)
208        self.closeEditor.emit(editor)
209
210
211def populateTableWidget(tableWidget):
212    staticData = (
213        ("Mass in B-Minor", "Baroque", "J.S. Bach", 5),
214        ("Three More Foxes", "Jazz", "Maynard Ferguson", 4),
215        ("Sex Bomb", "Pop", "Tom Jones", 3),
216        ("Barbie Girl", "Pop", "Aqua", 5),
217    )
218
219    for row, (title, genre, artist, rating) in enumerate(staticData):
220        item0 = QTableWidgetItem(title)
221        item1 = QTableWidgetItem(genre)
222        item2 = QTableWidgetItem(artist)
223        item3 = QTableWidgetItem()
224        item3.setData(0, StarRating(rating))
225        tableWidget.setItem(row, 0, item0)
226        tableWidget.setItem(row, 1, item1)
227        tableWidget.setItem(row, 2, item2)
228        tableWidget.setItem(row, 3, item3)
229
230
231if __name__ == '__main__':
232
233    import sys
234
235    app = QApplication(sys.argv)
236
237    tableWidget = QTableWidget(4, 4)
238    tableWidget.setItemDelegate(StarDelegate())
239    tableWidget.setEditTriggers(
240            QAbstractItemView.DoubleClicked | QAbstractItemView.SelectedClicked)
241    tableWidget.setSelectionBehavior(QAbstractItemView.SelectRows)
242
243    headerLabels = ("Title", "Genre", "Artist", "Rating")
244    tableWidget.setHorizontalHeaderLabels(headerLabels)
245
246    populateTableWidget(tableWidget)
247
248    tableWidget.resizeColumnsToContents()
249    tableWidget.resize(500, 300)
250    tableWidget.show()
251
252    sys.exit(app.exec_())
253