Source code for jdxi_editor.ui.style.icons

"""
Icon registry for JD-Xi Editor.

Provides centralized icon definitions and retrieval with fallback support.
"""

import os
from typing import Any, Literal

import qtawesome as qta
from decologr import Decologr as log
from PySide6.QtGui import QIcon, Qt
from PySide6.QtWidgets import QHBoxLayout, QLabel

from jdxi_editor.core.synth.type import JDXiSynth
from jdxi_editor.midi.data.digital.oscillator import WaveForm
from jdxi_editor.resources import resource_path
from jdxi_editor.ui.image.utils import base64_to_pixmap
from jdxi_editor.ui.image.waveform import generate_waveform_icon
from jdxi_editor.ui.style.jdxi import JDXiUIStyle


[docs] class WaveQTAIcon: """Wave Icon"""
[docs] TRIANGLE = "mdi.triangle-wave"
[docs] SINE = "mdi.sine-wave"
[docs] SAW: str = "mdi.sawtooth-wave"
[docs] SQUARE: str = "mdi.square-wave"
[docs] RANDOM: str = "mdi.wave"
[docs] WAVEFORM: str = "mdi.waveform"
[docs] class WaveSpec: """Wave"""
[docs] Form: WaveForm = WaveForm
[docs] Icon: WaveQTAIcon = WaveQTAIcon
[docs] class JDXiUIIconRegistry: """Centralized icon definitions and retrieval"""
[docs] MIXER = "mdi.equalizer"
[docs] WaveForm: WaveForm = WaveForm
[docs] Wave: WaveSpec = WaveSpec
[docs] FILTER = "ri.filter-3-fill"
[docs] POWER: str = "mdi.power"
[docs] AMPLIFIER = "mdi.amplifier"
# Action icons
[docs] CLEAR = "ei.broom"
[docs] RUN = "msc.run"
[docs] SAVE = "fa5.save"
[docs] DELETE = "mdi.delete-empty-outline"
[docs] REFRESH = "ei.refresh"
[docs] SETTINGS = "msc.settings"
[docs] EXPORT = "fa5s.file-export"
[docs] HELP = "mdi.help-rhombus-outline"
[docs] HELP_RHOMBUS = "mdi6.help-rhombus-outline"
[docs] QUIT = "mdi6.exit-to-app"
# File icons
[docs] FOLDER = "ph.folders-light"
[docs] FOLDER_OPENED = "msc.folder-opened"
[docs] FOLDER_NOTCH_OPEN = "ph.folder-notch-open-fill"
[docs] FILE_TEXT = "ph.file-text-light"
[docs] FILE_TABLE1 = "mdi.book-information-variant"
[docs] FILE_DOCUMENT = "mdi6.file-document-check-outline"
[docs] EXCEL = "mdi.microsoft-excel"
[docs] FILE_MTZ = "mdi.data-matrix-edit"
[docs] FILE_MOLECULE = "mdi.molecule"
[docs] FLOPPY_DISK = "ph.floppy-disk-fill"
# Ports etc
[docs] USB = "ri.usb-line"
# Misc
[docs] MAGIC = "mdi6.auto-fix"
# MIDI icons
[docs] MIDI_PORT = "mdi.midi-port"
[docs] MUSIC = "mdi.file-music-outline"
[docs] MUSIC_NOTES = "ph.music-notes-fill"
[docs] KEYBOARD = "mdi6.keyboard-settings-outline"
# Playback icons
[docs] PLAY = "ri.play-line"
[docs] STOP = "ri.stop-line"
[docs] PAUSE = "ri.pause-line"
[docs] SHUFFLE = "mdi.shuffle"
[docs] MUTE = "msc.mute"
# Instrument icons
[docs] PIANO = "msc.piano"
[docs] DRUM = "fa5s.drum"
[docs] DRUM_KIT = "drum_kit.png" # Resource file: resources/drum_kit.png
[docs] KICK_DRUM = "kick_drum-icon.png" # Resource file: resources/kick_drum-icon.png
[docs] KICK_DRUM_2 = "kick_drum2-icon.png" # Resource file: resources/kick_drum2-icon.png (white on black)
[docs] CYMBAL = "cymbal-icon.png" # Resource file: resources/cymbal-icon.png
[docs] DISTORTION = "mdi6.signal-distance-variant"
# Effect icons
[docs] EFFECT = "mdi.effect"
[docs] DELAY = "mdi.timer-outline"
[docs] REVERB = "mdi.wave"
[docs] MICROPHONE = "mdi.microphone"
[docs] EQUALIZER = "mdi.equalizer"
# Control icons
[docs] TUNE = "mdi.tune"
[docs] CLOCK = "mdi.clock-outline"
[docs] MUSIC_NOTE = "mdi.music-note"
[docs] MUSIC_NOTE_MULTIPLE = "fa5s.music"
[docs] CODE_BRACES = "mdi.code-braces"
[docs] CIRCLE_OUTLINE = "mdi.circle-outline"
[docs] VOLUME_HIGH = "mdi.volume-high"
[docs] COG_OUTLINE = "mdi.cog-outline"
[docs] DOTS_HORIZONTAL = "mdi.dots-horizontal"
[docs] PAN_HORIZONTAL = "mdi.pan-horizontal"
# Tab icons
[docs] SEARCH_WEB = "mdi6.search-web"
[docs] DATASET_PROCESSING = "mdi.database"
[docs] PROCESSED_DATASETS = "mdi.database-check"
[docs] MODELLED_STRUCTURES = "mdi.molecule"
[docs] RHOFIT_PIPELINE = "mdi.pipe"
# Navigation icons
[docs] BACK = "ri.arrow-go-back-fill"
[docs] FORWARD = "ri.arrow-go-forward-fill"
# Control icons
[docs] FORK = "ei.fork"
[docs] CPU = "mdi6.cpu-64-bit"
[docs] PANDA = "mdi6.panda"
[docs] DATASETS = "mdi.image-edit-outline"
[docs] DATABASE = "mdi.database"
[docs] SHIELD = "mdi.shield-account"
[docs] TRASH = "mdi.delete"
[docs] TRASH_FILL = "ph.trash-fill"
[docs] CLEANUP = "mdi.broom"
[docs] CANCEL = "mdi.cancel"
[docs] ADD = "mdi.plus"
[docs] PLUS_CIRCLE = "ph.plus-circle-fill"
DELETE = "mdi.delete"
[docs] PAUSE_ICON = "mdi.pause"
[docs] SERVER_PROCESS = "msc.server-process"
[docs] REPORT: str = "msc.report"
@staticmethod
[docs] def get_icon( icon_name: str, color: str = None, size: int = None, fallback: str = None ) -> QIcon: """ Get icon with fallback support. :param icon_name: Icon identifier (e.g., "msc.run") :param color: Optional color string (e.g., "#FF0000" or JDXiStyle.FOREGROUND) :param size: Optional size in pixels (defaults to JDXiStyle.ICON_SIZE) :param fallback: Fallback icon if primary fails :return: QIcon or None if both fail """ try: kwargs = {} if color: kwargs["color"] = color icon = qta.icon(icon_name, **kwargs) if icon.isNull(): raise ValueError(f"Icon {icon_name} is null") return icon except Exception as ex: log.debug(f"Failed to load icon {icon_name}: {ex}") # Try loading from resources (e.g. cymbal-icon.png) file_icon = JDXiUIIconRegistry.get_icon_from_resource(icon_name) if file_icon is not None and not file_icon.isNull(): return file_icon if fallback: try: kwargs = {} if color: kwargs["color"] = color icon = qta.icon(fallback, **kwargs) if not icon.isNull(): log.info(f"Using fallback icon {fallback} for {icon_name}") return icon except Exception as fallback_ex: log.debug(f"Failed to load fallback icon {fallback}: {fallback_ex}") log.warning(f"Could not load icon {icon_name}") return None
@staticmethod
[docs] def get_icon_from_resource(filename: str, size: int = None) -> QIcon | None: """ Load an icon from the resources directory (e.g. cymbal-icon.png). :param filename: Basename of the file in resources/ (e.g. "cymbal-icon.png") :param size: Optional size to scale the pixmap (width and height) :return: QIcon or None if file not found or load fails """ if not filename or not (filename.endswith(".png") or filename.endswith(".ico")): return None try: path = resource_path(os.path.join("resources", filename)) if not os.path.isfile(path): log.debug(f"Resource icon not found: {path}") return None icon = QIcon(path) if icon.isNull(): return None return icon except Exception as ex: log.debug(f"Failed to load resource icon {filename}: {ex}") return None
@staticmethod
[docs] def get_icon_pixmap( icon_name: str, color: str = None, size: int = None, fallback: str = None ): """ Get icon as QPixmap with fallback support. :param icon_name: Icon identifier :param color: Optional color string :param size: Optional size in pixels (defaults to JDXiStyle.ICON_SIZE) :param fallback: Fallback icon if primary fails :return: QPixmap or None if all fail """ icon = JDXiUIIconRegistry.get_icon(icon_name, color=color, fallback=fallback) if icon is None: return None icon_size = size or JDXiUIStyle.ICON_SIZE return icon.pixmap(icon_size, icon_size)
@staticmethod
[docs] def get_icon_safe( icon_name: str, color: str = None, size: int = None, fallback: str = None ) -> QIcon: """ Get icon with fallback support, returns empty QIcon if all fail. This version always returns a QIcon object (may be empty). :param icon_name: Icon identifier :param color: Optional color string :param size: Optional size in pixels (unused, kept for compatibility) :param fallback: Fallback icon if primary fails :return: QIcon (may be empty if all fail) """ icon = JDXiUIIconRegistry.get_icon(icon_name, color=color, fallback=fallback) if icon is None: # --- Return empty icon return qta.icon("") return icon
@staticmethod
[docs] def create_adsr_icons_row() -> QHBoxLayout: """Create ADSR icons row""" icon_hlayout = QHBoxLayout() for icon in [ "mdi.triangle-wave", "mdi.sine-wave", "fa5s.wave-square", "mdi.cosine-wave", "mdi.triangle-wave", "mdi.waveform", ]: icon_label = QLabel() icon_pixmap = qta.icon(icon, color=JDXiUIStyle.GREY).pixmap(30, 30) icon_label.setPixmap(icon_pixmap) icon_label.setAlignment(Qt.AlignmentFlag.AlignHCenter) icon_hlayout.addWidget(icon_label) return icon_hlayout
@staticmethod
[docs] def create_oscillator_icons_row() -> QHBoxLayout: """Create oscillator/waveform icons row for oscillator sections""" icon_hlayout = QHBoxLayout() for icon in [ "mdi.triangle-wave", "mdi.sine-wave", "fa5s.wave-square", "mdi.sawtooth-wave", "mdi.waveform", "mdi.sine-wave", ]: icon_label = QLabel() icon_pixmap = qta.icon(icon, color=JDXiUIStyle.GREY).pixmap(30, 30) icon_label.setPixmap(icon_pixmap) icon_label.setAlignment(Qt.AlignmentFlag.AlignHCenter) icon_hlayout.addWidget(icon_label) return icon_hlayout
@staticmethod
[docs] def create_generic_musical_icon_row() -> QHBoxLayout: # Icons icons_hlayout = QHBoxLayout() for icon_name in [ "ph.bell-ringing-bold", "mdi.call-merge", "mdi.account-voice", "ri.voiceprint-fill", "mdi.piano", ]: icon_label = QLabel() icon = qta.icon(icon_name, color=JDXiUIStyle.GREY) pixmap = icon.pixmap(24, 24) # Using fixed icon size icon_label.setPixmap(pixmap) icon_label.setAlignment(Qt.AlignHCenter) icons_hlayout.addWidget(icon_label) return icons_hlayout
@classmethod
[docs] def get_icon_by_qta_name(cls, name, color, scale_factor=1): """get icon by qta name""" try: return qta.icon(name, color, scale_factor) except Exception as ex: return qta.icon("mdi.piano")
@classmethod
[docs] def get_generated_icon( cls, name: Literal[ "adsr", "upsaw", "square", "pwsqu", "triangle", "sine", "saw", "spsaw", "pcm", "noise", "lpf_filter", "hpf_filter", "bypass_filter", "bpf_filter", "filter_sine", ], ): """get generated icon""" icon_base64 = generate_waveform_icon(name, JDXiUIStyle.WHITE, 1.0) return QIcon(base64_to_pixmap(icon_base64))
@staticmethod
[docs] def generate_waveform_icon_by_name( icon: QIcon | None, icon_name_str: Any | None ) -> QIcon: """generate waveform Icon by name""" # --- Check if icon_name_str matches a WaveformIconType attribute icon_type_value = getattr(WaveForm, icon_name_str, None) if icon_type_value is not None: # --- Use generate_waveform_icon directly for waveform/filter icons icon_base64 = generate_waveform_icon( icon_type_value, JDXiUIStyle.WHITE, 1.0 ) pixmap = base64_to_pixmap(icon_base64) if pixmap and not pixmap.isNull(): icon = QIcon(pixmap) return icon
@classmethod
[docs] def icon_for_synth(cls, synth) -> QIcon: mapping = { JDXiSynth.DIGITAL_SYNTH_1: cls.PIANO, JDXiSynth.DIGITAL_SYNTH_2: cls.PIANO, JDXiSynth.DRUM_KIT: cls.DRUM, JDXiSynth.ANALOG_SYNTH: cls.PIANO, JDXiSynth.MASTER: cls.MIXER, } try: return qta.icon(mapping[synth], color="#d0d0d0") except KeyError: raise ValueError(f"Unsupported synth type: {synth!r}")