From 65179d4816a39b0c92e9c6a981b491c60313634f Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Thu, 22 Jun 2023 13:23:27 +0300 Subject: [PATCH] composer: fix cursor/widget focus scrolling logic Scrolling up/down with scroll_{up,down} shortcuts didn't work correctly, because the form widget used its own shortcuts. This commit refactors the cursor logic. --- docs/meli.1 | 6 +- docs/meli.7 | 6 +- docs/meli.conf.5 | 4 +- docs/samples/sample-config.toml | 2 +- src/components/contacts.rs | 11 +- src/components/contacts/contact_list.rs | 2 +- src/components/mail/compose.rs | 165 +++++++++++++----- .../mail/compose/edit_attachments.rs | 58 ++++-- src/components/mail/listing.rs | 2 +- src/components/mail/view.rs | 157 ++++++++++------- src/components/mail/view/state.rs | 2 +- src/components/utilities/pager.rs | 2 +- src/components/utilities/widgets.rs | 61 ++++++- src/conf/shortcuts.rs | 2 +- src/state.rs | 26 ++- src/types.rs | 5 +- 16 files changed, 365 insertions(+), 146 deletions(-) diff --git a/docs/meli.1 b/docs/meli.1 index d497b89f7..c54d8dc1a 100644 --- a/docs/meli.1 +++ b/docs/meli.1 @@ -292,7 +292,7 @@ mode and key to exit. .It At any time you may press -.Shortcut e composing edit_mail Ns +.Shortcut e composing edit Ns to launch your editor (see .Xr meli.conf 5 COMPOSING Ns , setting @@ -320,7 +320,7 @@ To stop your editor and return to press .Aq Ctrl-z and to resume editing press the -.Ic edit_mail +.Ic edit command again. .El .Ss Attachments @@ -353,7 +353,7 @@ To save your draft without sending it, issue and select 'save as draft'. .sp To open a draft for further editing, select your draft in the mail listing and press -.Ic edit_mail Ns +.Ic edit Ns \&. .Sh CONTACTS .Nm diff --git a/docs/meli.7 b/docs/meli.7 index f41071703..75fbb29f0 100644 --- a/docs/meli.7 +++ b/docs/meli.7 @@ -605,7 +605,7 @@ Reply to all. .El .sp To launch your editor, press -.ShortcutPeriod e composing edit_mail +.ShortcutPeriod e composing edit \&. To send your draft, press .ShortcutPeriod s composing send_mail @@ -619,7 +619,7 @@ and select You can return to the draft by going to your .Qq Drafts mailbox and selecting -.ShortcutPeriod e envelope_view edit_mail +.ShortcutPeriod e envelope_view edit \&. .Bd -literal -offset center ┌────────────────────────────────────────────────────────────┐ @@ -648,7 +648,7 @@ mailbox and selecting .Ed .sp If you enable the embed terminal option, you can launch your terminal editor of choice when you press -.Ic edit_mail Ns +.Ic edit Ns \&. .Bd -literal -offset center ┌────────────────────────────────────────────────────────────┐ diff --git a/docs/meli.conf.5 b/docs/meli.conf.5 index daf6d4552..9a480dac5 100644 --- a/docs/meli.conf.5 +++ b/docs/meli.conf.5 @@ -114,7 +114,7 @@ editor_command = 'vim +/^$' [shortcuts] [shortcuts.composing] -edit_mail = 'e' +edit = 'e' [shortcuts.listing] new_mail = 'm' @@ -964,7 +964,7 @@ Toggle visibility of side menu in mail list. .sp .Em composing .Bl -tag -width 36n -.It Ic edit_mail +.It Ic edit Edit mail. .\" default value .Pq Em e diff --git a/docs/samples/sample-config.toml b/docs/samples/sample-config.toml index 2fce29617..a8b414160 100644 --- a/docs/samples/sample-config.toml +++ b/docs/samples/sample-config.toml @@ -101,7 +101,7 @@ # ###shortcuts #[shortcuts.composing] -#edit_mail = 'e' +#edit = 'e' # #[shortcuts.contact-list] #create_contact = 'c' diff --git a/src/components/contacts.rs b/src/components/contacts.rs index e992d72ae..f127dd9c3 100644 --- a/src/components/contacts.rs +++ b/src/components/contacts.rs @@ -77,7 +77,7 @@ impl ContactManager { } } - fn initialize(&mut self) { + fn initialize(&mut self, context: &Context) { let (width, _) = self.content.size(); let (x, _) = write_string_to_grid( @@ -113,7 +113,12 @@ impl ContactManager { ); } - self.form = FormWidget::new(("Save".into(), true)); + self.form = FormWidget::new( + ("Save".into(), true), + /* cursor_up_shortcut */ context.settings.shortcuts.general.scroll_up.clone(), + /* cursor_down_shortcut */ + context.settings.shortcuts.general.scroll_down.clone(), + ); self.form.add_button(("Cancel(Esc)".into(), false)); self.form .push(("NAME".into(), self.card.name().to_string())); @@ -142,7 +147,7 @@ impl ContactManager { impl Component for ContactManager { fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { if !self.initialized { - self.initialize(); + self.initialize(context); self.initialized = true; } diff --git a/src/components/contacts/contact_list.rs b/src/components/contacts/contact_list.rs index ffecf4507..8491a7b61 100644 --- a/src/components/contacts/contact_list.rs +++ b/src/components/contacts/contact_list.rs @@ -686,7 +686,7 @@ impl Component for ContactList { *draft.headers_mut().get_mut("To").unwrap() = format!("{} <{}>", &card.name(), &card.email()); let mut composer = Composer::with_account(account_hash, context); - composer.set_draft(draft); + composer.set_draft(draft, context); context .replies .push_back(UIEvent::Action(Tab(New(Some(Box::new(composer)))))); diff --git a/src/components/mail/compose.rs b/src/components/mail/compose.rs index 4b17636a7..cc37a6dc3 100644 --- a/src/components/mail/compose.rs +++ b/src/components/mail/compose.rs @@ -535,13 +535,13 @@ To: {} draft.attachments.push(preamble); draft.attachments.push(env.body_bytes(bytes).into()); } - composer.set_draft(draft); + composer.set_draft(draft, context); composer } - pub fn set_draft(&mut self, draft: Draft) { + pub fn set_draft(&mut self, draft: Draft, context: &Context) { self.draft = draft; - self.update_form(); + self.update_form(context); } fn update_draft(&mut self) { @@ -554,9 +554,22 @@ To: {} } } - fn update_form(&mut self) { + fn update_form(&mut self, context: &Context) { let old_cursor = self.form.cursor(); - self.form = FormWidget::new(("Save".into(), true)); + let shortcuts = self.shortcuts(context); + self.form = FormWidget::new( + ("Save".into(), true), + /* cursor_up_shortcut */ + shortcuts + .get(Shortcuts::COMPOSING) + .and_then(|c| c.get("scroll_up").cloned()) + .unwrap_or_else(|| context.settings.shortcuts.composing.scroll_up.clone()), + /* cursor_down_shortcut */ + shortcuts + .get(Shortcuts::COMPOSING) + .and_then(|c| c.get("scroll_down").cloned()) + .unwrap_or_else(|| context.settings.shortcuts.composing.scroll_down.clone()), + ); self.form.hide_buttons(); self.form.set_cursor(old_cursor); let headers = self.draft.headers(); @@ -814,8 +827,6 @@ impl Component for Composer { return; } - let width = width!(area); - if !self.initialized { #[cfg(feature = "gpgme")] if self.gpg_state.sign_mail.is_unset() { @@ -835,12 +846,14 @@ impl Component for Composer { ); } self.pager.update_from_str(self.draft.body(), Some(77)); - self.update_form(); + self.update_form(context); self.initialized = true; } let header_height = self.form.len(); let theme_default = crate::conf::value(context, "theme_default"); + let mid = 0; + /* let mid = if width > 80 { let width = width - 80; let mid = width / 2; @@ -861,6 +874,7 @@ impl Component for Composer { } else { 0 }; + */ let header_area = ( set_x(upper_left, mid + 1), @@ -971,8 +985,8 @@ impl Component for Composer { let stopped_message: String = format!("Process with PID {} has stopped.", guard.child_pid); let stopped_message_2: String = format!( - "-press '{}' (edit_mail shortcut) to re-activate.", - shortcuts[Shortcuts::COMPOSING]["edit_mail"] + "-press '{}' (edit shortcut) to re-activate.", + shortcuts[Shortcuts::COMPOSING]["edit"] ); const STOPPED_MESSAGE_3: &str = "-press Ctrl-C to forcefully kill it and return to editor."; @@ -1024,13 +1038,14 @@ impl Component for Composer { self.embed_area = (upper_left!(header_area), bottom_right!(body_area)); } - if !self.mode.is_edit_attachments() { - self.pager.set_dirty(true); - if self.pager.size().0 > width!(body_area) { - self.pager.set_initialised(false); - } - self.pager.draw(grid, body_area, context); + if self.pager.size().0 > width!(body_area) { + self.pager.set_initialised(false); } + // Force clean pager area, because if body height is less than body_area it will + // might leave draw artifacts in the remaining area. + clear_area(grid, body_area, theme_default); + self.set_dirty(true); + self.pager.draw(grid, body_area, context); match self.cursor { Cursor::Headers => { @@ -1123,6 +1138,58 @@ impl Component for Composer { self.pager.process_event(event, context); } let shortcuts = self.shortcuts(context); + // Process scrolling first, since in my infinite wisdom I made this so + // unnecessarily complex + match &event { + UIEvent::Input(ref key) + if self.mode.is_edit() + && shortcut!(key == shortcuts[Shortcuts::COMPOSING]["scroll_up"]) => + { + self.set_dirty(true); + self.cursor = match self.cursor { + // match order is evaluation order, so it matters here because of the if guard + // process_event side effects + Cursor::Attachments => Cursor::Encrypt, + Cursor::Encrypt => Cursor::Sign, + Cursor::Sign => Cursor::Body, + Cursor::Body if !self.pager.process_event(event, context) => { + self.form.process_event(event, context); + Cursor::Headers + } + Cursor::Body => Cursor::Body, + Cursor::Headers if self.form.process_event(event, context) => Cursor::Headers, + Cursor::Headers => Cursor::Headers, + }; + return true; + } + UIEvent::Input(ref key) + if self.mode.is_edit() + && shortcut!(key == shortcuts[Shortcuts::COMPOSING]["scroll_down"]) => + { + self.set_dirty(true); + self.cursor = match self.cursor { + Cursor::Headers if self.form.process_event(event, context) => Cursor::Headers, + Cursor::Headers => Cursor::Body, + Cursor::Body if self.pager.process_event(event, context) => Cursor::Body, + Cursor::Body => Cursor::Sign, + Cursor::Sign => Cursor::Encrypt, + Cursor::Encrypt => Cursor::Attachments, + Cursor::Attachments => Cursor::Attachments, + }; + return true; + } + _ => {} + } + if self.cursor == Cursor::Headers + && self.mode.is_edit() + && self.form.process_event(event, context) + { + if let UIEvent::InsertInput(_) = event { + self.has_changes = true; + } + self.set_dirty(true); + return true; + } match (&mut self.mode, &mut event) { (ViewMode::Edit, _) => { if self.pager.process_event(event, context) { @@ -1136,10 +1203,13 @@ impl Component for Composer { }) .process_event(event, context) { - if widget.buttons.result() == Some(FormButtonActions::Cancel) { + if matches!( + widget.buttons.result(), + Some(FormButtonActions::Cancel | FormButtonActions::Accept) + ) { self.mode = ViewMode::Edit; - self.set_dirty(true); } + self.set_dirty(true); return true; } } @@ -1237,6 +1307,7 @@ impl Component for Composer { } (ViewMode::Send(ref mut selector), _) => { if selector.process_event(event, context) { + self.set_dirty(true); return true; } } @@ -1247,13 +1318,15 @@ impl Component for Composer { if let Some(to_val) = result.downcast_mut::() { self.draft .set_header(HeaderName::TO, std::mem::take(to_val)); - self.update_form(); + self.update_form(context); } self.mode = ViewMode::Edit; + self.set_dirty(true); return true; } (ViewMode::SelectRecipients(ref mut selector), _) => { if selector.process_event(event, context) { + self.set_dirty(true); return true; } } @@ -1281,12 +1354,13 @@ impl Component for Composer { _ => {} } } - self.set_dirty(true); self.mode = ViewMode::Edit; + self.set_dirty(true); return true; } (ViewMode::Discard(_, ref mut selector), _) => { if selector.process_event(event, context) { + self.set_dirty(true); return true; } } @@ -1300,6 +1374,7 @@ impl Component for Composer { context .replies .push_back(UIEvent::Action(Tab(Kill(self.id)))); + self.set_dirty(true); return true; } 'n' => { @@ -1347,6 +1422,7 @@ impl Component for Composer { } (ViewMode::WaitingForSendResult(ref mut selector, _), _) => { if selector.process_event(event, context) { + self.set_dirty(true); return true; } } @@ -1373,20 +1449,12 @@ impl Component for Composer { #[cfg(feature = "gpgme")] (ViewMode::SelectEncryptKey(_, ref mut selector), _) => { if selector.process_event(event, context) { + self.set_dirty(true); return true; } } _ => {} } - if self.cursor == Cursor::Headers - && self.mode.is_edit() - && self.form.process_event(event, context) - { - if let UIEvent::InsertInput(_) = event { - self.has_changes = true; - } - return true; - } match *event { UIEvent::ConfigReload { old_settings: _ } => { @@ -1399,6 +1467,7 @@ impl Component for Composer { if self.mode.is_edit() && shortcut!(key == shortcuts[Shortcuts::COMPOSING]["scroll_up"]) => { + self.set_dirty(true); self.cursor = match self.cursor { Cursor::Headers => return true, Cursor::Body => { @@ -1409,12 +1478,12 @@ impl Component for Composer { Cursor::Encrypt => Cursor::Sign, Cursor::Attachments => Cursor::Encrypt, }; - self.dirty = true; } UIEvent::Input(ref key) if self.mode.is_edit() && shortcut!(key == shortcuts[Shortcuts::COMPOSING]["scroll_down"]) => { + self.set_dirty(true); self.cursor = match self.cursor { Cursor::Headers => Cursor::Body, Cursor::Body => Cursor::Sign, @@ -1422,7 +1491,6 @@ impl Component for Composer { Cursor::Encrypt => Cursor::Attachments, Cursor::Attachments => return true, }; - self.dirty = true; } UIEvent::Input(Key::Char('\n')) if self.mode.is_edit() @@ -1440,7 +1508,7 @@ impl Component for Composer { } _ => {} }; - self.dirty = true; + self.set_dirty(true); } UIEvent::Input(ref key) if shortcut!(key == shortcuts[Shortcuts::COMPOSING]["send_mail"]) @@ -1565,6 +1633,8 @@ impl Component for Composer { context .replies .push_back(UIEvent::EmbedInput((k.clone(), b.to_vec()))); + drop(embed_guard); + self.set_dirty(true); return true; } Ok(WaitStatus::Signaled(_, signal, _)) => { @@ -1608,7 +1678,7 @@ impl Component for Composer { UIEvent::Input(ref key) if self.mode.is_edit() && self.cursor == Cursor::Sign - && shortcut!(key == shortcuts[Shortcuts::COMPOSING]["edit_mail"]) => + && shortcut!(key == shortcuts[Shortcuts::COMPOSING]["edit"]) => { #[cfg(feature = "gpgme")] match melib::email::parser::address::rfc2822address_list( @@ -1648,7 +1718,7 @@ impl Component for Composer { UIEvent::Input(ref key) if self.mode.is_edit() && self.cursor == Cursor::Encrypt - && shortcut!(key == shortcuts[Shortcuts::COMPOSING]["edit_mail"]) => + && shortcut!(key == shortcuts[Shortcuts::COMPOSING]["edit"]) => { #[cfg(feature = "gpgme")] match melib::email::parser::address::rfc2822address_list( @@ -1688,10 +1758,10 @@ impl Component for Composer { UIEvent::Input(ref key) if self.mode.is_edit() && self.cursor == Cursor::Attachments - && shortcut!(key == shortcuts[Shortcuts::COMPOSING]["edit_mail"]) => + && shortcut!(key == shortcuts[Shortcuts::COMPOSING]["edit"]) => { self.mode = ViewMode::EditAttachments { - widget: EditAttachments::new(), + widget: EditAttachments::new(Some(self.account_hash)), }; self.set_dirty(true); @@ -1699,7 +1769,7 @@ impl Component for Composer { } UIEvent::Input(ref key) if self.embed.is_some() - && shortcut!(key == shortcuts[Shortcuts::COMPOSING]["edit_mail"]) => + && shortcut!(key == shortcuts[Shortcuts::COMPOSING]["edit"]) => { self.embed.as_ref().unwrap().lock().unwrap().wake_up(); match self.embed.take() { @@ -1743,7 +1813,7 @@ impl Component for Composer { } UIEvent::Input(ref key) if self.mode.is_edit() - && shortcut!(key == shortcuts[Shortcuts::COMPOSING]["edit_mail"]) => + && shortcut!(key == shortcuts[Shortcuts::COMPOSING]["edit"]) => { /* Edit draft in $EDITOR */ let editor = if let Some(editor_command) = @@ -1760,6 +1830,7 @@ impl Component for Composer { .to_string(), Some(NotificationType::Error(melib::error::ErrorKind::None)), )); + self.set_dirty(true); return true; } Ok(v) => v, @@ -1805,6 +1876,7 @@ impl Component for Composer { )); } } + self.set_dirty(true); return true; } /* Kill input thread so that spawned command can be sole receiver of stdin */ @@ -1834,6 +1906,7 @@ impl Component for Composer { )); context.replies.push_back(UIEvent::Fork(ForkType::Finished)); context.restore_input(); + self.set_dirty(true); return true; } } @@ -1913,6 +1986,7 @@ impl Component for Composer { format!("could not execute pipe command {}: {}", command, &err), Some(NotificationType::Error(melib::error::ErrorKind::External)), )); + self.set_dirty(true); return true; } } @@ -1945,9 +2019,12 @@ impl Component for Composer { } else { context.replies.push_back(UIEvent::Notification( None, - "You haven't defined any command to launch.".into(), + "You haven't defined any command to launch in \ + [terminal.file_picker_command]." + .into(), Some(NotificationType::Error(melib::error::ErrorKind::None)), )); + self.set_dirty(true); return true; }; /* Kill input thread so that spawned command can be sole receiver of stdin */ @@ -1997,6 +2074,7 @@ impl Component for Composer { Some(NotificationType::Error(melib::error::ErrorKind::External)), )); context.restore_input(); + self.set_dirty(true); return true; } } @@ -2031,6 +2109,7 @@ impl Component for Composer { Flag::SEEN | Flag::DRAFT, self.account_hash, ); + self.set_dirty(true); return true; } #[cfg(feature = "gpgme")] @@ -2057,7 +2136,13 @@ impl Component for Composer { fn is_dirty(&self) -> bool { match self.mode { ViewMode::Embed => true, - ViewMode::EditAttachments { ref widget } => widget.dirty || widget.buttons.is_dirty(), + ViewMode::EditAttachments { ref widget } => { + widget.dirty + || widget.buttons.is_dirty() + || self.dirty + || self.pager.is_dirty() + || self.form.is_dirty() + } ViewMode::Edit => self.dirty || self.pager.is_dirty() || self.form.is_dirty(), ViewMode::Discard(_, ref widget) => { widget.is_dirty() || self.pager.is_dirty() || self.form.is_dirty() diff --git a/src/components/mail/compose/edit_attachments.rs b/src/components/mail/compose/edit_attachments.rs index 98a89ac8a..6e5b00573 100644 --- a/src/components/mail/compose/edit_attachments.rs +++ b/src/components/mail/compose/edit_attachments.rs @@ -39,6 +39,8 @@ pub enum EditAttachmentMode { #[derive(Debug)] pub struct EditAttachments { + /// For shortcut setting retrieval. + pub account_hash: Option, pub mode: EditAttachmentMode, pub buttons: ButtonWidget, pub cursor: EditAttachmentCursor, @@ -47,12 +49,13 @@ pub struct EditAttachments { } impl EditAttachments { - pub fn new() -> Self { - let mut buttons = ButtonWidget::new(("Add".into(), FormButtonActions::Other("add"))); - buttons.push(("Go Back".into(), FormButtonActions::Cancel)); + pub fn new(account_hash: Option) -> Self { + //ButtonWidget::new(("Add".into(), FormButtonActions::Other("add"))); + let mut buttons = ButtonWidget::new(("Go Back".into(), FormButtonActions::Cancel)); buttons.set_focus(true); buttons.set_cursor(1); EditAttachments { + account_hash, mode: EditAttachmentMode::Overview, buttons, cursor: EditAttachmentCursor::Buttons, @@ -63,13 +66,31 @@ impl EditAttachments { } impl EditAttachmentsRefMut<'_, '_> { - fn new_edit_widget(&self, no: usize) -> Option>> { + fn new_edit_widget( + &self, + no: usize, + context: &Context, + ) -> Option>> { if no >= self.draft.attachments().len() { return None; } let filename = self.draft.attachments()[no].content_type().name(); let mime_type = self.draft.attachments()[no].content_type(); - let mut ret = FormWidget::new(("Save".into(), FormButtonActions::Accept)); + let shortcuts = self.shortcuts(context); + + let mut ret = FormWidget::new( + ("Save".into(), FormButtonActions::Accept), + /* cursor_up_shortcut */ + shortcuts + .get(Shortcuts::COMPOSING) + .and_then(|c| c.get("scroll_up").cloned()) + .unwrap_or_else(|| context.settings.shortcuts.composing.scroll_up.clone()), + /* cursor_down_shortcut */ + shortcuts + .get(Shortcuts::COMPOSING) + .and_then(|c| c.get("scroll_down").cloned()) + .unwrap_or_else(|| context.settings.shortcuts.composing.scroll_down.clone()), + ); ret.add_button(("Reset".into(), FormButtonActions::Reset)); ret.add_button(("Cancel".into(), FormButtonActions::Cancel)); @@ -188,7 +209,7 @@ impl Component for EditAttachmentsRefMut<'_, '_> { } Some(FormButtonActions::Reset) => { let no = *no; - if let Some(inner) = self.new_edit_widget(no) { + if let Some(inner) = self.new_edit_widget(no, context) { self.inner.mode = EditAttachmentMode::Edit { inner, no }; } } @@ -197,8 +218,12 @@ impl Component for EditAttachmentsRefMut<'_, '_> { return true; } } else { + let shortcuts = self.shortcuts(context); + match event { - UIEvent::Input(Key::Up) => { + UIEvent::Input(ref key) + if shortcut!(key == shortcuts[Shortcuts::COMPOSING]["scroll_up"]) => + { self.set_dirty(true); match self.inner.cursor { EditAttachmentCursor::AttachmentNo(ref mut n) => { @@ -224,7 +249,9 @@ impl Component for EditAttachmentsRefMut<'_, '_> { } return true; } - UIEvent::Input(Key::Down) => { + UIEvent::Input(ref key) + if shortcut!(key == shortcuts[Shortcuts::COMPOSING]["scroll_down"]) => + { self.set_dirty(true); match self.inner.cursor { EditAttachmentCursor::AttachmentNo(ref mut n) => { @@ -246,7 +273,7 @@ impl Component for EditAttachmentsRefMut<'_, '_> { UIEvent::Input(Key::Char('\n')) => { match self.inner.cursor { EditAttachmentCursor::AttachmentNo(ref no) => { - if let Some(inner) = self.new_edit_widget(*no) { + if let Some(inner) = self.new_edit_widget(*no, context) { self.inner.mode = EditAttachmentMode::Edit { inner, no: *no }; } self.set_dirty(true); @@ -293,8 +320,17 @@ impl Component for EditAttachmentsRefMut<'_, '_> { fn kill(&mut self, _uuid: ComponentId, _context: &mut Context) {} - fn shortcuts(&self, _context: &Context) -> ShortcutMaps { - ShortcutMaps::default() + fn shortcuts(&self, context: &Context) -> ShortcutMaps { + let mut map = ShortcutMaps::default(); + + let our_map: ShortcutMap = self + .inner + .account_hash + .map(|acc| account_settings!(context[acc].shortcuts.composing).key_values()) + .unwrap_or_else(|| context.settings.shortcuts.composing.key_values()); + map.insert(Shortcuts::COMPOSING, our_map); + + map } fn id(&self) -> ComponentId { diff --git a/src/components/mail/listing.rs b/src/components/mail/listing.rs index 82d6267fa..ed7dd3a94 100644 --- a/src/components/mail/listing.rs +++ b/src/components/mail/listing.rs @@ -2030,7 +2030,7 @@ impl Component for Listing { UIEvent::Action(Action::Compose(ComposeAction::Mailto(ref mailto))) => { let account_hash = context.accounts[self.cursor_pos.0].hash(); let mut composer = Composer::with_account(account_hash, context); - composer.set_draft(mailto.into()); + composer.set_draft(mailto.into(), context); context .replies .push_back(UIEvent::Action(Tab(New(Some(Box::new(composer)))))); diff --git a/src/components/mail/view.rs b/src/components/mail/view.rs index 83db95e04..13e2caee7 100644 --- a/src/components/mail/view.rs +++ b/src/components/mail/view.rs @@ -58,6 +58,7 @@ pub struct MailView { coordinates: Option<(AccountHash, MailboxHash, EnvelopeHash)>, dirty: bool, contact_selector: Option>>, + forward_dialog: Option>>>, theme_default: ThemeAttribute, active_jobs: HashSet, state: MailViewState, @@ -68,6 +69,7 @@ impl Clone for MailView { fn clone(&self) -> Self { MailView { contact_selector: None, + forward_dialog: None, state: MailViewState::default(), active_jobs: self.active_jobs.clone(), ..*self @@ -90,6 +92,7 @@ impl MailView { coordinates, dirty: true, contact_selector: None, + forward_dialog: None, theme_default: crate::conf::value(context, "mail.view.body"), active_jobs: Default::default(), state: MailViewState::default(), @@ -328,6 +331,8 @@ impl Component for MailView { }; if let Some(ref mut s) = self.contact_selector.as_mut() { s.draw(grid, area, context); + } else if let Some(ref mut s) = self.forward_dialog.as_mut() { + s.draw(grid, area, context); } self.dirty = false; @@ -339,13 +344,29 @@ impl Component for MailView { return false; } + if let Some(ref mut s) = self.contact_selector { + if s.process_event(event, context) { + return true; + } + } + + if let Some(ref mut s) = self.forward_dialog { + if s.process_event(event, context) { + return true; + } + } + /* If envelope data is loaded, pass it to envelope views */ if self.state.process_event(event, context) { return true; } - match (&mut self.contact_selector, &mut event) { - (Some(ref s), UIEvent::FinishedUIDialog(id, results)) if *id == s.id() => { + match ( + &mut self.contact_selector, + &mut self.forward_dialog, + &mut event, + ) { + (Some(ref s), _, UIEvent::FinishedUIDialog(id, results)) if *id == s.id() => { if let Some(results) = results.downcast_ref::>() { let account = &mut context.accounts[&coordinates.0]; { @@ -353,54 +374,61 @@ impl Component for MailView { account.address_book.add_card(card.clone()); } } + self.contact_selector = None; } self.set_dirty(true); return true; } - (Some(ref mut s), _) => { - if s.process_event(event, context) { - return true; + (_, Some(ref s), UIEvent::FinishedUIDialog(id, result)) if *id == s.id() => { + if let Some(result) = result.downcast_ref::>() { + self.forward_dialog = None; + if let Some(result) = *result { + self.perform_action(result, context); + } } + self.set_dirty(true); + return true; } - _ => match event { - UIEvent::StatusEvent(StatusEvent::JobFinished(ref job_id)) - if self.active_jobs.contains(job_id) => - { - match self.state { - MailViewState::LoadingBody { - ref mut handle, - pending_action: _, - } if handle.job_id == *job_id => { - match handle.chan.try_recv() { - Err(_) => { /* Job was canceled */ } - Ok(None) => { /* something happened, perhaps a worker - * thread panicked */ - } - Ok(Some(Ok(bytes))) => { - MailViewState::load_bytes(self, bytes, context); - } - Ok(Some(Err(err))) => { - self.state = MailViewState::Error { err }; - } + _ => {} + } + match &event { + UIEvent::StatusEvent(StatusEvent::JobFinished(ref job_id)) + if self.active_jobs.contains(job_id) => + { + match self.state { + MailViewState::LoadingBody { + ref mut handle, + pending_action: _, + } if handle.job_id == *job_id => { + match handle.chan.try_recv() { + Err(_) => { /* Job was canceled */ } + Ok(None) => { /* something happened, perhaps a worker + * thread panicked */ + } + Ok(Some(Ok(bytes))) => { + MailViewState::load_bytes(self, bytes, context); + } + Ok(Some(Err(err))) => { + self.state = MailViewState::Error { err }; } } - MailViewState::Init { .. } => { - self.init_futures(context); - } - MailViewState::Loaded { .. } => { - log::debug!( - "MailView.active_jobs contains job id {:?} but MailViewState is \ - already loaded; what job was this and why was it in active_jobs?", - job_id - ); - } - _ => {} } - self.active_jobs.remove(job_id); - self.set_dirty(true); + MailViewState::Init { .. } => { + self.init_futures(context); + } + MailViewState::Loaded { .. } => { + log::debug!( + "MailView.active_jobs contains job id {:?} but MailViewState is \ + already loaded; what job was this and why was it in active_jobs?", + job_id + ); + } + _ => {} } - _ => {} - }, + self.active_jobs.remove(job_id); + self.set_dirty(true); + } + _ => {} } let shortcuts = &self.shortcuts(context); @@ -436,27 +464,28 @@ impl Component for MailView { .forward_as_attachment ) { f if f.is_ask() => { - let id = self.id; - context.replies.push_back(UIEvent::GlobalUIDialog(Box::new( - UIConfirmationDialog::new( - "How do you want the email to be forwarded?", - vec![ - (true, "inline".to_string()), - (false, "as attachment".to_string()), - ], - true, - Some(Box::new(move |_: ComponentId, result: bool| { + self.forward_dialog = Some(Box::new(UIDialog::new( + "How do you want the email to be forwarded?", + vec![ + ( + Some(PendingReplyAction::ForwardInline), + "inline".to_string(), + ), + ( + Some(PendingReplyAction::ForwardAttachment), + "as attachment".to_string(), + ), + ], + true, + Some(Box::new( + move |id: ComponentId, result: &[Option]| { Some(UIEvent::FinishedUIDialog( id, - Box::new(if result { - PendingReplyAction::ForwardInline - } else { - PendingReplyAction::ForwardAttachment - }), + Box::new(result.get(0).cloned()), )) - })), - context, - ), + }, + )), + context, ))); } f if f.is_true() => { @@ -562,9 +591,10 @@ impl Component for MailView { return true; } UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Alt('')) - if self.contact_selector.is_some() => + if self.contact_selector.is_some() || self.forward_dialog.is_some() => { self.contact_selector = None; + self.forward_dialog = None; self.set_dirty(true); return true; } @@ -592,7 +622,7 @@ impl Component for MailView { let draft: Draft = mailto.into(); let mut composer = Composer::with_account(coordinates.0, context); - composer.set_draft(draft); + composer.set_draft(draft, context); context.replies.push_back(UIEvent::Action(Tab(New(Some( Box::new(composer), ))))); @@ -747,12 +777,19 @@ impl Component for MailView { .as_ref() .map(|s| s.is_dirty()) .unwrap_or(false) + || self + .forward_dialog + .as_ref() + .map(|s| s.is_dirty()) + .unwrap_or(false) } fn set_dirty(&mut self, value: bool) { self.dirty = value; if let Some(ref mut s) = self.contact_selector { s.set_dirty(value); + } else if let Some(ref mut s) = self.forward_dialog { + s.set_dirty(value); } self.state.set_dirty(value); } diff --git a/src/components/mail/view/state.rs b/src/components/mail/view/state.rs index dd01e978b..8fed73f38 100644 --- a/src/components/mail/view/state.rs +++ b/src/components/mail/view/state.rs @@ -24,7 +24,7 @@ use melib::{Envelope, Error, Mail, Result}; use super::{EnvelopeView, MailView, ViewSettings}; use crate::{jobs::JoinHandle, mailbox_settings, Component, Context, ShortcutMaps, UIEvent}; -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, PartialEq)] pub enum PendingReplyAction { Reply, ReplyToAuthor, diff --git a/src/components/utilities/pager.rs b/src/components/utilities/pager.rs index fe3d3bcd0..698e553e3 100644 --- a/src/components/utilities/pager.rs +++ b/src/components/utilities/pager.rs @@ -693,7 +693,7 @@ impl Component for Pager { } UIEvent::Input(ref key) if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_right"]) - && dbg!(self.cols_lt_width) => + && self.cols_lt_width => { self.movement = Some(PageMovement::Right(1)); self.dirty = true; diff --git a/src/components/utilities/widgets.rs b/src/components/utilities/widgets.rs index 0fc27752a..bdd6fdb71 100644 --- a/src/components/utilities/widgets.rs +++ b/src/components/utilities/widgets.rs @@ -203,7 +203,7 @@ pub enum FormButtonActions { Other(&'static str), } -#[derive(Debug, Default)] +#[derive(Debug)] pub struct FormWidget where T: 'static + std::fmt::Debug + Copy + Default + Send + Sync, @@ -217,21 +217,47 @@ where focus: FormFocus, hide_buttons: bool, dirty: bool, + cursor_up_shortcut: Key, + cursor_down_shortcut: Key, id: ComponentId, } +impl Default for FormWidget { + fn default() -> Self { + Self { + fields: Default::default(), + layout: Default::default(), + buttons: Default::default(), + focus: FormFocus::Fields, + hide_buttons: false, + field_name_max_length: 10, + cursor: 0, + dirty: true, + cursor_up_shortcut: Key::Up, + cursor_down_shortcut: Key::Down, + id: ComponentId::default(), + } + } +} + impl fmt::Display for FormWidget { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - Display::fmt("", f) + write!(f, "form") } } impl FormWidget { - pub fn new(action: (Cow<'static, str>, T)) -> FormWidget { + pub fn new( + action: (Cow<'static, str>, T), + cursor_up_shortcut: Key, + cursor_down_shortcut: Key, + ) -> FormWidget { FormWidget { buttons: ButtonWidget::new(action), focus: FormFocus::Fields, hide_buttons: false, + cursor_up_shortcut, + cursor_down_shortcut, id: ComponentId::default(), dirty: true, ..Default::default() @@ -269,6 +295,7 @@ impl FormWidget self.fields .insert(value.0, Field::Choice(value.1, 0, ComponentId::default())); } + pub fn push_cl(&mut self, value: (Cow<'static, str>, String, AutoCompleteFn)) { self.field_name_max_length = std::cmp::max(self.field_name_max_length, value.0.len()); self.layout.push(value.0.clone()); @@ -280,6 +307,7 @@ impl FormWidget )), ); } + pub fn push(&mut self, value: (Cow<'static, str>, String)) { self.field_name_max_length = std::cmp::max(self.field_name_max_length, value.0.len()); self.layout.push(value.0.clone()); @@ -309,6 +337,7 @@ impl FormWidget None } } + pub fn buttons_result(&self) -> Option { self.buttons.result } @@ -423,13 +452,19 @@ impl Component for context.dirty_areas.push_back(area); } } + fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool { - if self.focus == FormFocus::Buttons && self.buttons.process_event(event, context) { + if !self.hide_buttons + && self.focus == FormFocus::Buttons + && self.buttons.process_event(event, context) + { return true; } match *event { - UIEvent::Input(Key::Up) if self.focus == FormFocus::Buttons => { + UIEvent::Input(ref k) + if *k == self.cursor_up_shortcut && self.focus == FormFocus::Buttons => + { self.focus = FormFocus::Fields; self.buttons.set_focus(false); self.set_dirty(true); @@ -441,7 +476,7 @@ impl Component for self.set_dirty(true); return true; } - UIEvent::Input(Key::Up) => { + UIEvent::Input(ref k) if *k == self.cursor_up_shortcut => { self.cursor = self.cursor.saturating_sub(1); self.set_dirty(true); return true; @@ -452,12 +487,17 @@ impl Component for self.set_dirty(true); return true; } - UIEvent::Input(Key::Down) if self.cursor < self.layout.len().saturating_sub(1) => { + UIEvent::Input(ref k) + if *k == self.cursor_down_shortcut + && self.cursor < self.layout.len().saturating_sub(1) => + { self.cursor += 1; self.set_dirty(true); return true; } - UIEvent::Input(Key::Down) if self.focus == FormFocus::Fields => { + UIEvent::Input(ref k) + if *k == self.cursor_down_shortcut && self.focus == FormFocus::Fields => + { self.focus = FormFocus::Buttons; self.buttons.set_focus(true); self.set_dirty(true); @@ -517,9 +557,11 @@ impl Component for } false } + fn is_dirty(&self) -> bool { self.dirty || self.buttons.is_dirty() } + fn set_dirty(&mut self, value: bool) { self.dirty = value; self.buttons.set_dirty(value); @@ -630,6 +672,7 @@ where self.dirty = false; } } + fn process_event(&mut self, event: &mut UIEvent, _context: &mut Context) -> bool { match *event { UIEvent::Input(Key::Char('\n')) => { @@ -655,9 +698,11 @@ where } false } + fn is_dirty(&self) -> bool { self.dirty } + fn set_dirty(&mut self, value: bool) { self.dirty = value; } diff --git a/src/conf/shortcuts.rs b/src/conf/shortcuts.rs index 8f3328a1a..ecc5605ca 100644 --- a/src/conf/shortcuts.rs +++ b/src/conf/shortcuts.rs @@ -228,7 +228,7 @@ shortcut_key_values! { "general", shortcut_key_values! { "composing", pub struct ComposingShortcuts { - edit_mail |> "Edit mail." |> Key::Char('e'), + edit |> "Edit." |> Key::Char('e'), send_mail |> "Deliver draft to mailer" |> Key::Char('s'), scroll_up |> "Change field focus." |> Key::Char('k'), scroll_down |> "Change field focus." |> Key::Char('j') diff --git a/src/state.rs b/src/state.rs index de0d9b6bb..5b5e9dd4b 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1217,12 +1217,17 @@ impl State { (callback_fn.0)(&mut self.context); return; } - UIEvent::GlobalUIDialog(dialog) => { - self.overlay.insert(dialog.id(), dialog); + UIEvent::GlobalUIDialog { value, parent } => { + self.context.realized.insert(value.id(), parent); + self.overlay.insert(value.id(), value); + self.process_realizations(); return; } _ => {} } + + self.process_realizations(); + let Self { ref mut components, ref mut context, @@ -1237,6 +1242,15 @@ impl State { } } + if !self.context.replies.is_empty() { + let replies: smallvec::SmallVec<[UIEvent; 8]> = + self.context.replies.drain(0..).collect(); + // Pass replies to self and call count on the map iterator to force evaluation + replies.into_iter().map(|r| self.rcv_event(r)).count(); + } + } + + fn process_realizations(&mut self) { while let Some((id, parent)) = self.context.realized.pop() { match parent { None => { @@ -1271,6 +1285,7 @@ impl State { } } } + while let Some(id) = self.context.unrealized.pop() { let mut to_delete = BTreeSet::new(); for (desc, _) in self.component_tree.iter().filter(|(_, path)| { @@ -1285,13 +1300,6 @@ impl State { self.components.remove(&id); self.overlay.remove(&id); } - - if !self.context.replies.is_empty() { - let replies: smallvec::SmallVec<[UIEvent; 8]> = - self.context.replies.drain(0..).collect(); - // Pass replies to self and call count on the map iterator to force evaluation - replies.into_iter().map(|r| self.rcv_event(r)).count(); - } } pub fn try_wait_on_child(&mut self) -> Option { diff --git a/src/types.rs b/src/types.rs index 5369d5db5..de37c9fbc 100644 --- a/src/types.rs +++ b/src/types.rs @@ -154,7 +154,10 @@ pub enum UIEvent { content: UIMessage, }, Callback(CallbackFn), - GlobalUIDialog(Box), + GlobalUIDialog { + value: Box, + parent: Option, + }, Timer(TimerId), ConfigReload { old_settings: Box,