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.
This commit is contained in:
Manos Pitsidianakis 2019-10-20 11:14:29 +03:00
parent dc525b9ddd
commit 5beed91df2
No known key found for this signature in database
GPG Key ID: 73627C2F690DF710
6 changed files with 564 additions and 219 deletions

View File

@ -81,6 +81,9 @@ pub struct Card {
color: u8, color: u8,
last_edited: DateTime<Local>, last_edited: DateTime<Local>,
extra_properties: FnvHashMap<String, String>, extra_properties: FnvHashMap<String, String>,
/// If true, we can't make any changes because we do not manage this resource.
external_resource: bool,
} }
impl AddressBook { impl AddressBook {
@ -134,6 +137,7 @@ impl Card {
key: String::new(), key: String::new(),
last_edited: Local::now(), last_edited: Local::now(),
external_resource: false,
extra_properties: FnvHashMap::default(), extra_properties: FnvHashMap::default(),
color: 0, color: 0,
} }
@ -202,37 +206,50 @@ impl Card {
pub fn set_extra_property(&mut self, key: &str, value: String) { pub fn set_extra_property(&mut self, key: &str, value: String) {
self.extra_properties.insert(key.to_string(), value); self.extra_properties.insert(key.to_string(), value);
} }
pub fn extra_property(&self, key: &str) -> Option<&str> { pub fn extra_property(&self, key: &str) -> Option<&str> {
self.extra_properties.get(key).map(String::as_str) self.extra_properties.get(key).map(String::as_str)
} }
pub fn extra_properties(&self) -> &FnvHashMap<String, String> {
&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<FnvHashMap<String, String>> for Card { impl From<FnvHashMap<String, String>> for Card {
fn from(mut map: FnvHashMap<String, String>) -> Card { fn from(mut map: FnvHashMap<String, String>) -> Card {
let mut card = Card::new(); let mut card = Card::new();
if let Some(val) = map.remove("Title") { if let Some(val) = map.remove("TITLE") {
card.title = val; card.title = val;
} }
if let Some(val) = map.remove("Name") { if let Some(val) = map.remove("NAME") {
card.name = val; card.name = val;
} }
if let Some(val) = map.remove("Additional Name") { if let Some(val) = map.remove("ADDITIONAL NAME") {
card.additionalname = val; card.additionalname = val;
} }
if let Some(val) = map.remove("Name Prefix") { if let Some(val) = map.remove("NAME PREFIX") {
card.name_prefix = val; card.name_prefix = val;
} }
if let Some(val) = map.remove("Name Suffix") { if let Some(val) = map.remove("NAME SUFFIX") {
card.name_suffix = val; card.name_suffix = val;
} }
if let Some(val) = map.remove("E-mail") { if let Some(val) = map.remove("E-MAIL") {
card.email = val; card.email = val;
} }
if let Some(val) = map.remove("url") { if let Some(val) = map.remove("URL") {
card.url = val; card.url = val;
} }
if let Some(val) = map.remove("key") { if let Some(val) = map.remove("KEY") {
card.key = val; card.key = val;
} }
card.extra_properties = map; card.extra_properties = map;

View File

@ -65,7 +65,7 @@ pub struct ContentLine {
impl CardDeserializer { impl CardDeserializer {
pub fn from_str(mut input: &str) -> Result<VCard<impl VCardVersion>> { pub fn from_str(mut input: &str) -> Result<VCard<impl VCardVersion>> {
input = if !input.starts_with(HEADER) || !input.ends_with(FOOTER) { 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 { } else {
&input[HEADER.len()..input.len() - FOOTER.len()] &input[HEADER.len()..input.len() - FOOTER.len()]
}; };

View File

@ -28,21 +28,24 @@ pub use self::contact_list::*;
#[derive(Debug)] #[derive(Debug)]
enum ViewMode { enum ViewMode {
//ReadOnly, ReadOnly,
Read, Discard(Selector<char>),
//Edit, Edit,
//New, //New,
} }
#[derive(Debug)] #[derive(Debug)]
pub struct ContactManager { pub struct ContactManager {
id: ComponentId, id: ComponentId,
parent_id: ComponentId,
pub card: Card, pub card: Card,
mode: ViewMode, mode: ViewMode,
form: FormWidget, form: FormWidget,
account_pos: usize, account_pos: usize,
content: CellBuffer, content: CellBuffer,
dirty: bool, dirty: bool,
has_changes: bool,
initialized: bool, initialized: bool,
} }
@ -50,12 +53,14 @@ impl Default for ContactManager {
fn default() -> Self { fn default() -> Self {
ContactManager { ContactManager {
id: Uuid::nil(), id: Uuid::nil(),
parent_id: Uuid::nil(),
card: Card::new(), card: Card::new(),
mode: ViewMode::Read, mode: ViewMode::Edit,
form: FormWidget::default(), form: FormWidget::default(),
account_pos: 0, account_pos: 0,
content: CellBuffer::new(200, 100, Cell::with_char(' ')), content: CellBuffer::new(100, 1, Cell::with_char(' ')),
dirty: true, dirty: true,
has_changes: false,
initialized: false, initialized: false,
} }
} }
@ -77,20 +82,36 @@ impl ContactManager {
Color::Byte(250), Color::Byte(250),
Color::Default, Color::Default,
Attr::Default, Attr::Default,
((0, 0), (width, 0)), ((0, 0), (width - 1, 0)),
false, false,
); );
write_string_to_grid( let (x, y) = write_string_to_grid(
&self.card.last_edited(), &self.card.last_edited(),
&mut self.content, &mut self.content,
Color::Byte(250), Color::Byte(250),
Color::Default, Color::Default,
Attr::Default, Attr::Default,
((x, 0), (width, 0)), ((x, 0), (width - 1, 0)),
false, 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 = FormWidget::new("Save".into());
self.form.add_button(("Cancel".into(), false)); self.form.add_button(("Cancel(Esc)".into(), false));
self.form self.form
.push(("NAME".into(), self.card.name().to_string())); .push(("NAME".into(), self.card.name().to_string()));
self.form.push(( self.form.push((
@ -105,6 +126,13 @@ impl ContactManager {
.push(("E-MAIL".into(), self.card.email().to_string())); .push(("E-MAIL".into(), self.card.email().to_string()));
self.form.push(("URL".into(), self.card.url().to_string())); self.form.push(("URL".into(), self.card.url().to_string()));
self.form.push(("KEY".into(), self.card.key().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.initialize();
self.initialized = true; 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 upper_left = upper_left!(area);
let bottom_right = bottom_right!(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( self.form.draw(
grid, grid,
(set_y(upper_left, get_y(upper_left) + 1), bottom_right), (set_y(upper_left, get_y(upper_left) + 2), bottom_right),
context, 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); context.dirty_areas.push_back(area);
} }
fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool { fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
if self.form.process_event(event, context) { match self.mode {
match self.form.buttons_result() { ViewMode::Discard(ref mut selector) => {
None => {} if selector.process_event(event, context) {
Some(true) => { if selector.is_done() {
let fields = std::mem::replace(&mut self.form, FormWidget::default()) let s = match std::mem::replace(&mut self.mode, ViewMode::Edit) {
.collect() ViewMode::Discard(s) => s,
.unwrap(); _ => unreachable!(),
let fields: FnvHashMap<String, String> = fields };
.into_iter() let key = s.collect()[0] as char;
.map(|(s, v)| { match key {
( 'x' => {
s, context
match v { .replies
Field::Text(v, _) => v.as_str().to_string(), .push_back(UIEvent::Action(Tab(Kill(self.parent_id))));
Field::Choice(mut v, c) => v.remove(c), return true;
}, }
) 'n' => {}
}) 'y' => {}
.collect(); _ => {}
let mut new_card = Card::from(fields); }
new_card.set_id(*self.card.id()); }
context.accounts[self.account_pos] self.set_dirty();
.address_book return true;
.add_card(new_card); }
context }
.replies ViewMode::Edit => {
.push_back(UIEvent::StatusEvent(StatusEvent::DisplayMessage( if let &mut UIEvent::Input(Key::Esc) = event {
"Saved.".into(), if self.can_quit_cleanly(context) {
))); context
context.replies.push_back(UIEvent::ComponentKill(self.id)); .replies
} .push_back(UIEvent::Action(Tab(Kill(self.parent_id))));
Some(false) => { }
context.replies.push_back(UIEvent::ComponentKill(self.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<String, String> = 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 false
} }
fn is_dirty(&self) -> bool { 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) { fn set_dirty(&mut self) {
self.dirty = true; self.dirty = true;
self.initialized = false;
self.form.set_dirty(); self.form.set_dirty();
if let ViewMode::Discard(ref mut selector) = self.mode {
selector.set_dirty();
}
} }
fn id(&self) -> ComponentId { fn id(&self) -> ComponentId {
self.id self.id
} }
fn set_id(&mut self, id: ComponentId) { fn set_id(&mut self, id: ComponentId) {
self.id = id; 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
}
} }

View File

@ -1,6 +1,7 @@
use super::*; use super::*;
use melib::CardId; use melib::CardId;
use std::cmp;
const MAX_COLS: usize = 500; const MAX_COLS: usize = 500;
@ -16,13 +17,15 @@ pub struct ContactList {
new_cursor_pos: usize, new_cursor_pos: usize,
account_pos: usize, account_pos: usize,
length: usize, length: usize,
content: CellBuffer, data_columns: DataColumns,
initialized: bool,
id_positions: Vec<CardId>, id_positions: Vec<CardId>,
mode: ViewMode, mode: ViewMode,
dirty: bool, dirty: bool,
view: Option<Box<dyn Component>>, movement: Option<PageMovement>,
view: Option<ContactManager>,
id: ComponentId, id: ComponentId,
} }
@ -41,7 +44,6 @@ impl fmt::Display for ContactList {
impl ContactList { impl ContactList {
const DESCRIPTION: &'static str = "contact list"; const DESCRIPTION: &'static str = "contact list";
pub fn new() -> Self { pub fn new() -> Self {
let content = CellBuffer::new(0, 0, Cell::with_char(' '));
ContactList { ContactList {
cursor_pos: 0, cursor_pos: 0,
new_cursor_pos: 0, new_cursor_pos: 0,
@ -49,8 +51,10 @@ impl ContactList {
account_pos: 0, account_pos: 0,
id_positions: Vec::new(), id_positions: Vec::new(),
mode: ViewMode::List, mode: ViewMode::List,
content, data_columns: DataColumns::default(),
initialized: false,
dirty: true, dirty: true,
movement: None,
view: None, view: None,
id: ComponentId::new_v4(), id: ComponentId::new_v4(),
} }
@ -67,91 +71,141 @@ impl ContactList {
let account = &mut context.accounts[self.account_pos]; let account = &mut context.accounts[self.account_pos];
let book = &mut account.address_book; let book = &mut account.address_book;
self.length = book.len(); 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(); self.id_positions.clear();
if self.id_positions.capacity() < book.len() { if self.id_positions.capacity() < book.len() {
self.id_positions.reserve(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() { for c in book.values() {
self.id_positions.push(*c.id()); self.id_positions.push(*c.id());
maxima.0 = std::cmp::max(maxima.0, c.name().split_graphemes().len()); min_width.0 = cmp::max(min_width.0, c.name().split_graphemes().len()); /* name */
maxima.1 = std::cmp::max(maxima.1, c.email().split_graphemes().len()); 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( let (x, _) = write_string_to_grid(
"NAME", "NAME",
&mut self.content, &mut self.data_columns.columns[0],
Color::Black, Color::Black,
Color::White, Color::White,
Attr::Default, Attr::Bold,
((0, 0), (MAX_COLS - 1, self.length)), ((0, 0), (MAX_COLS - 1, self.length)),
false, false,
); );
for x in x..maxima.0 {
self.content[(x, 0)].set_bg(Color::White);
}
write_string_to_grid( write_string_to_grid(
"E-MAIL", "E-MAIL",
&mut self.content, &mut self.data_columns.columns[1],
Color::Black, Color::Black,
Color::White, Color::White,
Attr::Default, Attr::Bold,
((maxima.0, 0), (MAX_COLS - 1, self.length)), ((0, 0), (MAX_COLS - 1, self.length)),
false, false,
); );
for x in x..maxima.1 {
self.content[(x, 0)].set_bg(Color::White);
}
write_string_to_grid( write_string_to_grid(
"URL", "URL",
&mut self.content, &mut self.data_columns.columns[2],
Color::Black, Color::Black,
Color::White, Color::White,
Attr::Default, Attr::Bold,
((maxima.1, 0), (MAX_COLS - 1, self.length)), ((0, 0), (MAX_COLS - 1, self.length)),
false, false,
); );
for x in x..(MAX_COLS - 1) {
self.content[(x, 0)].set_bg(Color::White); let account = &mut context.accounts[self.account_pos];
} let book = &mut account.address_book;
for (i, c) in book.values().enumerate() { for (idx, c) in book.values().enumerate() {
self.id_positions.push(*c.id()); self.id_positions.push(*c.id());
write_string_to_grid( let (x, _) = write_string_to_grid(
c.name(), c.name(),
&mut self.content, &mut self.data_columns.columns[0],
Color::Default, Color::Default,
Color::Default, Color::Default,
Attr::Default, Attr::Default,
((0, i + 1), (MAX_COLS - 1, self.length)), ((0, idx + 1), (min_width.0, idx + 1)),
false, false,
); );
write_string_to_grid(
let (x, _) = write_string_to_grid(
c.email(), c.email(),
&mut self.content, &mut self.data_columns.columns[1],
Color::Default, Color::Default,
Color::Default, Color::Default,
Attr::Default, Attr::Default,
((maxima.0, i + 1), (MAX_COLS - 1, self.length)), ((0, idx + 1), (min_width.1, idx + 1)),
false, false,
); );
write_string_to_grid(
let (x, _) = write_string_to_grid(
c.url(), c.url(),
&mut self.content, &mut self.data_columns.columns[2],
Color::Default, Color::Default,
Color::Default, Color::Default,
Attr::Default, Attr::Default,
((maxima.1, i + 1), (MAX_COLS - 1, self.length)), ((0, idx + 1), (min_width.2, idx + 1)),
false, 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; return;
} }
if self.dirty { if !self.dirty {
return;
}
self.dirty = false;
if !self.initialized {
self.initialize(context); 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 upper_left = upper_left!(area);
let bottom_right = bottom_right!(area); let bottom_right = bottom_right!(area);
/* Reset previously highlighted line */ if self.length == 0 {
let fg_color = Color::Default; clear_area(grid, area);
let bg_color = Color::Default; 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( change_colors(
grid, grid,
( (upper_left, set_y(bottom_right, get_y(upper_left))),
pos_inc(upper_left, (0, self.cursor_pos + 1)), Color::Black,
set_y(bottom_right, get_y(upper_left) + self.cursor_pos + 1), Color::White,
),
fg_color,
bg_color,
); );
/* Highlight current line */ if top_idx + rows + 1 > self.length {
let bg_color = Color::Byte(246); clear_area(
change_colors( grid,
(
pos_inc(upper_left, (0, self.length - top_idx + 2)),
bottom_right,
),
);
}
self.highlight_line(
grid, grid,
( (
pos_inc(upper_left, (0, 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.new_cursor_pos + 1), set_y(
bottom_right,
get_y(upper_left) + (self.cursor_pos % rows) + 1,
),
), ),
fg_color, self.cursor_pos,
bg_color,
); );
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 { 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]; let shortcuts = &self.get_shortcuts(context)[Self::DESCRIPTION];
match *event { 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(); let mut manager = ContactManager::default();
manager.set_parent_id(self.id);
manager.account_pos = self.account_pos; manager.account_pos = self.account_pos;
let component = Box::new(manager);
self.mode = ViewMode::View(component.id()); self.mode = ViewMode::View(manager.id());
self.view = Some(component); self.view = Some(manager);
return true; 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 account = &mut context.accounts[self.account_pos];
let book = &mut account.address_book; let book = &mut account.address_book;
let card = book[&self.id_positions[self.cursor_pos]].clone(); let card = book[&self.id_positions[self.cursor_pos]].clone();
let mut manager = ContactManager::default(); let mut manager = ContactManager::default();
manager.set_parent_id(self.id);
manager.card = card; manager.card = card;
manager.account_pos = self.account_pos; manager.account_pos = self.account_pos;
let component = Box::new(manager);
self.mode = ViewMode::View(component.id()); self.mode = ViewMode::View(manager.id());
self.view = Some(component); self.view = Some(manager);
return true; return true;
} }
UIEvent::Input(Key::Char('n')) => { UIEvent::Input(Key::Char('n')) if self.view.is_none() => {
let card = Card::new(); let card = Card::new();
let mut manager = ContactManager::default(); let mut manager = ContactManager::default();
manager.set_parent_id(self.id);
manager.card = card; manager.card = card;
manager.account_pos = self.account_pos; manager.account_pos = self.account_pos;
let component = Box::new(manager);
self.mode = ViewMode::View(component.id()); self.mode = ViewMode::View(manager.id());
self.view = Some(component); self.view = Some(manager);
return true; return true;
} }
@ -267,6 +440,12 @@ impl Component for ContactList {
self.set_dirty(); self.set_dirty();
return true; return true;
} }
UIEvent::ChangeMode(UIMode::Normal) => {
self.set_dirty();
}
UIEvent::Resize => {
self.set_dirty();
}
_ => {} _ => {}
} }
false false
@ -297,13 +476,10 @@ impl Component for ContactList {
let config_map = context.settings.shortcuts.contact_list.key_values(); let config_map = context.settings.shortcuts.contact_list.key_values();
map.insert( map.insert(
self.to_string(), self.to_string(),
[ config_map
("create_contact", (*config_map["create_contact"]).clone()), .into_iter()
("edit_contact", (*config_map["edit_contact"]).clone()), .map(|(k, v)| (k, v.clone()))
] .collect(),
.iter()
.cloned()
.collect(),
); );
map map
@ -312,7 +488,15 @@ impl Component for ContactList {
fn id(&self) -> ComponentId { fn id(&self) -> ComponentId {
self.id self.id
} }
fn set_id(&mut self, id: ComponentId) { fn set_id(&mut self, id: ComponentId) {
self.id = id; 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)
}
} }

View File

@ -34,9 +34,9 @@ mod plain;
pub use self::plain::*; pub use self::plain::*;
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
pub(in crate::listing) struct DataColumns { pub struct DataColumns {
columns: [CellBuffer; 12], pub columns: [CellBuffer; 12],
widths: [usize; 12], // widths of columns calculated in first draw and after size changes pub widths: [usize; 12], // widths of columns calculated in first draw and after size changes
} }
#[derive(Debug)] #[derive(Debug)]
@ -52,7 +52,7 @@ pub(in crate::listing) struct CachedSearchStrings {
body: String, body: String,
} }
trait ListingTrait { pub trait ListingTrait {
fn coordinates(&self) -> (usize, usize, Option<EnvelopeHash>); fn coordinates(&self) -> (usize, usize, Option<EnvelopeHash>);
fn set_coordinates(&mut self, _: (usize, usize, Option<EnvelopeHash>)); fn set_coordinates(&mut self, _: (usize, usize, Option<EnvelopeHash>));
fn draw_list(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context); fn draw_list(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context);

View File

@ -238,6 +238,7 @@ impl FormWidget {
focus: FormFocus::Fields, focus: FormFocus::Fields,
hide_buttons: false, hide_buttons: false,
id: ComponentId::new_v4(), id: ComponentId::new_v4(),
dirty: true,
..Default::default() ..Default::default()
} }
} }
@ -310,75 +311,100 @@ impl Component for FormWidget {
let upper_left = upper_left!(area); let upper_left = upper_left!(area);
let bottom_right = bottom_right!(area); let bottom_right = bottom_right!(area);
for (i, k) in self.layout.iter().enumerate() { if self.dirty {
let v = self.fields.get_mut(k).unwrap(); clear_area(
/* 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(
grid, grid,
( (
pos_inc(upper_left, (self.field_name_max_length + 3, i)), upper_left,
set_y(bottom_right, i + get_y(upper_left)), set_y(bottom_right, get_y(upper_left) + self.layout.len()),
), ),
context,
); );
/* Highlight if necessary */ for (i, k) in self.layout.iter().enumerate() {
if i == self.cursor { let v = self.fields.get_mut(k).unwrap();
if self.focus == FormFocus::Fields { /* Write field label */
change_colors( write_string_to_grid(
grid, k.as_str(),
( grid,
pos_inc(upper_left, (0, i)), Color::Default,
set_y(bottom_right, i + get_y(upper_left)), Color::Default,
), Attr::Bold,
Color::Default, (
Color::Byte(246), pos_inc(upper_left, (1, i)),
); set_y(bottom_right, i + get_y(upper_left)),
} ),
if self.focus == FormFocus::TextInput { false,
v.draw_cursor( );
grid, /* draw field */
( v.draw(
pos_inc(upper_left, (self.field_name_max_length + 3, i)), 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, pos_inc(upper_left, (0, i)),
i + get_y(upper_left), set_y(bottom_right, i + get_y(upper_left)),
), ),
), Color::Default,
( Color::Byte(246),
pos_inc(upper_left, (self.field_name_max_length + 3, i + 1)), );
bottom_right, }
), if self.focus == FormFocus::TextInput {
context, 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(); let length = self.layout.len();
self.buttons.draw( clear_area(
grid, grid,
( (
pos_inc(upper_left, (1, length * 2 + 3)), pos_inc(upper_left, (0, length)),
set_y(bottom_right, length * 2 + 3 + get_y(upper_left)), 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); context.dirty_areas.push_back(area);
} }
fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool { fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
@ -389,6 +415,7 @@ impl Component for FormWidget {
match *event { match *event {
UIEvent::Input(Key::Up) if self.focus == FormFocus::Buttons => { UIEvent::Input(Key::Up) if self.focus == FormFocus::Buttons => {
self.focus = FormFocus::Fields; self.focus = FormFocus::Fields;
self.buttons.set_focus(false);
} }
UIEvent::InsertInput(Key::Up) if self.focus == FormFocus::TextInput => { UIEvent::InsertInput(Key::Up) if self.focus == FormFocus::TextInput => {
let field = self.fields.get_mut(&self.layout[self.cursor]).unwrap(); 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 => { UIEvent::Input(Key::Down) if self.focus == FormFocus::Fields => {
self.focus = FormFocus::Buttons; self.focus = FormFocus::Buttons;
self.buttons.set_focus(true);
if self.hide_buttons { if self.hide_buttons {
self.set_dirty(); self.set_dirty();
return false; return false;
@ -453,10 +481,11 @@ impl Component for FormWidget {
true true
} }
fn is_dirty(&self) -> bool { fn is_dirty(&self) -> bool {
self.dirty self.dirty || self.buttons.is_dirty()
} }
fn set_dirty(&mut self) { fn set_dirty(&mut self) {
self.dirty = true; self.dirty = true;
self.buttons.set_dirty();
} }
fn id(&self) -> ComponentId { fn id(&self) -> ComponentId {
@ -477,6 +506,8 @@ where
result: Option<T>, result: Option<T>,
cursor: usize, cursor: usize,
/// Is the button widget focused, i.e do we need to draw the highlighting?
focus: bool,
dirty: bool, dirty: bool,
id: ComponentId, id: ComponentId,
} }
@ -500,6 +531,7 @@ where
buttons: vec![init_val].into_iter().collect(), buttons: vec![init_val].into_iter().collect(),
result: None, result: None,
cursor: 0, cursor: 0,
focus: false,
dirty: true, dirty: true,
id: ComponentId::new_v4(), id: ComponentId::new_v4(),
} }
@ -513,6 +545,10 @@ where
pub fn is_resolved(&self) -> bool { pub fn is_resolved(&self) -> bool {
self.result.is_some() self.result.is_some()
} }
pub fn set_focus(&mut self, new_val: bool) {
self.focus = new_val;
}
} }
impl<T> Component for ButtonWidget<T> impl<T> Component for ButtonWidget<T>
@ -521,6 +557,7 @@ where
{ {
fn draw(&mut self, grid: &mut CellBuffer, area: Area, _context: &mut Context) { fn draw(&mut self, grid: &mut CellBuffer, area: Area, _context: &mut Context) {
if self.dirty { if self.dirty {
clear_area(grid, area);
let upper_left = upper_left!(area); let upper_left = upper_left!(area);
let mut len = 0; let mut len = 0;
@ -530,12 +567,12 @@ where
k.as_str(), k.as_str(),
grid, grid,
Color::Default, Color::Default,
if i == self.cursor { if i == self.cursor && self.focus {
Color::Byte(246) Color::Byte(246)
} else { } else {
Color::Default Color::Default
}, },
Attr::Default, Attr::Bold,
( (
pos_inc(upper_left, (len, 0)), pos_inc(upper_left, (len, 0)),
pos_inc(upper_left, (cur_len + len, 0)), pos_inc(upper_left, (cur_len + len, 0)),