diff --git a/crates/app/src/shot.rs b/crates/app/src/shot.rs index 8e4f29a..074cebc 100644 --- a/crates/app/src/shot.rs +++ b/crates/app/src/shot.rs @@ -13,7 +13,7 @@ //! restrict the run to a single palette. Captures land at //! `//.png`. -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use std::sync::Arc; use std::time::{Duration, Instant}; @@ -31,6 +31,7 @@ use plotx_io::xps::{ }; use plotx_io::{AxisSource, Dim, Domain, NmrData, NmrData2D, PseudoAxis, PseudoKind, QuadMode}; +mod capture; mod craft_shot; const FIT_LO: f64 = 1.4; @@ -88,6 +89,7 @@ enum Op { RegionResult, /// Open the result's synchronized read-only values. RegionData, + History(bool), XpsSetup, CraftSetup, XpsTab(plotx_core::state::XpsWorkbenchTab), @@ -136,6 +138,9 @@ const SCENES: &[Scene] = &[ act(2, Op::Zoom(0.75)), act(2, Op::Setup), shot(8, "band"), + act(2, Op::History(true)), + shot(8, "operation_history"), + act(2, Op::History(false)), act(2, Op::LineFit), shot(10, "fitted"), // The three widths bracket the Ribbon's width budget: 720 steps the @@ -331,7 +336,7 @@ impl ShotDriver { .collect() }); for (rel, image) in shots { - if let Err(error) = save_png(&self.dir.join(format!("{rel}.png")), &image) { + if let Err(error) = capture::save_png(&self.dir.join(format!("{rel}.png")), &image) { self.fail(app, ctx, format!("failed to save {rel}: {error}")); return; } @@ -373,6 +378,12 @@ fn run_op(op: Op, app: &mut PlotxApp, ctx: &egui::Context) -> Result<(), String> Op::DeltaCursor => delta_cursor(app)?, Op::PinSymmetry => pin_symmetry(app)?, Op::RegionResult => region_result(app), + Op::History(open) => { + app.session.ui.diagnostics_open = open; + ctx.data_mut(|data| { + data.insert_temp(egui::Id::new("operation_history_messages_tab"), true) + }); + } Op::RegionData => { app.session.ui.sheet_open = Some(1); app.session.ui.curve_fit_task_collapsed = true; @@ -754,43 +765,6 @@ fn synthetic_cosy() -> plotx_io::NmrData2D { } } -fn save_png(path: &Path, image: &egui::ColorImage) -> Result<(), String> { - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent) - .map_err(|error| format!("create {}: {error}", parent.display()))?; - } - let [width, height] = image.size; - // egui screenshots are opaque RGBA8, so straight-alpha encoding is exact. - image::save_buffer_with_format( - path, - image.as_raw(), - width as u32, - height as u32, - image::ColorType::Rgba8, - image::ImageFormat::Png, - ) - .map_err(|error| format!("encode {}: {error}", path.display())) -} - #[cfg(test)] -mod tests { - use super::*; - - #[test] - fn automated_exit_bypasses_dirty_project_prompt() { - let mut app = PlotxApp::new_with_settings(plotx_core::settings::Settings::default()); - app.mark_document_dirty(); - let ctx = egui::Context::default(); - - let output = ctx.run_ui(egui::RawInput::default(), |ui| { - request_exit(&mut app, ui.ctx()); - }); - - assert!(app.session.allow_close); - let root = output - .viewport_output - .get(&egui::ViewportId::ROOT) - .expect("root viewport output"); - assert!(root.commands.contains(&egui::ViewportCommand::Close)); - } -} +#[path = "shot/tests.rs"] +mod tests; diff --git a/crates/app/src/shot/capture.rs b/crates/app/src/shot/capture.rs new file mode 100644 index 0000000..c74404c --- /dev/null +++ b/crates/app/src/shot/capture.rs @@ -0,0 +1,19 @@ +use std::path::Path; + +pub(super) fn save_png(path: &Path, image: &egui::ColorImage) -> Result<(), String> { + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent) + .map_err(|error| format!("create {}: {error}", parent.display()))?; + } + let [width, height] = image.size; + // egui screenshots are opaque RGBA8, so straight-alpha encoding is exact. + image::save_buffer_with_format( + path, + image.as_raw(), + width as u32, + height as u32, + image::ColorType::Rgba8, + image::ImageFormat::Png, + ) + .map_err(|error| format!("encode {}: {error}", path.display())) +} diff --git a/crates/app/src/shot/tests.rs b/crates/app/src/shot/tests.rs new file mode 100644 index 0000000..7cde2dd --- /dev/null +++ b/crates/app/src/shot/tests.rs @@ -0,0 +1,19 @@ +use super::*; + +#[test] +fn automated_exit_bypasses_dirty_project_prompt() { + let mut app = PlotxApp::new_with_settings(plotx_core::settings::Settings::default()); + app.mark_document_dirty(); + let ctx = egui::Context::default(); + + let output = ctx.run_ui(egui::RawInput::default(), |ui| { + request_exit(&mut app, ui.ctx()); + }); + + assert!(app.session.allow_close); + let root = output + .viewport_output + .get(&egui::ViewportId::ROOT) + .expect("root viewport output"); + assert!(root.commands.contains(&egui::ViewportCommand::Close)); +} diff --git a/crates/app/src/ui/activity.rs b/crates/app/src/ui/activity.rs new file mode 100644 index 0000000..4ef2196 --- /dev/null +++ b/crates/app/src/ui/activity.rs @@ -0,0 +1,90 @@ +use egui::Ui; +use egui_phosphor::regular as icon; +use plotx_core::state::PlotxApp; + +use super::clipboard_table::ClipboardTablePaste; +use super::commands::{self, CommandId}; + +pub(super) fn messages_tab_id() -> egui::Id { + egui::Id::new("operation_history_messages_tab") +} + +pub(super) fn observe(app: &mut PlotxApp) { + app.session.status_history.observe(&app.session.status); +} + +pub(crate) fn open_diagnostics(app: &mut PlotxApp, ctx: &egui::Context) { + app.session.ui.diagnostics_open = true; + ctx.data_mut(|data| data.insert_temp(messages_tab_id(), false)); +} + +pub(super) fn history_button(app: &mut PlotxApp, clipboard: &mut ClipboardTablePaste, ui: &mut Ui) { + let response = ui + .add_sized( + [ + super::ribbon_chrome::SIDEBAR_TOGGLE_WIDTH, + ui.spacing().interact_size.y, + ], + egui::Button::new(icon::CLOCK_COUNTER_CLOCKWISE) + .frame_when_inactive(false) + .selected(app.session.ui.diagnostics_open), + ) + .on_hover_text(format!("Operation history\n{}", app.session.status)); + if super::pending_feedback(app).is_some() { + ui.painter().circle_filled( + response.rect.right_top() + egui::vec2(-5.0, 5.0), + 2.5, + ui.visuals().warn_fg_color, + ); + } + if response.clicked() { + commands::execute(CommandId::OperationHistory, app, clipboard, ui.ctx()); + ui.ctx() + .data_mut(|data| data.insert_temp(messages_tab_id(), true)); + } +} + +pub(super) fn messages(app: &PlotxApp, ui: &mut Ui) { + let count = app.session.status_history.messages().len(); + ui.vertical(|ui| { + if count == 0 { + ui.weak("No messages yet. Open data to begin."); + } + for message in app.session.status_history.messages().rev() { + let elapsed = message.recorded_at.elapsed().unwrap_or_default().as_secs(); + ui.horizontal_top(|ui| { + ui.add_sized( + [56.0, ui.spacing().interact_size.y], + egui::Label::new( + crate::typography::caption(format!( + "{}:{:02} ago", + elapsed / 60, + elapsed % 60 + )) + .color(ui.visuals().weak_text_color()), + ), + ); + ui.add(egui::Label::new(&message.text).wrap().selectable(true)); + }); + ui.separator(); + } + }); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn diagnostic_links_reveal_details_after_browsing_messages() { + let ctx = egui::Context::default(); + let mut app = PlotxApp::new_with_settings(plotx_core::settings::Settings::default()); + ctx.data_mut(|data| data.insert_temp(messages_tab_id(), true)); + open_diagnostics(&mut app, &ctx); + assert!(app.session.ui.diagnostics_open); + assert_eq!( + ctx.data(|data| data.get_temp::(messages_tab_id())), + Some(false) + ); + } +} diff --git a/crates/app/src/ui/command_exec.rs b/crates/app/src/ui/command_exec.rs index 3c96c63..7931ba5 100644 --- a/crates/app/src/ui/command_exec.rs +++ b/crates/app/src/ui/command_exec.rs @@ -164,7 +164,7 @@ fn execute_inner( app.session.updates.check_now(); app.open_settings(); } - CommandId::OperationHistory => app.session.ui.diagnostics_open = true, + CommandId::OperationHistory => super::activity::open_diagnostics(app, ctx), CommandId::About => app.session.ui.about_open = true, CommandId::SaveProcessingTemplate | CommandId::ApplyProcessingTemplate => { if let Some(dataset) = app.active_dataset() { diff --git a/crates/app/src/ui/diagnostics.rs b/crates/app/src/ui/diagnostics.rs index 89bbf88..c95ef51 100644 --- a/crates/app/src/ui/diagnostics.rs +++ b/crates/app/src/ui/diagnostics.rs @@ -4,9 +4,12 @@ pub(super) fn diagnostic_history_window(app: &mut PlotxApp, ctx: &egui::Context) if !app.session.ui.diagnostics_open { return; } + super::activity::observe(app); let mut open = true; let mut clear = false; + let tab_id = super::activity::messages_tab_id(); + let mut messages_tab = ctx.data(|data| data.get_temp::(tab_id).unwrap_or(false)); let copied_text = app.session.sanitized_diagnostics_text(); let window = egui::Window::new("Operation history") .default_width(620.0) @@ -41,44 +44,60 @@ pub(super) fn diagnostic_history_window(app: &mut PlotxApp, ctx: &egui::Context) }); }); ui.separator(); - egui::ScrollArea::vertical().show(ui, |ui| { - let mut any = false; - for operation in app.session.operation_history.operations().rev() { - any = true; - ui.group(|ui| { - ui.horizontal_wrapped(|ui| { - ui.label(crate::typography::headline(format!( - "#{} {}", - operation.id, - operation.kind.as_str() - ))); - ui.label(operation.outcome.as_str()); - }); - ui.label(&operation.summary); - for diagnostic in &operation.diagnostics { + ui.add(egui::Label::new(crate::typography::callout(&app.session.status)).wrap()); + if let Some(di) = app.active_dataset() { + ui.weak(app.doc.datasets[di].summary()); + } + ui.horizontal(|ui| { + ui.selectable_value(&mut messages_tab, true, "Messages"); + ui.selectable_value(&mut messages_tab, false, "Diagnostics"); + }); + ui.separator(); + egui::ScrollArea::vertical() + .id_salt(("history", messages_tab)) + .show(ui, |ui| { + if messages_tab { + super::activity::messages(app, ui); + return; + } + let mut any = false; + for operation in app.session.operation_history.operations().rev() { + any = true; + ui.group(|ui| { ui.horizontal_wrapped(|ui| { ui.label(crate::typography::headline(format!( - "{} {}", - diagnostic.severity.as_str(), - diagnostic.code.as_str() + "#{} {}", + operation.id, + operation.kind.as_str() ))); - ui.label(&diagnostic.message); + ui.label(operation.outcome.as_str()); }); - if let Some(source) = &diagnostic.source { - ui.weak(format!("source: {source}")); - } - for (key, value) in &diagnostic.context { - ui.weak(format!("{key}: {value}")); + ui.label(&operation.summary); + for diagnostic in &operation.diagnostics { + ui.horizontal_wrapped(|ui| { + ui.label(crate::typography::headline(format!( + "{} {}", + diagnostic.severity.as_str(), + diagnostic.code.as_str() + ))); + ui.label(&diagnostic.message); + }); + if let Some(source) = &diagnostic.source { + ui.weak(format!("source: {source}")); + } + for (key, value) in &diagnostic.context { + ui.weak(format!("{key}: {value}")); + } } - } - }); - ui.add_space(6.0); - } - if !any { - ui.weak("No structured operations have been recorded yet."); - } - }); + }); + ui.add_space(6.0); + } + if !any { + ui.weak("No structured operations have been recorded yet."); + } + }); }); + ctx.data_mut(|data| data.insert_temp(tab_id, messages_tab)); if clear { app.session.clear_operation_history(); diff --git a/crates/app/src/ui/mod.rs b/crates/app/src/ui/mod.rs index e47e927..4d0fbf2 100644 --- a/crates/app/src/ui/mod.rs +++ b/crates/app/src/ui/mod.rs @@ -1,3 +1,4 @@ +mod activity; pub(crate) mod affordance; pub(crate) mod align; pub(crate) mod arithmetic; @@ -149,7 +150,7 @@ pub fn render( }); feedback_banner(app, ui, dark); - render_status(app, ui, dark); + activity::observe(app); let workspace_width = ui.available_width(); sidebars::render(app, ui, dark, workspace_width); @@ -214,6 +215,7 @@ pub fn render( let now = ctx.input(|i| i.time); app.finish_pending_wheel_zoom(now, false); app.finish_pending_wheel_property(now, false); + activity::observe(app); } fn project_window_title(app: &PlotxApp) -> String { @@ -243,45 +245,6 @@ fn copy_table_export(ctx: &egui::Context, payload: plotx_core::data_export::Clip ctx.copy_text(payload.text); } -fn render_status(app: &PlotxApp, ui: &mut Ui, dark: bool) { - if !app.settings.appearance.show_status_bar { - return; - } - egui::Panel::bottom("status") - .frame( - card_frame( - dark, - egui::Margin { - left: 8, - right: 8, - top: 4, - bottom: 8, - }, - ) - .inner_margin(egui::Margin::symmetric(10, 4)), - ) - .show_separator_line(false) - .show_inside(ui, |ui| { - ui.horizontal(|ui| { - let show_summary = ui.available_width() > 460.0; - ui.add( - egui::Label::new(crate::typography::callout(&app.session.status)) - .truncate() - .sense(Sense::hover()), - ) - .on_hover_text(&app.session.status); - if show_summary && let Some(di) = app.active_dataset() { - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - ui.label( - crate::typography::callout(app.doc.datasets[di].summary()) - .color(ui.visuals().weak_text_color()), - ); - }); - } - }); - }); -} - /// The newest *unacknowledged* warning/failure plus how many older ones are /// still pending. Selecting by outcome — not recency of the whole history — /// means a success landing right after a failure (the second file of a batch, @@ -374,7 +337,7 @@ fn feedback_banner(app: &mut PlotxApp, ui: &mut Ui, dark: bool) { app.session.ui.dismissed_feedback_order = Some(completion_order); } if ui.button("Details").clicked() { - app.session.ui.diagnostics_open = true; + activity::open_diagnostics(app, ui.ctx()); } }, ); @@ -623,37 +586,6 @@ fn flush_frame(dark: bool, inner_margin: egui::Margin) -> egui::Frame { #[path = "sidebar_tests.rs"] mod sidebar_tests; -#[cfg(test)] -mod status_bar_tests { - use super::*; - use egui::{Pos2, RawInput, Rect, vec2}; - - fn remaining_height(app: &PlotxApp) -> f32 { - let ctx = crate::typography::test_context(); - let input = RawInput { - screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(800.0, 600.0))), - ..Default::default() - }; - let mut height = 0.0; - let _ = ctx.run_ui(input, |ui| { - render_status(app, ui, false); - height = ui.available_height(); - }); - height - } - - #[test] - fn status_bar_only_reserves_workspace_when_enabled() { - let mut app = PlotxApp::new_with_settings(plotx_core::settings::Settings::default()); - let hidden_height = remaining_height(&app); - - app.settings.appearance.show_status_bar = true; - let shown_height = remaining_height(&app); - - assert!(shown_height < hidden_height); - } -} - #[cfg(test)] mod feedback_tests { use super::*; diff --git a/crates/app/src/ui/properties/mod.rs b/crates/app/src/ui/properties/mod.rs index 74af151..acb028f 100644 --- a/crates/app/src/ui/properties/mod.rs +++ b/crates/app/src/ui/properties/mod.rs @@ -644,12 +644,6 @@ pub const PRESENTATIONS: &[PropertyPresentation] = &[ &[LocalizedText("appearance theme")], APPEARANCE_PREFERENCES_HOME, ), - preference_entry( - app_preferences::SHOW_STATUS_BAR, - "Show status bar", - &[LocalizedText("bottom status")], - APPEARANCE_PREFERENCES_HOME, - ), preference_entry( app_preferences::GRAPHICS_POWER, "Graphics processor", diff --git a/crates/app/src/ui/properties/tests.rs b/crates/app/src/ui/properties/tests.rs index 0dcabe3..f87b0b8 100644 --- a/crates/app/src/ui/properties/tests.rs +++ b/crates/app/src/ui/properties/tests.rs @@ -319,8 +319,8 @@ fn migrated_preferences_keep_their_real_section_density() { ), ( SettingsCategory::Appearance.section_id(), - 4, - "theme, status bar, GPU, and the accent override row", + 3, + "theme, GPU, and the accent override row", ), ( SettingsCategory::Processing.section_id(), diff --git a/crates/app/src/ui/ribbon.rs b/crates/app/src/ui/ribbon.rs index 9116ecd..7035224 100644 --- a/crates/app/src/ui/ribbon.rs +++ b/crates/app/src/ui/ribbon.rs @@ -197,6 +197,7 @@ fn render_chrome_controls( app.session.ui.ribbon_expanded = !app.session.ui.ribbon_expanded; } update_button(app, ui, compact_controls); + super::activity::history_button(app, clipboard, ui); let palette = commands::describe(app, CommandId::CommandPalette); let search_label = if compact_controls { icon::MAGNIFYING_GLASS.to_owned() diff --git a/crates/app/src/ui/ribbon_chrome.rs b/crates/app/src/ui/ribbon_chrome.rs index 4539820..0db873d 100644 --- a/crates/app/src/ui/ribbon_chrome.rs +++ b/crates/app/src/ui/ribbon_chrome.rs @@ -145,6 +145,8 @@ fn controls_width(app: &PlotxApp, ui: &Ui, compact: bool) -> f32 { + 2.0 * SIDEBAR_TOGGLE_WIDTH + 6.0 + 5.0 * spacing + // The history button and its gap. + + SIDEBAR_TOGGLE_WIDTH + CONTROL_SPACING } /// Fixed width of one sidebar layout toggle; shared with the width estimate diff --git a/crates/app/src/ui/windows/project.rs b/crates/app/src/ui/windows/project.rs index 9f8d3f5..ce881c8 100644 --- a/crates/app/src/ui/windows/project.rs +++ b/crates/app/src/ui/windows/project.rs @@ -17,7 +17,7 @@ pub(in crate::ui) fn save_project_window(app: &mut PlotxApp, ctx: &egui::Context if app.session.status.starts_with("Save failed:") { ui.colored_label(ui.visuals().error_fg_color, &app.session.status); if ui.link("Open diagnostic details").clicked() { - app.session.ui.diagnostics_open = true; + crate::ui::activity::open_diagnostics(app, ui.ctx()); } ui.add_space(8.0); } @@ -145,7 +145,7 @@ fn project_transition_dialog(app: &mut PlotxApp, ctx: &egui::Context, saving: bo .show(ui, |ui| { ui.colored_label(ui.visuals().error_fg_color, &app.session.status); if ui.link("Open diagnostic details").clicked() { - app.session.ui.diagnostics_open = true; + crate::ui::activity::open_diagnostics(app, ui.ctx()); } }); } diff --git a/crates/core/src/properties/app_preferences.rs b/crates/core/src/properties/app_preferences.rs index 89da461..043c962 100644 --- a/crates/core/src/properties/app_preferences.rs +++ b/crates/core/src/properties/app_preferences.rs @@ -21,7 +21,6 @@ pub const KEEP_EMPTY_SOURCE_CANVAS: PropertyId = pub const PROJECT_BACKUP_GENERATIONS: PropertyId = PropertyId("settings.general.project_backup_generations"); pub const THEME: PropertyId = PropertyId("settings.appearance.theme"); -pub const SHOW_STATUS_BAR: PropertyId = PropertyId("settings.appearance.show_status_bar"); pub const GRAPHICS_POWER: PropertyId = PropertyId("settings.appearance.graphics_power"); pub const ACCENT_COLOR: PropertyId = PropertyId("settings.appearance.accent.color"); pub const INCLUDE_VIEW_SNAPSHOTS: PropertyId = PropertyId("settings.export.include_view_snapshots"); @@ -146,13 +145,6 @@ pub(crate) const DEFINITIONS: &[PropertyDefinition] = &[ "Chrome theme", &["appearance theme", "light mode", "dark mode"], ), - app_definition( - SHOW_STATUS_BAR, - ValueSchema::Bool, - DefaultPolicy::Fixed(PropertyValue::Bool(false)), - "Show status bar", - &["status bar", "bottom status"], - ), app_definition( GRAPHICS_POWER, ValueSchema::Enum { @@ -324,7 +316,6 @@ fn value_of(app: &PlotxApp, id: PropertyId) -> Result PropertyValue::Enum(theme_key(settings.appearance.theme)), - SHOW_STATUS_BAR => PropertyValue::Bool(settings.appearance.show_status_bar), GRAPHICS_POWER => PropertyValue::Enum(graphics_key(settings.appearance.graphics_power)), ACCENT_COLOR => { let [r, g, b] = settings.appearance.canvas_accent.unwrap_or([ @@ -399,9 +390,6 @@ fn write_value( (THEME, PropertyValue::Enum(value)) => { settings.appearance.theme = theme(value).expect("validated theme") } - (SHOW_STATUS_BAR, PropertyValue::Bool(value)) => { - settings.appearance.show_status_bar = value - } (GRAPHICS_POWER, PropertyValue::Enum(value)) => { settings.appearance.graphics_power = graphics(value).expect("validated graphics power") } diff --git a/crates/core/src/properties/app_preferences_tests.rs b/crates/core/src/properties/app_preferences_tests.rs index c0b461b..46b7b88 100644 --- a/crates/core/src/properties/app_preferences_tests.rs +++ b/crates/core/src/properties/app_preferences_tests.rs @@ -133,7 +133,6 @@ fn all_thirteen_app_preferences_reset_through_their_catalog_definitions() { settings.general.keep_empty_source_canvas = true; settings.general.project_backup_generations = MAX_PROJECT_BACKUP_GENERATIONS; settings.appearance.theme = ThemeMode::Dark; - settings.appearance.show_status_bar = true; settings.appearance.graphics_power = GraphicsPowerPreference::HighPerformance; settings.appearance.canvas_accent = Some([12, 34, 56]); settings.export.include_view_snapshots = true; @@ -149,7 +148,6 @@ fn all_thirteen_app_preferences_reset_through_their_catalog_definitions() { KEEP_EMPTY_SOURCE_CANVAS, PROJECT_BACKUP_GENERATIONS, THEME, - SHOW_STATUS_BAR, GRAPHICS_POWER, ACCENT_COLOR, INCLUDE_VIEW_SNAPSHOTS, @@ -172,10 +170,6 @@ fn all_thirteen_app_preferences_reset_through_their_catalog_definitions() { let defaults = Settings::default(); assert_eq!(app.settings.general, defaults.general); assert_eq!(app.settings.appearance.theme, defaults.appearance.theme); - assert_eq!( - app.settings.appearance.show_status_bar, - defaults.appearance.show_status_bar - ); assert_eq!( app.settings.appearance.graphics_power, defaults.appearance.graphics_power diff --git a/crates/core/src/settings/model.rs b/crates/core/src/settings/model.rs index 4345d3a..d4754f3 100644 --- a/crates/core/src/settings/model.rs +++ b/crates/core/src/settings/model.rs @@ -87,8 +87,6 @@ pub struct AppearanceSettings { #[serde(default)] pub theme: ThemeMode, #[serde(default)] - pub show_status_bar: bool, - #[serde(default)] pub ui_scale: UiScaleSettings, #[serde(default)] pub graphics_power: GraphicsPowerPreference, diff --git a/crates/core/src/settings/tests.rs b/crates/core/src/settings/tests.rs index 6654e44..53a4f76 100644 --- a/crates/core/src/settings/tests.rs +++ b/crates/core/src/settings/tests.rs @@ -20,7 +20,6 @@ fn missing_fields_take_defaults() { settings.appearance.graphics_power, GraphicsPowerPreference::LowPower ); - assert!(!settings.appearance.show_status_bar); assert_eq!(settings.window.task_cards.craft.width, 520.0); assert_eq!(settings.window.task_cards.processing.width, 340.0); } @@ -104,7 +103,6 @@ fn save_and_load_roundtrip() { settings.general.project_backup_generations = 3; settings.export.include_view_snapshots = true; settings.export.trim_to_visible_content = true; - settings.appearance.show_status_bar = true; settings.window.task_cards.craft = TaskCardSize::new(612.0, 688.0); io::save_to_path(&path, &settings).unwrap(); @@ -116,7 +114,6 @@ fn save_and_load_roundtrip() { assert_eq!(loaded.general.project_backup_generations, 3); assert!(loaded.export.include_view_snapshots); assert!(loaded.export.trim_to_visible_content); - assert!(loaded.appearance.show_status_bar); assert_eq!( loaded.window.task_cards.craft, TaskCardSize::new(612.0, 688.0) diff --git a/crates/core/src/state/app_impl.rs b/crates/core/src/state/app_impl.rs index 2b828f8..7857f68 100644 --- a/crates/core/src/state/app_impl.rs +++ b/crates/core/src/state/app_impl.rs @@ -66,6 +66,7 @@ impl PlotxApp { secondary_sidebar_visible: true, status: "Open data or a project to begin.".into(), operation_history: OperationHistory::default(), + status_history: super::StatusHistory::default(), project_load_warnings: Vec::new(), recent_files: { let mut files = settings.recent.files.clone(); diff --git a/crates/core/src/state/mod.rs b/crates/core/src/state/mod.rs index 5983620..a97258a 100644 --- a/crates/core/src/state/mod.rs +++ b/crates/core/src/state/mod.rs @@ -48,6 +48,8 @@ mod app_impl_statistics_tests; mod app_impl_symmetry; mod app_impl_xps; mod app_state; +mod status_history; +pub use status_history::{StatusHistory, StatusMessage}; mod axis_overrides; mod board; mod charts; diff --git a/crates/core/src/state/status_history.rs b/crates/core/src/state/status_history.rs new file mode 100644 index 0000000..7d111e8 --- /dev/null +++ b/crates/core/src/state/status_history.rs @@ -0,0 +1,75 @@ +use std::collections::VecDeque; +use std::time::SystemTime; + +const MESSAGE_LIMIT: usize = 200; + +#[derive(Clone, Debug)] +pub struct StatusMessage { + pub recorded_at: SystemTime, + pub text: String, +} + +/// Session-only messages formerly displayed in the workspace status strip. +#[derive(Default)] +pub struct StatusHistory { + messages: VecDeque, + last_observed: String, +} + +impl StatusHistory { + pub fn observe(&mut self, status: &str) { + if self.last_observed == status { + return; + } + self.last_observed = status.to_owned(); + if status.trim().is_empty() { + return; + } + self.messages.push_back(StatusMessage { + recorded_at: SystemTime::now(), + text: status.to_owned(), + }); + if self.messages.len() > MESSAGE_LIMIT { + self.messages.pop_front(); + } + } + + pub fn messages(&self) -> impl DoubleEndedIterator + ExactSizeIterator { + self.messages.iter() + } + + pub fn clear(&mut self) { + // Keep the observation watermark so clearing does not reinsert the current message. + self.messages.clear(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn observations_deduplicate_and_clear_without_reappearing() { + let mut history = StatusHistory::default(); + history.observe("Loaded"); + history.observe("Loaded"); + assert_eq!(history.messages().len(), 1); + history.clear(); + history.observe("Loaded"); + assert_eq!(history.messages().len(), 0); + history.observe("Saved"); + history.observe("Loaded"); + assert_eq!(history.messages().len(), 2); + } + + #[test] + fn retains_recent_messages_in_order_with_a_bounded_size() { + let mut history = StatusHistory::default(); + for index in 0..250 { + history.observe(&format!("Message {index}")); + } + assert_eq!(history.messages().len(), MESSAGE_LIMIT); + assert_eq!(history.messages().next().unwrap().text, "Message 50"); + assert_eq!(history.messages().next_back().unwrap().text, "Message 249"); + } +} diff --git a/crates/core/src/state/ui_state.rs b/crates/core/src/state/ui_state.rs index 7c7dd0d..98769dd 100644 --- a/crates/core/src/state/ui_state.rs +++ b/crates/core/src/state/ui_state.rs @@ -626,6 +626,7 @@ pub struct Session { pub secondary_sidebar_visible: bool, pub status: String, pub operation_history: OperationHistory, + pub status_history: super::StatusHistory, /// Non-fatal resource failures collected while opening the current project. pub project_load_warnings: Vec, /// Runtime cache of the persisted recent-files list (newest first), seeded @@ -678,16 +679,18 @@ impl Session { self.operation_history.next_id() } - /// Stores the report and projects its summary onto the legacy status line. + /// Stores the report and makes its summary the current activity message. pub fn record_operation(&mut self, report: OperationReport) -> Option { let (record, value) = report.into_parts(); self.status = record.summary.clone(); + self.status_history.observe(&self.status); self.operation_history.push(record); value } pub fn clear_operation_history(&mut self) { self.operation_history.clear(); + self.status_history.clear(); } pub fn sanitized_diagnostics_text(&self) -> String { diff --git a/docs/src/content/docs/guides/contour-levels.md b/docs/src/content/docs/guides/contour-levels.md index b7f8c60..90a28e4 100644 --- a/docs/src/content/docs/guides/contour-levels.md +++ b/docs/src/content/docs/guides/contour-levels.md @@ -55,7 +55,7 @@ ceiling on what you can ask for: choose the **Absolute level** anchor to set any level you like, including one inside the noise. If you set an **Absolute level** the data never reaches, that half draws nothing -and the status bar reports both numbers — for example, *The positive contour +and the Operation history reports both numbers — for example, *The positive contour threshold 20000 is above this field's positive peak 1800, so no positive contours are drawn. Lower the threshold below 1800.* A slipped decimal point is visible at a glance that way. @@ -86,7 +86,7 @@ corner of the plot, resolved the same way as in the panel — `5 × σ = 1.2e4`. plot whose contour series do not all sit at the same level says so instead of showing one of them. Stepping is an ordinary edit: it can be undone, and a step past the highest value the current -anchor allows is refused, with the reason in the status bar. +anchor allows is refused, with the reason in the Operation history. ## Line width @@ -136,7 +136,7 @@ A ladder can also stop early at the bottom. A level that falls inside the noise crosses most of the grid, and there is a limit to how much line one plot can draw. Past that limit PlotX drops the remaining levels whole — never cutting a contour off part-way along its own path — and drops the same levels from both -signs, so what you see is a complete ladder with a higher floor. The status bar +signs, so what you see is a complete ladder with a higher floor. The Operation history says how many went and what to set instead, for example: *The lowest 14 contour levels were not drawn: at 5.052e4 and below, this field crosses more of the grid than one plot can render. Raise the lowest level to 6.820e4 or above to see @@ -149,7 +149,7 @@ signs — the negative half mirrors the positive ladder and applies its own sign Contour geometry, and the noise or background estimate a ladder is anchored to, are computed away from the interface so a large plane never freezes the -application. Until they land the plot is empty, and the status bar says which +application. Until they land the plot is empty, and the Operation history says which step is running — *Measuring this field's noise scale…*, then *Building contour geometry…* — so a plot that is merely slow is never mistaken for one that has failed. Large planes take a moment; the plot fills in by itself. The work is @@ -193,7 +193,7 @@ the range and you get a fraction, not a noise multiple. **Reset contour** rebuilds the whole contour — anchor, ladder and colours — from the defaults for this data. It touches only the series drawn as contours; anything else in the same plot, such as a heatmap underneath, is left as it is -and reported as skipped in the status bar. +and reported as skipped in the Operation history. ## Finding a setting diff --git a/docs/src/content/docs/guides/heatmap-range.md b/docs/src/content/docs/guides/heatmap-range.md index 66bdc37..946c11b 100644 --- a/docs/src/content/docs/guides/heatmap-range.md +++ b/docs/src/content/docs/guides/heatmap-range.md @@ -30,7 +30,7 @@ returns the whole range to the field's finite minimum and maximum as they are now, rather than restoring an older number. **Reset heatmap** rebuilds the whole heatmap encoding from its defaults, and touches only the series drawn as heatmaps: contours in the same plot are left -alone and reported as skipped in the status bar. +alone and reported as skipped in the Operation history. A field with no finite values has nothing to derive a scale from, and the rows say so instead of showing a number. diff --git a/docs/src/content/docs/guides/layout-and-export.md b/docs/src/content/docs/guides/layout-and-export.md index 21bcff5..c9b75a0 100644 --- a/docs/src/content/docs/guides/layout-and-export.md +++ b/docs/src/content/docs/guides/layout-and-export.md @@ -169,7 +169,7 @@ at. If the move leaves the source page empty, PlotX deletes that page as part of the drop, so the move and the deletion undo together. Hold `Alt` as you release to -keep the empty page instead; the status bar shows which way `Alt` will flip the +keep the empty page instead; the Operation history shows which way `Alt` will flip the current drop. To keep empty source pages by default, turn on **Keep source canvas when tiling its last object** in Preferences → General — `Alt` then removes them for that one drop. @@ -195,7 +195,7 @@ There are two ways in: - Run **Simplify Inner Axes** — on the Arrange Ribbon tab, in the canvas right-click Arrange menu, or from the command palette — to simplify plots that are already in place. It needs at least two plots aligned in a grid; - otherwise the status bar says what to fix. + otherwise the Operation history says what to fix. To bring text back on one panel, select it and use **Axes** in the Object inspector: the **X text** and **Y text** rows toggle **Tick labels** and @@ -283,7 +283,7 @@ What you control directly: em dash and the word *mixed* rather than presenting one of them as the setting; setting the row is what makes them agree. A selected series that is drawn as something else — a contour, say — is reported as skipped in the - status bar, and the rest still take the value. A width that differs from the + Operation history, and the rest still take the value. A width that differs from the one PlotX would choose for this data is marked with a dot, and the reset button beside it re-derives that default. - **Canvas themes** carry matching sizes — the Presentation Dark theme, for diff --git a/docs/src/content/docs/guides/processing.md b/docs/src/content/docs/guides/processing.md index a6a8c19..f2f8d88 100644 --- a/docs/src/content/docs/guides/processing.md +++ b/docs/src/content/docs/guides/processing.md @@ -45,7 +45,7 @@ invent an FID for data it never acquired. Processing opens as a card at the upper right of the canvas — from the **Process** tab of the Ribbon, or by activating a processing setting found with Ctrl+K. If the active dataset is not an NMR dataset, the -status bar reads *Select an NMR dataset before opening Processing.* instead. +Operation history reads *Select an NMR dataset before opening Processing.* instead. The card can also host the CRAFT, Regions, Curve Fit, and Statistics tasks. When two or more tasks are open, tabs appear along the top — **Process**, diff --git a/docs/src/content/docs/guides/pseudo-2d.md b/docs/src/content/docs/guides/pseudo-2d.md index b4d72ef..73a8288 100644 --- a/docs/src/content/docs/guides/pseudo-2d.md +++ b/docs/src/content/docs/guides/pseudo-2d.md @@ -66,7 +66,7 @@ chemical-shift × diffusion map. Map builds run in the background and you can keep working; **Cancel** discards one. A map cannot be rebuilt for the same dataset until the current build finishes or is cancelled. Changing the dataset's processing while a map is building cancels the build — the map would -no longer match the spectrum. The status bar tells you when that happens; +no longer match the spectrum. The Operation history tells you when that happens; simply rebuild the map after the processing change. Changing the processing also discards a finished map for the same reason, so the plot falls back to the stack until you rebuild. @@ -81,7 +81,7 @@ Both kinds of DOSY map are saved in the `.plotx` project along with your **Show** and **DOSY method** choices, so reopening a project puts the same plot back on screen without rebuilding. If a saved map was built from data that the project's stored processing no longer reproduces, PlotX keeps showing the saved -map and warns you — in the status bar as the project opens, and in the +map and warns you — in the Operation history as the project opens, and in the **Experiment** card. If a saved map cannot be read back at all, PlotX shows the stack instead and says so in the same two places. Rebuilding the map clears the warning. diff --git a/docs/src/content/docs/reference/reporting-problems.md b/docs/src/content/docs/reference/reporting-problems.md index 2416a21..8c07ec8 100644 --- a/docs/src/content/docs/reference/reporting-problems.md +++ b/docs/src/content/docs/reference/reporting-problems.md @@ -6,7 +6,7 @@ description: Find the diagnostic files to attach when reporting a PlotX crash or ## After a crash When PlotX catches an internal error, it saves a plain-text crash report and -shows its full path. On the next launch, the recovery dialog or status bar +shows its full path. On the next launch, the recovery dialog or Operation history shows that path again. Attach the report to a [GitHub issue](https://github.com/nmrtist/plotx/issues); it contains the PlotX version, platform, panic location, backtrace, and the tail of the session log. diff --git a/docs/src/content/docs/reference/ui-overview.md b/docs/src/content/docs/reference/ui-overview.md index 8418b18..ed0ac4c 100644 --- a/docs/src/content/docs/reference/ui-overview.md +++ b/docs/src/content/docs/reference/ui-overview.md @@ -35,9 +35,11 @@ introduces the same regions in walkthrough form. task row also holds the native window controls and project name. It is a shortcut surface: everything on it is also in the menus or command palette. See [the Ribbon](/reference/ribbon/) for every tab's groups. -- **Status bar** — the bottom strip, showing hints, progress, and selection - details. It is hidden by default to leave more room for the workspace; enable - **Show status bar** under **Preferences → Appearance** when you want it. +- **Operation history** — the clock-arrow button in the Ribbon task row opens + the current status and dataset summary. **Messages** keeps the latest 200 + observed status changes for this session; **Diagnostics** shows structured + operation reports. **Clear** clears both lists. Hover the button for the + current status. Errors and warnings also appear in the feedback banner. Both Side Bars can be shown or hidden at any time: click the pair of layout buttons at the right end of the Ribbon's task row, press diff --git a/docs/src/content/docs/zh-cn/guides/contour-levels.md b/docs/src/content/docs/zh-cn/guides/contour-levels.md index a362994..e24a3f2 100644 --- a/docs/src/content/docs/zh-cn/guides/contour-levels.md +++ b/docs/src/content/docs/zh-cn/guides/contour-levels.md @@ -44,7 +44,7 @@ measured`(未测出离散度):此时倍数无从锚定,层级会退回 **Absolute level**(绝对强度)锚定即可设成任意值,包括落在噪声里的值。 如果把 **Absolute level**(绝对强度)设成数据根本达不到的值,该半区将什么都不画, -状态栏会同时给出两个数字,例如:*The positive contour threshold 20000 is above +操作历史窗口会同时给出两个数字,例如:*The positive contour threshold 20000 is above this field's positive peak 1800, so no positive contours are drawn. Lower the threshold below 1800.*(正半区阈值 20000 高于该场的正向峰值 1800,因此不绘制正 等高线;请把阈值降到 1800 以下。)小数点打错一位,一眼就能看出来。 @@ -69,7 +69,7 @@ threshold below 1800.*(正半区阈值 20000 高于该场的正向峰值 1800 `5 × σ = 1.2e4`;若图中多条等高线谱线的最低层并不一致,它会如实说明,而不是拿 其中一条冒充整体。这种调整就是一次普通 编辑:可以撤销;若一步会越过当前锚定允许的最大值,该步会被拒绝,原因显示在 -状态栏。 +操作历史窗口。 ## 线宽 @@ -110,7 +110,7 @@ threshold below 1800.*(正半区阈值 20000 高于该场的正向峰值 1800 阶梯也可能从下端提前终止。落在噪声里的层会穿过网格的大部分区域,而一幅图能绘制 的线量是有上限的。超过该上限后,PlotX 会整层丢弃剩余层级——绝不会把某条等高线 从中途截断——并且正负两个半区同时丢弃同样的层,因此你看到的始终是一组完整的、 -只是下限更高的阶梯。状态栏会说明丢弃了多少层、应改设成什么,例如:*The lowest 14 +只是下限更高的阶梯。操作历史窗口会说明丢弃了多少层、应改设成什么,例如:*The lowest 14 contour levels were not drawn: at 5.052e4 and below, this field crosses more of the grid than one plot can render. Raise the lowest level to 6.820e4 or above to see every level the panel lists.*(最低的 14 层未绘制:在 5.052e4 及以下,该场 @@ -123,7 +123,7 @@ see every level the panel lists.*(最低的 14 层未绘制:在 5.052e4 及 ## 绘制过程中 等高线几何,以及阶梯所锚定的噪声或背景估计,都在界面之外计算,因此再大的平面也 -不会卡住整个程序。在结果到达之前图上是空的,状态栏会说明当前进行到哪一步—— +不会卡住整个程序。在结果到达之前图上是空的,操作历史窗口会说明当前进行到哪一步—— *Measuring this field's noise scale…*(正在测量该场的噪声尺度)、 *Building contour geometry…*(正在构建等高线几何)——这样"只是慢"就不会被当成 "坏掉了"。大平面需要片刻,图会自行填充。这些计算是共享的:同一份数据、同一组层级 @@ -158,7 +158,7 @@ see every level the panel lists.*(最低的 14 层未绘制:在 5.052e4 及 **Reset contour**(重置等高线)会按该数据的默认值重建整条等高线——锚定、阶梯和 颜色。它只作用于以等高线绘制的谱线;同一张图里的其他内容(例如底下的热图)保持 -原样,并在状态栏中作为跳过项报告。 +原样,并在操作历史窗口中作为跳过项报告。 ## 找到某个设置 diff --git a/docs/src/content/docs/zh-cn/guides/heatmap-range.md b/docs/src/content/docs/zh-cn/guides/heatmap-range.md index 73799cf..dd7f2f3 100644 --- a/docs/src/content/docs/zh-cn/guides/heatmap-range.md +++ b/docs/src/content/docs/zh-cn/guides/heatmap-range.md @@ -24,7 +24,7 @@ Secondary Side Bar 顶部的对象检查器中。分节标题会写明即将编 被改过的行会带一个圆点标记。悬停圆点可查看 PlotX 会取的值,点旁边的重置按钮即可 还原。重置任一行都会让整个区间回到该场当前的有限最小值与最大值,而不是恢复某个 旧数值。**Reset heatmap**(重置热图)则按默认值重建整个热图编码,并且只作用于以 -热图绘制的谱线:同一幅图里的等高线不受影响,并在状态栏中报告为已跳过。 +热图绘制的谱线:同一幅图里的等高线不受影响,并在操作历史窗口中报告为已跳过。 若某个场没有有限值,就无从推导色阶,相关行会如实说明,而不是给出一个数字。 diff --git a/docs/src/content/docs/zh-cn/guides/layout-and-export.md b/docs/src/content/docs/zh-cn/guides/layout-and-export.md index e1e056b..3e640e1 100644 --- a/docs/src/content/docs/zh-cn/guides/layout-and-export.md +++ b/docs/src/content/docs/zh-cn/guides/layout-and-export.md @@ -135,7 +135,7 @@ ACS、Elsevier、PNAS 和 IEEE,数值取自各出版社的作图规范)、 拖入的图落在指针所指的那一格里。 如果这次移动把源页面清空,PlotX 会随本次拖放一并删除该页面,移动与删除 -同属一步撤销/重做。释放鼠标时按住 `Alt` 则保留这个空页面;状态栏会提示 +同属一步撤销/重做。释放鼠标时按住 `Alt` 则保留这个空页面;操作历史窗口会提示 当前这次拖放按住 `Alt` 会变成哪种结果。若希望默认保留空的源页面,可在 偏好设置 → General 中打开 **Keep source canvas when tiling its last object**——此后按住 `Alt` 就只为这一次拖放删除空页面。 @@ -157,7 +157,7 @@ object**——此后按住 `Alt` 就只为这一次拖放删除空页面。 占用原先被隐藏文字占据的空间。 - 对已经排好的图形,可从 Arrange Ribbon 选项卡、画布右键的 Arrange 菜单 或命令面板运行 **Simplify Inner Axes**。它要求至少有两个图形已对齐成 - 网格;否则状态栏会提示需要先做什么。 + 网格;否则操作历史窗口会提示需要先做什么。 若要让某个分图重新显示文字,选中它后在对象检查器的 **Axes** 区域操作: **X text** 与 **Y text** 两行可分别切换 **Tick labels** 和 **Title**, @@ -226,7 +226,7 @@ NMR 核素质量数。新数据集默认使用 89 × 60 mm 单栏画布:单个 当所选序列的线宽并不一致时,控件显示破折号和 *mixed*,而不会把其中一 个值当成当前设置;此时写入一个值正是让它们重新一致的方式。所选序列中 - 以别的方式绘制的(例如等高线)会在状态栏里报告为跳过,其余序列照常生 + 以别的方式绘制的(例如等高线)会在操作历史窗口里报告为跳过,其余序列照常生 效。若某个线宽不同于 PlotX 会为这份数据选择的值,该行会标上圆点,旁边 的重置按钮按当前数据重新推导默认值。 - **画布主题**携带配套字号——例如 Presentation Dark 主题会放大坐标轴 diff --git a/docs/src/content/docs/zh-cn/guides/processing.md b/docs/src/content/docs/zh-cn/guides/processing.md index f8c7921..111ed1b 100644 --- a/docs/src/content/docs/zh-cn/guides/processing.md +++ b/docs/src/content/docs/zh-cn/guides/processing.md @@ -34,7 +34,7 @@ XPS 为每个谱区使用独立的有序 recipe,而不是 NMR 管线。recipe 处理以卡片形式出现在画布右上角,可从 Ribbon 的 **Process** 页签打开,或用 Ctrl+K 搜到某个处理设置后激活它。若当前数据集不是 NMR -数据集,状态栏会提示 *Select an NMR dataset before opening Processing.*,卡片 +数据集,操作历史窗口会提示 *Select an NMR dataset before opening Processing.*,卡片 不会打开。 该卡片也可以承载 CRAFT、Regions、Curve Fit 和 Statistics 任务。同时打开两个及以上 diff --git a/docs/src/content/docs/zh-cn/guides/pseudo-2d.md b/docs/src/content/docs/zh-cn/guides/pseudo-2d.md index 128f1ae..b14a047 100644 --- a/docs/src/content/docs/zh-cn/guides/pseudo-2d.md +++ b/docs/src/content/docs/zh-cn/guides/pseudo-2d.md @@ -54,7 +54,7 @@ traces…**。选择参考增量和峰区间后,在选择 **Apply** 前检查 变换(ILT),可生成完整的化学位移 × 扩散系数图。图在后台构建,期间可继续 操作;按**取消**可丢弃该构建。同一数据集在当前构建完成或取消前不能重复构建 同类图。构建期间修改该数据集的处理参数会取消构建——否则图与谱图将不再对应。 -状态栏会在发生取消时提示;处理修改完成后重新构建即可。出于同样的原因,修改 +操作历史窗口会在发生取消时提示;处理修改完成后重新构建即可。出于同样的原因,修改 处理参数也会丢弃已经算好的图,画面会退回堆叠图,直到你重新构建。 下一次 ILT 构建所用的设置按以下顺序确定:你在 **Experiment** 卡片中为该数据集 @@ -65,7 +65,7 @@ traces…**。选择参考增量和峰区间后,在选择 **Apply** 前检查 两种 DOSY 图都会随 `.plotx` 项目一起保存,**Show** 和 **DOSY method** 的选择 也一并保存,因此重新打开项目时无需重算即可看到同一张图。如果保存的图所依据的 数据与项目中保存的处理流程重建出的数据不再一致,PlotX 会继续显示保存的图并给出 -警告——打开项目时出现在状态栏,同时也出现在 **Experiment** 卡片中。如果保存的图 +警告——打开项目时出现在操作历史窗口,同时也出现在 **Experiment** 卡片中。如果保存的图 完全无法读回,PlotX 会改为显示堆叠图,并在同样两处说明。重新构建该图即可消除 警告。 diff --git a/docs/src/content/docs/zh-cn/reference/reporting-problems.md b/docs/src/content/docs/zh-cn/reference/reporting-problems.md index 869e494..28a055e 100644 --- a/docs/src/content/docs/zh-cn/reference/reporting-problems.md +++ b/docs/src/content/docs/zh-cn/reference/reporting-problems.md @@ -6,7 +6,7 @@ description: 报告 PlotX 崩溃或运行问题时,找到需要附上的诊断 ## 崩溃之后 PlotX 捕获到内部错误时,会保存纯文本崩溃报告并显示完整路径。下次启动时, -恢复对话框或状态栏会再次显示该路径。请把报告附到 +恢复对话框或操作历史窗口会再次显示该路径。请把报告附到 [GitHub issue](https://github.com/nmrtist/plotx/issues);其中包含 PlotX 版本、平台、panic 位置、回溯以及会话日志末尾。 diff --git a/docs/src/content/docs/zh-cn/reference/ui-overview.md b/docs/src/content/docs/zh-cn/reference/ui-overview.md index 89ee82a..7094b2d 100644 --- a/docs/src/content/docs/zh-cn/reference/ui-overview.md +++ b/docs/src/content/docs/zh-cn/reference/ui-overview.md @@ -30,9 +30,10 @@ PlotX 的界面为英文;手册中加粗的英文词即界面上的原文标 **Analyze**、**Figure**、**Arrange**、**View**)。在 macOS 上,其任务行 还承载原生窗口按钮和项目名。它是快捷入口:其上的一切也都能在菜单或命令面板中找到。 各页签的分组详见 [Ribbon](/zh-cn/reference/ribbon/)。 -- **状态栏**——底部条带,显示提示、进度和选择详情。默认隐藏,以便为工作区 - 留出更多空间;需要时可在 **Preferences → Appearance** 中开启 - **Show status bar**。 +- **操作历史(Operation history)**——点击 Ribbon 任务行的时钟箭头图标, + 可查看当前状态和数据集摘要。**Messages** 保留本次运行最近 200 条已观察到的 + 状态变化,**Diagnostics** 显示结构化操作报告;**Clear** 清空两个列表。 + 悬停图标可查看当前状态,错误与警告也会出现在反馈横幅中。 两个侧栏随时可以显示或隐藏:点击 Ribbon 任务行右端的一对布局按钮,按 Ctrl+B(左)或 Ctrl+Shift+B