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.

255 lines
13 KiB

4 years ago
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
"""
11 months ago
Copyright 2022, Nils Hilbricht, Germany ( https://www.hilbricht.net )
4 years ago
This file is part of the Laborejo Software Suite ( https://www.laborejo.org ),
4 years ago
Laborejo2 is free software: you can redistribute it and/or modify
4 years ago
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/>.
"""
4 years ago
import logging; logger = logging.getLogger(__name__); logger.info("import")
4 years ago
#Standard Library Modules
import os.path
#Third Party Modules
from PyQt5 import QtWidgets, QtCore, QtGui
#Template Modules
from template.qtgui.mainwindow import MainWindow as TemplateMainWindow
from template.qtgui.menu import Menu
from template.qtgui.about import About
translate = QtCore.QCoreApplication.translate
4 years ago
#Our modules
import engine.api as api
from engine.midiinput.stepmidiinput import stepMidiInput #singleton instance
4 years ago
from .constantsAndConfigs import constantsAndConfigs
from .menu import MenuActionDatabase
from .submenus import BlockPropertiesEdit
4 years ago
from .scoreview import ScoreView
from .trackEditor import TrackEditor
from .tracklistwidget import TrackListWidget
4 years ago
from .resources import *
from engine.config import METADATA
4 years ago
MAX_QT_SIZE = 2147483647-1
4 years ago
class MainWindow(TemplateMainWindow):
def __init__(self):
"""The order of calls is very important.
The split ploint is calling the super.__init. Some functions need to be called before,
some after.
For example:
The about dialog is created in the template main window init. So we need to set additional
help texts before that init.
"""
#Inject more help texts in the templates About "Did You Know" field.
#About.didYouKnow is a class variable.
#Make the first three words matter!
#Do not start them all with "You can..." or "...that you can", in response to the Did you know? title.
#We use injection into the class and not a parameter because this dialog gets shown by creating an object. We can't give the parameters when this is shown via the mainWindow menu.
About.didYouKnow = [
translate("About", "<p>Most commands work in the appending position (last position in a track) and apply to the item before it.</p><p>Use it to apply dots, sharps and flats on the item you just inserted without moving the cursor back and forth.</p>"),
translate("About", "<p>Learn the keyboard shortcuts! Laborejo is designed to work with the keyboard alone and with midi instruments for full speed.</p>Everytime you grab your mouse you loose concentration, precision and time."),
translate("About", "<p>Spread/shrink the space between notes with Ctrl+Shift+Mousewheel or Ctrl+Shift with Plus and Minus.</p>"),
translate("About", "<p>Click with the left mouse button to set the cursor to that position. Hold Shift to create a selection.</p>"),
translate("About", "<p>Most commands can be applied to single notes and selections equally.</p><p>Use Shift + movement-keys to create selections.</p>"),
translate("About", "<p>Blocks and Tracks can be moved in Block-View Mode [F6]. Use Shift+Middle Mouse Button to move blocks and Alt+Middle to reorder tracks.</p>"),
translate("About", "<p>There are no empty measures/bars.</p><p>Use Multi Measure Rests[R] instead. You need to have a metrical instruction for that [M].</p>"),
translate("About", "<p>Many Music Items like clefs can only be inserted, not edited. They are however such simplistic that delete-and-reinsert is equally time-efficient.</p>"),
translate("About", "<p>All notes should be considered non-transposing. Treat everything as 'in C'.</p><p>That said, there is a semitone transposition in the Track Properties [Ctrl+T].</p>"),
translate("About", "<p>Upbeats/anacrusis can be set per-track in the Track Properties [Ctrl+T].</p>"),
translate("About", "<p>There is no key-rebinding except numpad-shortcuts.</p>"),
translate("About", "<p>Hidden tracks still output sound.</p>"),
translate("About", "<p>Non-audible tracks still output instrument changes and CCs so that they can be switched on again in the middle of playback.</p>"),
] + About.didYouKnow
4 years ago
super().__init__()
#New menu entries and template-menu overrides
self.menu.addMenuEntry("menuDebug", "actionRedrawAllTracks", "Redraw all Tracks")
self.menu.connectMenuEntry("actionSave", api.save)
self.menu.hideSubmenu("menuFile")
self.menu.hideSubmenu("menuGeneric")
api.callbacks.setCursor.append(self.updateStatusBar) #returns a dict. This get's called after loading the file so the status bar is filled on self.show
4 years ago
#Create the Main Widgets in the Stacked Widget
horizontalLayout = QtWidgets.QHBoxLayout(self.ui.scoreViewParentContainer)
self.scoreSplitter = QtWidgets.QSplitter(self.ui.scoreViewParentContainer)
self.scoreSplitter.setOrientation(QtCore.Qt.Horizontal)
self.scoreView = ScoreView(mainWindow=self, parentSplitter=self.scoreSplitter)
#self.ui.mainStackWidget.addWidget(self.scoreView)
self.trackListWidget = TrackListWidget(mainWindow=self, parentSplitter=self.scoreSplitter)
horizontalLayout.addWidget(self.scoreSplitter)
self.ui.mainStackWidget.setCurrentIndex(self.ui.mainStackWidget.indexOf(self.ui.scoreViewParentContainer))
#Set the splitter ratio.
self.scoreView.setMinimumSize(1, 1)
self.trackListWidget.setMinimumSize(1, 1)
self.scoreSplitter.setSizes([8, 1])
#self.scoreSplitter.setSizes([MAX_QT_SIZE, 1])
4 years ago
self.trackEditor = QtWidgets.QScrollArea()
self.trackEditor.setWidgetResizable(True)
self.actualTrackEditor = TrackEditor(self)
self.trackEditor.setWidget(self.actualTrackEditor)
self.ui.actionData_Editor.setChecked(False)
self.ui.mainStackWidget.addWidget(self.trackEditor)
#Bind shortcuts to actions (as init effect)
#TODO: Integrate better into template menu system.
4 years ago
self.menuActionDatabase = MenuActionDatabase(self) #The menu needs to be started before api.startEngine
#Make toolbars unclosable
##self.ui.toolBar.setContextMenuPolicy(QtCore.Qt.PreventContextMenu) #only for right mouse clicks. Keyboard context menu key still works.
##self.ui.leftToolBar.setContextMenuPolicy(QtCore.Qt.PreventContextMenu)
self.setContextMenuPolicy(QtCore.Qt.NoContextMenu) #Make toolbars unclosable by preventing the main window from having context menus.
4 years ago
#The statusbar is intended for tooltips. To make it permanent we add our own widget
4 years ago
self.statusLabel = QtWidgets.QLabel()
self.statusBar().insertPermanentWidget(0, self.statusLabel)
self.scoreView.setFocus() #So the user can start typing from moment 0.
4 years ago
self.start() #Inherited from template main window #This shows the GUI, or not, depends on the NSM gui save setting. We need to call that after the menu, otherwise the about dialog will block and then we get new menu entries, which looks strange.
stepMidiInput.start() #imported directly. Handles everything else internally, we just need to start it after the engine somehow. Which is here.
4 years ago
#Check if to connect the metronome on startup. But only when not running under NSM
if api.isStandaloneMode():
self.ui.actionAutoconnect_Metronome.setEnabled(True)
settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"])
if settings.contains("autoconnectMetronome"):
autoconnectMixer = settings.value("autoconnectMetronome", type=bool)
else:
autoconnectMixer = False
self.ui.actionAutoconnect_Metronome.setChecked(autoconnectMixer)
self.reactToAutoconnectMixerCheckbox(autoconnectMixer) #do the connection
else:
self.ui.actionAutoconnect_Metronome.setEnabled(False)
#Populate the left toolbar. The upper toolbar is created in menu.py
4 years ago
self.ui.leftToolBar.addWidget(LeftToolBarPrevailingDuration(self)) #needs stepmidiinput started
#Now all tracks and items from a loaded backend-file are created. We can setup the initial editMode and viewPort.
4 years ago
self.scoreView.updateMode() #hide CCs at program start and other stuff
self.scoreView.scoreScene.grid.redrawTickGrid() #Init the grid only after everything got loaded and drawn to prevent a gap in the display. #TODO: which might be a bug. but this here works fine.
#There is so much going on in the engine, we never reach a save status on load.
#Here is the crowbar-method.
self.nsmClient.announceSaveStatus(isClean = True)
def zoom(self, scaleFactor:float):
7 months ago
"""Scale factor is absolute. zooming three times to 2.0 will result in 2.0"""
self.scoreView.zoom(scaleFactor)
def stretchXCoordinates(self, factor:float):
7 months ago
"""Cumulative factor. If you repeatedly send factor=2 it will double each time"""
self.scoreView.stretchXCoordinates(factor)
4 years ago
def updateStatusBar(self, exportCursorDict):
"""Every cursor movement updates the statusBar message"""
c = exportCursorDict
try:
i = c["item"]
except:
print (c)
4 years ago
if i:
ly = i.lilypond(carryLilypondRanges = {})
if (not ly) or len(ly) > 13:
4 years ago
ly = ""
else:
ly = " | Lilypond: <b>{}</b>".format(ly.replace("<", "&#60;").replace(">", "&#62;"))
4 years ago
itemMessage = "Item: <b>{}</b> {}".format(i.__class__.__name__, ly)
else:
itemMessage = "" #Appending
positionMessage = " Pitch: <b>{}</b> | Track: <b>{}-{}</b> | Block: <b>{}</b> | Pos: <b>{}</b> | Ticks: <b>{}</b> ".format(c["lilypondPitch"], c["trackIndex"]+1, c["trackName"], c["blockName"], c["position"], c["tickindex"])
4 years ago
message = "{} | {}".format(itemMessage, positionMessage)
#self.statusBar().showMessage(message) #overriden by tool tips, even empty ones
self.statusLabel.setText(message)
def toggleMainView(self):
"""Switch between the Track Editor and Score/Block Editor.
This can happen through the menuaction. There is another way through mouseReleaseEvent
in scoreScene, but this triggers the menu action as well.
Without menu action we don't change the checkbox."""
7 months ago
4 years ago
if self.ui.actionData_Editor.isChecked():
self.ui.mainStackWidget.setCurrentIndex(self.ui.mainStackWidget.indexOf(self.trackEditor))
self.scoreView.setEnabled(False) #disables shortcut like cursor movement, but not all of them.
self.menuActionDatabase.writeProtection(True)
self.trackEditor.setEnabled(True)
else:
self.ui.mainStackWidget.setCurrentIndex(self.ui.mainStackWidget.indexOf(self.ui.scoreViewParentContainer))
4 years ago
self.scoreView.setEnabled(True)
self.menuActionDatabase.writeProtection(False)
self.scoreView.updateMode()
self.trackEditor.setEnabled(False)
def reactToAutoconnectMixerCheckbox(self, state:bool):
"""Triggered by the user and programatically during startup in __init__"""
assert api.isStandaloneMode()
settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"])
settings.setValue("autoconnectMetronome", state)
if state:
api.connectMetronomeToSystemPorts()
def currentBlockProperties(self):
"""For menu action self.mainWindow.ui.actionBlock_Properties"""
BlockPropertiesEdit(self, staticExportItem = api.currentBlockExport())
4 years ago
class LeftToolBarPrevailingDuration(QtWidgets.QLabel):
def __init__(self, mainWindow):
super().__init__(self.makeText(api.D4))
self.mainWindow = mainWindow
self.setFont(constantsAndConfigs.musicFont) #TODO replace with svg
4 years ago
api.callbacks.prevailingBaseDurationChanged.append(self.changed)
def makeText(self, baseDuration):
if not stepMidiInput.midiInIsActive:
4 years ago
return ""
labelText = "<font size=6>"
for i in (api.D1, api.D2, api.D4, api.D8, api.D16): #,api.DB, api.DL):
if i == baseDuration:
labelText += "<font color='cyan'><b>"
4 years ago
labelText += constantsAndConfigs.realNoteDisplay[i]
if i == baseDuration:
labelText += "</b></font>"
4 years ago
labelText += "<br>"
labelText += "</font>"
return labelText
def changed(self, newBaseDuration):
self.setText(self.makeText(newBaseDuration))