diff --git a/qtgui/constantsAndConfigs.py b/qtgui/constantsAndConfigs.py index c126384..fa40c21 100644 --- a/qtgui/constantsAndConfigs.py +++ b/qtgui/constantsAndConfigs.py @@ -22,11 +22,15 @@ along with this program. If not, see . 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 diff --git a/qtgui/designer/mainwindow.py b/qtgui/designer/mainwindow.py index 214f3d6..d70ea83 100644 --- a/qtgui/designer/mainwindow.py +++ b/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")) - - diff --git a/qtgui/mainwindow.py b/qtgui/mainwindow.py index 1a2451f..dbed61b 100644 --- a/qtgui/mainwindow.py +++ b/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""" diff --git a/qtgui/scoreview.py b/qtgui/scoreview.py index 13f2f9c..d5e62b9 100644 --- a/qtgui/scoreview.py +++ b/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 diff --git a/qtgui/structures.py b/qtgui/structures.py index a832a55..f51ecbe 100644 --- a/qtgui/structures.py +++ b/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 diff --git a/qtgui/submenus.py b/qtgui/submenus.py index 11c09a9..542d9f4 100644 --- a/qtgui/submenus.py +++ b/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 . """ -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 ############