ElyDoc, ElyTable, ElyShow (LibreOfficeKit), ElyPDF (MuPDF), ElyTree (Git), ElyMark (Milkdown and CodeMirror in CEF). ElyMail and ElyAgent follow.
449 lines
14 KiB
Rust
449 lines
14 KiB
Rust
//! The rule dialogs: validation, conditional format, the format list.
|
|
|
|
use ely_gpui_component::{
|
|
buttons::{Button, ButtonVariant},
|
|
data_display::DescriptionList,
|
|
forms::{Choice, Input, Select, Switch, TextInput},
|
|
overlays::Dialog as Card,
|
|
};
|
|
use ely_lok::sheet::{Format, Validation};
|
|
use gpui::{App, Entity, ParentElement as _, SharedString, Styled as _, Window, div};
|
|
|
|
use super::Fields;
|
|
use crate::book::Book;
|
|
use crate::dialog::{Dialog, edit};
|
|
|
|
/// What a cell may hold.
|
|
const KINDS: [(&str, &str); 8] = [
|
|
("any", "table.rules.any"),
|
|
("whole", "table.rules.whole"),
|
|
("decimal", "table.rules.decimal"),
|
|
("date", "table.rules.date"),
|
|
("time", "table.rules.time"),
|
|
("textlength", "table.rules.textlength"),
|
|
("list", "table.rules.list"),
|
|
("custom", "table.rules.custom"),
|
|
];
|
|
|
|
const OPERATORS: [(&str, &str); 8] = [
|
|
("between", "table.rules.between"),
|
|
("notbetween", "table.rules.notbetween"),
|
|
("equal", "table.rules.equal"),
|
|
("notequal", "table.rules.notequal"),
|
|
("greater", "table.rules.greater"),
|
|
("less", "table.rules.less"),
|
|
("greaterequal", "table.rules.greaterequal"),
|
|
("lessequal", "table.rules.lessequal"),
|
|
];
|
|
|
|
const ALERTS: [(&str, &str); 3] = [
|
|
("stop", "table.rules.stop"),
|
|
("warning", "table.rules.warning"),
|
|
("info", "table.rules.info"),
|
|
];
|
|
|
|
/// Conditions a cell rule tests; more than validation has.
|
|
const CONDITIONS: [(&str, &str); 18] = [
|
|
("greater", "table.rules.greater"),
|
|
("less", "table.rules.less"),
|
|
("between", "table.rules.between"),
|
|
("notbetween", "table.rules.notbetween"),
|
|
("equal", "table.rules.equal"),
|
|
("notequal", "table.rules.notequal"),
|
|
("greaterequal", "table.rules.greaterequal"),
|
|
("lessequal", "table.rules.lessequal"),
|
|
("duplicate", "table.rules.duplicate"),
|
|
("unique", "table.rules.unique"),
|
|
("contains", "table.rules.contains"),
|
|
("beginswith", "table.rules.beginswith"),
|
|
("endswith", "table.rules.endswith"),
|
|
("top10", "table.rules.top"),
|
|
("bottom10", "table.rules.bottom"),
|
|
("aboveaverage", "table.rules.above_average"),
|
|
("belowaverage", "table.rules.below_average"),
|
|
("error", "table.rules.error"),
|
|
];
|
|
|
|
/// The kernel's cell styles a rule can apply.
|
|
const STYLES: [(&str, &str); 10] = [
|
|
("Good", "table.style.good"),
|
|
("Neutral", "table.style.neutral"),
|
|
("Bad", "table.style.bad"),
|
|
("Warning", "table.style.warning"),
|
|
("Error", "table.style.error"),
|
|
("Accent", "table.style.accent"),
|
|
("Accent 1", "table.style.accent1"),
|
|
("Accent 2", "table.style.accent2"),
|
|
("Accent 3", "table.style.accent3"),
|
|
("Note", "table.style.note"),
|
|
];
|
|
|
|
const FORMATS: [(&str, &str); 3] = [
|
|
("cell", "table.rules.cell"),
|
|
("scale", "table.rules.scale"),
|
|
("databar", "table.rules.databar"),
|
|
];
|
|
|
|
/// Color scales: label, low to high.
|
|
pub(super) const SCALES: [(&str, &[&str]); 4] = [
|
|
("table.rules.scale_rgy", &["#F8696B", "#FFEB84", "#63BE7B"]),
|
|
("table.rules.scale_gyr", &["#63BE7B", "#FFEB84", "#F8696B"]),
|
|
("table.rules.scale_wr", &["#FFFFFF", "#F8696B"]),
|
|
("table.rules.scale_wg", &["#FFFFFF", "#63BE7B"]),
|
|
];
|
|
|
|
/// Data bar colors.
|
|
pub(super) const BARS: [(&str, &str); 4] = [
|
|
("table.rules.bar_blue", "#638EC6"),
|
|
("table.rules.bar_green", "#63C384"),
|
|
("table.rules.bar_red", "#FF555A"),
|
|
("table.rules.bar_orange", "#FFB628"),
|
|
];
|
|
|
|
/// The table's value for a picked choice.
|
|
fn find(table: &[(&'static str, &'static str)], value: &str) -> &'static str {
|
|
table
|
|
.iter()
|
|
.find(|(key, _)| *key == value)
|
|
.map(|(key, _)| *key)
|
|
.expect("a listed choice")
|
|
}
|
|
|
|
/// A select over `table`; `set` takes the pick.
|
|
fn pick(
|
|
this: &Entity<Book>,
|
|
id: &'static str,
|
|
(table, selected): (&[(&'static str, &'static str)], &str),
|
|
set: fn(&mut Dialog, &'static str),
|
|
cx: &App,
|
|
) -> Select {
|
|
let choices = table
|
|
.iter()
|
|
.map(|(value, key)| Choice::new(*value, Book::text(cx, key, &[])));
|
|
let (this, table) = (this.clone(), table.to_vec());
|
|
Select::new(id, choices)
|
|
.selected(selected.to_string())
|
|
.on_change(move |value, _, cx| {
|
|
let value = find(&table, value);
|
|
edit(&this, cx, |dialog, _, _| set(dialog, value))
|
|
})
|
|
}
|
|
|
|
/// A switch; `set` takes its state.
|
|
fn switch(
|
|
this: &Entity<Book>,
|
|
id: &'static str,
|
|
on: bool,
|
|
set: fn(&mut Validation, bool),
|
|
) -> Switch {
|
|
let this = this.clone();
|
|
Switch::new(id, on).on_change(move |on, _, cx| {
|
|
edit(&this, cx, |dialog, _, _| {
|
|
if let Dialog::Validation { rule, .. } = dialog {
|
|
set(rule, on);
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
fn field(input: &Entity<TextInput>) -> gpui::Div {
|
|
div().w_full().max_w_64().child(Input::new(input))
|
|
}
|
|
|
|
fn rule_of(dialog: &mut Dialog) -> &mut Validation {
|
|
match dialog {
|
|
Dialog::Validation { rule, .. } => rule,
|
|
_ => unreachable!("a validation dialog"),
|
|
}
|
|
}
|
|
|
|
pub fn validation_dialog(
|
|
this: Entity<Book>,
|
|
(rule, fields): (&Validation, &Fields),
|
|
dismiss: impl Fn(&mut Window, &mut App) + 'static,
|
|
cx: &App,
|
|
) -> Card {
|
|
let label = |key: &str| Book::text(cx, key, &[]);
|
|
let mut rows = DescriptionList::new().item(
|
|
label("table.rules.allow"),
|
|
pick(
|
|
&this,
|
|
"table-rule-kind",
|
|
(&KINDS, &rule.kind),
|
|
|dialog, kind| rule_of(dialog).kind = kind.into(),
|
|
cx,
|
|
),
|
|
);
|
|
match rule.kind.as_str() {
|
|
"whole" | "decimal" | "date" | "time" | "textlength" => {
|
|
rows = rows
|
|
.item(
|
|
label("table.rules.data"),
|
|
pick(
|
|
&this,
|
|
"table-rule-operator",
|
|
(&OPERATORS, &rule.operator),
|
|
|dialog, op| rule_of(dialog).operator = op.into(),
|
|
cx,
|
|
),
|
|
)
|
|
.item(label("table.rules.value"), field(&fields.value1));
|
|
if matches!(rule.operator.as_str(), "between" | "notbetween") {
|
|
rows = rows.item(label("table.rules.and"), field(&fields.value2));
|
|
}
|
|
}
|
|
"list" => {
|
|
rows = rows
|
|
.item(label("table.rules.entries"), field(&fields.list))
|
|
.item(
|
|
label("table.rules.dropdown"),
|
|
switch(&this, "table-rule-dropdown", rule.dropdown, |rule, on| {
|
|
rule.dropdown = on
|
|
}),
|
|
);
|
|
}
|
|
"custom" => rows = rows.item(label("table.rules.formula"), field(&fields.value1)),
|
|
_ => {}
|
|
}
|
|
if rule.kind != "any" {
|
|
rows = rows.item(
|
|
label("table.rules.blank"),
|
|
switch(&this, "table-rule-blank", rule.ignore_blank, |rule, on| {
|
|
rule.ignore_blank = on
|
|
}),
|
|
);
|
|
}
|
|
rows = rows.item(
|
|
label("table.rules.help"),
|
|
switch(&this, "table-rule-help", rule.show_input, |rule, on| {
|
|
rule.show_input = on
|
|
}),
|
|
);
|
|
if rule.show_input {
|
|
rows = rows
|
|
.item(label("table.rules.title"), field(&fields.input_title))
|
|
.item(label("table.rules.message"), field(&fields.input_message));
|
|
}
|
|
if rule.kind != "any" {
|
|
rows = rows.item(
|
|
label("table.rules.alert"),
|
|
switch(&this, "table-rule-alert", rule.show_error, |rule, on| {
|
|
rule.show_error = on
|
|
}),
|
|
);
|
|
if rule.show_error {
|
|
rows = rows
|
|
.item(
|
|
label("table.rules.style"),
|
|
pick(
|
|
&this,
|
|
"table-rule-style",
|
|
(&ALERTS, &rule.error_style),
|
|
|dialog, style| rule_of(dialog).error_style = style.into(),
|
|
cx,
|
|
),
|
|
)
|
|
.item(label("table.rules.title"), field(&fields.error_title))
|
|
.item(label("table.rules.message"), field(&fields.error_message));
|
|
}
|
|
}
|
|
let go = label("table.rules.apply");
|
|
Card::new("table-rule", label("table.rules.validation_title"), dismiss)
|
|
.child(rows)
|
|
.action(ely_office::cancel(cx))
|
|
.action(move |_| {
|
|
Button::new("table-rule-go", go)
|
|
.variant(ButtonVariant::Primary)
|
|
.on_click(move |_, _, cx| this.update(cx, |book, cx| book.rule_asked(cx)))
|
|
})
|
|
}
|
|
|
|
/// The open conditional dialog's parts.
|
|
pub struct Shown<'a> {
|
|
pub kind: &'a str,
|
|
pub operator: &'a str,
|
|
pub style: &'a str,
|
|
pub preset: usize,
|
|
pub value1: &'a Entity<TextInput>,
|
|
pub value2: &'a Entity<TextInput>,
|
|
}
|
|
|
|
fn set_conditional(
|
|
dialog: &mut Dialog,
|
|
change: impl FnOnce(&mut &'static str, &mut &'static str, &mut &'static str, &mut usize),
|
|
) {
|
|
if let Dialog::Conditional {
|
|
kind,
|
|
operator,
|
|
style,
|
|
preset,
|
|
..
|
|
} = dialog
|
|
{
|
|
change(kind, operator, style, preset);
|
|
}
|
|
}
|
|
|
|
pub fn conditional_dialog(
|
|
this: Entity<Book>,
|
|
shown: Shown,
|
|
dismiss: impl Fn(&mut Window, &mut App) + 'static,
|
|
cx: &App,
|
|
) -> Card {
|
|
let label = |key: &str| Book::text(cx, key, &[]);
|
|
let mut rows = DescriptionList::new().item(
|
|
label("table.rules.kind"),
|
|
pick(
|
|
&this,
|
|
"table-format-kind",
|
|
(&FORMATS, shown.kind),
|
|
|dialog, value| {
|
|
set_conditional(dialog, |kind, _, _, preset| {
|
|
*kind = value;
|
|
*preset = 0;
|
|
})
|
|
},
|
|
cx,
|
|
),
|
|
);
|
|
match shown.kind {
|
|
"scale" | "databar" => {
|
|
let table: Vec<(SharedString, SharedString)> = if shown.kind == "scale" {
|
|
SCALES
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(at, (key, _))| (at.to_string().into(), label(key)))
|
|
.collect()
|
|
} else {
|
|
BARS.iter()
|
|
.enumerate()
|
|
.map(|(at, (key, _))| (at.to_string().into(), label(key)))
|
|
.collect()
|
|
};
|
|
let picked = this.clone();
|
|
let presets = Select::new(
|
|
"table-format-preset",
|
|
table
|
|
.into_iter()
|
|
.map(|(value, text)| Choice::new(value, text)),
|
|
)
|
|
.selected(shown.preset.to_string())
|
|
.on_change(move |value, _, cx| {
|
|
let at: usize = value.parse().expect("presets are indexes");
|
|
edit(&picked, cx, |dialog, _, _| {
|
|
set_conditional(dialog, |_, _, _, preset| *preset = at)
|
|
})
|
|
});
|
|
rows = rows.item(label("table.rules.colors"), presets);
|
|
}
|
|
_ => {
|
|
rows = rows.item(
|
|
label("table.rules.condition"),
|
|
pick(
|
|
&this,
|
|
"table-format-operator",
|
|
(&CONDITIONS, shown.operator),
|
|
|dialog, value| set_conditional(dialog, |_, operator, _, _| *operator = value),
|
|
cx,
|
|
),
|
|
);
|
|
match shown.operator {
|
|
"duplicate" | "unique" | "aboveaverage" | "belowaverage" | "error" => {}
|
|
"between" | "notbetween" => {
|
|
rows = rows
|
|
.item(label("table.rules.value"), field(shown.value1))
|
|
.item(label("table.rules.and"), field(shown.value2));
|
|
}
|
|
_ => rows = rows.item(label("table.rules.value"), field(shown.value1)),
|
|
}
|
|
rows = rows.item(
|
|
label("table.rules.cell_style"),
|
|
pick(
|
|
&this,
|
|
"table-format-style",
|
|
(&STYLES, shown.style),
|
|
|dialog, value| set_conditional(dialog, |_, _, style, _| *style = value),
|
|
cx,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
let go = label("table.rules.add");
|
|
Card::new(
|
|
"table-format",
|
|
label("table.rules.conditional_title"),
|
|
dismiss,
|
|
)
|
|
.child(rows)
|
|
.action(ely_office::cancel(cx))
|
|
.action(move |_| {
|
|
Button::new("table-format-go", go)
|
|
.variant(ButtonVariant::Primary)
|
|
.on_click(move |_, _, cx| this.update(cx, |book, cx| book.conditional_asked(cx)))
|
|
})
|
|
}
|
|
|
|
/// One format as the list shows it.
|
|
fn entry(format: &Format, cx: &App) -> SharedString {
|
|
let kind = FORMATS
|
|
.iter()
|
|
.find(|(value, _)| *value == format.kind)
|
|
.map_or_else(
|
|
|| format.kind.clone().into(),
|
|
|(_, key)| Book::text(cx, key, &[]),
|
|
);
|
|
let style = STYLES
|
|
.iter()
|
|
.find(|(value, _)| *value == format.style)
|
|
.map_or_else(
|
|
|| format.style.clone().into(),
|
|
|(_, key)| Book::text(cx, key, &[]),
|
|
);
|
|
let args = [
|
|
("range", format.range.as_str()),
|
|
("kind", kind.as_ref()),
|
|
("style", style.as_ref()),
|
|
("value", format.value.as_str()),
|
|
];
|
|
match format.kind.as_str() {
|
|
"cell" => Book::text(cx, "table.rules.entry_cell", &args),
|
|
_ => Book::text(cx, "table.rules.entry", &args),
|
|
}
|
|
}
|
|
|
|
pub fn formats_dialog(
|
|
this: Entity<Book>,
|
|
(formats, chosen): (&[Format], usize),
|
|
dismiss: impl Fn(&mut Window, &mut App) + 'static,
|
|
cx: &App,
|
|
) -> Card {
|
|
let choices = formats
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(at, format)| Choice::new(at.to_string(), entry(format, cx)));
|
|
let picked = this.clone();
|
|
let list = Select::new("table-formats-pick", choices)
|
|
.selected(chosen.to_string())
|
|
.on_change(move |value, _, cx| {
|
|
let at: usize = value.parse().expect("formats are listed by index");
|
|
edit(&picked, cx, |dialog, _, _| {
|
|
if let Dialog::Formats { chosen, .. } = dialog {
|
|
*chosen = at;
|
|
}
|
|
})
|
|
});
|
|
let go = Book::text(cx, "table.rules.remove", &[]);
|
|
Card::new(
|
|
"table-formats",
|
|
Book::text(cx, "table.rules.formats_title", &[]),
|
|
dismiss,
|
|
)
|
|
.child(list)
|
|
.action(ely_office::cancel(cx))
|
|
.action(move |_| {
|
|
Button::new("table-formats-remove", go)
|
|
.variant(ButtonVariant::Danger)
|
|
.on_click(move |_, _, cx| this.update(cx, |book, cx| book.format_removed(cx)))
|
|
})
|
|
}
|