meli/meli/src/utilities/dialogs.rs

648 lines
22 KiB
Rust

/*
* 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 <http://www.gnu.org/licenses/>.
*/
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<String>,
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<Box<dyn FnOnce(ComponentId, bool) -> Option<UIEvent> + 'static + Sync + Send>>,
>;
pub type UIDialog<T> = Selector<
T,
Option<Box<dyn FnOnce(ComponentId, &[T]) -> Option<UIEvent> + 'static + Sync + Send>>,
>;
impl<T: 'static + PartialEq + std::fmt::Debug + Clone + Sync + Send, F: 'static + Sync + Send>
std::fmt::Debug for Selector<T, F>
{
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
std::fmt::Display::fmt("Selector", f)
}
}
impl<T: 'static + PartialEq + std::fmt::Debug + Clone + Sync + Send, F: 'static + Sync + Send>
std::fmt::Display for Selector<T, F>
{
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
std::fmt::Display::fmt("Selector", f)
}
}
impl<T: 'static + PartialEq + std::fmt::Debug + Clone + Sync + Send, F: 'static + Sync + Send>
PartialEq for Selector<T, F>
{
fn eq(&self, other: &Selector<T, F>) -> bool {
self.entries == other.entries
}
}
impl<T: 'static + PartialEq + std::fmt::Debug + Clone + Sync + Send> Component for UIDialog<T> {
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<T: PartialEq + std::fmt::Debug + Clone + Sync + Send, F: 'static + Sync + Send>
Selector<T, F>
{
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::<Vec<String>>();
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<T> {
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<T: 'static + PartialEq + std::fmt::Debug + Clone + Sync + Send> UIDialog<T> {
fn done(&mut self) -> Option<UIEvent> {
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::<Vec<_>>()
.as_slice(),
)
})
}
fn cancel(&mut self) -> UIEvent {
let Self { ref id, .. } = self;
UIEvent::CanceledUIDialog(*id)
}
}
impl UIConfirmationDialog {
fn done(&mut self) -> Option<UIEvent> {
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)
}
}