Source code for display.digital

"""
digital_display.py

This module provides the DigitalDisplay class, a custom PySide6 QWidget designed
to simulate an LCD-style digital display for MIDI controllers, synthesizers,
or other music-related applications. The display shows preset and program
information along with an octave indicator.

Features:
- Displays a program name, program number, preset name, and preset number.
- Shows the current octave with a digital-style font.
- Customizable font family for the digital display.
- Resizable and styled for a retro LCD appearance.
- Provides setter methods to update displayed values dynamically.

Classes:
- DigitalDisplay: A QWidget subclass that renders a digital-style display.

Usage Example:
    display = DigitalDisplay()
    display.setPresetText("Grand Piano")
    display.setPresetNumber(12)
    display.setProgramText("User Program 1")
    display.setProgramNumber(5)
    display.setOctave(1)

Dependencies:
- PySide6.QtWidgets (QWidget, QSizePolicy)
- PySide6.QtGui (QPainter, QColor, QPen, QFont)

"""

import platform

from decologr import Decologr as log
from PySide6.QtCore import QRect
from PySide6.QtGui import QColor, QFont, QLinearGradient, QPainter, QPaintEvent, QPen
from PySide6.QtWidgets import QSizePolicy, QWidget

from jdxi_editor.jdxi.synth.type import JDXiSynth
from jdxi_editor.midi.data.programs.analog import ANALOG_PRESET_LIST
from jdxi_editor.midi.data.programs.digital import DIGITAL_PRESET_LIST
from jdxi_editor.midi.data.programs.drum import DRUM_KIT_LIST
from jdxi_editor.ui.windows.jdxi.dimensions import JDXiDimensions


[docs] class DigitalDisplayBase(QWidget): """Base class for JD-Xi style digital displays.""" def __init__( self, digital_font_family: str = "JD LCD Rounded", parent: QWidget = None ): super().__init__(parent) """Initialize the DigitalDisplayBase :param digital_font_family: str :param parent: QWidget """
[docs] self.digital_font_family = digital_font_family
[docs] self.display_texts = []
self.setMinimumSize(210, 70) self.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
[docs] def paintEvent(self, event: QPaintEvent) -> None: """Handles rendering of the digital display.""" painter = QPainter(self) if not painter.isActive(): return painter.setRenderHint(QPainter.Antialiasing, False) self.draw_display(painter)
[docs] def draw_display(self, painter: QPainter): """Draws the LCD-style display with a gradient glow effect.""" display_width, display_height = self.width(), self.height() # Gradient background gradient = QLinearGradient(0, 0, display_width, display_height) gradient.setColorAt(0.0, QColor("#321212")) gradient.setColorAt(0.3, QColor("#331111")) gradient.setColorAt(0.5, QColor("#551100")) gradient.setColorAt(0.7, QColor("#331111")) gradient.setColorAt(1.0, QColor("#111111")) painter.setBrush(gradient) painter.setPen(QPen(QColor("#000000"), 2)) painter.drawRect(0, 0, display_width, display_height) # Set font if platform.system() == "Windows": font_size = 13 else: font_size = 19 display_font = QFont(self.digital_font_family, font_size, QFont.Bold) painter.setFont(display_font) # Draw text y_offset = 10 for text in self.display_texts: painter.setPen(QPen(QColor("#FFAA33"))) # rect = QRect(10, y_offset, self.width() - 20, 30) # Proper text bounding area rect = QRect( 10, y_offset, self.width() - 20, 30 ) # Proper text bounding area painter.drawText(rect, 1, str(text)) y_offset += 30 # Space out text lines
[docs] def update_display(self, texts: list) -> None: """Update the display text and trigger repaint. :param texts: list """ self.display_texts = texts self.update()
[docs] def set_upper_display_text(self, text: str) -> None: """Update the display text and trigger repaint. :param text: list """ self.display_texts[0] = text self.update()
[docs] class DigitalTitle(DigitalDisplayBase): """Simplified display showing only the current tone name.""" def __init__( self, tone_name: str = "Init Tone", digital_font_family: str = "JD LCD Rounded", show_upper_text: bool = True, parent: QWidget = None, ): super().__init__(digital_font_family, parent) self.setMinimumSize( JDXiDimensions.DIGITAL_TITLE_WIDTH, JDXiDimensions.DIGITAL_TITLE_HEIGHT )
[docs] self.show_upper_text = show_upper_text
self.set_tone_name(tone_name)
[docs] def __del__(self): print(f"{self.__class__.__name__} was deleted")
[docs] def set_tone_name(self, tone_name: str) -> None: """Update the tone name display. :param tone_name: str """ if self.show_upper_text: # self.update_display(["Currently Editing:", tone_name]) self.update_display(["", tone_name]) else: self.update_display([tone_name])
@property
[docs] def text(self) -> str: return self.display_texts[-1] if self.display_texts else ""
[docs] def setText(self, value: str) -> None: """Alias for set_tone_name. :param value: str """ self.set_tone_name(value)
[docs] class DigitalDisplay(DigitalDisplayBase): """Digital LCD-style display widget.""" def __init__( self, current_octave: int = 0, digital_font_family: str = "JD LCD Rounded", active_synth: str = "D1", tone_name: str = "Init Tone", tone_number: int = 1, program_name: str = "Init Program", program_bank_letter: str = "A", program_number: int = 1, parent: QWidget = None, ): super().__init__(parent)
[docs] self.active_synth = active_synth
[docs] self.digital_font_family = digital_font_family
[docs] self.current_octave = current_octave
[docs] self.tone_name = tone_name
[docs] self.tone_number = tone_number
[docs] self.program_name = program_name or "Untitled Program"
[docs] self.program_number = program_number
[docs] self.program_bank_letter = program_bank_letter
[docs] self.program_id = self.program_bank_letter + str(self.program_number)
[docs] self.margin = 10 # Default margin for display elements
self.setMinimumSize( JDXiDimensions.DISPLAY_WIDTH, JDXiDimensions.DISPLAY_HEIGHT ) # Set size matching display self.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
[docs] def paintEvent(self, event: QPaintEvent) -> None: """Handles the rendering of the digital display. :param event: QPaintEvent """ painter = QPainter(self) if not painter.isActive(): return # Prevents drawing if painter failed to initialize painter.setRenderHint(QPainter.Antialiasing, False) self.draw_display(painter)
[docs] def draw_display(self, painter: QPainter): """Draws the JD-Xi style digital display with a gradient glow effect.""" display_x, display_y = 0, 0 display_width, display_height = self.width(), self.height() # 1. Create an orange glow gradient background gradient = QLinearGradient(0, 0, display_width, display_height) gradient.setColorAt(0.0, QColor("#321212")) # Darker edges gradient.setColorAt(0.3, QColor("#331111")) # Gray transition gradient.setColorAt(0.5, QColor("#551100")) # Orange glow center gradient.setColorAt(0.7, QColor("#331111")) # Gray transition gradient.setColorAt(1.0, QColor("#111111")) # Darker edges painter.setBrush(gradient) painter.setRenderHint(QPainter.Antialiasing, False) painter.setPen(QPen(QColor("#000000"), 2)) # black border painter.drawRect(display_x, display_y, display_width, display_height) # 2. Set font for digital display if platform.system() == "Windows": font_size = 15 else: font_size = 19 display_font = QFont(self.digital_font_family, font_size, QFont.Bold) painter.setFont(display_font) painter.setPen(QPen(QColor("#FFBB33"))) # Lighter orange for text # 3. Draw text with glowing effect tone_name_text = f" {self.active_synth}:{self.tone_name}" tone_name_text = ( tone_name_text[:21] + "…" if len(tone_name_text) > 22 else tone_name_text ) program_text = f"{self.program_id}:{self.program_name}" program_text = ( program_text[:21] + "…" if len(program_text) > 22 else program_text ) oct_text = f"Oct {self.current_octave:+}" if self.current_octave else "Oct 0" # Glow effect simulation (by drawing text multiple times with slight offsets) offsets = [(-2, -2), (1, -1), (-1, 1), (1, 1)] glow_color = QColor("#FF00") # Darker orange for glow effect for dx, dy in offsets: painter.setPen(QPen(glow_color)) painter.drawText(display_x + 7 + dx, display_y + 20 + dy, program_text) painter.drawText(display_x + 3 + dx, display_y + 50 + dy, tone_name_text) painter.drawText( display_x + display_width - 20 + dx, display_y + 30 + dy, oct_text ) # Draw the main text on top painter.setPen(QPen(QColor("#FFAA33"))) # Bright orange text painter.drawText(display_x + 7, display_y + 50, tone_name_text) painter.drawText(display_x + 7, display_y + 20, program_text) painter.drawText(display_x + display_width - 66, display_y + 50, oct_text)
# --- Property Setters ---
[docs] def setPresetText(self, text: str) -> None: """Set preset name and trigger repaint. :param text: str """ self.tone_name = text self.update()
[docs] def setPresetNumber(self, number: int) -> None: """Set preset number and trigger repaint. :param number: int """ self.tone_number = number self.update()
[docs] def setProgramText(self, text: str) -> None: """Set program name and trigger repaint. :param text: str """ self.program_name = text self.update()
[docs] def setProgramNumber(self, number: int) -> None: """Set program number and trigger repaint. :param number: int """ self.program_number = number self.update()
[docs] def setOctave(self, octave: int) -> None: """Set current octave and trigger repaint. :param octave: int """ self.current_octave = octave self.update()
[docs] def repaint_display( self, current_octave: int, tone_number: int, tone_name: str, program_name: str, active_synth: str = "D1", ) -> None: # Lazy import to avoid circular dependency from jdxi_editor.ui.editors.helpers.program import get_program_id_by_name self.current_octave = current_octave self.tone_number = tone_number self.tone_name = tone_name self.program_name = program_name or "Untitled Program" self.program_id = get_program_id_by_name(self.program_name) self.active_synth = active_synth self.update()
[docs] def _update_display( self, synth_type, digital1_tone_name, digital2_tone_name, drums_tone_name, analog_tone_name, tone_number, tone_name, program_name, program_number, program_bank_letter="A", # Default bank ): """Update the JD-Xi display image. :param synth_type: str :param digital1_tone_name: str :param digital2_tone_name: str :param drums_tone_name: str :param analog_tone_name: str """ # Lazy import to avoid circular dependency from jdxi_editor.ui.editors.helpers.preset import get_preset_list_number_by_name if synth_type == JDXiSynth.DIGITAL_SYNTH_1: tone_name = digital1_tone_name tone_number = get_preset_list_number_by_name(tone_name, DIGITAL_PRESET_LIST) active_synth = "D1" elif synth_type == JDXiSynth.DIGITAL_SYNTH_2: tone_name = digital2_tone_name active_synth = "D2" tone_number = get_preset_list_number_by_name(tone_name, DIGITAL_PRESET_LIST) elif synth_type == JDXiSynth.DRUM_KIT: tone_name = drums_tone_name active_synth = "DR" tone_number = get_preset_list_number_by_name(tone_name, DRUM_KIT_LIST) elif synth_type == JDXiSynth.ANALOG_SYNTH: tone_name = analog_tone_name active_synth = "AN" tone_number = get_preset_list_number_by_name(tone_name, ANALOG_PRESET_LIST) else: active_synth = "D1" log.message(f"current tone number: {tone_number}") log.message(f"current tone name: {tone_name}") self.repaint_display( current_octave=self.current_octave, tone_number=tone_number, tone_name=tone_name, program_name=program_name, active_synth=active_synth, )