aboutsummaryrefslogtreecommitdiffstats
path: root/gui-project
diff options
context:
space:
mode:
authorEddy Pedroni <epedroni@pm.me>2024-11-09 20:35:56 +0100
committerEddy Pedroni <epedroni@pm.me>2024-11-09 20:35:56 +0100
commitcda8197669409689be291660f93cb288ab2d31b3 (patch)
tree81db9b0c7c0491e0737cbffb39af6b935c0dfeb8 /gui-project
parenta2257a900d4fffd6f94b73f1c48c62370ed1d684 (diff)
Migrate to project-based structure
Diffstat (limited to 'gui-project')
-rw-r--r--gui-project/pyproject.toml24
-rw-r--r--gui-project/src/MainWindow.py111
-rw-r--r--gui-project/src/mainwindow.ui161
-rw-r--r--gui-project/src/solo_tool_gui.py261
4 files changed, 557 insertions, 0 deletions
diff --git a/gui-project/pyproject.toml b/gui-project/pyproject.toml
new file mode 100644
index 0000000..1e6fcf4
--- /dev/null
+++ b/gui-project/pyproject.toml
@@ -0,0 +1,24 @@
+[build-system]
+requires = ["setuptools"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "solo_tool_gui"
+authors = [
+ { name = "Eddy Pedroni", email = "epedroni@pm.me" },
+]
+description = "A Qt5-based GUI frontend for the solo_tool library"
+requires-python = ">=3.12"
+dependencies = [
+ "PyQt5>=5.6",
+ "solo_tool"
+]
+dynamic = ["version"]
+
+[project.optional-dependencies]
+dev = [
+]
+
+[project.gui-scripts]
+solo-tool-gui = "solo_tool_gui:main"
+
diff --git a/gui-project/src/MainWindow.py b/gui-project/src/MainWindow.py
new file mode 100644
index 0000000..137bd33
--- /dev/null
+++ b/gui-project/src/MainWindow.py
@@ -0,0 +1,111 @@
+# -*- coding: utf-8 -*-
+
+# Form implementation generated from reading ui file 'mainwindow.ui'
+#
+# Created by: PyQt5 UI code generator 5.15.6
+#
+# WARNING: Any manual changes made to this file will be lost when pyuic5 is
+# run again. Do not edit this file unless you know what you are doing.
+
+
+from PyQt5 import QtCore, QtGui, QtWidgets
+
+
+class Ui_MainWindow(object):
+ def setupUi(self, MainWindow):
+ MainWindow.setObjectName("MainWindow")
+ MainWindow.resize(971, 767)
+ self.centralwidget = QtWidgets.QWidget(MainWindow)
+ self.centralwidget.setObjectName("centralwidget")
+ self.verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget)
+ self.verticalLayout.setObjectName("verticalLayout")
+ self.listsLayout = QtWidgets.QHBoxLayout()
+ self.listsLayout.setSizeConstraint(QtWidgets.QLayout.SetDefaultConstraint)
+ self.listsLayout.setSpacing(6)
+ self.listsLayout.setObjectName("listsLayout")
+ self.songListView = QtWidgets.QListView(self.centralwidget)
+ self.songListView.setObjectName("songListView")
+ self.listsLayout.addWidget(self.songListView)
+ self.abListView = QtWidgets.QListView(self.centralwidget)
+ self.abListView.setObjectName("abListView")
+ self.listsLayout.addWidget(self.abListView)
+ self.verticalLayout.addLayout(self.listsLayout)
+ self.slidersLayout = QtWidgets.QVBoxLayout()
+ self.slidersLayout.setObjectName("slidersLayout")
+ self.songSlider = QtWidgets.QSlider(self.centralwidget)
+ self.songSlider.setMinimumSize(QtCore.QSize(0, 0))
+ self.songSlider.setOrientation(QtCore.Qt.Horizontal)
+ self.songSlider.setObjectName("songSlider")
+ self.slidersLayout.addWidget(self.songSlider)
+ self.aSlider = QtWidgets.QSlider(self.centralwidget)
+ self.aSlider.setOrientation(QtCore.Qt.Horizontal)
+ self.aSlider.setObjectName("aSlider")
+ self.slidersLayout.addWidget(self.aSlider)
+ self.bSlider = QtWidgets.QSlider(self.centralwidget)
+ self.bSlider.setOrientation(QtCore.Qt.Horizontal)
+ self.bSlider.setObjectName("bSlider")
+ self.slidersLayout.addWidget(self.bSlider)
+ self.verticalLayout.addLayout(self.slidersLayout)
+ self.buttonsLayout = QtWidgets.QGridLayout()
+ self.buttonsLayout.setObjectName("buttonsLayout")
+ self.pauseButton = QtWidgets.QPushButton(self.centralwidget)
+ self.pauseButton.setObjectName("pauseButton")
+ self.buttonsLayout.addWidget(self.pauseButton, 0, 1, 1, 1)
+ self.initMidiButton = QtWidgets.QPushButton(self.centralwidget)
+ self.initMidiButton.setObjectName("initMidiButton")
+ self.buttonsLayout.addWidget(self.initMidiButton, 0, 4, 1, 1)
+ self.rateSlider = QtWidgets.QSlider(self.centralwidget)
+ sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed)
+ sizePolicy.setHorizontalStretch(0)
+ sizePolicy.setVerticalStretch(0)
+ sizePolicy.setHeightForWidth(self.rateSlider.sizePolicy().hasHeightForWidth())
+ self.rateSlider.setSizePolicy(sizePolicy)
+ self.rateSlider.setOrientation(QtCore.Qt.Horizontal)
+ self.rateSlider.setObjectName("rateSlider")
+ self.buttonsLayout.addWidget(self.rateSlider, 2, 0, 1, 1)
+ self.playButton = QtWidgets.QPushButton(self.centralwidget)
+ self.playButton.setObjectName("playButton")
+ self.buttonsLayout.addWidget(self.playButton, 0, 0, 1, 1)
+ self.saveSessionButton = QtWidgets.QPushButton(self.centralwidget)
+ self.saveSessionButton.setObjectName("saveSessionButton")
+ self.buttonsLayout.addWidget(self.saveSessionButton, 0, 2, 1, 1)
+ self.storeAbButton = QtWidgets.QPushButton(self.centralwidget)
+ self.storeAbButton.setObjectName("storeAbButton")
+ self.buttonsLayout.addWidget(self.storeAbButton, 2, 3, 1, 1)
+ self.loadSessionButton = QtWidgets.QPushButton(self.centralwidget)
+ self.loadSessionButton.setObjectName("loadSessionButton")
+ self.buttonsLayout.addWidget(self.loadSessionButton, 0, 3, 1, 1)
+ self.abRepeatCheckBox = QtWidgets.QCheckBox(self.centralwidget)
+ self.abRepeatCheckBox.setObjectName("abRepeatCheckBox")
+ self.buttonsLayout.addWidget(self.abRepeatCheckBox, 2, 1, 1, 1)
+ self.addSongButton = QtWidgets.QPushButton(self.centralwidget)
+ self.addSongButton.setObjectName("addSongButton")
+ self.buttonsLayout.addWidget(self.addSongButton, 2, 4, 1, 1)
+ self.horizontalLayout = QtWidgets.QHBoxLayout()
+ self.horizontalLayout.setObjectName("horizontalLayout")
+ self.setAButton = QtWidgets.QPushButton(self.centralwidget)
+ self.setAButton.setObjectName("setAButton")
+ self.horizontalLayout.addWidget(self.setAButton)
+ self.setBButton = QtWidgets.QPushButton(self.centralwidget)
+ self.setBButton.setObjectName("setBButton")
+ self.horizontalLayout.addWidget(self.setBButton)
+ self.buttonsLayout.addLayout(self.horizontalLayout, 2, 2, 1, 1)
+ self.verticalLayout.addLayout(self.buttonsLayout)
+ MainWindow.setCentralWidget(self.centralwidget)
+
+ self.retranslateUi(MainWindow)
+ QtCore.QMetaObject.connectSlotsByName(MainWindow)
+
+ def retranslateUi(self, MainWindow):
+ _translate = QtCore.QCoreApplication.translate
+ MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
+ self.pauseButton.setText(_translate("MainWindow", "Pause"))
+ self.initMidiButton.setText(_translate("MainWindow", "Connect MIDI"))
+ self.playButton.setText(_translate("MainWindow", "Play"))
+ self.saveSessionButton.setText(_translate("MainWindow", "Save session"))
+ self.storeAbButton.setText(_translate("MainWindow", "Store AB"))
+ self.loadSessionButton.setText(_translate("MainWindow", "Load session"))
+ self.abRepeatCheckBox.setText(_translate("MainWindow", "AB repeat"))
+ self.addSongButton.setText(_translate("MainWindow", "Add song"))
+ self.setAButton.setText(_translate("MainWindow", "Set A"))
+ self.setBButton.setText(_translate("MainWindow", "Set B"))
diff --git a/gui-project/src/mainwindow.ui b/gui-project/src/mainwindow.ui
new file mode 100644
index 0000000..ac4d97b
--- /dev/null
+++ b/gui-project/src/mainwindow.ui
@@ -0,0 +1,161 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>MainWindow</class>
+ <widget class="QMainWindow" name="MainWindow">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>971</width>
+ <height>767</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>MainWindow</string>
+ </property>
+ <widget class="QWidget" name="centralwidget">
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <layout class="QHBoxLayout" name="listsLayout">
+ <property name="spacing">
+ <number>6</number>
+ </property>
+ <property name="sizeConstraint">
+ <enum>QLayout::SetDefaultConstraint</enum>
+ </property>
+ <item>
+ <widget class="QListView" name="songListView"/>
+ </item>
+ <item>
+ <widget class="QListView" name="abListView"/>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QVBoxLayout" name="slidersLayout">
+ <item>
+ <widget class="QSlider" name="songSlider">
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSlider" name="aSlider">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSlider" name="bSlider">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QGridLayout" name="buttonsLayout">
+ <item row="0" column="1">
+ <widget class="QPushButton" name="pauseButton">
+ <property name="text">
+ <string>Pause</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="4">
+ <widget class="QPushButton" name="initMidiButton">
+ <property name="text">
+ <string>Connect MIDI</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QSlider" name="rateSlider">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QPushButton" name="playButton">
+ <property name="text">
+ <string>Play</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="2">
+ <widget class="QPushButton" name="saveSessionButton">
+ <property name="text">
+ <string>Save session</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="3">
+ <widget class="QPushButton" name="storeAbButton">
+ <property name="text">
+ <string>Store AB</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="3">
+ <widget class="QPushButton" name="loadSessionButton">
+ <property name="text">
+ <string>Load session</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QCheckBox" name="abRepeatCheckBox">
+ <property name="text">
+ <string>AB repeat</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="4">
+ <widget class="QPushButton" name="addSongButton">
+ <property name="text">
+ <string>Add song</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="2">
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <widget class="QPushButton" name="setAButton">
+ <property name="text">
+ <string>Set A</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="setBButton">
+ <property name="text">
+ <string>Set B</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/gui-project/src/solo_tool_gui.py b/gui-project/src/solo_tool_gui.py
new file mode 100644
index 0000000..0488f84
--- /dev/null
+++ b/gui-project/src/solo_tool_gui.py
@@ -0,0 +1,261 @@
+import sys
+
+from PyQt5.QtGui import *
+from PyQt5.QtWidgets import *
+from PyQt5.QtCore import *
+from MainWindow import Ui_MainWindow
+
+from solo_tool import SoloTool
+from solo_tool.midi_controller_launchpad_mini import MidiController
+
+POSITION_FACTOR = 100000
+RATE_FACTOR = 10
+UI_REFRESH_PERIOD_MS = 500
+
+CHANGE_GUI = 0
+CHANGE_INTERNAL = 1
+
+class PlaylistModel(QAbstractListModel):
+ def __init__(self, soloTool, *args, **kwargs):
+ super(PlaylistModel, self).__init__(*args, **kwargs)
+ self.soloTool = soloTool
+
+ def data(self, index, role):
+ if role == Qt.DisplayRole:
+ from pathlib import Path
+ path = Path(self.soloTool.getSongs()[index.row()])
+ return path.name
+
+ def rowCount(self, index):
+ return len(self.soloTool.getSongs())
+
+class ABListModel(QAbstractListModel):
+ def __init__(self, soloTool, *args, **kwargs):
+ super(ABListModel, self).__init__(*args, **kwargs)
+ self.soloTool = soloTool
+
+ def data(self, index, role):
+ if role == Qt.DisplayRole:
+ ab = self.soloTool.getStoredAbLimits()[index.row()]
+ return f"{ab[0]} - {ab[1]}"
+
+ def rowCount(self, index):
+ return len(self.soloTool.getStoredAbLimits())
+
+class MainWindow(QMainWindow, Ui_MainWindow):
+ songChangeSignal = pyqtSignal(int)
+ abLimitsChangeSignal = pyqtSignal(int)
+
+ def __init__(self, *args, **kwargs):
+ super(MainWindow, self).__init__(*args, **kwargs)
+
+ self.setupUi(self)
+
+ self.timer = QTimer(self)
+ self.timer.setInterval(UI_REFRESH_PERIOD_MS)
+ self.timer.timeout.connect(self.timerCallback)
+
+ self.soloTool = SoloTool()
+ self.midiController = MidiController(self.soloTool)
+
+ self.playlistModel = PlaylistModel(self.soloTool)
+ self.songListView.setModel(self.playlistModel)
+ self.songListView.selectionModel().selectionChanged.connect(self.playlistSelectionChanged)
+ self.songChangePending = None
+ self.songChangeSignal.connect(self.currentSongChanged)
+ self.soloTool.registerCurrentSongCallback(self.songChangeSignal.emit)
+
+ self.abListModel = ABListModel(self.soloTool)
+ self.abListView.setModel(self.abListModel)
+ self.abListView.selectionModel().selectionChanged.connect(self.abListSelectionChanged)
+ self.abLimitsChangePending = None
+ self.abLimitsChangeSignal.connect(self.currentAbLimitsChanged)
+ self.soloTool.registerCurrentAbLimitsCallback(self.abLimitsChangeSignal.emit)
+
+ self.songSlider.setMaximum(POSITION_FACTOR)
+ self.songSlider.sliderPressed.connect(self.songSliderPressed)
+ self.songSlider.sliderReleased.connect(self.songSliderReleased)
+
+ self.aSlider.setMaximum(POSITION_FACTOR)
+ self.aSlider.sliderReleased.connect(self.abSliderReleased)
+ self.bSlider.setMaximum(POSITION_FACTOR)
+ self.bSlider.sliderReleased.connect(self.abSliderReleased)
+
+ self.rateSlider.setRange(int(0.5 * RATE_FACTOR), int(1.2 * RATE_FACTOR))
+ self.rateSlider.setSingleStep(int(0.1 * RATE_FACTOR))
+ self.rateSlider.setValue(int(1.0 * RATE_FACTOR))
+ self.rateSlider.sliderReleased.connect(self.rateSliderReleased)
+
+ self.playButton.pressed.connect(self.soloTool.play)
+ self.pauseButton.pressed.connect(self.soloTool.pause)
+ self.storeAbButton.pressed.connect(self.storeAbLimits)
+ self.setAButton.pressed.connect(self.setA)
+ self.setBButton.pressed.connect(self.setB)
+ self.saveSessionButton.pressed.connect(self.saveSession)
+ self.loadSessionButton.pressed.connect(self.loadSession)
+ self.addSongButton.pressed.connect(self.addSong)
+ self.abRepeatCheckBox.clicked.connect(self.toggleAbRepeat)
+ self.initMidiButton.pressed.connect(self.initMidi)
+
+ self.timer.start()
+
+ if len(sys.argv) > 1:
+ self.loadSession(sys.argv[1])
+
+ self.show()
+
+ def timerCallback(self):
+ position = self.soloTool.getPlaybackPosition() * POSITION_FACTOR
+ self.songSlider.setValue(int(position))
+ self.soloTool.tick()
+
+ def addSong(self):
+ path, _ = QFileDialog.getOpenFileName(self, "Open file", "", "mp3 Audio (*.mp3);FLAC audio (*.flac);All files (*.*)")
+ if path:
+ self.soloTool.addSong(path)
+ self.playlistModel.layoutChanged.emit()
+
+ def storeAbLimits(self):
+ a = self.aSlider.value() / float(POSITION_FACTOR)
+ b = self.bSlider.value() / float(POSITION_FACTOR)
+ self.soloTool.storeAbLimits(a, b)
+ self.abListModel.layoutChanged.emit()
+
+ def setA(self):
+ position = self.songSlider.value()
+ self.aSlider.setValue(position)
+ self.abSliderReleased()
+
+ def setB(self):
+ position = self.songSlider.value()
+ self.bSlider.setValue(position)
+ self.abSliderReleased()
+
+ def toggleAbRepeat(self):
+ enable = self.abRepeatCheckBox.isChecked()
+ self.soloTool.setAbLimitEnable(enable)
+
+ def saveSession(self):
+ path, _ = QFileDialog.getSaveFileName(self, "Open file", "", "session file (*.json)")
+ if path:
+ self.soloTool.saveSession(path)
+
+ def loadSession(self, path=None):
+ if path is None:
+ path, _ = QFileDialog.getOpenFileName(self, "Open file", "", "session file (*.json)")
+ if path is not None:
+ self.soloTool.loadSession(path)
+ self.playlistModel.layoutChanged.emit()
+ self.abListModel.layoutChanged.emit()
+
+ def songSliderPressed(self):
+ self.timer.stop()
+
+ def songSliderReleased(self):
+ position = self.songSlider.value() / float(POSITION_FACTOR)
+ self.soloTool.setPlaybackPosition(position)
+ self.timer.start()
+
+ def clearListViewSelection(self, listView):
+ i = listView.selectionModel().currentIndex()
+ listView.selectionModel().select(i, QItemSelectionModel.Deselect)
+
+ def abSliderReleased(self):
+ a = self.aSlider.value() / float(POSITION_FACTOR)
+ b = self.bSlider.value() / float(POSITION_FACTOR)
+ self.soloTool.setAbLimits(a, b)
+ self.clearListViewSelection(self.abListView)
+
+ def rateSliderReleased(self):
+ rate = self.rateSlider.value() / float(RATE_FACTOR)
+ self.soloTool.setPlaybackRate(rate)
+
+ def playlistSelectionChanged(self, i):
+ if self.songChangePending == CHANGE_INTERNAL:
+ self.songChangePending = None
+ else:
+ assert self.songChangePending is None
+ self.songChangePending = CHANGE_GUI
+ index = i.indexes()[0].row()
+ self.soloTool.setSong(index)
+
+ self.clearListViewSelection(self.abListView)
+ self.abListModel.layoutChanged.emit()
+
+ def currentSongChanged(self, songIndex):
+ if self.songChangePending == CHANGE_GUI:
+ self.songChangePending = None
+ else:
+ assert self.songChangePending is None
+ self.songChangePending = CHANGE_INTERNAL
+ i = self.playlistModel.createIndex(songIndex, 0)
+ self.songListView.selectionModel().select(i, QItemSelectionModel.ClearAndSelect)
+
+ def abListSelectionChanged(self, i):
+ if self.abLimitsChangePending == CHANGE_INTERNAL:
+ print("Ack internal change")
+ self.abLimitsChangePending = None
+ else:
+ assert self.abLimitsChangePending is None
+ if i is not None and not i.isEmpty():
+ print("Processing GUI change")
+ self.abLimitsChangePending = CHANGE_GUI
+ index = i.indexes()[0].row()
+ ab = self.soloTool.getStoredAbLimits()[index]
+ self.soloTool.loadAbLimits(index)
+ self.aSlider.setValue(int(ab[0] * POSITION_FACTOR))
+ self.bSlider.setValue(int(ab[1] * POSITION_FACTOR))
+
+ def currentAbLimitsChanged(self, abIndex):
+ if self.abLimitsChangePending == CHANGE_GUI:
+ print("Ack GUI change")
+ self.abLimitsChangePending = None
+ else:
+ assert self.abLimitsChangePending is None
+ print("Processing internal change")
+ self.abLimitsChangePending = CHANGE_INTERNAL
+ i = self.abListModel.createIndex(abIndex, 0)
+ self.abListView.selectionModel().select(i, QItemSelectionModel.ClearAndSelect)
+ ab = self.soloTool.getStoredAbLimits()[abIndex]
+ self.aSlider.setValue(int(ab[0] * POSITION_FACTOR))
+ self.bSlider.setValue(int(ab[1] * POSITION_FACTOR))
+
+ def initMidi(self):
+ try:
+ self.midiController.connect()
+ except Exception as e:
+ print("Error: could not connect to MIDI controller")
+ print(e)
+
+ def keyPressEvent(self, event):
+ if event.key() == Qt.Key_Super_L:
+ self.soloTool.jumpToA()
+
+def main():
+ app = QApplication([])
+ app.setApplicationName("Solo Tool")
+ app.setStyle("Fusion")
+
+ # Fusion dark palette from https://gist.github.com/QuantumCD/6245215.
+ palette = QPalette()
+ palette.setColor(QPalette.Window, QColor(53, 53, 53))
+ palette.setColor(QPalette.WindowText, Qt.white)
+ palette.setColor(QPalette.Base, QColor(25, 25, 25))
+ palette.setColor(QPalette.AlternateBase, QColor(53, 53, 53))
+ palette.setColor(QPalette.ToolTipBase, Qt.white)
+ palette.setColor(QPalette.ToolTipText, Qt.white)
+ palette.setColor(QPalette.Text, Qt.white)
+ palette.setColor(QPalette.Button, QColor(53, 53, 53))
+ palette.setColor(QPalette.ButtonText, Qt.white)
+ palette.setColor(QPalette.BrightText, Qt.red)
+ palette.setColor(QPalette.Link, QColor(42, 130, 218))
+ palette.setColor(QPalette.Highlight, QColor(42, 130, 218))
+ palette.setColor(QPalette.HighlightedText, Qt.black)
+ app.setPalette(palette)
+ app.setStyleSheet("QToolTip { color: #ffffff; background-color: #2a82da; border: 1px solid white; }")
+
+ window = MainWindow()
+ app.exec_()
+
+if __name__ == '__main__':
+ main()