diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 4ccdc489..6c70f3ae 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -10,46 +10,33 @@ jobs: build: strategy: matrix: - dotnet: [net481, net8] - configuration: [Debug, Release] + configuration: [Release] runs-on: windows-latest -# runs-on: ubuntu-latest # Error: setup-msbuild can only be run on Windows runners steps: - name: Checkout - uses: actions/checkout@v7 + uses: actions/checkout@v4 with: fetch-depth: 0 + - name: Setup .NET - uses: microsoft/setup-msbuild@v3 - - name: C# problem matcher - run: | - Invoke-WebRequest "https://raw.githubusercontent.com/actions/setup-dotnet/main/.github/csc.json" -OutFile "csc.json" - echo "::add-matcher::csc.json" -# uses: actions/setup-dotnet@v2 -# with: -# dotnet-version: 5.0.x -# - name: Restore dependencies -# run: dotnet restore - - name: Add Packages - run: | - dotnet restore + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + - name: Restore dependencies + run: dotnet restore EDSEditor.sln + - name: Build -# run: make all # builds the Debug configuration - run: | - msbuild -p:Build${{ matrix.dotnet }}=true /property:Configuration=${{ matrix.configuration }} -# run: dotnet build --no-restore + run: dotnet build EDSEditor.sln --configuration ${{ matrix.configuration }} --no-restore + - name: Test - run: | - dotnet test --no-build --verbosity normal Tests\bin\${{ matrix.configuration }}\${{ matrix.dotnet }}*\tests.dll - - name: GUITest - if: matrix.dotnet == 'net8' - run: | - dotnet test --no-build --verbosity normal GUITests\bin\${{ matrix.configuration }}\${{ matrix.dotnet }}*\GUITests.dll + run: dotnet test EDSEditor.sln --configuration ${{ matrix.configuration }} --no-build --verbosity normal + - name: Upload Artifact - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v4 with: - name: EDSEditor_${{ matrix.dotnet }} + name: CANopenEditor_net8.0_${{ matrix.configuration }} path: | - EDSEditorGUI\bin\Release\ - EDSSharp\bin\Release\ + EDSEditorGUI2\bin\${{ matrix.configuration }}\net8.0\ + libEDSsharp\bin\${{ matrix.configuration }}\net8.0\ diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 382ef33f..379e0303 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,4 +1,7 @@ -on: push +on: + push: + branches: + - main jobs: contrib-readme-job: @@ -8,11 +11,21 @@ jobs: contents: write pull-requests: write steps: - - name: Contribute List + - name: Contribute List (English) uses: akhilmhdh/contributors-readme-action@v2.3.11 with: use_username: true + readme_path: "README.md" commit_message: "[ROOT][DOCS] contrib-readme-action has updated readme" pr_title_on_protected: "[ROOT][DOCS] contributors readme action update" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Contribute List (Chinese) + uses: akhilmhdh/contributors-readme-action@v2.3.11 + with: + use_username: true + readme_path: "README_zh.md" + commit_message: "[ROOT][DOCS] contrib-readme-action has updated readme_zh" + pr_title_on_protected: "[ROOT][DOCS] contributors readme_zh action update" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c59fbe27..9504fa3e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,42 +1,107 @@ -name: make release +name: Release on: push: tags: - "v*.*.*" +permissions: + contents: write + jobs: build: - runs-on: windows-latest - permissions: - contents: write + strategy: + matrix: + include: + - os: windows-latest + rid: win-x64 + ext: .zip + - os: ubuntu-latest + rid: linux-x64 + ext: .tar.gz + - os: macos-latest + rid: osx-x64 + ext: .tar.gz + - os: macos-latest + rid: osx-arm64 + ext: .tar.gz + + runs-on: ${{ matrix.os }} + steps: - name: Checkout - uses: actions/checkout@v7 + uses: actions/checkout@v4 with: fetch-depth: 0 + - name: Setup .NET - uses: microsoft/setup-msbuild@v3 - - name: Add Packages + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Publish App run: | - dotnet restore - - name: Build + dotnet publish EDSEditorGUI2/EDSEditorGUI2.csproj -c Release -r ${{ matrix.rid }} --self-contained true -p:PublishSingleFile=true + + - name: Package Windows Zip + if: matrix.os == 'windows-latest' + run: | + Compress-Archive -Path "EDSEditorGUI2\bin\Release\net8.0\${{ matrix.rid }}\publish\*" -DestinationPath CANopenEditor-${{ github.ref_name }}-${{ matrix.rid }}${{ matrix.ext }} + + - name: Build Windows NSIS Installer + if: matrix.os == 'windows-latest' run: | - msbuild /property:Configuration=Release - - name: Check for release tag + choco install nsis -y + & "C:\Program Files (x86)\NSIS\makensis.exe" setup.nsi + Move-Item CANopenEditor-Setup.exe CANopenEditor-${{ github.ref_name }}-win-x64-Setup.exe + + - name: Package Linux/Mac Tarball + if: matrix.os != 'windows-latest' run: | - $release = "${{ github.ref_name }}" -match "v[\d]*[.][\d]*[.][\d]*$" - $prerelease = If ($release) {"false"} else {"true"} - echo "prerelease=$prerelease" >> $env:GITHUB_ENV - - name: Make release package + cd EDSEditorGUI2/bin/Release/net8.0/${{ matrix.rid }}/publish/ + tar -czvf ${{ github.workspace }}/CANopenEditor-${{ github.ref_name }}-${{ matrix.rid }}${{ matrix.ext }} * + + - name: Package Linux deb + if: matrix.os == 'ubuntu-latest' run: | - $tag = "${{ github.ref_name }}" - $filename = "CANopenEditor-$tag-binary.zip" - Compress-Archive -CompressionLevel Optimal -Path "EDSEditorGUI\bin\Release\*","EDSSharp\bin\Release\*" -DestinationPath $filename - echo "binary_name=$filename" >> $env:GITHUB_ENV - - name: make release - uses: softprops/action-gh-release@v3 + mkdir -p build_deb/DEBIAN + mkdir -p build_deb/opt/CANopenEditor + mkdir -p build_deb/usr/share/applications + + # Create control file + cat < build_deb/DEBIAN/control + Package: canopeneditor + Version: ${GITHUB_REF_NAME#v} + Section: utils + Priority: optional + Architecture: amd64 + Maintainer: Open Source Contributors + Description: CANopen Object Dictionary Editor + EOF + + # Copy binaries + cp -r EDSEditorGUI2/bin/Release/net8.0/linux-x64/publish/* build_deb/opt/CANopenEditor/ + + # Create desktop file + cat < build_deb/usr/share/applications/canopeneditor.desktop + [Desktop Entry] + Version=1.0 + Name=CANopen Editor + Comment=CANopen Object Dictionary and PDO Editor + Exec=/opt/CANopenEditor/EDSEditorGUI2 + Terminal=false + Type=Application + Categories=Development;Engineering; + EOF + + # Build deb + dpkg-deb --build build_deb CANopenEditor-${{ github.ref_name }}-linux-x64.deb + + - name: Release Files + uses: softprops/action-gh-release@v2 with: - prerelease: ${{ env.prerelease }} files: | - ${{ env.binary_name }} + CANopenEditor-*.zip + CANopenEditor-*.tar.gz + CANopenEditor-*.exe + CANopenEditor-*.deb diff --git a/.gitignore b/.gitignore index f0d41cf9..bb3805ca 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ edseditor-Setup.exe /SourceGrid_4.21 /packages/ *.bak +AppBuild diff --git a/EDSEditor.sln b/EDSEditor.sln index 00abe8c3..bd5a6a4b 100644 --- a/EDSEditor.sln +++ b/EDSEditor.sln @@ -3,17 +3,8 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.28307.329 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EDSEditorGUI", "EDSEditorGUI\EDSEditorGUI.csproj", "{13D2D8ED-242B-4283-BF14-AA79FE875F7C}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "libEDSsharp", "libEDSsharp\libEDSsharp.csproj", "{CC0FA4B1-2BFC-43B3-8C56-B428DF2D24E7}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{2A479BF3-7628-409B-8A29-9314C308445E}" - ProjectSection(ProjectDependencies) = postProject - {8B7A7545-6257-44BF-8868-F429E1B72C77} = {8B7A7545-6257-44BF-8868-F429E1B72C77} - EndProjectSection -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EDSSharp", "EDSSharp\EDSSharp.csproj", "{8B7A7545-6257-44BF-8868-F429E1B72C77}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EDSEditorGUI2", "EDSEditorGUI2\EDSEditorGUI2.csproj", "{F175A47B-8BB8-480F-8D31-AF802086B8B4}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GUITests", "GUITests\GUITests.csproj", "{9B9B5461-1864-484D-A49D-D39422DA16E0}" @@ -24,22 +15,10 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {13D2D8ED-242B-4283-BF14-AA79FE875F7C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {13D2D8ED-242B-4283-BF14-AA79FE875F7C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {13D2D8ED-242B-4283-BF14-AA79FE875F7C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {13D2D8ED-242B-4283-BF14-AA79FE875F7C}.Release|Any CPU.Build.0 = Release|Any CPU {CC0FA4B1-2BFC-43B3-8C56-B428DF2D24E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CC0FA4B1-2BFC-43B3-8C56-B428DF2D24E7}.Debug|Any CPU.Build.0 = Debug|Any CPU {CC0FA4B1-2BFC-43B3-8C56-B428DF2D24E7}.Release|Any CPU.ActiveCfg = Release|Any CPU {CC0FA4B1-2BFC-43B3-8C56-B428DF2D24E7}.Release|Any CPU.Build.0 = Release|Any CPU - {2A479BF3-7628-409B-8A29-9314C308445E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2A479BF3-7628-409B-8A29-9314C308445E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2A479BF3-7628-409B-8A29-9314C308445E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2A479BF3-7628-409B-8A29-9314C308445E}.Release|Any CPU.Build.0 = Release|Any CPU - {8B7A7545-6257-44BF-8868-F429E1B72C77}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8B7A7545-6257-44BF-8868-F429E1B72C77}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8B7A7545-6257-44BF-8868-F429E1B72C77}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8B7A7545-6257-44BF-8868-F429E1B72C77}.Release|Any CPU.Build.0 = Release|Any CPU {F175A47B-8BB8-480F-8D31-AF802086B8B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F175A47B-8BB8-480F-8D31-AF802086B8B4}.Debug|Any CPU.Build.0 = Debug|Any CPU {F175A47B-8BB8-480F-8D31-AF802086B8B4}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/EDSEditorGUI2/App.axaml b/EDSEditorGUI2/App.axaml index c1793b31..b3383914 100644 --- a/EDSEditorGUI2/App.axaml +++ b/EDSEditorGUI2/App.axaml @@ -6,14 +6,147 @@ RequestedThemeVariant="Default"> + + + + + + + + + #F3F4F6 + #FAFAFA + #E5E7EB + #FFFFFF + + #F5F5F5 + #EAEAEA + #FFFFFF + + #1A1A1A + #1A1A1A + #F5F5F5 + #1A1A1A + #D3D3D3 + #F5F5F5 + #F0E68C + + + #1E1E1E + #252526 + #2D2D30 + #252526 + + #333337 + #3E3E42 + #1E1E1E + + #333337 + #3E3E42 + + #DCDCDC + #DCDCDC + #DCDCDC + #DCDCDC + + #333337 + #3E3E42 + #1E1E1E + #333337 + + #DCDCDC + #A0A0A0 + #888888 + #DCDCDC + #404040 + #202020 + #4A4A20 + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/EDSEditorGUI2/App.axaml.cs b/EDSEditorGUI2/App.axaml.cs index 07140117..2a3ccdb5 100644 --- a/EDSEditorGUI2/App.axaml.cs +++ b/EDSEditorGUI2/App.axaml.cs @@ -5,8 +5,50 @@ using EDSEditorGUI2.ViewModels; using EDSEditorGUI2.Views; +using System; +using System.Globalization; +using System.Linq; +using System.Collections.Generic; +using Avalonia.Data.Converters; + namespace EDSEditorGUI2; +public class ValidationErrorConverter : IValueConverter +{ + public static readonly ValidationErrorConverter Instance = new(); + + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is Exception ex) + { + if (ex is InvalidCastException || ex is FormatException) + { + return "输入格式不正确,请输入有效的数字"; + } + return ex.Message; + } + if (value is System.ComponentModel.DataAnnotations.ValidationResult vr) + { + return vr.ErrorMessage ?? "验证失败"; + } + if (value != null) + { + var str = value.ToString(); + if (str != null && str.Contains("Could not convert")) + { + return "输入格式不正确,请输入有效的数字"; + } + return str; + } + return value; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} + public partial class App : Application { public override void Initialize() @@ -16,6 +58,9 @@ public override void Initialize() public override void OnFrameworkInitializationCompleted() { + ConfigurationManager.Load(); + ChangeLanguage(ConfigurationManager.Settings.CurrentLanguage); + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { // Line below is needed to remove Avalonia data validation. @@ -25,8 +70,28 @@ public override void OnFrameworkInitializationCompleted() { DataContext = new MainWindowViewModel(), }; + + if (desktop.Args != null && desktop.Args.Length > 0) + { + var filePath = desktop.Args[0]; + ((MainWindow)desktop.MainWindow).OpenProjectProgrammatically(filePath); + } } base.OnFrameworkInitializationCompleted(); } + + public static void ChangeLanguage(string langCode) + { + var app = Current; + if (app == null) return; + + var res = new Avalonia.Markup.Xaml.Styling.ResourceInclude(new System.Uri("avares://EDSEditorGUI2/App.axaml")) + { + Source = new System.Uri($"avares://EDSEditorGUI2/Assets/Langs/{langCode}.axaml") + }; + + app.Resources.MergedDictionaries.Clear(); + app.Resources.MergedDictionaries.Add(res); + } } \ No newline at end of file diff --git a/EDSEditorGUI2/Assets/Langs/en-US.axaml b/EDSEditorGUI2/Assets/Langs/en-US.axaml new file mode 100644 index 00000000..dd94a505 --- /dev/null +++ b/EDSEditorGUI2/Assets/Langs/en-US.axaml @@ -0,0 +1,285 @@ + + + _File + _New + _Open + _Save Project + _Save Project AS... + Open _Network XML + Save Networ_k XML + Recent + (Empty) + Clear Recent + File path is invalid or moved: + File exported successfully! + Open Folder + Are you sure you want to clear all recent records? + _Export + _Export CanOpenNode... + Save _All + _Quit + + New + Open + Save + Save As... + Close + New Device + Import Device + Remove Current Device + Export Current Device + Single Device Config (.xdd / .eds / .dcf) + CANopenNode Source (.c / .h) + PCanOpenNode Config (.json / .binpb) + Network Config (.nxdd / .nxdc) + + Export All Devices + Devices + + Welcome to CANopen Editor 2 + 📄 New Project + 📂 Open Project + + Light + Dark + System + + _Insert Profile + Insert Profile + _Reports + Tools + _Settings + _About + + Based on original work by the open-source community. + GitHub Repository + Check for Updates + + + Preferences + Language: + Theme: + Exported CANopenNode Version: + V4 (Latest) + Legacy (V1-V3) + Warnings: + Generic warnings + Variable rename warnings + Builder warnings + String length warnings + Struct member warnings + Save + Cancel + + + Insert from specific file (*.xpd, *.xdd, *.xdc) + Target: + Index Offset (Hex, single or space separated list for multiple insert) + Insert + Original Object + Insert + Deselect All + Select All + + + Device Info + Object Dictionary + TX PDO Mapping + RX PDO Mapping + Modules + + + Device Info: + Product name + Vendor name + General and Master Features: + LSS Slave + Master + Supported Baud Rates + auto + Project Info + Project file (version) + XDD v1.1 file - stripped + EDS file + DCF file + CANopenNode file (ver) + Documentation file + RPDO count + TPDO count + Node guarding Slave + Node guarding Master + No of monitored nodes + Net number + Net name + CANopen Manager + LSS Serial No. + DCF Specific Config + + + Communication Parameters + Manufacturer-Specific Parameters + Device Profile Parameters + + Sub + Name + DataType + SDO + PDO + SRDO + Default Value + + Add + Remove (shift up) + Remove (leave gap) + + Index + Sub Index + Name + Denotation + Description + Object settings + Object Type + Status + Storage Group + Data Type + Default value + HighLimit + Count Label + LowLimit + Access SDO + Actual Value + Disabled + Enabled + Access PDO + String Len Min + Count TPDO COS + Access SRDO + Save Changes + Autosave changes + + + Index + Sub + Name + Datatype + Bits + + Communication parameters + Mapping parameters + Communication + Mapping + Add new PDO + COB-ID + Transmission Type + Delete PDO + Inhibit + Event Timer + Save + SYNCs Count + Invalid + Empty + No. + Zoom: + Remove Item + Insert Padding + + + Clone + Delete + Enable / Disable + Enable + Disable + Index (hex) + Name + Object Type + Create + Dark + + + Number of connected modules + Modules List + Extended Objects + Module Comments + + Status + Index + Product Name + Version + Revision + Order Code + Name + + + Index Collision + Object dictionary already contains index {0}, how would you like to proceed? (Current progress: {1} / {2}) + Do this for all subsequent conflicts + Overwrite + Skip + Cancel + Basic Information + Sub-object Parameters + Detailed Parameters + Sub-object List + CANopenNode Specific Config + Baud Rate (kbps) + Discard + Creation Time + Unsaved modifications + Cancel + Product ID + Auto Detect + Product name cannot be empty + Index out of range. Valid range: 0x{0:X4} - 0x{1:X4}. + Enter detailed file description here (multiline supported)... + File not found + Error saving project. Possible incompatible characters or data corruption. Check terminal. + Vendor ID cannot be empty + Vendor Name + Creator + Modifier + Mapping Granularity + Device Info + File version cannot be empty + Chinese + File format might be corrupted or incomplete (missing device info). + Revision number cannot be empty + Must be a valid number or hex + Modification Time + OK + Save Failed + Product code cannot be empty + Vendor name cannot be empty + You have unsaved changes. Do you want to save them as a project before exiting? + Creator cannot be empty + Advanced Features + Sub-object name cannot be empty + Modifier cannot be empty + Unsaved Changes + Network name cannot be empty + Index {0} already exists. How to proceed?\n(Progress: {1} / {2}) + Product Name + Vendor ID + Cannot Open File + Node ID + Monitored nodes cannot exceed 127 + Revision + Save + Node Name + Enter detailed description here (multiline supported)... + Name cannot be empty! + Node name cannot be empty + File Info + File Description + Node ID must be between 1 and 127 + e.g., 0 100 500 + Index 0x{0:X4} already exists! + File Version + + Conflict Warning + [LSS Slave] and [Node ID] are mutually exclusive. Do you want to enable LSS Slave and clear the current Node ID? + [LSS Slave] and [Node ID] are mutually exclusive. Do you want to set the Node ID and disable the LSS Slave feature? + [Node Guarding Slave] and [0x1017 Producer Heartbeat Time] are mutually exclusive. Do you want to enable Node Guarding and delete the 0x1017 object? + [Node Guarding Slave] and [0x1017 Producer Heartbeat Time] are mutually exclusive. Do you want to add the 0x1017 object and disable Node Guarding Slave? + diff --git a/EDSEditorGUI2/Assets/Langs/zh-CN.axaml b/EDSEditorGUI2/Assets/Langs/zh-CN.axaml new file mode 100644 index 00000000..c8ff13b0 --- /dev/null +++ b/EDSEditorGUI2/Assets/Langs/zh-CN.axaml @@ -0,0 +1,288 @@ + + + 文件(_F) + 新建(_N) + 打开(_O) + 保存工程(_S) + 另存为(_A)... + 打开网络配置(_N) + 保存网络配置(_K) + 最近打开(R) + (暂无记录) + 清除最近打开的 + 打开文件夹 + 文件路径已失效或被移动: + 确定要清除所有最近打开的记录吗? + 文件导出成功! + 导出(_E) + 导出CanOpenNode代码(_C)... + 全部保存(_A) + 退出(_Q) + + 新建 + 打开 + 保存 + 另存为... + 关闭 + 新建设备 + 导入设备 + 移除当前设备 + 导出当前设备 + 单设备配置 (.xdd / .eds / .dcf) + CANopenNode 源码 (.c / .h) + PCanOpenNode 配置 (.json / .binpb) + 网络配置 (.nxdd / .nxdc) + + 导出所有设备 + 设备 + + 欢迎使用 CANopen Editor 2 + 📄 新建项目 + 📂 打开项目 + + 浅色 + 深色 + 跟随系统 + + 从模板插入对象(_I) + 从模板插入对象 + 报告(_R) + 设备文档 + 网络 PDO 报告 + 工具 + 设置(_S) + 关于(_A) + + 基于开源社区原始项目构建。 + GitHub 仓库 + 检查更新 + + + 偏好设置 + 语言选择: + 主题选择: + 导出的CANopenNode版本: + V4 最新版 + V1-V3 兼容版 + 警报配置: + 常规警告 + 变量重命名警告 + 代码构建警告 + 字符串长度警告 + 结构体成员对齐警告 + 保存 + 取消 + + + 从指定文件中插入 (*.xpd, *.xdd, *.xdc) + 目标设备: + 索引偏移 (十六进制,填单个或用空格隔开以支持批量插入) + 是否插入 + 原始对象字典 + 插入 + 取消全选 + 全选 + + + 设备信息 + 对象字典 + TPDO 映射 + RPDO 映射 + 模块 + + + 设备基本信息: + 产品名称 + 厂商名称 + 通用及主站特性: + LSS 从站 + CANopen 主站 + 支持的波特率 + 自动检测 + 项目信息 + 项目文件 (版本) + XDD v1.1 文件 + EDS 文件 + DCF 文件 + CANopenNode 文件 (版本) + 文档文件 + RPDO 数量 + TPDO 数量 + 节点守护 从站 + 节点守护 主站 + 监控节点数 + 网络编号 + 网络名称 + CANopen 管理器 + LSS 序列号 + DCF 专属配置 + + + 通信参数 + 制造商参数 + 设备规范参数 + + 子索引 + 名称 + 数据类型 + SDO + PDO + SRDO + 默认值 + + 添加 + 移除 (并上移) + 移除 (并留空) + + 索引 + 子索引 + 名称 + 表示符 + 描述 + 状态 + 对象设置 + 对象类型 + 数据类型 + 默认值 + 上限值 + 计数标签 + 下限值 + 存储组 + SDO 访问 + 当前值 + 已禁用 + 已启用 + PDO 访问 + 字符串最小长度 + TPDO COS计数 + SRDO 访问 + 保存修改 + 自动保存修改 + + + 索引 + 子索引 + 名称 + 数据类型 + 位数 + + 通信参数 + 映射参数 + 通信设置 + 映射 + 添加 PDO + COB-ID + 传输类型 + 删除 PDO + 禁止时间 + 事件定时器 + 保存 + SYNCs 数 + 失效 + + 序号 + 缩放 + 删除项 + 插入项 + + + 克隆 + 删除 + 启用/禁用 + 启用 + 禁用 + 索引 + 名称 + 对象类型 + 创建 + 取消 + + + 已连接模块数量 + 模块列表 + 扩展对象 + 模块描述 + + 状态 + 索引 + 产品名称 + 版本 + 修订号 + 订货号 + 名称 + + + 发现索引冲突 + 对象字典中已存在索引 {0},您希望如何处理? (当前进度: {1} / {2}) + 为之后的所有冲突项执行相同操作 + 覆盖 + 跳过 + 取消 + 基本信息 + 子对象参数 + 详细参数 + 子对象列表 + CANopenNode 专属配置 + CANopen 节点 ID + 波特率 (kbps) + 放弃 + 创建时间 + 有未保存的修改 + 取消 + 产品 ID + 自动检测 + 产品名称不能为空 + 索引超出范围。有效范围: 0x{0:X4} - 0x{1:X4}。 + 在此处输入详细的文件描述 (支持多行)... + 文件不存在 + 保存项目时遇到错误。可能是工程中存在不兼容的特殊字符或内部数据损坏。请检查终端输出获取详情。 + 供应商编号不能为空 + 厂商名称 + 创建者 + 修改者 + 映射粒度 + 设备信息 + 文件版本不能为空 + 中文 + 文件格式可能已损坏,或保存时不完整(缺少设备信息)。 + 修订版本号不能为空 + 必须是有效的数字或十六进制 + 修改时间 + 确定 + 保存失败 + 产品编号不能为空 + 供应商名称不能为空 + 您有未保存的修改,是否在退出前将它们保存为项目? + 创建者不能为空 + 高级特性 + 子对象名称不能为空 + 修改者不能为空 + 未保存的修改 + 网络名称不能为空 + 对象字典中已存在索引 {0},您希望如何处理?\n(当前进度: {1} / {2}) + 产品名称 + 厂商 ID + 无法打开文件 + 节点 ID + 监控节点数不能超过127 + 修订版本 + 保存 + 节点名称 + 在此处输入详细描述 (支持多行)... + 名称不能为空! + 节点名称不能为空 + 文件信息 + 文件描述 + 节点ID必须在1到127之间 + 例如:0 100 500 + 索引 0x{0:X4} 已经存在! + 文件版本 + + 冲突提示 + 【LSS 从站】与【节点ID】互斥。是否启用 LSS 从站并清空当前的节点 ID? + 【LSS 从站】与【节点ID】互斥。是否设置节点 ID 并取消 LSS 从站功能? + 【节点守护从站】与【0x1017 心跳报文】互斥。是否启用节点守护并删除 0x1017 对象? + 【节点守护从站】与【0x1017 心跳报文】互斥。是否添加 0x1017 对象并禁用节点守护从站? + diff --git a/EDSEditorGUI2/Assets/generate_icon.py b/EDSEditorGUI2/Assets/generate_icon.py new file mode 100644 index 00000000..fc2753a5 --- /dev/null +++ b/EDSEditorGUI2/Assets/generate_icon.py @@ -0,0 +1,61 @@ +from PIL import Image, ImageDraw, ImageFont +import os + +def create_icon(): + size = 256 + img = Image.new('RGBA', (size, size), (255, 255, 255, 0)) + draw = ImageDraw.Draw(img) + + bg_color = (41, 50, 65) # Dark slate + accent_color = (238, 108, 77) # Accent orange + + # Draw rounded rectangle + try: + draw.rounded_rectangle([(16, 16), (240, 240)], radius=40, fill=bg_color) + except AttributeError: + draw.rectangle([(16, 16), (240, 240)], fill=bg_color) + + # Draw text + try: + font = ImageFont.truetype("arialbd.ttf", 100) + except: + font = ImageFont.load_default() + + text_c = "C" + text_e = "E" + + try: + # Get bounding box for full text to center it perfectly + bbox_ce = draw.textbbox((0, 0), "CE", font=font) + + # Calculate exactly where to draw so the visual center matches the image center + # The visual center of the text when drawn at (0,0) is: + # cx = (bbox_ce[0] + bbox_ce[2]) / 2 + # cy = (bbox_ce[1] + bbox_ce[3]) / 2 + # We want the visual center to be at size / 2: + x = (size / 2) - ((bbox_ce[0] + bbox_ce[2]) / 2) + y = (size / 2) - ((bbox_ce[1] + bbox_ce[3]) / 2) + + # Get exact advance width of "C" to know where to place "E" + w_c = draw.textlength("C", font=font) + except AttributeError: + # Fallback for older Pillow + w, h = draw.textsize("CE", font=font) + w_c, _ = draw.textsize("C", font=font) + x = (size - w) / 2 + y = (size - h) / 2 + + # Draw "C" in white + draw.text((x, y), text_c, fill=(255, 255, 255), font=font) + + # Draw "E" in orange (accent_color) + # We use w_c for the X offset. + draw.text((x + w_c, y), text_e, fill=accent_color, font=font) + + out_path = r"c:\Users\14588\workspace\CANopenEditor\EDSEditorGUI2\Assets\icon.ico" + icon_sizes = [(16, 16), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)] + img.save(out_path, format="ICO", sizes=icon_sizes) + print("Icon generated successfully.") + +if __name__ == "__main__": + create_icon() diff --git a/EDSEditorGUI2/Assets/icon.ico b/EDSEditorGUI2/Assets/icon.ico new file mode 100644 index 00000000..076b4674 Binary files /dev/null and b/EDSEditorGUI2/Assets/icon.ico differ diff --git a/EDSEditorGUI2/ConfigurationManager.cs b/EDSEditorGUI2/ConfigurationManager.cs new file mode 100644 index 00000000..182ba990 --- /dev/null +++ b/EDSEditorGUI2/ConfigurationManager.cs @@ -0,0 +1,84 @@ +using System; +using System.IO; +using System.Text.Json; +using libEDSsharp; +using System.Globalization; + +namespace EDSEditorGUI2 +{ + public class AppSettingsData + { + public ExporterFactory.Exporter CurrentExporter { get; set; } = ExporterFactory.Exporter.CANOPENNODE_V4; + public string CurrentLanguage { get; set; } = ""; + public string CurrentTheme { get; set; } = "Default"; + public uint WarningMask { get; set; } = 0xFFFF; // default all warnings + public System.Collections.Generic.List RecentFiles { get; set; } = new(); + } + + public static class ConfigurationManager + { + private static readonly string ConfigPath; + public static AppSettingsData Settings { get; private set; } = new AppSettingsData(); + + static ConfigurationManager() + { + string homepath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), ".edseditor"); + if (!Directory.Exists(homepath)) + { + Directory.CreateDirectory(homepath); + } + ConfigPath = Path.Combine(homepath, "config.json"); + } + + public static void Load() + { + if (File.Exists(ConfigPath)) + { + try + { + string json = File.ReadAllText(ConfigPath); + var settings = JsonSerializer.Deserialize(json); + if (settings != null) + { + Settings = settings; + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Failed to load config: {ex}"); + } + } + + // Automatic System Language Detection Fallback + if (string.IsNullOrEmpty(Settings.CurrentLanguage)) + { + var culture = CultureInfo.CurrentUICulture.Name.ToLower(); + if (culture.Contains("zh")) + { + Settings.CurrentLanguage = "zh-CN"; + } + else + { + Settings.CurrentLanguage = "en-US"; + } + } + + // Sync legacy Warnings mask in libEDSsharp + Warnings.warning_mask = Settings.WarningMask; + } + + public static void Save() + { + try + { + Settings.WarningMask = Warnings.warning_mask; + string json = JsonSerializer.Serialize(Settings, new JsonSerializerOptions { WriteIndented = true }); + File.WriteAllText(ConfigPath, json); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Failed to save config: {ex}"); + } + } + } +} diff --git a/EDSEditorGUI2/Converter/AbsoluteHexConverter.cs b/EDSEditorGUI2/Converter/AbsoluteHexConverter.cs new file mode 100644 index 00000000..c38093d4 --- /dev/null +++ b/EDSEditorGUI2/Converter/AbsoluteHexConverter.cs @@ -0,0 +1,22 @@ +using Avalonia.Data.Converters; +using System; +using System.Globalization; + +namespace EDSEditorGUI2.Converter; + +public sealed class AbsoluteHexConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is int index) + { + return $"0x{Math.Abs(index):X4}"; + } + return value; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotSupportedException(); + } +} diff --git a/EDSEditorGUI2/Converter/BooleanToStatusConverter.cs b/EDSEditorGUI2/Converter/BooleanToStatusConverter.cs new file mode 100644 index 00000000..cb22a825 --- /dev/null +++ b/EDSEditorGUI2/Converter/BooleanToStatusConverter.cs @@ -0,0 +1,26 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace EDSEditorGUI2.Converter; + +public sealed class BooleanToStatusConverter : IValueConverter +{ + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is bool isDisabled) + { + return isDisabled ? 1 : 0; + } + return 0; + } + + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is int selectedIndex) + { + return selectedIndex == 1; // 1 means Disabled = true + } + return false; + } +} diff --git a/EDSEditorGUI2/Converter/BrushConverter.cs b/EDSEditorGUI2/Converter/BrushConverter.cs index c9edecbf..5f9a3811 100644 --- a/EDSEditorGUI2/Converter/BrushConverter.cs +++ b/EDSEditorGUI2/Converter/BrushConverter.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Globalization; using Avalonia; using Avalonia.Data.Converters; @@ -8,29 +8,19 @@ namespace EDSEditorGUI2.Converter; public sealed class BrushConverter : IValueConverter { - public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { - // TODO: Get theme and use the correct brush based on that - if (value is Boolean bValue) + if (value is bool bValue) { - if (bValue) + string resourceKey = bValue ? "SystemControlErrorTextForegroundBrush" : "SystemControlForegroundBaseHighBrush"; + if (Application.Current != null && Application.Current.TryGetResource(resourceKey, Application.Current.ActualThemeVariant, out var resource) && resource is IBrush brush) { - return new SolidColorBrush(Colors.Red); - } - else - { - var currentThemeVariant = Application.Current?.ActualThemeVariant; - - // TODO: figure out how to get the default forground from theme instead of hardcoding it - if (currentThemeVariant == Avalonia.Styling.ThemeVariant.Dark) - { - return new SolidColorBrush(Colors.White); - } - else - { - return new SolidColorBrush(Colors.Black); - } + return brush; } + + // Fallback just in case resources are missing + if (bValue) return new SolidColorBrush(Color.Parse("#C50F1F")); // Standard red + return new SolidColorBrush(Application.Current?.ActualThemeVariant == Avalonia.Styling.ThemeVariant.Dark ? Colors.White : Colors.Black); } return new SolidColorBrush(Colors.Orange); } diff --git a/EDSEditorGUI2/Converter/NewIndexMultiConvert.cs b/EDSEditorGUI2/Converter/NewIndexMultiConvert.cs index 5a5b8d47..9732e394 100644 --- a/EDSEditorGUI2/Converter/NewIndexMultiConvert.cs +++ b/EDSEditorGUI2/Converter/NewIndexMultiConvert.cs @@ -1,4 +1,4 @@ -using Avalonia.Data; +using Avalonia.Data; using Avalonia.Data.Converters; using Avalonia.Media.Immutable; using System; @@ -28,7 +28,8 @@ values[1] is not string name || values[2] is not int typeIndex) return BindingOperations.DoNothing; - int index = int.Parse(rawindex, NumberStyles.HexNumber); + if (!int.TryParse(rawindex.StartsWith("0x", StringComparison.OrdinalIgnoreCase) ? rawindex.Substring(2) : rawindex, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int index)) + return BindingOperations.DoNothing; var typeValues = Enum.GetNames(typeof(LibCanOpen.OdObject.Types.ObjectType)).Skip(1).ToArray(); bool parseOk = Enum.TryParse(typeValues[typeIndex], out LibCanOpen.OdObject.Types.ObjectType type); diff --git a/EDSEditorGUI2/Converter/SubIndexConverter.cs b/EDSEditorGUI2/Converter/SubIndexConverter.cs new file mode 100644 index 00000000..d58bbd4e --- /dev/null +++ b/EDSEditorGUI2/Converter/SubIndexConverter.cs @@ -0,0 +1,42 @@ +using Avalonia.Data.Converters; +using System; +using System.Collections.Generic; +using System.Globalization; + +namespace EDSEditorGUI2.Converter +{ + public class SubIndexConverter : IMultiValueConverter + { + public object? Convert(IList values, Type targetType, object? parameter, CultureInfo culture) + { + if (values != null && values.Count > 0) + { + if (values[0] is string keyString) + { + if (values.Count > 1 && values[1] is EDSEditorGUI2.ViewModels.ObjectDictionary dc) + { + if (dc.SelectedObjectType == LibCanOpen.OdObject.Types.ObjectType.Var) + { + return string.Empty; + } + } + + if (byte.TryParse(keyString, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out byte key)) + { + return $"0x{key:X2}"; + } + else if (keyString.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) + { + return keyString; + } + else if (int.TryParse(keyString, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int intKey)) + { + return $"0x{intKey:X2}"; + } + return keyString; + } + } + return string.Empty; + } + } +} diff --git a/EDSEditorGUI2/DataGridBehavior.cs b/EDSEditorGUI2/DataGridBehavior.cs new file mode 100644 index 00000000..09f8b67a --- /dev/null +++ b/EDSEditorGUI2/DataGridBehavior.cs @@ -0,0 +1,58 @@ +using System; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; + +namespace EDSEditorGUI2 +{ + public class DataGridBehavior + { + public static readonly AttachedProperty LockAutoMinWidthProperty = + AvaloniaProperty.RegisterAttached("LockAutoMinWidth"); + + public static void SetLockAutoMinWidth(DataGrid element, bool value) + { + element.SetValue(LockAutoMinWidthProperty, value); + } + + public static bool GetLockAutoMinWidth(DataGrid element) + { + return element.GetValue(LockAutoMinWidthProperty); + } + + static DataGridBehavior() + { + LockAutoMinWidthProperty.Changed.AddClassHandler(OnLockAutoMinWidthChanged); + } + + private static void OnLockAutoMinWidthChanged(DataGrid grid, AvaloniaPropertyChangedEventArgs e) + { + if (e.NewValue is bool b && b) + { + grid.LayoutUpdated += Grid_LayoutUpdated; + } + else + { + grid.LayoutUpdated -= Grid_LayoutUpdated; + } + } + + private static readonly System.Runtime.CompilerServices.ConditionalWeakTable _processedColumns = new(); + + private static void Grid_LayoutUpdated(object? sender, EventArgs e) + { + if (sender is DataGrid grid) + { + foreach (var col in grid.Columns) + { + if (col.ActualWidth > 0 && !_processedColumns.TryGetValue(col, out _)) + { + // Convert any auto/star sizing to absolute pixel width so it never changes + col.Width = new DataGridLength(col.ActualWidth, DataGridLengthUnitType.Pixel); + _processedColumns.Add(col, new object()); + } + } + } + } + } +} diff --git a/EDSEditorGUI2/EDSEditorGUI2.csproj b/EDSEditorGUI2/EDSEditorGUI2.csproj index b44a0554..225d4387 100644 --- a/EDSEditorGUI2/EDSEditorGUI2.csproj +++ b/EDSEditorGUI2/EDSEditorGUI2.csproj @@ -1,11 +1,13 @@ - + WinExe net8.0 enable true app.manifest + Assets\icon.ico true + @@ -14,16 +16,18 @@ - - - - - - - + + + Profiles\%(RecursiveDir)%(Filename)%(Extension) + PreserveNewest + PreserveNewest + + + style.css + PreserveNewest + PreserveNewest + + @@ -37,5 +41,21 @@ + + + + + + True + True + Resources.resx + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + diff --git a/EDSEditorGUI2/EDSEditorGUI2.csproj.user b/EDSEditorGUI2/EDSEditorGUI2.csproj.user new file mode 100644 index 00000000..7f4d98e3 --- /dev/null +++ b/EDSEditorGUI2/EDSEditorGUI2.csproj.user @@ -0,0 +1,9 @@ + + + + ProjectDebugger + + + EDSEditorGUI2 + + \ No newline at end of file diff --git a/EDSEditorGUI2/Icons.axaml b/EDSEditorGUI2/Icons.axaml index 5583834c..41f1d52e 100644 --- a/EDSEditorGUI2/Icons.axaml +++ b/EDSEditorGUI2/Icons.axaml @@ -18,6 +18,9 @@ M14.5,13 L14.5,3.75378577 C14.5,3.33978577 14.164,3.00378577 13.75,3.00378577 C13.336,3.00378577 13,3.33978577 13,3.75378577 L13,13 L3.75387573,13 C3.33987573,13 3.00387573,13.336 3.00387573,13.75 C3.00387573,14.164 3.33987573,14.5 3.75387573,14.5 L13,14.5 L13,23.7523651 C13,24.1663651 13.336,24.5023651 13.75,24.5023651 C14.164,24.5023651 14.5,24.1663651 14.5,23.7523651 L14.5,14.5 L23.7498262,14.5030754 C24.1638262,14.5030754 24.4998262,14.1670754 24.4998262,13.7530754 C24.4998262,13.3390754 24.1638262,13.0030754 23.7498262,13.0030754 L14.5,13 Z M3 5.75C3 4.23122 4.23122 3 5.75 3H15.7145C16.5764 3 17.4031 3.34241 18.0126 3.9519L20.0481 5.98744C20.6576 6.59693 21 7.42358 21 8.28553V18.25C21 19.7688 19.7688 21 18.25 21H5.75C4.23122 21 3 19.7688 3 18.25V5.75ZM5.75 4.5C5.05964 4.5 4.5 5.05964 4.5 5.75V18.25C4.5 18.9404 5.05964 19.5 5.75 19.5H6V14.25C6 13.0074 7.00736 12 8.25 12H15.75C16.9926 12 18 13.0074 18 14.25V19.5H18.25C18.9404 19.5 19.5 18.9404 19.5 18.25V8.28553C19.5 7.8214 19.3156 7.37629 18.9874 7.0481L16.9519 5.01256C16.6918 4.75246 16.3582 4.58269 16 4.52344V7.25C16 8.49264 14.9926 9.5 13.75 9.5H9.25C8.00736 9.5 7 8.49264 7 7.25V4.5H5.75ZM16.5 19.5V14.25C16.5 13.8358 16.1642 13.5 15.75 13.5H8.25C7.83579 13.5 7.5 13.8358 7.5 14.25V19.5H16.5ZM8.5 4.5V7.25C8.5 7.66421 8.83579 8 9.25 8H13.75C14.1642 8 14.5 7.66421 14.5 7.25V4.5H8.5Z M3 4.5C3 3.11929 4.11929 2 5.5 2H17C18.3807 2 19.5 3.11929 19.5 4.5V11.3135C19.0218 11.159 18.5195 11.0585 18 11.0189V4.5C18 3.94772 17.5523 3.5 17 3.5H5.5C4.94772 3.5 4.5 3.94772 4.5 4.5V18H11.0189C11.0585 18.5195 11.159 19.0218 11.3135 19.5H4.5C4.5 20.0523 4.94772 20.5 5.5 20.5H11.7322C12.0194 21.051 12.3832 21.5557 12.8096 22H5.5C4.11929 22 3 20.8807 3 19.5V4.5Z M17.5 12C20.5376 12 23 14.4624 23 17.5C23 20.5376 20.5376 23 17.5 23C14.4624 23 12 20.5376 12 17.5C12 14.4624 14.4624 12 17.5 12ZM18.0011 20.5035L18.0006 18H20.503C20.7792 18 21.003 17.7762 21.003 17.5C21.003 17.2239 20.7792 17 20.503 17H18.0005L18 14.4993C18 14.2231 17.7761 13.9993 17.5 13.9993C17.2239 13.9993 17 14.2231 17 14.4993L17.0005 17H14.4961C14.22 17 13.9961 17.2239 13.9961 17.5C13.9961 17.7762 14.22 18 14.4961 18H17.0006L17.0011 20.5035C17.0011 20.7797 17.225 21.0035 17.5011 21.0035C17.7773 21.0035 18.0011 20.7797 18.0011 20.5035Z M6 6C6 5.44772 6.44772 5 7 5H15C15.5523 5 16 5.44772 16 6V8C16 8.55228 15.5523 9 15 9H7C6.44772 9 6 8.55228 6 8V6ZM7.5 7.5H14.5V6.5H7.5V7.5Z - + M16,1H4C2.9,1,2,1.9,2,3v14h2V3h12V1z M19,5H8C6.9,5,6,5.9,6,7v14c0,1.1,0.9,2,2,2h11c1.1,0,2-0.9,2-2V7C21,5.9,20.1,5,19,5z M19,21H8V7h11V21z + M12,4.5C7,4.5 2.73,7.61 1,12c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z + M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78 3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z + diff --git a/EDSEditorGUI2/Mapper/ProtobufferViewModelMapper.cs b/EDSEditorGUI2/Mapper/ProtobufferViewModelMapper.cs index db06d9e4..7bc4f31e 100644 --- a/EDSEditorGUI2/Mapper/ProtobufferViewModelMapper.cs +++ b/EDSEditorGUI2/Mapper/ProtobufferViewModelMapper.cs @@ -1,4 +1,4 @@ -using AutoMapper; +using AutoMapper; using Google.Protobuf.WellKnownTypes; using LibCanOpen; using Microsoft.Extensions.Logging; @@ -6,68 +6,180 @@ namespace EDSEditorGUI2.Mapper { - public partial class ProtobufferViewModelMapper + public class ProtobufferViewModelMapper { - public static ViewModels.Device MapFromProtobuffer(CanOpenDevice source) + private static readonly IMapper _fromProtoMapper; + private static readonly IMapper _toProtoMapper; + + static ProtobufferViewModelMapper() { - var config = new MapperConfiguration(cfg => + var fromProtoConfig = new MapperConfiguration(cfg => { - cfg.CreateMap().ConvertUsing(ts => ts.ToDateTime()); + cfg.CreateMap().ConvertUsing(ts => ts == null ? default : ts.ToDateTime().ToLocalTime()); cfg.CreateMap() - .ForMember(dest => dest.FileVersion, opt => opt.MapFrom(src => src.FileVersion)) + .ForMember(dest => dest.FileVersion, opt => opt.MapFrom(src => string.IsNullOrEmpty(src.FileVersion) ? "1.0" : src.FileVersion)) .ForMember(dest => dest.Description, opt => opt.MapFrom(src => src.Description)) .ForMember(dest => dest.CreationTime, opt => opt.MapFrom(src => src.CreationTime)) .ForMember(dest => dest.CreatedBy, opt => opt.MapFrom(src => src.CreatedBy)) .ForMember(dest => dest.ModificationTime, opt => opt.MapFrom(src => src.ModificationTime)) .ForMember(dest => dest.ModifiedBy, opt => opt.MapFrom(src => src.ModifiedBy)); cfg.CreateMap, ViewModels.ObjectDictionary>().ConvertUsing(); - cfg.CreateMap() + cfg.CreateMap, System.Collections.ObjectModel.ObservableCollection>().ConvertUsing(); + cfg.CreateMap(MemberList.None) + .ForMember(d => d.Eds, opt => opt.Ignore()) + .ForMember(d => d.TxPdo, opt => opt.Ignore()) + .ForMember(d => d.RxPdo, opt => opt.Ignore()) + .ForMember(d => d.ProjectInfo, opt => opt.Ignore()) + .ForMember(dest => dest.FileInfo, opt => opt.MapFrom(src => src.FileInfo)) + .ForMember(dest => dest.DeviceInfo, opt => opt.MapFrom(src => src.DeviceInfo)) + .ForMember(dest => dest.DeviceCommissioning, opt => opt.MapFrom(src => src.DeviceCommissioning)) + .ForMember(dest => dest.Objects, opt => opt.MapFrom(src => src.Objects)) + .ForPath(dest => dest.ModuleInfo.NrSupportedModules, opt => opt.MapFrom(src => src.NrSupportedModules)) + .ForPath(dest => dest.ModuleInfo.Modules, opt => opt.MapFrom(src => src.Modules)); + cfg.CreateMap(MemberList.None) + .ForMember(dest => dest.RevisionNumber, opt => opt.MapFrom(src => src.RevisionNumber == 0 ? "1" : src.RevisionNumber.ToString())); + cfg.CreateMap(MemberList.None) + .ForMember(dest => dest.NodeId, opt => opt.MapFrom(src => src.NodeId == 0 ? (uint?)null : src.NodeId)) + .ForMember(dest => dest.Baudrate, opt => opt.MapFrom(src => src.Baudrate == 0 ? (uint?)null : src.Baudrate)); + }, LoggerFactory.Create(builder => { builder.AddDebug(); })); + fromProtoConfig.AssertConfigurationIsValid(); + _fromProtoMapper = fromProtoConfig.CreateMapper(); + + var toProtoConfig = new MapperConfiguration(cfg => + { + cfg.CreateMap().ConvertUsing(dt => + dt == DateTime.MinValue ? new Timestamp() : Timestamp.FromDateTime(dt.Kind == DateTimeKind.Utc ? dt : dt.ToUniversalTime()) + ); + cfg.CreateMap(MemberList.None); + cfg.CreateMap>().ConvertUsing(); + + cfg.CreateMap(MemberList.None) .ForMember(dest => dest.FileInfo, opt => opt.MapFrom(src => src.FileInfo)) .ForMember(dest => dest.DeviceInfo, opt => opt.MapFrom(src => src.DeviceInfo)) .ForMember(dest => dest.DeviceCommissioning, opt => opt.MapFrom(src => src.DeviceCommissioning)) - .ForMember(dest => dest.Objects, opt => opt.MapFrom(src => src.Objects)); - cfg.CreateMap(); - cfg.CreateMap(); + .ForMember(dest => dest.Objects, opt => opt.MapFrom(src => src.Objects)) + .ForMember(dest => dest.NrSupportedModules, opt => opt.MapFrom(src => src.ModuleInfo.NrSupportedModules)) + .ForMember(dest => dest.Modules, opt => opt.Ignore()); + + cfg.CreateMap(MemberList.None) + .ForMember(dest => dest.RevisionNumber, opt => opt.MapFrom(src => ParseRevisionNumber(src.RevisionNumber))); + cfg.CreateMap(MemberList.None); }, LoggerFactory.Create(builder => { builder.AddDebug(); })); - config.AssertConfigurationIsValid(); - var mapper = config.CreateMapper(); - var result = mapper.Map(source); - return result; + toProtoConfig.AssertConfigurationIsValid(); + _toProtoMapper = toProtoConfig.CreateMapper(); } + + private static uint ParseRevisionNumber(string revStr) + { + if (string.IsNullOrEmpty(revStr)) return 0; + return uint.TryParse(revStr, out uint rev) ? rev : 0; + } + + public static ViewModels.Device MapFromProtobuffer(CanOpenDevice source) + { + return _fromProtoMapper.Map(source); + } + public class ODConverter : ITypeConverter, ViewModels.ObjectDictionary>, ITypeConverter< ViewModels.ObjectDictionary, Google.Protobuf.Collections.MapField> { - public ViewModels.ObjectDictionary Convert(Google.Protobuf.Collections.MapField source, ViewModels.ObjectDictionary destination, ResolutionContext context) + private static readonly IMapper _toVmMapper; + private static readonly IMapper _toProtoMapper; + + static ODConverter() { - var config = new MapperConfiguration(cfg => + var toVmConfig = new MapperConfiguration(cfg => { - cfg.CreateMap(); - cfg.CreateMap(); + cfg.CreateMap(MemberList.None) + .ForMember(dest => dest.SubObjects, opt => opt.Ignore()) + .AfterMap((src, dest, ctx) => + { + dest.SubObjects.Clear(); + if (src.SubObjects != null) + { + foreach (var subItem in src.SubObjects) + { + var mappedSub = ctx.Mapper.Map(subItem.Value); + dest.SubObjects.Add(new System.Collections.Generic.KeyValuePair(subItem.Key, mappedSub)); + } + } + }); + cfg.CreateMap(MemberList.None); }, LoggerFactory.Create(builder => { builder.AddDebug(); })); - config.AssertConfigurationIsValid(); - var mapper = config.CreateMapper(); + toVmConfig.AssertConfigurationIsValid(); + _toVmMapper = toVmConfig.CreateMapper(); + var toProtoConfig = new MapperConfiguration(cfg => + { + cfg.CreateMap(MemberList.None) + .ForMember(dest => dest.SubObjects, opt => opt.Ignore()) + .AfterMap((src, dest, ctx) => + { + dest.SubObjects.Clear(); + if (src.SubObjects != null) + { + foreach (var subItem in src.SubObjects) + { + var mappedSub = ctx.Mapper.Map(subItem.Value); + dest.SubObjects.Add(subItem.Key, mappedSub); + } + } + }); + cfg.CreateMap(MemberList.None); + }, LoggerFactory.Create(builder => { builder.AddDebug(); })); + toProtoConfig.AssertConfigurationIsValid(); + _toProtoMapper = toProtoConfig.CreateMapper(); + } + + public ViewModels.ObjectDictionary Convert(Google.Protobuf.Collections.MapField source, ViewModels.ObjectDictionary destination, ResolutionContext context) + { destination = []; foreach (var item in source) { - destination.Add(item.Key, mapper.Map(item.Value)); + destination.Add(item.Key, _toVmMapper.Map(item.Value)); } return destination; } + public Google.Protobuf.Collections.MapField Convert(ViewModels.ObjectDictionary source, Google.Protobuf.Collections.MapField destination, ResolutionContext context) { - var config = new MapperConfiguration(cfg => + if (destination == null) destination = new Google.Protobuf.Collections.MapField(); + else destination.Clear(); + + foreach (var item in source) { - cfg.CreateMap(); - cfg.CreateMap(); - }, LoggerFactory.Create(builder => { builder.AddDebug(); })); - config.AssertConfigurationIsValid(); - var mapper = config.CreateMapper(); + destination.Add(item.Key, _toProtoMapper.Map(item.Value)); + } + return destination; + } + } - destination = []; + public class ModuleConverter : ITypeConverter, System.Collections.ObjectModel.ObservableCollection> + { + public System.Collections.ObjectModel.ObservableCollection Convert(Google.Protobuf.Collections.MapField source, System.Collections.ObjectModel.ObservableCollection destination, ResolutionContext context) + { + destination ??= new(); foreach (var item in source) { - destination.Add(item.Key, mapper.Map(item.Value)); + var vm = new ViewModels.ModuleItemViewModel + { + Index = item.Key, + ProductName = item.Value.Info?.ProductName ?? string.Empty, + ProductVersion = item.Value.Info?.ProductVersion ?? string.Empty, + ProductRevision = item.Value.Info?.ProductRevision ?? string.Empty, + OrderCode = item.Value.Info?.OrderCode ?? string.Empty, + IsConnected = item.Value.IsConnected, + Comments = string.Join("\r\n", item.Value.Comments) + }; + foreach (var extObj in item.Value.ExtendedObjects) + { + vm.ExtendedObjects.Add(new ViewModels.ModuleObjectViewModel + { + IndexHex = $"0x{extObj:X4}", + Name = $"Index {extObj:X4}" + }); + } + destination.Add(vm); } return destination; } @@ -75,14 +187,7 @@ public Google.Protobuf.Collections.MapField Convert(ViewModels public static CanOpenDevice MapToProtobuffer(ViewModels.Device source) { - var config = new MapperConfiguration(cfg => - { - //TODO - }, LoggerFactory.Create(builder => { builder.AddDebug(); })); - config.AssertConfigurationIsValid(); - var mapper = config.CreateMapper(); - var result = mapper.Map(source); - return result; + return _toProtoMapper.Map(source); } } } diff --git a/EDSEditorGUI2/Mapper/rewrite_mapper.py b/EDSEditorGUI2/Mapper/rewrite_mapper.py new file mode 100644 index 00000000..9a6ae250 --- /dev/null +++ b/EDSEditorGUI2/Mapper/rewrite_mapper.py @@ -0,0 +1,189 @@ +import re + +filepath = r'c:\Users\14588\workspace\CANopenEditor\EDSEditorGUI2\Mapper\ProtobufferViewModelMapper.cs' +with open(filepath, 'r', encoding='utf-8') as f: + code = f.read() + +# We will just write the code out completely, it's safer. +new_code = """using AutoMapper; +using Google.Protobuf.WellKnownTypes; +using LibCanOpen; +using Microsoft.Extensions.Logging; +using System; + +namespace EDSEditorGUI2.Mapper +{ + public class ProtobufferViewModelMapper + { + private static readonly IMapper _fromProtoMapper; + private static readonly IMapper _toProtoMapper; + + static ProtobufferViewModelMapper() + { + var fromProtoConfig = new MapperConfiguration(cfg => + { + cfg.CreateMap().ConvertUsing(ts => ts == null ? default : ts.ToDateTime().ToLocalTime()); + cfg.CreateMap() + .ForMember(dest => dest.FileVersion, opt => opt.MapFrom(src => src.FileVersion)) + .ForMember(dest => dest.Description, opt => opt.MapFrom(src => src.Description)) + .ForMember(dest => dest.CreationTime, opt => opt.MapFrom(src => src.CreationTime)) + .ForMember(dest => dest.CreatedBy, opt => opt.MapFrom(src => src.CreatedBy)) + .ForMember(dest => dest.ModificationTime, opt => opt.MapFrom(src => src.ModificationTime)) + .ForMember(dest => dest.ModifiedBy, opt => opt.MapFrom(src => src.ModifiedBy)); + cfg.CreateMap, ViewModels.ObjectDictionary>().ConvertUsing(); + cfg.CreateMap, System.Collections.ObjectModel.ObservableCollection>().ConvertUsing(); + cfg.CreateMap(MemberList.None) + .ForMember(d => d.Eds, opt => opt.Ignore()) + .ForMember(d => d.TxPdo, opt => opt.Ignore()) + .ForMember(d => d.RxPdo, opt => opt.Ignore()) + .ForMember(d => d.ProjectInfo, opt => opt.Ignore()) + .ForMember(dest => dest.FileInfo, opt => opt.MapFrom(src => src.FileInfo)) + .ForMember(dest => dest.DeviceInfo, opt => opt.MapFrom(src => src.DeviceInfo)) + .ForMember(dest => dest.DeviceCommissioning, opt => opt.MapFrom(src => src.DeviceCommissioning)) + .ForMember(dest => dest.Objects, opt => opt.MapFrom(src => src.Objects)) + .ForPath(dest => dest.ModuleInfo.NrSupportedModules, opt => opt.MapFrom(src => src.NrSupportedModules)) + .ForPath(dest => dest.ModuleInfo.Modules, opt => opt.MapFrom(src => src.Modules)); + cfg.CreateMap(MemberList.None) + .ForMember(dest => dest.RevisionNumber, opt => opt.MapFrom(src => src.RevisionNumber.ToString())); + cfg.CreateMap(MemberList.None); + }, LoggerFactory.Create(builder => { builder.AddDebug(); })); + fromProtoConfig.AssertConfigurationIsValid(); + _fromProtoMapper = fromProtoConfig.CreateMapper(); + + var toProtoConfig = new MapperConfiguration(cfg => + { + cfg.CreateMap().ConvertUsing(dt => + dt == DateTime.MinValue ? new Timestamp() : Timestamp.FromDateTime(dt.Kind == DateTimeKind.Utc ? dt : dt.ToUniversalTime()) + ); + cfg.CreateMap(MemberList.None); + cfg.CreateMap>().ConvertUsing(); + + cfg.CreateMap(MemberList.None) + .ForMember(dest => dest.FileInfo, opt => opt.MapFrom(src => src.FileInfo)) + .ForMember(dest => dest.DeviceInfo, opt => opt.MapFrom(src => src.DeviceInfo)) + .ForMember(dest => dest.DeviceCommissioning, opt => opt.MapFrom(src => src.DeviceCommissioning)) + .ForMember(dest => dest.Objects, opt => opt.MapFrom(src => src.Objects)) + .ForMember(dest => dest.NrSupportedModules, opt => opt.MapFrom(src => src.ModuleInfo.NrSupportedModules)) + .ForMember(dest => dest.Modules, opt => opt.Ignore()); + + cfg.CreateMap(MemberList.None) + .ForMember(dest => dest.RevisionNumber, opt => opt.MapFrom(src => ParseRevisionNumber(src.RevisionNumber))); + cfg.CreateMap(MemberList.None); + }, LoggerFactory.Create(builder => { builder.AddDebug(); })); + toProtoConfig.AssertConfigurationIsValid(); + _toProtoMapper = toProtoConfig.CreateMapper(); + } + + private static uint ParseRevisionNumber(string revStr) + { + if (string.IsNullOrEmpty(revStr)) return 0; + return uint.TryParse(revStr, out uint rev) ? rev : 0; + } + + public static ViewModels.Device MapFromProtobuffer(CanOpenDevice source) + { + return _fromProtoMapper.Map(source); + } + + public class ODConverter : ITypeConverter, ViewModels.ObjectDictionary>, + ITypeConverter< ViewModels.ObjectDictionary, Google.Protobuf.Collections.MapField> + { + private static readonly IMapper _toVmMapper; + private static readonly IMapper _toProtoMapper; + + static ODConverter() + { + var toVmConfig = new MapperConfiguration(cfg => + { + cfg.CreateMap(MemberList.None); + cfg.CreateMap(MemberList.None); + }, LoggerFactory.Create(builder => { builder.AddDebug(); })); + toVmConfig.AssertConfigurationIsValid(); + _toVmMapper = toVmConfig.CreateMapper(); + + var toProtoConfig = new MapperConfiguration(cfg => + { + cfg.CreateMap(MemberList.None) + .ForMember(dest => dest.SubObjects, opt => opt.Ignore()) + .AfterMap((src, dest, ctx) => + { + dest.SubObjects.Clear(); + if (src.SubObjects != null) + { + foreach (var subItem in src.SubObjects) + { + var mappedSub = ctx.Mapper.Map(subItem.Value); + dest.SubObjects.Add(subItem.Key, mappedSub); + } + } + }); + cfg.CreateMap(MemberList.None); + }, LoggerFactory.Create(builder => { builder.AddDebug(); })); + toProtoConfig.AssertConfigurationIsValid(); + _toProtoMapper = toProtoConfig.CreateMapper(); + } + + public ViewModels.ObjectDictionary Convert(Google.Protobuf.Collections.MapField source, ViewModels.ObjectDictionary destination, ResolutionContext context) + { + destination = []; + foreach (var item in source) + { + destination.Add(item.Key, _toVmMapper.Map(item.Value)); + } + return destination; + } + + public Google.Protobuf.Collections.MapField Convert(ViewModels.ObjectDictionary source, Google.Protobuf.Collections.MapField destination, ResolutionContext context) + { + if (destination == null) destination = new Google.Protobuf.Collections.MapField(); + else destination.Clear(); + + foreach (var item in source) + { + destination.Add(item.Key, _toProtoMapper.Map(item.Value)); + } + return destination; + } + } + + public class ModuleConverter : ITypeConverter, System.Collections.ObjectModel.ObservableCollection> + { + public System.Collections.ObjectModel.ObservableCollection Convert(Google.Protobuf.Collections.MapField source, System.Collections.ObjectModel.ObservableCollection destination, ResolutionContext context) + { + destination ??= new(); + foreach (var item in source) + { + var vm = new ViewModels.ModuleItemViewModel + { + Index = item.Key, + ProductName = item.Value.Info?.ProductName ?? string.Empty, + ProductVersion = item.Value.Info?.ProductVersion ?? string.Empty, + ProductRevision = item.Value.Info?.ProductRevision ?? string.Empty, + OrderCode = item.Value.Info?.OrderCode ?? string.Empty, + IsConnected = item.Value.IsConnected, + Comments = string.Join(\"\\r\\n\", item.Value.Comments) + }; + foreach (var extObj in item.Value.ExtendedObjects) + { + vm.ExtendedObjects.Add(new ViewModels.ModuleObjectViewModel + { + IndexHex = $\"0x{extObj:X4}\", + Name = $\"Index {extObj:X4}\" + }); + } + destination.Add(vm); + } + return destination; + } + } + + public static CanOpenDevice MapToProtobuffer(ViewModels.Device source) + { + return _toProtoMapper.Map(source); + } + } +} +""" + +with open(filepath, 'w', encoding='utf-8') as f: + f.write(new_code) diff --git a/EDSEditorGUI2/Program.cs b/EDSEditorGUI2/Program.cs index c22186a5..5eba6463 100644 --- a/EDSEditorGUI2/Program.cs +++ b/EDSEditorGUI2/Program.cs @@ -1,5 +1,6 @@ -using Avalonia; +using Avalonia; using System; +using Velopack; namespace EDSEditorGUI2; @@ -9,8 +10,13 @@ sealed class Program // SynchronizationContext-reliant code before AppMain is called: things aren't initialized // yet and stuff might break. [STAThread] - public static void Main(string[] args) => BuildAvaloniaApp() - .StartWithClassicDesktopLifetime(args); + public static void Main(string[] args) + { + VelopackApp.Build().Run(); + + BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + } // Avalonia configuration, don't remove; also used by visual designer. public static AppBuilder BuildAvaloniaApp() diff --git a/EDSEditorGUI2/Properties/Resources.Designer.cs b/EDSEditorGUI2/Properties/Resources.Designer.cs new file mode 100644 index 00000000..0d60e5d9 --- /dev/null +++ b/EDSEditorGUI2/Properties/Resources.Designer.cs @@ -0,0 +1,63 @@ +//------------------------------------------------------------------------------ +// +// 此代码由工具生成。 +// 运行时版本:4.0.30319.42000 +// +// 对此文件的更改可能会导致不正确的行为,并且如果 +// 重新生成代码,这些更改将会丢失。 +// +//------------------------------------------------------------------------------ + +namespace EDSEditorGUI2.Properties { + using System; + + + /// + /// 一个强类型的资源类,用于查找本地化的字符串等。 + /// + // 此类是由 StronglyTypedResourceBuilder + // 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。 + // 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen + // (以 /str 作为命令选项),或重新生成 VS 项目。 + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// 返回此类使用的缓存的 ResourceManager 实例。 + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("EDSEditorGUI2.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// 重写当前线程的 CurrentUICulture 属性,对 + /// 使用此强类型资源类的所有资源查找执行重写。 + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + } +} diff --git a/EDSEditorGUI2/Properties/Resources.resx b/EDSEditorGUI2/Properties/Resources.resx new file mode 100644 index 00000000..4fdb1b6a --- /dev/null +++ b/EDSEditorGUI2/Properties/Resources.resx @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/EDSEditorGUI2/Properties/launchSettings.json b/EDSEditorGUI2/Properties/launchSettings.json new file mode 100644 index 00000000..2edbc2d2 --- /dev/null +++ b/EDSEditorGUI2/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "windows": { + "commandName": "Project" + }, + "WSL": { + "commandName": "WSL2", + "distributionName": "" + } + } +} \ No newline at end of file diff --git a/EDSEditorGUI2/Releases/CANopenEditor-1.0.0-full.nupkg b/EDSEditorGUI2/Releases/CANopenEditor-1.0.0-full.nupkg new file mode 100644 index 00000000..f3b121c7 Binary files /dev/null and b/EDSEditorGUI2/Releases/CANopenEditor-1.0.0-full.nupkg differ diff --git a/EDSEditorGUI2/Releases/CANopenEditor-win-Portable.zip b/EDSEditorGUI2/Releases/CANopenEditor-win-Portable.zip new file mode 100644 index 00000000..7cbc11f0 Binary files /dev/null and b/EDSEditorGUI2/Releases/CANopenEditor-win-Portable.zip differ diff --git a/EDSEditorGUI2/Releases/RELEASES b/EDSEditorGUI2/Releases/RELEASES new file mode 100644 index 00000000..c6fba337 --- /dev/null +++ b/EDSEditorGUI2/Releases/RELEASES @@ -0,0 +1 @@ +4420051AEB9BBAAEAA1D408E8BE20E8828494645 CANopenEditor-1.0.0-full.nupkg 53728974 \ No newline at end of file diff --git a/EDSEditorGUI2/Releases/assets.win.json b/EDSEditorGUI2/Releases/assets.win.json new file mode 100644 index 00000000..2f50e375 --- /dev/null +++ b/EDSEditorGUI2/Releases/assets.win.json @@ -0,0 +1 @@ +[{"RelativeFileName":"CANopenEditor-win-Setup.exe","Type":"Installer"},{"RelativeFileName":"CANopenEditor-1.0.0-full.nupkg","Type":"Full"},{"RelativeFileName":"CANopenEditor-win-Portable.zip","Type":"Portable"}] \ No newline at end of file diff --git a/EDSEditorGUI2/Releases/releases.win.json b/EDSEditorGUI2/Releases/releases.win.json new file mode 100644 index 00000000..77d6b040 --- /dev/null +++ b/EDSEditorGUI2/Releases/releases.win.json @@ -0,0 +1 @@ +{"Assets":[{"PackageId":"CANopenEditor","Version":"1.0.0","Type":"Full","FileName":"CANopenEditor-1.0.0-full.nupkg","SHA1":"4420051AEB9BBAAEAA1D408E8BE20E8828494645","SHA256":"9774A3BCC8D7DDE981CCD3C3DBCF23437F5870D786D362128DE2EA5CC1041EF5","Size":53728974}]} \ No newline at end of file diff --git a/EDSEditorGUI2/ViewModels/Device.cs b/EDSEditorGUI2/ViewModels/Device.cs index 568bfb04..a739a622 100644 --- a/EDSEditorGUI2/ViewModels/Device.cs +++ b/EDSEditorGUI2/ViewModels/Device.cs @@ -1,4 +1,12 @@ -using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.ComponentModel; +using libEDSsharp; +using System.Linq; +using Avalonia.Threading; +using System.ComponentModel; +using Avalonia.Controls.ApplicationLifetimes; +using System.Collections.Specialized; +using System.Collections.Generic; +using Avalonia.Controls; namespace EDSEditorGUI2.ViewModels { @@ -6,8 +14,378 @@ public partial class Device : ObservableObject { public Device() { + SetupListeners(); } + private void SetupListeners() + { + if (DeviceInfo != null) + DeviceInfo.PropertyChanged += DeviceInfo_PropertyChanged; + if (DeviceCommissioning != null) + DeviceCommissioning.PropertyChanged += DeviceCommissioning_PropertyChanged; + if (Objects != null) + Objects.CollectionChanged += Objects_CollectionChanged; + } + + partial void OnProjectInfoChanged(ProjectInfo? oldValue, ProjectInfo newValue) + { + if (oldValue != null) oldValue.PropertyChanged -= SubObject_PropertyChanged; + if (newValue != null) newValue.PropertyChanged += SubObject_PropertyChanged; + } + + partial void OnFileInfoChanged(FileInfo? oldValue, FileInfo newValue) + { + if (oldValue != null) oldValue.PropertyChanged -= SubObject_PropertyChanged; + if (newValue != null) newValue.PropertyChanged += SubObject_PropertyChanged; + } + + private void SubObject_PropertyChanged(object? sender, PropertyChangedEventArgs e) + { + CheckAndRequestAutoSave(); + } + + partial void OnDeviceInfoChanged(DeviceInfo? oldValue, DeviceInfo newValue) + { + if (oldValue != null) oldValue.PropertyChanged -= DeviceInfo_PropertyChanged; + if (newValue != null) newValue.PropertyChanged += DeviceInfo_PropertyChanged; + } + + partial void OnDeviceCommissioningChanged(DeviceCommissioning? oldValue, DeviceCommissioning newValue) + { + if (oldValue != null) oldValue.PropertyChanged -= DeviceCommissioning_PropertyChanged; + if (newValue != null) newValue.PropertyChanged += DeviceCommissioning_PropertyChanged; + } + + partial void OnObjectsChanged(ObjectDictionary? oldValue, ObjectDictionary newValue) + { + if (oldValue != null) oldValue.CollectionChanged -= Objects_CollectionChanged; + if (newValue != null) newValue.CollectionChanged += Objects_CollectionChanged; + Attach1018Listeners(); + } + + private bool _isHandlingMutex = false; + private bool _isSyncingIdentity = false; + + public event System.EventHandler? AutoSaveRequested; + + private void CheckAndRequestAutoSave() + { + this.IsDirty = true; + if (!DeviceInfo.HasErrors && !DeviceCommissioning.HasErrors) + { + AutoSaveRequested?.Invoke(this, System.EventArgs.Empty); + } + } + + private void DeviceInfo_PropertyChanged(object? sender, PropertyChangedEventArgs e) + { + CheckAndRequestAutoSave(); + if (_isHandlingMutex || _isSyncingIdentity) return; + + if (e.PropertyName == nameof(DeviceInfo.LssSlave)) + { + if (DeviceInfo.LssSlave && DeviceCommissioning.NodeId.HasValue) + { + CheckMutexAndPromptAsync("str_mutex_lss_nodeid", + onConfirm: () => { DeviceCommissioning.NodeId = null; }, + onCancel: () => { DeviceInfo.LssSlave = false; } + ); + } + if (DeviceInfo.LssSlave && string.IsNullOrWhiteSpace(DeviceCommissioning.LssSerialNo)) + { + SyncFrom1018ToDevice(); + } + } + else if (e.PropertyName == nameof(DeviceInfo.NodeGuardingSlave)) + { + if (DeviceInfo.NodeGuardingSlave && Objects.ContainsKey("1017")) + { + CheckMutexAndPromptAsync("str_mutex_ngs_1017", + onConfirm: () => { Objects.Remove("1017"); }, + onCancel: () => { DeviceInfo.NodeGuardingSlave = false; } + ); + } + } + else if (e.PropertyName == nameof(DeviceInfo.VendorNumber) || + e.PropertyName == nameof(DeviceInfo.ProductNumber) || + e.PropertyName == nameof(DeviceInfo.RevisionNumber)) + { + SyncFromDeviceTo1018(); + } + } + + private void DeviceCommissioning_PropertyChanged(object? sender, PropertyChangedEventArgs e) + { + CheckAndRequestAutoSave(); + if (_isHandlingMutex || _isSyncingIdentity) return; + + if (e.PropertyName == nameof(DeviceCommissioning.NodeId)) + { + if (DeviceCommissioning.NodeId.HasValue && DeviceInfo.LssSlave) + { + CheckMutexAndPromptAsync("str_mutex_nodeid_lss", + onConfirm: () => { DeviceInfo.LssSlave = false; }, + onCancel: () => { DeviceCommissioning.NodeId = null; } + ); + } + } + else if (e.PropertyName == nameof(DeviceCommissioning.LssSerialNo)) + { + SyncFromDeviceTo1018(); + } + } + + private void Objects_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + CheckAndRequestAutoSave(); + + if (_isHandlingMutex) return; + + if (DeviceInfo.NodeGuardingSlave && (Objects.ContainsKey("1017") || Objects.ContainsKey("0x1017"))) + { + CheckMutexAndPromptAsync("str_mutex_1017_ngs", + onConfirm: () => { DeviceInfo.NodeGuardingSlave = false; }, + onCancel: () => { + Objects.Remove("1017"); + Objects.Remove("0x1017"); + } + ); + } + + if (TryGet1018Object(out _)) + { + Attach1018Listeners(); + SyncFrom1018ToDevice(); + } + } + + public void Attach1018Listeners() + { + if (Objects == null) return; + + if (TryGet1018Object(out var obj1018)) + { + obj1018.SubObjects.CollectionChanged -= SubObjects1018_CollectionChanged; + obj1018.SubObjects.CollectionChanged += SubObjects1018_CollectionChanged; + + foreach (var kvp in obj1018.SubObjects) + { + if (kvp.Value != null) + { + kvp.Value.PropertyChanged -= SubObject1018_PropertyChanged; + kvp.Value.PropertyChanged += SubObject1018_PropertyChanged; + } + } + } + } + + public bool TryGet1018Object(out OdObject obj1018) + { + obj1018 = null!; + if (Objects == null) return false; + if (Objects.TryGetValue("1018", out obj1018!)) return true; + if (Objects.TryGetValue("0x1018", out obj1018!)) return true; + if (Objects.TryGetValue("1018h", out obj1018!)) return true; + var kvp = Objects.FirstOrDefault(x => x.Key.TrimStart('0', 'x', 'X').Equals("1018", System.StringComparison.OrdinalIgnoreCase)); + if (kvp.Value != null) + { + obj1018 = kvp.Value; + return true; + } + return false; + } + + private void SubObjects1018_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + if (e.NewItems != null) + { + foreach (KeyValuePair kvp in e.NewItems) + { + if (kvp.Value != null) + { + kvp.Value.PropertyChanged -= SubObject1018_PropertyChanged; + kvp.Value.PropertyChanged += SubObject1018_PropertyChanged; + } + } + } + SyncFrom1018ToDevice(); + } + + private void SubObject1018_PropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (_isSyncingIdentity) return; + if (e.PropertyName == nameof(OdSubObject.DefaultValue)) + { + SyncFrom1018ToDevice(smartMerge: false); + } + } + + public void SyncFrom1018ToDevice(bool smartMerge = true) + { + if (_isSyncingIdentity) return; + if (!TryGet1018Object(out var obj1018)) return; + + _isSyncingIdentity = true; + try + { + foreach (var sub in obj1018.SubObjects) + { + if (sub.Value == null) continue; + string rawKey = sub.Key.Trim().TrimStart('0', 'x', 'X'); + if (string.IsNullOrEmpty(rawKey)) rawKey = "0"; + + if (rawKey == "1") // Vendor-ID + { + if (smartMerge && !string.IsNullOrWhiteSpace(DeviceInfo.VendorNumber) && DeviceInfo.VendorNumber != sub.Value.DefaultValue) + { + sub.Value.DefaultValue = DeviceInfo.VendorNumber; + } + else if (DeviceInfo.VendorNumber != sub.Value.DefaultValue) + { + DeviceInfo.VendorNumber = sub.Value.DefaultValue; + } + } + else if (rawKey == "2") // Product Code + { + if (smartMerge && !string.IsNullOrWhiteSpace(DeviceInfo.ProductNumber) && DeviceInfo.ProductNumber != sub.Value.DefaultValue) + { + sub.Value.DefaultValue = DeviceInfo.ProductNumber; + } + else if (DeviceInfo.ProductNumber != sub.Value.DefaultValue) + { + DeviceInfo.ProductNumber = sub.Value.DefaultValue; + } + } + else if (rawKey == "3") // Revision Number + { + if (smartMerge && !string.IsNullOrWhiteSpace(DeviceInfo.RevisionNumber) && DeviceInfo.RevisionNumber != "1" && DeviceInfo.RevisionNumber != sub.Value.DefaultValue) + { + sub.Value.DefaultValue = DeviceInfo.RevisionNumber; + } + else if (DeviceInfo.RevisionNumber != sub.Value.DefaultValue) + { + DeviceInfo.RevisionNumber = sub.Value.DefaultValue; + } + } + else if (rawKey == "4") // Serial Number + { + if (smartMerge && !string.IsNullOrWhiteSpace(DeviceCommissioning.LssSerialNo) && DeviceCommissioning.LssSerialNo != sub.Value.DefaultValue) + { + sub.Value.DefaultValue = DeviceCommissioning.LssSerialNo; + } + else if (DeviceCommissioning.LssSerialNo != sub.Value.DefaultValue) + { + DeviceCommissioning.LssSerialNo = sub.Value.DefaultValue; + } + } + } + } + finally + { + _isSyncingIdentity = false; + } + } + + public void SyncFromDeviceTo1018() + { + if (_isSyncingIdentity) return; + if (!TryGet1018Object(out var obj1018)) return; + + _isSyncingIdentity = true; + try + { + foreach (var sub in obj1018.SubObjects) + { + if (sub.Value == null) continue; + string rawKey = sub.Key.Trim().TrimStart('0', 'x', 'X'); + if (string.IsNullOrEmpty(rawKey)) rawKey = "0"; + + if (rawKey == "1") + { + if (sub.Value.DefaultValue != DeviceInfo.VendorNumber) + { + sub.Value.DefaultValue = DeviceInfo.VendorNumber; + } + } + else if (rawKey == "2") + { + if (sub.Value.DefaultValue != DeviceInfo.ProductNumber) + { + sub.Value.DefaultValue = DeviceInfo.ProductNumber; + } + } + else if (rawKey == "3") + { + if (sub.Value.DefaultValue != DeviceInfo.RevisionNumber) + { + sub.Value.DefaultValue = DeviceInfo.RevisionNumber; + } + } + else if (rawKey == "4") + { + if (sub.Value.DefaultValue != DeviceCommissioning.LssSerialNo) + { + sub.Value.DefaultValue = DeviceCommissioning.LssSerialNo; + } + } + } + } + finally + { + _isSyncingIdentity = false; + } + } + + private static bool TryParseUInt32(string? input, out uint result) + { + result = 0; + if (string.IsNullOrWhiteSpace(input)) return false; + input = input.Trim(); + if (input.StartsWith("0x", System.StringComparison.OrdinalIgnoreCase)) + { + return uint.TryParse(input.Substring(2), System.Globalization.NumberStyles.HexNumber, System.Globalization.CultureInfo.InvariantCulture, out result); + } + return uint.TryParse(input, System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, out result); + } + + private void CheckMutexAndPromptAsync(string resourceKey, System.Action onConfirm, System.Action onCancel) + { + Dispatcher.UIThread.Post(async () => + { + _isHandlingMutex = true; + try + { + var app = Avalonia.Application.Current; + var mainWindow = (app?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow; + if (mainWindow != null && mainWindow.FindResource("MutexConfirmDialog") is Avalonia.Controls.Control dialog) + { + var msg = app?.FindResource(resourceKey)?.ToString(); + dialog.DataContext = msg; + var result = await DialogHostAvalonia.DialogHost.Show(dialog, "RootDialogHost"); + if (result as string == "OK") + { + onConfirm?.Invoke(); + } + else + { + onCancel?.Invoke(); + } + } + else + { + onCancel?.Invoke(); + } + } + finally + { + _isHandlingMutex = false; + } + }); + } + + [ObservableProperty] + private bool _isDirty; + public override string ToString() { if (DeviceInfo == null) @@ -20,9 +398,61 @@ public override string ToString() } } + private libEDSsharp.EDSsharp _eds = null!; + public libEDSsharp.EDSsharp Eds + { + get => _eds; + set + { + _eds = value; + TxPdo = new DevicePDOViewModel(_eds, true); + RxPdo = new DevicePDOViewModel(_eds, false); + + if (_eds != null) + { + ProjectInfo.ProjectFile = System.IO.Path.GetFileName(_eds.projectFilename); + if (!string.IsNullOrEmpty(_eds.xddfilename_1_1)) + ProjectInfo.ProjectFileVersion = "v1.1"; + else if (!string.IsNullOrEmpty(_eds.xddfilename_1_0) && _eds.xddfilename_1_0 == _eds.projectFilename) + ProjectInfo.ProjectFileVersion = "v1.0"; + else + ProjectInfo.ProjectFileVersion = ""; + + ProjectInfo.XddFileStripped = System.IO.Path.GetFileName(_eds.xddfilenameStripped); + ProjectInfo.EdsFile = System.IO.Path.GetFileName(_eds.edsfilename); + ProjectInfo.DcfFile = System.IO.Path.GetFileName(_eds.dcffilename); + ProjectInfo.CanopenNodeFile = System.IO.Path.GetFileName(_eds.ODfilename); + ProjectInfo.CanopenNodeFileVersion = _eds.ODfileVersion; + ProjectInfo.DocumentationFile = System.IO.Path.GetFileName(_eds.mdfilename); + + DeviceInfo.RpdoCount = _eds.di.NrOfRXPDO.ToString(); + DeviceInfo.TpdoCount = _eds.di.NrOfTXPDO.ToString(); + DeviceInfo.NodeGuardingSlave = _eds.di.NG_Slave; + DeviceInfo.NodeGuardingMaster = _eds.di.NG_Master; + DeviceInfo.NumberOfMonitoredNodes = _eds.di.NrOfNG_MonitoredNodes.ToString(); + + DeviceCommissioning.NetNumber = _eds.dc.NetNumber == 0 ? null : _eds.dc.NetNumber; + DeviceCommissioning.NetName = _eds.dc.NetworkName; + DeviceCommissioning.CanopenManager = _eds.dc.CANopenManager; + DeviceCommissioning.LssSerialNo = _eds.dc.LSS_SerialNumber == 0 ? string.Empty : $"0x{_eds.dc.LSS_SerialNumber:X8}"; + Attach1018Listeners(); + SyncFrom1018ToDevice(); + } + } + } + + [ObservableProperty] + private DevicePDOViewModel _txPdo = null!; + + [ObservableProperty] + private DevicePDOViewModel _rxPdo = null!; + [ObservableProperty] private FileInfo _fileInfo = new(); + [ObservableProperty] + private ProjectInfo _projectInfo = new(); + [ObservableProperty] private DeviceInfo _deviceInfo = new(); @@ -32,6 +462,159 @@ public override string ToString() [ObservableProperty] private ObjectDictionary _objects = new(); + [ObservableProperty] + private bool _isSyncing; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsObjectDictionaryTabSelected))] + private int _selectedTabIndex = 0; + + partial void OnSelectedTabIndexChanged(int value) + { + if (value == 2 || value == 3) + { + SyncAvailableObjects(); + } + else if (value == 1) + { + SyncODFromEDS(); + } + } + + private void SyncODFromEDS() + { + if (_eds == null) return; + IsSyncing = true; + try + { + var proto = libEDSsharp.MappingEDS.MapToProtobuffer(_eds); + var deviceView = Mapper.ProtobufferViewModelMapper.MapFromProtobuffer(proto); + + foreach (var kvp in deviceView.Objects) + { + if (ushort.TryParse(kvp.Key.Replace("0x", ""), System.Globalization.NumberStyles.HexNumber, null, out ushort index)) + { + if (index >= 0x1400 && index <= 0x1BFF) + { + if (_objects.ContainsKey(kvp.Key)) + { + foreach (var sub in kvp.Value.SubObjects) + { + var matchingSub = _objects[kvp.Key].SubObjects.FirstOrDefault(s => s.Key == sub.Key); + if (matchingSub.Value != null) + { + matchingSub.Value.DefaultValue = sub.Value.DefaultValue; + } + } + } + } + } + } + } + finally + { + IsSyncing = false; + } + } + + private void SyncAvailableObjects() + { + if (_eds == null) return; + IsSyncing = true; + try + { + var updatedEds = GetUpdatedEds(); + + var keysToRemove = new System.Collections.Generic.List(); + foreach(var k in _eds.ods.Keys) + { + if ((k < 0x1400 || k > 0x1BFF) && !updatedEds.ods.ContainsKey(k)) + { + keysToRemove.Add(k); + } + } + foreach (var k in keysToRemove) + { + _eds.ods.Remove(k); + } + + foreach (var kvp in updatedEds.ods) + { + if (kvp.Key >= 0x1400 && kvp.Key <= 0x1BFF) + { + if (!_eds.ods.ContainsKey(kvp.Key)) + { + _eds.ods.Add(kvp.Key, kvp.Value); + } + } + else + { + _eds.ods[kvp.Key] = kvp.Value; + } + } + + _eds.dc = updatedEds.dc; + + TxPdo?.RefreshSlots(); + RxPdo?.RefreshSlots(); + } + finally + { + IsSyncing = false; + } + + TxPdo?.UpdateAvailableObjects(); + RxPdo?.UpdateAvailableObjects(); + } + + public bool IsObjectDictionaryTabSelected => SelectedTabIndex == 1; + + [ObservableProperty] + private ModuleViewModel _moduleInfo = new(); + + public libEDSsharp.EDSsharp GetUpdatedEds() + { + var proto = Mapper.ProtobufferViewModelMapper.MapToProtobuffer(this); + var updatedEds = libEDSsharp.MappingEDS.MapFromProtobuffer(proto); + + updatedEds.projectFilename = this.ProjectInfo.ProjectFile; + updatedEds.xddfilenameStripped = this.ProjectInfo.XddFileStripped; + updatedEds.edsfilename = this.ProjectInfo.EdsFile; + updatedEds.dcffilename = this.ProjectInfo.DcfFile; + updatedEds.ODfilename = this.ProjectInfo.CanopenNodeFile; + updatedEds.ODfileVersion = this.ProjectInfo.CanopenNodeFileVersion; + updatedEds.mdfilename = this.ProjectInfo.DocumentationFile; + + if (updatedEds.di == null) updatedEds.di = new libEDSsharp.DeviceInfo(); + updatedEds.di.NrOfRXPDO = ushort.TryParse(this.DeviceInfo.RpdoCount, out var r) ? r : (ushort)0; + updatedEds.di.NrOfTXPDO = ushort.TryParse(this.DeviceInfo.TpdoCount, out var t) ? t : (ushort)0; + updatedEds.di.NG_Slave = this.DeviceInfo.NodeGuardingSlave; + updatedEds.di.NG_Master = this.DeviceInfo.NodeGuardingMaster; + updatedEds.di.NrOfNG_MonitoredNodes = ushort.TryParse(this.DeviceInfo.NumberOfMonitoredNodes, out var n) ? n : (ushort)0; + + if (updatedEds.dc == null) updatedEds.dc = new libEDSsharp.DeviceCommissioning(); + updatedEds.dc.NodeID = (byte)(this.DeviceCommissioning.NodeId ?? 0); + updatedEds.dc.NodeName = this.DeviceCommissioning.NodeName; + updatedEds.dc.Baudrate = (ushort)(this.DeviceCommissioning.Baudrate ?? 0); + updatedEds.dc.NetNumber = this.DeviceCommissioning.NetNumber ?? 0; + updatedEds.dc.NetworkName = this.DeviceCommissioning.NetName; + updatedEds.dc.CANopenManager = this.DeviceCommissioning.CanopenManager; + updatedEds.dc.LSS_SerialNumber = TryParseUInt32(this.DeviceCommissioning.LssSerialNo, out var lssVal) ? lssVal : 0; + + if (_eds != null && _eds.ods != null) + { + foreach (var kvp in _eds.ods) + { + if (kvp.Key >= 0x1400 && kvp.Key <= 0x1BFF) + { + updatedEds.ods[kvp.Key] = kvp.Value; + } + } + } + + return updatedEds; + } + public void OnClickCommand() { // do something diff --git a/EDSEditorGUI2/ViewModels/DeviceCommissioning.cs b/EDSEditorGUI2/ViewModels/DeviceCommissioning.cs index aee74b5d..00cbc3ee 100644 --- a/EDSEditorGUI2/ViewModels/DeviceCommissioning.cs +++ b/EDSEditorGUI2/ViewModels/DeviceCommissioning.cs @@ -1,16 +1,34 @@ -using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.ComponentModel; using System; +using System.ComponentModel.DataAnnotations; + namespace EDSEditorGUI2.ViewModels; -public partial class DeviceCommissioning : ObservableObject +public partial class DeviceCommissioning : ObservableValidator { [ObservableProperty] - private UInt32 _nodeId; + [Range(1, 127, ErrorMessage = "节点ID必须在1到127之间")] + private UInt32? _nodeId; [ObservableProperty] + [Required(ErrorMessage = "节点名称不能为空")] private string _nodeName = string.Empty; [ObservableProperty] - private UInt32 _baudrate; + private UInt32? _baudrate; + + [ObservableProperty] + private UInt32? _netNumber; + + [ObservableProperty] + [Required(ErrorMessage = "网络名称不能为空")] + private string _netName = string.Empty; + + [ObservableProperty] + private bool _canopenManager; + + [ObservableProperty] + [RegularExpression(@"^(0[xX])?[0-9a-fA-F]+$|^\d+$", ErrorMessage = "必须是有效的数字或十六进制")] + private string _lssSerialNo = string.Empty; } diff --git a/EDSEditorGUI2/ViewModels/DeviceInfo.cs b/EDSEditorGUI2/ViewModels/DeviceInfo.cs index c9111e83..a6b7679a 100644 --- a/EDSEditorGUI2/ViewModels/DeviceInfo.cs +++ b/EDSEditorGUI2/ViewModels/DeviceInfo.cs @@ -1,15 +1,40 @@ -using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.ComponentModel; + +using System.ComponentModel.DataAnnotations; namespace EDSEditorGUI2.ViewModels; -public partial class DeviceInfo : ObservableObject +public partial class DeviceInfo : ObservableValidator { [ObservableProperty] + [Required(ErrorMessage = "供应商名称不能为空")] string _vendorName = string.Empty; [ObservableProperty] + [Required(ErrorMessage = "供应商编号不能为空")] + [RegularExpression(@"^(0[xX])?[0-9a-fA-F]+$|^\d+$", ErrorMessage = "必须是有效的数字或十六进制")] + string _vendorNumber = string.Empty; + + [ObservableProperty] + [Required(ErrorMessage = "产品名称不能为空")] string _productName = string.Empty; + [ObservableProperty] + [Required(ErrorMessage = "产品编号不能为空")] + [RegularExpression(@"^(0[xX])?[0-9a-fA-F]+$|^\d+$", ErrorMessage = "必须是有效的数字或十六进制")] + string _productNumber = string.Empty; + + [ObservableProperty] + [Required(ErrorMessage = "修订版本号不能为空")] + [RegularExpression(@"^(0[xX])?[0-9a-fA-F]+$|^\d+$", ErrorMessage = "必须是有效的数字或十六进制")] + string _revisionNumber = "1"; + + [ObservableProperty] + [NotifyDataErrorInfo] + [Required(ErrorMessage = "请输入有效的数字")] + [RegularExpression(@"^\d+$", ErrorMessage = "请输入有效的数字")] + string _granularity = "8"; + [ObservableProperty] bool _baudRate10; @@ -42,4 +67,28 @@ public partial class DeviceInfo : ObservableObject [ObservableProperty] bool _lssMaster; + + [ObservableProperty] + [NotifyDataErrorInfo] + [Required(ErrorMessage = "请输入有效的数字")] + [RegularExpression(@"^\d+$", ErrorMessage = "请输入有效的数字")] + string _rpdoCount = "0"; + + [ObservableProperty] + [NotifyDataErrorInfo] + [Required(ErrorMessage = "请输入有效的数字")] + [RegularExpression(@"^\d+$", ErrorMessage = "请输入有效的数字")] + string _tpdoCount = "0"; + + [ObservableProperty] + bool _nodeGuardingSlave; + + [ObservableProperty] + bool _nodeGuardingMaster; + + [ObservableProperty] + [NotifyDataErrorInfo] + [Required(ErrorMessage = "请输入有效的数字")] + [RegularExpression(@"^(?:[0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-7])$", ErrorMessage = "监控节点数必须在0到127之间")] + string _numberOfMonitoredNodes = "0"; } diff --git a/EDSEditorGUI2/ViewModels/DevicePDOViewModel.cs b/EDSEditorGUI2/ViewModels/DevicePDOViewModel.cs new file mode 100644 index 00000000..620b342c --- /dev/null +++ b/EDSEditorGUI2/ViewModels/DevicePDOViewModel.cs @@ -0,0 +1,304 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using libEDSsharp; +using System; +using System.Collections.ObjectModel; +using System.Linq; + +namespace EDSEditorGUI2.ViewModels +{ + public partial class DevicePDOViewModel : ObservableObject + { + public class AvailableObjectViewModel + { + public ODentry Entry { get; } + public AvailableObjectViewModel(ODentry entry, ushort subIndex) + { + Entry = entry; + IndexString = "0x" + entry.Index.ToString("X4"); + SubIndexString = "0x" + subIndex.ToString("X2"); + Name = entry.parameter_name; + ObjectTypeString = entry.datatype.ToString(); + TypeSize = entry.Sizeofdatatype().ToString(); + } + + public string IndexString { get; } + public string SubIndexString { get; } + public string Name { get; } + public string ObjectTypeString { get; } + public string TypeSize { get; } + } + + private EDSsharp _eds = null!; + private PDOHelper _helper = null!; + private bool _isTx; + + public DevicePDOViewModel(EDSsharp eds, bool isTx) + { + _eds = eds; + _isTx = isTx; + + if (_eds != null) + { + _helper = new PDOHelper(_eds); + _helper.build_PDOlists(); + UpdatePDOList(); + UpdateAvailableObjects(); + } + } + + public partial class PDOSlotViewModel : ObservableObject + { + public PDOSlot Slot { get; } + public PDOSlotViewModel(PDOSlot slot) + { + Slot = slot; + } + public string Name => (Slot.ConfigurationIndex >= 0x1800 ? "TPDO " : "RPDO ") + (Slot.ConfigurationIndex >= 0x1800 ? (Slot.ConfigurationIndex - 0x1800 + 1) : (Slot.ConfigurationIndex - 0x1400 + 1)); + public string DescriptionComm => Slot.DescriptionComm; + + public string Communication + { + get => "0x" + Slot.ConfigurationIndex.ToString("X4"); + } + + public string Mapping + { + get => "0x" + Slot.MappingIndex.ToString("X4"); + } + + public string COB + { + get => "0x" + Slot.COB.ToString("X"); + set { if (uint.TryParse(value.Replace("0x", ""), System.Globalization.NumberStyles.HexNumber, null, out uint val)) { Slot.COB = val; OnPropertyChanged(); OnPropertyChanged(nameof(Invalid)); } } + } + + public string TransmissionType + { + get => Slot.transmissiontype.ToString(); + set { if (byte.TryParse(value, out byte val)) { Slot.transmissiontype = val; OnPropertyChanged(); OnPropertyChanged(nameof(IsSyncStartEnabled)); } } + } + + public bool IsSyncStartEnabled => Slot.transmissiontype != 254 && Slot.transmissiontype != 255; + + public string Inhibit + { + get => Slot.inhibit.ToString(); + set { if (ushort.TryParse(value, out ushort val)) { Slot.inhibit = val; OnPropertyChanged(); } } + } + + public string EventTimer + { + get => Slot.eventtimer.ToString(); + set { if (ushort.TryParse(value, out ushort val)) { Slot.eventtimer = val; OnPropertyChanged(); } } + } + + public string SyncStart + { + get => Slot.syncstart.ToString(); + set { if (byte.TryParse(value, out byte val)) { Slot.syncstart = val; OnPropertyChanged(); } } + } + + public bool Invalid + { + get => Slot.invalid; + set { Slot.invalid = value; OnPropertyChanged(); OnPropertyChanged(nameof(COB)); } + } + } + + [ObservableProperty] + private ObservableCollection _slots = new(); + + private PDOSlotViewModel _selectedSlot = null!; + public PDOSlotViewModel SelectedSlot + { + get => _selectedSlot; + set + { + if (SetProperty(ref _selectedSlot, value)) + { + UpdateMappings(); + OnPropertyChanged(nameof(IsSlotSelected)); + } + } + } + + public bool IsSlotSelected => SelectedSlot != null; + + [ObservableProperty] + private ObservableCollection _availableObjects = new(); + + public class MappingEntryViewModel + { + public PDOMappingEntry Entry { get; } + public MappingEntryViewModel(PDOMappingEntry entry) + { + Entry = entry; + if (entry.entry != null) + { + IndexString = "0x" + entry.entry.Index.ToString("X4"); + SubIndexString = "0x" + (entry.entry.parent != null ? entry.entry.parent.subobjects.FirstOrDefault(x => x.Value == entry.entry).Key.ToString("X2") : "00"); + Name = entry.entry.parameter_name; + Width = entry.width.ToString(); + } + else + { + IndexString = ""; + SubIndexString = ""; + Name = "Empty"; + Width = ""; + } + } + public string IndexString { get; } + public string SubIndexString { get; } + public string Name { get; } + public string Width { get; } + public int BitWidth => Entry.width; + } + + [ObservableProperty] + private ObservableCollection _mappings = new(); + + private void UpdateMappings() + { + Mappings.Clear(); + if (SelectedSlot != null && SelectedSlot.Slot != null && SelectedSlot.Slot.Mapping != null) + { + foreach (var mapping in SelectedSlot.Slot.Mapping) + { + Mappings.Add(new MappingEntryViewModel(mapping)); + } + } + } + + public void InsertMapping(PDOSlotViewModel slotViewModel, int ordinal, AvailableObjectViewModel item) + { + if (slotViewModel == null || slotViewModel.Slot == null || item == null) return; + + slotViewModel.Slot.insertMapping(ordinal, new PDOMappingEntry(item.Entry, item.Entry.Sizeofdatatype())); + _helper.buildmappingsfromlists(false); + + UpdateMappings(); + // Fire CollectionChanged to trigger redraw + OnPropertyChanged(nameof(Mappings)); + // Trigger property changed on SelectedSlot so the View refreshes bindings + OnPropertyChanged(nameof(SelectedSlot)); + } + + public void RemoveMapping(PDOSlotViewModel slotViewModel, PDOMappingEntry entryToRemove) + { + if (slotViewModel == null || slotViewModel.Slot == null) return; + + slotViewModel.Slot.Mapping.Remove(entryToRemove); + _helper.buildmappingsfromlists(false); + + UpdateMappings(); + OnPropertyChanged(nameof(Mappings)); + OnPropertyChanged(nameof(SelectedSlot)); + } + + public void InsertDummyMapping(PDOSlotViewModel slotViewModel, int ordinal) + { + if (slotViewModel == null || slotViewModel.Slot == null) return; + + int width_limit = 64; + foreach (var m in slotViewModel.Slot.Mapping) width_limit -= m.width; + if (width_limit <= 0) return; + + libEDSsharp.PDOMappingEntry od = new libEDSsharp.PDOMappingEntry(); + od.entry = _eds.dummy_ods[0x002]; + od.width = System.Math.Min(od.entry.Sizeofdatatype(), width_limit); + + slotViewModel.Slot.Mapping.Insert(ordinal, od); + _helper.buildmappingsfromlists(false); + + UpdateMappings(); + OnPropertyChanged(nameof(Mappings)); + OnPropertyChanged(nameof(SelectedSlot)); + } + + public void UpdatePDOList() + { + Slots.Clear(); + if (_helper == null) return; + foreach (var slot in _helper.pdoslots.Where(s => s.isTXPDO() == _isTx)) + { + var vm = new PDOSlotViewModel(slot); + vm.PropertyChanged += (sender, args) => + { + _helper.buildmappingsfromlists(false); + }; + Slots.Add(vm); + } + if (Slots.Count > 0) + { + SelectedSlot = Slots[0]; + } + else + { + SelectedSlot = null!; + } + } + + public void RefreshSlots() + { + if (_helper == null) return; + _helper.build_PDOlists(); + UpdatePDOList(); + } + + + public void UpdateAvailableObjects() + { + AvailableObjects.Clear(); + if (_eds == null) return; + foreach (var kvp in _eds.ods) + { + var od = kvp.Value; + if (od.prop.CO_disabled == true) continue; + + if (od.objecttype == ObjectType.VAR && (od.PDOtype == PDOMappingType.optional || (_isTx && (od.PDOtype == PDOMappingType.TPDO)) || (!_isTx && (od.PDOtype == PDOMappingType.RPDO)))) + { + AvailableObjects.Add(new AvailableObjectViewModel(od, 0)); + } + + foreach (var kvp2 in od.subobjects) + { + var odsub = kvp2.Value; + if (kvp2.Key == 0) continue; + + if (odsub.PDOtype == PDOMappingType.optional || (_isTx && (odsub.PDOtype == PDOMappingType.TPDO)) || (!_isTx && (odsub.PDOtype == PDOMappingType.RPDO))) + { + AvailableObjects.Add(new AvailableObjectViewModel(odsub, kvp2.Key)); + } + } + } + } + [RelayCommand] + public void AddPDO() + { + if (_helper == null) return; + var gap = _helper.findPDOslotgap(_isTx); + if (gap != 0) + { + _helper.addPDOslot(gap); + _helper.buildmappingsfromlists(false); + UpdatePDOList(); + var addedSlot = Slots.FirstOrDefault(s => s.Slot.ConfigurationIndex == gap); + if (addedSlot != null) + { + SelectedSlot = addedSlot; + } + } + } + + [RelayCommand] + public void RemovePDO() + { + if (_helper == null || SelectedSlot == null) return; + _helper.removePDOslot(SelectedSlot.Slot.ConfigurationIndex); + _helper.buildmappingsfromlists(false); + UpdatePDOList(); + } + } +} diff --git a/EDSEditorGUI2/ViewModels/FileInfo.cs b/EDSEditorGUI2/ViewModels/FileInfo.cs index 7d2f5596..4a73e4b5 100644 --- a/EDSEditorGUI2/ViewModels/FileInfo.cs +++ b/EDSEditorGUI2/ViewModels/FileInfo.cs @@ -1,12 +1,15 @@ -using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.ComponentModel; using System; +using System.ComponentModel.DataAnnotations; + namespace EDSEditorGUI2.ViewModels; -public partial class FileInfo : ObservableObject +public partial class FileInfo : ObservableValidator { [ObservableProperty] - private string _fileVersion = string.Empty; + [Required(ErrorMessage = "文件版本不能为空")] + string _fileVersion = "1.0"; [ObservableProperty] private string _description = string.Empty; @@ -15,11 +18,13 @@ public partial class FileInfo : ObservableObject private DateTime _creationTime; [ObservableProperty] - private string _createdBy = string.Empty; + [Required(ErrorMessage = "创建者不能为空")] + string _createdBy = string.Empty; [ObservableProperty] private DateTime _modificationTime; [ObservableProperty] - private string _modifiedBy = string.Empty; + [Required(ErrorMessage = "修改者不能为空")] + string _modifiedBy = string.Empty; } diff --git a/EDSEditorGUI2/ViewModels/MainWindowViewModel.cs b/EDSEditorGUI2/ViewModels/MainWindowViewModel.cs index 5e05163e..f4df0434 100644 --- a/EDSEditorGUI2/ViewModels/MainWindowViewModel.cs +++ b/EDSEditorGUI2/ViewModels/MainWindowViewModel.cs @@ -1,23 +1,172 @@ -using Avalonia.Media; +using Avalonia.Media; using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; using EDSEditorGUI2.Mapper; using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Linq; namespace EDSEditorGUI2.ViewModels; public partial class MainWindowViewModel : ViewModelBase { - int Counter = 0; - public void AddNewDevice(object sender) + int Counter = 1; + public bool HasNoDevice => Network.Count == 0; + + public ObservableCollection RecentFiles { get; } = new(); + + public bool HasRecentFiles => RecentFiles.Count > 0; + + public void LoadRecentFiles() + { + RecentFiles.Clear(); + foreach (var file in ConfigurationManager.Settings.RecentFiles) + { + RecentFiles.Add(file); + } + OnPropertyChanged(nameof(HasRecentFiles)); + } + + public void AddRecentFile(string path) + { + if (RecentFiles.Contains(path)) + { + RecentFiles.Remove(path); + } + RecentFiles.Insert(0, path); + if (RecentFiles.Count > 10) + { + RecentFiles.RemoveAt(RecentFiles.Count - 1); + } + ConfigurationManager.Settings.RecentFiles.Clear(); + ConfigurationManager.Settings.RecentFiles.AddRange(RecentFiles); + ConfigurationManager.Save(); + OnPropertyChanged(nameof(HasRecentFiles)); + } + + [RelayCommand] + public void RemoveRecentFile(string path) + { + if (RecentFiles.Contains(path)) + { + RecentFiles.Remove(path); + ConfigurationManager.Settings.RecentFiles.Clear(); + ConfigurationManager.Settings.RecentFiles.AddRange(RecentFiles); + ConfigurationManager.Save(); + OnPropertyChanged(nameof(HasRecentFiles)); + } + } + + [RelayCommand] + public void ClearRecentFiles() + { + RecentFiles.Clear(); + ConfigurationManager.Settings.RecentFiles.Clear(); + ConfigurationManager.Save(); + OnPropertyChanged(nameof(HasRecentFiles)); + } + + [ObservableProperty] + private string? _currentProjectPath; + + public string? CurrentProjectName + { + get + { + if (string.IsNullOrEmpty(CurrentProjectPath)) return null; + var name = System.IO.Path.GetFileNameWithoutExtension(CurrentProjectPath); + return IsDirty ? name + " *" : name; + } + } + + partial void OnCurrentProjectPathChanged(string? value) + { + OnPropertyChanged(nameof(CurrentProjectName)); + OnPropertyChanged(nameof(IsProjectMode)); + OnPropertyChanged(nameof(CanRemoveDevice)); + } + + public bool IsProjectMode + { + get + { + if (string.IsNullOrEmpty(CurrentProjectPath)) return Network.Count > 0; + string ext = System.IO.Path.GetExtension(CurrentProjectPath).ToLower(); + return ext == ".cpj" || ext == ".xpd" || ext == ".rpj"; // Assuming .rpj is a typo for cpj, but just in case + } + } + + public bool CanRemoveDevice => IsProjectMode && SelectedDevice != null; + + + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(WindowTitle))] + [NotifyPropertyChangedFor(nameof(CurrentProjectName))] + private bool _isDirty; + + public string WindowTitle => IsDirty ? "CANopen Editor 2 *" : "CANopen Editor 2"; + + [ObservableProperty] + private bool _isLoading; + + public MainWindowViewModel() + { + LoadRecentFiles(); + + Network.CollectionChanged += (s, e) => + { + OnPropertyChanged(nameof(HasNoDevice)); + OnPropertyChanged(nameof(HasDevice)); + OnPropertyChanged(nameof(IsProjectMode)); + OnPropertyChanged(nameof(CanRemoveDevice)); + + if (e.NewItems != null) + { + foreach (Device device in e.NewItems) + { + device.PropertyChanged += Device_PropertyChanged; + } + } + if (e.OldItems != null) + { + foreach (Device device in e.OldItems) + { + device.PropertyChanged -= Device_PropertyChanged; + } + } + }; + } + + private void Device_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(Device.IsDirty)) + { + if (sender is Device device && device.IsDirty) + { + this.IsDirty = true; + } + } + } + + public bool HasDevice => Network.Count > 0; + + public void AddNewDevice(object? sender = null) { var device = new LibCanOpen.CanOpenDevice { DeviceInfo = new() { - ProductName = "New Product" + Counter.ToString() + ProductName = "NewDevice" + Counter.ToString(), + Granularity = 8 }, + DeviceCommissioning = new(), + FileInfo = new() + { + CreationTime = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(DateTime.UtcNow), + ModificationTime = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(DateTime.UtcNow) + } }; Counter++; @@ -33,18 +182,49 @@ public void AddNewDevice(object sender) //device.Dock = DockStyle.Fill; //device.dispatch_updateOD(); - var DeviceView = ProtobufferViewModelMapper.MapFromProtobuffer(device); - Network.Add(DeviceView); + var deviceView = ProtobufferViewModelMapper.MapFromProtobuffer(device); + deviceView.Eds = new libEDSsharp.EDSsharp(); + Network.Add(deviceView); + SelectedDevice = deviceView; + } + + [RelayCommand] + public void CloseDevice(Device device) + { + if (device != null && Network.Contains(device)) + { + Network.Remove(device); + if (SelectedDevice == device) + { + SelectedDevice = Network.Count > 0 ? Network[0] : null; + } + } + } + + [RelayCommand] + public void CloseAllDevices() + { + Network.Clear(); + SelectedDevice = null; + CurrentProjectPath = null; + IsDirty = false; + } + + [RelayCommand] + public void NewProject() + { + Network.Clear(); + AddNewDevice(); } public void InitMergeStatus(Device profile, List offsets) { MergeStatus.Clear(); - if (SelectedDevice is not null) + if (SelectedDevice is not null && profile is not null && profile.Objects is not null) { foreach (var obj in profile.Objects) { - int mergeIndex = Int32.Parse(obj.Key); + int mergeIndex = Convert.ToInt32(obj.Key, 16); List objectOffset = []; foreach (var offset in offsets) { @@ -54,7 +234,7 @@ public void InitMergeStatus(Device profile, List offsets) ODIndexMergeStatus ms = new() { Insert = true, - OriginalObject = $"0x{mergeIndex:x} - {obj.Value.Name}", + OriginalObject = $"0x{mergeIndex:X4} - {obj.Value.Name}", Offsets = objectOffset, OriginalIndex = mergeIndex, #pragma warning disable MVVMTK0034 // Direct field reference to [ObservableProperty] backing field @@ -63,12 +243,37 @@ public void InitMergeStatus(Device profile, List offsets) TextBrush = new SolidColorBrush(Colors.Black), }; + ms.PropertyChanged += MergeStatus_PropertyChanged; + MergeStatus.Add(ms); } UpdateMergeStatus(offsets); } } + private void MergeStatus_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(ODIndexMergeStatus.Insert) && sender is ODIndexMergeStatus status) + { + int index = status.OriginalIndex; + int twinIndex = -1; + + if (index >= 0x1800 && index <= 0x18FF) twinIndex = index + 0x0200; + else if (index >= 0x1A00 && index <= 0x1AFF) twinIndex = index - 0x0200; + else if (index >= 0x1400 && index <= 0x14FF) twinIndex = index + 0x0200; + else if (index >= 0x1600 && index <= 0x16FF) twinIndex = index - 0x0200; + + if (twinIndex != -1) + { + var twin = MergeStatus.FirstOrDefault(x => x.OriginalIndex == twinIndex); + if (twin != null && twin.Insert != status.Insert) + { + twin.Insert = status.Insert; + } + } + } + } + /// /// Update profile merge status by checking for collisions /// @@ -132,6 +337,12 @@ public void UpdateMergeStatus(List offsets) } } } + + // Update IndexCollision status for the entire row + foreach (var row in MergeStatus) + { + row.IndexCollision = row.Offsets.Exists(o => o.Collision); + } } } #pragma warning disable CA1822 // Mark members as static @@ -145,6 +356,20 @@ public void UpdateMergeStatus(List offsets) [ObservableProperty] public int _insertObjectsOffset; + private Device? _selectedDevice; + public Device? SelectedDevice + { + get => _selectedDevice; + set + { + if (SetProperty(ref _selectedDevice, value)) + { + OnPropertyChanged(nameof(CanRemoveDevice)); + DisplayedDevice = value; + } + } + } + [ObservableProperty] - public Device? _selectedDevice; + private Device? _displayedDevice; } diff --git a/EDSEditorGUI2/ViewModels/ModuleItemViewModel.cs b/EDSEditorGUI2/ViewModels/ModuleItemViewModel.cs new file mode 100644 index 00000000..2d41f874 --- /dev/null +++ b/EDSEditorGUI2/ViewModels/ModuleItemViewModel.cs @@ -0,0 +1,42 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using System.Collections.ObjectModel; + +namespace EDSEditorGUI2.ViewModels; + +public partial class ModuleItemViewModel : ObservableObject +{ + [ObservableProperty] + private uint _index; + + [ObservableProperty] + private string _productName = string.Empty; + + [ObservableProperty] + private string _productVersion = string.Empty; + + [ObservableProperty] + private string _productRevision = string.Empty; + + [ObservableProperty] + private string _orderCode = string.Empty; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsConnectedBrush))] + private bool _isConnected; + + public Avalonia.Media.IBrush IsConnectedBrush => IsConnected ? Avalonia.Media.Brushes.MediumSeaGreen : Avalonia.Media.Brushes.Tomato; + + [ObservableProperty] + private string _comments = string.Empty; + + public ObservableCollection ExtendedObjects { get; } = new(); +} + +public partial class ModuleObjectViewModel : ObservableObject +{ + [ObservableProperty] + private string _indexHex = string.Empty; + + [ObservableProperty] + private string _name = string.Empty; +} diff --git a/EDSEditorGUI2/ViewModels/ModuleViewModel.cs b/EDSEditorGUI2/ViewModels/ModuleViewModel.cs new file mode 100644 index 00000000..2bd9fd57 --- /dev/null +++ b/EDSEditorGUI2/ViewModels/ModuleViewModel.cs @@ -0,0 +1,15 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using System.Collections.ObjectModel; + +namespace EDSEditorGUI2.ViewModels; + +public partial class ModuleViewModel : ObservableObject +{ + [ObservableProperty] + private uint _nrSupportedModules; + + public ObservableCollection Modules { get; } = new(); + + [ObservableProperty] + private ModuleItemViewModel? _selectedModule; +} diff --git a/EDSEditorGUI2/ViewModels/ObjectDictionary.cs b/EDSEditorGUI2/ViewModels/ObjectDictionary.cs index 560af730..32145fe2 100644 --- a/EDSEditorGUI2/ViewModels/ObjectDictionary.cs +++ b/EDSEditorGUI2/ViewModels/ObjectDictionary.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -55,7 +55,9 @@ public bool Remove(string key) public bool TryGetValue(string key, out OdObject value) { +#pragma warning disable CS8601 return Dictionary.TryGetValue(key, out value); +#pragma warning restore CS8601 } public ICollection Values @@ -152,7 +154,7 @@ public void AddRange(IDictionary items) { _Dictionary = new Dictionary(items); } - OnCollectionChanged(NotifyCollectionChangedAction.Add, items.ToArray()); + OnCollectionChanged(); } } @@ -171,13 +173,12 @@ private void Insert(string key, OdObject value, bool add) return; } Dictionary[key] = value; - - OnCollectionChanged(NotifyCollectionChangedAction.Replace, new KeyValuePair(key, value), new KeyValuePair(key, item)); + OnCollectionChanged(); } else { Dictionary[key] = value; - OnCollectionChanged(NotifyCollectionChangedAction.Add, new KeyValuePair(key, value)); + OnCollectionChanged(); } } @@ -226,6 +227,10 @@ private void OnCollectionChanged(NotifyCollectionChangedAction action, IList new public void AddIndex(int index, string name, LibCanOpen.OdObject.Types.ObjectType type) { var strIndex = index.ToString("X4"); + if (this.ContainsKey(strIndex)) + { + return; + } var newObj = new OdObject { Name = name, @@ -244,7 +249,7 @@ public void AddIndex(int index, string name, LibCanOpen.OdObject.Types.ObjectTyp Srdo = LibCanOpen.OdSubObject.Types.AccessSRDO.No, DefaultValue = "0" }; - newObj.SubObjects.Add(new KeyValuePair("0x0", newSub)); + newObj.SubObjects.Add(new KeyValuePair("00", newSub)); } else { @@ -266,9 +271,8 @@ public void AddIndex(int index, string name, LibCanOpen.OdObject.Types.ObjectTyp Srdo = LibCanOpen.OdSubObject.Types.AccessSRDO.No, DefaultValue = "0" }; - - newObj.SubObjects.Add(new KeyValuePair("0x0", CountSub)); - newObj.SubObjects.Add(new KeyValuePair("0x1", Sub1)); + newObj.SubObjects.Add(new KeyValuePair("00", CountSub)); + newObj.SubObjects.Add(new KeyValuePair("01", Sub1)); } Add(new KeyValuePair(strIndex, newObj)); } @@ -294,10 +298,30 @@ public KeyValuePair SelectedObject { this._selectedObject = value; NotifyPropertyChanged(); + NotifyPropertyChanged(nameof(ActiveName)); + NotifyPropertyChanged(nameof(ActiveAlias)); + NotifyPropertyChanged(nameof(SelectedObjectType)); + + if (value.Value != null && value.Value.ObjectType == LibCanOpen.OdObject.Types.ObjectType.Var && value.Value.SubObjects != null && value.Value.SubObjects.Count > 0) + { + SelectedSubObject = value.Value.SubObjects[0]; + } + else + { + SelectedSubObject = default; + } } } } + public LibCanOpen.OdObject.Types.ObjectType SelectedObjectType + { + get + { + return _selectedObject.Value?.ObjectType ?? LibCanOpen.OdObject.Types.ObjectType.Var; + } + } + public KeyValuePair SelectedSubObject { get @@ -311,6 +335,58 @@ public KeyValuePair SelectedSubObject { _selectedSubObject = value; NotifyPropertyChanged(); + NotifyPropertyChanged(nameof(ActiveName)); + NotifyPropertyChanged(nameof(ActiveAlias)); + } + } + } + + public string ActiveName + { + get + { + if (SelectedSubObject.Value != null) + return SelectedSubObject.Value.Name; + if (SelectedObject.Value != null) + return SelectedObject.Value.Name; + return string.Empty; + } + set + { + if (SelectedSubObject.Value != null) + { + SelectedSubObject.Value.Name = value; + NotifyPropertyChanged(); + } + else if (SelectedObject.Value != null) + { + SelectedObject.Value.Name = value; + NotifyPropertyChanged(); + } + } + } + + public string ActiveAlias + { + get + { + if (SelectedSubObject.Value != null) + return SelectedSubObject.Value.Alias; + if (SelectedObject.Value != null) + return SelectedObject.Value.Alias; + return string.Empty; + } + set + { + if (SelectedSubObject.Value != null) + { + SelectedSubObject.Value.Alias = value; + NotifyPropertyChanged(); + } + else if (SelectedObject.Value != null) + { + SelectedObject.Value.Alias = value; + NotifyPropertyChanged(); } } } diff --git a/EDSEditorGUI2/ViewModels/OdObject.cs b/EDSEditorGUI2/ViewModels/OdObject.cs index 01c83e8a..cd2c0ff3 100644 --- a/EDSEditorGUI2/ViewModels/OdObject.cs +++ b/EDSEditorGUI2/ViewModels/OdObject.cs @@ -1,4 +1,4 @@ -using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.ComponentModel; using libEDSsharp; using System; using System.Collections.Generic; @@ -23,12 +23,21 @@ public partial class OdObject : ObservableObject [ObservableProperty] LibCanOpen.OdObject.Types.ObjectType _objectType; + + public bool IsVar => ObjectType == LibCanOpen.OdObject.Types.ObjectType.Var; + public bool IsRecordOrArray => ObjectType == LibCanOpen.OdObject.Types.ObjectType.Record || ObjectType == LibCanOpen.OdObject.Types.ObjectType.Array; + + partial void OnObjectTypeChanged(LibCanOpen.OdObject.Types.ObjectType value) + { + OnPropertyChanged(nameof(IsVar)); + OnPropertyChanged(nameof(IsRecordOrArray)); + } [ObservableProperty] string _countLabel = string.Empty; [ObservableProperty] - string _storageGroup = string.Empty; + string _storageGroup = "RAM"; [ObservableProperty] bool flagsPDO; @@ -101,10 +110,15 @@ public partial class OdObject : ObservableObject if (sub.Key.ToInteger() > newSubIndex) newSubIndex = sub.Key.ToInteger(); - newSubObjects.Add(new KeyValuePair((newSubIndex++).ToHexString(), subOd)); + newSubObjects.Add(new KeyValuePair((newSubIndex++).ToString("X2"), subOd)); - if (selected.Value == subOd) - newSubObjects.Add(new KeyValuePair((newSubIndex++).ToHexString(), newOd)); + if (selected.Value != null && selected.Value == subOd) + newSubObjects.Add(new KeyValuePair((newSubIndex++).ToString("X2"), newOd)); + } + + if (selected.Value == null) + { + newSubObjects.Add(new KeyValuePair((newSubIndex++).ToString("X2"), newOd)); } SubObjects = newSubObjects; @@ -137,7 +151,7 @@ public bool RemoveSubEntry(KeyValuePair subObjectToRemove, ObservableCollection> newSubObjects = []; UInt16 subIndex = 0; foreach (var subOd in SubObjects) - newSubObjects.Add(new KeyValuePair((subIndex++).ToHexString(), subOd.Value)); + newSubObjects.Add(new KeyValuePair((subIndex++).ToString("X2"), subOd.Value)); SubObjects = newSubObjects; } diff --git a/EDSEditorGUI2/ViewModels/OdSubObject.cs b/EDSEditorGUI2/ViewModels/OdSubObject.cs index 1256593c..38688325 100644 --- a/EDSEditorGUI2/ViewModels/OdSubObject.cs +++ b/EDSEditorGUI2/ViewModels/OdSubObject.cs @@ -1,18 +1,21 @@ -using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.ComponentModel; using static LibCanOpen.OdSubObject.Types; +using System.ComponentModel.DataAnnotations; + namespace EDSEditorGUI2.ViewModels; -public partial class OdSubObject : ObservableObject +public partial class OdSubObject : ObservableValidator { [ObservableProperty] + [Required(ErrorMessage = "子对象名称不能为空")] private string _name = string.Empty; [ObservableProperty] private string _alias = string.Empty; [ObservableProperty] - private DataType _dataType; + private LibCanOpen.OdSubObject.Types.DataType _dataType; [ObservableProperty] private AccessSDO _sdo; diff --git a/EDSEditorGUI2/ViewModels/PreferencesViewModel.cs b/EDSEditorGUI2/ViewModels/PreferencesViewModel.cs new file mode 100644 index 00000000..bf222753 --- /dev/null +++ b/EDSEditorGUI2/ViewModels/PreferencesViewModel.cs @@ -0,0 +1,119 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using libEDSsharp; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace EDSEditorGUI2.ViewModels +{ + public partial class PreferencesViewModel : ObservableObject + { + [ObservableProperty] + private bool _genericWarning; + + [ObservableProperty] + private bool _renameWarning; + + [ObservableProperty] + private bool _buildWarning; + + [ObservableProperty] + private bool _stringWarning; + + [ObservableProperty] + private bool _structWarning; + + [ObservableProperty] + private string _selectedExporterString; + + [ObservableProperty] + private string _selectedLanguage; + + public class LanguageOption + { + public string Code { get; set; } + public string Display { get; set; } + public LanguageOption(string code, string display) { Code = code; Display = display; } + public override string ToString() => Display; + } + + public List AvailableLanguages { get; } = new() + { + new("en-US", "English"), + new("zh-CN", "中文") + }; + + [ObservableProperty] + private string _selectedTheme; + + private LanguageOption _selectedLanguageOption; + public LanguageOption SelectedLanguageOption + { + get => _selectedLanguageOption; + set + { + SetProperty(ref _selectedLanguageOption, value); + SelectedLanguage = value?.Code ?? "en-US"; + } + } + + public PreferencesViewModel() + { + UInt32 mask = Warnings.warning_mask; + GenericWarning = (mask & 0x01) == 0x01; + RenameWarning = (mask & 0x02) == 0x02; + BuildWarning = (mask & 0x04) == 0x04; + StringWarning = (mask & 0x08) == 0x08; + StructWarning = (mask & 0x10) == 0x10; + + SelectedExporterString = ConfigurationManager.Settings.CurrentExporter.ToString(); + SelectedLanguage = ConfigurationManager.Settings.CurrentLanguage; + _selectedLanguageOption = AvailableLanguages.FirstOrDefault(x => x.Code == SelectedLanguage) ?? AvailableLanguages[0]; + + SelectedTheme = ConfigurationManager.Settings.CurrentTheme ?? "Default"; + } + + [RelayCommand] + private void Save() + { + UInt32 mask = 0xFFE0; // preserve upper bits + + if (GenericWarning) mask |= 0x01; + if (RenameWarning) mask |= 0x02; + if (BuildWarning) mask |= 0x04; + if (StringWarning) mask |= 0x08; + if (StructWarning) mask |= 0x10; + + Warnings.warning_mask = mask; + if (Enum.TryParse(SelectedExporterString, out var parsedExporter)) + { + ConfigurationManager.Settings.CurrentExporter = parsedExporter; + } + else + { + ConfigurationManager.Settings.CurrentExporter = ExporterFactory.Exporter.CANOPENNODE_V4; + } + ConfigurationManager.Settings.CurrentLanguage = SelectedLanguage; + ConfigurationManager.Settings.CurrentTheme = SelectedTheme ?? "Default"; + + // Save to JSON + ConfigurationManager.Save(); + + // Apply language globally + App.ChangeLanguage(SelectedLanguage); + + // Apply theme globally + var app = Avalonia.Application.Current; + if (app != null) + { + app.RequestedThemeVariant = ConfigurationManager.Settings.CurrentTheme switch + { + "Light" => Avalonia.Styling.ThemeVariant.Light, + "Dark" => Avalonia.Styling.ThemeVariant.Dark, + _ => Avalonia.Styling.ThemeVariant.Default + }; + } + } + } +} diff --git a/EDSEditorGUI2/ViewModels/ProjectInfo.cs b/EDSEditorGUI2/ViewModels/ProjectInfo.cs new file mode 100644 index 00000000..46b0a0da --- /dev/null +++ b/EDSEditorGUI2/ViewModels/ProjectInfo.cs @@ -0,0 +1,30 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace EDSEditorGUI2.ViewModels; + +public partial class ProjectInfo : ObservableValidator +{ + [ObservableProperty] + private string _projectFile = string.Empty; + + [ObservableProperty] + private string _projectFileVersion = string.Empty; + + [ObservableProperty] + private string _xddFileStripped = string.Empty; + + [ObservableProperty] + private string _edsFile = string.Empty; + + [ObservableProperty] + private string _dcfFile = string.Empty; + + [ObservableProperty] + private string _canopenNodeFile = string.Empty; + + [ObservableProperty] + private string _canopenNodeFileVersion = string.Empty; + + [ObservableProperty] + private string _documentationFile = string.Empty; +} diff --git a/EDSEditorGUI2/Views/CollisionDialog.axaml b/EDSEditorGUI2/Views/CollisionDialog.axaml new file mode 100644 index 00000000..a8757649 --- /dev/null +++ b/EDSEditorGUI2/Views/CollisionDialog.axaml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/EDSEditorGUI2/Views/DeviceODView.axaml b/EDSEditorGUI2/Views/DeviceODView.axaml index e8a8ac54..6c7f580d 100644 --- a/EDSEditorGUI2/Views/DeviceODView.axaml +++ b/EDSEditorGUI2/Views/DeviceODView.axaml @@ -1,4 +1,4 @@ - + + + + + + - - - - - - - - - + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + - - + + + + + + + - - - - + + + - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + + + - - - - - - + + + + + - - - - - - + + + + + - - + + + - - - - Autosave - + + + + + + + + + - + diff --git a/EDSEditorGUI2/Views/DeviceODView.axaml.cs b/EDSEditorGUI2/Views/DeviceODView.axaml.cs index 491110cc..409f9ca6 100644 --- a/EDSEditorGUI2/Views/DeviceODView.axaml.cs +++ b/EDSEditorGUI2/Views/DeviceODView.axaml.cs @@ -49,36 +49,73 @@ private void IndexGridSelectionChanged(object? sender, SelectionChangedEventArgs { if (sender is DataGrid s && DataContext is ViewModels.ObjectDictionary dc) { - if (s.SelectedItem is KeyValuePair selected) + MainWindow? mw = TopLevel.GetTopLevel(this) as MainWindow; + if (mw != null) mw.IsProgrammaticChange = true; + try { - dc.SelectedObject = selected; - foreach (var dg in _odViews) + if (s.SelectedItem is KeyValuePair selected) { - if (dg != s) + dc.SelectedObject = selected; + foreach (var dg in _odViews) + { + if (dg != s) + { + dg.SelectedItem = null; + } + } + + if (selected.Value.SubObjects.Count > 0) + { + subindexGrid.SelectedItem = selected.Value.SubObjects[0]; + } + else { - dg.SelectedItem = null; subindexGrid.SelectedItem = null; } } } + finally + { + if (mw != null) mw.IsProgrammaticChange = false; + } } } + private void SubObjectDataGrid_PointerPressed(object? sender, Avalonia.Input.PointerPressedEventArgs e) + { + if (e.GetCurrentPoint(this).Properties.IsRightButtonPressed) + { + if (sender is DataGrid dg && e.Source is Avalonia.Controls.Control control && control.DataContext is KeyValuePair) + { + dg.SelectedItem = control.DataContext; + } + } + } + private void subindexGridSelectionChanged(object? sender, SelectionChangedEventArgs e) { if (sender is DataGrid s && DataContext is ViewModels.ObjectDictionary dc) { - if (s.SelectedItem is KeyValuePair selected) + MainWindow? mw = TopLevel.GetTopLevel(this) as MainWindow; + if (mw != null) mw.IsProgrammaticChange = true; + try { - dc.SelectedSubObject = selected; - dc.SelectedSubObjects.Clear(); - foreach (var row in s.SelectedItems) + if (s.SelectedItem is KeyValuePair selected) { - if (row is KeyValuePair subObj) + dc.SelectedSubObject = selected; + dc.SelectedSubObjects.Clear(); + foreach (var row in s.SelectedItems) { - dc.SelectedSubObjects.Add(subObj); + if (row is KeyValuePair subObj) + { + dc.SelectedSubObjects.Add(subObj); + } } } } + finally + { + if (mw != null) mw.IsProgrammaticChange = false; + } } } private void ContextMenuSubObjectAddClick(object? sender, RoutedEventArgs e) @@ -86,11 +123,37 @@ private void ContextMenuSubObjectAddClick(object? sender, RoutedEventArgs e) if (DataContext is ViewModels.ObjectDictionary dc) { var selectedObj = dc.SelectedObject.Value; - ObservableCollection> selection = []; + ViewModels.OdSubObject? lastAdded = null; + + var selectedRows = dc.SelectedSubObjects.ToList(); + if (selectedRows.Count == 0) + { + lastAdded = selectedObj.AddSubEntry(new KeyValuePair()); + } + else + { + foreach (var row in selectedRows) + { + lastAdded = selectedObj.AddSubEntry(row); + } + } + + if (lastAdded != null) + { + var kvpToSelect = selectedObj.SubObjects.FirstOrDefault(x => x.Value == lastAdded); + if (kvpToSelect.Value != null) + { + Avalonia.Threading.Dispatcher.UIThread.Post(() => + { + subindexGrid.SelectedItem = kvpToSelect; + subindexGrid.ScrollIntoView(kvpToSelect, null); + }); + } + } - foreach (var row in dc.SelectedSubObjects) + if (TopLevel.GetTopLevel(this) is MainWindow mainWindow) { - selectedObj.AddSubEntry(row); + mainWindow.TriggerAutoSave(); } } } @@ -108,6 +171,11 @@ private void ContextMenuSubObjectRemoveClick(object? sender, RoutedEventArgs e) { selectedObject.RemoveSubEntry(item, renumber); } + + if (TopLevel.GetTopLevel(this) is MainWindow mainWindow) + { + mainWindow.TriggerAutoSave(); + } } } } diff --git a/EDSEditorGUI2/Views/DevicePDOView.axaml b/EDSEditorGUI2/Views/DevicePDOView.axaml index 6a45cafe..c73ffbbc 100644 --- a/EDSEditorGUI2/Views/DevicePDOView.axaml +++ b/EDSEditorGUI2/Views/DevicePDOView.axaml @@ -5,74 +5,137 @@ xmlns:converter="using:EDSEditorGUI2.Converter" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - mc:Ignorable="d" d:DesignWidth="1000" d:DesignHeight="450" + xmlns:local="using:EDSEditorGUI2" + mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="800" x:Class="EDSEditorGUI2.Views.DevicePDOView" - x:DataType="vm:ObjectDictionary"> + x:DataType="vm:DevicePDOViewModel"> - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - - + + - - + + + + - - - + + - - - Invalid - - - - - - - - Zoom: - - + + diff --git a/EDSEditorGUI2/Views/DevicePDOView.axaml.cs b/EDSEditorGUI2/Views/DevicePDOView.axaml.cs index b7917baf..288dc61d 100644 --- a/EDSEditorGUI2/Views/DevicePDOView.axaml.cs +++ b/EDSEditorGUI2/Views/DevicePDOView.axaml.cs @@ -1,18 +1,353 @@ using Avalonia.Controls; using Avalonia.Media; using System.Collections.Generic; +using System; +using Avalonia.Layout; +using EDSEditorGUI2.ViewModels; +using System.Collections.Specialized; +using Avalonia.Threading; namespace EDSEditorGUI2.Views; public partial class DevicePDOView : UserControl { private List _bitColumns = []; + private DevicePDOViewModel? _vm; + public DevicePDOView() { InitializeComponent(); + MappingGrid.RowDefinitions.Add(new RowDefinition(GridLength.Auto)); + MappingGrid.RowDefinitions.Add(new RowDefinition(GridLength.Auto)); + CreateMappingBitsAndBytesIndication(); Zoom.Value = 100; + + subindexGrid.AddHandler(Avalonia.Input.InputElement.PointerPressedEvent, SubindexGrid_PointerPressed, Avalonia.Interactivity.RoutingStrategies.Tunnel); + subindexGrid.AddHandler(Avalonia.Input.InputElement.PointerMovedEvent, SubindexGrid_PointerMoved, Avalonia.Interactivity.RoutingStrategies.Tunnel); + } + + private Avalonia.Point? _dragStartPoint; + + private void SubindexGrid_PointerPressed(object? sender, Avalonia.Input.PointerPressedEventArgs e) + { + if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + { + _dragStartPoint = e.GetCurrentPoint(this).Position; + } + else + { + _dragStartPoint = null; + } + } + + private async void SubindexGrid_PointerMoved(object? sender, Avalonia.Input.PointerEventArgs e) + { + if (_dragStartPoint == null || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + return; + + var point = e.GetCurrentPoint(this); + var distance = Math.Sqrt(Math.Pow(point.Position.X - _dragStartPoint.Value.X, 2) + Math.Pow(point.Position.Y - _dragStartPoint.Value.Y, 2)); + + if (distance > 5 && subindexGrid.SelectedItem is DevicePDOViewModel.AvailableObjectViewModel draggedItem) + { + _dragStartPoint = null; + + var dragData = new Avalonia.Input.DataObject(); + dragData.Set("AvailableObjectViewModel", draggedItem); + + await Avalonia.Input.DragDrop.DoDragDrop(e, dragData, Avalonia.Input.DragDropEffects.Copy); + } + } + + private bool _isLoaded = false; + protected override void OnAttachedToVisualTree(Avalonia.VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + _isLoaded = true; + Dispatcher.UIThread.InvokeAsync(UpdateGraphicalMappings); + } + + protected override void OnDataContextChanged(EventArgs e) + { + base.OnDataContextChanged(e); + + if (_vm != null) + { + _vm.Mappings.CollectionChanged -= Mappings_CollectionChanged; + } + + _vm = DataContext as DevicePDOViewModel; + + if (_vm != null) + { + _vm.Mappings.CollectionChanged += Mappings_CollectionChanged; + if (this.VisualRoot != null && _isLoaded) + { + Dispatcher.UIThread.InvokeAsync(UpdateGraphicalMappings); + } + } + } + + private void Mappings_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + if (this.VisualRoot != null && _isLoaded) + { + Dispatcher.UIThread.InvokeAsync(UpdateGraphicalMappings); + } + } + + private void UpdateGraphicalMappings() + { + if (_vm == null) return; + + string emptyText = "Empty"; + if (this.TryFindResource("str_pdo_empty", out object? emptyObj)) + { + emptyText = emptyObj?.ToString() ?? "Empty"; + } + + var elementsToRemove = new List(); + foreach (var child in MappingGrid.Children) + { + if (Grid.GetRow((Control)child) >= 2) + { + elementsToRemove.Add((Control)child); + } + } + foreach (var el in elementsToRemove) + { + MappingGrid.Children.Remove(el); + } + + while (MappingGrid.RowDefinitions.Count > 2) + { + MappingGrid.RowDefinitions.RemoveAt(MappingGrid.RowDefinitions.Count - 1); + } + + if (_vm.Slots.Count == 0) return; + + int row = 2; + int mappingIndex = 1; + + foreach (var slot in _vm.Slots) + { + MappingGrid.RowDefinitions.Add(new RowDefinition(GridLength.Auto)); + + var idBorder = new Border { BorderThickness = new Avalonia.Thickness(0, 0, 1, 1), Padding = new Avalonia.Thickness(10, 0, 10, 0), IsHitTestVisible = false }; + idBorder.Bind(Border.BorderBrushProperty, this.GetResourceObservable("TableBorderBrush")); + idBorder.Child = new TextBlock { Text = mappingIndex.ToString(), VerticalAlignment = VerticalAlignment.Center }; + AddToMappingGrid(idBorder, row, 0); + + var indexBorder = new Border { BorderThickness = new Avalonia.Thickness(0, 0, 1, 1), Padding = new Avalonia.Thickness(10, 0, 10, 0), IsHitTestVisible = false }; + indexBorder.Bind(Border.BorderBrushProperty, this.GetResourceObservable("TableBorderBrush")); + var indexTextBlock = new TextBlock { VerticalAlignment = VerticalAlignment.Center }; + indexTextBlock.Bind(TextBlock.TextProperty, new Avalonia.Data.Binding("Communication") { Source = slot }); + indexBorder.Child = indexTextBlock; + AddToMappingGrid(indexBorder, row, 1); + + var cobBorder = new Border { BorderThickness = new Avalonia.Thickness(0, 0, 1, 1), Padding = new Avalonia.Thickness(10, 0, 10, 0), IsHitTestVisible = false }; + cobBorder.Bind(Border.BorderBrushProperty, this.GetResourceObservable("TableBorderBrush")); + var cobTextBlock = new TextBlock { VerticalAlignment = VerticalAlignment.Center }; + cobTextBlock.Bind(TextBlock.TextProperty, new Avalonia.Data.Binding("COB") { Source = slot }); + cobBorder.Child = cobTextBlock; + AddToMappingGrid(cobBorder, row, 2); + + int currentBit = 0; + int currentOrdinal = 0; + if (slot.Slot != null && slot.Slot.Mapping != null) + { + foreach (var mapping in slot.Slot.Mapping) + { + var entry = new EDSEditorGUI2.ViewModels.DevicePDOViewModel.MappingEntryViewModel(mapping); + int width = entry.BitWidth; + if (width > 0 && currentBit + width <= 64) + { + var isAvailable = !string.IsNullOrEmpty(entry.IndexString); + var bgBrushName = isAvailable ? "TableFilledBgBrush" : "TableEmptyBgBrush"; + + var border = new Border + { + BorderThickness = new Avalonia.Thickness(0, 0, 1, 1), + IsHitTestVisible = true, + Child = new TextBlock + { + Text = isAvailable ? (entry.IndexString + "/" + entry.SubIndexString + "/" + entry.Name) : emptyText, + VerticalAlignment = VerticalAlignment.Center, + HorizontalAlignment = HorizontalAlignment.Center, + TextWrapping = TextWrapping.NoWrap, + ClipToBounds = true + } + }; + border.Bind(Border.BackgroundProperty, this.GetResourceObservable(bgBrushName)); + border.Bind(Border.BorderBrushProperty, this.GetResourceObservable("TableBorderBrush")); + + ToolTip.SetTip(border, isAvailable ? (entry.IndexString + "/" + entry.SubIndexString + "/" + entry.Name) : emptyText); + + if (_vm != null) + { + var menu = new ContextMenu(); + var items = new System.Collections.Generic.List(); + + if (isAvailable) + { + object? removeHeaderObj = null; + this.TryFindResource("str_pdo_remove_item", out removeHeaderObj); + var removeMenuItem = new MenuItem { Header = removeHeaderObj?.ToString() ?? "Remove Item" }; + removeMenuItem.Click += (s, e) => { + _vm.RemoveMapping(slot, mapping); + Dispatcher.UIThread.InvokeAsync(UpdateGraphicalMappings, Avalonia.Threading.DispatcherPriority.Background); + MarkDirty(); + }; + items.Add(removeMenuItem); + } + + object? insertHeaderObj = null; + this.TryFindResource("str_pdo_insert_item", out insertHeaderObj); + var insertMenuItem = new MenuItem { Header = insertHeaderObj?.ToString() ?? "Insert Padding" }; + int captureOrdinal = currentOrdinal; + insertMenuItem.Click += (s, e) => { + _vm.InsertDummyMapping(slot, captureOrdinal); + Dispatcher.UIThread.InvokeAsync(UpdateGraphicalMappings); + MarkDirty(); + }; + items.Add(insertMenuItem); + + menu.ItemsSource = items; + border.ContextMenu = menu; + } + + SetupDropTarget(border, slot, currentOrdinal); + + AddToMappingGrid(border, row, 3 + currentBit, width); + currentBit += width; + currentOrdinal++; + } + else + { + currentBit += width; + currentOrdinal++; + } + } + } + + // Fill remainder with Empty + if (currentBit < 64) + { + int remaining = 64 - currentBit; + var border = new Border + { + BorderThickness = new Avalonia.Thickness(0, 0, 1, 1), + IsHitTestVisible = true, + Child = new TextBlock + { + Text = emptyText, + VerticalAlignment = VerticalAlignment.Center, + HorizontalAlignment = HorizontalAlignment.Center, + TextWrapping = TextWrapping.NoWrap, + ClipToBounds = true + } + }; + border.Bind(Border.BackgroundProperty, this.GetResourceObservable("TableEmptyBgBrush")); + border.Bind(Border.BorderBrushProperty, this.GetResourceObservable("TableBorderBrush")); + + ToolTip.SetTip(border, emptyText); + + if (_vm != null) + { + var menu = new ContextMenu(); + var items = new System.Collections.Generic.List(); + + object? insertHeaderObj = null; + this.TryFindResource("str_pdo_insert_item", out insertHeaderObj); + var insertMenuItem = new MenuItem { Header = insertHeaderObj?.ToString() ?? "Insert Padding" }; + int captureOrdinal = currentOrdinal; + insertMenuItem.Click += (s, e) => { + _vm.InsertDummyMapping(slot, captureOrdinal); + Dispatcher.UIThread.InvokeAsync(UpdateGraphicalMappings); + MarkDirty(); + }; + items.Add(insertMenuItem); + + menu.ItemsSource = items; + border.ContextMenu = menu; + } + + SetupDropTarget(border, slot, currentOrdinal); + AddToMappingGrid(border, row, 3 + currentBit, remaining); + } + + // Click handler to select slot + bool isSelected = (slot == _vm.SelectedSlot); + var rowOverlay = new Border + { + Background = isSelected ? new Avalonia.Media.SolidColorBrush(Avalonia.Media.Color.FromArgb(50, 0, 120, 215)) : Brushes.Transparent, + Cursor = new Avalonia.Input.Cursor(Avalonia.Input.StandardCursorType.Hand) + }; + + // Hover effect + rowOverlay.PointerEntered += (s, e) => + { + if (!isSelected) + { + bool isDarkHover = Avalonia.Application.Current?.ActualThemeVariant == Avalonia.Styling.ThemeVariant.Dark; + rowOverlay.Background = isDarkHover ? new Avalonia.Media.SolidColorBrush(Avalonia.Media.Color.FromArgb(20, 255, 255, 255)) : new Avalonia.Media.SolidColorBrush(Avalonia.Media.Color.FromArgb(20, 0, 0, 0)); + } + }; + rowOverlay.PointerExited += (s, e) => + { + if (!isSelected) rowOverlay.Background = Brushes.Transparent; + }; + + Grid.SetRow(rowOverlay, row); + Grid.SetColumn(rowOverlay, 0); + Grid.SetColumnSpan(rowOverlay, 67); // Span across all 67 columns (0 to 66) + rowOverlay.PointerPressed += (s, e) => { _vm.SelectedSlot = slot; }; + MappingGrid.Children.Insert(0, rowOverlay); + + row++; + mappingIndex++; + } + } + + private void SetupDropTarget(Control target, DevicePDOViewModel.PDOSlotViewModel slot, int ordinal) + { + Avalonia.Input.DragDrop.SetAllowDrop(target, true); + target.AddHandler(Avalonia.Input.DragDrop.DragOverEvent, (s, e) => + { + if (e.Data.Contains("AvailableObjectViewModel")) + { + e.DragEffects = Avalonia.Input.DragDropEffects.Copy; + } + else + { + e.DragEffects = Avalonia.Input.DragDropEffects.None; + } + e.Handled = true; + }, Avalonia.Interactivity.RoutingStrategies.Bubble); + + target.AddHandler(Avalonia.Input.DragDrop.DropEvent, (s, e) => + { + if (e.Data.Get("AvailableObjectViewModel") is DevicePDOViewModel.AvailableObjectViewModel vmItem && _vm != null) + { + _vm.InsertMapping(slot, ordinal, vmItem); + Dispatcher.UIThread.InvokeAsync(UpdateGraphicalMappings); + MarkDirty(); + } + e.Handled = true; + }, Avalonia.Interactivity.RoutingStrategies.Bubble); + } + + private void MarkDirty() + { + if (Avalonia.Application.Current?.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktop) + { + if (desktop.MainWindow is MainWindow mw) + { + mw.TriggerAutoSave(); + } + } } void CreateMappingBitsAndBytesIndication() @@ -20,19 +355,24 @@ void CreateMappingBitsAndBytesIndication() //Bits for (int i = 0; i < 64; i++) { + var border = new Border { BorderThickness = new Avalonia.Thickness(0, 0, 1, 1) }; + border.Bind(Border.BackgroundProperty, this.GetResourceObservable("GlobalExpanderHeaderBackground")); + border.Bind(Border.BorderBrushProperty, this.GetResourceObservable("TableBorderBrush")); var indication = new TextBlock { Text = i.ToString(), VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center, + HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, TextWrapping = TextWrapping.Wrap, }; if ((i % 8) == 0) { indication.Foreground = Brushes.Red; } - AddToMappingGrid(indication, 0, 3 + i); + border.Child = indication; + AddToMappingGrid(border, 0, 3 + i); - var newColumn = new ColumnDefinition(new GridLength(10 * 1.0)); + var newColumn = new ColumnDefinition(new GridLength(11.5 * 1.0)); _bitColumns.Add(newColumn); MappingGrid.ColumnDefinitions.Add(newColumn); @@ -40,16 +380,21 @@ void CreateMappingBitsAndBytesIndication() //Bytes for (int i = 0; i < 8; i++) { + var border = new Border { BorderThickness = new Avalonia.Thickness(0, 0, 1, 1) }; + border.Bind(Border.BackgroundProperty, this.GetResourceObservable("GlobalExpanderHeaderBackground")); + border.Bind(Border.BorderBrushProperty, this.GetResourceObservable("TableBorderBrush")); var indication = new TextBlock { Text = $"Byte {i}", HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, - + FontWeight = Avalonia.Media.FontWeight.Medium, TextWrapping = TextWrapping.Wrap, }; - AddToMappingGrid(indication, 1, 3 + (i*8), 8); + border.Child = indication; + AddToMappingGrid(border, 1, 3 + (i*8), 8); } } + void AddToMappingGrid(Control element, int row,int column, int columnspam = 1) { Grid.SetRow(element, row); @@ -75,7 +420,7 @@ private void ChangeMappingZoom(double zoomPercent) var zoom = zoomPercent / 100; foreach (var column in _bitColumns) { - column.Width = new GridLength(10 * zoom); + column.Width = new GridLength(11.5 * zoom); } } } diff --git a/EDSEditorGUI2/Views/DeviceView.axaml b/EDSEditorGUI2/Views/DeviceView.axaml index 707f1da9..82d43f49 100644 --- a/EDSEditorGUI2/Views/DeviceView.axaml +++ b/EDSEditorGUI2/Views/DeviceView.axaml @@ -8,21 +8,121 @@ x:Class="EDSEditorGUI2.Views.DeviceView" x:DataType="vm:Device"> - - - + + + + + + + + + + + + + + + + + + - - + + + + - - + + + + - - + + + + - - This is tab 3 content + + + + diff --git a/EDSEditorGUI2/Views/DeviceView.axaml.cs b/EDSEditorGUI2/Views/DeviceView.axaml.cs index 13758624..639e89e0 100644 --- a/EDSEditorGUI2/Views/DeviceView.axaml.cs +++ b/EDSEditorGUI2/Views/DeviceView.axaml.cs @@ -1,11 +1,135 @@ +using System; using Avalonia.Controls; namespace EDSEditorGUI2.Views; public partial class DeviceView : UserControl { + private DeviceInfoView? _deviceInfoView; + private DeviceODView? _deviceODView; + private DevicePDOView? _txPdoView; + private DevicePDOView? _rxPdoView; + private ModuleView? _moduleView; + public DeviceView() { InitializeComponent(); + + var tabControl = this.FindControl("MainTabControl"); + if (tabControl != null) + { + tabControl.SelectionChanged += TabControl_SelectionChanged; + } + } + + protected override void OnDataContextChanged(EventArgs e) + { + base.OnDataContextChanged(e); + UpdateTabContent(); + + if (DataContext is EDSEditorGUI2.ViewModels.Device device) + { + Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() => PreloadTabs(device), Avalonia.Threading.DispatcherPriority.Background); + } + } + + private void PreloadTabs(EDSEditorGUI2.ViewModels.Device device) + { + var tabControl = this.FindControl("MainTabControl"); + if (tabControl == null) return; + + if (_deviceODView == null) + { + _deviceODView = new DeviceODView(); + var tab = this.FindControl("TabOD"); + if (tab != null) tab.Content = _deviceODView; + } + _deviceODView.DataContext = device.Objects; + + if (_txPdoView == null) + { + _txPdoView = new DevicePDOView(); + var tab = this.FindControl("TabTxPdo"); + if (tab != null) tab.Content = _txPdoView; + } + _txPdoView.DataContext = device.TxPdo; + + if (_rxPdoView == null) + { + _rxPdoView = new DevicePDOView(); + var tab = this.FindControl("TabRxPdo"); + if (tab != null) tab.Content = _rxPdoView; + } + _rxPdoView.DataContext = device.RxPdo; + + if (_moduleView == null) + { + _moduleView = new ModuleView(); + var tab = this.FindControl("TabModule"); + if (tab != null) tab.Content = _moduleView; + } + _moduleView.DataContext = device.ModuleInfo; + } + + private void TabControl_SelectionChanged(object? sender, SelectionChangedEventArgs e) + { + UpdateTabContent(); + } + + private void UpdateTabContent() + { + if (DataContext is not EDSEditorGUI2.ViewModels.Device device) return; + + var tabControl = this.FindControl("MainTabControl"); + if (tabControl == null) return; + + var selectedTab = tabControl.SelectedItem as TabItem; + if (selectedTab == null) return; + + if (selectedTab.Name == "TabDeviceInfo") + { + if (_deviceInfoView == null) + { + _deviceInfoView = new DeviceInfoView(); + selectedTab.Content = _deviceInfoView; + } + _deviceInfoView.DataContext = device; + } + else if (selectedTab.Name == "TabOD") + { + if (_deviceODView == null) + { + _deviceODView = new DeviceODView(); + selectedTab.Content = _deviceODView; + } + _deviceODView.DataContext = device.Objects; + } + else if (selectedTab.Name == "TabTxPdo") + { + if (_txPdoView == null) + { + _txPdoView = new DevicePDOView(); + selectedTab.Content = _txPdoView; + } + _txPdoView.DataContext = device.TxPdo; + } + else if (selectedTab.Name == "TabRxPdo") + { + if (_rxPdoView == null) + { + _rxPdoView = new DevicePDOView(); + selectedTab.Content = _rxPdoView; + } + _rxPdoView.DataContext = device.RxPdo; + } + else if (selectedTab.Name == "TabModule") + { + if (_moduleView == null) + { + _moduleView = new ModuleView(); + selectedTab.Content = _moduleView; + } + _moduleView.DataContext = device.ModuleInfo; + } } } \ No newline at end of file diff --git a/EDSEditorGUI2/Views/InsertObjectsWindow.axaml b/EDSEditorGUI2/Views/InsertObjectsWindow.axaml new file mode 100644 index 00000000..06c4e558 --- /dev/null +++ b/EDSEditorGUI2/Views/InsertObjectsWindow.axaml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + - diff --git a/EDSEditorGUI2/Views/ODIndexRangeView.axaml.cs b/EDSEditorGUI2/Views/ODIndexRangeView.axaml.cs index 63d9cdd0..7d2df3da 100644 --- a/EDSEditorGUI2/Views/ODIndexRangeView.axaml.cs +++ b/EDSEditorGUI2/Views/ODIndexRangeView.axaml.cs @@ -16,24 +16,108 @@ public ODIndexRangeView() InitializeComponent(); var values = Enum.GetNames(typeof(OdObject.Types.ObjectType)).Skip(1).ToArray(); type.ItemsSource = values; + type.SelectedIndex = 0; - grid.LoadingRow += GridLoadingRow; + index.TextChanged += Index_TextChanged; } - /// - /// Hides rows with indexes that is not in min&max range - /// - /// sender object - /// event param - private void GridLoadingRow(object? sender, DataGridRowEventArgs e) + private void Index_TextChanged(object? sender, TextChangedEventArgs e) { - if(e.Row.DataContext != null) + ValidateIndex(); + } + + private bool ValidateIndex() + { + if (string.IsNullOrWhiteSpace(index.Text)) + { + errorText.IsVisible = false; + return false; + } + + int min = Convert.ToInt32(MinIndex, 16); + int max = Convert.ToInt32(MaxIndex, 16); + + if (!int.TryParse(index.Text, System.Globalization.NumberStyles.HexNumber, null, out int parsedIndex)) + { + errorText.Text = "必须输入十六进制数字"; + errorText.IsVisible = true; + return false; + } + + if (parsedIndex < min || parsedIndex > max) + { + errorText.Text = string.Format(Avalonia.Application.Current.FindResource("str_dyn_009") as string ?? "", min, max); + errorText.IsVisible = true; + return false; + } + + errorText.IsVisible = false; + return true; + } + + private Avalonia.Collections.DataGridCollectionView? _collectionView; + private bool _isDataLoaded = false; + + public static readonly StyledProperty FilteredItemsProperty = + AvaloniaProperty.Register(nameof(FilteredItems)); + + public Avalonia.Collections.DataGridCollectionView? FilteredItems + { + get { return GetValue(FilteredItemsProperty); } + set { SetValue(FilteredItemsProperty, value); } + } + + protected override void OnAttachedToVisualTree(Avalonia.VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + if (!_isDataLoaded) + { + LoadData(); + } + } + + protected override void OnDataContextChanged(EventArgs e) + { + base.OnDataContextChanged(e); + _isDataLoaded = false; + + if (this.IsEffectivelyVisible || this.Parent == null) // Parent check is tricky, but IsEffectivelyVisible is safe + { + // wait, if we are in TabControl, we might be attached when switched + } + + if (this.VisualRoot != null) + { + LoadData(); + } + else + { + FilteredItems = null; // Clear to free memory + } + } + + private void LoadData() + { + if (_isDataLoaded) return; + if (DataContext is System.Collections.IEnumerable collection) { - var dc = (System.Collections.Generic.KeyValuePair)e.Row.DataContext; - int index = int.Parse(dc.Key, System.Globalization.NumberStyles.HexNumber); int min = Convert.ToInt32(MinIndex, 16); int max = Convert.ToInt32(MaxIndex, 16); - e.Row.IsVisible = (min <= index && index <= max); + + _collectionView = new Avalonia.Collections.DataGridCollectionView(collection); + _collectionView.Filter = item => + { + if (item is System.Collections.Generic.KeyValuePair dc) + { + if (int.TryParse(dc.Key, System.Globalization.NumberStyles.HexNumber, null, out int index)) + { + return min <= index && index <= max; + } + } + return false; + }; + FilteredItems = _collectionView; + _isDataLoaded = true; } } @@ -42,7 +126,7 @@ private void GridLoadingRow(object? sender, DataGridRowEventArgs e) public string Heading { get { return GetValue(HeadingProperty); } - set { SetValue(HeadingProperty, value); HeadingText.Text = value; } + set { SetValue(HeadingProperty, value); } } public static readonly StyledProperty MinIndexProperty = @@ -63,6 +147,7 @@ public string MaxIndex private async void AddIndex(object? sender, RoutedEventArgs e) { + errorText.IsVisible = false; await DialogHost.Show(Resources["NewIndexDialog"]!, "NoAnimationDialogHost", OnDialogClosing); } @@ -72,17 +157,133 @@ private void OnDialogClosing(object? sender, DialogClosingEventArgs e) { if (DataContext is ViewModels.ObjectDictionary dc && e.Parameter is NewIndexRequest param) { + if (!ValidateIndex()) + { + e.Cancel(); + return; + } + + if (string.IsNullOrWhiteSpace(param.Name)) + { + errorText.Text = Avalonia.Application.Current.FindResource("str_dyn_046") as string; + errorText.IsVisible = true; + e.Cancel(); + return; + } + + if (dc.ContainsKey(param.Index.ToString("X4"))) + { + errorText.Text = string.Format(Avalonia.Application.Current.FindResource("str_dyn_052") as string ?? "", param.Index); + errorText.IsVisible = true; + e.Cancel(); + return; + } + dc.AddIndex(param.Index, param.Name, param.Type); + _collectionView?.Refresh(); + + var newKey = param.Index.ToString("X4"); + var newlyAdded = dc.FirstOrDefault(x => x.Key == newKey); + if (newlyAdded.Key != null) + { + Avalonia.Threading.Dispatcher.UIThread.Post(() => + { + grid.SelectedItem = newlyAdded; + grid.ScrollIntoView(newlyAdded, null); + }); + } + + if ((Avalonia.Application.Current?.ApplicationLifetime as Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime)?.MainWindow is MainWindow mainWindow) + { + mainWindow.TriggerAutoSave(); + } + } + } + } + + private void RemoveIndex(object? sender, RoutedEventArgs e) + { + if (DataContext is ViewModels.ObjectDictionary dc && grid.SelectedItem is System.Collections.Generic.KeyValuePair selected) + { + dc.Remove(selected.Key); + _collectionView?.Refresh(); + if ((Avalonia.Application.Current?.ApplicationLifetime as Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime)?.MainWindow is MainWindow mainWindow) + { + mainWindow.TriggerAutoSave(); + } + } + } + + private void ToggleIndex(object? sender, RoutedEventArgs e) + { + if (grid.SelectedItem is System.Collections.Generic.KeyValuePair selected) + { + selected.Value.Disabled = !selected.Value.Disabled; + _collectionView?.Refresh(); + if ((Avalonia.Application.Current?.ApplicationLifetime as Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime)?.MainWindow is MainWindow mainWindow) + { + mainWindow.TriggerAutoSave(); + } + } + } + + private async void CloneIndex(object? sender, RoutedEventArgs e) + { + if (DataContext is ViewModels.ObjectDictionary dc && grid.SelectedItem is System.Collections.Generic.KeyValuePair selected) + { + var mainWindow = (Avalonia.Application.Current?.ApplicationLifetime as Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime)?.MainWindow as MainWindow; + if (mainWindow != null) + { + var tempObjects = new ViewModels.ObjectDictionary(); + if (grid.SelectedItems != null && grid.SelectedItems.Count > 0) + { + foreach (System.Collections.Generic.KeyValuePair item in grid.SelectedItems) + { + tempObjects.Add(new System.Collections.Generic.KeyValuePair(item.Key, item.Value)); + } + } + else + { + tempObjects.Add(new System.Collections.Generic.KeyValuePair(selected.Key, selected.Value)); + } + var tempDevice = new ViewModels.Device { Objects = tempObjects }; + + await mainWindow.MergeObjectsIntoDeviceAsync(tempDevice); } } } - private async void RemoveIndex(object? sender, RoutedEventArgs e) + private void ContextMenuSubObjectRemoveClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e) { - await DialogHost.Show(Resources["NewIndexDialog"]!, "NoAnimationDialogHost"); } private void DataGrid_SelectionChanged(object? sender, Avalonia.Controls.SelectionChangedEventArgs e) { } + + private void DataGrid_PointerPressed(object? sender, Avalonia.Input.PointerPressedEventArgs e) + { + if (e.GetCurrentPoint(this).Properties.IsRightButtonPressed) + { + if (e.Source is Avalonia.Controls.Control control && control.DataContext is System.Collections.Generic.KeyValuePair) + { + grid.SelectedItem = control.DataContext; + } + } + } + + private void DataGrid_LoadingRow(object? sender, Avalonia.Controls.DataGridRowEventArgs e) + { + if (e.Row.DataContext is System.Collections.Generic.KeyValuePair pair) + { + if (pair.Value.Disabled) + { + e.Row.Classes.Add("disabled"); + } + else + { + e.Row.Classes.Remove("disabled"); + } + } + } } \ No newline at end of file diff --git a/EDSEditorGUI2/Views/PreferencesView.axaml b/EDSEditorGUI2/Views/PreferencesView.axaml new file mode 100644 index 00000000..80762a01 --- /dev/null +++ b/EDSEditorGUI2/Views/PreferencesView.axaml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +