Browse Source

Work in progress. sorry for the mess

master
Nils 2 weeks ago
parent
commit
284e14ed55
12 changed files with 482 additions and 41 deletions
  1. +6
    -1
      CHANGELOG
  2. +23
    -2
      engine/api.py
  3. +24
    -0
      engine/vico/__init__.py
  4. +28
    -0
      engine/vico/api.py
  5. +74
    -0
      engine/vico/event.py
  6. +183
    -0
      engine/vico/track.py
  7. +11
    -7
      qtgui/designer/mainwindow.py
  8. +14
    -8
      qtgui/designer/mainwindow.ui
  9. +15
    -8
      qtgui/mainwindow.py
  10. +24
    -8
      qtgui/pattern_grid.py
  11. +72
    -7
      qtgui/songeditor.py
  12. +8
    -0
      qtgui/timeline.py

+ 6
- 1
CHANGELOG View File

@@ -1,4 +1,9 @@
2021-04-15 Version 2.1.0
2021-04-15 Version 2.1.1
Add status bar to explain possible user actions (like "use shift + mousewheel to transpose measure")
Streamline mousewheel behaviour in song editor. It now always scrolls without shift / alt key, no more accidental transpositions.
Fix wrong playback cursor speed.

2021-02-15 Version 2.1.0
Full Undo/Redo
Add track groups, which double as midi bus. This enhances overview for more complex projects.
Add option to follow the playhead in pattern view (or not), required by the much longer patterns. Also better scrolling and playhead is always visible now.

+ 23
- 2
engine/api.py View File

@@ -1,6 +1,25 @@
#! /usr/bin/env python3
# -*- coding: utf-8 -*-

"""
Copyright 2021, Nils Hilbricht, Germany ( https://www.hilbricht.net )

This file is part of the Laborejo Software Suite ( https://www.laborejo.org ),

This 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/>.
"""

import logging; logger = logging.getLogger(__name__); logger.info("import")

#Standard Library Modules
@@ -16,7 +35,10 @@ from template.engine.duration import baseDurationToTraditionalNumber
from template.helper import compress

#Our own engine Modules
pass
#for readability import vico/pianoroll tracks from their own file
from engine.vico import api as vico



DEFAULT_FACTOR = 1 #for the GUI.

@@ -1347,4 +1369,3 @@ def noteOff(trackId, row):
midipitch = track.pattern.scale[row]
cbox.send_midi_event(0x80+track.midiChannel, midipitch, track.pattern.averageVelocity, output=track.cboxMidiOutAbstraction)



+ 24
- 0
engine/vico/__init__.py View File

@@ -0,0 +1,24 @@
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Copyright 2021, Nils Hilbricht, Germany ( https://www.hilbricht.net )

This file is part of the Laborejo Software Suite ( https://www.laborejo.org ),

This 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/>.
"""

import logging; logger = logging.getLogger(__name__); logger.info("import")

#This file only exists as a reminder to _not_ create it again wrongly in the future.

+ 28
- 0
engine/vico/api.py View File

@@ -0,0 +1,28 @@
#! /usr/bin/env python3
# -*- coding: utf-8 -*-

"""
Copyright 2021, Nils Hilbricht, Germany ( https://www.hilbricht.net )

This file is part of the Laborejo Software Suite ( https://www.laborejo.org ),

This 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/>.
"""

import logging; logger = logging.getLogger(__name__); logger.info("import")

from template.engine.input_midi import MidiInput

def addTrack():
print ("vico track")

+ 74
- 0
engine/vico/event.py View File

@@ -0,0 +1,74 @@
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Copyright 2021, Nils Hilbricht, Germany ( https://www.hilbricht.net )

This file is part of the Laborejo Software Suite ( https://www.laborejo.org ),

This 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")

class Event:
"""
[0x90, 60, 100] for a loud middle c on channel 0
[0xE0, 4, 85] pitchbend to 85*128 + 4 = 10884 steps

There is no duration value. Connections, e.g. note rectangles in the GUI, have to be calculated.

Byte2 can be None, for Program Change or Channel Pressure.

Vico events have no channel. The recorded channel is converted to channel 0.
"""
def __init__(self, position:int, status:int, byte1:int, byte2:int, layer:int, freeText:str=""):
self.position = position
self.status = status # e.g. 0x90 for note-on or 0xE0 for pitchbend
self.byte1 = byte1 # e.g. 60 for middle c
self.byte2 = byte2 #eg. 100 for a rather loud note. Can be None for Program Changes.
self.layer = layer #0-9 incl. . Events need to know their layer for undo.
self.freeText = freeText
#self.parentLayer = Injected and changed by the Layer itself.

def serialize(self)->tuple:
return (int(self.position), self.status, self.byte1, self.byte2, self.freeText)

def export(self)->dict:
return {
"id": id(self),
"position": self.position,
"status" : self.status,
"byte1" : self.byte1,
"byte2" : self.byte2,
"layer" : self.layer,
"freeText" : self.freeText,
}


def toCboxBytes(self)->bytes:
byte1 = pitch.midiPitchLimiter(self.byte1, 0)
byte2 = pitch.midiPitchLimiter(self.byte2, 0)
status = self.status + self.parentLayer.midiChannel - 1 #we index channels from 1 to 16, so -1 here because status is itself already chan 1
if self.position >= 0:
return cbox.Pattern.serialize_event(self.position, status, byte1, byte2)
else:
logger.warning(f"Event {self.byte1},{self.byte2} has position less than 0. Limiting to 0 in midi output. Please fix manually")
return cbox.Pattern.serialize_event(0, status, byte1, byte2)

def __repr__(self):
return str(self.export())

def copy(self):
"""Returns a standalone copy of self. Needs to be inserted into a layer."""
#return Event(*self.serialize(), layer=None)
return Event(position=self.position, status=self.status, byte1=self.byte1, byte2=self.byte2, layer=None, freeText=self.freeText)

+ 183
- 0
engine/vico/track.py View File

@@ -0,0 +1,183 @@
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Copyright 2021, Nils Hilbricht, Germany ( https://www.hilbricht.net )

This file is part of the Laborejo Software Suite ( https://www.laborejo.org ),

This 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")

#Python Standard Library
from collections import defaultdict
from statistics import median

#Third Party Modules
from calfbox import cbox

#Template Modules
import template.engine.sequencer
import template.engine.pitch as pitch
from template.engine.duration import DB, DL, D1, D2, D4, D8, D16, D32, D64, D128, D256, D512, D1024


class Track(object):
"""
Main data structure is self.events which holds tickspositions and tuples.
"""

def __init__(self, parentData,
name:str="",
color:str=None,
simpleNoteNames:List[str]=None):

logger.info("Creating empty Vico Track instance")
self.parentData = parentData
self.sequencerInterface = template.engine.sequencer.SequencerInterface(parentTrack=self, name=name) #needs parentData
self.color = color if color else "#00FFFF" # "#rrggbb" in hex. no alpha. a convenience slot for the GUI to save a color.
self.patternLengthMultiplicator = 1 #int. >= 1 the multiplicator is added after all other calculations, like subdivions. We can't integrate this into howManyUnits because that is the global score value
self.midiChannel = 0 # 0-15 midi channel is always set.
self.group = "" # "" is a standalone track. Using a name here will group these tracks together. A GUI can use this information. Also all tracks in a group share a single jack out port.
self.visible = True #only used together with groups. the api and our Datas setGroup function take care that standalone tracks are never hidden.

self.events = defaultdict(list) # statusType: [Event]
self.dirty = False # indicates wether this needs re-export. Obviously not saved.

self._processAfterInit()





def __init__(self, parentTrack, index:int):
self.parentTrack = parentTrack
self.index = index
self.color = "cyan"
self.events = defaultdict(list) # statusType: [Event]
self.dirty = False # indicates wether this needs re-export. Obviously not saved.
self.midiChannel = 1 #1-16 inclusive.
self._processAfterInit()

def _processAfterInit(self):
"""Call this after either init or instanceFromSerializedData"""
self.cachedMedianVelocity = 64
self.cachedLastEventPosition = 0 #set in generateCalfboxMidi. Used by the track
self.cachedFirstEventPosition = 0 #set in generateCalfboxMidi. Used by the track

def newEvent(self, tickindex:int, statusType:int, byte1:int, byte2:int, freeText:str):
"""The only place in the program where events get created, except Score.getCopyBufferCopy
and where self.parentTrack.parentData.allEventsById gets populated. """
ev = Event(tickindex, statusType, byte1, byte2, self.index, freeText)
ev.parentLayer = self
self.parentTrack.parentData.allEventsById[id(ev)] = ev
self.events[statusType].append(ev)
self.dirty = True
return ev

def insertEvent(self, event):
"""Insert an existing Event object, that was at one time created through newEvent"""
assert not event in self.events[event.status]
self.events[event.status].append(event)
event.parentLayer = self
self.dirty = True

def deleteEvent(self, event):
"""Don't be surprised. This gets called by api.createNote to temporarily remove
notes and later add them with insertEvent again"""
try:
self.events[event.status].remove(event)
except ValueError:
logger.error(f"Event was not in our list. Layer{self.index}. Event: {event}")
self.dirty = True

#Save / Load / Export
def serialize(self)->dict:
events = []

for statusType, eventlist in self.events.items():
for event in eventlist:
events.append(event.serialize())

return {
"index" : self.index,
"color" : self.color,
"events": events,
"midiChannel": self.midiChannel,
}

@classmethod
def instanceFromSerializedData(cls, parentTrack, serializedData):
self = cls.__new__(cls)
self.parentTrack = parentTrack
self.color = serializedData["color"]
self.index = serializedData["index"]
self.midiChannel = serializedData["midiChannel"]
self.events = defaultdict(list) # statusType: [Event]
for seriEvent in serializedData["events"]: #tuples or list
self.newEvent(*seriEvent)
self.dirty = True
self._processAfterInit()
return self

def generateCalfboxMidi(self):
"""We use subtracks, therefore they do not change the cachedDuration of the song.
Instead we save our own version of cached ticks of the actual song length.
Useful for the GUI so it doesn not have to draw MAX_DURATION"""
if self.dirty:
velocities = []
blob = bytes()
maxPos = 0
minPos = template.engine.sequencer.MAXIMUM_TICK_DURATION
for status, eventlist in self.events.items():
for event in eventlist:
if event.position > maxPos:
maxPos = event.position
if event.position < minPos:
minPos = event.position
if event.status == 0x90:
velocities.append(event.byte2)
blob += event.toCboxBytes()

if velocities:
self.cachedMedianVelocity = int(median(velocities))

self.parentTrack.sequencerInterface.setSubtrack(self.index, [(blob, 0, maxPos+1)]) #(bytes-blob, position, length)
self.dirty = False
self.cachedLastEventPosition = maxPos
self.cachedFirstEventPosition = minPos


def _exportEvents(self) -> list:
"""Includes generating midi.
This is not called after every change!
In fact it is only triggered by api._layerChanged which happens once on program start
and on very extensive operations on the whole layer (like transpose)
"""
result = []
for status, eventlist in self.events.items():
for event in eventlist:
result.append(event.export())
self.generateCalfboxMidi()
return sorted(result, key=lambda e: e["position"])


def export(self)->dict:
return {
"index" : self.index,
"color" : self.color,
"events" : self._exportEvents(), #side effect: generate midi
}



+ 11
- 7
qtgui/designer/mainwindow.py View File

@@ -166,11 +166,8 @@ class Ui_MainWindow(object):
self.menubar.setGeometry(QtCore.QRect(0, 0, 1057, 20))
self.menubar.setObjectName("menubar")
MainWindow.setMenuBar(self.menubar)
self.actionAddTrack = QtWidgets.QAction(MainWindow)
self.actionAddTrack.setText("Add Track")
self.actionAddTrack.setToolTip("Add a new Track")
self.actionAddTrack.setShortcut("")
self.actionAddTrack.setObjectName("actionAddTrack")
self.actionAddPattern = QtWidgets.QAction(MainWindow)
self.actionAddPattern.setObjectName("actionAddPattern")
self.actionPlayPause = QtWidgets.QAction(MainWindow)
self.actionPlayPause.setText("PlayPause")
self.actionPlayPause.setShortcut("Space")
@@ -186,14 +183,21 @@ class Ui_MainWindow(object):
self.actionClone_Selected_Track.setText("Clone selected Track")
self.actionClone_Selected_Track.setShortcut("")
self.actionClone_Selected_Track.setObjectName("actionClone_Selected_Track")
self.actionAddPianoRoll = QtWidgets.QAction(MainWindow)
self.actionAddPianoRoll.setObjectName("actionAddPianoRoll")
self.toolBar.addAction(self.actionClone_Selected_Track)
self.toolBar.addAction(self.actionAddTrack)
self.toolBar.addAction(self.actionAddPattern)
self.toolBar.addAction(self.actionAddPianoRoll)

self.retranslateUi(MainWindow)
QtCore.QMetaObject.connectSlotsByName(MainWindow)

def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
self.actionAddTrack.setIconText(_translate("MainWindow", "Add Track"))
self.actionAddPattern.setText(_translate("MainWindow", "Add Pattern"))
self.actionAddPattern.setIconText(_translate("MainWindow", "Add Pattern"))
self.actionAddPattern.setToolTip(_translate("MainWindow", "Add a new Pattern Track"))
self.actionToStart.setShortcut(_translate("MainWindow", "Home"))
self.actionClone_Selected_Track.setIconText(_translate("MainWindow", "Clone selected Track"))
self.actionAddPianoRoll.setText(_translate("MainWindow", "Add PianoRoll"))
self.actionAddPianoRoll.setToolTip(_translate("MainWindow", "Add PianoRoll"))

+ 14
- 8
qtgui/designer/mainwindow.ui View File

@@ -375,7 +375,8 @@
<bool>false</bool>
</attribute>
<addaction name="actionClone_Selected_Track"/>
<addaction name="actionAddTrack"/>
<addaction name="actionAddPattern"/>
<addaction name="actionAddPianoRoll"/>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
@@ -387,18 +388,15 @@
</rect>
</property>
</widget>
<action name="actionAddTrack">
<action name="actionAddPattern">
<property name="text">
<string notr="true">Add Track</string>
<string>Add Pattern</string>
</property>
<property name="iconText">
<string>Add Track</string>
<string>Add Pattern</string>
</property>
<property name="toolTip">
<string notr="true">Add a new Track</string>
</property>
<property name="shortcut">
<string notr="true"/>
<string>Add a new Pattern Track</string>
</property>
</action>
<action name="actionPlayPause">
@@ -436,6 +434,14 @@
<string notr="true"/>
</property>
</action>
<action name="actionAddPianoRoll">
<property name="text">
<string>Add PianoRoll</string>
</property>
<property name="toolTip">
<string>Add PianoRoll</string>
</property>
</action>
</widget>
<resources/>
<connections/>

+ 15
- 8
qtgui/mainwindow.py View File

@@ -207,6 +207,11 @@ class MainWindow(TemplateMainWindow):
#Toolbar, which needs the widgets above already established
self._populateToolbar()

#Statusbar will show possible actions, such as "use scrollwheel to transpose"
#self.statusBar().showMessage(QtCore.QCoreApplication.translate("Statusbar", ""))
self.statusBar().showMessage("")


api.session.data.setLanguageForEmptyFile(language = QtCore.QLocale().languageToString(QtCore.QLocale().language())) #TODO: this is a hack because we access the session directly. But this is also a function tied to Qts language string. Two wrongs...

self.start() #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.
@@ -260,7 +265,7 @@ class MainWindow(TemplateMainWindow):
#Functions depend on getting set after getting called. They need to know the old track!
self.currentTrackId = newCurrentTrackId

def addTrack(self):
def addPatternTrack(self):
"""Add a new track and initialize it with some data from the current one"""
scale = api.session.data.trackById(self.currentTrackId).pattern.scale #TODO: Access to the sessions data structure directly instead of api. Not good. Getter function or api @property is cleaner.
api.addTrack(scale)
@@ -298,11 +303,13 @@ class MainWindow(TemplateMainWindow):
"""Called once at the creation of the GUI"""
self.ui.toolBar.contextMenuEvent = api.nothing #remove that annoying Checkbox context menu

#Designer Buttons
self.ui.actionAddTrack.triggered.connect(self.addTrack)
#Designer Actions
self.ui.actionAddPattern.triggered.connect(self.addPatternTrack)
self.ui.actionClone_Selected_Track.triggered.connect(self.cloneSelectedTrack)
self.ui.actionAddPianoRoll.triggered.connect(api.vico.addTrack) #no need for our own function, these track types don't have a scale

#New Widgets
#New Widgets. Toolbar in Designer can only have QActions, while in reality it can hold any widget. So we need to add them in code:
#We first define then, then add them below.

#BPM. Always in quarter notes to keep it simple
beatsPerMinuteBlock = QtWidgets.QWidget()
@@ -315,7 +322,6 @@ class MainWindow(TemplateMainWindow):
bpmCheckbox.setToolTip(QtCore.QCoreApplication.translate("Toolbar", "Deactivate to beccome JACK Transport Slave. Activate for Master."))
bpmLayout.addWidget(bpmCheckbox)


beatsPerMinute = QtWidgets.QSpinBox()
beatsPerMinute.setToolTip(bpmCheckbox.toolTip())
beatsPerMinute.setMinimum(0) #0 means off
@@ -486,11 +492,12 @@ class MainWindow(TemplateMainWindow):
spacer.setFixedWidth(5)
self.ui.toolBar.addWidget(spacer)

#Clone Track and AddTrack button is added through Designer but we change the text here to get a translation
#Clone Track and addPatternTrack button is added through Designer but we change the text here to get a translation
self.ui.actionClone_Selected_Track.setText(QtCore.QCoreApplication.translate("Toolbar", "Clone Selected Track"))
self.ui.actionClone_Selected_Track.setToolTip(QtCore.QCoreApplication.translate("Toolbar", "Use this! Create a new track that inherits everything but the content from the original. Already jack connected!"))
self.ui.actionAddTrack.setText(QtCore.QCoreApplication.translate("Toolbar", "Add Track"))
self.ui.actionAddTrack.setToolTip(QtCore.QCoreApplication.translate("Toolbar", "Add a complete empty track that needs to be connected to an instrument manually."))
self.ui.actionAddPattern.setText(QtCore.QCoreApplication.translate("Toolbar", "Add Track"))
self.ui.actionAddPattern.setToolTip(QtCore.QCoreApplication.translate("Toolbar", "Add a complete empty track that needs to be connected to an instrument manually."))

spacer()
self.ui.toolBar.addWidget(beatsPerMinuteBlock) # combined widget with its label and translation included
spacer()

+ 24
- 8
qtgui/pattern_grid.py View File

@@ -61,6 +61,7 @@ class PatternGrid(QtWidgets.QGraphicsScene):
def __init__(self, parentView):
super().__init__()
self.parentView = parentView
self.statusMessage = self.parentView.parentMainWindow.statusBar().showMessage #a version with the correct path of this is in every class of Patroneo
self._steps = {} # (x,y):Step()
self._labels = [] #Step numbers

@@ -98,6 +99,7 @@ class PatternGrid(QtWidgets.QGraphicsScene):
api.callbacks.subdivisionsChanged.append(self.guicallback_subdivisionsChanged)
api.callbacks.patternLengthMultiplicatorChanged.append(self.callback_patternLengthMultiplicatorChanged)


def callback_patternLengthMultiplicatorChanged(self, exportDict):
self._tracks[exportDict["id"]] = exportDict
self._fullRedraw(exportDict["id"])
@@ -294,8 +296,9 @@ class PatternGrid(QtWidgets.QGraphicsScene):
self._play(event)
event.accept()

if not type(self.itemAt(event.scenePos().x(), event.scenePos().y(), self.parentView.transform())) is Step:
self.showVelocities()
#Use hover velocity control instead
#if not type(self.itemAt(event.scenePos().x(), event.scenePos().y(), self.parentView.transform())) is Step:
# self.showVelocities()
else:
event.ignore()
super().mousePressEvent(event)
@@ -467,10 +470,11 @@ class PatternGrid(QtWidgets.QGraphicsScene):
step.setApperance()

class Step(QtWidgets.QGraphicsRectItem):

"""The representation of a note"""

def __init__(self, parentScene, column, row): #Factor and Velocity are set on activation
self.parentScene = parentScene
self.statusMessage = self.parentScene.parentView.parentMainWindow.statusBar().showMessage #a version with the correct path of this is in every class of Patroneo
self.column = column #grid coordinates, not pixels
self.row = row
offset = 2
@@ -500,6 +504,8 @@ class Step(QtWidgets.QGraphicsRectItem):

self.setApperance() #sets color, size and exceedPlayback warning. not velocity.



def setApperance(self):
"""sets color, main/sub size and exceedPlayback warning. not velocity.
This gets called quite often. On mouse down and on release for starters."""
@@ -639,8 +645,10 @@ class Step(QtWidgets.QGraphicsRectItem):
if the mouse cursor is not on that item anymore"""
if self.status:
event.accept()
self.statusMessage(QtCore.QCoreApplication.translate("Statusbar", "Note: Left click do deactivate. Middle click to listen. MouseWheel to change volume. Right click for pattern options."))
self._rememberVelocity = self.velocity
else:
self.statusMessage(QtCore.QCoreApplication.translate("Statusbar", "Left click do activate note. Middle click to listen. Right click for pattern options."))
event.ignore()

def hoverLeaveEvent(self, event):
@@ -648,6 +656,7 @@ class Step(QtWidgets.QGraphicsRectItem):
by the item. Which makes sense because we want to receive mouseRelease on an item even
if the mouse cursor is not on that item anymore"""
self.velocityNumber.hide()
self.statusMessage("")
if self.status:
event.accept()
if self.status and not self.velocity == self._rememberVelocity:
@@ -656,7 +665,6 @@ class Step(QtWidgets.QGraphicsRectItem):
else:
event.ignore()


def wheelEvent(self, event):
"""This buffers until hoverLeaveEvent and then the new value is sent in self.hoverLeaveEvent"""
if self.status:
@@ -681,11 +689,19 @@ class Scale(QtWidgets.QGraphicsRectItem):
def __init__(self, parentScene):
super().__init__(0,0,0,0)
self.parentScene = parentScene
self.statusMessage = self.parentScene.parentView.parentMainWindow.statusBar().showMessage #a version with the correct path of this is in every class of Patroneo
self.pitchWidgets = [] #sorted from top to bottom in Step Rect and scene coordinates
self.simpleNoteNames = None #list of 128 notes. use index with note name. Can be changed at runtime. Never empty.
api.callbacks.trackMetaDataChanged.append(self.callback_trackMetaDataChanged)
#self.buildScale(1) #also sets the positions of the buttons above

self.setAcceptHoverEvents(True)

def hoverEnterEvent(self, event):
self.statusMessage(QtCore.QCoreApplication.translate("Statusbar", "Pitch in MIDI half-tones. 60 = middle C. Enter number or spin the mouse wheel to change."))
def hoverLeaveEvent(self, event):
self.statusMessage("")

def callback_trackMetaDataChanged(self, exportDict):
#Order matters. We need to set the notenames before the scale.
self.buildScale(exportDict["numberOfSteps"])
@@ -1007,6 +1023,8 @@ class VelocityControls(QtWidgets.QWidget):

self.parentScene = patternScene
self.mainWindow = mainWindow
self.statusMessage = self.parentScene.parentView.parentMainWindow.statusBar().showMessage #a version with the correct path of this is in every class of Patroneo


layout = QtWidgets.QHBoxLayout()
layout.setSpacing(0)
@@ -1035,9 +1053,11 @@ class VelocityControls(QtWidgets.QWidget):
self.parentScene.showVelocities()

def enterEvent(self, event):
self.statusMessage(QtCore.QCoreApplication.translate("Statusbar", "Click to change volume for all notes in single steps, spin mouse wheel to change in steps of 10."))
self.parentScene.showVelocities()

def leaveEvent(self, event):
self.statusMessage("")
self.parentScene.hideVelocities()

def velocityUp(self):
@@ -1049,7 +1069,3 @@ class VelocityControls(QtWidgets.QWidget):
api.changePatternVelocity(trackId=self.mainWindow.currentTrackId, steps=-1)
#The steps got redrawn, therefore their velocity numbers are now invislbe again. We are still in hover modus:
self.parentScene.showVelocities()





+ 72
- 7
qtgui/songeditor.py View File

@@ -43,6 +43,7 @@ class SongEditor(QtWidgets.QGraphicsScene):
def __init__(self, parentView):
super().__init__()
self.parentView = parentView
self.statusMessage = self.parentView.parentMainWindow.statusBar().showMessage #a version with the correct path of this is in every class of Patroneo

#Set color, otherwise it will be transparent in window managers or wayland that want that.
self.backColor = QtGui.QColor(55, 61, 69)
@@ -129,6 +130,7 @@ class SongEditor(QtWidgets.QGraphicsScene):
if not exportDict["group"] in groupsSeen: #first encounter
groupsSeen.add(exportDict["group"])
groupRect = QtWidgets.QGraphicsRectItem(0,0, exportDict["numberOfMeasures"]*SIZE_UNIT, SIZE_UNIT)
groupRect.trackGroup = exportDict["group"] #add a marker for double clicks, so that we don't have to create a whole new class.
role = QtGui.QPalette.Window
c = self.parentView.parentMainWindow.fPalBlue.color(role)
groupRect.setBrush(c)
@@ -226,6 +228,17 @@ class SongEditor(QtWidgets.QGraphicsScene):
track = self.tracks[exportDict["id"]]
track.updatePatternLengthMultiplicator(exportDict)

def mouseDoubleClickEvent(self, event):
item = self.itemAt(event.scenePos().x(), event.scenePos().y(), self.parentView.transform())
event.ignore() #send to child widget
if item:
try:
item.trackGroup
api.setGroupVisible(item.trackGroup)
except:
pass


class TrackStructure(QtWidgets.QGraphicsRectItem):
"""From left to right. Holds two lines to show the "staffline" and a number of switches,
colored rectangles to indicate where a pattern is activated on the timeline"""
@@ -233,6 +246,7 @@ class TrackStructure(QtWidgets.QGraphicsRectItem):
def __init__(self, parentScene):
super().__init__(0,0,1,SIZE_UNIT)
self.parentScene = parentScene
self.statusMessage = self.parentScene.parentView.parentMainWindow.statusBar().showMessage #a version with the correct path of this is in every class of Patroneo
self.setAcceptHoverEvents(True) #for the preview highlight switch

self.exportDict = None #self.update gets called immediately after creation.
@@ -491,10 +505,12 @@ class TrackStructure(QtWidgets.QGraphicsRectItem):

def hoverEnterEvent(self, event):
self._highlightSwitch.show()
self.statusMessage(QtCore.QCoreApplication.translate("Statusbar", "Empty Measure: Left click to activate. Middle click to show as shadows in current pattern. Right click for measure group options.")) #Yes, this is the track. Empty measures are not objects.
#This seemed to be a good idea but horrible UX. If you move the mouse down to edit a pattern you end up choosing the last track
#self.scene().parentView.parentMainWindow.chooseCurrentTrack(self.exportDict)

def hoverLeaveEvent(self, event):
self.statusMessage("")
self._highlightSwitch.hide()

def hoverMoveEvent(self, event):
@@ -540,6 +556,7 @@ class Switch(QtWidgets.QGraphicsRectItem):
"""
def __init__(self, parentTrackStructure, position):
self.parentTrackStructure = parentTrackStructure
self.statusMessage = self.parentTrackStructure.parentScene.parentView.parentMainWindow.statusBar().showMessage #a version with the correct path of this is in every class of Patroneo
self.position = position
super().__init__(0,0,SIZE_UNIT,SIZE_UNIT)

@@ -622,11 +639,14 @@ class Switch(QtWidgets.QGraphicsRectItem):
event.ignore()

def hoverEnterEvent(self, event):
"""Only active switches"""
self.statusMessage(QtCore.QCoreApplication.translate("Statusbar", "Measure: Left click to deactivate. Middle click to show as shadows in current pattern. Shift+MouseWheel for half tone transposition. Alt+MouseWheel for in-scale transposition. Right click for measure group options."))
self._bufferScaleTranspose = self.scaleTranspose
self._bufferHalftoneTranspose = self.halftoneTranspose

def hoverLeaveEvent(self, event):
"""only triggered when active/shown"""
self.statusMessage("")
event.accept()

#Scale Transpose. Independent of Halftone Transpose
@@ -672,13 +692,12 @@ class Switch(QtWidgets.QGraphicsRectItem):
event.ignore()
#super.wheelEvent(event)



class TrackLabelEditor(QtWidgets.QGraphicsScene):
"""Only the track labels"""
"""Only the track labels: names, colors, groups"""
def __init__(self, parentView):
super().__init__()
self.parentView = parentView
self.statusMessage = self.parentView.parentMainWindow.statusBar().showMessage #a version with the correct path of this is in every class of Patroneo
self.tracks = {} #TrackID:TrackLabel
self.groups = [] #GroupLabel()

@@ -884,6 +903,8 @@ class TrackLabel(QtWidgets.QGraphicsRectItem):
def __init__(self, parentScene, width, height):
super().__init__(0, 0, width, height)
self.parentScene = parentScene
self.statusMessage = self.parentScene.parentView.parentMainWindow.statusBar().showMessage #a version with the correct path of this is in every class of Patroneo

self.setPen(QtGui.QPen(QtCore.Qt.NoPen))
self.setFlag(self.ItemIgnoresTransformations) #zoom will repostion but not make the font bigger.

@@ -892,7 +913,7 @@ class TrackLabel(QtWidgets.QGraphicsRectItem):
self.positioningHandle = TrackLabel.PositioningHandle(parentTrackLabel=self)
self.positioningHandle.setParentItem(self)
self.positioningHandle.setPos(0,0)
self.positioningHandle.setToolTip(QtCore.QCoreApplication.translate("TrackLabel", "grab and move to reorder tracks"))

self.lengthMultiplicatorSpinBox = TrackLabel.lengthMultiplicatorSpinBox(parentTrackLabel=self)
self.lengthMultiplicatorSpinBox.setParentItem(self)
@@ -901,7 +922,7 @@ class TrackLabel(QtWidgets.QGraphicsRectItem):
self.colorButton = TrackLabel.ColorPicker(parentTrackLabel=self)
self.colorButton.setParentItem(self)
self.colorButton.setPos(4*SIZE_UNIT, 3)
self.colorButton.setToolTip(QtCore.QCoreApplication.translate("TrackLabel", "change track color"))

self.lineEdit = TrackLabel.NameLineEdit(parentTrackLabel=self)
self.label = QtWidgets.QGraphicsProxyWidget()
@@ -914,6 +935,7 @@ class TrackLabel(QtWidgets.QGraphicsRectItem):
super().__init__()

self.parentTrackLabel = parentTrackLabel
self.setAcceptHoverEvents(True)
self.spinBox = QtWidgets.QSpinBox()
self.spinBox.setSuffix("x")
#self.spinBox.setFrame(True)
@@ -921,6 +943,11 @@ class TrackLabel(QtWidgets.QGraphicsRectItem):
self.setWidget(self.spinBox)
self.spinBox.valueChanged.connect(self.spinBoxValueChanged) #Callback for setting is in ParentTrackLabel.update

def hoverEnterEvent(self, event):
self.parentTrackLabel.statusMessage(QtCore.QCoreApplication.translate("Statusbar", "Measure length multiplicator. Enter number or spin the mouse wheel to change."))
def hoverLeaveEvent(self, event):
self.parentTrackLabel.statusMessage("")

def spinBoxValueChanged(self):
self.parentTrackLabel.parentScene.parentView.parentMainWindow.chooseCurrentTrack(self.parentTrackLabel.exportDict)
api.setTrackPatternLengthMultiplicator(self.parentTrackLabel.exportDict["id"], self.spinBox.value())
@@ -931,6 +958,13 @@ class TrackLabel(QtWidgets.QGraphicsRectItem):
super().__init__(0,0,SIZE_UNIT*0.75,SIZE_UNIT*0.75)
self.parentTrackLabel = parentTrackLabel
self.setBrush(QtGui.QColor("cyan"))
self.setAcceptHoverEvents(True)
self.setToolTip(QtCore.QCoreApplication.translate("TrackLabel", "change track color"))

def hoverEnterEvent(self, event):
self.parentTrackLabel.statusMessage(QtCore.QCoreApplication.translate("Statusbar", "Left click to change track color"))
def hoverLeaveEvent(self, event):
self.parentTrackLabel.statusMessage("")

def mousePressEvent(self, event):
if event.button() == QtCore.Qt.LeftButton:
@@ -950,6 +984,7 @@ class TrackLabel(QtWidgets.QGraphicsRectItem):
class PositioningHandle(QtWidgets.QGraphicsEllipseItem):
def __init__(self, parentTrackLabel):
super().__init__(0,0,SIZE_UNIT-2,SIZE_UNIT-2)
self.setToolTip(QtCore.QCoreApplication.translate("TrackLabel", "grab and move to reorder tracks"))
self.parentTrackLabel = parentTrackLabel
self.setPen(QtGui.QPen(QtCore.Qt.NoPen))
role = QtGui.QPalette.ToolTipBase
@@ -967,6 +1002,12 @@ class TrackLabel(QtWidgets.QGraphicsRectItem):

self._cursorPosOnMoveStart = None

self.setAcceptHoverEvents(True)
def hoverEnterEvent(self, event):
self.parentTrackLabel.statusMessage(QtCore.QCoreApplication.translate("Statusbar", "Hold left mouse button and move to reorder tracks"))
def hoverLeaveEvent(self, event):
self.parentTrackLabel.statusMessage("")

def yPos2trackIndex(self, y):
"""0 based"""
pos = round(y / SIZE_UNIT)
@@ -1008,6 +1049,12 @@ class TrackLabel(QtWidgets.QGraphicsRectItem):
self.editingFinished.connect(self.sendToEngine)
self.returnPressed.connect(self.enter)

def enterEvent(self, event):
self.parentTrackLabel.statusMessage(QtCore.QCoreApplication.translate("Statusbar", "Click to select track. Double click to change track name"))

def leaveEvent(self, event):
self.parentTrackLabel.statusMessage("")

def mousePressEvent(self,event):
"""We also need to force this track as active"""
event.accept() #we need this for doubleClick
@@ -1039,6 +1086,8 @@ class TrackLabel(QtWidgets.QGraphicsRectItem):
# super().keyPressEvent(event)




def update(self, exportDict):
self.exportDict = exportDict
self.lineEdit.setText(exportDict["sequencerInterface"]["name"])
@@ -1077,6 +1126,7 @@ class GroupLabel(QtWidgets.QGraphicsRectItem):
super().__init__(0, 0, width, height)

self.parentScene = parentScene
self.statusMessage = self.parentScene.parentView.parentMainWindow.statusBar().showMessage #a version with the correct path of this is in every class of Patroneo
self.group = name
self.visible = visible #if that changes it will change only on creation of a GroupLabel instance
self.setPen(QtGui.QPen(QtCore.Qt.NoPen))
@@ -1092,7 +1142,7 @@ class GroupLabel(QtWidgets.QGraphicsRectItem):
self.positioningHandle = GroupLabel.PositioningHandle(parentGroupLabel=self)
self.positioningHandle.setParentItem(self)
self.positioningHandle.setPos(0,0)
self.positioningHandle.setToolTip(QtCore.QCoreApplication.translate("GroupLabel", "grab and move to reorder groups"))

if visible:
name = "▼ " + name
@@ -1108,7 +1158,14 @@ class GroupLabel(QtWidgets.QGraphicsRectItem):
self.qLabel.setMinimumSize(QtCore.QSize(0, SIZE_UNIT))
self.qLabel.setStyleSheet("background-color: rgba(0,0,0,0)") #transparent so we see the RectItem color

def mousePressEvent(self,event):
self.setAcceptHoverEvents(True)

def hoverEnterEvent(self, event):
self.statusMessage(QtCore.QCoreApplication.translate("Statusbar", "Track Group: Double Click to show or hide. You can also double click the empty group spacers above the tracks."))
def hoverLeaveEvent(self, event):
self.statusMessage("")

def mouseDoubleClickEvent(self,event):
"""Without this no PositionHandle mouseMove and mouse Release events!!!
Also no double click"""
#super().mousePressEvent(event)
@@ -1122,6 +1179,7 @@ class GroupLabel(QtWidgets.QGraphicsRectItem):
class PositioningHandle(QtWidgets.QGraphicsEllipseItem):
def __init__(self, parentGroupLabel):
super().__init__(0,0,SIZE_UNIT-2,SIZE_UNIT-2)
self.setToolTip(QtCore.QCoreApplication.translate("GroupLabel", "grab and move to reorder groups"))
self.parentGroupLabel = parentGroupLabel
self.setPen(QtGui.QPen(QtCore.Qt.NoPen))
role = QtGui.QPalette.ToolTipBase
@@ -1139,6 +1197,13 @@ class GroupLabel(QtWidgets.QGraphicsRectItem):

self._cursorPosOnMoveStart = None

self.setAcceptHoverEvents(True)

def hoverEnterEvent(self, event):
self.parentGroupLabel.statusMessage(QtCore.QCoreApplication.translate("Statusbar", "Hold left mouse button and move to reorder track groups"))
def hoverLeaveEvent(self, event):
self.parentGroupLabel.statusMessage("")

def yPos2trackIndex(self, y):
"""0 based"""
pos = round(y / SIZE_UNIT)

+ 8
- 0
qtgui/timeline.py View File

@@ -31,6 +31,7 @@ class Timeline(QtWidgets.QGraphicsScene):
def __init__(self, parentView):
super().__init__()
self.parentView = parentView
self.statusMessage = self.parentView.parentMainWindow.statusBar().showMessage #a version with the correct path of this is in every class of Patroneo
self.addItem(TimelineRect(parentScene=self))

#Set color, otherwise it will be transparent in window managers or wayland that want that.
@@ -49,6 +50,7 @@ class TimelineRect(QtWidgets.QGraphicsRectItem):
self.height = 25
super().__init__(0, 0, 1, self.height)
self.parentScene = parentScene
self.statusMessage = self.parentScene.parentView.parentMainWindow.statusBar().showMessage #a version with the correct path of this is in every class of Patroneo
self._cachedExportDictScore = {}

role = QtGui.QPalette.Light
@@ -71,6 +73,12 @@ class TimelineRect(QtWidgets.QGraphicsRectItem):

self.setToolTip(QtCore.QCoreApplication.translate("Timeline", "Click to set playback position. Scroll with mousewheel to adjust measure grouping."))

self.setAcceptHoverEvents(True)
def hoverEnterEvent(self, event):
self.statusMessage(QtCore.QCoreApplication.translate("Statusbar", "Timeline: Click to set playback position. Scroll with mousewheel to adjust measure grouping. Right click on measures below for options to use these groups."))
def hoverLeaveEvent(self, event):
self.statusMessage("")

def cache_timesignature(self, howManyUnits, whatTypeOfUnit):
self._cachedExportDictScore["howManyUnits"] = howManyUnits
self._cachedExportDictScore["whatTypeOfUnit"] = whatTypeOfUnit

Loading…
Cancel
Save