1#############################################################################
2##
3## Copyright (C) 2015 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 QFileInfo, QPoint, QRect, qRound, Qt, QTime, QTimer
44from PyQt5.QtGui import (QFontMetricsF, QImage, QPainter, QPixmap, QPolygon,
45        QRegion)
46from PyQt5.QtWidgets import (QApplication, QFrame, QGraphicsScene,
47        QGraphicsView, QGraphicsWidget, QMessageBox, QWidget)
48
49from colors import Colors
50from demoitem import DemoItem
51from demotextitem import DemoTextItem
52from imageitem import ImageItem
53from menumanager import MenuManager
54
55
56class MainWindow(QGraphicsView):
57    def __init__(self, parent=None):
58        super(MainWindow, self).__init__(parent)
59
60        self.imagesDir = QFileInfo(__file__).absolutePath() + '/images'
61
62        self.updateTimer = QTimer(self)
63        self.demoStartTime = QTime()
64        self.fpsTime = QTime()
65        self.background = QPixmap()
66
67        self.scene = None
68        self.mainSceneRoot = None
69        self.frameTimeList = []
70        self.fpsHistory = []
71
72        self.currentFps = Colors.fps
73        self.fpsMedian = -1
74        self.fpsLabel = None
75        self.pausedLabel = None
76        self.doneAdapt = False
77        self.useTimer = False
78        self.updateTimer.setSingleShot(True)
79        self.companyLogo = None
80        self.qtLogo = None
81
82        self.setupWidget()
83        self.setupScene()
84        self.setupSceneItems()
85        self.drawBackgroundToPixmap()
86
87    def setupWidget(self):
88        desktop = QApplication.desktop()
89        screenRect = desktop.screenGeometry(desktop.primaryScreen())
90        windowRect = QRect(0, 0, 800, 600)
91
92        if screenRect.width() < 800:
93            windowRect.setWidth(screenRect.width())
94
95        if screenRect.height() < 600:
96            windowRect.setHeight(screenRect.height())
97
98        windowRect.moveCenter(screenRect.center())
99        self.setGeometry(windowRect)
100        self.setMinimumSize(80, 60)
101
102        self.setWindowTitle("PyQt Examples")
103        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
104        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
105        self.setFrameStyle(QFrame.NoFrame)
106        self.setRenderingSystem()
107        self.updateTimer.timeout.connect(self.tick)
108
109    def setRenderingSystem(self):
110        self.setCacheMode(QGraphicsView.CacheBackground)
111        self.setViewport(QWidget())
112
113    def start(self):
114        self.switchTimerOnOff(True)
115        self.demoStartTime.restart()
116        MenuManager.instance().itemSelected(MenuManager.ROOT,
117                Colors.rootMenuName)
118        Colors.debug("- starting demo")
119
120    def enableMask(self, enable):
121        if not enable or Colors.noWindowMask:
122            self.clearMask()
123        else:
124            region = QPolygon([
125                    # North side.
126                    0, 0,
127                    800, 0,
128                    # East side.
129                    # 800, 70,
130                    # 790, 90,
131                    # 790, 480,
132                    # 800, 500,
133                    800, 600,
134                    # South side.
135                    700, 600,
136                    670, 590,
137                    130, 590,
138                    100, 600,
139                    0, 600,
140                    # West side.
141                    # 0, 550,
142                    # 10, 530,
143                    # 10, 520,
144                    # 0, 520,
145                    0, 0])
146
147            self.setMask(QRegion(region))
148
149    def setupScene(self):
150        self.scene = QGraphicsScene(self)
151        self.scene.setSceneRect(0, 0, 800, 600)
152        self.setScene(self.scene)
153        self.scene.setItemIndexMethod(QGraphicsScene.NoIndex)
154
155    def switchTimerOnOff(self, on):
156        ticker = MenuManager.instance().ticker
157        if ticker and ticker.scene():
158            ticker.tickOnPaint = not on or Colors.noTimerUpdate
159
160        if on and not Colors.noTimerUpdate:
161            self.useTimer = True
162            self.fpsTime = QTime.currentTime()
163            self.updateTimer.start(int(1000 / Colors.fps))
164            update_mode = QGraphicsView.NoViewportUpdate
165        else:
166            self.useTimer = False
167            self.updateTimer.stop()
168
169            if Colors.noTicker:
170                update_mode = QGraphicsView.MinimalViewportUpdate
171            else:
172                update_mode = QGraphicsView.SmartViewportUpdate
173
174        self.setViewportUpdateMode(update_mode)
175
176    def measureFps(self):
177        # Calculate time difference.
178        t = self.fpsTime.msecsTo(QTime.currentTime())
179        if t == 0:
180            t = 0.01
181
182        self.currentFps = (1000.0 / t)
183        self.fpsHistory.append(self.currentFps)
184        self.fpsTime = QTime.currentTime()
185
186        # Calculate median.
187        size = len(self.fpsHistory)
188
189        if size == 10:
190            self.fpsHistory.sort()
191            self.fpsMedian = self.fpsHistory[int(size / 2)]
192            if self.fpsMedian == 0:
193                self.fpsMedian = 0.01
194
195            self.fpsHistory = []
196
197            return True
198
199        return False
200
201    def forceFpsMedianCalculation(self):
202        # Used for adaption in case things are so slow that no median has yet
203        # been calculated.
204        if self.fpsMedian != -1:
205            return
206
207        size = len(self.fpsHistory)
208
209        if size == 0:
210            self.fpsMedian = 0.01
211            return
212
213        self.fpsHistory.sort()
214        self.fpsMedian = self.fpsHistory[size // 2]
215        if self.fpsMedian == 0:
216            self.fpsMedian = 0.01
217
218    def tick(self):
219        medianChanged = self.measureFps()
220        self.checkAdapt()
221
222        if medianChanged and self.fpsLabel and Colors.showFps:
223            self.fpsLabel.setText("FPS: %d" % int(self.currentFps))
224
225        if MenuManager.instance().ticker:
226            MenuManager.instance().ticker.tick()
227
228        self.viewport().update()
229
230        if self.useTimer:
231            self.updateTimer.start(int(1000 / Colors.fps))
232
233    def setupSceneItems(self):
234        if Colors.showFps:
235            self.fpsLabel = DemoTextItem("FPS: --", Colors.buttonFont(),
236                    Qt.white, -1, None, DemoTextItem.DYNAMIC_TEXT)
237            self.fpsLabel.setZValue(1000)
238            self.fpsLabel.setPos(Colors.stageStartX,
239                    600 - QFontMetricsF(Colors.buttonFont()).height() - 5)
240
241        self.mainSceneRoot = QGraphicsWidget()
242        self.scene.addItem(self.mainSceneRoot)
243
244        self.companyLogo = ImageItem(
245                QImage(self.imagesDir + '/trolltech-logo.png'),
246                1000, 1000, None, True, 0.5)
247        self.qtLogo = ImageItem(QImage(self.imagesDir + '/qtlogo_small.png'),
248                1000, 1000, None, True, 0.5)
249        self.companyLogo.setZValue(100)
250        self.qtLogo.setZValue(100)
251        self.pausedLabel = DemoTextItem("PAUSED", Colors.buttonFont(),
252                Qt.white, -1, None)
253        self.pausedLabel.setZValue(100)
254        fm = QFontMetricsF(Colors.buttonFont())
255        self.pausedLabel.setPos(Colors.stageWidth - fm.width("PAUSED"),
256                590 - fm.height())
257        self.pausedLabel.setRecursiveVisible(False)
258
259    def checkAdapt(self):
260        if self.doneAdapt or Colors.noTimerUpdate or self.demoStartTime.elapsed() < 2000:
261            return
262
263        self.doneAdapt = True
264        self.forceFpsMedianCalculation()
265        Colors.benchmarkFps = self.fpsMedian
266        Colors.debug("- benchmark: %d FPS" % int(Colors.benchmarkFps))
267
268        if Colors.noAdapt:
269            return
270
271        if self.fpsMedian < 30:
272            ticker = MenuManager.instance().ticker
273            if ticker and ticker.scene():
274                self.scene.removeItem(ticker)
275                Colors.noTimerUpdate = True
276                self.switchTimerOnOff(False)
277
278                if self.fpsLabel:
279                    self.fpsLabel.setText("FPS: (%d)" % int(self.fpsMedian))
280
281                Colors.debug("- benchmark adaption: removed ticker (fps < 30)")
282
283            if self.fpsMedian < 20:
284                Colors.noAnimations = True
285                Colors.debug("- benchmark adaption: animations switched off (fps < 20)")
286
287            Colors.adapted = True
288
289    def drawBackgroundToPixmap(self):
290        r = self.scene.sceneRect()
291        self.background = QPixmap(qRound(r.width()), qRound(r.height()))
292        self.background.fill(Qt.black)
293        painter = QPainter(self.background)
294
295        bg = QImage(self.imagesDir + '/demobg.png')
296        painter.drawImage(0, 0, bg)
297
298    def drawBackground(self, painter, rect):
299        painter.drawPixmap(QPoint(0, 0), self.background)
300
301    def toggleFullscreen(self):
302        if self.isFullScreen():
303            self.enableMask(True)
304            self.showNormal()
305            if MenuManager.instance().ticker:
306                MenuManager.instance().ticker.pause(False)
307        else:
308            self.enableMask(False)
309            self.showFullScreen()
310
311    def keyPressEvent(self, event):
312        if event.key() == Qt.Key_Escape:
313            QApplication.quit()
314        elif event.key() == Qt.Key_F1:
315            s = ""
316            s += "\nAdapt: "
317            s += ["on", "off"][Colors.noAdapt]
318            s += "\nAdaption occured: "
319            s += ["no", "yes"][Colors.adapted]
320            w = QWidget()
321            s += "\nColor bit depth: %d" % w.depth()
322            s += "\nWanted FPS: %d" % Colors.fps
323            s += "\nBenchmarked FPS: ";
324            if Colors.benchmarkFps != -1:
325                s += "%d" % Colors.benchmarkFps
326            else:
327                s += "not calculated"
328            s += "\nAnimations: ";
329            s += ["on", "off"][Colors.noAnimations]
330            s += "\nBlending: ";
331            s += ["on", "off"][Colors.useEightBitPalette]
332            s += "\nTicker: ";
333            s += ["on", "off"][Colors.noTicker]
334            s += "\nPixmaps: ";
335            s += ["off", "on"][Colors.usePixmaps]
336            s += "\nRescale images on resize: ";
337            s += ["on", "off"][Colors.noRescale]
338            s += "\nTimer based updates: ";
339            s += ["on", "off"][Colors.noTimerUpdate]
340            s += "\nSeparate loop: ";
341            s += ["no", "yes"][Colors.useLoop]
342            s += "\nScreen sync: ";
343            s += ["yes", "no"][Colors.noScreenSync]
344            QMessageBox.information(None, "Current configuration", s)
345
346        super(MainWindow, self).keyPressEvent(event)
347
348    def focusInEvent(self, event):
349        if not Colors.pause:
350            return
351
352        if MenuManager.instance().ticker:
353            MenuManager.instance().ticker.pause(False)
354
355        code = MenuManager.instance().currentMenuCode
356        if code in (MenuManager.ROOT, MenuManager.MENU1):
357            self.switchTimerOnOff(True)
358
359        self.pausedLabel.setRecursiveVisible(False)
360
361    def focusOutEvent(self, event):
362        if not Colors.pause:
363            return
364
365        if MenuManager.instance().ticker:
366            MenuManager.instance().ticker.pause(True)
367
368        code = MenuManager.instance().currentMenuCode
369        if code in (MenuManager.ROOT, MenuManager.MENU1):
370            self.switchTimerOnOff(False)
371
372        self.pausedLabel.setRecursiveVisible(True)
373
374    def resizeEvent(self, event):
375        self.resetTransform()
376        self.scale(event.size().width() / 800.0, event.size().height() / 600.0)
377
378        super(MainWindow, self).resizeEvent(event)
379
380        DemoItem.setTransform(self.transform())
381
382        if self.companyLogo:
383            r = self.scene.sceneRect()
384            ttb = self.companyLogo.boundingRect()
385            self.companyLogo.setPos(int((r.width() - ttb.width()) / 2),
386                    595 - ttb.height())
387            qtb = self.qtLogo.boundingRect()
388            self.qtLogo.setPos(802 - qtb.width(), 0)
389
390        # Changing size will almost always hurt FPS during the change so ignore
391        # it.
392        self.fpsHistory = []
393