diff --git a/src/components/alert.rs b/src/components/alert.rs new file mode 100644 index 0000000..3c6908d --- /dev/null +++ b/src/components/alert.rs @@ -0,0 +1,124 @@ +use relm4::{ComponentParts, ComponentSender, SimpleComponent}; +use gtk::prelude::*; + +/// Configuration for the alert dialog component +pub struct AlertSettings { + /// Large text + pub text: String, + /// Optional secondary, smaller text + pub secondary_text: Option, + /// Modal dialogs freeze other windows as long they are visible + pub is_modal: bool, + /// Sets color of the accept button to red if the theme supports it + pub destructive_accept: bool, + /// Text for confirm button + pub confirm_label: String, + /// Text for cancel button + pub cancel_label: String, + /// Text for third option button. If [`None`] the third button won't be created. + pub option_label: Option, +} + +/// Alert dialog component. +pub struct Alert { + settings: AlertSettings, + is_active: bool, +} + +/// Messages that can be sent to the alert dialog component +#[derive(Debug)] +pub enum AlertMsg { + /// Message sent by the parent to view the dialog + Show, + + #[doc(hidden)] + Response(gtk::ResponseType), +} + +/// User action performed on the alert dialog. +#[derive(Debug)] +pub enum AlertResponse { + /// User clicked confirm button. + Confirm, + + /// User clicked cancel button. + Cancel, + + /// User clicked user-supplied option. + Option, +} + +/// Widgets of the alert dialog component. +#[relm4::component(pub)] +impl SimpleComponent for Alert { + type Widgets = AlertWidgets; + type Init = AlertSettings; + type Input = AlertMsg; + type Output = AlertResponse; + + view! { + #[name = "dialog"] + gtk::MessageDialog { + set_message_type: gtk::MessageType::Question, + #[watch] + set_visible: model.is_active, + connect_response[sender] => move |_, response| { + sender.input(AlertMsg::Response(response)); + }, + + // Apply configuration + set_text: Some(&model.settings.text), + set_secondary_text: model.settings.secondary_text.as_deref(), + set_modal: model.settings.is_modal, + add_button: (&model.settings.confirm_label, gtk::ResponseType::Accept), + add_button: (&model.settings.cancel_label, gtk::ResponseType::Cancel), + } + } + + fn init( + settings: AlertSettings, + root: &Self::Root, + sender: ComponentSender, + ) -> ComponentParts { + let model = Alert { + settings, + is_active: false, + }; + + let widgets = view_output!(); + + if let Some(option_label) = &model.settings.option_label { + widgets + .dialog + .add_button(option_label, gtk::ResponseType::Other(0)); + } + + if model.settings.destructive_accept { + let accept_widget = widgets + .dialog + .widget_for_response(gtk::ResponseType::Accept) + .expect("No button for accept response set"); + accept_widget.add_css_class("destructive-action"); + } + + ComponentParts { model, widgets } + } + + fn update(&mut self, input: AlertMsg, sender: ComponentSender) { + match input { + AlertMsg::Show => { + self.is_active = true; + } + AlertMsg::Response(ty) => { + self.is_active = false; + sender + .output(match ty { + gtk::ResponseType::Accept => AlertResponse::Confirm, + gtk::ResponseType::Other(_) => AlertResponse::Option, + _ => AlertResponse::Cancel, + }) + .unwrap(); + } + } + } +} diff --git a/src/components/alg_page.rs b/src/components/alg_page.rs index a2f22ce..a5954c7 100644 --- a/src/components/alg_page.rs +++ b/src/components/alg_page.rs @@ -1,5 +1,8 @@ +use crate::utils::ini_to_table; use adw::prelude::*; use gtk::prelude::*; +use gtk::ListItem; +use ini::Ini; use relm4::{component, component::Component, ComponentParts, ComponentSender}; use relm4::{ factory::FactoryView, @@ -8,17 +11,16 @@ use relm4::{ view, FactorySender, RelmObjectExt, }; use std::collections::HashMap; -use ini::Ini; -use crate::utils::ini_to_table; +use relm4::binding::{BoolBinding, U8Binding}; #[derive(Debug)] pub struct AlgPage { - alg_list: TypedColumnView, + alg_list: TypedColumnView, } #[derive(Debug)] pub enum AlgPageMsg { - New(Ini) + New(Ini), } #[component(pub)] @@ -55,6 +57,7 @@ impl Component for AlgPage { alg_list.append_column::(); alg_list.append_column::(); alg_list.append_column::(); + alg_list.append_column::(); let model = AlgPage { alg_list }; let list_view = &model.alg_list.view; @@ -67,7 +70,7 @@ impl Component for AlgPage { AlgPageMsg::New(ini) => { let lists = ini_to_table(&ini); for list in lists { - let item = AlgListItem::new(list.0,list.1,"".to_string(),"".to_string()); + let item = AlgListItem::new(list.0, list.1, "".to_string(), list.2); self.alg_list.append(item); } } @@ -81,6 +84,7 @@ pub(super) struct AlgListItem { description: String, version: String, tag: String, + selected: BoolBinding } impl AlgListItem { @@ -90,6 +94,7 @@ impl AlgListItem { version, description, tag, + selected: BoolBinding::new(true) } } } @@ -98,6 +103,36 @@ pub(super) struct NameColumn; pub(super) struct VersionColumn; pub(super) struct DescriptionColumn; pub(super) struct TagColumn; +pub(super) struct SelectColumn; + +pub struct SelectWidgetColumn { + select: gtk::CheckButton, +} + +impl RelmColumn for SelectColumn { + type Item = AlgListItem; + type Root = gtk::Box; + type Widgets = SelectWidgetColumn; + const COLUMN_NAME: &'static str = "Select"; + + fn bind(_item: &mut Self::Item, _widgets: &mut Self::Widgets, _root: &mut Self::Root) { + _widgets.select.add_write_only_binding(&_item.selected.clone(), "active"); + } + + fn setup(list_item: &ListItem) -> (Self::Root, Self::Widgets) { + view! { + root = gtk::Box{ + #[name="select"] + gtk::CheckButton{ + } + } + } + + let widgets = SelectWidgetColumn { select }; + + return (root, widgets); + } +} impl LabelColumn for TagColumn { type Item = AlgListItem; diff --git a/src/components/app.rs b/src/components/app.rs index 169936d..d189126 100644 --- a/src/components/app.rs +++ b/src/components/app.rs @@ -1,8 +1,14 @@ +use crate::components::alert::{Alert, AlertMsg, AlertResponse, AlertSettings}; +use crate::components::alg_page::AlgPage; use crate::components::new_project::NewPageModel; use adw::prelude::*; use gtk::prelude::*; use gtk::Widget; -use relm4::{actions::RelmActionGroup, component::{AsyncComponentController, AsyncController}, Component, ComponentParts, ComponentSender}; +use relm4::{ + actions::RelmActionGroup, + component::{AsyncComponentController, AsyncController}, + Component, ComponentController, ComponentParts, ComponentSender, Controller, SimpleComponent, +}; use std::{ any::Any, borrow::{Borrow, BorrowMut}, @@ -12,7 +18,6 @@ use std::{ rc::Rc, sync::{Arc, Mutex}, }; -use crate::components::alg_page::AlgPage; relm4::new_action_group!(FileActionGroup, "file"); relm4::new_stateless_action!(OpenAction, FileActionGroup, "open"); @@ -21,9 +26,14 @@ pub type ElementKey = String; #[derive(Debug)] pub enum AppMsg { NewProject, + CloseRequest, + Close, + Ignore, + Save, } pub struct AppModel { new_page_model: AsyncController, + dialog: Controller, tracker: usize, } @@ -42,12 +52,12 @@ impl Component for AppModel { view! { #[root] main_window=adw::ApplicationWindow { - set_default_width: 900, + set_default_width: 1200, set_default_height: 600, + set_width_request:1200, set_focus_on_click:true, connect_close_request[sender] => move |_| { - let app = relm4::main_application(); - app.quit(); + sender.input(AppMsg::CloseRequest); gtk::Inhibit(true) }, gtk::Box{ @@ -144,8 +154,23 @@ impl Component for AppModel { .launch(()) .forward(sender.input_sender(), |a| AppMsg::NewProject); - let model = AppModel { + dialog: Alert::builder() + .transient_for(root) + .launch(AlertSettings { + text: String::from("Do you want to quit without saving?"), + secondary_text: Some(String::from("Your counter hasn't reached 42 yet")), + confirm_label: String::from("Close without saving"), + cancel_label: String::from("Cancel"), + option_label: Some(String::from("Save")), + is_modal: true, + destructive_accept: true, + }) + .forward(sender.input_sender(), |msg| match msg { + AlertResponse::Cancel => AppMsg::Ignore, + AlertResponse::Option => AppMsg::Save, + AlertResponse::Confirm => AppMsg::Close, + }), new_page_model, tracker: 0, }; @@ -164,9 +189,16 @@ impl Component for AppModel { ) { match msg { AppMsg::NewProject => { - // widgets.stack.set_visible_child_name("new_page"); widgets.stack.push_by_tag("new_page"); } + AppMsg::CloseRequest => { + self.dialog.emit(AlertMsg::Show); + } + + AppMsg::Close => { + relm4::main_application().quit(); + } + _ => {} } } diff --git a/src/components/mod.rs b/src/components/mod.rs index b879e40..4ee41ab 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -3,6 +3,8 @@ mod app; mod history_list; mod new_project; mod setting_item; +mod alert; + pub use app::*; pub use new_project::*; pub use setting_item::*; diff --git a/src/utils.rs b/src/utils.rs index fc8f27c..0198e6d 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,8 +1,8 @@ use crate::error::SourceError; use crate::CONFIG; use git2::{Cred, FetchOptions, RemoteCallbacks, Repository}; -use std::path::PathBuf; use ini::Ini; +use std::path::PathBuf; use tokio::task; pub async fn get_alg_lists() -> Result, SourceError> { @@ -58,20 +58,20 @@ pub async fn get_alg_lists() -> Result, SourceError> { Ok(list) } -pub fn ini_to_table(ini: &Ini) -> Vec<(String,String)> { +pub fn ini_to_table(ini: &Ini) -> Vec<(String, String, String)> { let mut result = Vec::new(); let lib_sec = ini.section(Some("lib")); let alg_sec = ini.section(Some("algorithms")); - if let Some(lib) = lib_sec{ - for (key,value) in lib.iter(){ - result.push((key.to_string(), value.to_string())); + if let Some(lib) = lib_sec { + for (key, value) in lib.iter() { + result.push((key.to_string(), value.to_string(), "lib".to_string())); } } - if let Some(alg) = alg_sec{ - for (key,value) in alg.iter(){ - result.push((key.to_string(), value.to_string())); + if let Some(alg) = alg_sec { + for (key, value) in alg.iter() { + result.push((key.to_string(), value.to_string(), "algorithms".to_string())); } } result