mirror of
https://github.com/storytold/gridcraft.git
synced 2026-11-02 07:06:48 -06:00
egui clicks a focused control on Enter as on Space, so Enter on the focused category list opened it, and the dialog's OK handling took the same key press and inserted the selected function. Enter in the open list did the same while picking a category. The dialog now leaves Enter to the category list while the list has the focus or its menu is open (or was open as the frame started, since Enter on a row closes it in that frame). Enter in the search box or on the function list still confirms. Test: function_library::enter_on_the_category_list_does_not_confirm_insert_function tabs to the list, presses Enter (the list opens, the dialog stays open, nothing is inserted), presses Enter again in the open list (still open, nothing inserted), then Shift+Tab back to the search box, where Enter confirms.
129 lines
6.7 KiB
Rust
129 lines
6.7 KiB
Rust
use super::show::{ok_cancel, take_first_focus};
|
|
use super::{Dialog, Json, SheetApp, json, vec2};
|
|
|
|
/// Insert Function: search names (as the formula language spells them) and descriptions, or pick
|
|
/// a category (Most Recently Used, All or one category), then a function; each hit shows its
|
|
/// category and description. OK starts editing `=NAME(` (or appends to the formula).
|
|
pub(super) fn insert_function(app: &mut SheetApp, ui: &mut egui::Ui, d: &mut Dialog, confirm: &mut bool) {
|
|
use crate::function_library::{self as lib, ALL, MOST_RECENTLY_USED};
|
|
let language = app.ui.language;
|
|
ui.horizontal(|ui| {
|
|
ui.label(language.tr("Search:"));
|
|
let r = ui.add(egui::TextEdit::singleline(&mut d.search).desired_width(300.0).hint_text(language.tr("e.g. lookup, average, date")));
|
|
// Once only, so Tab can move on to the category list.
|
|
take_first_focus(d, &r);
|
|
});
|
|
// A new search (typed, or set through the control channel) looks through every function, as
|
|
// Excel's does.
|
|
if d.values.get("searched").and_then(Json::as_str) != Some(d.search.as_str()) {
|
|
d.list_index = 0;
|
|
if !d.search.trim().is_empty() {
|
|
d.values.insert("category".into(), json!(ALL));
|
|
}
|
|
d.values.insert("searched".into(), json!(d.search));
|
|
}
|
|
// The category may be given by label or by engine id (a Function Library menu, `ui.dialog.set`).
|
|
let saved = d.values.get("category").and_then(Json::as_str).unwrap_or(MOST_RECENTLY_USED).to_string();
|
|
let mut category = if saved.eq_ignore_ascii_case(ALL) {
|
|
ALL.to_string()
|
|
} else {
|
|
lib::category_id(&saved).map(lib::category_label).unwrap_or(MOST_RECENTLY_USED).to_string()
|
|
};
|
|
// The category list is the dialog's only menu, so a menu open as the frame starts is its own
|
|
// (Enter on a row picks it and closes the menu in this frame).
|
|
let menu_was_open = egui::Popup::is_any_open(ui.ctx());
|
|
let picker = ui.horizontal(|ui| {
|
|
let label = ui.label(language.tr("Or select a category:"));
|
|
craft_ui::ComboBox::from_id_salt("insertFunctionCategory")
|
|
.width(220.0)
|
|
.selected_text(language.tr(&category))
|
|
.show_ui(ui, |ui| {
|
|
for c in [MOST_RECENTLY_USED, ALL].into_iter().chain(lib::CATEGORIES.iter().map(|(_, l)| *l)) {
|
|
ui.selectable_value(&mut category, c.to_string(), language.tr(c));
|
|
}
|
|
})
|
|
.response
|
|
.labelled_by(label.id)
|
|
});
|
|
// Enter on the category list or in its menu opens it or picks a category, as Space does
|
|
// (egui clicks a focused control on either key); it does not also confirm the dialog.
|
|
let picker = picker.inner;
|
|
if menu_was_open || picker.has_focus() || craft_ui::ComboBox::is_open(ui.ctx(), picker.id) {
|
|
ui.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::Enter));
|
|
}
|
|
if category != saved {
|
|
d.list_index = 0;
|
|
}
|
|
d.values.insert("category".into(), json!(category));
|
|
let locale = language.formula_locale();
|
|
// The names and descriptions searched are the ones shown, so in the interface language.
|
|
let q = d.search.trim().to_lowercase();
|
|
let matches = |f: &lib::FnEntry| {
|
|
q.is_empty()
|
|
|| locale.function_name(&f.name).to_lowercase().contains(&q)
|
|
|| language.function_description(&f.description).unwrap_or(&f.description).to_lowercase().contains(&q)
|
|
};
|
|
let hits: Vec<&lib::FnEntry> = if category == MOST_RECENTLY_USED {
|
|
lib::recent(app).iter().filter_map(|n| lib::find(n)).filter(|f| matches(f)).collect()
|
|
} else {
|
|
let id = lib::category_id(&category);
|
|
// The catalog is fixed (some 500 functions), so All lists every one.
|
|
lib::all().iter().filter(|f| id.is_none_or(|id| f.category == id) && matches(f)).collect()
|
|
};
|
|
let t = crate::theme::Tokens::get(ui.ctx());
|
|
let font = egui::TextStyle::Body.resolve(ui.style());
|
|
let rows: Vec<(&str, &str)> = hits.iter().map(|f| (locale.function_name(&f.name), language.tr(lib::category_label(&f.category)))).collect();
|
|
// The category column starts after the widest name shown, so a long name in any formula
|
|
// language or font never runs into it.
|
|
let name_w = rows.iter().map(|(n, _)| ui.painter().layout_no_wrap(n.to_string(), font.clone(), t.text).size().x).fold(0.0_f32, f32::max);
|
|
egui::ScrollArea::vertical().max_height(260.0).auto_shrink([false, false]).show(ui, |ui| {
|
|
if rows.is_empty() {
|
|
ui.label(egui::RichText::new(language.tr("No functions match.")).italics());
|
|
}
|
|
let row_h = ui.text_style_height(&egui::TextStyle::Body) + 4.0;
|
|
for (i, (name, category)) in rows.iter().enumerate() {
|
|
let (rect, r) = ui.allocate_exact_size(vec2(ui.available_width().max(name_w + 120.0), row_h), egui::Sense::click());
|
|
if i == d.list_index {
|
|
ui.painter().rect_filled(rect, 3.0, ui.visuals().selection.bg_fill);
|
|
} else if r.hovered() {
|
|
ui.painter().rect_filled(rect, 3.0, t.hover);
|
|
}
|
|
let color = if i == d.list_index { ui.visuals().selection.stroke.color } else { t.text };
|
|
ui.painter().text(rect.left_center() + vec2(6.0, 0.0), egui::Align2::LEFT_CENTER, *name, font.clone(), color);
|
|
ui.painter().text(rect.left_center() + vec2(6.0 + name_w + 16.0, 0.0), egui::Align2::LEFT_CENTER, *category, font.clone(), t.text_dim);
|
|
r.widget_info(|| egui::WidgetInfo::selected(egui::Role::Button, true, i == d.list_index, format!("{name} ({category})")));
|
|
if r.clicked() {
|
|
d.list_index = i;
|
|
}
|
|
if r.double_clicked() {
|
|
d.list_index = i;
|
|
*confirm = true;
|
|
}
|
|
}
|
|
});
|
|
if let Some(f) = hits.get(d.list_index) {
|
|
ui.separator();
|
|
ui.label(egui::RichText::new(crate::formula_locale::signature(&f.name, locale, language).unwrap_or_default()).strong());
|
|
ui.label(crate::formula_locale::description(&f.name, locale, language).unwrap_or_default());
|
|
}
|
|
let mut open = true;
|
|
ok_cancel(ui, confirm, &mut open);
|
|
// The function counts as recently used once the formula is entered (the engine notes it).
|
|
if *confirm && let Some(f) = hits.get(d.list_index) {
|
|
let n = app
|
|
.session
|
|
.active()
|
|
.map(|doc| crate::formula_locale::completion_name(&f.name, locale, &doc.wb, doc.wb.active_sheet()))
|
|
.unwrap_or(&f.name);
|
|
let cur = app.editor.as_ref().map(|e| e.text.clone());
|
|
let text = match cur {
|
|
Some(t) if t.starts_with('=') => format!("{t}{n}("),
|
|
_ => format!("={n}("),
|
|
};
|
|
app.begin_edit(Some(text), false);
|
|
}
|
|
if !open {
|
|
*confirm = true;
|
|
}
|
|
}
|