forked from 1b-agent/opencadstudio-upstream
feat(ui): bring ALIASEDIT to shortcut-editor parity
Port press-to-capture dialog safeguards to the alias editor: transient top draft row with explicit accept, duplicate-alias and unknown-command danger styling plus persistent banners, reset-to-defaults with inline confirmation, Apply and Apply-and-Exit, alias count, Esc-cancels-draft-first, and unsaved-changes guard on close.
This commit is contained in:
1 parent
457f4b3dbb
commit
1057790456
5 files changed
+513
-23
No files matched your search
+86
-1
@@ -164,6 +164,13 @@ fn default_map() -> FxHashMap<String, String> {
|
||||
parse_pgp(DEFAULT_ALIASES_PGP)
|
||||
}
|
||||
|
||||
/// Sorted factory-default rows for the ALIASEDIT reset action.
|
||||
pub(super) fn default_alias_rows() -> Vec<(String, String)> {
|
||||
let mut rows: Vec<(String, String)> = default_map().into_iter().collect();
|
||||
rows.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
rows
|
||||
}
|
||||
|
||||
/// Load the alias table at boot. Native reads `ocad.pgp`; web reads the same
|
||||
/// text from `localStorage`. Missing or unavailable storage falls back to the
|
||||
/// embedded defaults.
|
||||
@@ -315,10 +322,88 @@ impl OpenCADStudio {
|
||||
.alias_editor_rows
|
||||
.iter()
|
||||
.filter(|(a, c)| !a.trim().is_empty() && !c.trim().is_empty())
|
||||
.map(|(a, c)| (a.trim().to_string(), c.trim().to_string()))
|
||||
.map(|(a, c)| {
|
||||
(
|
||||
a.trim().to_uppercase(),
|
||||
c.trim().to_uppercase(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
self.set_command_aliases(map);
|
||||
}
|
||||
|
||||
/// Commit the working rows, discarding an unfinished draft and reporting
|
||||
/// duplicates. Shared by Apply and Apply-and-Exit. Mirrors
|
||||
/// `finish_shortcut_editor`.
|
||||
pub(super) fn finish_alias_editor(&mut self) {
|
||||
if self.alias_pending_add && !self.alias_editor_rows.is_empty() {
|
||||
self.alias_editor_rows.remove(0);
|
||||
self.alias_pending_add = false;
|
||||
self.command_line
|
||||
.push_error(crate::tf!("Incomplete alias row discarded.").as_ref());
|
||||
}
|
||||
let mut seen = rustc_hash::FxHashSet::default();
|
||||
let mut duplicates = Vec::new();
|
||||
for (alias, _) in &self.alias_editor_rows {
|
||||
let alias = alias.trim().to_uppercase();
|
||||
if alias.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if !seen.insert(alias.clone()) {
|
||||
duplicates.push(alias);
|
||||
}
|
||||
}
|
||||
if !duplicates.is_empty() {
|
||||
self.command_line.push_error(
|
||||
crate::tf!(
|
||||
"Duplicate alias(es) ignored on Apply: {}",
|
||||
duplicates.join(", ")
|
||||
)
|
||||
.as_ref(),
|
||||
);
|
||||
}
|
||||
self.apply_alias_editor_rows();
|
||||
self.command_line.push_info(
|
||||
crate::tf!("{} alias(es) applied.", self.command_aliases.len()).as_ref(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Reset the working rows and live table to the shipped defaults.
|
||||
pub(super) fn reset_aliases_to_defaults(&mut self) {
|
||||
self.alias_editor_rows = default_alias_rows();
|
||||
self.alias_pending_add = false;
|
||||
self.alias_reset_confirm = false;
|
||||
self.apply_alias_editor_rows();
|
||||
}
|
||||
|
||||
/// True when the working rows differ from the live table — the editor
|
||||
/// has un-applied changes a close would discard.
|
||||
pub(super) fn alias_editor_dirty(&self) -> bool {
|
||||
let rows: FxHashMap<String, String> = self
|
||||
.alias_editor_rows
|
||||
.iter()
|
||||
.filter_map(|(alias, command)| {
|
||||
let alias = alias.trim().to_uppercase();
|
||||
let command = command.trim().to_uppercase();
|
||||
(!alias.is_empty() && !command.is_empty()).then_some((alias, command))
|
||||
})
|
||||
.collect();
|
||||
rows != self.command_aliases
|
||||
}
|
||||
|
||||
/// A pending add finishes once its draft row (row 0) has both an alias
|
||||
/// and a command; it then becomes a regular row. Called by the draft
|
||||
/// row's check button — typing alone never finishes it.
|
||||
pub(super) fn finish_pending_alias_add(&mut self) {
|
||||
if !self.alias_pending_add {
|
||||
return;
|
||||
}
|
||||
if let Some((alias, command)) = self.alias_editor_rows.first() {
|
||||
if !alias.trim().is_empty() && !command.trim().is_empty() {
|
||||
self.alias_pending_add = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1099,6 +1099,16 @@ pub(super) struct OpenCADStudio {
|
||||
/// Working buffer for the ALIASEDIT modal: `(alias, command)` rows being
|
||||
/// edited. Seeded from `command_aliases` on open, committed back on close.
|
||||
alias_editor_rows: Vec<(String, String)>,
|
||||
/// True while a freshly added (top) row is an unfinished draft: it exists
|
||||
/// only until its alias and command are filled (accept) or it is cancelled
|
||||
/// (Esc / ✕ / dialog close), so the list never keeps an empty row.
|
||||
/// Mirrors `shortcut_pending_add`.
|
||||
alias_pending_add: bool,
|
||||
/// True while the "Reset to default" confirmation is showing.
|
||||
alias_reset_confirm: bool,
|
||||
/// True while the "unsaved changes will be discarded" confirmation
|
||||
/// overlays the editor: the user tried to close with un-applied rows.
|
||||
alias_close_confirm: bool,
|
||||
|
||||
// ── Named Parameters ──────────────────────────────────────────────────
|
||||
/// Working buffer for the PARAMETERS modal. Unlike `alias_editor_rows`,
|
||||
@@ -2986,6 +2996,22 @@ pub enum Message {
|
||||
AliasEditorRemove(usize),
|
||||
/// Commit the edited rows to the alias table (Apply button); stays open.
|
||||
AliasEditorApply,
|
||||
/// Apply the working rows and close the dialog.
|
||||
AliasEditorApplyExit,
|
||||
/// The check button on a draft row: finish the addition without applying.
|
||||
AliasEditorDraftAccept,
|
||||
/// Cancel a pending draft row (Esc / Cancel add).
|
||||
AliasEditorDraftCancel,
|
||||
/// Show the "Reset to default" confirmation.
|
||||
AliasEditorResetAsk,
|
||||
/// Reset aliases to the shipped defaults.
|
||||
AliasEditorResetConfirm,
|
||||
/// Hide the reset confirmation without resetting.
|
||||
AliasEditorResetDeny,
|
||||
/// Discard un-applied rows and close the editor.
|
||||
AliasEditorCloseDiscard,
|
||||
/// Keep editing: hide the discard confirmation.
|
||||
AliasEditorCloseKeep,
|
||||
// ── Named Parameters (PARAMETERS) ───────────────────────────────────
|
||||
/// Open the named-parameter editor, seeding rows from the active tab's
|
||||
/// `Scene::named_parameters`.
|
||||
@@ -3980,6 +4006,9 @@ impl OpenCADStudio {
|
||||
// Command aliases (populated from ocad.pgp just after construction)
|
||||
command_aliases: rustc_hash::FxHashMap::default(),
|
||||
alias_editor_rows: Vec::new(),
|
||||
alias_pending_add: false,
|
||||
alias_reset_confirm: false,
|
||||
alias_close_confirm: false,
|
||||
named_parameter_editor_rows: Vec::new(),
|
||||
// Layout Manager
|
||||
layout_manager_selected: "Model".to_string(),
|
||||
|
||||
+80
-3
@@ -201,7 +201,12 @@ impl OpenCADStudio {
|
||||
Some(GeometricTolerance) => self.geometric_tolerance = None,
|
||||
// Closing (✕) discards edits made since the last Apply — matching the
|
||||
// style editors. Committing happens only through the Apply button.
|
||||
Some(Aliases) => self.alias_editor_rows.clear(),
|
||||
Some(Aliases) => {
|
||||
self.alias_editor_rows.clear();
|
||||
self.alias_pending_add = false;
|
||||
self.alias_reset_confirm = false;
|
||||
self.alias_close_confirm = false;
|
||||
}
|
||||
Some(Shortcuts) => {
|
||||
self.shortcut_editor_rows.clear();
|
||||
self.shortcut_capture_row = None;
|
||||
@@ -283,6 +288,12 @@ impl OpenCADStudio {
|
||||
if matches!(msg, Message::CommandEscape)
|
||||
|| matches!(&msg, Message::ShortcutPressed(key) if key.rsplit('+').next() == Some("ESCAPE"))
|
||||
{
|
||||
// Esc backs out of a pending ALIASEDIT draft first, mirroring
|
||||
// the shortcut editor's capture cancel; the next Esc closes.
|
||||
if self.active_modal == Some(super::ModalKind::Aliases) && self.alias_pending_add
|
||||
{
|
||||
return self.update(Message::AliasEditorDraftCancel);
|
||||
}
|
||||
return self.update(Message::CloseModal);
|
||||
}
|
||||
if is_modal_blocked_key_msg(&msg) {
|
||||
@@ -6577,6 +6588,9 @@ impl OpenCADStudio {
|
||||
.collect();
|
||||
rows.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
self.alias_editor_rows = rows;
|
||||
self.alias_pending_add = false;
|
||||
self.alias_reset_confirm = false;
|
||||
self.alias_close_confirm = false;
|
||||
self.active_modal = Some(super::ModalKind::Aliases);
|
||||
Task::none()
|
||||
}
|
||||
@@ -6591,25 +6605,80 @@ impl OpenCADStudio {
|
||||
AliasField::Command => rowdata.1 = value,
|
||||
}
|
||||
}
|
||||
// Typing never finishes the addition — only the draft row's
|
||||
// check button does, so no half-visible "ghost" row appears.
|
||||
Task::none()
|
||||
}
|
||||
Message::AliasEditorAdd => {
|
||||
self.alias_editor_rows.push((String::new(), String::new()));
|
||||
if self.alias_pending_add {
|
||||
// One draft at a time: keep the existing top draft.
|
||||
} else {
|
||||
// The draft row goes to the top of the list so it is
|
||||
// visible without scrolling.
|
||||
self.alias_editor_rows
|
||||
.insert(0, (String::new(), String::new()));
|
||||
self.alias_pending_add = true;
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::AliasEditorDraftAccept => {
|
||||
// The check button: finish the addition (keep the row in the
|
||||
// working table) without applying it. Only complete drafts
|
||||
// can be accepted — the button is disabled otherwise.
|
||||
self.finish_pending_alias_add();
|
||||
Task::none()
|
||||
}
|
||||
Message::AliasEditorDraftCancel => {
|
||||
// Esc backs out and abandons an unfinished draft.
|
||||
if self.alias_pending_add && !self.alias_editor_rows.is_empty() {
|
||||
self.alias_editor_rows.remove(0);
|
||||
self.alias_pending_add = false;
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::AliasEditorRemove(idx) => {
|
||||
if idx < self.alias_editor_rows.len() {
|
||||
self.alias_editor_rows.remove(idx);
|
||||
}
|
||||
if idx == 0 {
|
||||
// Removing the draft row is the ✕ cancel path.
|
||||
self.alias_pending_add = false;
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::AliasEditorApply => {
|
||||
self.apply_alias_editor_rows();
|
||||
self.finish_alias_editor();
|
||||
Task::none()
|
||||
}
|
||||
Message::AliasEditorApplyExit => {
|
||||
self.finish_alias_editor();
|
||||
self.close_active_modal();
|
||||
Task::none()
|
||||
}
|
||||
Message::AliasEditorResetAsk => {
|
||||
self.alias_reset_confirm = true;
|
||||
Task::none()
|
||||
}
|
||||
Message::AliasEditorResetDeny => {
|
||||
self.alias_reset_confirm = false;
|
||||
Task::none()
|
||||
}
|
||||
Message::AliasEditorResetConfirm => {
|
||||
self.reset_aliases_to_defaults();
|
||||
self.command_line.push_info(
|
||||
crate::tf!("{} alias(es) applied.", self.command_aliases.len()).as_ref(),
|
||||
);
|
||||
Task::none()
|
||||
}
|
||||
Message::AliasEditorCloseDiscard => {
|
||||
self.alias_close_confirm = false;
|
||||
self.close_active_modal();
|
||||
Task::none()
|
||||
}
|
||||
Message::AliasEditorCloseKeep => {
|
||||
self.alias_close_confirm = false;
|
||||
Task::none()
|
||||
}
|
||||
|
||||
// ── Named Parameters (PARAMETERS) ─────────────────────────────────
|
||||
Message::NamedParametersOpen => {
|
||||
@@ -7260,6 +7329,14 @@ impl OpenCADStudio {
|
||||
return Task::none();
|
||||
}
|
||||
self.shortcut_close_confirm = false;
|
||||
if self.active_modal == Some(super::ModalKind::Aliases)
|
||||
&& !self.alias_close_confirm
|
||||
&& self.alias_editor_dirty()
|
||||
{
|
||||
self.alias_close_confirm = true;
|
||||
return Task::none();
|
||||
}
|
||||
self.alias_close_confirm = false;
|
||||
if self.active_modal == Some(super::ModalKind::DraftingSettings)
|
||||
&& !self.drafting_settings_close_confirm
|
||||
&& self.drafting_settings_dirty()
|
||||
|
||||
@@ -167,6 +167,65 @@ impl OpenCADStudio {
|
||||
)
|
||||
}
|
||||
super::super::ModalKind::Aliases => {
|
||||
// Aliases claimed by two rows — the cells turn red and a
|
||||
// persistent warning names the command already using each
|
||||
// alias; the last row wins in the alias map on Apply.
|
||||
let mut alias_rows: std::collections::BTreeMap<String, Vec<&(String, String)>> =
|
||||
std::collections::BTreeMap::new();
|
||||
for row in &self.alias_editor_rows {
|
||||
let alias = row.0.trim().to_uppercase();
|
||||
if !alias.is_empty() {
|
||||
alias_rows.entry(alias).or_default().push(row);
|
||||
}
|
||||
}
|
||||
let duplicate_aliases: Vec<String> = alias_rows
|
||||
.iter()
|
||||
.filter(|(_, rows)| rows.len() > 1)
|
||||
.map(|(alias, _)| alias.clone())
|
||||
.collect();
|
||||
let duplicate_conflicts: Vec<(String, String)> = alias_rows
|
||||
.iter()
|
||||
.filter(|(_, rows)| rows.len() > 1)
|
||||
.map(|(alias, rows)| {
|
||||
let command = rows
|
||||
.iter()
|
||||
.find(|(_, command)| !command.is_empty())
|
||||
.map(|(_, command)| command.clone())
|
||||
.unwrap_or_default();
|
||||
(alias.clone(), command)
|
||||
})
|
||||
.collect();
|
||||
let duplicate_set: rustc_hash::FxHashSet<String> =
|
||||
duplicate_aliases.into_iter().collect();
|
||||
// Commands the dispatcher can't run: not a registered
|
||||
// command, plugin command, or input action.
|
||||
let valid: rustc_hash::FxHashSet<String> =
|
||||
crate::command::all_registered_command_names()
|
||||
.into_iter()
|
||||
.map(str::to_uppercase)
|
||||
.chain(
|
||||
self.command_line
|
||||
.dynamic_commands
|
||||
.iter()
|
||||
.map(|cmd| cmd.to_uppercase()),
|
||||
)
|
||||
.chain(
|
||||
crate::app::shortcuts::INPUT_ACTIONS
|
||||
.iter()
|
||||
.map(|action| action.to_string()),
|
||||
)
|
||||
.collect();
|
||||
let unknown_commands: Vec<String> = self
|
||||
.alias_editor_rows
|
||||
.iter()
|
||||
.filter_map(|(_, command)| {
|
||||
let command = command.trim();
|
||||
(!command.is_empty() && !valid.contains(command))
|
||||
.then(|| command.to_string())
|
||||
})
|
||||
.collect();
|
||||
let unknown_set: rustc_hash::FxHashSet<String> =
|
||||
unknown_commands.into_iter().collect();
|
||||
sized_flow(
|
||||
ex,
|
||||
480,
|
||||
@@ -174,6 +233,12 @@ impl OpenCADStudio {
|
||||
|flow| {
|
||||
crate::ui::window::alias_editor::view_window(
|
||||
&self.alias_editor_rows,
|
||||
self.alias_pending_add,
|
||||
self.alias_reset_confirm,
|
||||
&duplicate_set,
|
||||
&duplicate_conflicts,
|
||||
&unknown_set,
|
||||
self.alias_close_confirm,
|
||||
flow,
|
||||
)
|
||||
},
|
||||
|
||||
+253
-19
@@ -1,8 +1,10 @@
|
||||
//! Command-alias editor — an in-canvas modal (Plan B) for adding, remapping and
|
||||
//! removing command-line aliases (the `ocad.pgp` table). Opened by ALIASEDIT.
|
||||
//! Rows are `(alias, command)`; edits are buffered in `alias_editor_rows` and
|
||||
//! committed to the alias table when the dialog closes. Mirrors the editable-row
|
||||
//! pattern of the attribute editor and plugin manager.
|
||||
//! committed to the alias table on Apply. Mirrors the shortcut editor:
|
||||
//! transient draft row with explicit accept, duplicate/unknown validation,
|
||||
//! reset-to-defaults with confirmation, apply-and-exit, alias count, and an
|
||||
//! unsaved-changes guard on close.
|
||||
|
||||
use crate::app::Message;
|
||||
use iced::widget::{button, column, container, row, scrollable, text, text_input, Space};
|
||||
@@ -25,14 +27,54 @@ fn muted_style(theme: &Theme) -> iced::widget::text::Style {
|
||||
}
|
||||
}
|
||||
|
||||
fn danger_text_style(theme: &Theme) -> iced::widget::text::Style {
|
||||
iced::widget::text::Style {
|
||||
color: Some(theme.palette().danger.base.color),
|
||||
}
|
||||
}
|
||||
|
||||
/// Text-input variant flagging an invalid value: danger border and value
|
||||
/// color on the theme's danger weak background.
|
||||
fn danger_input_style(
|
||||
theme: &Theme,
|
||||
status: iced::widget::text_input::Status,
|
||||
) -> iced::widget::text_input::Style {
|
||||
let danger = theme.palette().danger.base;
|
||||
iced::widget::text_input::Style {
|
||||
background: Background::Color(theme.palette().danger.weak.color),
|
||||
border: iced::border::rounded(4)
|
||||
.color(danger.color)
|
||||
.width(
|
||||
if matches!(
|
||||
status,
|
||||
iced::widget::text_input::Status::Focused { is_hovered: _ }
|
||||
) {
|
||||
1.5
|
||||
} else {
|
||||
1.0
|
||||
},
|
||||
),
|
||||
icon: danger.color,
|
||||
placeholder: danger.color.scale_alpha(0.7),
|
||||
value: danger.text,
|
||||
selection: danger.color,
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the alias editor content. `rows` is the live working buffer.
|
||||
pub fn view_window(
|
||||
rows: &[(String, String)],
|
||||
pub fn view_window<'a>(
|
||||
rows: &'a [(String, String)],
|
||||
pending_add: bool,
|
||||
reset_confirm: bool,
|
||||
duplicate_aliases: &rustc_hash::FxHashSet<String>,
|
||||
duplicate_conflicts: &[(String, String)],
|
||||
unknown_commands: &rustc_hash::FxHashSet<String>,
|
||||
close_confirm: bool,
|
||||
sizing: crate::ui::modal::ModalSizing,
|
||||
) -> Element<'_, Message> {
|
||||
) -> Element<'a, Message> {
|
||||
let title = text(t!("Command Aliases")).size(15);
|
||||
let hint = text(t!(
|
||||
"Type an alias and the command it runs (e.g. L → LINE). Apply to save to ocad.pgp; closing discards unapplied edits."
|
||||
"Click + Add alias, type the alias and command, accept the row, then Apply. Esc cancels the pending row."
|
||||
))
|
||||
.size(11)
|
||||
.style(muted_style);
|
||||
@@ -46,7 +88,7 @@ pub fn view_window(
|
||||
row![
|
||||
container(text(t!("Alias")).size(11).style(muted_style)).width(Length::Fixed(120.0)),
|
||||
container(text(t!("Command")).size(11).style(muted_style)).width(sizing.width),
|
||||
Space::new().width(Length::Fixed(30.0)),
|
||||
Space::new().width(Length::Fixed(62.0)),
|
||||
]
|
||||
.spacing(8),
|
||||
)
|
||||
@@ -54,39 +96,167 @@ pub fn view_window(
|
||||
|
||||
let mut list = column![].spacing(3);
|
||||
for (idx, (alias, cmd)) in rows.iter().enumerate() {
|
||||
let alias_key = alias.trim().to_uppercase();
|
||||
let duplicate = !alias_key.is_empty() && duplicate_aliases.contains(alias_key.as_str());
|
||||
let alias_box = text_input(t!("alias").as_ref(), alias)
|
||||
.on_input(move |v| Message::AliasEditorInput { idx, field: AliasField::Alias, value: v })
|
||||
.size(13)
|
||||
.padding([3, 6])
|
||||
.width(Length::Fixed(120.0));
|
||||
let alias_box = if duplicate {
|
||||
alias_box.style(danger_input_style)
|
||||
} else {
|
||||
alias_box
|
||||
};
|
||||
let unknown_command =
|
||||
!cmd.trim().is_empty() && unknown_commands.contains(cmd.trim());
|
||||
let cmd_box = text_input(t!("command").as_ref(), cmd)
|
||||
.on_input(move |v| Message::AliasEditorInput { idx, field: AliasField::Command, value: v })
|
||||
.size(13)
|
||||
.padding([3, 6])
|
||||
.width(sizing.width);
|
||||
let del = button(crate::ui::icons::themed_danger_text(crate::ui::icons::CLOSE, 12.0))
|
||||
let cmd_box = if unknown_command {
|
||||
cmd_box.style(danger_input_style)
|
||||
} else {
|
||||
cmd_box
|
||||
};
|
||||
// Draft rows get a check (finish the addition, without applying) and
|
||||
// a cancel ✕; committed rows get the trash bin.
|
||||
let remove = if pending_add && idx == 0 {
|
||||
let done_ok = !alias.trim().is_empty()
|
||||
&& !cmd.trim().is_empty()
|
||||
&& !duplicate
|
||||
&& !unknown_commands.contains(cmd.trim());
|
||||
row![
|
||||
button(crate::ui::icons::themed_success_text(
|
||||
crate::ui::icons::CHECK,
|
||||
12.0,
|
||||
))
|
||||
.on_press_maybe(done_ok.then_some(Message::AliasEditorDraftAccept))
|
||||
.padding([2, 6])
|
||||
.style(button::success),
|
||||
button(crate::ui::icons::themed_danger_text(
|
||||
crate::ui::icons::CLOSE,
|
||||
12.0,
|
||||
))
|
||||
.on_press(Message::AliasEditorDraftCancel)
|
||||
.padding([2, 6])
|
||||
.style(button::danger),
|
||||
]
|
||||
.spacing(4)
|
||||
.align_y(iced::Center)
|
||||
} else {
|
||||
row![button(crate::ui::icons::themed_danger_text(
|
||||
crate::ui::icons::TRASH,
|
||||
12.0,
|
||||
))
|
||||
.on_press(Message::AliasEditorRemove(idx))
|
||||
.padding([2, 6])
|
||||
.style(button::danger);
|
||||
.style(button::danger)]
|
||||
.align_y(iced::Center)
|
||||
};
|
||||
list = list.push(
|
||||
row![alias_box, cmd_box, del]
|
||||
row![alias_box, cmd_box, remove]
|
||||
.spacing(8)
|
||||
.align_y(iced::Center),
|
||||
);
|
||||
}
|
||||
|
||||
let add = button(text(t!("+ Add alias")).size(12))
|
||||
.on_press(Message::AliasEditorAdd)
|
||||
.padding([4, 10])
|
||||
.style(button::secondary);
|
||||
|
||||
// While a draft row is pending, the add button doubles as the visible
|
||||
// cancel affordance (Esc works too).
|
||||
let add = if pending_add {
|
||||
button(text(t!("Cancel add (Esc)")).size(12))
|
||||
.on_press(Message::AliasEditorDraftCancel)
|
||||
.padding([4, 10])
|
||||
.style(button::danger)
|
||||
} else {
|
||||
button(text(t!("+ Add alias")).size(12))
|
||||
.on_press(Message::AliasEditorAdd)
|
||||
.padding([4, 10])
|
||||
.style(button::secondary)
|
||||
};
|
||||
// The reset confirmation replaces the alias count so the warning
|
||||
// gets the whole left side of the bar.
|
||||
let stats = if reset_confirm {
|
||||
row![Space::new().width(Length::Fixed(0.0))]
|
||||
} else {
|
||||
row![text(format!("{}: {}", t!("Number of aliases"), rows.len()))
|
||||
.size(12)
|
||||
.style(muted_style)]
|
||||
};
|
||||
// Reset asks for confirmation in place: the add/reset buttons are
|
||||
// replaced by the question with Yes / No.
|
||||
let (reset_area, add_area) = if reset_confirm {
|
||||
(
|
||||
row![
|
||||
text(t!(
|
||||
"Are you sure you want to reset? You will lose all your current aliases!"
|
||||
))
|
||||
.size(12)
|
||||
.style(danger_text_style),
|
||||
button(text(t!("Yes, reset")).size(12))
|
||||
.on_press(Message::AliasEditorResetConfirm)
|
||||
.padding([4, 10])
|
||||
.style(button::danger),
|
||||
button(text(t!("No")).size(12))
|
||||
.on_press(Message::AliasEditorResetDeny)
|
||||
.padding([4, 10])
|
||||
.style(button::secondary),
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(iced::Center),
|
||||
row![Space::new().width(Length::Fixed(0.0))],
|
||||
)
|
||||
} else {
|
||||
(
|
||||
row![button(text(t!("Reset to default")).size(12))
|
||||
.on_press(Message::AliasEditorResetAsk)
|
||||
.padding([4, 10])
|
||||
.style(button::secondary)]
|
||||
.spacing(8)
|
||||
.align_y(iced::Center),
|
||||
row![add],
|
||||
)
|
||||
};
|
||||
// Apply — primary action; commits the rows to ocad.pgp and stays open.
|
||||
let apply = button(text(t!("Apply")).size(12))
|
||||
.on_press(Message::AliasEditorApply)
|
||||
.padding([4, 16])
|
||||
.style(button::primary);
|
||||
let apply_exit = button(text(t!("Apply && Exit")).size(12))
|
||||
.on_press(Message::AliasEditorApplyExit)
|
||||
.padding([4, 16])
|
||||
.style(button::primary);
|
||||
// The reset confirmation takes over the whole action bar.
|
||||
let apply_area = if reset_confirm {
|
||||
row![Space::new().width(Length::Fixed(0.0))]
|
||||
} else {
|
||||
row![apply, apply_exit].spacing(8).align_y(iced::Center)
|
||||
};
|
||||
|
||||
container(
|
||||
// Persistent validation warnings: shown until every conflicting alias is
|
||||
// resolved (edited or a row removed) and every command is runnable.
|
||||
let mut conflict_banner = column![].spacing(2);
|
||||
for (alias, command) in duplicate_conflicts {
|
||||
conflict_banner = conflict_banner.push(
|
||||
text(crate::tf!(
|
||||
"Alias already used for command: {} → {}",
|
||||
alias,
|
||||
command
|
||||
))
|
||||
.size(12)
|
||||
.style(danger_text_style),
|
||||
);
|
||||
}
|
||||
for command in unknown_commands {
|
||||
conflict_banner = conflict_banner.push(
|
||||
text(crate::tf!("Unknown command: {}", command))
|
||||
.size(12)
|
||||
.style(danger_text_style),
|
||||
);
|
||||
}
|
||||
|
||||
let content = container(
|
||||
column![
|
||||
title,
|
||||
hint,
|
||||
@@ -94,7 +264,11 @@ pub fn view_window(
|
||||
head,
|
||||
scrollable(container(list).padding(gutter)).height(sizing.height),
|
||||
Space::new().height(6),
|
||||
row![add, Space::new().width(sizing.width), apply].align_y(iced::Center),
|
||||
conflict_banner,
|
||||
Space::new().height(4),
|
||||
row![stats, add_area, reset_area, Space::new().width(sizing.width), apply_area]
|
||||
.spacing(8)
|
||||
.align_y(iced::Center),
|
||||
]
|
||||
.spacing(6)
|
||||
.width(sizing.width)
|
||||
@@ -108,6 +282,66 @@ pub fn view_window(
|
||||
theme.palette().background.base.color,
|
||||
)),
|
||||
..Default::default()
|
||||
})
|
||||
.into()
|
||||
});
|
||||
|
||||
// Unsaved-changes guard: closing with un-applied rows stacks a dimmed
|
||||
// shield and a confirmation panel on top of the editor.
|
||||
if !close_confirm {
|
||||
return content.into();
|
||||
}
|
||||
let shield = iced::widget::mouse_area(
|
||||
container(Space::new())
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.style(|theme: &Theme| container::Style {
|
||||
background: Some(Background::Color(
|
||||
theme
|
||||
.palette()
|
||||
.background
|
||||
.strongest
|
||||
.color
|
||||
.scale_alpha(0.55),
|
||||
)),
|
||||
..Default::default()
|
||||
}),
|
||||
)
|
||||
.on_press(Message::AliasEditorCloseKeep)
|
||||
.interaction(iced::mouse::Interaction::Idle);
|
||||
let panel = container(
|
||||
column![
|
||||
text(t!("Unsaved changes will be discarded.")).size(14),
|
||||
Space::new().height(10),
|
||||
row![
|
||||
button(text(t!("Discard && close")).size(12))
|
||||
.on_press(Message::AliasEditorCloseDiscard)
|
||||
.padding([4, 12])
|
||||
.style(button::danger),
|
||||
button(text(t!("Keep editing")).size(12))
|
||||
.on_press(Message::AliasEditorCloseKeep)
|
||||
.padding([4, 12])
|
||||
.style(button::secondary),
|
||||
]
|
||||
.spacing(8),
|
||||
]
|
||||
.spacing(4),
|
||||
)
|
||||
.padding(16)
|
||||
.width(Length::Fixed(320.0))
|
||||
.style(|theme: &Theme| container::Style {
|
||||
background: Some(Background::Color(
|
||||
theme.palette().background.base.color,
|
||||
)),
|
||||
border: iced::Border {
|
||||
color: theme.palette().background.neutral.color,
|
||||
width: 1.0,
|
||||
radius: 6.0.into(),
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
let centered = container(panel)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.align_x(iced::alignment::Horizontal::Center)
|
||||
.align_y(iced::alignment::Vertical::Center);
|
||||
iced::widget::stack![content, iced::widget::opaque(shield), centered].into()
|
||||
}
|
||||
Reference in new issue
Block a user