Add PropertyConnector classes, which know how to connect widgets to properties.

model-merge
Ben Niemann 4 years ago
parent 83d3257dca
commit b0fdeca943
  1. 124
      noisicaa/builtin_nodes/midi_cc_to_cv/node_ui.py
  2. 2
      noisicaa/builtin_nodes/midi_cc_to_cv/node_ui_test.py
  3. 39
      noisicaa/builtin_nodes/midi_source/node_ui.py
  4. 24
      noisicaa/builtin_nodes/score_track/node_ui.py
  5. 1
      noisicaa/ui/CMakeLists.txt
  6. 153
      noisicaa/ui/property_connector.py

@ -22,7 +22,7 @@
import logging
import math
from typing import cast, Any, Dict, List
from typing import cast, Any, Dict, List, Tuple
from PyQt5.QtCore import Qt
from PyQt5 import QtCore
@ -33,6 +33,7 @@ from noisicaa import core
from noisicaa import music
from noisicaa.ui import ui_base
from noisicaa.ui import control_value_dial
from noisicaa.ui import property_connector
from noisicaa.ui.graph import base_node
from . import model
from . import processor_messages
@ -89,6 +90,29 @@ class LearnButton(QtWidgets.QToolButton):
self.setPalette(palette)
class MidiChannelSpinBox(QtWidgets.QSpinBox):
def textFromValue(self, value: int) -> str:
return super().textFromValue(value + 1)
def valueFromText(self, text: str) -> int:
return super().valueFromText(text) - 1
def validate(self, text: str, pos: int) -> Tuple[QtGui.QValidator.State, str, int]:
text = text.strip()
if not text:
return (QtGui.QValidator.Intermediate, text, pos)
try:
value = int(text) - 1
except ValueError:
return (QtGui.QValidator.Invalid, text, pos)
if self.minimum() <= value <= self.maximum():
return (QtGui.QValidator.Acceptable, text, pos)
return (QtGui.QValidator.Invalid, text, pos)
class ChannelUI(ui_base.ProjectMixin, core.AutoCleanupMixin, QtCore.QObject):
def __init__(self, channel: model.MidiCCtoCVChannel, **kwargs: Any) -> None:
super().__init__(**kwargs)
@ -100,21 +124,24 @@ class ChannelUI(ui_base.ProjectMixin, core.AutoCleanupMixin, QtCore.QObject):
self.add_cleanup_function(self.__listeners.cleanup)
self.__learning = False
self.__midi_channel = QtWidgets.QSpinBox()
self.__midi_channel = MidiChannelSpinBox()
self.__midi_channel.setObjectName('channel[%016x]:midi_channel' % channel.id)
self.__midi_channel.setRange(1, 16)
self.__midi_channel.setValue(self.__channel.midi_channel + 1)
self.__midi_channel.valueChanged.connect(self.__midiChannelEdited)
self.__listeners['midi_channel'] = self.__channel.midi_channel_changed.add(
self.__midiChannelChanged)
self.__midi_channel.setKeyboardTracking(False)
self.__midi_channel.setRange(0, 15)
self.__midi_channel_connector = property_connector.QSpinBoxConnector(
self.__midi_channel, self.__channel, 'midi_channel',
mutation_name='%s: Change MIDI channel' % self.__node.name,
context=self.context)
self.add_cleanup_function(self.__midi_channel_connector.cleanup)
self.__midi_controller = QtWidgets.QSpinBox()
self.__midi_controller.setObjectName('channel[%016x]:midi_controller' % channel.id)
self.__midi_controller.setRange(0, 127)
self.__midi_controller.setValue(self.__channel.midi_controller)
self.__midi_controller.valueChanged.connect(self.__midiControllerEdited)
self.__listeners['midi_controller'] = self.__channel.midi_controller_changed.add(
self.__midiControllerChanged)
self.__midi_controller_connector = property_connector.QSpinBoxConnector(
self.__midi_controller, self.__channel, 'midi_controller',
mutation_name='%s: Change MIDI controller' % self.__node.name,
context=self.context)
self.add_cleanup_function(self.__midi_controller_connector.cleanup)
self.__learn_timeout = QtCore.QTimer()
self.__learn_timeout.setInterval(5000)
@ -130,24 +157,34 @@ class ChannelUI(ui_base.ProjectMixin, core.AutoCleanupMixin, QtCore.QObject):
min_value_validator = QtGui.QDoubleValidator()
min_value_validator.setRange(-100000, 100000, 3)
self.__min_value.setValidator(min_value_validator)
self.__min_value.setText(fmt_value(self.__channel.min_value))
self.__min_value.editingFinished.connect(self.__minValueEdited)
self.__listeners['min_value'] = self.__channel.min_value_changed.add(self.__minValueChanged)
self.__min_value_connector = property_connector.QLineEditConnector[float](
self.__min_value, self.__channel, 'min_value',
mutation_name='%s: Change min. value' % self.__node.name,
parse_func=float,
display_func=fmt_value,
context=self.context)
self.add_cleanup_function(self.__min_value_connector.cleanup)
self.__max_value = QtWidgets.QLineEdit()
self.__max_value.setObjectName('channel[%016x]:max_value' % channel.id)
max_value_validator = QtGui.QDoubleValidator()
max_value_validator.setRange(-100000, 100000, 3)
self.__max_value.setValidator(max_value_validator)
self.__max_value.setText(fmt_value(self.__channel.max_value))
self.__max_value.editingFinished.connect(self.__maxValueEdited)
self.__listeners['max_value'] = self.__channel.max_value_changed.add(self.__maxValueChanged)
self.__max_value_connector = property_connector.QLineEditConnector[float](
self.__max_value, self.__channel, 'max_value',
mutation_name='%s: Change max. value' % self.__node.name,
parse_func=float,
display_func=fmt_value,
context=self.context)
self.add_cleanup_function(self.__max_value_connector.cleanup)
self.__log_scale = QtWidgets.QCheckBox()
self.__log_scale.setObjectName('channel[%016x]:log_scale' % channel.id)
self.__log_scale.setChecked(self.__channel.log_scale)
self.__log_scale.stateChanged.connect(self.__logScaleEdited)
self.__listeners['log_scale'] = channel.log_scale_changed.add(self.__logScaleChanged)
self.__log_scale_connector = property_connector.QCheckBoxConnector(
self.__log_scale, self.__channel, 'log_scale',
mutation_name='%s: Change log scale' % self.__node.name,
context=self.context)
self.add_cleanup_function(self.__log_scale_connector.cleanup)
self.__current_value = control_value_dial.ControlValueDial()
self.__current_value.setRange(0.0, 1.0)
@ -210,53 +247,6 @@ class ChannelUI(ui_base.ProjectMixin, core.AutoCleanupMixin, QtCore.QObject):
else:
self.__learnStop()
def __midiChannelChanged(self, change: music.PropertyValueChange[int]) -> None:
self.__midi_channel.setValue(change.new_value + 1)
def __midiChannelEdited(self, value: int) -> None:
value -= 1
if value != self.__channel.midi_channel:
with self.project.apply_mutations('%s: Change MIDI channel' % self.__node.name):
self.__channel.midi_channel = value
def __midiControllerChanged(self, change: music.PropertyValueChange[int]) -> None:
self.__midi_controller.setValue(change.new_value)
def __midiControllerEdited(self, value: int) -> None:
if value != self.__channel.midi_controller:
with self.project.apply_mutations('%s: Change MIDI controller' % self.__node.name):
self.__channel.midi_controller = value
def __minValueChanged(self, change: music.PropertyValueChange[float]) -> None:
self.__min_value.setText(fmt_value(self.__channel.min_value))
def __minValueEdited(self) -> None:
state, _, _ = self.__min_value.validator().validate(self.__min_value.text(), 0)
if state == QtGui.QValidator.Acceptable:
value = float(self.__min_value.text())
if value != self.__channel.min_value:
with self.project.apply_mutations('%s: Change min. value' % self.__node.name):
self.__channel.min_value = value
def __maxValueChanged(self, change: music.PropertyValueChange[float]) -> None:
self.__max_value.setText(fmt_value(self.__channel.max_value))
def __maxValueEdited(self) -> None:
state, _, _ = self.__max_value.validator().validate(self.__max_value.text(), 0)
if state == QtGui.QValidator.Acceptable:
value = float(self.__max_value.text())
if value != self.__channel.max_value:
with self.project.apply_mutations('%s: Change max. value' % self.__node.name):
self.__channel.max_value = value
def __logScaleChanged(self, change: music.PropertyValueChange[bool]) -> None:
self.__log_scale.setChecked(self.__channel.log_scale)
def __logScaleEdited(self, value: bool) -> None:
if value != self.__channel.log_scale:
with self.project.apply_mutations('%s: Change log scale' % self.__node.name):
self.__channel.log_scale = value
class MidiCCtoCVNodeWidget(ui_base.ProjectMixin, core.AutoCleanupMixin, QtWidgets.QScrollArea):
def __init__(self, node: model.MidiCCtoCV, **kwargs: Any) -> None:

@ -83,7 +83,7 @@ class MidiCCtoCVNodeWidgetTest(uitest.ProjectMixin, uitest.UITestCase):
self.node.channels[0]. midi_channel = 12
editor = self._getEditor(QtWidgets.QSpinBox, self.node.channels[0], 'midi_channel')
self.assertEqual(editor.value(), 13)
self.assertEqual(editor.value(), 12)
async def test_channel_midi_controller_changed(self):
with self.project.apply_mutations('test'):

@ -33,6 +33,7 @@ from noisicaa.ui import device_list
from noisicaa.ui import dynamic_layout
from noisicaa.ui import piano
from noisicaa.ui import ui_base
from noisicaa.ui import property_connector
from noisicaa.ui.graph import base_node
from . import model
from . import processor_messages
@ -46,28 +47,26 @@ class MidiSourceNodeWidget(ui_base.ProjectMixin, core.AutoCleanupMixin, QtWidget
self.__node = node
self.__listeners = core.ListenerMap[str]()
self.add_cleanup_function(self.__listeners.cleanup)
form = QtWidgets.QWidget(self)
form.setAutoFillBackground(False)
form.setAttribute(Qt.WA_NoSystemBackground, True)
self.__device_uri = device_list.PortSelector(self.app.devices, form)
self.__device_uri.setSelectedPort(self.__node.device_uri)
self.__device_uri.selectedPortChanged.connect(self.__deviceURIEdited)
self.__listeners['device_uri'] = self.__node.device_uri_changed.add(
lambda change: self.__device_uri.setSelectedPort(change.new_value))
self.__device_uri_connector = property_connector.PortSelectorConnector(
self.__device_uri, self.__node, 'device_uri',
mutation_name='%s: Change device' % self.__node.name,
context=self.context)
self.add_cleanup_function(self.__device_uri_connector.cleanup)
self.__channel_filter = QtWidgets.QComboBox(form)
for value, text in [
(-1, "All channels")] + [(value, "%d" % (value + 1)) for value in range(0, 16)]:
self.__channel_filter.addItem(text, value)
if value == self.__node.channel_filter:
self.__channel_filter.setCurrentIndex(self.__channel_filter.count() - 1)
self.__channel_filter.currentIndexChanged.connect(self.__channelFilterEdited)
self.__listeners['channel_filter'] = (
self.__node.channel_filter_changed.add(self.__channelFilterChanged))
self.__channel_filter_connector = property_connector.QComboBoxConnector[int](
self.__channel_filter, self.__node, 'channel_filter',
mutation_name='%s: Change MIDI channel filter' % self.__node.name,
context=self.context)
self.add_cleanup_function(self.__channel_filter_connector.cleanup)
form_layout = QtWidgets.QFormLayout()
form_layout.setVerticalSpacing(1)
@ -89,22 +88,6 @@ class MidiSourceNodeWidget(ui_base.ProjectMixin, core.AutoCleanupMixin, QtWidget
)
self.setLayout(layout)
def __deviceURIEdited(self, uri: str) -> None:
if uri != self.__node.device_uri:
with self.project.apply_mutations('%s: Change device' % self.__node.name):
self.__node.device_uri = uri
def __channelFilterChanged(self, change: music.PropertyValueChange[int]) -> None:
for idx in range(self.__channel_filter.count()):
if self.__channel_filter.itemData(idx) == change.new_value:
self.__channel_filter.setCurrentIndex(idx)
def __channelFilterEdited(self) -> None:
channel_filter = self.__channel_filter.currentData()
if channel_filter != self.__node.channel_filter:
with self.project.apply_mutations('%s: Change MIDI channel filter' % self.__node.name):
self.__node.channel_filter = channel_filter
def __noteOn(self, pitch: value_types.Pitch) -> None:
if self.__node.channel_filter >= 0:
channel = self.__node.channel_filter

@ -32,6 +32,7 @@ from noisicaa import core
from noisicaa import music
from noisicaa.constants import DATA_DIR
from noisicaa.ui import ui_base
from noisicaa.ui import property_connector
from noisicaa.ui.graph import track_node
from . import model
@ -44,22 +45,21 @@ class ScoreTrackWidget(ui_base.ProjectMixin, core.AutoCleanupMixin, QtWidgets.QS
self.__track = track
self.__listeners = core.ListenerMap[str]()
self.add_cleanup_function(self.__listeners.cleanup)
body = QtWidgets.QWidget(self)
body.setAutoFillBackground(False)
body.setAttribute(Qt.WA_NoSystemBackground, True)
self.__transpose_octaves = QtWidgets.QSpinBox(body)
self.__transpose_octaves.setVisible(True)
self.__transpose_octaves.setKeyboardTracking(False)
self.__transpose_octaves.setSuffix(' octaves')
self.__transpose_octaves.setRange(-4, 4)
self.__transpose_octaves.setSingleStep(1)
self.__transpose_octaves.valueChanged.connect(self.onTransposeOctavesEdited)
self.__transpose_octaves.setVisible(True)
self.__transpose_octaves.setValue(self.__track.transpose_octaves)
self.__listeners['track:transpose_octaves'] = (
self.__track.transpose_octaves_changed.add(self.onTransposeOctavesChanged))
connector = property_connector.QSpinBoxConnector(
self.__transpose_octaves, self.__track, 'transpose_octaves',
mutation_name='%s: Change transpose' % self.__track.name,
context=self.context)
self.add_cleanup_function(connector.cleanup)
layout = QtWidgets.QFormLayout()
layout.setVerticalSpacing(1)
@ -71,14 +71,6 @@ class ScoreTrackWidget(ui_base.ProjectMixin, core.AutoCleanupMixin, QtWidgets.QS
self.setFrameShape(QtWidgets.QFrame.NoFrame)
self.setWidget(body)
def onTransposeOctavesChanged(self, change: music.PropertyValueChange[int]) -> None:
self.__transpose_octaves.setValue(change.new_value)
def onTransposeOctavesEdited(self, transpose_octaves: int) -> None:
if transpose_octaves != self.__track.transpose_octaves:
with self.project.apply_mutations('%s: Change transpose' % self.__track.name):
self.__track.transpose_octaves = transpose_octaves
class ScoreTrackNode(track_node.TrackNode):
def __init__(self, *, node: music.BaseNode, **kwargs: Any) -> None:

@ -41,6 +41,7 @@ add_python_package(
project_registry.py
project_view.py
project_view_test.py
property_connector.py
qled.py
qprogressindicator.py
render_dialog.py

@ -0,0 +1,153 @@
#!/usr/bin/python3
# @begin:license
#
# Copyright (c) 2015-2019, Benjamin Niemann <pink@odahoda.de>
#
# This program 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 2 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, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# @end:license
import logging
from typing import Any, Callable, Generic, TypeVar
from PyQt5 import QtGui
from PyQt5 import QtWidgets
from noisicaa import core
from noisicaa import music
from noisicaa.ui import ui_base
from noisicaa.ui import device_list
logger = logging.getLogger(__name__)
W = TypeVar('W', bound=QtWidgets.QWidget)
V = TypeVar('V')
class PropertyConnector(Generic[V, W], ui_base.ProjectMixin, core.AutoCleanupMixin, object):
def __init__(
self,
widget: W,
obj: music.ObjectBase,
prop: str,
*,
mutation_name: str,
**kwargs: Any) -> None:
super().__init__(**kwargs)
self._widget = widget
self._obj = obj
self._prop_name = prop
self._mutation_name = mutation_name
self._connectToWidget()
listener = self._obj.change_callbacks[self._prop_name].add(self._propertyChanged)
self.add_cleanup_function(listener.remove)
def value(self) -> V:
return getattr(self._obj, self._prop_name)
def setValue(self, value: V) -> None:
if value != getattr(self._obj, self._prop_name):
with self.project.apply_mutations(self._mutation_name):
setattr(self._obj, self._prop_name, value)
def _connectToWidget(self) -> None:
raise NotImplementedError
def _propertyChanged(self, change: music.PropertyValueChange) -> None:
raise NotImplementedError
class PortSelectorConnector(PropertyConnector[str, device_list.PortSelector]):
def _connectToWidget(self) -> None:
self._widget.setSelectedPort(self.value())
connection = self._widget.selectedPortChanged.connect(self.setValue)
self.add_cleanup_function(lambda: self._widget.selectedPortChanged.disconnect(connection))
def _propertyChanged(self, change: music.PropertyValueChange) -> None:
self._widget.setSelectedPort(change.new_value)
class QComboBoxConnector(Generic[V], PropertyConnector[V, QtWidgets.QComboBox]):
def _connectToWidget(self) -> None:
idx = self._widget.findData(self.value())
if idx >= 0:
self._widget.setCurrentIndex(idx)
connection = self._widget.currentIndexChanged.connect(self._widgetChanged)
self.add_cleanup_function(lambda: self._widget.currentIndexChanged.disconnect(connection))
def _widgetChanged(self) -> None:
self.setValue(self._widget.currentData())
def _propertyChanged(self, change: music.PropertyValueChange) -> None:
idx = self._widget.findData(change.new_value)
if idx >= 0:
self._widget.setCurrentIndex(idx)
class QSpinBoxConnector(PropertyConnector[int, QtWidgets.QSpinBox]):
def _connectToWidget(self) -> None:
self._widget.setValue(self.value())
connection = self._widget.valueChanged.connect(self.setValue)
self.add_cleanup_function(lambda: self._widget.valueChanged.disconnect(connection))
def _propertyChanged(self, change: music.PropertyValueChange) -> None:
self._widget.setValue(change.new_value)
class QLineEditConnector(Generic[V], PropertyConnector[V, QtWidgets.QLineEdit]):
def __init__(
self,
widget: QtWidgets.QLineEdit,
obj: music.ObjectBase,
prop: str,
*,
parse_func: Callable[[str], V],
display_func: Callable[[V], str],
**kwargs: Any) -> None:
self.__parse_func = parse_func
self.__display_func = display_func
super().__init__(widget, obj, prop, **kwargs)
def _connectToWidget(self) -> None:
self._widget.setText(self.__display_func(self.value()))
connection = self._widget.editingFinished.connect(self._widgetChanged)
self.add_cleanup_function(lambda: self._widget.editingFinished.disconnect(connection))
def _widgetChanged(self) -> None:
text = self._widget.text()
validator = self._widget.validator()
if validator is not None:
state, _, _ = validator.validate(self._widget.text(), 0)
if state != QtGui.QValidator.Acceptable:
return
value = self.__parse_func(text)
self.setValue(value)
def _propertyChanged(self, change: music.PropertyValueChange) -> None:
self._widget.setText(self.__display_func(change.new_value))
class QCheckBoxConnector(PropertyConnector[bool, QtWidgets.QCheckBox]):
def _connectToWidget(self) -> None:
self._widget.setChecked(self.value())
connection = self._widget.stateChanged.connect(self.setValue)
self.add_cleanup_function(lambda: self._widget.stateChanged.disconnect(connection))
def _propertyChanged(self, change: music.PropertyValueChange) -> None:
self._widget.setChecked(change.new_value)
Loading…
Cancel
Save