diff --git a/.gitignore b/.gitignore index dd93f3d..7ce1313 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,4 @@ BundleArtifacts/ /src/UI/WinUI (Package)/WinUI (Package).assets.cache .claude +results.sarif diff --git a/.gitmodules b/.gitmodules index 92596a9..b7e84bf 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "src/UI/UsbSerialForAndroid"] path = src/UI/UsbSerialForAndroid url = https://github.com/bytedreamer/UsbSerialForAndroid +[submodule "lib/Guidelines"] + path = lib/Guidelines + url = https://github.com/Z-bit-Systems-LLC/Guidelines.git diff --git a/CLAUDE.md b/CLAUDE.md index 9f624dd..c630ba8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -121,14 +121,54 @@ $sarif.runs[0].results | Where-Object { $_.level -eq 'warning' } | ForEach-Objec - Use meaningful variable names that reflect their purpose - Keep methods focused and small with a single responsibility +## Shared UI Components + +This project uses the [Guidelines](https://github.com/Z-bit-Systems-LLC/Guidelines) shared WPF library for design system components. + +### Working with the Guidelines Submodule + +**Initial clone (new developers):** +```bash +git clone --recursive https://github.com/Z-bit-Systems-LLC/OSDP-Bench.git +``` + +**Update Guidelines to latest version:** +```bash +git submodule update --remote lib/Guidelines +git add lib/Guidelines +git commit -m "Update Guidelines submodule to latest version" +``` + +**Making changes to Guidelines:** +If you need to modify the shared library: +1. Make changes in `lib/Guidelines/` +2. Test changes in OSDP-Bench (local project reference) +3. Commit and push in Guidelines repository +4. Update submodule reference in OSDP-Bench + +### Design System Reference +See `lib/Guidelines/src/ZBitSystems.Wpf.UI/Styles/StyleGuide.md` for: +- Available styles and design tokens +- Standard components and layouts +- Theme-aware semantic colors +- Usage examples and best practices + ## UI Style Guidelines - **Always use standard styles** - Apply predefined styles from the design system instead of inline properties - **Use design tokens for spacing** - Reference `{StaticResource Margin.Card}` instead of hardcoding values - **Apply theme-aware semantic colors** - Use `{DynamicResource SemanticSuccessBrush}` for automatic light/dark theme support -- **Follow the style hierarchy** - Check ComponentStyles.xaml and LayoutTemplates.xaml before creating custom styles +- **Follow the style hierarchy** - Check the shared design system (Guidelines library) and LayoutTemplates.xaml before creating custom styles - **Update existing code** - When modifying files, replace inline styling with standard styles - **Create reusable patterns** - If you find yourself repeating XAML structures, consider adding a new style or template - **Use WrapPanel for responsive layouts** - When controls should be horizontal on wide screens but wrap to vertical on narrow screens, use WrapPanel instead of fixed Grid layouts +- **Avoid Grid row collisions in responsive layouts** - When using code-behind `SizeChanged` handlers to reposition elements between Grid rows, ensure moved elements don't share a row with existing static content. Always verify that the target row is either empty or the static content is also relocated - **Prefer dynamic resources for colors** - Use `{DynamicResource}` instead of `{StaticResource}` for colors to ensure theme compatibility -For detailed UI styling guidelines and examples, see: `src/UI/Windows/Styles/StyleGuide.md` +### Documentation References + +**Shared Design System (Guidelines Library):** +- `lib/Guidelines/src/ZBitSystems.Wpf.UI/Styles/StyleGuide.md` - Complete style guide with design tokens, semantic colors, and component styles + +**OSDP-Specific Styling:** +- `src/UI/Windows/Styles/LayoutTemplates.xaml` - Application-specific layout templates (connection status, page headers with activity indicators) +- `src/UI/Windows/Styles/ExampleImplementation.md` - OSDP-specific implementation examples and migration guide diff --git a/OSDP-Bench.sln b/OSDP-Bench.sln index 50cfe1f..9f41519 100644 --- a/OSDP-Bench.sln +++ b/OSDP-Bench.sln @@ -17,6 +17,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ci", "ci", "{5D426D86-43BB- ci\azure-pipelines.yml = ci\azure-pipelines.yml ci\build.yml = ci\build.yml ci\package.yml = ci\package.yml + ci\ui-tests.yml = ci\ui-tests.yml EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Windows", "src\UI\Windows\Windows.csproj", "{DD8BD355-FB29-42BD-AADD-C0E391EE1825}" @@ -30,6 +31,18 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docs", "Docs", "{ED2EC291-3 CLAUDE.md = CLAUDE.md EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "lib", "lib", "{3A8DF596-E814-FECC-DD4B-D8EF8AAC1A0D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Guidelines", "Guidelines", "{704960A3-6C7D-5CB1-1F34-76459FC7CBAE}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{DDC3EA95-9E27-4E54-C9B7-EF99E65DDDE0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ZBitSystems.Wpf.UI", "lib\Guidelines\src\ZBitSystems.Wpf.UI\ZBitSystems.Wpf.UI.csproj", "{B5DAB0A8-CF87-4799-8BAE-08CE2A33E97E}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{0C88DD14-F956-CE84-757C-A364CCF449FC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UI.Tests", "test\UI.Tests\UI.Tests.csproj", "{D8F38927-04C5-4FB4-B7CC-50E201818F89}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -104,6 +117,46 @@ Global {79C7EB0F-A75B-4DA7-BDDF-12C9714B48DF}.Release|x64.Build.0 = Release|x64 {79C7EB0F-A75B-4DA7-BDDF-12C9714B48DF}.Release|x86.ActiveCfg = Release|Any CPU {79C7EB0F-A75B-4DA7-BDDF-12C9714B48DF}.Release|x86.Build.0 = Release|Any CPU + {B5DAB0A8-CF87-4799-8BAE-08CE2A33E97E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B5DAB0A8-CF87-4799-8BAE-08CE2A33E97E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B5DAB0A8-CF87-4799-8BAE-08CE2A33E97E}.Debug|ARM.ActiveCfg = Debug|Any CPU + {B5DAB0A8-CF87-4799-8BAE-08CE2A33E97E}.Debug|ARM.Build.0 = Debug|Any CPU + {B5DAB0A8-CF87-4799-8BAE-08CE2A33E97E}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {B5DAB0A8-CF87-4799-8BAE-08CE2A33E97E}.Debug|ARM64.Build.0 = Debug|Any CPU + {B5DAB0A8-CF87-4799-8BAE-08CE2A33E97E}.Debug|x64.ActiveCfg = Debug|Any CPU + {B5DAB0A8-CF87-4799-8BAE-08CE2A33E97E}.Debug|x64.Build.0 = Debug|Any CPU + {B5DAB0A8-CF87-4799-8BAE-08CE2A33E97E}.Debug|x86.ActiveCfg = Debug|Any CPU + {B5DAB0A8-CF87-4799-8BAE-08CE2A33E97E}.Debug|x86.Build.0 = Debug|Any CPU + {B5DAB0A8-CF87-4799-8BAE-08CE2A33E97E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B5DAB0A8-CF87-4799-8BAE-08CE2A33E97E}.Release|Any CPU.Build.0 = Release|Any CPU + {B5DAB0A8-CF87-4799-8BAE-08CE2A33E97E}.Release|ARM.ActiveCfg = Release|Any CPU + {B5DAB0A8-CF87-4799-8BAE-08CE2A33E97E}.Release|ARM.Build.0 = Release|Any CPU + {B5DAB0A8-CF87-4799-8BAE-08CE2A33E97E}.Release|ARM64.ActiveCfg = Release|Any CPU + {B5DAB0A8-CF87-4799-8BAE-08CE2A33E97E}.Release|ARM64.Build.0 = Release|Any CPU + {B5DAB0A8-CF87-4799-8BAE-08CE2A33E97E}.Release|x64.ActiveCfg = Release|Any CPU + {B5DAB0A8-CF87-4799-8BAE-08CE2A33E97E}.Release|x64.Build.0 = Release|Any CPU + {B5DAB0A8-CF87-4799-8BAE-08CE2A33E97E}.Release|x86.ActiveCfg = Release|Any CPU + {B5DAB0A8-CF87-4799-8BAE-08CE2A33E97E}.Release|x86.Build.0 = Release|Any CPU + {D8F38927-04C5-4FB4-B7CC-50E201818F89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D8F38927-04C5-4FB4-B7CC-50E201818F89}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D8F38927-04C5-4FB4-B7CC-50E201818F89}.Debug|ARM.ActiveCfg = Debug|Any CPU + {D8F38927-04C5-4FB4-B7CC-50E201818F89}.Debug|ARM.Build.0 = Debug|Any CPU + {D8F38927-04C5-4FB4-B7CC-50E201818F89}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {D8F38927-04C5-4FB4-B7CC-50E201818F89}.Debug|ARM64.Build.0 = Debug|Any CPU + {D8F38927-04C5-4FB4-B7CC-50E201818F89}.Debug|x64.ActiveCfg = Debug|Any CPU + {D8F38927-04C5-4FB4-B7CC-50E201818F89}.Debug|x64.Build.0 = Debug|Any CPU + {D8F38927-04C5-4FB4-B7CC-50E201818F89}.Debug|x86.ActiveCfg = Debug|Any CPU + {D8F38927-04C5-4FB4-B7CC-50E201818F89}.Debug|x86.Build.0 = Debug|Any CPU + {D8F38927-04C5-4FB4-B7CC-50E201818F89}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D8F38927-04C5-4FB4-B7CC-50E201818F89}.Release|Any CPU.Build.0 = Release|Any CPU + {D8F38927-04C5-4FB4-B7CC-50E201818F89}.Release|ARM.ActiveCfg = Release|Any CPU + {D8F38927-04C5-4FB4-B7CC-50E201818F89}.Release|ARM.Build.0 = Release|Any CPU + {D8F38927-04C5-4FB4-B7CC-50E201818F89}.Release|ARM64.ActiveCfg = Release|Any CPU + {D8F38927-04C5-4FB4-B7CC-50E201818F89}.Release|ARM64.Build.0 = Release|Any CPU + {D8F38927-04C5-4FB4-B7CC-50E201818F89}.Release|x64.ActiveCfg = Release|Any CPU + {D8F38927-04C5-4FB4-B7CC-50E201818F89}.Release|x64.Build.0 = Release|Any CPU + {D8F38927-04C5-4FB4-B7CC-50E201818F89}.Release|x86.ActiveCfg = Release|Any CPU + {D8F38927-04C5-4FB4-B7CC-50E201818F89}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -111,6 +164,11 @@ Global GlobalSection(NestedProjects) = preSolution {5D426D86-43BB-4D6D-A6BF-0ACC65A19C92} = {8FB5794C-299E-4B9E-8D0D-6BFC695DA91B} {DD8BD355-FB29-42BD-AADD-C0E391EE1825} = {6AF5FFEF-AFBA-4BB8-A14F-FDE4AEEC19AB} + {704960A3-6C7D-5CB1-1F34-76459FC7CBAE} = {3A8DF596-E814-FECC-DD4B-D8EF8AAC1A0D} + {DDC3EA95-9E27-4E54-C9B7-EF99E65DDDE0} = {704960A3-6C7D-5CB1-1F34-76459FC7CBAE} + {B5DAB0A8-CF87-4799-8BAE-08CE2A33E97E} = {DDC3EA95-9E27-4E54-C9B7-EF99E65DDDE0} + {0F31AAB4-B6FB-4DF1-B6BA-2AF58FADB005} = {0C88DD14-F956-CE84-757C-A364CCF449FC} + {D8F38927-04C5-4FB4-B7CC-50E201818F89} = {0C88DD14-F956-CE84-757C-A364CCF449FC} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {09928BD4-7D4E-4C9A-86DA-49BC36820716} diff --git a/README.md b/README.md index acf4738..78eadf2 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A professional tool for configuring and troubleshooting OSDP devices. -[![.NET](https://img.shields.io/badge/.NET-8.0-blue)](https://dotnet.microsoft.com/) +[![.NET](https://img.shields.io/badge/.NET-10.0-blue)](https://dotnet.microsoft.com/) [![License](https://img.shields.io/badge/License-Eclipse%202.0-green.svg)](LICENSE) ## About @@ -21,7 +21,7 @@ Core functionality is under an open source license to help increase the adoption - **Packet Tracing** - View detailed OSDP communication packets - **Capture Packet Export** - Export captured packets for offline analysis and sharing - **Multi-language Support** - Available in multiple languages -- **Cross-platform Core** - Core logic library built on .NET 8.0 can be reused across different platforms +- **Cross-platform Core** - Core logic library built on .NET 10.0 can be reused across different platforms ## Get OSDP Bench @@ -36,15 +36,15 @@ OSDP Bench is available for purchase on multiple platforms: ### Prerequisites -- .NET 8.0 SDK or later -- Windows 10/11 (for WinUI version) +- .NET 10.0 SDK or later +- Windows 10/11 (for Windows version) - Serial port access for device communication ### Building from Source -1. Clone the repository: +1. Clone the repository with submodules: ```bash - git clone https://github.com/bytedreamer/OSDP-Bench.git + git clone --recursive https://github.com/bytedreamer/OSDP-Bench.git cd OSDP-Bench ``` @@ -68,17 +68,14 @@ OSDP Bench is available for purchase on multiple platforms: ## Documentation ### Project Documentation -- **[Developer Guidelines](docs/CLAUDE.md)** - Development guidelines and build commands -- **[UI Style Guide](src/UI/Windows/Styles/StyleGuide.md)** - Comprehensive design system and styling guidelines +- **[Developer Guidelines](CLAUDE.md)** - Development guidelines and build commands + +### Shared Libraries +- **[Guidelines](https://github.com/Z-bit-Systems-LLC/Guidelines)** - Shared WPF components and CI/CD documentation ### Architecture Plans - **[Connection Plugin Architecture](docs/CONNECTION_PLUGIN_ARCHITECTURE.md)** - Plan for implementing pluggable connection types (Serial, Bluetooth, Network) -### Localization -- **[Localization Plan](docs/LOCALIZATION_PLAN.md)** - Multi-language support implementation -- **[Language Switching](docs/LANGUAGE_SWITCHING_DEMO.md)** - Language switching functionality -- **[Language Fixes](docs/LANGUAGE_SWITCHING_FIX.md)** - Language switching issue fixes - ## Contributing We welcome contributions! Please follow these guidelines: diff --git a/ci/azure-pipelines.yml b/ci/azure-pipelines.yml index 461a9b9..886cd51 100644 --- a/ci/azure-pipelines.yml +++ b/ci/azure-pipelines.yml @@ -8,6 +8,14 @@ trigger: pr: none +schedules: +- cron: '0 6 * * *' + displayName: 'Daily full UI test run' + branches: + include: + - main + always: true + pool: vmImage: 'windows-latest' @@ -22,8 +30,34 @@ jobs: vmImage: 'windows-latest' steps: - checkout: self + submodules: true - template: build.yml + - job: ui_tests + displayName: 'UI Smoke Tests' + pool: + vmImage: 'windows-latest' + dependsOn: + build + continueOnError: true + steps: + - checkout: self + submodules: true + - template: ui-tests-smoke.yml + + - job: ui_tests_full + displayName: 'UI Tests (Full)' + condition: or(eq(variables['Build.Reason'], 'Schedule'), startsWith(variables['Build.SourceBranch'], 'refs/tags/v')) + pool: + vmImage: 'windows-latest' + dependsOn: + build + continueOnError: true + steps: + - checkout: self + submodules: true + - template: ui-tests.yml + - job: package displayName: 'Package and Publish' condition: and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/v')) @@ -33,6 +67,7 @@ jobs: build steps: - checkout: self + submodules: true persistCredentials: true clean: true - template: package.yml diff --git a/ci/build.yml b/ci/build.yml index 5fb76d4..a49e3b0 100644 --- a/ci/build.yml +++ b/ci/build.yml @@ -16,7 +16,7 @@ steps: displayName: 'dotnet test' inputs: command: 'test' - projects: 'test/*Tests/*.csproj' + projects: 'test/Core.Tests/Core.Tests.csproj' arguments: '--configuration $(buildConfiguration) --collect:"XPlat Code Coverage"' - task: reportgenerator@5 diff --git a/ci/ui-tests-smoke.yml b/ci/ui-tests-smoke.yml new file mode 100644 index 0000000..45ccd2f --- /dev/null +++ b/ci/ui-tests-smoke.yml @@ -0,0 +1,20 @@ +steps: + - task: UseDotNet@2 + displayName: 'Install .NET SDK' + inputs: + packageType: 'sdk' + version: '10.x' + + - task: DotNetCoreCLI@2 + displayName: 'dotnet build UI.Tests' + inputs: + command: 'build' + projects: 'test/UI.Tests/UI.Tests.csproj' + arguments: '--configuration $(buildConfiguration)' + + - task: DotNetCoreCLI@2 + displayName: 'dotnet test UI.Tests (Smoke)' + inputs: + command: 'test' + projects: 'test/UI.Tests/UI.Tests.csproj' + arguments: '--configuration $(buildConfiguration) --filter TestCategory=Smoke' diff --git a/ci/ui-tests.yml b/ci/ui-tests.yml new file mode 100644 index 0000000..ae25884 --- /dev/null +++ b/ci/ui-tests.yml @@ -0,0 +1,20 @@ +steps: + - task: UseDotNet@2 + displayName: 'Install .NET SDK' + inputs: + packageType: 'sdk' + version: '10.x' + + - task: DotNetCoreCLI@2 + displayName: 'dotnet build UI.Tests' + inputs: + command: 'build' + projects: 'test/UI.Tests/UI.Tests.csproj' + arguments: '--configuration $(buildConfiguration)' + + - task: DotNetCoreCLI@2 + displayName: 'dotnet test UI.Tests' + inputs: + command: 'test' + projects: 'test/UI.Tests/UI.Tests.csproj' + arguments: '--configuration $(buildConfiguration)' diff --git a/docs/LANGUAGE_SWITCHING_DEMO.md b/docs/LANGUAGE_SWITCHING_DEMO.md deleted file mode 100644 index 3aaf1ee..0000000 --- a/docs/LANGUAGE_SWITCHING_DEMO.md +++ /dev/null @@ -1,131 +0,0 @@ -# Language Switching UI Implementation - -## βœ… Implementation Complete! - -The UI language switching functionality has been successfully implemented in OSDP-Bench. Users can now change the application language dynamically through the UI. - -## 🎯 Features Implemented - -### **1. Language Selection UI** -- **Location**: Language selector in the main window title bar -- **Control**: ComboBox showing available languages with native names -- **Tooltip**: Helpful tooltip showing "Select Language" - -### **2. Supported Languages** -Currently configured for: -- **English** (en-US) - Fully implemented -- **Spanish** (es-ES) - Sample translations provided -- **French** (fr-FR) - Ready for translation -- **German** (de-DE) - Ready for translation -- **Japanese** (ja-JP) - Ready for translation - -### **3. Dynamic UI Updates** -- **Real-time switching**: UI updates immediately when language is changed -- **All components respond**: XAML bindings, ViewModels, and code-behind all update -- **Persistent selection**: Selected language is maintained during app session -- **Error handling**: Graceful fallback to English if translation fails - -## πŸ› οΈ Technical Implementation - -### **Architecture Components:** - -#### **LanguageSelectionViewModel** -```csharp -public partial class LanguageSelectionViewModel : ObservableObject -{ - public ObservableCollection AvailableLanguages { get; } - [ObservableProperty] private LanguageItem? _selectedLanguage; - - // Automatically triggers language change when selection changes - partial void OnSelectedLanguageChanged(LanguageItem? value) { ... } -} -``` - -#### **Enhanced Resources Class** -```csharp -public class Resources : INotifyPropertyChanged -{ - public static event PropertyChangedEventHandler? PropertyChanged; - public static void ChangeCulture(CultureInfo newCulture) { ... } - public static string GetString(string key) { ... } -} -``` - -#### **Dynamic LocalizeExtension** -- Creates bindings that automatically update when culture changes -- Uses `LocalizedStringBinding` for live UI updates -- Fallback to static strings if binding fails - -#### **Enhanced LocalizationService** -```csharp -public interface ILocalizationService -{ - void ChangeCulture(CultureInfo culture); - void ChangeCulture(string cultureName); - event EventHandler? CultureChanged; -} -``` - -### **UI Integration:** - -#### **MainWindow.xaml** -```xml - - - - - - -``` - -#### **Dependency Injection** -```csharp -services.AddSingleton(); -``` - -## πŸ§ͺ How to Test - -### **1. Using the UI (when Windows project runs):** -1. Open the application -2. Look for the "Language" dropdown in the title bar -3. Select "EspaΓ±ol" from the dropdown -4. Watch as the UI elements update to Spanish -5. Navigate between pages to see consistent translation - -### **2. Programmatically:** -```csharp -var localizationService = serviceProvider.GetService(); -localizationService.ChangeCulture("es-ES"); // Switch to Spanish -localizationService.ChangeCulture("en-US"); // Switch back to English -``` - -## πŸ“Š Sample Translations Provided - -The Spanish resource file (`Resources.es.resx`) includes sample translations for: -- **Connection Status**: Connected β†’ Conectado, Disconnected β†’ Desconectado -- **Page Titles**: Connect β†’ Conectar, Manage β†’ Gestionar, Monitor β†’ Monitor -- **Navigation**: Connect to PD β†’ Conectar a PD -- **UI Elements**: Language β†’ Idioma, Select Language β†’ Seleccionar idioma - -## πŸš€ Ready for Production - -### **To add new languages:** -1. Create new resource file: `Resources.[culture].resx` (e.g., `Resources.fr.resx`) -2. Add translations for all keys from the main `Resources.resx` -3. The language will automatically appear in the dropdown - -### **Translation workflow:** -1. Export main `Resources.resx` keys -2. Send to translators -3. Import translated strings into culture-specific files -4. Deploy and test - -## 🎯 Next Steps Available - -1. **Culture Persistence**: Save user's language preference to settings -2. **Translation Management**: Build tools for managing translations -3. **RTL Support**: Add right-to-left language support -4. **Professional Translation**: Replace sample translations with professional ones - -The language switching infrastructure is production-ready and easily extensible for additional languages and features! \ No newline at end of file diff --git a/docs/LANGUAGE_SWITCHING_FIX.md b/docs/LANGUAGE_SWITCHING_FIX.md deleted file mode 100644 index 8e80fd7..0000000 --- a/docs/LANGUAGE_SWITCHING_FIX.md +++ /dev/null @@ -1,76 +0,0 @@ -# Language Switching UI Build Fix - -## ❌ Issue Identified -**Error**: `'ResourceDictionary' does not contain a definition for 'PropertyChanged'` - -**Root Cause**: In WPF applications, when you use `Resources.PropertyChanged`, the compiler interprets `Resources` as the WPF built-in `Resources` property (which is a `ResourceDictionary`) instead of our custom `OSDPBench.Core.Resources.Resources` class. - -## βœ… Fix Applied - -### **Files Fixed:** - -#### **1. ConnectPage.xaml.cs** -```csharp -// BEFORE (❌ Error) -Resources.PropertyChanged += OnResourcesPropertyChanged; - -// AFTER (βœ… Fixed) -OSDPBench.Core.Resources.Resources.PropertyChanged += OnResourcesPropertyChanged; -``` - -#### **2. LocalizedStringBinding.cs** -```csharp -// BEFORE (❌ Error) -Resources.PropertyChanged += OnResourcesPropertyChanged; -public string Value => Resources.GetString(_key); -Resources.PropertyChanged -= OnResourcesPropertyChanged; - -// AFTER (βœ… Fixed) -OSDPBench.Core.Resources.Resources.PropertyChanged += OnResourcesPropertyChanged; -public string Value => OSDPBench.Core.Resources.Resources.GetString(_key); -OSDPBench.Core.Resources.Resources.PropertyChanged -= OnResourcesPropertyChanged; -``` - -#### **3. Cleanup** -- Removed unnecessary `using OSDPBench.Core.Resources;` statements -- Used fully qualified names to avoid ambiguity - -## 🎯 Resolution Strategy - -### **Why This Happens:** -In WPF, every `FrameworkElement` has a `Resources` property of type `ResourceDictionary`. When we wrote: -```csharp -Resources.PropertyChanged += ... -``` -The compiler thought we meant: -```csharp -this.Resources.PropertyChanged += ... // ResourceDictionary doesn't have PropertyChanged! -``` - -### **Solution:** -Use fully qualified class names to be explicit: -```csharp -OSDPBench.Core.Resources.Resources.PropertyChanged += ... -``` - -## βœ… Verification - -### **Build Status:** -- βœ… Core project builds successfully -- βœ… All 58 tests pass -- βœ… No compilation errors -- βœ… Dynamic language switching functionality intact -- βœ… UI binding system working correctly - -### **Components Verified:** -- βœ… Resources class with INotifyPropertyChanged -- βœ… LocalizedStringBinding for dynamic updates -- βœ… ConnectPage dynamic property updates -- βœ… LanguageSelectionViewModel (was already correct) - -## πŸš€ Result - -The UI language switching system now builds correctly and is ready for use! The namespace collision issue has been resolved while maintaining all the dynamic functionality. - -### **Key Learning:** -When working with WPF applications that have custom `Resources` classes, always use fully qualified names to avoid conflicts with the built-in WPF `Resources` property. \ No newline at end of file diff --git a/docs/LOCALIZATION_PLAN.md b/docs/LOCALIZATION_PLAN.md deleted file mode 100644 index 59efbe8..0000000 --- a/docs/LOCALIZATION_PLAN.md +++ /dev/null @@ -1,214 +0,0 @@ -# OSDP-Bench Localization Plan - -## Overview -This document outlines the tasks required to implement full localization support for the OSDP-Bench application UI. - -## Task List - -### 1. Infrastructure Setup -- [x] Create Resources folder structure in Core and Windows projects -- [x] Set up default English resource files (Resources.resx) -- [x] Configure resource file properties for code generation -- [x] Add necessary NuGet packages for localization support -- [x] Consolidate all resources into Core project for cross-platform sharing - -### 2. String Extraction - -#### XAML Files -Extract all hardcoded strings from: -- [x] **ConnectPage.xaml** - - "Serial Port Selection" - - "Connect to PD" - - "Discovery will only work properly with a single device connected" - - "Start Discovery", "Cancel Discovery", "Disconnect" - - "Baud Rate", "Address", "Use Secure Channel", "Use Default Key" - - "Security Key" - - Connection status messages - -- [x] **ManagePage.xaml** - - "Device Information" - - "Device Action" - - "Device has not been Identified" - - "The Connection page will provide more details" - - "S/N - " prefix - -- [x] **MonitorPage.xaml** - - "Device is not connected" - - "The Connection page will provide more details" - - "Monitoring is not available for secure channel" - - "An update will be out soon that supports secure channel" - - DataGrid column headers: "TimeStamp", "Interval (ms)", "Direction", "Address", "Type", "Details" - - "Expand" button text - -- [x] **InfoPage.xaml** - - "OSDP Bench" - - "License Info" - - License type headers: "EPL 2.0", "Apache 2.0", "MIT" - -- [x] **MainWindow.xaml** - - Window title - - Navigation menu items - - Any tooltips or status bar text - -#### ViewModels -Extract strings from: -- [x] **ConnectViewModel** - - Status messages (Connected, Disconnected, Discovering, etc.) - - Error messages - - USB status messages - - Validation messages - -- [x] **ManageViewModel** - - Device action names - - Status messages - - Error handling messages - -- [x] **MonitorViewModel** - - Any dynamic status or error messages (No hardcoded strings found) - -#### Code-behind Files -- [x] Extract any UI-related strings from .xaml.cs files -- [x] Review converters for hardcoded strings (No localization needed) - -### 3. Resource Implementation - -#### Resource Files Structure -- [x] Create Resources.resx (default/English) -- [x] Create Resources.Designer.cs (auto-generated) -- [x] Set up comprehensive resource categories: - - Connection Status Messages - - USB Status Messages - - Error Messages - - Page Titles - - UI Elements (buttons, labels, headers) - - Dialog Messages - - Console Error Messages - - Activity Indicators - - Navigation Menu Items -- [ ] Set up resource file naming convention for other languages: - - Resources.es.resx (Spanish) - - Resources.fr.resx (French) - - Resources.de.resx (German) - - Resources.ja.resx (Japanese) - - etc. - -#### XAML Updates -- [x] Replace hardcoded strings with resource bindings -- [x] Implement markup extension for easy resource access -- [x] Fix compilation issues with markup extension -- [x] Update all DataTemplates -- [x] Update all Converters that return strings (No changes needed) - -### 4. Localization Service Implementation - -- [x] Create ILocalizationService interface -- [x] Implement LocalizationService with: - - Current culture property - - Culture change event - - Get localized string method - - Supported cultures list - -- [x] Integrate with dependency injection -- [x] Implement culture persistence in user settings - -### 5. Dynamic Language Switching - -- [x] Implement INotifyPropertyChanged for resource changes -- [x] Create mechanism to refresh all UI elements -- [x] Handle special cases: - - ComboBox items - - Dynamic content - - Data-bound text -- [x] Create LocalizedStringBinding for automatic UI updates -- [x] Fix namespace collision with WPF ResourceDictionary - -### 6. UI Enhancements - -- [x] Add language selection UI in InfoPage -- [x] Create LanguageSelectionViewModel with culture management -- [x] Implement automatic Windows locale detection -- [x] Add sample Spanish translations for demonstration -- [x] Display current language indicator in language dropdown - -### 7. Culture-Specific Formatting - -- [ ] Configure number formatting per culture -- [ ] Configure date/time formatting -- [ ] Handle decimal separators -- [ ] Handle currency if applicable - -### 8. RTL Language Support - -- [ ] Test FlowDirection for RTL languages -- [ ] Adjust layouts for RTL compatibility -- [ ] Mirror appropriate icons/images - -### 9. Testing & Validation - -- [ ] Create test translation files -- [ ] Test all UI elements with longest possible translations -- [ ] Test with shortest translations -- [ ] Verify no text truncation -- [ ] Test dynamic language switching -- [ ] Test culture-specific formatting - -### 10. Documentation - -- [ ] Create translator guidelines document -- [ ] Document string context for translators -- [ ] Create translation template file -- [ ] Document how to add new languages -- [ ] Create list of do's and don'ts for translators - -### 11. Advanced Features (Future) - -- [ ] Implement pluralization support -- [ ] Add context-specific translations -- [ ] Support for regional variants (en-US vs en-GB) -- [ ] Translation memory integration -- [ ] Automated translation testing - -## Implementation Priority - -1. **High Priority**: Infrastructure, string extraction, basic resource implementation βœ… **COMPLETED** -2. **Medium Priority**: Dynamic switching, UI for language selection βœ… **COMPLETED** -3. **Low Priority**: RTL support, advanced features, comprehensive documentation - -## Current Status (Final Update) - -### βœ… Completed Tasks: -- **Complete Infrastructure Setup** - All resource files, folder structure, and build configuration -- **Complete String Extraction** - All XAML files, ViewModels, and code-behind files processed -- **Complete Resource Implementation** - Comprehensive Resources.resx with 500+ localized strings -- **Working Localization System** - All hardcoded strings replaced with resource calls -- **Dynamic Language Switching** - Real-time UI updates when language changes -- **Language Selection UI** - ComboBox in InfoPage with automatic Windows locale detection -- **Build Verification** - Core project compiles, all 58 tests pass, Windows project loads correctly - -### πŸ“Š Resource Categories Implemented: -- **117 Connection Status Messages** - All device states and connection scenarios -- **4 USB Status Messages** - Device insertion/removal notifications -- **8 Error Messages** - Connection failures, validation errors -- **4 Page Titles** - All major application pages -- **50+ UI Elements** - Buttons, labels, headers, form controls -- **8 Dialog Messages** - Reset device, update communications, vendor lookup -- **2 Console Error Messages** - Debugging and error logging -- **2 Activity Indicators** - Tx/Rx communication status -- **4 Navigation Menu Items** - Main application navigation - -### πŸš€ System Ready for Production: -The localization system is **fully functional** with dynamic language switching! The application automatically detects Windows locale and provides a user-friendly language selection interface with persistent settings. Remaining optional tasks: -1. Add professional translations for additional languages (Resources.fr.resx, Resources.de.resx, etc.) -2. Add RTL language support for future languages - -## Notes - -- Consider using WPF Localization Extension (WPFLocalizeExtension) NuGet package for easier implementation -- Ensure all developers follow localization practices for new features -- Set up CI/CD to validate resource files -- Consider professional translation services for production languages - -## Resources - -- [WPF Globalization and Localization Overview](https://docs.microsoft.com/en-us/dotnet/desktop/wpf/advanced/wpf-globalization-and-localization-overview) -- [Best Practices for Developing World-Ready Applications](https://docs.microsoft.com/en-us/dotnet/standard/globalization-localization/best-practices-for-developing-world-ready-apps) \ No newline at end of file diff --git a/lib/Guidelines b/lib/Guidelines new file mode 160000 index 0000000..252a0f9 --- /dev/null +++ b/lib/Guidelines @@ -0,0 +1 @@ +Subproject commit 252a0f94ba8aa40896f83cce3417e046e1131dc4 diff --git a/src/Core/Actions/ControlBuzzerAction.cs b/src/Core/Actions/ControlBuzzerAction.cs index 7e50f4d..f43e312 100644 --- a/src/Core/Actions/ControlBuzzerAction.cs +++ b/src/Core/Actions/ControlBuzzerAction.cs @@ -1,5 +1,7 @@ -ο»Ώusing OSDP.Net; +using OSDP.Net; using OSDP.Net.Model.CommandData; +using OSDP.Net.Model.ReplyData; +using OSDPBench.Core.Models; namespace OSDPBench.Core.Actions; @@ -12,14 +14,24 @@ public class ControlBuzzerAction : IDeviceAction public string Name => "Test Buzzer"; /// - public string PerformActionName => "Three Quick Beeps"; + public string PerformActionName => Resources.Resources.GetString("Manage_Send"); + + /// + public CapabilityFunction? RequiredCapability => CapabilityFunction.ReaderAudibleOutput; /// public async Task PerformAction(ControlPanel panel, Guid connectionId, byte address, object? parameter) { + var buzzerParams = parameter as BuzzerParameters; + var result = await panel.ReaderBuzzerControl(connectionId, address, - new ReaderBuzzerControl(0, ToneCode.Default, 1, 1, 3)); + new ReaderBuzzerControl( + buzzerParams?.ReaderNumber ?? 0, + buzzerParams?.ToneCode ?? ToneCode.Default, + buzzerParams?.OnTime ?? 1, + buzzerParams?.OffTime ?? 1, + buzzerParams?.Count ?? 3)); return result; } -} \ No newline at end of file +} diff --git a/src/Core/Actions/IDeviceAction.cs b/src/Core/Actions/IDeviceAction.cs index 2d12ec1..ee1fa05 100644 --- a/src/Core/Actions/IDeviceAction.cs +++ b/src/Core/Actions/IDeviceAction.cs @@ -1,4 +1,5 @@ ο»Ώusing OSDP.Net; +using OSDP.Net.Model.ReplyData; namespace OSDPBench.Core.Actions; @@ -17,6 +18,11 @@ public interface IDeviceAction /// string PerformActionName { get; } + /// + /// Gets the capability function required for this action, or null if no capability check is needed. + /// + CapabilityFunction? RequiredCapability => null; + /// /// Performs an action on a device. /// diff --git a/src/Core/Actions/SetLedAction.cs b/src/Core/Actions/SetLedAction.cs index 59cda88..77acddb 100644 --- a/src/Core/Actions/SetLedAction.cs +++ b/src/Core/Actions/SetLedAction.cs @@ -1,5 +1,7 @@ -ο»Ώusing OSDP.Net; +using OSDP.Net; using OSDP.Net.Model.CommandData; +using OSDP.Net.Model.ReplyData; +using OSDPBench.Core.Models; namespace OSDPBench.Core.Actions; @@ -12,50 +14,35 @@ public class SetReaderLedAction : IDeviceAction public string Name => "Test LED"; /// - public string PerformActionName => "Flash"; - - /// - /// Available LED colors for selection - /// - public static readonly Dictionary AvailableColors = new() - { - { "Red", LedColor.Red }, - { "Green", LedColor.Green }, - { "Amber", LedColor.Amber }, - { "Blue", LedColor.Blue } - }; + public string PerformActionName => Resources.Resources.GetString("Manage_Send"); + + /// + public CapabilityFunction? RequiredCapability => CapabilityFunction.ReaderLEDControl; /// public async Task PerformAction(ControlPanel panel, Guid connectionId, byte address, object? parameter) { - // Default to red if no color is specified - var selectedColor = LedColor.Red; - - // Parse the color parameter if provided - if (parameter is string colorName && AvailableColors.TryGetValue(colorName, out var color)) - { - selectedColor = color; - } + var ledParams = parameter as LedParameters; var result = await panel.ReaderLedControl(connectionId, address, new ReaderLedControls([ new ReaderLedControl( - 0, // LED number - 0, // reader number - TemporaryReaderControlCode.SetTemporaryAndStartTimer, - 10, // temporary timer - 10, // temporary timer count - selectedColor, // temporary on color - LedColor.Black, // temporary off color - 50, // temporary blink rate - PermanentReaderControlCode.Nop, - 0, // permanent timer - 0, // permanent timer count - LedColor.Black, // permanent on color - LedColor.Black // permanent off color + ledParams?.ReaderNumber ?? 0, + ledParams?.LedNumber ?? 0, + ledParams?.TemporaryMode ?? TemporaryReaderControlCode.SetTemporaryAndStartTimer, + ledParams?.TemporaryOnTime ?? 10, + ledParams?.TemporaryOffTime ?? 10, + ledParams?.TemporaryOnColor ?? LedColor.Red, + ledParams?.TemporaryOffColor ?? LedColor.Black, + ledParams?.TemporaryTimer ?? 50, + ledParams?.PermanentMode ?? PermanentReaderControlCode.Nop, + ledParams?.PermanentOnTime ?? 0, + ledParams?.PermanentOffTime ?? 0, + ledParams?.PermanentOnColor ?? LedColor.Black, + ledParams?.PermanentOffColor ?? LedColor.Black ) ])); return result; } -} \ No newline at end of file +} diff --git a/src/Core/Models/BuzzerParameters.cs b/src/Core/Models/BuzzerParameters.cs new file mode 100644 index 0000000..3a536c9 --- /dev/null +++ b/src/Core/Models/BuzzerParameters.cs @@ -0,0 +1,40 @@ +using OSDP.Net.Model.CommandData; + +namespace OSDPBench.Core.Models; + +/// +/// Represents user-configurable parameters for the buzzer control command. +/// +public class BuzzerParameters +{ + /// + /// The reader number (0 = first reader). + /// + public byte ReaderNumber { get; set; } + + /// + /// The tone code to use (Off or Default). + /// + public ToneCode ToneCode { get; set; } = ToneCode.Default; + + /// + /// ON time in units of 100ms. + /// + public byte OnTime { get; set; } = 1; + + /// + /// OFF time in units of 100ms. + /// + public byte OffTime { get; set; } = 1; + + /// + /// Repeat count (0 = infinite). + /// + public byte Count { get; set; } = 3; + + /// + /// Gets a value indicating whether the ON time is invalid. + /// ON time must be non-zero unless the tone code is set to off. + /// + public bool IsOnTimeInvalid => ToneCode != ToneCode.Off && OnTime == 0; +} diff --git a/src/Core/Models/CapablitiesLookup.cs b/src/Core/Models/CapablitiesLookup.cs index ce26c4c..14ec4be 100644 --- a/src/Core/Models/CapablitiesLookup.cs +++ b/src/Core/Models/CapablitiesLookup.cs @@ -21,6 +21,18 @@ public CapabilitiesLookup(DeviceCapabilities deviceCapabilities) SecureChannel = deviceCapabilities.Capabilities .FirstOrDefault(capability => capability.Function == CapabilityFunction.CommunicationSecurity) ?.Compliance == 1; + + AudioOutputComplianceLevel = deviceCapabilities.Capabilities + .FirstOrDefault(capability => capability.Function == CapabilityFunction.ReaderAudibleOutput) + ?.Compliance ?? 0; + + AudioOutput = AudioOutputComplianceLevel >= 1; + + LedControlComplianceLevel = deviceCapabilities.Capabilities + .FirstOrDefault(capability => capability.Function == CapabilityFunction.ReaderLEDControl) + ?.Compliance ?? 0; + + LedControl = LedControlComplianceLevel >= 1; } /// @@ -33,4 +45,24 @@ public CapabilitiesLookup(DeviceCapabilities deviceCapabilities) /// // ReSharper disable once UnusedAutoPropertyAccessor.Global public bool SecureChannel { get; } + + /// + /// Is audio output (buzzer) capable + /// + public bool AudioOutput { get; } + + /// + /// Audio output compliance level (0 = not supported, 1 = on/off only, 2 = timed operation) + /// + public byte AudioOutputComplianceLevel { get; } + + /// + /// Is LED control capable + /// + public bool LedControl { get; } + + /// + /// LED control compliance level (0 = not supported, 1 = on/off only, 2 = timed operation, 3 = timed, bi-color, tri-color) + /// + public byte LedControlComplianceLevel { get; } } \ No newline at end of file diff --git a/src/Core/Models/LedParameters.cs b/src/Core/Models/LedParameters.cs new file mode 100644 index 0000000..a206f1a --- /dev/null +++ b/src/Core/Models/LedParameters.cs @@ -0,0 +1,90 @@ +using OSDP.Net.Model.CommandData; + +namespace OSDPBench.Core.Models; + +/// +/// Represents user-configurable parameters for the LED control command. +/// +public class LedParameters +{ + /// + /// The reader number (0 = first reader). + /// + public byte ReaderNumber { get; set; } + + /// + /// The LED number (0 = first LED). + /// + public byte LedNumber { get; set; } + + /// + /// The temporary reader control code. + /// + public TemporaryReaderControlCode TemporaryMode { get; set; } = TemporaryReaderControlCode.SetTemporaryAndStartTimer; + + /// + /// Temporary ON time in units of 100ms. + /// + public byte TemporaryOnTime { get; set; } = 10; + + /// + /// Temporary OFF time in units of 100ms. + /// + public byte TemporaryOffTime { get; set; } = 10; + + /// + /// The color displayed during the temporary ON state. + /// + public LedColor TemporaryOnColor { get; set; } = LedColor.Red; + + /// + /// The color displayed during the temporary OFF state. + /// + public LedColor TemporaryOffColor { get; set; } = LedColor.Black; + + /// + /// Total duration of the temporary state in units of 100ms. + /// + public ushort TemporaryTimer { get; set; } = 50; + + /// + /// The permanent reader control code. + /// + public PermanentReaderControlCode PermanentMode { get; set; } = PermanentReaderControlCode.Nop; + + /// + /// Permanent ON time in units of 100ms. + /// + public byte PermanentOnTime { get; set; } + + /// + /// Permanent OFF time in units of 100ms. + /// + public byte PermanentOffTime { get; set; } + + /// + /// The color displayed during the permanent ON state. + /// + public LedColor PermanentOnColor { get; set; } = LedColor.Black; + + /// + /// The color displayed during the permanent OFF state. + /// + public LedColor PermanentOffColor { get; set; } = LedColor.Black; + + /// + /// Gets a value indicating whether the temporary timing values are invalid. + /// Both ON and OFF time cannot be zero when the mode sets a temporary state. + /// + public bool IsTemporaryTimingInvalid => + TemporaryMode == TemporaryReaderControlCode.SetTemporaryAndStartTimer && + TemporaryOnTime == 0 && TemporaryOffTime == 0; + + /// + /// Gets a value indicating whether the permanent timing values are invalid. + /// Both ON and OFF time cannot be zero when the mode sets a permanent state. + /// + public bool IsPermanentTimingInvalid => + PermanentMode == PermanentReaderControlCode.SetPermanentState && + PermanentOnTime == 0 && PermanentOffTime == 0; +} diff --git a/src/Core/Models/UserSettings.cs b/src/Core/Models/UserSettings.cs deleted file mode 100644 index f10398e..0000000 --- a/src/Core/Models/UserSettings.cs +++ /dev/null @@ -1,42 +0,0 @@ -namespace OSDPBench.Core.Models; - -/// -/// Represents user settings that persist between application sessions -/// -public class UserSettings -{ - /// - /// Gets or sets the user's preferred culture/language - /// - public string PreferredCulture { get; set; } = "en-US"; - - /// - /// Gets or sets the window width - /// - public double WindowWidth { get; set; } = 800; - - /// - /// Gets or sets the window height - /// - public double WindowHeight { get; set; } = 600; - - /// - /// Gets or sets the window left position (null means center on screen) - /// - public double? WindowLeft { get; set; } - - /// - /// Gets or sets the window top position (null means center on screen) - /// - public double? WindowTop { get; set; } - - /// - /// Gets or sets whether the window is maximized - /// - public bool IsMaximized { get; set; } - - /// - /// Gets or sets whether to skip language mismatch checking - /// - public bool SkipLanguageMismatchCheck { get; set; } -} \ No newline at end of file diff --git a/src/Core/Resources/Resources.resx b/src/Core/Resources/Resources.resx index 0af3983..cb50b82 100644 --- a/src/Core/Resources/Resources.resx +++ b/src/Core/Resources/Resources.resx @@ -255,6 +255,14 @@ Connect Button text to connect to device + + No serial ports detected + Warning message when no serial ports are available + + + 00112233445566778899AABBCCDDEEFF + Placeholder text for security key input fields + @@ -277,6 +285,46 @@ LED Color Label for LED color selection + + Reader Number + Label for LED reader number input + + + LED Number + Label for LED number input + + + Temporary + Tab header for temporary LED settings + + + Permanent + Tab header for permanent LED settings + + + Mode + Label for LED control mode selection + + + ON Color + Label for LED ON color selection + + + OFF Color + Label for LED OFF color selection + + + ON Time (x 100ms) + Label for LED ON time input in units of 100ms + + + OFF Time (x 100ms) + Label for LED OFF time input in units of 100ms + + + Timer (x 100ms) + Label for LED timer duration input in units of 100ms + Selected File Label for file selection in file transfer @@ -305,6 +353,58 @@ Disconnect from passive monitoring to perform device operations Details for passive monitoring notice on Manage page + + Tone Code + Label for buzzer tone code selection + + + Reader Number + Label for buzzer reader number input + + + ON Time (x 100ms) + Label for buzzer ON time input in units of 100ms + + + OFF Time (x 100ms) + Label for buzzer OFF time input in units of 100ms + + + Count (0 = infinite) + Label for buzzer repeat count input + + + Signal + Group header for buzzer signal settings (tone code, reader number) + + + Timing + Group header for buzzer timing settings (on/off time, count) + + + ON time must be greater than zero when tone is active. + Validation message when buzzer ON time is zero with a non-Off tone code + + + ON and OFF time cannot both be zero. + Validation message when both LED ON and OFF time values are zero + + + Send + Button text to send a command to the device + + + This device does not support {0}. + Message when device lacks a required capability. {0} = capability name + + + Audio Output + Display name for the Audio Output capability + + + LED Control + Display name for the LED Control capability + @@ -756,4 +856,22 @@ Status Column header for status in supervision history + + + + Packet Trace + Screen reader name for the packet trace data grid + + + Card Read History + Screen reader name for the card read history data grid + + + Application Logo + Screen reader name for the application logo image + + + Status Indicator + Screen reader name for LED status indicator + \ No newline at end of file diff --git a/src/Core/Services/IUserSettingsService.cs b/src/Core/Services/IUserSettingsService.cs index 1a2e15e..826a6d4 100644 --- a/src/Core/Services/IUserSettingsService.cs +++ b/src/Core/Services/IUserSettingsService.cs @@ -1,5 +1,3 @@ -using OSDPBench.Core.Models; - namespace OSDPBench.Core.Services; /// @@ -8,26 +6,26 @@ namespace OSDPBench.Core.Services; public interface IUserSettingsService { /// - /// Gets the current user settings + /// Gets the user's preferred culture/language /// - UserSettings Settings { get; } - + string PreferredCulture { get; } + /// - /// Loads user settings from storage + /// Gets whether to skip language mismatch checking /// - /// Task representing the async operation - Task LoadAsync(); - + bool SkipLanguageMismatchCheck { get; } + /// - /// Saves user settings to storage + /// Updates the preferred culture and saves settings /// + /// The culture name to save /// Task representing the async operation - Task SaveAsync(); + Task UpdatePreferredCultureAsync(string cultureName); /// - /// Updates settings using an action and saves them + /// Updates the skip language mismatch check preference and saves settings /// - /// Action to update the settings + /// Whether to skip language mismatch checking /// Task representing the async operation - Task UpdateSettingsAsync(Action updateAction); -} \ No newline at end of file + Task UpdateSkipLanguageMismatchCheckAsync(bool skip); +} diff --git a/src/Core/Services/LanguageMismatchService.cs b/src/Core/Services/LanguageMismatchService.cs index c94630d..5f6878a 100644 --- a/src/Core/Services/LanguageMismatchService.cs +++ b/src/Core/Services/LanguageMismatchService.cs @@ -32,14 +32,14 @@ public async Task CheckAndPromptForLanguageMismatchAsync() { // Only check if this is not the user's first time setting a language // (i.e., they have a saved preference) - if (string.IsNullOrEmpty(_userSettingsService.Settings.PreferredCulture)) + if (string.IsNullOrEmpty(_userSettingsService.PreferredCulture)) { // First time user - don't prompt, just use system language return; } // Check if user has disabled language mismatch checking - if (_userSettingsService.Settings.SkipLanguageMismatchCheck) + if (_userSettingsService.SkipLanguageMismatchCheck) { // User has opted out of language mismatch checks return; @@ -61,8 +61,7 @@ public async Task CheckAndPromptForLanguageMismatchAsync() // Save the "don't ask again" preference if (dontAskAgain) { - await _userSettingsService.UpdateSettingsAsync(settings => - settings.SkipLanguageMismatchCheck = true); + await _userSettingsService.UpdateSkipLanguageMismatchCheckAsync(true); } if (userWantsToSwitch) diff --git a/src/Core/Services/LocalizationService.cs b/src/Core/Services/LocalizationService.cs index 088c570..940aacf 100644 --- a/src/Core/Services/LocalizationService.cs +++ b/src/Core/Services/LocalizationService.cs @@ -19,11 +19,11 @@ public LocalizationService(IUserSettingsService? userSettingsService) _userSettingsService = userSettingsService; // Initialize culture from settings or system default - if (_userSettingsService?.Settings.PreferredCulture != null) + if (_userSettingsService?.PreferredCulture != null) { try { - _currentCulture = new CultureInfo(_userSettingsService.Settings.PreferredCulture); + _currentCulture = new CultureInfo(_userSettingsService.PreferredCulture); } catch { @@ -102,10 +102,9 @@ public void ChangeCulture(CultureInfo culture) // Save preference to settings if (_userSettingsService != null) { - _ = Task.Run(async () => + _ = Task.Run(async () => { - await _userSettingsService.UpdateSettingsAsync(settings => - settings.PreferredCulture = culture.Name); + await _userSettingsService.UpdatePreferredCultureAsync(culture.Name); }); } diff --git a/src/Core/ViewModels/Pages/ConfigurationViewModel.cs b/src/Core/ViewModels/Pages/ConfigurationViewModel.cs index dc7d8ea..0c395e3 100644 --- a/src/Core/ViewModels/Pages/ConfigurationViewModel.cs +++ b/src/Core/ViewModels/Pages/ConfigurationViewModel.cs @@ -207,16 +207,12 @@ private void DeviceManagementServiceOnNakReplyReceived(object? sender, string na [ObservableProperty] private string _securityKey = string.Empty; /// - /// Called when UseDefaultKey property changes. In passive mode, prevents unchecking - /// unless a custom key is provided. + /// Called when UseDefaultKey property changes. /// partial void OnUseDefaultKeyChanged(bool value) { - // In passive mode, don't allow unchecking default key without a custom key - if (!value && IsPassiveMode && string.IsNullOrWhiteSpace(SecurityKey)) - { - UseDefaultKey = true; - } + _ = value; // Intentionally unused - only triggering dependent property notification + NotifySecurityKeyValidationChanged(); } /// @@ -230,6 +226,79 @@ partial void OnSecurityKeyChanged(string value) { UseDefaultKey = true; } + + OnPropertyChanged(nameof(SecurityKeyCharacterCount)); + NotifySecurityKeyValidationChanged(); + } + + partial void OnUseSecureChannelChanged(bool value) + { + _ = value; // Intentionally unused - only triggering dependent property notification + NotifySecurityKeyValidationChanged(); + } + + /// + /// Gets the character count display for the security key field (e.g., "16/32"). + /// + public string SecurityKeyCharacterCount => $"{SecurityKey.Length}/32"; + + /// + /// Gets a value indicating whether the security key is valid for the current configuration. + /// Returns true when using the default key, when secure channel is off, or when the key is exactly 32 hex chars. + /// + public bool IsSecurityKeyValid + { + get + { + // Default key or no secure channel means no custom key validation needed + if (UseDefaultKey) return true; + if (!UseSecureChannel && !IsPassiveMode) return true; + + return IsValidHexKey(SecurityKey); + } + } + + /// + /// Gets a value indicating whether the security key is invalid for the current configuration. + /// Inverse of for convenient XAML DataTrigger binding. + /// + public bool IsSecurityKeyInvalid => !IsSecurityKeyValid; + + /// + /// Gets a value indicating whether the device can be connected. + /// Requires a serial port to be available and the security key to be valid (or not needed). + /// + public bool CanConnectDevice => SelectedSerialPort != null && IsSecurityKeyValid; + + /// + /// Gets a value indicating whether passive monitoring can be started. + /// Requires the security key to be valid (or using default key). + /// + public bool CanStartPassiveMonitoring => IsSecurityKeyValid; + + /// + /// Checks whether the given string is a valid 32-character hexadecimal key. + /// + public static bool IsValidHexKey(string key) + { + if (key.Length != 32) return false; + + foreach (var c in key) + { + if (!char.IsAsciiHexDigit(c)) return false; + } + + return true; + } + + private void NotifySecurityKeyValidationChanged() + { + OnPropertyChanged(nameof(IsSecurityKeyValid)); + OnPropertyChanged(nameof(IsSecurityKeyInvalid)); + OnPropertyChanged(nameof(CanConnectDevice)); + OnPropertyChanged(nameof(CanStartPassiveMonitoring)); + ConnectDeviceCommand.NotifyCanExecuteChanged(); + StartPassiveMonitoringCommand.NotifyCanExecuteChanged(); } [ObservableProperty] private DateTime _lastTxActiveTime; @@ -334,6 +403,13 @@ partial void OnIsDiscoverModeSelectedChanged(bool value) NotifyButtonVisibilityChanged(); } + partial void OnSelectedSerialPortChanged(AvailableSerialPort? value) + { + _ = value; // Intentionally unused - only triggering dependent property notification + OnPropertyChanged(nameof(CanConnectDevice)); + ConnectDeviceCommand.NotifyCanExecuteChanged(); + } + private async Task InitializeSerialPorts() { try @@ -462,7 +538,7 @@ private void HandleSuccessfulDiscovery(DiscoveryResult result) ConnectedBaudRate = result.Connection.BaudRate; } - [RelayCommand] + [RelayCommand(CanExecute = nameof(CanConnectDevice))] private async Task ConnectDevice() { if (!ValidateSerialPort()) return; @@ -525,7 +601,7 @@ private async Task DisconnectDevice() LastRxActiveTime = DateTime.MinValue; } - [RelayCommand] + [RelayCommand(CanExecute = nameof(CanStartPassiveMonitoring))] private async Task StartPassiveMonitoring() { if (!ValidateSerialPort()) return; diff --git a/src/Core/ViewModels/Pages/ManageViewModel.cs b/src/Core/ViewModels/Pages/ManageViewModel.cs index b03fb6d..d3cfb03 100644 --- a/src/Core/ViewModels/Pages/ManageViewModel.cs +++ b/src/Core/ViewModels/Pages/ManageViewModel.cs @@ -270,6 +270,7 @@ private static bool SecurityKeysEqual(byte[]? key1, byte[]? key2) private void UpdateFields() { IdentityLookup = _deviceManagementService.IdentityLookup; + CapabilitiesLookup = _deviceManagementService.CapabilitiesLookup; ConnectedPortName = _deviceManagementService.PortName; ConnectedAddress = _deviceManagementService.Address; ConnectedBaudRate = _deviceManagementService.BaudRate; @@ -385,6 +386,8 @@ public void ClearSupervisionHistory() [ObservableProperty] private IdentityLookup? _identityLookup; + [ObservableProperty] private CapabilitiesLookup? _capabilitiesLookup; + [ObservableProperty] private StatusLevel _statusLevel = StatusLevel.Disconnected; [ObservableProperty] private ObservableCollection _availableDeviceActions = diff --git a/src/UI/Windows/App.xaml b/src/UI/Windows/App.xaml index 904c180..b2b16a4 100644 --- a/src/UI/Windows/App.xaml +++ b/src/UI/Windows/App.xaml @@ -11,10 +11,11 @@ - - - - + + + + + diff --git a/src/UI/Windows/App.xaml.cs b/src/UI/Windows/App.xaml.cs index 860ee18..3c97893 100644 --- a/src/UI/Windows/App.xaml.cs +++ b/src/UI/Windows/App.xaml.cs @@ -1,4 +1,4 @@ -ο»Ώusing System.Windows; +using System.Windows; using System.Windows.Threading; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -10,7 +10,6 @@ using OSDPBench.Windows.Views.Windows; using Wpf.Ui; using Wpf.Ui.Abstractions; - namespace OSDPBench.Windows; /// @@ -23,42 +22,61 @@ public partial class App // https://docs.microsoft.com/dotnet/core/extensions/dependency-injection // https://docs.microsoft.com/dotnet/core/extensions/configuration // https://docs.microsoft.com/dotnet/core/extensions/logging - private static readonly IHost Host = Microsoft.Extensions.Hosting.Host - .CreateDefaultBuilder() - .ConfigureServices((_, services) => - { - services.AddHostedService(); - - // Theme manipulation - services.AddSingleton(); - - // TaskBar manipulation - services.AddSingleton(); - - // Service containing navigation, same as INavigationWindow... but without window - services.AddSingleton(); - services.AddSingleton(); - - // Main window with navigation - services.AddSingleton(); - services.AddSingleton(); - - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - }).Build(); + private static IHost? _host; + + /// + /// Builds and returns the application host. + /// + protected static IHost BuildHost(Action? configureOverrides = null) + { + return Host + .CreateDefaultBuilder() + .ConfigureServices((_, services) => + { + ConfigureServices(services); + configureOverrides?.Invoke(services); + }).Build(); + } + + /// + /// Registers application services with the dependency injection container. + /// + /// The service collection to configure. + protected static void ConfigureServices(IServiceCollection services) + { + services.AddHostedService(); + + // Theme manipulation + services.AddSingleton(); + + // TaskBar manipulation + services.AddSingleton(); + + // Service containing navigation, same as INavigationWindow... but without window + services.AddSingleton(); + services.AddSingleton(); + + // Main window with navigation + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(sp => sp.GetRequiredService()); + services.AddSingleton(); + services.AddSingleton(); + } /// /// Gets registered service. @@ -68,50 +86,49 @@ public partial class App public static T GetService() where T : class { - return (Host.Services.GetService(typeof(T)) as T)!; + return (_host!.Services.GetService(typeof(T)) as T)!; } /// /// Occurs when the application is loading. /// - private async void OnStartup(object sender, StartupEventArgs e) + private void OnStartup(object sender, StartupEventArgs e) { - Host.Start(); - - // Initialize user settings before other services - var userSettingsService = Host.Services.GetService(); - if (userSettingsService != null) - { - await userSettingsService.LoadAsync(); - } - + // Register the localization provider for Guidelines XAML markup extensions + // Must be set before Host.Start() which creates windows that use localization + ZBitSystems.Wpf.UI.Localization.LocalizationService.Provider = new ResourceLocalizationProvider(); + + _host = BuildHost(); + _host.Start(); + // Initialize the localization service to apply saved culture - var localizationService = Host.Services.GetService(); - if (localizationService != null && userSettingsService?.Settings.PreferredCulture != null) + var userSettingsService = _host.Services.GetService(); + var localizationService = _host.Services.GetService(); + if (localizationService != null && userSettingsService?.PreferredCulture != null) { try { - localizationService.ChangeCulture(userSettingsService.Settings.PreferredCulture); + localizationService.ChangeCulture(userSettingsService.PreferredCulture); } catch { // If culture loading fails, continue with system default } } - - Host.Services.GetService(); - Host.Services.GetService(); - + + _host.Services.GetService(); + _host.Services.GetService(); + // Check for language mismatch after UI is initialized _ = Task.Run(async () => { // Small delay to ensure UI is fully loaded await Task.Delay(500); - + // Run on UI thread to ensure proper dialog display and culture updates await Current.Dispatcher.InvokeAsync(async () => { - var languageMismatchService = Host.Services.GetService(); + var languageMismatchService = _host.Services.GetService(); if (languageMismatchService != null) { await languageMismatchService.CheckAndPromptForLanguageMismatchAsync(); @@ -125,16 +142,18 @@ await Current.Dispatcher.InvokeAsync(async () => /// private async void OnExit(object sender, ExitEventArgs e) { + if (_host == null) return; + // Dispose of services that need explicit cleanup - var configurationViewModel = Host.Services.GetService(); + var configurationViewModel = _host.Services.GetService(); configurationViewModel?.Dispose(); - - var usbMonitor = Host.Services.GetService(); + + var usbMonitor = _host.Services.GetService(); usbMonitor?.Dispose(); - - await Host.StopAsync(); - Host.Dispose(); + await _host.StopAsync(); + + _host.Dispose(); } /// @@ -144,4 +163,4 @@ private void OnDispatcherUnhandledException(object sender, DispatcherUnhandledEx { // For more info see https://docs.microsoft.com/en-us/dotnet/api/system.windows.application.dispatcherunhandledexception?view=windowsdesktop-6.0 } -} \ No newline at end of file +} diff --git a/src/UI/Windows/Converters/BooleanToVisibilityConverter.cs b/src/UI/Windows/Converters/BooleanToVisibilityConverter.cs deleted file mode 100644 index 45ef506..0000000 --- a/src/UI/Windows/Converters/BooleanToVisibilityConverter.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Globalization; -using System.Windows; -using System.Windows.Data; - -namespace OSDPBench.Windows.Converters; - -public class BooleanToVisibilityConverter : IValueConverter -{ - public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value is bool booleanValue) - { - // Handle the "Invert" parameter - var isInverted = parameter != null && parameter.ToString()!.Equals("Invert", StringComparison.OrdinalIgnoreCase); - - // Return the inverted or non-inverted result - return isInverted ? booleanValue ? Visibility.Collapsed : Visibility.Visible : - booleanValue ? Visibility.Visible : Visibility.Collapsed; - } - - return Visibility.Collapsed; // Default - } - - public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value is Visibility visibilityValue) - { - var isInverted = parameter != null && parameter.ToString()!.Equals("Invert", StringComparison.OrdinalIgnoreCase); - - // Convert visibility back to boolean - return isInverted ? visibilityValue == Visibility.Collapsed : visibilityValue == Visibility.Visible; - } - - return false; // Default - } -} \ No newline at end of file diff --git a/src/UI/Windows/Converters/IndexToVisibilityConverter.cs b/src/UI/Windows/Converters/IndexToVisibilityConverter.cs deleted file mode 100644 index d37da24..0000000 --- a/src/UI/Windows/Converters/IndexToVisibilityConverter.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Globalization; -using System.Windows; -using System.Windows.Data; - -namespace OSDPBench.Windows.Converters; - -public class IndexToVisibilityConverter : IValueConverter -{ - public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value is int index && parameter is string paramString) - { - // Support pipe-separated indices like "1|2" - var targetIndices = paramString.Split('|'); - foreach (var targetStr in targetIndices) - { - if (int.TryParse(targetStr.Trim(), out int targetIndex) && index == targetIndex) - { - return Visibility.Visible; - } - } - } - - return Visibility.Collapsed; - } - - public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - throw new NotImplementedException(); - } -} \ No newline at end of file diff --git a/src/UI/Windows/Converters/InverseBoolConverter.cs b/src/UI/Windows/Converters/InverseBoolConverter.cs deleted file mode 100644 index 919e276..0000000 --- a/src/UI/Windows/Converters/InverseBoolConverter.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace OSDPBench.Windows.Converters; - -using System; -using System.Globalization; -using System.Windows.Data; - -/// -/// Converts a boolean value to its inverse value. -/// -public class InverseBoolConverter : IValueConverter -{ - public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value is bool boolValue) - { - return !boolValue; - } - - return false; - } - - public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value is bool boolValue) - { - return !boolValue; - } - - return false; - } -} diff --git a/src/UI/Windows/Converters/LedColorToBrushConverter.cs b/src/UI/Windows/Converters/LedColorToBrushConverter.cs new file mode 100644 index 0000000..db5967d --- /dev/null +++ b/src/UI/Windows/Converters/LedColorToBrushConverter.cs @@ -0,0 +1,32 @@ +using System.Globalization; +using System.Windows.Data; +using System.Windows.Media; +using OSDP.Net.Model.CommandData; + +namespace OSDPBench.Windows.Converters; + +internal class LedColorToBrushConverter : IValueConverter +{ + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + var ledColor = value is LedColor color ? color : LedColor.Black; + + return ledColor switch + { + LedColor.Black => new SolidColorBrush(Colors.Black), + LedColor.Red => new SolidColorBrush(Colors.Red), + LedColor.Green => new SolidColorBrush(Colors.Green), + LedColor.Amber => new SolidColorBrush(Color.FromRgb(255, 191, 0)), + LedColor.Blue => new SolidColorBrush(Colors.Blue), + LedColor.Magenta => new SolidColorBrush(Colors.Magenta), + LedColor.Cyan => new SolidColorBrush(Colors.Cyan), + LedColor.White => new SolidColorBrush(Colors.White), + _ => new SolidColorBrush(Colors.Transparent) + }; + } + + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} diff --git a/src/UI/Windows/Converters/NullToVisibilityConverter.cs b/src/UI/Windows/Converters/NullToVisibilityConverter.cs deleted file mode 100644 index 689dcde..0000000 --- a/src/UI/Windows/Converters/NullToVisibilityConverter.cs +++ /dev/null @@ -1,20 +0,0 @@ -ο»Ώusing System.Globalization; -using System.Windows; -using System.Windows.Data; - -namespace OSDPBench.Windows.Converters; - -public class NullToVisibilityConverter : IValueConverter -{ - public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - bool isVisible = value != null; - if (parameter != null && bool.Parse(parameter.ToString() ?? "False")) isVisible = !isVisible; - return isVisible ? Visibility.Visible : Visibility.Collapsed; - } - - public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - throw new NotImplementedException(); - } -} \ No newline at end of file diff --git a/src/UI/Windows/Converters/StringToVisibilityConverter.cs b/src/UI/Windows/Converters/StringToVisibilityConverter.cs deleted file mode 100644 index 0ee4820..0000000 --- a/src/UI/Windows/Converters/StringToVisibilityConverter.cs +++ /dev/null @@ -1,28 +0,0 @@ -ο»Ώusing System.Globalization; -using System.Windows; -using System.Windows.Data; - -namespace OSDPBench.Windows.Converters; - -public class StringToVisibilityConverter : IValueConverter -{ - public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (parameter is string paramString && paramString.Contains(';')) - { - // Split the parameter string by comma - var acceptableValues = paramString.Split(';').Select(p => p.Trim()); - - // Check if the value matches any of the acceptable values - return acceptableValues.Contains(value?.ToString()) ? Visibility.Visible : Visibility.Collapsed; - } - - // Original behavior for backward compatibility - return value?.ToString() == parameter?.ToString() ? Visibility.Visible : Visibility.Collapsed; - } - - public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - throw new NotImplementedException(); - } -} \ No newline at end of file diff --git a/src/UI/Windows/Helpers/CopyTextBoxHelper.cs b/src/UI/Windows/Helpers/CopyTextBoxHelper.cs deleted file mode 100644 index 6b0479f..0000000 --- a/src/UI/Windows/Helpers/CopyTextBoxHelper.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System.Windows; -using System.Windows.Controls; -using System.Windows.Input; - -namespace OSDPBench.Windows.Helpers; - -public static class CopyTextBoxHelper -{ - public static readonly DependencyProperty CopyCommandProperty = - DependencyProperty.RegisterAttached( - "CopyCommand", - typeof(ICommand), - typeof(CopyTextBoxHelper), - new PropertyMetadata(null)); - - public static ICommand GetCopyCommand(DependencyObject obj) - { - return (ICommand)obj.GetValue(CopyCommandProperty); - } - - public static void SetCopyCommand(DependencyObject obj, ICommand value) - { - obj.SetValue(CopyCommandProperty, value); - } - - public static readonly ICommand DefaultCopyCommand = new RelayCommand( - parameter => - { - if (parameter is TextBox textBox && !string.IsNullOrEmpty(textBox.Text)) - { - try - { - Clipboard.SetText(textBox.Text); - } - catch (System.Runtime.InteropServices.COMException) - { - // Clipboard is locked by another application, ignore silently - // This is a common Windows issue and shouldn't crash the app - } - } - }); -} - -public class RelayCommand(Action execute, Func? canExecute = null) - : ICommand -{ - private readonly Action _execute = execute ?? throw new ArgumentNullException(nameof(execute)); - - public event EventHandler? CanExecuteChanged - { - add => CommandManager.RequerySuggested += value; - remove => CommandManager.RequerySuggested -= value; - } - - public bool CanExecute(object? parameter) - { - return parameter != null && (canExecute?.Invoke(parameter) ?? true); - } - - public void Execute(object? parameter) - { - if (parameter != null) _execute(parameter); - } -} \ No newline at end of file diff --git a/src/UI/Windows/Markup/LocalizeExtension.cs b/src/UI/Windows/Markup/LocalizeExtension.cs deleted file mode 100644 index 84c3227..0000000 --- a/src/UI/Windows/Markup/LocalizeExtension.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System.Windows.Markup; -using System.Windows.Data; -using OSDPBench.Core.Resources; - -namespace OSDPBench.Windows.Markup; - -/// -/// Markup extension for accessing localized resources in XAML -/// -public class LocalizeExtension : MarkupExtension -{ - /// - /// Gets or sets the resource key - /// - public string Key { get; set; } = string.Empty; - - /// - /// Initializes a new instance of the LocalizeExtension - /// - public LocalizeExtension() - { - } - - /// - /// Initializes a new instance of the LocalizeExtension with a key - /// - /// The resource key - public LocalizeExtension(string key) - { - Key = key; - } - - /// - /// Provides the localized value or a binding if possible - /// - /// The service provider - /// The localized string value or binding - public override object ProvideValue(IServiceProvider serviceProvider) - { - if (string.IsNullOrEmpty(Key)) - return "[MISSING_KEY]"; - - try - { - // Create a binding to the LocalizedStringBinding for dynamic updates - var localizedBinding = new LocalizedStringBinding(Key); - var binding = new Binding(nameof(LocalizedStringBinding.Value)) - { - Source = localizedBinding, - Mode = BindingMode.OneWay - }; - - return binding.ProvideValue(serviceProvider); - } - catch - { - // Fallback to static string if binding fails - try - { - return Resources.GetString(Key); - } - catch - { - return $"[{Key}]"; - } - } - } -} \ No newline at end of file diff --git a/src/UI/Windows/Markup/LocalizedStringBinding.cs b/src/UI/Windows/Markup/LocalizedStringBinding.cs deleted file mode 100644 index 59074f4..0000000 --- a/src/UI/Windows/Markup/LocalizedStringBinding.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.ComponentModel; - -namespace OSDPBench.Windows.Markup; - -/// -/// Provides a binding that automatically updates when the culture changes -/// -public class LocalizedStringBinding : INotifyPropertyChanged -{ - private readonly string _key; - - public LocalizedStringBinding(string key) - { - _key = key; - - // Subscribe to culture changes - Core.Resources.Resources.PropertyChanged += OnResourcesPropertyChanged; - } - - public string Value => Core.Resources.Resources.GetString(_key); - - private void OnResourcesPropertyChanged(object? sender, PropertyChangedEventArgs e) - { - // When culture changes, notify that our Value property has changed - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value))); - } - - public event PropertyChangedEventHandler? PropertyChanged; - - ~LocalizedStringBinding() - { - Core.Resources.Resources.PropertyChanged -= OnResourcesPropertyChanged; - } -} \ No newline at end of file diff --git a/src/UI/Windows/Services/AppSettings.cs b/src/UI/Windows/Services/AppSettings.cs new file mode 100644 index 0000000..4f22868 --- /dev/null +++ b/src/UI/Windows/Services/AppSettings.cs @@ -0,0 +1,20 @@ +using ZBitSystems.Wpf.UI.Settings; + +namespace OSDPBench.Windows.Services; + +/// +/// Application-specific settings extending Guidelines' UserSettings base class. +/// Inherits window state properties (position, size, maximized) and IWindowStateStorage implementation. +/// +public class AppSettings : UserSettings +{ + /// + /// Gets or sets the user's preferred culture/language + /// + public string PreferredCulture { get; set; } = "en-US"; + + /// + /// Gets or sets whether to skip language mismatch checking + /// + public bool SkipLanguageMismatchCheck { get; set; } +} diff --git a/src/UI/Windows/Services/AppUserSettingsService.cs b/src/UI/Windows/Services/AppUserSettingsService.cs new file mode 100644 index 0000000..3ccadaa --- /dev/null +++ b/src/UI/Windows/Services/AppUserSettingsService.cs @@ -0,0 +1,55 @@ +using OSDPBench.Core.Services; +using ZBitSystems.Wpf.UI.Settings; + +namespace OSDPBench.Windows.Services; + +/// +/// Windows implementation of user settings using Guidelines' JsonUserSettingsService. +/// Wraps JsonUserSettingsService<AppSettings> and implements Core's IUserSettingsService. +/// +public class AppUserSettingsService : IUserSettingsService +{ + private readonly JsonUserSettingsService _inner; + + /// + /// Initializes a new instance of the AppUserSettingsService + /// + public AppUserSettingsService() + { + _inner = new JsonUserSettingsService("OSDPBench"); + } + + /// + /// Gets the current application settings (includes window state and app-specific properties) + /// + public AppSettings Settings => _inner.Settings; + + /// + public string PreferredCulture => _inner.Settings.PreferredCulture; + + /// + public bool SkipLanguageMismatchCheck => _inner.Settings.SkipLanguageMismatchCheck; + + /// + public async Task UpdatePreferredCultureAsync(string cultureName) + { + _inner.Settings.PreferredCulture = cultureName; + await _inner.SaveAsync(); + } + + /// + public async Task UpdateSkipLanguageMismatchCheckAsync(bool skip) + { + _inner.Settings.SkipLanguageMismatchCheck = skip; + await _inner.SaveAsync(); + } + + /// + /// Saves the current settings to storage + /// + /// Task representing the async operation + public async Task SaveAsync() + { + await _inner.SaveAsync(); + } +} diff --git a/src/UI/Windows/Services/ResourceLocalizationProvider.cs b/src/UI/Windows/Services/ResourceLocalizationProvider.cs new file mode 100644 index 0000000..091add2 --- /dev/null +++ b/src/UI/Windows/Services/ResourceLocalizationProvider.cs @@ -0,0 +1,34 @@ +using System.ComponentModel; +using System.Globalization; +using ZBitSystems.Wpf.UI.Localization; + +namespace OSDPBench.Windows.Services; + +/// +/// Adapts Core's static Resources class to the Guidelines ILocalizationProvider interface +/// +public class ResourceLocalizationProvider : ILocalizationProvider +{ + public ResourceLocalizationProvider() + { + Core.Resources.Resources.PropertyChanged += OnResourcesPropertyChanged; + } + + /// + public string GetString(string key) + { + return Core.Resources.Resources.GetString(key); + } + + /// + public string CurrentCulture => + Core.Resources.Resources.Culture?.Name ?? CultureInfo.CurrentUICulture.Name; + + private void OnResourcesPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(string.Empty)); + } + + /// + public event PropertyChangedEventHandler? PropertyChanged; +} diff --git a/src/UI/Windows/Services/WindowsLanguageMismatchService.cs b/src/UI/Windows/Services/WindowsLanguageMismatchService.cs index 489c346..c52da76 100644 --- a/src/UI/Windows/Services/WindowsLanguageMismatchService.cs +++ b/src/UI/Windows/Services/WindowsLanguageMismatchService.cs @@ -33,14 +33,14 @@ public async Task CheckAndPromptForLanguageMismatchAsync() System.Diagnostics.Debug.WriteLine("LanguageMismatchService: Starting check..."); // Only check if this is not the user's first time setting a language - if (string.IsNullOrEmpty(_userSettingsService.Settings.PreferredCulture)) + if (string.IsNullOrEmpty(_userSettingsService.PreferredCulture)) { System.Diagnostics.Debug.WriteLine("LanguageMismatchService: First time user - skipping"); return; } // Check if the user has disabled language mismatch checking - if (_userSettingsService.Settings.SkipLanguageMismatchCheck) + if (_userSettingsService.SkipLanguageMismatchCheck) { return; } @@ -62,8 +62,7 @@ public async Task CheckAndPromptForLanguageMismatchAsync() // Save the "don't ask again" preference if (dontAskAgain) { - await _userSettingsService.UpdateSettingsAsync(settings => - settings.SkipLanguageMismatchCheck = true); + await _userSettingsService.UpdateSkipLanguageMismatchCheckAsync(true); } if (userWantsToSwitch) diff --git a/src/UI/Windows/Services/WindowsUserSettingsService.cs b/src/UI/Windows/Services/WindowsUserSettingsService.cs deleted file mode 100644 index 5bd26c2..0000000 --- a/src/UI/Windows/Services/WindowsUserSettingsService.cs +++ /dev/null @@ -1,130 +0,0 @@ -using System.IO; -using System.Runtime.InteropServices; -using System.Text.Json; -using OSDPBench.Core.Models; -using OSDPBench.Core.Services; - -namespace OSDPBench.Windows.Services; - -/// -/// Windows implementation of a user settings service using JSON file storage -/// -public class WindowsUserSettingsService : IUserSettingsService -{ - private const string SettingsFileName = "settings.json"; - private readonly string _settingsFilePath; - private UserSettings _settings; - - /// - /// Initializes a new instance of the WindowsUserSettingsService - /// - public WindowsUserSettingsService() - { - var appFolderPath = GetSettingsFolderPath(); - Directory.CreateDirectory(appFolderPath); - _settingsFilePath = Path.Combine(appFolderPath, SettingsFileName); - _settings = LoadSettingsFromFile(); - } - - private UserSettings LoadSettingsFromFile() - { - try - { - if (File.Exists(_settingsFilePath)) - { - var json = File.ReadAllText(_settingsFilePath); - var loadedSettings = JsonSerializer.Deserialize(json); - if (loadedSettings != null) - { - return loadedSettings; - } - } - } - catch - { - // If loading fails, use default settings - } - - return new UserSettings(); - } - - private static string GetSettingsFolderPath() - { - if (IsPackagedApp()) - { - // For Microsoft Store packaged apps, use the app's local folder - // which is automatically managed and cleaned up with the app - var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); - return localAppData; - } - - // For unpackaged apps, use traditional AppData location with app subfolder - var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); - return Path.Combine(appDataPath, "OSDPBench"); - } - - private static bool IsPackagedApp() - { - // Check if running as a packaged app using the kernel32 API - var length = 0u; - var result = GetCurrentPackageFullName(ref length, null); - return result != AppmodelErrorNoPackage; - } - - private const int AppmodelErrorNoPackage = 15700; - - [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = false)] - private static extern int GetCurrentPackageFullName(ref uint packageFullNameLength, char[]? packageFullName); - - /// - public UserSettings Settings => _settings; - - /// - public async Task LoadAsync() - { - try - { - if (File.Exists(_settingsFilePath)) - { - var json = await File.ReadAllTextAsync(_settingsFilePath); - var loadedSettings = JsonSerializer.Deserialize(json); - if (loadedSettings != null) - { - _settings = loadedSettings; - } - } - } - catch (Exception) - { - // If loading fails, use default settings - _settings = new UserSettings(); - } - } - - /// - public async Task SaveAsync() - { - try - { - var json = JsonSerializer.Serialize(_settings, new JsonSerializerOptions - { - WriteIndented = true - }); - await File.WriteAllTextAsync(_settingsFilePath, json); - } - catch (Exception) - { - // Silently fail - settings will revert to defaults next time - } - } - - /// - /// Updates a setting and saves - /// - /// Action to update the settings - public async Task UpdateSettingsAsync(Action updateAction) - { - updateAction(_settings); - await SaveAsync(); - } -} \ No newline at end of file diff --git a/src/UI/Windows/Styles/ComponentStyles.xaml b/src/UI/Windows/Styles/ComponentStyles.xaml deleted file mode 100644 index e4af3c0..0000000 --- a/src/UI/Windows/Styles/ComponentStyles.xaml +++ /dev/null @@ -1,299 +0,0 @@ -ο»Ώ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/UI/Windows/Styles/DesignTokens.xaml b/src/UI/Windows/Styles/DesignTokens.xaml deleted file mode 100644 index 8c2be05..0000000 --- a/src/UI/Windows/Styles/DesignTokens.xaml +++ /dev/null @@ -1,65 +0,0 @@ -ο»Ώ - - - - - 4 - 8 - 16 - 24 - 32 - 48 - - - 10,5 - 0,0,16,0 - 8,4 - 0,0,0,16 - - - 16 - 8,4 - 16,8 - - - 4 - 8 - 12 - - - 12 - 14 - 16 - 20 - 24 - 32 - 36 - - - - - - - - - - - - - - - - - - - 28 - 32 - 40 - - 120 - 200 - 280 - - \ No newline at end of file diff --git a/src/UI/Windows/Styles/ExampleImplementation.md b/src/UI/Windows/Styles/ExampleImplementation.md index f0f3bd4..c4d516d 100644 --- a/src/UI/Windows/Styles/ExampleImplementation.md +++ b/src/UI/Windows/Styles/ExampleImplementation.md @@ -16,7 +16,7 @@ This example shows how to apply the new style system to improve consistency and HorizontalAlignment="Right" VerticalAlignment="Center"> - + @@ -28,7 +28,7 @@ This example shows how to apply the new style system to improve consistency and @@ -48,10 +48,10 @@ This example shows how to apply the new style system to improve consistency and + Text="{localization:Localize Connect_SerialPortSelection}"/> - - - - - + - @@ -114,7 +111,7 @@ - @@ -135,7 +132,7 @@ - @@ -151,13 +148,13 @@ - - - -{StaticResource Spacing.Small} -{StaticResource Spacing.Medium} -{StaticResource Spacing.Large} -{StaticResource Spacing.XLarge} -{StaticResource Spacing.XXLarge} -``` - -#### Standard Margins & Padding -```xml -{StaticResource Margin.Card} -{StaticResource Margin.Control} -{StaticResource Margin.Button} -{StaticResource Padding.Card} -{StaticResource Padding.Control} -``` - -#### Typography Scale -```xml -{StaticResource FontSize.Caption} -{StaticResource FontSize.Body} -{StaticResource FontSize.BodyLarge} -{StaticResource FontSize.Subtitle} -{StaticResource FontSize.Title} -{StaticResource FontSize.Headline} -{StaticResource FontSize.Display} -``` - -### 2. Component Styles (`ComponentStyles.xaml`) -Reusable styles for UI components. - -#### Typography Usage -```xml - - - - - - - - - - - - - -``` - -#### Form Controls -```xml - -