Browse Source

adapt to template

master
Nils 3 years ago
parent
commit
84800debab
  1. 140
      qtgui/constantsAndConfigs.py
  2. 5
      qtgui/designer/mainwindow.py
  3. 32
      qtgui/mainwindow.py
  4. 140
      qtgui/scoreview.py
  5. 220
      qtgui/structures.py
  6. 140
      qtgui/submenus.py

140
qtgui/constantsAndConfigs.py

@ -22,11 +22,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging; logging.info("import {}".format(__file__))
from template.qtgui.constantsAndConfigs import ConstantsAndConfigs as TemplateConstantsAndConfigs
import engine.api as api
from PyQt5 import QtGui
class ConstantsAndConfigs(object):
class ConstantsAndConfigs(TemplateConstantsAndConfigs):
def __init__(self):
super().__init__()
self.ticksToPixelRatio = 128*3*5 #bigger value means less space between notes #api.D128 and 128*3*5 is also good. This gets changed during runtime as "Stretch Factor". Also: the QSettings save and reload this on a per-file basis.
#a quarter note has 53760 ticks. that is 53760 / 1920 = 28 pixels.
self.stafflineGap = 7 #it is 7 because it is an odd number, which has a middle. 123 4 567 which translates 1:1 to pixels.
@ -35,144 +39,12 @@ class ConstantsAndConfigs(object):
self.negativeMagicPixel = -2
self.trackHeight = self.stafflineGap * 10
self.trackHeight = self.stafflineGap * 12
self.gridRhythm = api.D4 #Default value is quarter. The QSettings save and reload this on a per-file basis.
self.gridOpacity = 0.1 #this is only the default value. Once changed a QSetting will be used instead.
self.noteHeadMode = True #False means rectangle noteheads
self.followPlayhead = True #camera follows the playhead during playback.
self.duringPlayback = False #set by a callback.
self.velocityToPixelRatio = 2 #2.0 is the default. 128 pixels maximum.
self.noteHeadMode = True #False means rectangle noteheads
self.availableEditModes = ("notation", "cc", "block")
self.ccViewValue = 0 #0-127. Only has effect if ccView is True
self.ccViewCallbacks = [] #list of functions. For example the main Window CC spin box and the block Editor CC spin box.
self._snapToGrid = True # For CC point and Tempo point placements
self.snapToGridCallbacks = [] # see ccViewCallbacks
self.zoomFactor = 1 # #Initial Zoom Level. Also hardcoded into scoreView.zoomNull
self.maximumZoomOut = 0.25
#fonts which are used in the QGraphicsScenes. They are special because they don't scale with the DPI.
self.fontDB = QtGui.QFontDatabase()
#fid = self.fontDB.addApplicationFont("gui/resources/freesans.ttf")
#if fid == -1:
# raise ValueError("Freesans.ttf not loaded ")
fid = self.fontDB.addApplicationFont(":euterpe.ttf")
if fid == -1:
raise ValueError("euterpe.ttf not loaded. Make sure resources were generated")
#For easier programWide access move these to constantsAndConfigs
#constantsAndConfigs.theFont = self.fontDB.font("FreeSans", "", 13)
#constantsAndConfigs.theFont.setPixelSize(13) #It is very important to set the pixel size before setting the font to the text
self.musicFont = self.fontDB.font("Euterpe", "", 13)
self.musicFont.setPixelSize(13) #It is very important to set the pixel size before setting the font to the text
self.dynamics = ["f", "ff", "p", "pp", "mf", "mp", "tacet"]
self.prettyRhythms = [ #list because we need item order
(api.DL, "0.25 Longa"),
(api.DB, "0.5 Brevis"),
(api.D1, "1 Whole"),
(api.D2, "2 Half"),
(api.D4, "4 Quarter"),
(api.D8, "8 Eigth"),
(api.D16, "16 Sixteenth"),
(api.D32, "32 Thirthy-Second"),
(api.D64, "64 Sixty-Fourth"),
(api.D128, "128 Hundred Twenty-Eighth "),
(api.D256, "256 Two-hundred Fifty-Sixth"),
]
self.prettyRhythmsStrings = [v for k,v in self.prettyRhythms]
self.prettyRhythmsValues = [k for k,v in self.prettyRhythms]
self.prettyExtendedRhythms = [ #list because we need item order
(api.DL, "0.25 Longa"),
(api.DB, "0.5 Brevis"),
(api.D1, "1 Whole"),
(api.D2, "2 Half "),
(api.D4, "4 Quarter"),
(api.D8, "8 Eigth"),
(api.D16, "16 Sixteenth"),
(api.D32, "32 Thirthy-Second"),
(api.D64, "64 Sixty-Fourth"),
(api.D128, "128 Hundred Twenty-Eighth"),
(1.5*api.DL, "Dotted 0.25 Longa"),
(1.5*api.DB, "Dotted 0.5 Brevis"),
(1.5*api.D1, "Dotted 1 Whole"),
(1.5*api.D2, "Dotted 2 Half"),
(1.5*api.D4, "Dotted 4 Quarter"),
(1.5*api.D8, "Dotted 8 Eigth "),
(1.5*api.D16, "Dotted 16 Sixteenth"),
(1.5*api.D32, "Dotted 32 Thirthy-Second"),
(1.5*api.D64, "Dotted 64 Sixty-Fourth "),
(1.5*api.D128, "Dotted 128 Hundred Twenty-Eighth "),
]
self.prettyExtendedRhythmsStrings = [v for k,v in self.prettyExtendedRhythms]
self.prettyExtendedRhythmsValues = [k for k,v in self.prettyExtendedRhythms]
#use with constantsAndConfigs.musicFont
self.realNoteDisplay = {
api.DB : "𝅜",
api.D1 : "𝅝",
api.D2 : "𝅗𝅥",
api.D4 : "𝅘𝅥",
api.D8 : "𝅘𝅥𝅮",
api.D16 : "𝅘𝅥𝅯",
api.D32 : "𝅘𝅥𝅰",
api.D64 : "𝅘𝅥𝅱",
api.D128 : "𝅘𝅥𝅲",
1.5 * api.DB : "𝅜𝅭", #dotted DB
1.5 * api.D1 : "𝅝𝅭", #dotted D1
1.5 * api.D2 : "𝅗𝅥𝅭", #dotted D2
1.5 * api.D4 : "𝅘𝅥𝅭", #dotted D4
1.5 * api.D8 : "𝅘𝅥𝅮𝅭", #dotted D8
1.5 * api.D16 : "𝅘𝅥𝅯𝅭", #dotted D16
1.5 * api.D32 : "𝅘𝅥𝅰𝅭", #dotted D32
1.5 * api.D64 : "𝅘𝅥𝅱𝅭", #dotted D64
1.5 * api.D128 : "𝅘𝅥𝅲𝅭", #dotted D128
2.25 * api.DB : "𝅜𝅭", #double dotted DB
2.25 * api.D1 : "𝅝𝅭", #double dotted D1
2.25 * api.D2 : "𝅗𝅥𝅭", #double dotted D2
2.25 * api.D4 : "𝅘𝅥𝅭", #double dotted D4
2.25 * api.D8 : "𝅘𝅥𝅮𝅭", #double dotted D8
2.25 * api.D16 : "𝅘𝅥𝅯𝅭", #double dotted D16
2.25 * api.D32 : "𝅘𝅥𝅰𝅭", #double dotted D32
2.25 * api.D64 : "𝅘𝅥𝅱𝅭", #double dotted D64
2.25 * api.D128 : "𝅘𝅥𝅲𝅭", #double dotted D128
}
self.commonNotes = [ #list because we need item order
(api.D1, "𝅝"), #D1
(api.D2 , "𝅗𝅥"), #D2
(api.D4 , "𝅘𝅥"), #D4
(api.D8 , "𝅘𝅥𝅮"), #D8
(1.5 * api.D1 , "𝅝𝅭"), #dotted D1
(1.5 * api.D2 , "𝅗𝅥𝅭"), #dotted D2
(1.5 * api.D4 , "𝅘𝅥𝅭"), #dotted D4
(1.5 * api.D8 , "𝅘𝅥𝅮𝅭"), #dotted D8
]
self.realNotesStrings = [v for k,v in self.commonNotes]
self.realNotesValues = [k for k,v in self.commonNotes]
@property
def snapToGrid(self):
return self._snapToGrid
@snapToGrid.setter
def snapToGrid(self, value):
self._snapToGrid = value
for func in self.snapToGridCallbacks:
func()
constantsAndConfigs = ConstantsAndConfigs() #singleton

5
qtgui/designer/mainwindow.py

@ -2,10 +2,11 @@
# Form implementation generated from reading ui file 'mainwindow.ui'
#
# Created by: PyQt5 UI code generator 5.12.1
# Created by: PyQt5 UI code generator 5.13.0
#
# WARNING! All changes made in this file will be lost!
from PyQt5 import QtCore, QtGui, QtWidgets
@ -967,5 +968,3 @@ class Ui_MainWindow(object):
self.actionLyBarline.setText(_translate("MainWindow", "Barline"))
self.actionLyFree_Instruction.setText(_translate("MainWindow", "Free Instruction"))
self.actionLyRepeat.setText(_translate("MainWindow", "Repeat"))

32
qtgui/mainwindow.py

@ -84,11 +84,9 @@ class MainWindow(TemplateMainWindow):
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
self.initiGuiSharedDataToSave()
#Create the Main Widgets in the Stacked Widget
self.scoreView = ScoreView(self)
self.ui.mainStackWidget.addWidget(self.scoreView)
@ -108,9 +106,9 @@ class MainWindow(TemplateMainWindow):
#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) #instead prevent the main window from having context menus.
self.setContextMenuPolicy(QtCore.Qt.NoContextMenu) #Make toolbars unclosable by preventing the main window from having context menus.
#The statusbar is used for tooltips. To make it permanent we add our own widget
#The statusbar is intended for tooltips. To make it permanent we add our own widget
self.statusLabel = QtWidgets.QLabel()
self.statusBar().insertPermanentWidget(0, self.statusLabel)
@ -126,30 +124,6 @@ class MainWindow(TemplateMainWindow):
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.
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"])
def updateStatusBar(self, exportCursorDict):
"""Every cursor movement updates the statusBar message"""

140
qtgui/scoreview.py

@ -24,149 +24,33 @@ import logging; logging.info("import {}".format(__file__))
#Third Party
from PyQt5 import QtCore, QtGui, QtWidgets, QtOpenGL
#from PyQt5 import QtOpenGL
from PyQt5 import QtCore, QtGui, QtWidgets
#Template Modules
from template.helper import onlyOne
from template.qtgui.scoreView import ScoreView as TemplateScoreView
#Our Modules
from .submenus import GridRhytmEdit
from .constantsAndConfigs import constantsAndConfigs
from .structures import GuiScore
import engine.api as api
class ScoreView(QtWidgets.QGraphicsView):
class ScoreView(TemplateScoreView):
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)
super().__init__(mainWindow, GuiScore(self)) #GuiScore is saved as self.scoreScene
#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.
@ -190,11 +74,6 @@ class ScoreView(QtWidgets.QGraphicsView):
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
@ -208,6 +87,13 @@ class ScoreView(QtWidgets.QGraphicsView):
else:
raise ValueError("Edit Mode unknown")
def resizeEvent(self, event):
self.scoreScene.grid.reactToresizeEventOrZoom()
super().resizeEvent(event)
def changeGridRhythm(self):
GridRhytmEdit(mainWindow=self.mainWindow) #handles everything.
def updateMode(self, *args):
"""Switch through different views for editing:
notes and item edit

220
qtgui/structures.py

@ -31,6 +31,7 @@ from .cursor import Cursor, Playhead, Selection
from .conductor import Conductor, ConductorTransparentBlock
from .graphs import CCGraphTransparentBlock
from template.qtgui.helper import stretchLine, stretchRect, callContextMenu, removeInstancesFromScene
from template.qtgui.grid import GuiGrid
from .submenus import BlockPropertiesEdit
from . import graphs
from hashlib import md5 #string to color
@ -958,222 +959,3 @@ class GuiScore(QtWidgets.QGraphicsScene):
super().mouseReleaseEvent(event)
class GuiGrid(QtWidgets.QGraphicsItemGroup):
"""The grid consists of vertical and horizontal lines.
Horiontal lines help to eyeball what pitch the cursor is on, vertical help to eyeball
the rhythm or tick positions e.g. when trying to find the right place for a tempo change or CC.
Horizontal lines have always a more-than-enough width and X position.
Vertical lines are always at the top of the screen and reach down more-than-enough.
We only need to take care that there are enough lines, not about their dimensions or positions.
Never is a line deleted, only new lines are added if new tracks are added or the musics overall
duration increases.
Also GraphicsView resize event and zooming out adds new lines.
A complete clean and redraw only happens when the tick rhythm changes.
"""
def __init__(self, parent):
super(GuiGrid, self).__init__()
self.parent = parent #QGraphicsScene
self.initialGridExists = False
self.gapVertical = None #gets recalculated if gridRhythm is changed by the user, or through stretching.
self.gapHorizontal = None #is constant, but for symetry reasons this is put into redrawTickGrid as well
self.width = None #recalculated in reactToresizeEventOrZoom
self.height = None #recalculated in reactToresizeEventOrZoom
self.linesHorizontal = [] #later the grid lines will be stored here
self.linesVertical = [] #later the grid lines will be stored here
self.horizontalScrollbarWaitForGapJump = 0
self.oldHorizontalValue = 0
self.verticalScrollbarWaitForGapJump = 0
self.oldVerticalValue = 0
self.parent.parentView.verticalScrollBar().valueChanged.connect(self.reactToVerticalScroll)
self.parent.parentView.horizontalScrollBar().valueChanged.connect(self.reactToHorizontalScroll)
self.setOpacity(constantsAndConfigs.gridOpacity)
gridPen = QtGui.QPen(QtCore.Qt.DotLine)
gridPen.setCosmetic(True)
def reactToHorizontalScroll(self, value):
if not self.initialGridExists:
return
#Keep horizontal lines in view
leftBorderAsScenePos = self.parent.parentView.mapToScene(0, 0).x()
for hline in self.linesHorizontal:
hline.setX(leftBorderAsScenePos)
#Shift vertical lines to new positions, respecting the grid steps
delta = value - self.oldHorizontalValue #in pixel. positive=right, negative=left. the higher the number the faster the scrolling.
delta *= 1/constantsAndConfigs.zoomFactor
self.horizontalScrollbarWaitForGapJump += delta
self.oldHorizontalValue = value
if abs(self.horizontalScrollbarWaitForGapJump) > self.gapVertical: #collect scrollpixels until we scrolled more than one gap
gapMultiplier, rest = divmod(self.horizontalScrollbarWaitForGapJump, self.gapVertical) #really fast scrolling can jump more than one gap
for vline in self.linesVertical:
vline.setX(vline.x() + gapMultiplier*self.gapVertical)
self.horizontalScrollbarWaitForGapJump = rest #keep the rest for the next scroll
assert abs(self.horizontalScrollbarWaitForGapJump) < self.gapVertical
def reactToVerticalScroll(self, value):
if not self.initialGridExists:
return
#Keep vertical lines in view
topBorderAsScenePos = self.parent.parentView.mapToScene(0, 0).y()
for vline in self.linesVertical:
vline.setY(topBorderAsScenePos)
#Shift horizontal lines to a new position, respecting the staffline gap
delta = value - self.oldVerticalValue #in pixel. positive=down, negative=up. the higher the number the faster the scrolling.
delta *= 1/constantsAndConfigs.zoomFactor
self.verticalScrollbarWaitForGapJump += delta
self.oldVerticalValue = value
if abs(self.verticalScrollbarWaitForGapJump) > self.gapHorizontal: #collect scrollpixels until we scrolled more than one gap
gapMultiplier, rest = divmod(self.verticalScrollbarWaitForGapJump, self.gapHorizontal) #really fast scrolling can jump more than one gap
for hline in self.linesHorizontal:
hline.setY(hline.y() + gapMultiplier*self.gapHorizontal)
self.verticalScrollbarWaitForGapJump = rest #keep the rest for the next scroll
assert abs(self.verticalScrollbarWaitForGapJump) < self.gapHorizontal
def reactToresizeEventOrZoom(self):
"""Called by the Views resizeEvent.
When the views geometry changes or zooms we may need to create more lines.
Never delete and never make smaller though."""
scoreViewWidgetHeight = self.parent.parentView.height()
scoreViewWidgetWidth = self.parent.parentView.width()
if (not self.height) or scoreViewWidgetHeight * 1.1 > self.height:
self.height = scoreViewWidgetHeight * 1.1
if (not self.width) or scoreViewWidgetWidth * 1.1 > self.width:
self.width = scoreViewWidgetWidth * 1.1
if self.initialGridExists:
assert self.linesHorizontal and self.linesVertical, (self.linesHorizontal, self.linesVertical)
assert self.parent.parentView.isVisible(), self.parent.parentView.isVisible()
self._fillVerticalLines()
self._fillHorizontalLines()
#else:
#reactToresizeEventOrZoom without grid
#self.redrawTickGrid() #this happens only once, on program start. Afterwards it's user triggered by changing the grid rhythm
def _fillVerticalLines(self):
"""Only allowed to get called by reactToresizeEventOrZoom because we need an updated
self.height value"""
#Check if we need longer lines. Do this before creating new lines, because new lines
#have the heighest
oldHeight = self.linesVertical[-1].line().y2() #this is a bit less than .length() so we use that
if self.height > oldHeight:
for vline in self.linesVertical:
line = vline.line()
line.setLength(self.height)
vline.setLine(line)
#Check if we need new lines
newLineCount = int(self.width / self.gapVertical)
if newLineCount > self.nrOfVerticalLines:
newLineCount += int(newLineCount - self.nrOfVerticalLines)
self._createVerticalLines(start=self.nrOfVerticalLines, end=newLineCount) #This draws only yet nonexisting lines.
self.nrOfVerticalLines = newLineCount #for next time
def _fillHorizontalLines(self):
"""Only allowed to get called by reactToresizeEventOrZoom because we need an updated
self.width value.
To be honest.. this is also called by stretchXCoordinates when shrinking the display so
we need more lines. That does not depend on self.width but on the newLineCount """
#Check if we need longer lines. Do this before creating new lines, because new lines
#have the heighest
oldWidth = self.linesHorizontal[-1].line().x2() #this is a bit less than .length() so we use that
if self.width > oldWidth:
for hline in self.linesHorizontal:
line = hline.line()
line.setLength(self.width)
hline.setLine(line)
#Check if we need new lines
newLineCount = int(self.height / self.gapHorizontal)
if newLineCount > self.nrOfHorizontalLines:
newLineCount += int(newLineCount - self.nrOfHorizontalLines)
self._createHorizontalLines(start=self.nrOfHorizontalLines, end=newLineCount) #This draws only yet nonexisting lines.
self.nrOfHorizontalLines = newLineCount #for next time
def stretchXCoordinates(self, factor):
"""Reposition the items on the X axis.
Call goes through all parents/children, starting from ScoreView._stretchXCoordinates.
Docstring there."""
self.gapVertical = constantsAndConfigs.gridRhythm / constantsAndConfigs.ticksToPixelRatio #this is necessarry every time after stretching (ticksToPixelRatio) and after changing the gridRhythm
for vline in self.linesVertical:
vline.setX(vline.x() * factor)
self._fillVerticalLines()
#for hline in self.linesHorizontal:
# stretchLine(hline, factor)
def updateMode(self, nameAsString):
assert nameAsString in constantsAndConfigs.availableEditModes
if nameAsString == "block":
for l in self.linesVertical:
l.hide()
else:
for l in self.linesVertical:
l.show()
def _createVerticalLines(self, start, end):
"""Lines get an offset that matches the grid-rythm.
So we get more draw area left, if the sceneRect will move into that area for some reason"""
for i in range(start, end): #range includes the first value but not the end. We know that and all functions in GuiGrid are designed accordingly. For example reactToLongerDuration starts on the highest value of the old number of lines since that was never drawn in the last round.
vline = self.parent.addLine(0, -5*self.gapHorizontal, 0, self.height, GuiGrid.gridPen) #x1, y1, x2, y2, pen
self.addToGroup(vline) #first add to group, then set pos
vline.setPos(i * self.gapVertical, 0)
vline.setEnabled(False)
self.linesVertical.append(vline)
def _createHorizontalLines(self, start, end):
"""Lines get an offset that matches the pitch and staff-lines
So we get more draw area on top, if the sceneRect will move into that area for some reason"""
for i in range(start, end): #range includes the first value but not the end. We know that and all functions in GuiGrid are designed accordingly. For example reactToMoreTracks starts on the highest value of the old number of lines since that was never drawn in the last round.
hline = self.parent.addLine(0, 0, self.width, 0, GuiGrid.gridPen) #x1, y1, x2, y2, pen
self.addToGroup(hline) #first add to group, then set pos
hline.setPos(0, i * self.gapHorizontal)
hline.setEnabled(False)
self.linesHorizontal.append(hline)
def redrawTickGrid(self):
"""A complete redraw.
This gets called once after the main window gets shown. (in init main window).
After that only called after the used changes the tick rhythm manually.
"""
assert self.parent.parentView.isVisible()
if self.initialGridExists:
#it is possible that the grid did not change. proceed nevertheless
for l in self.linesHorizontal + self.linesVertical:
self.parent.removeWhenIdle(l)
self.linesHorizontal = [] #like the staff lines
self.linesVertical = [] #makes comparing tick positions easier by eye
self.gapVertical = constantsAndConfigs.gridRhythm / constantsAndConfigs.ticksToPixelRatio #this is necessarry every time after stretching (ticksToPixelRatio) and after changing the gridRhythm
self.gapHorizontal = constantsAndConfigs.stafflineGap #never changes
self.nrOfHorizontalLines = int(self.height / self.gapHorizontal) #we save that value if the score grows and we need more lines. Initially we use the screen size, not the content.
self.nrOfVerticalLines = int(self.width / self.gapVertical) #we save that value if the score grows and we need more lines. Initially we use the screen size, not the content.
self._createVerticalLines(0, self.nrOfVerticalLines)
self._createHorizontalLines(0, self.nrOfHorizontalLines)
self.initialGridExists = True

140
qtgui/submenus.py

@ -20,13 +20,12 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
from typing import Iterable, Callable, Tuple
from PyQt5 import QtCore, QtGui, QtWidgets
import engine.api as api
import template.engine.pitch as pitch
from template.qtgui.helper import QHLine
from template.qtgui.submenus import *
from .constantsAndConfigs import constantsAndConfigs
from .designer.tickWidget import Ui_tickWidget
@ -145,61 +144,7 @@ class TickWidget(QtWidgets.QDialog):
self.ui.durationLabel.setText(" + ".join(text))
#There are two types of submenus in this file. The majority is created in menu.py during start up. Like Clef, KeySig etc. These don't need to ask for any dynamic values.
#The other is like SecondaryTempoChangeMenu. In menu.py this is bound with a lambda construct so a new instance gets created each time the action is called by the user. Thats why this function has self.__call__ in its init.
class Submenu(QtWidgets.QDialog):
#TODO: instead of using a QDialog we could use a QWidget and use it as proxy widget on the graphic scene, placing the menu where the input cursor is.
def __init__(self, mainWindow, labelString):
super().__init__(mainWindow) #if you don't set the parent to the main window the whole screen will be the root and the dialog pops up in the middle of it.
#self.setModal(True) #we don't need this when called with self.exec() instead of self.show()
self.layout = QtWidgets.QFormLayout()
#self.layout = QtWidgets.QVBoxLayout()
self.setLayout(self.layout)
label = QtWidgets.QLabel(labelString) #"Choose a clef" or so.
self.layout.addWidget(label)
#self.setFocus(); #self.grabKeyboard(); #redundant for a proper modal dialog. Leave here for documentation reasons.
def keyPressEvent(self, event):
"""Escape closes the dialog by default.
We want Enter as "accept value"
All other methods of mixing editing, window focus and signals
results in strange qt behaviour, triggering the api function twice or more.
Especially unitbox.editingFinished is too easy to trigger.
The key-event method turned out to be the most straightforward way."""
try:
getattr(self, "process")
k = event.key() #49=1, 50=2 etc.
if k == 0x01000004 or k == 0x01000005: #normal enter or keypad enter
event.ignore()
self.process()
else: #Pressed Esc
self.abortHandler()
super().keyPressEvent(event)
except AttributeError:
super().keyPressEvent(event)
def showEvent(self, event):
#TODO: not optimal but better than nothing.
super().showEvent(event)
#self.resize(self.layout.geometry().width(), self.layout.geometry().height())
self.resize(self.childrenRect().height(), self.childrenRect().width())
self.updateGeometry()
def abortHandler(self):
pass
def __call__(self):
"""This instance can be called like a function"""
self.exec() #blocks until the dialog gets closed
"""
Most submenus have the line "lambda, r, value=value"...
the r is the return value we get automatically from the Qt buttons which need to be handled.
"""
class SecondaryClefMenu(Submenu):
@ -288,22 +233,6 @@ class SecondaryMetricalInstructionMenu(Submenu):
button.clicked.connect(function)
button.clicked.connect(self.done)
class ChooseOne(Submenu):
"""A generic submenu that presents a list of options to the users.
Only supports up to ten entries, for number shortcuts"""
def __init__(self, mainWindow, title:str, lst:Iterable[Tuple[str, Callable]]):
if len(lst) > 9:
raise ValueError(f"ChooseOne submenu supports up to nine entries. You have {len(lst)}")
super().__init__(mainWindow, title)
for number, (prettyname, function) in enumerate(lst):
button = QtWidgets.QPushButton(f"[{number+1}] {prettyname}")
button.setShortcut(QtGui.QKeySequence(str(number+1)))
button.setStyleSheet("Text-align:left; padding: 5px;");
self.layout.addWidget(button)
button.clicked.connect(function)
button.clicked.connect(self.done)
class SecondaryTempoChangeMenu(Submenu):
"""A single tempo change where the user can decide which reference unit and how many of them
per minute.
@ -433,38 +362,6 @@ class TempoBlockPropertiesEdit(Submenu):
api.changeTempoBlock(self.staticExportItem["id"], newParametersDict)
self.done(True)
class GridRhytmEdit(Submenu):
def __init__(self, mainWindow):
super().__init__(mainWindow, "")
self.mainWindow = mainWindow
self.layout.insertRow(0, QtWidgets.QLabel("Edit Grid"))
self.duration = CombinedTickWidget()
self.duration.setValue(constantsAndConfigs.gridRhythm)
self.layout.addRow("duration in ticks", self.duration)
self.opacity = QtWidgets.QSlider(QtCore.Qt.Horizontal)
self.opacity.setMinimum(0)
self.opacity.setMaximum(50)
self.opacityLabel = QtWidgets.QLabel("opacity: {}%".format(int(constantsAndConfigs.gridOpacity * 100)))
self.layout.addRow(self.opacityLabel, self.opacity)
self.opacity.valueChanged.connect(lambda: self.opacityLabel.setText("opacity: {}%".format(self.opacity.value())))
self.opacity.setValue(int(constantsAndConfigs.gridOpacity * 100))
self.opacity.valueChanged.connect(lambda: self.mainWindow.scoreView.scoreScene.grid.setOpacity(self.opacity.value() / 100)) #only react to changes after the initial value was set.
self.__call__()
def process(self):
constantsAndConfigs.gridRhythm = self.duration.value()
constantsAndConfigs.gridOpacity = self.opacity.value() / 100
api.session.guiSharedDataToSave["grid_opacity"] = constantsAndConfigs.gridOpacity
api.session.guiSharedDataToSave["grid_rhythm"] = constantsAndConfigs.gridRhythm
self.mainWindow.scoreView.scoreScene.grid.redrawTickGrid() #opacity was already set live, but finally it will be used here again.
self.done(True)
def abortHandler(self):
self.mainWindow.scoreView.scoreScene.grid.setOpacity(constantsAndConfigs.gridOpacity) #reset to initial value and undo the live preview
class TransposeMenu(Submenu):
def __init__(self, mainWindow, what):
super().__init__(mainWindow, "Transpose {}".format(what.title()))
@ -616,6 +513,41 @@ class SecondaryChannelChangeMenu(Submenu):
self.done(True)
class GridRhytmEdit(Submenu):
def __init__(self, mainWindow):
super().__init__(mainWindow, "")
self.mainWindow = mainWindow
self.layout.insertRow(0, QtWidgets.QLabel("Edit Grid"))
self.duration = CombinedTickWidget()
self.duration.setValue(constantsAndConfigs.gridRhythm)
self.layout.addRow("duration in ticks", self.duration)
self.opacity = QtWidgets.QSlider(QtCore.Qt.Horizontal)
self.opacity.setMinimum(0)
self.opacity.setMaximum(50)
self.opacityLabel = QtWidgets.QLabel("opacity: {}%".format(int(constantsAndConfigs.gridOpacity * 100)))
self.layout.addRow(self.opacityLabel, self.opacity)
self.opacity.valueChanged.connect(lambda: self.opacityLabel.setText("opacity: {}%".format(self.opacity.value())))
self.opacity.setValue(int(constantsAndConfigs.gridOpacity * 100))
self.opacity.valueChanged.connect(lambda: self.mainWindow.scoreView.scoreScene.grid.setOpacity(self.opacity.value() / 100)) #only react to changes after the initial value was set.
self.__call__()
def process(self):
constantsAndConfigs.gridRhythm = self.duration.value()
constantsAndConfigs.gridOpacity = self.opacity.value() / 100
api.session.guiSharedDataToSave["grid_opacity"] = constantsAndConfigs.gridOpacity
api.session.guiSharedDataToSave["grid_rhythm"] = constantsAndConfigs.gridRhythm
self.mainWindow.scoreView.scoreScene.grid.redrawTickGrid() #opacity was already set live, but finally it will be used here again.
self.done(True)
def abortHandler(self):
self.mainWindow.scoreView.scoreScene.grid.setOpacity(constantsAndConfigs.gridOpacity) #reset to initial value and undo the live preview
#Normal Functions
############

Loading…
Cancel
Save