mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-28 20:56:54 -06:00
Merge PR #1277: Zoom dialog: the reference app's layout, current zoom pre-selected, Fit selection (#930)
This commit is contained in:
@@ -47,13 +47,28 @@ impl Dialog {
|
||||
&self.name
|
||||
}
|
||||
|
||||
/// Sets a form value. In Zoom, a Magnification choice is also the percentage `view.zoom` stores.
|
||||
/// Sets a form value. In Zoom, a Magnification choice is also the percentage `view.zoom`
|
||||
/// stores, a typed percentage selects Custom, and Fit selection makes OK run
|
||||
/// `view.zoomToSelection` (its view size was captured when the dialog opened).
|
||||
pub(crate) fn set_value(&mut self, key: &str, value: Json) {
|
||||
if self.name == "zoom"
|
||||
&& key == "preset"
|
||||
&& let Some(p) = value.as_str().and_then(|s| s.parse::<f64>().ok())
|
||||
{
|
||||
self.values.insert("percent".into(), json!(p));
|
||||
if self.name == "zoom" {
|
||||
match key {
|
||||
"preset" => match value.as_str() {
|
||||
Some("fit") => self.command = Some("view.zoomToSelection"),
|
||||
Some(s) => {
|
||||
self.command = Some("view.zoom");
|
||||
if let Ok(p) = s.parse::<f64>() {
|
||||
self.values.insert("percent".into(), json!(p));
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
},
|
||||
"percent" => {
|
||||
self.command = Some("view.zoom");
|
||||
self.values.insert("preset".into(), json!(zoom_preset(value.as_f64().unwrap_or(0.0))));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
self.values.insert(key.into(), value);
|
||||
}
|
||||
@@ -526,20 +541,19 @@ impl Dialog {
|
||||
],
|
||||
json!({"groupBy": "A", "function": "sum"}),
|
||||
),
|
||||
"zoom" => Dialog::form(
|
||||
"zoom",
|
||||
"Zoom",
|
||||
"view.zoom",
|
||||
vec![
|
||||
Choice {
|
||||
key: "preset",
|
||||
label: "Magnification",
|
||||
options: vec![("200", "200%"), ("100", "100%"), ("75", "75%"), ("50", "50%"), ("25", "25%")],
|
||||
},
|
||||
Number { key: "percent", label: "Custom (%):" },
|
||||
],
|
||||
json!({"percent": app.session.active().and_then(|d| d.wb.active().map(|s| s.zoom)).unwrap_or(100)}),
|
||||
),
|
||||
"zoom" => {
|
||||
// The current zoom is pre-selected (Custom when it matches no preset). Fit
|
||||
// selection needs the grid's size, so it is captured here for `view.zoomToSelection`.
|
||||
let zoom = app.session.active().and_then(|d| d.wb.active().map(|s| s.zoom)).unwrap_or(100);
|
||||
let view = app.grid.cells_rect.unwrap_or(egui::Rect::from_min_size(egui::pos2(0.0, 0.0), vec2(1200.0, 700.0)));
|
||||
Dialog::form(
|
||||
"zoom",
|
||||
"Zoom",
|
||||
"view.zoom",
|
||||
vec![],
|
||||
json!({"percent": zoom, "preset": zoom_preset(zoom as f64), "viewWidth": view.width(), "viewHeight": view.height()}),
|
||||
)
|
||||
}
|
||||
"defineName" => {
|
||||
// Name Manager › Edit… passes the name being edited (`oldName`, `oldScope`) and
|
||||
// its current fields; New… and Define Name start from the selection.
|
||||
@@ -987,13 +1001,6 @@ fn build_params(d: &Dialog) -> Json {
|
||||
}
|
||||
}
|
||||
}
|
||||
"zoom" => {
|
||||
if let Some(p) = d.values.get("preset").and_then(Json::as_str)
|
||||
&& d.values.get("percent").is_none()
|
||||
{
|
||||
v["percent"] = json!(p.parse::<f64>().unwrap_or(100.0));
|
||||
}
|
||||
}
|
||||
"cfQuick" => {
|
||||
let mut rule = Map::new();
|
||||
for (k, val) in &d.values {
|
||||
@@ -1037,6 +1044,7 @@ pub fn show(app: &mut SheetApp, ctx: &egui::Context) {
|
||||
"recommendedCharts" => 600.0,
|
||||
"analysisToolPak" => 420.0,
|
||||
"solver" => 460.0,
|
||||
"zoom" => 220.0,
|
||||
_ => 380.0,
|
||||
};
|
||||
let window = egui::Window::new(app.ui.language.tr(&d.title))
|
||||
@@ -1057,6 +1065,7 @@ pub fn show(app: &mut SheetApp, ctx: &egui::Context) {
|
||||
"insertFunction" => insert_function(app, ui, &mut d, &mut confirm),
|
||||
"find" => find(app, ui, &mut d),
|
||||
"goTo" => go_to(app, ui, &mut d, &mut confirm),
|
||||
"zoom" => zoom(ui, &mut d, &mut confirm, &mut open),
|
||||
"sort" => sort(app, ui, &mut d, &mut confirm),
|
||||
"nameManager" => name_manager(app, ui, &mut d, &mut open),
|
||||
"manageRules" => manage_rules(app, ui),
|
||||
@@ -1896,6 +1905,43 @@ fn find(app: &mut SheetApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Zoom's Magnification presets, as `preset` values and labels, in Excel's order.
|
||||
const ZOOM_PRESETS: [(&str, &str); 5] = [("200", "200%"), ("100", "100%"), ("75", "75%"), ("50", "50%"), ("25", "25%")];
|
||||
|
||||
/// The `preset` that shows a zoom percentage: its Magnification entry, or Custom.
|
||||
fn zoom_preset(percent: f64) -> &'static str {
|
||||
ZOOM_PRESETS.iter().map(|(val, _)| *val).find(|val| val.parse::<f64>().ok() == Some(percent)).unwrap_or("custom")
|
||||
}
|
||||
|
||||
/// View › Zoom, laid out as Excel's: one Magnification column of 200% to 25%, Fit selection and
|
||||
/// Custom with its percentage box. The current zoom is pre-selected; typing a percentage
|
||||
/// selects Custom; Fit selection zooms so the selected cells fill the grid.
|
||||
fn zoom(ui: &mut egui::Ui, d: &mut Dialog, confirm: &mut bool, open: &mut bool) {
|
||||
let lang = crate::i18n::current(ui.ctx());
|
||||
let cur = d.values.get("preset").and_then(Json::as_str).unwrap_or("custom").to_string();
|
||||
ui.label(egui::RichText::new(lang.tr("Magnification")).strong());
|
||||
for (val, text) in ZOOM_PRESETS {
|
||||
if ui.radio(cur == val, text).clicked() {
|
||||
d.set_value("preset", json!(val));
|
||||
}
|
||||
}
|
||||
if ui.radio(cur == "fit", lang.tr("Fit selection")).clicked() {
|
||||
d.set_value("preset", json!("fit"));
|
||||
}
|
||||
ui.horizontal(|ui| {
|
||||
if ui.radio(cur == "custom", format!("{}:", lang.tr("Custom"))).clicked() {
|
||||
d.set_value("preset", json!("custom"));
|
||||
}
|
||||
// Whole percentages, as Excel shows them; the engine clamps to 10..400 as well.
|
||||
let mut percent = d.values.get("percent").and_then(Json::as_f64).unwrap_or(100.0).round() as i64;
|
||||
if ui.add(egui::DragValue::new(&mut percent).range(10..=400).speed(1.0)).changed() {
|
||||
d.set_value("percent", json!(percent));
|
||||
}
|
||||
ui.label("%");
|
||||
});
|
||||
ok_cancel(ui, confirm, open);
|
||||
}
|
||||
|
||||
fn go_to(app: &mut SheetApp, ui: &mut egui::Ui, d: &mut Dialog, confirm: &mut bool) {
|
||||
let names: Vec<String> = app.session.active().map(|doc| doc.wb.names.iter().map(|n| n.name.clone()).collect()).unwrap_or_default();
|
||||
for n in names {
|
||||
|
||||
@@ -386,6 +386,8 @@ const JAPANESE: &[(&str, &str)] = &[
|
||||
("Gridlines", "目盛線"),
|
||||
("Headings", "見出し"),
|
||||
("Zoom", "ズーム"),
|
||||
("Magnification", "倍率"),
|
||||
("Fit selection", "選択範囲に合わせる"),
|
||||
("100%", "100%"),
|
||||
("Zoom to\nSelection", "選択範囲を拡大"),
|
||||
("Freeze\nPanes", "ウィンドウ枠の固定"),
|
||||
|
||||
@@ -192,6 +192,8 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"Gridlines" => "Mřížka",
|
||||
"Headings" => "Záhlaví",
|
||||
"Zoom" => "Lupa",
|
||||
"Magnification" => "Měřítko",
|
||||
"Fit selection" => "Přizpůsobit výběru",
|
||||
"Forecast\nSheet" => "List\nprognózy",
|
||||
"Add footer" => "Přidat zápatí",
|
||||
"Add header" => "Přidat záhlaví",
|
||||
|
||||
@@ -192,6 +192,8 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"Gridlines" => "Gitternetzlinien",
|
||||
"Headings" => "Überschriften",
|
||||
"Zoom" => "Zoom",
|
||||
"Magnification" => "Vergrößerung",
|
||||
"Fit selection" => "An Markierung anpassen",
|
||||
"Forecast\nSheet" => "Prognose-\nblatt",
|
||||
"Add footer" => "Fußzeile hinzufügen",
|
||||
"Add header" => "Kopfzeile hinzufügen",
|
||||
|
||||
@@ -193,6 +193,8 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"Gridlines" => "Líneas de cuadrícula",
|
||||
"Headings" => "Encabezados",
|
||||
"Zoom" => "Zoom",
|
||||
"Magnification" => "Ampliación",
|
||||
"Fit selection" => "Ajustar la selección",
|
||||
"Forecast\nSheet" => "Hoja de\npronóstico",
|
||||
"Add footer" => "Agregar pie de página",
|
||||
"Add header" => "Agregar encabezado",
|
||||
|
||||
@@ -214,6 +214,8 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"Gridlines" => "Quadrillage",
|
||||
"Headings" => "En-têtes",
|
||||
"Zoom" => "Zoom",
|
||||
"Magnification" => "Zoom",
|
||||
"Fit selection" => "Ajuster la sélection",
|
||||
"100%" => "100 %",
|
||||
"Zoom to\nSelection" => "Zoom sur la\nsélection",
|
||||
"Freeze\nPanes" => "Figer les\nvolets",
|
||||
|
||||
@@ -211,6 +211,8 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"Gridlines" => "눈금선",
|
||||
"Headings" => "머리글",
|
||||
"Zoom" => "확대/축소",
|
||||
"Magnification" => "배율",
|
||||
"Fit selection" => "선택 영역에 맞춤",
|
||||
"100%" => "100%",
|
||||
"Zoom to\nSelection" => "선택 영역에\n맞춤",
|
||||
"Freeze\nPanes" => "틀\n고정",
|
||||
|
||||
@@ -212,6 +212,7 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"Gridlines" => "Linhas de Grade",
|
||||
"Headings" => "Títulos",
|
||||
"Zoom" => "Zoom",
|
||||
"Fit selection" => "Ajustar seleção",
|
||||
"100%" => "100%",
|
||||
"Zoom to\nSelection" => "Zoom na\nSeleção",
|
||||
"Freeze\nPanes" => "Congelar\nPainéis",
|
||||
|
||||
@@ -211,6 +211,8 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"Gridlines" => "Сетка",
|
||||
"Headings" => "Заголовки",
|
||||
"Zoom" => "Масштаб",
|
||||
"Magnification" => "Масштаб",
|
||||
"Fit selection" => "По выделению",
|
||||
"100%" => "100%",
|
||||
"Zoom to\nSelection" => "Масштаб по\nвыделению",
|
||||
"Freeze\nPanes" => "Закрепить\nобласти",
|
||||
|
||||
@@ -206,6 +206,8 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"Gridlines" => "Linije mreže",
|
||||
"Headings" => "Zaglavlja",
|
||||
"Zoom" => "Uvećanje",
|
||||
"Magnification" => "Uvećanje",
|
||||
"Fit selection" => "Uklopi izbor",
|
||||
"100%" => "100%",
|
||||
"Zoom to\nSelection" => "Uvećaj na\nselekciju",
|
||||
"Freeze\nPanes" => "Zamrzni\nokna",
|
||||
|
||||
@@ -211,6 +211,8 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"Gridlines" => "网格线",
|
||||
"Headings" => "标题",
|
||||
"Zoom" => "缩放",
|
||||
"Magnification" => "缩放比例",
|
||||
"Fit selection" => "恰好容纳选定区域",
|
||||
"100%" => "100%",
|
||||
"Zoom to\nSelection" => "缩放到\n所选内容",
|
||||
"Freeze\nPanes" => "冻结\n窗格",
|
||||
|
||||
@@ -83,3 +83,65 @@ fn a_custom_percentage_after_a_magnification_wins_and_an_unedited_ok_keeps_the_z
|
||||
control(&mut h, "ui.dialog.confirm", json!({}));
|
||||
assert_eq!(zoom(&h), 225);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_current_zoom_is_preselected_as_its_magnification_or_as_custom() {
|
||||
let mut h = harness();
|
||||
h.state_mut().open_dialog("zoom", json!({}));
|
||||
h.run_steps(2);
|
||||
let d = h.state().dialog.clone().unwrap();
|
||||
assert_eq!(d.values["preset"], json!("100"));
|
||||
assert_eq!(d.values["percent"], json!(100));
|
||||
control(&mut h, "ui.dialog.cancel", json!({}));
|
||||
h.state_mut().run("view.zoom", json!({"percent": 130})).unwrap();
|
||||
h.state_mut().open_dialog("zoom", json!({}));
|
||||
h.run_steps(2);
|
||||
let d = h.state().dialog.clone().unwrap();
|
||||
assert_eq!(d.values["preset"], json!("custom"));
|
||||
assert_eq!(d.values["percent"], json!(130));
|
||||
// An unedited OK keeps the zoom.
|
||||
let r = control(&mut h, "ui.dialog.confirm", json!({}));
|
||||
assert_eq!(r["ok"], json!(true), "{r}");
|
||||
assert_eq!(zoom(&h), 130);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_typed_percentage_selects_custom_and_sets_that_zoom() {
|
||||
let mut h = harness();
|
||||
control(&mut h, "ui.dialog", json!({"name": "zoom"}));
|
||||
control(&mut h, "ui.dialog.set", json!({"field": "preset", "value": "fit"}));
|
||||
control(&mut h, "ui.dialog.set", json!({"field": "percent", "value": 150}));
|
||||
assert_eq!(h.state().dialog.as_ref().unwrap().values["preset"], json!("custom"));
|
||||
let r = control(&mut h, "ui.dialog.confirm", json!({}));
|
||||
assert_eq!(r["ok"], json!(true), "{r}");
|
||||
assert_eq!(zoom(&h), 150);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fit_selection_zooms_so_the_selected_cells_fill_the_grid() {
|
||||
let mut h = harness();
|
||||
h.state_mut().run("selection.set", json!({"range": "A1:J40"})).unwrap();
|
||||
h.run_steps(2);
|
||||
let view = h.state().grid.cells_rect.unwrap();
|
||||
h.state_mut().open_dialog("zoom", json!({}));
|
||||
h.run_steps(6);
|
||||
h.get_by_label("Fit selection").click();
|
||||
h.run_steps(6);
|
||||
h.get_by_label(" OK ").click();
|
||||
h.run_steps(6);
|
||||
assert!(h.state().dialog.is_none());
|
||||
// The same answer the ribbon's Zoom to Selection gives for this grid.
|
||||
let mut expected = Session::new();
|
||||
expected.new_workbook();
|
||||
expected.execute("selection.set", json!({"range": "A1:J40"})).unwrap();
|
||||
let r = expected.execute("view.zoomToSelection", json!({"viewWidth": view.width(), "viewHeight": view.height()})).unwrap();
|
||||
assert_eq!(json!(zoom(&h)), r["zoom"]);
|
||||
assert!(zoom(&h) > 10 && zoom(&h) < 100, "{}", zoom(&h));
|
||||
// Fit selection over the control channel, before any frame is drawn, uses the grid size captured on opening.
|
||||
h.state_mut().run("selection.set", json!({"range": "A1"})).unwrap();
|
||||
control(&mut h, "ui.dialog", json!({"name": "zoom"}));
|
||||
control(&mut h, "ui.dialog.set", json!({"field": "preset", "value": "fit"}));
|
||||
let r = control(&mut h, "ui.dialog.confirm", json!({}));
|
||||
assert_eq!(r["ok"], json!(true), "{r}");
|
||||
assert_eq!(zoom(&h), 400);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user