From b565585123275e20b90d9dc4cd3281a5aedfb00a Mon Sep 17 00:00:00 2001 From: Miguel Aranha Baldi Horlle Date: Thu, 9 May 2024 07:31:48 -0300 Subject: [PATCH] UI/UX Task manager - UX revamped: using responsive side panel. - Introducing color configuration for connections. - Introducing global default messages ordering configuration on settings page. - Another minor improvements. --- src/backend/repository.rs | 68 +++++- src/backend/settings.rs | 6 +- src/component/app.rs | 308 ++++++++++++++++--------- src/component/banner.rs | 78 ------- src/component/connection_list.rs | 142 +++++++++--- src/component/connection_page.rs | 206 +++++++++++------ src/component/messages/messages_tab.rs | 133 ++++++----- src/component/mod.rs | 3 +- src/component/settings_dialog.rs | 108 ++++++++- src/component/topics/topics_page.rs | 31 ++- src/lib.rs | 1 + src/main.rs | 3 +- src/styles.less | 2 +- 13 files changed, 712 insertions(+), 377 deletions(-) delete mode 100644 src/component/banner.rs diff --git a/src/backend/repository.rs b/src/backend/repository.rs index 87fc032..14763bb 100644 --- a/src/backend/repository.rs +++ b/src/backend/repository.rs @@ -36,6 +36,7 @@ pub struct KrustConnection { pub sasl_mechanism: Option, pub sasl_username: Option, pub sasl_password: Option, + pub color: Option, } #[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq)] pub struct Partition { @@ -411,6 +412,16 @@ impl Repository { .execute_batch( " ALTER TABLE kr_topic ADD COLUMN favourite INTEGER DEFAULT 0; + ", + ) + .map_err(ExternalError::DatabaseError).unwrap_or_else(|e| { + warn!("kr_topic.favourite: {:?}", e); + }); + let _result_add_connection_color = self + .conn + .execute_batch( + " + ALTER TABLE kr_connection ADD COLUMN color TEXT DEFAULT NULL; ", ) .map_err(ExternalError::DatabaseError).unwrap_or_else(|e| { @@ -419,8 +430,32 @@ impl Repository { Ok(()) } + pub fn connection_by_id(&mut self, id: usize) -> Option { + let mut stmt = self.conn.prepare_cached("SELECT id, name, brokersList, securityType, saslMechanism, saslUsername, saslPassword, color FROM kr_connection WHERE id = ?").expect("Should return prepared statement"); + let rows = stmt + .query_row(params![id], |row| { + Ok(KrustConnection { + id: row.get(0).unwrap_or(None), + name: row.get(1).unwrap_or_default(), + brokers_list: row.get(2).unwrap_or_default(), + security_type: KrustConnectionSecurityType::from_str( + row.get::(3).unwrap_or_default().as_str(), + ) + .unwrap_or_default(), + sasl_mechanism: row.get(4).unwrap_or(None), + sasl_username: row.get(5).unwrap_or(None), + sasl_password: row.get(6).unwrap_or(None), + color: row.get(7).unwrap_or(None), + }) + }) + .map_err(ExternalError::DatabaseError); + match rows { + Ok(conn) => Some(conn), + Err(_) => None, + } + } pub fn list_all_connections(&mut self) -> Result, ExternalError> { - let mut stmt = self.conn.prepare_cached("SELECT id, name, brokersList, securityType, saslMechanism, saslUsername, saslPassword from kr_connection")?; + let mut stmt = self.conn.prepare_cached("SELECT id, name, brokersList, securityType, saslMechanism, saslUsername, saslPassword, color FROM kr_connection ORDER BY name")?; let rows = stmt .query_map([], |row| { Ok(KrustConnection { @@ -434,6 +469,7 @@ impl Repository { sasl_mechanism: row.get(4)?, sasl_username: row.get(5)?, sasl_password: row.get(6)?, + color: row.get(7)?, }) }) .map_err(ExternalError::DatabaseError)?; @@ -455,8 +491,9 @@ impl Repository { let sasl = konn.sasl_mechanism.clone(); let sasl_username = konn.sasl_username.clone(); let sasl_password = konn.sasl_password.clone(); - let mut stmt_by_id = self.conn.prepare_cached("SELECT id, name, brokersList, securityType, saslMechanism, saslUsername, saslPassword from kr_connection where id = ?1")?; - let mut stmt_by_name = self.conn.prepare_cached("SELECT id, name, brokersList, securityType, saslMechanism, saslUsername, saslPassword from kr_connection where name = ?1")?; + let color = konn.color.clone(); + let mut stmt_by_id = self.conn.prepare_cached("SELECT id, name, brokersList, securityType, saslMechanism, saslUsername, saslPassword, color from kr_connection where id = ?1")?; + let mut stmt_by_name = self.conn.prepare_cached("SELECT id, name, brokersList, securityType, saslMechanism, saslUsername, saslPassword, color from kr_connection where name = ?1")?; let row_to_model = move |row: &Row<'_>| { Ok(KrustConnection { id: row.get(0)?, @@ -469,6 +506,7 @@ impl Repository { sasl_mechanism: row.get(4)?, sasl_username: row.get(5)?, sasl_password: row.get(6)?, + color: row.get(7)?, }) }; let maybe_konn = match id { @@ -482,14 +520,14 @@ impl Repository { match maybe_konn { Ok(konn_to_update) => { - let mut up_stmt = self.conn.prepare_cached("UPDATE kr_connection SET name = :name, brokersList = :brokers, securityType = :security, saslMechanism = :sasl, saslUsername = :sasl_u, saslPassword = :sasl_p WHERE id = :id")?; + let mut up_stmt = self.conn.prepare_cached("UPDATE kr_connection SET name = :name, brokersList = :brokers, securityType = :security, saslMechanism = :sasl, saslUsername = :sasl_u, saslPassword = :sasl_p, color = :color WHERE id = :id")?; up_stmt - .execute(named_params! { ":id": &konn_to_update.id.unwrap(), ":name": &name, ":brokers": &brokers, ":security": security.to_string(), ":sasl": &sasl, ":sasl_u": &sasl_username, ":sasl_p": &sasl_password }) + .execute(named_params! { ":id": &konn_to_update.id.unwrap(), ":name": &name, ":brokers": &brokers, ":security": security.to_string(), ":sasl": &sasl, ":sasl_u": &sasl_username, ":sasl_p": &sasl_password, ":color": &color }) .map_err(ExternalError::DatabaseError) - .map( |_| {KrustConnection { id: konn_to_update.id, name, brokers_list: brokers, security_type: security, sasl_mechanism: sasl, sasl_username, sasl_password }}) + .map( |_| {KrustConnection { id: konn_to_update.id, name, brokers_list: brokers, security_type: security, sasl_mechanism: sasl, sasl_username, sasl_password, color }}) } Err(_) => { - let mut ins_stmt = self.conn.prepare_cached("INSERT INTO kr_connection (id, name, brokersList, securityType, saslMechanism, saslUsername, saslPassword) VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING id")?; + let mut ins_stmt = self.conn.prepare_cached("INSERT INTO kr_connection (id, name, brokersList, securityType, saslMechanism, saslUsername, saslPassword, color) VALUES (?, ?, ?, ?, ?, ?, ?, ?) RETURNING id")?; ins_stmt .query_row( params![ @@ -500,6 +538,7 @@ impl Repository { &konn.sasl_mechanism, &konn.sasl_username, &konn.sasl_password, + &konn.color, ], |row| { Ok(KrustConnection { @@ -510,6 +549,7 @@ impl Repository { sasl_mechanism: sasl, sasl_username, sasl_password, + color }) }, ) @@ -569,6 +609,20 @@ impl Repository { .map_err(ExternalError::DatabaseError) } + pub fn delete_connection( + &mut self, + conn_id: usize, + ) -> Result { + let mut stmt_by_id = self.conn.prepare_cached( + "DELETE FROM kr_connection + WHERE id = :cid", + )?; + + stmt_by_id + .execute(named_params! { ":cid": &conn_id,}) + .map_err(ExternalError::DatabaseError) + } + pub fn find_topic(&mut self, conn_id: usize, topic_name: &String) -> Option { let stmt = self.conn .prepare_cached("SELECT connection_id, name, cached, favourite FROM kr_topic WHERE connection_id = :cid AND name = :topic"); diff --git a/src/backend/settings.rs b/src/backend/settings.rs index 2b09b4c..419855a 100644 --- a/src/backend/settings.rs +++ b/src/backend/settings.rs @@ -3,7 +3,7 @@ use std::fs::File; use std::path::PathBuf; use tracing::*; -use crate::{config::{ensure_app_config_dir, ExternalError}, DATE_TIME_FORMAT, DATE_TIME_WITH_MILLIS_FORMAT}; +use crate::{component::settings_dialog::MessagesSortOrder, config::{ensure_app_config_dir, ExternalError}, DATE_TIME_FORMAT, DATE_TIME_WITH_MILLIS_FORMAT}; /// Application global settings #[derive(Debug, Clone, Serialize, Deserialize)] @@ -12,6 +12,8 @@ pub struct Settings { /// cache directory as string. pub cache_dir: String, pub is_full_timestamp: bool, + pub messages_sort_column: String, + pub messages_sort_column_order: String, } impl Settings { @@ -64,6 +66,8 @@ impl Default for Settings { Settings { cache_dir: default_cache_dir, is_full_timestamp: false, + messages_sort_column: "Offset".to_string(), + messages_sort_column_order: MessagesSortOrder::Default.to_string(), } } } diff --git a/src/component/app.rs b/src/component/app.rs index 6054a41..49cdab3 100644 --- a/src/component/app.rs +++ b/src/component/app.rs @@ -1,11 +1,16 @@ //! Application entrypoint. -use gtk::{glib, prelude::*}; +use std::{collections::HashMap, time::Duration}; + +use adw::{prelude::*, Toast}; +use gtk::glib; use relm4::{ + abstractions::Toaster, actions::{RelmAction, RelmActionGroup}, factory::FactoryVecDeque, main_adw_application, prelude::*, + MessageBroker, }; use relm4_components::alert::{Alert, AlertMsg, AlertResponse, AlertSettings}; use tracing::{error, info, warn}; @@ -13,7 +18,12 @@ use tracing::{error, info, warn}; use crate::{ backend::repository::{KrustConnection, KrustTopic, Repository}, component::{ - banner::BANNER_BROKER, connection_list::KrustConnectionOutput, connection_page::{ConnectionPageModel, ConnectionPageMsg, ConnectionPageOutput}, settings_dialog::{SettingsDialogInit, SettingsDialogMsg}, status_bar::{StatusBarModel, STATUS_BROKER}, task_manager::{TaskManagerModel, TASK_MANAGER_BROKER}, topics::topics_page::{TopicsPageMsg, TopicsPageOutput} + connection_list::{KrustConnectionMsg, KrustConnectionOutput}, + connection_page::{ConnectionPageModel, ConnectionPageMsg, ConnectionPageOutput}, + settings_dialog::{SettingsDialogInit, SettingsDialogMsg}, + status_bar::{StatusBarModel, STATUS_BROKER}, + task_manager::{TaskManagerModel, TASK_MANAGER_BROKER}, + topics::topics_page::{TopicsPageMsg, TopicsPageOutput}, }, config::State, modals::about::AboutDialog, @@ -21,7 +31,10 @@ use crate::{ }; use super::{ - banner::AppBannerModel, connection_list::ConnectionListModel, messages::messages_page::{MessagesPageModel, MessagesPageMsg}, settings_dialog::SettingsDialogModel, topics::topics_page::TopicsPageModel + connection_list::ConnectionListModel, + messages::messages_page::{MessagesPageModel, MessagesPageMsg}, + settings_dialog::SettingsDialogModel, + topics::topics_page::TopicsPageModel, }; #[derive(Debug)] @@ -36,13 +49,21 @@ pub enum AppMsg { ShowTopicsPage(KrustConnection), ShowTopicsPageByIndex(i32), ShowMessagesPage(KrustConnection, KrustTopic), - RemoveConnection(DynamicIndex), + RemoveConnection(DynamicIndex, KrustConnection), ShowSettings, SavedSettings, + ShowToast(String, String), + HideToast(String), +} + +#[derive(Debug)] +pub enum AppCommand { + LateHide(String), } pub struct AppModel { - _app_banner: Controller, + toaster: Toaster, + toasts: HashMap, _status_bar: Controller, _task_manager: Controller, close_dialog: Controller, @@ -60,12 +81,14 @@ relm4::new_stateless_action!(pub(super) AddConnection, WindowActionGroup, "add-c relm4::new_stateless_action!(pub(super) ShortcutsAction, WindowActionGroup, "show-help-overlay"); relm4::new_stateless_action!(AboutAction, WindowActionGroup, "about"); +pub static TOASTER_BROKER: MessageBroker = MessageBroker::new(); + #[relm4::component(pub)] impl Component for AppModel { type Init = (); type Input = AppMsg; type Output = (); - type CommandOutput = (); + type CommandOutput = AppCommand; menu! { primary_menu: { @@ -83,97 +106,117 @@ impl Component for AppModel { set_visible: true, set_title: Some(APP_NAME), set_icon_name: Some(APP_ID), - gtk::Box { - set_orientation: gtk::Orientation::Vertical, - - adw::HeaderBar { - pack_end = >k::MenuButton { - set_icon_name: "open-menu-symbolic", - set_menu_model: Some(&primary_menu), - } - }, - #[name(main_paned)] - gtk::Paned { - set_orientation: gtk::Orientation::Horizontal, - set_resize_start_child: true, - set_wide_handle: true, + set_width_request: 380, + set_height_request: 380, + set_default_size: (800, 600), + set_show_menubar: true, + #[local_ref] + toast_overlay -> adw::ToastOverlay { + #[name = "main_paned"] + adw::OverlaySplitView { + set_enable_show_gesture: false, + set_enable_hide_gesture: false, + set_max_sidebar_width: 600.0, + //set_min_sidebar_width: 410.0, + set_sidebar_width_unit: adw::LengthUnit::Px, + set_sidebar_width_fraction: 0.2136, #[wrap(Some)] - set_start_child = >k::ScrolledWindow { - set_min_content_width: 200, - set_hexpand: true, - set_vexpand: true, - set_propagate_natural_width: true, - #[wrap(Some)] - set_child = >k::Box { - set_orientation: gtk::Orientation::Vertical, - gtk::StackSwitcher { - set_overflow: gtk::Overflow::Hidden, - set_orientation: gtk::Orientation::Horizontal, - set_stack: Some(&main_stack), - set_hexpand: false, - set_vexpand: false, - set_valign: gtk::Align::Baseline, - set_halign: gtk::Align::Center, + set_sidebar = &adw::ToolbarView { + add_top_bar = &adw::HeaderBar { + set_show_title: true, + #[wrap(Some)] + set_title_widget = &adw::WindowTitle { + set_title: "Connections", }, - gtk::ScrolledWindow { - connections.widget() -> >k::ListBox { - set_selection_mode: gtk::SelectionMode::Single, - set_hexpand: true, - set_vexpand: true, - set_show_separators: true, - add_css_class: "rich-list", - connect_row_activated[sender] => move |list_box, row| { - info!("clicked on connection: {:?} - {:?}", list_box, row.index()); - sender.input(AppMsg::ShowTopicsPageByIndex(row.index())); + pack_start = >k::ToggleButton { + set_icon_name: "list-add-symbolic", + set_tooltip_text: Some("Add a new kafka connection"), + set_action_name: Some("win.add-connection"), + }, + pack_end = >k::MenuButton { + set_icon_name: "open-menu-symbolic", + set_menu_model: Some(&primary_menu), + } + }, + #[wrap(Some)] + set_content = >k::ScrolledWindow { + set_min_content_width: 200, + set_hexpand: true, + set_vexpand: true, + set_propagate_natural_width: true, + #[wrap(Some)] + set_child = >k::Box { + set_orientation: gtk::Orientation::Vertical, + gtk::StackSwitcher { + set_overflow: gtk::Overflow::Hidden, + set_orientation: gtk::Orientation::Horizontal, + set_stack: Some(&main_stack), + set_hexpand: false, + set_vexpand: false, + set_valign: gtk::Align::Baseline, + set_halign: gtk::Align::Center, + }, + gtk::ScrolledWindow { + connections.widget() -> >k::ListBox { + set_selection_mode: gtk::SelectionMode::Single, + set_hexpand: true, + set_vexpand: true, + set_show_separators: true, + add_css_class: "rich-list", + connect_row_activated[sender] => move |list_box, row| { + info!("clicked on connection: {:?} - {:?}", list_box, row.index()); + sender.input(AppMsg::ShowTopicsPageByIndex(row.index())); + }, }, }, + task_manager.widget() -> &adw::Bin {}, }, - task_manager.widget() -> &adw::Bin {}, }, }, #[wrap(Some)] - set_end_child = >k::ScrolledWindow { - set_hexpand: true, - set_vexpand: true, + set_content = &adw::ToolbarView { + add_top_bar = &adw::HeaderBar { + pack_start: toggle_pane_button = >k::ToggleButton { + set_icon_name: "sidebar-show-symbolic", + set_active: true, + set_visible: false, + } + }, #[wrap(Some)] - set_child = >k::Box { - set_orientation: gtk::Orientation::Vertical, - app_banner.widget() -> &adw::Banner {}, - #[name(main_stack)] - gtk::Stack { - add_child = >k::Box { - set_halign: gtk::Align::Center, - set_orientation: gtk::Orientation::Vertical, - #[name="support_logo"] - gtk::Picture { - set_vexpand: true, - set_hexpand: true, - set_margin_top: 48, - set_margin_bottom: 48, + set_content = >k::ScrolledWindow { + set_hexpand: true, + set_vexpand: true, + #[wrap(Some)] + set_child = >k::Box { + set_orientation: gtk::Orientation::Vertical, + #[name(main_stack)] + gtk::Stack { + add_child = >k::Box { + set_halign: gtk::Align::Center, + set_orientation: gtk::Orientation::Vertical, + #[name="support_logo"] + gtk::Picture { + set_vexpand: true, + set_hexpand: true, + set_margin_top: 48, + set_margin_bottom: 48, + }, + } -> { + set_title: "Home", + set_name: "Home", + }, + add_child = topics_page.widget() -> &adw::TabOverview {} -> { + set_name: "Topics", + set_title: "Topics", + }, + add_child = messages_page.widget() -> &adw::TabOverview {} -> { + set_name: "Messages", + set_title: "Messages", }, - } -> { - set_title: "Home", - set_name: "Home", - }, - add_child = connection_page.widget() -> >k::Grid {} -> { - set_name: "Connection", - }, - add_child = topics_page.widget() -> &adw::TabOverview {} -> { - set_name: "Topics", - set_title: "Topics", - }, - add_child = messages_page.widget() -> &adw::TabOverview {} -> { - set_name: "Messages", - set_title: "Messages", }, }, }, - }, - }, - gtk::Box { - set_visible: false, - add_css_class: "status-bar", - status_bar.widget() -> >k::CenterBox {} + } }, }, @@ -181,11 +224,12 @@ impl Component for AppModel { sender.input(AppMsg::Close); gtk::glib::Propagation::Stop }, - } } fn init(_params: (), root: Self::Root, sender: ComponentSender) -> ComponentParts { + let toaster = Toaster::default(); + let toast_overlay = toaster.overlay_widget(); let about_dialog = AboutDialog::builder() .transient_for(&root) .launch(()) @@ -195,9 +239,6 @@ impl Component for AppModel { .launch_with_broker((), &STATUS_BROKER) .detach(); - let app_banner: Controller = AppBannerModel::builder() - .launch_with_broker((), &BANNER_BROKER) - .detach(); let task_manager: Controller = TaskManagerModel::builder() .launch_with_broker((), &TASK_MANAGER_BROKER) .detach(); @@ -206,7 +247,7 @@ impl Component for AppModel { .launch(gtk::ListBox::default()) .forward(sender.input_sender(), |output| match output { KrustConnectionOutput::Add => AppMsg::ShowConnection, - KrustConnectionOutput::Remove(index) => AppMsg::RemoveConnection(index), + KrustConnectionOutput::Remove(index, conn) => AppMsg::RemoveConnection(index, conn), KrustConnectionOutput::Edit(index, conn) => { AppMsg::ShowEditConnectionPage(index, conn) } @@ -233,7 +274,7 @@ impl Component for AppModel { .detach(); let settings_dialog: Controller = SettingsDialogModel::builder() - .launch(SettingsDialogInit{}) + .launch(SettingsDialogInit {}) .detach(); let state = State::read().unwrap_or_default(); @@ -278,7 +319,8 @@ impl Component for AppModel { Err(e) => error!("error loading connections: {:?}", e), } let model = AppModel { - _app_banner: app_banner, + toaster: toaster, + toasts: HashMap::new(), _status_bar: status_bar, _task_manager: task_manager, close_dialog: Alert::builder() @@ -313,6 +355,18 @@ impl Component for AppModel { _: &Self::Root, ) { match msg { + AppMsg::ShowToast(id, text) => { + let toast = adw::Toast::builder().title(&text).timeout(0).build(); + self.toasts.insert(id, toast.clone()); + self.toaster.add_toast(toast); + } + AppMsg::HideToast(id) => { + info!("hide_toast::{}", &id); + let command_sender = sender.command_sender().clone(); + gtk::glib::timeout_add_once(Duration::from_secs(2), move || { + command_sender.emit(AppCommand::LateHide(id)); + }); + } AppMsg::CloseIgnore => { (); } @@ -326,7 +380,7 @@ impl Component for AppModel { info!("|-->Showing new connection page"); self.connection_page.emit(ConnectionPageMsg::New); self.connection_page.widget().set_visible(true); - widgets.main_stack.set_visible_child_name("Connection"); + //widgets.main_stack.set_visible_child_name("Connection"); } AppMsg::AddConnection(conn) => { info!("|-->Adding connection "); @@ -336,7 +390,7 @@ impl Component for AppModel { AppMsg::SaveConnection(maybe_idx, conn) => { info!("|-->Saving connection {:?}", conn); - widgets.main_stack.set_visible_child_name("Home"); + //widgets.main_stack.set_visible_child_name("Home"); let mut repo = Repository::new(); let result = repo.save_connection(&conn); match (maybe_idx, result) { @@ -352,6 +406,7 @@ impl Component for AppModel { conn_to_update.sasl_mechanism = new_conn.sasl_mechanism; conn_to_update.sasl_username = new_conn.sasl_username; conn_to_update.sasl_password = new_conn.sasl_password; + conn_to_update.color = new_conn.color; } None => warn!("no connection to update"), }; @@ -360,12 +415,13 @@ impl Component for AppModel { error!("error saving connection: {:?}", e); } }; + self.connections.broadcast(KrustConnectionMsg::Refresh); } AppMsg::ShowEditConnectionPage(index, conn) => { info!("|-->Show edit connection page for {:?}", conn); self.connection_page .emit(ConnectionPageMsg::Edit(index, conn)); - widgets.main_stack.set_visible_child_name("Connection"); + //widgets.main_stack.set_visible_child_name("Connection"); } AppMsg::ShowTopicsPage(conn) => { info!("|-->Show edit connection page for {:?}", conn); @@ -396,8 +452,18 @@ impl Component for AppModel { widgets.main_stack.set_visible_child_name("Home"); } } - AppMsg::RemoveConnection(index) => { - info!("Removing connection {:?}", index); + AppMsg::RemoveConnection(index, conn) => { + info!("Removing connection {:?}::{:?}", index, conn); + let mut repo = Repository::new(); + let result = repo.delete_connection(conn.id.unwrap()); + match result { + Ok(_) => { + self.connections.guard().remove(index.current_index()); + } + Err(e) => { + error!("error saving connection: {:?}", e); + } + }; } AppMsg::ShowMessagesPage(connection, topic) => { self.messages_page @@ -414,6 +480,24 @@ impl Component for AppModel { } self.update_view(widgets, sender); } + + fn update_cmd_with_view( + &mut self, + _widgets: &mut Self::Widgets, + message: Self::CommandOutput, + _sender: ComponentSender, + _root: &Self::Root, + ) { + match message { + AppCommand::LateHide(id) => { + if let Some(toast) = self.toasts.remove(&id) { + info!("hide_toast::removed::{}", &id); + toast.dismiss(); + } + } + } + } + fn shutdown(&mut self, widgets: &mut Self::Widgets, _output: relm4::Sender) { info!("app::saving window state"); widgets @@ -434,15 +518,15 @@ impl AppModelWidgets { fn save_window_size(&self) -> Result<(), glib::BoolError> { let (width, height) = self.main_window.default_size(); let is_maximized = self.main_window.is_maximized(); - let separator = if self.main_paned.position() < 405 { - 405 - } else { - self.main_paned.position() - }; + // let separator = if self.main_paned.position() < 405 { + // 405 + // } else { + // self.main_paned.position() + // }; let new_state = State { width, height, - separator_position: separator, + separator_position: 300, is_maximized, }; @@ -454,6 +538,22 @@ impl AppModelWidgets { } fn load_window_size(&self) { + let breakpoint = adw::Breakpoint::new(adw::BreakpointCondition::new_length( + adw::BreakpointConditionLengthType::MaxWidth, + 1510.0, + adw::LengthUnit::Px, + )); + breakpoint.add_setter(&self.main_paned, "collapsed", &true.to_value()); + breakpoint.add_setter(&self.main_paned, "enable-show-gesture", &true.to_value()); + breakpoint.add_setter(&self.main_paned, "enable-hide-gesture", &true.to_value()); + breakpoint.add_setter(&self.toggle_pane_button, "visible", &true.to_value()); + let _toggle_sidebar_binding = self + .toggle_pane_button + .bind_property("active", &self.main_paned, "show-sidebar") + .bidirectional() + .sync_create() + .build(); + self.main_window.add_breakpoint(breakpoint); info!("loading window size"); let state = State::read() .map_err(|e| { @@ -463,11 +563,11 @@ impl AppModelWidgets { .unwrap_or_default(); let width = &state.width; let height = &state.height; - let paned_position = &state.separator_position; + let _paned_position = &state.separator_position; let is_maximized = &state.is_maximized; self.main_window.set_default_size(*width, *height); - self.main_paned.set_position(*paned_position); + //self.main_paned.set_position(*paned_position); if *is_maximized { info!("should maximize"); diff --git a/src/component/banner.rs b/src/component/banner.rs deleted file mode 100644 index f51b2a0..0000000 --- a/src/component/banner.rs +++ /dev/null @@ -1,78 +0,0 @@ -//! Window banner for showing messages. - -use std::time::Duration; - -use relm4::prelude::*; -use relm4::MessageBroker; - -pub static BANNER_BROKER: MessageBroker = MessageBroker::new(); - -#[derive(Debug)] -pub struct AppBannerModel {} - -#[derive(Debug)] -pub enum AppBannerMsg { - Show(String), - Hide, -} -#[derive(Debug)] -pub enum AppBannerCommand { - LateHide, -} - -#[relm4::component(pub)] -impl Component for AppBannerModel { - type Widgets = AppBannerWidgets; - type Init = (); - type Input = AppBannerMsg; - type Output = (); - type CommandOutput = AppBannerCommand; - - view! { - #[name(app_banner)] - adw::Banner {} - } - - fn init(_: (), root: Self::Root, _sender: ComponentSender) -> ComponentParts { - let model = AppBannerModel {}; - - let widgets = view_output!(); - - ComponentParts { model, widgets } - } - - fn update_with_view( - &mut self, - widgets: &mut Self::Widgets, - input: Self::Input, - sender: ComponentSender, - _root: &Self::Root, - ){ - match input { - AppBannerMsg::Show(text) => { - widgets.app_banner.set_title(text.as_str()); - widgets.app_banner.set_revealed(true); - } - AppBannerMsg::Hide => { - gtk::glib::timeout_add_once(Duration::from_secs(2), move ||{ - sender.command_sender().emit(AppBannerCommand::LateHide); - }); - } - } - } - - fn update_cmd_with_view( - &mut self, - widgets: &mut Self::Widgets, - message: Self::CommandOutput, - _sender: ComponentSender, - _root: &Self::Root, - ) { - match message { - AppBannerCommand::LateHide => { - widgets.app_banner.set_title(""); - widgets.app_banner.set_revealed(false); - } - } - } -} diff --git a/src/component/connection_list.rs b/src/component/connection_list.rs index 9e31a09..93f2751 100644 --- a/src/component/connection_list.rs +++ b/src/component/connection_list.rs @@ -5,20 +5,25 @@ use relm4::{ }; use tracing::info; -use crate::backend::repository::{KrustConnection, KrustConnectionSecurityType}; +use crate::{ + backend::repository::{KrustConnection, KrustConnectionSecurityType}, + Repository, +}; -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum KrustConnectionMsg { Connect, Disconnect, Edit(DynamicIndex), + Remove(DynamicIndex), + Refresh, } #[derive(Debug)] pub enum KrustConnectionOutput { Add, Edit(DynamicIndex, KrustConnection), - Remove(DynamicIndex), + Remove(DynamicIndex, KrustConnection), ShowTopics(KrustConnection), } @@ -31,6 +36,7 @@ pub struct ConnectionListModel { pub sasl_mechanism: Option, pub sasl_username: Option, pub sasl_password: Option, + pub color: Option, pub is_connected: bool, } @@ -44,6 +50,7 @@ impl From<&mut ConnectionListModel> for KrustConnection { sasl_mechanism: value.sasl_mechanism.clone(), sasl_username: value.sasl_username.clone(), sasl_password: value.sasl_password.clone(), + color: value.color.clone(), } } } @@ -56,36 +63,47 @@ impl FactoryComponent for ConnectionListModel { type ParentWidget = gtk::ListBox; view! { - #[root] - gtk::Box { - set_orientation: gtk::Orientation::Horizontal, - set_spacing: 10, + #[root] + gtk::Box { + set_orientation: gtk::Orientation::Horizontal, + set_spacing: 10, - #[name(connect_button)] - gtk::ToggleButton { - set_label: "Connect", - add_css_class: "krust-toggle", - connect_toggled[sender] => move |btn| { - if btn.is_active() { - sender.input(KrustConnectionMsg::Connect); - } else { - sender.input(KrustConnectionMsg::Disconnect); - } - }, - }, - gtk::Button { - set_icon_name: "emblem-system-symbolic", - connect_clicked[sender, index] => move |_| { - sender.input(KrustConnectionMsg::Edit(index.clone())); - }, - }, - #[name(label)] - gtk::Label { - #[watch] - set_label: &self.name, - set_width_chars: 3, - }, - } + #[name(connect_button)] + gtk::ToggleButton { + set_label: "Connect", + add_css_class: "krust-toggle", + connect_toggled[sender] => move |btn| { + if btn.is_active() { + sender.input(KrustConnectionMsg::Connect); + } else { + sender.input(KrustConnectionMsg::Disconnect); + } + }, + }, + gtk::Button { + set_tooltip_text: Some("Edit connection"), + set_icon_name: "emblem-system-symbolic", + add_css_class: "circular", + connect_clicked[sender, index] => move |_| { + sender.input(KrustConnectionMsg::Edit(index.clone())); + }, + }, + gtk::Button { + set_tooltip_text: Some("Delete connection"), + set_icon_name: "edit-delete-symbolic", + add_css_class: "circular", + connect_clicked[sender, index] => move |_| { + sender.input(KrustConnectionMsg::Remove(index.clone())); + }, + }, + #[name(label)] + gtk::Label { + #[watch] + set_label: &self.name, + set_tooltip_text: Some(&self.name), + set_width_chars: 3, + }, + } } fn init_model(conn: Self::Init, _index: &DynamicIndex, _sender: FactorySender) -> Self { @@ -97,13 +115,38 @@ impl FactoryComponent for ConnectionListModel { sasl_mechanism: conn.sasl_mechanism, sasl_username: conn.sasl_username, sasl_password: conn.sasl_password, + color: conn.color, is_connected: false, } } - - fn update(&mut self, msg: Self::Input, sender: FactorySender) { + fn post_view(&self, widgets: &mut Self::Widgets) {} + fn update_with_view( + &mut self, + widgets: &mut Self::Widgets, + msg: Self::Input, + sender: FactorySender, + ) { match msg { KrustConnectionMsg::Connect => { + let mut conn = Repository::new(); + let conn = conn.connection_by_id(self.id.unwrap()).unwrap(); + let css_provider = gtk::CssProvider::new(); + let color = conn + .color + .clone() + .unwrap_or("rgb(183, 243, 155)".to_string()); + let color = color.as_str(); + let css_class = format!("custom_color_{}", self.id.unwrap()); + css_provider.load_from_string( + format!(".{} {{ background: {};}}", css_class, color).as_str(), + ); + let display = widgets.connect_button.display(); + gtk::style_context_add_provider_for_display( + &display, + &css_provider, + gtk::STYLE_PROVIDER_PRIORITY_APPLICATION, + ); + widgets.connect_button.add_css_class(&css_class); info!("Connect request for {}", self.name); self.is_connected = true; let conn: KrustConnection = self.into(); @@ -113,6 +156,8 @@ impl FactoryComponent for ConnectionListModel { } KrustConnectionMsg::Disconnect => { info!("Disconnect request for {}", self.name); + let css_class = format!("custom_color_{}", self.id.unwrap()); + widgets.connect_button.remove_css_class(&css_class); self.is_connected = false; } KrustConnectionMsg::Edit(index) => { @@ -121,6 +166,35 @@ impl FactoryComponent for ConnectionListModel { .output(KrustConnectionOutput::Edit(index, self.into())) .unwrap(); } + KrustConnectionMsg::Remove(index) => { + info!("Edit request for {}", self.name); + sender + .output(KrustConnectionOutput::Remove(index, self.into())) + .unwrap(); + } + KrustConnectionMsg::Refresh => { + widgets.label.set_label(&self.name); + if self.is_connected { + let css_provider = gtk::CssProvider::new(); + let color = self + .color + .clone() + .unwrap_or("rgb(183, 243, 155)".to_string()); + let color = color.as_str(); + let css_class = format!("custom_color_{}", self.id.unwrap()); + css_provider.load_from_string( + format!(".{} {{ background: {};}}", css_class, color).as_str(), + ); + let display = widgets.connect_button.display(); + gtk::style_context_add_provider_for_display( + &display, + &css_provider, + gtk::STYLE_PROVIDER_PRIORITY_APPLICATION, + ); + widgets.connect_button.remove_css_class(&css_class); + widgets.connect_button.add_css_class(&css_class); + }; + } } } } diff --git a/src/component/connection_page.rs b/src/component/connection_page.rs index 978e2cc..5aa92e4 100644 --- a/src/component/connection_page.rs +++ b/src/component/connection_page.rs @@ -1,12 +1,52 @@ #![allow(deprecated)] use std::borrow::Borrow; -use gtk::prelude::*; +use adw::prelude::*; +use gtk::{gdk, gio}; use relm4::{factory::DynamicIndex, *}; -use relm4_components::simple_combo_box::{SimpleComboBox, SimpleComboBoxMsg}; +use relm4_components::simple_adw_combo_row::{SimpleComboRow, SimpleComboRowMsg}; use tracing::info; -use crate::backend::repository::{KrustConnection, KrustConnectionSecurityType}; +use crate::{backend::repository::{KrustConnection, KrustConnectionSecurityType}, Repository}; + +// Color picker dialog +#[derive(Debug)] +pub struct ColorPickerDialog {} + +impl SimpleComponent for ColorPickerDialog { + type Init = (); + type Widgets = gtk::ColorDialog; + type Input = (); + type Output = (); + type Root = gtk::ColorDialog; + + fn init_root() -> Self::Root { + let about = gtk::ColorDialog::builder().build(); + about + } + + fn init( + _: Self::Init, + root: Self::Root, + _sender: ComponentSender, + ) -> ComponentParts { + let model = Self {}; + + let widgets = root.clone(); + + ComponentParts { model, widgets } + } + + fn update_view(&self, dialog: &mut Self::Widgets, _sender: ComponentSender) { + let parent = &relm4::main_application().active_window().unwrap(); + let cancellable: Option<&gio::Cancellable> = None; + dialog.choose_rgba(Some(parent), None, cancellable, |selected_color| { + info!("color::{:?}", selected_color); + }); + } +} + +// Color picker dialog #[derive(Debug)] pub struct ConnectionPageModel { @@ -18,7 +58,8 @@ pub struct ConnectionPageModel { sasl_mechanism: String, sasl_username: String, sasl_password: String, - security_type_combo: Controller>, + security_type_combo: Controller>, + color_picker_dialog: Controller, } #[derive(Debug)] @@ -42,58 +83,63 @@ impl Component for ConnectionPageModel { type Output = ConnectionPageOutput; view! { - #[root] - gtk::Grid { - set_margin_all: 10, - set_row_spacing: 6, - set_column_spacing: 10, - attach[0,0,1,2] = >k::Label { - set_label: "Name" - }, - attach[1,0,1,2]: name_entry = >k::Entry { - set_hexpand: true, - set_text: model.name.as_str(), - }, - attach[0,4,1,2] = >k::Label { - set_label: "Brokers" - }, - attach[1,4,1,2]: brokers_entry = >k::Entry { - set_hexpand: true, - set_text: model.brokers_list.as_str(), - }, - attach[0,8,1,2] = >k::Label { - set_label: "Security type" - }, - attach[1,8,1,2] = model.security_type_combo.widget() -> >k::ComboBoxText {}, - attach[0,16,1,2] = >k::Label { - set_label: "SASL mechanism" - }, - attach[1,16,1,2]: sasl_mechanism_entry = >k::Entry { - set_hexpand: true, - set_text: model.sasl_mechanism.as_str(), - }, - attach[0,24,1,2] = >k::Label { - set_label: "SASL username" - }, - attach[1,24,1,2]: sasl_username_entry = >k::Entry { - set_hexpand: true, - set_text: model.sasl_username.as_str(), - }, - attach[0,28,1,2] = >k::Label { - set_label: "SASL password" - }, - attach[1,28,1,2]: sasl_password_entry = >k::PasswordEntry { - set_hexpand: true, - set_text: model.sasl_password.as_str(), - }, - attach[1,128,1,2] = >k::Button { - set_label: "Save", - add_css_class: "suggested-action", - connect_clicked[sender] => move |_btn| { - sender.input(ConnectionPageMsg::Save) - }, - }, - } + #[root] + adw::PreferencesDialog { + set_title: "Connection", + set_height_request: 500, + add = &adw::PreferencesPage { + add = &adw::PreferencesGroup { + #[name = "name_entry" ] + adw::EntryRow { + set_title: "Name", + set_text: model.name.as_str(), + }, + #[name = "brokers_entry" ] + adw::EntryRow { + set_title: "Brokers", + set_text: model.brokers_list.as_str(), + }, + model.security_type_combo.widget() -> &adw::ComboRow { + set_title: "Security type", + set_subtitle: "Select security type", + set_use_subtitle: true, + }, + #[name = "sasl_mechanism_entry" ] + adw::EntryRow { + set_title: "SASL mechanism", + set_text: model.sasl_mechanism.as_str(), + }, + #[name = "sasl_username_entry" ] + adw::EntryRow { + set_title: "SASL username", + set_text: model.sasl_username.as_str(), + }, + #[name = "sasl_password_entry" ] + adw::PasswordEntryRow { + set_title: "SASL password", + set_text: model.sasl_password.as_str(), + }, + #[name = "color_button" ] + gtk::ColorDialogButton { + set_dialog = model.color_picker_dialog.widget() -> >k::ColorDialog {}, + connect_rgba_notify => move |btn| { + info!("color changed::{}", btn.rgba().to_str()); + + }, + }, + gtk::Button { + set_label: "Save", + add_css_class: "suggested-action", + set_vexpand: true, + set_valign: gtk::Align::End, + set_margin_top: 20, + connect_clicked[sender] => move |_btn| { + sender.input(ConnectionPageMsg::Save) + }, + }, + }, + }, + } } fn init( @@ -102,8 +148,8 @@ impl Component for ConnectionPageModel { sender: ComponentSender, ) -> ComponentParts { let default_idx = 0; - let security_type_combo = SimpleComboBox::builder() - .launch(SimpleComboBox { + let security_type = SimpleComboRow::builder() + .launch(SimpleComboRow { variants: KrustConnectionSecurityType::VALUES.to_vec(), active_index: Some(default_idx), }) @@ -111,8 +157,10 @@ impl Component for ConnectionPageModel { sender.input_sender(), ConnectionPageMsg::SecurityTypeChanged, ); - security_type_combo.widget().queue_allocate(); + //let security_type_combo = security_type.widget(); let current = current_connection.clone(); + let color_picker_dialog = ColorPickerDialog::builder().launch(()).detach(); + let model = ConnectionPageModel { current_index: None, name: current @@ -130,7 +178,7 @@ impl Component for ConnectionPageModel { .as_ref() .map(|c| c.security_type.clone()) .unwrap_or_default(), - security_type_combo, + security_type_combo: security_type, sasl_mechanism: current .borrow() .as_ref() @@ -147,6 +195,7 @@ impl Component for ConnectionPageModel { .map(|c| c.sasl_password.clone().unwrap_or_default()) .unwrap_or_default(), current: current_connection, + color_picker_dialog: color_picker_dialog, }; //let security_type_combo = model.security_type_combo.widget(); let widgets = view_output!(); @@ -159,7 +208,7 @@ impl Component for ConnectionPageModel { widgets: &mut Self::Widgets, msg: ConnectionPageMsg, sender: ComponentSender, - _: &Self::Root, + root: &Self::Root, ) { info!("received message: {:?}", msg); @@ -174,9 +223,9 @@ impl Component for ConnectionPageModel { KrustConnectionSecurityType::PLAINTEXT => false, KrustConnectionSecurityType::SASL_PLAINTEXT => true, }; - widgets.sasl_mechanism_entry.set_sensitive(sasl_visible); - widgets.sasl_username_entry.set_sensitive(sasl_visible); - widgets.sasl_password_entry.set_sensitive(sasl_visible); + widgets.sasl_mechanism_entry.set_visible(sasl_visible); + widgets.sasl_username_entry.set_visible(sasl_visible); + widgets.sasl_password_entry.set_visible(sasl_visible); } ConnectionPageMsg::New => { widgets.name_entry.set_text(""); @@ -184,12 +233,12 @@ impl Component for ConnectionPageModel { widgets.sasl_mechanism_entry.set_text(""); widgets.sasl_username_entry.set_text(""); widgets.sasl_password_entry.set_text(""); - widgets.sasl_mechanism_entry.set_sensitive(false); - widgets.sasl_username_entry.set_sensitive(false); - widgets.sasl_password_entry.set_sensitive(false); + widgets.sasl_mechanism_entry.set_visible(false); + widgets.sasl_username_entry.set_visible(false); + widgets.sasl_password_entry.set_visible(false); self.security_type_combo .sender() - .emit(SimpleComboBoxMsg::SetActiveIdx(0)); + .emit(SimpleComboRowMsg::SetActiveIdx(0)); self.name = String::default(); self.brokers_list = String::default(); self.security_type = KrustConnectionSecurityType::default(); @@ -198,6 +247,9 @@ impl Component for ConnectionPageModel { self.sasl_password = String::default(); self.current = None; self.current_index = None; + root.queue_allocate(); + let parent = &relm4::main_application().active_window().unwrap(); + root.present(parent); } ConnectionPageMsg::Save => { let name = widgets.name_entry.text().to_string(); @@ -215,6 +267,8 @@ impl Component for ConnectionPageModel { vstr => Some(vstr.to_string()), }; let security_type = self.security_type.clone(); + let color = widgets.color_button.rgba(); + info!("selected color::{:?}", color); widgets.name_entry.set_text(""); widgets.brokers_entry.set_text(""); widgets.sasl_username_entry.set_text(""); @@ -232,12 +286,18 @@ impl Component for ConnectionPageModel { sasl_password, sasl_mechanism, security_type, + color: Some(color.to_string()), }, )) .unwrap(); + root.close(); } ConnectionPageMsg::Edit(index, connection) => { let idx = Some(index.clone()); + let connection = Repository::new().connection_by_id(connection.id.unwrap()).unwrap(); + let color = connection.color.clone().unwrap_or("rgb(183, 243, 155)".to_string()); + let color = gdk::RGBA::parse(color).expect("Should return RGBA color"); + widgets.color_button.set_rgba(&color); let conn = connection.clone(); self.current_index = idx; self.current = Some(connection); @@ -251,13 +311,15 @@ impl Component for ConnectionPageModel { widgets .brokers_entry .set_text(self.brokers_list.clone().as_str()); + widgets.sasl_mechanism_entry.set_text(&self.sasl_mechanism); let combo_idx = KrustConnectionSecurityType::VALUES .iter() .position(|v| *v == self.security_type) - .unwrap_or_default(); + .expect("Should return option index"); + info!("connection_dialog::security_type::index::{}", combo_idx); self.security_type_combo .sender() - .emit(SimpleComboBoxMsg::SetActiveIdx(combo_idx)); + .emit(SimpleComboRowMsg::SetActiveIdx(combo_idx)); widgets .sasl_username_entry .set_text(self.sasl_username.clone().as_str()); @@ -271,7 +333,9 @@ impl Component for ConnectionPageModel { widgets.sasl_mechanism_entry.set_sensitive(sasl_visible); widgets.sasl_username_entry.set_sensitive(sasl_visible); widgets.sasl_password_entry.set_sensitive(sasl_visible); - //mem::swap(self, &mut model_from(idx, Some(conn))); + root.queue_allocate(); + let parent = &relm4::main_application().active_window().unwrap(); + root.present(parent); } }; diff --git a/src/component/messages/messages_tab.rs b/src/component/messages/messages_tab.rs index a0bb3ea..fa3bd88 100644 --- a/src/component/messages/messages_tab.rs +++ b/src/component/messages/messages_tab.rs @@ -1,5 +1,6 @@ #![allow(deprecated)] use std::borrow::Borrow; +use std::str::FromStr; // See: https://gitlab.gnome.org/GNOME/gtk/-/issues/5644 use chrono::{TimeZone, Utc}; @@ -21,7 +22,7 @@ use tokio_util::sync::CancellationToken; use tracing::{debug, info, trace}; use crate::backend::settings::Settings; -use crate::component::banner::{AppBannerMsg, BANNER_BROKER}; +use crate::component::settings_dialog::MessagesSortOrder; use crate::component::task_manager::{Task, TaskManagerMsg, TaskVariant, TASK_MANAGER_BROKER}; use crate::{ backend::{ @@ -41,6 +42,7 @@ use crate::{ }, Repository, }; +use crate::{AppMsg, TOASTER_BROKER}; use super::message_viewer::{MessageViewerModel, MessageViewerMsg}; use super::messages_send_dialog::MessagesSendDialogMsg; @@ -111,7 +113,7 @@ pub enum CommandMsg { CopyToClipboard(String), } -const AVAILABLE_PAGE_SIZES: [u16; 6] = [50, 100, 500, 1000, 2000, 5000]; +const AVAILABLE_PAGE_SIZES: [u16; 7] = [1000, 2000, 5000, 7000, 10000, 20000, 50000]; #[relm4::factory(pub)] impl FactoryComponent for MessagesTabModel { @@ -158,6 +160,7 @@ impl FactoryComponent for MessagesTabModel { set_hexpand: true, #[name(btn_get_messages)] gtk::Button { + set_tooltip_text: Some("Show messages"), set_icon_name: "media-playback-start-symbolic", connect_clicked[sender] => move |_| { sender.input(MessagesTabMsg::GetMessages); @@ -165,6 +168,7 @@ impl FactoryComponent for MessagesTabModel { }, #[name(btn_stop_messages)] gtk::Button { + set_tooltip_text: Some("Stop current task"), set_icon_name: "media-playback-stop-symbolic", set_margin_start: 5, connect_clicked[sender] => move |_| { @@ -173,6 +177,7 @@ impl FactoryComponent for MessagesTabModel { }, #[name(btn_cache_refresh)] gtk::Button { + set_tooltip_text: Some("Refresh cache"), set_icon_name: "media-playlist-repeat-symbolic", set_margin_start: 5, connect_clicked[sender] => move |_| { @@ -181,6 +186,7 @@ impl FactoryComponent for MessagesTabModel { }, #[name(btn_send_messages)] gtk::Button { + set_tooltip_text: Some("Send messages"), set_icon_name: "list-add-symbolic", set_margin_start: 5, connect_clicked[sender] => move |_| { @@ -198,6 +204,7 @@ impl FactoryComponent for MessagesTabModel { }, #[name(cache_timestamp)] gtk::Label { + set_tooltip_text: Some("Last cache refresh timestamp"), set_margin_start: 5, set_label: "", set_visible: false, @@ -229,6 +236,7 @@ impl FactoryComponent for MessagesTabModel { set_vexpand: true, set_hexpand: true, set_propagate_natural_width: true, + #[name = "messages_view" ] self.messages_wrapper.view.clone() -> gtk::ColumnView { set_vexpand: true, set_hexpand: true, @@ -564,9 +572,11 @@ impl FactoryComponent for MessagesTabModel { widgets.cached_controls.set_visible(true); widgets.cached_centered_controls.set_visible(true); widgets.live_controls.set_visible(false); + widgets.btn_cache_refresh.set_visible(true); MessagesMode::Cached { refresh: false } } else { widgets.live_controls.set_visible(true); + widgets.btn_cache_refresh.set_visible(false); widgets.cached_controls.set_visible(false); widgets.cached_centered_controls.set_visible(false); widgets.cache_timestamp.set_visible(false); @@ -687,7 +697,6 @@ impl FactoryComponent for MessagesTabModel { } MessagesTabMsg::GetMessages => { //sender.output(MessagesTabOutput::ShowBanner("Working...".to_string())).unwrap(); - BANNER_BROKER.send(AppBannerMsg::Show("Working...".to_string())); STATUS_BROKER.send(StatusBarMsg::Start); on_loading(widgets, false); let mode = self.mode; @@ -714,25 +723,24 @@ impl FactoryComponent for MessagesTabModel { Some(task_name), Some(self.token.clone()), ); + TOASTER_BROKER.send(AppMsg::ShowToast(task.id.clone(), "Working...".to_string())); TASK_MANAGER_BROKER.send(TaskManagerMsg::AddTask(task.clone())); sender.oneshot_command(async move { // Run async background task let messages_worker = MessagesWorker::new(); let result = &messages_worker - .get_messages( - &MessagesRequest { - task: Some(task), - mode: mode, - connection: conn, - topic: topic.clone(), - page_operation: PageOp::Next, - page_size, - offset_partition: (0, 0), - search: search, - fetch, - max_messages, - }, - ) + .get_messages(&MessagesRequest { + task: Some(task), + mode: mode, + connection: conn, + topic: topic.clone(), + page_operation: PageOp::Next, + page_size, + offset_partition: (0, 0), + search: search, + fetch, + max_messages, + }) .await .unwrap(); let total = result.total; @@ -742,7 +750,6 @@ impl FactoryComponent for MessagesTabModel { } MessagesTabMsg::GetNextMessages => { STATUS_BROKER.send(StatusBarMsg::Start); - BANNER_BROKER.send(AppBannerMsg::Show("Working...".to_string())); on_loading(widgets, false); let mode = self.mode; let topic = self.topic.clone().unwrap(); @@ -778,25 +785,24 @@ impl FactoryComponent for MessagesTabModel { Some(task_name), Some(self.token.clone()), ); + TOASTER_BROKER.send(AppMsg::ShowToast(task.id.clone(), "Working...".to_string())); TASK_MANAGER_BROKER.send(TaskManagerMsg::AddTask(task.clone())); sender.oneshot_command(async move { // Run async background task let messages_worker = MessagesWorker::new(); let result = &messages_worker - .get_messages( - &MessagesRequest { - task: Some(task.clone()), - mode: mode, - connection: conn, - topic: topic.clone(), - page_operation: PageOp::Next, - page_size, - offset_partition: (offset, partition), - search: search, - fetch, - max_messages, - }, - ) + .get_messages(&MessagesRequest { + task: Some(task.clone()), + mode: mode, + connection: conn, + topic: topic.clone(), + page_operation: PageOp::Next, + page_size, + offset_partition: (offset, partition), + search: search, + fetch, + max_messages, + }) .await .unwrap(); let total = result.total; @@ -806,7 +812,6 @@ impl FactoryComponent for MessagesTabModel { } MessagesTabMsg::GetPreviousMessages => { STATUS_BROKER.send(StatusBarMsg::Start); - BANNER_BROKER.send(AppBannerMsg::Show("Working...".to_string())); on_loading(widgets, false); let mode = self.mode; let topic = self.topic.clone().unwrap(); @@ -838,25 +843,24 @@ impl FactoryComponent for MessagesTabModel { Some(task_name), Some(self.token.clone()), ); + TOASTER_BROKER.send(AppMsg::ShowToast(task.id.clone(), "Working...".to_string())); TASK_MANAGER_BROKER.send(TaskManagerMsg::AddTask(task.clone())); sender.oneshot_command(async move { // Run async background task let messages_worker = MessagesWorker::new(); let result = &messages_worker - .get_messages( - &MessagesRequest { - task: Some(task.clone()), - mode:mode, - connection: conn, - topic: topic.clone(), - page_operation: PageOp::Prev, - page_size, - offset_partition: (offset, partition), - search: search, - fetch, - max_messages, - }, - ) + .get_messages(&MessagesRequest { + task: Some(task.clone()), + mode: mode, + connection: conn, + topic: topic.clone(), + page_operation: PageOp::Prev, + page_size, + offset_partition: (offset, partition), + search: search, + fetch, + max_messages, + }) .await .unwrap(); let total = result.total; @@ -878,7 +882,8 @@ impl FactoryComponent for MessagesTabModel { }); } MessagesTabMsg::UpdateMessages(response) => { - let timestamp_formatter = Settings::read().unwrap_or_default().timestamp_formatter(); + let settings = Settings::read().unwrap_or_default(); + let timestamp_formatter = settings.timestamp_formatter(); let total = response.total; self.topic = response.topic.clone(); match self.mode { @@ -894,6 +899,25 @@ impl FactoryComponent for MessagesTabModel { response.messages.first(), response.messages.last(), ); + let sort_column = settings.messages_sort_column; + let sort_column = self + .messages_wrapper + .get_columns() + .get(sort_column.as_str()); + let sort_order = + MessagesSortOrder::from_str(settings.messages_sort_column_order.as_str()) + .unwrap_or_default(); + let sort_type = match sort_order { + MessagesSortOrder::Ascending => gtk::SortType::Ascending, + MessagesSortOrder::Descending => gtk::SortType::Descending, + MessagesSortOrder::Default => match self.fetch_type { + KafkaFetch::Newest => gtk::SortType::Descending, + KafkaFetch::Oldest => gtk::SortType::Ascending, + }, + }; + + info!("sort_column::{:?}, sort_type::{:?}", sort_column, sort_type); + widgets.messages_view.sort_by_column(sort_column, sort_type); self.messages_wrapper.extend_from_iter( response .messages @@ -915,7 +939,7 @@ impl FactoryComponent for MessagesTabModel { .unwrap_or(String::default()); widgets.cache_timestamp.set_label(&cache_ts); widgets.cache_timestamp.set_visible(true); - BANNER_BROKER.send(AppBannerMsg::Hide); + TOASTER_BROKER.send(AppMsg::HideToast(response.task.clone().unwrap().id.clone())); STATUS_BROKER.send(StatusBarMsg::StopWithInfo { text: Some(format!("{} messages loaded!", self.messages_wrapper.len())), }); @@ -1061,11 +1085,12 @@ fn copy_all_as_csv( Err(_) => value.replace('\n', ""), }; let timestamp = item.borrow().timestamp; - let timestamp = Utc.timestamp_millis_opt(timestamp.unwrap_or_default()) - .unwrap() - .with_timezone(&America::Sao_Paulo) - .format(×tamp_format) - .to_string(); + let timestamp = Utc + .timestamp_millis_opt(timestamp.unwrap_or_default()) + .unwrap() + .with_timezone(&America::Sao_Paulo) + .format(×tamp_format) + .to_string(); let record = StringRecord::from(vec![ partition.to_string(), offset.to_string(), diff --git a/src/component/mod.rs b/src/component/mod.rs index 3591e1c..9f88902 100644 --- a/src/component/mod.rs +++ b/src/component/mod.rs @@ -6,7 +6,6 @@ pub mod topics; pub(crate) mod connection_list; mod connection_page; -mod settings_dialog; +pub(crate) mod settings_dialog; mod status_bar; -mod banner; pub(crate) mod task_manager; diff --git a/src/component/settings_dialog.rs b/src/component/settings_dialog.rs index 87ffa4d..c7ff448 100644 --- a/src/component/settings_dialog.rs +++ b/src/component/settings_dialog.rs @@ -2,17 +2,37 @@ use std::path::PathBuf; use adw::prelude::*; use relm4::{gtk, Component, ComponentController, ComponentParts, ComponentSender, Controller}; -use relm4_components::open_dialog::{ - OpenDialog, OpenDialogMsg, OpenDialogResponse, OpenDialogSettings, +use relm4_components::{ + open_dialog::{OpenDialog, OpenDialogMsg, OpenDialogResponse, OpenDialogSettings}, + simple_adw_combo_row::{SimpleComboRow, SimpleComboRowMsg}, }; +use strum::{Display, EnumString}; use tracing::*; use crate::backend::settings::Settings; +const MESSAGE_COLUMNS: [&str;4] = ["Offset", "Partition", "Key", "Date/time (Timestamp)"]; + +#[derive(Clone, Debug, Display, EnumString, Default)] +pub enum MessagesSortOrder { + Ascending, + Descending, + #[default] + Default, +} + +impl MessagesSortOrder { + pub const VALUES: [Self; 3] = [Self::Default, Self::Ascending, Self::Descending]; +} + pub struct SettingsDialogModel { cache_dir: String, cache_dir_dialog: Controller, is_full_timestamp: bool, + messages_sort_column_combo: Controller>, + messages_sort_column_order_combo: Controller>, + messages_sort_column: String, + messages_sort_column_order: String, } #[derive(Debug)] @@ -23,6 +43,8 @@ pub enum SettingsDialogMsg { OpenCacheDir(PathBuf), SwitchFullTimestamp, Ignore, + MessagesColumnSelected(usize), + MessagesColumnOrderSelected(usize), } pub struct SettingsDialogInit {} @@ -50,7 +72,20 @@ impl Component for SettingsDialogModel { set_subtitle: "Show message timestamp with milliseconds", set_active: model.is_full_timestamp, connect_active_notify => SettingsDialogMsg::SwitchFullTimestamp, - } + }, + }, + add = &adw::PreferencesGroup { + set_title: "Sorting", + #[local_ref] + messages_sort_column_combo -> adw::ComboRow { + set_title: "Column", + set_subtitle: "Default sort column", + }, + #[local_ref] + messages_sort_column_order_combo -> adw::ComboRow { + set_title: "Order", + set_subtitle: "Default sort order for column", + }, }, add = &adw::PreferencesGroup { set_title: "Caching", @@ -101,11 +136,38 @@ impl Component for SettingsDialogModel { OpenDialogResponse::Accept(path) => SettingsDialogMsg::OpenCacheDir(path), OpenDialogResponse::Cancel => SettingsDialogMsg::Ignore, }); + let default_idx = 0; + let default_message_column_combo = SimpleComboRow::builder() + .launch(SimpleComboRow { + variants: MESSAGE_COLUMNS.iter() + .map(|s| s.to_string()) + .collect(), + active_index: Some(default_idx), + }) + .forward( + sender.input_sender(), + SettingsDialogMsg::MessagesColumnSelected, + ); + let default_message_column_order_combo = SimpleComboRow::builder() + .launch(SimpleComboRow { + variants: MessagesSortOrder::VALUES.to_vec(), + active_index: Some(default_idx), + }) + .forward( + sender.input_sender(), + SettingsDialogMsg::MessagesColumnOrderSelected, + ); let model = SettingsDialogModel { cache_dir: current.cache_dir, cache_dir_dialog, is_full_timestamp: current.is_full_timestamp, + messages_sort_column_combo: default_message_column_combo, + messages_sort_column_order_combo: default_message_column_order_combo, + messages_sort_column: current.messages_sort_column, + messages_sort_column_order: current.messages_sort_column_order, }; + let messages_sort_column_combo = model.messages_sort_column_combo.widget(); + let messages_sort_column_order_combo = model.messages_sort_column_order_combo.widget(); let widgets = view_output!(); ComponentParts { model, widgets } } @@ -118,11 +180,40 @@ impl Component for SettingsDialogModel { root: &Self::Root, ) { match message { + SettingsDialogMsg::MessagesColumnSelected(_idx) => { + let column = match self.messages_sort_column_combo.model().get_active_elem() { + Some(opt) => opt.clone(), + None => "Offset".to_string(), + }; + info!("selected column {}", column); + self.messages_sort_column = column; + sender.input(SettingsDialogMsg::Save); + } + SettingsDialogMsg::MessagesColumnOrderSelected(_idx) => { + let column_order = match self.messages_sort_column_order_combo.model().get_active_elem() { + Some(opt) => opt.to_string(), + None => MessagesSortOrder::Default.to_string(), + }; + info!("selected column_order {}", column_order); + self.messages_sort_column_order = column_order; + sender.input(SettingsDialogMsg::Save); + } SettingsDialogMsg::Show => { let parent = &relm4::main_application().active_window().unwrap(); let current_settings = Settings::read().unwrap_or_default(); self.cache_dir = current_settings.cache_dir; self.is_full_timestamp = current_settings.is_full_timestamp; + self.messages_sort_column = current_settings.messages_sort_column; + let combo_idx = MESSAGE_COLUMNS + .iter() + .position(|v| *v == self.messages_sort_column.as_str()) + .expect("Should return option index"); + self.messages_sort_column_combo.emit(SimpleComboRowMsg::SetActiveIdx(combo_idx)); + let combo_idx = MessagesSortOrder::VALUES + .iter() + .position(|v| *v.to_string() == self.messages_sort_column_order) + .expect("Should return option index"); + self.messages_sort_column_order_combo.emit(SimpleComboRowMsg::SetActiveIdx(combo_idx)); root.queue_allocate(); root.present(parent); } @@ -147,11 +238,14 @@ impl Component for SettingsDialogModel { } SettingsDialogMsg::Save => { let cache_dir = self.cache_dir.clone(); - let settings = Settings { cache_dir: cache_dir, is_full_timestamp: self.is_full_timestamp }; + let settings = Settings { + cache_dir: cache_dir, + is_full_timestamp: self.is_full_timestamp, + messages_sort_column: self.messages_sort_column.clone(), + messages_sort_column_order: self.messages_sort_column_order.clone(), + }; info!("settings_dialog::saving::{:?}", settings); - settings - .write() - .expect("should write current settings"); + settings.write().expect("should write current settings"); } } } diff --git a/src/component/topics/topics_page.rs b/src/component/topics/topics_page.rs index 4565255..ec43004 100644 --- a/src/component/topics/topics_page.rs +++ b/src/component/topics/topics_page.rs @@ -1,13 +1,9 @@ use crate::{ - backend:: - repository::{KrustConnection, KrustTopic} - , + backend::repository::{KrustConnection, KrustTopic}, component::topics::topics_tab::{TopicsTabInit, TopicsTabOutput}, }; use adw::{prelude::*, TabPage}; -use relm4::{ - actions::RelmAction, factory::FactoryVecDeque, * -}; +use relm4::{actions::RelmAction, factory::FactoryVecDeque, *}; use tracing::*; use super::topics_tab::TopicsTabModel; @@ -93,7 +89,9 @@ impl Component for TopicsPageModel { let topics = FactoryVecDeque::builder() .launch(adw::TabView::default()) .forward(sender.output_sender(), |msg| match msg { - TopicsTabOutput::OpenMessagesPage(conn, topic) => TopicsPageOutput::OpenMessagesPage(conn, topic), + TopicsTabOutput::OpenMessagesPage(conn, topic) => { + TopicsPageOutput::OpenMessagesPage(conn, topic) + } }); let topics_viewer: &adw::TabView = topics.widget(); @@ -109,7 +107,8 @@ impl Component for TopicsPageModel { let widgets = view_output!(); - let mut topics_tabs_actions = relm4::actions::RelmActionGroup::::new(); + let mut topics_tabs_actions = + relm4::actions::RelmActionGroup::::new(); let tabs_sender = sender.input_sender().clone(); let close_tab_action = RelmAction::::new_stateless(move |_| { tabs_sender.send(TopicsPageMsg::MenuPageClosed).unwrap(); @@ -130,7 +129,11 @@ impl Component for TopicsPageModel { }; ComponentParts { model, widgets } } + fn post_view(&self, widgets: &mut Self::Widgets) { + //widgets.topics_tabs.remove_css_class(&css_class); + //widgets.topics_tabs.add_css_class(&css_class); + } fn update_with_view( &mut self, widgets: &mut Self::Widgets, @@ -170,12 +173,10 @@ impl Component for TopicsPageModel { } TopicsPageMsg::PageAdded(page, index) => { let tab_model = self.topics.get(index.try_into().unwrap()).unwrap(); - let title = format!( - "{}", - tab_model.current.clone().unwrap().name, - ); + let title = format!("{}", tab_model.current.clone().unwrap().name,); page.set_title(title.as_str()); page.set_live_thumbnail(true); + widgets.topics_viewer.set_selected_page(&page); } TopicsPageMsg::MenuPagePin => { @@ -194,10 +195,7 @@ impl Component for TopicsPageModel { for i in 0..topics.len() { let tp = topics.get_mut(i); if let Some(tp) = tp { - let title = format!( - "{}", - tp.current.clone().unwrap().name.clone(), - ); + let title = format!("{}", tp.current.clone().unwrap().name.clone(),); info!("PageClosed [{}][{}={}]", i, title, page.title()); if title.eq(&page.title().to_string()) { idx = Some(i); @@ -220,5 +218,4 @@ impl Component for TopicsPageModel { self.update_view(widgets, sender); } - } diff --git a/src/lib.rs b/src/lib.rs index 6c2e3bd..39468a3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,6 +16,7 @@ pub use component::app::AppModel; pub use component::app::AppMsg; pub use backend::repository::Repository; pub use component::messages::messages_tab::MessagesSearchAction; +pub use component::app::TOASTER_BROKER; pub const KRUST_QUALIFIER: &str = "io"; pub const KRUST_ORGANIZATION: &str = "miguelbaldi"; diff --git a/src/main.rs b/src/main.rs index a431412..b0f419e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ use gtk::gio; use gtk::gio::ApplicationFlags; use gtk::prelude::ApplicationExt; use krust::APP_RESOURCE_PATH; +use krust::TOASTER_BROKER; use tracing::*; use tracing_subscriber::filter; use tracing_subscriber::prelude::*; @@ -84,7 +85,7 @@ fn main() -> Result<(), ()> { setup_shortcuts(&app); - let app = RelmApp::from_app(app); + let app = RelmApp::from_app(app).with_broker(&TOASTER_BROKER); app.set_global_css(include_str!("styles.less")); info!("running application"); app.visible_on_activate(false).run::(()); diff --git a/src/styles.less b/src/styles.less index b3b077e..08f8f15 100644 --- a/src/styles.less +++ b/src/styles.less @@ -15,7 +15,7 @@ button.krust-toggle { } button.krust-toggle:checked { - background: #b7f39b; + //background: #b7f39b; text-decoration: underline; }