From 5beed91df2b45fc506fd24494e7e766bf536e4fc Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Sun, 20 Oct 2019 11:14:29 +0300 Subject: [PATCH] contacts: add support for externally managed contacts Adds support for contacts (Cards) marked as `external_resource` which prevents modifications from happening. No way to import external contacts is added yet. --- melib/src/addressbook.rs | 33 +- melib/src/addressbook/vcard.rs | 2 +- ui/src/components/contacts.rs | 229 +++++++++---- ui/src/components/contacts/contact_list.rs | 360 ++++++++++++++++----- ui/src/components/mail/listing.rs | 8 +- ui/src/components/utilities/widgets.rs | 151 +++++---- 6 files changed, 564 insertions(+), 219 deletions(-) diff --git a/melib/src/addressbook.rs b/melib/src/addressbook.rs index 79ed117d..481114a9 100644 --- a/melib/src/addressbook.rs +++ b/melib/src/addressbook.rs @@ -81,6 +81,9 @@ pub struct Card { color: u8, last_edited: DateTime, extra_properties: FnvHashMap, + + /// If true, we can't make any changes because we do not manage this resource. + external_resource: bool, } impl AddressBook { @@ -134,6 +137,7 @@ impl Card { key: String::new(), last_edited: Local::now(), + external_resource: false, extra_properties: FnvHashMap::default(), color: 0, } @@ -202,37 +206,50 @@ impl Card { pub fn set_extra_property(&mut self, key: &str, value: String) { self.extra_properties.insert(key.to_string(), value); } + pub fn extra_property(&self, key: &str) -> Option<&str> { self.extra_properties.get(key).map(String::as_str) } + + pub fn extra_properties(&self) -> &FnvHashMap { + &self.extra_properties + } + + pub fn set_external_resource(&mut self, new_val: bool) { + self.external_resource = new_val; + } + + pub fn external_resource(&self) -> bool { + self.external_resource + } } impl From> for Card { fn from(mut map: FnvHashMap) -> Card { let mut card = Card::new(); - if let Some(val) = map.remove("Title") { + if let Some(val) = map.remove("TITLE") { card.title = val; } - if let Some(val) = map.remove("Name") { + if let Some(val) = map.remove("NAME") { card.name = val; } - if let Some(val) = map.remove("Additional Name") { + if let Some(val) = map.remove("ADDITIONAL NAME") { card.additionalname = val; } - if let Some(val) = map.remove("Name Prefix") { + if let Some(val) = map.remove("NAME PREFIX") { card.name_prefix = val; } - if let Some(val) = map.remove("Name Suffix") { + if let Some(val) = map.remove("NAME SUFFIX") { card.name_suffix = val; } - if let Some(val) = map.remove("E-mail") { + if let Some(val) = map.remove("E-MAIL") { card.email = val; } - if let Some(val) = map.remove("url") { + if let Some(val) = map.remove("URL") { card.url = val; } - if let Some(val) = map.remove("key") { + if let Some(val) = map.remove("KEY") { card.key = val; } card.extra_properties = map; diff --git a/melib/src/addressbook/vcard.rs b/melib/src/addressbook/vcard.rs index 6114cc4e..4447f93b 100644 --- a/melib/src/addressbook/vcard.rs +++ b/melib/src/addressbook/vcard.rs @@ -65,7 +65,7 @@ pub struct ContentLine { impl CardDeserializer { pub fn from_str(mut input: &str) -> Result> { input = if !input.starts_with(HEADER) || !input.ends_with(FOOTER) { - return Err(MeliError::new(format!("Error while parsing vcard: input does not start or end with correct header and footer. input is:\n{}", input))); + return Err(MeliError::new(format!("Error while parsing vcard: input does not start or end with correct header and footer. input is:\n{:?}", input))); } else { &input[HEADER.len()..input.len() - FOOTER.len()] }; diff --git a/ui/src/components/contacts.rs b/ui/src/components/contacts.rs index 20c80768..cd1fb8fb 100644 --- a/ui/src/components/contacts.rs +++ b/ui/src/components/contacts.rs @@ -28,21 +28,24 @@ pub use self::contact_list::*; #[derive(Debug)] enum ViewMode { - //ReadOnly, - Read, - //Edit, + ReadOnly, + Discard(Selector), + Edit, //New, } #[derive(Debug)] pub struct ContactManager { id: ComponentId, + parent_id: ComponentId, pub card: Card, mode: ViewMode, form: FormWidget, account_pos: usize, content: CellBuffer, dirty: bool, + has_changes: bool, + initialized: bool, } @@ -50,12 +53,14 @@ impl Default for ContactManager { fn default() -> Self { ContactManager { id: Uuid::nil(), + parent_id: Uuid::nil(), card: Card::new(), - mode: ViewMode::Read, + mode: ViewMode::Edit, form: FormWidget::default(), account_pos: 0, - content: CellBuffer::new(200, 100, Cell::with_char(' ')), + content: CellBuffer::new(100, 1, Cell::with_char(' ')), dirty: true, + has_changes: false, initialized: false, } } @@ -77,20 +82,36 @@ impl ContactManager { Color::Byte(250), Color::Default, Attr::Default, - ((0, 0), (width, 0)), + ((0, 0), (width - 1, 0)), false, ); - write_string_to_grid( + let (x, y) = write_string_to_grid( &self.card.last_edited(), &mut self.content, Color::Byte(250), Color::Default, Attr::Default, - ((x, 0), (width, 0)), + ((x, 0), (width - 1, 0)), false, ); + + if self.card.external_resource() { + self.mode = ViewMode::ReadOnly; + self.content + .resize(self.content.size().0, 2, Cell::default()); + let (x, y) = write_string_to_grid( + "This contact's origin is external and cannot be edited within meli.", + &mut self.content, + Color::Byte(250), + Color::Default, + Attr::Default, + ((x, y), (width - 1, y)), + false, + ); + } + self.form = FormWidget::new("Save".into()); - self.form.add_button(("Cancel".into(), false)); + self.form.add_button(("Cancel(Esc)".into(), false)); self.form .push(("NAME".into(), self.card.name().to_string())); self.form.push(( @@ -105,6 +126,13 @@ impl ContactManager { .push(("E-MAIL".into(), self.card.email().to_string())); self.form.push(("URL".into(), self.card.url().to_string())); self.form.push(("KEY".into(), self.card.key().to_string())); + for (k, v) in self.card.extra_properties() { + self.form.push((k.into(), v.to_string())); + } + } + + pub fn set_parent_id(&mut self, new_val: ComponentId) { + self.parent_id = new_val; } } @@ -114,87 +142,166 @@ impl Component for ContactManager { self.initialize(); self.initialized = true; } - clear_area(grid, area); - let (width, _height) = self.content.size(); - copy_area(grid, &self.content, area, ((0, 0), (width - 1, 0))); let upper_left = upper_left!(area); let bottom_right = bottom_right!(area); + + if self.dirty { + let (width, _height) = self.content.size(); + clear_area( + grid, + (upper_left, set_y(bottom_right, get_y(upper_left) + 1)), + ); + copy_area_with_break(grid, &self.content, area, ((0, 0), (width - 1, 0))); + self.dirty = false; + } + self.form.draw( grid, - (set_y(upper_left, get_y(upper_left) + 1), bottom_right), + (set_y(upper_left, get_y(upper_left) + 2), bottom_right), context, ); + match self.mode { + ViewMode::Discard(ref mut selector) => { + /* Let user choose whether to quit with/without saving or cancel */ + selector.draw(grid, center_area(area, selector.content.size()), context); + } + _ => {} + } + context.dirty_areas.push_back(area); } fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool { - if self.form.process_event(event, context) { - match self.form.buttons_result() { - None => {} - Some(true) => { - let fields = std::mem::replace(&mut self.form, FormWidget::default()) - .collect() - .unwrap(); - let fields: FnvHashMap = fields - .into_iter() - .map(|(s, v)| { - ( - s, - match v { - Field::Text(v, _) => v.as_str().to_string(), - Field::Choice(mut v, c) => v.remove(c), - }, - ) - }) - .collect(); - let mut new_card = Card::from(fields); - new_card.set_id(*self.card.id()); - context.accounts[self.account_pos] - .address_book - .add_card(new_card); - context - .replies - .push_back(UIEvent::StatusEvent(StatusEvent::DisplayMessage( - "Saved.".into(), - ))); - context.replies.push_back(UIEvent::ComponentKill(self.id)); - } - Some(false) => { - context.replies.push_back(UIEvent::ComponentKill(self.id)); + match self.mode { + ViewMode::Discard(ref mut selector) => { + if selector.process_event(event, context) { + if selector.is_done() { + let s = match std::mem::replace(&mut self.mode, ViewMode::Edit) { + ViewMode::Discard(s) => s, + _ => unreachable!(), + }; + let key = s.collect()[0] as char; + match key { + 'x' => { + context + .replies + .push_back(UIEvent::Action(Tab(Kill(self.parent_id)))); + return true; + } + 'n' => {} + 'y' => {} + _ => {} + } + } + self.set_dirty(); + return true; + } + } + ViewMode::Edit => { + if let &mut UIEvent::Input(Key::Esc) = event { + if self.can_quit_cleanly(context) { + context + .replies + .push_back(UIEvent::Action(Tab(Kill(self.parent_id)))); + } + return true; + } + if self.form.process_event(event, context) { + match self.form.buttons_result() { + None => {} + Some(true) => { + let fields = std::mem::replace(&mut self.form, FormWidget::default()) + .collect() + .unwrap(); + let fields: FnvHashMap = fields + .into_iter() + .map(|(s, v)| { + ( + s, + match v { + Field::Text(v, _) => v.as_str().to_string(), + Field::Choice(mut v, c) => v.remove(c), + }, + ) + }) + .collect(); + let mut new_card = Card::from(fields); + new_card.set_id(*self.card.id()); + context.accounts[self.account_pos] + .address_book + .add_card(new_card); + context.replies.push_back(UIEvent::StatusEvent( + StatusEvent::DisplayMessage("Saved.".into()), + )); + context.replies.push_back(UIEvent::ComponentKill(self.id)); + } + Some(false) => { + context.replies.push_back(UIEvent::ComponentKill(self.id)); + } + } + self.set_dirty(); + if let UIEvent::InsertInput(_) = event { + self.has_changes = true; + } + return true; + } + } + ViewMode::ReadOnly => { + if let &mut UIEvent::Input(Key::Esc) = event { + if self.can_quit_cleanly(context) { + context.replies.push_back(UIEvent::ComponentKill(self.id)); + } + return true; } } - return true; } - /* - match *event { - UIEvent::Input(Key::Char('\n')) => { - context.replies.push_back(UIEvent { - id: 0, - event_type: UIEvent::ComponentKill(self.id), - }); - return true; - }, - _ => {}, - } - */ false } fn is_dirty(&self) -> bool { - self.dirty | self.form.is_dirty() + self.dirty + || self.form.is_dirty() + || if let ViewMode::Discard(ref selector) = self.mode { + selector.is_dirty() + } else { + false + } } fn set_dirty(&mut self) { self.dirty = true; - self.initialized = false; self.form.set_dirty(); + if let ViewMode::Discard(ref mut selector) = self.mode { + selector.set_dirty(); + } } fn id(&self) -> ComponentId { self.id } + fn set_id(&mut self, id: ComponentId) { self.id = id; } + + fn can_quit_cleanly(&mut self, context: &Context) -> bool { + if !self.has_changes { + return true; + } + + /* Play it safe and ask user for confirmation */ + self.mode = ViewMode::Discard(Selector::new( + "this contact has unsaved changes", + vec![ + ('x', "quit without saving".to_string()), + ('y', "save draft and quit".to_string()), + ('n', "cancel".to_string()), + ], + true, + context, + )); + self.set_dirty(); + false + } } diff --git a/ui/src/components/contacts/contact_list.rs b/ui/src/components/contacts/contact_list.rs index f973eabd..144a51cb 100644 --- a/ui/src/components/contacts/contact_list.rs +++ b/ui/src/components/contacts/contact_list.rs @@ -1,6 +1,7 @@ use super::*; use melib::CardId; +use std::cmp; const MAX_COLS: usize = 500; @@ -16,13 +17,15 @@ pub struct ContactList { new_cursor_pos: usize, account_pos: usize, length: usize, - content: CellBuffer, + data_columns: DataColumns, + initialized: bool, id_positions: Vec, mode: ViewMode, dirty: bool, - view: Option>, + movement: Option, + view: Option, id: ComponentId, } @@ -41,7 +44,6 @@ impl fmt::Display for ContactList { impl ContactList { const DESCRIPTION: &'static str = "contact list"; pub fn new() -> Self { - let content = CellBuffer::new(0, 0, Cell::with_char(' ')); ContactList { cursor_pos: 0, new_cursor_pos: 0, @@ -49,8 +51,10 @@ impl ContactList { account_pos: 0, id_positions: Vec::new(), mode: ViewMode::List, - content, + data_columns: DataColumns::default(), + initialized: false, dirty: true, + movement: None, view: None, id: ComponentId::new_v4(), } @@ -67,91 +71,141 @@ impl ContactList { let account = &mut context.accounts[self.account_pos]; let book = &mut account.address_book; self.length = book.len(); - self.content - .resize(MAX_COLS, book.len() + 1, Cell::with_char(' ')); - - clear_area(&mut self.content, ((0, 0), (MAX_COLS - 1, self.length))); self.id_positions.clear(); if self.id_positions.capacity() < book.len() { self.id_positions.reserve(book.len()); } - let mut maxima = ("Name".len(), "E-mail".len()); + self.dirty = true; + let mut min_width = ("Name".len(), "E-mail".len(), 0, 0, 0); for c in book.values() { self.id_positions.push(*c.id()); - maxima.0 = std::cmp::max(maxima.0, c.name().split_graphemes().len()); - maxima.1 = std::cmp::max(maxima.1, c.email().split_graphemes().len()); + min_width.0 = cmp::max(min_width.0, c.name().split_graphemes().len()); /* name */ + min_width.1 = cmp::max(min_width.1, c.email().split_graphemes().len()); /* email */ + min_width.2 = cmp::max(min_width.2, c.url().split_graphemes().len()); + /* url */ } - maxima.0 += 5; - maxima.1 += maxima.0 + 5; + + /* name column */ + self.data_columns.columns[0] = CellBuffer::new_with_context( + min_width.0, + self.length + 1, + Cell::with_char(' '), + context, + ); + /* email column */ + self.data_columns.columns[1] = CellBuffer::new_with_context( + min_width.1, + self.length + 1, + Cell::with_char(' '), + context, + ); + /* url column */ + self.data_columns.columns[2] = CellBuffer::new_with_context( + min_width.2, + self.length + 1, + Cell::with_char(' '), + context, + ); let (x, _) = write_string_to_grid( "NAME", - &mut self.content, + &mut self.data_columns.columns[0], Color::Black, Color::White, - Attr::Default, + Attr::Bold, ((0, 0), (MAX_COLS - 1, self.length)), false, ); - for x in x..maxima.0 { - self.content[(x, 0)].set_bg(Color::White); - } write_string_to_grid( "E-MAIL", - &mut self.content, + &mut self.data_columns.columns[1], Color::Black, Color::White, - Attr::Default, - ((maxima.0, 0), (MAX_COLS - 1, self.length)), + Attr::Bold, + ((0, 0), (MAX_COLS - 1, self.length)), false, ); - for x in x..maxima.1 { - self.content[(x, 0)].set_bg(Color::White); - } + write_string_to_grid( "URL", - &mut self.content, + &mut self.data_columns.columns[2], Color::Black, Color::White, - Attr::Default, - ((maxima.1, 0), (MAX_COLS - 1, self.length)), + Attr::Bold, + ((0, 0), (MAX_COLS - 1, self.length)), false, ); - for x in x..(MAX_COLS - 1) { - self.content[(x, 0)].set_bg(Color::White); - } - for (i, c) in book.values().enumerate() { + + let account = &mut context.accounts[self.account_pos]; + let book = &mut account.address_book; + for (idx, c) in book.values().enumerate() { self.id_positions.push(*c.id()); - write_string_to_grid( + let (x, _) = write_string_to_grid( c.name(), - &mut self.content, + &mut self.data_columns.columns[0], Color::Default, Color::Default, Attr::Default, - ((0, i + 1), (MAX_COLS - 1, self.length)), + ((0, idx + 1), (min_width.0, idx + 1)), false, ); - write_string_to_grid( + + let (x, _) = write_string_to_grid( c.email(), - &mut self.content, + &mut self.data_columns.columns[1], Color::Default, Color::Default, Attr::Default, - ((maxima.0, i + 1), (MAX_COLS - 1, self.length)), + ((0, idx + 1), (min_width.1, idx + 1)), false, ); - write_string_to_grid( + + let (x, _) = write_string_to_grid( c.url(), - &mut self.content, + &mut self.data_columns.columns[2], Color::Default, Color::Default, Attr::Default, - ((maxima.1, i + 1), (MAX_COLS - 1, self.length)), + ((0, idx + 1), (min_width.2, idx + 1)), false, ); } + + if self.length == 0 { + let message = "Address book is empty.".to_string(); + self.data_columns.columns[0] = CellBuffer::new_with_context( + message.len(), + self.length + 1, + Cell::with_char(' '), + context, + ); + write_string_to_grid( + &message, + &mut self.data_columns.columns[0], + Color::Default, + Color::Default, + Attr::Default, + ((0, 0), (MAX_COLS - 1, 0)), + false, + ); + return; + } + } + + fn highlight_line(&mut self, grid: &mut CellBuffer, area: Area, idx: usize) { + let upper_left = upper_left!(area); + let bottom_right = bottom_right!(area); + + /* Reset previously highlighted line */ + let fg_color = Color::Default; + let bg_color = if idx == self.new_cursor_pos { + Color::Byte(246) + } else { + Color::Default + }; + change_colors(grid, area, fg_color, bg_color); } } @@ -162,49 +216,163 @@ impl Component for ContactList { return; } - if self.dirty { + if !self.dirty { + return; + } + self.dirty = false; + if !self.initialized { self.initialize(context); - copy_area( - grid, - &self.content, - area, - ( - (0, 0), - (MAX_COLS - 1, self.content.size().1.saturating_sub(1)), - ), - ); - context.dirty_areas.push_back(area); - self.dirty = false; } let upper_left = upper_left!(area); let bottom_right = bottom_right!(area); - /* Reset previously highlighted line */ - let fg_color = Color::Default; - let bg_color = Color::Default; + if self.length == 0 { + clear_area(grid, area); + copy_area( + grid, + &self.data_columns.columns[0], + area, + ((0, 0), pos_dec(self.data_columns.columns[0].size(), (1, 1))), + ); + context.dirty_areas.push_back(area); + return; + } + let rows = get_y(bottom_right) - get_y(upper_left); + + if let Some(mvm) = self.movement.take() { + match mvm { + PageMovement::PageUp => { + self.new_cursor_pos = self.new_cursor_pos.saturating_sub(rows); + } + PageMovement::PageDown => { + if self.new_cursor_pos + rows + 1 < self.length { + self.new_cursor_pos += rows; + } else if self.new_cursor_pos + rows > self.length { + self.new_cursor_pos = self.length - 1; + } else { + self.new_cursor_pos = (self.length / rows) * rows; + } + } + PageMovement::Home => { + self.new_cursor_pos = 0; + } + PageMovement::End => { + if self.new_cursor_pos + rows > self.length { + self.new_cursor_pos = self.length - 1; + } else { + self.new_cursor_pos = (self.length / rows) * rows; + } + } + } + } + + let prev_page_no = (self.cursor_pos).wrapping_div(rows); + let page_no = (self.new_cursor_pos).wrapping_div(rows); + + let top_idx = page_no * rows; + + /* If cursor position has changed, remove the highlight from the previous position and + * apply it in the new one. */ + if self.cursor_pos != self.new_cursor_pos && prev_page_no == page_no { + let old_cursor_pos = self.cursor_pos; + self.cursor_pos = self.new_cursor_pos; + for idx in &[old_cursor_pos, self.new_cursor_pos] { + if *idx >= self.length { + continue; //bounds check + } + let new_area = ( + set_y(upper_left, get_y(upper_left) + (*idx % rows) + 1), + set_y(bottom_right, get_y(upper_left) + (*idx % rows) + 1), + ); + self.highlight_line(grid, new_area, *idx); + context.dirty_areas.push_back(new_area); + } + return; + } else if self.cursor_pos != self.new_cursor_pos { + self.cursor_pos = self.new_cursor_pos; + } + if self.new_cursor_pos >= self.length { + self.new_cursor_pos = self.length - 1; + self.cursor_pos = self.new_cursor_pos; + } + + let width = width!(area); + self.data_columns.widths = Default::default(); + self.data_columns.widths[0] = self.data_columns.columns[0].size().0; /* name */ + self.data_columns.widths[1] = self.data_columns.columns[1].size().0; /* email*/ + self.data_columns.widths[2] = self.data_columns.columns[2].size().0; /* url */ + + let min_col_width = std::cmp::min( + 15, + std::cmp::min(self.data_columns.widths[0], self.data_columns.widths[1]), + ); + if self.data_columns.widths[0] + self.data_columns.widths[1] + 3 * min_col_width + 8 > width + { + let remainder = + width.saturating_sub(self.data_columns.widths[0] + self.data_columns.widths[1] + 4); + self.data_columns.widths[2] = remainder / 6; + } + clear_area(grid, area); + /* Page_no has changed, so draw new page */ + let mut x = get_x(upper_left); + for i in 0..self.data_columns.columns.len() { + let (column_width, column_height) = self.data_columns.columns[i].size(); + if self.data_columns.widths[i] == 0 { + continue; + } + copy_area( + grid, + &self.data_columns.columns[i], + ( + set_x(upper_left, x), + set_x( + bottom_right, + std::cmp::min(get_x(bottom_right), x + (self.data_columns.widths[i])), + ), + ), + ( + (0, top_idx), + ( + column_width.saturating_sub(1), + column_height.saturating_sub(1), + ), + ), + ); + x += self.data_columns.widths[i] + 2; // + SEPARATOR + if x > get_x(bottom_right) { + break; + } + } + change_colors( grid, - ( - pos_inc(upper_left, (0, self.cursor_pos + 1)), - set_y(bottom_right, get_y(upper_left) + self.cursor_pos + 1), - ), - fg_color, - bg_color, + (upper_left, set_y(bottom_right, get_y(upper_left))), + Color::Black, + Color::White, ); - /* Highlight current line */ - let bg_color = Color::Byte(246); - change_colors( + if top_idx + rows + 1 > self.length { + clear_area( + grid, + ( + pos_inc(upper_left, (0, self.length - top_idx + 2)), + bottom_right, + ), + ); + } + self.highlight_line( grid, ( - pos_inc(upper_left, (0, self.new_cursor_pos + 1)), - set_y(bottom_right, get_y(upper_left) + self.new_cursor_pos + 1), + set_y(upper_left, get_y(upper_left) + (self.cursor_pos % rows) + 1), + set_y( + bottom_right, + get_y(upper_left) + (self.cursor_pos % rows) + 1, + ), ), - fg_color, - bg_color, + self.cursor_pos, ); - self.cursor_pos = self.new_cursor_pos; + context.dirty_areas.push_back(area); } fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool { @@ -215,39 +383,44 @@ impl Component for ContactList { } let shortcuts = &self.get_shortcuts(context)[Self::DESCRIPTION]; match *event { - UIEvent::Input(ref key) if *key == shortcuts["create_contact"] => { + UIEvent::Input(ref key) + if *key == shortcuts["create_contact"] && self.view.is_none() => + { let mut manager = ContactManager::default(); + manager.set_parent_id(self.id); manager.account_pos = self.account_pos; - let component = Box::new(manager); - self.mode = ViewMode::View(component.id()); - self.view = Some(component); + self.mode = ViewMode::View(manager.id()); + self.view = Some(manager); return true; } - UIEvent::Input(ref key) if *key == shortcuts["edit_contact"] && self.length > 0 => { + UIEvent::Input(ref key) + if *key == shortcuts["edit_contact"] && self.length > 0 && self.view.is_none() => + { let account = &mut context.accounts[self.account_pos]; let book = &mut account.address_book; let card = book[&self.id_positions[self.cursor_pos]].clone(); let mut manager = ContactManager::default(); + manager.set_parent_id(self.id); manager.card = card; manager.account_pos = self.account_pos; - let component = Box::new(manager); - self.mode = ViewMode::View(component.id()); - self.view = Some(component); + self.mode = ViewMode::View(manager.id()); + self.view = Some(manager); return true; } - UIEvent::Input(Key::Char('n')) => { + UIEvent::Input(Key::Char('n')) if self.view.is_none() => { let card = Card::new(); let mut manager = ContactManager::default(); + manager.set_parent_id(self.id); manager.card = card; manager.account_pos = self.account_pos; - let component = Box::new(manager); - self.mode = ViewMode::View(component.id()); - self.view = Some(component); + + self.mode = ViewMode::View(manager.id()); + self.view = Some(manager); return true; } @@ -267,6 +440,12 @@ impl Component for ContactList { self.set_dirty(); return true; } + UIEvent::ChangeMode(UIMode::Normal) => { + self.set_dirty(); + } + UIEvent::Resize => { + self.set_dirty(); + } _ => {} } false @@ -297,13 +476,10 @@ impl Component for ContactList { let config_map = context.settings.shortcuts.contact_list.key_values(); map.insert( self.to_string(), - [ - ("create_contact", (*config_map["create_contact"]).clone()), - ("edit_contact", (*config_map["edit_contact"]).clone()), - ] - .iter() - .cloned() - .collect(), + config_map + .into_iter() + .map(|(k, v)| (k, v.clone())) + .collect(), ); map @@ -312,7 +488,15 @@ impl Component for ContactList { fn id(&self) -> ComponentId { self.id } + fn set_id(&mut self, id: ComponentId) { self.id = id; } + + fn can_quit_cleanly(&mut self, context: &Context) -> bool { + self.view + .as_mut() + .map(|p| p.can_quit_cleanly(context)) + .unwrap_or(true) + } } diff --git a/ui/src/components/mail/listing.rs b/ui/src/components/mail/listing.rs index 7fa837c6..a2682146 100644 --- a/ui/src/components/mail/listing.rs +++ b/ui/src/components/mail/listing.rs @@ -34,9 +34,9 @@ mod plain; pub use self::plain::*; #[derive(Debug, Default, Clone)] -pub(in crate::listing) struct DataColumns { - columns: [CellBuffer; 12], - widths: [usize; 12], // widths of columns calculated in first draw and after size changes +pub struct DataColumns { + pub columns: [CellBuffer; 12], + pub widths: [usize; 12], // widths of columns calculated in first draw and after size changes } #[derive(Debug)] @@ -52,7 +52,7 @@ pub(in crate::listing) struct CachedSearchStrings { body: String, } -trait ListingTrait { +pub trait ListingTrait { fn coordinates(&self) -> (usize, usize, Option); fn set_coordinates(&mut self, _: (usize, usize, Option)); fn draw_list(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context); diff --git a/ui/src/components/utilities/widgets.rs b/ui/src/components/utilities/widgets.rs index 749e63fa..3acdb904 100644 --- a/ui/src/components/utilities/widgets.rs +++ b/ui/src/components/utilities/widgets.rs @@ -238,6 +238,7 @@ impl FormWidget { focus: FormFocus::Fields, hide_buttons: false, id: ComponentId::new_v4(), + dirty: true, ..Default::default() } } @@ -310,75 +311,100 @@ impl Component for FormWidget { let upper_left = upper_left!(area); let bottom_right = bottom_right!(area); - for (i, k) in self.layout.iter().enumerate() { - let v = self.fields.get_mut(k).unwrap(); - /* Write field label */ - write_string_to_grid( - k.as_str(), - grid, - Color::Default, - Color::Default, - Attr::Default, - ( - pos_inc(upper_left, (1, i)), - set_y(bottom_right, i + get_y(upper_left)), - ), - false, - ); - /* draw field */ - v.draw( + if self.dirty { + clear_area( grid, ( - pos_inc(upper_left, (self.field_name_max_length + 3, i)), - set_y(bottom_right, i + get_y(upper_left)), + upper_left, + set_y(bottom_right, get_y(upper_left) + self.layout.len()), ), - context, ); - /* Highlight if necessary */ - if i == self.cursor { - if self.focus == FormFocus::Fields { - change_colors( - grid, - ( - pos_inc(upper_left, (0, i)), - set_y(bottom_right, i + get_y(upper_left)), - ), - Color::Default, - Color::Byte(246), - ); - } - if self.focus == FormFocus::TextInput { - v.draw_cursor( - grid, - ( - pos_inc(upper_left, (self.field_name_max_length + 3, i)), + for (i, k) in self.layout.iter().enumerate() { + let v = self.fields.get_mut(k).unwrap(); + /* Write field label */ + write_string_to_grid( + k.as_str(), + grid, + Color::Default, + Color::Default, + Attr::Bold, + ( + pos_inc(upper_left, (1, i)), + set_y(bottom_right, i + get_y(upper_left)), + ), + false, + ); + /* draw field */ + v.draw( + grid, + ( + pos_inc(upper_left, (self.field_name_max_length + 3, i)), + set_y(bottom_right, i + get_y(upper_left)), + ), + context, + ); + + /* Highlight if necessary */ + if i == self.cursor { + if self.focus == FormFocus::Fields { + change_colors( + grid, ( - get_x(upper_left) + self.field_name_max_length + 3, - i + get_y(upper_left), + pos_inc(upper_left, (0, i)), + set_y(bottom_right, i + get_y(upper_left)), ), - ), - ( - pos_inc(upper_left, (self.field_name_max_length + 3, i + 1)), - bottom_right, - ), - context, - ); + Color::Default, + Color::Byte(246), + ); + } + if self.focus == FormFocus::TextInput { + v.draw_cursor( + grid, + ( + pos_inc(upper_left, (self.field_name_max_length + 3, i)), + ( + get_x(upper_left) + self.field_name_max_length + 3, + i + get_y(upper_left), + ), + ), + ( + pos_inc(upper_left, (self.field_name_max_length + 3, i + 1)), + bottom_right, + ), + context, + ); + } } } - } - if !self.hide_buttons { + let length = self.layout.len(); - self.buttons.draw( + clear_area( grid, ( - pos_inc(upper_left, (1, length * 2 + 3)), - set_y(bottom_right, length * 2 + 3 + get_y(upper_left)), + pos_inc(upper_left, (0, length)), + set_y(bottom_right, length + 2 + get_y(upper_left)), ), - context, ); + if !self.hide_buttons { + self.buttons.draw( + grid, + ( + pos_inc(upper_left, (1, length + 3)), + set_y(bottom_right, length + 3 + get_y(upper_left)), + ), + context, + ); + } + clear_area( + grid, + ( + set_y(upper_left, length + 4 + get_y(upper_left)), + bottom_right, + ), + ); + self.dirty = false; } - self.dirty = false; context.dirty_areas.push_back(area); } fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool { @@ -389,6 +415,7 @@ impl Component for FormWidget { match *event { UIEvent::Input(Key::Up) if self.focus == FormFocus::Buttons => { self.focus = FormFocus::Fields; + self.buttons.set_focus(false); } UIEvent::InsertInput(Key::Up) if self.focus == FormFocus::TextInput => { let field = self.fields.get_mut(&self.layout[self.cursor]).unwrap(); @@ -406,6 +433,7 @@ impl Component for FormWidget { } UIEvent::Input(Key::Down) if self.focus == FormFocus::Fields => { self.focus = FormFocus::Buttons; + self.buttons.set_focus(true); if self.hide_buttons { self.set_dirty(); return false; @@ -453,10 +481,11 @@ impl Component for FormWidget { true } fn is_dirty(&self) -> bool { - self.dirty + self.dirty || self.buttons.is_dirty() } fn set_dirty(&mut self) { self.dirty = true; + self.buttons.set_dirty(); } fn id(&self) -> ComponentId { @@ -477,6 +506,8 @@ where result: Option, cursor: usize, + /// Is the button widget focused, i.e do we need to draw the highlighting? + focus: bool, dirty: bool, id: ComponentId, } @@ -500,6 +531,7 @@ where buttons: vec![init_val].into_iter().collect(), result: None, cursor: 0, + focus: false, dirty: true, id: ComponentId::new_v4(), } @@ -513,6 +545,10 @@ where pub fn is_resolved(&self) -> bool { self.result.is_some() } + + pub fn set_focus(&mut self, new_val: bool) { + self.focus = new_val; + } } impl Component for ButtonWidget @@ -521,6 +557,7 @@ where { fn draw(&mut self, grid: &mut CellBuffer, area: Area, _context: &mut Context) { if self.dirty { + clear_area(grid, area); let upper_left = upper_left!(area); let mut len = 0; @@ -530,12 +567,12 @@ where k.as_str(), grid, Color::Default, - if i == self.cursor { + if i == self.cursor && self.focus { Color::Byte(246) } else { Color::Default }, - Attr::Default, + Attr::Bold, ( pos_inc(upper_left, (len, 0)), pos_inc(upper_left, (cur_len + len, 0)),