diff --git a/testangel/locales/en/main.ftl b/testangel/locales/en/main.ftl index 338f943..43f10b9 100644 --- a/testangel/locales/en/main.ftl +++ b/testangel/locales/en/main.ftl @@ -94,6 +94,12 @@ action-save-open-error-serializing-error = The action could not be saved due to action-save-open-error-action-not-version-compatible = The action you tried to load is not compatible with this version of { app-name }. action-save-open-error-missing-instruction = The instruction for step { $step } (with internal ID: { $error }) in this action is missing. +action-metadata-label = Action Metadata +action-metadata-name = Action Name +action-metadata-group = Action Group +action-metadata-author = Author +action-metadata-description = Description +action-metadata-visible = Visible in Flow Editor action-step-label = Step { $step }: { $name } # Execution diff --git a/testangel/src/next_ui/actions/header.rs b/testangel/src/next_ui/actions/header.rs index 57b2629..f93d628 100644 --- a/testangel/src/next_ui/actions/header.rs +++ b/testangel/src/next_ui/actions/header.rs @@ -105,8 +105,10 @@ impl Component for ActionsHeader { gtk::Button { set_icon_name: relm4_icons::icon_name::PLAY, set_tooltip: &lang::lookup("action-header-run"), - #[watch] - set_sensitive: model.action_open, + // TODO uncomment when execution dialog ready + //#[watch] + //set_sensitive: model.action_open, + set_sensitive: false, connect_clicked[sender] => move |_| { // unwrap rationale: receivers will never be dropped sender.output(ActionsHeaderOutput::RunAction).unwrap(); diff --git a/testangel/src/next_ui/actions/instruction_component.rs b/testangel/src/next_ui/actions/instruction_component.rs index ff8f874..79dddb8 100644 --- a/testangel/src/next_ui/actions/instruction_component.rs +++ b/testangel/src/next_ui/actions/instruction_component.rs @@ -87,7 +87,6 @@ impl FactoryComponent for InstructionComponent { view! { root = gtk::Box { - set_margin_all: 5, set_orientation: gtk::Orientation::Vertical, set_spacing: 5, diff --git a/testangel/src/next_ui/actions/metadata_component.rs b/testangel/src/next_ui/actions/metadata_component.rs new file mode 100644 index 0000000..dfd3c36 --- /dev/null +++ b/testangel/src/next_ui/actions/metadata_component.rs @@ -0,0 +1,129 @@ +use relm4::{adw, gtk, Component}; +use adw::prelude::*; +use testangel::types::Action; + +use crate::next_ui::lang; + +#[derive(Debug)] +pub enum MetadataInput { + /// Inform the metadata component that the action has changed and as such + /// it should reload the metadata values + ChangeAction(Action), +} + +#[derive(Clone, Debug, Default)] +pub struct MetadataOutput { + pub new_name: Option, + pub new_group: Option, + pub new_author: Option, + pub new_description: Option, + pub new_visible: Option, +} + +#[derive(Debug)] +pub struct Metadata; + +#[relm4::component(pub)] +impl Component for Metadata { + type Init = (); + type Input = MetadataInput; + type Output = MetadataOutput; + type CommandOutput = (); + + view! { + adw::PreferencesGroup { + set_title: &lang::lookup("action-metadata-label"), + + #[name = "name"] + adw::EntryRow { + set_title: &lang::lookup("action-metadata-name"), + + connect_changed[sender] => move |entry| { + let _ = sender.output(MetadataOutput { + new_name: Some(entry.text().to_string()), + ..Default::default() + }); + }, + }, + #[name = "group"] + adw::EntryRow { + set_title: &lang::lookup("action-metadata-group"), + + connect_changed[sender] => move |entry| { + let _ = sender.output(MetadataOutput { + new_group: Some(entry.text().to_string()), + ..Default::default() + }); + }, + }, + #[name = "author"] + adw::EntryRow { + set_title: &lang::lookup("action-metadata-author"), + + connect_changed[sender] => move |entry| { + let _ = sender.output(MetadataOutput { + new_author: Some(entry.text().to_string()), + ..Default::default() + }); + }, + }, + #[name = "description"] + adw::EntryRow { + set_title: &lang::lookup("action-metadata-description"), + + connect_changed[sender] => move |entry| { + let _ = sender.output(MetadataOutput { + new_description: Some(entry.text().to_string()), + ..Default::default() + }); + }, + }, + adw::ActionRow { + set_title: &lang::lookup("action-metadata-visible"), + + #[name = "visible"] + add_suffix = >k::Switch { + connect_state_set[sender] => move |_switch, state| { + let _ = sender.output(MetadataOutput { + new_visible: Some(state), + ..Default::default() + }); + gtk::Inhibit(false) + }, + }, + + } + }, + } + + fn init( + _init: Self::Init, + root: &Self::Root, + sender: relm4::ComponentSender, + ) -> relm4::ComponentParts { + let model = Metadata; + let widgets = view_output!(); + + relm4::ComponentParts { model, widgets } + } + + fn update_with_view( + &mut self, + widgets: &mut Self::Widgets, + message: Self::Input, + sender: relm4::ComponentSender, + _root: &Self::Root, + ) { + match message { + MetadataInput::ChangeAction(action) => { + widgets.name.set_text(&action.friendly_name); + widgets.group.set_text(&action.group); + widgets.author.set_text(&action.author); + widgets.description.set_text(&action.description); + widgets.visible.set_active(action.visible); + } + } + + self.update_view(widgets, sender) + } +} diff --git a/testangel/src/next_ui/actions/mod.rs b/testangel/src/next_ui/actions/mod.rs index b4c2fb1..90ddf3a 100644 --- a/testangel/src/next_ui/actions/mod.rs +++ b/testangel/src/next_ui/actions/mod.rs @@ -16,6 +16,7 @@ use super::{file_filters, lang}; mod instruction_component; mod execution_dialog; pub mod header; +mod metadata_component; pub enum SaveOrOpenActionError { IoError(std::io::Error), @@ -110,6 +111,8 @@ pub enum ActionInputs { /// The [`InstructionConfiguration`] has changed for the step indicated by the [`DynamicIndex`]. /// This does not refresh the UI. ConfigUpdate(DynamicIndex, InstructionConfiguration), + /// The metadata has been updated and the action should be updated to reflect that + MetadataUpdated(metadata_component::MetadataOutput), } #[derive(Clone, Debug)] pub enum ActionOutputs { @@ -126,6 +129,7 @@ pub struct ActionsModel { open_path: Option, needs_saving: bool, header: Rc>, + metadata: Controller, live_instructions_list: FactoryVecDeque, execution_dialog: Option>, @@ -198,10 +202,12 @@ impl ActionsModel { )) } } - self.open_action = Some(action); + + self.open_action = Some(action.clone()); self.header.emit(header::ActionsHeaderInput::ChangeActionOpen( self.open_action.is_some(), )); + self.metadata.emit(metadata_component::MetadataInput::ChangeAction(action)); self.open_path = Some(file); self.needs_saving = false; log::debug!("New action open."); @@ -314,25 +320,32 @@ impl Component for ActionsModel { set_vexpand: true, set_hscrollbar_policy: gtk::PolicyType::Never, - gtk::Box { - set_orientation: gtk::Orientation::Vertical, - set_margin_all: 5, - + if model.open_action.is_none() { adw::StatusPage { set_title: &lang::lookup("nothing-open"), set_description: Some(&lang::lookup("action-nothing-open-description")), set_icon_name: Some(relm4_icons::icon_name::LIGHTBULB), - #[watch] - set_visible: model.open_action.is_none(), set_vexpand: true, - }, - - #[local_ref] - live_instructions_list -> gtk::Box { + } + } else { + gtk::Box { set_orientation: gtk::Orientation::Vertical, - set_spacing: 5, - }, - }, + set_margin_all: 10, + set_spacing: 10, + + model.metadata.widget(), + + gtk::Separator { + set_orientation: gtk::Orientation::Horizontal, + }, + + #[local_ref] + live_instructions_list -> gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_spacing: 5, + }, + } + } }, }, } @@ -365,6 +378,7 @@ impl Component for ActionsModel { execution_dialog: None, header, live_instructions_list: FactoryVecDeque::new(gtk::Box::default(), sender.input_sender()), + metadata: metadata_component::Metadata::builder().launch(()).forward(sender.input_sender(), |msg| ActionInputs::MetadataUpdated(msg)), }; // Trigger update actions from model @@ -385,6 +399,27 @@ impl Component for ActionsModel { ) { match message { ActionInputs::NoOp => (), + + ActionInputs::MetadataUpdated(meta) => { + if let Some(action) = self.open_action.as_mut() { + if let Some(new_name) = meta.new_name { + action.friendly_name = new_name; + } + if let Some(new_group) = meta.new_group { + action.group = new_group; + } + if let Some(new_author) = meta.new_author { + action.author = new_author; + } + if let Some(new_description) = meta.new_description { + action.description = new_description; + } + if let Some(new_visible) = meta.new_visible { + action.visible = new_visible; + } + } + } + ActionInputs::ActionsMapChanged(new_map) => { self.action_map = new_map.clone(); self.header.emit(header::ActionsHeaderInput::ActionsMapChanged(new_map));