From deedbca58183902a62ed3fbdc4f065a13aa90e0e Mon Sep 17 00:00:00 2001 From: Andrey Kutejko Date: Sun, 6 Aug 2023 13:46:25 +0200 Subject: [PATCH] Replace a modal dialog for entering file's password with a sliding pane. --- Cargo.lock | 27 +++++ Cargo.toml | 1 + src/main_window.rs | 32 +++-- src/ui/dialogs/mod.rs | 1 - src/ui/dialogs/read_file.rs | 84 ------------- src/ui/mod.rs | 1 + src/ui/open_file.rs | 229 ++++++++++++++++++++++++++++++++++++ src/utils/ui.rs | 16 +++ 8 files changed, 296 insertions(+), 95 deletions(-) delete mode 100644 src/ui/dialogs/read_file.rs create mode 100644 src/ui/open_file.rs diff --git a/Cargo.lock b/Cargo.lock index 41e29f9b..30b5b7fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -170,6 +170,21 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "futures" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.28" @@ -177,6 +192,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -213,6 +229,12 @@ dependencies = [ "syn 2.0.28", ] +[[package]] +name = "futures-sink" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" + [[package]] name = "futures-task" version = "0.3.28" @@ -225,9 +247,13 @@ version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" dependencies = [ + "futures-channel", "futures-core", + "futures-io", "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", "slab", @@ -627,6 +653,7 @@ dependencies = [ "awesome-gtk", "cbc", "deflate", + "futures", "glib-build-tools", "gtk4", "inflate", diff --git a/Cargo.toml b/Cargo.toml index 4afc3619..616a9337 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ readme = "README.md" [dependencies] once_cell = "1" +futures = "0.3" os_str_bytes = "6" rand = "0.8" diff --git a/src/main_window.rs b/src/main_window.rs index 79b7649b..cd28a57d 100644 --- a/src/main_window.rs +++ b/src/main_window.rs @@ -10,10 +10,10 @@ use crate::ui::dialogs::ask::{confirm_likely, confirm_unlikely}; use crate::ui::dialogs::ask_save::{ask_save, AskSave}; use crate::ui::dialogs::change_password::change_password; use crate::ui::dialogs::file_chooser; -use crate::ui::dialogs::read_file::read_file; use crate::ui::dialogs::say::{say_error, say_info}; use crate::ui::edit_record::edit_record; use crate::ui::group_selector::select_group; +use crate::ui::open_file::OpenFile; use crate::ui::record_type_popover::RecordTypePopoverBuilder; use crate::ui::record_view::view::PSRecordView; use crate::ui::search::{PSSearchBar, SearchEvent, SearchEventType}; @@ -62,6 +62,8 @@ mod imp { pub dashboard: PSDashboard, + pub open_file: OpenFile, + pub toast: Toast, pub search_bar: PSSearchBar, pub nav_bar: PSNavBar, @@ -196,6 +198,7 @@ mod imp { .set_transition_type(gtk::StackTransitionType::SlideLeftRight); self.stack .add_named(&self.dashboard.get_widget(), Some("dashboard")); + self.stack.add_named(&self.open_file, Some("open_file")); self.stack.add_named( &overlayed(&tree_container, &self.toast.as_widget()), Some("file"), @@ -517,8 +520,24 @@ impl PSMainWindow { } } + async fn load_data(&self, filename: PathBuf) -> Option<(RecordTree, String)> { + self.imp().stack.set_visible_child_name("open_file"); + + let result = self + .imp() + .open_file + .run(move |password| format::load_file(&filename, password)) + .await; + + if result.is_none() { + self.imp().stack.set_visible_child_name("dashboard"); + } + + result + } + pub async fn do_open_file(&self, filename: &Path) { - if let Some((data, password)) = load_data(filename.to_owned(), self.upcast_ref()).await { + if let Some((data, password)) = self.load_data(filename.to_owned()).await { self.imp().cache.get().unwrap().add_file(filename); *self.file_mut() = OpenedFile { @@ -651,7 +670,7 @@ impl PSMainWindow { let window = self.upcast_ref(); let Some(filename) = file_chooser::open_file(window).await else { return }; - let Some((extra_records, _password)) = load_data(filename, window).await else { return }; + let Some((extra_records, _password)) = self.load_data(filename).await else { return }; // TODO: maybe do merge into current folder? let merged_tree = crate::model::merge_trees::merge_trees(&self.file().data, &extra_records); @@ -891,13 +910,6 @@ impl PSMainWindow { } } -async fn load_data(filename: PathBuf, parent_window: >k::Window) -> Option<(RecordTree, String)> { - read_file(parent_window, move |password| { - format::load_file(&filename, password) - }) - .await -} - async fn new_password(parent_window: >k::Window) -> Option { // TODO: ADD confirmation let mut form = ui::forms::form::Form::new(); diff --git a/src/ui/dialogs/mod.rs b/src/ui/dialogs/mod.rs index d0128cf1..3633a52e 100644 --- a/src/ui/dialogs/mod.rs +++ b/src/ui/dialogs/mod.rs @@ -4,5 +4,4 @@ pub mod ask_save; pub mod change_password; pub mod file_chooser; pub mod preferences; -pub mod read_file; pub mod say; diff --git a/src/ui/dialogs/read_file.rs b/src/ui/dialogs/read_file.rs deleted file mode 100644 index b6f7dab1..00000000 --- a/src/ui/dialogs/read_file.rs +++ /dev/null @@ -1,84 +0,0 @@ -use crate::error::*; -use crate::ui::error_label::create_error_label; -use gtk::{glib, prelude::*}; - -pub async fn read_file( - parent_window: >k::Window, - read_file_callback: R, -) -> Option<(T, String)> -where - T: 'static, - R: Fn(&str) -> Result + 'static, -{ - let dlg = gtk::Dialog::builder() - .modal(true) - .transient_for(parent_window) - .use_header_bar(1) - .title("Enter password") - .icon_name("password-storage") - .resizable(false) - .build(); - dlg.add_button("_Cancel", gtk::ResponseType::Cancel); - dlg.add_button("_Open file", gtk::ResponseType::Accept); - dlg.set_default_response(gtk::ResponseType::Accept); - - let error_label = create_error_label(); - - let label = gtk::Label::builder() - .label("Password") - .xalign(0f32) - .yalign(0.5f32) - .build(); - - let entry = gtk::Entry::builder() - .can_focus(true) - .activates_default(true) - .visibility(false) - .hexpand(true) - .build(); - - let grid = gtk::Grid::builder() - .margin_start(8) - .margin_end(8) - .margin_top(8) - .margin_bottom(8) - .column_spacing(8) - .row_spacing(8) - .build(); - grid.attach(&error_label, 0, 0, 2, 1); - grid.attach(&label, 0, 1, 1, 1); - grid.attach(&entry, 1, 1, 1, 1); - - dlg.content_area().append(&grid); - dlg.content_area().set_spacing(8); - - entry.connect_changed(glib::clone!(@weak dlg => move |e| { - dlg.set_response_sensitive( - gtk::ResponseType::Accept, - e.chars(0, -1).len() > 0, - ); - })); - - dlg.set_response_sensitive(gtk::ResponseType::Accept, false); - - loop { - let button = dlg.run_future().await; - - if button != gtk::ResponseType::Accept { - dlg.hide(); - return None; - } - - let password = entry.text(); - match read_file_callback(&password) { - Ok(document) => { - dlg.hide(); - return Some((document, password.into())); - } - Err(e) => { - error_label.set_visible(true); - error_label.set_label(&format!("Can't open this file.\n{e}")); - } - } - } -} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 87891620..69a73f56 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -9,6 +9,7 @@ pub mod forms; pub mod group_selector; pub mod menu; pub mod nav_bar; +pub mod open_file; pub mod password_editor; pub mod password_strength_bar; pub mod record_type_popover; diff --git a/src/ui/open_file.rs b/src/ui/open_file.rs new file mode 100644 index 00000000..2bc2d233 --- /dev/null +++ b/src/ui/open_file.rs @@ -0,0 +1,229 @@ +use crate::error::*; +use futures::channel::mpsc::{channel, Receiver, Sender}; +use futures::stream::StreamExt; +use gtk::{gdk, glib, prelude::*, subclass::prelude::*}; +use std::cell::RefCell; +use std::error::Error; + +mod imp { + use super::*; + use crate::utils::ui::{hexpander, vexpander}; + use awesome_gtk::widget::AwesomeWidgetTraverseExt; + + pub struct OpenFile { + pub entry: gtk::Entry, + pub error_label: gtk::Label, + pub open_button: gtk::Button, + pub key_controller: gtk::EventControllerKey, + pub receiver: RefCell>>, + } + + #[glib::object_subclass] + impl ObjectSubclass for OpenFile { + const NAME: &'static str = "PSOpenFile"; + type Type = super::OpenFile; + type ParentType = gtk::Widget; + + fn new() -> Self { + let error_label = gtk::Label::builder() + .xalign(0.5) + .yalign(1.0) + .vexpand(true) + .build(); + error_label.add_css_class("error"); + + let open_button = gtk::Button::builder() + .label("_Open file") + .use_underline(true) + .hexpand(true) + .build(); + open_button.add_css_class("suggested-action"); + + Self { + entry: gtk::Entry::builder() + .can_focus(true) + .activates_default(true) + .visibility(false) + .hexpand(true) + .build(), + error_label, + open_button, + key_controller: Default::default(), + receiver: Default::default(), + } + } + } + + impl ObjectImpl for OpenFile { + fn constructed(&self) { + self.parent_constructed(); + + let obj = self.obj(); + obj.set_layout_manager(Some(gtk::BinLayout::new())); + + let grid = gtk::Grid::builder() + .width_request(400) + .hexpand(true) + .vexpand(true) + .margin_start(8) + .margin_end(8) + .margin_top(8) + .margin_bottom(8) + .column_spacing(8) + .row_spacing(8) + .build(); + grid.set_parent(&*obj); + + let (sender, receiver) = channel::(0); + *self.receiver.borrow_mut() = Some(receiver); + + let label = gtk::Label::builder() + .label("_Password") + .use_underline(true) + .mnemonic_widget(&self.entry) + .xalign(0_f32) + .yalign(0.5_f32) + .build(); + + let cancel_button = gtk::Button::builder() + .label("_Cancel") + .use_underline(true) + .hexpand(true) + .build(); + cancel_button.connect_clicked({ + let sender = ResponseSender::new(&sender); + move |_| sender.send(gtk::ResponseType::Cancel) + }); + + self.open_button.connect_clicked({ + let sender = ResponseSender::new(&sender); + move |_| sender.send(gtk::ResponseType::Accept) + }); + + self.entry.connect_changed( + glib::clone!(@weak self.open_button as open_button => move |e| { + let text_length = e.chars(0, -1).len(); + open_button.set_sensitive(text_length > 0); + }), + ); + + self.entry.connect_activate({ + let sender = ResponseSender::new(&sender); + move |_| sender.send(gtk::ResponseType::Accept) + }); + + self.entry.add_controller(self.key_controller.clone()); + self.key_controller.connect_key_pressed({ + let sender = ResponseSender::new(&sender); + move |_, key, _keycode, _modifier| { + if key == gdk::Key::Escape { + sender.send(gtk::ResponseType::Cancel); + } + glib::Propagation::Proceed + } + }); + let button_box = gtk::Box::builder() + .orientation(gtk::Orientation::Horizontal) + .homogeneous(true) + // .hexpand(true) + .halign(gtk::Align::Center) + .spacing(8) + .build(); + button_box.append(&cancel_button); + button_box.append(&self.open_button); + + grid.attach(&self.error_label, 0, 0, 4, 1); + + grid.attach(&hexpander(), 0, 1, 1, 2); + grid.attach(&hexpander(), 3, 1, 1, 2); + + grid.attach(&label, 1, 1, 1, 1); + grid.attach(&self.entry, 2, 1, 1, 1); + + grid.attach(&button_box, 1, 2, 2, 1); + + grid.attach(&vexpander(), 0, 3, 3, 1); + } + + fn dispose(&self) { + for child in self.obj().children() { + child.unparent(); + } + } + } + + impl WidgetImpl for OpenFile {} + + impl OpenFile { + pub fn reset(&self) { + self.entry.set_text(""); + self.error_label.set_label(""); + self.open_button.set_sensitive(false); + } + + pub fn set_error(&self, error: &dyn Error) { + // self.error_label.set_visible(true); + self.error_label.set_label(&format!("{}", error)); + } + + pub async fn next_response(&self) -> Option { + let response = self.receiver.borrow_mut().as_mut()?.next().await?; + Some(response) + } + } +} + +glib::wrapper! { + pub struct OpenFile(ObjectSubclass) + @extends gtk::Widget; +} + +impl Default for OpenFile { + fn default() -> Self { + glib::Object::builder().build() + } +} + +impl OpenFile { + pub async fn run(&self, read_file_callback: R) -> Option<(T, String)> + where + T: 'static, + R: Fn(&str) -> Result + 'static, + { + self.show(); + self.imp().reset(); + self.imp().entry.grab_focus(); + + loop { + let button = self.imp().next_response().await?; + + if button != gtk::ResponseType::Accept { + self.imp().reset(); + return None; + } + + let password = self.imp().entry.text(); + match read_file_callback(&password) { + Ok(document) => { + self.imp().reset(); + return Some((document, password.into())); + } + Err(e) => self.imp().set_error(&*e), + } + } + } +} + +struct ResponseSender(RefCell>); + +impl ResponseSender { + fn new(sender: &Sender) -> Self { + Self(RefCell::new(sender.clone())) + } + + fn send(&self, response: gtk::ResponseType) { + if let Err(error) = self.0.borrow_mut().try_send(response) { + eprintln!("{}", error); + } + } +} diff --git a/src/utils/ui.rs b/src/utils/ui.rs index e20cbe63..98e1f2ae 100644 --- a/src/utils/ui.rs +++ b/src/utils/ui.rs @@ -152,3 +152,19 @@ pub fn title_and_subtitle(title: &str, subtitle: &str) -> gtk::Widget { vbox.append(&title_label(subtitle, "subtitle")); vbox.upcast() } + +pub fn hexpander() -> gtk::Widget { + gtk::Label::builder() + .hexpand(true) + .vexpand(false) + .build() + .upcast() +} + +pub fn vexpander() -> gtk::Widget { + gtk::Label::builder() + .hexpand(false) + .vexpand(true) + .build() + .upcast() +}