//! The Insert menu: pivot tables and charts (TABLE-10, 11). use ely_gpui_component::forms::TextInput; use ely_lok::sheet::{self, Chart, Layout, Pivot, Selection}; use ely_office::prelude::*; use gpui::{ App, Context, Div, Entity, InteractiveElement as _, Menu, MenuItem, Window, actions, }; use crate::book::Book; use crate::dialog::Dialog; mod dialogs; pub use dialogs::{chart_dialog, pivot_dialog}; actions!(table, [MakePivot, RefreshPivot, MakeChart]); pub fn menu(cx: &App) -> Menu { let label = |key: &str| Book::text(cx, key, &[]); Menu::new(label("menu.insert")).items([ MenuItem::action(label("table.pivot.make"), MakePivot), MenuItem::action(label("table.pivot.refresh"), RefreshPivot), MenuItem::separator(), MenuItem::action(label("table.chart.make"), MakeChart), ]) } pub fn bind(root: Div, cx: &mut Context) -> Div { root.on_action(cx.listener(|book, _: &MakePivot, _, cx| book.ask(Asked::Pivot, cx))) .on_action(cx.listener(|book, _: &RefreshPivot, _, cx| book.ask(Asked::Refresh, cx))) .on_action(cx.listener(|book, _: &MakeChart, _, cx| book.ask(Asked::Chart, cx))) } #[derive(Clone, Copy, Debug)] pub(crate) enum Asked { Pivot, Refresh, Chart, } /// A chart dialog's text fields. pub struct ChartFields { pub range: Entity, pub look: ely_office::chart::LookFields, } /// What the kernel holds where the user asked. enum Found { Pivot(Option), Chart(Option), } impl Book { pub(crate) fn ask(&mut self, asked: Asked, cx: &mut Context) { let Some(id) = self.core.active else { return self.warn_key("command.blocked.no_document", cx); }; self.ask_here(id, asked, cx); } /// Reads the pivot table or chart here, after input. fn ask_here(&mut self, id: u64, asked: Asked, cx: &mut Context) { if !self.settled(id, cx) { return self.when_settled(id, move |book, _, cx| book.ask_here(id, asked, cx)); } self.ask_in( id, move |worker, doc| match asked { Asked::Chart => Ok(Found::Chart(sheet::chart( &worker.values(doc, sheet::CHART)?, )?)), _ => Ok(Found::Pivot(sheet::pivot( &worker.values(doc, sheet::PIVOT)?, )?)), }, move |book, read, window, cx| match (asked, read) { (_, Err(error)) => book.engine_failed(&error.to_string(), cx), (Asked::Refresh, Ok(Found::Pivot(Some(_)))) => { book.run_in(Some(id), "pivot.refresh", &[], cx) } (Asked::Refresh, Ok(_)) => book.warn_key("table.pivot.none", cx), (_, Ok(Found::Pivot(Some(pivot)))) => { book.open_dialog(Dialog::Pivot { tab: id, source: pivot.source, fields: pivot.fields, layout: pivot.layout, editing: true, }); cx.notify(); } (_, Ok(Found::Chart(Some(chart)))) => { book.chart_dialog(id, Some(chart), "", window, cx) } (_, Ok(_)) => book.data_of( id, true, move |book, picked, _, window, cx| book.new_dialog(id, asked, picked, window, cx), cx, ), }, cx, ); } /// A create dialog over the picked data. fn new_dialog( &mut self, id: u64, asked: Asked, picked: Selection, window: &mut Window, cx: &mut Context, ) { log::info!("table: {asked:?} from {}", picked.range); if let Asked::Chart = asked { return self.chart_dialog(id, None, &picked.data, window, cx); } if picked.top.iter().any(String::is_empty) { return self.warn_key("table.pivot.headers", cx); } self.open_dialog(Dialog::Pivot { tab: id, source: picked.data, fields: picked.top, layout: Layout::default(), editing: false, }); cx.notify(); } fn chart_dialog( &mut self, id: u64, chart: Option, range: &str, window: &mut Window, cx: &mut Context, ) { let look = chart .as_ref() .map_or_else(ely_office::chart::fresh, |chart| chart.look.clone()); let range = chart.as_ref().map_or(range, |chart| &chart.range); let fields = ChartFields { range: ely_office::chart::field(range, "table.chart.range", window, cx), look: ely_office::chart::LookFields::new(&look, window, cx), }; self.open_dialog(Dialog::Chart { tab: id, chart: chart.map(Box::new), look: Box::new(look), fields: Box::new(fields), }); cx.notify(); } /// Makes or changes the pivot table the dialog describes. pub(crate) fn pivot_asked(&mut self, cx: &mut Context) { let Some(Dialog::Pivot { tab, source, layout, editing, .. }) = self.dialog.take() else { return; }; let source = if editing { "" } else { source.as_str() }; self.run_in(Some(tab), "pivot.make", &[&layout.to_json(source, "")], cx); } /// Inserts or changes the chart the dialog describes. pub(crate) fn chart_asked(&mut self, cx: &mut Context) { let Some(Dialog::Chart { tab, chart, look, fields, }) = self.dialog.take() else { return; }; let look = fields.look.read(&look, cx).to_json(); let range = fields.range.read(cx).text().trim().to_string(); match chart { Some(chart) => { let headers = |on: bool| if on { "true" } else { "false" }; let values = [ chart.name.as_str(), &range, headers(chart.col_headers), headers(chart.row_headers), &look, ]; self.run_in(Some(tab), "chart.change", &values, cx) } None => self.run_in(Some(tab), "chart.insert", &[&range, "false", &look], cx), } } }