ui: revamp option dialog

Selector component shows choices/options to the user. Ok and Cancel
buttons were added, along with a window border and window title.
embed
Manos Pitsidianakis 2019-10-03 01:03:20 +03:00
parent fb8a4b020d
commit cd761b3166
Signed by: Manos Pitsidianakis
GPG Key ID: 73627C2F690DF710
4 changed files with 512 additions and 232 deletions

View File

@ -73,21 +73,13 @@ impl Default for Composer {
#[derive(Debug)]
enum ViewMode {
Discard(Uuid),
Discard(Uuid, Selector<char>),
Edit,
//Selector(Selector),
ThreadView,
}
impl ViewMode {
fn is_discard(&self) -> bool {
if let ViewMode::Discard(_) = self {
true
} else {
false
}
}
fn is_edit(&self) -> bool {
if let ViewMode::Edit = self {
true
@ -452,62 +444,11 @@ impl Component for Composer {
self.pager.set_dirty();
self.pager.draw(grid, body_area, context);
}
ViewMode::Discard(_) => {
ViewMode::Discard(_, ref mut s) => {
self.pager.set_dirty();
self.pager.draw(grid, body_area, context);
/* Let user choose whether to quit with/without saving or cancel */
let mid_x = { std::cmp::max(width!(area) / 2, width / 2) - width / 2 };
let mid_y = { std::cmp::max(height!(area) / 2, 11) - 11 };
let upper_left = upper_left!(body_area);
let bottom_right = bottom_right!(body_area);
let area = (
pos_inc(upper_left, (mid_x, mid_y)),
pos_dec(bottom_right, (mid_x, mid_y)),
);
create_box(grid, area);
let area = (
pos_inc(upper_left, (mid_x + 2, mid_y + 2)),
pos_dec(
bottom_right,
(mid_x.saturating_sub(2), mid_y.saturating_sub(2)),
),
);
let (_, y) = write_string_to_grid(
&format!("Draft \"{:10}\"", self.draft.headers()["Subject"]),
grid,
Color::Default,
Color::Default,
Attr::Default,
area,
true,
);
let (_, y) = write_string_to_grid(
"[x] quit without saving",
grid,
Color::Byte(124),
Color::Default,
Attr::Default,
(set_y(upper_left!(area), y + 2), bottom_right!(area)),
true,
);
let (_, y) = write_string_to_grid(
"[y] save draft and quit",
grid,
Color::Byte(124),
Color::Default,
Attr::Default,
(set_y(upper_left!(area), y + 1), bottom_right!(area)),
true,
);
write_string_to_grid(
"[n] cancel",
grid,
Color::Byte(124),
Color::Default,
Attr::Default,
(set_y(upper_left!(area), y + 1), bottom_right!(area)),
true,
);
s.draw(grid, center_area(area, s.content.size()), context);
}
}
@ -535,6 +476,95 @@ impl Component for Composer {
return true;
}
}
(ViewMode::Discard(_, ref mut selector), _, _) => {
if selector.process_event(event, context) {
if selector.is_done() {
let (u, s) = match std::mem::replace(&mut self.mode, ViewMode::ThreadView) {
ViewMode::Discard(u, s) => (u, s),
_ => unreachable!(),
};
let key = s.collect()[0] as char;
match key {
'x' => {
context.replies.push_back(UIEvent::Action(Tab(Kill(u))));
return true;
}
'n' => {}
'y' => {
let mut failure = true;
let draft = std::mem::replace(&mut self.draft, Draft::default());
let draft = draft.finalise().unwrap();
for folder in &[
&context.accounts[self.account_cursor]
.special_use_folder(SpecialUseMailbox::Drafts),
&context.accounts[self.account_cursor]
.special_use_folder(SpecialUseMailbox::Inbox),
&context.accounts[self.account_cursor]
.special_use_folder(SpecialUseMailbox::Normal),
] {
if folder.is_none() {
continue;
}
let folder = folder.unwrap();
if let Err(e) = context.accounts[self.account_cursor].save(
draft.as_bytes(),
folder,
Some(Flag::SEEN | Flag::DRAFT),
) {
debug!("{:?} could not save draft msg", e);
log(
format!(
"Could not save draft in '{}' folder: {}.",
folder,
e.to_string()
),
ERROR,
);
context.replies.push_back(UIEvent::Notification(
Some(format!(
"Could not save draft in '{}' folder.",
folder
)),
e.into(),
Some(NotificationType::ERROR),
));
} else {
failure = false;
break;
}
}
if failure {
let file =
create_temp_file(draft.as_bytes(), None, None, false);
debug!("message saved in {}", file.path.display());
log(
format!(
"Message was stored in {} so that you can restore it manually.",
file.path.display()
),
INFO,
);
context.replies.push_back(UIEvent::Notification(
Some("Could not save in any folder".into()),
format!(
"Message was stored in {} so that you can restore it manually.",
file.path.display()
),
Some(NotificationType::INFO),
));
}
context.replies.push_back(UIEvent::Action(Tab(Kill(u))));
return true;
}
_ => {}
}
self.set_dirty();
}
return true;
}
}
_ => {}
}
if self.form.process_event(event, context) {
@ -574,85 +604,6 @@ impl Component for Composer {
UIEvent::Input(Key::Down) => {
self.cursor = Cursor::Body;
}
UIEvent::Input(Key::Char(key)) if self.mode.is_discard() => {
match (key, &self.mode) {
('x', ViewMode::Discard(u)) => {
context.replies.push_back(UIEvent::Action(Tab(Kill(*u))));
return true;
}
('n', _) => {}
('y', ViewMode::Discard(u)) => {
let mut failure = true;
let draft = std::mem::replace(&mut self.draft, Draft::default());
let draft = draft.finalise().unwrap();
for folder in &[
&context.accounts[self.account_cursor]
.special_use_folder(SpecialUseMailbox::Drafts),
&context.accounts[self.account_cursor]
.special_use_folder(SpecialUseMailbox::Inbox),
&context.accounts[self.account_cursor]
.special_use_folder(SpecialUseMailbox::Normal),
] {
if folder.is_none() {
continue;
}
let folder = folder.unwrap();
if let Err(e) = context.accounts[self.account_cursor].save(
draft.as_bytes(),
folder,
Some(Flag::SEEN | Flag::DRAFT),
) {
debug!("{:?} could not save draft msg", e);
log(
format!(
"Could not save draft in '{}' folder: {}.",
folder,
e.to_string()
),
ERROR,
);
context.replies.push_back(UIEvent::Notification(
Some(format!("Could not save draft in '{}' folder.", folder)),
e.into(),
Some(NotificationType::ERROR),
));
} else {
failure = false;
break;
}
}
if failure {
let file = create_temp_file(draft.as_bytes(), None, None, false);
debug!("message saved in {}", file.path.display());
log(
format!(
"Message was stored in {} so that you can restore it manually.",
file.path.display()
),
INFO,
);
context.replies.push_back(UIEvent::Notification(
Some("Could not save in any folder".into()),
format!(
"Message was stored in {} so that you can restore it manually.",
file.path.display()
),
Some(NotificationType::INFO),
));
}
context.replies.push_back(UIEvent::Action(Tab(Kill(*u))));
return true;
}
_ => {
return false;
}
}
self.mode = ViewMode::ThreadView;
self.set_dirty();
return true;
}
/* Switch to thread view mode if we're on Edit mode */
UIEvent::Input(Key::Char('v')) if self.mode.is_edit() => {
self.mode = ViewMode::ThreadView;
@ -818,7 +769,18 @@ impl Component for Composer {
}
fn kill(&mut self, uuid: Uuid, _context: &mut Context) {
self.mode = ViewMode::Discard(uuid);
self.mode = ViewMode::Discard(
uuid,
Selector::new(
"this draft has unsaved changes",
vec![
('x', "quit without saving".to_string()),
('y', "save draft and quit".to_string()),
('n', "cancel".to_string()),
],
true,
),
);
}
fn get_shortcuts(&self, context: &Context) -> ShortcutMaps {
@ -855,7 +817,18 @@ impl Component for Composer {
fn can_quit_cleanly(&mut self) -> bool {
/* Play it safe and ask user for confirmation */
self.mode = ViewMode::Discard(self.id);
self.mode = ViewMode::Discard(
self.id,
Selector::new(
"this draft has unsaved changes",
vec![
('x', "quit without saving".to_string()),
('y', "save draft and quit".to_string()),
('n', "cancel".to_string()),
],
true,
),
);
self.set_dirty();
false
}

View File

@ -44,7 +44,7 @@ enum ViewMode {
Attachment(usize),
Raw,
Subview,
ContactSelector(Selector),
ContactSelector(Selector<Card>),
}
impl Default for ViewMode {
@ -60,6 +60,12 @@ impl ViewMode {
_ => false,
}
}
fn is_contact_selector(&self) -> bool {
match self {
ViewMode::ContactSelector(_) => true,
_ => false,
}
}
}
/// Contains an Envelope view, with sticky headers, a pager for the body, and subviews for more
@ -630,16 +636,15 @@ impl Component for MailView {
s.draw(grid, (set_y(upper_left, y + 1), bottom_right), context);
}
}
ViewMode::ContactSelector(ref mut s) => {
clear_area(grid, (set_y(upper_left, y + 1), bottom_right));
s.draw(grid, (set_y(upper_left, y + 1), bottom_right), context);
}
_ => {
if let Some(p) = self.pager.as_mut() {
p.draw(grid, (set_y(upper_left, y + 1), bottom_right), context);
}
}
}
if let ViewMode::ContactSelector(ref mut s) = self.mode {
s.draw(grid, center_area(area, s.content.size()), context);
}
self.dirty = false;
}
@ -654,6 +659,19 @@ impl Component for MailView {
}
ViewMode::ContactSelector(ref mut s) => {
if s.process_event(event, context) {
if s.is_done() {
if let ViewMode::ContactSelector(s) =
std::mem::replace(&mut self.mode, ViewMode::Normal)
{
let account = &mut context.accounts[self.coordinates.0];
{
for card in s.collect() {
account.address_book.add_card(card);
}
}
}
self.set_dirty();
}
return true;
}
}
@ -667,55 +685,22 @@ impl Component for MailView {
}
match *event {
UIEvent::Input(Key::Char('c')) => {
if let ViewMode::ContactSelector(_) = self.mode {
if let ViewMode::ContactSelector(s) =
std::mem::replace(&mut self.mode, ViewMode::Normal)
{
let account = &mut context.accounts[self.coordinates.0];
let mut results = Vec::new();
{
let envelope: &Envelope = &account.get_env(&self.coordinates.2);
for c in s.collect() {
let c = usize::from_ne_bytes({
[c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7]]
});
for (idx, env) in envelope
.from()
.iter()
.chain(envelope.to().iter())
.enumerate()
{
if idx != c {
continue;
}
let mut new_card: Card = Card::new();
new_card.set_email(env.get_email());
new_card.set_name(env.get_display_name());
results.push(new_card);
}
}
}
for c in results {
account.address_book.add_card(c);
}
}
return true;
}
UIEvent::Input(Key::Char('c')) if !self.mode.is_contact_selector() => {
let account = &mut context.accounts[self.coordinates.0];
let envelope: &Envelope = &account.get_env(&self.coordinates.2);
let mut entries = Vec::new();
for (idx, env) in envelope
.from()
.iter()
.chain(envelope.to().iter())
.enumerate()
{
entries.push((idx.to_ne_bytes().to_vec(), format!("{}", env)));
for addr in envelope.from().iter().chain(envelope.to().iter()) {
let mut new_card: Card = Card::new();
new_card.set_email(addr.get_email());
new_card.set_name(addr.get_display_name());
entries.push((new_card, format!("{}", addr)));
}
self.mode = ViewMode::ContactSelector(Selector::new(entries, true));
self.mode = ViewMode::ContactSelector(Selector::new(
"select contacts to add",
entries,
false,
));
self.dirty = true;
return true;
}

View File

@ -1568,29 +1568,44 @@ impl Component for Tabbed {
}
}
type EntryIdentifier = Vec<u8>;
/// Shows selection to user
#[derive(Debug, Copy, PartialEq, Clone)]
enum SelectorCursor {
/// Cursor is at an entry
Entry(usize),
/// Cursor is located on the Ok button
Ok,
/// Cursor is located on the Cancel button
Cancel,
}
/// Shows a little window with options for user to select.
///
/// Instantiate with Selector::new(). Set single_only to true if user should only choose one of the
/// options. After passing input events to this component, check Selector::is_done to see if the
/// user has finalised their choices. Collect the choices by consuming the Selector with
/// Selector::collect()
#[derive(Debug, PartialEq, Clone)]
pub struct Selector {
single_only: bool,
pub struct Selector<T: PartialEq + Debug + Clone + Sync + Send> {
/// allow only one selection
entries: Vec<(EntryIdentifier, bool)>,
selected_entry_count: u32,
content: CellBuffer,
single_only: bool,
entries: Vec<(T, bool)>,
pub content: CellBuffer,
cursor: usize,
cursor: SelectorCursor,
/// If true, user has finished their selection
done: bool,
dirty: bool,
id: ComponentId,
}
impl fmt::Display for Selector {
impl<T: PartialEq + Debug + Clone + Sync + Send> fmt::Display for Selector<T> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
Display::fmt("Selector", f)
}
}
impl Component for Selector {
impl<T: PartialEq + Debug + Clone + Sync + Send> Component for Selector<T> {
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
let (width, height) = self.content.size();
copy_area_with_break(grid, &self.content, area, ((0, 0), (width, height)));
@ -1598,17 +1613,24 @@ impl Component for Selector {
}
fn process_event(&mut self, event: &mut UIEvent, _context: &mut Context) -> bool {
let (width, height) = self.content.size();
match *event {
UIEvent::Input(Key::Char('\t')) => {
self.entries[self.cursor].1 = !self.entries[self.cursor].1;
if self.entries[self.cursor].1 {
match (event, self.cursor) {
(UIEvent::Input(Key::Char('\n')), _) if self.single_only => {
/* User can only select one entry, so Enter key finalises the selection */
self.done = true;
return true;
}
(UIEvent::Input(Key::Char('\n')), SelectorCursor::Entry(c)) if !self.single_only => {
/* User can select multiple entries, so Enter key toggles the entry under the
* cursor */
self.entries[c].1 = !self.entries[c].1;
if self.entries[c].1 {
write_string_to_grid(
"x",
&mut self.content,
Color::Default,
Color::Default,
Attr::Default,
((1, self.cursor), (width, self.cursor)),
((3, c + 2), (width - 2, c + 2)),
false,
);
} else {
@ -1618,23 +1640,170 @@ impl Component for Selector {
Color::Default,
Color::Default,
Attr::Default,
((1, self.cursor), (width, self.cursor)),
((3, c + 2), (width - 2, c + 2)),
false,
);
}
self.dirty = true;
return true;
}
UIEvent::Input(Key::Up) if self.cursor > 0 => {
self.cursor -= 1;
(UIEvent::Input(Key::Char('\n')), SelectorCursor::Ok) if !self.single_only => {
self.done = true;
return true;
}
(UIEvent::Input(Key::Char('\n')), SelectorCursor::Cancel) if !self.single_only => {
for e in self.entries.iter_mut() {
e.1 = false;
}
self.done = true;
return true;
}
(UIEvent::Input(Key::Up), SelectorCursor::Entry(c)) if c > 0 => {
if self.single_only {
// Redraw selection
change_colors(
&mut self.content,
((2, c + 2), (width - 2, c + 2)),
Color::Default,
Color::Default,
);
change_colors(
&mut self.content,
((2, c + 1), (width - 2, c + 1)),
Color::Default,
Color::Byte(8),
);
self.entries[c].1 = false;
self.entries[c - 1].1 = true;
} else {
// Redraw cursor
change_colors(
&mut self.content,
((2, c + 2), (4, c + 2)),
Color::Default,
Color::Default,
);
change_colors(
&mut self.content,
((2, c + 1), (4, c + 1)),
Color::Default,
Color::Byte(8),
);
}
self.cursor = SelectorCursor::Entry(c - 1);
self.dirty = true;
return true;
}
UIEvent::Input(Key::Down) if self.cursor < height.saturating_sub(1) => {
self.cursor += 1;
(UIEvent::Input(Key::Up), SelectorCursor::Ok)
| (UIEvent::Input(Key::Up), SelectorCursor::Cancel) => {
change_colors(
&mut self.content,
((width / 2, height - 2), (width - 1, height - 2)),
Color::Default,
Color::Default,
);
let c = self.entries.len().saturating_sub(1);
self.cursor = SelectorCursor::Entry(c);
change_colors(
&mut self.content,
((2, c + 2), (4, c + 2)),
Color::Default,
Color::Byte(8),
);
self.dirty = true;
return true;
}
(UIEvent::Input(Key::Down), SelectorCursor::Entry(c))
if c < self.entries.len().saturating_sub(1) =>
{
if self.single_only {
// Redraw selection
change_colors(
&mut self.content,
((2, c + 2), (width - 2, c + 2)),
Color::Default,
Color::Default,
);
change_colors(
&mut self.content,
((2, c + 3), (width - 2, c + 3)),
Color::Default,
Color::Byte(8),
);
self.entries[c].1 = false;
self.entries[c + 1].1 = true;
} else {
// Redraw cursor
change_colors(
&mut self.content,
((2, c + 2), (4, c + 2)),
Color::Default,
Color::Default,
);
change_colors(
&mut self.content,
((2, c + 3), (4, c + 3)),
Color::Default,
Color::Byte(8),
);
}
self.cursor = SelectorCursor::Entry(c + 1);
self.dirty = true;
return true;
}
(UIEvent::Input(Key::Down), SelectorCursor::Entry(c)) if !self.single_only => {
self.cursor = SelectorCursor::Ok;
change_colors(
&mut self.content,
((2, c + 2), (4, c + 2)),
Color::Default,
Color::Default,
);
change_colors(
&mut self.content,
((width / 2, height - 2), (width / 2 + 1, height - 2)),
Color::Default,
Color::Byte(8),
);
self.dirty = true;
return true;
}
(UIEvent::Input(Key::Down), _) | (UIEvent::Input(Key::Up), _) => return true,
(UIEvent::Input(Key::Right), SelectorCursor::Ok) => {
self.cursor = SelectorCursor::Cancel;
change_colors(
&mut self.content,
((width / 2, height - 2), (width / 2 + 1, height - 2)),
Color::Default,
Color::Default,
);
change_colors(
&mut self.content,
((width / 2 + 6, height - 2), (width / 2 + 11, height - 2)),
Color::Default,
Color::Byte(8),
);
self.dirty = true;
return true;
}
(UIEvent::Input(Key::Left), SelectorCursor::Cancel) => {
self.cursor = SelectorCursor::Ok;
change_colors(
&mut self.content,
((width / 2, height - 2), (width / 2 + 1, height - 2)),
Color::Default,
Color::Byte(8),
);
change_colors(
&mut self.content,
((width / 2 + 6, height - 2), (width / 2 + 11, height - 2)),
Color::Default,
Color::Default,
);
self.dirty = true;
return true;
}
(UIEvent::Input(Key::Left), _) | (UIEvent::Input(Key::Right), _) => return true,
_ => {}
}
@ -1655,44 +1824,179 @@ impl Component for Selector {
}
}
impl Selector {
pub fn new(mut entries: Vec<(EntryIdentifier, String)>, single_only: bool) -> Selector {
let width = entries
.iter()
.max_by_key(|e| e.1.len())
.map(|v| v.1.len())
.unwrap_or(0)
+ 4;
let height = entries.len();
impl<T: PartialEq + Debug + Clone + Sync + Send> Selector<T> {
pub fn new(title: &str, entries: Vec<(T, String)>, single_only: bool) -> Selector<T> {
let width = std::cmp::max(
"OK Cancel".len(),
std::cmp::max(
entries
.iter()
.max_by_key(|e| e.1.len())
.map(|v| v.1.len())
.unwrap_or(0),
title.len(),
),
) + 7;
let height = entries.len()
+ 4
+ if single_only {
0
} else {
/* Extra room for buttons Okay/Cancel */
3
};
let mut content = CellBuffer::new(width, height, Cell::with_char(' '));
let identifiers = entries
.iter_mut()
.map(|(id, _)| (std::mem::replace(&mut *id, Vec::new()), false))
.collect();
for (i, e) in entries.into_iter().enumerate() {
write_string_to_grid(
"┏━",
&mut content,
Color::Byte(8),
Color::Default,
Attr::Default,
((0, 0), (width - 1, 0)),
false,
);
let (x, _) = write_string_to_grid(
title,
&mut content,
Color::Default,
Color::Default,
Attr::Default,
((2, 0), (width - 1, 0)),
false,
);
for i in 1..(width - title.len() - 1) {
write_string_to_grid(
&format!("[ ] {}", e.1),
"",
&mut content,
Color::Byte(8),
Color::Default,
Attr::Default,
((x + i, 0), (width - 1, 0)),
false,
);
}
write_string_to_grid(
"",
&mut content,
Color::Byte(8),
Color::Default,
Attr::Default,
((width - 1, 0), (width - 1, 0)),
false,
);
write_string_to_grid(
"",
&mut content,
Color::Byte(8),
Color::Default,
Attr::Default,
((0, height - 1), (width - 1, height - 1)),
false,
);
write_string_to_grid(
&"".repeat(width - 2),
&mut content,
Color::Byte(8),
Color::Default,
Attr::Default,
((1, height - 1), (width - 2, height - 1)),
false,
);
write_string_to_grid(
"",
&mut content,
Color::Byte(8),
Color::Default,
Attr::Default,
((width - 1, height - 1), (width - 1, height - 1)),
false,
);
for i in 1..height - 1 {
write_string_to_grid(
"",
&mut content,
Color::Byte(8),
Color::Default,
Attr::Default,
((0, i), (width - 1, i)),
false,
);
write_string_to_grid(
"",
&mut content,
Color::Byte(8),
Color::Default,
Attr::Default,
((width - 1, i), (width - 1, i)),
false,
);
}
if single_only {
for (i, e) in entries.iter().enumerate() {
write_string_to_grid(
&e.1,
&mut content,
Color::Default,
if i == 0 {
Color::Byte(8)
} else {
Color::Default
},
Attr::Default,
((2, i + 2), (width - 1, i + 2)),
false,
);
}
} else {
for (i, e) in entries.iter().enumerate() {
write_string_to_grid(
&format!("[ ] {}", e.1),
&mut content,
Color::Default,
Color::Default,
Attr::Default,
((2, i + 2), (width - 1, i + 2)),
false,
);
if i == 0 {
content[(2, i + 2)].set_bg(Color::Byte(8));
content[(3, i + 2)].set_bg(Color::Byte(8));
content[(4, i + 2)].set_bg(Color::Byte(8));
}
}
write_string_to_grid(
"OK Cancel",
&mut content,
Color::Default,
Color::Default,
Attr::Bold,
((width / 2, height - 2), (width - 1, height - 2)),
false,
);
}
let mut identifiers: Vec<(T, bool)> =
entries.into_iter().map(|(id, _)| (id, false)).collect();
if single_only {
/* set default option */
identifiers[0].1 = true;
}
Selector {
single_only,
entries: identifiers,
selected_entry_count: 0,
content,
cursor: 0,
cursor: SelectorCursor::Entry(0),
done: false,
dirty: true,
id: ComponentId::new_v4(),
}
}
pub fn collect(self) -> Vec<EntryIdentifier> {
pub fn is_done(&self) -> bool {
self.done
}
pub fn collect(self) -> Vec<T> {
self.entries
.into_iter()
.filter(|v| v.1)

View File

@ -799,3 +799,21 @@ pub fn clear_area(grid: &mut CellBuffer, area: Area) {
}
}
}
pub fn center_area(area: Area, (width, height): (usize, usize)) -> Area {
let mid_x = { std::cmp::max(width!(area) / 2, width / 2) - width / 2 };
let mid_y = { std::cmp::max(height!(area) / 2, height / 2) - height / 2 };
let (upper_x, upper_y) = upper_left!(area);
let (max_x, max_y) = bottom_right!(area);
(
(
std::cmp::min(max_x, upper_x + mid_x),
std::cmp::min(max_y, upper_y + mid_y),
),
(
std::cmp::min(max_x, upper_x + mid_x + width),
std::cmp::min(max_y, upper_y + mid_y + height),
),
)
}