diff --git a/CHANGELOG.md b/CHANGELOG.md index e224e6886..32bb9929b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Added shortcuts for focusing to sidebar menu and back to the e-mail view (`focus_left` and `focus_right`) - `f76f4ea3` A new manual page, `meli.7` which contains a general tutorial for using meli. -- `f76f4ea3` Added shortcuts for focusing to sidebar menu and back to the e-mail view (`focus_on_menu` and `focus_on_list`) - `cbe593cf` add configurable header preample suffix and prefix for editing - `a484b397` Added instructions and information to error shown when libnotmuch could not be found. - `a484b397` Added configuration setting `library_file_path` to notmuch backend if user wants to specify the library's location manually. diff --git a/docs/meli.7 b/docs/meli.7 index 02b665ee7..0567926e6 100644 --- a/docs/meli.7 +++ b/docs/meli.7 @@ -233,10 +233,10 @@ Press to toggle the sidebars visibility. .Pp Press -.Shortcut Left listing focus_on_menu +.Shortcut Left listing focus_right to switch focus on the sidebar menu. Press -.Shortcut Right listing focus_on_list +.Shortcut Right listing focus_left to switch focus on the e-mail list. .Pp On the e-mail list, press diff --git a/docs/meli.conf.5 b/docs/meli.conf.5 index b8b5eef91..163e9722a 100644 --- a/docs/meli.conf.5 +++ b/docs/meli.conf.5 @@ -781,6 +781,14 @@ Decrease sidebar width. Toggle visibility of side menu in mail list. .\" default value .Pq Em ` +.It Ic focus_left +Switch focus on the left. +.\" default value +.Pq Em Left +.It Ic focus_right +Switch focus on the right. +.\" default value +.Pq Em Right .It Ic exit_entry Exit e-mail entry. .\" default value diff --git a/src/components/mail/listing.rs b/src/components/mail/listing.rs index b00bfdcbc..a4143616c 100644 --- a/src/components/mail/listing.rs +++ b/src/components/mail/listing.rs @@ -61,6 +61,13 @@ pub use self::plain::*; mod offline; pub use self::offline::*; +#[derive(Debug, Copy, Clone)] +pub enum Focus { + None, + Entry, + EntryFullscreen, +} + #[derive(Debug, Copy, PartialEq, Clone)] pub enum Modifier { SymmetricDifference, @@ -516,6 +523,8 @@ pub trait ListingTrait: Component { None } fn set_movement(&mut self, mvm: PageMovement); + fn focus(&self) -> Focus; + fn set_focus(&mut self, new_value: Focus, context: &mut Context); } #[derive(Debug)] @@ -655,7 +664,7 @@ impl Component for Listing { let bottom_right = bottom_right!(area); let total_cols = get_x(bottom_right) - get_x(upper_left); - let right_component_width = if self.menu_visibility { + let right_component_width = if self.is_menu_visible() { if self.focus == ListingFocus::Menu { (self.ratio * total_cols) / 100 } else { @@ -925,7 +934,7 @@ impl Component for Listing { if self.focus == ListingFocus::Mailbox { match *event { UIEvent::Input(Key::Mouse(MouseEvent::Press(MouseButton::Left, x, _y))) - if self.menu_visibility => + if self.is_menu_visible() => { match self.menu_width { WidgetWidth::Hold(wx) | WidgetWidth::Set(wx) @@ -942,7 +951,7 @@ impl Component for Listing { self.set_dirty(true); return true; } - UIEvent::Input(Key::Mouse(MouseEvent::Hold(x, _y))) if self.menu_visibility => { + UIEvent::Input(Key::Mouse(MouseEvent::Hold(x, _y))) if self.is_menu_visible() => { match self.menu_width { WidgetWidth::Hold(ref mut hx) => { *hx = usize::from(x).saturating_sub(1); @@ -952,7 +961,9 @@ impl Component for Listing { self.set_dirty(true); return true; } - UIEvent::Input(Key::Mouse(MouseEvent::Release(x, _y))) if self.menu_visibility => { + UIEvent::Input(Key::Mouse(MouseEvent::Release(x, _y))) + if self.is_menu_visible() => + { match self.menu_width { WidgetWidth::Hold(_) => { self.menu_width = WidgetWidth::Set(usize::from(x).saturating_sub(1)); @@ -963,8 +974,8 @@ impl Component for Listing { return true; } UIEvent::Input(ref k) - if self.menu_visibility - && shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_on_menu"]) => + if self.is_menu_visible() + && shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_left"]) => { self.focus = ListingFocus::Menu; if self.show_menu_scrollbar != ShowMenuScrollbar::Never { @@ -1337,7 +1348,7 @@ impl Component for Listing { } else if self.focus == ListingFocus::Menu { match *event { UIEvent::Input(ref k) - if shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_on_list"]) => + if shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_right"]) => { self.focus = ListingFocus::Mailbox; context @@ -2374,4 +2385,8 @@ impl Listing { self.get_status(context), ))); } + + fn is_menu_visible(&self) -> bool { + !matches!(self.component.focus(), Focus::EntryFullscreen) && self.menu_visibility + } } diff --git a/src/components/mail/listing/compact.rs b/src/components/mail/listing/compact.rs index fd09a080f..bf051ced8 100644 --- a/src/components/mail/listing/compact.rs +++ b/src/components/mail/listing/compact.rs @@ -187,7 +187,7 @@ pub struct CompactListing { dirty: bool, force_draw: bool, /// If `self.view` exists or not. - unfocused: bool, + focus: Focus, view: ThreadView, row_updates: SmallVec<[ThreadHash; 8]>, color_cache: ColorCache, @@ -304,7 +304,7 @@ impl MailListingTrait for CompactListing { if !force && old_cursor_pos == self.new_cursor_pos { self.view.update(context); - } else if self.unfocused { + } else if self.unfocused() { let thread = self.get_thread_under_cursor(self.cursor_pos.2); self.view = ThreadView::new(self.new_cursor_pos, thread, None, context); @@ -490,7 +490,7 @@ impl ListingTrait for CompactListing { fn set_coordinates(&mut self, coordinates: (AccountHash, MailboxHash)) { self.new_cursor_pos = (coordinates.0, coordinates.1, 0); - self.unfocused = false; + self.focus = Focus::None; self.view = ThreadView::default(); self.filtered_selection.clear(); self.filtered_order.clear(); @@ -818,7 +818,7 @@ impl ListingTrait for CompactListing { } fn unfocused(&self) -> bool { - self.unfocused + !matches!(self.focus, Focus::None) } fn set_modifier_active(&mut self, new_val: bool) { @@ -837,6 +837,33 @@ impl ListingTrait for CompactListing { self.movement = Some(mvm); self.set_dirty(true); } + + fn set_focus(&mut self, new_value: Focus, context: &mut Context) { + match new_value { + Focus::None => { + self.view + .process_event(&mut UIEvent::VisibilityChange(false), context); + self.dirty = true; + /* If self.row_updates is not empty and we exit a thread, the row_update events + * will be performed but the list will not be drawn. So force a draw in any case. + * */ + self.force_draw = true; + } + Focus::Entry => { + self.force_draw = true; + self.dirty = true; + self.view.set_dirty(true); + } + Focus::EntryFullscreen => { + self.view.set_dirty(true); + } + } + self.focus = new_value; + } + + fn focus(&self) -> Focus { + self.focus + } } impl fmt::Display for CompactListing { @@ -863,13 +890,13 @@ impl CompactListing { filtered_selection: Vec::new(), filtered_order: HashMap::default(), selection: HashMap::default(), + focus: Focus::None, row_updates: SmallVec::new(), data_columns: DataColumns::default(), rows_drawn: SegmentTree::default(), rows: vec![], dirty: true, force_draw: true, - unfocused: false, view: ThreadView::default(), color_cache: ColorCache::default(), movement: None, @@ -1465,10 +1492,15 @@ impl CompactListing { impl Component for CompactListing { fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { - if !self.unfocused { - if !self.is_dirty() { - return; - } + if !self.is_dirty() { + return; + } + + if matches!(self.focus, Focus::EntryFullscreen) { + return self.view.draw(grid, area, context); + } + + if !self.unfocused() { let mut area = area; if !self.filter_term.is_empty() { let (upper_left, bottom_right) = area; @@ -1715,40 +1747,81 @@ impl Component for CompactListing { } self.dirty = false; } + fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool { - if self.unfocused && self.view.process_event(event, context) { + let shortcuts = self.get_shortcuts(context); + + match (&event, self.focus) { + (UIEvent::Input(ref k), Focus::Entry) + if shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_right"]) => + { + self.set_focus(Focus::EntryFullscreen, context); + return true; + } + (UIEvent::Input(ref k), Focus::EntryFullscreen) + if shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_left"]) => + { + self.set_focus(Focus::Entry, context); + return true; + } + (UIEvent::Input(ref k), Focus::Entry) + if shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_left"]) => + { + self.set_focus(Focus::None, context); + return true; + } + _ => {} + } + + if self.unfocused() && self.view.process_event(event, context) { return true; } - let shortcuts = self.get_shortcuts(context); if self.length > 0 { match *event { UIEvent::Input(ref k) - if !self.unfocused - && shortcut!(k == shortcuts[Listing::DESCRIPTION]["open_entry"]) => + if matches!(self.focus, Focus::None) + && (shortcut!(k == shortcuts[Listing::DESCRIPTION]["open_entry"]) + || shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_right"])) => { let thread = self.get_thread_under_cursor(self.cursor_pos.2); self.view = ThreadView::new(self.cursor_pos, thread, None, context); - self.unfocused = true; - self.dirty = true; + self.set_focus(Focus::Entry, context); return true; } UIEvent::Input(ref k) - if self.unfocused + if matches!(self.focus, Focus::Entry) && shortcut!(k == shortcuts[Listing::DESCRIPTION]["exit_entry"]) => { - self.unfocused = false; - self.view - .process_event(&mut UIEvent::VisibilityChange(false), context); - self.dirty = true; - /* If self.row_updates is not empty and we exit a thread, the row_update events - * will be performed but the list will not be drawn. So force a draw in any case. - * */ - self.force_draw = true; + self.set_focus(Focus::None, context); + return true; + } + UIEvent::Input(ref k) + if matches!(self.focus, Focus::None) + && shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_right"]) => + { + self.set_focus(Focus::Entry, context); + return true; + } + UIEvent::Input(ref k) + if !matches!(self.focus, Focus::None) + && shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_left"]) => + { + match self.focus { + Focus::Entry => { + self.set_focus(Focus::None, context); + } + Focus::EntryFullscreen => { + self.set_focus(Focus::Entry, context); + } + Focus::None => { + unreachable!(); + } + } return true; } UIEvent::Input(ref key) - if !self.unfocused + if !self.unfocused() && shortcut!(key == shortcuts[Listing::DESCRIPTION]["select_entry"]) => { if self.modifier_active && self.modifier_command.is_none() { @@ -1762,7 +1835,7 @@ impl Component for CompactListing { } UIEvent::Action(ref action) => { match action { - Action::Sort(field, order) if !self.unfocused => { + Action::Sort(field, order) if !self.unfocused() => { debug!("Sort {:?} , {:?}", field, order); self.sort = (*field, *order); self.sortcmd = true; @@ -1774,13 +1847,13 @@ impl Component for CompactListing { } return true; } - Action::SubSort(field, order) if !self.unfocused => { + Action::SubSort(field, order) if !self.unfocused() => { debug!("SubSort {:?} , {:?}", field, order); self.subsort = (*field, *order); // FIXME: perform subsort. return true; } - Action::Listing(ToggleThreadSnooze) if !self.unfocused => { + Action::Listing(ToggleThreadSnooze) if !self.unfocused() => { let thread = self.get_thread_under_cursor(self.cursor_pos.2); let account = &mut context.accounts[&self.cursor_pos.0]; account @@ -1871,7 +1944,7 @@ impl Component for CompactListing { self.dirty = true; - if self.unfocused { + if self.unfocused() { self.view .process_event(&mut UIEvent::EnvelopeRename(*old_hash, *new_hash), context); } @@ -1901,7 +1974,7 @@ impl Component for CompactListing { self.dirty = true; - if self.unfocused { + if self.unfocused() { self.view .process_event(&mut UIEvent::EnvelopeUpdate(*env_hash), context); } @@ -1913,7 +1986,7 @@ impl Component for CompactListing { self.dirty = true; } UIEvent::Input(Key::Esc) - if !self.unfocused + if !self.unfocused() && self.selection.values().cloned().any(std::convert::identity) => { for v in self.selection.values_mut() { @@ -1922,13 +1995,13 @@ impl Component for CompactListing { self.dirty = true; return true; } - UIEvent::Input(Key::Esc) if !self.unfocused && !self.filter_term.is_empty() => { + UIEvent::Input(Key::Esc) if !self.unfocused() && !self.filter_term.is_empty() => { self.set_coordinates((self.new_cursor_pos.0, self.new_cursor_pos.1)); self.refresh_mailbox(context, false); self.set_dirty(true); return true; } - UIEvent::Action(Action::Listing(Search(ref filter_term))) if !self.unfocused => { + UIEvent::Action(Action::Listing(Search(ref filter_term))) if !self.unfocused() => { match context.accounts[&self.cursor_pos.0].search( filter_term, self.sort, @@ -1950,7 +2023,7 @@ impl Component for CompactListing { }; self.set_dirty(true); } - UIEvent::Action(Action::Listing(Select(ref search_term))) if !self.unfocused => { + UIEvent::Action(Action::Listing(Select(ref search_term))) if !self.unfocused() => { match context.accounts[&self.cursor_pos.0].search( search_term, self.sort, @@ -2017,23 +2090,24 @@ impl Component for CompactListing { } false } + fn is_dirty(&self) -> bool { - self.dirty - || if self.unfocused { - self.view.is_dirty() - } else { - false - } + match self.focus { + Focus::None => self.dirty, + Focus::Entry => self.dirty || self.view.is_dirty(), + Focus::EntryFullscreen => self.view.is_dirty(), + } } + fn set_dirty(&mut self, value: bool) { self.dirty = value; - if self.unfocused { + if self.unfocused() { self.view.set_dirty(value); } } fn get_shortcuts(&self, context: &Context) -> ShortcutMaps { - let mut map = if self.unfocused { + let mut map = if self.unfocused() { self.view.get_shortcuts(context) } else { ShortcutMaps::default() diff --git a/src/components/mail/listing/conversations.rs b/src/components/mail/listing/conversations.rs index 95c56c4e4..0252f7095 100644 --- a/src/components/mail/listing/conversations.rs +++ b/src/components/mail/listing/conversations.rs @@ -114,7 +114,7 @@ pub struct ConversationsListing { dirty: bool, force_draw: bool, /// If `self.view` exists or not. - unfocused: bool, + focus: Focus, view: ThreadView, row_updates: SmallVec<[ThreadHash; 8]>, color_cache: ColorCache, @@ -215,7 +215,7 @@ impl MailListingTrait for ConversationsListing { if !force && old_cursor_pos == self.new_cursor_pos && old_mailbox_hash == self.cursor_pos.1 { self.view.update(context); - } else if self.unfocused { + } else if self.unfocused() { let thread_group = self.get_thread_under_cursor(self.cursor_pos.2); self.view = ThreadView::new(self.new_cursor_pos, thread_group, None, context); @@ -352,7 +352,7 @@ impl ListingTrait for ConversationsListing { fn set_coordinates(&mut self, coordinates: (AccountHash, MailboxHash)) { self.new_cursor_pos = (coordinates.0, coordinates.1, 0); - self.unfocused = false; + self.focus = Focus::None; self.view = ThreadView::default(); self.filtered_selection.clear(); self.filtered_order.clear(); @@ -537,7 +537,7 @@ impl ListingTrait for ConversationsListing { } fn unfocused(&self) -> bool { - self.unfocused + !matches!(self.focus, Focus::None) } fn set_modifier_active(&mut self, new_val: bool) { @@ -556,6 +556,33 @@ impl ListingTrait for ConversationsListing { self.movement = Some(mvm); self.set_dirty(true); } + + fn set_focus(&mut self, new_value: Focus, context: &mut Context) { + match new_value { + Focus::None => { + self.view + .process_event(&mut UIEvent::VisibilityChange(false), context); + self.dirty = true; + /* If self.row_updates is not empty and we exit a thread, the row_update events + * will be performed but the list will not be drawn. So force a draw in any case. + * */ + self.force_draw = true; + } + Focus::Entry => { + self.force_draw = true; + self.dirty = true; + self.view.set_dirty(true); + } + Focus::EntryFullscreen => { + self.view.set_dirty(true); + } + } + self.focus = new_value; + } + + fn focus(&self) -> Focus { + self.focus + } } impl fmt::Display for ConversationsListing { @@ -569,7 +596,7 @@ impl ConversationsListing { //const PADDING_CHAR: char = ' '; //░'; pub fn new(coordinates: (AccountHash, MailboxHash)) -> Box { - Box::new(ConversationsListing { + Box::new(Self { cursor_pos: (coordinates.0, 1, 0), new_cursor_pos: (coordinates.0, coordinates.1, 0), length: 0, @@ -586,7 +613,7 @@ impl ConversationsListing { rows: Ok(Vec::with_capacity(1024)), dirty: true, force_draw: true, - unfocused: false, + focus: Focus::None, view: ThreadView::default(), color_cache: ColorCache::default(), movement: None, @@ -907,6 +934,11 @@ impl Component for ConversationsListing { if !self.is_dirty() { return; } + + if matches!(self.focus, Focus::EntryFullscreen) { + return self.view.draw(grid, area, context); + } + let (upper_left, bottom_right) = area; { let mut area = area; @@ -1142,7 +1174,7 @@ impl Component for ConversationsListing { self.draw_list(grid, area, context); } } - if self.unfocused { + if matches!(self.focus, Focus::Entry) { if self.length == 0 && self.dirty { clear_area(grid, area, self.color_cache.theme_default); context.dirty_areas.push_back(area); @@ -1157,40 +1189,81 @@ impl Component for ConversationsListing { } self.dirty = false; } + fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool { - if self.unfocused && self.view.process_event(event, context) { + let shortcuts = self.get_shortcuts(context); + + match (&event, self.focus) { + (UIEvent::Input(ref k), Focus::Entry) + if shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_right"]) => + { + self.set_focus(Focus::EntryFullscreen, context); + return true; + } + (UIEvent::Input(ref k), Focus::EntryFullscreen) + if shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_left"]) => + { + self.set_focus(Focus::Entry, context); + return true; + } + (UIEvent::Input(ref k), Focus::Entry) + if shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_left"]) => + { + self.set_focus(Focus::None, context); + return true; + } + _ => {} + } + + if self.unfocused() && self.view.process_event(event, context) { return true; } - let shortcuts = self.get_shortcuts(context); if self.length > 0 { match *event { UIEvent::Input(ref k) - if !self.unfocused - && shortcut!(k == shortcuts[Listing::DESCRIPTION]["open_entry"]) => + if matches!(self.focus, Focus::None) + && (shortcut!(k == shortcuts[Listing::DESCRIPTION]["open_entry"]) + || shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_right"])) => { let thread = self.get_thread_under_cursor(self.cursor_pos.2); self.view = ThreadView::new(self.cursor_pos, thread, None, context); - self.unfocused = true; - self.dirty = true; + self.set_focus(Focus::Entry, context); return true; } UIEvent::Input(ref k) - if self.unfocused + if !matches!(self.focus, Focus::None) && shortcut!(k == shortcuts[Listing::DESCRIPTION]["exit_entry"]) => { - self.unfocused = false; - self.view - .process_event(&mut UIEvent::VisibilityChange(false), context); - self.dirty = true; - /* If self.row_updates is not empty and we exit a thread, the row_update events - * will be performed but the list will not be drawn. So force a draw in any case. - * */ - self.force_draw = true; + self.set_focus(Focus::None, context); + return true; + } + UIEvent::Input(ref k) + if matches!(self.focus, Focus::Entry) + && shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_right"]) => + { + self.set_focus(Focus::EntryFullscreen, context); + return true; + } + UIEvent::Input(ref k) + if !matches!(self.focus, Focus::None) + && shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_left"]) => + { + match self.focus { + Focus::Entry => { + self.set_focus(Focus::None, context); + } + Focus::EntryFullscreen => { + self.set_focus(Focus::Entry, context); + } + Focus::None => { + unreachable!(); + } + } return true; } UIEvent::Input(ref key) - if !self.unfocused + if !self.unfocused() && shortcut!(key == shortcuts[Listing::DESCRIPTION]["select_entry"]) => { if self.modifier_active && self.modifier_command.is_none() { @@ -1221,7 +1294,7 @@ impl Component for ConversationsListing { self.dirty = true; - if self.unfocused { + if self.unfocused() { self.view.process_event( &mut UIEvent::EnvelopeRename(*old_hash, *new_hash), context, @@ -1253,13 +1326,13 @@ impl Component for ConversationsListing { self.dirty = true; - if self.unfocused { + if self.unfocused() { self.view .process_event(&mut UIEvent::EnvelopeUpdate(*env_hash), context); } } UIEvent::Action(ref action) => match action { - Action::SubSort(field, order) if !self.unfocused => { + Action::SubSort(field, order) if !self.unfocused() => { debug!("SubSort {:?} , {:?}", field, order); self.subsort = (*field, *order); // FIXME subsort @@ -1271,7 +1344,7 @@ impl Component for ConversationsListing { //} return true; } - Action::Sort(field, order) if !self.unfocused => { + Action::Sort(field, order) if !self.unfocused() => { debug!("Sort {:?} , {:?}", field, order); // FIXME sort /* @@ -1291,7 +1364,7 @@ impl Component for ConversationsListing { */ return true; } - Action::Listing(ToggleThreadSnooze) if !self.unfocused => { + Action::Listing(ToggleThreadSnooze) if !self.unfocused() => { let thread = self.get_thread_under_cursor(self.cursor_pos.2); let account = &mut context.accounts[&self.cursor_pos.0]; account @@ -1359,7 +1432,7 @@ impl Component for ConversationsListing { self.dirty = true; } UIEvent::Action(ref action) => match action { - Action::Listing(Search(ref filter_term)) if !self.unfocused => { + Action::Listing(Search(ref filter_term)) if !self.unfocused() => { match context.accounts[&self.cursor_pos.0].search( filter_term, self.sort, @@ -1385,7 +1458,7 @@ impl Component for ConversationsListing { _ => {} }, UIEvent::Input(Key::Esc) - if !self.unfocused + if !self.unfocused() && self.selection.values().cloned().any(std::convert::identity) => { for (k, v) in self.selection.iter_mut() { @@ -1398,7 +1471,7 @@ impl Component for ConversationsListing { return true; } UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Char('')) - if !self.unfocused && !&self.filter_term.is_empty() => + if !self.unfocused() && !&self.filter_term.is_empty() => { self.set_coordinates((self.new_cursor_pos.0, self.new_cursor_pos.1)); self.refresh_mailbox(context, false); @@ -1432,23 +1505,24 @@ impl Component for ConversationsListing { false } + fn is_dirty(&self) -> bool { - self.dirty - || if self.unfocused { - self.view.is_dirty() - } else { - false - } + match self.focus { + Focus::None => self.dirty, + Focus::Entry => self.dirty || self.view.is_dirty(), + Focus::EntryFullscreen => self.view.is_dirty(), + } } + fn set_dirty(&mut self, value: bool) { - if self.unfocused { + if self.unfocused() { self.view.set_dirty(value); } self.dirty = value; } fn get_shortcuts(&self, context: &Context) -> ShortcutMaps { - let mut map = if self.unfocused { + let mut map = if self.unfocused() { self.view.get_shortcuts(context) } else { ShortcutMaps::default() diff --git a/src/components/mail/listing/offline.rs b/src/components/mail/listing/offline.rs index 2c04702b2..940f5ae96 100644 --- a/src/components/mail/listing/offline.rs +++ b/src/components/mail/listing/offline.rs @@ -84,6 +84,12 @@ impl ListingTrait for OfflineListing { } fn set_movement(&mut self, _: PageMovement) {} + + fn focus(&self) -> Focus { + Focus::None + } + + fn set_focus(&mut self, _new_value: Focus, _context: &mut Context) {} } impl fmt::Display for OfflineListing { diff --git a/src/components/mail/listing/plain.rs b/src/components/mail/listing/plain.rs index 0228fa649..c4073f11c 100644 --- a/src/components/mail/listing/plain.rs +++ b/src/components/mail/listing/plain.rs @@ -146,7 +146,7 @@ pub struct PlainListing { dirty: bool, force_draw: bool, /// If `self.view` exists or not. - unfocused: bool, + focus: Focus, view: MailView, row_updates: SmallVec<[EnvelopeHash; 8]>, _row_updates: SmallVec<[ThreadHash; 8]>, @@ -296,7 +296,7 @@ impl MailListingTrait for PlainListing { let temp = (self.new_cursor_pos.0, self.new_cursor_pos.1, env_hash); if !force && old_cursor_pos == self.new_cursor_pos { self.view.update(temp, context); - } else if self.unfocused { + } else if self.unfocused() { self.view = MailView::new(temp, None, None, context); } } @@ -335,7 +335,7 @@ impl ListingTrait for PlainListing { fn set_coordinates(&mut self, coordinates: (AccountHash, MailboxHash)) { self.new_cursor_pos = (coordinates.0, coordinates.1, 0); - self.unfocused = false; + self.focus = Focus::None; self.view = MailView::default(); self.filtered_selection.clear(); self.filtered_order.clear(); @@ -641,13 +641,44 @@ impl ListingTrait for PlainListing { } fn unfocused(&self) -> bool { - self.unfocused + !matches!(self.focus, Focus::None) } fn set_movement(&mut self, mvm: PageMovement) { self.movement = Some(mvm); self.set_dirty(true); } + + fn set_focus(&mut self, new_value: Focus, context: &mut Context) { + match new_value { + Focus::None => { + self.view + .process_event(&mut UIEvent::VisibilityChange(false), context); + self.dirty = true; + /* If self.row_updates is not empty and we exit a thread, the row_update events + * will be performed but the list will not be drawn. So force a draw in any case. + * */ + self.force_draw = true; + } + Focus::Entry => { + let env_hash = self.get_env_under_cursor(self.cursor_pos.2, context); + let temp = (self.cursor_pos.0, self.cursor_pos.1, env_hash); + self.view = MailView::new(temp, None, None, context); + self.force_draw = true; + self.dirty = true; + self.view.set_dirty(true); + } + Focus::EntryFullscreen => { + self.dirty = true; + self.view.set_dirty(true); + } + } + self.focus = new_value; + } + + fn focus(&self) -> Focus { + self.focus + } } impl fmt::Display for PlainListing { @@ -680,7 +711,7 @@ impl PlainListing { data_columns: DataColumns::default(), dirty: true, force_draw: true, - unfocused: false, + focus: Focus::None, view: MailView::default(), color_cache: ColorCache::default(), active_jobs: HashMap::default(), @@ -1060,10 +1091,15 @@ impl PlainListing { impl Component for PlainListing { fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { - if !self.unfocused { - if !self.is_dirty() { - return; - } + if !self.is_dirty() { + return; + } + + if matches!(self.focus, Focus::EntryFullscreen) { + return self.view.draw(grid, area, context); + } + + if matches!(self.focus, Focus::None) { let mut area = area; if !self.filter_term.is_empty() { let (upper_left, bottom_right) = area; @@ -1129,48 +1165,79 @@ impl Component for PlainListing { } self.dirty = false; } + fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool { - if self.unfocused && self.view.process_event(event, context) { + let shortcuts = self.get_shortcuts(context); + + match (&event, self.focus) { + (UIEvent::Input(ref k), Focus::Entry) + if shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_right"]) => + { + self.set_focus(Focus::EntryFullscreen, context); + return true; + } + (UIEvent::Input(ref k), Focus::EntryFullscreen) + if shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_left"]) => + { + self.set_focus(Focus::Entry, context); + return true; + } + (UIEvent::Input(ref k), Focus::Entry) + if shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_left"]) => + { + self.set_focus(Focus::None, context); + return true; + } + _ => {} + } + + if self.unfocused() && self.view.process_event(event, context) { return true; } - let shortcuts = self.get_shortcuts(context); if self.length > 0 { match *event { UIEvent::Input(ref k) - if !self.unfocused - && shortcut!(k == shortcuts[Listing::DESCRIPTION]["open_entry"]) => + if matches!(self.focus, Focus::None) + && (shortcut!(k == shortcuts[Listing::DESCRIPTION]["open_entry"]) + || shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_right"])) => { - let env_hash = self.get_env_under_cursor(self.cursor_pos.2, context); - let temp = (self.cursor_pos.0, self.cursor_pos.1, env_hash); - self.view = MailView::new(temp, None, None, context); - self.unfocused = true; - self.dirty = true; + self.set_focus(Focus::Entry, context); return true; } UIEvent::Input(ref k) - if self.unfocused + if !matches!(self.focus, Focus::None) && shortcut!(k == shortcuts[Listing::DESCRIPTION]["exit_entry"]) => { - self.unfocused = false; - self.view - .process_event(&mut UIEvent::VisibilityChange(false), context); - self.dirty = true; - /* If self.row_updates is not empty and we exit a thread, the row_update events - * will be performed but the list will not be drawn. So force a draw in any case. - * */ - self.force_draw = true; + self.set_focus(Focus::None, context); + return true; + } + UIEvent::Input(ref k) + if !matches!(self.focus, Focus::None) + && shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_left"]) => + { + match self.focus { + Focus::Entry => { + self.set_focus(Focus::None, context); + } + Focus::EntryFullscreen => { + self.set_focus(Focus::Entry, context); + } + Focus::None => { + unreachable!(); + } + } return true; } UIEvent::Input(ref key) - if !self.unfocused + if !self.unfocused() && shortcut!(key == shortcuts[Listing::DESCRIPTION]["select_entry"]) => { let env_hash = self.get_env_under_cursor(self.cursor_pos.2, context); self.selection.entry(env_hash).and_modify(|e| *e = !*e); } UIEvent::Action(ref action) => match action { - Action::SubSort(field, order) if !self.unfocused => { + Action::SubSort(field, order) if !self.unfocused() => { debug!("SubSort {:?} , {:?}", field, order); self.subsort = (*field, *order); //if !self.filtered_selection.is_empty() { @@ -1181,7 +1248,7 @@ impl Component for PlainListing { //} return true; } - Action::Sort(field, order) if !self.unfocused => { + Action::Sort(field, order) if !self.unfocused() => { debug!("Sort {:?} , {:?}", field, order); self.sort = (*field, *order); return true; @@ -1189,7 +1256,7 @@ impl Component for PlainListing { Action::Listing(a @ ListingAction::SetSeen) | Action::Listing(a @ ListingAction::SetUnseen) | Action::Listing(a @ ListingAction::Delete) - if !self.unfocused => + if !self.unfocused() => { let is_selection_empty = self.selection.values().cloned().any(std::convert::identity); @@ -1295,7 +1362,7 @@ impl Component for PlainListing { self.dirty = true; - if self.unfocused { + if self.unfocused() { self.view .process_event(&mut UIEvent::EnvelopeRename(*old_hash, *new_hash), context); } @@ -1314,7 +1381,7 @@ impl Component for PlainListing { self.row_updates.push(*env_hash); self.dirty = true; - if self.unfocused { + if self.unfocused() { self.view .process_event(&mut UIEvent::EnvelopeUpdate(*env_hash), context); } @@ -1326,7 +1393,7 @@ impl Component for PlainListing { self.dirty = true; } UIEvent::Input(Key::Esc) - if !self.unfocused + if !self.unfocused() && self.selection.values().cloned().any(std::convert::identity) => { for v in self.selection.values_mut() { @@ -1335,13 +1402,13 @@ impl Component for PlainListing { self.dirty = true; return true; } - UIEvent::Input(Key::Esc) if !self.unfocused && !self.filter_term.is_empty() => { + UIEvent::Input(Key::Esc) if !self.unfocused() && !self.filter_term.is_empty() => { self.set_coordinates((self.new_cursor_pos.0, self.new_cursor_pos.1)); self.set_dirty(true); self.refresh_mailbox(context, false); return true; } - UIEvent::Action(Action::Listing(Search(ref filter_term))) if !self.unfocused => { + UIEvent::Action(Action::Listing(Search(ref filter_term))) if !self.unfocused() => { match context.accounts[&self.cursor_pos.0].search( filter_term, self.sort, @@ -1389,23 +1456,23 @@ impl Component for PlainListing { } false } + fn is_dirty(&self) -> bool { - self.dirty - || if self.unfocused { - self.view.is_dirty() - } else { - false - } + match self.focus { + Focus::None => self.dirty, + Focus::Entry | Focus::EntryFullscreen => self.view.is_dirty(), + } } + fn set_dirty(&mut self, value: bool) { self.dirty = value; - if self.unfocused { + if self.unfocused() { self.view.set_dirty(value); } } fn get_shortcuts(&self, context: &Context) -> ShortcutMaps { - let mut map = if self.unfocused { + let mut map = if self.unfocused() { self.view.get_shortcuts(context) } else { ShortcutMaps::default() diff --git a/src/components/mail/listing/thread.rs b/src/components/mail/listing/thread.rs index 029a0408c..396e272ac 100644 --- a/src/components/mail/listing/thread.rs +++ b/src/components/mail/listing/thread.rs @@ -128,7 +128,7 @@ pub struct ThreadListing { /// If we must redraw on next redraw event dirty: bool, /// If `self.view` is focused or not. - unfocused: bool, + focus: Focus, initialised: bool, view: Option, movement: Option, @@ -410,9 +410,10 @@ impl ListingTrait for ThreadListing { fn coordinates(&self) -> (AccountHash, MailboxHash) { (self.new_cursor_pos.0, self.new_cursor_pos.1) } + fn set_coordinates(&mut self, coordinates: (AccountHash, MailboxHash)) { self.new_cursor_pos = (coordinates.0, coordinates.1, 0); - self.unfocused = false; + self.focus = Focus::None; self.view = None; self.order.clear(); self.row_updates.clear(); @@ -741,13 +742,55 @@ impl ListingTrait for ThreadListing { } fn unfocused(&self) -> bool { - self.unfocused + !matches!(self.focus, Focus::None) } fn set_movement(&mut self, mvm: PageMovement) { self.movement = Some(mvm); self.set_dirty(true); } + + fn set_focus(&mut self, new_value: Focus, context: &mut Context) { + match new_value { + Focus::None => { + self.view = None; + self.dirty = true; + /* If self.row_updates is not empty and we exit a thread, the row_update events + * will be performed but the list will not be drawn. So force a draw in any case. + * */ + // self.force_draw = true; + } + Focus::Entry => { + // self.force_draw = true; + self.dirty = true; + let coordinates = ( + self.cursor_pos.0, + self.cursor_pos.1, + self.get_env_under_cursor(self.cursor_pos.2, context), + ); + + if let Some(ref mut v) = self.view { + v.update(coordinates, context); + } else { + self.view = Some(MailView::new(coordinates, None, None, context)); + } + + if let Some(ref mut s) = self.view { + s.set_dirty(true); + } + } + Focus::EntryFullscreen => { + if let Some(ref mut s) = self.view { + s.set_dirty(true); + } + } + } + self.focus = new_value; + } + + fn focus(&self) -> Focus { + self.focus + } } impl fmt::Display for ThreadListing { @@ -772,7 +815,7 @@ impl ThreadListing { selection: HashMap::default(), order: HashMap::default(), dirty: true, - unfocused: false, + focus: Focus::None, view: None, initialised: false, movement: None, @@ -1092,10 +1135,17 @@ impl Component for ThreadListing { } } */ - if !self.unfocused { - if !self.is_dirty() { - return; + if !self.is_dirty() { + return; + } + + if matches!(self.focus, Focus::EntryFullscreen) { + if let Some(v) = self.view.as_mut() { + return v.draw(grid, area, context); } + } + + if !self.unfocused() { self.dirty = false; /* Draw the entire list */ self.draw_list(grid, area, context); @@ -1198,12 +1248,38 @@ impl Component for ThreadListing { self.dirty = false; } } + fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool { + let shortcuts = self.get_shortcuts(context); + + match (&event, self.focus) { + (UIEvent::Input(ref k), Focus::Entry) + if shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_right"]) => + { + self.set_focus(Focus::EntryFullscreen, context); + return true; + } + (UIEvent::Input(ref k), Focus::EntryFullscreen) + if shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_left"]) => + { + self.set_focus(Focus::Entry, context); + return true; + } + (UIEvent::Input(ref k), Focus::Entry) + if shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_left"]) => + { + self.set_focus(Focus::None, context); + return true; + } + _ => {} + } + if let Some(ref mut v) = self.view { - if v.process_event(event, context) { + if !matches!(self.focus, Focus::None) && v.process_event(event, context) { return true; } } + match *event { UIEvent::ConfigReload { old_settings: _ } => { self.color_cache = ColorCache { @@ -1238,18 +1314,43 @@ impl Component for ThreadListing { } self.set_dirty(true); } - UIEvent::Input(Key::Char('\n')) if !self.unfocused => { - self.unfocused = true; - self.dirty = true; + UIEvent::Input(ref k) + if matches!(self.focus, Focus::None) + && (shortcut!(k == shortcuts[Listing::DESCRIPTION]["open_entry"]) + || shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_right"])) => + { + self.set_focus(Focus::Entry, context); return true; } - UIEvent::Input(Key::Char('i')) if self.unfocused => { - self.unfocused = false; - if let Some(ref mut s) = self.view { - s.process_event(&mut UIEvent::VisibilityChange(false), context); + UIEvent::Input(ref k) + if !matches!(self.focus, Focus::None) + && shortcut!(k == shortcuts[Listing::DESCRIPTION]["exit_entry"]) => + { + self.set_focus(Focus::None, context); + return true; + } + UIEvent::Input(ref k) + if !matches!(self.focus, Focus::Entry) + && shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_right"]) => + { + self.set_focus(Focus::EntryFullscreen, context); + return true; + } + UIEvent::Input(ref k) + if !matches!(self.focus, Focus::None) + && shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_left"]) => + { + match self.focus { + Focus::Entry => { + self.set_focus(Focus::None, context); + } + Focus::EntryFullscreen => { + self.set_focus(Focus::Entry, context); + } + Focus::None => { + unreachable!(); + } } - self.dirty = true; - self.view = None; return true; } UIEvent::MailboxUpdate((ref idxa, ref idxf)) @@ -1275,7 +1376,7 @@ impl Component for ThreadListing { self.dirty = true; - if self.unfocused { + if self.unfocused() { if let Some(v) = self.view.as_mut() { v.process_event( &mut UIEvent::EnvelopeRename(*old_hash, *new_hash), @@ -1301,7 +1402,7 @@ impl Component for ThreadListing { self.dirty = true; - if self.unfocused { + if self.unfocused() { if let Some(v) = self.view.as_mut() { v.process_event(&mut UIEvent::EnvelopeUpdate(*env_hash), context); } @@ -1328,7 +1429,7 @@ impl Component for ThreadListing { self.refresh_mailbox(context, false); return true; } - Action::Listing(Search(ref filter_term)) if !self.unfocused => { + Action::Listing(Search(ref filter_term)) if !self.unfocused() => { match context.accounts[&self.cursor_pos.0].search( filter_term, self.sort, @@ -1379,20 +1480,36 @@ impl Component for ThreadListing { } false } + fn is_dirty(&self) -> bool { - self.dirty || self.view.as_ref().map(|p| p.is_dirty()).unwrap_or(false) + match self.focus { + Focus::None => self.dirty, + Focus::Entry => self.dirty || self.view.as_ref().map(|p| p.is_dirty()).unwrap_or(false), + Focus::EntryFullscreen => self.view.as_ref().map(|p| p.is_dirty()).unwrap_or(false), + } } + fn set_dirty(&mut self, value: bool) { if let Some(p) = self.view.as_mut() { p.set_dirty(value); }; self.dirty = value; } + fn get_shortcuts(&self, context: &Context) -> ShortcutMaps { - self.view - .as_ref() - .map(|p| p.get_shortcuts(context)) - .unwrap_or_default() + let mut map = if self.unfocused() { + self.view + .as_ref() + .map(|p| p.get_shortcuts(context)) + .unwrap_or_default() + } else { + ShortcutMaps::default() + }; + + let config_map = context.settings.shortcuts.listing.key_values(); + map.insert(Listing::DESCRIPTION, config_map); + + map } fn id(&self) -> ComponentId { diff --git a/src/conf/shortcuts.rs b/src/conf/shortcuts.rs index 63e634201..45e7cbcf5 100644 --- a/src/conf/shortcuts.rs +++ b/src/conf/shortcuts.rs @@ -160,8 +160,8 @@ shortcut_key_values! { "listing", increase_sidebar |> "Increase sidebar width." |> Key::Ctrl('p'), decrease_sidebar |> "Decrease sidebar width." |> Key::Ctrl('o'), toggle_menu_visibility |> "Toggle visibility of side menu in mail list." |> Key::Char('`'), - focus_on_menu |> "Switch focus on sidebar menu." |> Key::Left, - focus_on_list |> "Switch focus on mail list." |> Key::Right, + focus_left |> "Switch focus on the left." |> Key::Left, + focus_right |> "Switch focus on the right." |> Key::Right, exit_entry |> "Exit e-mail entry." |> Key::Char('i'), open_entry |> "Open e-mail entry." |> Key::Char('\n') }