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.
252 lines
10 KiB
252 lines
10 KiB
#! /usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Copyright 2018, Nils Hilbricht, Germany ( https://www.hilbricht.net )
|
|
|
|
This file is part of the Laborejo Software Suite ( https://www.laborejo.org ),
|
|
more specifically its template base application.
|
|
|
|
The Template Base 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/>.
|
|
"""
|
|
|
|
|
|
|
|
#Third Party
|
|
from PyQt5 import QtCore, QtGui, QtWidgets, QtOpenGL
|
|
#from PyQt5 import QtOpenGL
|
|
|
|
#Template Modules
|
|
from template.helper import onlyOne
|
|
|
|
#Our Modules
|
|
|
|
from .constantsAndConfigs import constantsAndConfigs
|
|
from .structures import GuiScore
|
|
import engine.api as api
|
|
|
|
class ScoreView(QtWidgets.QGraphicsView):
|
|
def __init__(self, mainWindow):
|
|
super().__init__()
|
|
self.mainWindow = mainWindow
|
|
|
|
#OpenGL has a huge positive impact on performance
|
|
if True: #for testing
|
|
viewport = QtOpenGL.QGLWidget(QtOpenGL.QGLFormat(QtOpenGL.QGL.SampleBuffers))
|
|
viewport.format().setSwapInterval(0) #disable VSync.
|
|
viewport.setAutoFillBackground(False)
|
|
|
|
viewport = QtWidgets.QOpenGLWidget()
|
|
viewportFormat = QtGui.QSurfaceFormat()
|
|
viewportFormat.setSwapInterval(0) #disable VSync
|
|
#viewportFormat.setSamples(2**8) #By default, the highest number of samples available is used.
|
|
viewportFormat.setDefaultFormat(viewportFormat)
|
|
viewport.setFormat(viewportFormat)
|
|
self.setViewport(viewport)
|
|
|
|
|
|
self.setAlignment(QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop)
|
|
#self.setDragMode(QtWidgets.QGraphicsView.RubberBandDrag)
|
|
self.setDragMode(QtWidgets.QGraphicsView.NoDrag)
|
|
|
|
self.scoreScene = GuiScore(self)
|
|
self.setScene(self.scoreScene)
|
|
|
|
api.callbacks.setCursor.append(self.centerOnCursor) #returns a dict
|
|
api.callbacks.updateBlockTrack.append(self.updateMode) # We need this after every update because the track is redrawn after each update and we don't know what to show
|
|
|
|
self.xFactor = 1 #keep track of the x stretch factor.
|
|
|
|
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)
|
|
|
|
self._zoom() #no parameters, uses config values
|
|
|
|
def resizeEvent(self, event):
|
|
self.scoreScene.grid.reactToresizeEventOrZoom()
|
|
super().resizeEvent(event)
|
|
|
|
def changeGridRhythm(self):
|
|
GridRhytmEdit(mainWindow=self.mainWindow) #handles everything.
|
|
|
|
def centerOnCursor(self, cursorExportObject):
|
|
if (not constantsAndConfigs.followPlayhead) or not api.playbackStatus():
|
|
self.centerOn(self.scoreScene.cursor.scenePos())
|
|
#discard cursorExportObject.
|
|
|
|
def wheelEvent(self, ev):
|
|
modifiers = QtWidgets.QApplication.keyboardModifiers()
|
|
if modifiers == QtCore.Qt.ControlModifier:
|
|
if ev.angleDelta().y() > 0:
|
|
self.widen()
|
|
else:
|
|
self.shrinken()
|
|
ev.accept()
|
|
|
|
elif modifiers == QtCore.Qt.ControlModifier | QtCore.Qt.ShiftModifier:
|
|
if ev.angleDelta().y() > 0:
|
|
self.zoomIn()
|
|
else:
|
|
self.zoomOut()
|
|
ev.accept()
|
|
else:
|
|
super().wheelEvent(ev) #send to the items
|
|
|
|
def _zoom(self):
|
|
api.session.guiSharedDataToSave["zoom_factor"] = constantsAndConfigs.zoomFactor
|
|
self.resetTransform()
|
|
self.scoreScene.grid.setVisible(constantsAndConfigs.zoomFactor >= 1)
|
|
self.scale(constantsAndConfigs.zoomFactor, constantsAndConfigs.zoomFactor)
|
|
self.centerOnCursor(None)
|
|
|
|
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).
|
|
"""
|
|
self.xFactor *= factor
|
|
constantsAndConfigs.ticksToPixelRatio /= factor
|
|
api.session.guiSharedDataToSave["ticks_to_pixel_ratio"] = constantsAndConfigs.ticksToPixelRatio
|
|
self.scoreScene.stretchXCoordinates(factor)
|
|
self.centerOnCursor(None)
|
|
return True
|
|
|
|
def widen(self):
|
|
self._stretchXCoordinates(1*1.2) #2 is also good
|
|
|
|
def shrinken(self):
|
|
self._stretchXCoordinates(1/1.2) #0.5 is also good
|
|
|
|
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 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 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 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.
|
|
"""
|
|
|
|
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
|
|
|
|
if self.mainWindow.ui.actionCC_Mode.isChecked():
|
|
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():
|
|
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():
|
|
self.mainWindow.menuActionDatabase.noteEditMode()
|
|
self.mainWindow.menuActionDatabase.writeProtection(True)
|
|
self.scoreScene.updateMode("block")
|
|
self.mainWindow.menuActionDatabase.loadToolbarContext("block")
|
|
else:
|
|
raise ValueError("Edit Mode unknown")
|
|
|