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.
232 lines
10 KiB
232 lines
10 KiB
#! /usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Copyright 2022, Nils Hilbricht, Germany ( https://www.hilbricht.net )
|
|
|
|
This file is part of the Laborejo Software Suite ( https://www.laborejo.org ),
|
|
|
|
Laborejo2 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")
|
|
|
|
|
|
#Third Party
|
|
from PyQt5 import QtCore, QtGui, QtWidgets
|
|
|
|
#Template Modules
|
|
from template.helper import onlyOne
|
|
|
|
#Our Modules
|
|
from .constantsAndConfigs import constantsAndConfigs
|
|
from .scorescene import GuiScore
|
|
from .submenus import GridRhytmEdit
|
|
import engine.api as api
|
|
|
|
class ScoreView(QtWidgets.QGraphicsView):
|
|
def __init__(self, mainWindow):
|
|
super().__init__()
|
|
self.mainWindow = mainWindow
|
|
|
|
self.scoreScene = GuiScore(parentView=self)
|
|
self.setScene(self.scoreScene)
|
|
|
|
opengl = True
|
|
if opengl:
|
|
#Scrolling without openGl is sluggish. This is a sure improvement. -2022.
|
|
from PyQt5 import QtOpenGL
|
|
viewport = QtWidgets.QOpenGLWidget()
|
|
#viewport = QtOpenGL.QGLWidget(QtOpenGL.QGLFormat(QtOpenGL.QGL.SampleBuffers))
|
|
#These special parameters should not matter. Run with the default.
|
|
viewportFormat = QtGui.QSurfaceFormat()
|
|
viewportFormat.setSwapInterval(0) #disable VSync
|
|
viewportFormat.setDefaultFormat(viewportFormat)
|
|
viewport.setFormat(viewportFormat)
|
|
self.setViewport(viewport)
|
|
|
|
self.viewport().setAutoFillBackground(False)
|
|
|
|
self.setAlignment(QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop)
|
|
|
|
#self.setDragMode(QtWidgets.QGraphicsView.RubberBandDrag)
|
|
#self.setDragMode(QtWidgets.QGraphicsView.ScrollHandDrag)
|
|
self.setDragMode(QtWidgets.QGraphicsView.NoDrag)
|
|
|
|
|
|
api.callbacks.setCursor.append(self.centerOnCursor) #returns a dict
|
|
self._lastSavedMode = None #CC, Blocks, Notes. for self.updateMode
|
|
api.callbacks.updateBlockTrack.append(self.updateModeSingleTrackRedraw) # We need this after every update because the track is redrawn after each update and we don't know what mode-components to show
|
|
|
|
style = """
|
|
QScrollBar:horizontal {
|
|
border: 1px solid black;
|
|
}
|
|
|
|
QScrollBar::handle:horizontal {
|
|
background: #00b2b2;
|
|
}
|
|
|
|
QScrollBar:vertical {
|
|
border: 1px solid black;
|
|
}
|
|
|
|
QScrollBar::handle:vertical {
|
|
background: #00b2b2;
|
|
}
|
|
"""
|
|
self.setStyleSheet(style)
|
|
|
|
|
|
|
|
def wheelEvent(self, event):
|
|
if QtWidgets.QApplication.keyboardModifiers() in (QtCore.Qt.ControlModifier, QtCore.Qt.ControlModifier|QtCore.Qt.ShiftModifier): #a workaround for a qt bug. see score.wheelEvent docstring.
|
|
event.ignore() #do not send to scene, but tell the mainWindow to use it.
|
|
else:
|
|
super().wheelEvent(event) #send to scene
|
|
|
|
def centerOnCursor(self, cursorExportObject):
|
|
if (not constantsAndConfigs.followPlayhead) or not api.playbackStatus():
|
|
self.centerOn(self.scoreScene.cursor.scenePos())
|
|
#discard cursorExportObject.
|
|
|
|
def toggleFollowPlayhead(self):
|
|
constantsAndConfigs.followPlayhead = not constantsAndConfigs.followPlayhead
|
|
self.mainWindow.ui.actionFollow_Playhead.setChecked(constantsAndConfigs.followPlayhead)
|
|
#we register a callback in self init that checks constantsAndConfigs.followPlayhead
|
|
|
|
def stretchXCoordinates(self, factor):
|
|
"""Cumulative factor, multiplication"""
|
|
self.scoreScene.stretchXCoordinates(factor)
|
|
self.centerOnCursor(None)
|
|
|
|
def zoom(self, scaleFactor:float):
|
|
"""Scale factor is absolute"""
|
|
self.resetTransform()
|
|
self.scale(scaleFactor, scaleFactor)
|
|
|
|
def toggleNoteheadsRectangles(self):
|
|
"""Each notehead/rectangle toggles its own state.
|
|
That means each GuiChord gets toggled individually.
|
|
Both versions, notehead and rectangle, exist all the time, so
|
|
nothing gets recreated, just visibility toggled.
|
|
|
|
Rectangles/Noteheads are their own realm and do not conflict
|
|
with CC View toggle or other view modes."""
|
|
|
|
constantsAndConfigs.noteHeadMode = not constantsAndConfigs.noteHeadMode
|
|
self.mainWindow.ui.actionToggle_Notehead_Rectangles.setChecked(not constantsAndConfigs.noteHeadMode)
|
|
self.scoreScene.toggleNoteheadsRectangles()
|
|
|
|
def _switchToRectanglesForFunction(self, function):
|
|
"""some actions like actionVelocityMore, actionVelocityLess,
|
|
actionDurationModMore, actionDurationModLess,
|
|
actionReset_Velocity_Duration_Mod can be called even if
|
|
not in rectangle mode. We switch into rectangle mode for them
|
|
so the user can see the changes."""
|
|
if not self.mainWindow.ui.actionToggle_Notehead_Rectangles.isChecked():
|
|
self.toggleNoteheadsRectangles()
|
|
function() #this is most likely an api function
|
|
|
|
def resizeEvent(self, event):
|
|
"""Triggers at least when the window is resized"""
|
|
self.scoreScene.grid.reactToresizeEventOrZoom()
|
|
super().resizeEvent(event)
|
|
|
|
def changeGridRhythm(self):
|
|
GridRhytmEdit(mainWindow=self.mainWindow) #handles everything.
|
|
|
|
def mode(self):
|
|
"""Return the current edit mode as string.
|
|
Mostly needed for structures blockAt and other
|
|
functions that need to find the target of a mouse click."""
|
|
if self.mainWindow.ui.actionCC_Mode.isChecked():
|
|
return "cc"
|
|
elif self.mainWindow.ui.actionNotation_Mode.isChecked():
|
|
return "notation"
|
|
elif self.mainWindow.ui.actionBlock_Mode.isChecked():
|
|
return "block"
|
|
else:
|
|
raise ValueError("Edit Mode unknown")
|
|
|
|
def updateModeSingleTrackRedraw(self, trackId:int, trackExport:tuple):
|
|
"""trackExport is a tuple of block export dicts"""
|
|
self.scoreScene.updateModeSingleTrackRedraw(nameAsString=self.mode(), trackId=trackId, trackExport=trackExport)
|
|
|
|
|
|
def updateMode(self, *args):
|
|
"""Switch through different views for editing:
|
|
notes and item edit
|
|
CC curves
|
|
note-blocks only (without items)
|
|
|
|
Which mode is active depends entirely on the state of qt checkboxes in the menu.
|
|
The menu-actions call this function. We make sure and double sure that there is never
|
|
ambiguity in the menu or in the program mode itself.
|
|
|
|
If you want to check for the mode in any place of the program use self.mode().
|
|
It is just above this function.
|
|
|
|
Therefore: Every mode switch is only allowed through this function.
|
|
There is no direct setting of the mode. You should call the menu
|
|
action directly if you want to programatically change the mode.
|
|
|
|
Different functions and callbacks call this as an update, and they just send their arguments,
|
|
which have different types and formats. We don't care since our only information is the menu
|
|
state and the variables we set ourselves.
|
|
|
|
This is rarely called without arguments, manually, on program start just to
|
|
filter out the unused part.
|
|
|
|
"""
|
|
|
|
assert onlyOne((self.mainWindow.ui.actionCC_Mode.isChecked(), self.mainWindow.ui.actionNotation_Mode.isChecked(), self.mainWindow.ui.actionBlock_Mode.isChecked()))
|
|
|
|
if self.mainWindow.ui.actionData_Editor.isChecked():
|
|
#no modechange in the track editor
|
|
return
|
|
|
|
calledByMenu = args==(True,) # and not by track update or manually on e.g. program start to only show the right portions of the scene
|
|
|
|
|
|
if self.mainWindow.ui.actionCC_Mode.isChecked():
|
|
#if calledByMenu and self._lastSavedMode == "block":
|
|
# self.stretchXCoordinates(4.0) #return from half sized mode. If we do not come from block mode this is not needed. CC and Note mode have the same scaling
|
|
|
|
self.mainWindow.menuActionDatabase.ccEditMode()
|
|
self.mainWindow.menuActionDatabase.writeProtection(True)
|
|
self.scoreScene.updateMode("cc")
|
|
self.mainWindow.menuActionDatabase.loadToolbarContext("cc")
|
|
|
|
elif self.mainWindow.ui.actionNotation_Mode.isChecked():
|
|
#if calledByMenu and self._lastSavedMode == "block":
|
|
# self.stretchXCoordinates(4.0) #return from half sized mode. If we do not come from block mode this is not needed. CC and Note mode have the same scaling
|
|
|
|
self.mainWindow.menuActionDatabase.noteEditMode()
|
|
self.mainWindow.menuActionDatabase.writeProtection(False)
|
|
self.scoreScene.updateMode("notation")
|
|
self.mainWindow.menuActionDatabase.loadToolbarContext("notation")
|
|
|
|
elif self.mainWindow.ui.actionBlock_Mode.isChecked():
|
|
assert calledByMenu, "We currently assume that only the program start calls this function not via the menu, and that chooses notation mode"
|
|
#if not self._lastSavedMode == "block": #check that we don't go from block mode to block mode again, since XScaling is cumulative
|
|
# self.stretchXCoordinates(0.25) #go into small scaling mode. 0.25 is hardcoded and the same as scene.updateModeSingleTrackRedraw
|
|
self.mainWindow.menuActionDatabase.noteEditMode()
|
|
self.mainWindow.menuActionDatabase.writeProtection(True)
|
|
self.scoreScene.updateMode("block")
|
|
self.mainWindow.menuActionDatabase.loadToolbarContext("block")
|
|
|
|
else:
|
|
raise ValueError("Edit Mode unknown")
|
|
|
|
self._lastSavedMode = self.mode()
|
|
|