mod action_converters;
mod binding_helpers;
mod color_editor;
mod editor;
mod input_column;
mod learn_capture;
mod monitor_panel;
mod output_column;
mod test_panel;
mod value_editor;
mod value_source_converters;
use crate::midi_learn::MidiPortDiagnostics;
use crate::{
midi_learn::LearnState,
ui::{
asset_tree::{AssetTree, TREE_WIDTH, TreeSelection},
window_common::{gled_window_frame, raise, tool_window_builder},
},
};
use editor::midi_controller_editor;
use egui::epaint::text::{LayoutJob, TextFormat};
use egui::{Align, Context, FontSelection, Id, Ui, Vec2, ViewportId};
use egui_phosphor_icons::Icon;
use gled_model::{asset::midi_controller::MidiController, collection::Collections};
use gled_protocol::midi::MidiMonitorEvent;
use gled_protocol::midi::TestCommand;
use kanal::{Receiver, Sender};
use monitor_panel::show_live_midi_monitor;
use std::collections::{HashMap, HashSet, VecDeque};
const MAX_EVENTS: usize = 1000;
fn iconized(ui: &Ui, icon: Icon, text: &str) -> LayoutJob {
let mut layout_job = LayoutJob::default();
icon.regular().append_to(
&mut layout_job,
ui.style(),
FontSelection::Default,
Align::Center,
);
layout_job.append(text, 0.0, TextFormat::default());
layout_job
}
#[derive(Debug, Default)]
pub(crate) struct MidiControllersWindow {
open: bool,
raise: bool,
dirty: bool,
tree: AssetTree<MidiController>,
filter: String,
events: VecDeque<MidiMonitorEvent>,
test_state_by_controller:
HashMap<gled_model::asset_id::AssetId<MidiController>, MidiControllerTestState>,
test_output_released: bool,
}
#[derive(Debug, Clone, Default)]
struct MidiControllerTestState {
selected_output_port: Option<String>,
value_preview_binding_indices: HashSet<usize>,
value_preview_binding_indices_previous: HashSet<usize>,
blackout_blink_preview_binding_indices: HashSet<usize>,
blackout_blink_preview_binding_indices_previous: HashSet<usize>,
hovered_status_data1: Option<(u8, u8)>,
}
impl MidiControllersWindow {
fn poll(&mut self, receiver: &Receiver<MidiMonitorEvent>) {
while let Ok(Some(event)) = receiver.try_recv() {
if self.events.len() >= MAX_EVENTS {
self.events.pop_front();
}
self.events.push_back(event);
}
}
pub(crate) fn diagnostics(&self) -> Vec<MidiPortDiagnostics> {
let mut by_port: HashMap<String, MidiPortDiagnostics> = HashMap::new();
for event in &self.events {
let entry =
by_port
.entry(event.port_name.clone())
.or_insert_with(|| MidiPortDiagnostics {
port_name: event.port_name.clone(),
..Default::default()
});
match event.label.as_str() {
"input connected" => entry.input_connected = true,
"output connected" => entry.output_connected = true,
"input connect failed" => entry.input_connect_failed += 1,
"output connect failed" => entry.output_connect_failed += 1,
"midi" => entry.message_count += 1,
_ => {}
}
}
let mut list: Vec<_> = by_port.into_values().collect();
list.sort_by(|a, b| a.port_name.cmp(&b.port_name));
list
}
#[cfg_attr(feature = "profiling", profiling::function)]
pub(crate) fn update(
&mut self,
ctx: &Context,
project: &mut Option<gled_model::asset::project::Project>,
collections: &mut Collections,
receiver: &Receiver<MidiMonitorEvent>,
test_command_sender: &Sender<TestCommand>,
learn_state: &mut LearnState,
) {
if self.open {
crate::ui::frame_requests::watch_midi();
}
learn_state.set_streaming_enabled(self.open);
self.poll(receiver);
if !self.open {
self.release_test_output(test_command_sender);
return;
}
self.test_output_released = false;
let diagnostics = self.diagnostics();
ctx.show_viewport_immediate(
ViewportId(Id::new("midi controllers window")),
tool_window_builder("MIDI Controllers")
.with_inner_size(Vec2::new(1400.0, 740.0))
.with_min_inner_size(Vec2::new(1240.0, 740.0)),
|ctx, _viewport_class| {
raise(ctx, &mut self.raise);
ctx.input(|input| {
if input.viewport().close_requested()
|| crate::ui::window_common::embedded_close_clicked("MIDI Controllers")
{
self.open = false;
learn_state.set_streaming_enabled(false);
}
});
gled_window_frame(ctx, "MIDI Controllers", |ui| {
egui::Panel::left("midi controllers tree")
.exact_size(TREE_WIDTH)
.resizable(false)
.show(ui, |ui| {
if self.tree.show(
ui,
ui.make_persistent_id("midi_controllers_tree"),
collections,
) {
self.dirty = false;
}
});
egui::Panel::right("midi_monitor_panel")
.exact_size(330.0)
.resizable(false)
.show(ui, |ui| {
show_live_midi_monitor(ui, &mut self.events, &diagnostics);
});
egui::CentralPanel::default().show(ui, |ui| {
self.tree.common_settings(ui, &mut self.dirty, collections);
match self.tree.selected() {
TreeSelection::Asset(controller) => {
ui.separator();
let test_state = self
.test_state_by_controller
.entry(controller.id)
.or_default();
midi_controller_editor(
ui,
controller,
test_state,
test_command_sender,
&mut self.dirty,
project,
collections,
&mut self.filter,
&diagnostics,
learn_state,
);
}
_ => {
for state in self.test_state_by_controller.values_mut() {
state.value_preview_binding_indices.clear();
}
}
}
});
});
},
);
}
pub(crate) fn open(&mut self) {
self.open = true;
self.raise = true;
}
fn release_test_output(&mut self, test_command_sender: &Sender<TestCommand>) {
if !self.test_output_released {
let _ = test_command_sender.send(TestCommand::UnsetTestDevice);
self.test_output_released = true;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_closed_window_stops_the_test_output_once() {
let (sender, receiver) = kanal::unbounded();
let mut window = MidiControllersWindow::default();
window.release_test_output(&sender);
window.release_test_output(&sender);
assert_eq!(receiver.len(), 1);
window.test_output_released = false;
window.release_test_output(&sender);
assert_eq!(receiver.len(), 2);
}
}