1
0
mirror of synced 2024-12-22 17:13:08 +01:00

More refactoring

This commit is contained in:
Mark van Renswoude 2024-08-20 12:02:42 +02:00
parent dced5cba03
commit 19a039f98d
51 changed files with 1198 additions and 1026 deletions

View File

@ -1,5 +1,6 @@
{
"nixEnvSelector.nixFile": "${workspaceFolder}/shell.nix",
"files.trimTrailingWhitespace": true,
"rust-analyzer.imports.granularity.group": "item"
"rust-analyzer.imports.granularity.group": "item",
"rust-analyzer.check.command": "clippy"
}

671
Linux/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,22 +1,6 @@
[package]
name = "MassiveKnob"
version = "0.1.0"
edition = "2021"
build = "build.rs"
[dependencies]
anyhow = "1.0.86"
env_logger = "0.11.3"
log = "0.4.21"
platform-dirs = "0.3.0"
regex = "1.10.5"
relm4 = "0.8.1"
relm4-icons = "0.8.3"
rust-i18n = "3.0.1"
serde = { version = "1.0.203", features = ["derive"] }
serde_json = "1.0.117"
serialport = { version = "4.4.0", default-features = false }
tracker = "0.2.2"
[dependencies.min-rs]
git = "https://github.com/MvRens/min-rs.git"
[workspace]
resolver = "2"
members = [
"backend",
"ui-gtk"
]

23
Linux/backend/Cargo.toml Normal file
View File

@ -0,0 +1,23 @@
[package]
name = "massiveknob_backend"
version = "0.1.0"
edition = "2021"
build = "build.rs"
[dependencies]
anyhow = "1.0.86"
log = "0.4.21"
platform-dirs = "0.3.0"
regex = "1.10.5"
rust-i18n = "3.0.1"
serde = { version = "1.0.203", features = ["derive"] }
serde_json = "1.0.117"
serialport = { version = "4.4.0", default-features = false }
tracker = "0.2.2"
[dependencies.uuid]
version = "1.10.0"
features = [ "v4", "fast-rng", "macro-diagnostics" ]
[dependencies.min-rs]
git = "https://github.com/MvRens/min-rs.git"

View File

@ -0,0 +1,9 @@
_version: 2
actions:
pipewire:
group_name:
en: Pipewire
set_volume:
name:
en: Set volume

View File

@ -0,0 +1,6 @@
_version: 2
devices:
serial_min:
name:
en: Serial device using MIN protocol

View File

@ -0,0 +1,42 @@
pub mod pipewire;
use crate::util::unique_id::UniqueId;
pub enum ActionGroup
{
Pipewire(pipewire::PipewireActionGroupInfo)
}
pub trait ActionGroupInfo
{
/// The unique ID of the group. This should remain stable across releases for
/// the purpose of storing it in the user's configuration.
fn unique_id(&self) -> UniqueId;
/// The name of the group for display purposes.
fn name(&self) -> String
{
t!(format!("actions.{}.name", self.unique_id().as_str()).as_str()).to_string()
}
}
pub trait ActionInfo
{
/// The unique ID of the item. This should remain stable across releases for
/// the purpose of storing it in the user's configuration.
fn unique_id(&self) -> UniqueId;
/// The unique ID of the group this action belongs to.
fn group_id(&self) -> UniqueId;
/// The name of the item for display purposes.
fn name(&self) -> String
{
t!(format!("actions.{}.{}.name", self.group_id().as_str(), self.unique_id().as_str()).as_str()).to_string()
}
}

View File

@ -0,0 +1,23 @@
use crate::util::unique_id::UniqueId;
use super::ActionGroupInfo;
pub mod set_volume;
pub enum PipewireAction
{
SetVolume(set_volume::PipewireSetVolumeActionInfo)
}
pub struct PipewireActionGroupInfo
{
}
impl ActionGroupInfo for PipewireActionGroupInfo
{
fn unique_id(&self) -> UniqueId { UniqueId::from("pipewire") }
}

View File

@ -0,0 +1,13 @@
use crate::actions::ActionInfo;
use crate::util::unique_id::UniqueId;
pub struct PipewireSetVolumeActionInfo
{
}
impl ActionInfo for PipewireSetVolumeActionInfo
{
fn unique_id(&self) -> crate::util::unique_id::UniqueId { UniqueId::from("set_volume") }
fn group_id(&self) -> crate::util::unique_id::UniqueId { UniqueId::from("pipewire") }
}

View File

@ -19,6 +19,7 @@ pub struct ConfigManager
impl ConfigManager
{
#![allow(clippy::new_without_default)]
pub fn new() -> Self
{
let appdirs = AppDirs::new(Some("massiveknob"), false).unwrap();
@ -37,7 +38,7 @@ impl ConfigManager
{
return OptionResult::None;
}
match std::fs::File::open(path)
{
Ok(v) => OptionResult::Some(v),
@ -62,7 +63,7 @@ impl ConfigManager
}
}
}
std::fs::File::create(path)
}
}

View File

@ -0,0 +1,18 @@
use uuid::Uuid;
use super::DeviceContext;
pub struct EmulatorDevice
{
}
impl EmulatorDevice
{
pub fn new(_context: DeviceContext, _instance_id: Uuid) -> Self
{
Self
{
}
}
}

View File

@ -0,0 +1,87 @@
pub mod emulator;
pub mod serial_min;
use emulator::EmulatorDevice;
use serial_min::SerialMinDevice;
use uuid::Uuid;
use crate::util::unique_id::UniqueId;
// TODO use a procedural macro to build the registry based on attributes in the Device enum?
pub enum Device
{
Emulator(emulator::EmulatorDevice),
SerialMin(serial_min::SerialMinDevice)
}
pub struct DeviceInfo
{
/// The unique ID of the item. This should remain stable across releases for
/// the purpose of storing it in the user's configuration.
pub unique_id: UniqueId,
pub factory: fn(DeviceContext, Uuid) -> Device
}
pub struct DeviceRegistry
{
info: Vec<DeviceInfo>
}
pub struct DeviceContext
{
// Arc<ConfigManager>
}
impl DeviceRegistry
{
#![allow(clippy::new_without_default)]
pub fn new() -> Self
{
Self
{
info: vec!(
DeviceInfo
{
unique_id: UniqueId::from("emulator"),
factory: |context, instance_id| Device::Emulator(EmulatorDevice::new(context, instance_id))
},
DeviceInfo
{
unique_id: UniqueId::from("serial_min"),
factory: |context, instance_id| Device::SerialMin(SerialMinDevice::new(context, instance_id))
}
)
}
}
pub fn iter(&self) -> impl Iterator<Item = &DeviceInfo>
{
self.info.iter()
}
pub fn by_id(&self, id: &UniqueId) -> Option<&DeviceInfo>
{
self.info.iter().find(|v| &v.unique_id == id)
}
}
impl DeviceInfo
{
/// The name of the item for display purposes.
pub fn name(&self) -> String
{
t!(format!("devices.{}.name", self.unique_id.as_str()).as_str()).to_string()
}
}

View File

@ -0,0 +1,19 @@
use uuid::Uuid;
use super::DeviceContext;
pub struct SerialMinDevice
{
}
impl SerialMinDevice
{
pub fn new(_context: DeviceContext, _instance_id: Uuid) -> Self
{
Self
{
}
}
}

12
Linux/backend/src/lib.rs Normal file
View File

@ -0,0 +1,12 @@
#[macro_use]
extern crate rust_i18n;
i18n!("locales");
pub mod util;
pub mod devices;
pub mod actions;
pub mod config;
pub mod orchestrator;

View File

@ -129,12 +129,16 @@ impl MainWindow
{
if initial
{
self.orchestrator.with_active_device(|device_instance, cookie| { widget = device_instance.create_settings_widget(cookie) });
if let Some(active_device) = self.orchestrator.active_device()
{
active_device
}
self.orchestrator.with_active_device(|device_instance| { widget = device_instance.create_settings_widget() });
}
else
{
let device_id = self.devices_sorted[index].unique_id.clone();
self.orchestrator.set_active_device_id(&device_id, |device_instance, cookie| { widget = device_instance.create_settings_widget(cookie) });
self.orchestrator.set_active_device_id(&device_id, |device_instance| { widget = device_instance.create_settings_widget() });
}
}

View File

@ -1,13 +1,14 @@
use std::borrow::{Borrow, BorrowMut};
use std::sync::Arc;
use crate::actions;
//use crate::actions::ActionRegistryItem;
use uuid::Uuid;
use crate::config::json::JsonConfigManager;
use crate::config::{ConfigManager, ConfigName};
use crate::devices::{self, Device};
use crate::devices::DeviceRegistryItem;
use crate::registry::Registry;
use crate::config::ConfigName;
use crate::config::ConfigManager;
use crate::devices::DeviceContext;
use crate::devices::DeviceInfo;
use crate::devices::DeviceRegistry;
use crate::devices::Device;
use crate::util::unique_id::UniqueId;
@ -19,44 +20,30 @@ pub struct Orchestrator
config_manager: ConfigManager,
settings_name: ConfigName,
device_registry: Registry<DeviceRegistryItem>,
//action_registry: Registry<ActionRegistryItem>,
device_registry: DeviceRegistry,
// current_device_instance: Option<Box<dyn Device>>
active_device: Option<ActiveDevice>
}
impl Orchestrator
{
#![allow(clippy::new_without_default)]
pub fn new() -> Self
{
let config_manager = ConfigManager::new();
let settings_name = ConfigName::new("settings");
let settings = match config_manager.read_json(&settings_name).expect("Error reading settings")
{
None => settings::Settings::default(),
Some(v) => v
};
let settings = config_manager.read_json(&settings_name).expect("Error reading settings").unwrap_or_default();
let mut device_registry = Registry::new();
let mut action_registry = Registry::new();
devices::register(&mut device_registry);
actions::register(&mut action_registry);
let mut instance = Self
{
config_manager,
settings_name,
device_registry,
//action_registry,
device_registry: DeviceRegistry::new(),
//settings,
active_device: None
};
@ -75,7 +62,7 @@ impl Orchestrator
}
pub fn devices(&self) -> impl Iterator<Item = &DeviceRegistryItem>
pub fn devices(&self) -> impl Iterator<Item = &DeviceInfo>
{
self.device_registry.iter()
}
@ -87,38 +74,31 @@ impl Orchestrator
Some(active_device.id.clone())
}
/*
pub fn current_device(&self) -> Option<&DeviceRegistryItem>
pub fn active_device(&self) -> Option<DeviceReference>
{
let Some(device_id) = self.current_device_id() else { return None };
self.device_registry.by_id(device_id)
}
*/
pub fn with_active_device<F>(&self, callback: F) where F: FnOnce(&dyn Device)
{
let Some(active_device) = self.active_device else { return };
let Some(active_device) = &self.active_device else { return None };
let instance = active_device.instance.clone();
callback(instance.as_ref().as_ref());
self.active_device.as_ref().map(|device| callback(&*device.instance.clone()));
Some(instance)
}
pub fn set_active_device_id<F>(&mut self, id: &UniqueId, on_changed: F) where F: FnOnce(&dyn Device)
pub fn set_active_device_id(&mut self, id: &UniqueId) -> DeviceReference
{
let id = Some(id.clone());
if id == self.active_device_id() { return }
if let Some(active_device) = &self.active_device
{
if *id == active_device.id
{
return active_device.instance.clone();
}
}
self.set_active_device(id);
let active_device = self.set_active_device(Some(id.clone())).unwrap_or_else(|| panic!("Invalid device ID: {}", id.as_str()));
self.store_settings();
self.with_active_device(on_changed);
active_device
}
@ -127,11 +107,7 @@ impl Orchestrator
{
let settings = settings::Settings
{
device_id: match &self.active_device
{
None => None,
Some(v) => Some(v.id.clone().into())
}
device_id: self.active_device.as_ref().map(|v| v.id.clone().into())
};
if let Err(e) = self.config_manager.write_json(&self.settings_name, &settings)
@ -142,9 +118,9 @@ impl Orchestrator
fn set_active_device(&mut self, id: Option<UniqueId>)
fn set_active_device(&mut self, id: Option<UniqueId>) -> Option<DeviceReference>
{
self.active_device = match id
let new_device = match id
{
None => None,
Some(v) =>
@ -153,14 +129,25 @@ impl Orchestrator
None => None,
Some(d) =>
{
let context = DeviceContext
{
};
let instance_id = Uuid::new_v4();
Some(ActiveDevice
{
id: v.clone(),
instance: Arc::new((d.factory)())
instance: Arc::new((d.factory)(context, instance_id))
})
}
}
};
let result = new_device.as_ref().map(|v| v.instance.clone());
self.active_device = new_device;
result
}
}
@ -175,8 +162,11 @@ impl Drop for Orchestrator
pub type DeviceReference = Arc<Device>;
struct ActiveDevice
{
id: UniqueId,
instance: Arc<Box<dyn Device>>
instance: DeviceReference
}

View File

@ -0,0 +1,7 @@
use serde::{Serialize, Deserialize};
#[derive(Default, Serialize, Deserialize)]
pub struct Settings
{
pub device_id: Option<String>
}

View File

@ -68,11 +68,11 @@ impl<T: ValidatedStringPattern> From<String> for ValidatedString<T>
}
impl<T: ValidatedStringPattern> Into<String> for ValidatedString<T>
impl<T: ValidatedStringPattern> From<ValidatedString<T>> for String
{
fn into(self) -> String
fn from(val: ValidatedString<T>) -> Self
{
self.inner.clone()
val.inner.clone()
}
}

View File

@ -1,37 +0,0 @@
pub mod pipewire;
use crate::registry::Registry;
use crate::registry::RegistryItem;
use crate::util::unique_id::UniqueId;
pub struct ActionRegistryItem
{
pub unique_id: UniqueId
}
pub trait Action
{
}
impl RegistryItem for ActionRegistryItem
{
fn unique_id(&self) -> UniqueId
{
self.unique_id.clone()
}
fn name(&self) -> String
{
t!(format!("actions.{}.name", self.unique_id.as_str()).as_str()).to_string()
}
}
pub fn register(registry: &mut Registry<ActionRegistryItem>)
{
pipewire::register(registry);
}

View File

@ -1,14 +0,0 @@
use crate::registry::Registry;
use crate::util::unique_id::UniqueId;
use super::ActionRegistryItem;
pub mod set_volume;
pub fn register(registry: &mut Registry<ActionRegistryItem>)
{
registry.register(ActionRegistryItem
{
unique_id: UniqueId::from("pipewire.set_volume")
});
}

View File

@ -1,48 +0,0 @@
use gtk::prelude::*;
use relm4::prelude::*;
pub struct EmulatorWindow
{
}
#[derive(Debug)]
pub enum EmulatorWindowMessage
{
}
#[relm4::component(pub)]
impl SimpleComponent for EmulatorWindow
{
type Init = ();
type Input = EmulatorWindowMessage;
type Output = ();
view!
{
gtk::Window
{
set_title: Some(&t!("emulatorwindow.title")),
set_default_size: (300, 500)
}
}
fn init(_data: Self::Init, root: Self::Root, _sender: ComponentSender<Self>, ) -> ComponentParts<Self>
{
let model = EmulatorWindow {};
let widgets = view_output!();
root.set_visible(true);
ComponentParts { model, widgets }
}
fn update(&mut self, msg: Self::Input, _sender: ComponentSender<Self>)
{
match msg
{
}
}
}

View File

@ -1,58 +0,0 @@
use emulatorwindow::EmulatorWindow;
use relm4::component::Connector;
use relm4::Component;
use relm4::gtk::prelude::*;
use relm4::ComponentController;
use crate::registry::Registry;
use crate::util::unique_id::UniqueId;
use super::Device;
use super::DeviceRegistryItem;
pub mod emulatorwindow;
pub struct EmulatorWindowDevice
{
window: Connector<EmulatorWindow>
}
impl EmulatorWindowDevice
{
fn new() -> Self
{
let builder = EmulatorWindow::builder();
let window = builder.launch({});
EmulatorWindowDevice
{
window
}
}
}
impl Device for EmulatorWindowDevice
{
}
impl Drop for EmulatorWindowDevice
{
fn drop(&mut self)
{
self.window.widget().close();
}
}
pub fn register(registry: &mut Registry<DeviceRegistryItem>)
{
registry.register(DeviceRegistryItem {
unique_id: UniqueId::from("emulator"),
factory: || Box::new(EmulatorWindowDevice::new()),
settings_widget_factory: || None
});
}

View File

@ -1,43 +0,0 @@
pub mod emulator;
pub mod serial_min;
use crate::registry::Registry;
use crate::registry::RegistryItem;
use crate::ui::EmbeddedWidgetConnector;
use crate::util::unique_id::UniqueId;
pub struct DeviceRegistryItem
{
pub unique_id: UniqueId,
pub factory: fn() -> Box<dyn Device>,
pub settings_widget_factory: fn() -> Option<Box<dyn EmbeddedWidgetConnector>>
}
pub trait Device
{
}
impl RegistryItem for DeviceRegistryItem
{
fn unique_id(&self) -> UniqueId
{
self.unique_id.clone()
}
fn name(&self) -> String
{
t!(format!("devices.{}.name", self.unique_id.as_str()).as_str()).to_string()
}
}
pub fn register(registry: &mut Registry<DeviceRegistryItem>)
{
emulator::register(registry);
serial_min::register(registry);
}

View File

@ -1,38 +0,0 @@
use relm4::Component;
use settingswidget::SerialMinSettingsInit;
use settingswidget::SerialMinSettingsWidget;
use crate::registry::Registry;
use crate::util::unique_id::UniqueId;
use super::Device;
use super::DeviceRegistryItem;
pub mod settingswidget;
pub struct SerialMinDevice
{
}
impl Device for SerialMinDevice
{
}
pub fn register(registry: &mut Registry<DeviceRegistryItem>)
{
registry.register(DeviceRegistryItem {
unique_id: UniqueId::from("serial_min"),
factory: || Box::new(SerialMinDevice {}),
settings_widget_factory: ||
{
let builder = SerialMinSettingsWidget::builder();
Some(Box::new(builder.launch(SerialMinSettingsInit
{
})))
}
});
}

View File

@ -1,30 +0,0 @@
use env_logger::Env;
use relm4::prelude::*;
#[macro_use]
extern crate rust_i18n;
i18n!("locales");
pub mod util;
pub mod ui;
pub mod registry;
pub mod devices;
pub mod actions;
pub mod config;
pub mod orchestrator;
pub mod mainwindow;
fn main()
{
env_logger::Builder::from_env(Env::default().default_filter_or("info"))
// .format_timestamp(None)
.init();
relm4_icons::initialize_icons();
let app = RelmApp::new("com.github.mvrens.massiveknob");
app.run::<mainwindow::MainWindow>(());
}

View File

@ -1,19 +0,0 @@
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
pub struct Settings
{
pub device_id: Option<String>
}
impl Default for Settings
{
fn default() -> Self
{
Self
{
device_id: None
}
}
}

View File

@ -1,56 +0,0 @@
use std::collections::HashMap;
use log;
use crate::util::unique_id::UniqueId;
pub trait RegistryItem
{
/// The unique ID of the item. This should remain stable across releases for
/// the purpose of storing it in the user's configuration.
fn unique_id(&self) -> UniqueId;
/// The name of the item for display purposes.
fn name(&self) -> String;
}
pub struct Registry<T> where T: RegistryItem
{
items: HashMap<String, T>
}
impl<T> Registry<T> where T: RegistryItem
{
pub fn new() -> Self
{
Self
{
items: HashMap::new()
}
}
pub fn register(&mut self, device: T)
{
let device_id = device.unique_id();
log::debug!("Registered device: [{}] {}", device_id.as_str(), device.name());
self.items.insert(String::from(device_id.as_str()), device);
}
pub fn iter(&self) -> impl Iterator<Item = &T>
{
self.items.values()
}
pub fn by_id(&self, id: &UniqueId) -> Option<&T>
{
self.items.get(id.as_str())
}
}

16
Linux/ui-gtk/Cargo.toml Normal file
View File

@ -0,0 +1,16 @@
[package]
name = "massiveknob_ui_gtk"
version = "0.1.0"
edition = "2021"
build = "build.rs"
[dependencies]
env_logger = "0.11.3"
gtk = { version = "0.9.0", package = "gtk4", features = ["v4_14"] }
log = "0.4.21"
rust-i18n = "3.0.1"
serialport = { version = "4.4.0", default-features = false }
tracker = "0.2.2"
[dependencies.massiveknob_backend]
path = "../backend"

4
Linux/ui-gtk/build.rs Normal file
View File

@ -0,0 +1,4 @@
fn main()
{
println!("cargo:rerun-if-changed=locales");
}

2
Linux/ui-gtk/icons.toml Normal file
View File

@ -0,0 +1,2 @@
base_resource_path = "/com/github/mvrens/massiveknob/"
icons = []

View File

@ -0,0 +1,6 @@
_version: 2
devices:
emulator:
name:
en: Emulator

View File

@ -0,0 +1,101 @@
use massiveknob_backend::orchestrator::DeviceReference;
use crate::ui::uicomponent::UiComponent;
use crate::ui::uicomponent::UiComponentState;
pub struct EmulatorSettingsUiInit
{
pub device: DeviceReference
}
pub struct EmulatorSettingsUi
{
}
pub struct EmulatorSettingsUiWidgets
{
}
impl UiComponent for EmulatorSettingsUi
{
type Root = gtk::Box;
type Widgets = EmulatorSettingsUiWidgets;
type Init = EmulatorSettingsUiInit;
type State = Self;
fn build_root(_init: &Self::Init) -> Self::Root
{
gtk::Box::builder()
.hexpand(true)
.orientation(gtk::Orientation::Vertical)
.spacing(8)
.build()
}
fn build_widgets(_root: &Self::Root, _init: &Self::Init) -> Self::Widgets
{
Self::Widgets
{
}
}
fn init(_root: &Self::Root, _state: &std::rc::Rc<std::cell::RefCell<Self::State>>)
{
}
}
impl UiComponentState<EmulatorSettingsUi> for EmulatorSettingsUi
{
fn new(_init: (), _widgets: EmulatorSettingsUiWidgets) -> Self
{
Self
{
}
}
}
/*
#[relm4::component(pub)]
impl SimpleComponent for EmulatorWindow
{
type Init = ();
type Input = EmulatorWindowMessage;
type Output = ();
view!
{
gtk::Window
{
set_title: Some(&t!("emulatorwindow.title")),
set_default_size: (300, 500)
}
}
fn init(_data: Self::Init, root: Self::Root, _sender: ComponentSender<Self>, ) -> ComponentParts<Self>
{
let model = EmulatorWindow {};
let widgets = view_output!();
root.set_visible(true);
ComponentParts { model, widgets }
}
fn update(&mut self, msg: Self::Input, _sender: ComponentSender<Self>)
{
match msg
{
}
}
}
*/

View File

@ -0,0 +1,30 @@
use emulator_settings_ui::EmulatorSettingsUi;
use emulator_settings_ui::EmulatorSettingsUiInit;
use massiveknob_backend::devices::Device;
use massiveknob_backend::orchestrator::DeviceReference;
use serial_min_settings_ui::SerialMinSettingsUi;
use serial_min_settings_ui::SerialMinSettingsUiInit;
use crate::ui::uicomponent::UiComponent;
use crate::ui::uicomponent::UiComponentConnectorWidget;
pub mod emulator_settings_ui;
pub mod serial_min_settings_ui;
pub struct DeviceSettingsUiBuilder
{
}
impl DeviceSettingsUiBuilder
{
pub fn build(device: DeviceReference) -> Box<dyn UiComponentConnectorWidget>
{
match device.as_ref()
{
Device::Emulator(_) => Box::new(EmulatorSettingsUi::builder().build(EmulatorSettingsUiInit { device })),
Device::SerialMin(_) => Box::new(SerialMinSettingsUi::builder().build(SerialMinSettingsUiInit { device }))
}
}
}

View File

@ -1,15 +1,13 @@
use gtk::StringList;
use gtk::glib::clone;
use relm4::component::Connector;
use relm4::prelude::*;
use relm4::gtk::prelude::*;
use serialport::SerialPortInfo;
use crate::ui::EmbeddedWidgetConnector;
use gtk::prelude::*;
use gtk::StringList;
use massiveknob_backend::orchestrator::DeviceReference;
use crate::ui::uicomponent::UiComponent;
use crate::ui::uicomponent::UiComponentState;
#[tracker::track]
pub struct SerialMinSettingsWidget
pub struct SerialMinSettingsUi
{
#[do_not_track]
ports: Vec<String>,
@ -19,28 +17,130 @@ pub struct SerialMinSettingsWidget
}
#[derive(Debug)]
pub enum SerialMinSettingsWidgetMessage
pub struct SerialMinSettingsUiInit
{
PortChanged(usize),
CustomPortChanged(String)
pub device: DeviceReference
}
pub struct SerialMinSettingsInit
{
// this needs a good design - we need to be able to modify the device instance, but
// we can't pass the reference or an Rc to the device due to the design of create_settings_widget.
// Either seperate the UI from the Device, or pass a middle man (sender/receiver style perhaps?)
}
pub struct SerialMinSettingsWidgets
pub struct SerialMinSettingsUiWidgets
{
custom_port_input: gtk::Entry
}
impl UiComponent for SerialMinSettingsUi
{
type Root = gtk::Box;
type Widgets = SerialMinSettingsUiWidgets;
type Init = SerialMinSettingsUiInit;
type State = Self;
fn build_root(_init: &Self::Init) -> Self::Root
{
gtk::Box::builder()
.hexpand(true)
.orientation(gtk::Orientation::Vertical)
.spacing(8)
.build()
}
fn build_widgets(root: &Self::Root, init: &Self::Init) -> Self::Widgets
{
let port_label = gtk::Label::builder()
.label(t!("serial_min.settings.port.label"))
.halign(gtk::Align::Start)
.build();
root.append(&port_label);
let port_model_vec: Vec<&str> = ports.iter().map(|p| p.as_str()).collect();
let port_model = StringList::new(&port_model_vec);
port_model.append(t!("serial_min.settings.port.custom").as_ref());
let port_select = gtk::DropDown::builder()
.model(&port_model)
.build();
root.append(&port_select);
let port_select_cloned = port_select.clone();
port_select.connect_selected_notify(clone!(
move |_|
{
let active_index = port_select_cloned.selected();
if active_index == gtk::ffi::GTK_INVALID_LIST_POSITION { return };
if let Ok(active_index_usize) = usize::try_from(active_index)
{
//sender.input(SerialMinSettingsWidgetMessage::PortChanged(active_index_usize));
}
}));
let custom_port_input = gtk::Entry::builder()
.hexpand(true)
.placeholder_text(t!("serial_min.settings.custom_port_placeholder"))
.build();
root.append(&custom_port_input);
let custom_port_input_cloned = custom_port_input.clone();
/*
custom_port_input.connect_changed(clone!(
@strong sender => move |_|
{
sender.input(SerialMinSettingsWidgetMessage::CustomPortChanged(String::from(custom_port_input_cloned.text().as_str())));
}
));
*/
Self::Widgets
{
custom_port_input
}
}
fn init(root: &Self::Root, state: &std::rc::Rc<std::cell::RefCell<Self::State>>)
{
.selected(ports.len().try_into().unwrap_or(gtk::ffi::GTK_INVALID_LIST_POSITION))
}
}
impl UiComponentState<SerialMinSettingsUi> for SerialMinSettingsUi
{
fn new(_init: <SerialMinSettingsUi as UiComponent>::Init, _widgets: <SerialMinSettingsUi as UiComponent>::Widgets) -> Self
{
let ports_list = serialport::available_ports().unwrap_or_default();
let ports: Vec<String> = ports_list.iter().map(|p| p.port_name.clone()).collect();
// TODO read settings
Self
{
ports,
custom_port: String::default(),
custom_port_visible: false,
tracker: 0
}
}
}
/*
impl SimpleComponent for SerialMinSettingsWidget
{
type Init = SerialMinSettingsInit;
@ -62,81 +162,7 @@ impl SimpleComponent for SerialMinSettingsWidget
fn init(_data: Self::Init, root: Self::Root, sender: ComponentSender<Self>, ) -> ComponentParts<Self>
{
let port_label = gtk::Label::builder()
.label(t!("serial_min.settings.port.label"))
.halign(gtk::Align::Start)
.build();
root.append(&port_label);
let ports_list = serialport::available_ports().unwrap_or(Vec::<SerialPortInfo>::new());
let ports: Vec<String> = ports_list.iter().map(|p| p.port_name.clone()).collect();
let port_model_vec: Vec<&str> = ports.iter().map(|p| p.as_str()).collect();
let port_model = StringList::new(&port_model_vec);
port_model.append(t!("serial_min.settings.port.custom").as_ref());
let port_select = gtk::DropDown::builder()
.model(&port_model)
.selected(ports.len().try_into().unwrap_or(gtk::ffi::GTK_INVALID_LIST_POSITION))
.build();
root.append(&port_select);
let port_select_cloned = port_select.clone();
port_select.connect_selected_notify(clone!(
@strong sender => move |_|
{
let active_index = port_select_cloned.selected();
if active_index == gtk::ffi::GTK_INVALID_LIST_POSITION { return };
if let Ok(active_index_usize) = usize::try_from(active_index)
{
sender.input(SerialMinSettingsWidgetMessage::PortChanged(active_index_usize));
}
}));
let custom_port_input = gtk::Entry::builder()
.hexpand(true)
.placeholder_text(t!("serial_min.settings.custom_port_placeholder"))
.build();
root.append(&custom_port_input);
let custom_port_input_cloned = custom_port_input.clone();
custom_port_input.connect_changed(clone!(
@strong sender => move |_|
{
sender.input(SerialMinSettingsWidgetMessage::CustomPortChanged(String::from(custom_port_input_cloned.text().as_str())));
}
));
let model = SerialMinSettingsWidget
{
ports,
custom_port: String::new(),
custom_port_visible: true,
tracker: 0
};
// TODO load settings
let widgets = SerialMinSettingsWidgets
{
custom_port_input
};
ComponentParts { model, widgets }
}
@ -180,4 +206,4 @@ impl EmbeddedWidgetConnector for Connector<SerialMinSettingsWidget>
{
self.widget().as_ref()
}
}
} */

53
Linux/ui-gtk/src/main.rs Normal file
View File

@ -0,0 +1,53 @@
use std::sync::Arc;
use std::sync::Mutex;
use env_logger::Env;
use gtk::glib;
use gtk::prelude::*;
use mainwindow::MainWindow;
use mainwindow::MainWindowInit;
use massiveknob_backend::orchestrator::Orchestrator;
use ui::uicomponent::UiComponent;
const APP_ID: &str = "com.github.mvrens.massiveknob";
#[macro_use]
extern crate rust_i18n;
i18n!("locales");
pub mod ui;
pub mod devices;
pub mod mainwindow;
fn main() -> glib::ExitCode
{
env_logger::Builder::from_env(Env::default().default_filter_or("info"))
// .format_timestamp(None)
.init();
let app = gtk::Application::builder()
.application_id(APP_ID)
.build();
app.connect_activate(activate);
app.run()
}
fn activate(app: &gtk::Application)
{
let orchestrator = Arc::new(Mutex::new(Orchestrator::new()));
let mainwindow = MainWindow::builder().build(MainWindowInit
{
app: app.clone(),
orchestrator: orchestrator.clone()
});
mainwindow.root.present();
}

View File

@ -0,0 +1,257 @@
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::Arc;
use std::sync::Mutex;
use gtk::glib;
use gtk::glib::clone;
use gtk::prelude::*;
use massiveknob_backend::orchestrator::Orchestrator;
use massiveknob_backend::util::unique_id::UniqueId;
use crate::devices::DeviceSettingsUiBuilder;
use crate::ui::uicomponent::UiComponent;
use crate::ui::uicomponent::UiComponentState;
pub struct MainWindowInit
{
pub app: gtk::Application,
pub orchestrator: Arc<Mutex<Orchestrator>>
}
pub struct MainWindow
{
orchestrator: Arc<Mutex<Orchestrator>>,
devices_sorted: Vec<SortedDevice>,
widgets: MainWindowWidgets,
device_settings_widget: Option<gtk::Widget>
}
pub struct MainWindowWidgets
{
device: MainWindowDeviceWidgets
}
pub struct MainWindowDeviceWidgets
{
devices_dropdown: gtk::DropDown,
settings_container: gtk::Box
}
impl UiComponent for MainWindow
{
type Root = gtk::ApplicationWindow;
type Widgets = MainWindowWidgets;
type Init = MainWindowInit;
type State = Self;
fn build_root(init: &Self::Init) -> Self::Root
{
gtk::ApplicationWindow::builder()
.application(&init.app)
.title(t!("mainwindow.title"))
.default_width(500)
.default_height(500)
.build()
}
fn build_widgets(root: &Self::Root, _init: &Self::Init) -> Self::Widgets
{
let tabs = gtk::Notebook::builder().build();
root.set_child(Some(&tabs));
Self::Widgets
{
device: Self::build_device_tab(&tabs)
}
}
fn init(_root: &Self::Root, state: &Rc<RefCell<Self::State>>)
{
{
let state_borrowed = state.borrow();
let devices_dropdown = state_borrowed.widgets.device.devices_dropdown.clone();
let orchestrator = state_borrowed.orchestrator.lock().unwrap();
let active_device_id = orchestrator.active_device_id();
let mut active_device_index = gtk::ffi::GTK_INVALID_LIST_POSITION;
let devices_dropdown_list = gtk::StringList::default();
for (index, device) in state_borrowed.devices_sorted.iter().enumerate()
{
devices_dropdown_list.append(device.name.as_str());
if let Some(device_id) = &active_device_id
{
if *device_id == device.unique_id
{
active_device_index = index as u32;
}
}
}
devices_dropdown.set_model(Some(&devices_dropdown_list));
devices_dropdown.set_selected(active_device_index);
devices_dropdown.connect_selected_notify(clone!(
#[weak]
state,
move |_|
{
let mut state = state.borrow_mut();
state.update_active_device(true);
}
));
}
let mut state = state.borrow_mut();
state.update_active_device(false);
}
}
impl UiComponentState<MainWindow> for MainWindow
{
fn new(init: MainWindowInit, widgets: MainWindowWidgets) -> Self
{
let mut devices_sorted: Vec<SortedDevice>;
{
let orchestrator = init.orchestrator.lock().unwrap();
devices_sorted = orchestrator.devices()
.map(|device| SortedDevice
{
unique_id: device.unique_id.clone(),
name: device.name()
})
.collect();
devices_sorted.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
}
Self
{
orchestrator: init.orchestrator.clone(),
devices_sorted,
widgets,
device_settings_widget: None
}
}
}
impl MainWindow
{
fn build_device_tab(tabs: &gtk::Notebook) -> MainWindowDeviceWidgets
{
//let sender = self.sender;
let tab = Self::build_box_tab(tabs, "mainwindow.tab.device");
let label = gtk::Label::builder()
.label(t!("mainwindow.deviceType.label"))
.halign(gtk::Align::Start)
.build();
tab.append(&label);
let devices_dropdown = gtk::DropDown::builder().build();
tab.append(&devices_dropdown);
let settings_container = gtk::Box::builder().build();
tab.append(&settings_container);
MainWindowDeviceWidgets
{
devices_dropdown,
settings_container
}
}
fn build_box_tab(notebook: &gtk::Notebook, title_key: &str) -> gtk::Box
{
let tab = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.spacing(8)
.margin_start(8)
.margin_end(8)
.margin_top(8)
.margin_bottom(8)
.build();
let tab_label = gtk::Label::builder()
.label(t!(title_key))
.build();
notebook.append_page(&tab, Some(&tab_label));
tab
}
fn update_active_device(&mut self, set_active: bool)
{
log::info!("update_active_device");
let active_index = self.widgets.device.devices_dropdown.selected();
if active_index == gtk::ffi::GTK_INVALID_LIST_POSITION { return };
let Ok(active_index_usize) = usize::try_from(active_index) else { return };
let selected_device = &self.devices_sorted[active_index_usize];
let device;
{
let mut orchestrator = self.orchestrator.lock().unwrap();
if set_active
{
device = Some(orchestrator.set_active_device_id(&selected_device.unique_id));
}
else
{
device = orchestrator.active_device();
}
}
if let Some(prev_widget) = &self.device_settings_widget
{
self.widgets.device.settings_container.remove(prev_widget);
}
if let Some(device) = device
{
let widget = DeviceSettingsUiBuilder::build(device.clone());
self.widgets.device.settings_container.append(&widget);
self.device_settings_widget = Some(widget);
}
else
{
self.device_settings_widget = None;
}
}
}
#[derive(Clone)]
struct SortedDevice
{
unique_id: UniqueId,
name: String
}

View File

@ -0,0 +1,8 @@
pub mod uicomponent;
#[deprecated]
pub trait EmbeddedWidgetConnector
{
fn root(&self) -> gtk::Widget;
}

View File

@ -0,0 +1,104 @@
// Much credit to Relm4 for the inspiration. I initially used it, but felt
// like I wasn't using much of the abstractions it provided. But some parts
// are still very useful and I recommend giving it a try for your project!
use gtk::prelude::*;
use std::cell::RefCell;
use std::marker::PhantomData;
use std::rc::Rc;
pub trait UiComponent : Sized
{
type Root : IsA<gtk::Widget>;
type Widgets;
type Init;
type State : UiComponentState<Self>;
fn builder() -> UiComponentBuilder<Self>
{
UiComponentBuilder::<Self>::default()
}
fn build_root(init: &Self::Init) -> Self::Root;
fn build_widgets(root: &Self::Root, init: &Self::Init) -> Self::Widgets;
fn init(root: &Self::Root, state: &Rc<RefCell<Self::State>>);
}
pub trait UiComponentState<C: UiComponent>
{
fn new(init: C::Init, widgets: C::Widgets) -> Self;
}
pub struct UiComponentConnector<C: UiComponent>
{
pub root: C::Root,
pub state: Rc<RefCell<C::State>>
}
pub trait UiComponentConnectorWidget
{
fn root(&self) -> gtk::Widget;
}
pub struct UiComponentBuilder<C: UiComponent>
{
marker: PhantomData<C>
}
impl<C: UiComponent> Default for UiComponentBuilder<C>
{
fn default() -> Self
{
Self
{
marker: PhantomData::<C>
}
}
}
impl<C: UiComponent> UiComponentBuilder<C>
{
pub fn build(&self, init: C::Init) -> UiComponentConnector<C>
{
let root = C::build_root(&init);
let widgets = C::build_widgets(&root, &init);
let state = Rc::new(RefCell::new(C::State::new(init, widgets)));
C::init(&root, &state);
UiComponentConnector::<C>
{
root,
state
}
}
}
impl<C: UiComponent> From<UiComponentConnector<C>> for gtk::Widget
{
fn from(val: UiComponentConnector<C>) -> Self
{
val.root.into()
}
}
impl<C: UiComponent> UiComponentConnectorWidget for UiComponentConnector<C>
{
fn root(&self) -> gtk::Widget
{
self.root.clone().into()
}
}