diff --git a/src/main_window.rs b/src/main_window.rs index e787b5c..d11f52d 100644 --- a/src/main_window.rs +++ b/src/main_window.rs @@ -1,7 +1,7 @@ use crate::cache::Cache; use crate::config::ConfigService; use crate::format; -use crate::model::record::{RecordType, RECORD_TYPE_GENERIC}; +use crate::model::record::{Record, RecordType, RECORD_TYPE_GENERIC}; use crate::model::tree::RecordNode; use crate::model::tree::RecordTree; use crate::ui; @@ -10,7 +10,7 @@ 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::say::say_error; -use crate::ui::edit_record::edit_record; +use crate::ui::edit_record::dialog::edit_record; use crate::ui::forms::entry::form_password_entry; use crate::ui::open_file::OpenFile; use crate::ui::search::SearchEvent; @@ -526,14 +526,7 @@ impl PSMainWindow { let record_type = RecordType::find(&record_type_name).unwrap_or(&*RECORD_TYPE_GENERIC); let empty_record = record_type.new_record(); - let Some(new_record) = edit_record( - &empty_record, - self.upcast_ref(), - "Add record", - self.get_usernames(), - ) - .await - else { + let Some(new_record) = self.edit_record("Add record", &empty_record).await else { return; }; @@ -548,15 +541,15 @@ impl PSMainWindow { } impl PSMainWindow { + async fn edit_record(&self, title: &str, record: &Record) -> Option { + let result = edit_record(record, self.upcast_ref(), title, self.get_usernames()).await; + self.imp().file_pane.grab_focus_to_view(); + pending().await; + result + } + async fn action_edit(&self, position: u32, record_node: RecordNode) { - let Some(new_record) = edit_record( - record_node.record(), - self.upcast_ref(), - "Edit record", - self.get_usernames(), - ) - .await - else { + let Some(new_record) = self.edit_record("Edit record", record_node.record()).await else { return; }; diff --git a/src/ui/edit_record.rs b/src/ui/edit_record/dialog.rs similarity index 97% rename from src/ui/edit_record.rs rename to src/ui/edit_record/dialog.rs index cb928f9..3f5d2da 100644 --- a/src/ui/edit_record.rs +++ b/src/ui/edit_record/dialog.rs @@ -1,12 +1,12 @@ -use super::edit_object::edit_object; -use super::forms::base::*; -use super::forms::entry::*; -use super::forms::form::*; -use super::forms::multiline::*; -use super::password_editor::PasswordEditor; use crate::model::record::FIELD_NAME; use crate::model::record::RECORD_TYPES; use crate::model::record::{FieldType, Record, RecordType}; +use crate::ui::edit_object::edit_object; +use crate::ui::forms::base::*; +use crate::ui::forms::entry::*; +use crate::ui::forms::form::*; +use crate::ui::forms::multiline::*; +use crate::ui::password_editor::PasswordEditor; use crate::ui::record_type_popover::RecordTypePopoverBuilder; use gtk::{glib, prelude::*}; use std::cell::RefCell; diff --git a/src/ui/edit_record/mod.rs b/src/ui/edit_record/mod.rs new file mode 100644 index 0000000..f1f497f --- /dev/null +++ b/src/ui/edit_record/mod.rs @@ -0,0 +1,3 @@ +pub mod dialog; +mod record_form; +mod record_widget; diff --git a/src/ui/edit_record/record_form.rs b/src/ui/edit_record/record_form.rs new file mode 100644 index 0000000..fbad3e2 --- /dev/null +++ b/src/ui/edit_record/record_form.rs @@ -0,0 +1,160 @@ +use crate::model::record::{Field, FieldType, Record, RecordType}; +use gtk::{glib, prelude::*, subclass::prelude::*}; + +mod imp { + use super::*; + use crate::ui::forms::base::*; + use crate::ui::forms::entry::*; + use crate::ui::forms::multiline::*; + use crate::ui::password_editor::PasswordEditor; + use once_cell::sync::{Lazy, OnceCell}; + + struct FormEntry { + field: &'static Field, + widget: Box>, + } + + pub struct RecordForm { + grid: gtk::Grid, + pub record_type: OnceCell<&'static RecordType>, + entries: OnceCell>, + } + + #[glib::object_subclass] + impl ObjectSubclass for RecordForm { + const NAME: &'static str = "PSRecordForm"; + type Type = super::RecordForm; + type ParentType = gtk::Widget; + + fn new() -> Self { + Self { + grid: gtk::Grid::builder() + .column_spacing(10) + .row_spacing(10) + .build(), + record_type: Default::default(), + entries: Default::default(), + } + } + } + + impl ObjectImpl for RecordForm { + fn constructed(&self) { + self.parent_constructed(); + + let obj = self.obj(); + obj.set_layout_manager(Some(gtk::BinLayout::new())); + + self.grid.set_parent(&*obj); + } + + fn signals() -> &'static [glib::subclass::Signal] { + static SIGNALS: Lazy> = + Lazy::new(|| vec![glib::subclass::Signal::builder("record-changed").build()]); + &SIGNALS + } + + fn dispose(&self) { + while let Some(child) = self.obj().first_child() { + child.unparent(); + } + } + } + + impl WidgetImpl for RecordForm {} + + impl RecordForm { + pub fn init(&self, record_type: &'static RecordType, names: &[String]) { + self.record_type.set(record_type).unwrap(); + + let mut entries: Vec = Vec::new(); + for (index, field) in record_type.fields.iter().enumerate() { + let mut widget: Box> = match field.field_type { + FieldType::Text => Box::new(form_entry()), + FieldType::MultiLine => Box::new(MultiLine::new()), + FieldType::Name => Box::new(form_entry_with_completion(names)), + FieldType::Password => Box::new(PasswordEditor::new()), + FieldType::Secret => Box::new(form_password_entry()), + }; + + let label_widget = gtk::Label::builder() + .label(field.title) + .xalign(0_f32) + .yalign(0.5_f32) + .build(); + self.grid.attach(&label_widget, 0, index as i32, 1, 1); + + widget.connect_changed(Box::new(glib::clone!(@weak self as this => move |_| { + this.obj().emit_by_name::<()>("record-changed", &[]); + }))); + self.grid + .attach(&widget.get_widget(), 1, index as i32, 1, 1); + + entries.push(FormEntry { field, widget }); + } + self.entries.set(entries).ok().unwrap(); + } + + pub fn record(&self) -> Record { + let mut record = self.record_type.get().unwrap().new_record(); + for &FormEntry { field, ref widget } in self.entries.get().unwrap().iter() { + if let Some(value) = widget.get_value() { + record.set_field(field, &value); + } + } + record + } + + pub fn set_record(&self, record: &Record) { + for &FormEntry { field, ref widget } in self.entries.get().unwrap().iter() { + widget.set_value(Some(&record.get_field(field).to_string())); + } + } + + pub fn grab_focus_to_editor(&self) { + if let Some(first) = self.entries.get().unwrap().first() { + first.widget.get_widget().grab_focus(); + } + } + } +} + +glib::wrapper! { + pub struct RecordForm(ObjectSubclass) + @extends gtk::Widget; +} + +impl RecordForm { + pub fn new(record_type: &'static RecordType, names: &[String]) -> Self { + let obj: Self = glib::Object::builder().build(); + obj.imp().init(record_type, names); + obj + } + + pub fn record_type(&self) -> &'static RecordType { + self.imp().record_type.get().unwrap() + } + + pub fn record(&self) -> Record { + self.imp().record() + } + + pub fn set_record(&self, record: &Record) { + self.imp().set_record(record); + } + + pub fn grab_focus_to_editor(&self) { + self.imp().grab_focus_to_editor(); + } + + pub fn connect_record_changed(&self, f: F) -> glib::signal::SignalHandlerId + where + F: Fn(&Self) + 'static, + { + self.connect_closure( + "record-changed", + false, + glib::closure_local!(move |self_: &Self| (f)(self_)), + ) + } +} diff --git a/src/ui/edit_record/record_widget.rs b/src/ui/edit_record/record_widget.rs new file mode 100644 index 0000000..962dfea --- /dev/null +++ b/src/ui/edit_record/record_widget.rs @@ -0,0 +1,226 @@ +use super::record_form::RecordForm; +use crate::model::record::{Record, RecordType, FIELD_NAME, RECORD_TYPES}; +use crate::ui::record_type_popover::RecordTypePopoverBuilder; +use gtk::{glib, prelude::*, subclass::prelude::*}; +use std::cell::RefCell; + +mod imp { + use super::*; + use once_cell::sync::Lazy; + + pub struct RecordWidget { + grid: gtk::Grid, + icon: gtk::Image, + type_label: gtk::Label, + convert_button: gtk::MenuButton, + open_button: gtk::Button, + form: RefCell>, + pub names: RefCell>, + } + + #[glib::object_subclass] + impl ObjectSubclass for RecordWidget { + const NAME: &'static str = "PSRecordWidget"; + type Type = super::RecordWidget; + type ParentType = gtk::Widget; + + fn new() -> Self { + Self { + grid: gtk::Grid::builder() + .column_spacing(10) + .row_spacing(10) + .row_homogeneous(false) + .column_homogeneous(false) + .build(), + icon: gtk::Image::builder() + .icon_size(gtk::IconSize::Large) + .margin_start(16) + .margin_end(16) + .margin_top(16) + .margin_bottom(8) + .halign(gtk::Align::Center) + .vexpand(false) + .build(), + type_label: gtk::Label::builder().vexpand(false).xalign(0.5_f32).build(), + convert_button: gtk::MenuButton::builder() + .label("Convert to...") + .visible(false) + .build(), + open_button: gtk::Button::builder().label("Open").visible(false).build(), + form: Default::default(), + names: Default::default(), + } + } + } + + impl ObjectImpl for RecordWidget { + fn constructed(&self) { + self.parent_constructed(); + + let obj = self.obj(); + obj.set_layout_manager(Some(gtk::BinLayout::new())); + obj.set_hexpand(true); + obj.set_vexpand(true); + + self.grid.set_parent(&*obj); + + self.grid.attach(&self.icon, 0, 0, 1, 1); + self.grid.attach(&self.type_label, 0, 1, 1, 1); + self.grid.attach(&self.convert_button, 0, 2, 1, 1); + self.grid.attach(&self.open_button, 0, 3, 1, 1); + + let expander = gtk::Label::builder().vexpand(true).build(); + self.grid.attach(&expander, 0, 4, 1, 1); + + let separator = gtk::Separator::builder() + .orientation(gtk::Orientation::Vertical) + .build(); + self.grid.attach(&separator, 1, 0, 1, 5); + + self.open_button + .connect_clicked(glib::clone!(@weak self as imp => move |_| imp.open())); + } + + fn signals() -> &'static [glib::subclass::Signal] { + static SIGNALS: Lazy> = + Lazy::new(|| vec![glib::subclass::Signal::builder("record-changed").build()]); + &SIGNALS + } + + fn dispose(&self) { + while let Some(child) = self.obj().first_child() { + child.unparent(); + } + } + } + + impl WidgetImpl for RecordWidget {} + + impl RecordWidget { + fn record_type(&self) -> Option<&'static RecordType> { + self.form.borrow().as_ref().map(|form| form.record_type()) + } + + fn set_record_type(&self, record_type: &'static RecordType) { + if self.record_type() == Some(record_type) { + return; + } + + self.icon.set_icon_name(Some(record_type.icon)); + self.type_label.set_label(record_type.title); + if !record_type.is_group { + let convert_to_types = RECORD_TYPES + .iter() + .filter(|rt| !rt.is_group && **rt != record_type) + .cloned() + .collect::>(); + + let popover = RecordTypePopoverBuilder::default() + .record_types(&convert_to_types) + .on_activate(glib::clone!(@weak self as this => move |dest_record_type| { + this.convert_to(dest_record_type); + })) + .build(); + + self.convert_button.set_popover(Some(&popover)); + self.convert_button.show(); + } else { + self.convert_button.hide(); + } + + if let Some(old_form) = self.grid.child_at(2, 0) { + self.grid.remove(&old_form); + } + let form = RecordForm::new(record_type, &self.names.borrow()); + form.connect_record_changed(glib::clone!(@weak self as this => move |_| { + this.obj().emit_by_name::<()>("record-changed", &[]); + })); + *self.form.borrow_mut() = Some(form.clone()); + + form.set_hexpand(true); + form.set_vexpand(true); + self.grid.attach(&form, 2, 0, 1, 5); + + self.grid.set_focus_child(Some(&form)); + } + + fn raw_record(&self) -> Option { + self.form.borrow().as_ref().map(|f| f.record()) + } + + pub fn record(&self) -> Option { + self.raw_record().filter(|r| !r.name().is_empty()) + } + + pub fn set_record(&self, record: &Record) { + self.set_record_type(record.record_type); + + let has_url = record.url().is_some(); + self.open_button.set_visible(has_url); + + self.form.borrow().as_ref().unwrap().set_record(record); + } + + fn open(&self) { + let Some(record) = self.raw_record() else { + return; + }; + let Some(url) = record.url() else { return }; + let window = self.obj().root().and_downcast::(); + gtk::show_uri(window.as_ref(), url, 0); + } + + fn convert_to(&self, dest_record_type: &'static RecordType) { + let mut new_record = dest_record_type.new_record(); + if let Some(record) = self.raw_record() { + let name = record.get_field(&FIELD_NAME); + new_record.set_field(&FIELD_NAME, name); + new_record.join(&record); + } + self.set_record(&new_record); + } + + pub fn grab_focus_to_editor(&self) { + self.form + .borrow() + .as_ref() + .map(|w| w.grab_focus_to_editor()); + } + } +} + +glib::wrapper! { + pub struct RecordWidget(ObjectSubclass) + @extends gtk::Widget; +} + +impl RecordWidget { + pub fn new(names: Vec) -> Self { + let obj: Self = glib::Object::builder().build(); + *obj.imp().names.borrow_mut() = names; + obj + } + + pub fn record(&self) -> Option { + self.imp().record() + } + + pub fn set_record(&self, record: &Record) { + self.imp().set_record(record); + } + + pub fn grab_focus_to_editor(&self) { + self.imp().grab_focus_to_editor(); + } + + pub fn connect_record_changed(&self, f: F) -> glib::signal::SignalHandlerId + where + F: Fn(&Self) + 'static, + { + self.connect_closure( + "record-changed", + false, + glib::closure_local!(move |self_: &Self| (f)(self_)), + ) + } +} diff --git a/src/ui/file_pane.rs b/src/ui/file_pane.rs index 574c2f4..2041c9d 100644 --- a/src/ui/file_pane.rs +++ b/src/ui/file_pane.rs @@ -270,6 +270,10 @@ impl Default for FilePane { } impl FilePane { + pub fn grab_focus_to_view(&self) { + self.imp().view.grab_focus(); + } + pub fn file(&self) -> Ref<'_, RecordTree> { self.imp().file.borrow() } @@ -281,7 +285,7 @@ impl FilePane { self.imp().view.select_position_async(0).await; self.selection_changed(gtk::Bitset::new_empty()); - self.imp().view.grab_focus(); + self.grab_focus_to_view(); } fn get_record(&self, position: u32) -> Option { diff --git a/src/ui/record_view/view.rs b/src/ui/record_view/view.rs index 9c388f7..2d43b4d 100644 --- a/src/ui/record_view/view.rs +++ b/src/ui/record_view/view.rs @@ -44,6 +44,7 @@ mod imp { self.parent_constructed(); let obj = self.obj(); + obj.set_focusable(true); obj.set_layout_manager(Some(gtk::BinLayout::new())); self.list_view