1#############################################################################
2##
3## Copyright (C) 2013 Riverbank Computing Limited.
4## Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies).
5## All rights reserved.
6##
7## This file is part of the examples of PyQt.
8##
9## $QT_BEGIN_LICENSE:LGPL$
10## Commercial Usage
11## Licensees holding valid Qt Commercial licenses may use this file in
12## accordance with the Qt Commercial License Agreement provided with the
13## Software or, alternatively, in accordance with the terms contained in
14## a written agreement between you and Nokia.
15##
16## GNU Lesser General Public License Usage
17## Alternatively, this file may be used under the terms of the GNU Lesser
18## General Public License version 2.1 as published by the Free Software
19## Foundation and appearing in the file LICENSE.LGPL included in the
20## packaging of this file.  Please review the following information to
21## ensure the GNU Lesser General Public License version 2.1 requirements
22## will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
23##
24## In addition, as a special exception, Nokia gives you certain additional
25## rights.  These rights are described in the Nokia Qt LGPL Exception
26## version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
27##
28## GNU General Public License Usage
29## Alternatively, this file may be used under the terms of the GNU
30## General Public License version 3.0 as published by the Free Software
31## Foundation and appearing in the file LICENSE.GPL included in the
32## packaging of this file.  Please review the following information to
33## ensure the GNU General Public License version 3.0 requirements will be
34## met: http://www.gnu.org/copyleft/gpl.html.
35##
36## If you have questions regarding the use of this file, please contact
37## Nokia at qt-info@nokia.com.
38## $QT_END_LICENSE$
39##
40#############################################################################
41
42
43from PyQt5.QtCore import QPointF, QRect, QRectF, QSize, Qt
44from PyQt5.QtGui import (QColor, QImage, QLinearGradient, QPainter,
45        QPainterPath, QPen)
46
47from colors import Colors
48from demoitem import DemoItem
49from demoitemanimation import DemoItemAnimation
50from demotextitem import DemoTextItem
51from scanitem import ScanItem
52
53
54class ButtonBackground(DemoItem):
55    def __init__(self, type, highlighted, pressed, logicalSize, parent):
56        super(ButtonBackground, self).__init__(parent)
57
58        self.type = type
59        self.highlighted = highlighted
60        self.pressed = pressed
61        self.logicalSize = logicalSize
62        self.useSharedImage('%s%d%d%d' % (__file__, type, highlighted, pressed))
63
64    def createImage(self, transform):
65        if self.type in (TextButton.SIDEBAR, TextButton.PANEL):
66            return self.createRoundButtonBackground(transform)
67        else:
68            return self.createArrowBackground(transform)
69
70    def createRoundButtonBackground(self, transform):
71        scaledRect = transform.mapRect(QRect(0, 0,
72                self.logicalSize.width(), self.logicalSize.height()))
73
74        image = QImage(scaledRect.width(), scaledRect.height(),
75                QImage.Format_ARGB32_Premultiplied)
76        image.fill(QColor(0, 0, 0, 0).rgba())
77        painter = QPainter(image)
78        painter.setRenderHint(QPainter.SmoothPixmapTransform)
79        painter.setRenderHint(QPainter.Antialiasing)
80        painter.setPen(Qt.NoPen)
81
82        if Colors.useEightBitPalette:
83            painter.setPen(QColor(120, 120, 120))
84            if self.pressed:
85                painter.setBrush(QColor(60, 60, 60))
86            elif self.highlighted:
87                painter.setBrush(QColor(100, 100, 100))
88            else:
89                painter.setBrush(QColor(80, 80, 80))
90        else:
91            outlinebrush = QLinearGradient(0, 0, 0, scaledRect.height())
92            brush = QLinearGradient(0, 0, 0, scaledRect.height())
93
94            brush.setSpread(QLinearGradient.PadSpread)
95            highlight = QColor(255, 255, 255, 70)
96            shadow = QColor(0, 0, 0, 70)
97            sunken = QColor(220, 220, 220, 30)
98
99            if self.type == TextButton.PANEL:
100                normal1 = QColor(200, 170, 160, 50)
101                normal2 = QColor(50, 10, 0, 50)
102            else:
103                normal1 = QColor(255, 255, 245, 60)
104                normal2 = QColor(255, 255, 235, 10)
105
106            if self.pressed:
107                outlinebrush.setColorAt(0, shadow)
108                outlinebrush.setColorAt(1, highlight)
109                brush.setColorAt(0, sunken)
110                painter.setPen(Qt.NoPen)
111            else:
112                outlinebrush.setColorAt(1, shadow)
113                outlinebrush.setColorAt(0, highlight)
114                brush.setColorAt(0, normal1)
115                if not self.highlighted:
116                    brush.setColorAt(1, normal2)
117                painter.setPen(QPen(outlinebrush, 1))
118
119            painter.setBrush(brush)
120
121        if self.type == TextButton.PANEL:
122            painter.drawRect(0, 0, scaledRect.width(), scaledRect.height())
123        else:
124            painter.drawRoundedRect(0, 0, scaledRect.width(),
125                    scaledRect.height(), 10, 90, Qt.RelativeSize)
126
127        return image
128
129    def createArrowBackground(self, transform):
130        scaledRect = transform.mapRect(QRect(0, 0,
131                self.logicalSize.width(), self.logicalSize.height()))
132
133        image = QImage(scaledRect.width(), scaledRect.height(),
134                QImage.Format_ARGB32_Premultiplied)
135        image.fill(QColor(0, 0, 0, 0).rgba())
136        painter = QPainter(image)
137        painter.setRenderHint(QPainter.SmoothPixmapTransform)
138        painter.setRenderHint(QPainter.Antialiasing)
139        painter.setPen(Qt.NoPen)
140
141        if Colors.useEightBitPalette:
142            painter.setPen(QColor(120, 120, 120))
143            if self.pressed:
144                painter.setBrush(QColor(60, 60, 60))
145            elif self.highlighted:
146                painter.setBrush(QColor(100, 100, 100))
147            else:
148                painter.setBrush(QColor(80, 80, 80))
149        else:
150            outlinebrush = QLinearGradient(0, 0, 0, scaledRect.height())
151            brush = QLinearGradient(0, 0, 0, scaledRect.height())
152
153            brush.setSpread(QLinearGradient.PadSpread)
154            highlight = QColor(255, 255, 255, 70)
155            shadow = QColor(0, 0, 0, 70)
156            sunken = QColor(220, 220, 220, 30)
157            normal1 = QColor(200, 170, 160, 50)
158            normal2 = QColor(50, 10, 0, 50)
159
160            if self.pressed:
161                outlinebrush.setColorAt(0, shadow)
162                outlinebrush.setColorAt(1, highlight)
163                brush.setColorAt(0, sunken)
164                painter.setPen(Qt.NoPen)
165            else:
166                outlinebrush.setColorAt(1, shadow)
167                outlinebrush.setColorAt(0, highlight)
168                brush.setColorAt(0, normal1)
169                if not self.highlighted:
170                    brush.setColorAt(1, normal2)
171                painter.setPen(QPen(outlinebrush, 1))
172
173            painter.setBrush(brush);
174
175        painter.drawRect(0, 0, scaledRect.width(), scaledRect.height())
176
177        xOff = scaledRect.width() / 2
178        yOff = scaledRect.height() / 2
179        sizex = 3.0 * transform.m11()
180        sizey = 1.5 * transform.m22()
181        if self.type == TextButton.UP:
182            sizey *= -1
183        path = QPainterPath()
184        path.moveTo(xOff, yOff + (5 * sizey))
185        path.lineTo(xOff - (4 * sizex), yOff - (3 * sizey))
186        path.lineTo(xOff + (4 * sizex), yOff - (3 * sizey))
187        path.lineTo(xOff, yOff + (5 * sizey))
188        painter.drawPath(path)
189
190        return image
191
192
193class TextButton(DemoItem):
194    BUTTON_WIDTH = 180
195    BUTTON_HEIGHT = 19
196
197    LEFT, RIGHT = range(2)
198
199    SIDEBAR, PANEL, UP, DOWN = range(4)
200
201    ON, OFF, HIGHLIGHT, DISABLED = range(4)
202
203    def __init__(self, text, align=LEFT, userCode=0, parent=None, type=SIDEBAR):
204        super(TextButton, self).__init__(parent)
205
206        # Prevent a circular import.
207        from menumanager import MenuManager
208        self._menu_manager = MenuManager.instance()
209
210        self.menuString = text
211        self.buttonLabel = text
212        self.alignment = align
213        self.buttonType = type
214        self.userCode = userCode
215        self.scanAnim = None
216        self.bgOn = None
217        self.bgOff = None
218        self.bgHighlight = None
219        self.bgDisabled = None
220        self.state = TextButton.OFF
221
222        self.setAcceptHoverEvents(True)
223        self.setCursor(Qt.PointingHandCursor)
224
225        # Calculate the button size.
226        if type in (TextButton.SIDEBAR, TextButton.PANEL):
227            self.logicalSize = QSize(TextButton.BUTTON_WIDTH, TextButton.BUTTON_HEIGHT)
228        else:
229            self.logicalSize = QSize(int((TextButton.BUTTON_WIDTH / 2.0) - 5), int(TextButton.BUTTON_HEIGHT * 1.5))
230
231        self._prepared = False
232
233    def setMenuString(self, menu):
234        self.menuString = menu
235
236    def prepare(self):
237        if not self._prepared:
238            self.setupHoverText()
239            self.setupScanItem()
240            self.setupButtonBg()
241            self._prepared = True
242
243    def boundingRect(self):
244        return QRectF(0, 0, self.logicalSize.width(),
245                self.logicalSize.height())
246
247    def setupHoverText(self):
248        if not self.buttonLabel:
249            return
250
251        textItem = DemoTextItem(self.buttonLabel, Colors.buttonFont(),
252                Colors.buttonText, -1, self)
253        textItem.setZValue(self.zValue() + 2)
254        textItem.setPos(16, 0)
255
256    def setupScanItem(self):
257        if Colors.useButtonBalls:
258            scanItem = ScanItem(self)
259            scanItem.setZValue(self.zValue() + 1)
260
261            self.scanAnim = DemoItemAnimation(scanItem)
262
263            x = 1.0
264            y = 1.5
265            stop = TextButton.BUTTON_WIDTH - scanItem.boundingRect().width() - x
266            if self.alignment == TextButton.LEFT:
267                self.scanAnim.setDuration(2500)
268                self.scanAnim.setKeyValueAt(0.0, QPointF(x, y))
269                self.scanAnim.setKeyValueAt(0.5, QPointF(x, y))
270                self.scanAnim.setKeyValueAt(0.7, QPointF(stop, y))
271                self.scanAnim.setKeyValueAt(1.0, QPointF(x, y))
272                scanItem.setPos(QPointF(x, y))
273            else:
274                self.scanAnim.setKeyValueAt(0.0, QPointF(stop, y))
275                self.scanAnim.setKeyValueAt(0.5, QPointF(x, y))
276                self.scanAnim.setKeyValueAt(1.0, QPointF(stop, y))
277                scanItem.setPos(QPointF(stop, y))
278
279    def setState(self, state):
280        self.state = state
281        self.bgOn.setRecursiveVisible(state == TextButton.ON)
282        self.bgOff.setRecursiveVisible(state == TextButton.OFF)
283        self.bgHighlight.setRecursiveVisible(state == TextButton.HIGHLIGHT)
284        self.bgDisabled.setRecursiveVisible(state == TextButton.DISABLED)
285        if state == TextButton.DISABLED:
286            self.setCursor(Qt.ArrowCursor)
287        else:
288            self.setCursor(Qt.PointingHandCursor)
289
290    def setupButtonBg(self):
291        self.bgOn = ButtonBackground(self.buttonType, True, True,
292                self.logicalSize, self)
293        self.bgOff = ButtonBackground(self.buttonType, False, False,
294                self.logicalSize, self)
295        self.bgHighlight = ButtonBackground(self.buttonType, True, False,
296                self.logicalSize, self)
297        self.bgDisabled = ButtonBackground(self.buttonType, True, True,
298                self.logicalSize, self)
299        self.setState(TextButton.OFF)
300
301    def hoverEnterEvent(self, event):
302        if not self.isEnabled() or self.state == TextButton.DISABLED:
303            return
304
305        if self.state == TextButton.OFF:
306            self.setState(TextButton.HIGHLIGHT)
307
308            if Colors.noAnimations and Colors.useButtonBalls:
309                # Wait a bit in the beginning to enhance the effect.  We have
310                # to do this here so that the adaption can be dynamic.
311                self.scanAnim.setDuration(1000)
312                self.scanAnim.setKeyValueAt(0.2, self.scanAnim.posAt(0))
313
314            if (self._menu_manager.window.fpsMedian > 10 or Colors.noAdapt or
315                    Colors.noTimerUpdate):
316                if Colors.useButtonBalls:
317                    self.scanAnim.play(True, True)
318
319    def hoverLeaveEvent(self, event):
320        if self.state == TextButton.DISABLED:
321            return
322
323        self.setState(TextButton.OFF)
324
325        if Colors.noAnimations and Colors.useButtonBalls:
326            self.scanAnim.stop()
327
328    def mousePressEvent(self, event):
329        if self.state == TextButton.DISABLED:
330            return
331
332        if self.state == TextButton.HIGHLIGHT or self.state == TextButton.OFF:
333            self.setState(TextButton.ON)
334
335    def mouseReleaseEvent(self, event):
336        if self.state == TextButton.ON:
337            self.setState(TextButton.OFF)
338            if self.isEnabled() and self.boundingRect().contains(event.pos()):
339                self._menu_manager.itemSelected(self.userCode, self.menuString)
340
341    def animationStarted(self, _):
342        if self.state == TextButton.DISABLED:
343            return
344
345        self.setState(TextButton.OFF)
346