From 3d124782d5e0b430097a0553a6c8bd8d23cc8fbb Mon Sep 17 00:00:00 2001 From: ETWen Date: Sat, 4 Jul 2026 08:52:55 +0800 Subject: [PATCH] feat(ui): Add menu bar and customizable quick toolbar (v0.7.0) - Menu bar groups all functions: File (import/MRU/exports), View (zoom/standard views/ortho/unit), Measure (7 checkable modes with hotkey hints), Transform (align/drag/gizmo/rotate), Tools (interference/section), QuickBar (customize/reset) - QuickBarViewModel registry: per-button visibility via XAML indexer binding QuickBar[Key].IsChecked; buttons keep static bindings - Common-user defaults visible (import/recent/zoom/iso/ortho/point/ distance/circle/angle/interference/section/clear/unit); the rest live in menus; selection persists in settings.json QuickBarKeys - Section parameter bar is its own row, shown only while section is enabled (with close button); no longer occupies space permanently - Bump version to 0.7.0; update ARCHITECTURE/CLAUDE docs (Phase 16) Co-Authored-By: Claude Fable 5 --- ARCHITECTURE.md | 23 ++ CLAUDE.md | 4 + src/STPViewer/Converters/FileNameConverter.cs | 15 ++ src/STPViewer/MainWindow.xaml | 218 ++++++++++++++---- src/STPViewer/STPViewer.csproj | 2 +- src/STPViewer/Services/SettingsService.cs | 3 + .../ViewModels/MainViewModel.Section.cs | 5 + src/STPViewer/ViewModels/MainViewModel.cs | 5 + src/STPViewer/ViewModels/QuickBarViewModel.cs | 103 +++++++++ 9 files changed, 337 insertions(+), 41 deletions(-) create mode 100644 src/STPViewer/Converters/FileNameConverter.cs create mode 100644 src/STPViewer/ViewModels/QuickBarViewModel.cs diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 53f438d..f167981 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -463,6 +463,29 @@ WPF retained-mode 每幀重走 visual tree → frame rate 崩。瀏覽模式因 --- +## Development Phases — 第八輪(UI 重整 v0.7.0) + +### Phase 16 — 選單列 + 可自訂快速列(工作量:M) + +**問題:** 功能累積後單一工具列 30+ 顆按鈕擠在一排,視窗不夠寬就溢出到「▾」,難找難按。 + +- [x] **選單列**(Menu):全部功能分類下拉 — 檔案(匯入/MRU/四種匯出)、視圖(全覽/標準視圖/正交/單位)、 + 量測(7 種模式 checkable + 快速鍵提示 + 清除)、變換(對齊/三點/拖曳/操作器/旋轉XYZ)、 + 工具(干涉/剖面)、快速列(自訂按鈕勾選 + 恢復預設) +- [x] **快速工具列**(`QuickBarViewModel`):只顯示使用者勾選的常用按鈕; + 註冊表定義全部按鈕(key/選單名/預設值),XAML 以索引子綁定 + `Visibility="{Binding QuickBar[Key].IsChecked, Converter=BoolToVis}"`(按鈕本體與綁定保持靜態) +- [x] 預設常用:匯入、最近、全覽、等角、正交、點、距離、圓、角度、干涉、剖面、清除、mm⇄in; + 其餘(邊/面/面距/對齊/三點/旋轉/拖曳/操作器/四種匯出/前上右視圖)預設收進選單 +- [x] 勾選清單隨 settings.json(`QuickBarKeys`)保存;null/新按鈕用註冊表預設值 +- [x] **剖面參數列**獨立成第三列,只在 `SectionEnabled` 時顯示(含「✕ 關閉」); + 平常整條隱藏,不再常駐佔位 + +**驗收:** 選單所有功能可用(checkable 與工具列 toggle 同步);快速列勾選即時增減、重開保留; +既有快速鍵 P/D/E/F/C/A/M + Esc 不受影響 + +--- + ## Future Extensions(下一輪) - **大檔重開快取** — 匯入成功後把三角網格 + 裝配樹序列化成 sidecar 快取檔(以來源檔 hash 驗證), diff --git a/CLAUDE.md b/CLAUDE.md index b7b93be..f83cf17 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -47,6 +47,10 @@ dotnet publish src/STPViewer -c Release -o publish/STPViewer (量孔對孔 pitch 靠這個)。不要把圓心吸附拿掉或改成永遠優先頂點 - 鍵盤快速鍵在 `MainWindow.Window_PreviewKeyDown`:**焦點在 TextBoxBase/ComboBox 時直接 return** (樹搜尋框、剖面數值框要能打字),新增輸入控件不用再各自處理 +- UI 三層(v0.7.0):**選單列**(全部功能,分類下拉)+ **快速列**(`QuickBarViewModel` 註冊表, + 使用者勾選常用按鈕,XAML 索引子綁定 `QuickBar[Key].IsChecked` 控 Visibility,按鈕本體維持靜態綁定)+ + **剖面參數列**(只在 SectionEnabled 顯示)。新增功能要三處都接:選單分類、快速列註冊表 + (含預設值)+ 對應 XAML 按鈕;勾選清單存 settings.json `QuickBarKeys`(null = 預設) - 使用者設定 `SettingsService`(%LOCALAPPDATA%\STPViewer\settings.json):視窗(MainWindow 管)+ 單位/MRU(VM `LoadSettings`/`SaveSettingsInto`);壞檔回預設、儲存失敗靜默 - 匯出 STEP 走 `CADability.ExportStep.WriteToFile(file, Project.CreateSimpleProject()+Model.Add)`; diff --git a/src/STPViewer/Converters/FileNameConverter.cs b/src/STPViewer/Converters/FileNameConverter.cs new file mode 100644 index 0000000..924ea12 --- /dev/null +++ b/src/STPViewer/Converters/FileNameConverter.cs @@ -0,0 +1,15 @@ +using System; +using System.Globalization; +using System.Windows.Data; + +namespace STPViewer.Converters; + +/// 完整路徑 → 檔名(選單「最近檔案」顯示用;tooltip 仍給完整路徑) +public class FileNameConverter : IValueConverter +{ + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) => + value is string s ? System.IO.Path.GetFileName(s) : ""; + + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => + throw new NotSupportedException(); +} diff --git a/src/STPViewer/MainWindow.xaml b/src/STPViewer/MainWindow.xaml index adfc8db..a5f440f 100644 --- a/src/STPViewer/MainWindow.xaml +++ b/src/STPViewer/MainWindow.xaml @@ -12,111 +12,249 @@ + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - 正交 - - 📍 點 📏 距離 📐 邊 ⬛ 面 ⭕ 圓 ∠ 角度 ⇔ 面距 - 🎯 對齊 🎯 三點 - - - - - 🖐 拖曳 - ⊹ 操作器 - - - - - mm ⇄ in - - - - - + + + + - ✂ 剖面 + + VerticalAlignment="Center" ToolTip="剖切軸向(3點 = 在模型上點 3 點定義任意平面)"> X Y Z 3點 + VerticalAlignment="Center" Margin="4,0" ToolTip="剖面位置(沿法向 %)" /> + VerticalAlignment="Center" ToolTip="剖面位置 %(0–100,可直接輸入,離開欄位生效)" /> + Margin="4,0" ToolTip="切換保留側" /> + diff --git a/src/STPViewer/STPViewer.csproj b/src/STPViewer/STPViewer.csproj index e717461..6125ed3 100644 --- a/src/STPViewer/STPViewer.csproj +++ b/src/STPViewer/STPViewer.csproj @@ -6,7 +6,7 @@ enable enable true - 0.6.0 + 0.7.0 diff --git a/src/STPViewer/Services/SettingsService.cs b/src/STPViewer/Services/SettingsService.cs index 245cc1d..139cdfc 100644 --- a/src/STPViewer/Services/SettingsService.cs +++ b/src/STPViewer/Services/SettingsService.cs @@ -15,6 +15,9 @@ public class AppSettings public bool WindowMaximized { get; set; } public bool UseInch { get; set; } public List RecentFiles { get; set; } = new(); + + /// 快速工具列顯示的按鈕 key 清單;null = 用預設(QuickBarViewModel.Registry) + public List? QuickBarKeys { get; set; } } /// 設定讀寫:壞檔/缺檔一律回預設值,儲存失敗靜默(設定不值得讓程式閃退) diff --git a/src/STPViewer/ViewModels/MainViewModel.Section.cs b/src/STPViewer/ViewModels/MainViewModel.Section.cs index 95311cb..5b960f2 100644 --- a/src/STPViewer/ViewModels/MainViewModel.Section.cs +++ b/src/STPViewer/ViewModels/MainViewModel.Section.cs @@ -5,6 +5,7 @@ using System.Windows.Media; using System.Windows.Media.Media3D; using System.Windows.Threading; using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; using HelixToolkit.Wpf; using STPViewer.Models; using STPViewer.Services; @@ -38,6 +39,10 @@ public partial class MainViewModel [ObservableProperty] private bool sectionFlip; + /// 剖面參數列的「✕ 關閉」按鈕(參數列只在剖面開啟時顯示) + [RelayCommand] + private void CloseSection() => SectionEnabled = false; + // ── 3點自訂剖切平面(SectionAxisIndex == 3,v0.5.0)── private const int CustomAxisIndex = 3; private Vector3D? _customNormal; // null = 尚未定義(等使用者點 3 點) diff --git a/src/STPViewer/ViewModels/MainViewModel.cs b/src/STPViewer/ViewModels/MainViewModel.cs index c87a920..2883c8a 100644 --- a/src/STPViewer/ViewModels/MainViewModel.cs +++ b/src/STPViewer/ViewModels/MainViewModel.cs @@ -86,6 +86,9 @@ public partial class MainViewModel : ObservableObject public ObservableCollection Roots { get; } = new(); public ObservableCollection Measurements { get; } = new(); + /// 快速工具列自訂(選單列有全部功能,快速列只放勾選的常用按鈕) + public QuickBarViewModel QuickBar { get; } = new(); + // ─── 使用者設定(視窗由 MainWindow 處理;VM 管單位 + MRU)───── public void LoadSettings(AppSettings s) @@ -94,12 +97,14 @@ public partial class MainViewModel : ObservableObject RecentFiles.Clear(); foreach (string f in s.RecentFiles.Where(File.Exists).Take(SettingsService.MaxRecentFiles)) RecentFiles.Add(f); + QuickBar.Load(s.QuickBarKeys); } public void SaveSettingsInto(AppSettings s) { s.UseInch = UseInch; s.RecentFiles = RecentFiles.ToList(); + s.QuickBarKeys = QuickBar.ToKeys(); } private void TouchRecent(string path) diff --git a/src/STPViewer/ViewModels/QuickBarViewModel.cs b/src/STPViewer/ViewModels/QuickBarViewModel.cs new file mode 100644 index 0000000..d3075d2 --- /dev/null +++ b/src/STPViewer/ViewModels/QuickBarViewModel.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; + +namespace STPViewer.ViewModels; + +/// 快速列單一按鈕的顯示開關(選單「快速列 → 自訂按鈕」的 checkbox 項) +public partial class QuickBarItemViewModel : ObservableObject +{ + public string Key { get; } + public string Label { get; } + public bool Default { get; } + + [ObservableProperty] + private bool isChecked; + + public QuickBarItemViewModel(string key, string label, bool @default) + { + Key = key; + Label = label; + Default = @default; + isChecked = @default; + } +} + +/// +/// 快速工具列自訂:所有功能都在選單列,快速列只放使用者勾選的常用按鈕。 +/// XAML 以索引子綁定:Visibility="{Binding QuickBar[Point].IsChecked, Converter=…}"。 +/// 勾選狀態隨 settings.json(QuickBarKeys)保存;null = 用預設。 +/// +public partial class QuickBarViewModel : ObservableObject +{ + /// 註冊表 = 快速列順序 + 選單顯示名 + 是否預設顯示(一般 user 常用) + private static readonly (string Key, string Label, bool Default)[] Registry = + { + ("Import", "📂 匯入", true), + ("Recent", "▾ 最近檔案", true), + ("ZoomAll", "🔍 全覽", true), + ("ViewIso", "視圖:等角", true), + ("ViewFront", "視圖:前", false), + ("ViewTop", "視圖:上", false), + ("ViewRight", "視圖:右", false), + ("Ortho", "正交投影", true), + ("Point", "📍 點", true), + ("Distance", "📏 距離", true), + ("Edge", "📐 邊", false), + ("Face", "⬛ 面", false), + ("Circle", "⭕ 圓", true), + ("Angle", "∠ 角度", true), + ("FaceDistance", "⇔ 面距", false), + ("Align", "🎯 兩點對齊", false), + ("Align3", "🎯 三點對齊", false), + ("RotateX", "旋轉 ↻X", false), + ("RotateY", "旋轉 ↻Y", false), + ("RotateZ", "旋轉 ↻Z", false), + ("Drag", "🖐 拖曳", false), + ("Gizmo", "⊹ 操作器", false), + ("Interference", "🧩 干涉", true), + ("Section", "✂ 剖面", true), + ("Clear", "🧹 清除量測", true), + ("Unit", "mm ⇄ in", true), + ("ExportCsv", "💾 匯出 CSV", false), + ("Screenshot", "📷 截圖", false), + ("ExportStep", "📤 匯出 STEP", false), + ("ExportStl", "📐 匯出 STL", false), + }; + + private readonly Dictionary _byKey; + + /// 依快速列順序的全部項目(選單自訂清單用) + public IReadOnlyList Items { get; } + + public QuickBarViewModel() + { + Items = Registry.Select(r => new QuickBarItemViewModel(r.Key, r.Label, r.Default)).ToList(); + _byKey = Items.ToDictionary(i => i.Key); + } + + /// XAML 索引子綁定入口:QuickBar[Point].IsChecked + public QuickBarItemViewModel this[string key] => _byKey[key]; + + /// 套用保存的勾選清單;null = 維持預設(含未知 key 容錯:新版本新增按鈕用預設值) + public void Load(List? visibleKeys) + { + if (visibleKeys is null) return; + var set = new HashSet(visibleKeys, StringComparer.Ordinal); + foreach (QuickBarItemViewModel item in Items) + item.IsChecked = set.Contains(item.Key); + } + + /// 目前勾選清單(存 settings.json) + public List ToKeys() => + Items.Where(i => i.IsChecked).Select(i => i.Key).ToList(); + + [RelayCommand] + private void ResetDefaults() + { + foreach (QuickBarItemViewModel item in Items) + item.IsChecked = item.Default; + } +}