/* * meli * * Copyright 2020 Manos Pitsidianakis * * This file is part of meli. * * meli is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * meli is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with meli. If not, see . */ use super::*; const OK_CANCEL: &str = "OK Cancel"; const OK_OFFSET: usize = 0; const OK_LENGTH: usize = "OK".len(); const CANCEL_OFFSET: usize = "OK ".len(); const CANCEL_LENGTH: usize = "Cancel".len(); #[derive(Debug, Copy, PartialEq, Eq, Clone)] enum SelectorCursor { Unfocused, /// 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() pub struct Selector< T: 'static + PartialEq + std::fmt::Debug + Clone + Sync + Send, F: 'static + Sync + Send, > { /// allow only one selection single_only: bool, entries: Vec<(T, bool)>, entry_titles: Vec, theme_default: ThemeAttribute, cursor: SelectorCursor, vertical_alignment: Alignment, horizontal_alignment: Alignment, title: String, /// If true, user has finished their selection done: bool, done_fn: F, dirty: bool, id: ComponentId, } pub type UIConfirmationDialog = Selector< bool, Option Option + 'static + Sync + Send>>, >; pub type UIDialog = Selector< T, Option Option + 'static + Sync + Send>>, >; impl std::fmt::Debug for Selector { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { std::fmt::Display::fmt("Selector", f) } } impl std::fmt::Display for Selector { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { std::fmt::Display::fmt("Selector", f) } } impl PartialEq for Selector { fn eq(&self, other: &Selector) -> bool { self.entries == other.entries } } impl Component for UIDialog { fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { Selector::draw(self, grid, area, context); } fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool { if let UIEvent::ConfigReload { old_settings: _ } = event { self.initialise(context); self.set_dirty(true); return false; } let shortcuts = self.shortcuts(context); let mut highlighted_attrs = crate::conf::value(context, "widgets.options.highlighted"); if !context.settings.terminal.use_color() { highlighted_attrs.attrs |= Attr::REVERSE; } 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; if let Some(event) = self.done() { context.replies.push_back(event); self.unrealize(context); } 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; self.dirty = true; return true; } (UIEvent::Input(Key::Char('\n')), SelectorCursor::Ok) if !self.single_only => { self.done = true; if let Some(event) = self.done() { context.replies.push_back(event); self.unrealize(context); } return true; } (UIEvent::Input(Key::Esc), _) => { for e in self.entries.iter_mut() { e.1 = false; } if !self.done { self.unrealize(context); } self.done = true; _ = self.done(); context.replies.push_back(self.cancel()); return false; } (UIEvent::Input(Key::Char('\n')), SelectorCursor::Cancel) if !self.single_only => { for e in self.entries.iter_mut() { e.1 = false; } self.done = true; if let Some(event) = self.done() { context.replies.push_back(event); self.unrealize(context); } return true; } (UIEvent::Input(ref key), SelectorCursor::Unfocused) if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_down"]) => { if self.single_only { self.entries[0].1 = true; } self.cursor = SelectorCursor::Entry(0); self.dirty = true; return true; } (UIEvent::Input(ref key), SelectorCursor::Entry(c)) if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_up"]) && c > 0 => { if self.single_only { // Redraw selection self.entries[c].1 = false; self.entries[c - 1].1 = true; } self.cursor = SelectorCursor::Entry(c - 1); self.dirty = true; return true; } (UIEvent::Input(ref key), SelectorCursor::Ok) | (UIEvent::Input(ref key), SelectorCursor::Cancel) if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_up"]) => { let c = self.entries.len().saturating_sub(1); self.cursor = SelectorCursor::Entry(c); self.dirty = true; return true; } (UIEvent::Input(ref key), SelectorCursor::Entry(c)) if c < self.entries.len().saturating_sub(1) && shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_down"]) => { if self.single_only { // Redraw selection self.entries[c].1 = false; self.entries[c + 1].1 = true; } self.cursor = SelectorCursor::Entry(c + 1); self.dirty = true; return true; } (UIEvent::Input(ref key), SelectorCursor::Entry(c)) if !self.single_only && shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_down"]) => { self.cursor = SelectorCursor::Ok; self.dirty = true; return true; } (UIEvent::Input(ref key), SelectorCursor::Ok) if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_right"]) => { self.cursor = SelectorCursor::Cancel; self.dirty = true; return true; } (UIEvent::Input(ref key), SelectorCursor::Cancel) if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_left"]) => { self.cursor = SelectorCursor::Ok; self.dirty = true; return true; } (UIEvent::Input(ref key), _) if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_left"]) || shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_right"]) || shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_up"]) || shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_down"]) => { return true } _ => {} } false } fn shortcuts(&self, context: &Context) -> ShortcutMaps { let mut map = ShortcutMaps::default(); map.insert( Shortcuts::GENERAL, context.settings.shortcuts.general.key_values(), ); map } fn is_dirty(&self) -> bool { self.dirty } fn set_dirty(&mut self, value: bool) { self.dirty = value; } fn id(&self) -> ComponentId { self.id } } impl Component for UIConfirmationDialog { fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { Selector::draw(self, grid, area, context); } fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool { if let UIEvent::ConfigReload { old_settings: _ } = event { self.initialise(context); self.set_dirty(true); return false; } let shortcuts = self.shortcuts(context); let mut highlighted_attrs = crate::conf::value(context, "widgets.options.highlighted"); if !context.settings.terminal.use_color() { highlighted_attrs.attrs |= Attr::REVERSE; } 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; if let Some(event) = self.done() { context.replies.push_back(event); self.unrealize(context); } 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; self.dirty = true; return true; } (UIEvent::Input(Key::Char('\n')), SelectorCursor::Ok) if !self.single_only => { self.done = true; if let Some(event) = self.done() { context.replies.push_back(event); self.unrealize(context); } return true; } (UIEvent::Input(Key::Esc), _) => { for e in self.entries.iter_mut() { e.1 = false; } if !self.done { self.unrealize(context); } self.done = true; _ = self.done(); context.replies.push_back(self.cancel()); return false; } (UIEvent::Input(Key::Char('\n')), SelectorCursor::Cancel) if !self.single_only => { for e in self.entries.iter_mut() { e.1 = false; } self.done = true; if let Some(event) = self.done() { context.replies.push_back(event); self.unrealize(context); } return true; } (UIEvent::Input(ref key), SelectorCursor::Entry(c)) if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_up"]) && c > 0 => { if self.single_only { // Redraw selection self.entries[c].1 = false; self.entries[c - 1].1 = true; } self.cursor = SelectorCursor::Entry(c - 1); self.dirty = true; return true; } (UIEvent::Input(ref key), SelectorCursor::Ok) | (UIEvent::Input(ref key), SelectorCursor::Cancel) if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_up"]) => { let c = self.entries.len().saturating_sub(1); self.cursor = SelectorCursor::Entry(c); self.dirty = true; return true; } (UIEvent::Input(ref key), SelectorCursor::Unfocused) if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_down"]) => { if self.single_only { self.entries[0].1 = true; } self.cursor = SelectorCursor::Entry(0); self.dirty = true; return true; } (UIEvent::Input(ref key), SelectorCursor::Entry(c)) if c < self.entries.len().saturating_sub(1) && shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_down"]) => { if self.single_only { // Redraw selection self.entries[c].1 = false; self.entries[c + 1].1 = true; } self.cursor = SelectorCursor::Entry(c + 1); self.dirty = true; return true; } (UIEvent::Input(ref key), SelectorCursor::Entry(c)) if !self.single_only && shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_down"]) => { self.cursor = SelectorCursor::Ok; self.dirty = true; return true; } (UIEvent::Input(ref key), SelectorCursor::Ok) if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_right"]) => { self.cursor = SelectorCursor::Cancel; self.dirty = true; return true; } (UIEvent::Input(ref key), SelectorCursor::Cancel) if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_left"]) => { self.cursor = SelectorCursor::Ok; self.dirty = true; return true; } (UIEvent::Input(ref key), _) if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_left"]) || shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_right"]) || shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_up"]) || shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_down"]) => { return true } _ => {} } false } fn shortcuts(&self, context: &Context) -> ShortcutMaps { let mut map = ShortcutMaps::default(); map.insert( Shortcuts::GENERAL, context.settings.shortcuts.general.key_values(), ); map } fn is_dirty(&self) -> bool { self.dirty } fn set_dirty(&mut self, value: bool) { self.dirty = value; } fn id(&self) -> ComponentId { self.id } } impl Selector { pub fn new( title: &str, mut entries: Vec<(T, String)>, single_only: bool, done_fn: F, context: &Context, ) -> Self { let entry_titles = entries .iter_mut() .map(|(_id, ref mut title)| std::mem::take(title)) .collect::>(); let mut identifiers: Vec<(T, bool)> = entries.into_iter().map(|(id, _)| (id, false)).collect(); if single_only { /* set default option */ identifiers[0].1 = true; } let mut ret = Self { single_only, entries: identifiers, entry_titles, cursor: SelectorCursor::Unfocused, vertical_alignment: Alignment::Center, horizontal_alignment: Alignment::Center, title: title.to_string(), done: false, done_fn, dirty: true, theme_default: Default::default(), id: ComponentId::default(), }; ret.initialise(context); ret } fn initialise(&mut self, context: &Context) { self.theme_default = crate::conf::value(context, "theme_default"); } pub fn is_done(&self) -> bool { self.done } pub fn collect(self) -> Vec { self.entries .into_iter() .filter(|v| v.1) .map(|(id, _)| id) .collect() } fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { let shortcuts = context.settings.shortcuts.general.key_values(); let navigate_help_string = format!( "Navigate options with {} to go down, {} to go up, select with {}", shortcuts["scroll_down"], shortcuts["scroll_up"], Key::Char('\n') ); let width = std::cmp::max( self.entry_titles.iter().map(|e| e.len()).max().unwrap_or(0) + 3, std::cmp::max(self.title.len(), navigate_help_string.len()) + 3, ) + 3; let height = self.entries.len() + { /* padding */ 3 }; let dialog_area = area.align_inside( (width, height), self.horizontal_alignment, self.vertical_alignment, ); let inner_area = create_box(grid, dialog_area); grid.clear_area(inner_area, self.theme_default); grid.write_string( &self.title, self.theme_default.fg, self.theme_default.bg, self.theme_default.attrs | Attr::BOLD, dialog_area.skip_cols(2), None, ); grid.write_string( &navigate_help_string, self.theme_default.fg, self.theme_default.bg, self.theme_default.attrs | Attr::ITALICS, dialog_area.skip_cols(2).skip_rows(height), None, ); let inner_area = inner_area.skip_cols(1).skip_rows(1); let width = std::cmp::max( OK_CANCEL.len(), std::cmp::max( self.entry_titles .iter() .max_by_key(|e| e.len()) .map(|v| v.len()) .unwrap_or(0), self.title.len(), ), ) + 5; let height = self.entries.len() + if self.single_only { 0 } else { /* Extra room for buttons Okay/Cancel */ 2 }; if self.single_only { for (i, e) in self.entry_titles.iter().enumerate() { grid.write_string( e, self.theme_default.fg, self.theme_default.bg, self.theme_default.attrs, inner_area.nth_row(i), None, ); } } else { for (i, e) in self.entry_titles.iter().enumerate() { grid.write_string( &format!("[{}] {}", if self.entries[i].1 { "x" } else { " " }, e), self.theme_default.fg, self.theme_default.bg, self.theme_default.attrs, inner_area.nth_row(i), None, ); } grid.write_string( OK_CANCEL, self.theme_default.fg, self.theme_default.bg, self.theme_default.attrs | Attr::BOLD, inner_area .nth_row(height - 1) .skip_cols((width - OK_CANCEL.len()) / 2), None, ); } context.dirty_areas.push_back(dialog_area); self.dirty = false; } } impl UIDialog { fn done(&mut self) -> Option { let Self { ref mut done_fn, ref mut entries, ref id, .. } = self; done_fn.take().and_then(|done_fn| { done_fn( *id, entries .iter() .filter(|v| v.1) .map(|(id, _)| id) .cloned() .collect::>() .as_slice(), ) }) } fn cancel(&mut self) -> UIEvent { let Self { ref id, .. } = self; UIEvent::CanceledUIDialog(*id) } } impl UIConfirmationDialog { fn done(&mut self) -> Option { let Self { ref mut done_fn, ref mut entries, ref id, .. } = self; done_fn.take().and_then(|done_fn| { done_fn( *id, entries .iter() .filter(|v| v.1) .map(|(id, _)| id) .cloned() .any(std::convert::identity), ) }) } fn cancel(&mut self) -> UIEvent { let Self { ref id, .. } = self; UIEvent::CanceledUIDialog(*id) } }