# vPresent is an immersive presentation creation and display application.
# Copyright (C) 2007-2008 by Infiscape Corporation
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

import sys, math
from PyQt4 import QtCore, QtGui

class DoubleRange:
   def __init__(self):
      self.mMinRelStep = 1.0e-10
      self.mDefaultRelStep = 1.0e-2
      self.mMinEps = 1.0e-10

      self.mMinValue = 0.0
      self.mMaxValue = 0.0
      self.mStep = 1.0
      self.mPageSize = 1
      self.mIsValid = False
      self.mValue = 0.0
      self.mExactValue = 0.0
      self.mExactPrevValue = 0.0
      self.mPrevValue = 0.0
      self.mPeriodic = False

   def setValid(self, isValid):
      if (isValid != self.mIsValid):
         self.mIsValid = isValid
         self.valueChange()

   def isValid(self):
      return self.mIsValid

   def setNewValue(self, x, align = False):
      vmin = 0.0
      vmax = 0.0
    
      self.mPrevValue = self.mValue

      vmin = min(self.mMinValue, self.mMaxValue)
      vmax = max(self.mMinValue, self.mMaxValue)

      #
      # Range check
      #
      if (x < vmin):
         if ((self.mPeriodic) and (vmin != vmax)):
            self.mValue = x + math.ceil( (vmin - x) / (vmax - vmin ) ) * (vmax - vmin)
         else:
            self.mValue = vmin;
      elif (x > vmax):
         if ((self.mPeriodic) and (vmin != vmax)):
            self.mValue = x - math.ceil( ( x - vmax) / (vmax - vmin )) * (vmax - vmin)
         else:
            self.mValue = vmax
      else:
         self.mValue = x

      self.mExactPrevValue = self.mExactValue
      self.mExactValue = self.mValue
    
      # align to grid
      if (align):
         if (self.mStep != 0.0):
            self.mValue = self.mMinValue + round((self.mValue - self.mMinValue) / self.mStep) * self.mStep
         else:
            self.mValue = self.mMinValue
        
         # correct rounding error at the border
         if (abs(self.mValue - self.mMaxValue) < self.mMinEps * abs(self.mStep)):
            self.mValue = self.mMaxValue

         # correct rounding error if value = 0
         if (abs(self.mValue) < self.mMinEps * abs(self.mStep)):
            self.mValue = 0.0

      if (not self.mIsValid or self.mPrevValue != self.mValue):
         self.mIsValid = True
         self.valueChange();

   def fitValue(self, x):
      self.setNewValue(x, True)

   def setValue(self, x):
      self.setNewValue(x, False)

   def setRange(self, vmin, vmax, vstep, pageSize):
      rchg = ((self.mMaxValue != vmax) or (self.mMinValue != vmin))
    
      if rchg: 
         self.mMinValue = vmin
         self.mMaxValue = vmax
    
      #
      # look if the step width has an acceptable 
      # value or otherwise change it.
      #
      self.setStep(vstep)

      #
      # limit page size
      #
      self.mPageSize = max(0, min(pageSize, int(abs((self.mMaxValue - self.mMinValue) / self.mStep))))
    
      # 
      # If the value lies out of the range, it 
      # will be changed. Note that it will not be adjusted to 
      # the new step width.
      self.setNewValue(self.mValue, False)
    
      # call notifier after the step width has been 
      # adjusted.
      if rchg:
         self.rangeChange()

   def setStep(self, vstep):
      intv = self.mMaxValue - self.mMinValue
    
      newStep = 0.0
      if vstep == 0.0:
         newStep = intv * self.mDefaultRelStep
      else:
         if ((intv > 0) and (vstep < 0)) or ((intv < 0) and (vstep > 0)):
            newStep = -vstep
         else:
            newStep = vstep
        
         if abs(newStep) < abs(self.mMinRelStep * intv):
            newStep = self.mMinRelStep * intv
    
      if newStep != self.mStep:
         self.mStep = newStep
         self.stepChange()

   def setPeriodic(self, tf):
      self.mPeriodic = tf

   def incValue(self, nSteps):
      if self.isValid():
         self.setNewValue(self.mValue + float(nSteps) * self.mStep, True)

   def incPages(self, nPages):
      if self.isValid():
         self.setNewValue(self.mValue + float(nPages) * float(self.mPageSize) * self.mStep, True)

   def valueChange(self):
      pass

   def rangeChange(self):
      pass

   def stepChange(self):
      pass

   def step(self):
      return abs(self.mStep)

   def maxValue(self):
      return self.mMaxValue
    
   def minValue(self):
      return self.mMinValue

   def periodic(self):
      return self.mPeriodic

   def pageSize(self):
      return self.mPageSize

   def value(self):
      return self.mValue

   def exactValue(self):
      return self.mExactValue

   def exactPrevValue(self):
      return self.mExactPrevValue; 

   def prevValue(self):
      return self.mPrevValue


class Wheel(QtGui.QWidget, DoubleRange):
   def __init__(self, parent = None):
      QtGui.QWidget.__init__(self, parent)
      DoubleRange.__init__(self)
      self.ScrNone = 0
      self.ScrMouse = 1
      self.ScrTimer = 2
      self.ScrDirect = 3
      self.ScrPage = 4
      self.WHEEL_DELTA = 120

      #### Abstract
      self.mScrollMode = self.ScrNone
      self.mMouseOffset = 0.0
      self.mDirection = 0
      self.mTracking = True

      self.mTimerID = 0
      self.mUpdateTime = 150
      self.mTimerClick = 0
      self.mTime = QtCore.QTime()
      self.mSpeed = 1.0
      self.mMass = 0.0
      self.mOrientation = QtCore.Qt.Horizontal
      self.mReadOnly = False
      ####

      self.NUM_COLORS = 30
      self.mSliderRect = QtCore.QRect()
      self.mViewAngle = 175.0;
      self.mTotalAngle = 360.0;
      self.mTickCount = 10;
      self.mIntBorder = 2;
      self.mBorderWidth = 2;
      self.mWheelWidth = 20;

      self.mColors = []
      for i in xrange(self.NUM_COLORS):
         self.mColors.append(QtGui.QColor())

      self.initWheel()
      self.setFocusPolicy(QtCore.Qt.TabFocus)

      self.setRange(0, 100.0, 1.0, 1.0)

   def initWheel(self):
      self.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Fixed)
      self.setAttribute(QtCore.Qt.WA_WState_OwnSizePolicy, False)
      self.setUpdateTime(50);

   def setColorArray(self):
      if self.mColors is None:
         return

      light = QtGui.QColor(self.palette().color(QtGui.QPalette.Light))
      dark = QtGui.QColor(self.palette().color(QtGui.QPalette.Dark))

      if (not self.mColors[0].isValid() or
          self.mColors[0] != light or
          self.mColors[self.NUM_COLORS - 1] != dark):
         self.mColors[0] = light
         self.mColors[self.NUM_COLORS - 1] = dark

         (lh, ls, lv, a) = self.mColors[0].getRgb()
         (dh, ds, dv, a) = self.mColors[self.NUM_COLORS - 1].getRgb()

         for i in xrange(1, self.NUM_COLORS):
            factor = float(i) / float(self.NUM_COLORS)

            self.mColors[i].setRgb( lh + int( float(dh - lh) * factor ),
                                   ls + int( float(ds - ls) * factor ),
                                   lv + int( float(dv - lv) * factor ))

   def setTickCnt(self, cnt):
      self.mTickCount = max(6, min(cnt, 50))
      self.update

   def tickCnt(self):
      return self.mTickCount

   #def mass(self):
   #   return self.mMass

   def setInternalBorder(self, w):
      d = min( self.width(), self.height() ) / 3
      w = min( w, d )
      self.mIntBorder = max( w, 1 )
      self.layoutWheel();

   def internalBorder(self):
      return self.mIntBorder

   def drawWheelBackground(self, p, r):
      p.save()

      #
      # initialize pens
      #
      light = self.palette().color(QtGui.QPalette.Light)
      dark = self.palette().color(QtGui.QPalette.Dark)

      lightPen = QtGui.QPen()
      lightPen.setColor(light)
      lightPen.setWidth(self.mIntBorder)

      darkPen = QtGui.QPen()
      darkPen.setColor(dark)
      darkPen.setWidth(self.mIntBorder)

      self.setColorArray()

      #
      # initialize auxiliary variables
      #

      nFields = self.NUM_COLORS * 13 / 10
      hiPos = nFields - self.NUM_COLORS + 1

      if ( self.getOrientation() == QtCore.Qt.Horizontal ):
         rx = r.x()
         ry = r.y() + self.mIntBorder
         rh = r.height() - 2* self.mIntBorder
         rw = r.width()
         #
         #  draw shaded background
         #
         x1 = rx
         for i in xrange(1, nFields):
            x2 = rx + (rw * i) / nFields;
            p.fillRect(x1, ry, x2-x1 + 1 ,rh, self.mColors[abs(i-hiPos)])
            x1 = x2 + 1
         p.fillRect(x1, ry, rw - (x1 - rx), rh, self.mColors[self.NUM_COLORS - 1])

         #
         # draw internal border
         #
         p.setPen(lightPen)
         ry = r.y() + self.mIntBorder / 2
         p.drawLine(r.x(), ry, r.x() + r.width() , ry)

         p.setPen(darkPen)
         ry = r.y() + r.height() - (self.mIntBorder - self.mIntBorder / 2)
         p.drawLine(r.x(), ry , r.x() + r.width(), ry)
      else: # Qt.Vertical
         rx = r.x() + self.mIntBorder;
         ry = r.y();
         rh = r.height();
         rw = r.width() - 2 * self.mIntBorder;

         #
         # draw shaded background
         #
         y1 = ry
         for i in xrange(1, nFields):
            y2 = ry + (rh * i) / self.Fields
            p.fillRect(rx, y1, rw, y2-y1 + 1, self.mColors[abs(i-hiPos)])
            y1 = y2 + 1
         p.fillRect(rx, y1, rw, rh - (y1 - ry), self.mColors[self.NUM_COLORS - 1])

         #
         #  draw internal borders
         #
         p.setPen(lightPen)
         rx = r.x() + self.mIntBorder / 2
         p.drawLine(rx, r.y(), rx, r.y() + r.height())

         p.setPen(darkPen);
         rx = r.x() + r.width() - (self.mIntBorder - self.mIntBorder / 2)
         p.drawLine(rx, r.y(), rx , r.y() + r.height())

      p.restore()

   def setTotalAngle(self, angle):
      if ( angle < 0.0 ):
         angle = 0.0

      self.mTotalAngle = angle
      self.update()

   def totalAngle(self):
      return self.mTotalAngle

   def getOrientation(self):
      return self.mOrientation

   def maxValue(self):
      return 100.0

   def minValue(self):
      return 1.0

   def setOrientation(self, o):
      if ( self.getOrientation() == o ):
         return;

      if ( not self.testAttribute(QtCore.Qt.WA_WState_OwnSizePolicy) ):
         sp = self.sizePolicy()
         sp.transpose();
         self.setSizePolicy(sp)

         self.setAttribute(QtCore.Qt.WA_WState_OwnSizePolicy, False)
      self.mOrientation = o
      self.layoutWheel();

   def setViewAngle(self, angle):
      self.mViewAngle = max(10.0, min(angle, 175.0))
      self.update()

   def viewAngle(self):
      return self.mViewAngle

   def drawWheel(self, p, r):
      #
      # draw background gradient
      #
      self.drawWheelBackground( p, r )

      if ( self.maxValue() == self.minValue() or self.mTotalAngle == 0.0 ):
         return

      light = self.palette().color(QtGui.QPalette.Light)
      dark = self.palette().color(QtGui.QPalette.Dark)

      if self.minValue() < self.maxValue():
         sign = 1.0
      else:
         sign = -1.0
      cnvFactor = abs(self.mTotalAngle / (self.maxValue() - self.minValue()))
      halfIntv = 0.5 * self.mViewAngle / cnvFactor
      loValue = self.value() - halfIntv
      hiValue = self.value() + halfIntv
      tickWidth = 360.0 / float(self.mTickCount) / cnvFactor
      M_PI = 3.1415
      sinArc = math.sin(self.mViewAngle * M_PI / 360.0)
      cnvFactor *= M_PI / 180.0


      #
      # draw grooves
      #
      if ( self.getOrientation() == QtCore.Qt.Horizontal ):
         halfSize = float(r.width()) * 0.5

         l1 = r.y() + self.mIntBorder
         l2 = r.y() + r.height() - self.mIntBorder - 1

         # draw one point over the border if border > 1
         if ( self.mIntBorder > 1 ):
            l1 -= 1
            l2 += 1
        
         maxpos = r.x() + r.width() - 2
         minpos = r.x() + 2

         #
         # draw tick marks
         #
         tickValue = math.ceil(loValue / tickWidth) * tickWidth
         while tickValue < hiValue:
            #
            #  calculate position
            #
            tickPos = (r.x() + r.width() -
                      int( halfSize * (sinArc + sign *  math.sin((tickValue - self.value()) * cnvFactor))
                    / sinArc))
            #
            # draw vertical line
            #
            if ( (tickPos <= maxpos) and (tickPos > minpos) ):
                p.setPen(dark)
                p.drawLine(tickPos -1 , l1, tickPos - 1,  l2 ); 
                p.setPen(light)
                p.drawLine(tickPos, l1, tickPos, l2); 
            tickValue += tickWidth
      elif ( self.getOrientation() == QtCore.Qt.Vertical ):
         halfSize = float(r.height()) * 0.5

         l1 = r.x() + self.mIntBorder;
         l2 = r.x() + r.width() - self.mIntBorder - 1;

         if ( self.mIntBorder > 1 ):
            l1 -= 1
            l2 += 1

         maxpos = r.y() + r.height() - 2
         minpos = r.y() + 2

         #
         # draw tick marks
         #
         tickValue = math.ceil(loValue / tickWidth) * tickWidth
         while (tickValue < hiValue):
            #
            # calculate position
            #
            tickPos = r.y() + int( halfSize *
                (sinArc + sign * math.sin((tickValue - self.value()) * cnvFactor))
                / sinArc)

            #
            #  draw horizontal line
            #
            if ( (tickPos <= maxpos) and (tickPos > minpos) ):
               p.setPen(dark)
               p.drawLine(l1, tickPos - 1 ,l2, tickPos - 1)
               p.setPen(light)
               p.drawLine(l1, tickPos, l2, tickPos)
            tickValue += tickWidth

   def getValue(self, p):
      # The reference position is arbitrary, but the
      # sign of the offset is important
      if ( self.getOrientation() == QtCore.Qt.Vertical ):
         w = self.mSliderRect.height()
         dx = self.mSliderRect.y() - p.y()
      else:
         w = self.mSliderRect.width()
         dx = p.x() - self.mSliderRect.x()

      # w pixels is an arc of viewAngle degrees,
      # so we convert change in pixels to change in angle
      ang = dx * self.mViewAngle / w

      # value range maps to totalAngle degrees,
      # so convert the change in angle to a change in value
      val = ang * ( self.maxValue() - self.minValue() ) / self.mTotalAngle

      # Note, range clamping and rasterizing to step is automatically
      # handled by QwtAbstractSlider, so we simply return the change in value
      return val

   def resizeEvent(self, e):
      self.layoutWheel(False)

   def layoutWheel(self, update_geometry):
      r = self.rect()
      self.mSliderRect.setRect(r.x() + self.mBorderWidth, r.y() + self.mBorderWidth,
        r.width() - 2*self.mBorderWidth, r.height() - 2*self.mBorderWidth)

      if ( update_geometry ):
        self.updateGeometry()
        self.update()

   def paintEvent(self, e):
      # Use double-buffering
      ur = e.rect()
      if ur.isValid():
         painter = QtGui.QPainter(self)
         self.draw(painter, ur)

   def draw(self, painter, r):
      QtGui.qDrawShadePanel( painter, self.rect().x(), self.rect().y(),
         self.rect().width(), self.rect().height(),
         self.palette(), 
         True, self.mBorderWidth )

      self.drawWheel(painter, self.mSliderRect)

      if self.hasFocus():
         pass
         # XXX:
         #QwtPainter.drawFocusRect(painter, this);

   def valueChange(self):
      if (self.mTracking):
         self.emit(QtCore.SIGNAL("valueChanged(double)"), self.value())
      self.update()

   def getScrollMode(self, p):
      if ( self.mSliderRect.contains(p) ):
         scrollMode = self.ScrMouse;
      else:
         scrollMode = self.ScrNone;

      direction = 0
      return (scrollMode, direction)

   def setMass(self, val):
      self.mMass = val

   def setWheelWidth(self, w):
      self.mWheelWidth = w
      self.layoutWheel()

   def sizeHint(self):
      return self.minimumSizeHint()

   def minimumSizeHint(self):
      sz = QtCore.QSize( 3*self.mWheelWidth + 2*self.mBorderWidth,
                        self.mWheelWidth + 2*self.mBorderWidth )
      if ( not self.getOrientation() == QtCore.Qt.Horizontal ):
         sz.transpose()
      return sz;

   def paletteChange(self, p):
      self.update()

   #======== Abstract =========
   def setReadOnly(self, readOnly):
      self.mReadOnly = readOnly
      self.update()

   def isReadOnly(self):
      return self.mReadOnly

   def stopMoving(self):
      if(self.mTimerID):
         self.killTimer(self.mTimerID)
         self.mTimerID = 0

   def setUpdateTime(self, t):
      if (t < 50):
         t = 50
      self.mUpdateTime = t

   def mousePressEvent(self, e):
      if self.isReadOnly():
         e.ignore()
         return
      if not self.isValid():
         return

      p = e.pos()

      self.mTimerClick = 0

      (self.mScrollMode, self.mDirection) = self.getScrollMode(p)
      self.stopMoving()

      if self.ScrPage == self.mScrollMode or self.ScrTimer == self.mScrollMode:
         self.mMouseOffset = 0;
         self.mTimerID = self.startTimer(max(250, 2 * self.mUpdateTime))
      elif self.ScrMouse == self.mScrollMode:
         self.mTime.start()
         self.mSpeed = 0
         self.mMouseOffset = self.getValue(p) - self.value()
         self.emit(QtCore.SIGNAL("sliderPressed()"))
      else:
         self.mMouseOffset = 0
         self.mDirection = 0

   def buttonReleased(self):
      if ((not self.mTracking) or (self.value() != self.prevValue())):
         self.emit(QtCore.SIGNAL("valueChanged(double)"), self.value())

   def mouseReleaseEvent(self, e):
      if self.isReadOnly():
         e.ignore()
         return
      if not self.isValid():
         return

      inc = self.step()

      if self.ScrMouse == self.mScrollMode:
         self.setPosition(e.pos())
         self.mDirection = 0
         self.mMouseOffset = 0
         if (self.mMass > 0.0):
            ms = self.mTime.elapsed()
            if ((abs(self.mSpeed) >  0.0) and (ms < 50)):
               self.mTimerID = startTimer(self.mUpdateTime);
         else:
            self.mScrollMode = self.ScrNone
            self.buttonReleased();
         self.emit(QtCore.SIGNAL("sliderReleased"))

      elif self.ScrDirect == self.mScrollMode:
         self.setPosition(e.pos())
         self.mDirection = 0
         self.mMouseOffset = 0
         self.mScrollMode = self.ScrNone
         self.buttonReleased()

      elif self.ScrPage == self.mScrollMode:
         self.stopMoving()
         #if (not self.mTimerClick):
         #   QwtDoubleRange::incPages(self.mDirection);
         self.mTimerClick = 0
         self.buttonReleased()
         self.mScrollMode = self.ScrNone

      elif self.ScrTimer == self.mScrollMode:
         self.stopMoving();
         if (not self.mTimerClick):
            DoubleRange.fitValue(self, self.value() + self.mDirection * inc)
         self.mTimerClick = 0
         self.buttonReleased()
         self.mScrollMode = self.ScrNone
      else:
         self.mScrollMode = self.ScrNone
         self.buttonReleased()

   def setPosition(self, p):
      DoubleRange.fitValue(self, self.getValue(p) - self.mMouseOffset);

   def setTracking(self, enable):
      self.mTracking = enable

   def mouseMoveEvent(self, e):
      if self.isReadOnly():
         print "Ignore"
         e.ignore()
         return

      if not self.isValid():
         print "Not valid"
         return

      if (self.mScrollMode == self.ScrMouse ):
         self.setPosition(e.pos())
         if (self.mMass > 0.0):
            ms = float(self.mTime.elapsed())
            if (ms < 1.0):
               ms = 1.0
            self.mSpeed = (self.exactValue() - self.exactPrevValue()) / ms
            self.mTime.start();
         if (self.value() != self.prevValue()):
            self.emit(QtCore.SIGNAL("sliderMoved(double)"), self.value())

   def wheelEvent(self, e):
      if self.isReadOnly():
         e.ignore()
         return

      if not self.isValid():
         return

      mode = self.ScrNone
      direction = 0

      # Give derived classes a chance to say ScrNone
      (mode, direction) = self.getScrollMode(e.pos())
      if ( mode != self.ScrNone ):
         inc = e.delta() / self.WHEEL_DELTA;
         self.incPages(inc);
         if (self.value() != self.prevValue()):
            self.emit(QtCore.SIGNAL("sliderMoved(double)"), self.value())

   def keyPressEvent(self, e):
      if self.isReadOnly():
         e.ignore()
         return

      if not self.isValid():
         return

      increment = 0

      if QtCore.Qt.Key_Down == e.key():
         if self.mOrientation == QtCore.Qt.Vertical:
            increment = -1
      elif QtCore.Qt.Key_Up == e.key():
         if self.mOrientation == QtCore.Qt.Vertical:
            increment = 1
      elif QtCore.Qt.Key_Left == e.key():
         if self.mOrientation == QtCore.Qt.Horizontal:
            increment = -1
      elif QtCore.Qt.Key_Right == e.key():
         if self.mOrientation == QtCore.Qt.Horizontal:
            increment = 1;
      else:
         e.ignore()

      if increment != 0:
         self.incValue(increment)
         if (self.value() != self.prevValue()):
            self.emit(QtCore.SIGNAL("sliderMoved(double)"), self.value())

   def timerEvent(self, e):
      inc = self.step()

      if self.ScrMouse == self.mScrollMode:
         if self.mMass > 0.0:
            self.mSpeed *= math.exp( - float(self.mUpdateTime) * 0.001 / self.mMass )
            newval = self.exactValue() + self.mSpeed * float(self.mUpdateTime)
            self.fitValue(newval)
            # stop if self.mSpeed < one step per second
            if (abs(self.mSpeed) < 0.001 * abs(self.step())):
               self.mSpeed = 0
               self.stopMoving()
               self.buttonReleased()
         else:
            self.stopMoving()

      elif self.ScrPage == self.mScrollMode:
         DoubleRange.incPages(self. self.mDirection)
         if not self.mTimerClick:
            self.killTimer(self.mTimerID)
            self.mTimerID = self.startTimer(self.mUpdateTime)
      elif self.ScrTimer == self.mScrollMode:
         DoubleRange.fitValue(self, self.value() +  float(self.mDirection) * inc)
         if not self.mTimerClick:
            self.killTimer(self.mTimerID)
            self.mTimerID = self.startTimer(self.mUpdateTime)
      else:
         self.stopMoving()

      self.mTimerClick = 1

   def setMass(self, val):
      if (val < 0.001):
         self.mMass = 0.0
      elif (val > 100.0):
         self.mMass = 100.0
      else:
         self.mMass = val

   def setValue(self, val):
      if (self.mScrollMode == self.ScrMouse):
         self.stopMoving()
      DoubleRange.setValue(self,val)


   def fitValue(self, val):
      if (self.mScrollMode == self.ScrMouse):
         self.stopMoving()
      DoubleRange.fitValue(self, val)


   def incValue(self, steps):
      if (self.mScrollMode == self.ScrMouse):
         self.stopMoving()
      DoubleRange.incValue(self, steps)

   def setMouseOffset(self, offset):
      self.mMouseOffset = offset

   def mouseOffset(self):
      return self.mMouseOffset;

   def scrollMode(self):
      return self.mScrollMode

if __name__ == "__main__":
    app = QtGui.QApplication(sys.argv)
    wheel = Wheel()
    wheel.show()
    sys.exit(app.exec_())
