"""
DrumEditor Module
=================
This module provides the `DrumEditor` class, which serves as
an editor for JD-Xi Drum Kit parameters.
It enables users to modify drum kit settings,
select presets, and send MIDI messages to address connected JD-Xi synthesizer.
Classes
-------
- `DrumEditor`: A graphical editor for JD-Xi drum kits, supporting preset
selection, parameter adjustments, and MIDI communication.
Dependencies
------------
- `PySide6.QtWidgets` for UI components.
- `PySide6.QtCore` for Qt core functionality.
- `jdxi_manager.midi.data.parameter.drums.DrumParameter` for drum parameter definitions.
- `jdxi_manager.midi.data.presets.data.DRUM_PRESETS_ENUMERATED` for enumerated drum presets.
- `jdxi_manager.midi.data.presets.preset_type.PresetType` for preset categorization.
- `jdxi_manager.midi.io.MIDIHelper` for MIDI communication.
- `jdxi_manager.midi.preset.loader.PresetLoader` for loading JD-Xi presets.
- `jdxi_manager.ui.editors.drum_partial.DrumPartialEditor` for managing individual drum partials.
- `jdxi_manager.ui.style.Style` for UI styling.
- `jdxi_manager.ui.editors.base.SynthEditor` as the base class for the editor.
- `jdxi_manager.midi.data.constants.sysex.DIGITAL_SYNTH_1` for SysEx address handling.
- `jdxi_manager.ui.widgets.preset.combo_box.PresetComboBox` for preset selection.
Features
--------
- Displays and edits JD-Xi drum kit parameters.
- Supports drum kit preset selection and loading.
- Provides sliders, spin boxes, and combo boxes for adjusting kit parameters.
- Includes address tabbed interface for managing individual drum partials.
- Sends MIDI System Exclusive (SysEx) messages to update the JD-Xi in real time.
Usage
-----
To use the `DrumEditor`, instantiate it with an optional `MIDIHelper` instance:
.. code-block:: python
from jdxi_editor.midi.io import MIDIHelper
from jdxi_editor.ui.editors.drum_editor import DrumEditor
from PySide6.QtWidgets import QApplication
app = QApplication([])
midi_helper = MIDIHelper()
editor = DrumEditor(midi_helper)
editor.show()
app.exec()
"""
from typing import Dict, Optional, Union
from decologr import Decologr as log
from PySide6.QtCore import Qt
from PySide6.QtGui import QKeySequence, QShortcut
from PySide6.QtWidgets import (
QGroupBox,
QHBoxLayout,
QScrollArea,
QTabWidget,
QVBoxLayout,
QWidget,
)
from jdxi_editor.jdxi.preset.helper import JDXiPresetHelper
from jdxi_editor.jdxi.preset.widget import InstrumentPresetWidget
from jdxi_editor.jdxi.style import JDXiStyle
from jdxi_editor.jdxi.synth.type import JDXiSynth
from jdxi_editor.midi.data.address.address import AddressOffsetProgramLMB
from jdxi_editor.midi.data.drum.data import JDXiMapPartialDrum
from jdxi_editor.midi.data.parameter.drum.common import DrumCommonParam
from jdxi_editor.midi.data.parameter.drum.partial import DrumPartialParam
from jdxi_editor.midi.io.helper import MidiIOHelper
from jdxi_editor.ui.editors.drum.common import DrumCommonSection
from jdxi_editor.ui.editors.drum.partial.editor import DrumPartialEditor
from jdxi_editor.ui.editors.synth.editor import SynthEditor
from jdxi_editor.ui.widgets.dialog.progress import ProgressDialog
[docs]
class DrumCommonEditor(SynthEditor):
"""Editor for JD-Xi Drum Kit parameters"""
def __init__(
self,
midi_helper: Optional[MidiIOHelper] = None,
preset_helper: Optional[JDXiPresetHelper] = None,
parent: Optional[QWidget] = None,
):
super().__init__(midi_helper, parent)
# Helpers
[docs]
self.instrument_image_group: QGroupBox | None = None
[docs]
self.preset_helper = preset_helper
[docs]
self.midi_helper = midi_helper
[docs]
self.partial_number = 0
self._init_synth_data(synth_type=JDXiSynth.DRUM_KIT, partial_number=0)
[docs]
self.sysex_current_data = None
[docs]
self.sysex_previous_data = None
[docs]
self.partial_mapping = JDXiMapPartialDrum.MAP
# UI Elements
[docs]
self.main_window = parent
[docs]
self.partial_editors = {}
[docs]
self.instrument_image_label = None
[docs]
self.instrument_title_label = None
[docs]
self.controls: Dict[Union[DrumPartialParam, DrumCommonParam], QWidget] = {}
self.setup_ui()
self.update_instrument_image()
# Setup signal handlers
if self.midi_helper:
self.midi_helper.midi_program_changed.connect(self._handle_program_change)
self.midi_helper.midi_control_changed.connect(self._handle_control_change)
self.midi_helper.midi_sysex_json.connect(self._dispatch_sysex_to_area)
[docs]
self.refresh_shortcut = QShortcut(QKeySequence.StandardKey.Refresh, self)
self.refresh_shortcut.activated.connect(self.data_request)
# Request initial state data & show the editor
self.data_request()
# self.show()
[docs]
def setup_ui(self) -> None:
"""Setup the UI components for the drum editor."""
main_layout = QVBoxLayout(self)
self.setMinimumSize(1100, 500)
# splitter = QSplitter(Qt.Orientation.Vertical)
self.presets_parts_tab_widget = QTabWidget()
main_layout.addWidget(self.presets_parts_tab_widget)
instrument_widget = QWidget()
instrument_vrow_layout = QVBoxLayout(instrument_widget)
# Use InstrumentPresetWidget for consistent layout
self.instrument_preset = InstrumentPresetWidget(parent=self)
self.instrument_preset.setup_header_layout()
self.instrument_preset.setup()
instrument_preset_group = self.instrument_preset.create_instrument_preset_group(
synth_type="Drums"
)
self.instrument_preset.add_preset_group(instrument_preset_group)
self.instrument_preset.add_stretch()
(
self.instrument_image_group,
self.instrument_image_label,
self.instrument_group_layout,
) = self.instrument_preset.create_instrument_image_group()
self.address.lmb = AddressOffsetProgramLMB.COMMON
self.instrument_image_group.setMinimumWidth(JDXiStyle.INSTRUMENT_IMAGE_WIDTH)
self.instrument_preset.add_image_group(self.instrument_image_group)
self.instrument_preset.add_stretch()
self.update_instrument_image()
instrument_vrow_layout.addWidget(self.instrument_preset)
self.presets_parts_tab_widget.addTab(instrument_widget, "Drum Kit Presets")
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
scroll.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
self.partial_tab_widget.setStyleSheet(JDXiStyle.TABS_DRUMS)
scroll.setWidget(self.partial_tab_widget)
self.presets_parts_tab_widget.addTab(scroll, "Drum Kit Parts")
self.presets_parts_tab_widget.setStyleSheet(JDXiStyle.TABS_DRUMS)
self.partial_tab_widget.setStyleSheet(JDXiStyle.TABS_DRUMS)
self._setup_partial_editors()
# Create and add the common section
self.common_section = DrumCommonSection(
controls=self.controls,
create_parameter_combo_box=self._create_parameter_combo_box,
create_parameter_slider=self._create_parameter_slider,
midi_helper=self.midi_helper,
address=self.address,
)
self.partial_tab_widget.addTab(self.common_section, "Common")
self.update_instrument_image()
self.partial_tab_widget.currentChanged.connect(self.update_partial_number)
self.midi_helper.midi_sysex_json.connect(self._dispatch_sysex_to_area)
# Register the callback for incoming MIDI messages
self.data_request()
[docs]
def _handle_program_change(self, channel: int, program: int):
"""
Handle program change messages by requesting updated data
:param channel: int
:param program: int
"""
log.message(
f"Program change {program} detected on channel {channel}, requesting data update"
)
self.data_request(channel, program)
[docs]
def _setup_partial_editors(self):
"""
Setup the 36 partial editors
"""
total = len(self.partial_mapping)
# Use splash screen if available, otherwise fall back to ProgressDialog
splash = None
splash_progress = None
splash_status = None
if (
self.main_window
and hasattr(self.main_window, "splash")
and self.main_window.splash
):
splash = self.main_window.splash
splash_progress = self.main_window.splash_progress_bar
splash_status = self.main_window.splash_status_label
if splash and splash_progress and splash_status:
# Update splash screen
splash_status.setText(f"Loading drum kit parts ({total} parts)...")
splash_progress.setValue(60) # Start at 60% for drum kit loading
from PySide6.QtWidgets import QApplication
QApplication.processEvents()
else:
# Fall back to ProgressDialog if splash screen not available
progress_dialog = ProgressDialog(
"Initializing Editor Window", "Loading drum kit:...", total, self
)
progress_dialog.show()
for count, (partial_name, partial_number) in enumerate(
self.partial_mapping.items(), 1
):
if splash and splash_progress and splash_status:
# Update splash screen
splash_status.setText(f"Loading {partial_name} ({count} of {total})")
# Progress from 60% to 90% for drum kit loading
progress_value = 60 + int((count / total) * 30)
splash_progress.setValue(progress_value)
from PySide6.QtWidgets import QApplication
QApplication.processEvents()
elif not splash:
# Use ProgressDialog
progress_dialog.progress_bar.setFormat(
f"Loading {partial_name} ({count} of {total})"
)
editor = DrumPartialEditor(
midi_helper=self.midi_helper,
partial_number=partial_number,
partial_name=partial_name,
parent=self,
)
self.partial_editors[partial_number] = editor
self.partial_tab_widget.addTab(editor, partial_name)
if not splash:
progress_dialog.update_progress(count)
if not splash:
progress_dialog.close()
elif splash_progress:
# Complete drum kit loading
splash_progress.setValue(90)
splash_status.setText("Drum kit loaded successfully")
from PySide6.QtWidgets import QApplication
QApplication.processEvents()
[docs]
def update_partial_number(self, index: int):
"""
Update the current partial number based on tab index
:param index: int partial number
"""
try:
partial_name = list(self.partial_editors.keys())[index]
self.partial_number = index
log.message(f"Updated to partial {partial_name} (index {index})")
except IndexError:
log.message(f"Invalid partial index: {index}")
[docs]
def _update_partial_controls(
self, partial_no: int, sysex_data: dict, successes: list, failures: list
) -> None:
"""
apply partial ui updates
:param partial_no: int
:param sysex_data: dict
:param successes: list
:param failures: list
:return:
"""
for param_name, param_value in sysex_data.items():
param = DrumPartialParam.get_by_name(param_name)
if param:
self._update_partial_slider(
partial_no, param, param_value, successes, failures
)
else:
failures.append(param_name)
[docs]
def _update_common_controls(
self,
partial: int, # pylint: disable=unused-argument
sysex_data: Dict,
successes: list = None,
failures: list = None,
):
"""
Update the UI components for tone common and modify parameters.
:param partial: int
:param sysex_data: Dictionary containing SysEx data
:param successes: List of successful parameters
:param failures: List of failed parameters
:return: None
"""
log.header_message("Tone common")
for param_name, param_value in sysex_data.items():
param = DrumCommonParam.get_by_name(param_name)
log.message(f"Tone common: param_name: {param} {param_value}")
try:
if param:
self._update_slider(param, param_value)
else:
failures.append(param_name)
except Exception as ex:
log.error(f"Error {ex} occurred")
log.debug_info(successes, failures)