From 5b5217efeb72aa545aaba7c7b756512e80ea750d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Moreau?= Date: Wed, 9 Sep 2026 15:47:31 -0400 Subject: [PATCH 1/3] Add zip/tar archive explorer sample app --- README.md | 1 + apps/archive-tool/.cargo/config.toml | 5 + apps/archive-tool/.gitignore | 6 + apps/archive-tool/Cargo.lock | 350 +++++ apps/archive-tool/Cargo.toml | 14 + apps/archive-tool/README.md | 45 + apps/archive-tool/avalonia-app.json | 15 + apps/archive-tool/file-associations/README.md | 23 + .../linux-desktop-entry.desktop | 9 + .../file-associations/linux-mime-type.xml | 19 + .../macos-Info.plist.snippet | 33 + .../windows-file-association.reg | 37 + .../generated/.avalonia-viewmodel.owned.json | 17 + .../generated/RustViewRegistry.g.cs | 45 + .../generated/generated_view_models.rs | 362 +++++ .../generated/view-model.contract.md | 119 ++ apps/archive-tool/global.json | 9 + .../managed/Consumer.Presentation.csproj | 24 + .../Generated/.avalonia-viewmodel.owned.json | 25 + .../Generated/EntryRowViewModelAdapter.g.cs | 552 ++++++++ .../Generated/EntryRowViewModelMetadata.g.cs | 32 + .../Generated/MainViewModelAdapter.g.cs | 866 ++++++++++++ .../managed/Generated/MainViewModelMenus.g.cs | 222 +++ .../Generated/MainViewModelMetadata.g.cs | 84 ++ .../managed/Views/MainWindow.axaml | 380 ++++++ .../managed/Views/MainWindow.axaml.cs | 41 + apps/archive-tool/src/archive.rs | 904 ++++++++++++ apps/archive-tool/src/main.rs | 1206 +++++++++++++++++ apps/archive-tool/tests/test-bundle.ps1 | 130 ++ apps/archive-tool/view-model.ir.json | 726 ++++++++++ 30 files changed, 6301 insertions(+) create mode 100644 apps/archive-tool/.cargo/config.toml create mode 100644 apps/archive-tool/.gitignore create mode 100644 apps/archive-tool/Cargo.lock create mode 100644 apps/archive-tool/Cargo.toml create mode 100644 apps/archive-tool/README.md create mode 100644 apps/archive-tool/avalonia-app.json create mode 100644 apps/archive-tool/file-associations/README.md create mode 100644 apps/archive-tool/file-associations/linux-desktop-entry.desktop create mode 100644 apps/archive-tool/file-associations/linux-mime-type.xml create mode 100644 apps/archive-tool/file-associations/macos-Info.plist.snippet create mode 100644 apps/archive-tool/file-associations/windows-file-association.reg create mode 100644 apps/archive-tool/generated/.avalonia-viewmodel.owned.json create mode 100644 apps/archive-tool/generated/RustViewRegistry.g.cs create mode 100644 apps/archive-tool/generated/generated_view_models.rs create mode 100644 apps/archive-tool/generated/view-model.contract.md create mode 100644 apps/archive-tool/global.json create mode 100644 apps/archive-tool/managed/Consumer.Presentation.csproj create mode 100644 apps/archive-tool/managed/Generated/.avalonia-viewmodel.owned.json create mode 100644 apps/archive-tool/managed/Generated/EntryRowViewModelAdapter.g.cs create mode 100644 apps/archive-tool/managed/Generated/EntryRowViewModelMetadata.g.cs create mode 100644 apps/archive-tool/managed/Generated/MainViewModelAdapter.g.cs create mode 100644 apps/archive-tool/managed/Generated/MainViewModelMenus.g.cs create mode 100644 apps/archive-tool/managed/Generated/MainViewModelMetadata.g.cs create mode 100644 apps/archive-tool/managed/Views/MainWindow.axaml create mode 100644 apps/archive-tool/managed/Views/MainWindow.axaml.cs create mode 100644 apps/archive-tool/src/archive.rs create mode 100644 apps/archive-tool/src/main.rs create mode 100644 apps/archive-tool/tests/test-bundle.ps1 create mode 100644 apps/archive-tool/view-model.ir.json diff --git a/README.md b/README.md index f3cffa1..78c0a32 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ pinned `avalonia-src` producer submodule at commit | `apps/system-monitor/` | NeoHtop, a real Rust-owned system monitor with compiled AXAML presentation and a packaged NativeAOT host | | `apps/pdf-viewer/` | A PDF Oxide-powered sample viewer with rendered page navigation and extracted text | | `apps/hash-calculator/` | A streaming checksum utility (MD5, SHA-1, SHA-256, SHA-512, BLAKE3) with compare and copy | +| `apps/archive-tool/` | A zip/tar explorer that lists entries and extracts selected files | | `tests/` | Host, IR, and generator test suites | | `samples/` | `RustViewModelSample.Managed` - the sample presentation project the host consumes | | `build/` | Vendored MSBuild configuration (versioning, signing, analyzers, xunit) | diff --git a/apps/archive-tool/.cargo/config.toml b/apps/archive-tool/.cargo/config.toml new file mode 100644 index 0000000..81b00ec --- /dev/null +++ b/apps/archive-tool/.cargo/config.toml @@ -0,0 +1,5 @@ +[target.x86_64-pc-windows-msvc] +rustflags = ["-C", "target-feature=+crt-static"] + +[target.aarch64-pc-windows-msvc] +rustflags = ["-C", "target-feature=+crt-static"] diff --git a/apps/archive-tool/.gitignore b/apps/archive-tool/.gitignore new file mode 100644 index 0000000..0559651 --- /dev/null +++ b/apps/archive-tool/.gitignore @@ -0,0 +1,6 @@ +.avalonia/ +bin/ +obj/ +target/ +*.user +*.suo diff --git a/apps/archive-tool/Cargo.lock b/apps/archive-tool/Cargo.lock new file mode 100644 index 0000000..a476cbc --- /dev/null +++ b/apps/archive-tool/Cargo.lock @@ -0,0 +1,350 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "archive-tool" +version = "0.1.0" +dependencies = [ + "avalonia", + "flate2", + "tar", + "zip", +] + +[[package]] +name = "avalonia" +version = "0.1.0" +dependencies = [ + "avalonia-sys", +] + +[[package]] +name = "avalonia-sys" +version = "0.1.0" +dependencies = [ + "libloading", +] + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "crc32fast" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + +[[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "crc32fast", + "miniz_oxide", + "zlib-rs", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + +[[package]] +name = "zip" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" +dependencies = [ + "arbitrary", + "crc32fast", + "crossbeam-utils", + "displaydoc", + "flate2", + "indexmap", + "memchr", + "thiserror", + "zopfli", +] + +[[package]] +name = "zlib-rs" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] diff --git a/apps/archive-tool/Cargo.toml b/apps/archive-tool/Cargo.toml new file mode 100644 index 0000000..c33f044 --- /dev/null +++ b/apps/archive-tool/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "archive-tool" +version = "0.1.0" +edition = "2021" +publish = false +description = "A sample Rustolonia zip/tar explorer" + +[dependencies] +avalonia = { path = "../../rust/avalonia" } +flate2 = "1" +tar = "0.4" +zip = { version = "2", default-features = false, features = ["deflate"] } + +[workspace] diff --git a/apps/archive-tool/README.md b/apps/archive-tool/README.md new file mode 100644 index 0000000..c3361c6 --- /dev/null +++ b/apps/archive-tool/README.md @@ -0,0 +1,45 @@ +# Rustolonia archive explorer + +This sample is a sibling external consumer under `apps/` that lists zip and tar +archives and extracts selected entries. Rust owns format detection, listing, +zip-slip path checks and extraction; the generated view-model bridge exposes +that state to the compiled Avalonia presentation. Listing and extraction run on +worker threads so the UI stays responsive. + +Supported formats: `.zip`, `.tar`, `.tar.gz` / `.tgz` (no 7z/RAR codecs). With +no arguments the app waits for **Open**, a dropped file, or an "open with" +activation. Folder browsing, CRC, Test, Extract here, smart extract, New archive +from a folder, and opening nested zip/tar entries stay on those crates. + +## Build on Windows x64 + +From the repository root: + +```powershell +pwsh ./rust/build-app.ps1 ` + -ProducerRoot ./avalonia-src ` + -Manifest ./apps/archive-tool/avalonia-app.json ` + -UpdateLockFile +``` + +The portable bundle is written to `apps/archive-tool/artifacts/win-x64`. +Subsequent locked builds omit `-UpdateLockFile`. + +## Tests + +```powershell +Push-Location ./apps/archive-tool +cargo test --locked +Pop-Location +``` + +After building, the Windows UI Automation smoke test checks the idle window +and natural shutdown. It never opens a picker: + +```powershell +powershell.exe -NoProfile -STA -ExecutionPolicy Bypass ` + -File ./apps/archive-tool/tests/test-bundle.ps1 +``` + +The app is a portable Win32 GUI bundle, not an installer. File-association +metadata snippets are under `file-associations/`. diff --git a/apps/archive-tool/avalonia-app.json b/apps/archive-tool/avalonia-app.json new file mode 100644 index 0000000..a9465dd --- /dev/null +++ b/apps/archive-tool/avalonia-app.json @@ -0,0 +1,15 @@ +{ + "version": 1, + "presentationProject": "managed/Consumer.Presentation.csproj", + "viewModelIr": "view-model.ir.json", + "generatedAdaptersDirectory": "managed/Generated", + "generatedRegistryFile": "generated/RustViewRegistry.g.cs", + "generatedRustFile": "generated/generated_view_models.rs", + "generatedContractFile": "generated/view-model.contract.md", + "cargoManifest": "Cargo.toml", + "packageName": "archive-tool", + "binary": "archive-tool", + "rid": "win-x64", + "configuration": "Release", + "outputDirectory": "artifacts/win-x64" +} diff --git a/apps/archive-tool/file-associations/README.md b/apps/archive-tool/file-associations/README.md new file mode 100644 index 0000000..bc9477a --- /dev/null +++ b/apps/archive-tool/file-associations/README.md @@ -0,0 +1,23 @@ +# File type association metadata + +These are packaging metadata snippets, not installers. They register archive +types so the desktop shell launches this application with the selected file +path as a command-line argument; the runtime side of "open with" is already +wired, because `App::run` forwards this process's arguments to the managed +desktop lifetime and `AppScope::activation_items()` returns them normalized +(see `rust/DESKTOP_FILES.md` in the pinned Avalonia producer checkout). + +Deliberately out of scope here: MSIX packaging, `.msi`/`.pkg`/`.deb`/`.rpm` +installers, notarization, and any store submission. Those belong to whatever +distribution channel a consumer chooses; this workflow ships a deterministic +per-RID directory (see `PRODUCTIZATION.md`). + +These snippets cover `.zip`, `.tar`, and `.tgz`. A real installer should still +own the association lifecycle and should avoid claiming archives without an +explicit user choice. + +| Platform | File | Applied by | +| --- | --- | --- | +| Windows | `windows-file-association.reg` | Your installer writing the same keys, or `reg import` for local testing. | +| Linux | `linux-desktop-entry.desktop`, `linux-mime-type.xml` | `desktop-file-install` / `xdg-mime install` from your package's post-install step. | +| macOS | `macos-Info.plist.snippet` | Merged into the `.app` bundle's `Info.plist`. | diff --git a/apps/archive-tool/file-associations/linux-desktop-entry.desktop b/apps/archive-tool/file-associations/linux-desktop-entry.desktop new file mode 100644 index 0000000..b1ae7ac --- /dev/null +++ b/apps/archive-tool/file-associations/linux-desktop-entry.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Type=Application +Name=archive-tool +Comment=Rustolonia zip/tar explorer +Exec=/opt/archive-tool/archive-tool %F +Icon=archive-tool +Terminal=false +Categories=Utility; +MimeType=application/zip;application/x-tar;application/x-gtar;application/gzip; diff --git a/apps/archive-tool/file-associations/linux-mime-type.xml b/apps/archive-tool/file-associations/linux-mime-type.xml new file mode 100644 index 0000000..f65972f --- /dev/null +++ b/apps/archive-tool/file-associations/linux-mime-type.xml @@ -0,0 +1,19 @@ + + + + ZIP archive + + + + + TAR archive + + + + + Gzip TAR archive + + + + + diff --git a/apps/archive-tool/file-associations/macos-Info.plist.snippet b/apps/archive-tool/file-associations/macos-Info.plist.snippet new file mode 100644 index 0000000..89889dc --- /dev/null +++ b/apps/archive-tool/file-associations/macos-Info.plist.snippet @@ -0,0 +1,33 @@ + + + + CFBundleDocumentTypes + + + CFBundleTypeName + ZIP archive + CFBundleTypeRole + Viewer + LSHandlerRank + Alternate + LSItemContentTypes + + public.zip-archive + + + + CFBundleTypeName + TAR archive + CFBundleTypeRole + Viewer + LSHandlerRank + Alternate + LSItemContentTypes + + public.tar-archive + org.gnu.gnu-zip-tar-archive + + + + + diff --git a/apps/archive-tool/file-associations/windows-file-association.reg b/apps/archive-tool/file-associations/windows-file-association.reg new file mode 100644 index 0000000..0972c07 --- /dev/null +++ b/apps/archive-tool/file-associations/windows-file-association.reg @@ -0,0 +1,37 @@ +Windows Registry Editor Version 5.00 + +; File type association for the archive explorer. +; +; "%1" is what makes the shell pass the selected file to the executable; the +; Rust entry point forwards it to the managed desktop lifetime, so it arrives +; through AppScope::activation_items(). +; +; Written under HKEY_CURRENT_USER so it can be applied and undone without +; elevation while developing. + +[HKEY_CURRENT_USER\Software\Classes\.zip] +@="archive-tool.Document" + +[HKEY_CURRENT_USER\Software\Classes\.zip\OpenWithProgids] +"archive-tool.Document"="" + +[HKEY_CURRENT_USER\Software\Classes\.tar] +@="archive-tool.Document" + +[HKEY_CURRENT_USER\Software\Classes\.tar\OpenWithProgids] +"archive-tool.Document"="" + +[HKEY_CURRENT_USER\Software\Classes\.tgz] +@="archive-tool.Document" + +[HKEY_CURRENT_USER\Software\Classes\.tgz\OpenWithProgids] +"archive-tool.Document"="" + +[HKEY_CURRENT_USER\Software\Classes\archive-tool.Document] +@="Archive" + +[HKEY_CURRENT_USER\Software\Classes\archive-tool.Document\DefaultIcon] +@="C:\\Program Files\\archive-tool\\archive-tool.exe,0" + +[HKEY_CURRENT_USER\Software\Classes\archive-tool.Document\shell\open\command] +@="\"C:\\Program Files\\archive-tool\\archive-tool.exe\" \"%1\"" diff --git a/apps/archive-tool/generated/.avalonia-viewmodel.owned.json b/apps/archive-tool/generated/.avalonia-viewmodel.owned.json new file mode 100644 index 0000000..e757e86 --- /dev/null +++ b/apps/archive-tool/generated/.avalonia-viewmodel.owned.json @@ -0,0 +1,17 @@ +{ + "generator": "avalonia-viewmodel", + "files": [ + { + "path": "generated_view_models.rs", + "sha256": "1659194e3c38535cea684366c29db4451d97c7da59c3f9a19a00286b7f32c8d5" + }, + { + "path": "RustViewRegistry.g.cs", + "sha256": "79ef3106f64111ec092d47cae6c402065741f96d86df921097f49c369214426e" + }, + { + "path": "view-model.contract.md", + "sha256": "5c37d40185adc79c562d8d96ede9fc383941daa54e935d1fb4f6565becc0ce53" + } + ] +} diff --git a/apps/archive-tool/generated/RustViewRegistry.g.cs b/apps/archive-tool/generated/RustViewRegistry.g.cs new file mode 100644 index 0000000..d828066 --- /dev/null +++ b/apps/archive-tool/generated/RustViewRegistry.g.cs @@ -0,0 +1,45 @@ +// +#nullable enable +using System; +using Avalonia.Controls; +using Avalonia.Rust; +using Avalonia.Rust.Interop; +using Avalonia.Threading; + +namespace Avalonia.Host.Generated.ViewModels; + +internal static class RustViewRegistry +{ + internal static Window Create(int viewId, IAvnRustViewModel model) => viewId switch + { + 1 => new global::ArchiveTool.Presentation.Views.MainWindow(model), + _ => throw new global::System.ArgumentOutOfRangeException(nameof(viewId)), + }; + + private static TWindow CreateDynamic( + IAvnRustViewModel model, + RustViewModelDescriptor descriptor) + where TWindow : Window, new() + { + var adapter = new ReflectableRustViewModelAdapter(model, descriptor, Dispatch); + try + { + var window = new TWindow { DataContext = adapter }; + window.Closed += (_, _) => adapter.Dispose(); + return window; + } + catch + { + adapter.Dispose(); + throw; + } + } + + private static void Dispatch(Action action) + { + if (Dispatcher.UIThread.CheckAccess()) + action(); + else + Dispatcher.UIThread.Invoke(action); + } +} diff --git a/apps/archive-tool/generated/generated_view_models.rs b/apps/archive-tool/generated/generated_view_models.rs new file mode 100644 index 0000000..d1f5422 --- /dev/null +++ b/apps/archive-tool/generated/generated_view_models.rs @@ -0,0 +1,362 @@ +//! Generated from view-model.ir.json. Do not edit. + +#![allow(dead_code)] + +#[derive(Clone, Debug)] +pub struct MainViewModelSink(crate::view_model::ViewModelSink); + +/// Declared capacity of the `MainViewModel` recent-file list. +pub const MAIN_VIEW_MODEL_RECENT_FILES_CAPACITY: usize = 5; + +impl MainViewModelSink { + pub fn set_title(&self, value: impl AsRef) -> crate::Result<()> { self.0.set_string(1, value) } + pub fn set_status(&self, value: impl AsRef) -> crate::Result<()> { self.0.set_string(2, value) } + pub fn set_archive_name(&self, value: impl AsRef) -> crate::Result<()> { self.0.set_string(3, value) } + pub fn set_archive_kind(&self, value: impl AsRef) -> crate::Result<()> { self.0.set_string(4, value) } + pub fn set_entry_count_label(&self, value: impl AsRef) -> crate::Result<()> { self.0.set_string(5, value) } + pub fn set_filter_text(&self, value: impl AsRef) -> crate::Result<()> { self.0.set_string(6, value) } + pub fn set_selected_index(&self, value: i64) -> crate::Result<()> { self.0.set_integer(7, value) } + pub fn set_selected_key(&self, value: impl AsRef) -> crate::Result<()> { self.0.set_string(8, value) } + pub fn set_sort_direction(&self, value: impl AsRef) -> crate::Result<()> { self.0.set_string(9, value) } + pub fn set_is_loading(&self, value: bool) -> crate::Result<()> { self.0.set_boolean(10, value) } + pub fn set_can_extract_selected(&self, value: bool) -> crate::Result<()> { self.0.set_boolean(11, value) } + pub fn set_can_extract_all(&self, value: bool) -> crate::Result<()> { self.0.set_boolean(12, value) } + pub fn set_selected_count_label(&self, value: impl AsRef) -> crate::Result<()> { self.0.set_string(13, value) } + pub fn set_current_path(&self, value: impl AsRef) -> crate::Result<()> { self.0.set_string(14, value) } + pub fn set_can_go_up(&self, value: bool) -> crate::Result<()> { self.0.set_boolean(15, value) } + pub fn set_totals_label(&self, value: impl AsRef) -> crate::Result<()> { self.0.set_string(16, value) } + pub fn set_open_after_extract(&self, value: bool) -> crate::Result<()> { self.0.set_boolean(17, value) } + pub fn set_can_test(&self, value: bool) -> crate::Result<()> { self.0.set_boolean(18, value) } + pub fn add_recent_files(&self, value: impl AsRef) -> crate::Result<()> { self.0.add_string(1, value) } + pub fn insert_recent_files(&self, index: i32, value: impl AsRef) -> crate::Result<()> { self.0.insert_string(1, index, value) } + pub fn replace_recent_files(&self, index: i32, value: impl AsRef) -> crate::Result<()> { self.0.replace_string(1, index, value) } + pub fn add_entries(&self, value: impl EntryRowViewModel) -> crate::Result<()> { self.0.add_model(2, EntryRowViewModelDispatch { model: value }) } + pub fn insert_entries(&self, index: i32, value: impl EntryRowViewModel) -> crate::Result<()> { self.0.insert_model(2, index, EntryRowViewModelDispatch { model: value }) } + pub fn replace_entries(&self, index: i32, value: impl EntryRowViewModel) -> crate::Result<()> { self.0.replace_model(2, index, EntryRowViewModelDispatch { model: value }) } + pub fn remove_recent_files(&self, index: i32) -> crate::Result<()> { self.0.remove_string_at(1, index) } + pub fn move_recent_files(&self, from_index: i32, to_index: i32) -> crate::Result<()> { self.0.move_string_item(1, from_index, to_index) } + pub fn clear_recent_files(&self) -> crate::Result<()> { self.0.clear_string_collection(1) } + pub fn remove_entries(&self, index: i32) -> crate::Result<()> { self.0.remove_model_at(2, index) } + pub fn move_entries(&self, from_index: i32, to_index: i32) -> crate::Result<()> { self.0.move_model_item(2, from_index, to_index) } + pub fn clear_entries(&self) -> crate::Result<()> { self.0.clear_model_collection(2) } + pub fn set_open_file_enabled(&self, enabled: bool) -> crate::Result<()> { self.0.set_command_enabled(1, enabled) } + pub fn set_extract_selected_enabled(&self, enabled: bool) -> crate::Result<()> { self.0.set_command_enabled(2, enabled) } + pub fn set_extract_all_enabled(&self, enabled: bool) -> crate::Result<()> { self.0.set_command_enabled(3, enabled) } + pub fn set_select_all_enabled(&self, enabled: bool) -> crate::Result<()> { self.0.set_command_enabled(4, enabled) } + pub fn set_clear_selection_enabled(&self, enabled: bool) -> crate::Result<()> { self.0.set_command_enabled(5, enabled) } + pub fn set_sort_entries_enabled(&self, enabled: bool) -> crate::Result<()> { self.0.set_command_enabled(6, enabled) } + pub fn set_open_recent_file_enabled(&self, enabled: bool) -> crate::Result<()> { self.0.set_command_enabled(7, enabled) } + pub fn set_exit_enabled(&self, enabled: bool) -> crate::Result<()> { self.0.set_command_enabled(8, enabled) } + pub fn set_go_up_enabled(&self, enabled: bool) -> crate::Result<()> { self.0.set_command_enabled(9, enabled) } + pub fn set_open_item_enabled(&self, enabled: bool) -> crate::Result<()> { self.0.set_command_enabled(10, enabled) } + pub fn set_extract_here_enabled(&self, enabled: bool) -> crate::Result<()> { self.0.set_command_enabled(11, enabled) } + pub fn set_test_archive_enabled(&self, enabled: bool) -> crate::Result<()> { self.0.set_command_enabled(12, enabled) } + pub fn set_new_archive_enabled(&self, enabled: bool) -> crate::Result<()> { self.0.set_command_enabled(13, enabled) } + pub fn set_invert_selection_enabled(&self, enabled: bool) -> crate::Result<()> { self.0.set_command_enabled(14, enabled) } + pub fn set_copy_path_enabled(&self, enabled: bool) -> crate::Result<()> { self.0.set_command_enabled(15, enabled) } + /// Publishes the most-recently-used storage URIs into `RecentFiles`. + /// + /// The list is bounded by its own capacity (5), so this replaces a + /// handful of entries rather than a data set; the generated menu derives + /// each header from the URI and passes the URI back as the command parameter. + pub fn publish_recent_files(&self, recent: &crate::RecentFileList) -> crate::Result<()> { + self.0.clear_string_collection(1)?; + for uri in recent.entries() { self.0.add_string(1, uri)?; } + Ok(()) + } + pub fn set_title_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(1, message) } + pub fn set_status_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(2, message) } + pub fn set_archive_name_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(3, message) } + pub fn set_archive_kind_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(4, message) } + pub fn set_entry_count_label_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(5, message) } + pub fn set_filter_text_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(6, message) } + pub fn set_selected_index_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(7, message) } + pub fn set_selected_key_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(8, message) } + pub fn set_sort_direction_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(9, message) } + pub fn set_is_loading_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(10, message) } + pub fn set_can_extract_selected_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(11, message) } + pub fn set_can_extract_all_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(12, message) } + pub fn set_selected_count_label_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(13, message) } + pub fn set_current_path_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(14, message) } + pub fn set_can_go_up_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(15, message) } + pub fn set_totals_label_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(16, message) } + pub fn set_open_after_extract_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(17, message) } + pub fn set_can_test_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(18, message) } + /// Creates a worker-safe immutable update batch with a monotonic generation. + pub fn batch(&self, generation: i64) -> MainViewModelSinkBatch { MainViewModelSinkBatch(crate::view_model::ViewModelBatch::new(generation)) } + pub fn submit_batch(&self, batch: MainViewModelSinkBatch) -> crate::Result { self.0.submit_batch(batch.0) } +} + +pub struct MainViewModelSinkBatch(crate::view_model::ViewModelBatch); + +impl MainViewModelSinkBatch { + pub fn set_title(&mut self, value: impl AsRef) { self.0.push_string(1, 1, 0, value); } + pub fn set_title_error(&mut self, message: impl AsRef) { self.0.push_string(18, 1, 0, message); } + pub fn clear_title_error(&mut self) { self.0.push_clear_error(1); } + pub fn set_status(&mut self, value: impl AsRef) { self.0.push_string(1, 2, 0, value); } + pub fn set_status_error(&mut self, message: impl AsRef) { self.0.push_string(18, 2, 0, message); } + pub fn clear_status_error(&mut self) { self.0.push_clear_error(2); } + pub fn set_archive_name(&mut self, value: impl AsRef) { self.0.push_string(1, 3, 0, value); } + pub fn set_archive_name_error(&mut self, message: impl AsRef) { self.0.push_string(18, 3, 0, message); } + pub fn clear_archive_name_error(&mut self) { self.0.push_clear_error(3); } + pub fn set_archive_kind(&mut self, value: impl AsRef) { self.0.push_string(1, 4, 0, value); } + pub fn set_archive_kind_error(&mut self, message: impl AsRef) { self.0.push_string(18, 4, 0, message); } + pub fn clear_archive_kind_error(&mut self) { self.0.push_clear_error(4); } + pub fn set_entry_count_label(&mut self, value: impl AsRef) { self.0.push_string(1, 5, 0, value); } + pub fn set_entry_count_label_error(&mut self, message: impl AsRef) { self.0.push_string(18, 5, 0, message); } + pub fn clear_entry_count_label_error(&mut self) { self.0.push_clear_error(5); } + pub fn set_filter_text(&mut self, value: impl AsRef) { self.0.push_string(1, 6, 0, value); } + pub fn set_filter_text_error(&mut self, message: impl AsRef) { self.0.push_string(18, 6, 0, message); } + pub fn clear_filter_text_error(&mut self) { self.0.push_clear_error(6); } + pub fn set_selected_index(&mut self, value: i64) { self.0.push_integer(7, value); } + pub fn set_selected_index_error(&mut self, message: impl AsRef) { self.0.push_string(18, 7, 0, message); } + pub fn clear_selected_index_error(&mut self) { self.0.push_clear_error(7); } + pub fn set_selected_key(&mut self, value: impl AsRef) { self.0.push_string(1, 8, 0, value); } + pub fn set_selected_key_error(&mut self, message: impl AsRef) { self.0.push_string(18, 8, 0, message); } + pub fn clear_selected_key_error(&mut self) { self.0.push_clear_error(8); } + pub fn set_sort_direction(&mut self, value: impl AsRef) { self.0.push_string(1, 9, 0, value); } + pub fn set_sort_direction_error(&mut self, message: impl AsRef) { self.0.push_string(18, 9, 0, message); } + pub fn clear_sort_direction_error(&mut self) { self.0.push_clear_error(9); } + pub fn set_is_loading(&mut self, value: bool) { self.0.push_boolean(3, 10, value); } + pub fn set_is_loading_error(&mut self, message: impl AsRef) { self.0.push_string(18, 10, 0, message); } + pub fn clear_is_loading_error(&mut self) { self.0.push_clear_error(10); } + pub fn set_can_extract_selected(&mut self, value: bool) { self.0.push_boolean(3, 11, value); } + pub fn set_can_extract_selected_error(&mut self, message: impl AsRef) { self.0.push_string(18, 11, 0, message); } + pub fn clear_can_extract_selected_error(&mut self) { self.0.push_clear_error(11); } + pub fn set_can_extract_all(&mut self, value: bool) { self.0.push_boolean(3, 12, value); } + pub fn set_can_extract_all_error(&mut self, message: impl AsRef) { self.0.push_string(18, 12, 0, message); } + pub fn clear_can_extract_all_error(&mut self) { self.0.push_clear_error(12); } + pub fn set_selected_count_label(&mut self, value: impl AsRef) { self.0.push_string(1, 13, 0, value); } + pub fn set_selected_count_label_error(&mut self, message: impl AsRef) { self.0.push_string(18, 13, 0, message); } + pub fn clear_selected_count_label_error(&mut self) { self.0.push_clear_error(13); } + pub fn set_current_path(&mut self, value: impl AsRef) { self.0.push_string(1, 14, 0, value); } + pub fn set_current_path_error(&mut self, message: impl AsRef) { self.0.push_string(18, 14, 0, message); } + pub fn clear_current_path_error(&mut self) { self.0.push_clear_error(14); } + pub fn set_can_go_up(&mut self, value: bool) { self.0.push_boolean(3, 15, value); } + pub fn set_can_go_up_error(&mut self, message: impl AsRef) { self.0.push_string(18, 15, 0, message); } + pub fn clear_can_go_up_error(&mut self) { self.0.push_clear_error(15); } + pub fn set_totals_label(&mut self, value: impl AsRef) { self.0.push_string(1, 16, 0, value); } + pub fn set_totals_label_error(&mut self, message: impl AsRef) { self.0.push_string(18, 16, 0, message); } + pub fn clear_totals_label_error(&mut self) { self.0.push_clear_error(16); } + pub fn set_open_after_extract(&mut self, value: bool) { self.0.push_boolean(3, 17, value); } + pub fn set_open_after_extract_error(&mut self, message: impl AsRef) { self.0.push_string(18, 17, 0, message); } + pub fn clear_open_after_extract_error(&mut self) { self.0.push_clear_error(17); } + pub fn set_can_test(&mut self, value: bool) { self.0.push_boolean(3, 18, value); } + pub fn set_can_test_error(&mut self, message: impl AsRef) { self.0.push_string(18, 18, 0, message); } + pub fn clear_can_test_error(&mut self) { self.0.push_clear_error(18); } + pub fn add_recent_files(&mut self, value: impl AsRef) { self.0.push_string(7, 1, 0, value); } + pub fn insert_recent_files(&mut self, index: i32, value: impl AsRef) { self.0.push_string(9, 1, index, value); } + pub fn replace_recent_files(&mut self, index: i32, value: impl AsRef) { self.0.push_string(11, 1, index, value); } + pub fn replace_recent_files_snapshot>(&mut self, values: impl IntoIterator) { self.0.push_string_snapshot(1, values); } + pub fn remove_recent_files(&mut self, index: i32) { self.0.push_indices(13, 1, index, 0); } + pub fn move_recent_files(&mut self, from_index: i32, to_index: i32) { self.0.push_indices(14, 1, from_index, to_index); } + pub fn clear_recent_files(&mut self) { self.0.push_indices(19, 1, 0, 0); } + pub fn add_entries(&mut self, value: impl EntryRowViewModel) { self.0.push_model(8, 2, 0, EntryRowViewModelDispatch { model: value }); } + pub fn insert_entries(&mut self, index: i32, value: impl EntryRowViewModel) { self.0.push_model(10, 2, index, EntryRowViewModelDispatch { model: value }); } + pub fn replace_entries(&mut self, index: i32, value: impl EntryRowViewModel) { self.0.push_model(12, 2, index, EntryRowViewModelDispatch { model: value }); } + pub fn replace_entries_snapshot(&mut self, values: impl IntoIterator) { self.0.push_model_snapshot(2, values.into_iter().map(|value| EntryRowViewModelDispatch { model: value })); } + pub fn remove_entries(&mut self, index: i32) { self.0.push_model_indices(13, 2, index, 0); } + pub fn move_entries(&mut self, from_index: i32, to_index: i32) { self.0.push_model_indices(14, 2, from_index, to_index); } + pub fn clear_entries(&mut self) { self.0.push_model_clear(2); } + pub fn set_open_file_enabled(&mut self, enabled: bool) { self.0.push_boolean(17, 1, enabled); } + pub fn set_extract_selected_enabled(&mut self, enabled: bool) { self.0.push_boolean(17, 2, enabled); } + pub fn set_extract_all_enabled(&mut self, enabled: bool) { self.0.push_boolean(17, 3, enabled); } + pub fn set_select_all_enabled(&mut self, enabled: bool) { self.0.push_boolean(17, 4, enabled); } + pub fn set_clear_selection_enabled(&mut self, enabled: bool) { self.0.push_boolean(17, 5, enabled); } + pub fn set_sort_entries_enabled(&mut self, enabled: bool) { self.0.push_boolean(17, 6, enabled); } + pub fn set_open_recent_file_enabled(&mut self, enabled: bool) { self.0.push_boolean(17, 7, enabled); } + pub fn set_exit_enabled(&mut self, enabled: bool) { self.0.push_boolean(17, 8, enabled); } + pub fn set_go_up_enabled(&mut self, enabled: bool) { self.0.push_boolean(17, 9, enabled); } + pub fn set_open_item_enabled(&mut self, enabled: bool) { self.0.push_boolean(17, 10, enabled); } + pub fn set_extract_here_enabled(&mut self, enabled: bool) { self.0.push_boolean(17, 11, enabled); } + pub fn set_test_archive_enabled(&mut self, enabled: bool) { self.0.push_boolean(17, 12, enabled); } + pub fn set_new_archive_enabled(&mut self, enabled: bool) { self.0.push_boolean(17, 13, enabled); } + pub fn set_invert_selection_enabled(&mut self, enabled: bool) { self.0.push_boolean(17, 14, enabled); } + pub fn set_copy_path_enabled(&mut self, enabled: bool) { self.0.push_boolean(17, 15, enabled); } + /// Stages the most-recently-used storage URIs as one `RecentFiles` snapshot. + pub fn set_recent_files(&mut self, recent: &crate::RecentFileList) { self.0.push_string_snapshot(1, recent.entries()); } +} + +pub trait MainViewModel: Send + 'static { + fn attach(&mut self, sink: MainViewModelSink) -> crate::Result<()>; + fn detach(&mut self) -> crate::Result<()>; + fn set_filter_text(&mut self, value: String) -> crate::Result<()>; + fn set_selected_index(&mut self, value: i64) -> crate::Result<()>; + fn set_selected_key(&mut self, value: String) -> crate::Result<()>; + fn set_open_after_extract(&mut self, value: bool) -> crate::Result<()>; + fn open_file(&mut self) -> crate::Result<()>; + fn extract_selected(&mut self) -> crate::Result<()>; + fn extract_all(&mut self) -> crate::Result<()>; + fn select_all(&mut self) -> crate::Result<()>; + fn clear_selection(&mut self) -> crate::Result<()>; + fn sort_entries(&mut self, value: String) -> crate::Result<()>; + fn open_recent_file(&mut self, value: String) -> crate::Result<()>; + fn exit(&mut self) -> crate::Result<()>; + fn go_up(&mut self) -> crate::Result<()>; + fn open_item(&mut self) -> crate::Result<()>; + fn extract_here(&mut self) -> crate::Result<()>; + fn test_archive(&mut self) -> crate::Result<()>; + fn new_archive(&mut self) -> crate::Result<()>; + fn invert_selection(&mut self) -> crate::Result<()>; + fn copy_path(&mut self) -> crate::Result<()>; +} + +struct MainViewModelDispatch { model: T } + +impl crate::view_model::DynamicViewModel for MainViewModelDispatch { + fn attach(&mut self, sink: crate::view_model::ViewModelSink) -> crate::Result<()> { self.model.attach(MainViewModelSink(sink)) } + fn detach(&mut self) -> crate::Result<()> { self.model.detach() } + fn set_string(&mut self, property_id: i32, value: String) -> crate::Result<()> { + match property_id { + 6 => self.model.set_filter_text(value), + 8 => self.model.set_selected_key(value), + _ => Err(crate::Error::InvalidViewModelMember { kind: "property", id: property_id }), + } + } + fn set_integer(&mut self, property_id: i32, value: i64) -> crate::Result<()> { + match property_id { + 7 => self.model.set_selected_index(value), + _ => Err(crate::Error::InvalidViewModelMember { kind: "property", id: property_id }), + } + } + fn set_boolean(&mut self, property_id: i32, value: bool) -> crate::Result<()> { + match property_id { + 17 => self.model.set_open_after_extract(value), + _ => Err(crate::Error::InvalidViewModelMember { kind: "property", id: property_id }), + } + } + fn set_double(&mut self, property_id: i32, _value: f64) -> crate::Result<()> { + Err(crate::Error::InvalidViewModelMember { kind: "property", id: property_id }) + } + fn execute(&mut self, command_id: i32, parameter: Option) -> crate::Result<()> { + match command_id { + 4 => self.model.select_all(), + 5 => self.model.clear_selection(), + 6 => self.model.sort_entries(parameter.unwrap_or_default()), + 7 => self.model.open_recent_file(parameter.unwrap_or_default()), + 8 => self.model.exit(), + 9 => self.model.go_up(), + 14 => self.model.invert_selection(), + _ => Err(crate::Error::InvalidViewModelMember { kind: "command", id: command_id }), + } + } + fn begin_async(&mut self, command_id: i32, _parameter: Option) -> crate::Result<()> { + match command_id { + 1 => self.model.open_file(), + 2 => self.model.extract_selected(), + 3 => self.model.extract_all(), + 10 => self.model.open_item(), + 11 => self.model.extract_here(), + 12 => self.model.test_archive(), + 13 => self.model.new_archive(), + 15 => self.model.copy_path(), + _ => Err(crate::Error::InvalidViewModelMember { kind: "command", id: command_id }), + } + } +} + +pub fn mount_main_window(scope: &crate::AppScope, model: impl MainViewModel) -> crate::Result<()> { scope.mount_dynamic_view_model(1, MainViewModelDispatch { model }) } + +#[derive(Clone, Debug)] +pub struct EntryRowViewModelSink(crate::view_model::ViewModelSink); + +impl EntryRowViewModelSink { + pub fn set_key(&self, value: impl AsRef) -> crate::Result<()> { self.0.set_string(1, value) } + pub fn set_path(&self, value: impl AsRef) -> crate::Result<()> { self.0.set_string(2, value) } + pub fn set_name(&self, value: impl AsRef) -> crate::Result<()> { self.0.set_string(3, value) } + pub fn set_kind(&self, value: impl AsRef) -> crate::Result<()> { self.0.set_string(4, value) } + pub fn set_size_label(&self, value: impl AsRef) -> crate::Result<()> { self.0.set_string(5, value) } + pub fn set_packed_label(&self, value: impl AsRef) -> crate::Result<()> { self.0.set_string(6, value) } + pub fn set_modified(&self, value: impl AsRef) -> crate::Result<()> { self.0.set_string(7, value) } + pub fn set_is_selected(&self, value: bool) -> crate::Result<()> { self.0.set_boolean(8, value) } + pub fn set_crc_label(&self, value: impl AsRef) -> crate::Result<()> { self.0.set_string(9, value) } + pub fn set_ratio_label(&self, value: impl AsRef) -> crate::Result<()> { self.0.set_string(10, value) } + pub fn set_open_enabled(&self, enabled: bool) -> crate::Result<()> { self.0.set_command_enabled(1, enabled) } + pub fn set_key_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(1, message) } + pub fn set_path_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(2, message) } + pub fn set_name_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(3, message) } + pub fn set_kind_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(4, message) } + pub fn set_size_label_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(5, message) } + pub fn set_packed_label_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(6, message) } + pub fn set_modified_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(7, message) } + pub fn set_is_selected_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(8, message) } + pub fn set_crc_label_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(9, message) } + pub fn set_ratio_label_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(10, message) } + /// Creates a worker-safe immutable update batch with a monotonic generation. + pub fn batch(&self, generation: i64) -> EntryRowViewModelSinkBatch { EntryRowViewModelSinkBatch(crate::view_model::ViewModelBatch::new(generation)) } + pub fn submit_batch(&self, batch: EntryRowViewModelSinkBatch) -> crate::Result { self.0.submit_batch(batch.0) } +} + +pub struct EntryRowViewModelSinkBatch(crate::view_model::ViewModelBatch); + +impl EntryRowViewModelSinkBatch { + pub fn set_key(&mut self, value: impl AsRef) { self.0.push_string(1, 1, 0, value); } + pub fn set_key_error(&mut self, message: impl AsRef) { self.0.push_string(18, 1, 0, message); } + pub fn clear_key_error(&mut self) { self.0.push_clear_error(1); } + pub fn set_path(&mut self, value: impl AsRef) { self.0.push_string(1, 2, 0, value); } + pub fn set_path_error(&mut self, message: impl AsRef) { self.0.push_string(18, 2, 0, message); } + pub fn clear_path_error(&mut self) { self.0.push_clear_error(2); } + pub fn set_name(&mut self, value: impl AsRef) { self.0.push_string(1, 3, 0, value); } + pub fn set_name_error(&mut self, message: impl AsRef) { self.0.push_string(18, 3, 0, message); } + pub fn clear_name_error(&mut self) { self.0.push_clear_error(3); } + pub fn set_kind(&mut self, value: impl AsRef) { self.0.push_string(1, 4, 0, value); } + pub fn set_kind_error(&mut self, message: impl AsRef) { self.0.push_string(18, 4, 0, message); } + pub fn clear_kind_error(&mut self) { self.0.push_clear_error(4); } + pub fn set_size_label(&mut self, value: impl AsRef) { self.0.push_string(1, 5, 0, value); } + pub fn set_size_label_error(&mut self, message: impl AsRef) { self.0.push_string(18, 5, 0, message); } + pub fn clear_size_label_error(&mut self) { self.0.push_clear_error(5); } + pub fn set_packed_label(&mut self, value: impl AsRef) { self.0.push_string(1, 6, 0, value); } + pub fn set_packed_label_error(&mut self, message: impl AsRef) { self.0.push_string(18, 6, 0, message); } + pub fn clear_packed_label_error(&mut self) { self.0.push_clear_error(6); } + pub fn set_modified(&mut self, value: impl AsRef) { self.0.push_string(1, 7, 0, value); } + pub fn set_modified_error(&mut self, message: impl AsRef) { self.0.push_string(18, 7, 0, message); } + pub fn clear_modified_error(&mut self) { self.0.push_clear_error(7); } + pub fn set_is_selected(&mut self, value: bool) { self.0.push_boolean(3, 8, value); } + pub fn set_is_selected_error(&mut self, message: impl AsRef) { self.0.push_string(18, 8, 0, message); } + pub fn clear_is_selected_error(&mut self) { self.0.push_clear_error(8); } + pub fn set_crc_label(&mut self, value: impl AsRef) { self.0.push_string(1, 9, 0, value); } + pub fn set_crc_label_error(&mut self, message: impl AsRef) { self.0.push_string(18, 9, 0, message); } + pub fn clear_crc_label_error(&mut self) { self.0.push_clear_error(9); } + pub fn set_ratio_label(&mut self, value: impl AsRef) { self.0.push_string(1, 10, 0, value); } + pub fn set_ratio_label_error(&mut self, message: impl AsRef) { self.0.push_string(18, 10, 0, message); } + pub fn clear_ratio_label_error(&mut self) { self.0.push_clear_error(10); } + pub fn set_open_enabled(&mut self, enabled: bool) { self.0.push_boolean(17, 1, enabled); } +} + +pub trait EntryRowViewModel: Send + 'static { + fn attach(&mut self, sink: EntryRowViewModelSink) -> crate::Result<()>; + fn detach(&mut self) -> crate::Result<()>; + fn set_is_selected(&mut self, value: bool) -> crate::Result<()>; + fn open(&mut self) -> crate::Result<()>; +} + +struct EntryRowViewModelDispatch { model: T } + +impl crate::view_model::DynamicViewModel for EntryRowViewModelDispatch { + fn attach(&mut self, sink: crate::view_model::ViewModelSink) -> crate::Result<()> { self.model.attach(EntryRowViewModelSink(sink)) } + fn detach(&mut self) -> crate::Result<()> { self.model.detach() } + fn set_string(&mut self, property_id: i32, _value: String) -> crate::Result<()> { + Err(crate::Error::InvalidViewModelMember { kind: "property", id: property_id }) + } + fn set_integer(&mut self, property_id: i32, _value: i64) -> crate::Result<()> { + Err(crate::Error::InvalidViewModelMember { kind: "property", id: property_id }) + } + fn set_boolean(&mut self, property_id: i32, value: bool) -> crate::Result<()> { + match property_id { + 8 => self.model.set_is_selected(value), + _ => Err(crate::Error::InvalidViewModelMember { kind: "property", id: property_id }), + } + } + fn set_double(&mut self, property_id: i32, _value: f64) -> crate::Result<()> { + Err(crate::Error::InvalidViewModelMember { kind: "property", id: property_id }) + } + fn execute(&mut self, command_id: i32, _parameter: Option) -> crate::Result<()> { + match command_id { + 1 => self.model.open(), + _ => Err(crate::Error::InvalidViewModelMember { kind: "command", id: command_id }), + } + } + fn begin_async(&mut self, command_id: i32, _parameter: Option) -> crate::Result<()> { + Err(crate::Error::InvalidViewModelMember { kind: "command", id: command_id }) + } +} diff --git a/apps/archive-tool/generated/view-model.contract.md b/apps/archive-tool/generated/view-model.contract.md new file mode 100644 index 0000000..950d475 --- /dev/null +++ b/apps/archive-tool/generated/view-model.contract.md @@ -0,0 +1,119 @@ +# Generated Rust view-model contract + +Schema version: `5` + +## Model `MainViewModel` (`1`) + +| Kind | ID | Name | Type | Direction | +| --- | ---: | --- | --- | --- | +| Property | 1 | `Title` | `String` | Rust to managed | +| Property | 2 | `Status` | `String` | Rust to managed | +| Property | 3 | `ArchiveName` | `String` | Rust to managed | +| Property | 4 | `ArchiveKind` | `String` | Rust to managed | +| Property | 5 | `EntryCountLabel` | `String` | Rust to managed | +| Property | 6 | `FilterText` | `String` | Rust and managed | +| Property | 7 | `SelectedIndex` | `Integer` | Rust and managed | +| Property | 8 | `SelectedKey` | `String` | Rust and managed | +| Property | 9 | `SortDirection` | `String` | Rust to managed | +| Property | 10 | `IsLoading` | `Boolean` | Rust to managed | +| Property | 11 | `CanExtractSelected` | `Boolean` | Rust to managed | +| Property | 12 | `CanExtractAll` | `Boolean` | Rust to managed | +| Property | 13 | `SelectedCountLabel` | `String` | Rust to managed | +| Property | 14 | `CurrentPath` | `String` | Rust to managed | +| Property | 15 | `CanGoUp` | `Boolean` | Rust to managed | +| Property | 16 | `TotalsLabel` | `String` | Rust to managed | +| Property | 17 | `OpenAfterExtract` | `Boolean` | Rust and managed | +| Property | 18 | `CanTest` | `Boolean` | Rust to managed | +| Collection | 1 | `RecentFiles` | `String` | Rust to managed | +| Collection | 2 | `Entries` | Model `EntryRowViewModel` | Rust to managed | +| Async command | 1 | `OpenFile` | None | Managed to Rust | +| Async command | 2 | `ExtractSelected` | None | Managed to Rust | +| Async command | 3 | `ExtractAll` | None | Managed to Rust | +| Command | 4 | `SelectAll` | None | Managed to Rust | +| Command | 5 | `ClearSelection` | None | Managed to Rust | +| Command | 6 | `SortEntries` | None | Managed to Rust | +| Command | 7 | `OpenRecentFile` | None | Managed to Rust | +| Command | 8 | `Exit` | None | Managed to Rust | +| Command | 9 | `GoUp` | None | Managed to Rust | +| Async command | 10 | `OpenItem` | None | Managed to Rust | +| Async command | 11 | `ExtractHere` | None | Managed to Rust | +| Async command | 12 | `TestArchive` | None | Managed to Rust | +| Async command | 13 | `NewArchive` | None | Managed to Rust | +| Command | 14 | `InvertSelection` | None | Managed to Rust | +| Async command | 15 | `CopyPath` | None | Managed to Rust | + +### Recent files `RecentFiles` + +Storage URIs published into collection `RecentFiles`, capacity 5, activated by `OpenRecentFileCommand` with the chosen URI as its command parameter. + +### Application menu `Main` (`1`) + +| ID | Item | Kind | Header | Command | Gesture | Bound member | +| ---: | --- | --- | --- | --- | --- | --- | +| 1 | `File` | Submenu | _File | - | - | - | +| 2 |     `OpenFile` | Command | _Open archive... | `OpenFileCommand` | `Ctrl+O` | - | +| 3 |     `NewArchive` | Command | _New archive... | `NewArchiveCommand` | `Ctrl+N` | - | +| 4 |     `Recent` | RecentFiles | Recent _files | - | - | recent files | +| 5 |     `FileSeparator` | Separator | - | - | - | - | +| 6 |     `ExtractSelected` | Command | _Extract selected... | `ExtractSelectedCommand` | `Ctrl+E` | - | +| 7 |     `ExtractHere` | Command | Extract _here | `ExtractHereCommand` | `Ctrl+Shift+H` | - | +| 8 |     `ExtractAll` | Command | Extract _all... | `ExtractAllCommand` | `Ctrl+Shift+E` | - | +| 9 |     `TestArchive` | Command | _Test archive | `TestArchiveCommand` | `Ctrl+T` | - | +| 10 |     `FileSeparator2` | Separator | - | - | - | - | +| 11 |     `Exit` | Command | E_xit | `ExitCommand` | `Ctrl+Q` | - | +| 12 | `Edit` | Submenu | _Edit | - | - | - | +| 13 |     `OpenItem` | Command | O_pen | `OpenItemCommand` | `Enter` | - | +| 14 |     `GoUp` | Command | Go _up | `GoUpCommand` | `Alt+Up` | - | +| 15 |     `EditSeparator` | Separator | - | - | - | - | +| 16 |     `SelectAll` | Command | Select _all | `SelectAllCommand` | `Ctrl+A` | - | +| 17 |     `InvertSelection` | Command | _Invert selection | `InvertSelectionCommand` | - | - | +| 18 |     `ClearSelection` | Command | _Clear selection | `ClearSelectionCommand` | - | - | +| 19 |     `CopyPath` | Command | Copy _path | `CopyPathCommand` | `Ctrl+C` | - | +| 20 | `View` | Submenu | _View | - | - | - | +| 21 |     `OpenAfterExtract` | Toggle | Open folder after e_xtract | - | - | `OpenAfterExtract` | + +### Context menu `Entries` (`2`) + +| ID | Item | Kind | Header | Command | Gesture | Bound member | +| ---: | --- | --- | --- | --- | --- | --- | +| 1 | `OpenRow` | Command | Open | `OpenItemCommand` | - | - | +| 2 | `ExtractRow` | Command | Extract... | `ExtractSelectedCommand` | - | - | +| 3 | `ExtractHereRow` | Command | Extract here | `ExtractHereCommand` | - | - | +| 4 | `ContextSeparator` | Separator | - | - | - | - | +| 5 | `CopyRow` | Command | Copy path | `CopyPathCommand` | - | - | + +### Table `Entries` + +| ID | Name | Header | Row path | Width | Resize | Sort | Alignment | +| ---: | --- | --- | --- | --- | --- | --- | --- | +| 1 | `Name` | Name | `Name` | * | Yes | Yes | Left | +| 2 | `Kind` | Kind | `Kind` | 80 | Yes | Yes | Left | +| 3 | `Size` | Size | `SizeLabel` | 90 | Yes | Yes | Right | +| 4 | `Packed` | Packed | `PackedLabel` | 90 | Yes | Yes | Right | +| 5 | `Ratio` | Ratio | `RatioLabel` | 70 | Yes | Yes | Right | +| 6 | `Modified` | Modified | `Modified` | 140 | Yes | Yes | Left | +| 7 | `Crc` | CRC | `CrcLabel` | 90 | Yes | Yes | Left | +Selection: index `SelectedIndex`, key `SelectedKey`, row key `Key`. +Sort: `SortEntries` command, initial column `Name`, direction property `SortDirection`. + +## Model `EntryRowViewModel` (`2`) + +| Kind | ID | Name | Type | Direction | +| --- | ---: | --- | --- | --- | +| Property | 1 | `Key` | `String` | Rust to managed | +| Property | 2 | `Path` | `String` | Rust to managed | +| Property | 3 | `Name` | `String` | Rust to managed | +| Property | 4 | `Kind` | `String` | Rust to managed | +| Property | 5 | `SizeLabel` | `String` | Rust to managed | +| Property | 6 | `PackedLabel` | `String` | Rust to managed | +| Property | 7 | `Modified` | `String` | Rust to managed | +| Property | 8 | `IsSelected` | `Boolean` | Rust and managed | +| Property | 9 | `CrcLabel` | `String` | Rust to managed | +| Property | 10 | `RatioLabel` | `String` | Rust to managed | +| Command | 1 | `Open` | None | Managed to Rust | + +## Views + +| ID | Name | Model | Managed type | Binding path | +| ---: | --- | --- | --- | --- | +| 1 | `MainWindow` | `MainViewModel` | `ArchiveTool.Presentation.Views.MainWindow` | Generated CLR properties | diff --git a/apps/archive-tool/global.json b/apps/archive-tool/global.json new file mode 100644 index 0000000..c745e9d --- /dev/null +++ b/apps/archive-tool/global.json @@ -0,0 +1,9 @@ +{ + "sdk": { + "version": "10.0.201", + "rollForward": "latestFeature" + }, + "test": { + "runner": "Microsoft.Testing.Platform" + } +} diff --git a/apps/archive-tool/managed/Consumer.Presentation.csproj b/apps/archive-tool/managed/Consumer.Presentation.csproj new file mode 100644 index 0000000..8b53fa4 --- /dev/null +++ b/apps/archive-tool/managed/Consumer.Presentation.csproj @@ -0,0 +1,24 @@ + + + net10.0 + enable + true + false + true + true + + $(MSBuildThisFileDirectory)../../../avalonia-src + $(MSBuildThisFileDirectory)../../.. + + + + + + + + + diff --git a/apps/archive-tool/managed/Generated/.avalonia-viewmodel.owned.json b/apps/archive-tool/managed/Generated/.avalonia-viewmodel.owned.json new file mode 100644 index 0000000..8272ad2 --- /dev/null +++ b/apps/archive-tool/managed/Generated/.avalonia-viewmodel.owned.json @@ -0,0 +1,25 @@ +{ + "generator": "avalonia-viewmodel", + "files": [ + { + "path": "EntryRowViewModelAdapter.g.cs", + "sha256": "094a89722f00f5c808b38be3713d284ca7c347fd59a0600e9401dfaaba861b93" + }, + { + "path": "EntryRowViewModelMetadata.g.cs", + "sha256": "0567d7d13b010b2adc2857b7dc4a5c3ec7bd41e61873179f43814797a3db640b" + }, + { + "path": "MainViewModelAdapter.g.cs", + "sha256": "36bfbb71aa5f067289a9ba7f2b5f80d688de2d0069b4346492196a2e90ed723e" + }, + { + "path": "MainViewModelMenus.g.cs", + "sha256": "cc25583382a4a75a0714c30ea7058d2b065bb217b7a14de18e54a62f74259827" + }, + { + "path": "MainViewModelMetadata.g.cs", + "sha256": "6cfb4664542a750f56108dcf5f39505022250c56a79e315e4019e2407a024408" + } + ] +} diff --git a/apps/archive-tool/managed/Generated/EntryRowViewModelAdapter.g.cs b/apps/archive-tool/managed/Generated/EntryRowViewModelAdapter.g.cs new file mode 100644 index 0000000..8a5ee7b --- /dev/null +++ b/apps/archive-tool/managed/Generated/EntryRowViewModelAdapter.g.cs @@ -0,0 +1,552 @@ +// +#nullable enable +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.Marshalling; +using System.Windows.Input; +using Avalonia.Rust; +using Avalonia.Rust.Interop; +using Avalonia.Threading; + +namespace ArchiveTool.Presentation.Generated; + +[GeneratedComClass] +public sealed partial class EntryRowViewModelAdapter : IAvnRustVmSink, IAvnRustVmSink2, IAvnRustVmSink3, IRustVmStringSnapshotSink, IRustVmModelSnapshotSink, IRustVmBatchTarget, IRustVmTableSelectionBatchTarget, INotifyPropertyChanged, INotifyDataErrorInfo, IDisposable +{ + private readonly IAvnRustViewModel _model; + private readonly Action _dispatch; + private readonly Action? _post; + private readonly RustVmBatchCoordinator _batch; + private readonly Dictionary _errors = new(StringComparer.Ordinal); + private readonly RustVmInboundWriteTracker _inboundWrites = new(); + private string _key = ""; + private string _path = ""; + private string _name = ""; + private string _kind = ""; + private string _sizeLabel = ""; + private string _packedLabel = ""; + private string _modified = ""; + private bool _isSelected = false; + private string _crcLabel = ""; + private string _ratioLabel = ""; + + /// Creates an adapter that dispatches and posts through . + public EntryRowViewModelAdapter(IAvnRustViewModel model) : this(model, null, null) { } + + /// + /// Creates an adapter with a custom synchronous dispatch for the legacy v1/v2 + /// sink path. Kept as a distinct CLR signature (not an optional parameter) so + /// already-compiled callers keep binding to it. + /// + public EntryRowViewModelAdapter(IAvnRustViewModel model, Action? dispatch) : this(model, dispatch, null) { } + + /// + /// Creates an adapter with a custom synchronous for the + /// legacy v1/v2 sink path and a custom nonblocking for + /// batch submission. + /// + public EntryRowViewModelAdapter(IAvnRustViewModel model, Action? dispatch, Action? post) + { + _model = model; + _dispatch = dispatch ?? Dispatch; + _post = post; + _batch = new RustVmBatchCoordinator(this, post); + OpenCommand = new DelegateCommand(parameter => Check(_model.Execute(1, null))); + try + { + Check(_model.Attach(this)); + } + catch + { + try { _model.Detach(); } + catch { } + DisposeNestedAdapters(); + throw; + } + } + + public event PropertyChangedEventHandler? PropertyChanged; + public event EventHandler? ErrorsChanged; + + public string Key + { + get => _key; + } + + public string Path + { + get => _path; + } + + public string Name + { + get => _name; + } + + public string Kind + { + get => _kind; + } + + public string SizeLabel + { + get => _sizeLabel; + } + + public string PackedLabel + { + get => _packedLabel; + } + + public string Modified + { + get => _modified; + } + + public bool IsSelected + { + get => _isSelected; + set + { + var accepted = value; + if (Equals(_isSelected, accepted)) + return; + var previous = _isSelected; + var inbound = _inboundWrites.Begin(8); + try + { + Check(_model.SetBoolean(8, (accepted ? 1 : 0))); + if (!_inboundWrites.WasPublished(inbound)) + { + _inboundWrites.CommitLocal(8); + SetField(ref _isSelected, accepted, nameof(IsSelected)); + } + } + catch + { + if (_inboundWrites.ShouldRollback(inbound)) + { + _inboundWrites.CommitLocal(8); + SetField(ref _isSelected, previous, nameof(IsSelected)); + } + throw; + } + finally { _inboundWrites.End(inbound); } + } + } + + public string CrcLabel + { + get => _crcLabel; + } + + public string RatioLabel + { + get => _ratioLabel; + } + + public DelegateCommand OpenCommand { get; } + + public bool HasErrors => _errors.Count > 0; + + public IEnumerable GetErrors(string? propertyName) => + propertyName is not null && _errors.TryGetValue(propertyName, out var message) + ? new[] { message } + : Array.Empty(); + + public int SetString(int propertyId, string? value) + { + var inbound = _inboundWrites.MarkPublication(propertyId); + return propertyId switch + { + 1 => Apply(() => { var converted = value ?? ""; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_key, converted)) SetField(ref _key, converted, nameof(Key)); }), + 2 => Apply(() => { var converted = value ?? ""; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_path, converted)) SetField(ref _path, converted, nameof(Path)); }), + 3 => Apply(() => { var converted = value ?? ""; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_name, converted)) SetField(ref _name, converted, nameof(Name)); }), + 4 => Apply(() => { var converted = value ?? ""; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_kind, converted)) SetField(ref _kind, converted, nameof(Kind)); }), + 5 => Apply(() => { var converted = value ?? ""; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_sizeLabel, converted)) SetField(ref _sizeLabel, converted, nameof(SizeLabel)); }), + 6 => Apply(() => { var converted = value ?? ""; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_packedLabel, converted)) SetField(ref _packedLabel, converted, nameof(PackedLabel)); }), + 7 => Apply(() => { var converted = value ?? ""; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_modified, converted)) SetField(ref _modified, converted, nameof(Modified)); }), + 9 => Apply(() => { var converted = value ?? ""; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_crcLabel, converted)) SetField(ref _crcLabel, converted, nameof(CrcLabel)); }), + 10 => Apply(() => { var converted = value ?? ""; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_ratioLabel, converted)) SetField(ref _ratioLabel, converted, nameof(RatioLabel)); }), + _ => unchecked((int)0x80070057), + }; + } + + public int SetInteger(int propertyId, long value) + { + var inbound = _inboundWrites.MarkPublication(propertyId); + return propertyId switch + { + _ => unchecked((int)0x80070057), + }; + } + + public int SetBoolean(int propertyId, int value) + { + var inbound = _inboundWrites.MarkPublication(propertyId); + return propertyId switch + { + 8 => Apply(() => { var converted = value != 0; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_isSelected, converted)) SetField(ref _isSelected, converted, nameof(IsSelected)); }), + _ => unchecked((int)0x80070057), + }; + } + + public int SetDouble(int propertyId, double value) + { + var inbound = _inboundWrites.MarkPublication(propertyId); + return propertyId switch + { + _ => unchecked((int)0x80070057), + }; + } + + public int SetNull(int propertyId) + { + var inbound = _inboundWrites.MarkPublication(propertyId); + return propertyId switch + { + _ => unchecked((int)0x80070057), + }; + } + + public int SetModel(int propertyId, IAvnRustViewModel? model) => propertyId switch + { + _ => unchecked((int)0x80070057), + }; + + public int AddModel(int collectionId, IAvnRustViewModel? model) => collectionId switch + { + _ => unchecked((int)0x80070057), + }; + + public int InsertString(int collectionId, int index, string? value) => collectionId switch + { + _ => unchecked((int)0x80070057), + }; + + public int InsertModel(int collectionId, int index, IAvnRustViewModel? model) => collectionId switch + { + _ => unchecked((int)0x80070057), + }; + + public int ReplaceString(int collectionId, int index, string? value) => collectionId switch + { + _ => unchecked((int)0x80070057), + }; + + public int ReplaceModel(int collectionId, int index, IAvnRustViewModel? model) => collectionId switch + { + _ => unchecked((int)0x80070057), + }; + + public int RemoveAt(int collectionId, int index) => collectionId switch + { + _ => unchecked((int)0x80070057), + }; + + public int MoveItem(int collectionId, int fromIndex, int toIndex) => collectionId switch + { + _ => unchecked((int)0x80070057), + }; + + public int ClearCollection(int collectionId) => collectionId switch + { + _ => unchecked((int)0x80070057), + }; + + public int SetCommandEnabled(int commandId, int enabled) => commandId switch + { + 1 => Apply(() => OpenCommand.SetEnabled(enabled != 0)), + _ => unchecked((int)0x80070057), + }; + + public int SetPropertyError(int propertyId, string? message) => propertyId switch + { + 1 => Apply(() => SetError(nameof(Key), message)), + 2 => Apply(() => SetError(nameof(Path), message)), + 3 => Apply(() => SetError(nameof(Name), message)), + 4 => Apply(() => SetError(nameof(Kind), message)), + 5 => Apply(() => SetError(nameof(SizeLabel), message)), + 6 => Apply(() => SetError(nameof(PackedLabel), message)), + 7 => Apply(() => SetError(nameof(Modified), message)), + 8 => Apply(() => SetError(nameof(IsSelected), message)), + 9 => Apply(() => SetError(nameof(CrcLabel), message)), + 10 => Apply(() => SetError(nameof(RatioLabel), message)), + _ => unchecked((int)0x80070057), + }; + + public int AddString(int collectionId, string? value) => collectionId switch + { + _ => unchecked((int)0x80070057), + }; + + public int ReplaceStringSnapshot(int collectionId, IReadOnlyList values) => collectionId switch + { + _ => unchecked((int)0x80070057), + }; + + public int ReplaceModelSnapshot(int collectionId, IReadOnlyList values) => collectionId switch + { + _ => unchecked((int)0x80070057), + }; + + bool IRustVmBatchTarget.TryGetProperty(int propertyId, out RustVmBatchProperty property) + { + property = propertyId switch + { + 1 => new RustVmBatchProperty(nameof(Key), RustVmValueWireKind.String, false, false), + 2 => new RustVmBatchProperty(nameof(Path), RustVmValueWireKind.String, false, false), + 3 => new RustVmBatchProperty(nameof(Name), RustVmValueWireKind.String, false, false), + 4 => new RustVmBatchProperty(nameof(Kind), RustVmValueWireKind.String, false, false), + 5 => new RustVmBatchProperty(nameof(SizeLabel), RustVmValueWireKind.String, false, false), + 6 => new RustVmBatchProperty(nameof(PackedLabel), RustVmValueWireKind.String, false, false), + 7 => new RustVmBatchProperty(nameof(Modified), RustVmValueWireKind.String, false, false), + 8 => new RustVmBatchProperty(nameof(IsSelected), RustVmValueWireKind.Boolean, false, false), + 9 => new RustVmBatchProperty(nameof(CrcLabel), RustVmValueWireKind.String, false, false), + 10 => new RustVmBatchProperty(nameof(RatioLabel), RustVmValueWireKind.String, false, false), + _ => default, + }; + return property.Name is not null; + } + + bool IRustVmBatchTarget.TryGetCollection(int collectionId, out RustVmBatchCollectionInfo collection) + { + collection = collectionId switch + { + _ => default, + }; + return collection.Items is not null; + } + + bool IRustVmBatchTarget.TryGetCommand(int commandId, out IRustVmBatchCommand command) + { + command = commandId switch + { + 1 => OpenCommand, + _ => null!, + }; + return command is not null; + } + + bool IRustVmBatchTarget.IsEnumValueDefined(int propertyId, long value) => propertyId switch + { + _ => false, + }; + + IDisposable IRustVmBatchTarget.CreateNestedProperty(int propertyId, IAvnRustViewModel model) => propertyId switch + { + _ => throw new ArgumentOutOfRangeException(nameof(propertyId)), + }; + + IDisposable IRustVmBatchTarget.CreateNestedElement(int collectionId, IAvnRustViewModel model) => collectionId switch + { + _ => throw new ArgumentOutOfRangeException(nameof(collectionId)), + }; + + bool IRustVmBatchTarget.CommitProperty(int propertyId, in RustVmBatchValue value, out IDisposable? replaced) + { + replaced = null; + switch (propertyId) + { + case 1: + { + var next = value.Text ?? ""; + if (Equals(_key, next)) return false; + _key = next; + return true; + } + case 2: + { + var next = value.Text ?? ""; + if (Equals(_path, next)) return false; + _path = next; + return true; + } + case 3: + { + var next = value.Text ?? ""; + if (Equals(_name, next)) return false; + _name = next; + return true; + } + case 4: + { + var next = value.Text ?? ""; + if (Equals(_kind, next)) return false; + _kind = next; + return true; + } + case 5: + { + var next = value.Text ?? ""; + if (Equals(_sizeLabel, next)) return false; + _sizeLabel = next; + return true; + } + case 6: + { + var next = value.Text ?? ""; + if (Equals(_packedLabel, next)) return false; + _packedLabel = next; + return true; + } + case 7: + { + var next = value.Text ?? ""; + if (Equals(_modified, next)) return false; + _modified = next; + return true; + } + case 8: + { + var next = value.Boolean; + if (Equals(_isSelected, next)) return false; + _isSelected = next; + return true; + } + case 9: + { + var next = value.Text ?? ""; + if (Equals(_crcLabel, next)) return false; + _crcLabel = next; + return true; + } + case 10: + { + var next = value.Text ?? ""; + if (Equals(_ratioLabel, next)) return false; + _ratioLabel = next; + return true; + } + default: return false; + } + } + + bool IRustVmBatchTarget.CommitError(string propertyName, string? message) => + RustVmBatchErrors.Set(_errors, propertyName, message); + + bool IRustVmTableSelectionBatchTarget.IsPostCollectionPropertyNotification(string propertyName, IReadOnlySet changedCollections) => propertyName switch + { + _ => false, + }; + + void IRustVmBatchTarget.RaisePropertyChanged(string propertyName) => + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + + void IRustVmBatchTarget.RaiseErrorsChanged(string propertyName) => + ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName)); + + /// + /// Enqueues one immutable batch. This call never reads the batch, applies it, + /// or completes it on the submitting (Rust worker) stack. + /// + public int SubmitBatch(IAvnRustVmUpdateBatch? batch) => _batch.Submit(batch); + + /// + /// Detaches the model and disposes every nested adapter exactly once. When a + /// batch notification triggers this re-entrantly, the gate defers the cleanup + /// until the batch's commit and notifications have finished. + /// + public void Dispose() => _batch.Dispose(DisposeCore); + + private void DisposeCore() + { + try + { + Check(_model.Detach()); + } + finally + { + DisposeNestedAdapters(); + } + } + + private void DisposeNestedAdapters() + { + } + + private static void TryDispose(IDisposable? value) + { + try { value?.Dispose(); } + catch { } + } + + private int Apply(Action action) => Apply(() => + { + action(); + return 0; + }); + + private int Apply(Func action) + { + if (_batch.IsClosed) return 0; + var hresult = 0; + void ApplyIfAlive() + { + if (!_batch.IsClosed) + { + try { hresult = action(); } + catch { hresult = unchecked((int)0x80004005); } + } + } + try { _dispatch(ApplyIfAlive); } + catch { return unchecked((int)0x80004005); } + return hresult; + } + + private static void Dispatch(Action action) + { + if (Dispatcher.UIThread.CheckAccess()) action(); + else Dispatcher.UIThread.Invoke(action); + } + + private static void Check(int hresult) + { + if (hresult < 0) Marshal.ThrowExceptionForHR(hresult); + } + + private void SetField(ref T field, T value, [CallerMemberName] string? propertyName = null) + { + if (Equals(field, value)) return; + field = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + private void SetError(string propertyName, string? message) + { + if (RustVmBatchErrors.Set(_errors, propertyName, message)) + ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName)); + } + + public sealed class DelegateCommand(Action execute) : ICommand, IRustVmBatchCommand + { + private bool _canExecute = true; + + public DelegateCommand(Action execute) : this(_ => execute()) { } + + public event EventHandler? CanExecuteChanged; + public bool CanExecute(object? parameter) => _canExecute; + public void Execute(object? parameter) => execute(parameter); + + public void SetEnabled(bool value) + { + if (SetEnabledCore(value)) RaiseCanExecuteChanged(); + } + + public bool SetEnabledCore(bool enabled) + { + if (_canExecute == enabled) return false; + _canExecute = enabled; + return true; + } + + public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty); + } +} diff --git a/apps/archive-tool/managed/Generated/EntryRowViewModelMetadata.g.cs b/apps/archive-tool/managed/Generated/EntryRowViewModelMetadata.g.cs new file mode 100644 index 0000000..84e09d6 --- /dev/null +++ b/apps/archive-tool/managed/Generated/EntryRowViewModelMetadata.g.cs @@ -0,0 +1,32 @@ +// +#nullable enable +using System.Collections.Generic; +using Avalonia.Rust; + +namespace ArchiveTool.Presentation.Generated; + +public static class EntryRowViewModelMetadata +{ + public static RustViewModelDescriptor Descriptor { get; } = new( + 2, + "EntryRowViewModel", + [ + new(1, "Key", RustViewModelValueKind.String, false, false, "", null), + new(2, "Path", RustViewModelValueKind.String, false, false, "", null), + new(3, "Name", RustViewModelValueKind.String, false, false, "", null), + new(4, "Kind", RustViewModelValueKind.String, false, false, "", null), + new(5, "SizeLabel", RustViewModelValueKind.String, false, false, "", null), + new(6, "PackedLabel", RustViewModelValueKind.String, false, false, "", null), + new(7, "Modified", RustViewModelValueKind.String, false, false, "", null), + new(8, "IsSelected", RustViewModelValueKind.Boolean, true, false, false, null), + new(9, "CrcLabel", RustViewModelValueKind.String, false, false, "", null), + new(10, "RatioLabel", RustViewModelValueKind.String, false, false, "", null), + ], + [ + ], + [ + new(1, "OpenCommand", false, null, false, null, false, false), + ], + [ + ]); +} diff --git a/apps/archive-tool/managed/Generated/MainViewModelAdapter.g.cs b/apps/archive-tool/managed/Generated/MainViewModelAdapter.g.cs new file mode 100644 index 0000000..b90e4fc --- /dev/null +++ b/apps/archive-tool/managed/Generated/MainViewModelAdapter.g.cs @@ -0,0 +1,866 @@ +// +#nullable enable +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.Marshalling; +using System.Windows.Input; +using Avalonia.Rust; +using Avalonia.Rust.Interop; +using Avalonia.Threading; + +namespace ArchiveTool.Presentation.Generated; + +[GeneratedComClass] +public sealed partial class MainViewModelAdapter : IAvnRustVmSink, IAvnRustVmSink2, IAvnRustVmSink3, IRustVmStringSnapshotSink, IRustVmModelSnapshotSink, IRustVmBatchTarget, IRustVmTableSelectionBatchTarget, INotifyPropertyChanged, INotifyDataErrorInfo, IDisposable +{ + private readonly IAvnRustViewModel _model; + private readonly Action _dispatch; + private readonly Action? _post; + private readonly RustVmBatchCoordinator _batch; + private readonly Dictionary _errors = new(StringComparer.Ordinal); + private readonly RustVmInboundWriteTracker _inboundWrites = new(); + private string _title = "Archive Explorer"; + private string _status = "Open an archive to get started."; + private string _archiveName = ""; + private string _archiveKind = ""; + private string _entryCountLabel = "No archive loaded"; + private string _filterText = ""; + private long _selectedIndex = -1L; + private string _selectedKey = ""; + private string _sortDirection = "Name Ascending"; + private bool _isLoading = false; + private bool _canExtractSelected = false; + private bool _canExtractAll = false; + private string _selectedCountLabel = "0 selected"; + private string _currentPath = ""; + private bool _canGoUp = false; + private string _totalsLabel = ""; + private bool _openAfterExtract = true; + private bool _canTest = false; + + /// Creates an adapter that dispatches and posts through . + public MainViewModelAdapter(IAvnRustViewModel model) : this(model, null, null) { } + + /// + /// Creates an adapter with a custom synchronous dispatch for the legacy v1/v2 + /// sink path. Kept as a distinct CLR signature (not an optional parameter) so + /// already-compiled callers keep binding to it. + /// + public MainViewModelAdapter(IAvnRustViewModel model, Action? dispatch) : this(model, dispatch, null) { } + + /// + /// Creates an adapter with a custom synchronous for the + /// legacy v1/v2 sink path and a custom nonblocking for + /// batch submission. + /// + public MainViewModelAdapter(IAvnRustViewModel model, Action? dispatch, Action? post) + { + _model = model; + _dispatch = dispatch ?? Dispatch; + _post = post; + _batch = new RustVmBatchCoordinator(this, post); + OpenFileCommand = new DelegateCommand(parameter => Check(_model.BeginAsync(1, null))); + ExtractSelectedCommand = new DelegateCommand(parameter => Check(_model.BeginAsync(2, null))); + ExtractAllCommand = new DelegateCommand(parameter => Check(_model.BeginAsync(3, null))); + SelectAllCommand = new DelegateCommand(parameter => Check(_model.Execute(4, null))); + ClearSelectionCommand = new DelegateCommand(parameter => Check(_model.Execute(5, null))); + SortEntriesCommand = new DelegateCommand(parameter => Check(_model.Execute(6, parameter as string))); + OpenRecentFileCommand = new DelegateCommand(parameter => Check(_model.Execute(7, parameter as string))); + ExitCommand = new DelegateCommand(parameter => Check(_model.Execute(8, null))); + GoUpCommand = new DelegateCommand(parameter => Check(_model.Execute(9, null))); + OpenItemCommand = new DelegateCommand(parameter => Check(_model.BeginAsync(10, null))); + ExtractHereCommand = new DelegateCommand(parameter => Check(_model.BeginAsync(11, null))); + TestArchiveCommand = new DelegateCommand(parameter => Check(_model.BeginAsync(12, null))); + NewArchiveCommand = new DelegateCommand(parameter => Check(_model.BeginAsync(13, null))); + InvertSelectionCommand = new DelegateCommand(parameter => Check(_model.Execute(14, null))); + CopyPathCommand = new DelegateCommand(parameter => Check(_model.BeginAsync(15, null))); + try + { + Check(_model.Attach(this)); + } + catch + { + try { _model.Detach(); } + catch { } + DisposeNestedAdapters(); + throw; + } + } + + public event PropertyChangedEventHandler? PropertyChanged; + public event EventHandler? ErrorsChanged; + + public string Title + { + get => _title; + } + + public string Status + { + get => _status; + } + + public string ArchiveName + { + get => _archiveName; + } + + public string ArchiveKind + { + get => _archiveKind; + } + + public string EntryCountLabel + { + get => _entryCountLabel; + } + + public string FilterText + { + get => _filterText; + set + { + var accepted = value ?? ""; + if (Equals(_filterText, accepted)) + return; + var previous = _filterText; + var inbound = _inboundWrites.Begin(6); + try + { + Check(_model.SetString(6, accepted)); + if (!_inboundWrites.WasPublished(inbound)) + { + _inboundWrites.CommitLocal(6); + SetField(ref _filterText, accepted, nameof(FilterText)); + } + } + catch + { + if (_inboundWrites.ShouldRollback(inbound)) + { + _inboundWrites.CommitLocal(6); + SetField(ref _filterText, previous, nameof(FilterText)); + } + throw; + } + finally { _inboundWrites.End(inbound); } + } + } + + public long SelectedIndex + { + get => _selectedIndex; + set + { + var accepted = value; + if (Equals(_selectedIndex, accepted)) + return; + var previous = _selectedIndex; + var inbound = _inboundWrites.Begin(7); + try + { + Check(_model.SetInteger(7, accepted)); + if (!_inboundWrites.WasPublished(inbound)) + { + _inboundWrites.CommitLocal(7); + SetField(ref _selectedIndex, accepted, nameof(SelectedIndex)); + } + } + catch + { + if (_inboundWrites.ShouldRollback(inbound)) + { + _inboundWrites.CommitLocal(7); + SetField(ref _selectedIndex, previous, nameof(SelectedIndex)); + } + throw; + } + finally { _inboundWrites.End(inbound); } + } + } + + public string SelectedKey + { + get => _selectedKey; + set + { + var accepted = value ?? ""; + if (Equals(_selectedKey, accepted)) + return; + var previous = _selectedKey; + var inbound = _inboundWrites.Begin(8); + try + { + Check(_model.SetString(8, accepted)); + if (!_inboundWrites.WasPublished(inbound)) + { + _inboundWrites.CommitLocal(8); + SetField(ref _selectedKey, accepted, nameof(SelectedKey)); + } + } + catch + { + if (_inboundWrites.ShouldRollback(inbound)) + { + _inboundWrites.CommitLocal(8); + SetField(ref _selectedKey, previous, nameof(SelectedKey)); + } + throw; + } + finally { _inboundWrites.End(inbound); } + } + } + + public string SortDirection + { + get => _sortDirection; + } + + public bool IsLoading + { + get => _isLoading; + } + + public bool CanExtractSelected + { + get => _canExtractSelected; + } + + public bool CanExtractAll + { + get => _canExtractAll; + } + + public string SelectedCountLabel + { + get => _selectedCountLabel; + } + + public string CurrentPath + { + get => _currentPath; + } + + public bool CanGoUp + { + get => _canGoUp; + } + + public string TotalsLabel + { + get => _totalsLabel; + } + + public bool OpenAfterExtract + { + get => _openAfterExtract; + set + { + var accepted = value; + if (Equals(_openAfterExtract, accepted)) + return; + var previous = _openAfterExtract; + var inbound = _inboundWrites.Begin(17); + try + { + Check(_model.SetBoolean(17, (accepted ? 1 : 0))); + if (!_inboundWrites.WasPublished(inbound)) + { + _inboundWrites.CommitLocal(17); + SetField(ref _openAfterExtract, accepted, nameof(OpenAfterExtract)); + } + } + catch + { + if (_inboundWrites.ShouldRollback(inbound)) + { + _inboundWrites.CommitLocal(17); + SetField(ref _openAfterExtract, previous, nameof(OpenAfterExtract)); + } + throw; + } + finally { _inboundWrites.End(inbound); } + } + } + + public bool CanTest + { + get => _canTest; + } + + public BatchObservableCollection RecentFiles { get; } = []; + public BatchObservableCollection Entries { get; } = []; + + public DelegateCommand OpenFileCommand { get; } + public DelegateCommand ExtractSelectedCommand { get; } + public DelegateCommand ExtractAllCommand { get; } + public DelegateCommand SelectAllCommand { get; } + public DelegateCommand ClearSelectionCommand { get; } + public DelegateCommand SortEntriesCommand { get; } + public DelegateCommand OpenRecentFileCommand { get; } + public DelegateCommand ExitCommand { get; } + public DelegateCommand GoUpCommand { get; } + public DelegateCommand OpenItemCommand { get; } + public DelegateCommand ExtractHereCommand { get; } + public DelegateCommand TestArchiveCommand { get; } + public DelegateCommand NewArchiveCommand { get; } + public DelegateCommand InvertSelectionCommand { get; } + public DelegateCommand CopyPathCommand { get; } + + public bool HasErrors => _errors.Count > 0; + + public IEnumerable GetErrors(string? propertyName) => + propertyName is not null && _errors.TryGetValue(propertyName, out var message) + ? new[] { message } + : Array.Empty(); + + public int SetString(int propertyId, string? value) + { + var inbound = _inboundWrites.MarkPublication(propertyId); + return propertyId switch + { + 1 => Apply(() => { var converted = value ?? ""; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_title, converted)) SetField(ref _title, converted, nameof(Title)); }), + 2 => Apply(() => { var converted = value ?? ""; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_status, converted)) SetField(ref _status, converted, nameof(Status)); }), + 3 => Apply(() => { var converted = value ?? ""; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_archiveName, converted)) SetField(ref _archiveName, converted, nameof(ArchiveName)); }), + 4 => Apply(() => { var converted = value ?? ""; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_archiveKind, converted)) SetField(ref _archiveKind, converted, nameof(ArchiveKind)); }), + 5 => Apply(() => { var converted = value ?? ""; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_entryCountLabel, converted)) SetField(ref _entryCountLabel, converted, nameof(EntryCountLabel)); }), + 6 => Apply(() => { var converted = value ?? ""; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_filterText, converted)) SetField(ref _filterText, converted, nameof(FilterText)); }), + 8 => Apply(() => { var converted = value ?? ""; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_selectedKey, converted)) SetField(ref _selectedKey, converted, nameof(SelectedKey)); }), + 9 => Apply(() => { var converted = value ?? ""; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_sortDirection, converted)) SetField(ref _sortDirection, converted, nameof(SortDirection)); }), + 13 => Apply(() => { var converted = value ?? ""; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_selectedCountLabel, converted)) SetField(ref _selectedCountLabel, converted, nameof(SelectedCountLabel)); }), + 14 => Apply(() => { var converted = value ?? ""; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_currentPath, converted)) SetField(ref _currentPath, converted, nameof(CurrentPath)); }), + 16 => Apply(() => { var converted = value ?? ""; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_totalsLabel, converted)) SetField(ref _totalsLabel, converted, nameof(TotalsLabel)); }), + _ => unchecked((int)0x80070057), + }; + } + + public int SetInteger(int propertyId, long value) + { + var inbound = _inboundWrites.MarkPublication(propertyId); + return propertyId switch + { + 7 => Apply(() => { var converted = value; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_selectedIndex, converted)) SetField(ref _selectedIndex, converted, nameof(SelectedIndex)); }), + _ => unchecked((int)0x80070057), + }; + } + + public int SetBoolean(int propertyId, int value) + { + var inbound = _inboundWrites.MarkPublication(propertyId); + return propertyId switch + { + 10 => Apply(() => { var converted = value != 0; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_isLoading, converted)) SetField(ref _isLoading, converted, nameof(IsLoading)); }), + 11 => Apply(() => { var converted = value != 0; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_canExtractSelected, converted)) SetField(ref _canExtractSelected, converted, nameof(CanExtractSelected)); }), + 12 => Apply(() => { var converted = value != 0; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_canExtractAll, converted)) SetField(ref _canExtractAll, converted, nameof(CanExtractAll)); }), + 15 => Apply(() => { var converted = value != 0; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_canGoUp, converted)) SetField(ref _canGoUp, converted, nameof(CanGoUp)); }), + 17 => Apply(() => { var converted = value != 0; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_openAfterExtract, converted)) SetField(ref _openAfterExtract, converted, nameof(OpenAfterExtract)); }), + 18 => Apply(() => { var converted = value != 0; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_canTest, converted)) SetField(ref _canTest, converted, nameof(CanTest)); }), + _ => unchecked((int)0x80070057), + }; + } + + public int SetDouble(int propertyId, double value) + { + var inbound = _inboundWrites.MarkPublication(propertyId); + return propertyId switch + { + _ => unchecked((int)0x80070057), + }; + } + + public int SetNull(int propertyId) + { + var inbound = _inboundWrites.MarkPublication(propertyId); + return propertyId switch + { + _ => unchecked((int)0x80070057), + }; + } + + public int SetModel(int propertyId, IAvnRustViewModel? model) => propertyId switch + { + _ => unchecked((int)0x80070057), + }; + + public int AddModel(int collectionId, IAvnRustViewModel? model) => collectionId switch + { + 2 => model is null ? unchecked((int)0x80070057) : Apply(() => Entries.Add(new global::ArchiveTool.Presentation.Generated.EntryRowViewModelAdapter(model, _dispatch, _post))), + _ => unchecked((int)0x80070057), + }; + + public int InsertString(int collectionId, int index, string? value) => collectionId switch + { + 1 => Apply(() => { if ((uint)index > (uint)RecentFiles.Count) return unchecked((int)0x80070057); RecentFiles.Insert(index, value ?? ""); return 0; }), + _ => unchecked((int)0x80070057), + }; + + public int InsertModel(int collectionId, int index, IAvnRustViewModel? model) => collectionId switch + { + 2 => model is null ? unchecked((int)0x80070057) : Apply(() => { if ((uint)index > (uint)Entries.Count) return unchecked((int)0x80070057); Entries.Insert(index, new global::ArchiveTool.Presentation.Generated.EntryRowViewModelAdapter(model, _dispatch, _post)); return 0; }), + _ => unchecked((int)0x80070057), + }; + + public int ReplaceString(int collectionId, int index, string? value) => collectionId switch + { + 1 => Apply(() => { if ((uint)index >= (uint)RecentFiles.Count) return unchecked((int)0x80070057); RecentFiles[index] = value ?? ""; return 0; }), + _ => unchecked((int)0x80070057), + }; + + public int ReplaceModel(int collectionId, int index, IAvnRustViewModel? model) => collectionId switch + { + 2 => model is null ? unchecked((int)0x80070057) : Apply(() => + { + if ((uint)index >= (uint)Entries.Count) return unchecked((int)0x80070057); + var previous = Entries[index]; + Entries[index] = new global::ArchiveTool.Presentation.Generated.EntryRowViewModelAdapter(model, _dispatch, _post); + previous.Dispose(); + return 0; + }), + _ => unchecked((int)0x80070057), + }; + + public int RemoveAt(int collectionId, int index) => collectionId switch + { + 1 => Apply(() => { if ((uint)index >= (uint)RecentFiles.Count) return unchecked((int)0x80070057); RecentFiles.RemoveAt(index); return 0; }), + 2 => Apply(() => + { + if ((uint)index >= (uint)Entries.Count) return unchecked((int)0x80070057); + var item = Entries[index]; + Entries.RemoveAt(index); + item.Dispose(); + return 0; + }), + _ => unchecked((int)0x80070057), + }; + + public int MoveItem(int collectionId, int fromIndex, int toIndex) => collectionId switch + { + 1 => Apply(() => { if ((uint)fromIndex >= (uint)RecentFiles.Count || (uint)toIndex >= (uint)RecentFiles.Count) return unchecked((int)0x80070057); RecentFiles.Move(fromIndex, toIndex); return 0; }), + 2 => Apply(() => { if ((uint)fromIndex >= (uint)Entries.Count || (uint)toIndex >= (uint)Entries.Count) return unchecked((int)0x80070057); Entries.Move(fromIndex, toIndex); return 0; }), + _ => unchecked((int)0x80070057), + }; + + public int ClearCollection(int collectionId) => collectionId switch + { + 1 => Apply(RecentFiles.Clear), + 2 => Apply(() => + { + foreach (var item in Entries) item.Dispose(); + Entries.Clear(); + }), + _ => unchecked((int)0x80070057), + }; + + public int SetCommandEnabled(int commandId, int enabled) => commandId switch + { + 1 => Apply(() => OpenFileCommand.SetEnabled(enabled != 0)), + 2 => Apply(() => ExtractSelectedCommand.SetEnabled(enabled != 0)), + 3 => Apply(() => ExtractAllCommand.SetEnabled(enabled != 0)), + 4 => Apply(() => SelectAllCommand.SetEnabled(enabled != 0)), + 5 => Apply(() => ClearSelectionCommand.SetEnabled(enabled != 0)), + 6 => Apply(() => SortEntriesCommand.SetEnabled(enabled != 0)), + 7 => Apply(() => OpenRecentFileCommand.SetEnabled(enabled != 0)), + 8 => Apply(() => ExitCommand.SetEnabled(enabled != 0)), + 9 => Apply(() => GoUpCommand.SetEnabled(enabled != 0)), + 10 => Apply(() => OpenItemCommand.SetEnabled(enabled != 0)), + 11 => Apply(() => ExtractHereCommand.SetEnabled(enabled != 0)), + 12 => Apply(() => TestArchiveCommand.SetEnabled(enabled != 0)), + 13 => Apply(() => NewArchiveCommand.SetEnabled(enabled != 0)), + 14 => Apply(() => InvertSelectionCommand.SetEnabled(enabled != 0)), + 15 => Apply(() => CopyPathCommand.SetEnabled(enabled != 0)), + _ => unchecked((int)0x80070057), + }; + + public int SetPropertyError(int propertyId, string? message) => propertyId switch + { + 1 => Apply(() => SetError(nameof(Title), message)), + 2 => Apply(() => SetError(nameof(Status), message)), + 3 => Apply(() => SetError(nameof(ArchiveName), message)), + 4 => Apply(() => SetError(nameof(ArchiveKind), message)), + 5 => Apply(() => SetError(nameof(EntryCountLabel), message)), + 6 => Apply(() => SetError(nameof(FilterText), message)), + 7 => Apply(() => SetError(nameof(SelectedIndex), message)), + 8 => Apply(() => SetError(nameof(SelectedKey), message)), + 9 => Apply(() => SetError(nameof(SortDirection), message)), + 10 => Apply(() => SetError(nameof(IsLoading), message)), + 11 => Apply(() => SetError(nameof(CanExtractSelected), message)), + 12 => Apply(() => SetError(nameof(CanExtractAll), message)), + 13 => Apply(() => SetError(nameof(SelectedCountLabel), message)), + 14 => Apply(() => SetError(nameof(CurrentPath), message)), + 15 => Apply(() => SetError(nameof(CanGoUp), message)), + 16 => Apply(() => SetError(nameof(TotalsLabel), message)), + 17 => Apply(() => SetError(nameof(OpenAfterExtract), message)), + 18 => Apply(() => SetError(nameof(CanTest), message)), + _ => unchecked((int)0x80070057), + }; + + public int AddString(int collectionId, string? value) => collectionId switch + { + 1 => Apply(() => RecentFiles.Add(value ?? "")), + _ => unchecked((int)0x80070057), + }; + + public int ReplaceStringSnapshot(int collectionId, IReadOnlyList values) => collectionId switch + { + 1 => Apply(() => RecentFiles.ReplaceSnapshot(values)), + _ => unchecked((int)0x80070057), + }; + + public int ReplaceModelSnapshot(int collectionId, IReadOnlyList values) => collectionId switch + { + 2 => Apply(() => + { + var staged = new List(); + try { foreach (var value in values) staged.Add(new global::ArchiveTool.Presentation.Generated.EntryRowViewModelAdapter(value, _dispatch, _post)); } + catch { foreach (var value in staged) TryDispose(value); throw; } + var previous = Entries.ToArray(); + Entries.ReplaceSnapshot(staged); + foreach (var value in previous) TryDispose(value); + }), + _ => unchecked((int)0x80070057), + }; + + bool IRustVmBatchTarget.TryGetProperty(int propertyId, out RustVmBatchProperty property) + { + property = propertyId switch + { + 1 => new RustVmBatchProperty(nameof(Title), RustVmValueWireKind.String, false, false), + 2 => new RustVmBatchProperty(nameof(Status), RustVmValueWireKind.String, false, false), + 3 => new RustVmBatchProperty(nameof(ArchiveName), RustVmValueWireKind.String, false, false), + 4 => new RustVmBatchProperty(nameof(ArchiveKind), RustVmValueWireKind.String, false, false), + 5 => new RustVmBatchProperty(nameof(EntryCountLabel), RustVmValueWireKind.String, false, false), + 6 => new RustVmBatchProperty(nameof(FilterText), RustVmValueWireKind.String, false, false), + 7 => new RustVmBatchProperty(nameof(SelectedIndex), RustVmValueWireKind.Integer, false, false), + 8 => new RustVmBatchProperty(nameof(SelectedKey), RustVmValueWireKind.String, false, false), + 9 => new RustVmBatchProperty(nameof(SortDirection), RustVmValueWireKind.String, false, false), + 10 => new RustVmBatchProperty(nameof(IsLoading), RustVmValueWireKind.Boolean, false, false), + 11 => new RustVmBatchProperty(nameof(CanExtractSelected), RustVmValueWireKind.Boolean, false, false), + 12 => new RustVmBatchProperty(nameof(CanExtractAll), RustVmValueWireKind.Boolean, false, false), + 13 => new RustVmBatchProperty(nameof(SelectedCountLabel), RustVmValueWireKind.String, false, false), + 14 => new RustVmBatchProperty(nameof(CurrentPath), RustVmValueWireKind.String, false, false), + 15 => new RustVmBatchProperty(nameof(CanGoUp), RustVmValueWireKind.Boolean, false, false), + 16 => new RustVmBatchProperty(nameof(TotalsLabel), RustVmValueWireKind.String, false, false), + 17 => new RustVmBatchProperty(nameof(OpenAfterExtract), RustVmValueWireKind.Boolean, false, false), + 18 => new RustVmBatchProperty(nameof(CanTest), RustVmValueWireKind.Boolean, false, false), + _ => default, + }; + return property.Name is not null; + } + + bool IRustVmBatchTarget.TryGetCollection(int collectionId, out RustVmBatchCollectionInfo collection) + { + collection = collectionId switch + { + 1 => new RustVmBatchCollectionInfo(nameof(RecentFiles), RustVmValueWireKind.String, RecentFiles), + 2 => new RustVmBatchCollectionInfo(nameof(Entries), RustVmValueWireKind.Model, Entries), + _ => default, + }; + return collection.Items is not null; + } + + bool IRustVmBatchTarget.TryGetCommand(int commandId, out IRustVmBatchCommand command) + { + command = commandId switch + { + 1 => OpenFileCommand, + 2 => ExtractSelectedCommand, + 3 => ExtractAllCommand, + 4 => SelectAllCommand, + 5 => ClearSelectionCommand, + 6 => SortEntriesCommand, + 7 => OpenRecentFileCommand, + 8 => ExitCommand, + 9 => GoUpCommand, + 10 => OpenItemCommand, + 11 => ExtractHereCommand, + 12 => TestArchiveCommand, + 13 => NewArchiveCommand, + 14 => InvertSelectionCommand, + 15 => CopyPathCommand, + _ => null!, + }; + return command is not null; + } + + bool IRustVmBatchTarget.IsEnumValueDefined(int propertyId, long value) => propertyId switch + { + _ => false, + }; + + IDisposable IRustVmBatchTarget.CreateNestedProperty(int propertyId, IAvnRustViewModel model) => propertyId switch + { + _ => throw new ArgumentOutOfRangeException(nameof(propertyId)), + }; + + IDisposable IRustVmBatchTarget.CreateNestedElement(int collectionId, IAvnRustViewModel model) => collectionId switch + { + 2 => new global::ArchiveTool.Presentation.Generated.EntryRowViewModelAdapter(model, _dispatch, _post), + _ => throw new ArgumentOutOfRangeException(nameof(collectionId)), + }; + + bool IRustVmBatchTarget.CommitProperty(int propertyId, in RustVmBatchValue value, out IDisposable? replaced) + { + replaced = null; + switch (propertyId) + { + case 1: + { + var next = value.Text ?? ""; + if (Equals(_title, next)) return false; + _title = next; + return true; + } + case 2: + { + var next = value.Text ?? ""; + if (Equals(_status, next)) return false; + _status = next; + return true; + } + case 3: + { + var next = value.Text ?? ""; + if (Equals(_archiveName, next)) return false; + _archiveName = next; + return true; + } + case 4: + { + var next = value.Text ?? ""; + if (Equals(_archiveKind, next)) return false; + _archiveKind = next; + return true; + } + case 5: + { + var next = value.Text ?? ""; + if (Equals(_entryCountLabel, next)) return false; + _entryCountLabel = next; + return true; + } + case 6: + { + var next = value.Text ?? ""; + if (Equals(_filterText, next)) return false; + _filterText = next; + return true; + } + case 7: + { + var next = value.Integer; + if (Equals(_selectedIndex, next)) return false; + _selectedIndex = next; + return true; + } + case 8: + { + var next = value.Text ?? ""; + if (Equals(_selectedKey, next)) return false; + _selectedKey = next; + return true; + } + case 9: + { + var next = value.Text ?? ""; + if (Equals(_sortDirection, next)) return false; + _sortDirection = next; + return true; + } + case 10: + { + var next = value.Boolean; + if (Equals(_isLoading, next)) return false; + _isLoading = next; + return true; + } + case 11: + { + var next = value.Boolean; + if (Equals(_canExtractSelected, next)) return false; + _canExtractSelected = next; + return true; + } + case 12: + { + var next = value.Boolean; + if (Equals(_canExtractAll, next)) return false; + _canExtractAll = next; + return true; + } + case 13: + { + var next = value.Text ?? ""; + if (Equals(_selectedCountLabel, next)) return false; + _selectedCountLabel = next; + return true; + } + case 14: + { + var next = value.Text ?? ""; + if (Equals(_currentPath, next)) return false; + _currentPath = next; + return true; + } + case 15: + { + var next = value.Boolean; + if (Equals(_canGoUp, next)) return false; + _canGoUp = next; + return true; + } + case 16: + { + var next = value.Text ?? ""; + if (Equals(_totalsLabel, next)) return false; + _totalsLabel = next; + return true; + } + case 17: + { + var next = value.Boolean; + if (Equals(_openAfterExtract, next)) return false; + _openAfterExtract = next; + return true; + } + case 18: + { + var next = value.Boolean; + if (Equals(_canTest, next)) return false; + _canTest = next; + return true; + } + default: return false; + } + } + + bool IRustVmBatchTarget.CommitError(string propertyName, string? message) => + RustVmBatchErrors.Set(_errors, propertyName, message); + + bool IRustVmTableSelectionBatchTarget.IsPostCollectionPropertyNotification(string propertyName, IReadOnlySet changedCollections) => propertyName switch + { + nameof(SelectedIndex) => changedCollections.Contains(nameof(Entries)), + nameof(SelectedKey) => changedCollections.Contains(nameof(Entries)), + _ => false, + }; + + void IRustVmBatchTarget.RaisePropertyChanged(string propertyName) => + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + + void IRustVmBatchTarget.RaiseErrorsChanged(string propertyName) => + ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName)); + + /// + /// Enqueues one immutable batch. This call never reads the batch, applies it, + /// or completes it on the submitting (Rust worker) stack. + /// + public int SubmitBatch(IAvnRustVmUpdateBatch? batch) => _batch.Submit(batch); + + /// + /// Detaches the model and disposes every nested adapter exactly once. When a + /// batch notification triggers this re-entrantly, the gate defers the cleanup + /// until the batch's commit and notifications have finished. + /// + public void Dispose() => _batch.Dispose(DisposeCore); + + private void DisposeCore() + { + try + { + Check(_model.Detach()); + } + finally + { + DisposeNestedAdapters(); + } + } + + private void DisposeNestedAdapters() + { + foreach (var item in Entries) TryDispose(item); + } + + private static void TryDispose(IDisposable? value) + { + try { value?.Dispose(); } + catch { } + } + + private int Apply(Action action) => Apply(() => + { + action(); + return 0; + }); + + private int Apply(Func action) + { + if (_batch.IsClosed) return 0; + var hresult = 0; + void ApplyIfAlive() + { + if (!_batch.IsClosed) + { + try { hresult = action(); } + catch { hresult = unchecked((int)0x80004005); } + } + } + try { _dispatch(ApplyIfAlive); } + catch { return unchecked((int)0x80004005); } + return hresult; + } + + private static void Dispatch(Action action) + { + if (Dispatcher.UIThread.CheckAccess()) action(); + else Dispatcher.UIThread.Invoke(action); + } + + private static void Check(int hresult) + { + if (hresult < 0) Marshal.ThrowExceptionForHR(hresult); + } + + private void SetField(ref T field, T value, [CallerMemberName] string? propertyName = null) + { + if (Equals(field, value)) return; + field = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + private void SetError(string propertyName, string? message) + { + if (RustVmBatchErrors.Set(_errors, propertyName, message)) + ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName)); + } + + public sealed class DelegateCommand(Action execute) : ICommand, IRustVmBatchCommand + { + private bool _canExecute = true; + + public DelegateCommand(Action execute) : this(_ => execute()) { } + + public event EventHandler? CanExecuteChanged; + public bool CanExecute(object? parameter) => _canExecute; + public void Execute(object? parameter) => execute(parameter); + + public void SetEnabled(bool value) + { + if (SetEnabledCore(value)) RaiseCanExecuteChanged(); + } + + public bool SetEnabledCore(bool enabled) + { + if (_canExecute == enabled) return false; + _canExecute = enabled; + return true; + } + + public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty); + } +} diff --git a/apps/archive-tool/managed/Generated/MainViewModelMenus.g.cs b/apps/archive-tool/managed/Generated/MainViewModelMenus.g.cs new file mode 100644 index 0000000..27c9d19 --- /dev/null +++ b/apps/archive-tool/managed/Generated/MainViewModelMenus.g.cs @@ -0,0 +1,222 @@ +// +#nullable enable +using System; +using System.Collections.Generic; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Rust; + +namespace ArchiveTool.Presentation.Generated; + +/// +/// Generated menus, keyboard accelerators and recent-file projection for MainViewModel. +/// +public static class MainViewModelMenus +{ + /// Maximum recent-file entries projected into a submenu. + public const int RecentFilesCapacity = 5; + + /// Placeholder header shown while the recent-file list is empty. + public const string RecentFilesEmptyHeader = "(no recent files)"; + + /// + /// Builds the Main application menu, appending every declared + /// accelerator to . + /// + public static NativeMenu CreateMain(MainViewModelAdapter model, RustMenuScope scope, IList? keyBindings = null) + { + ArgumentNullException.ThrowIfNull(model); + ArgumentNullException.ThrowIfNull(scope); + var menu = new NativeMenu(); + var fileItem = new NativeMenuItem("_File"); + var fileMenu = new NativeMenu(); + fileItem.Menu = fileMenu; + var openFileCommand = new RustMenuCommand(parameter => { model.OpenFileCommand.Execute(parameter); }, () => model.OpenFileCommand.CanExecute(null)); + openFileCommand.TrackSource(model.OpenFileCommand, scope); + var openFileItem = new NativeMenuItem("_Open archive...") { Command = openFileCommand }; + openFileItem.Gesture = RustMenu.ParseGesture("Ctrl+O"); + fileMenu.Items.Add(openFileItem); + keyBindings?.Add(new KeyBinding { Gesture = RustMenu.ParseGesture("Ctrl+O")!, Command = openFileCommand }); + var newArchiveCommand = new RustMenuCommand(parameter => { model.NewArchiveCommand.Execute(parameter); }, () => model.NewArchiveCommand.CanExecute(null)); + newArchiveCommand.TrackSource(model.NewArchiveCommand, scope); + var newArchiveItem = new NativeMenuItem("_New archive...") { Command = newArchiveCommand }; + newArchiveItem.Gesture = RustMenu.ParseGesture("Ctrl+N"); + fileMenu.Items.Add(newArchiveItem); + keyBindings?.Add(new KeyBinding { Gesture = RustMenu.ParseGesture("Ctrl+N")!, Command = newArchiveCommand }); + var recentItem = new NativeMenuItem("Recent _files"); + var recentMenu = new NativeMenu(); + recentItem.Menu = recentMenu; + var recentActivate = new RustMenuCommand(parameter => model.OpenRecentFileCommand.Execute(parameter)); + recentActivate.TrackSource(model.OpenRecentFileCommand, scope); + scope.ObserveCollection(model.RecentFiles, () => RustMenu.FillRecentFiles(recentMenu.Items, model.RecentFiles, recentActivate, RecentFilesEmptyHeader, RecentFilesCapacity)); + fileMenu.Items.Add(recentItem); + fileMenu.Items.Add(new NativeMenuItemSeparator()); + var extractSelectedCommand = new RustMenuCommand(parameter => { model.ExtractSelectedCommand.Execute(parameter); }, () => model.ExtractSelectedCommand.CanExecute(null)); + extractSelectedCommand.TrackSource(model.ExtractSelectedCommand, scope); + var extractSelectedItem = new NativeMenuItem("_Extract selected...") { Command = extractSelectedCommand }; + extractSelectedItem.Gesture = RustMenu.ParseGesture("Ctrl+E"); + fileMenu.Items.Add(extractSelectedItem); + keyBindings?.Add(new KeyBinding { Gesture = RustMenu.ParseGesture("Ctrl+E")!, Command = extractSelectedCommand }); + var extractHereCommand = new RustMenuCommand(parameter => { model.ExtractHereCommand.Execute(parameter); }, () => model.ExtractHereCommand.CanExecute(null)); + extractHereCommand.TrackSource(model.ExtractHereCommand, scope); + var extractHereItem = new NativeMenuItem("Extract _here") { Command = extractHereCommand }; + extractHereItem.Gesture = RustMenu.ParseGesture("Ctrl+Shift+H"); + fileMenu.Items.Add(extractHereItem); + keyBindings?.Add(new KeyBinding { Gesture = RustMenu.ParseGesture("Ctrl+Shift+H")!, Command = extractHereCommand }); + var extractAllCommand = new RustMenuCommand(parameter => { model.ExtractAllCommand.Execute(parameter); }, () => model.ExtractAllCommand.CanExecute(null)); + extractAllCommand.TrackSource(model.ExtractAllCommand, scope); + var extractAllItem = new NativeMenuItem("Extract _all...") { Command = extractAllCommand }; + extractAllItem.Gesture = RustMenu.ParseGesture("Ctrl+Shift+E"); + fileMenu.Items.Add(extractAllItem); + keyBindings?.Add(new KeyBinding { Gesture = RustMenu.ParseGesture("Ctrl+Shift+E")!, Command = extractAllCommand }); + var testArchiveCommand = new RustMenuCommand(parameter => { model.TestArchiveCommand.Execute(parameter); }, () => model.TestArchiveCommand.CanExecute(null)); + testArchiveCommand.TrackSource(model.TestArchiveCommand, scope); + var testArchiveItem = new NativeMenuItem("_Test archive") { Command = testArchiveCommand }; + testArchiveItem.Gesture = RustMenu.ParseGesture("Ctrl+T"); + fileMenu.Items.Add(testArchiveItem); + keyBindings?.Add(new KeyBinding { Gesture = RustMenu.ParseGesture("Ctrl+T")!, Command = testArchiveCommand }); + fileMenu.Items.Add(new NativeMenuItemSeparator()); + var exitCommand = new RustMenuCommand(parameter => { model.ExitCommand.Execute(parameter); }, () => model.ExitCommand.CanExecute(null)); + exitCommand.TrackSource(model.ExitCommand, scope); + var exitItem = new NativeMenuItem("E_xit") { Command = exitCommand }; + exitItem.Gesture = RustMenu.ParseGesture("Ctrl+Q"); + fileMenu.Items.Add(exitItem); + keyBindings?.Add(new KeyBinding { Gesture = RustMenu.ParseGesture("Ctrl+Q")!, Command = exitCommand }); + menu.Items.Add(fileItem); + var editItem = new NativeMenuItem("_Edit"); + var editMenu = new NativeMenu(); + editItem.Menu = editMenu; + var openItemCommand = new RustMenuCommand(parameter => { model.OpenItemCommand.Execute(parameter); }, () => model.OpenItemCommand.CanExecute(null)); + openItemCommand.TrackSource(model.OpenItemCommand, scope); + var openItemItem = new NativeMenuItem("O_pen") { Command = openItemCommand }; + openItemItem.Gesture = RustMenu.ParseGesture("Enter"); + editMenu.Items.Add(openItemItem); + keyBindings?.Add(new KeyBinding { Gesture = RustMenu.ParseGesture("Enter")!, Command = openItemCommand }); + var goUpCommand = new RustMenuCommand(parameter => { model.GoUpCommand.Execute(parameter); }, () => model.GoUpCommand.CanExecute(null)); + goUpCommand.TrackSource(model.GoUpCommand, scope); + var goUpItem = new NativeMenuItem("Go _up") { Command = goUpCommand }; + goUpItem.Gesture = RustMenu.ParseGesture("Alt+Up"); + editMenu.Items.Add(goUpItem); + keyBindings?.Add(new KeyBinding { Gesture = RustMenu.ParseGesture("Alt+Up")!, Command = goUpCommand }); + editMenu.Items.Add(new NativeMenuItemSeparator()); + var selectAllCommand = new RustMenuCommand(parameter => { model.SelectAllCommand.Execute(parameter); }, () => model.SelectAllCommand.CanExecute(null)); + selectAllCommand.TrackSource(model.SelectAllCommand, scope); + var selectAllItem = new NativeMenuItem("Select _all") { Command = selectAllCommand }; + selectAllItem.Gesture = RustMenu.ParseGesture("Ctrl+A"); + editMenu.Items.Add(selectAllItem); + keyBindings?.Add(new KeyBinding { Gesture = RustMenu.ParseGesture("Ctrl+A")!, Command = selectAllCommand }); + var invertSelectionCommand = new RustMenuCommand(parameter => { model.InvertSelectionCommand.Execute(parameter); }, () => model.InvertSelectionCommand.CanExecute(null)); + invertSelectionCommand.TrackSource(model.InvertSelectionCommand, scope); + var invertSelectionItem = new NativeMenuItem("_Invert selection") { Command = invertSelectionCommand }; + editMenu.Items.Add(invertSelectionItem); + var clearSelectionCommand = new RustMenuCommand(parameter => { model.ClearSelectionCommand.Execute(parameter); }, () => model.ClearSelectionCommand.CanExecute(null)); + clearSelectionCommand.TrackSource(model.ClearSelectionCommand, scope); + var clearSelectionItem = new NativeMenuItem("_Clear selection") { Command = clearSelectionCommand }; + editMenu.Items.Add(clearSelectionItem); + var copyPathCommand = new RustMenuCommand(parameter => { model.CopyPathCommand.Execute(parameter); }, () => model.CopyPathCommand.CanExecute(null)); + copyPathCommand.TrackSource(model.CopyPathCommand, scope); + var copyPathItem = new NativeMenuItem("Copy _path") { Command = copyPathCommand }; + copyPathItem.Gesture = RustMenu.ParseGesture("Ctrl+C"); + editMenu.Items.Add(copyPathItem); + keyBindings?.Add(new KeyBinding { Gesture = RustMenu.ParseGesture("Ctrl+C")!, Command = copyPathCommand }); + menu.Items.Add(editItem); + var viewItem = new NativeMenuItem("_View"); + var viewMenu = new NativeMenu(); + viewItem.Menu = viewMenu; + var openAfterExtractCommand = new RustMenuCommand(parameter => { model.OpenAfterExtract = !model.OpenAfterExtract; }, null); + var openAfterExtractItem = new NativeMenuItem("Open folder after e_xtract") { Command = openAfterExtractCommand }; + openAfterExtractItem.ToggleType = MenuItemToggleType.CheckBox; + scope.Observe("OpenAfterExtract", () => openAfterExtractItem.IsChecked = model.OpenAfterExtract); + viewMenu.Items.Add(openAfterExtractItem); + menu.Items.Add(viewItem); + return menu; + } + + /// + /// Attaches the Main menu and its accelerators to a top-level. + /// + /// + /// The menu is exported natively where the platform has a menu bar (macOS) + /// and rendered in-window by a NativeMenuBar control everywhere else; + /// both read the same attached value, so there is no platform branch. Only a + /// real native menu bar handles its own shortcuts, so the declared gestures + /// are additionally installed as key bindings on the top-level. + /// + public static RustMenuAttachment AttachMain(TopLevel target, MainViewModelAdapter model) + { + ArgumentNullException.ThrowIfNull(target); + ArgumentNullException.ThrowIfNull(model); + var scope = new RustMenuScope(model); + try + { + var keyBindings = new List(); + var menu = CreateMain(model, scope, keyBindings); + return RustMenuAttachment.Attach(target, menu, keyBindings, scope); + } + catch + { + scope.Dispose(); + throw; + } + } + + /// + /// Builds the items of the Entries context menu. + /// + public static IReadOnlyList CreateEntriesItems(MainViewModelAdapter model, RustMenuScope scope) + { + ArgumentNullException.ThrowIfNull(model); + ArgumentNullException.ThrowIfNull(scope); + var items = new List(); + var openRowCommand = new RustMenuCommand(parameter => { model.OpenItemCommand.Execute(parameter); }, () => model.OpenItemCommand.CanExecute(null)); + openRowCommand.TrackSource(model.OpenItemCommand, scope); + var openRowItem = new MenuItem { Header = "Open", Command = openRowCommand }; + items.Add(openRowItem); + var extractRowCommand = new RustMenuCommand(parameter => { model.ExtractSelectedCommand.Execute(parameter); }, () => model.ExtractSelectedCommand.CanExecute(null)); + extractRowCommand.TrackSource(model.ExtractSelectedCommand, scope); + var extractRowItem = new MenuItem { Header = "Extract...", Command = extractRowCommand }; + items.Add(extractRowItem); + var extractHereRowCommand = new RustMenuCommand(parameter => { model.ExtractHereCommand.Execute(parameter); }, () => model.ExtractHereCommand.CanExecute(null)); + extractHereRowCommand.TrackSource(model.ExtractHereCommand, scope); + var extractHereRowItem = new MenuItem { Header = "Extract here", Command = extractHereRowCommand }; + items.Add(extractHereRowItem); + items.Add(new Separator()); + var copyRowCommand = new RustMenuCommand(parameter => { model.CopyPathCommand.Execute(parameter); }, () => model.CopyPathCommand.CanExecute(null)); + copyRowCommand.TrackSource(model.CopyPathCommand, scope); + var copyRowItem = new MenuItem { Header = "Copy path", Command = copyRowCommand }; + items.Add(copyRowItem); + return items; + } + +} + +/// +/// Generated context menu Entries for MainViewModel. +/// +/// +/// Declared directly in compiled AXAML - for example +/// <TableView.ContextMenu><vm:MainViewModelEntriesContextMenu /></TableView.ContextMenu>. +/// A context menu is parented to the control it is attached to, so it inherits +/// that control's data context and binds itself when the data context arrives. +/// +public sealed class MainViewModelEntriesContextMenu : ContextMenu +{ + private RustMenuScope? _scope; + + protected override void OnDataContextChanged(EventArgs e) + { + base.OnDataContextChanged(e); + _scope?.Dispose(); + _scope = null; + if (DataContext is MainViewModelAdapter model) + { + var scope = new RustMenuScope(model); + _scope = scope; + ItemsSource = MainViewModelMenus.CreateEntriesItems(model, scope); + } + else + { + ItemsSource = null; + } + } +} diff --git a/apps/archive-tool/managed/Generated/MainViewModelMetadata.g.cs b/apps/archive-tool/managed/Generated/MainViewModelMetadata.g.cs new file mode 100644 index 0000000..e5b7d5a --- /dev/null +++ b/apps/archive-tool/managed/Generated/MainViewModelMetadata.g.cs @@ -0,0 +1,84 @@ +// +#nullable enable +using System.Collections.Generic; +using Avalonia.Rust; +using Avalonia.Controls; +using Avalonia.Layout; + +namespace ArchiveTool.Presentation.Generated; + +public static class MainViewModelMetadata +{ + public static RustViewModelDescriptor Descriptor { get; } = new( + 1, + "MainViewModel", + [ + new(1, "Title", RustViewModelValueKind.String, false, false, "Archive Explorer", null), + new(2, "Status", RustViewModelValueKind.String, false, false, "Open an archive to get started.", null), + new(3, "ArchiveName", RustViewModelValueKind.String, false, false, "", null), + new(4, "ArchiveKind", RustViewModelValueKind.String, false, false, "", null), + new(5, "EntryCountLabel", RustViewModelValueKind.String, false, false, "No archive loaded", null), + new(6, "FilterText", RustViewModelValueKind.String, true, false, "", null), + new(7, "SelectedIndex", RustViewModelValueKind.Integer, true, false, -1L, null), + new(8, "SelectedKey", RustViewModelValueKind.String, true, false, "", null), + new(9, "SortDirection", RustViewModelValueKind.String, false, false, "Name Ascending", null), + new(10, "IsLoading", RustViewModelValueKind.Boolean, false, false, false, null), + new(11, "CanExtractSelected", RustViewModelValueKind.Boolean, false, false, false, null), + new(12, "CanExtractAll", RustViewModelValueKind.Boolean, false, false, false, null), + new(13, "SelectedCountLabel", RustViewModelValueKind.String, false, false, "0 selected", null), + new(14, "CurrentPath", RustViewModelValueKind.String, false, false, "", null), + new(15, "CanGoUp", RustViewModelValueKind.Boolean, false, false, false, null), + new(16, "TotalsLabel", RustViewModelValueKind.String, false, false, "", null), + new(17, "OpenAfterExtract", RustViewModelValueKind.Boolean, true, false, true, null), + new(18, "CanTest", RustViewModelValueKind.Boolean, false, false, false, null), + ], + [ + new(1, "RecentFiles", RustViewModelValueKind.String, null, null, null, null, false), + new(2, "Entries", RustViewModelValueKind.Model, global::ArchiveTool.Presentation.Generated.EntryRowViewModelMetadata.Descriptor, CreateEntriesTable(), null, null, false), + ], + [ + new(1, "OpenFileCommand", true, null, false, null, false, false), + new(2, "ExtractSelectedCommand", true, null, false, null, false, false), + new(3, "ExtractAllCommand", true, null, false, null, false, false), + new(4, "SelectAllCommand", false, null, false, null, false, false), + new(5, "ClearSelectionCommand", false, null, false, null, false, false), + new(6, "SortEntriesCommand", false, null, true, null, false, false), + new(7, "OpenRecentFileCommand", false, null, true, null, false, false), + new(8, "ExitCommand", false, null, false, null, false, false), + new(9, "GoUpCommand", false, null, false, null, false, false), + new(10, "OpenItemCommand", true, null, false, null, false, false), + new(11, "ExtractHereCommand", true, null, false, null, false, false), + new(12, "TestArchiveCommand", true, null, false, null, false, false), + new(13, "NewArchiveCommand", true, null, false, null, false, false), + new(14, "InvertSelectionCommand", false, null, false, null, false, false), + new(15, "CopyPathCommand", true, null, false, null, false, false), + ], + [ + ]); + + public static RustTableDescriptor EntriesTable { get; } = CreateEntriesTable(); + + public static IReadOnlyList CreateEntriesTableColumns() => + [ + new() { Header = "Name", Width = new global::Avalonia.Controls.GridLength(1D, global::Avalonia.Controls.GridUnitType.Star), MinWidth = 160D, CanUserResize = true, HorizontalContentAlignment = HorizontalAlignment.Left }, + new() { Header = "Kind", Width = new global::Avalonia.Controls.GridLength(80D, global::Avalonia.Controls.GridUnitType.Pixel), MinWidth = 64D, CanUserResize = true, HorizontalContentAlignment = HorizontalAlignment.Left }, + new() { Header = "Size", Width = new global::Avalonia.Controls.GridLength(90D, global::Avalonia.Controls.GridUnitType.Pixel), MinWidth = 72D, CanUserResize = true, HorizontalContentAlignment = HorizontalAlignment.Right }, + new() { Header = "Packed", Width = new global::Avalonia.Controls.GridLength(90D, global::Avalonia.Controls.GridUnitType.Pixel), MinWidth = 72D, CanUserResize = true, HorizontalContentAlignment = HorizontalAlignment.Right }, + new() { Header = "Ratio", Width = new global::Avalonia.Controls.GridLength(70D, global::Avalonia.Controls.GridUnitType.Pixel), MinWidth = 56D, CanUserResize = true, HorizontalContentAlignment = HorizontalAlignment.Right }, + new() { Header = "Modified", Width = new global::Avalonia.Controls.GridLength(140D, global::Avalonia.Controls.GridUnitType.Pixel), MinWidth = 110D, CanUserResize = true, HorizontalContentAlignment = HorizontalAlignment.Left }, + new() { Header = "CRC", Width = new global::Avalonia.Controls.GridLength(90D, global::Avalonia.Controls.GridUnitType.Pixel), MinWidth = 72D, CanUserResize = true, HorizontalContentAlignment = HorizontalAlignment.Left }, + ]; + + private static RustTableDescriptor CreateEntriesTable() => new( + [ + new(1, "Name", "Name", "Name", null, true, false, 160D, null, true, true, RustTableHorizontalAlignment.Left), + new(2, "Kind", "Kind", "Kind", 80D, false, false, 64D, null, true, true, RustTableHorizontalAlignment.Left), + new(3, "Size", "Size", "SizeLabel", 90D, false, false, 72D, null, true, true, RustTableHorizontalAlignment.Right), + new(4, "Packed", "Packed", "PackedLabel", 90D, false, false, 72D, null, true, true, RustTableHorizontalAlignment.Right), + new(5, "Ratio", "Ratio", "RatioLabel", 70D, false, false, 56D, null, true, true, RustTableHorizontalAlignment.Right), + new(6, "Modified", "Modified", "Modified", 140D, false, false, 110D, null, true, true, RustTableHorizontalAlignment.Left), + new(7, "Crc", "CRC", "CrcLabel", 90D, false, false, 72D, null, true, true, RustTableHorizontalAlignment.Left), + ], + new("SelectedIndex", "SelectedKey", "Key"), + new("SortEntriesCommand", "Name", "SortDirection")); +} diff --git a/apps/archive-tool/managed/Views/MainWindow.axaml b/apps/archive-tool/managed/Views/MainWindow.axaml new file mode 100644 index 0000000..6ef4dae --- /dev/null +++ b/apps/archive-tool/managed/Views/MainWindow.axaml @@ -0,0 +1,380 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +