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.embed
parent
dc525b9ddd
commit
5beed91df2
|
@ -81,6 +81,9 @@ pub struct Card {
|
|||
color: u8,
|
||||
last_edited: DateTime<Local>,
|
||||
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 {
|
||||
|
@ -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<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 {
|
||||
fn from(mut map: FnvHashMap<String, String>) -> 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;
|
||||
|
|
|
@ -65,7 +65,7 @@ pub struct ContentLine {
|
|||
impl CardDeserializer {
|
||||
pub fn from_str(mut input: &str) -> Result<VCard<impl VCardVersion>> {
|
||||
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()]
|
||||
};
|
||||
|
|
|
@ -28,21 +28,24 @@ pub use self::contact_list::*;
|
|||
|
||||
#[derive(Debug)]
|
||||
enum ViewMode {
|
||||
//ReadOnly,
|
||||
Read,
|
||||
//Edit,
|
||||
ReadOnly,
|
||||
Discard(Selector<char>),
|
||||
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,21 +142,71 @@ 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 {
|
||||
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 => {}
|
||||
|
@ -153,48 +231,77 @@ impl Component for ContactManager {
|
|||
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::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;
|
||||
}
|
||||
/*
|
||||
match *event {
|
||||
UIEvent::Input(Key::Char('\n')) => {
|
||||
context.replies.push_back(UIEvent {
|
||||
id: 0,
|
||||
event_type: UIEvent::ComponentKill(self.id),
|
||||
});
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<CardId>,
|
||||
|
||||
mode: ViewMode,
|
||||
dirty: bool,
|
||||
view: Option<Box<dyn Component>>,
|
||||
movement: Option<PageMovement>,
|
||||
view: Option<ContactManager>,
|
||||
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,
|
||||
Color::Black,
|
||||
Color::White,
|
||||
Attr::Default,
|
||||
((maxima.1, 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() {
|
||||
self.id_positions.push(*c.id());
|
||||
|
||||
write_string_to_grid(
|
||||
"URL",
|
||||
&mut self.data_columns.columns[2],
|
||||
Color::Black,
|
||||
Color::White,
|
||||
Attr::Bold,
|
||||
((0, 0), (MAX_COLS - 1, self.length)),
|
||||
false,
|
||||
);
|
||||
|
||||
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());
|
||||
|
||||
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 {
|
||||
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);
|
||||
if !self.dirty {
|
||||
return;
|
||||
}
|
||||
self.dirty = false;
|
||||
if !self.initialized {
|
||||
self.initialize(context);
|
||||
}
|
||||
|
||||
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.new_cursor_pos + 1)),
|
||||
set_y(bottom_right, get_y(upper_left) + self.new_cursor_pos + 1),
|
||||
pos_inc(upper_left, (0, self.length - top_idx + 2)),
|
||||
bottom_right,
|
||||
),
|
||||
fg_color,
|
||||
bg_color,
|
||||
);
|
||||
self.cursor_pos = self.new_cursor_pos;
|
||||
}
|
||||
self.highlight_line(
|
||||
grid,
|
||||
(
|
||||
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,
|
||||
),
|
||||
),
|
||||
self.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,12 +476,9 @@ 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()
|
||||
config_map
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k, v.clone()))
|
||||
.collect(),
|
||||
);
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<EnvelopeHash>);
|
||||
fn set_coordinates(&mut self, _: (usize, usize, Option<EnvelopeHash>));
|
||||
fn draw_list(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context);
|
||||
|
|
|
@ -238,6 +238,7 @@ impl FormWidget {
|
|||
focus: FormFocus::Fields,
|
||||
hide_buttons: false,
|
||||
id: ComponentId::new_v4(),
|
||||
dirty: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
@ -310,6 +311,15 @@ impl Component for FormWidget {
|
|||
let upper_left = upper_left!(area);
|
||||
let bottom_right = bottom_right!(area);
|
||||
|
||||
if self.dirty {
|
||||
clear_area(
|
||||
grid,
|
||||
(
|
||||
upper_left,
|
||||
set_y(bottom_right, get_y(upper_left) + self.layout.len()),
|
||||
),
|
||||
);
|
||||
|
||||
for (i, k) in self.layout.iter().enumerate() {
|
||||
let v = self.fields.get_mut(k).unwrap();
|
||||
/* Write field label */
|
||||
|
@ -318,7 +328,7 @@ impl Component for FormWidget {
|
|||
grid,
|
||||
Color::Default,
|
||||
Color::Default,
|
||||
Attr::Default,
|
||||
Attr::Bold,
|
||||
(
|
||||
pos_inc(upper_left, (1, i)),
|
||||
set_y(bottom_right, i + get_y(upper_left)),
|
||||
|
@ -367,18 +377,34 @@ impl Component for FormWidget {
|
|||
}
|
||||
}
|
||||
}
|
||||
if !self.hide_buttons {
|
||||
|
||||
let length = self.layout.len();
|
||||
clear_area(
|
||||
grid,
|
||||
(
|
||||
pos_inc(upper_left, (0, length)),
|
||||
set_y(bottom_right, length + 2 + get_y(upper_left)),
|
||||
),
|
||||
);
|
||||
if !self.hide_buttons {
|
||||
self.buttons.draw(
|
||||
grid,
|
||||
(
|
||||
pos_inc(upper_left, (1, length * 2 + 3)),
|
||||
set_y(bottom_right, length * 2 + 3 + get_y(upper_left)),
|
||||
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;
|
||||
}
|
||||
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<T>,
|
||||
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<T> Component for ButtonWidget<T>
|
||||
|
@ -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)),
|
||||
|
|
Loading…
Reference in New Issue