You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

335 lines
14 KiB

#! /usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Copyright 2020, Nils Hilbricht, Germany ( https://www.hilbricht.net )
This file is part of the Laborejo Software Suite ( https://www.laborejo.org ),
This application 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 3 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, see <http://www.gnu.org/licenses/>.
"""
import logging; logger = logging.getLogger(__name__); logger.info("import")
#Standard Library
import os
import os.path
from sys import argv as sysargv
from sys import exit as sysexit
#Third Party
from PyQt5 import QtCore, QtGui, QtWidgets
logger.info(f"PyQt Version: {QtCore.PYQT_VERSION_STR}")
#from PyQt5 import QtOpenGL
#Template Modules
from .nsmclient import NSMClient
from .usermanual import UserManual
from .debugScript import DebugScriptRunner
from .menu import Menu
from .resources import *
from .about import About
from .helper import setPaletteAndFont
from .eventloop import EventLoop
from template.start import PATHS, qtApp
#Client modules
from engine.config import * #imports METADATA
import engine.api as api #This loads the engine and starts a session.
from qtgui.designer.mainwindow import Ui_MainWindow #The MainWindow designer file is loaded from the CLIENT side
from qtgui.resources import *
from qtgui.constantsAndConfigs import constantsAndConfigs
api.session.eventLoop = EventLoop()
#QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_DontUseNativeMenuBar) #Force a real menu bar. Qt on wayland will not display it otherwise.
#Setup the translator before classes are set up. Otherwise we can't use non-template translation.
#to test use LANGUAGE=de_DE.UTF-8 . not LANG=
language = QtCore.QLocale().languageToString(QtCore.QLocale().language())
logger.info("{}: Language set to {}".format(METADATA["name"], language))
if language in METADATA["supportedLanguages"]:
templateTranslator = QtCore.QTranslator()
templateTranslator.load(METADATA["supportedLanguages"][language], ":/template/translations/") #colon to make it a resource URL
qtApp.installTranslator(templateTranslator)
otherTranslator = QtCore.QTranslator()
otherTranslator.load(METADATA["supportedLanguages"][language], ":translations") #colon to make it a resource URL
qtApp.installTranslator(otherTranslator)
else:
"""silently fall back to English by doing nothing"""
class MainWindow(QtWidgets.QMainWindow):
"""Before the mainwindow class is even parsed all the engine imports are done.
As side effects they set up our session, callbacks and api. They are now waiting
for the start signal which will be send by NSM. Session Management simulates user actions,
so their place is in the (G)UI. Therefore the MainWindows role is to set up the nsm client."""
def __init__(self):
super().__init__()
self.qtApp = qtApp
self.qtApp.setWindowIcon(QtGui.QIcon(":icon.png")) #non-template part of the program
QtGui.QIcon.setThemeName("hicolor") #audio applications can be found here. We have no need for other icons.
logger.info("Init MainWindow")
#Callbacks. Must be registered before startEngine.
api.callbacks.message.append(self.callback_message)
#NSM Client
self.nsmClient = NSMClient(prettyName = METADATA["name"], #will raise an error and exit if this is not run from NSM
supportsSaveStatus = True,
saveCallback = api.session.nsm_saveCallback,
openOrNewCallback = api.session.nsm_openOrNewCallback,
exitProgramCallback = self._nsmQuit,
hideGUICallback = self.hideGUI,
showGUICallback = self.showGUI,
loggingLevel = logging.getLogger().level,
)
#Set up the user interface from Designer and other widgets
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.fPalBlue = setPaletteAndFont(self.qtApp)
self.userManual = UserManual(mainWindow=self) #starts hidden. menu shows, closeEvent hides.
self.xFactor = 1 #keep track of the x stretch factor.
self.setWindowTitle(self.nsmClient.ourClientNameUnderNSM)
self.qtApp.setApplicationName(self.nsmClient.ourClientNameUnderNSM)
self.qtApp.setApplicationDisplayName(self.nsmClient.ourClientNameUnderNSM)
self.qtApp.setOrganizationName("Laborejo Software Suite")
self.qtApp.setOrganizationDomain("laborejo.org")
self.qtApp.setApplicationVersion(METADATA["version"])
self.setAcceptDrops(True)
self.debugScriptRunner = DebugScriptRunner(apilocals=locals()) #needs to have trueInit called after the session and nsm was set up. Which happens in startEngine.
self.debugScriptRunner.trueInit(nsmClient=self.nsmClient)
#Show the About Dialog the first time the program starts up.
#This is the initial state user/system wide and not a saved in NSM nor bound to the NSM ID (like window position)
settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"])
if not settings.contains("showAboutDialog"):
settings.setValue("showAboutDialog", METADATA["showAboutDialogFirstStart"])
self.about = About(mainWindow=self) #This does not show, it only creates. Showing is decided in self.start
self.ui.menubar.setNativeMenuBar(False) #Force a real menu bar. Qt on wayland will not display it otherwise.
self.menu = Menu(mainWindow=self) #needs the about dialog, save file and the api.session ready.
self.initiGuiSharedDataToSave()
def start(self):
api.session.eventLoop.start() #The event loop must be started after the qt app
api.session.eventLoop.fastConnect(self.nsmClient.reactToMessage)
api.startEngine(self.nsmClient) #Load the file, start the eventLoop. Triggers all the callbacks that makes us draw.
if api.session.guiWasSavedAsNSMVisible:
self.showGUI()
settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"])
if settings.contains("showAboutDialog") and settings.value("showAboutDialog", type=bool):
QtCore.QTimer.singleShot(100, self.about.show) #Qt Event loop is not ready at that point. We need to wait for the paint event. This is not to stall for time: Using the event loop guarantees that it exists
elif not self.nsmClient.sessionName == "NOT-A-SESSION":
#TODO: this is a hack until we figure out how to cleanly handle hide vs quite from outside the application
self.hideGUI()
#def event(self, event):
# print (event.type())
# return super().event(event)
def callback_message(self, title, text):
QtWidgets.QMessageBox.warning(self, title ,text)
def dragEnterEvent(self, event):
"""Needs self.setAcceptDrops(True) in init"""
if event.mimeData().hasUrls():
event.accept()
else:
event.ignore()
def dropEvent(self, event):
"""Needs self.setAcceptDrops(True) in init.
Having that function in the mainWindow will not make drops available for subwindows
like About or UserManual. """
for url in event.mimeData().urls():
filePath = url.toLocalFile()
#Decide here if you want only files, only directories, both etc.
if os.path.isfile(filePath) and filePath.lower().endswith(".sf2"):
#linkedPath = self.nsmClient.importResource(filePath)
print ("received drop")
def storeWindowSettings(self):
"""Window state is not saved in the real save file. That would lead to portability problems
between computers, like different screens and resolutions.
For convenience that means we just use the damned qt settings and save wherever qt wants.
We don't use the NSM id, session share their placement.
bottom line: get a tiling window manager.
"""
settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"])
settings.setValue("geometry", self.saveGeometry())
settings.setValue("windowState", self.saveState())
def restoreWindowSettings(self):
"""opposite of storeWindowSettings. Read there."""
settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"])
if settings.contains("geometry") and settings.contains("windowState"):
self.restoreGeometry(settings.value("geometry"))
self.restoreState(settings.value("windowState"))
def initiGuiSharedDataToSave(self):
"""Called by init"""
if not "last_export_dir" in api.session.guiSharedDataToSave:
api.session.guiSharedDataToSave["last_export_dir"] = os.path.expanduser("~")
if "grid_opacity" in api.session.guiSharedDataToSave:
constantsAndConfigs.gridOpacity = float(api.session.guiSharedDataToSave["grid_opacity"])
if "grid_rhythm" in api.session.guiSharedDataToSave:
#setting this is enough. When the grid gets created it fetches the constantsAndConfigs value.
#Set only in submenus.GridRhytmEdit
constantsAndConfigs.gridRhythm = int(api.session.guiSharedDataToSave["grid_rhythm"])
#Stretch
if "ticks_to_pixel_ratio" in api.session.guiSharedDataToSave:
#setting this is enough. Drawing on startup uses the constantsAndConfigs value.
#Set only in ScoreView._stretchXCoordinates
constantsAndConfigs.ticksToPixelRatio = float(api.session.guiSharedDataToSave["ticks_to_pixel_ratio"])
if "zoom_factor" in api.session.guiSharedDataToSave:
#setting this is enough. Drawing on startup uses the constantsAndConfigs value.
#Set only in ScoreView._zoom
constantsAndConfigs.zoomFactor = float(api.session.guiSharedDataToSave["zoom_factor"])
#Zoom and Stretch
def wheelEvent(self, ev):
modifiers = QtWidgets.QApplication.keyboardModifiers()
if modifiers == QtCore.Qt.ControlModifier:
ev.accept()
if ev.angleDelta().y() > 0:
self.zoomIn()
else:
self.zoomOut()
elif modifiers == QtCore.Qt.ControlModifier | QtCore.Qt.ShiftModifier:
ev.accept()
if ev.angleDelta().y() > 0:
self.widen()
else:
self.shrinken()
else:
super().wheelEvent(ev) #send to the items
def _zoom(self):
api.session.guiSharedDataToSave["zoom_factor"] = constantsAndConfigs.zoomFactor
#Send to client mainWindow
self.zoom(constantsAndConfigs.zoomFactor)
def zoom(self, scaleFactor:float):
raise NotImplementedError("Reimplement this in your program. Can do nothing, if you want. See template/qtgui/mainwindow.py for example implementation")
#self.scoreView.resetTransform()
#self.scoreView.scale(scaleFactor, scaleFactor)
def stretchXCoordinates(self, factor):
raise NotImplementedError("Reimplement this in your program. Can do nothing, if you want. See template/qtgui/mainwindow.py for example implementation")
#self.scoreView.stretchXCoordinates(factor)
#self.scoreView.centerOnCursor()
def zoomIn(self):
constantsAndConfigs.zoomFactor = round(constantsAndConfigs.zoomFactor + 0.25, 2)
if constantsAndConfigs.zoomFactor > 2.5:
constantsAndConfigs.zoomFactor = 2.5
self._zoom()
return True
def zoomOut(self):
constantsAndConfigs.zoomFactor = round(constantsAndConfigs.zoomFactor - 0.25, 2)
if constantsAndConfigs.zoomFactor < constantsAndConfigs.maximumZoomOut:
constantsAndConfigs.zoomFactor = constantsAndConfigs.maximumZoomOut
self._zoom()
return True
def zoomNull(self):
constantsAndConfigs.zoomFactor = 1
self._zoom()
def _stretchXCoordinates(self, factor):
"""Reposition the items on the X axis.
Call goes through all parents/children, starting from here.
The parent sets the X coordinates of its children.
Then the parent calls the childs _stretchXCoordinates() method if the child has children
itself. For example a rectangleItem has a position which is set by the parent. But the
rectangleItem has a right border which needs to be adjusted as well. This right border is
treated as child of the rectItem, handled by rectItem._stretchXCoordinates(factor).
"""
if self.xFactor * factor < 0.015:
return
self.xFactor *= factor
constantsAndConfigs.ticksToPixelRatio /= factor
api.session.guiSharedDataToSave["ticks_to_pixel_ratio"] = constantsAndConfigs.ticksToPixelRatio
self.stretchXCoordinates(factor)
return True
def widen(self):
#self._stretchXCoordinates(1*1.2) #leads to rounding errors
self._stretchXCoordinates(2)
def shrinken(self):
#self._stretchXCoordinates(1/1.2) #leads to rounding errors
self._stretchXCoordinates(0.5)
#Close and exit
def _nsmQuit(self, ourPath, sessionName, ourClientNameUnderNSM):
logger.info("Qt main window received NSM exit callback. Calling pythons system exit. ")
self.storeWindowSettings()
#api.stopEngine() #will be called trough sessions atexit
#self.qtApp.quit() #does not work. This will fail and pynsmclient2 will send SIGKILL
sysexit() #works, NSM cleanly detects a quit. Triggers the session atexit condition
logger.error("Code executed after sysexit. This message should not have been visible.")
#Code here never gets executed.
def closeEvent(self, event):
"""This is the manual close event, not the NSM Message.
Ignore. We use it to send the GUI into hiding."""
event.ignore()
self.hideGUI()
def hideGUI(self):
self.storeWindowSettings()
self.hide()
self.nsmClient.announceGuiVisibility(False)
def showGUI(self):
self.restoreWindowSettings()
self.show()
self.nsmClient.announceGuiVisibility(True)