diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/actions.rs | 3 | ||||
| -rw-r--r-- | src/components/header.rs | 234 | ||||
| -rw-r--r-- | src/components/mod.rs | 16 | ||||
| -rw-r--r-- | src/handlers/mod.rs | 1 | ||||
| -rw-r--r-- | src/handlers/preferences.rs | 70 | ||||
| -rw-r--r-- | src/handlers/view.rs | 21 | ||||
| -rw-r--r-- | src/main.rs | 377 |
7 files changed, 417 insertions, 305 deletions
diff --git a/src/actions.rs b/src/actions.rs index 1d60820..33289b1 100644 --- a/src/actions.rs +++ b/src/actions.rs @@ -15,6 +15,7 @@ // along with this program. If not, see <http://www.gnu.org/licenses/>. use std::path::PathBuf; +use wmap_renderer::StageType; use crate::preferences::UserPreferences; @@ -43,7 +44,7 @@ impl ImageFormat { #[derive(Debug, Clone)] pub enum Action { ChangeOrientation, - StageTypeSelected(usize), + StageTypeSelected(StageType), SourceChanged, Zoom(f64), ZoomOut, diff --git a/src/components/header.rs b/src/components/header.rs new file mode 100644 index 0000000..7a39490 --- /dev/null +++ b/src/components/header.rs @@ -0,0 +1,234 @@ +use gtk::{gio, prelude::*}; +use relm4::prelude::*; +use relm4::actions::{ActionablePlus, RelmAction}; + +use wmap_renderer::StageType; + +use crate::tr; +use crate::preferences::models::{CustomStage, Template}; +use crate::stages::{ALL_STAGE_TYPES, LocalizedStageType}; + +use crate::{ + ChangeOrientationAction, ExportImageAction, actions::Action +}; +use crate::icon_names::shipped::{ + IMAGE_REGULAR, MENU_LARGE, SPLIT_HORIZONTAL_REGULAR, SPLIT_VERTICAL_REGULAR +}; + +// Model and Input Messages + +pub struct Header { + stage_type_list: gtk::StringList, + available_stage_types: Vec<StageType>, + orientation: gtk::Orientation, + horizontal_layout_enabled: bool, + templates_menu: gio::Menu, + main_menu: gio::Menu, + layout_action: RelmAction<ChangeOrientationAction> +} + +pub struct HeaderInit { + pub layout_action: RelmAction<ChangeOrientationAction>, + pub custom_stages: Vec<CustomStage>, + pub templates: Vec<Template>, + pub orientation: gtk::Orientation, + pub horizontal_layout_enabled: bool, +} + +#[derive(Debug, Clone)] +pub enum HeaderAction { + SetHorizontalLayoutEnabled(bool), + SetLayoutOrientation(gtk::Orientation), + SetTemplates(Vec<Template>), + SetCustomStages(Vec<CustomStage>), + + // Internal + StageTypeSelected(usize) +} + +#[relm4::component(pub)] +impl SimpleComponent for Header { + type Init = HeaderInit; + type Input = HeaderAction; + type Output = Action; + + view! { + adw::HeaderBar { + pack_start = &adw::ToolbarView { + gtk::Button::with_label(&match model.orientation { + gtk::Orientation::Horizontal => tr!("command.view.use_vertical_layout"), + _ => tr!("command.view.use_horizontal_layout") + }) { + #[watch] + set_icon_name: match model.orientation { + gtk::Orientation::Horizontal => SPLIT_VERTICAL_REGULAR, + _ => SPLIT_HORIZONTAL_REGULAR + }, + #[watch] + set_visible: model.horizontal_layout_enabled, + ActionablePlus::set_stateless_action::<ChangeOrientationAction>: &(), + } + }, + pack_end = >k::Box { + + gtk::Button::with_label(&tr!("command.file.export")) { + set_icon_name: IMAGE_REGULAR, + ActionablePlus::set_stateless_action::<ExportImageAction>: &(), + }, + + gtk::DropDown { + set_model: Some(&model.stage_type_list), + set_show_arrow: true, + set_selected: 0, + connect_selected_notify[sender] => move |dropdown| { + let selected = dropdown.selected(); + sender.input(HeaderAction::StageTypeSelected(selected as usize)); + }, + }, + gtk::MenuButton { + set_icon_name: MENU_LARGE, + #[wrap(Some)] + set_popover = >k::PopoverMenu::from_model(Some(&model.main_menu)) {} + } + } + } + } + + fn init( + init: Self::Init, + root: Self::Root, + sender: ComponentSender<Self>, + ) -> ComponentParts<Self> { + let (templates_menu, main_menu) = Self::init_main_menu(&init.templates); + + let stage_type_list = Self::init_stage_type_list(); + + let mut model = Header { + stage_type_list, + available_stage_types: ALL_STAGE_TYPES.to_vec(), + orientation: init.orientation, + horizontal_layout_enabled: init.horizontal_layout_enabled, + layout_action: init.layout_action, + templates_menu, + main_menu + }; + let widgets = view_output!(); + + model.rebuild_custom_stages(&init.custom_stages); + + ComponentParts { model, widgets } + } + + fn update(&mut self, message: Self::Input, sender: ComponentSender<Self>) { + match message { + HeaderAction::SetHorizontalLayoutEnabled(is_enabled) => { + self.horizontal_layout_enabled = is_enabled; + self.layout_action.set_enabled(is_enabled); + }, + HeaderAction::SetLayoutOrientation(orientation) => self.orientation = orientation, + HeaderAction::SetTemplates(templates) => { + self.templates_menu.remove_all(); + for template in &templates { + self.templates_menu.append( + Some(&template.name), + Some(&format!("window.new-from-template::{}", template.id)), + ); + } + }, + HeaderAction::SetCustomStages(stages) => { + self.rebuild_custom_stages(&stages); + }, + HeaderAction::StageTypeSelected(index) => { + if index < self.available_stage_types.len() + && let Some(stage_type) = self.available_stage_types.get(index) + { + let _ = sender.output(Action::StageTypeSelected(stage_type.clone())); + } + }, + } + } +} + +impl Header { + fn init_main_menu(templates: &Vec<Template>) -> (gio::Menu, gio::Menu) { + let templates_menu = gio::Menu::new(); + for template in templates { + templates_menu.append( + Some(&template.name), + Some(&format!("window.new-from-template::{}", template.id)), + ); + } + + let main_menu = gio::Menu::new(); + main_menu.append(Some(&tr!("command.file.new")), Some("window.new")); + main_menu.append_submenu( + Some(&tr!("command.file.new_from_template")), + &templates_menu, + ); + main_menu.append(Some(&tr!("command.file.open")), Some("window.open")); + main_menu.append(Some(&tr!("command.file.save")), Some("window.save")); + main_menu.append(Some(&tr!("command.file.save_as")), Some("window.save-as")); + main_menu.append(Some(&tr!("command.file.close")), Some("window.close")); + main_menu.append( + Some(&tr!("command.file.export")), + Some("window.export-image"), + ); + + let layout_section = gio::Menu::new(); + layout_section.append( + Some(&tr!("command.view.use_vertical_layout")), + Some("window.change-orientation"), + ); + main_menu.append_section(None, &layout_section); + + let zoom_section = gio::Menu::new(); + zoom_section.append(Some(&tr!("command.view.zoom_in")), Some("window.zoom-in")); + zoom_section.append(Some(&tr!("command.view.zoom_out")), Some("window.zoom-out")); + main_menu.append_section(None, &zoom_section); + + let prefs_section = gio::Menu::new(); + prefs_section.append( + Some(&tr!("command.application.preferences")), + Some("window.preferences"), + ); + main_menu.append_section(None, &prefs_section); + + (templates_menu, main_menu) + } + + fn init_stage_type_list() -> gtk::StringList { + let stage_types: Vec<String> = ALL_STAGE_TYPES + .iter() + .map(LocalizedStageType::localized_name) + .collect(); + let stage_type_refs: Vec<&str> = stage_types + .iter() + .map(std::string::String::as_str) + .collect(); + gtk::StringList::new(&stage_type_refs) + } + + fn rebuild_custom_stages(&mut self, stages: &Vec<CustomStage>) { + let mut stage_types: Vec<StageType> = ALL_STAGE_TYPES.to_vec(); + + for custom in stages { + stage_types.push(StageType::Custom { + name: custom.name.clone(), + i: custom.stage.i.clone(), + ii: custom.stage.ii.clone(), + iii: custom.stage.iii.clone(), + iv: custom.stage.iv.clone(), + }); + } + + while self.stage_type_list.n_items() > 0 { + self.stage_type_list.remove(0); + } + + for stage_type in &stage_types { + self.stage_type_list.append(&stage_type.localized_name()); + } + + self.available_stage_types = stage_types; + } +} diff --git a/src/components/mod.rs b/src/components/mod.rs new file mode 100644 index 0000000..d32bbea --- /dev/null +++ b/src/components/mod.rs @@ -0,0 +1,16 @@ +// Map, wardley map editor for linux +// Copyright (C) 2026 Rubén Beltrán del Río + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. +pub mod header; diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 235afc2..4e53195 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -15,5 +15,6 @@ // along with this program. If not, see <http://www.gnu.org/licenses/>. pub mod export; pub mod file; +pub mod preferences; pub mod view; pub mod zoom; diff --git a/src/handlers/preferences.rs b/src/handlers/preferences.rs new file mode 100644 index 0000000..f915898 --- /dev/null +++ b/src/handlers/preferences.rs @@ -0,0 +1,70 @@ +// Map, wardley map editor for linux +// Copyright (C) 2026 Rubén Beltrán del Río + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. +use relm4::prelude::*; +use adw::prelude::*; + +use crate::file_registry; +use crate::preferences::{PreferencesWindow, UserPreferences, storage, window}; +use crate::actions::Action; +use crate::components::header::HeaderAction; + +use crate::AppModel; + +/// Shows the preferences window. +pub fn show(model: &mut AppModel, sender: &ComponentSender<AppModel>) { + if model.preferences_window.is_none() { + let controller = PreferencesWindow::builder() + .transient_for(&model.window) + .launch(model.preferences.clone()) + .forward(sender.input_sender(), |output| match output { + window::PreferencesOutput::PreferencesChanged(preferences) => { + Action::PreferencesChanged(preferences) + } + window::PreferencesOutput::Closed => { + Action::PreferencesWindowClosed + } + }); + model.preferences_window = Some(controller); + } + if let Some(ref controller) = model.preferences_window { + controller.widget().present(); + } +} + +/// Closes the preferences window. +pub fn close(model: &mut AppModel) { + model.preferences_window = None; +} + +/// Reloads the preferences. +pub fn reload(model: &mut AppModel) { + model.preferences = storage::load(); + model.apply_preferences(); + model.header.emit(HeaderAction::SetCustomStages(model.preferences.custom_stages.clone())); + model.header.emit(HeaderAction::SetCustomStages(model.preferences.custom_stages.clone())); + model.header.emit(HeaderAction::SetTemplates(model.preferences.map_templates.clone())); + model.update_image(); +} + +/// Updates preferences after they change. +pub fn update(model: &mut AppModel, preferences: UserPreferences) { + model.preferences = preferences; + model.apply_preferences(); + model.header.emit(HeaderAction::SetCustomStages(model.preferences.custom_stages.clone())); + model.header.emit(HeaderAction::SetTemplates(model.preferences.map_templates.clone())); + model.update_image(); + file_registry::broadcast_to_all_windows(&Action::ReloadPreferences); +} diff --git a/src/handlers/view.rs b/src/handlers/view.rs index 660f7b9..87c4172 100644 --- a/src/handlers/view.rs +++ b/src/handlers/view.rs @@ -13,9 +13,11 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see <http://www.gnu.org/licenses/>. -use relm4::gtk; +use relm4::prelude::*; +use crate::components::header::HeaderAction; use crate::AppModel; +use wmap_renderer::StageType; /// Toggles between horizontal and vertical layout orientation. pub fn change_orientation(model: &mut AppModel) { @@ -23,14 +25,17 @@ pub fn change_orientation(model: &mut AppModel) { gtk::Orientation::Horizontal => gtk::Orientation::Vertical, _ => gtk::Orientation::Horizontal, }; + model.header.emit(HeaderAction::SetLayoutOrientation(model.orientation)); } /// Updates the selected stage type from dropdown selection. -pub fn stage_type_selected(model: &mut AppModel, index: usize) { - if index < model.available_stage_types.len() - && let Some(stage_type) = model.available_stage_types.get(index) - { - model.stage_type = stage_type.clone(); - model.update_image(); - } +pub fn stage_type_selected(model: &mut AppModel, stage_type: StageType) { + model.stage_type = stage_type; + model.update_image(); +} + +/// Updates the selected stage type from dropdown selection. +pub fn change_horizontal_layout(model: &mut AppModel, is_enabled: bool) { + model.horizontal_layout_enabled = is_enabled; + model.header.emit(HeaderAction::SetHorizontalLayoutEnabled(is_enabled)); } diff --git a/src/main.rs b/src/main.rs index e1d3bc9..c9393f4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,6 +26,7 @@ mod i18n; mod preferences; mod stages; mod ui_helpers; +mod components; mod icon_names { #![allow(clippy::doc_markdown)] // Generated code from relm4_icons_build @@ -33,29 +34,53 @@ mod icon_names { } use std::cell::RefCell; +use std::convert::identity; use std::path::PathBuf; use crate::actions::Action; -use crate::icon_names::shipped::{ - IMAGE_REGULAR, MENU_LARGE, SPLIT_HORIZONTAL_REGULAR, SPLIT_VERTICAL_REGULAR, ZOOM_IN_REGULAR, - ZOOM_OUT_REGULAR, -}; +use crate::icon_names::shipped::{ ZOOM_IN_REGULAR, ZOOM_OUT_REGULAR }; use cairo::ImageSurface; -use gtk::gio; use gtk::glib; -use relm4::{ - Component, ComponentController, ComponentParts, ComponentSender, RelmApp, RelmWidgetExt, - SimpleComponent, - actions::{AccelsPlus, ActionablePlus, RelmAction, RelmActionGroup}, - gtk, adw -}; +use relm4::prelude::*; +use relm4::actions::{AccelsPlus, ActionablePlus, RelmAction, RelmActionGroup}; use adw::prelude::*; + use sourceview5::LanguageManager; use sourceview5::prelude::*; -use stages::{ALL_STAGE_TYPES, LocalizedStageType}; + +use stages::LocalizedStageType; use wmap_parser::{Map, parse}; use wmap_renderer::{Configuration, StageType, render_to_surface}; +use components::header::{Header, HeaderInit}; + +/// Macro to reduce boilerplate when registering actions with accelerators. +macro_rules! register_action { + ($group:expr, $app:expr, $sender:expr, $action_type:ty, $message:expr, $accelerators:expr) => {{ + let sender = $sender.clone(); + let action: RelmAction<$action_type> = RelmAction::new_stateless(move |_| { + let _ = sender.input($message); + }); + $group.add_action(action.clone()); + $app.set_accelerators_for_action::<$action_type>($accelerators); + action + }}; +} + +/// Macro to further reduce the boilerplate, by allowing terser registration. +macro_rules! register_actions { + ($group:expr, $app:expr, $sender:expr, [ + $( $action_type:ty => $message:expr, $accelerators:expr );+ $(;)? + ]) => {{ + $( + register_action!( + $group, $app, $sender, + $action_type, $message, $accelerators + ); + )+ + }}; +} + // Action group and action type declarations relm4::new_action_group!(WindowActionGroup, "window"); relm4::new_stateless_action!(NewAction, WindowActionGroup, "new"); @@ -82,18 +107,6 @@ impl relm4::actions::ActionName for NewFromTemplateAction { const NAME: &'static str = "new-from-template"; } -/// Macro to reduce boilerplate when registering actions with accelerators. -macro_rules! register_action { - ($group:expr, $app:expr, $sender:expr, $action_type:ty, $message:expr, $accelerators:expr) => {{ - let sender = $sender.clone(); - let action: RelmAction<$action_type> = RelmAction::new_stateless(move |_| { - sender.input($message); - }); - $group.add_action(action.clone()); - $app.set_accelerators_for_action::<$action_type>($accelerators); - action - }}; -} struct AppInit { zoom: f64, @@ -103,8 +116,6 @@ struct AppInit { struct AppModel { orientation: gtk::Orientation, - stage_type_list: gtk::StringList, - available_stage_types: Vec<StageType>, stage_type: StageType, source: sourceview5::Buffer, source_view: sourceview5::View, @@ -123,10 +134,9 @@ struct AppModel { preferences: preferences::UserPreferences, preferences_window: Option<relm4::Controller<preferences::PreferencesWindow>>, editor_css_provider: gtk::CssProvider, - templates_menu: gio::Menu, - main_menu: gio::Menu, horizontal_layout_enabled: bool, - layout_action: RelmAction<ChangeOrientationAction> + // Child Components + header: Controller<Header> } impl AppModel { @@ -222,40 +232,6 @@ impl AppModel { } } - fn rebuild_stage_type_list(&mut self) { - let mut stage_types: Vec<StageType> = ALL_STAGE_TYPES.to_vec(); - - for custom in &self.preferences.custom_stages { - stage_types.push(StageType::Custom { - name: custom.name.clone(), - i: custom.stage.i.clone(), - ii: custom.stage.ii.clone(), - iii: custom.stage.iii.clone(), - iv: custom.stage.iv.clone(), - }); - } - - while self.stage_type_list.n_items() > 0 { - self.stage_type_list.remove(0); - } - - for stage_type in &stage_types { - self.stage_type_list.append(&stage_type.localized_name()); - } - - self.available_stage_types = stage_types; - } - - fn rebuild_templates_menu(&self) { - self.templates_menu.remove_all(); - for template in &self.preferences.map_templates { - self.templates_menu.append( - Some(&template.name), - Some(&format!("window.new-from-template::{}", template.id)), - ); - } - } - pub fn open_new_window(file_path: Option<PathBuf>, initial_content: Option<String>) { let controller = AppModel::builder() .launch(AppInit { @@ -273,18 +249,6 @@ impl AppModel { file_registry::store_controller(controller); } - fn init_stage_type_list() -> gtk::StringList { - let stage_types: Vec<String> = ALL_STAGE_TYPES - .iter() - .map(stages::LocalizedStageType::localized_name) - .collect(); - let stage_type_refs: Vec<&str> = stage_types - .iter() - .map(std::string::String::as_str) - .collect(); - gtk::StringList::new(&stage_type_refs) - } - fn init_source_buffer( init: &AppInit, sender: &ComponentSender<Self>, @@ -342,51 +306,6 @@ impl AppModel { (drawing_area, map, render_configuration, stage_type, surface) } - fn init_main_menu(prefs: &preferences::UserPreferences) -> (gio::Menu, gio::Menu) { - let templates_menu = gio::Menu::new(); - for template in &prefs.map_templates { - templates_menu.append( - Some(&template.name), - Some(&format!("window.new-from-template::{}", template.id)), - ); - } - - let main_menu = gio::Menu::new(); - main_menu.append(Some(&tr!("command.file.new")), Some("window.new")); - main_menu.append_submenu( - Some(&tr!("command.file.new_from_template")), - &templates_menu, - ); - main_menu.append(Some(&tr!("command.file.open")), Some("window.open")); - main_menu.append(Some(&tr!("command.file.save")), Some("window.save")); - main_menu.append(Some(&tr!("command.file.save_as")), Some("window.save-as")); - main_menu.append(Some(&tr!("command.file.close")), Some("window.close")); - main_menu.append( - Some(&tr!("command.file.export")), - Some("window.export-image"), - ); - - let layout_section = gio::Menu::new(); - layout_section.append( - Some(&tr!("command.view.use_vertical_layout")), - Some("window.change-orientation"), - ); - main_menu.append_section(None, &layout_section); - - let zoom_section = gio::Menu::new(); - zoom_section.append(Some(&tr!("command.view.zoom_in")), Some("window.zoom-in")); - zoom_section.append(Some(&tr!("command.view.zoom_out")), Some("window.zoom-out")); - main_menu.append_section(None, &zoom_section); - - let prefs_section = gio::Menu::new(); - prefs_section.append( - Some(&tr!("command.application.preferences")), - Some("window.preferences"), - ); - main_menu.append_section(None, &prefs_section); - - (templates_menu, main_menu) - } } #[relm4::component] @@ -403,49 +322,8 @@ impl SimpleComponent for AppModel { set_default_width: constants::DEFAULT_WINDOW_WIDTH, set_default_height: constants::DEFAULT_WINDOW_HEIGHT, - gtk::Box { - set_orientation: gtk::Orientation::Vertical, - set_spacing: 0, - - adw::HeaderBar { - pack_start = &adw::ToolbarView { - gtk::Button::with_label(&match model.orientation { - gtk::Orientation::Horizontal => tr!("command.view.use_vertical_layout"), - _ => tr!("command.view.use_horizontal_layout") - }) { - #[watch] - set_icon_name: match model.orientation { - gtk::Orientation::Horizontal => SPLIT_VERTICAL_REGULAR, - _ => SPLIT_HORIZONTAL_REGULAR - }, - #[watch] - set_visible: model.horizontal_layout_enabled, - ActionablePlus::set_stateless_action::<ChangeOrientationAction>: &(), - } - }, - pack_end = &adw::ToolbarView { - - gtk::Button::with_label(&tr!("command.file.export")) { - set_icon_name: IMAGE_REGULAR, - ActionablePlus::set_stateless_action::<ExportImageAction>: &(), - }, - - gtk::DropDown { - set_model: Some(&model.stage_type_list), - set_show_arrow: true, - set_selected: 0, - connect_selected_notify[sender] => move |dropdown| { - let selected = dropdown.selected(); - sender.input(Action::StageTypeSelected(selected as usize)); - }, - }, - gtk::MenuButton { - set_icon_name: MENU_LARGE, - #[wrap(Some)] - set_popover = >k::PopoverMenu::from_model(Some(&model.main_menu)) {} - } - } - }, + adw::ToolbarView { + add_top_bar = model.header.widget(), gtk::Paned { #[watch] @@ -477,7 +355,7 @@ impl SimpleComponent for AppModel { } }, - gtk::ActionBar { + add_bottom_bar = >k::ActionBar { pack_end = >k::Box { set_orientation: gtk::Orientation::Horizontal, @@ -528,7 +406,6 @@ impl SimpleComponent for AppModel { root: Self::Root, sender: ComponentSender<Self>, ) -> ComponentParts<Self> { - let stage_type_list = Self::init_stage_type_list(); let (source, initial_content, current_file) = Self::init_source_buffer(&init, &sender); @@ -552,30 +429,26 @@ impl SimpleComponent for AppModel { file_registry::register_file(path, &root); } - let prefs = preferences::storage::load(); - let (templates_menu, main_menu) = Self::init_main_menu(&prefs); + let preferences = preferences::storage::load(); - // Actions + let orientation = gtk::Orientation::Horizontal; + let horizontal_layout_enabled = false; + // Actions let application = relm4::main_application(); let mut action_group = RelmActionGroup::<WindowActionGroup>::new(); - register_action!( - action_group, - application, - sender, - ZoomInAction, - Action::ZoomIn, - &["<Primary>equal", "<Primary>plus"] - ); - register_action!( - action_group, - application, - sender, - ZoomOutAction, - Action::ZoomOut, - &["<Primary>minus"] - ); + register_actions!(action_group, application, sender, [ + ZoomInAction => Action::ZoomIn, &["<Primary>equal", "<Primary>plus"]; + ZoomOutAction => Action::ZoomOut, &["<Primary>minus"]; + ExportImageAction => Action::ExportImage, &["<Primary>e"]; + NewAction => Action::New, &["<Primary>n"]; + SaveAction => Action::Save { close_after: false }, &["<Primary>s"]; + SaveAsAction => Action::SaveAs { close_after: false }, &["<Primary><Shift>s"]; + OpenAction => Action::Open, &["<Primary>o"]; + CloseAction => Action::CloseWindow, &["<Primary>w"]; + PreferencesAction => Action::ShowPreferences, &["<Primary>comma"] + ]); let layout_action = register_action!( action_group, application, @@ -584,62 +457,6 @@ impl SimpleComponent for AppModel { Action::ChangeOrientation, &["<Primary>l"] ); - register_action!( - action_group, - application, - sender, - ExportImageAction, - Action::ExportImage, - &["<Primary>e"] - ); - register_action!( - action_group, - application, - sender, - NewAction, - Action::New, - &["<Primary>n"] - ); - register_action!( - action_group, - application, - sender, - SaveAction, - Action::Save { close_after: false }, - &["<Primary>s"] - ); - register_action!( - action_group, - application, - sender, - SaveAsAction, - Action::SaveAs { close_after: false }, - &["<Primary><Shift>s"] - ); - register_action!( - action_group, - application, - sender, - OpenAction, - Action::Open, - &["<Primary>o"] - ); - register_action!( - action_group, - application, - sender, - CloseAction, - Action::CloseWindow, - &["<Primary>w"] - ); - register_action!( - action_group, - application, - sender, - PreferencesAction, - Action::ShowPreferences, - &["<Primary>comma"] - ); let sender_clone = sender.clone(); let action: RelmAction<NewFromTemplateAction> = @@ -649,10 +466,20 @@ impl SimpleComponent for AppModel { action_group.add_action(action); action_group.register_for_widget(&root); + + // Child Components + let header = Header::builder() + .launch(HeaderInit { + layout_action, + custom_stages: preferences.custom_stages.clone(), + templates: preferences.map_templates.clone(), + orientation, + horizontal_layout_enabled + }) + .forward(sender.input_sender(), identity); + let mut model = AppModel { - orientation: gtk::Orientation::Horizontal, - stage_type_list, - available_stage_types: ALL_STAGE_TYPES.to_vec(), + orientation, stage_type, source, source_view: source_view.clone(), @@ -668,17 +495,14 @@ impl SimpleComponent for AppModel { pending_load_events: RefCell::new(0), initialized: false, window: root.clone(), - preferences: prefs, + preferences, preferences_window: None, editor_css_provider, - templates_menu, - main_menu, horizontal_layout_enabled: true, - layout_action + header }; model.apply_preferences(); - model.rebuild_stage_type_list(); model.update_image(); file_registry::register_sender(sender.input_sender().clone()); @@ -723,7 +547,7 @@ impl SimpleComponent for AppModel { fn update(&mut self, message: Self::Input, sender: ComponentSender<Self>) { match message { Action::ChangeOrientation => handlers::view::change_orientation(self), - Action::StageTypeSelected(index) => handlers::view::stage_type_selected(self, index), + Action::StageTypeSelected(stage_type) => handlers::view::stage_type_selected(self, stage_type), Action::SourceChanged => { let pending = *self.pending_load_events.borrow(); @@ -785,54 +609,15 @@ impl SimpleComponent for AppModel { Action::MarkInitialized => self.initialized = true, // Layout Transition - Action::DisableHorizontalLayout => { - self.horizontal_layout_enabled = false; - self.layout_action.set_enabled(false); - }, - Action::EnableHorizontalLayout => { - self.horizontal_layout_enabled = true; - self.layout_action.set_enabled(true); - }, + Action::DisableHorizontalLayout => handlers::view::change_horizontal_layout(self, false), + Action::EnableHorizontalLayout => handlers::view::change_horizontal_layout(self, true), // Preferences - Action::ShowPreferences => { - if self.preferences_window.is_none() { - let controller = preferences::PreferencesWindow::builder() - .transient_for(&self.window) - .launch(self.preferences.clone()) - .forward(sender.input_sender(), |output| match output { - preferences::window::PreferencesOutput::PreferencesChanged(prefs) => { - Action::PreferencesChanged(prefs) - } - preferences::window::PreferencesOutput::Closed => { - Action::PreferencesWindowClosed - } - }); - self.preferences_window = Some(controller); - } - if let Some(ref controller) = self.preferences_window { - controller.widget().present(); - } - } - Action::PreferencesChanged(prefs) => { - self.preferences = prefs; - self.apply_preferences(); - self.rebuild_stage_type_list(); - self.rebuild_templates_menu(); - self.update_image(); - file_registry::broadcast_to_all_windows(&Action::ReloadPreferences); - } - Action::PreferencesWindowClosed => { - self.preferences_window = None; - } - Action::ReloadPreferences => { - self.preferences = preferences::storage::load(); - self.apply_preferences(); - self.rebuild_stage_type_list(); - self.rebuild_templates_menu(); - self.update_image(); - } + Action::ShowPreferences => handlers::preferences::show(self, &sender), + Action::PreferencesChanged(preferences) => handlers::preferences::update(self, preferences), + Action::PreferencesWindowClosed => handlers::preferences::close(self), + Action::ReloadPreferences => handlers::preferences::reload(self), } } } @@ -842,8 +627,8 @@ fn main() { relm4_icons::initialize_icons(icon_names::GRESOURCE_BYTES, icon_names::RESOURCE_PREFIX); - let prefs = preferences::storage::load(); - let initial_content = prefs + let preferences = preferences::storage::load(); + let initial_content = preferences .map_templates .iter() .find(|t| t.is_default) |