diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e12279..cf556a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,94 @@ # WASimCommander - Change Log +## 1.2.0.0 (next) + +### WASimModule +* Fix binary data representation in results for named variable requests with 1-4 byte integer value sizes (`int8` - `int32` types) -- the result data would be encoded as a float type instead. ([8c7724e6]) +* Restore ability to use Unit type specifiers when setting and getting Local vars. ([e16049ac]) +* Added ability to specify/set a default 'L' var value and unit type in `GetCreate` command to use if the variable needs to be created. ([61a52674]) +* `GetCreate` and `SetCreate` commands for non-L types now silently fall back to `Get` and `Set` respectively. ([61a52674]) +* Fixed that command response for `GetCreate` was always sent as if responding to a `Get` command. ([61a52674]) +* Added `requestId` to error logging and response output for data requests and add more info for `Get` command errors. ([17791eef]) +* Added ability to return string type results for Get commands and Named data requests by converting them to calculator expressions automatically on the server. ([983e7ab6]) +* Improved automatic conversion to calc code for other variable types by including the unit type, if given, and narrowing numeric results to integer types if needed. ([983e7ab6]) +* Prevent possible simulator hang on exit when quitting with active client(s) connections. ([70e0ef31]) +* Event loop processing is now paused/restarted also based on whether any connected client(s) have active data requests and if they are paused or not (previously it was only based on if any clients were connected at all). ([90242ed4]) +* Fixes a logged SimConnect error when trying to unsubscribe from the "Frame" event (cause unknown). ([90242ed4]) +* Data requests with `Once` type update period are now queued if data updates are paused when the request is submitted. These requests will be sent when/if updates are resumed again by the client. Fixes that data would be sent anyway when the request is initially submitted, even if updates are paused. ([fe99bbb2]) +* Update reference list of KEY events and aliases as of MSFS SDK v0.22.3.0. ([f045e150]) + +[8c7724e6]: https://github.com/mpaperno/WASimCommander/commit/8c7724e60ed94e622d5ee2669cf7e000031c2c18 +[e16049ac]: https://github.com/mpaperno/WASimCommander/commit/e16049ac69ff15cdcdd9084c7fdab6920a1ffba1 +[61a52674]: https://github.com/mpaperno/WASimCommander/commit/61a52674e0dff7e1f3e63ed73a0bed711bb2c479 +[17791eef]: https://github.com/mpaperno/WASimCommander/commit/17791eefecc86454c031636a5da9c19d56e21139 +[983e7ab6]: https://github.com/mpaperno/WASimCommander/commit/983e7ab609e81af81525ff84431b1c4557447d87 +[70e0ef31]: https://github.com/mpaperno/WASimCommander/commit/70e0ef31b01a1a772d9e49102e0a77ec6f3e928b +[90242ed4]: https://github.com/mpaperno/WASimCommander/commit/90242ed494069aba5bcdad839914b9fcfc6521e2 +[fe99bbb2]: https://github.com/mpaperno/WASimCommander/commit/fe99bbb25c5dd907e8a4d513769759c4b430580f +[f045e150]: https://github.com/mpaperno/WASimCommander/commit/f045e15007abd6b7b05b97c004a7a55488a33a9b + +### WASimClient and WASimClient_CLI (managed .NET) +* Fixed incoming data size check for variable requests which are less than 4 bytes in size. ([c8e74dfa]) +* Fixed early timeout being reported on long-running `list()` requests (eg.thousands of L vars). ([a05a28c3]) +* Restored ability to specify Unit type for L vars and support for GetCreate with default value/unit and added extra features: ([3090d534], [0a30646d]) + * Added unit name parameter to `setLocalVariable()` and `setOrCreateLocalVariable()`. + * Added `getOrCreateLocalVariable()`. + * Added `VariableRequest::createLVar` property. + * Add optional `create` flag and unit name to `VariableRequest()` c'tor overloads. +* Added async option to `saveDataRequest()` which doesn't wait for server response (`saveDataRequestAsync()` for the C# version). ([82ea4252], [0a30646d]) +* Added ability to return a string value with `getVariable()` to make use of new WASimModule feature. ([8e75eb8c], [0e54794b]) +* The request updates paused state (set with `setDataRequestsPaused()`) is now saved locally even if not connected to server and will be sent to server upon connection and before sending any queued data requests. + This allows connecting and sending queued requests but suspending any actual value checks until needed. ([bea8bccb]) +* The `setVariable()` method now verifies that the specified variable type is settable before sending the command to the server. ([576914a2]) +* Removed logged version mismatch warning on Ping response. +* Documentation updates. + +[c8e74dfa]: https://github.com/mpaperno/WASimCommander/commit/c8e74dfa706647cf785c7e6c811731d8945e49c6 +[a05a28c3]: https://github.com/mpaperno/WASimCommander/commit/a05a28c3d1af56444be3fbe54f619e62548736a0 +[3090d534]: https://github.com/mpaperno/WASimCommander/commit/3090d5344c3a34c62e81f61237fe1fd91f6b11c5 +[0a30646d]: https://github.com/mpaperno/WASimCommander/commit/0a30646d0ae985580d67ed40c8a441a0f5a0ba17 +[82ea4252]: https://github.com/mpaperno/WASimCommander/commit/82ea4252bd25423bbeab354799d6be41f053880e +[8e75eb8c]: https://github.com/mpaperno/WASimCommander/commit/8e75eb8c087f5a39fee93c2b7d073500e4f14664 +[0e54794b]: https://github.com/mpaperno/WASimCommander/commit/0e54794b2ec8411f42d34a7696426724ffc5e932 +[bea8bccb]: https://github.com/mpaperno/WASimCommander/commit/bea8bccba38fae987690d5af259f6f8b22fbc781 +[576914a2]: https://github.com/mpaperno/WASimCommander/commit/576914a235c81b73ba0ea85655d913b61cbc5015 + +### WASimClient_CLI (managed .NET) +* Fixed possible exception when assembling list lookup results dictionary in the off-case of duplicate keys. ([cf46967b]) + +[cf46967b]: https://github.com/mpaperno/WASimCommander/commit/cf46967b499a9bb19a77a14a47bd2ac29b4d0989 + +### WASimUI +* Added database of Simulator Variables, Key Events, and Unit types imported from SimConnect SDK online documentation. This is used for: + * Typing suggestions in the related form fields when entering names of 'A' vars, Key Events, or Unit types. + * Available as a popup search window from each related form (Variables, Key Events, Data Requests) via button/menu/CTRL-F shortcut. + * Can be opened as a standalone window for browsing and searching all imported data by type. +* Added ability to import and export Data Requests in _MSFS/SimConnect Touch Portal Plugin_ format with a new editor window available to adjust plugin-specific data before export (category, format, etc.) +* Fixed that the state of current item selections in tables wasn't always properly detected and buttons didn't get enabled/disabled when needed (eg. "Remove Requests" button). +* Added ability to toggle visibility of each main form area of the UI from the View menu (eg. Variables or Key Events groups). Choices are preserved between sessions. +* Simplified the connection/disconnection procedure by providing one action/button for both Sim and Server connections (independent actions still available via extension menu). +* Typing suggestions in combo boxes now use a drop-down menu style selection list by default, and the behavior can be configured independently for each one. +* String type variables can now be used in the "Variables" section for `Get` commands. +* Unit type specifier is now shown and used for 'L' variables as well (unit is optional). +* Added "Get or Create" action/button for 'L' vars. +* The list of 'L' variables loaded from simulator is now sorted alphabetically. +* The Size field in Data Request form is automatically populated with a likely match when a new Unit type is selected. +* Many improvements in table views (all options are saved to user settings and persist between sessions): + * All column widths are now re-sizable in all tables. + * Columns can be toggled on/off in the views (r-click for context menu). + * Can now be sorted by multiple columns (CTRL-click). + * Option to show filtering (searching) text fields for each column. Filters support wildcards and optional regular expressions. + * Font size can be adjusted (using context menu or CTRL key with `+`, `-`, or `0` to reset. + * Tooltips shown with data values when hovered over table cells (readable even if text is too long to fit in the column). +* Numerous shortcuts and context menus added throughout, each relevant to the respective forms/tables currently being used or clicked. +* Last selected variable types and data request type are saved between sessions. +* Most actions/buttons which require a server connection to work are now disabled when not connected. +* When loading data requests from a file while connected to the server, the requests are now sent asynchronously, improving UI responsiveness. +* More minor quality-of-life improvements! + +**Full log:** [v1.1.2.0...HEAD](https://github.com/mpaperno/WASimCommander/compare/1.1.2.0...next) + +--- ## 1.1.2.0 (23-Feb-2023) ### WASimModule diff --git a/README.md b/README.md index ceca2f6..71eb586 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ [![GitHub release (latest by date including pre-releases)](https://img.shields.io/github/v/release/mpaperno/WASimCommander?include_prereleases)](https://github.com/mpaperno/WASimCommander/releases) [![GPLv3 License](https://img.shields.io/badge/license-GPLv3-blue.svg)](LICENSE.GPL.txt) [![LGPGv3 License](https://img.shields.io/badge/license-LGPLv3-blue.svg)](LICENSE.LGPL.txt) +[![API Documentation](https://img.shields.io/badge/API-Documentation-07A7EC?labelColor=black)](https://wasimcommander.max.paperno.us/) [![Discord](https://img.shields.io/static/v1?style=flat&color=7289DA&&labelColor=7289DA&message=Discord%20Chat&label=&logo=discord&logoColor=white)](https://discord.gg/meWyE4dcAt) @@ -15,7 +16,7 @@ **A WASM module-based Server and a full Client API combination.** This project is geared towards other MSFS developers/coders who need a convenient way to remotely access parts of the Simulator which are normally -inaccessible via _SimConnect_, such as locally-defined aircraft variables or custom events. +inaccessible via _SimConnect_, such as some variable types and 'H' events, and running RPN "calculator code" directly on the sim. The Client API can be utilized natively from C++, or via .NET managed assembly from C#, Python, or other languages. @@ -27,7 +28,7 @@ to _SimConnect_ for basic functionality like reading/setting Simulation Variable One of the motivations for this project was seeing multiple MSFS tool authors and casual hackers creating their own WASM modules and protocols just to support their own product or need. There is nothing wrong with this, of course, but for the Sim user it can be a disadvantage on several levels. They may end up running -multiple versions of modules which all do eseentially the same thing, and it may be confusing which WASM module they need to support which tool, +multiple versions of modules which all do essentially the same thing, and it may be confusing which WASM module they need to support which tool, just to name two obvious issues. For the developer, programming the WASM modules comes with its own quirks, too, not to mention the time involved. And regardless of the supposed isolated environment a WASM module is supposed to run in, it's still very easy to take down the whole Simulator with some errant code... ;-) @@ -60,7 +61,7 @@ On a more practical note, I am using it with the [MSFS Touch Portal Plugin](http - Any calculator code saved in subscriptions is **pre-compiled to a more efficient byte code** representation before being passed to the respective calculator functions. This significantly improves performance for recurring calculations. - **Register Named Events**: - - Save recurring "set events," like activiating controls using calculator code, for more efficient and simpler re-use. + - Save recurring "set events," like activating controls using calculator code, for more efficient and simpler re-use. Saved calculator code is pre-compiled to a more efficient byte code representation before being passed to the calculator function. This significantly improves performance for recurring events. - Registered events can be executed "natively" via _WASim API_ by simply sending a short command with the saved event ID. @@ -68,7 +69,7 @@ On a more practical note, I am using it with the [MSFS Touch Portal Plugin](http - Event names can be completely custom (including a `.` (period) as per SimConnect convention), or derive from the connected Client's name (to ensure uniqueness). - **Send Simulator "Key Events"** directly by ID or name (instead of going through the SimConnect mapping process or executing calculator code). Much more efficient than the other methods. - **New in v1.1.0:** Send Key Events with up to 5 values (like the new `SimConnect_TransmitClientEvent_EX1()`). -- **Remote Logging**: Log messages (errors, warnings, debug, etc) can optionally be sent to the Client, with specific minimum level (eg. only warnings and errros). +- **Remote Logging**: Log messages (errors, warnings, debug, etc) can optionally be sent to the Client, with specific minimum level (eg. only warnings and errors). - **Ping** the Server to check that the WASM module is installed and running before trying to connect or use its features. #### Core Components @@ -77,9 +78,9 @@ On a more practical note, I am using it with the [MSFS Touch Portal Plugin](http - Well-defined message API for communication between Server module and any client implementation. - Uses standard SimConnect messages for the base network "transport" layer. - All data allocations are on client side, so SimConnect limits in WASM module are bypassed (can in theory support unlimited clients). -- No wasted data allocations, each data/variable subscription is stored independently avoiding complications with offets or data overflows. +- No wasted data allocations, each data/variable subscription is stored independently avoiding complications with offsets or data overflows. - Minimum possible impact on MSFS in terms of memory and CPU usage; practically zero effect for Sim user when no clients are connected (Server is idle). -- Server periodically checks that a client is still connected by sending "hearbeat" ping requests and enforcing a timeout if no response is received. +- Server periodically checks that a client is still connected by sending "heartbeat" ping requests and enforcing a timeout if no response is received. - Extensive logging at configurable levels (debug/info/warning/etc) to multiple destinations (file/console/remote) for both Server and Client. - Uses an efficient **lazy logging** implementation which doesn't evaluate any arguments if the log message will be discarded anyway (eg. a DEBUG level message when minimum logging level is INFO). @@ -90,7 +91,9 @@ On a more practical note, I am using it with the [MSFS Touch Portal Plugin](http #### Desktop GUI - Includes a full-featured desktop application which demonstrates/tests all available features of the API. -- Fully usable as a standalone application which saves preferences, imports/exports lists of data subscriptions/registered events, and other usabililty features. +- Fully usable as a standalone application which saves preferences, imports/exports lists of data subscriptions/registered events, and other friendly features. +- Very useful for "exploring" the simulator in general, like checking variable values, testing effects of key events and RPN calculator code. +- Can be used with the [MSFS/SimConnect Touch Portal Plugin](https://github.com/mpaperno/MSFSTouchPortalPlugin) for import/export of custom variable request definitions.

 

@@ -100,7 +103,7 @@ On a more practical note, I am using it with the [MSFS Touch Portal Plugin](http ------------- -### Downloads +### Downloads and Updates Over in the [Releases](https://github.com/mpaperno/WASimCommander/releases) there are 3 packages provided. (The actual file names have version numbers appended.) - `WASimCommander_SDK` - All header files, pre-built static and dynamic libs, packaged WASM module, pre-build GUI, reference documentation, and other tools/examples. @@ -111,14 +114,16 @@ _Watch_ -> _Custom_ -> _Releases_ this repo (button at top) or subscribe to the Update announcements are also posted on my Discord server's [WASimCommander release announcement channel](https://discord.gg/StbmZ2ZgsF). +The SDK and updates are [published on Flightsim.to](https://flightsim.to/file/36474/wasimcommander) where one could "subscribe" to release notifications (account required). + ------------- ### Documentation & Examples There are three basic console-style tests/examples included for `C++`, `C#`, and `Python` in the [src/Testing](https://github.com/mpaperno/WASimCommander/tree/main/src/Testing) folder. If you like reading code, this is the place to start. -API docuemntation generated from source comments is published here: https://mpaperno.github.io/WASimCommander/
-A good place to start with the docs is probably the [`WASimClient`](https://mpaperno.github.io/WASimCommander/class_w_a_sim_commander_1_1_client_1_1_w_a_sim_client.html) page. +API documentation generated from source comments is published here: https://wasimcommander.max.paperno.us/
+A good place to start with the docs is probably the [`WASimClient`](https://wasimcommander.max.paperno.us/class_w_a_sim_commander_1_1_client_1_1_w_a_sim_client.html) page. The GUI is written in C++ (using Qt library for UI), and while not the simplest example, _is_ a full implementation of almost all the available API features. The main `WASimClient` interactions all happen in the `MainWindow::Private` class at the top of the @@ -141,7 +146,7 @@ The module also logs to a file, though it's a bit tricky to find. On my edition `D:\WpSystem\S-1-5-21-611220451-769921231-644967174-1000\AppData\Local\Packages\Microsoft.FlightSimulator_8wekyb3d8bbwe\LocalState\packages\wasimcommander-module\work` To enable more verbose logging on the module at startup, edit the `server_conf.ini` file which is found in the module's install folder -(`Comunity\wasimcommander-module\modules`). There are comments in there indicating the options. +(`Community\wasimcommander-module\modules`). There are comments in there indicating the options. Keep in mind that the server logging level can also be changed remotely at runtime, but of course that only works if you can establish a connection to the module in the first place. @@ -185,12 +190,17 @@ Uses and includes [_IniPP_ by Matthias C. M. Troffaes](https://github.com/mcmtro Uses the _Microsoft SimConnect SDK_ under the terms of the _MS Flight Simulator SDK EULA (11/2019)_ document. -The GUI component uses portions of the [_Qt Library_](http://qt.io) under the terms of the GPL v3 license. +WASimUI (GUI): +- Uses portions of the [_Qt Library_](http://qt.io) under the terms of the GPL v3 license. +- Uses and includes the following symbol fonts for icons, under the terms of their respective licenses: + - [IcoMoon Free](https://icomoon.io/#icons-icomoon) - IcoMoon.io, GPL v3. + - [Material Icons](https://material.io/) - Google, Apache License v2.0. +- Uses modified versions of `FilterTableHeader` and `FilterLineEdit` components from [DB Browser for SQLite](https://github.com/sqlitebrowser/sqlitebrowser) under GPL v3 license. +- Uses modified version of `MultisortTableView` from under GPL v3 license. +- Uses Natural (alpha-numeric) sorting algorithm implementation for _Qt_ by Litkevich Yuriy (public domain). -The GUI component uses and includes the following symbol fonts for icons, under the terms of their respective licenses: -- [IcoMoon Free](https://icomoon.io/#icons-icomoon) - IcoMoon.io, GPL v3. -- [Material Icons](https://material.io/) - Google, Apache License v2.0. +Documentation generated with [Doxygen](https://www.doxygen.nl/) and styled with the most excellent [Doxygen Awesome](https://jothepro.github.io/doxygen-awesome-css). ------------- ### Copyright, License, and Disclaimer diff --git a/build/Make-Version.ps1 b/build/Make-Version.ps1 index 03bab61..07ca9a3 100644 --- a/build/Make-Version.ps1 +++ b/build/Make-Version.ps1 @@ -40,5 +40,7 @@ function Make-Version { Merge-Tokens -InputFile $SrcPath\include\wasim_version.in -OutputFile $SrcPath\include\wasim_version.h -Tokens $Tokens -NoWarning Merge-Tokens -InputFile $SrcPath\WASimClient_CLI\AssemblyInfo.cpp.in -OutputFile $SrcPath\WASimClient_CLI\AssemblyInfo.cpp -Tokens $Tokens -NoWarning Merge-Tokens -InputFile $DocPath\version.Doxyfile.in -OutputFile $DocPath\version.Doxyfile -Tokens $Tokens -NoWarning + $path = "${SrcPath}\WASimModule\WASimModuleProject\WASimCommander-Module\PackageDefinitions\wasimcommander-module.xml" + Merge-Tokens -InputFile "${path}.in" -OutputFile $path -Tokens $Tokens -NoWarning } diff --git a/build/Merge-Tokens.ps1 b/build/Merge-Tokens.ps1 index 7833f4e..096d67d 100644 --- a/build/Merge-Tokens.ps1 +++ b/build/Merge-Tokens.ps1 @@ -83,11 +83,12 @@ function Merge-Tokens { Exit 1 } + $TmpFile = [System.IO.Path]::GetTempFileName() + # If the OutputFile is null, we will write to a temporary file if ([string]::IsNullOrWhiteSpace($OutputFile)) { Write-Verbose "OutputFile was omitted. Replacing InputFile." - $OutputFile = [System.IO.Path]::GetTempFileName() - $ReplaceInputFile = $true + $OutputFile = $InputFile } # Empty OutputFile if it already exists @@ -115,8 +116,6 @@ function Merge-Tokens { $usedTokens = New-Object -TypeName "System.Collections.ArrayList" #$sw = [System.IO.File]::AppendText($OutputFile) - # hack to force no-BOM UTF8 on PS v5.x - " " | Out-File -Encoding ASCII -NoNewline -FilePath $OutputFile (Get-Content $InputFile) | ForEach-Object { $line = $_ $totalTokens += GetTokenCount($line) @@ -134,13 +133,11 @@ function Merge-Tokens { } $missedTokens += GetTokenCount($line) #$sw.WriteLine($line) - $line | Out-File -Append -Encoding UTF8 -FilePath $OutputFile + $line | Out-File -Append -Encoding UTF8 -FilePath $TmpFile } - # If no OutputFile was given, we will replace the InputFile with the temporary file - if ($ReplaceInputFile) { - Get-Content -Path $OutputFile | Out-File -FilePath $InputFile -Encoding UTF8 - } + # Remove UTF8 BOM + Get-Content -Path $TmpFile | Out-File -FilePath $OutputFile # Write warning if there were tokens given in the Token parameter which were not replaced if (!$NoWarning -and $usedTokens.Count -ne $Tokens.Count) { diff --git a/build/build.ps1 b/build/build.ps1 index 1654baf..e9e3d85 100644 --- a/build/build.ps1 +++ b/build/build.ps1 @@ -8,7 +8,7 @@ Param( [string[]]$Targets = "all", [string]$RootPath = "..", - [string[]]$Configuration = @("Debug", "Debug-DLL", "Release-DLL", "Release-net6", "Release-netfw", "Release"), + [string[]]$Configuration = @("Debug", "Debug-DLL", "Release-DLL", "Release-net6", "Release-net7", "Release-netfw", "Release"), [string]$Platform = "x64", [string[]]$Projects = "all", [string]$BuildType = "Clean,Rebuild", @@ -154,6 +154,9 @@ if ($LastExitCode -ge 8) { Write-Output($LastExitCode); Exit 1 } # .NET 6 robocopy "$BuildPath\${CLIENT_NAME}_CLI\Release-net6-$Platform" "${csLibPath}\net6" *.dll *.pdb *.xml *.ini $copyOptions if ($LastExitCode -ge 8) { Write-Output($LastExitCode); Exit 1 } +# .NET 7 +robocopy "$BuildPath\${CLIENT_NAME}_CLI\Release-net7-$Platform" "${csLibPath}\net7" *.dll *.pdb *.xml *.ini $copyOptions +if ($LastExitCode -ge 8) { Write-Output($LastExitCode); Exit 1 } # .NET Framework robocopy "$BuildPath\${CLIENT_NAME}_CLI\Release-netfw-$Platform" "${csLibPath}\net46" *.dll *.pdb *.xml *.ini $copyOptions if ($LastExitCode -ge 8) { Write-Output($LastExitCode); Exit 1 } diff --git a/build/version.ps1 b/build/version.ps1 index 412ce4a..ad58de9 100644 --- a/build/version.ps1 +++ b/build/version.ps1 @@ -1,7 +1,7 @@ $VER_MAJOR = 1 -$VER_MINOR = 1 -$VER_PATCH = 2 +$VER_MINOR = 2 +$VER_PATCH = 0 $VER_BUILD = 0 $VER_COMIT = 0 $VER_NAME = "" diff --git a/docs/Doxyfile b/docs/Doxyfile index 39357a5..0ebb4fa 100644 --- a/docs/Doxyfile +++ b/docs/Doxyfile @@ -13,11 +13,14 @@ MULTILINE_CPP_IS_BRIEF = YES TAB_SIZE = 2 ALIASES = \ "refwc{1}=\ref WASimCommander::\1 \"\1\"" \ - "refwce{1}=\ref WASimCommander::Enums::\1 \"\1\"" \ - "refwcc{1}=\ref WASimCommander::Client::\1 \"\1\"" \ - "refwcli{1}=\ref WASimCommander::CLI::\1 \"\1\"" \ - "refwclie{1}=\ref WASimCommander::CLI::Enums::\1 \"\1\"" \ - "refwclis{1}=\ref WASimCommander::CLI::Structs::\1 \"\1\"" \ + "refwce{1}=\ref WASimCommander::Enums::\1 \"Enums::\1\"" \ + "refwcc{1}=\ref WASimCommander::Client::\1 \"Client::\1\"" \ + "refwccc{1}=\ref WASimCommander::Client::WASimClient::\1 \"WASimClient::\1\"" \ + "refwcli{1}=\ref WASimCommander::CLI::\1 \"CLI::\1\"" \ + "refwclie{1}=\ref WASimCommander::CLI::Enums::\1 \"CLI::Enums::\1\"" \ + "refwclis{1}=\ref WASimCommander::CLI::Structs::\1 \"CLI::Structs::\1\"" \ + "refwclic{1}=\ref WASimCommander::CLI::Client::\1 \"CLI::Client::\1\"" \ + "refwclicc{1}=\ref WASimCommander::CLI::Client::WASimClient\1 \"CLI::Client::WASimClient::\1\"" \ "default{1}=\nDefault value is \c \1.\n" \ "reimp{1}=Reimplemented from \c \1." \ "reimp=Reimplemented from superclass." \ @@ -25,7 +28,8 @@ ALIASES = \ "pacc=\par Access functions:^^" \ "psig=\par Notifier signal:^^" \ "intern=\parInternal use only." \ - "qflags{2}=

The \ref \1 type is a typedef for `QFlags<\2>`. It stores an OR combination of \ref \2 values.

" + "qflags{2}=

The \ref \1 type is a typedef for `QFlags<\2>`. It stores an OR combination of \ref \2 values.

" \ + "since{1}=\par **Since \1**" TOC_INCLUDE_HEADINGS = 5 AUTOLINK_SUPPORT = YES diff --git a/docs/version.Doxyfile b/docs/version.Doxyfile index e2cf38f..26c8061 100644 --- a/docs/version.Doxyfile +++ b/docs/version.Doxyfile @@ -1,7 +1,7 @@ - + # Doxyfile 1.8.17 # THIS FILE IS GENERATED BY A SCRIPT, CHANGES WILL NOT PERSIST. EDIT THE CORRESPONDING .in TEMPLATE FILE INSTEAD. PROJECT_NAME = "WASimCommander" -PROJECT_NUMBER = v1.1.2.0 +PROJECT_NUMBER = v1.2.0.0 PROJECT_BRIEF = "Remote access to the Microsoft Flight Simulator 2020 Gauge API." diff --git a/src/Testing/CS_BasicConsole/CS_BasicConsole.csproj b/src/Testing/CS_BasicConsole/CS_BasicConsole.csproj index 4ced449..6407f30 100644 --- a/src/Testing/CS_BasicConsole/CS_BasicConsole.csproj +++ b/src/Testing/CS_BasicConsole/CS_BasicConsole.csproj @@ -2,7 +2,6 @@ Exe - net6.0-windows disable enable CS_BasicConsole.Program @@ -11,14 +10,26 @@ False none x64 + Debug;Release;Release-net7 + 1.1.0.0 + 1.1.0.0 + net6.0-windows embedded + net6.0-windows + embedded + True + + + + net7.0-windows embedded + True diff --git a/src/Testing/CS_BasicConsole/Program.cs b/src/Testing/CS_BasicConsole/Program.cs index cca9973..08c8050 100644 --- a/src/Testing/CS_BasicConsole/Program.cs +++ b/src/Testing/CS_BasicConsole/Program.cs @@ -129,7 +129,8 @@ static void Main(string[] _) // Test subscribing to a string type value. We'll use the Sim var "TITLE" (airplane name), which can only be retrieved using calculator code. // We allocate 32 Bytes here to hold the result and we request this one with an update period of Once, which will return a result right away // but will not be scheduled for regular updates. If we wanted to update this value later, we could call the client's `updateDataRequest(requestId)` method. - hr = client.saveDataRequest(new DataRequest( + // Also we can use the "async" version which doesn't wait for the server to respond before returning. We're going to wait for a result anyway after submitting the request. + hr = client.saveDataRequestAsync(new DataRequest( requestId: (uint)Requests.REQUEST_ID_2_STR, resultType: CalcResultType.String, calculatorCode: "(A:TITLE, String)", @@ -198,7 +199,8 @@ static void ClientStatusHandler(ClientEvent ev) // Event handler for showing listing results (eg. local vars list) static void ListResultsHandler(ListResult lr) { - Log(lr.ToString()); // just use the ToString() override + Log($"Got {lr.list.Count} results for list type {lr.listType}. (Uncomment next line in ListResultsHandler() to print them.)"); + //Log(lr.ToString()); // To print all the items just use the ToString() override. // signal completion dataUpdateEvent.Set(); } @@ -206,7 +208,7 @@ static void ListResultsHandler(ListResult lr) // Event handler to process data value subscription updates. static void DataSubscriptionHandler(DataRequestRecord dr) { - Console.Write($"<< Got Data for request {(Requests)dr.requestId} \"{dr.nameOrCode}\" with Value: "); + Console.Write($"[{DateTime.Now.ToString("mm:ss.fff")}] << Got Data for request {(Requests)dr.requestId} \"{dr.nameOrCode}\" with Value: "); // Convert the received data into a value using DataRequestRecord's tryConvert() methods. // This could be more efficient in a "real" application, but it's good enough for our tests with only 2 value types. if (dr.tryConvert(out float fVal)) @@ -215,14 +217,14 @@ static void DataSubscriptionHandler(DataRequestRecord dr) Console.WriteLine($"(string) \"{sVal}\""); } else - Console.WriteLine("Could not convert result data to value!"); + Log("Could not convert result data to value!", "!!"); // signal completion dataUpdateEvent.Set(); } static void Log(string msg, string prfx = "=:") { - Console.WriteLine(prfx + ' ' + msg); + Console.WriteLine("[{0}] {1} {2}", DateTime.Now.ToString("mm:ss.fff"), prfx, msg); } } diff --git a/src/WASimClient/WASimClient.cpp b/src/WASimClient/WASimClient.cpp index 8aa2668..4365817 100644 --- a/src/WASimClient/WASimClient.cpp +++ b/src/WASimClient/WASimClient.cpp @@ -234,6 +234,7 @@ class WASimClient::Private atomic_bool simConnected = false; atomic_bool serverConnected = false; atomic_bool logCDAcreated = false; + atomic_bool requestsPaused = false; HANDLE hSim = nullptr; HANDLE hSimEvent = nullptr; @@ -676,6 +677,8 @@ class WASimClient::Private listResult.reset(); // make sure server knows our desired log level and set up data area/request if needed updateServerLogLevel(); + // set update status of data requests before adding any, in case we don't actually want results yet + sendServerCommand(Command(CommandId::Subscribe, (requestsPaused ? 0 : 1))); // (re-)register (or delete) any saved DataRequests registerAllDataRequests(); // same with calculator events @@ -784,7 +787,7 @@ class WASimClient::Private return &reponses.try_emplace(token, token, cv).first->second; } - // Blocks and waits for a response to a specific command token. + // Blocks and waits for a response to a specific command token. `timeout` can be -1 to use `extraPredicate` only (which is then required). HRESULT waitCommandResponse(uint32_t token, Command *response, uint32_t timeout = 0, std::function extraPredicate = nullptr) { TrackedResponse *tr = findTrackedResponse(token); @@ -795,8 +798,10 @@ class WASimClient::Private if (!timeout) timeout = settings.networkTimeout; - auto stop_waiting = [=]() { - return !serverConnected || (tr->response.commandId != CommandId::None && tr->response.token == token && (!extraPredicate || extraPredicate())); + bool stopped = false; + auto stop_waiting = [=, &stopped]() { + return stopped = + !serverConnected || (tr->response.commandId != CommandId::None && tr->response.token == token && (!extraPredicate || extraPredicate())); }; HRESULT hr = E_TIMEOUT; @@ -805,10 +810,20 @@ class WASimClient::Private } else { unique_lock lock(tr->mutex); - if (cv->wait_for(lock, chrono::milliseconds(timeout), stop_waiting)) - hr = S_OK; + if (timeout > 0) { + if (cv->wait_for(lock, chrono::milliseconds(timeout), stop_waiting)) + hr = S_OK; + } + else if (!!extraPredicate) { + cv->wait(lock, stop_waiting); + hr = stopped ? ERROR_ABANDONED_WAIT_0 : S_OK; + } + else { + hr = E_INVALIDARG; + LOG_DBG << "waitCommandResponse() requires a predicate condition when timeout parameter is < 0."; + } } - if (SUCCEEDED(hr) && response) { + if (SUCCEEDED(hr) && !!response) { unique_lock lock(tr->mutex); *response = move(tr->response); } @@ -836,14 +851,17 @@ class WASimClient::Private void waitListRequestEnd() { auto stop_waiting = [this]() { - //shared_lock lock(listResult.mutex); return listResult.nextTimeout.load() >= Clock::now(); }; Command response; - HRESULT hr = waitCommandResponse(listResult.token, &response, 0, stop_waiting); - if (hr == E_TIMEOUT) { + HRESULT hr = waitCommandResponse(listResult.token, &response, -1, stop_waiting); + if (hr == ERROR_ABANDONED_WAIT_0) { LOG_ERR << "List request timed out."; + hr = E_TIMEOUT; + } + else if (hr != S_OK) { + LOG_ERR << "List request failed with result: " << LOG_HR(hr); } else if (response.commandId != CommandId::Ack) { LOG_WRN << "Server returned Nak for list request of " << Utilities::getEnumName(listResult.listType.load(), LookupItemTypeNames); @@ -911,7 +929,7 @@ class WASimClient::Private return sValue; } - HRESULT getVariable(const VariableRequest &v, double *result) + HRESULT getVariable(const VariableRequest &v, double *result, std::string *sResult = nullptr, double dflt = 0.0) { const string sValue = buildVariableCommandString(v, false); if (sValue.empty() || sValue.length() >= STRSZ_CMD) @@ -919,7 +937,7 @@ class WASimClient::Private HRESULT hr; Command response; - if FAILED(hr = sendCommandWithResponse(Command(CommandId::Get, v.variableType, sValue.c_str()), &response)) + if FAILED(hr = sendCommandWithResponse(Command(v.createLVar && v.variableType == 'L' ? CommandId::GetCreate : CommandId::Get, v.variableType, sValue.c_str(), dflt), &response)) return hr; if (response.commandId != CommandId::Ack) { LOG_WRN << "Get Variable request for " << quoted(sValue) << " returned Nak response. Reason, if any: " << quoted(response.sData); @@ -927,15 +945,21 @@ class WASimClient::Private } if (result) *result = response.fData; + if (sResult) + *sResult = response.sData; return S_OK; } - HRESULT setVariable(const VariableRequest &v, const double value, bool create = false) + HRESULT setVariable(const VariableRequest &v, const double value) { - const string sValue = buildVariableCommandString(v, true); - if (sValue.empty() || sValue.length() >= STRSZ_CMD) - return E_INVALIDARG; - return sendServerCommand(Command(create ? CommandId::SetCreate : CommandId::Set, v.variableType, sValue.c_str(), value)); + if (Utilities::isSettableVariableType(v.variableType)) { + const string sValue = buildVariableCommandString(v, true); + if (sValue.empty() || sValue.length() >= STRSZ_CMD) + return E_INVALIDARG; + return sendServerCommand(Command(v.createLVar && v.variableType == 'L' ? CommandId::SetCreate : CommandId::Set, v.variableType, sValue.c_str(), value)); + } + LOG_WRN << "Cannot Set a variable of type '" << v.variableType << "'."; + return E_INVALIDARG; } #pragma endregion @@ -971,14 +995,14 @@ class WASimClient::Private // Writes DataRequest data to the corresponding CDA and waits for an Ack/Nak from server. // If the DataRequest::requestType == None, the request will be deleted by the server and the response wait is skipped. - HRESULT sendDataRequest(const DataRequest &req) + HRESULT sendDataRequest(const DataRequest &req, bool async) { HRESULT hr; if FAILED(hr = writeDataRequest(req)) return hr; // check if just deleting an existing request and don't wait around for that response - if (req.requestType == RequestType::None) + if (async || req.requestType == RequestType::None) return hr; shared_ptr cv = make_shared(); @@ -1033,7 +1057,7 @@ class WASimClient::Private return SimConnectHelper::removeClientDataDefinition(hSim, tr->dataId); } - HRESULT addOrUpdateRequest(const DataRequest &req) + HRESULT addOrUpdateRequest(const DataRequest &req, bool async) { if (req.requestType == RequestType::None) return removeRequest(req.requestId); @@ -1082,7 +1106,7 @@ class WASimClient::Private hr = registerDataRequestArea(tr, isNewRequest, dataAllocationChanged); if SUCCEEDED(hr) { // send the request and wait for Ack; Request may timeout or return a Nak. - hr = sendDataRequest(req); + hr = sendDataRequest(req, async); } if (FAILED(hr) && isNewRequest) { // delete a new request if anything failed @@ -1248,7 +1272,8 @@ class WASimClient::Private case SIMCONNECT_RECV_ID_CLIENT_DATA: { SIMCONNECT_RECV_CLIENT_DATA* data = (SIMCONNECT_RECV_CLIENT_DATA*)pData; LOG_TRC << LOG_SC_RCV_CLIENT_DATA(data); - const size_t dataSize = (size_t)pData->dwSize + 4 - sizeof(SIMCONNECT_RECV_CLIENT_DATA); // dwSize reports 4 bytes less than actual size of SIMCONNECT_RECV_CLIENT_DATA + // dwSize always under-reports by 4 bytes when sizeof(SIMCONNECT_RECV_CLIENT_DATA) is subtracted, and the minimum reported size is 4 bytes even for 0-3 bytes of actual data. + const size_t dataSize = (size_t)pData->dwSize + 4 - sizeof(SIMCONNECT_RECV_CLIENT_DATA); switch (data->dwRequestID) { case DATA_REQ_RESPONSE: { @@ -1355,12 +1380,11 @@ class WASimClient::Private LOG_WRN << "DataRequest ID " << data->dwRequestID - SIMCONNECTID_LAST << " not found in tracked requests."; return; } - // be paranoid - if (dataSize != tr->dataSize) { + // be paranoid; note that the reported pData->dwSize is never less than 4 bytes. + if (dataSize < tr->dataSize) { LOG_CRT << "Invalid data result size! Expected " << tr->dataSize << " but got " << dataSize; return; } - //unique_lock lock(mtxRequests); unique_lock datalock(tr->m_dataMutex); memcpy(tr->data.data(), (void*)&data->dwData, tr->dataSize); tr->lastUpdate = chrono::duration_cast(chrono::system_clock::now().time_since_epoch()).count(); @@ -1387,8 +1411,6 @@ class WASimClient::Private serverVersion = data->dwData; serverLastSeen = Clock::now(); LOG_DBG << "Got ping response at " << Utilities::timePointToString(serverLastSeen.load()) << " with version " << STREAM_HEX8(serverVersion); - if (serverVersion != WSMCMND_VERSION) - LOG_WRN << "Server version " << STREAM_HEX8(serverVersion) << " does not match WASimClient version " << STREAM_HEX8(WSMCMND_VERSION); break; default: @@ -1526,38 +1548,41 @@ HRESULT WASimClient::executeCalculatorCode(const std::string &code, CalcResultTy #pragma region Variable accessors ---------------------------------------------- -HRESULT WASimClient::getVariable(const VariableRequest & variable, double * pfResult) +HRESULT WASimClient::getVariable(const VariableRequest & variable, double * pfResult, std::string *psResult) { if (variable.variableId > -1 && !Utilities::isIndexedVariableType(variable.variableType)) { LOG_ERR << "Cannot get variable type '" << variable.variableType << "' by index."; return E_INVALIDARG; } - return d->getVariable(variable, pfResult); + return d->getVariable(variable, pfResult, psResult); } -HRESULT WASimClient::getLocalVariable(const std::string &variableName, double * pfResult) { - return d->getVariable(VariableRequest('L', variableName), pfResult); +HRESULT WASimClient::getLocalVariable(const std::string &variableName, double *pfResult, const std::string &unitName) { + return d->getVariable(VariableRequest(variableName, false, unitName), pfResult); } +HRESULT WASimClient::getOrCreateLocalVariable(const std::string &variableName, double *pfResult, double defaultValue, const std::string &unitName) { + return d->getVariable(VariableRequest(variableName, true, unitName), pfResult, nullptr, defaultValue); +} HRESULT WASimClient::setVariable(const VariableRequest & variable, const double value) { return d->setVariable(variable, value); } -HRESULT WASimClient::setLocalVariable(const std::string &variableName, const double value) { - return d->setVariable(VariableRequest('L', variableName), value, false); +HRESULT WASimClient::setLocalVariable(const std::string &variableName, const double value, const std::string &unitName) { + return d->setVariable(VariableRequest(variableName, false, unitName), value); } -HRESULT WASimClient::setOrCreateLocalVariable(const std::string &variableName, const double value) { - return d->setVariable(VariableRequest('L', variableName), value, true); +HRESULT WASimClient::setOrCreateLocalVariable(const std::string &variableName, const double value, const std::string &unitName) { + return d->setVariable(VariableRequest(variableName, true, unitName), value); } #pragma endregion #pragma region Data Requests ---------------------------------------------- -HRESULT WASimClient::saveDataRequest(const DataRequest &request) { - return d->addOrUpdateRequest(request); +HRESULT WASimClient::saveDataRequest(const DataRequest &request, bool async) { + return d->addOrUpdateRequest(request, async); } HRESULT WASimClient::removeDataRequest(const uint32_t requestId) { @@ -1607,7 +1632,14 @@ vector WASimClient::dataRequestIdsList() const } HRESULT WASimClient::setDataRequestsPaused(bool paused) const { - return d_const->sendServerCommand(Command(CommandId::Subscribe, (paused ? 0 : 1))); + if (isConnected()) { + HRESULT hr = d_const->sendServerCommand(Command(CommandId::Subscribe, (paused ? 0 : 1))); + if SUCCEEDED(hr) + d->requestsPaused = paused; + return hr; + } + d->requestsPaused = paused; + return S_OK; } #pragma endregion Data diff --git a/src/WASimClient_CLI/AssemblyInfo.cpp b/src/WASimClient_CLI/AssemblyInfo.cpp index 7e6462d..c4989cc 100644 --- a/src/WASimClient_CLI/AssemblyInfo.cpp +++ b/src/WASimClient_CLI/AssemblyInfo.cpp @@ -1,4 +1,4 @@ - + // THIS FILE IS GENERATED BY A SCRIPT, CHANGES WILL NOT PERSIST. EDIT THE CORRESPONDING .in TEMPLATE FILE INSTEAD. using namespace System; @@ -22,8 +22,8 @@ using namespace System::Security::Permissions; [assembly:AssemblyTrademarkAttribute(L"")]; [assembly:AssemblyCultureAttribute(L"")]; -[assembly:AssemblyVersionAttribute(L"1.1.2.0")]; -[assembly:AssemblyFileVersionAttribute("1.1.2.0")]; -[assembly:AssemblyInformationalVersionAttribute("1.1.2.0")]; +[assembly:AssemblyVersionAttribute(L"1.2.0.0")]; +[assembly:AssemblyFileVersionAttribute("1.2.0.0")]; +[assembly:AssemblyInformationalVersionAttribute("1.2.0.0")]; [assembly:ComVisible(false)]; diff --git a/src/WASimClient_CLI/Structs.h b/src/WASimClient_CLI/Structs.h index 1fa01ff..16e5e5a 100644 --- a/src/WASimClient_CLI/Structs.h +++ b/src/WASimClient_CLI/Structs.h @@ -34,6 +34,9 @@ using namespace System::Collections::Generic; using namespace System::Runtime::InteropServices; using namespace msclr::interop; +// We need this to ignore the errors about "unknown" pragmas which actually work to suppress bogus Intellisense errors. Yeah. +#pragma warning(disable:4068) + /// WASimCommander::CLI::Structs namespace. /// CLI/.NET versions of WASimCommander API and Client data structures. namespace WASimCommander::CLI::Structs @@ -148,9 +151,11 @@ namespace WASimCommander::CLI::Structs explicit Command(CommandId id) : commandId(id) { } explicit Command(CommandId id, uint32_t uData) : uData(uData), commandId(id) { } explicit Command(CommandId id, uint32_t uData, double fData) : uData(uData), fData(fData), commandId(id) { } +#pragma diag_suppress 144 // a value of type "System::String ^" cannot be used to initialize an entity of type "unsigned char" (sData is not a uchar... someone's confused) explicit Command(CommandId id, uint32_t uData, String ^sData) : uData(uData), fData(0.0), commandId(id), sData{sData} { } explicit Command(CommandId id, uint32_t uData, String ^sData, double fData) : uData(uData), fData(fData), commandId(id), sData{sData} { } explicit Command(CommandId id, uint32_t uData, String ^sData, double fData, int32_t token) : token(token), uData(uData), fData(fData), commandId(id), sData{sData} { } +#pragma diag_restore 144 void setStringData(String ^sData) { @@ -250,16 +255,19 @@ namespace WASimCommander::CLI::Structs requestType(RequestType::Calculated), calcResultType(resultType), nameOrCode(calculatorCode) { } + /// Set the `nameOrCode` member using a `string` type value. void setNameOrCode(String ^nameOrCode) { this->nameOrCode = char_array(nameOrCode); } + /// Set the `unitName` member using a `string` type value. void setUnitName(String ^unitName) { this->unitName = char_array(unitName); } + /// Serializes this `DataRequest` to a string for debugging purposes. String ^ToString() override { String ^str = String::Format( @@ -319,11 +327,14 @@ namespace WASimCommander::CLI::Structs array ^data {}; ///< Value data array. - /// Tries to populate a value reference of the desired type and returns true or false + /// Tries to populate a value reference of the desired type `T` and returns true or false /// depending on if the conversion was valid (meaning the size of requested type matches the data size). /// If the conversion fails, result is default-initialized. - /// The requested type must be a `value` type (not reference) and be default-constructible, (eg. numerics, chars), or fixed-size arrays of such types. - generic where T : value class, gcnew() + /// The requested type (`T`) must be a `value` type (not reference) and be default-constructible, (eg. numerics, chars), or fixed-size arrays of such types. + generic +#if !DOXYGEN + where T : value class, gcnew() +#endif inline bool tryConvert([Out] T %result) { if (data->Length == (int)sizeof(T)) { @@ -349,7 +360,8 @@ namespace WASimCommander::CLI::Structs return true; } - // Implicit conversion operators for various types + /// \name Implicit conversion operators for various types. + /// \{ inline static operator double(DataRequestRecord ^dr) { return dr->toType(); } inline static operator float(DataRequestRecord ^dr) { return dr->toType(); } inline static operator int64_t(DataRequestRecord ^dr) { return dr->toType(); } @@ -361,6 +373,7 @@ namespace WASimCommander::CLI::Structs inline static operator int8_t(DataRequestRecord ^dr) { return dr->toType(); } inline static operator uint8_t(DataRequestRecord ^dr) { return dr->toType(); } inline static operator String ^(DataRequestRecord ^dr) { return dr->toType(); } + /// \} // can't get generic to work //generic where T : value class, gcnew() @@ -372,6 +385,8 @@ namespace WASimCommander::CLI::Structs // return ret; //} + /// Serializes this `DataRequestRecord` to string for debugging purposes. + /// To return the request's _value_ as a string, see `tryConvert()` or the `String ^()` operator. \sa DataRequest::ToString() String ^ToString() override { return String::Format( "{0}; DataRequestRecord {{Last Update: {1}; Data: {2}}}", @@ -445,8 +460,10 @@ namespace WASimCommander::CLI::Structs explicit ListResult(const WASimCommander::Client::ListResult &r) : listType{(LookupItemType)r.listType}, result(r.result), list{gcnew ListCollectionType((int)r.list.size()) } { +#pragma diag_suppress 2242 // for list[] operator: expression must have pointer-to-object or handle-to-C++/CLI-array type but it has type "ListCollectionType ^" (um... isn't `list` a pointer?) for (const auto &pr : r.list) - list->Add(pr.first, gcnew String(pr.second.c_str())); + list[pr.first] = gcnew String(pr.second.c_str()); +#pragma diag_default 2242 } }; @@ -494,6 +511,7 @@ namespace WASimCommander::CLI::Structs int variableId { -1 }; int unitId { -1 }; Byte simVarIndex { 0 }; + bool createLVar = false; VariableRequest() {} /// Construct a variable request for specified variable type ('A', 'L', etc) and variable name. @@ -517,6 +535,14 @@ namespace WASimCommander::CLI::Structs /// Construct a variable request a Local variable ('L') with the specified name. explicit VariableRequest(String ^localVariableName) : variableType{'L'}, variableName{localVariableName} { } + /// Construct a variable request for a Local ('L') variable with the specified name. + /// `createVariable` will create the L var on the simulator if it doesn't exist yet (for "Get" as well as "Set" commands). An optional unit name can also be provided. + explicit VariableRequest(String ^localVariableName, bool createVariable) : + variableType{'L'}, variableName{localVariableName}, createLVar{createVariable} { } + /// Construct a variable request for a Local ('L') variable with the specified name. + /// `createVariable` will create the L var on the simulator if it doesn't exist yet (for "Get" as well as "Set" commands). An unit name can also be provided with this overload. + explicit VariableRequest(String ^localVariableName, bool createVariable, String ^unitName) : + variableType{'L'}, variableName{localVariableName}, unitName{unitName}, createLVar{createVariable} { } /// Construct a variable request a Local variable ('L') with the specified numeric ID. explicit VariableRequest(int localVariableId) : variableType{'L'}, variableId{localVariableId} { } @@ -532,7 +558,7 @@ namespace WASimCommander::CLI::Structs inline operator WASimCommander::Client::VariableRequest() { marshal_context mc; - WASimCommander::Client::VariableRequest r((char)variableType, mc.marshal_as(variableName), mc.marshal_as(unitName), simVarIndex); + WASimCommander::Client::VariableRequest r((char)variableType, mc.marshal_as(variableName), mc.marshal_as(unitName), simVarIndex, createLVar); r.variableId = variableId; r.unitId = unitId; return r; diff --git a/src/WASimClient_CLI/WASimClient_CLI.cpp b/src/WASimClient_CLI/WASimClient_CLI.cpp index f234258..720e6f5 100644 --- a/src/WASimClient_CLI/WASimClient_CLI.cpp +++ b/src/WASimClient_CLI/WASimClient_CLI.cpp @@ -145,6 +145,24 @@ ref class WASimClient::Private return (HR)hr; } + inline HR getVariable(VariableRequest ^var, interior_ptr pfResult, interior_ptr psResult) + { + pin_ptr pf = pfResult; + std::string s { }; + HR ret = (HR)client->getVariable(var, pf, psResult ? &s : nullptr); + if (psResult) + *psResult = marshal_as(s); + return ret; + } + + inline HR getOrCreateLocalVariable(String ^ name, interior_ptr unit, double defaultValue, interior_ptr pfResult) + { + pin_ptr pf = pfResult; + if (unit) + return (HR)client->getOrCreateLocalVariable(marshal_as(name), pf, defaultValue, marshal_as((String ^)*unit)); + return (HR)client->getOrCreateLocalVariable(marshal_as(name), pf, defaultValue); + } + ~Private() { if (client) { @@ -197,18 +215,38 @@ WASimClient::!WASimClient() m_client = nullptr; } -inline HR WASimClient::executeCalculatorCode(String ^ code, CalcResultType resultType, [Out] double % pfResult) { +inline HR WASimClient::executeCalculatorCode(String ^ code, CalcResultType resultType, double %pfResult) { return d->executeCalculatorCode(code, resultType, &pfResult, nullptr); } -inline HR WASimClient::executeCalculatorCode(String ^ code, CalcResultType resultType, [Out] String ^% psResult) { +inline HR WASimClient::executeCalculatorCode(String ^ code, CalcResultType resultType, String^ %psResult) { return d->executeCalculatorCode(code, resultType, nullptr, &psResult); } -inline HR WASimClient::executeCalculatorCode(String ^ code, CalcResultType resultType, [Out] double % pfResult, [Out] String ^% psResult) { +inline HR WASimClient::executeCalculatorCode(String ^ code, CalcResultType resultType, double %pfResult, String^ %psResult) { return d->executeCalculatorCode(code, resultType, &pfResult, &psResult); } +inline HR WASimClient::getVariable(VariableRequest ^ var, double %pfResult) { + return d->getVariable(var, &pfResult, nullptr); +} + +inline HR WASimClient::getVariable(VariableRequest ^ var, String^ %psResult) { + return d->getVariable(var, nullptr, &psResult); +} + +inline HR WASimClient::getVariable(VariableRequest ^ var, double %pfResult, String^ %psResult) { + return d->getVariable(var, &pfResult, &psResult); +} + +inline HR WASimClient::getOrCreateLocalVariable(String ^variableName, double defaultValue, double %pfResult) { + return d->getOrCreateLocalVariable(variableName, nullptr, defaultValue, &pfResult); +} + +inline HR WASimClient::getOrCreateLocalVariable(String ^variableName, String ^unitName, double defaultValue, double %pfResult) { + return d->getOrCreateLocalVariable(variableName, &unitName, defaultValue, &pfResult); +} + inline array^ WASimClient::dataRequests() { const std::vector &res = m_client->dataRequests(); diff --git a/src/WASimClient_CLI/WASimClient_CLI.h b/src/WASimClient_CLI/WASimClient_CLI.h index 33dd6a6..d7ccff8 100644 --- a/src/WASimClient_CLI/WASimClient_CLI.h +++ b/src/WASimClient_CLI/WASimClient_CLI.h @@ -52,18 +52,25 @@ namespace WASimCommander::CLI::Client /// /// The main difference is that callbacks from the C++ version are delivered here as managed Events, with defined delegate types to handle them. /// And unlike the callback system, the events can have multiple subscribers if needed. + /// + /// \note Events are delivered asyncronously from a separtely running thread. The event handlers should be reentrant since they could be callled at any time. \n + /// Typically, interactions with GUI components will not be possible directly from inside the event handlers -- use a `Dispatcher` to marshal GUI interactions + /// back to the main thread. public ref class WASimClient { public: // Delegates ----------------------------------- + /// \name Event handler delegate types + /// \{ delegate void ClientEventDelegate(ClientEvent ^); ///< Event delegate for Client events (`OnClientEvent`) delegate void ListResultsDelegate(ListResult ^); ///< Event delegate for delivering list results, eg. of local variables sent from Server (`OnListResults`). delegate void DataDelegate(DataRequestRecord ^); ///< Event delegate for subscription result data (`OnDataReceived`). delegate void LogDelegate(LogRecord ^, LogSource); ///< Event delegate for log entries (from both Client and Server) (`OnLogRecordReceived`). delegate void CommandResultDelegate(Command ^); ///< Event delegate for command responses returned from server (`OnCommandResult`). delegate void ResponseDelegate(Command ^); ///< Event delegate for all Command structures received from server (`OnResponseReceived`). + /// \} // Events ----------------------------------- @@ -83,18 +90,26 @@ namespace WASimCommander::CLI::Client #undef DELEGATE_DECL /// Construct a new client with the given ID. The ID must be unique among any other possible clients and cannot be zero. - /// See \refwcc{WASimClient::WASimClient()} for more details. + /// See \refwccc{WASimClient()} for more details. explicit WASimClient(UInt32 clientId); /// Construct a new client with the given ID and with initial settings read from the file specified in `configFile` (.ini format, see default file for example). - /// The client ID must be unique among any other possible clients and cannot be zero. See \refwcc{WASimClient::WASimClient()} for more details. + /// The client ID must be unique among any other possible clients and cannot be zero. See \refwccc{WASimClient()} for more details. explicit WASimClient(UInt32 clientId, String ^configFile); - /// This class implements a Disposable type object and should be disposed-of appropriately. +#if DOXYGEN + /// This class implements a Disposable type object and should be disposed-of appropriately by calling `client.Dispose()` when the instance is no longer needed. /// Any open network connections are automatically closed upon destruction, though it is better to close them yourself before deleting the client. + void Dispose(); +#else ~WASimClient(); - !WASimClient(); ///< \private + !WASimClient(); +#endif // Status ----------------------------------- + /// \name Network actions, status, and settings + /// \{ + + /// Get current connection status of this client. \sa WASimCommander::Client::ClientStatus ClientStatus status() { return (ClientStatus)m_client->status(); } /// Returns true if connected to the Simulator (SimConnect). bool isInitialized() { return m_client->isInitialized(); } @@ -109,92 +124,131 @@ namespace WASimCommander::CLI::Client /// Connect to the Simulator engine on a local connection. /// (optional) Maximum time to wait for response, in milliseconds. Zero (default) means to use the `defaultTimeout()` value. - /// \return See \refwcc{connectSimulator(uint32_t)} + /// \return See \refwccc{connectSimulator(uint32_t)} HR connectSimulator([Optional] Nullable timeout) { return (HR)m_client->connectSimulator(timeout.HasValue ? timeout.Value : 0); } /// Connect to the Simulator engine using a specific network configuration ID from a SimConnect.cfg file. The file must be in the same folder as the executable running the Client. /// network configuration ID from a SimConnect.cfg file. The file must be in the same folder as the executable running the Client. \n /// (optional) Maximum time to wait for response, in milliseconds. Zero (default) means to use the `defaultTimeout()` value. - /// \return See \refwcc{connectSimulator(int,uint32_t)} + /// \return See \refwccc{connectSimulator(int,uint32_t)} HR connectSimulator(int networkConfigId, [Optional] Nullable timeout) { return (HR)m_client->connectSimulator(networkConfigId, timeout.HasValue ? timeout.Value : 0); } + /// See \refwccc{disconnectSimulator()} void disconnectSimulator() { m_client->disconnectSimulator(); } + /// See \refwccc{pingServer()} uint32_t pingServer([Optional] Nullable timeout) { return m_client->pingServer(timeout.HasValue ? timeout.Value : 0); } + /// See \refwccc{connectServer()} HR connectServer([Optional] Nullable timeout) { return (HR)m_client->connectServer(timeout.HasValue ? timeout.Value : 0); } - void disconnectServer() { m_client->disconnectServer(); } + void disconnectServer() { m_client->disconnectServer(); } ///< See \refwccc{disconnectServer()} // Settings ----------------------------------- - uint32_t defaultTimeout() { return m_client->defaultTimeout(); } - void setDefaultTimeout(uint32_t ms) { m_client->setDefaultTimeout(ms); } - int networkConfigurationId() { return m_client->networkConfigurationId(); } - void setNetworkConfigurationId(int configId) { m_client->setNetworkConfigurationId(configId); } + uint32_t defaultTimeout() { return m_client->defaultTimeout(); } ///< See \refwccc{defaultTimeout()} + void setDefaultTimeout(uint32_t ms) { m_client->setDefaultTimeout(ms); } ///< See \refwccc{setDefaultTimeout()} + int networkConfigurationId() { return m_client->networkConfigurationId(); } ///< See \refwccc{networkConfigurationId()} + void setNetworkConfigurationId(int configId) { m_client->setNetworkConfigurationId(configId); } ///< See \refwccc{setNetworkConfigurationId()} + + /// \} + /// \name High level API + /// \{ // Calculator code ----------------------------------- - /// Execute calculator code without result + /// Execute calculator code without result \sa \refwccc{executeCalculatorCode()} HR executeCalculatorCode(String^ code) { return (HR)m_client->executeCalculatorCode(marshal_as(code)); } - /// Execute calculator code with a numeric result type. \private + /// Execute calculator code with a numeric result type. \sa \refwccc{executeCalculatorCode()} HR executeCalculatorCode(String^ code, CalcResultType resultType, [Out] double %pfResult); - /// Execute calculator code with a string result type. \private + /// Execute calculator code with a string result type. \sa \refwccc{executeCalculatorCode()} HR executeCalculatorCode(String^ code, CalcResultType resultType, [Out] String^ %psResult); - /// Execute calculator code with both numeric and string results. \private + /// Execute calculator code with both numeric and string results. \sa \refwccc{executeCalculatorCode()} HR executeCalculatorCode(String^ code, CalcResultType resultType, [Out] double %pfResult, [Out] String^ %psResult); // Variables accessors ------------------------------ - HR getVariable(VariableRequest ^var, [Out] double %pfResult) { - pin_ptr pf = &pfResult; - return (HR)m_client->getVariable(var, pf); - } + /// Get the value of a variable with a numeric result type. This is the most typical use case since most variable types are numeric. \sa \refwccc{getVariable()} + HR getVariable(VariableRequest ^var, [Out] double %pfResult); + /// Get the value of a variable with a string result type. The request is executed as calculator code since that is the only way to get string results. + /// Note that only some 'A', 'C', and 'T' type variables can have a string value type in the first place. \sa \refwccc{getVariable()} + HR getVariable(VariableRequest ^var, [Out] String^ %psResult); + /// Get the value of a variable with _either_ a numeric or string result based on the unit type of the requested variable. + /// The request is executed as calculator code since that is the only way to get string results. Unlike `executeCalculatorCode()`, this method will not return a string representation of a numeric value. + /// Note that only some 'A', 'C', and 'T' type variables can have a string value type in the first place. \sa \refwccc{getVariable()} + HR getVariable(VariableRequest ^var, [Out] double %pfResult, [Out] String^ %psResult); ///< See \refwccc{getVariable()} + + /// See \refwccc{getLocalVariable()} HR getLocalVariable(String ^variableName, [Out] double %pfResult) { return getVariable(gcnew VariableRequest(variableName), pfResult); } - + /// See \refwccc{getLocalVariable()} + HR getLocalVariable(String ^variableName, String ^unitName, [Out] double %pfResult) { return getVariable(gcnew VariableRequest(variableName, false, unitName), pfResult); } + /// \sa \refwccc{getOrCreateLocalVariable()} + HR getOrCreateLocalVariable(String ^variableName, double defaultValue, [Out] double %pfResult); + /// \sa \refwccc{getOrCreateLocalVariable()} + HR getOrCreateLocalVariable(String ^variableName, String ^unitName, double defaultValue, [Out] double %pfResult); + + /// See \refwccc{setVariable()} HR setVariable(VariableRequest ^var, const double value) { return (HR)m_client->setVariable(var, value); } + /// See \refwccc{setLocalVariable()} HR setLocalVariable(String ^variableName, const double value) { return (HR)m_client->setLocalVariable(marshal_as(variableName), value); } + HR setLocalVariable(String ^variableName, String ^unitName, const double value) { + return (HR)m_client->setLocalVariable(marshal_as(variableName), value, marshal_as(unitName)); + } + /// See \refwccc{setOrCreateLocalVariable()} HR setOrCreateLocalVariable(String ^variableName, const double value) { return (HR)m_client->setOrCreateLocalVariable(marshal_as(variableName), value); } + /// See \refwccc{setOrCreateLocalVariable()} + HR setOrCreateLocalVariable(String ^variableName, String ^unitName, const double value) { + return (HR)m_client->setOrCreateLocalVariable(marshal_as(variableName), value, marshal_as(unitName)); + } // Data subscriptions ------------------------------- - HR saveDataRequest(DataRequest ^request) { return (HR)m_client->saveDataRequest(request); } - HR removeDataRequest(const uint32_t requestId) { return (HR)m_client->removeDataRequest(requestId); } - HR updateDataRequest(uint32_t requestId) { return (HR)m_client->updateDataRequest(requestId); } + HR saveDataRequest(DataRequest ^request) { return (HR)m_client->saveDataRequest(request); } ///< See \refwccc{saveDataRequest()} as used with `async = false` + HR saveDataRequestAsync(DataRequest ^request) { return (HR)m_client->saveDataRequest(request, true); } ///< See \refwccc{saveDataRequest()} as used with `async = true` + HR removeDataRequest(const uint32_t requestId) { return (HR)m_client->removeDataRequest(requestId); } ///< See \refwccc{removeDataRequest()} + HR updateDataRequest(uint32_t requestId) { return (HR)m_client->updateDataRequest(requestId); } ///< See \refwccc{updateDataRequest()} - DataRequestRecord ^dataRequest(uint32_t requestId) { return gcnew DataRequestRecord(m_client->dataRequest(requestId)); } - array ^dataRequests(); - array ^dataRequestIdsList(); + DataRequestRecord ^dataRequest(uint32_t requestId) { return gcnew DataRequestRecord(m_client->dataRequest(requestId)); } ///< See \refwccc{dataRequest()} + array ^dataRequests(); ///< See \refwccc{dataRequests()} + array ^dataRequestIdsList(); ///< See \refwccc{dataRequestIdsList()} - HR setDataRequestsPaused(bool paused) { return (HR)m_client->setDataRequestsPaused(paused); } + HR setDataRequestsPaused(bool paused) { return (HR)m_client->setDataRequestsPaused(paused); } ///< See \refwccc{setDataRequestsPaused()} // Custom Event registration -------------------------- - HR registerEvent(RegisteredEvent ^eventData) { return (HR)m_client->registerEvent(eventData); } - HR removeEvent(uint32_t eventId) { return (HR)m_client->removeEvent(eventId); } - HR transmitEvent(uint32_t eventId) { return (HR)m_client->transmitEvent(eventId); } + HR registerEvent(RegisteredEvent ^eventData) { return (HR)m_client->registerEvent(eventData); } ///< See \refwccc{registerEvent()} + HR removeEvent(uint32_t eventId) { return (HR)m_client->removeEvent(eventId); } ///< See \refwccc{removeEvent()} + HR transmitEvent(uint32_t eventId) { return (HR)m_client->transmitEvent(eventId); } ///< See \refwccc{transmitEvent()} - RegisteredEvent ^registeredEvent(uint32_t eventId) { return gcnew RegisteredEvent(m_client->registeredEvent(eventId)); } - array ^registeredEvents(); + RegisteredEvent ^registeredEvent(uint32_t eventId) { return gcnew RegisteredEvent(m_client->registeredEvent(eventId)); } ///< See \refwccc{registeredEvent()} + array ^registeredEvents(); ///< See \refwccc{registeredEvents()} // Simulator Key Events ------------------------------ + /// See \refwccc{sendKeyEvent(uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t) const} HR sendKeyEvent(uint32_t keyEventId, [Optional] Nullable v1, [Optional] Nullable v2, [Optional] Nullable v3, [Optional] Nullable v4, [Optional] Nullable v5) { return (HR)m_client->sendKeyEvent(keyEventId, v1.GetValueOrDefault(0), v2.GetValueOrDefault(0), v3.GetValueOrDefault(0), v4.GetValueOrDefault(0), v5.GetValueOrDefault(0)); } + /// See \refwccc{sendKeyEvent(const std::string&, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t)} HR sendKeyEvent(String ^keyEventName, [Optional] Nullable v1, [Optional] Nullable v2, [Optional] Nullable v3, [Optional] Nullable v4, [Optional] Nullable v5) { return (HR)m_client->sendKeyEvent(marshal_as(keyEventName), v1.GetValueOrDefault(0), v2.GetValueOrDefault(0), v3.GetValueOrDefault(0), v4.GetValueOrDefault(0), v5.GetValueOrDefault(0)); } // Meta data retrieval -------------------------------- + /// See \refwccc{list()} HR list(LookupItemType itemsType) { return (HR)m_client->list((WSE::LookupItemType)itemsType); } + /// See \refwccc{lookup()} HR lookup(LookupItemType itemType, String ^itemName, [Out] Int32 %piResult) { pin_ptr pi = &piResult; return (HR)m_client->lookup((WSE::LookupItemType)itemType, marshal_as(itemName), pi); } - // Low level API -------------------------------- + /// \} + /// \name Low level API + /// \{ + /// See \refwccc{sendCommand()} HR sendCommand(Command ^command) { return (HR)m_client->sendCommand(command); } + /// See \refwccc{sendCommandWithResponse()} HR sendCommandWithResponse(Command ^command, [Out] Command^ %response, [Optional] Nullable timeout) { WASimCommander::Command resp {}; @@ -203,15 +257,21 @@ namespace WASimCommander::CLI::Client return (HR)hr; } - // Logging settings -------------------------------- + /// \} + /// \name Logging settings + /// \{ + /// See \refwccc{logLevel()} LogLevel logLevel(LogFacility facility, LogSource source) { return (LogLevel)m_client->logLevel((WSE::LogFacility)facility, (WASimCommander::Client::LogSource)source); } + /// See \refwccc{setLogLevel()} void setLogLevel(LogLevel level, LogFacility facility, LogSource source) { m_client->setLogLevel((WSE::LogLevel)level, (WSE::LogFacility)facility, (WASimCommander::Client::LogSource)source); } + /// \} + private: WASimCommander::Client::WASimClient *m_client = nullptr; diff --git a/src/WASimClient_CLI/WASimClient_CLI.vcxproj b/src/WASimClient_CLI/WASimClient_CLI.vcxproj index 280e67b..b50c9aa 100644 --- a/src/WASimClient_CLI/WASimClient_CLI.vcxproj +++ b/src/WASimClient_CLI/WASimClient_CLI.vcxproj @@ -9,6 +9,10 @@ Release-net6 x64 + + Release-net7 + x64 + Release-netfw x64 @@ -56,6 +60,16 @@ x64 true + + false + v143 + NetCore + net7.0 + DynamicLibrary + Unicode + x64 + true + false v143 @@ -83,6 +97,10 @@ + + + + @@ -108,6 +126,13 @@ $(MSFS_SDK)\SimConnect SDK\include;$(IncludePath) WASimCommander.WASimClient + + true + true + false + $(MSFS_SDK)\SimConnect SDK\include;$(IncludePath) + WASimCommander.WASimClient + true false @@ -192,7 +217,7 @@ $(ProjectDir)deps.manifest %(AdditionalManifestFiles) - + NotUsing Level3 @@ -218,6 +243,32 @@ $(ProjectDir)deps.manifest %(AdditionalManifestFiles) + + + NotUsing + Level3 + NETFRAMEWORK;WSMCMND_API_STATIC;_CRT_SECURE_NO_DEPRECATE;_CRT_NONSTDC_NO_DEPRECATE_DEBUG;_WINDOWS;NDEBUG;%(PreprocessorDefinitions) + stdcpp17 + stdc17 + /Zc:__cplusplus /Zc:twoPhase- + true + Speed + true + true + StdCall + MultiThreadedDLL + true + 4635;%(DisableSpecificWarnings) + true + + + /ignore:4099 + Default + + + $(ProjectDir)deps.manifest %(AdditionalManifestFiles) + + diff --git a/src/WASimCommander.sln b/src/WASimCommander.sln index f3a2cb8..2a1577c 100644 --- a/src/WASimCommander.sln +++ b/src/WASimCommander.sln @@ -76,6 +76,8 @@ Global Release-DLL|x64 = Release-DLL|x64 Release-net6|MSFS = Release-net6|MSFS Release-net6|x64 = Release-net6|x64 + Release-net7|MSFS = Release-net7|MSFS + Release-net7|x64 = Release-net7|x64 Release-netfw|MSFS = Release-netfw|MSFS Release-netfw|x64 = Release-netfw|x64 EndGlobalSection @@ -96,6 +98,9 @@ Global {A5468B35-BBBD-4C55-97ED-81BFE343B0E4}.Release-DLL|x64.ActiveCfg = Release|MSFS {A5468B35-BBBD-4C55-97ED-81BFE343B0E4}.Release-net6|MSFS.ActiveCfg = Release|MSFS {A5468B35-BBBD-4C55-97ED-81BFE343B0E4}.Release-net6|x64.ActiveCfg = Release|MSFS + {A5468B35-BBBD-4C55-97ED-81BFE343B0E4}.Release-net7|MSFS.ActiveCfg = Release|MSFS + {A5468B35-BBBD-4C55-97ED-81BFE343B0E4}.Release-net7|MSFS.Build.0 = Release|MSFS + {A5468B35-BBBD-4C55-97ED-81BFE343B0E4}.Release-net7|x64.ActiveCfg = Release|MSFS {A5468B35-BBBD-4C55-97ED-81BFE343B0E4}.Release-netfw|MSFS.ActiveCfg = Release|MSFS {A5468B35-BBBD-4C55-97ED-81BFE343B0E4}.Release-netfw|MSFS.Build.0 = Release|MSFS {A5468B35-BBBD-4C55-97ED-81BFE343B0E4}.Release-netfw|x64.ActiveCfg = Release|MSFS @@ -115,6 +120,10 @@ Global {639093FF-FD94-4E89-92AC-C6FABE5DF664}.Release-net6|MSFS.ActiveCfg = Release|x64 {639093FF-FD94-4E89-92AC-C6FABE5DF664}.Release-net6|x64.ActiveCfg = Release|x64 {639093FF-FD94-4E89-92AC-C6FABE5DF664}.Release-net6|x64.Build.0 = Release|x64 + {639093FF-FD94-4E89-92AC-C6FABE5DF664}.Release-net7|MSFS.ActiveCfg = Release-DLL|x64 + {639093FF-FD94-4E89-92AC-C6FABE5DF664}.Release-net7|MSFS.Build.0 = Release-DLL|x64 + {639093FF-FD94-4E89-92AC-C6FABE5DF664}.Release-net7|x64.ActiveCfg = Release|x64 + {639093FF-FD94-4E89-92AC-C6FABE5DF664}.Release-net7|x64.Build.0 = Release|x64 {639093FF-FD94-4E89-92AC-C6FABE5DF664}.Release-netfw|MSFS.ActiveCfg = Release|x64 {639093FF-FD94-4E89-92AC-C6FABE5DF664}.Release-netfw|MSFS.Build.0 = Release|x64 {639093FF-FD94-4E89-92AC-C6FABE5DF664}.Release-netfw|x64.ActiveCfg = Release|x64 @@ -132,6 +141,9 @@ Global {E9AD8656-009B-4A6A-832B-0D19DE288BFC}.Release-DLL|x64.ActiveCfg = Release-DLL|x64 {E9AD8656-009B-4A6A-832B-0D19DE288BFC}.Release-net6|MSFS.ActiveCfg = Release-DLL|x64 {E9AD8656-009B-4A6A-832B-0D19DE288BFC}.Release-net6|x64.ActiveCfg = Release|x64 + {E9AD8656-009B-4A6A-832B-0D19DE288BFC}.Release-net7|MSFS.ActiveCfg = Release-DLL|x64 + {E9AD8656-009B-4A6A-832B-0D19DE288BFC}.Release-net7|MSFS.Build.0 = Release-DLL|x64 + {E9AD8656-009B-4A6A-832B-0D19DE288BFC}.Release-net7|x64.ActiveCfg = Release|x64 {E9AD8656-009B-4A6A-832B-0D19DE288BFC}.Release-netfw|MSFS.ActiveCfg = Release|x64 {E9AD8656-009B-4A6A-832B-0D19DE288BFC}.Release-netfw|MSFS.Build.0 = Release|x64 {E9AD8656-009B-4A6A-832B-0D19DE288BFC}.Release-netfw|x64.ActiveCfg = Release|x64 @@ -152,6 +164,10 @@ Global {DAF5B792-C4E6-4E54-9CBF-0A0335E80306}.Release-net6|MSFS.Build.0 = Release-net6|x64 {DAF5B792-C4E6-4E54-9CBF-0A0335E80306}.Release-net6|x64.ActiveCfg = Release-net6|x64 {DAF5B792-C4E6-4E54-9CBF-0A0335E80306}.Release-net6|x64.Build.0 = Release-net6|x64 + {DAF5B792-C4E6-4E54-9CBF-0A0335E80306}.Release-net7|MSFS.ActiveCfg = Release-net7|x64 + {DAF5B792-C4E6-4E54-9CBF-0A0335E80306}.Release-net7|MSFS.Build.0 = Release-net7|x64 + {DAF5B792-C4E6-4E54-9CBF-0A0335E80306}.Release-net7|x64.ActiveCfg = Release-net7|x64 + {DAF5B792-C4E6-4E54-9CBF-0A0335E80306}.Release-net7|x64.Build.0 = Release-net7|x64 {DAF5B792-C4E6-4E54-9CBF-0A0335E80306}.Release-netfw|MSFS.ActiveCfg = Release|x64 {DAF5B792-C4E6-4E54-9CBF-0A0335E80306}.Release-netfw|MSFS.Build.0 = Release|x64 {DAF5B792-C4E6-4E54-9CBF-0A0335E80306}.Release-netfw|x64.ActiveCfg = Release-netfw|x64 @@ -164,11 +180,14 @@ Global {5B7D7234-D6C8-4D1F-B135-C5297D6476D8}.Release|MSFS.ActiveCfg = Release|x64 {5B7D7234-D6C8-4D1F-B135-C5297D6476D8}.Release|MSFS.Build.0 = Release|x64 {5B7D7234-D6C8-4D1F-B135-C5297D6476D8}.Release|x64.ActiveCfg = Release|x64 - {5B7D7234-D6C8-4D1F-B135-C5297D6476D8}.Release|x64.Build.0 = Release|x64 {5B7D7234-D6C8-4D1F-B135-C5297D6476D8}.Release-DLL|MSFS.ActiveCfg = Release|x64 {5B7D7234-D6C8-4D1F-B135-C5297D6476D8}.Release-DLL|x64.ActiveCfg = Release|x64 {5B7D7234-D6C8-4D1F-B135-C5297D6476D8}.Release-net6|MSFS.ActiveCfg = Release|x64 {5B7D7234-D6C8-4D1F-B135-C5297D6476D8}.Release-net6|x64.ActiveCfg = Release|x64 + {5B7D7234-D6C8-4D1F-B135-C5297D6476D8}.Release-net6|x64.Build.0 = Release|x64 + {5B7D7234-D6C8-4D1F-B135-C5297D6476D8}.Release-net7|MSFS.ActiveCfg = Release|x64 + {5B7D7234-D6C8-4D1F-B135-C5297D6476D8}.Release-net7|MSFS.Build.0 = Release|x64 + {5B7D7234-D6C8-4D1F-B135-C5297D6476D8}.Release-net7|x64.ActiveCfg = Release-net7|x64 {5B7D7234-D6C8-4D1F-B135-C5297D6476D8}.Release-netfw|MSFS.ActiveCfg = Release|x64 {5B7D7234-D6C8-4D1F-B135-C5297D6476D8}.Release-netfw|MSFS.Build.0 = Release|x64 {5B7D7234-D6C8-4D1F-B135-C5297D6476D8}.Release-netfw|x64.ActiveCfg = Release|x64 @@ -187,6 +206,9 @@ Global {523ABD54-4C1A-4F21-8977-5EFA5821F71D}.Release-DLL|x64.ActiveCfg = Release-DLL|x64 {523ABD54-4C1A-4F21-8977-5EFA5821F71D}.Release-net6|MSFS.ActiveCfg = Release-DLL|x64 {523ABD54-4C1A-4F21-8977-5EFA5821F71D}.Release-net6|x64.ActiveCfg = Release|x64 + {523ABD54-4C1A-4F21-8977-5EFA5821F71D}.Release-net7|MSFS.ActiveCfg = Release-DLL|x64 + {523ABD54-4C1A-4F21-8977-5EFA5821F71D}.Release-net7|MSFS.Build.0 = Release-DLL|x64 + {523ABD54-4C1A-4F21-8977-5EFA5821F71D}.Release-net7|x64.ActiveCfg = Release|x64 {523ABD54-4C1A-4F21-8977-5EFA5821F71D}.Release-netfw|MSFS.ActiveCfg = Release|x64 {523ABD54-4C1A-4F21-8977-5EFA5821F71D}.Release-netfw|MSFS.Build.0 = Release|x64 {523ABD54-4C1A-4F21-8977-5EFA5821F71D}.Release-netfw|x64.ActiveCfg = Release|x64 @@ -200,6 +222,8 @@ Global {C6D4303F-E717-4257-990B-2CAE894898A0}.Release-DLL|x64.ActiveCfg = Release|Any CPU {C6D4303F-E717-4257-990B-2CAE894898A0}.Release-net6|MSFS.ActiveCfg = Release|Any CPU {C6D4303F-E717-4257-990B-2CAE894898A0}.Release-net6|x64.ActiveCfg = Release|Any CPU + {C6D4303F-E717-4257-990B-2CAE894898A0}.Release-net7|MSFS.ActiveCfg = Release|Any CPU + {C6D4303F-E717-4257-990B-2CAE894898A0}.Release-net7|x64.ActiveCfg = Release|Any CPU {C6D4303F-E717-4257-990B-2CAE894898A0}.Release-netfw|MSFS.ActiveCfg = Release|Any CPU {C6D4303F-E717-4257-990B-2CAE894898A0}.Release-netfw|x64.ActiveCfg = Release|Any CPU EndGlobalSection diff --git a/src/WASimModule/WASimModule.cpp b/src/WASimModule/WASimModule.cpp index b2bcdca..8ccc264 100644 --- a/src/WASimModule/WASimModule.cpp +++ b/src/WASimModule/WASimModule.cpp @@ -131,11 +131,23 @@ struct TrackedRequest : DataRequest void checkRequestType() { - // Anything besides L/A/T type vars just gets converted to calc code. - if (requestType == RequestType::Named && variableId < 0 && !Utilities::isIndexedVariableType(varTypePrefix)) { - const ostringstream codeStr = ostringstream() << "(" << varTypePrefix << ':' << nameOrCode << ')'; + // Anything besides L/A/T type vars just gets converted to calc code, as well as any A vars with "string" unit type. + bool isString = false; + if (requestType == RequestType::Named && variableId < 0 && + (!Utilities::isIndexedVariableType(varTypePrefix) || (isString = (unitId < 0 && varTypePrefix == 'A' && !strcasecmp(unitName, "string")))) + ) { + ostringstream codeStr = ostringstream() << '(' << varTypePrefix << ':' << nameOrCode; + if (unitName[0] != '\0') + codeStr << ',' << unitName; + codeStr << ')'; setNameOrCode(codeStr.str().c_str()); requestType = RequestType::Calculated; + if (isString) + calcResultType = CalcResultType::String; + else if (valueSize > DATA_TYPE_INT64 || valueSize < 4) + calcResultType = CalcResultType::Integer; + else + calcResultType = CalcResultType::Double; } } @@ -215,7 +227,7 @@ struct calcResult_t string sVal; void setF(const FLOAT64 val) { fVal = val; resultSize = sizeof(FLOAT64); resultMemberIndex = 0; } void setI(const SINT32 val) { iVal = val; resultSize = sizeof(SINT32); resultMemberIndex = 1; } - void setS(const string &&val) { sVal = move(val); sVal.resize(strSize); resultSize = strSize; resultMemberIndex = 2; } + void setS(const string &&val) { sVal = std::move(val); sVal.resize(strSize); resultSize = strSize; resultMemberIndex = 2; } }; typedef map clientMap_t; @@ -237,7 +249,6 @@ steady_clock::time_point g_tpNextTick { steady_clock::now() }; SIMCONNECT_CLIENT_EVENT_ID g_nextClientEventId = SIMCONNECTID_LAST; SIMCONNECT_CLIENT_DATA_DEFINITION_ID g_nextClienDataId = SIMCONNECTID_LAST; bool g_triggersRegistered = false; -bool g_simConnectQuitEvent = false; #pragma endregion Globals //---------------------------------------------------------------------------- @@ -246,6 +257,8 @@ bool g_simConnectQuitEvent = false; bool sendResponse(const Client *c, const Command &cmd) { + if (c->status != ClientStatus::Connected) + return false; LOG_TRC << "Sending command to " << c->name << ": " << cmd; return INVOKE_SIMCONNECT( SetClientData, g_hSimConnect, @@ -279,6 +292,8 @@ bool sendPing(const Client *c) { bool sendLogRecord(const Client *c, const LogRecord &log) { + if (c->status != ClientStatus::Connected) + return false; //if (c->logLevel < LogLevel::Trace) LOG_TRC << "Sending LogRecord to " << c->name << ": " << log; return INVOKE_SIMCONNECT( SetClientData, g_hSimConnect, @@ -290,6 +305,8 @@ bool sendLogRecord(const Client *c, const LogRecord &log) bool writeRequestData(const Client *c, const TrackedRequest *tr, void *data) { + if (c->status != ClientStatus::Connected) + return false; LOG_TRC << "Writing request ID " << tr->requestId << " data for " << c->name << " to CDA / CDD ID " << tr->dataId << " of size " << tr->dataSize; return INVOKE_SIMCONNECT( SetClientData, g_hSimConnect, @@ -464,7 +481,7 @@ Client *getOrCreateClient(uint32_t clientId) registerClientKeyEventDataArea(&c); // move client record into map - Client *pC = &g_mClients.emplace(clientId, move(c)).first->second; + Client *pC = &g_mClients.emplace(clientId, std::move(c)).first->second; // save mappings of the command and request data IDs (which SimConnect sends us) to the client record; for lookup in message dispatch. g_mDefinitionIds.emplace(piecewise_construct, forward_as_tuple(c.cddID_command), forward_as_tuple(RecordType::CommandData, pC)); // no try_emplace? g_mDefinitionIds.emplace(piecewise_construct, forward_as_tuple(c.cddID_request), forward_as_tuple(RecordType::RequestData, pC)); @@ -496,7 +513,9 @@ Client *connectClient(uint32_t id) return connectClient(getOrCreateClient(id)); } -void checkTriggerEventNeeded(); // forward, in Utility Functions just below +// forwards, in Utility Functions just below +void checkTriggerEventNeeded(); +void resumeTriggerEvent(); // marks client as disconnected or timed out and clears any saved requests/events void disconnectClient(Client *c, ClientStatus newStatus = ClientStatus::Disconnected) @@ -532,6 +551,12 @@ void disconnectAllClients(const char *message = nullptr) disconnectClient(&it.second, message, ClientStatus::Disconnected); } +void disableAllClients() +{ + for (auto &it : g_mClients) + it.second.status = ClientStatus::Disconnected; +} + // callback for logfault IdProxyHandler log handler void CALLBACK clientLogHandler(const uint32_t id, const logfault::Message &msg) { @@ -567,19 +592,19 @@ bool setClientLogLevel(Client *c, LogLevel level) //---------------------------------------------------------------------------- // this runs once we have any requests to keep updated which essentially starts the tick() processing. -void registerTriggerEvents() +void resumeTriggerEvent() { // use "Frame" event as trigger for our tick() loop - if FAILED(INVOKE_SIMCONNECT(SubscribeToSystemEvent, g_hSimConnect, (SIMCONNECT_CLIENT_EVENT_ID)EVENT_FRAME, "Frame")) + if FAILED(INVOKE_SIMCONNECT(SetSystemEventState, g_hSimConnect, (SIMCONNECT_CLIENT_EVENT_ID)EVENT_FRAME, SIMCONNECT_STATE_ON)) return; g_triggersRegistered = true; LOG_INF << "DataRequest data update processing started."; } // and here is the opposite... if all clients disconnect we can stop the ticker loop. -void unregisterTriggerEvents() +void pauseTriggerEvent() { - if FAILED(INVOKE_SIMCONNECT(UnsubscribeFromSystemEvent, g_hSimConnect, (SIMCONNECT_CLIENT_EVENT_ID)EVENT_FRAME)) + if FAILED(INVOKE_SIMCONNECT(SetSystemEventState, g_hSimConnect, (SIMCONNECT_CLIENT_EVENT_ID)EVENT_FRAME, SIMCONNECT_STATE_OFF)) return; g_triggersRegistered = false; LOG_INF << "DataRequest update processing stopped."; @@ -591,10 +616,11 @@ void unregisterTriggerEvents() void checkTriggerEventNeeded() { for (const clientMap_t::value_type &it : g_mClients) { - if (it.second.status == ClientStatus::Connected) + const Client &c = it.second; + if (c.status == ClientStatus::Connected && !c.pauseDataUpdates && c.requests.size()) return; } - unregisterTriggerEvents(); + pauseTriggerEvent(); } bool execCalculatorCode(const char *code, calcResult_t &result, bool precompiled = false) @@ -634,11 +660,14 @@ bool getNamedVariableValue(char varType, calcResult_t &result) case 'L': if (result.varId < 0) return false; - result.setF(get_named_variable_value(result.varId)); + if (result.unitId > -1) + result.setF(get_named_variable_typed_value(result.varId, result.unitId)); + else + result.setF(get_named_variable_value(result.varId)); break; case 'A': - if (result.varId < 0) + if (result.varId < 0 || result.unitId < 0) return false; result.setF(aircraft_varget(result.varId, result.unitId, result.varIndex)); break; @@ -754,11 +783,11 @@ int getVariableId(char varType, const char *name, bool createLocal = false) // Parse a command string to find a variable name/unit/index and populates the respective reference params. // Lookups are done on var names, depending on varType, and unit strings, to attempt conversion to IDs. // Used by setVariable() and getVariable(). Only handles A/L/T var types (not needed for others). -bool parseVariableString(const char varType, const char *data, ID &varId, bool createLocal, ENUM *unitId = nullptr, uint8_t *varIndex = nullptr, string *varName = nullptr) +bool parseVariableString(const char varType, const char *data, ID &varId, bool createLocal, ENUM *unitId = nullptr, uint8_t *varIndex = nullptr, string *varName = nullptr, bool *existed = nullptr) { string_view svVar(data, strlen(data)), svUnit{}; - if (varType == 'A') { + if (varType != 'T') { // Check for unit type after variable name/id and comma const size_t idx = svVar.find(','); if (idx != string::npos) { @@ -766,12 +795,12 @@ bool parseVariableString(const char varType, const char *data, ID &varId, bool c svVar.remove_suffix(svVar.size() - idx); } // check for index value at end of SimVar name/ID - if (svVar.size() > 3) { + if (varType == 'A' && svVar.size() > 3) { const string_view &svIndex = svVar.substr(svVar.size() - 3); const size_t idx = svIndex.find(':'); if (idx != string::npos) { // strtoul returns zero if conversion fails, which works fine since zero is not a valid simvar index - if (varIndex) + if (!!varIndex) *varIndex = strtoul(svIndex.data() + idx + 1, nullptr, 10); svVar.remove_suffix(3 - idx); } @@ -781,18 +810,28 @@ bool parseVariableString(const char varType, const char *data, ID &varId, bool c if (svVar.empty()) return false; - if (varName) - *varName = string(svVar); // Try to parse the remaining var name string as a numeric ID auto result = from_chars(svVar.data(), svVar.data() + svVar.size(), varId); // if number conversion failed, look up variable id - if (result.ec != errc()) - varId = getVariableId(varType, string(svVar).c_str(), createLocal); + if (result.ec != errc()) { + const std::string vname(svVar); + if (createLocal && !!existed) { + varId = check_named_variable(vname.c_str()); + *existed = varId > -1; + if (!*existed) + varId = register_named_variable(vname.c_str()); + } + else { + varId = getVariableId(varType, vname.c_str(), createLocal); + } + if (!!varName) + *varName = vname; + } // failed to find a numeric id, whole input was invalid if (varId < 0) return false; // check for unit specification - if (unitId && !svUnit.empty()) { + if (!!unitId && !svUnit.empty()) { // try to parse the string as a numeric ID result = from_chars(svUnit.data(), svUnit.data() + svUnit.size(), *unitId); // if number conversion failed, look up unit id @@ -967,36 +1006,54 @@ void getVariable(const Client *c, const Command *const cmd) const char varType = char(cmd->uData); const char *data = cmd->sData; LOG_TRC << "getVariable(" << varType << ", " << quoted(data) << ") for client " << c->name; - if (cmd->commandId == CommandId::GetCreate && varType != 'L') { - logAndNak(c, *cmd, ostringstream() << "The GetCreate command only supports the 'L' variable type."); - return; - } - // Anything besides L/A/T type vars just gets converted to calc code. - if (!Utilities::isIndexedVariableType(varType)) { + + // Anything besides L/A/T type vars just gets converted to calc code. Also if a "string" type unit A var is requested. + size_t datalen; + bool isString = false; + if (!Utilities::isIndexedVariableType(varType) || (isString = varType == 'A' && (datalen = strlen(data)) > 6 && !strcasecmp(data + datalen-6, "string"))) { const ostringstream codeStr = ostringstream() << "(" << varType << ':' << data << ')'; - const Command execCmd(cmd->commandId, +CalcResultType::Double, codeStr.str().c_str(), 0.0, cmd->token); + CalcResultType ctype = isString ? CalcResultType::String : CalcResultType::Double; + const Command execCmd(cmd->commandId, +ctype, codeStr.str().c_str(), 0.0, cmd->token); return execCalculatorCode(c, &execCmd); } + ID varId{-1}; ENUM unitId{-1}; uint8_t varIndex{0}; string varName; - if (!parseVariableString(varType, data, varId, (cmd->commandId == CommandId::GetCreate), &unitId, &varIndex, &varName)) { + bool existed = true; + if (!parseVariableString(varType, data, varId, (cmd->commandId == CommandId::GetCreate && varType == 'L'), &unitId, &varIndex, &varName, &existed)) { logAndNak(c, *cmd, ostringstream() << "Could not resolve Variable ID for Get command from string " << quoted(data)); return; } + // !existed can only happen for an L var if we just created it. In that case it has a default value and unit type (0.0, number). + if (!existed) { + if (unitId > -1) + set_named_variable_typed_value(varId, cmd->fData, unitId); + else if (cmd->fData != 0.0) + set_named_variable_value(varId, cmd->fData); + sendResponse(c, Command(CommandId::Ack, (uint32_t)CommandId::GetCreate, nullptr, cmd->fData, cmd->token)); + LOG_DBG << "getVariable(" << quoted(data) << ") created new variable for client " << c->name; + return; + } + + if (unitId < 0 && varType == 'A') { + logAndNak(c, *cmd, ostringstream() << "Could not resolve Unit ID for Get command from string " << quoted(data)); + return; + } + calcResult_t res = calcResult_t { CalcResultType::Double, STRSZ_CMD, varId, unitId, varIndex, varName.c_str() }; if (!getNamedVariableValue(varType, res)) - return logAndNak(c, *cmd, ostringstream() << "getNamedVariableValue() returned error result for code " << quoted(data)); - Command resp(CommandId::Ack, (uint32_t)CommandId::Get); + return logAndNak(c, *cmd, ostringstream() << "getNamedVariableValue() returned error result for variable: " << quoted(data)); + Command resp(CommandId::Ack, (uint32_t)cmd->commandId); resp.token = cmd->token; switch (res.resultMemberIndex) { case 0: resp.fData = res.fVal; break; case 1: resp.fData = res.iVal; break; case 2: resp.setStringData(res.sVal.c_str()); break; default: - return logAndNak(c, *cmd, ostringstream() << "getNamedVariableValue() returned invalid result index " << res.resultMemberIndex); + return logAndNak(c, *cmd, ostringstream() << "getNamedVariableValue() for " << quoted(data) << " returned invalid result index: " << res.resultMemberIndex); } sendResponse(c, resp); } @@ -1007,10 +1064,7 @@ void setVariable(const Client *c, const Command *const cmd) const char *data = cmd->sData; const double value = cmd->fData; LOG_TRC << "setVariable(" << varType << ", " << quoted(data) << ", " << value << ") for client " << c->name; - if (cmd->commandId == CommandId::SetCreate && varType != 'L') { - logAndNak(c, *cmd, ostringstream() << "The SetCreate command only supports the 'L' variable type."); - return; - } + // Anything besides an L var just gets converted to calc code. if (varType != 'L') { const ostringstream codeStr = ostringstream() << fixed << setprecision(7) << value << " (>" << varType << ':' << data << ')'; @@ -1019,11 +1073,16 @@ void setVariable(const Client *c, const Command *const cmd) } ID varId{-1}; - if (!parseVariableString(varType, data, varId, (cmd->commandId == CommandId::SetCreate))) { + ENUM unitId{-1}; + if (!parseVariableString(varType, data, varId, (cmd->commandId == CommandId::SetCreate), &unitId)) { logAndNak(c, *cmd, ostringstream() << "Could not resolve Variable ID for Set command from string " << quoted(data)); return; } - set_named_variable_value(varId, value); + + if (unitId > -1) + set_named_variable_typed_value(varId, value, unitId); + else + set_named_variable_value(varId, value); sendAckNak(c, *cmd); } @@ -1077,17 +1136,45 @@ bool updateRequestValue(const Client *c, TrackedRequest *tr, bool compareCheck = // double case 0: // convert the value if necessary for proper binary representation - if (tr->dataSize == sizeof(float)) - data = &(f32 = (float)res.fVal); - else if (tr->valueSize == DATA_TYPE_INT64) // better way? - data = &(i64 = (int64_t)res.fVal); - else - data = &res.fVal; + switch (tr->valueSize) { + // ordered most to least likely + case DATA_TYPE_DOUBLE: + case sizeof(double): + data = &res.fVal; + break; + case DATA_TYPE_FLOAT: + case sizeof(float): + data = &(f32 = (float)res.fVal); + break; + case DATA_TYPE_INT32: + case 3: + data = &(res.iVal = (int32_t)res.fVal); + break; + case DATA_TYPE_INT8: + case 1: + data = &(res.iVal = (int8_t)res.fVal); + break; + case DATA_TYPE_INT16: + case 2: + data = &(res.iVal = (int16_t)res.fVal); + break; + case DATA_TYPE_INT64: + // the widest integer any gauge API function returns is 48b (for token/MODULE_VAR) so 53b precision is OK here + data = &(i64 = (int64_t)res.fVal); + break; + default: + data = &res.fVal; + break; + } break; // int32 - case 1: data = &res.iVal; break; + case 1: + data = &res.iVal; + break; // string - case 2: data = (void *)res.sVal.data(); break; + case 2: + data = (void *)res.sVal.data(); + break; } if (compareCheck && tr->compareCheck && !memcmp(data, tr->data.data(), tr->dataSize)) { @@ -1112,26 +1199,26 @@ bool removeRequest(Client *c, const uint32_t requestId) c->requests.erase(tr->requestId); LOG_DBG << "Deleted DataRequest ID " << requestId; sendAckNak(c, CommandId::Subscribe, true, requestId); + if (g_triggersRegistered && !c->requests.size()) + checkTriggerEventNeeded(); // check if anyone is still connected return true; } -// returns true if request has been scheduled *and* will require regular updates (period > UpdatePeriod::Once) +// returns true if request has been scheduled or removed bool addOrUpdateRequest(Client *c, const DataRequest *const req) { LOG_DBG << "Got DataRequest from Client " << c->name << ": " << *req; // request type of "None" actually means to delete an existing request - if (req->requestType == RequestType::None) { - removeRequest(c, req->requestId); - return false; // no updates needed - } + if (req->requestType == RequestType::None) + return removeRequest(c, req->requestId); // setup response Command for Ack/Nak const Command resp(CommandId::Subscribe, 0, nullptr, .0, req->requestId); // check for empty name/code if (req->nameOrCode[0] == '\0') { - logAndNak(c, resp, ostringstream() << "Error in DataRequest ID: " << req->requestId << "; Parameter 'nameOrCode' cannot be empty."); + logAndNak(c, resp, ostringstream() << "Error in DataRequest ID " << req->requestId << ": Parameter 'nameOrCode' cannot be empty."); return false; } @@ -1145,29 +1232,29 @@ bool addOrUpdateRequest(Client *c, const DataRequest *const req) const SIMCONNECT_CLIENT_DATA_DEFINITION_ID newDataId = g_nextClienDataId++; // create a new data area and add definition if (!registerClientVariableDataArea(c, req->requestId, newDataId, actualValSize, req->valueSize)) { - logAndNak(c, resp, ostringstream() << "Failed to create ClientDataDefinition, check log messages."); + logAndNak(c, resp, ostringstream() << "Error in DataRequest ID " << req->requestId << ": Failed to create ClientDataDefinition, check log messages."); return false; } - + // this may change the request from a named to a calculated type for vars/string types which don't have native gauge API access functions. tr = &c->requests.emplace(piecewise_construct, forward_as_tuple(req->requestId), forward_as_tuple(*req, newDataId)).first->second; // no try_emplace? } else { // Existing request if (actualValSize > tr->dataSize) { - logAndNak(c, resp, ostringstream() << "Value size cannot be increased after request is created."); + logAndNak(c, resp, ostringstream() << "Error in DataRequest ID " << req->requestId << ": Value size cannot be increased after request is created."); return false; } // recreate data definition if necessary if (actualValSize != tr->dataSize) { // remove definition if FAILED(SimConnectHelper::removeClientDataDefinition(g_hSimConnect, tr->dataId)) { - logAndNak(c, resp, ostringstream() << "Failed to clear ClientDataDefinition, check log messages."); + logAndNak(c, resp, ostringstream() << "Error in DataRequest ID " << req->requestId << ": Failed to clear ClientDataDefinition, check log messages."); return false; } // add definition if FAILED(SimConnectHelper::addClientDataDefinition(g_hSimConnect, tr->dataId, req->valueSize)) { - logAndNak(c, resp, ostringstream() << "Failed to create ClientDataDefinition, check log messages."); + logAndNak(c, resp, ostringstream() << "Error in DataRequest ID " << req->requestId << ": Failed to create ClientDataDefinition, check log messages."); return false; } } @@ -1182,10 +1269,10 @@ bool addOrUpdateRequest(Client *c, const DataRequest *const req) tr->variableId = getVariableId(tr->varTypePrefix, tr->nameOrCode); if (tr->variableId < 0) { if (tr->varTypePrefix == 'T') { - LOG_WRN << "Token variable named " << quoted(tr->nameOrCode) << " was not found. Will fall back to initialize_var_by_name()."; + LOG_WRN << "Warning in DataRequest ID " << req->requestId << ": Token variable named " << quoted(tr->nameOrCode) << " was not found. Will fall back to initialize_var_by_name()."; } else { - LOG_WRN << "Variable named " << quoted(tr->nameOrCode) << " was not found, disabling updates."; + LOG_ERR << "Error in DataRequest ID " << req->requestId << ": Variable named " << quoted(tr->nameOrCode) << " was not found, disabling updates."; tr->period = UpdatePeriod::Never; } } @@ -1193,13 +1280,21 @@ bool addOrUpdateRequest(Client *c, const DataRequest *const req) // look up unit ID if we don't have one already if (tr->unitId < 0 && tr->unitName[0] != '\0') { tr->unitId = get_units_enum(tr->unitName); - if (tr->unitId < 0) - LOG_WRN << "Unit named " << quoted(tr->unitName) << " was not found."; + if (tr->unitId < 0) { + if (tr->varTypePrefix == 'A') { + LOG_ERR << "Error in DataRequest ID " << req->requestId << ": Unit named " << quoted(tr->unitName) << " was not found, disabling updates."; + tr->period = UpdatePeriod::Never; + } + // maybe an L var... unit is not technically required. + else { + LOG_WRN << "Warning in DataRequest ID " << req->requestId << ": Unit named " << quoted(tr->unitName) << " was not found, no unit type will be used."; + } + } } } // calculated value, update compiled string if needed // NOTE: compiling code for format_calculator_string() doesn't seem to work as advertised in the docs, see: - // https://devsupport.flightsimulator.com/questions/9513/gauge-calculator-code-precompile-with-code-meant-f.html + // https://devsupport.flightsimulator.com/t/gauge-calculator-code-precompile-with-code-meant-for-format-calculator-string-reports-format-errors/4457 else if (tr->calcResultType != CalcResultType::Formatted && tr->calcBytecode.empty()) { // assume the command has changed and re-compile PCSTRINGZ pCompiled = nullptr; @@ -1212,20 +1307,60 @@ bool addOrUpdateRequest(Client *c, const DataRequest *const req) } else { LOG_WRN << "Calculator string compilation failed. gauge_calculator_code_precompile() returned: " << boolalpha << ok - << "; size: " << pCompiledSize << "; Result null? " << (pCompiled == nullptr) << "; Original code : " << quoted(tr->nameOrCode); + << " for request ID " << tr->requestId << ". Size: " << pCompiledSize << "; Result null ? " << (pCompiled == nullptr) << "; Original code : " << quoted(tr->nameOrCode); } } - // make sure any ms interval is >= our minimum tick time - if (tr->period == UpdatePeriod::Millisecond) - tr->interval = max((time_t)tr->interval, TICK_PERIOD_MS); sendAckNak(c, resp, true); - if (tr->period != UpdatePeriod::Never) - updateRequestValue(c, tr, false); + + if (tr->period != UpdatePeriod::Never) { + // make sure any ms interval is >= our minimum tick time + if (tr->period == UpdatePeriod::Millisecond && tr->interval < TICK_PERIOD_MS) + tr->interval = TICK_PERIOD_MS; + // If updates are not paused then send the value now; this takes care of "Once" type requests as well. + if (!c->pauseDataUpdates) + updateRequestValue(c, tr, false); + // If they're paused and this is a "Once" type request, use the interval as a flag to indicate that an update + // is pending for this request, which is then handled in setSuspendClientDataUpdates(false) below to send the value. + else if (tr->period == UpdatePeriod::Once) + tr->interval = 1; + } LOG_DBG << (isNewRequest ? "Added " : "Updated ") << *tr; - return (tr->period > UpdatePeriod::Once); + if (!g_triggersRegistered && tr->period > UpdatePeriod::Once && !c->pauseDataUpdates) + resumeTriggerEvent(); // start update loop + return true; +} + +std::string setSuspendClientDataUpdates(Client *c, bool suspend) +{ + if (c->pauseDataUpdates == suspend) + return suspend ? "Data Subscriptions already paused" : "Data Subscriptions already active"; + + if (suspend) { + c->pauseDataUpdates = true; + checkTriggerEventNeeded(); + return "Data Subscription updates suspended"; + } + + // Check for any "Once" type requests which are still pending and send them. + // While we're at it we can also check if there are any data updates which need scheduling. + bool resume = false; + for (requestMap_t::value_type &rp : c->requests) { + if (rp.second.period >= UpdatePeriod::Tick) { + resume = true; + } + else if (rp.second.period == UpdatePeriod::Once && rp.second.interval == 1) { + rp.second.interval = 0; + updateRequestValue(c, &rp.second); + } + } + c->pauseDataUpdates = false; + if (resume && !g_triggersRegistered) + resumeTriggerEvent(); + return "Data Subscription updates resumed"; } + #pragma endregion Data Requests #pragma region Registered Calculator Events ---------------------------------------------- @@ -1387,9 +1522,9 @@ void tick() g_tpNextTick = now + milliseconds(TICK_PERIOD_MS); for (clientMap_t::value_type &cp : g_mClients) { - if (cp.second.status != ClientStatus::Connected) - continue; Client &c = cp.second; + if (c.status != ClientStatus::Connected || c.pauseDataUpdates) + continue; // check for timeout if (now >= c.nextTimeout) { disconnectClient(&c, "Client connection timed out.", ClientStatus::TimedOut); @@ -1400,8 +1535,7 @@ void tick() c.nextHearbeat = now + seconds(CONN_HEARTBEAT_SEC); sendPing(&c); } - if (c.pauseDataUpdates) - continue; + // process data requests for (requestMap_t::value_type &rp : c.requests) { TrackedRequest &r = rp.second; // check if update needed @@ -1455,8 +1589,7 @@ void processCommand(Client *c, const Command *const cmd) break; case CommandId::Subscribe: - c->pauseDataUpdates = !cmd->uData; - ackMsg = c->pauseDataUpdates ? "Data Subscription updates suspended" : "Data Subscription updates resumed"; + ackMsg = setSuspendClientDataUpdates(c, !cmd->uData); break; case CommandId::Update: @@ -1473,8 +1606,7 @@ void processCommand(Client *c, const Command *const cmd) case CommandId::Disconnect: disconnectClient(c); - ackMsg = "Disconnected by Client command."; - break; + return; case CommandId::Ping: // just ACK the ping case CommandId::Connect: // client was already re-connected or we wouldn't be here, just ACK @@ -1542,8 +1674,8 @@ void CALLBACK dispatchMessage(SIMCONNECT_RECV* pData, DWORD cbData, void*) } if (c->status == ClientStatus::Connected) updateClientTimeout(c); // update heartbeat timer - else if (!connectClient(c)) // assume client wants to re-connect if they're not already - break; // unlikely + else + connectClient(c); // assume client wants to re-connect if they're not already const size_t dataSize = (size_t)pData->dwSize + 4 - sizeof(SIMCONNECT_RECV_CLIENT_DATA); // dwSize reports 4 bytes less than actual size of SIMCONNECT_RECV_CLIENT_DATA switch (dr->type) { @@ -1571,8 +1703,7 @@ void CALLBACK dispatchMessage(SIMCONNECT_RECV* pData, DWORD cbData, void*) LOG_CRT << "Invalid DataRequest struct data size! Expected " << sizeof(DataRequest) << " but got " << dataSize; return; } - if (addOrUpdateRequest(c, reinterpret_cast(&data->dwData)) && !g_triggersRegistered) - registerTriggerEvents(); // start update loop + addOrUpdateRequest(c, reinterpret_cast(&data->dwData)); break; default: @@ -1590,8 +1721,9 @@ void CALLBACK dispatchMessage(SIMCONNECT_RECV* pData, DWORD cbData, void*) SimConnectHelper::logSimVersion(pData); break; + // This doesn't seem to ever actually happen... but I guess it may just start to one day ¯\_(ツ)_/¯ case SIMCONNECT_RECV_ID_QUIT: - g_simConnectQuitEvent = true; + disableAllClients(); LOG_DBG << "Received quit command from SimConnect."; break; @@ -1658,6 +1790,13 @@ MSFS_CALLBACK void module_init(void) // register incoming Ping event and add to notification group (this is technically not "critical" to operation so do not exit on error here SimConnectHelper::newClientEvent(g_hSimConnect, CLI_EVENT_PING, string(EVENT_NAME_PING, strlen(EVENT_NAME_PING)), GROUP_DEFAULT); + // use "Frame" event as trigger for our tick() loop + if FAILED(hr = SimConnect_SubscribeToSystemEvent(g_hSimConnect, (SIMCONNECT_CLIENT_EVENT_ID)EVENT_FRAME, "Frame")) { + LOG_CRT << "SimConnect_SubscribeToSystemEvent failed with " << LOG_HR(hr);; + return; + } + pauseTriggerEvent(); // pause frame updates for now + // Go if FAILED(hr = SimConnect_CallDispatch(g_hSimConnect, dispatchMessage, nullptr)) { LOG_CRT << "SimConnect_CallDispatch failed with " << LOG_HR(hr);; @@ -1669,18 +1808,11 @@ MSFS_CALLBACK void module_init(void) MSFS_CALLBACK void module_deinit(void) { + // don't send out any more data at this point (especially logs!) as it may prevent the simulator from exiting (new in SU13, yay!) + disableAllClients(); LOG_INF << "Stopping " WSMCMND_PROJECT_NAME " " WSMCMND_SERVER_NAME; - - if (g_hSimConnect != INVALID_HANDLE_VALUE) { - // Unloading of module would typically only happen when simulator quits, except during development and manual project rebuild. - // The below code is just for that occasion. Normally any connected Clients should detect shutdown via the SimConnect Close event. - if (!g_simConnectQuitEvent) { - // Disconnect any clients (does not seem to actually send any data... SimConnect context destroyed?) - LOG_DBG << "SimConnect apparently didn't send Quit message... disconnecting any clients."; - disconnectAllClients("Server is shutting down."); - } + if (g_hSimConnect != INVALID_HANDLE_VALUE) SimConnect_Close(g_hSimConnect); - } g_hSimConnect = INVALID_HANDLE_VALUE; } diff --git a/src/WASimModule/WASimModuleProject/WASimCommander-Module/PackageDefinitions/wasimcommander-module.xml b/src/WASimModule/WASimModuleProject/WASimCommander-Module/PackageDefinitions/wasimcommander-module.xml index fda8511..3b530ce 100644 --- a/src/WASimModule/WASimModuleProject/WASimCommander-Module/PackageDefinitions/wasimcommander-module.xml +++ b/src/WASimModule/WASimModuleProject/WASimCommander-Module/PackageDefinitions/wasimcommander-module.xml @@ -1,10 +1,10 @@ - + MISC WASimCommander WASM Module - Maxim Paperno + Copyright Maxim Paperno; All rights reserved. false @@ -29,4 +29,3 @@ - diff --git a/src/WASimModule/WASimModuleProject/WASimCommander-Module/PackageDefinitions/wasimcommander-module.xml.in b/src/WASimModule/WASimModuleProject/WASimCommander-Module/PackageDefinitions/wasimcommander-module.xml.in new file mode 100644 index 0000000..1492c1a --- /dev/null +++ b/src/WASimModule/WASimModuleProject/WASimCommander-Module/PackageDefinitions/wasimcommander-module.xml.in @@ -0,0 +1,31 @@ + + + + MISC + @PROJECT_NAME@ WASM Module + + @PROJECT_COPY@ + + + false + false + + + + ContentInfo + + false + + PackageDefinitions\wasimcommander-module\ContentInfo\ + ContentInfo\wasimcommander-module\ + + + Copy + + false + + PackageSources\@SERVER_NAME@\ + modules\ + + + diff --git a/src/WASimModule/WASimModuleProject/WASimCommander-Module/PackageDefinitions/wasimcommander-module/ReleaseNotes.xml b/src/WASimModule/WASimModuleProject/WASimCommander-Module/PackageDefinitions/wasimcommander-module/ReleaseNotes.xml deleted file mode 100644 index a82e22f..0000000 --- a/src/WASimModule/WASimModuleProject/WASimCommander-Module/PackageDefinitions/wasimcommander-module/ReleaseNotes.xml +++ /dev/null @@ -1,16 +0,0 @@ -Initial release! v1.0.0.5-beta1 -v1.0.0.6-beta2 -* Fixed that Formatted type calculation -results cannot be pre-compiled. -* Event loop is paused when last client -disconnects. -WASimCommander v1.1.0.0 -* Minor updates for SU10: Updated lookup lists of Token vars -and Key events; Use new trigger_key_event_EX1(). -* Rebuilt using /O1 optimization flag. -WASimCommander v1.1.1.0 -* Updated lookup lists of Key events for SU11/SDK0.20.5.0. -* Rebuilt using /O3 optimization flag as per new -documentation recommendations and since fixes in SU11. -WASimCommander v1.1.2.0 -* Fixed KEY event alias for "AUTORUDDER_TOGGLE" -> KEY_AUTOCOORD_TOGGLE. diff --git a/src/WASimModule/key_events.h b/src/WASimModule/key_events.h index b4ad340..befaa62 100644 --- a/src/WASimModule/key_events.h +++ b/src/WASimModule/key_events.h @@ -1899,64 +1899,79 @@ namespace WASimCommander { { "CYCLIC_LONGITUDINAL_DOWN", KEY_CYCLIC_LONGITUDINAL_DOWN }, { "CYCLIC_LONGITUDINAL_UP", KEY_CYCLIC_LONGITUDINAL_UP }, + // SDK 0.21.0.0 + { "ELECT_FUEL_PUMP_SET", KEY_ELECT_FUEL_PUMP_SET }, + + // SDK 0.22.3.0 + { "3RD_PARTY_WINDOW_OPEN_PRIMARY", KEY_3RD_PARTY_WINDOW_OPEN_PRIMARY }, + { "3RD_PARTY_WINDOW_OPEN_SECONDARY", KEY_3RD_PARTY_WINDOW_OPEN_SECONDARY }, + { "3RD_PARTY_WINDOW_MOVE_DOWN", KEY_3RD_PARTY_WINDOW_MOVE_DOWN }, + { "3RD_PARTY_WINDOW_MOVE_UP", KEY_3RD_PARTY_WINDOW_MOVE_UP }, + { "3RD_PARTY_WINDOW_VALIDATE", KEY_3RD_PARTY_WINDOW_VALIDATE }, + { "HELI_BEEP_SET", KEY_HELI_BEEP_SET }, + // Aliases for published Event IDs which do not match KEY IDs - { "ADF1_WHOLE_DEC", KEY_ADF_WHOLE_DEC }, - { "ADF1_WHOLE_INC", KEY_ADF_WHOLE_INC }, - { "ALTITUDE_SLOT_INDEX_SET", KEY_AP_ALTITUDE_SLOT_INDEX_SET }, - { "ANTIDETONATION_TANK_VALVE_TOGGLE", KEY_TOGGLE_ANTIDETONATION_TANK_VALVE }, - { "AP_HEADING_BUG_SET_EX1", KEY_HEADING_BUG_SET_EX1 }, - { "AP_PANEL_MACH_HOLD_TOGGLE", KEY_AUTOPILOT_MACH_HOLD_CURRENT }, - { "AP_PANEL_SPEED_HOLD_TOGGLE", KEY_AUTOPILOT_AIRSPEED_HOLD_CURRENT }, - { "ATTITUDE_BARS_POSITION_DOWN", KEY_ATTITUDE_BARS_POSITION_DEC }, - { "ATTITUDE_BARS_POSITION_UP", KEY_ATTITUDE_BARS_POSITION_INC }, - { "ATTITUDE_CAGE_BUTTON", KEY_TOGGLE_ATTITUDE_CAGE }, - { "AUTORUDDER_TOGGLE", KEY_AUTOCOORD_TOGGLE }, - { "BACK_TO_FLY", KEY_NULL }, // Not a real Event ?? - { "COM_STBY_RADIO_SWAP", KEY_COM_STBY_RADIO_SWITCH_TO }, - { "DECREASE_AUTOBRAKE_CONTROL", KEY_DEC_AUTOBRAKE_CONTROL }, - { "DECREASE_DECISION_ALTITUDE_MSL", KEY_DECISION_ALTITUDE_MSL_DEC }, - { "DECREASE_DECISION_HEIGHT", KEY_DECISION_HEIGHT_DEC }, - { "INCREASE_DECISION_ALTITUDE_MSL", KEY_DECISION_ALTITUDE_MSL_INC }, - { "INCREASE_DECISION_HEIGHT", KEY_DECISION_HEIGHT_INC }, - { "SET_DECISION_ALTITUDE_MSL", KEY_DECISION_ALTITUDE_MSL_SET }, - { "FLIGHT_LEVEL_CHANGE", KEY_AP_FLIGHT_LEVEL_CHANGE }, - { "FLIGHT_LEVEL_CHANGE_OFF", KEY_AP_FLIGHT_LEVEL_CHANGE_OFF }, - { "FLIGHT_LEVEL_CHANGE_ON", KEY_AP_FLIGHT_LEVEL_CHANGE_ON }, - { "HEADING_SLOT_INDEX_SET", KEY_AP_HEADING_SLOT_INDEX_SET }, - { "INCREASE_AUTOBRAKE_CONTROL", KEY_INC_AUTOBRAKE_CONTROL }, - { "KNEEBOARD_VIEW", KEY_KNEEBOARD }, - { "MP_ACTIVATE_CHAT", KEY_MULTIPLAYER_ACTIVATE_CHAT }, - { "MP_BROADCAST_VOICE_CAPTURE_START", KEY_MULTIPLAYER_BROADCAST_VOICE_CAPTURE_START }, - { "MP_BROADCAST_VOICE_CAPTURE_STOP", KEY_MULTIPLAYER_BROADCAST_VOICE_CAPTURE_STOP }, - { "MP_CHAT", KEY_MULTIPLAYER_CHAT }, - { "MP_PAUSE_SESSION", KEY_MULTIPLAYER_PAUSE_SESSION }, - { "MP_PLAYER_CYCLE", KEY_MULTIPLAYER_PLAYER_CYCLE }, - { "MP_PLAYER_FOLLOW", KEY_MULTIPLAYER_PLAYER_FOLLOW }, - { "MP_TRANSFER_CONTROL", KEY_MULTIPLAYER_TRANSFER_CONTROL }, - { "MP_VOICE_CAPTURE_START", KEY_MULTIPLAYER_VOICE_CAPTURE_START }, - { "MP_VOICE_CAPTURE_STOP", KEY_MULTIPLAYER_VOICE_CAPTURE_STOP }, - { "NITROUS_TANK_VALVE_TOGGLE", KEY_TOGGLE_NITROUS_TANK_VALVE } , + { "ADF1_WHOLE_DEC", KEY_ADF_WHOLE_DEC }, + { "ADF1_WHOLE_INC", KEY_ADF_WHOLE_INC }, + { "ALTITUDE_SLOT_INDEX_SET", KEY_AP_ALTITUDE_SLOT_INDEX_SET }, + { "ANTIDETONATION_TANK_VALVE_TOGGLE", KEY_TOGGLE_ANTIDETONATION_TANK_VALVE }, + { "AP_HEADING_BUG_SET_EX1", KEY_HEADING_BUG_SET_EX1 }, + { "AP_PANEL_MACH_HOLD_TOGGLE", KEY_AUTOPILOT_MACH_HOLD_CURRENT }, + { "AP_PANEL_SPEED_HOLD_TOGGLE", KEY_AUTOPILOT_AIRSPEED_HOLD_CURRENT }, + { "ATTITUDE_BARS_POSITION_DOWN", KEY_ATTITUDE_BARS_POSITION_DEC }, + { "ATTITUDE_BARS_POSITION_UP", KEY_ATTITUDE_BARS_POSITION_INC }, + { "ATTITUDE_CAGE_BUTTON", KEY_TOGGLE_ATTITUDE_CAGE }, + { "AUTORUDDER_TOGGLE", KEY_AUTOCOORD_TOGGLE }, + { "BACK_TO_FLY", KEY_NULL }, // Not a real Event ?? + { "COM_STBY_RADIO_SWAP", KEY_COM_STBY_RADIO_SWITCH_TO }, + { "DECREASE_AUTOBRAKE_CONTROL", KEY_DEC_AUTOBRAKE_CONTROL }, + { "DECREASE_DECISION_ALTITUDE_MSL", KEY_DECISION_ALTITUDE_MSL_DEC }, + { "DECREASE_DECISION_HEIGHT", KEY_DECISION_HEIGHT_DEC }, + { "DECREASE_HELO_GOV_BEEP", KEY_HELI_BEEP_DECREASE }, + { "FLIGHT_LEVEL_CHANGE", KEY_AP_FLIGHT_LEVEL_CHANGE }, + { "FLIGHT_LEVEL_CHANGE_OFF", KEY_AP_FLIGHT_LEVEL_CHANGE_OFF }, + { "FLIGHT_LEVEL_CHANGE_ON", KEY_AP_FLIGHT_LEVEL_CHANGE_ON }, + { "HEADING_SLOT_INDEX_SET", KEY_AP_HEADING_SLOT_INDEX_SET }, + { "INCREASE_AUTOBRAKE_CONTROL", KEY_INC_AUTOBRAKE_CONTROL }, + { "INCREASE_DECISION_ALTITUDE_MSL", KEY_DECISION_ALTITUDE_MSL_INC }, + { "INCREASE_DECISION_HEIGHT", KEY_DECISION_HEIGHT_INC }, + { "INCREASE_HELO_GOV_BEEP", KEY_HELI_BEEP_INCREASE }, + { "KNEEBOARD_VIEW", KEY_KNEEBOARD }, + { "MP_ACTIVATE_CHAT", KEY_MULTIPLAYER_ACTIVATE_CHAT }, + { "MP_BROADCAST_VOICE_CAPTURE_START", KEY_MULTIPLAYER_BROADCAST_VOICE_CAPTURE_START }, + { "MP_BROADCAST_VOICE_CAPTURE_STOP", KEY_MULTIPLAYER_BROADCAST_VOICE_CAPTURE_STOP }, + { "MP_CHAT", KEY_MULTIPLAYER_CHAT }, + { "MP_PAUSE_SESSION", KEY_MULTIPLAYER_PAUSE_SESSION }, + { "MP_PLAYER_CYCLE", KEY_MULTIPLAYER_PLAYER_CYCLE }, + { "MP_PLAYER_FOLLOW", KEY_MULTIPLAYER_PLAYER_FOLLOW }, + { "MP_TRANSFER_CONTROL", KEY_MULTIPLAYER_TRANSFER_CONTROL }, + { "MP_VOICE_CAPTURE_START", KEY_MULTIPLAYER_VOICE_CAPTURE_START }, + { "MP_VOICE_CAPTURE_STOP", KEY_MULTIPLAYER_VOICE_CAPTURE_STOP }, + { "NITROUS_TANK_VALVE_TOGGLE", KEY_TOGGLE_NITROUS_TANK_VALVE } , { "PRESSURIZATION_PRESSURE_DUMP_SWTICH", KEY_PRESSURIZATION_PRESSURE_DUMP_SWITCH }, - { "RELOAD_USER_AIRCRAFT", KEY_CONTROL_RELOAD_USER_AIRCRAFT }, - { "REQUEST_FUEL_KEY", KEY_REQUEST_FUEL }, - { "ROTOR_BRAKE_SET", KEY_AXIS_ROTOR_BRAKE_SET }, - { "RPM_SLOT_INDEX_SET", KEY_AP_RPM_SLOT_INDEX_SET }, - { "SET_REVERSE_THRUST_OFF", KEY_SET_THROTTLE_REVERSE_THRUST_OFF }, - { "SET_REVERSE_THRUST_ON", KEY_SET_THROTTLE_REVERSE_THRUST_ON }, - { "SPEED_SLOT_INDEX_SET", KEY_AP_SPEED_SLOT_INDEX_SET }, - { "TOGGLE_AUTOFEATHER_ARM", KEY_TOGGLE_ARM_AUTOFEATHER }, - { "TOGGLE_DME", KEY_DME_TOGGLE } , - { "TOGGLE_PROPELLER_SYNC", KEY_TOGGLE_PROP_SYNC }, - { "TOGGLE_PUSHBACK", KEY_PUSHBACK_SET }, // ? - { "TOW_PLANE_REQUEST", KEY_REQUEST_TOW_PLANE }, - { "TRUE_AIRSPEED_CAL_DEC", KEY_TRUE_AIRSPEED_CALIBRATE_DEC }, - { "TRUE_AIRSPEED_CAL_INC", KEY_TRUE_AIRSPEED_CALIBRATE_INC }, - { "VARIOMETER_SOUND_TOGGLE", KEY_TOGGLE_VARIOMETER_SWITCH }, // ? - { "VERTICAL_SPEED_SET", KEY_AP_VS_SET }, - { "VIEW_AXIS_INDICATOR_CYCLE", KEY_AXIS_INDICATOR_CYCLE }, - { "VIEW_CAMERA_SELECT_START", KEY_VIEW_CAMERA_SELECT_STARTING }, - { "VIEW_WINDOW_TITLES_TOGGLE", KEY_WINDOW_TITLES_TOGGLE }, - { "VS_SLOT_INDEX_SET", KEY_AP_VS_SLOT_INDEX_SET }, + { "RELOAD_USER_AIRCRAFT", KEY_CONTROL_RELOAD_USER_AIRCRAFT }, + { "REQUEST_FUEL_KEY", KEY_REQUEST_FUEL }, + { "ROTOR_AXIS_TAIL_ROTOR_SET", KEY_AXIS_TAIL_ROTOR_SET }, + { "ROTOR_BRAKE_SET", KEY_AXIS_ROTOR_BRAKE_SET }, + { "RPM_SLOT_INDEX_SET", KEY_AP_RPM_SLOT_INDEX_SET }, + { "SET_DECISION_ALTITUDE_MSL", KEY_DECISION_ALTITUDE_MSL_SET }, + { "SET_HELO_GOV_BEEP", KEY_HELI_BEEP_SET }, + { "SET_REVERSE_THRUST_OFF", KEY_SET_THROTTLE_REVERSE_THRUST_OFF }, + { "SET_REVERSE_THRUST_ON", KEY_SET_THROTTLE_REVERSE_THRUST_ON }, + { "SPEED_SLOT_INDEX_SET", KEY_AP_SPEED_SLOT_INDEX_SET }, + { "TOGGLE_AUTOFEATHER_ARM", KEY_TOGGLE_ARM_AUTOFEATHER }, + { "TOGGLE_DME", KEY_DME_TOGGLE } , + { "TOGGLE_PROPELLER_SYNC", KEY_TOGGLE_PROP_SYNC }, + { "TOGGLE_PUSHBACK", KEY_PUSHBACK_SET }, // ? + { "TOW_PLANE_REQUEST", KEY_REQUEST_TOW_PLANE }, + { "TRUE_AIRSPEED_CAL_DEC", KEY_TRUE_AIRSPEED_CALIBRATE_DEC }, + { "TRUE_AIRSPEED_CAL_INC", KEY_TRUE_AIRSPEED_CALIBRATE_INC }, + { "VARIOMETER_SOUND_TOGGLE", KEY_TOGGLE_VARIOMETER_SWITCH }, // ? + { "VERTICAL_SPEED_SET", KEY_AP_VS_SET }, + { "VIEW_AXIS_INDICATOR_CYCLE", KEY_AXIS_INDICATOR_CYCLE }, + { "VIEW_CAMERA_SELECT_START", KEY_VIEW_CAMERA_SELECT_STARTING }, + { "VIEW_WINDOW_TITLES_TOGGLE", KEY_WINDOW_TITLES_TOGGLE }, + { "VS_SLOT_INDEX_SET", KEY_AP_VS_SLOT_INDEX_SET }, }; diff --git a/src/WASimUI/DocImports.h b/src/WASimUI/DocImports.h new file mode 100644 index 0000000..6f1a094 --- /dev/null +++ b/src/WASimUI/DocImports.h @@ -0,0 +1,219 @@ +/* +This file is part of the WASimCommander project. +https://github.com/mpaperno/WASimCommander + +COPYRIGHT: (c) Maxim Paperno; All Rights Reserved. + +This file may be used under the terms of the GNU General Public License (GPL) +as published by the Free Software Foundation, either version 3 of the Licenses, +or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +A copy of the GNU GPL is included with this project +and is also available at . +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Widgets.h" + +namespace WASimUiNS { + namespace DocImports +{ +Q_NAMESPACE + +const QString DB_NAME = QStringLiteral("MSFS_SDK_Doc_Import"); + +enum RecordType : quint8 { Unknown, SimVars, KeyEvents, SimVarUnits }; +Q_ENUM_NS(RecordType) + +static const QVector RecordTypeNames { + "Unknown", "Simulator Variables", "Key Events", "Variable Units" +}; +static QString recordTypeName(RecordType type) { + return RecordTypeNames.value(type, RecordTypeNames[RecordType::Unknown]); +} + +static bool createConnection() +{ + QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE", DB_NAME); + db.setDatabaseName(DB_NAME + ".sqlite3"); + db.setConnectOptions("QSQLITE_OPEN_READONLY;QSQLITE_ENABLE_SHARED_CACHE;QSQLITE_ENABLE_REGEXP=30"); + if (db.open()) + return true; + qDebug() << "Failed to open database" << DB_NAME << ".sqlite3 with:" << db.lastError(); + db = QSqlDatabase(); + QSqlDatabase::removeDatabase(DB_NAME); + return false; +} + +static QSqlDatabase getConnection(bool open = true) +{ + if (!QSqlDatabase::contains(DB_NAME)) { + if (!createConnection()) + return QSqlDatabase::database(); + } + return QSqlDatabase::database(DB_NAME, open); +} + + +// ---------------------------------------- +// DocImportsModel +// ---------------------------------------- + +class DocImportsModel : public QSqlTableModel +{ + Q_OBJECT + +public: + DocImportsModel(QObject *parent = nullptr, RecordType type = RecordType::Unknown) + : QSqlTableModel(parent, getConnection()) + { + setEditStrategy(QSqlTableModel::OnManualSubmit); + //qDebug() << "Opened DB" << database().connectionName() << database().databaseName(); + if (type != RecordType::Unknown) + setRecordType(type); + } + + RecordType recordType() const { return m_recordType; } + + QDateTime lastDataUpdate(RecordType type, QString *fromUrl = nullptr) + { + QDateTime lu; + if (type == RecordType::Unknown) + return lu; + + const QString table(QMetaEnum::fromType().valueToKey(type)); + QSqlQuery qry("SELECT LastUpdate, FromURL FROM ImportMeta WHERE TableName = ? ", database()); + qry.addBindValue(table); + qry.exec(); + if (qry.next()) { + lu = qry.value(0).toDateTime(); + lu.setTimeZone(QTimeZone::utc()); + lu = lu.toLocalTime(); + if (fromUrl) + *fromUrl = qry.value(1).toString(); + } + else if (qry.lastError().isValid()) { + qDebug() << "ImportMeta query for table" << table << "failed with:" << qry.lastError(); + } + else { + qDebug() << "ImportMeta query for table" << table << "returned no results."; + } + qry.finish(); + return lu; + } + + void setRecordType(RecordType type) + { + m_recordType = type; + QString table; + switch (type) { + case RecordType::SimVars: + case RecordType::KeyEvents: + case RecordType::SimVarUnits: + table = QString(QMetaEnum::fromType().valueToKey(type)); + break; + + default: + return; + } + + setTable(table); + select(); + + //setQuery("SELECT * FROM " + table, m_db); + if (lastError().isValid()) { + qDebug() << "Query for table" << table << "failed with: " << lastError(); + return; + } + + int col = fieldIndex("Multiplayer"); + if (col > -1) + removeColumn(col); + } + + Qt::ItemFlags flags(const QModelIndex &idx) const override { + if (idx.isValid()) + return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemNeverHasChildren; + return Qt::NoItemFlags; + } + + QVariant data(const QModelIndex &idx, int role) const override { + if (role == Qt::ToolTipRole) + role = Qt::DisplayRole; + return QSqlTableModel::data(idx, role); + } + + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override { + if (role == Qt::EditRole || role == Qt::ToolTipRole) + return QSqlTableModel::headerData(section, orientation, Qt::DisplayRole); + return QSqlTableModel::headerData(section, orientation, role); + } + +private: + RecordType m_recordType = RecordType::Unknown; + +}; + + +// ---------------------------------------- +// NameCompleter +// ---------------------------------------- + +class NameCompleter : public QCompleter +{ + Q_OBJECT + +public: + NameCompleter(RecordType recordType, QObject *parent = nullptr) : + QCompleter(parent) + { + DocImportsModel *m = new DocImportsModel(this, recordType); + const int modelColumn = m->fieldIndex("Name"); + if (recordType != RecordType::SimVarUnits) + m->setFilter(QStringLiteral("Deprecated = 0")); + m->setSort(modelColumn, Qt::AscendingOrder); + + setModel(m); + setCompletionColumn(modelColumn); + setModelSorting(QCompleter::CaseSensitivelySortedModel); + setCompletionMode(QCompleter::PopupCompletion); + setCaseSensitivity(Qt::CaseInsensitive); + setFilterMode(Qt::MatchContains); + setMaxVisibleItems(12); + } + + DocImportsModel *model() const { return (DocImportsModel *)QCompleter::model(); } + +}; + + +// ---------------------------------------- +// RecordTypeComboBox +// ---------------------------------------- + +class RecordTypeComboBox : public EnumsComboBox +{ +public: + RecordTypeComboBox(QWidget *p = nullptr) : + EnumsComboBox(DocImports::RecordTypeNames, DocImports::RecordType::SimVars, p) { + setToolTip(tr("Select a documentation record type to browse.")); + } +}; + + } // DocImports +} // WASimUiNS diff --git a/src/WASimUI/DocImportsBrowser.h b/src/WASimUI/DocImportsBrowser.h new file mode 100644 index 0000000..de30a1d --- /dev/null +++ b/src/WASimUI/DocImportsBrowser.h @@ -0,0 +1,242 @@ +/* +This file is part of the WASimCommander project. +https://github.com/mpaperno/WASimCommander + +COPYRIGHT: (c) Maxim Paperno; All Rights Reserved. + +This file may be used under the terms of the GNU General Public License (GPL) +as published by the Free Software Foundation, either version 3 of the Licenses, +or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +A copy of the GNU GPL is included with this project +and is also available at . +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "ui_DocImportsBrowser.h" +#include "DocImports.h" +#include "FilterTableHeader.h" + +namespace WASimUiNS { + namespace DocImports +{ + +class DocImportsBrowser : public QWidget +{ + Q_OBJECT + +public: + enum ViewMode { FullViewMode, PopupViewMode }; + Q_ENUM(ViewMode) + + DocImportsBrowser(QWidget *parent = nullptr, RecordType type = RecordType::Unknown, ViewMode viewMode = ViewMode::FullViewMode) + : QWidget(parent), + m_model(new DocImportsModel(this)) + { + setObjectName(QStringLiteral("DocImportsBrowser")); + ui.setupUi(this); + + setWindowTitle(tr("SimConnect SDK Reference Browser")); + setContextMenuPolicy(Qt::ActionsContextMenu); + + ui.cbRecordType->setCurrentIndex(-1); + connect(ui.cbRecordType, &DataComboBox::currentDataChanged, this, &DocImportsBrowser::setRecordTypeVar); + + ui.tableView->setWordWrap(false); + ui.tableView->setEditTriggers(QTableView::NoEditTriggers); + ui.tableView->horizontalHeader()->setDefaultSectionSize(175); + ui.tableView->verticalHeader()->setSectionResizeMode(QHeaderView::Fixed); + ui.tableView->setFiltersVisible(true); + + ui.tableView->setModel(m_model); + + //addAction(ui.tableView->actionsMenu(this)->menuAction()); + addAction(ui.tableView->columnToggleMenuAction()); + addAction(ui.tableView->fontSizeMenuAction()); + addAction(ui.tableView->filterToggleAction()); + + QAction *closeAct = new QAction(QIcon("close.glyph"), tr("Close"), this); + closeAct->setShortcuts({ QKeySequence(Qt::Key_Escape), QKeySequence::Close }); + connect(closeAct, &QAction::triggered, this, &DocImportsBrowser::close); + addAction(closeAct); + + ui.textBrowser->document()->setIndentWidth(2.0); + ui.textBrowser->document()->setDefaultStyleSheet(QStringLiteral( + "dd { margin-bottom: 6px; }" + )); + + if (viewMode == ViewMode::FullViewMode) + connect(ui.tableView->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &DocImportsBrowser::showRecordDetails); + else + connect(ui.tableView->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &DocImportsBrowser::onCurrentRowChanged); + + connect(ui.tableView, &QTableView::doubleClicked, this, [=](const QModelIndex &idx) { + emit itemSelected(ui.tableView->proxyModel()->mapToSource(idx)); + }); + + setRecordType(type); + setViewMode(viewMode); + setInitialSize(); + loadSettings(); + } + + DocImportsModel *model() const { return m_model; } + +public Q_SLOTS: + void setViewMode(ViewMode mode) + { + m_viewMode = mode; + if (mode == ViewMode::FullViewMode) { + ui.cbRecordType->setVisible(true); + ui.toolbarLabel->setText(tr("Select Record Type:")); + ui.lblTitle->setText( + tr("Data is imported from SimConnect SDK web page documentation. Use the filters in each column to search.") + ); + } + else { + ui.cbRecordType->setVisible(false); + ui.toolbarLabel->setText("" + DocImports::recordTypeName(m_model->recordType()) + ""); + ui.lblTitle->setText( + tr("Double-click to select a record and return to the main window. Press Escape key to close. Use the filters to search.") + ); + } + } + + void setRecordType(RecordType type) + { + if (type == RecordType::Unknown || type == m_recordType) + return; + saveTypeSettings(); + m_recordType = type; + m_model->setRecordType(type); + if (ui.textBrowser->property("currentRow").isValid()) + ui.textBrowser->clear(); + loadTypeSettings(); + ui.lblLastUpdate->setText(tr("Data updated on:   %1").arg(m_model->lastDataUpdate(type).toString("d MMMM yyyy"))); + + if (!qobject_cast(sender())) + ui.cbRecordType->setCurrentData((int)type); + } + + void setRecordTypeVar(const QVariant &type) { setRecordType((RecordType)type.toUInt()); } + + void showRecordDetails(const QModelIndex &key) + { + if (m_model->recordType() == RecordType::Unknown || !key.isValid()) + return; + + const QAbstractItemModel *model = ui.tableView->model(); + QString sb(QStringLiteral("
")); + for (int i = 0; i < model->columnCount(); ++i) { + const QVariant &d = model->data(model->index(key.row(), i), Qt::DisplayRole); + if (d.isValid()) + sb.append(QStringLiteral("
%1
%2
").arg(model->headerData(i, Qt::Horizontal).toString(), toHtml(d.toString()))); + } + sb.append(QStringLiteral("
")); + ui.textBrowser->setHtml(sb); + ui.textBrowser->setProperty("currentRow", key.row()); + } + + void saveSettings() const + { + QSettings s; + s.beginGroup(objectName()); + s.beginGroup(QString(QMetaEnum::fromType().valueToKey(m_viewMode))); + s.setValue(QStringLiteral("windowGeo"), saveGeometry()); + s.setValue(QStringLiteral("splitterState"), ui.splitter->saveState()); + s.endGroup(); + s.endGroup(); + } + + void loadSettings() + { + QSettings s; + s.beginGroup(objectName()); + s.beginGroup(QString(QMetaEnum::fromType().valueToKey(m_viewMode))); + restoreGeometry(s.value(QStringLiteral("windowGeo")).toByteArray()); + ui.splitter->restoreState(s.value(QStringLiteral("splitterState")).toByteArray()); + s.endGroup(); + s.endGroup(); + } + + void saveTypeSettings() const + { + if (m_model->recordType() == RecordType::Unknown) + return; + QSettings s; + s.beginGroup(objectName()); + s.beginGroup(QString(QMetaEnum::fromType().valueToKey(m_recordType))); + s.setValue(QStringLiteral("tableViewState"), ui.tableView->saveState()); + s.endGroup(); + s.endGroup(); + } + + void loadTypeSettings() + { + if (m_model->recordType() == RecordType::Unknown) + return; + QSettings s; + s.beginGroup(objectName()); + s.beginGroup(QString(QMetaEnum::fromType().valueToKey(m_recordType))); + ui.tableView->restoreState(s.value(QStringLiteral("tableViewState")).toByteArray()); + s.endGroup(); + s.endGroup(); + } + +Q_SIGNALS: + void itemSelected(const QModelIndex &row); + +protected: + void closeEvent(QCloseEvent *ev) override { + saveSettings(); + saveTypeSettings(); + ev->accept(); + } + + void showEvent(QShowEvent *ev) override { + QWidget::showEvent(ev); + if (m_viewMode == ViewMode::PopupViewMode) + ui.tableView->setFilterFocus(2); + } + +private Q_SLOTS: + void onCurrentRowChanged(const QModelIndex &sel, const QModelIndex &prev) { + if (prev.isValid()) + showRecordDetails(sel); + } + + void setInitialSize() { + if (m_viewMode == ViewMode::PopupViewMode) + resize(width(), 300); + } + +private: + QString toHtml(const QVariant &txt) + { + QString ret(txt.toString().toHtmlEscaped()); + ret.replace('\n', "
"); + return ret; + } + + Ui::DocImportsBrowserClass ui; + DocImportsModel *m_model; + RecordType m_recordType = RecordType::Unknown; + ViewMode m_viewMode = ViewMode::FullViewMode; +}; + + } // DocImports +} // WASimUiNS diff --git a/src/WASimUI/DocImportsBrowser.ui b/src/WASimUI/DocImportsBrowser.ui new file mode 100644 index 0000000..a752754 --- /dev/null +++ b/src/WASimUI/DocImportsBrowser.ui @@ -0,0 +1,132 @@ + + + DocImportsBrowserClass + + + + 0 + 0 + 1170 + 660 + + + + DocImportsBrowser + + + + + + + 0 + + + 0 + + + + + Select Record Type: + + + cbRecordType + + + + + + + + + + + 0 + 0 + + + + + 8 + + + + Data is imported from SimConnect SDK web page documentation. Use the filters in each column to search (hover or r-click on them for details). + + + Qt::AlignCenter + + + + + + + + + + + + + + + + + + 0 + 0 + + + + Qt::Horizontal + + + 6 + + + + + QAbstractScrollArea::AdjustToContents + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;"> +<p align="center" style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:9pt;">Select a record from the table to view details here.</span></p></body></html> + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + true + + + + + + + + + + BuddyLabel + QLabel +
BuddyLabel.h
+
+ + WASimUiNS::CustomTableView + QTableView +
CustomTableView.h
+
+ + WASimUiNS::DocImports::RecordTypeComboBox + QComboBox +
DocImports.h
+
+
+ + cbRecordType + tableView + textBrowser + + + +
diff --git a/src/WASimUI/EventsModel.h b/src/WASimUI/EventsModel.h index d16a332..e0c1f43 100644 --- a/src/WASimUI/EventsModel.h +++ b/src/WASimUI/EventsModel.h @@ -103,7 +103,6 @@ class EventsModel : public QStandardItemModel EventRecord req(-1); if (row >= rowCount()) return req; - QStandardItem *idItem = item(row, COL_ID); req.eventId = item(row, COL_ID)->data(DataRole).toUInt(); req.code = item(row, COL_CODE)->text().toStdString(); req.name = item(row, COL_NAME)->text().toStdString(); @@ -116,12 +115,9 @@ class EventsModel : public QStandardItemModel if (row < 0) row = rowCount(); - setItem(row, COL_ID, new QStandardItem(QString("%1").arg(req.eventId))); - item(row, COL_ID)->setData(req.eventId, DataRole); - - setItem(row, COL_CODE, new QStandardItem(QString::fromStdString(req.code))); - setItem(row, COL_NAME, new QStandardItem(QString::fromStdString(req.name))); - + setOrCreateItem(row, COL_ID, QString::number(req.eventId), req.eventId); + setOrCreateItem(row, COL_CODE, QString::fromStdString(req.code)); + setOrCreateItem(row, COL_NAME, QString::fromStdString(req.name)); return index(row, 0); } @@ -182,21 +178,24 @@ class EventsModel : public QStandardItemModel return ret; } - static inline QModelIndexList flattenIndexList(const QModelIndexList &list) +signals: + void rowCountChanged(int rows); + +protected: + QStandardItem *setOrCreateItem(int row, int col, const QString &text, const QVariant &data = QVariant()) { - QModelIndexList ret; - QModelIndex lastIdx; - for (const QModelIndex &idx : list) { - if (idx.column() == COL_ID && lastIdx.row() != idx.row() && idx.row() < idx.model()->rowCount()) - ret.append(idx); - lastIdx = idx; + QStandardItem *itm = item(row, col); + if (!itm){ + setItem(row, col, new QStandardItem()); + itm = item(row, col); } - return ret; + itm->setText(text); + itm->setToolTip(text); + if (data.isValid()) + itm->setData(data, DataRole); + return itm; } -signals: - void rowCountChanged(int rows); - private: uint32_t m_nextEventId = 0; diff --git a/src/WASimUI/LogConsole.cpp b/src/WASimUI/LogConsole.cpp new file mode 100644 index 0000000..24a458e --- /dev/null +++ b/src/WASimUI/LogConsole.cpp @@ -0,0 +1,179 @@ +/* +This file is part of the WASimCommander project. +https://github.com/mpaperno/WASimCommander + +COPYRIGHT: (c) Maxim Paperno; All Rights Reserved. + +This file may be used under the terms of the GNU General Public License (GPL) +as published by the Free Software Foundation, either version 3 of the Licenses, +or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +A copy of the GNU GPL is included with this project +and is also available at . +*/ + +#include "LogConsole.h" + +#include "Utils.h" +#include "Widgets.h" + +#include "client/WASimClient.h" + +using namespace WASimUiNS; +using namespace WASimCommander; +using namespace WASimCommander::Client; +using namespace WASimCommander::Enums; + +LogConsole::LogConsole(QWidget *parent) + : QWidget(parent), + logModel{new LogRecordsModel(this)} +{ + setObjectName(QStringLiteral("LogConsole")); + + ui.setupUi(this); + + ui.logView->setModel(logModel); + ui.logView->sortByColumn(LogRecordsModel::COL_TS, Qt::AscendingOrder); + //ui.logView->horizontalHeader()->setSortIndicator(LogRecordsModel::COL_TS, Qt::AscendingOrder); + ui.logView->horizontalHeader()->setSortIndicatorShown(false); + ui.logView->horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive); + ui.logView->horizontalHeader()->setSectionsMovable(true); + ui.logView->horizontalHeader()->resizeSection(LogRecordsModel::COL_LEVEL, 70); + ui.logView->horizontalHeader()->resizeSection(LogRecordsModel::COL_TS, 110); + ui.logView->horizontalHeader()->resizeSection(LogRecordsModel::COL_SOURCE, 20); + ui.logView->horizontalHeader()->setToolTip(tr("Severity Level | Timestamp | Source | Message")); + + // Set and connect Log Level combo boxes for Client and Server logging levels + ui.cbLogLevelCallback->setProperties( (LogLevel)-1, LogFacility::Remote, LogSource::Client); + ui.cbLogLevelFile->setProperties( (LogLevel)-1, LogFacility::File, LogSource::Client); + ui.cbLogLevelConsole->setProperties( (LogLevel)-1, LogFacility::Console, LogSource::Client); + ui.cbLogLevelServer->setProperties( (LogLevel)-1, LogFacility::Remote, LogSource::Server); + ui.cbLogLevelServerFile->setProperties( (LogLevel)-1, LogFacility::File, LogSource::Server); // unknown level at startup + ui.cbLogLevelServerConsole->setProperties( (LogLevel)-1, LogFacility::Console, LogSource::Server); // unknown level at startup + // Since the LogLevelComboBox types store the facility and source properties (which we just set), we can use one event handler for all of them. + auto setLogLevel = [=](LogLevel level) { + if (LogLevelComboBox *cb = qobject_cast(sender())) + if (!!wsClient) + wsClient->setLogLevel(level, cb->facility(), cb->source()); + }; + connect(ui.cbLogLevelCallback, &LogLevelComboBox::levelChanged, this, setLogLevel); + connect(ui.cbLogLevelFile, &LogLevelComboBox::levelChanged, this, setLogLevel); + connect(ui.cbLogLevelConsole, &LogLevelComboBox::levelChanged, this, setLogLevel); + connect(ui.cbLogLevelServer, &LogLevelComboBox::levelChanged, this, setLogLevel); + connect(ui.cbLogLevelServerFile, &LogLevelComboBox::levelChanged, this, setLogLevel); + connect(ui.cbLogLevelServerConsole, &LogLevelComboBox::levelChanged, this, setLogLevel); + +#define FILTER_ACTION(LVL, NAME, BTN) { \ + QAction *act = new QAction(QIcon(Utils::iconNameForLogLevel(LogLevel::LVL)), tr("Toggle %1 Messages").arg(NAME), this); \ + act->setToolTip(tr("Toggle visibility of %1-level log messages.").arg(NAME)); \ + act->setCheckable(true); \ + act->setChecked(true); \ + ui.btnLogFilt_##BTN->setDefaultAction(act); \ + addAction(act); \ + connect(act, &QAction::triggered, this, [this](bool en) { logModel->setLevelFilter(LogLevel::LVL, !en); }); \ + } + FILTER_ACTION(Error, tr("Error"), ERR); + FILTER_ACTION(Warning, tr("Warning"), WRN); + FILTER_ACTION(Info, tr("Info"), INF); + FILTER_ACTION(Debug, tr("Debug"), DBG); + FILTER_ACTION(Trace, tr("Trace"), TRC); +#undef FILTER_ACTION + +#define FILTER_ACTION(SRC, NAME, BTN) { \ + QAction *act = new QAction(QIcon(Utils::iconNameForLogSource(+##SRC)), tr("Toggle %1 Records").arg(NAME), this); \ + act->setToolTip(tr("Toggle visibility of log messages from %1.").arg(NAME)); \ + act->setCheckable(true); \ + act->setChecked(true); \ + ui.btnLogFilt_##BTN->setDefaultAction(act); \ + addAction(act); \ + connect(act, &QAction::triggered, this, [this](bool en) { logModel->setSourceFilter(+##SRC, !en); }); \ + } + FILTER_ACTION(LogSource::Server, tr("Server"), Server); + FILTER_ACTION(LogSource::Client, tr("Client"), Client); + FILTER_ACTION(LogRecordsModel::LogSource::UI, tr("UI"), UI); +#undef FILTER_ACTION + + QIcon logPauseIcon(QStringLiteral("pause.glyph")); + logPauseIcon.addFile(QStringLiteral("play_arrow.glyph"), QSize(), QIcon::Normal, QIcon::On); + QAction *pauseLogScrollAct = new QAction(logPauseIcon, tr("Pause Log Scroll"), this); + pauseLogScrollAct->setToolTip(tr("

Toggle scrolling of the log window. Scrolling can also be paused by selecting a log entry row.

")); + pauseLogScrollAct->setCheckable(true); + ui.btnLogPause->setDefaultAction(pauseLogScrollAct); + addAction(pauseLogScrollAct); + connect(pauseLogScrollAct, &QAction::triggered, this, [this](bool en) { if (!en) ui.logView->selectionModel()->clear(); }); // clear log view selection on "un-pause" + + QAction *clearLogWindowAct = new QAction(QIcon(QStringLiteral("delete.glyph")), tr("Clear Log Window"), this); + clearLogWindowAct->setToolTip(tr("Clear the log window.")); + ui.btnLogClear->setDefaultAction(clearLogWindowAct); + addAction(clearLogWindowAct); + connect(clearLogWindowAct, &QAction::triggered, this, [this]() { logModel->clear(); }); + + QIcon wordWrapIcon(QStringLiteral("wrap_text.glyph")); + wordWrapIcon.addFile(QStringLiteral("notes.glyph"), QSize(), QIcon::Normal, QIcon::On); + QAction *wordWrapLogWindowAct = new QAction(wordWrapIcon, tr("Log Word Wrap"), this); + wordWrapLogWindowAct->setToolTip(tr("Toggle word wrapping of the log window.")); + wordWrapLogWindowAct->setCheckable(true); + wordWrapLogWindowAct->setChecked(true); + ui.btnLogWordWrap->setDefaultAction(wordWrapLogWindowAct); + addAction(wordWrapLogWindowAct); + connect(wordWrapLogWindowAct, &QAction::toggled, this, [this](bool chk) { ui.logView->setWordWrap(chk); ui.logView->resizeRowsToContents(); }); + + // connect the log model record added signal to make sure last record remains in view, unless scroll lock is enabled + connect(logModel, &LogRecordsModel::recordAdded, this, [=](const QModelIndex &i) { + // make sure log view scroll to bottom on insertions, unless a row is selected or scroll pause is set. + ui.logView->resizeRowToContents(i.row()); + if (!pauseLogScrollAct->isChecked() && !ui.logView->selectionModel()->hasSelection()) + ui.logView->scrollToBottom(/*i, QAbstractItemView::PositionAtBottom*/); + }); + // connect log viewer selection model to show pause button active while there is a selection + connect(ui.logView->selectionModel(), &QItemSelectionModel::selectionChanged, this, [=](const QItemSelection &sel, const QItemSelection&) { + pauseLogScrollAct->setChecked(ui.logView->selectionModel()->hasSelection()); + }); + +} + +LogConsole::~LogConsole() +{} + +void LogConsole::setClient(WASimCommander::Client::WASimClient *c) { + wsClient = c; + // Log messages can go right to the log records model. Log messages may arrive at any time, possibly from different threads, + // so placing them into a model allow proper sorting (and filtering). + connect(this, &LogConsole::logMessageReady, logModel, &LogRecordsModel::addRecord, Qt::QueuedConnection); + c->setLogCallback([=](const LogRecord &l, LogSource s) { emit logMessageReady(l, +s); }); + + ui.cbLogLevelCallback->setLevel(wsClient->logLevel(LogFacility::Remote, LogSource::Client)); + ui.cbLogLevelFile->setLevel(wsClient->logLevel( LogFacility::File, LogSource::Client)); + ui.cbLogLevelConsole->setLevel(wsClient->logLevel( LogFacility::Console, LogSource::Client)); + ui.cbLogLevelServer->setLevel(wsClient->logLevel( LogFacility::Remote, LogSource::Server)); +} + +LogRecordsModel *LogConsole::getModel() const { return logModel; } + +void LogConsole::saveSettings() const +{ + QSettings set; + set.beginGroup(objectName()); + set.setValue(QStringLiteral("logViewHeaderState"), ui.logView->horizontalHeader()->saveState()); + set.endGroup(); +} + +void LogConsole::loadSettings() +{ + QSettings set; + set.beginGroup(objectName()); + if (set.contains(QStringLiteral("logViewHeaderState"))) + ui.logView->horizontalHeader()->restoreState(set.value(QStringLiteral("logViewHeaderState")).toByteArray()); + set.endGroup(); +} + +void LogConsole::logMessage(int level, const QString & msg) const +{ + LogRecord l((LogLevel)level, qPrintable(msg)); + emit logMessageReady(l, +LogRecordsModel::LogSource::UI); +} diff --git a/src/WASimUI/LogConsole.h b/src/WASimUI/LogConsole.h new file mode 100644 index 0000000..6782cac --- /dev/null +++ b/src/WASimUI/LogConsole.h @@ -0,0 +1,57 @@ +/* +This file is part of the WASimCommander project. +https://github.com/mpaperno/WASimCommander + +COPYRIGHT: (c) Maxim Paperno; All Rights Reserved. + +This file may be used under the terms of the GNU General Public License (GPL) +as published by the Free Software Foundation, either version 3 of the Licenses, +or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +A copy of the GNU GPL is included with this project +and is also available at . +*/ + +#pragma once + +#include +#include +#include "ui_LogConsole.h" +#include "LogRecordsModel.h" +#include "WASimCommander.h" + +class WASimCommander::Client::WASimClient; + +namespace WASimUiNS { + +class LogConsole : public QWidget +{ + Q_OBJECT + +public: + LogConsole(QWidget *parent = nullptr); + ~LogConsole(); + + void setClient(WASimCommander::Client::WASimClient *c); + WASimUiNS::LogRecordsModel *getModel() const; + +public Q_SLOTS: + void saveSettings() const; + void loadSettings(); + void logMessage(int level, const QString &msg) const; + +signals: + void logMessageReady(const WASimCommander::LogRecord &r, quint8 src) const; + +private: + Ui::LogConsole ui; + WASimUiNS::LogRecordsModel *logModel = nullptr; + WASimCommander::Client::WASimClient *wsClient = nullptr; +}; + +} diff --git a/src/WASimUI/LogConsole.ui b/src/WASimUI/LogConsole.ui new file mode 100644 index 0000000..4fe52f9 --- /dev/null +++ b/src/WASimUI/LogConsole.ui @@ -0,0 +1,497 @@ + + + Copyright Maxim Paperno; all rights reserved. Licensed under GPL v3 (or later) + LogConsole + + + + 0 + 0 + 1000 + 395 + + + + + 0 + 3 + + + + Qt::ActionsContextMenu + + + Log Output + + + + 0 + + + 5 + + + 0 + + + 5 + + + 6 + + + + + + Courier New + 9 + + + + Log Records + + + QAbstractScrollArea::AdjustToContents + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + + 16 + 16 + + + + Qt::ElideMiddle + + + QAbstractItemView::ScrollPerItem + + + QAbstractItemView::ScrollPerPixel + + + Qt::DotLine + + + false + + + false + + + false + + + 20 + + + 60 + + + false + + + false + + + true + + + false + + + + + + + 5 + + + 6 + + + 2 + + + + + + 0 + 0 + + + + Log Window: + + + + + + + Pause + + + Qt::ToolButtonIconOnly + + + + + + + Server + + + Qt::ToolButtonIconOnly + + + false + + + + + + + UI + + + Qt::ToolButtonIconOnly + + + false + + + + + + + + 0 + 0 + + + + Console: + + + + + + + + 0 + 0 + + + + <p>Set Server File Log Level (note that until/unless set via this control, the initial level is unknown).</p> + + + + + + + Debug + + + Qt::ToolButtonIconOnly + + + false + + + + + + + + 0 + 0 + + + + File: + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + Info + + + Qt::ToolButtonIconOnly + + + false + + + + + + + Error + + + Qt::ToolButtonIconOnly + + + false + + + + + + + + 0 + 0 + + + + <p>Set Server Console Log Level (note that until/unless set via this control, the initial level is unknown).</p> + + + + + + + Warning + + + Qt::ToolButtonIconOnly + + + false + + + + + + + Client + + + Qt::ToolButtonIconOnly + + + false + + + + + + + Clear + + + Qt::ToolButtonIconOnly + + + + + + + + 0 + 0 + + + + Client + + + + + + + + 0 + 0 + + + + Set Server Callback Log Level (output directed to this log window). + + + + + + + + 0 + 0 + + + + File: + + + + + + + + 0 + 0 + + + + Set Client File Log Level + + + + + + + Qt::Horizontal + + + + 4 + 20 + + + + + + + + + 0 + 0 + + + + Set Client Callback Log Level (output directed to this log window) + + + + + + + + 0 + 0 + + + + Set Client Console Log Level + + + + + + + + 0 + 0 + + + + Server: + + + + + + + WW + + + Qt::ToolButtonIconOnly + + + + + + + + 0 + 0 + + + + Console: + + + + + + + + 0 + 0 + + + + Log Window: + + + + + + + Trace + + + Qt::ToolButtonIconOnly + + + false + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + + + + WASimUiNS::LogLevelComboBox + QComboBox +
Widgets.h
+
+
+ + +
diff --git a/src/WASimUI/RequestsExport.cpp b/src/WASimUI/RequestsExport.cpp new file mode 100644 index 0000000..bf639d0 --- /dev/null +++ b/src/WASimUI/RequestsExport.cpp @@ -0,0 +1,316 @@ +/* +This file is part of the WASimCommander project. +https://github.com/mpaperno/WASimCommander + +COPYRIGHT: (c) Maxim Paperno; All Rights Reserved. + +This file may be used under the terms of the GNU General Public License (GPL) +as published by the Free Software Foundation, either version 3 of the Licenses, +or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +A copy of the GNU GPL is included with this project +and is also available at . +*/ + +#include +#include +#include + +#include "RequestsExport.h" +#include "RequestsFormat.h" +#include "RequestsModel.h" +//#include "Widgets.h" + +using namespace WASimUiNS; + +static bool editFormEmpty(const Ui::RequestsExport ui) +{ + return ui.cbDefaultCategory->currentText().isEmpty() && + ui.cbIdPrefix->currentText().isEmpty() && + ui.cbFormat->currentText().isEmpty() && + ui.cbDefault->currentText().isEmpty() && + (ui.cbReplWhat->currentText().isEmpty() || ui.cbReplCol->currentData().toInt() < 0); +} + +static void toggleEditFormBtn(const Ui::RequestsExport ui) +{ + bool en = !editFormEmpty(ui); + bool sel = ui.tableView->selectionModel()->hasSelection(); + ui.pbSetValues->defaultAction()->setEnabled(en && sel); + ui.pbClearValues->defaultAction()->setEnabled(en); + ui.pbRegen->menu()->setEnabled(sel); + ui.pbRegen->setEnabled(sel); + ui.pbExportSel->defaultAction()->setEnabled(sel); +} + + +RequestsExportWidget::RequestsExportWidget(RequestsModel *model, QWidget *parent) + : QWidget(parent) +{ + setObjectName(QStringLiteral("RequestsExportWidget")); + ui.setupUi(this); + + ui.tableView->setExportCategories(RequestsFormat::categoriesList()); + setModel(model); + + ui.cbDefaultCategory->addItem(0, ""); + const auto &cats = RequestsFormat::categoriesList(); + ui.cbDefaultCategory->addItems(cats.values(), cats.keys()); + + ui.cbIdPrefix->setClearButtonEnabled(); + ui.cbFormat->setClearButtonEnabled(); + ui.cbDefault->setClearButtonEnabled(); + + ui.cbReplCol->addItem("", -1); + for (int i = RequestsModel::COL_FIRST_META; i <= RequestsModel::COL_LAST_META; ++i) + if (i != RequestsModel::COL_META_CAT) + ui.cbReplCol->addItem(m_model->columnNames[i], i); + + ui.cvReplType->addItem(tr("Replace"), 0); + ui.cvReplType->addItem(tr("Repl. Regex"), 1); + ui.cbReplWhat->setPlaceholderText(tr("Search for...")); + ui.cbReplWhat->setClearButtonEnabled(); + ui.cbReplWith->setPlaceholderText(tr("Replace with...")); + ui.cbReplWith->setClearButtonEnabled(); + +#define MAKE_ACTION(ACT, TTL, ICN, BTN, M, TT, KS) \ + QAction *ACT = new QAction(QIcon(QStringLiteral(##ICN ".glyph")), TTL, this); ACT->setAutoRepeat(false); \ + ACT->setToolTip(TT); ui.##BTN->setDefaultAction(ACT); addAction(ACT); ACT->setShortcut(KS); \ + connect(ACT, &QAction::triggered, this, &RequestsExportWidget::M) +#define MAKE_ACTION_PB(ACT, TTL, IT, ICN, BTN, M, TT, KS) MAKE_ACTION(ACT, TTL, ICN, BTN, M, TT, KS); ACT->setIconText(" " + IT) +#define MAKE_ACTION_PD(ACT, TTL, IT, ICN, BTN, M, TT, KS) MAKE_ACTION_PB(ACT, TTL, IT, ICN, BTN, M, TT, KS); ACT->setDisabled(true) + + MAKE_ACTION_PB(exportAllAct, tr("Export All Request(s)"), tr("Export All"), "download_for_offline", pbExportAll, exportAll, + tr("Export all the Data Request(s) currently shown in the table."), QKeySequence::Save); + MAKE_ACTION_PD(exportSelAct, tr("Export Selected Request(s)"), tr("Export Selected"), "downloading", pbExportSel, exportSelected, + tr("Export only the Data Request(s) currently selected in the table."), QKeySequence(Qt::ControlModifier | Qt::ShiftModifier | Qt::Key_S)); + + MAKE_ACTION_PD(updateSelAct, tr("Update Selected Request(s)"), tr("Update Selected"), "edit_note", pbSetValues, updateBulk, + tr("

Bulk-update any selected Data Request(s) in the table. Only non-empty fields will be applied to their respective columns in the selected records.

" + "Warning! there is no way to undo bulk updates."), QKeySequence(Qt::ControlModifier | Qt::Key_Return)); + + MAKE_ACTION_PD(clearEditAct, tr("Clear Bulk Update Form"), tr("Clear Form"), "scale=.9/backspace", pbClearValues, clearForm, + tr("Reset all fields in the editor to empty default values."), QKeySequence(Qt::ControlModifier | Qt::Key_D)); + + MAKE_ACTION_PB(closeAct, tr("Close Window"), tr("Close"), "fg=blue/close", pbCancel, close, + tr("Close this export window. Any changes made here are preserved."), QKeySequence::Close); + + QMenu *updateMenu = new QMenu(tr("Regenerate Data..."), ui.pbRegen); + updateMenu->setIcon(QIcon(QStringLiteral("auto_mode.glyph"))); + updateMenu->setToolTip(tr("

Regenerate new export IDs or display names based on current values on selected request row(s).

")); + updateMenu->addAction(QIcon(QStringLiteral("format_list_numbered.glyph")), tr("Regenerate Export ID(s)"), this, &RequestsExportWidget::regenIds); + updateMenu->addAction(QIcon(QStringLiteral("title.glyph")), tr("Regenerate Display Name(s)"), this, &RequestsExportWidget::regenNames); + updateMenu->setDisabled(true); + + ui.pbRegen->setIcon(updateMenu->icon()); + ui.pbRegen->setMenu(updateMenu); + ui.pbRegen->setDisabled(true); + //ui.pbRegen->setHidden(true); + + addAction(updateMenu->menuAction()); + addAction(ui.tableView->actionsMenu(this)->menuAction()); + + connect(ui.cbDefaultCategory, &DataComboBox::currentDataChanged, this, [&]() { toggleEditFormBtn(ui); }); + connect(ui.cbIdPrefix, &QComboBox::currentTextChanged, this, [&]() { toggleEditFormBtn(ui); }); + connect(ui.cbFormat, &QComboBox::currentTextChanged, this, [&]() { toggleEditFormBtn(ui); }); + connect(ui.cbDefault, &QComboBox::currentTextChanged, this, [&]() { toggleEditFormBtn(ui); }); + connect(ui.cbReplCol, &DataComboBox::currentDataChanged, this, [&]() { toggleEditFormBtn(ui); }); + connect(ui.cbReplWhat, &QComboBox::currentTextChanged, this, [&]() { toggleEditFormBtn(ui); }); + + loadSettings(); +} + +void RequestsExportWidget::setModel(RequestsModel *model) { + m_model = model; + ui.tableView->setModel(model); + if (!model) + return; + + ui.tableView->moveColumn(RequestsModel::COL_META_CAT, 0); + ui.tableView->moveColumn(RequestsModel::COL_META_ID, 1); + ui.tableView->moveColumn(RequestsModel::COL_META_NAME, 2); + + ui.tableView->hideColumn(RequestsModel::COL_ID); + ui.tableView->hideColumn(RequestsModel::COL_TYPE); + ui.tableView->hideColumn(RequestsModel::COL_SIZE); + ui.tableView->hideColumn(RequestsModel::COL_VALUE); + ui.tableView->hideColumn(RequestsModel::COL_TIMESATMP); + + ensureDefaultValues(); + + connect(ui.tableView->selectionModel(), &QItemSelectionModel::selectionChanged, this, [=](const QItemSelection &sel, const QItemSelection &) { + toggleEditFormBtn(ui); + int cnt = ui.tableView->selectionModel()->selectedRows().count(); + const QString txt(tr("Update %1 %2").arg(cnt).arg(cnt == 1 ? tr("Record") : tr("Records"))); + ui.pbSetValues->defaultAction()->setText(txt); + ui.pbSetValues->defaultAction()->setIconText(txt); + }); + +} + +void RequestsExportWidget::closeEvent(QCloseEvent *ev) { + saveSettings(); + ev->accept(); +} + +RequestsModel * RequestsExportWidget::model() const { return m_model; } + +void RequestsExportWidget::exportAll() { exportRecords(true); } +void RequestsExportWidget::exportSelected() { exportRecords(false); } + +void RequestsExportWidget::exportRecords(bool all) +{ + if ((all && !m_model->rowCount()) || (!all && !ui.tableView->selectionModel()->hasSelection())) + return; + + const QString &fname = QFileDialog::getSaveFileName(this, tr("Export %1 Requests").arg(all ? tr("All") : tr("Selected")), m_lastFile, QStringLiteral("INI files (*.ini)")); + if (fname.isEmpty()) + return; + + if (fname != m_lastFile) { + m_lastFile = fname; + Q_EMIT lastUsedFileChanged(fname); + } + + const QModelIndexList list = all ? m_model->allRequests() : ui.tableView->selectionModel()->selectedRows(RequestsModel::COL_ID); + RequestsFormat::exportToPluginFormat(m_model, list, fname); +} + +void RequestsExportWidget::updateBulk() +{ + const QModelIndexList &list = ui.tableView->selectionModel()->selectedRows(); + if (list.isEmpty() || editFormEmpty(ui)) + return; + QString cid = ui.cbDefaultCategory->currentData().toString(); + QString idp = ui.cbIdPrefix->currentText(); + QString fmt = ui.cbFormat->currentText(); + QString def = ui.cbDefault->currentText(); + + for (const QModelIndex &r : list) { + if (!cid.isEmpty()) { + m_model->setData(m_model->index(r.row(), RequestsModel::COL_META_CAT), cid, Qt::EditRole); + m_model->setData(m_model->index(r.row(), RequestsModel::COL_META_CAT), ui.cbDefaultCategory->currentText(), Qt::ToolTipRole); + } + + if (!fmt.isEmpty()) { + if (fmt == "''" || fmt == "\"\"") + fmt.clear(); + m_model->setData(m_model->index(r.row(), RequestsModel::COL_META_FMT), fmt, Qt::EditRole); + m_model->setData(m_model->index(r.row(), RequestsModel::COL_META_FMT), fmt, Qt::ToolTipRole); + } + + if (!def.isEmpty()) { + if (def == "''" || def == "\"\"") + def.clear(); + m_model->setData(m_model->index(r.row(), RequestsModel::COL_META_DEF), def, Qt::EditRole); + m_model->setData(m_model->index(r.row(), RequestsModel::COL_META_DEF), def, Qt::ToolTipRole); + } + + if (!idp.isEmpty()) { + const QModelIndex col = m_model->index(r.row(), RequestsModel::COL_META_ID); + QString val = m_model->data(col, Qt::EditRole).toString(); + if (idp.startsWith('!')) + val.replace(QRegularExpression("^" + QRegularExpression::escape(idp.remove(0, 1))), ""); + else + val.prepend(idp); + m_model->setData(col, val, Qt::EditRole); + m_model->setData(col, val, Qt::ToolTipRole); + } + + if (!ui.cbReplWhat->currentText().isEmpty() && ui.cbReplCol->currentData().toInt() > -1) { + const QModelIndex col = m_model->index(r.row(), ui.cbReplCol->currentData().toInt()); + if (col.isValid()) { + QString val = m_model->data(col, Qt::EditRole).toString(); + if (ui.cvReplType->currentData().toInt() == 0) + val.replace(ui.cbReplWhat->currentText(), ui.cbReplWith->currentText()); + else + val.replace(QRegularExpression(ui.cbReplWhat->currentText()), ui.cbReplWith->currentText()); + m_model->setData(col, val, Qt::EditRole); + m_model->setData(col, val, Qt::ToolTipRole); + } + } + + } +} + +void RequestsExportWidget::regenIds() +{ + const QModelIndexList &list = ui.tableView->selectionModel()->selectedRows(RequestsModel::COL_META_ID); + if (list.isEmpty()) + return; + RequestRecord req; + QString id; + for (const QModelIndex &idx : list) { + req = m_model->getRequest(idx.row()); + id = RequestsFormat::generateIdFromName(req); + if (req.properties.value("id").toString() != id) { + m_model->setData(idx, id, Qt::EditRole); + m_model->setData(idx, id, Qt::ToolTipRole); + } + } +} + +void RequestsExportWidget::regenNames() +{ + const QModelIndexList &list = ui.tableView->selectionModel()->selectedRows(RequestsModel::COL_META_NAME); + if (list.isEmpty()) + return; + RequestRecord req; + QString name; + for (const QModelIndex &idx : list) { + req = m_model->getRequest(idx.row()); + name = RequestsFormat::generateDefaultName(req); + if (req.properties.value("id").toString() != name) { + m_model->setData(idx, name, Qt::EditRole); + m_model->setData(idx, name, Qt::ToolTipRole); + } + } +} + +void RequestsExportWidget::ensureDefaultValues() +{ + for (int row = 0; row < m_model->rowCount(); ++row) { + RequestRecord req = m_model->getRequest(row); + RequestsFormat::generateRequiredProperties(req); + m_model->updateFromMetaData(row, req); + } +} + +void RequestsExportWidget::clearForm() +{ + ui.cbDefaultCategory->setCurrentIndex(0); + ui.cbIdPrefix->clear(); + ui.cbDefault->clear(); + ui.cbFormat->clear(); + ui.cbReplCol->setCurrentData(-1); + toggleEditFormBtn(ui); +} + +void RequestsExportWidget::saveSettings() const +{ + QSettings s; + s.beginGroup(objectName()); + s.setValue(QStringLiteral("windowGeo"), saveGeometry()); + s.setValue(QStringLiteral("tableViewState"), ui.tableView->saveState()); + const QList editable = findChildren(); + for (DeletableItemsComboBox *cb : editable) + s.setValue(cb->objectName(), cb->saveState()); + s.endGroup(); +} + +void RequestsExportWidget::loadSettings() +{ + QSettings s; + s.beginGroup(objectName()); + restoreGeometry(s.value(QStringLiteral("windowGeo")).toByteArray()); + ui.tableView->restoreState(s.value(QStringLiteral("tableViewState")).toByteArray()); + const QList editable = findChildren(); + for (DeletableItemsComboBox *cb : editable) + cb->restoreState(s.value(cb->objectName()).toByteArray()); + s.endGroup(); +} diff --git a/src/WASimUI/RequestsExport.h b/src/WASimUI/RequestsExport.h new file mode 100644 index 0000000..99f8ead --- /dev/null +++ b/src/WASimUI/RequestsExport.h @@ -0,0 +1,65 @@ +/* +This file is part of the WASimCommander project. +https://github.com/mpaperno/WASimCommander + +COPYRIGHT: (c) Maxim Paperno; All Rights Reserved. + +This file may be used under the terms of the GNU General Public License (GPL) +as published by the Free Software Foundation, either version 3 of the Licenses, +or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +A copy of the GNU GPL is included with this project +and is also available at . +*/ + +#pragma once + +#include +#include "ui_RequestsExport.h" +#include "DataComboBox.h" + +namespace WASimUiNS { + +class RequestsModel; + +class RequestsExportWidget : public QWidget +{ + Q_OBJECT + +public: + explicit RequestsExportWidget(RequestsModel *model, QWidget *parent = nullptr); + RequestsModel *model() const; + +public Q_SLOTS: + void setLastUsedFile(const QString &fn) { m_lastFile = fn; }; + void exportAll(); + void exportSelected(); + +Q_SIGNALS: + void lastUsedFileChanged(const QString &fn); + +protected: + void closeEvent(QCloseEvent *) override; + +private: + void setModel(RequestsModel *model); + void exportRecords(bool all = true); + void ensureDefaultValues(); + void updateBulk(); + void regenIds(); + void regenNames(); + void clearForm(); + void saveSettings() const; + void loadSettings(); + + QString m_lastFile; + RequestsModel *m_model = nullptr; + Ui::RequestsExport ui; +}; + +} // WASimUiNS diff --git a/src/WASimUI/RequestsExport.ui b/src/WASimUI/RequestsExport.ui new file mode 100644 index 0000000..d7cb32e --- /dev/null +++ b/src/WASimUI/RequestsExport.ui @@ -0,0 +1,518 @@ + + + RequestsExport + + + + 0 + 0 + 1290 + 760 + + + + Qt::ActionsContextMenu + + + Export Requests + + + + 6 + + + 6 + + + 6 + + + 10 + + + + + + 0 + 0 + + + + Bulk Update Selected Item(s) + + + + 8 + + + + + 20 + + + 10 + + + 10 + + + + + Clear Form + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + <p>Regenerate new export IDs or display names based on current values on selected request row(s).</p> + + + Regenerate... + + + + + + + Update Selected + + + + + + + + + <p>Sets the plugin display category on each selected item.</p> + + + + + + + + 0 + 0 + + + + Default Value + + + + + + + 0 + + + + + + 0 + 0 + + + + In + + + cbReplCol + + + + + + + + 0 + 0 + + + + Select which column to search in. + + + + + + + + 0 + 0 + + + + Select the syntax for search/replace, exact match or regular expression. + + + + + + + + 0 + 0 + + + + + 100 + 0 + + + + <p>What to replace. The comparison is case-sensitive. Select "Replace Regex" in the preceeding option to use regular expression syntax here and backreferences in the replacement value.</p> +<p>Press Enter when done to save your entry for later selection. Saved items can be removed by right-clicking on them while the list is open.</p> + + + QComboBox::InsertAlphabetically + + + QComboBox::AdjustToContents + + + + + + + + 0 + 0 + + + + with + + + cbReplWith + + + + + + + + 0 + 0 + + + + + 100 + 0 + + + + <p>Replacement string. Use backslashes (<tt>&#92;</tt>) for regular expression capture references, for example: <tt">&#92;1</tt> for first capture group.</p> +<p>Press Enter when done to save your entry for later selection. Saved items can be removed by right-clicking on them while the list is open.</p> + + + QComboBox::InsertAlphabetically + + + QComboBox::AdjustToContents + + + + + + + + + + 0 + 0 + + + + ID Prefix + + + + + + + + 0 + 0 + + + + Format + + + + + + + + 0 + 0 + + + + Category + + + cbDefaultCategory + + + + + + + + 0 + 0 + + + + + 100 + 0 + + + + <p>Prepends the prefix to the ID of each selected item.</p> +<p>To remove a prefix (any string at the beginning of an ID), start the value here with a exclamation mark (<tt>!</tt>). For example: <tt>!MyPrefix_</tt></p> +<p>Press Enter when done to save your entry for later selection. Saved items can be removed by right-clicking on them while the list is open.</p> + + + QComboBox::InsertAlphabetically + + + QComboBox::AdjustToContents + + + + + + + + 0 + 0 + + + + + 100 + 0 + + + + <p>Set the formatting string on selected item(s).</p> +<p>To set the formatting to an empty value, enter two single or double quotes (<tt>''</tt> or <tt>&quot;&quot;</tt>).</p> +<p>Press Enter when done to save your entry for later selection. Saved items can be removed by right-clicking on them while the list is open.</p> + + + QComboBox::InsertAlphabetically + + + QComboBox::AdjustToContents + + + + + + + + 0 + 0 + + + + + 100 + 0 + + + + <p>Set the default value on selected item(s).</p> +<p>To clear the default value, enter two single or double quotes (<tt>''</tt> or <tt>&quot;&quot;</tt>).</p> +<p>Press Enter when done to save your entry for later selection. Saved items can be removed by right-clicking on them while the list is open.</p> + + + QComboBox::InsertAlphabetically + + + QComboBox::AdjustToContents + + + + + + + + + + 10 + + + 10 + + + + + Close + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Export Selected + + + + + + + Export All + + + + + + + + + + 0 + 0 + + + + + Segoe UI Semibold + 75 + true + + + + <html><head/><body><p align="center"><span style=" font-size:10pt;">Export Data Requests to MSFS/SimConnect Touch Portal Plugin Format</span></p></body></html> + + + Qt::RichText + + + + + + + + + + + 0 + 0 + + + + + Segoe UI + + + + <html><head/><body><p>The produced INI file can be used directly in the plugin as a &quot;Variables Definition&quot; file to provide custom states. See the wiki article <a href="https://github.com/mpaperno/MSFSTouchPortalPlugin/wiki/Using-Custom-States-and-Simulator-Variables">Using Custom States and Simulator Variables</a> for more information.</p><p>The <i>Category</i>, <i>Export ID</i>, <i>Display Name</i>, <i>Default</i>, and <i>Format</i> columns can be edited by clicking on the respective field in the table. Apply changes in bulk to selected table row(s) using the form on the left (hover over form fields for more details). <span style=" font-weight:600;">Note</span>: there is no way to undo bulk edits.</p><p>The records will be exported in the same order as in the table below (CTRL-click headings to sort by multiple columns).</p></body></html> + + + Qt::AutoText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + 10 + + + -1 + + + true + + + Qt::LinksAccessibleByMouse + + + + + + + + + ActionPushButton + QPushButton +
ActionPushButton.h
+
+ + DeletableItemsComboBox + QComboBox +
DeletableItemsComboBox.h
+
+ + WASimUiNS::RequestsTableView + QTableView +
RequestsTableView.h
+
+ + BuddyLabel + QLabel +
BuddyLabel.h
+
+ + DataComboBox + QComboBox +
DataComboBox.h
+
+
+ + pbCancel + pbExportSel + pbExportAll + cbDefaultCategory + cbFormat + cbIdPrefix + cbDefault + cbReplCol + cvReplType + cbReplWhat + cbReplWith + pbClearValues + pbRegen + pbSetValues + tableView + + + +
diff --git a/src/WASimUI/RequestsFormat.h b/src/WASimUI/RequestsFormat.h new file mode 100644 index 0000000..c10389d --- /dev/null +++ b/src/WASimUI/RequestsFormat.h @@ -0,0 +1,247 @@ +/* +This file is part of the WASimCommander project. +https://github.com/mpaperno/WASimCommander + +COPYRIGHT: (c) Maxim Paperno; All Rights Reserved. + +This file may be used under the terms of the GNU General Public License (GPL) +as published by the Free Software Foundation, either version 3 of the Licenses, +or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +A copy of the GNU GPL is included with this project +and is also available at . +*/ + +#pragma once + +#include +#include +#include "RequestsModel.h" + +namespace WASimUiNS { + namespace RequestsFormat +{ + + static QMap categoriesList() + { + static QMap cats { + {"AutoPilot", "AutoPilot"}, + {"Camera", "Camera & Views"}, + {"Communication", "Radio & Navigation"}, + {"Electrical", "Electrical"}, + {"Engine", "Engine"}, + {"Environment", "Environment"}, + {"Failures", "Failures"}, + {"FlightInstruments", "Flight Instruments"}, + {"FlightSystems", "Flight Systems"}, + {"Fuel", "Fuel"}, + {"Miscellaneous", "Miscellaneous"}, + {"SimSystem", "Simulator System"}, + }; + return cats; + } + + static QString generateDefaultName(const RequestRecord &req) + { + QString name = req.requestType == WASimCommander::Enums::RequestType::Calculated ? + "CalculatorResult_" + QString::number(req.requestId) : QString(req.nameOrCode).simplified(); + if (req.varTypePrefix == 'A' && req.simVarIndex) + name += ' ' + QString::number(req.simVarIndex); + return name; + } + + static QString generateIdFromName(const RequestRecord &req) + { + static const QRegularExpression idRegex = QRegularExpression("(?:\\b|\\W|_)(\\w)"); + // convert ID to CamelCase + QString id = req.properties.value("name").toString().toLower() /*.replace(' ', '_')*/; + QRegularExpressionMatchIterator mi = idRegex.globalMatch(id); + while (mi.hasNext()) { + QRegularExpressionMatch m = mi.next(); + id.replace(m.capturedStart(), m.capturedLength(), m.captured().toUpper()); + } + id.replace(' ', ""); + if (req.varTypePrefix == 'A' && req.simVarIndex) + id += QString::number(req.simVarIndex); + return id; + } + + static void generateRequiredProperties(RequestRecord &req) + { + // friendly name + if (req.properties.value("name").toString().isEmpty()) + req.properties["name"] = generateDefaultName(req); + // required unique ID + if (req.properties.value("id").toString().isEmpty()) + req.properties["id"] = generateIdFromName(req); + // sorting category id and name + if (req.properties.value("categoryId").toString().isEmpty()) { + req.properties["categoryId"] = QStringLiteral("Miscellaneous"); + req.properties["category"] = QStringLiteral("Miscellaneous"); + } + // optional default value + if (req.properties.value("default").toString().isEmpty() && QString(req.unitName) != QLatin1Literal("string")) + req.properties["default"] = 0; + // optional formatting string + if (req.properties.value("format").toString().isEmpty() && (req.valueSize == WS::DATA_TYPE_FLOAT || req.valueSize == WS::DATA_TYPE_DOUBLE)) + req.properties["format"] = QStringLiteral("F2"); + } + + static QTextStream &addField(QTextStream &out, const char *key, const QVariant &value, bool quoted = false) { + out << key << " = "; + if (quoted) + out << '"'; + out << value.toString(); + if (quoted) + out << '"'; + return out << endl; + } + + static int exportToPluginFormat(RequestsModel *model, const QModelIndexList &rows, const QString &filepath) + { + //const QModelIndexList rows = selection ? selection->selectedRows(RequestsModel::COL_ID) : model->allRequests(); + if (rows.isEmpty()) + return 0; + + QSaveFile *f = new QSaveFile(filepath); + if (!f->open(QFile::WriteOnly | QFile::Truncate | QFile::Text)) + return 0; + QTextStream out(f); + + RequestRecord req; + QString tmp; + for (const QModelIndex &r : rows) { + req = model->getRequest(r.row()); + + generateRequiredProperties(req); + + out << '[' << req.properties.value("id").toString() << ']' << endl; + addField(out, "CategoryId", req.properties.value("categoryId")); + addField(out, "Name", req.properties.value("name"), true); + addField(out, "VariableType", QChar(req.varTypePrefix)); + + tmp = req.nameOrCode; + if (req.varTypePrefix == 'A' && req.simVarIndex) + tmp += ':' + QString::number(req.simVarIndex); + addField(out, "SimVarName", tmp, true); + + if (req.varTypePrefix == 'Q') + addField(out, "CalcResultType", Utils::getEnumName(req.calcResultType, WSEnums::CalcResultTypeNames)); + else + addField(out, "Unit", req.unitName, true); + + tmp = req.properties.value("default").toString(); + if (!tmp.isEmpty()) + addField(out, "DefaultValue", tmp, true); + + tmp = req.properties.value("format").toString(); + if (!tmp.isEmpty()) + addField(out, "StringFormat", tmp, true); + + if (req.period != WSEnums::UpdatePeriod::Tick) + addField(out, "UpdatePeriod", Utils::getEnumName(req.period, WSEnums::UpdatePeriodNames)); + if (req.interval) + addField(out, "UpdateInterval", req.interval); + if (req.deltaEpsilon && QString(req.unitName) != QLatin1Literal("string")) + addField(out, "DeltaEpsilon", QString::number(req.deltaEpsilon)); + + out << endl; + } + + f->commit(); + return rows.count(); + } + + + static QString cleanValue(const QSettings &s, const QString &key, const QString &def = QString()) + { + // QSettings doesn't properly strip trailing comments that start with '#' (';' are OK), so we have to do it.. :( + static const QRegularExpression stripTrailingComments("(? importFromPluginFormat(RequestsModel *model, const QString &filepath) + { + QSettings s(filepath, QSettings::IniFormat); + s.setAtomicSyncRequired(false); + + int enumIdx; + QList ret; + const QStringList list = s.childGroups(); + for (const QString &key : list) { + s.beginGroup(key); + if (!s.contains("Name") || !s.contains("SimVarName")) + continue; + + RequestRecord req; + QModelIndex ri = model->match(model->index(0, RequestsModel::COL_META_ID), Qt::EditRole, key, 1, Qt::MatchExactly | Qt::MatchWrap).value(0); + if (ri.isValid()) + req = model->getRequest(ri.row()); + else + req = RequestRecord(model->nextRequestId(), 'A', nullptr, 0); + + req.properties["id"] = key; + req.properties["name"] = s.value("Name"); + req.properties["categoryId"] = cleanValue(s, "CategoryId", "Miscellaneous").trimmed(); + req.properties["category"] = categoriesList().value(req.properties["categoryId"].toString()); + + if (s.contains("VariableType")) + req.varTypePrefix = qPrintable(cleanValue(s, "VariableType"))[0]; + + QString simVarName = cleanValue(s, "SimVarName").trimmed(); + + if (req.varTypePrefix == 'Q') { + req.requestType = WSEnums::RequestType::Calculated; + enumIdx = Utils::indexOfString(WSEnums::CalcResultTypeNames, qPrintable(cleanValue(s, "CalcResultType", "Double").trimmed())); + req.calcResultType = enumIdx > -1 ? (WSEnums::CalcResultType)enumIdx : WSEnums::CalcResultType::Double; + req.metaType = Utils::calcResultTypeToMetaType(req.calcResultType); + req.valueSize = Utils::metaTypeToSimType(req.metaType); + } + else { + req.requestType = WSEnums::RequestType::Named; + req.setUnitName(qPrintable(cleanValue(s, "Unit", "number").trimmed())); + req.metaType = Utils::unitToMetaType(req.unitName); + req.valueSize = Utils::metaTypeToSimType(req.metaType); + if (req.varTypePrefix == 'A' && simVarName.contains(':')) { + const QStringList ni = simVarName.split(':'); + if (ni.length() == 2) { + simVarName = ni.first(); + req.simVarIndex = ni.last().toInt(); + } + } + } + req.setNameOrCode(qPrintable(simVarName)); + + if (s.contains("UpdatePeriod")) { + enumIdx = Utils::indexOfString(WSEnums::UpdatePeriodNames, qPrintable(cleanValue(s, "UpdatePeriod").trimmed())); + if (enumIdx > -1) + req.calcResultType = (WSEnums::CalcResultType)enumIdx; + } + + if (s.contains("Interval")) + req.interval = s.value("Interval").toUInt(); + if (s.contains("DeltaEpsilon")) + req.deltaEpsilon = s.value("DeltaEpsilon").toFloat(); + + if (s.contains("StringFormat")) + req.properties["format"] = s.value("StringFormat"); + if (s.contains("DefaultValue")) + req.properties["default"] = s.value("DefaultValue"); + + model->addRequest(req); + ret << req; + s.endGroup(); + } + return ret; + } + + + } +} diff --git a/src/WASimUI/RequestsModel.h b/src/WASimUI/RequestsModel.h index 8c6fdf5..02745be 100644 --- a/src/WASimUI/RequestsModel.h +++ b/src/WASimUI/RequestsModel.h @@ -47,18 +47,17 @@ struct RequestRecord : public WASimCommander::Client::DataRequestRecord uint32_t interval; UpdatePeriod period; RequestType requestType; - union { - CalcResultType calcResultType; - uint8_t simVarIndex; - }; + CalcResultType calcResultType; + uint8_t simVarIndex; char varTypePrefix; char nameOrCode[STRSZ_REQ]; char unitName[STRSZ_UNIT]; // From DataRequestRecord time_t lastUpdate; - uint32_t dataSize; + std::vector data; */ int metaType = QMetaType::UnknownType; + QVariantMap properties {}; using DataRequestRecord::DataRequestRecord; @@ -117,7 +116,6 @@ struct RequestRecord : public WASimCommander::Client::DataRequestRecord r.varTypePrefix = pfx.at(0).toLatin1(); return in; } - }; Q_DECLARE_METATYPE(RequestRecord) @@ -134,14 +132,49 @@ class RequestsModel : public QStandardItemModel private: Q_OBJECT - enum Roles { DataRole = Qt::UserRole + 1, MetaTypeRole }; public: + enum Roles { DataRole = Qt::UserRole + 1, MetaTypeRole, PropertiesRole }; enum Columns { - COL_ID, COL_TYPE, COL_RES_TYPE, COL_NAME, COL_SIZE, COL_UNIT, COL_IDX, COL_PERIOD, COL_INTERVAL, COL_EPSILON, COL_VALUE, COL_TIMESATMP, COL_ENUM_END + COL_ID, + COL_TYPE, + COL_RES_TYPE, + COL_NAME, + COL_IDX, + COL_UNIT, + COL_SIZE, + COL_PERIOD, + COL_INTERVAL, + COL_EPSILON, + COL_VALUE, + COL_TIMESATMP, + COL_META_ID, + COL_META_NAME, + COL_META_CAT, + COL_META_DEF, + COL_META_FMT, + COL_ENUM_END, + COL_FIRST_META = COL_META_ID, + COL_LAST_META = COL_META_FMT, }; const QStringList columnNames = { - tr("ID"), tr("Type"), tr("Res/Var Type"), tr("Name or Code"), tr("Size"), tr("Unit"), tr("Idx"), tr("Period"), tr("Intvl"), tr("ΔΕ"), tr("Value"), tr("Last Updt.") + tr("ID"), + tr("Type"), + tr("Res/Var"), + tr("Name or Code"), + tr("Idx"), + tr("Unit"), + tr("Size"), + tr("Period"), + tr("Intvl"), + tr("ΔΕ"), + tr("Value"), + tr("Last Updt."), + tr("Export ID"), + tr("Display Name"), + tr("Category"), + tr("Default"), + tr("Format"), }; RequestsModel(QObject *parent = nullptr) : @@ -160,6 +193,21 @@ class RequestsModel : public QStandardItemModel return item(row, COL_ID)->data(DataRole).toUInt(); } + int findRequestRow(uint32_t requestId) const + { + if (rowCount()) { + const QModelIndexList src = match(index(0, COL_ID), DataRole, requestId, 1, Qt::MatchExactly); + //qDebug() << requestId << src << (!src.isEmpty() ? src.first() : QModelIndex()); + if (!src.isEmpty()) + return src.first().row(); + } + return -1; + } + + QModelIndexList allRequests() const { + return match(index(0, COL_ID), Qt::EditRole, "*", -1, Qt::MatchWildcard | Qt::MatchWrap); + } + void setRequestValue(const WASimCommander::Client::DataRequestRecord &res) { const int row = findRequestRow(res.requestId); @@ -169,15 +217,19 @@ class RequestsModel : public QStandardItemModel // set data display const int dataType = item(row, COL_ID)->data(MetaTypeRole).toInt(); QVariant v = Utils::convertValueToType(dataType, res); - item(row, COL_VALUE)->setText(v.toString()); - item(row, COL_VALUE)->setData(v); + QStandardItem *itm = item(row, COL_VALUE); + itm->setText(v.toString()); + itm->setData(v, DataRole); + itm->setToolTip(itm->text()); // update timestamp column const auto ts = QDateTime::fromMSecsSinceEpoch(res.lastUpdate); - const auto lastts = item(row, COL_TIMESATMP)->data().toULongLong(); + itm = item(row, COL_TIMESATMP); + const auto lastts = itm->data().toULongLong(); const uint64_t tsDelta = lastts ? res.lastUpdate - lastts : 0; - item(row, COL_TIMESATMP)->setText(QString("%1 (%2)").arg(ts.toString("hh:mm:ss.zzz")).arg(tsDelta)); - item(row, COL_TIMESATMP)->setData(res.lastUpdate); + itm->setText(QString("%1 (%2)").arg(ts.toString("hh:mm:ss.zzz")).arg(tsDelta)); + itm->setToolTip(itm->text()); + itm->setData(res.lastUpdate, DataRole); qDebug() << "Saved result" << v << "for request ID" << res.requestId << "ts" << res.lastUpdate << "size" << res.data.size() << "type" << (QMetaType::Type)dataType << "data : " << QByteArray((const char *)res.data.data(), res.data.size()).toHex(':'); } @@ -205,90 +257,90 @@ class RequestsModel : public QStandardItemModel req.metaType = item(row, COL_ID)->data(MetaTypeRole).toInt(); + req.properties["id"] = item(row, COL_META_ID)->data(Qt::EditRole).toString(); + req.properties["name"] = item(row, COL_META_NAME)->data(Qt::EditRole).toString(); + req.properties["categoryId"] = item(row, COL_META_CAT)->data(Qt::EditRole).toString(); + req.properties["category"] = item(row, COL_META_CAT)->data(Qt::ToolTipRole).toString(); + req.properties["default"] = item(row, COL_META_DEF)->data(Qt::EditRole).toString(); + req.properties["format"] = item(row, COL_META_FMT)->data(Qt::EditRole).toString(); + //std::cout << req << std::endl; return req; } QModelIndex addRequest(const RequestRecord &req) { + static const QString NA = tr("-", "Used for non-applicable column values, like 'N/A'."); // tr("N/A") + int row = findRequestRow(req.requestId); const bool newRow = row < 0; if (newRow) row = rowCount(); - setItem(row, COL_ID, new QStandardItem(QString("%1").arg(req.requestId))); - item(row, COL_ID)->setData(req.requestId, DataRole); - item(row, COL_ID)->setData(req.metaType, MetaTypeRole); + QStandardItem *itm = setOrCreateItem(row, COL_ID, QString::number(req.requestId), req.requestId); + itm->setData(req.metaType, MetaTypeRole); + itm->setData(req.properties, PropertiesRole); - setItem(row, COL_TYPE, new QStandardItem(WSEnums::RequestTypeNames[+req.requestType])); - item(row, COL_TYPE)->setData(+req.requestType); + setOrCreateItem(row, COL_TYPE, WSEnums::RequestTypeNames[+req.requestType], +req.requestType); if (req.requestType == WSEnums::RequestType::Calculated) { - setItem(row, COL_RES_TYPE, new QStandardItem(WSEnums::CalcResultTypeNames[+req.calcResultType])); - item(row, COL_RES_TYPE)->setData(+req.calcResultType); - setItem(row, COL_IDX, new QStandardItem(tr("N/A"))); - setItem(row, COL_UNIT, new QStandardItem(tr("N/A"))); - item(row, COL_IDX)->setEnabled(false); - item(row, COL_UNIT)->setEnabled(false); + setOrCreateItem(row, COL_RES_TYPE, WSEnums::CalcResultTypeNames[+req.calcResultType], +req.calcResultType); + setOrCreateItem(row, COL_IDX, NA, QString::number(req.simVarIndex), false); + setOrCreateItem(row, COL_UNIT, NA, QString(req.unitName), false); } else { - setItem(row, COL_RES_TYPE, new QStandardItem(QString(req.varTypePrefix))); - item(row, COL_RES_TYPE)->setData(req.varTypePrefix); - if (Utils::isUnitBasedVariableType(req.varTypePrefix)) { - setItem(row, COL_UNIT, new QStandardItem(QString(req.unitName))); - } - else { - setItem(row, COL_UNIT, new QStandardItem(tr("N/A"))); - item(row, COL_UNIT)->setEnabled(false); - } - if (req.varTypePrefix == 'A') { - setItem(row, COL_IDX, new QStandardItem(QString("%1").arg(req.simVarIndex))); - } - else { - setItem(row, COL_IDX, new QStandardItem(tr("N/A"))); - item(row, COL_IDX)->setEnabled(false); - } + setOrCreateItem(row, COL_RES_TYPE, QString(req.varTypePrefix), req.varTypePrefix); + if (Utils::isUnitBasedVariableType(req.varTypePrefix)) + setOrCreateItem(row, COL_UNIT, req.unitName, QString(req.unitName)); + else + setOrCreateItem(row, COL_UNIT, NA, QString(req.unitName), false); + if (req.varTypePrefix == 'A') + setOrCreateItem(row, COL_IDX, QString::number(req.simVarIndex)); + else + setOrCreateItem(row, COL_IDX, NA, QString::number(req.simVarIndex), false); } - item(row, COL_UNIT)->setData(QString(req.unitName)); if (req.metaType == QMetaType::UnknownType) - setItem(row, COL_SIZE, new QStandardItem(QString("%1").arg(req.valueSize))); + setOrCreateItem(row, COL_SIZE, QString::number(req.valueSize), req.valueSize); else if (req.metaType > QMetaType::User) - setItem(row, COL_SIZE, new QStandardItem(QString("String (%1 B)").arg(req.metaType - QMetaType::User))); + setOrCreateItem(row, COL_SIZE, QString("String (%1 B)").arg(req.metaType - QMetaType::User), req.valueSize); else - setItem(row, COL_SIZE, new QStandardItem(QString("%1 (%2 B)").arg(QString(QMetaType::typeName(req.metaType)).replace("q", "")).arg(QMetaType::sizeOf(req.metaType)))); - item(row, COL_SIZE)->setData(req.valueSize); - - setItem(row, COL_PERIOD, new QStandardItem(WSEnums::UpdatePeriodNames[+req.period])); - item(row, COL_PERIOD)->setData(+req.period); + setOrCreateItem(row, COL_SIZE, QString("%1 (%2 B)").arg(QString(QMetaType::typeName(req.metaType)).replace("q", "")).arg(QMetaType::sizeOf(req.metaType)), req.valueSize); - setItem(row, COL_NAME, new QStandardItem(QString(req.nameOrCode))); - setItem(row, COL_INTERVAL, new QStandardItem(QString("%1").arg(req.interval))); + setOrCreateItem(row, COL_PERIOD, WSEnums::UpdatePeriodNames[+req.period], +req.period); + setOrCreateItem(row, COL_NAME, req.nameOrCode); + setOrCreateItem(row, COL_INTERVAL, QString::number(req.interval)); - if (req.metaType > QMetaType::UnknownType && req.metaType < QMetaType::User) { - setItem(row, COL_EPSILON, new QStandardItem(QString("%1").arg(req.deltaEpsilon, 0, 'f', 7))); - } - else { - setItem(row, COL_EPSILON, new QStandardItem(tr("N/A"))); - item(row, COL_EPSILON)->setEnabled(false); - } - item(row, COL_EPSILON)->setData(req.deltaEpsilon); + if (req.metaType > QMetaType::UnknownType && req.metaType < QMetaType::User) + setOrCreateItem(row, COL_EPSILON, QString::number(req.deltaEpsilon), req.deltaEpsilon); + else + setOrCreateItem(row, COL_EPSILON, NA, req.deltaEpsilon, false); if (newRow) { - setItem(row, COL_VALUE, new QStandardItem("???")); - setItem(row, COL_TIMESATMP, new QStandardItem("Never")); - item(row, COL_TIMESATMP)->setData(0); + setOrCreateItem(row, COL_VALUE, tr("???")); + setOrCreateItem(row, COL_TIMESATMP, tr("Never"), 0); } + updateFromMetaData(row, req); + return index(row, 0); } + void updateFromMetaData(int row, const RequestRecord &req) + { + // Meta data for exports + setOrCreateEditableItem(row, COL_META_ID, req.properties.value("id").toString()); + setOrCreateEditableItem(row, COL_META_NAME, req.properties.value("name").toString()); + setOrCreateEditableItem(row, COL_META_CAT, req.properties.value("categoryId").toString(), req.properties.value("category").toString()); + setOrCreateEditableItem(row, COL_META_DEF, req.properties.value("default").toString()); + setOrCreateEditableItem(row, COL_META_FMT, req.properties.value("format").toString()); + } + void removeRequest(const uint32_t requestId) { const int row = findRequestRow(requestId); if (row > -1) removeRow(row); - qDebug() << requestId << row; } void removeRequests(const QList requestIds) @@ -341,34 +393,31 @@ class RequestsModel : public QStandardItemModel return ret; } - static inline QModelIndexList flattenIndexList(const QModelIndexList &list) - { - QModelIndexList ret; - QModelIndex lastIdx; - for (const QModelIndex &idx : list) { - if (idx.column() == COL_ID && lastIdx.row() != idx.row() && idx.row() < idx.model()->rowCount()) - ret.append(idx); - lastIdx = idx; - } - return ret; - } - signals: void rowCountChanged(int rows); - private: - uint32_t m_nextRequestId = 0; - - int findRequestRow(uint32_t requestId) const + protected: + QStandardItem *setOrCreateItem(int row, int col, const QString &text, const QVariant &data = QVariant(), bool en = true, bool edit = false, const QString &tt = QString()) { - if (rowCount()) { - const QModelIndexList src = match(index(0, COL_ID), DataRole, requestId, 1, Qt::MatchExactly); - //qDebug() << requestId << src << (!src.isEmpty() ? src.first() : QModelIndex()); - if (!src.isEmpty()) - return src.first().row(); + QStandardItem *itm = item(row, col); + if (!itm){ + setItem(row, col, new QStandardItem()); + itm = item(row, col); } - return -1; + itm->setText(text); + itm->setToolTip(tt.isEmpty() ? text : tt); + itm->setEnabled(en); + itm->setEditable(edit); + if (data.isValid()) + itm->setData(data, DataRole); + return itm; } + QStandardItem *setOrCreateEditableItem(int row, int col, const QString &text, const QString &tt = QString()) { + return setOrCreateItem(row, col, text, QVariant(), true, true, tt); + } + + private: + uint32_t m_nextRequestId = 0; }; diff --git a/src/WASimUI/RequestsTableView.h b/src/WASimUI/RequestsTableView.h new file mode 100644 index 0000000..dacf822 --- /dev/null +++ b/src/WASimUI/RequestsTableView.h @@ -0,0 +1,113 @@ +/* +This file is part of the WASimCommander project. +https://github.com/mpaperno/WASimCommander + +COPYRIGHT: (c) Maxim Paperno; All Rights Reserved. + +This file may be used under the terms of the GNU General Public License (GPL) +as published by the Free Software Foundation, either version 3 of the Licenses, +or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +A copy of the GNU GPL is included with this project +and is also available at . +*/ + +#pragma once + +#include +#include +#include +#include +#include + +#include "CustomTableView.h" + +#include "RequestsModel.h" +#include "Widgets.h" + +namespace WASimUiNS +{ + +class CategoryDelegate : public QStyledItemDelegate +{ + Q_OBJECT + public: + QMap textDataMap {}; + + using QStyledItemDelegate::QStyledItemDelegate; + + QWidget *createEditor(QWidget *p, const QStyleOptionViewItem &opt, const QModelIndex &index) const override + { + DataComboBox *cb = new DataComboBox(p); + cb->addItems(textDataMap); + connect(cb, &DataComboBox::currentTextChanged, this, &CategoryDelegate::commit); + return cb; + } + + void setEditorData(QWidget *editor, const QModelIndex &index) const override { + editor->setProperty("currentText", index.data(Qt::EditRole)); + } + void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override { + model->setData(index, editor->property("currentText"), Qt::EditRole); + model->setData(index, editor->property("currentData"), Qt::ToolTipRole); + } + + void commit() { + if (DataComboBox *cb = qobject_cast(sender())) + emit commitData(cb); + } +}; + +class RequestsTableView : public CustomTableView +{ + Q_OBJECT + + public: + RequestsTableView(QWidget *parent) + : CustomTableView(parent), + m_cbCategoryDelegate{new CategoryDelegate(this)} + { + setObjectName(QStringLiteral("RequestsTableView")); + + } + + public Q_SLOTS: + void setExportCategories(const QMap &map) { m_cbCategoryDelegate->textDataMap = map; } + + void setModel(RequestsModel *model) + { + CustomTableView::setModel(model); + + QHeaderView *hdr = horizontalHeader(); + hdr->resizeSection(RequestsModel::COL_ID, 40); + hdr->resizeSection(RequestsModel::COL_TYPE, 65); + hdr->resizeSection(RequestsModel::COL_RES_TYPE, 55); + hdr->resizeSection(RequestsModel::COL_NAME, 265); + hdr->resizeSection(RequestsModel::COL_IDX, 30); + hdr->resizeSection(RequestsModel::COL_UNIT, 55); + hdr->resizeSection(RequestsModel::COL_SIZE, 85); + hdr->resizeSection(RequestsModel::COL_PERIOD, 60); + hdr->resizeSection(RequestsModel::COL_INTERVAL, 40); + hdr->resizeSection(RequestsModel::COL_EPSILON, 60); + hdr->resizeSection(RequestsModel::COL_VALUE, 70); + hdr->resizeSection(RequestsModel::COL_TIMESATMP, 70); + + hdr->resizeSection(RequestsModel::COL_META_ID, 130); + hdr->resizeSection(RequestsModel::COL_META_NAME, 175); + hdr->resizeSection(RequestsModel::COL_META_CAT, 110); + hdr->resizeSection(RequestsModel::COL_META_DEF, 70); + hdr->resizeSection(RequestsModel::COL_META_FMT, 50); + + setItemDelegateForColumn(RequestsModel::COL_META_CAT, m_cbCategoryDelegate); + } + + private: + CategoryDelegate *m_cbCategoryDelegate; +}; + +} diff --git a/src/WASimUI/Utils.h b/src/WASimUI/Utils.h index ac8cb4f..aa2845e 100644 --- a/src/WASimUI/Utils.h +++ b/src/WASimUI/Utils.h @@ -48,8 +48,9 @@ and is also available at . namespace WASimUiNS { -namespace WSEnums = WASimCommander::Enums; -namespace WSCEnums = WASimCommander::Client; +namespace WS = WASimCommander; +namespace WSEnums = WS::Enums; +namespace WSCEnums = WS::Client; // Custom "+" operator for strong enum types to cast to underlying type. template ::value, bool> = true> @@ -173,11 +174,11 @@ class Utils break; case QMetaType::Char: case QMetaType::SChar: - v.setValue(qint8(res)); + v.setValue(qint16((int8_t)res)); // upcast for printing break; case QMetaType::UChar: case QMetaType::Bool: - v.setValue(quint8(res)); + v.setValue(quint16((uint8_t)res)); // upcast for printing break; case QMetaType::Short: v.setValue(qint16(res)); @@ -206,6 +207,27 @@ class Utils return v; } + static int unitToMetaType(QString unit) + { + static const QStringList integralUnits { + "enum", "mask", "flags", "integer", + "position", "position 16k", "position 32k", "position 128", + "frequency bcd16", "frequency bcd32", "bco16", "bcd16", "bcd32", + "seconds", "minutes", "hours", "days", "years", + "celsius scaler 16k", "celsius scaler 256" + }; + static const QStringList boolUnits { "bool", "boolean" }; + + unit = unit.toLower().simplified(); + if (unit == "string") + return QMetaType::User + 256; + if (boolUnits.contains(unit)) + return QMetaType::UChar; + if (integralUnits.contains(unit)) + return QMetaType::Int; + return QMetaType::Double; + } + static bool isUnitBasedVariableType(const char type) { static const std::vector VAR_TYPES_UNIT_BASED = { 'A', 'C', 'E', 'L', 'P' }; return find(VAR_TYPES_UNIT_BASED.cbegin(), VAR_TYPES_UNIT_BASED.cend(), type) != VAR_TYPES_UNIT_BASED.cend(); @@ -388,7 +410,7 @@ class Utils palette.setColor(QPalette::All, QPalette::Highlight, cHlt); palette.setColor(QPalette::Disabled, QPalette::Highlight, QColor("#9Cbbd5ff")); - const QColor cLnkTxt("#6685ff"); + const QColor cLnkTxt("#5eb5ff"); palette.setColor(QPalette::All, QPalette::Link, cLnkTxt); palette.setColor(QPalette::Disabled, QPalette::Link, cLnkTxt.darker()); diff --git a/src/WASimUI/WASimUI.cpp b/src/WASimUI/WASimUI.cpp index d6ce9cb..0270dd6 100644 --- a/src/WASimUI/WASimUI.cpp +++ b/src/WASimUI/WASimUI.cpp @@ -20,29 +20,36 @@ and is also available at . #include #include +#include +#include #include #include #include #include +#include #include #include #include -#include #include -#include #include #include #include #include #include "WASimUI.h" -#include "RequestsModel.h" + +#include "DocImports.h" +#include "DocImportsBrowser.h" #include "EventsModel.h" -#include "LogRecordsModel.h" +#include "LogConsole.h" +#include "RequestsExport.h" +#include "RequestsFormat.h" +#include "RequestsModel.h" #include "Utils.h" #include "Widgets.h" using namespace WASimUiNS; +using namespace DocImports; using namespace WASimCommander; using namespace WASimCommander::Client; using namespace WASimCommander::Enums; @@ -55,6 +62,13 @@ class WASimUIPrivate { friend class WASimUI; Q_DECLARE_TR_FUNCTIONS(WASimUI) + + struct FormWidget { + QString name; + QWidget *w; + QAction *a; + }; + public: WASimUI *q; Ui::WASimUIClass *ui; @@ -62,18 +76,20 @@ class WASimUIPrivate StatusWidget *statWidget; RequestsModel *reqModel; EventsModel *eventsModel; - LogRecordsModel *logModel; + RequestsExportWidget *reqExportWidget = nullptr; + DocImportsBrowser *docBrowserWidget = nullptr; + QAction *toggleConnAct = nullptr; QAction *initAct = nullptr; QAction *connectAct = nullptr; ClientStatus clientStatus = ClientStatus::Idle; QString lastRequestsFile; QString lastEventsFile; uint32_t nextCmdToken = 0x0000FFFF; + QVector formWidgets { }; WASimUIPrivate(WASimUI *q) : q(q), ui(&q->ui), reqModel(new RequestsModel(q)), eventsModel(new EventsModel(q)), - logModel(new LogRecordsModel(q)), statWidget(new StatusWidget(q)), client(new WASimClient(0xDEADBEEF)) { @@ -90,9 +106,17 @@ class WASimUIPrivate client->setCommandResultCallback(&WASimUI::commandResultReady, q); client->setDataCallback(&WASimUI::dataResultReady, q); client->setListResultsCallback(&WASimUI::listResults, q); - client->setLogCallback([=](const LogRecord &l, LogSource s) { emit q->logMessageReady(l, +s); }); + + // Connect our own signals for client callback handling in a thread-safe manner, marshaling back to GUI thread as needed. + // The "signal" methods are "emitted" by the Client as callbacks, registered in Private::setupClient(). + QObject::connect(q, &WASimUI::clientEvent, q, &WASimUI::onClientEvent, Qt::QueuedConnection); + QObject::connect(q, &WASimUI::listResults, q, &WASimUI::onListResults, Qt::QueuedConnection); + // Data updates can go right to the requests model. + QObject::connect(q, &WASimUI::dataResultReady, reqModel, &RequestsModel::setRequestValue, Qt::QueuedConnection); } + bool isConnected() const { return client->isConnected(); } + bool checkConnected() { if (client->isConnected()) @@ -101,6 +125,38 @@ class WASimUIPrivate return false; } + void pingServer() { + quint32 v = client->pingServer(); + if (v > 0) + logUiMessage(tr("Server responded to ping with version: %1").arg(v, 8, 16, QChar('0')), CommandId::Ping, LogLevel::Info); + else + logUiMessage(tr("Ping request timed out!"), CommandId::Ping); + } + + void toggleConnection(bool sim, bool wasim) + { + if (client->isConnected()) { + if (wasim) + client->disconnectServer(); + if (sim) + client->disconnectSimulator(); + return; + } + if (client->isInitialized() && !wasim) { + client->disconnectSimulator(); + return; + } + if (!client->isInitialized()) { + if (SUCCEEDED(client->connectSimulator()) && wasim) + client->connectServer(); + } + else if (wasim && !client->isConnected()) { + client->connectServer(); + } + } + + // Calculator Code form ------------------------------------------------- + void runCalcCode() { if (!checkConnected()) @@ -124,7 +180,6 @@ class WASimUIPrivate else { ui->leCalcResult->setText(tr("Execute failed, check log.")); } - } void copyCalcCodeToRequest() @@ -136,10 +191,16 @@ class WASimUIPrivate //ui->cbValueSize->setCurrentData(Utils::calcResultTypeToMetaType(CalcResultType(ui->cbCalcResultType->currentData().toUInt()))); } - void refreshLVars() { - client->list(); + void updateCalcCodeFormState(const QString &txt) { + const bool en = !txt.isEmpty(); + ui->btnCalc->defaultAction()->setEnabled(en && isConnected()); + ui->btnAddEvent->defaultAction()->setEnabled(en); + ui->btnUpdateEvent->defaultAction()->setEnabled(en); + ui->btnCopyCalcToRequest->defaultAction()->setEnabled(en); } + // Lookups form ------------------------------------------------- + void lookupItem() { if (!checkConnected()) @@ -157,7 +218,13 @@ class WASimUIPrivate ui->leLookupResult->setText(tr("Lookup failed.")); } - void getLocalVar() + // Variables form ------------------------------------------------- + + void refreshLVars() { + client->list(); + } + + void getLocalVar(bool create = false) { if (!checkConnected()) return; @@ -168,14 +235,27 @@ class WASimUIPrivate return; } double result; - const HRESULT hr = client->getVariable(VariableRequest(vtype, varName.toStdString(), ui->cbSetVarUnitName->currentText().toStdString(), ui->sbGetSetSimVarIndex->value()), &result); - if (hr == S_OK) - ui->leVarResult->setText(QString("%1").arg(result, 0, 'f', 7)); + std::string sRes; + HRESULT hr; + if (vtype == 'L' && create) + hr = client->getOrCreateLocalVariable(varName.toStdString(), &result, ui->dsbSetVarValue->value(), ui->cbSetVarUnitName->currentText().toStdString()); else - ui->leVarResult->setText(QString("Error: 0x%1").arg((quint32)hr, 8, 16, QChar('0'))); + hr = client->getVariable(VariableRequest(vtype, varName.toStdString(), ui->cbSetVarUnitName->currentText().toStdString(), ui->sbGetSetSimVarIndex->value()), &result, &sRes); + if (hr == S_OK) { + if (ui->cbSetVarUnitName->currentText() == QLatin1Literal("string")) + ui->leVarResult->setText(sRes.empty() ? tr("Result returned empty string") : QString::fromStdString(sRes)); + else + ui->leVarResult->setText(QString::number(result)); + return; + } + ui->leVarResult->setText(QString("Error: 0x%1").arg((quint32)hr, 8, 16, QChar('0'))); + logUiMessage(tr("Variable request failed."), CommandId::Get); } - void setLocalVar(bool create = false) { + void setLocalVar(bool create = false) + { + if (!checkConnected()) + return; const char vtype = ui->cbGetSetVarType->currentData().toChar().toLatin1(); const QString &varName = vtype == 'L' ? ui->cbLvars->currentText() : ui->cbVariableName->currentText(); if (varName.isEmpty()) { @@ -183,11 +263,35 @@ class WASimUIPrivate return; } if (vtype == 'L' && create) - client->setOrCreateLocalVariable(varName.toStdString(), ui->dsbSetVarValue->value()); + client->setOrCreateLocalVariable(varName.toStdString(), ui->dsbSetVarValue->value(), ui->cbSetVarUnitName->currentText().toStdString()); else client->setVariable(VariableRequest(vtype, varName.toStdString(), ui->cbSetVarUnitName->currentText().toStdString(), ui->sbGetSetSimVarIndex->value()), ui->dsbSetVarValue->value()); } + void toggleSetGetVariableType() + { + const QChar vtype = ui->cbGetSetVarType->currentData().toChar(); + bool isLocal = vtype == 'L', isSimVar = vtype == 'A'; + ui->wLocalVarsForm->setVisible(isLocal); + ui->wOtherVarsForm->setVisible(!isLocal); + ui->wGetSetSimVarIndex->setVisible(isSimVar); + ui->btnSetCreate->setVisible(isLocal); + ui->btnGetCreate->setVisible(isLocal); + bool hasUnit = ui->cbGetSetVarType->currentText().contains('*'); + ui->cbSetVarUnitName->setVisible(hasUnit); + ui->lblSetVarUnit->setVisible(hasUnit); + if (isSimVar) { + ui->cbVariableName->setCompleter(new DocImports::NameCompleter(DocImports::RecordType::SimVars, ui->cbNameOrCode)); + ui->cbVariableName->lineEdit()->addAction(ui->btnFindSimVar->defaultAction(), QLineEdit::TrailingPosition); + } + else { + ui->cbVariableName->lineEdit()->removeAction(ui->btnFindSimVar->defaultAction()); + ui->cbVariableName->resetCompleter(); + if (isLocal) + ui->cbSetVarUnitName->setCurrentText(""); + } + } + void copyLocalVarToRequest() { setRequestFormId(-1); @@ -196,9 +300,22 @@ class WASimUIPrivate ui->cbNameOrCode->setCurrentText(vtype == 'L' ? ui->cbLvars->currentText() : ui->cbVariableName->currentText()); ui->cbVariableType->setCurrentData(vtype); ui->cbUnitName->setCurrentText(ui->cbSetVarUnitName->currentText()); - ui->cbValueSize->setCurrentData(+QMetaType::Double); + ui->cbValueSize->setCurrentData(Utils::unitToMetaType(ui->cbUnitName->currentText())); + } + + void updateLocalVarsFormState() { + const bool isLocal = ui->wLocalVarsForm->isVisible(); + const bool haveData = !(isLocal ? ui->cbLvars->currentText().isEmpty() : ui->cbVariableName->currentText().isEmpty()); + const bool en = haveData && isConnected(); + ui->btnGetVar->defaultAction()->setEnabled(en); + ui->btnSetVar->defaultAction()->setEnabled(en); + ui->btnSetCreate->defaultAction()->setEnabled(en && isLocal); + ui->btnGetCreate->defaultAction()->setEnabled(en && isLocal); + ui->btnCopyLVarToRequest->defaultAction()->setEnabled(haveData); } + // Key Events form ------------------------------------------------- + void sendKeyEventForm() { if (!checkConnected()) @@ -228,7 +345,6 @@ class WASimUIPrivate client->sendCommand(cmd); } - // Data Requests handling ------------------------------------------------- void toggleRequestType() @@ -237,20 +353,35 @@ class WASimUIPrivate ui->cbRequestCalcResultType->setVisible(isCalc); ui->lblRequestCalcResultType->setVisible(isCalc); ui->cbVariableType->setVisible(!isCalc); - //ui->lblUnit->setVisible(!isCalc); - //ui->cbUnitName->setVisible(!isCalc); toggleRequestVariableType(); + if (isCalc) + ui->cbValueSize->setCurrentData(Utils::calcResultTypeToMetaType(CalcResultType(ui->cbRequestCalcResultType->currentData().toUInt()))); + else + ui->cbValueSize->setCurrentData(Utils::unitToMetaType(ui->cbUnitName->currentText())); } void toggleRequestVariableType() { + const QChar type = ui->cbVariableType->currentData().toChar(); bool isCalc = ui->rbRequestType_Calculated->isChecked(); - bool needIdx = !isCalc && ui->cbVariableType->currentData().toChar() == 'A'; + bool needIdx = !isCalc && type == 'A'; ui->lblIndex->setVisible(needIdx); ui->sbSimVarIndex->setVisible(needIdx); bool hasUnit = !isCalc && ui->cbVariableType->currentText().contains('*'); ui->lblUnit->setVisible(hasUnit); ui->cbUnitName->setVisible(hasUnit); + + if (needIdx) { + ui->cbNameOrCode->setCompleter(new DocImports::NameCompleter(DocImports::RecordType::SimVars, ui->cbNameOrCode)); + ui->cbNameOrCode->lineEdit()->addAction(ui->btnReqFindSimVar->defaultAction(), QLineEdit::TrailingPosition); + } + else { + // restore default completer + ui->cbNameOrCode->resetCompleter(); + ui->cbNameOrCode->lineEdit()->removeAction(ui->btnReqFindSimVar->defaultAction()); + if (type == 'L') + ui->cbUnitName->setCurrentText(""); + } } void setRequestFormId(uint32_t id) @@ -258,11 +389,11 @@ class WASimUIPrivate ui->wRequestForm->setProperty("requestId", id); if ((int)id > -1) { ui->lblCurrentRequestId->setText(QString("%1").arg(id)); - ui->pbUpdateRequest->setEnabled(true); + ui->btnUpdateRequest->defaultAction()->setEnabled(true); } else { ui->lblCurrentRequestId->setText(tr("New")); - ui->pbUpdateRequest->setEnabled(false); + ui->btnUpdateRequest->defaultAction()->setEnabled(false); } } @@ -273,11 +404,15 @@ class WASimUIPrivate return; } - uint32_t requestId = ui->wRequestForm->property("requestId").toUInt(); - if ((int)requestId < 0 || !update) - requestId = reqModel->nextRequestId(); + RequestRecord req; + int row = -1; + if (update && ui->wRequestForm->property("requestId").toInt() > -1) + row = reqModel->findRequestRow(ui->wRequestForm->property("requestId").toUInt()); + if (row < 0) + req = RequestRecord(reqModel->nextRequestId()); + else + req = reqModel->getRequest(row); - RequestRecord req(requestId); req.requestType = ui->rbRequestType_Calculated->isChecked() ? RequestType::Calculated : RequestType::Named; if (req.requestType == RequestType::Calculated) { req.calcResultType = (CalcResultType)ui->cbRequestCalcResultType->currentData().toUInt(); @@ -289,7 +424,6 @@ class WASimUIPrivate req.setNameOrCode(qPrintable(ui->cbNameOrCode->currentText())); req.setUnitName(qPrintable(ui->cbUnitName->currentText())); - int metaType = QMetaType::UnknownType; if (ui->cbValueSize->currentData().isValid()) req.valueSize = Utils::metaTypeToSimType(req.metaType = ui->cbValueSize->currentData().toInt()); else @@ -305,7 +439,7 @@ class WASimUIPrivate //std::cout << req << std::endl; setRequestFormId(req.requestId); - if FAILED(client->saveDataRequest(req)) + if (FAILED(client->saveDataRequest(req)) && row < 0) return; const QModelIndex idx = reqModel->addRequest(req); ui->requestsView->selectRow(idx.row()); @@ -330,16 +464,24 @@ class WASimUIPrivate ui->cbUnitName->setCurrentText(req.unitName); ui->sbSimVarIndex->setValue(req.simVarIndex); } - if (req.metaType == QMetaType::UnknownType) - ui->cbValueSize->setCurrentText(QString("%1").arg(req.valueSize)); - else - ui->cbValueSize->setCurrentData(req.metaType); ui->cbNameOrCode->setCurrentText(req.nameOrCode); ui->cbPeriod->setCurrentData(+req.period); ui->sbInterval->setValue(req.interval); ui->dsbDeltaEpsilon->setValue(req.deltaEpsilon); } + void clearRequestForm() + { + setRequestFormId(-1); + ui->cbNameOrCode->setCurrentText(""); + ui->cbUnitName->setCurrentText(""); + ui->cbValueSize->setCurrentText(""); + ui->sbSimVarIndex->setValue(0); + ui->cbPeriod->setCurrentData(+UpdatePeriod::Tick); + ui->sbInterval->setValue(0); + ui->dsbDeltaEpsilon->setValue(0.0); + } + void removeRequests(const QModelIndexList &list) { for (const QModelIndex &idx : list) { @@ -351,7 +493,7 @@ class WASimUIPrivate } void removeSelectedRequests() { - removeRequests(reqModel->flattenIndexList(ui->requestsView->selectionModel()->selectedIndexes())); + removeRequests(ui->requestsView->selectionModel()->selectedRows(RequestsModel::COL_ID)); } void removeAllRequests() { @@ -367,12 +509,21 @@ class WASimUIPrivate { if (!checkConnected()) return; - const QModelIndexList list = reqModel->flattenIndexList(ui->requestsView->selectionModel()->selectedIndexes()); + const QModelIndexList list = ui->requestsView->selectionModel()->selectedRows(RequestsModel::COL_ID); for (const QModelIndex &idx : list) client->updateDataRequest(reqModel->requestId(idx.row())); } - void saveRequests() + void pauseRequests(bool chk) { + static const QIcon dataResumeIcon(QStringLiteral("play_arrow.glyph")); + static const QIcon dataPauseIcon(QStringLiteral("pause.glyph")); + client->setDataRequestsPaused(chk); + ui->btnReqestsPause->defaultAction()->setIconText(chk ? tr("Resume") : tr("Suspend")); + // for some reason the checked icon "on" state doesn't work automatically like it should... + ui->btnReqestsPause->setIcon(chk ? dataResumeIcon : dataPauseIcon); + }; + + void saveRequests(bool forExport = false) { if (!reqModel->rowCount()) return; @@ -381,10 +532,35 @@ class WASimUIPrivate if (fname.isEmpty()) return; lastRequestsFile = fname; - const int rows = reqModel->saveToFile(fname); + const int rows = forExport ? RequestsFormat::exportToPluginFormat(reqModel, reqModel->allRequests(), fname) : reqModel->saveToFile(fname); logUiMessage(tr("Saved %1 Data Request(s) to file: %2").arg(rows).arg(fname), CommandId::Ack, LogLevel::Info); } + void exportRequests() + { + if (!reqModel->rowCount()) + return; + + if (reqExportWidget) { + if (reqExportWidget->isMinimized()) { + reqExportWidget->showNormal(); + } + else { + reqExportWidget->raise(); + reqExportWidget->activateWindow(); + } + return; + } + + reqExportWidget = new RequestsExportWidget(reqModel, q); + reqExportWidget->setWindowFlag(Qt::Dialog); + reqExportWidget->setAttribute(Qt::WA_DeleteOnClose); + reqExportWidget->setLastUsedFile(lastRequestsFile); + QObject::connect(reqExportWidget, &RequestsExportWidget::lastUsedFileChanged, [&](const QString &fn) { lastRequestsFile = fn; }); + QObject::connect(reqExportWidget, &QObject::destroyed, q, [=]() { reqExportWidget = nullptr; }); + reqExportWidget->show(); + } + void loadRequests(bool replace) { const QString &fname = QFileDialog::getOpenFileName(q, tr("Select a saved Requests file"), lastRequestsFile, QStringLiteral("INI files (*.ini)")); @@ -393,13 +569,33 @@ class WASimUIPrivate lastRequestsFile = fname; if (replace) removeAllRequests(); - const QList &added = reqModel->loadFromFile(fname); + + QFile f(fname); + if (!f.open(QFile::ReadOnly | QFile::Text)) { + logUiMessage(tr("Could not open file '%1' for reading").arg(fname), CommandId::Nak, LogLevel::Error); + return; + } + const QString first = f.readLine(); + f.close(); + const bool isNative = first.startsWith(QLatin1Literal("[Requests]")); + + const QList &added = isNative ? reqModel->loadFromFile(fname) : RequestsFormat::importFromPluginFormat(reqModel, fname); for (const DataRequest &req : added) - client->saveDataRequest(req); + client->saveDataRequest(req, true); // async logUiMessage(tr("Loaded %1 Data Request(s) from file: %2").arg(added.count()).arg(fname), CommandId::Ack, LogLevel::Info); } + void toggleRequestButtonsState() + { + bool conn = isConnected(); + bool hasSel = ui->requestsView->selectionModel()->hasSelection(); + bool hasRecords = reqModel->rowCount() > 0; + ui->btnReqestsRemove->defaultAction()->setEnabled(hasSel); + ui->btnReqestsUpdate->defaultAction()->setEnabled(hasSel && conn); + ui->btnReqestsPause->defaultAction()->setEnabled(hasRecords); + ui->btnReqestsSave->defaultAction()->setEnabled(hasRecords); + } // Registered calc events ------------------------------------------------- @@ -464,7 +660,7 @@ class WASimUIPrivate } void removeSelectedEvents() { - removeEvents(eventsModel->flattenIndexList(ui->eventsView->selectionModel()->selectedIndexes())); + removeEvents(ui->eventsView->selectionModel()->selectedRows(EventsModel::COL_ID)); } void removeAllEvents() { @@ -481,7 +677,7 @@ class WASimUIPrivate { if (!checkConnected()) return; - const QModelIndexList list = eventsModel->flattenIndexList(ui->eventsView->selectionModel()->selectedIndexes()); + const QModelIndexList list = ui->eventsView->selectionModel()->selectedRows(EventsModel::COL_ID); for (const QModelIndex &idx : list) client->transmitEvent(eventsModel->eventId(idx.row())); } @@ -514,17 +710,63 @@ class WASimUIPrivate logUiMessage(tr("Loaded %1 Data Request(s) from file: %2").arg(added.count()).arg(fname), CommandId::Ack, LogLevel::Info); } + void toggleEventButtonsState() + { + bool hasSel = ui->eventsView->selectionModel()->hasSelection(); + ui->btnEventsRemove->defaultAction()->setEnabled(hasSel); + ui->btnEventsTransmit->defaultAction()->setEnabled(hasSel && isConnected()); + } + // Utilities ------------------------------------------------- void logUiMessage(const QString &msg, CommandId cmd = CommandId::None, LogLevel level = LogLevel::Error) { - LogRecord l(level, qPrintable(msg)); - emit q->logMessageReady(l, +LogRecordsModel::LogSource::UI); + ui->wLogWindow->logMessage(+level, msg); if (cmd != CommandId::None) emit q->commandResultReady(Command(level == LogLevel::Error ? CommandId::Nak : CommandId::Ack, +cmd, qPrintable(msg))); } + void openDocsLookup(DocImports::RecordType type, QComboBox *cb) + { + DocImportsBrowser *browser = new DocImportsBrowser(q, type, DocImportsBrowser::ViewMode::PopupViewMode); + browser->setAttribute(Qt::WA_DeleteOnClose); + browser->setWindowFlag(Qt::Dialog); + browser->setWindowModality(Qt::ApplicationModal); + browser->show(); + const QPoint qPos = q->mapToGlobal(QPoint(0,0)); + const QPoint cbPos = cb->mapToGlobal(QPoint(0, cb->height())); + const QRect rect(qPos, q->size()); + QPoint pos = QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, browser->size(), rect).topLeft(); + pos.setY(rect.y() + cbPos.y() - qPos.y()); + browser->move(pos); + QObject::connect(browser, &DocImportsBrowser::itemSelected, q, [=](const QModelIndex &row) { + if (row.isValid()) + cb->setCurrentText(browser->model()->record(row.row()).field("Name").value().toString()); + browser->close(); + }); + } + + void openDocsLookupWindow() + { + if (docBrowserWidget) { + if (docBrowserWidget->isMinimized()) { + docBrowserWidget->showNormal(); + } + else { + docBrowserWidget->raise(); + docBrowserWidget->activateWindow(); + } + return; + } + + docBrowserWidget = new DocImportsBrowser(q); + docBrowserWidget->setAttribute(Qt::WA_DeleteOnClose); + docBrowserWidget->setWindowFlag(Qt::Dialog); + docBrowserWidget->show(); + QObject::connect(docBrowserWidget, &QObject::destroyed, q, [=]() { docBrowserWidget = nullptr; }); + } + // Save/Load UI settings ------------------------------------------------- @@ -533,8 +775,15 @@ class WASimUIPrivate QSettings set; set.setValue(QStringLiteral("mainWindowGeo"), q->saveGeometry()); set.setValue(QStringLiteral("mainWindowState"), q->saveState()); - set.setValue(QStringLiteral("eventsViewHeaderState"), ui->eventsView->horizontalHeader()->saveState()); - set.setValue(QStringLiteral("logViewHeaderState"), ui->logView->horizontalHeader()->saveState()); + set.setValue(QStringLiteral("eventsViewState"), ui->eventsView->saveState()); + set.setValue(QStringLiteral("requestsViewState"), ui->requestsView->saveState()); + ui->wLogWindow->saveSettings(); + + // Visible form widgets + set.beginGroup(QStringLiteral("Widgets")); + for (const FormWidget &vw : qAsConst(formWidgets)) + set.setValue(vw.name, vw.a->isChecked()); + set.endGroup(); set.setValue(QStringLiteral("useDarkTheme"), Utils::isDarkStyle()); set.setValue(QStringLiteral("lastRequestsFile"), lastRequestsFile); @@ -543,21 +792,35 @@ class WASimUIPrivate set.beginGroup(QStringLiteral("EditableSelectors")); const QList editable = q->findChildren(); for (DeletableItemsComboBox *cb : editable) - set.setValue(cb->objectName(), cb->editedItems()); + set.setValue(cb->objectName(), cb->saveState()); + set.endGroup(); + + // Variables form + set.beginGroup(ui->wLocalVarsForm->objectName()); + set.setValue(ui->cbGetSetVarType->objectName(), ui->cbGetSetVarType->currentData()); + set.endGroup(); + + // Requests form + set.beginGroup(ui->wRequests->objectName()); + set.setValue(ui->bgrpRequestType->objectName(), ui->rbRequestType_Named->isChecked()); + set.setValue(ui->cbVariableType->objectName(), ui->cbVariableType->currentData()); set.endGroup(); } void readSettings() { QSettings set; - if (set.contains(QStringLiteral("mainWindowGeo"))) - q->restoreGeometry(set.value(QStringLiteral("mainWindowGeo")).toByteArray()); - if (set.contains(QStringLiteral("mainWindowState"))) - q->restoreState(set.value(QStringLiteral("mainWindowState")).toByteArray()); - if (set.contains(QStringLiteral("eventsViewHeaderState"))) - ui->eventsView->horizontalHeader()->restoreState(set.value(QStringLiteral("eventsViewHeaderState")).toByteArray()); - if (set.contains(QStringLiteral("logViewHeaderState"))) - ui->logView->horizontalHeader()->restoreState(set.value(QStringLiteral("logViewHeaderState")).toByteArray()); + q->restoreGeometry(set.value(QStringLiteral("mainWindowGeo")).toByteArray()); + q->restoreState(set.value(QStringLiteral("mainWindowState")).toByteArray()); + ui->eventsView->restoreState(set.value(QStringLiteral("eventsViewState")).toByteArray()); + ui->requestsView->restoreState(set.value(QStringLiteral("requestsViewState")).toByteArray()); + ui->wLogWindow->loadSettings(); + + // Visible form widgets + set.beginGroup(QStringLiteral("Widgets")); + for (const FormWidget &vw : qAsConst(formWidgets)) + vw.a->setChecked(set.value(vw.name, vw.a->isChecked()).toBool()); + set.endGroup(); const QString defaultFileLoc = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); lastRequestsFile = set.value(QStringLiteral("lastRequestsFile"), defaultFileLoc + QStringLiteral("/WASimUI-requests.ini")).toString(); @@ -569,16 +832,24 @@ class WASimUIPrivate set.beginGroup(QStringLiteral("EditableSelectors")); const QList editable = q->findChildren(); for (DeletableItemsComboBox *cb : editable) { - if (set.contains(cb->objectName())) { - QStringList val = set.value(cb->objectName()).toStringList(); - if (val.isEmpty()) - continue; - if (cb->insertPolicy() != QComboBox::InsertAlphabetically) - std::sort(val.begin(), val.end()); - cb->insertEditedItems(val); - } + // check for old version settings format + if (set.value(cb->objectName()).canConvert()) + cb->insertEditedItems(set.value(cb->objectName()).toStringList()); + else + cb->restoreState(set.value(cb->objectName()).toByteArray()); } set.endGroup(); + + // Variables form + set.beginGroup(ui->wLocalVarsForm->objectName()); + ui->cbGetSetVarType->setCurrentData(set.value(ui->cbGetSetVarType->objectName(), ui->cbGetSetVarType->currentData())); + set.endGroup(); + + // Requests form + set.beginGroup(ui->wRequests->objectName()); + ui->rbRequestType_Named->setChecked(set.value(ui->bgrpRequestType->objectName(), ui->rbRequestType_Named->isChecked()).toBool()); + ui->cbVariableType->setCurrentData(set.value(ui->cbVariableType->objectName(), ui->cbVariableType->currentData())); + set.endGroup(); } }; @@ -588,6 +859,28 @@ class WASimUIPrivate // WASimUI // ------------------------------------------------------------- +// Action creation macros used below +#define GLYPH_STR(ICN) QStringLiteral(##ICN ".glyph") +#define GLYPH_ICON(ICN) QIcon(GLYPH_STR(ICN)) + +#define MAKE_ACTION_NW(ACT, TTL, ICN, TT) QAction *ACT = new QAction(GLYPH_ICON(ICN), TTL, this); ACT->setAutoRepeat(false); ACT->setToolTip(TT) +#define MAKE_ACTION_NB(ACT, TTL, ICN, W, TT) MAKE_ACTION_NW(ACT, TTL, ICN, TT); ui.##W->addAction(ACT) +#define MAKE_ACTION_NC(ACT, TTL, ICN, BTN, W, TT) MAKE_ACTION_NB(ACT, TTL, ICN, W, TT); ui.##BTN->setDefaultAction(ACT) + +#define MAKE_ACTION_CONN(ACT, M) connect(ACT, &QAction::triggered, this, [this](bool chk) { d->##M; }) +#define MAKE_ACTION_SCUT(ACT, KS) ACT->setShortcut(KS); ACT->setShortcutContext(Qt::WidgetWithChildrenShortcut) +#define MAKE_ACTION_ITXT(ACT, T) ACT->setIconText(" " + T) + +#define MAKE_ACTION(ACT, TTL, ICN, BTN, W, M, TT) MAKE_ACTION_NC(ACT, TTL, ICN, BTN, W, TT); MAKE_ACTION_CONN(ACT, M) +#define MAKE_ACTION_D(ACT, TTL, ICN, BTN, W, M, TT) MAKE_ACTION(ACT, TTL, ICN, BTN, W, M, TT); ACT->setDisabled(true) +#define MAKE_ACTION_SC(ACT, TTL, ICN, BTN, W, M, TT, KS) MAKE_ACTION(ACT, TTL, ICN, BTN, W, M, TT); MAKE_ACTION_SCUT(ACT, KS) +#define MAKE_ACTION_SC_D(ACT, TTL, ICN, BTN, W, M, TT, KS) MAKE_ACTION_SC(ACT, TTL, ICN, BTN, W, M, TT, KS); ACT->setDisabled(true) +#define MAKE_ACTION_PB(ACT, TTL, IT, ICN, BTN, W, M, TT, KS) MAKE_ACTION_SC(ACT, TTL, ICN, BTN, W, M, TT, KS); MAKE_ACTION_ITXT(ACT, IT) +#define MAKE_ACTION_PB_D(ACT, TTL, IT, ICN, BTN, W, M, TT, KS) MAKE_ACTION_SC_D(ACT, TTL, ICN, BTN, W, M, TT, KS); MAKE_ACTION_ITXT(ACT, IT) +#define MAKE_ACTION_PB_NC(ACT, TTL, IT, ICN, BTN, W, TT) MAKE_ACTION_NC(ACT, TTL, ICN, BTN, W, TT); MAKE_ACTION_ITXT(ACT, IT) +#define MAKE_ACTION_NW_SC(ACT, TTL, IT, ICN, M, TT, KS) MAKE_ACTION_NW(ACT, TTL, ICN, TT); MAKE_ACTION_CONN(ACT, M); MAKE_ACTION_ITXT(ACT, IT); MAKE_ACTION_SCUT(ACT, KS) +// --------------------------------- + WASimUI::WASimUI(QWidget *parent) : QMainWindow(parent), d(new WASimUIPrivate(this)) @@ -641,19 +934,50 @@ WASimUI::WASimUI(QWidget *parent) : ui.dsbDeltaEpsilon->setMaximum(FLT_MAX); // maximum lengths for text edit boxes ui.leCmdSData->setMaxLength(STRSZ_CMD); - ui.cbCalculatorCode->lineEdit()->setMaxLength(STRSZ_CMD); - ui.cbVariableName->lineEdit()->setMaxLength(STRSZ_CMD); + ui.cbCalculatorCode->setMaxLength(STRSZ_CMD); + ui.cbVariableName->setMaxLength(STRSZ_CMD); ui.cbLvars->lineEdit()->setMaxLength(STRSZ_CMD); - ui.cbLookupName->lineEdit()->setMaxLength(STRSZ_CMD); + ui.cbLookupName->setMaxLength(STRSZ_CMD); ui.leEventName->setMaxLength(STRSZ_ENAME); - ui.cbNameOrCode->lineEdit()->setMaxLength(STRSZ_REQ); + ui.cbNameOrCode->setMaxLength(STRSZ_REQ); + + // Unit name suggestions completer from imported docs. + ui.cbUnitName->setCompleter(new DocImports::NameCompleter(DocImports::RecordType::SimVarUnits, ui.cbUnitName), true); + ui.cbSetVarUnitName->setCompleter(new DocImports::NameCompleter(DocImports::RecordType::SimVarUnits, ui.cbSetVarUnitName), true); + ui.cbSetVarUnitName->setCompleterOptionsButtonEnabled(); + // Enable clear and suggestion options buttons on the data lookup combos. + ui.cbCalculatorCode->setCompleterOptions(Qt::MatchContains, QCompleter::PopupCompletion); + ui.cbCalculatorCode->setClearButtonEnabled(); + ui.cbLvars->setCompleterOptions(Qt::MatchStartsWith, QCompleter::PopupCompletion); + ui.cbLvars->setClearButtonEnabled(); + ui.cbVariableName->setCompleterOptions(Qt::MatchContains, QCompleter::PopupCompletion); + ui.cbVariableName->setClearButtonEnabled(); + ui.cbKeyEvent->setCompleterOptions(Qt::MatchContains, QCompleter::PopupCompletion); + ui.cbKeyEvent->setClearButtonEnabled(); + ui.cbNameOrCode->setCompleterOptions(Qt::MatchContains, QCompleter::PopupCompletion); + ui.cbNameOrCode->setClearButtonEnabled(); + ui.cbUnitName->setCompleterOptions(Qt::MatchStartsWith, QCompleter::PopupCompletion); + + // Init the calculator editor form + ui.wCalcForm->setProperty("eventId", -1); + ui.btnUpdateEvent->setVisible(false); + // Connect variable selector to enable/disable relevant actions + connect(ui.cbCalculatorCode, &QComboBox::currentTextChanged, this, [=](const QString &txt) { d->updateCalcCodeFormState(txt); }); + + // Key event name completer + ui.cbKeyEvent->setCompleter(new DocImports::NameCompleter(DocImports::RecordType::KeyEvents, ui.cbKeyEvent)); // Set up the Requests table view + ui.requestsView->setExportCategories(RequestsFormat::categoriesList()); ui.requestsView->setModel(d->reqModel); - ui.requestsView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); - ui.requestsView->horizontalHeader()->setSectionsMovable(true); + // Hide unused columns + QHeaderView *hdr = ui.requestsView->header(); + for (int i = RequestsModel::COL_FIRST_META; i <= RequestsModel::COL_LAST_META; ++i) + hdr->hideSection(i); // connect double click action to populate the request editor form connect(ui.requestsView, &QTableView::doubleClicked, this, [this](const QModelIndex &idx) { d->populateRequestForm(idx); }); + // Connect to table view selection model to en/disable the remove/update actions when selection changes. + connect(ui.requestsView->selectionModel(), &QItemSelectionModel::selectionChanged, this, [=]() { d->toggleRequestButtonsState(); }); // Set up the Events table view ui.eventsView->setModel(d->eventsModel); @@ -663,33 +987,38 @@ WASimUI::WASimUI(QWidget *parent) : ui.eventsView->horizontalHeader()->resizeSection(EventsModel::COL_CODE, 140); // connect double click action to populate the event editor form connect(ui.eventsView, &QTableView::doubleClicked, this, [this](const QModelIndex &idx) { d->populateEventForm(idx); }); + // Connect to table view selection model to en/disable the remove/update actions when selection changes. + connect(ui.eventsView->selectionModel(), &QItemSelectionModel::selectionChanged, this, [=]() { d->toggleEventButtonsState(); }); // Set up the Log table view - ui.logView->setModel(d->logModel); - ui.logView->sortByColumn(LogRecordsModel::COL_TS, Qt::AscendingOrder); - //ui.logView->horizontalHeader()->setSortIndicator(LogRecordsModel::COL_TS, Qt::AscendingOrder); - ui.logView->horizontalHeader()->setSortIndicatorShown(false); - ui.logView->horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive); - ui.logView->horizontalHeader()->setSectionsMovable(true); - ui.logView->horizontalHeader()->resizeSection(LogRecordsModel::COL_LEVEL, 70); - ui.logView->horizontalHeader()->resizeSection(LogRecordsModel::COL_TS, 110); - ui.logView->horizontalHeader()->resizeSection(LogRecordsModel::COL_SOURCE, 20); - ui.logView->horizontalHeader()->setToolTip(tr("Severity Level | Timestamp | Source | Message")); + ui.wLogWindow->setClient(d->client); // Set initial state of Variables form, Local var type is default. - ui.wOtherVarsForm->setVisible(false); - ui.wGetSetSimVarIndex->setVisible(false); + d->toggleSetGetVariableType(); - // Init the request and calc editor forms - ui.wRequestForm->setProperty("requestId", -1); - ui.wCalcForm->setProperty("eventId", -1); + // Connect variable selector to enable/disable relevant actions + connect(ui.cbLvars, &QComboBox::currentTextChanged, this, [=]() { d->updateLocalVarsFormState(); }); + connect(ui.cbVariableName, &QComboBox::currentTextChanged, this, [=]() { d->updateLocalVarsFormState(); }); + + // connect to variable type combo box to switch between views for local vars vs. everything else + connect(ui.cbGetSetVarType, &DataComboBox::currentDataChanged, this, [=](const QVariant &) { + d->toggleSetGetVariableType(); + d->updateLocalVarsFormState(); + }); - // Update the Data Request form UI based on default type. + // Init the request editor form + ui.wRequestForm->setProperty("requestId", -1); + // Update the Data Request form UI based on default types. d->toggleRequestType(); + // Connect the request type radio buttons to toggle the UI. connect(ui.bgrpRequestType, QOverload::of(&QButtonGroup::buttonToggled), this, [this](int,bool) { d->toggleRequestType(); }); // show/hide SimVar index spin box based on type of variable selected connect(ui.cbVariableType, &DataComboBox::currentDataChanged, this, [this](const QVariant&) { d->toggleRequestVariableType(); }); + // Connect the data request unit type selector to choose a default result size + connect(ui.cbUnitName, &DataComboBox::currentTextChanged, this, [this](const QString &data) { + ui.cbValueSize->setCurrentData(Utils::unitToMetaType(data)); + }); // Connect the data request calc result type selector to choose a default result size connect(ui.cbRequestCalcResultType, &DataComboBox::currentDataChanged, this, [this](const QVariant &data) { ui.cbValueSize->setCurrentData(Utils::calcResultTypeToMetaType(CalcResultType(data.toUInt()))); @@ -703,310 +1032,178 @@ WASimUI::WASimUI(QWidget *parent) : connect(ui.cbPeriod, &DataComboBox::currentDataChanged, this, [this](const QVariant &data) { ui.sbInterval->setEnabled(data.toUInt() >= +UpdatePeriod::Tick); }); - // connect the Data Request save/add buttons - connect(ui.pbAddRequest, &QPushButton::clicked, this, [this]() { d->handleRequestForm(false); }); - connect(ui.pbUpdateRequest, &QPushButton::clicked, this, [this]() { d->handleRequestForm(true); }); - - // Set and connect Log Level combo boxes for Client and Server logging levels - ui.cbLogLevelCallback->setProperties(d->client->logLevel( LogFacility::Remote, LogSource::Client), LogFacility::Remote, LogSource::Client); - ui.cbLogLevelFile->setProperties(d->client->logLevel( LogFacility::File, LogSource::Client), LogFacility::File, LogSource::Client); - ui.cbLogLevelConsole->setProperties(d->client->logLevel( LogFacility::Console, LogSource::Client), LogFacility::Console, LogSource::Client); - ui.cbLogLevelServer->setProperties(d->client->logLevel( LogFacility::Remote, LogSource::Server), LogFacility::Remote, LogSource::Server); - ui.cbLogLevelServerFile->setProperties( (LogLevel)-1, LogFacility::File, LogSource::Server); // unknown level at startup - ui.cbLogLevelServerConsole->setProperties( (LogLevel)-1, LogFacility::Console, LogSource::Server); // unknown level at startup - // Since the LogLevelComboBox types store the facility and source properties (which we just set), we can use one event handler for all of them. - auto setLogLevel = [=](LogLevel level) { - if (LogLevelComboBox *cb = qobject_cast(sender())) - d->client->setLogLevel(level, cb->facility(), cb->source()); - }; - connect(ui.cbLogLevelCallback, &LogLevelComboBox::levelChanged, this, setLogLevel); - connect(ui.cbLogLevelFile, &LogLevelComboBox::levelChanged, this, setLogLevel); - connect(ui.cbLogLevelConsole, &LogLevelComboBox::levelChanged, this, setLogLevel); - connect(ui.cbLogLevelServer, &LogLevelComboBox::levelChanged, this, setLogLevel); - connect(ui.cbLogLevelServerFile, &LogLevelComboBox::levelChanged, this, setLogLevel); - connect(ui.cbLogLevelServerConsole, &LogLevelComboBox::levelChanged, this, setLogLevel); - - // Connect our own signals for client callback handling in a thread-safe manner, marshaling back to GUI thread as needed. - // The "signal" methods are "emitted" by the Client as callbacks, registered in Private::setupClient(). - connect(this, &WASimUI::clientEvent, this, &WASimUI::onClientEvent, Qt::QueuedConnection); - connect(this, &WASimUI::listResults, this, &WASimUI::onListResults, Qt::QueuedConnection); - // Data updates can go right to the requests model. - connect(this, &WASimUI::dataResultReady, d->reqModel, &RequestsModel::setRequestValue, Qt::QueuedConnection); - // Log messages can go right to the log records model. Log messages may arrive at any time, possibly from different threads, - // so placing them into a model allow proper sorting (and filtering). - connect(this, &WASimUI::logMessageReady, d->logModel, &LogRecordsModel::addRecord, Qt::QueuedConnection); - d->logUiMessage("Hello!", CommandId::Ack, LogLevel::Info); + + // connect to requests model row removed to check if the current editor needs to be reset, otherwise the "Save" button stays active and re-adds a deleted request. + connect(d->reqModel, &RequestsModel::rowsAboutToBeRemoved, this, [this](const QModelIndex &, int first, int last) { + const int current = d->reqModel->findRequestRow(ui.wRequestForm->property("requestId").toInt()); + if (current >= first && current <= last) + d->setRequestFormId(-1); + }); // Set up actions for triggering various events. Actions are typically mapped to UI elements like buttons and menu items and can be reused in multiple places. + // Network connection actions + + // Toggle overall connection, both the SimConnect part and the WASimModule part. + QIcon connIco = GLYPH_ICON("link"); + connIco.addFile(GLYPH_STR("link_off"), QSize(), QIcon::Mode::Normal, QIcon::State::On); + d->toggleConnAct = new QAction(connIco, tr("Connect"), this); + d->toggleConnAct->setToolTip(tr("

Toggle connection to WASimModule Server. This affects both the simulator connection (SimConnect) and the main server.

" + "

Use the sub-menu for individual actions.

")); + d->toggleConnAct->setCheckable(true); + d->toggleConnAct->setShortcut(QKeySequence(Qt::Key_F2)); + connect(d->toggleConnAct, &QAction::triggered, this, [this]() { d->toggleConnection(true, true); }, Qt::QueuedConnection); + // Connect/Disconnect Simulator. - QIcon initIco(QStringLiteral("phone_in_talk.glyph")); - initIco.addFile(QStringLiteral("call_end.glyph"), QSize(), QIcon::Mode::Normal, QIcon::State::On); + QIcon initIco = GLYPH_ICON("phone_in_talk"); + initIco.addFile(GLYPH_STR("call_end"), QSize(), QIcon::Mode::Normal, QIcon::State::On); d->initAct = new QAction(initIco, tr("Connect to Simulator"), this); d->initAct->setCheckable(true); - connect(d->initAct, &QAction::triggered, this, [this]() { - if (d->client->isInitialized()) - d->client->disconnectSimulator(); - else - d->client->connectSimulator(); - }, Qt::QueuedConnection); + d->initAct->setShortcut(QKeySequence(Qt::Key_F5)); + connect(d->initAct, &QAction::triggered, this, [this]() { d->toggleConnection(true, false); }, Qt::QueuedConnection); // Connect/Disconnect WASim Server - QIcon connIco(QStringLiteral("link.glyph")); - connIco.addFile(QStringLiteral("link_off.glyph"), QSize(), QIcon::Mode::Normal, QIcon::State::On); d->connectAct = new QAction(connIco, tr("Connect to Server"), this); d->connectAct->setCheckable(true); - connect(d->connectAct, &QAction::triggered, this, [this]() { - if (d->client->isConnected()) - d->client->disconnectServer(); - else - d->client->connectServer(); - }, Qt::QueuedConnection); + d->connectAct->setShortcut(QKeySequence(Qt::Key_F6)); + connect(d->connectAct, &QAction::triggered, this, [this]() { d->toggleConnection(false, true); }, Qt::QueuedConnection); // Ping the server. - QAction *pingAct = new QAction(QIcon(QStringLiteral("leak_add.glyph")), tr("Ping Server"), this); - connect(pingAct, &QAction::triggered, this, [this]() { d->client->pingServer(); }); + QAction *pingAct = new QAction(GLYPH_ICON("leak_add"), tr("Ping Server"), this); + pingAct->setShortcut(QKeySequence(Qt::Key_F7)); + connect(pingAct, &QAction::triggered, this, [this]() { d->pingServer(); }); + // Sub-menu for individual connection actions. + QMenu *connectMenu = new QMenu(tr("Connection actions"), this); + connectMenu->setIcon(GLYPH_ICON("settings_remote")); + connectMenu->addActions({ d->initAct, pingAct, d->connectAct }); + //d->toggleConnAct->setMenu(connectMenu); // Calculator code actions // Exec calculator code - QAction *execCalcAct = new QAction(QIcon(QStringLiteral("IcoMoon-Free/calculator.glyph")), tr("Execute Calculator Code"), this); - execCalcAct->setToolTip(tr("Execute Calculator Code")); - execCalcAct->setDisabled(true); - connect(execCalcAct, &QAction::triggered, this, [this]() { d->runCalcCode(); }); - ui.btnCalc->setDefaultAction(execCalcAct); - + MAKE_ACTION_SC_D(execCalcAct, tr("Execute Calculator Code"), "IcoMoon-Free/calculator", btnCalc, wCalcForm, runCalcCode(), tr("Execute Calculator Code."), QKeySequence(Qt::ControlModifier | Qt::Key_Return)); // Register calculator code event - QAction *regEventAct = new QAction(QIcon(QStringLiteral("control_point.glyph")), tr("Register Event"), this); - regEventAct->setToolTip(tr("Register this calculator code as a new Event.")); - regEventAct->setDisabled(true); - connect(regEventAct, &QAction::triggered, this, [this]() { d->registerEvent(false); }); - ui.btnAddEvent->setDefaultAction(regEventAct); - + MAKE_ACTION_D(regEventAct, tr("Register Event"), "control_point", btnAddEvent, wCalcForm, registerEvent(false), tr("Register this calculator code as a new Event.")); // Save edited calculator code event - QAction *saveEventAct = new QAction(QIcon(QStringLiteral("edit.glyph")), tr("Update Event"), this); - saveEventAct->setToolTip(tr("Update existing event with new calculator code (name cannot be changed).")); - connect(saveEventAct, &QAction::triggered, this, [this]() { d->registerEvent(true); }); - saveEventAct->setDisabled(true); - ui.btnUpdateEvent->setDefaultAction(saveEventAct); - ui.btnUpdateEvent->setVisible(false); - + MAKE_ACTION_D(saveEventAct, tr("Update Event"), "edit", btnUpdateEvent, wCalcForm, registerEvent(true), tr("Update existing event with new calculator code (name cannot be changed).")); // Copy calculator code as new Data Request - QAction *copyCalcAct = new QAction(QIcon(QStringLiteral("move_to_inbox.glyph")), tr("Copy to Data Request"), this); - copyCalcAct->setToolTip(tr("Copy Calculator Code to new Data Request")); - copyCalcAct->setDisabled(true); - connect(copyCalcAct, &QAction::triggered, this, [this]() { d->copyCalcCodeToRequest(); }); - ui.btnCopyCalcToRequest->setDefaultAction(copyCalcAct); - - // Connect variable selector to enable/disable relevant actions - connect(ui.cbCalculatorCode, &QComboBox::currentTextChanged, this, [=](const QString &txt) { - const bool en = !txt.isEmpty(); - execCalcAct->setEnabled(en); - regEventAct->setEnabled(en); - saveEventAct->setEnabled(en); - copyCalcAct->setEnabled(en); - }); - + MAKE_ACTION_SC_D(copyCalcAct, tr("Copy to Data Request"), "move_to_inbox", btnCopyCalcToRequest, wCalcForm, copyCalcCodeToRequest(), + tr("Copy Calculator Code to new Data Request."), QKeySequence(Qt::ControlModifier | Qt::Key_Down)); // Variables section actions // Request Local Vars list - QAction *reloadLVarsAct = new QAction(QIcon(QStringLiteral("autorenew.glyph")), tr("Reload L.Vars"), this); - reloadLVarsAct->setToolTip(tr("Reload Local Variables")); - connect(reloadLVarsAct, &QAction::triggered, this, [this]() { d->refreshLVars(); }); - ui.btnList->setDefaultAction(reloadLVarsAct); - - // Lookup a variable/unit ID - QAction *lookupItemAct = new QAction(QIcon(QStringLiteral("search.glyph")), tr("Lookup"), this); - lookupItemAct->setToolTip(tr("Query server for ID of named item (Lookup command).")); - connect(lookupItemAct, &QAction::triggered, this, [this]() { d->lookupItem(); }); - ui.btnVarLookup->setDefaultAction(lookupItemAct); - + MAKE_ACTION(reloadLVarsAct, tr("Reload L.Vars"), "autorenew", btnList, wVariables, refreshLVars(), tr("Reload Local Variables.")); // Get local variable value - QAction *getVarAct = new QAction(QIcon(QStringLiteral("rotate=180/send.glyph")), tr("Get Variable"), this); - getVarAct->setToolTip(tr("Get Variable Value.")); - getVarAct->setDisabled(true); - connect(getVarAct, &QAction::triggered, this, [this]() { d->getLocalVar(); }); - ui.btnGetVar->setDefaultAction(getVarAct); - + MAKE_ACTION_SC_D(getVarAct, tr("Get Variable"), "rotate=180/send", btnGetVar, wVariables, getLocalVar(), tr("Get Variable Value."), QKeySequence(Qt::ControlModifier | Qt::Key_Return)); // Set variable value - QAction *setVarAct = new QAction(QIcon(QStringLiteral("send.glyph")), tr("Set Variable"), this); - setVarAct->setToolTip(tr("Set Variable Value.")); - setVarAct->setDisabled(true); - connect(setVarAct, &QAction::triggered, this, [this]() { d->setLocalVar(); }); - ui.btnSetVar->setDefaultAction(setVarAct); - + MAKE_ACTION_SC_D(setVarAct, tr("Set Variable"), "send", btnSetVar, wVariables, setLocalVar(), tr("Set Variable Value."), QKeySequence(Qt::ControlModifier | Qt::ShiftModifier | Qt::Key_Return)); // Set or Create local variable - QAction *setCreateVarAct = new QAction(QIcon(QStringLiteral("overlay=\\align=AlignRight\\scale=.95\\fg=#17dde8\\add/send.glyph")), tr("Set/Create Variable"), this); - setCreateVarAct->setToolTip(tr("Set Or Create Local Variable.")); - setCreateVarAct->setDisabled(true); - connect(setCreateVarAct, &QAction::triggered, this, [this]() { d->setLocalVar(true); }); - ui.btnSetCreate->setDefaultAction(setCreateVarAct); - + MAKE_ACTION_D(setCreateVarAct, tr("Set/Create Variable"), "overlay=\\align=AlignRight\\fg=#17dd29\\add/send", btnSetCreate, wVariables, setLocalVar(true), + tr("Set Or Create Local Variable.")); + // Get or Create local variable + MAKE_ACTION_D(getCreateVarAct, tr("Get/Create Variable"), "overlay=\\align=AlignLeft\\fg=#17dd29\\add/rotate=180/send", btnGetCreate, wVariables, getLocalVar(true), + tr("Get Or Create Local Variable. The specified value and unit will be used as defaults if the variable is created.")); // Copy LVar as new Data Request - QAction *copyVarAct = new QAction(QIcon(QStringLiteral("move_to_inbox.glyph")), tr("Copy to Data Request"), this); - copyVarAct->setToolTip(tr("Copy Variable to new Data Request")); - copyVarAct->setDisabled(true); - connect(copyVarAct, &QAction::triggered, this, [this]() { d->copyLocalVarToRequest(); }); - ui.btnCopyLVarToRequest->setDefaultAction(copyVarAct); - - auto updateLocalVarsFormState = [=](const QString &) { - const bool en = (ui.wLocalVarsForm->isVisible() && !ui.cbLvars->currentText().isEmpty()) || - (ui.wOtherVarsForm->isVisible() && !ui.cbVariableName->currentText().isEmpty()); - getVarAct->setEnabled(en); - setVarAct->setEnabled(en); - setCreateVarAct->setEnabled(en); - copyVarAct->setEnabled(en); - }; - - // Connect variable selector to enable/disable relevant actions - connect(ui.cbLvars, &QComboBox::currentTextChanged, this, updateLocalVarsFormState); - connect(ui.cbVariableName, &QComboBox::currentTextChanged, this, updateLocalVarsFormState); - - // connect to variable type combo box to switch between views for local vars vs. everything else - connect(ui.cbGetSetVarType, &DataComboBox::currentDataChanged, this, [=](const QVariant &vtype) { - bool isLocal = vtype.toChar() == 'L'; - ui.wLocalVarsForm->setVisible(isLocal); - ui.wOtherVarsForm->setVisible(!isLocal); - ui.wGetSetSimVarIndex->setVisible(!isLocal && vtype.toChar() == 'A'); // sim var index box visible only for... simvars! - ui.btnSetCreate->setVisible(isLocal); - bool hasUnit = ui.cbGetSetVarType->currentText().contains('*'); - ui.cbSetVarUnitName->setVisible(hasUnit); - ui.lblSetVarUnit->setVisible(hasUnit); - updateLocalVarsFormState(QString()); - }); - - - // Send Key Event action - QAction *sendKeyEventAct = new QAction(QIcon(QStringLiteral("send.glyph")), tr("Send Key Event"), this); - sendKeyEventAct->setToolTip(tr("Send the specified Key Event to the server.")); - connect(sendKeyEventAct, &QAction::triggered, this, [this]() { d->sendKeyEventForm(); }); - ui.btnKeyEventSend->setDefaultAction(sendKeyEventAct); - + MAKE_ACTION_SC_D(copyVarAct, tr("Copy to Data Request"), "move_to_inbox", btnCopyLVarToRequest, wVariables, copyLocalVarToRequest(), tr("Copy Variable to new Data Request."), + QKeySequence(Qt::ControlModifier | Qt::Key_Down)); + // Open docs import browser for Sim Vars + MAKE_ACTION_SC(findSimVarAct, tr("Sim Var Lookup"), "search", btnFindSimVar, wVariables, openDocsLookup(DocImports::RecordType::SimVars, ui.cbVariableName), + tr("Open a new window to search and select Simulator Variables from imported MSFS SDK documentation."), QKeySequence::Find); + ui.btnFindSimVar->setVisible(false); // hide the button, we put the action into the combo box for now + + // Lookup action + MAKE_ACTION_SC(lookupItemAct, tr("Lookup"), "search", btnVarLookup, wDataLookup, lookupItem(), tr("Query server for ID of named item (Lookup command)."), QKeySequence(Qt::ControlModifier | Qt::Key_Return)); + + // Send Key Event Form + MAKE_ACTION_SC(sendKeyEventAct, tr("Send Key Event"), "send", btnKeyEventSend, wKeyEvent, sendKeyEventForm(), tr("Send the specified Key Event to the server."), QKeySequence(Qt::ControlModifier | Qt::Key_Return)); + // Open docs import browser for Key Events + MAKE_ACTION_SC(findKeyEventAct, tr("Key Event Lookup"), "search", btnFindEvent, wKeyEvent, openDocsLookup(DocImports::RecordType::KeyEvents, ui.cbKeyEvent), + tr("Open a new window to search and select Key Events from imported MSFS SDK documentation."), QKeySequence::Find); + ui.cbKeyEvent->lineEdit()->addAction(findKeyEventAct, QLineEdit::TrailingPosition); + ui.btnFindEvent->setHidden(true); // hide the button, we put the action into the combo box for now // Send Command action - QAction *sendCmdAct = new QAction(QIcon(QStringLiteral("keyboard_command_key.glyph")), tr("Send Command"), this); - sendCmdAct->setToolTip(tr("Send the selected Command to the server.")); - connect(sendCmdAct, &QAction::triggered, this, [this]() { d->sendCommandForm(); }); - ui.btnCmdSend->setDefaultAction(sendCmdAct); + MAKE_ACTION_SC(sendCmdAct, tr("Send Command"), "keyboard_command_key", btnCmdSend, wCommand, sendCommandForm(), tr("Send the selected Command to the server."), QKeySequence(Qt::ControlModifier | Qt::Key_Return)); + // Requests editor form and model view actions - // Requests model view actions + // connect the Data Request save/add buttons + MAKE_ACTION_PB(addReqAct, tr("Add Request"), tr("Add"), "add", btnAddRequest, wRequestForm, handleRequestForm(false), + tr("Add new request record from current form entries."), QKeySequence(Qt::ControlModifier | Qt::Key_Return)); + MAKE_ACTION_PB_D(saveReqAct, tr("Save Edited Request"), tr("Save"), "edit", btnUpdateRequest, wRequestForm, handleRequestForm(true), + tr("Update the existing request record from current form entries."), QKeySequence(Qt::ControlModifier | Qt::ShiftModifier | Qt::Key_Return)); + MAKE_ACTION_PB(updReqAct, tr("Clear Form"), tr("Clear"), "scale=.9/backspace", btnClearRequest, wRequestForm, clearRequestForm(), + tr("Reset the editor form to default values."), QKeySequence(Qt::ControlModifier | Qt::Key_Backspace)); + + // Open docs import browser for Sim Vars + MAKE_ACTION_SC(findReqSimVarAct, tr("Sim Var Lookup"), "search", btnReqFindSimVar, wRequestForm, openDocsLookup(DocImports::RecordType::SimVars, ui.cbNameOrCode), + tr("Open a new window to search and select Simulator Variables from imported MSFS SDK documentation."), QKeySequence::Find); + ui.btnReqFindSimVar->setVisible(false); // hide the button, we put the action into the combo box for now // Remove selected Data Request(s) from item model/view - QAction *removeRequestsAct = new QAction(QIcon(QStringLiteral("delete_forever.glyph")), tr("Remove Selected Data Request(s)"), this); - removeRequestsAct->setIconText(tr("Remove")); - removeRequestsAct->setToolTip(tr("Delete the selected Data Request(s).")); - removeRequestsAct->setDisabled(true); - ui.pbReqestsRemove->setDefaultAction(removeRequestsAct); - connect(removeRequestsAct, &QAction::triggered, this, [this]() { d->removeSelectedRequests(); }); - + MAKE_ACTION_PB_D(removeRequestsAct, tr("Remove Selected Data Request(s)"), tr("Remove"), "fg=#c2d32e2e/delete_forever", btnReqestsRemove, wRequests, removeSelectedRequests(), + tr("Delete the selected Data Request(s)."), QKeySequence(Qt::ControlModifier | Qt::Key_D)); // Update data of selected Data Request(s) in item model/view - QAction *updateRequestsAct = new QAction(QIcon(QStringLiteral("refresh.glyph")), tr("Update Selected Data Request(s)"), this); - updateRequestsAct->setIconText(tr("Update")); - updateRequestsAct->setToolTip(tr("Request data update on selected Data Request(s).")); - updateRequestsAct->setDisabled(true); - ui.pbReqestsUpdate->setDefaultAction(updateRequestsAct); - connect(updateRequestsAct, &QAction::triggered, this, [this]() { d->updateSelectedRequests(); }); - - // Connect to table view selection model to en/disable the remove/update actions when selection changes. - connect(ui.requestsView->selectionModel(), &QItemSelectionModel::selectionChanged, this, [=](const QItemSelection &sel, const QItemSelection &) { - removeRequestsAct->setDisabled(sel.isEmpty()); - updateRequestsAct->setDisabled(sel.isEmpty()); - }); + MAKE_ACTION_PB_D(updateRequestsAct, tr("Update Selected Data Request(s)"), tr("Update"), "refresh", btnReqestsUpdate, wRequests, updateSelectedRequests(), + tr("Request data update on selected Data Request(s)."), QKeySequence(Qt::ControlModifier | Qt::Key_R, QKeySequence::Refresh)); // Pause/resume data updates of requests - QIcon dataPauseIcon(QStringLiteral("pause.glyph")); - dataPauseIcon.addFile(QStringLiteral("play_arrow.glyph"), QSize(), QIcon::Normal, QIcon::On); - //dataPauseIcon.addFile(QStringLiteral("play_arrow.glyph"), QSize(), QIcon::Active, QIcon::On); - QAction *pauseRequestsAct = new QAction(dataPauseIcon, tr("Toggle Updates"), this); - pauseRequestsAct->setIconText(tr("Suspend")); - pauseRequestsAct->setToolTip(tr("Temporarily pause all data value updates on Server side.")); + MAKE_ACTION_PB_D(pauseRequestsAct, tr("Toggle Updates"), tr("Suspend"), "pause", btnReqestsPause, wRequests, pauseRequests(chk), + tr("Temporarily pause all data value updates on Server side."), QKeySequence(Qt::ControlModifier | Qt::Key_U)); pauseRequestsAct->setCheckable(true); - pauseRequestsAct->setDisabled(true); - ui.pbReqestsPause->setDefaultAction(pauseRequestsAct); - connect(pauseRequestsAct, &QAction::triggered, this, [=](bool chk) { - static const QIcon dataResumeIcon(QStringLiteral("play_arrow.glyph")); - d->client->setDataRequestsPaused(chk); - pauseRequestsAct->setIconText(chk ? tr("Resume") : tr("Suspend")); - // for some reason the checked icon "on" state doesn't work automatically like it should... - ui.pbReqestsPause->setIcon(chk ? dataResumeIcon : dataPauseIcon); - }); // Save current Requests to a file - QAction *saveRequestsAct = new QAction(QIcon(QStringLiteral("save.glyph")), tr("Save Requests"), this); - saveRequestsAct->setIconText(tr("Save")); - saveRequestsAct->setToolTip(tr("Save current Requests list to file.")); + MAKE_ACTION_PB_NC(saveRequestsAct, tr("Save Requests"), tr("Save"), "save", btnReqestsSave, wRequests, tr("Save requests to a file for later use or bring up a dialog with export options.")); saveRequestsAct->setDisabled(true); - ui.pbReqestsSave->setDefaultAction(saveRequestsAct); - connect(saveRequestsAct, &QAction::triggered, this, [this]() { d->saveRequests(); }); + QMenu *saveRequestsMenu = new QMenu(tr("Requests Save Action"), this); + saveRequestsMenu->addAction(GLYPH_ICON("keyboard_command_key"), tr("In native WASimUI format"), this, [this]() { d->saveRequests(false); }, QKeySequence::Save); + saveRequestsMenu->addAction(GLYPH_ICON("overlay=align=AlignRight\\fg=#5eb5ff\\arrow_outward/touch_app"), tr("Export for Touch Portal Plugin with Editor..."), + this, [this]() { d->exportRequests(); }, QKeySequence(Qt::ControlModifier | Qt::ShiftModifier | Qt::Key_S)); + saveRequestsMenu->addAction(GLYPH_ICON("touch_app"), tr("Export for Touch Portal Plugin Directly"), + this, [this]() { d->saveRequests(true); }, QKeySequence(Qt::ControlModifier | Qt::ShiftModifier | Qt::AltModifier | Qt::Key_S)); + saveRequestsAct->setMenu(saveRequestsMenu); // Load Requests from a file. This is actually two actions: load and append to existing records + load and replace existing records. - QAction *loadRequestsAct = new QAction(QIcon(QStringLiteral("folder_open.glyph")), tr("Load Requests"), this); - loadRequestsAct->setIconText(tr("Load")); - loadRequestsAct->setToolTip(tr("Load saved Requests from file.")); + MAKE_ACTION_PB_NC(loadRequestsAct, tr("Load Requests"), tr("Load"), "folder_open", btnReqestsLoad, wRequests, + tr("

Load or Import Requests from a file.

Files can be in \"native\" WASimUI or MSFS Touch Portal Plugin formats. File type is detected automatically.

")); + loadRequestsAct->setShortcut(QKeySequence::Open); QMenu *loadRequestsMenu = new QMenu(tr("Requests Load Action"), this); - QAction *loadReplaceAct = loadRequestsMenu->addAction(QIcon(QStringLiteral("view_list.glyph")), tr("Replace Existing")); - QAction *loadAppendAct = loadRequestsMenu->addAction(QIcon(QStringLiteral("playlist_add.glyph")), tr("Append to Existing")); - ui.pbReqestsLoad->setDefaultAction(loadRequestsAct); - connect(loadReplaceAct, &QAction::triggered, this, [this]() { d->loadRequests(true); }); - connect(loadAppendAct, &QAction::triggered, this, [this]() { d->loadRequests(false); }); + loadRequestsMenu->addAction(GLYPH_ICON("view_list"), tr("Replace Existing"), this, [this]() { d->loadRequests(true); }, QKeySequence(Qt::ControlModifier | Qt::ShiftModifier | Qt::Key_O)); + loadRequestsMenu->addAction(GLYPH_ICON("playlist_add."), tr("Append to Existing"), this, [this]() { d->loadRequests(false); }, QKeySequence(Qt::ControlModifier | Qt::AltModifier | Qt::Key_O)); connect(loadRequestsAct, &QAction::triggered, this, [=]() { if (!loadRequestsAct->menu()) d->loadRequests(true); }); - // Change the action type depending on number of current rows in data requests model, with or w/out a menu of Add/Replace Existing options. + // Change the load/save requests action type depending on number of current rows in data requests model, with or w/out a menu of Add/Replace Existing options. connect(d->reqModel, &RequestsModel::rowCountChanged, this, [=](int rows) { if (rows) { if (!loadRequestsAct->menu()) loadRequestsAct->setMenu(loadRequestsMenu); } - else if (loadRequestsAct->menu()) { + else if (loadRequestsAct->menu()) loadRequestsAct->setMenu(nullptr); - } - saveRequestsAct->setEnabled(rows > 0); - pauseRequestsAct->setEnabled(rows > 0); + d->toggleRequestButtonsState(); }, Qt::QueuedConnection); + // Add column toggle and font size actions to the parent widgets + ui.wRequestForm->addAction(ui.requestsView->actionsMenu(ui.wRequests)->menuAction()); + ui.wRequests->addAction(ui.requestsView->actionsMenu(ui.wRequests)->menuAction()); // Registered calculator events model view actions // Remove selected Data Request(s) from item model/view - QAction *removeEventsAct = new QAction(QIcon(QStringLiteral("delete_forever.glyph")), tr("Remove Selected Event(s)"), this); - removeEventsAct->setIconText(tr("Remove")); - removeEventsAct->setToolTip(tr("Delete the selected Event(s).")); - removeEventsAct->setDisabled(true); - ui.pbEventsRemove->setDefaultAction(removeEventsAct); - connect(removeEventsAct, &QAction::triggered, this, [this]() { d->removeSelectedEvents(); }); - + MAKE_ACTION_PB_D(removeEventsAct, tr("Remove Selected Event(s)"), tr("Remove"), "fg=#c2d32e2e/delete_forever", btnEventsRemove, wEventsList, removeSelectedEvents(), + tr("Delete the selected Event(s)."), QKeySequence(Qt::ControlModifier | Qt::Key_D)); // Update data of selected Data Request(s) in item model/view - QAction *updateEventsAct = new QAction(QIcon(QStringLiteral("rotate=180/play_for_work.glyph")), tr("Transmit Selected Event(s)"), this); - updateEventsAct->setIconText(tr("Transmit")); - updateEventsAct->setToolTip(tr("Trigger selected Event(s).")); - updateEventsAct->setDisabled(true); - ui.pbEventsTransmit->setDefaultAction(updateEventsAct); - connect(updateEventsAct, &QAction::triggered, this, [this]() { d->transmitSelectedEvents(); }); - - // Connect to table view selection model to en/disable the remove/update actions when selection changes. - connect(ui.eventsView->selectionModel(), &QItemSelectionModel::selectionChanged, this, [=](const QItemSelection &sel, const QItemSelection &) { - removeEventsAct->setDisabled(sel.isEmpty()); - updateEventsAct->setDisabled(sel.isEmpty()); - }); + MAKE_ACTION_PB_D(updateEventsAct, tr("Transmit Selected Event(s)"), tr("Transmit"), "play_for_work", btnEventsTransmit, wEventsList, transmitSelectedEvents(), + tr("Trigger the selected Event(s)."), QKeySequence(Qt::ControlModifier | Qt::Key_T)); // Save current Events to a file - QAction *saveEventsAct = new QAction(QIcon(QStringLiteral("save.glyph")), tr("Save Events"), this); - saveEventsAct->setIconText(tr("Save")); - saveEventsAct->setToolTip(tr("Save current Events list to file.")); - saveEventsAct->setDisabled(true); - ui.pbEventsSave->setDefaultAction(saveEventsAct); - connect(saveEventsAct, &QAction::triggered, this, [this]() { d->saveEvents(); }); - + MAKE_ACTION_PB_D(saveEventsAct, tr("Save Events"), tr("Save"), "save", btnEventsSave, wEventsList, saveEvents(), tr("Save current Events list to file."), QKeySequence::Save); // Load Events from a file. This is actually two actions: load and append to existing records + load and replace existing records. - QAction *loadEventsAct = new QAction(QIcon(QStringLiteral("folder_open.glyph")), tr("Load Events"), this); - loadEventsAct->setIconText(tr("Load")); - loadEventsAct->setToolTip(tr("Load saved Events from file.")); + MAKE_ACTION_PB_NC(loadEventsAct, tr("Load Events"), tr("Load"), "folder_open", btnEventsLoad, wEventsList, tr("Load saved Events from file.")); QMenu *loadEventsMenu = new QMenu(tr("Events Load Action"), this); - QAction *replaceEventsAct = loadEventsMenu->addAction(QIcon(QStringLiteral("view_list.glyph")), tr("Replace Existing")); - QAction *appendEventsAct = loadEventsMenu->addAction(QIcon(QStringLiteral("playlist_add.glyph")), tr("Append to Existing")); - ui.pbEventsLoad->setDefaultAction(loadEventsAct); + QAction *replaceEventsAct = loadEventsMenu->addAction(GLYPH_ICON("view_list"), tr("Replace Existing")); + QAction *appendEventsAct = loadEventsMenu->addAction(GLYPH_ICON("playlist_add"), tr("Append to Existing")); connect(replaceEventsAct, &QAction::triggered, this, [this]() { d->loadEvents(true); }); connect(appendEventsAct, &QAction::triggered, this, [this]() { d->loadEvents(false); }); connect(loadEventsAct, &QAction::triggered, this, [=]() { if (!loadEventsAct->menu()) d->loadEvents(true); }); @@ -1016,131 +1213,59 @@ WASimUI::WASimUI(QWidget *parent) : if (!loadEventsAct->menu()) loadEventsAct->setMenu(loadEventsMenu); } - else if (loadEventsAct->menu()) { + else if (loadEventsAct->menu()) loadEventsAct->setMenu(nullptr); - } saveEventsAct->setEnabled(rows > 0); }, Qt::QueuedConnection); - - - // Logging window actions - - QAction *filterErrorsAct = new QAction(QIcon(Utils::iconNameForLogLevel(LogLevel::Error)), tr("Toggle Errors"), this); - filterErrorsAct->setToolTip(tr("Toggle visibility of Error-level log messages.")); - filterErrorsAct->setCheckable(true); - filterErrorsAct->setChecked(true); - ui.btnLogFilt_ERR->setDefaultAction(filterErrorsAct); - connect(filterErrorsAct, &QAction::triggered, this, [this](bool en) { d->logModel->setLevelFilter(LogLevel::Error, !en); }); - - QAction *filterWarningsAct = new QAction(QIcon(Utils::iconNameForLogLevel(LogLevel::Warning)), tr("Toggle Warnings"), this); - filterWarningsAct->setToolTip(tr("Toggle visibility of Warning-level log messages.")); - filterWarningsAct->setCheckable(true); - filterWarningsAct->setChecked(true); - ui.btnLogFilt_WRN->setDefaultAction(filterWarningsAct); - connect(filterWarningsAct, &QAction::triggered, this, [this](bool en) { d->logModel->setLevelFilter(LogLevel::Warning, !en); }); - - QAction *filterInfoAct = new QAction(QIcon(Utils::iconNameForLogLevel(LogLevel::Info)), tr("Toggle Info"), this); - filterInfoAct->setToolTip(tr("Toggle visibility of Information-level log messages.")); - filterInfoAct->setCheckable(true); - filterInfoAct->setChecked(true); - ui.btnLogFilt_INF->setDefaultAction(filterInfoAct); - connect(filterInfoAct, &QAction::triggered, this, [this](bool en) { d->logModel->setLevelFilter(LogLevel::Info, !en); }); - - QAction *filterDebugAct = new QAction(QIcon(Utils::iconNameForLogLevel(LogLevel::Debug)), tr("Toggle Debug"), this); - filterDebugAct->setToolTip(tr("Toggle visibility of Debug-level log messages.")); - filterDebugAct->setCheckable(true); - filterDebugAct->setChecked(true); - ui.btnLogFilt_DBG->setDefaultAction(filterDebugAct); - connect(filterDebugAct, &QAction::triggered, this, [this](bool en) { d->logModel->setLevelFilter(LogLevel::Debug, !en); }); - - QAction *filterTraceAct = new QAction(QIcon(Utils::iconNameForLogLevel(LogLevel::Trace)), tr("Toggle Traces"), this); - filterTraceAct->setToolTip(tr("Toggle visibility of Trace-level log messages.")); - filterTraceAct->setCheckable(true); - filterTraceAct->setChecked(true); - ui.btnLogFilt_TRC->setDefaultAction(filterTraceAct); - connect(filterTraceAct, &QAction::triggered, this, [this](bool en) { d->logModel->setLevelFilter(LogLevel::Trace, !en); }); - - QAction *filterServerAct = new QAction(QIcon(Utils::iconNameForLogSource(+LogSource::Server)), tr("Toggle Server Records"), this); - filterServerAct->setToolTip(tr("Toggle visibility of log messages from Server.")); - filterServerAct->setCheckable(true); - filterServerAct->setChecked(true); - ui.btnLogFilt_Server->setDefaultAction(filterServerAct); - connect(filterServerAct, &QAction::triggered, this, [this](bool en) { d->logModel->setSourceFilter(+LogSource::Server, !en); }); - - QAction *filterClientAct = new QAction(QIcon(Utils::iconNameForLogSource(+LogSource::Client)), tr("Toggle Client Records"), this); - filterClientAct->setToolTip(tr("Toggle visibility of log messages from Client.")); - filterClientAct->setCheckable(true); - filterClientAct->setChecked(true); - ui.btnLogFilt_Client->setDefaultAction(filterClientAct); - connect(filterClientAct, &QAction::triggered, this, [this](bool en) { d->logModel->setSourceFilter(+LogSource::Client, !en); }); - - QAction *filterUILogAct = new QAction(QIcon(Utils::iconNameForLogSource(+LogRecordsModel::LogSource::UI)), tr("Toggle UI Records"), this); - filterUILogAct->setToolTip(tr("Toggle visibility of log messages from this UI.")); - filterUILogAct->setCheckable(true); - filterUILogAct->setChecked(true); - ui.btnLogFilt_UI->setDefaultAction(filterUILogAct); - connect(filterUILogAct, &QAction::triggered, this, [this](bool en) { d->logModel->setSourceFilter(+LogRecordsModel::LogSource::UI, !en); }); - - QIcon logPauseIcon(QStringLiteral("pause.glyph")); - logPauseIcon.addFile(QStringLiteral("play_arrow.glyph"), QSize(), QIcon::Normal, QIcon::On); - QAction *pauseLogScrollAct = new QAction(logPauseIcon, tr("Pause Log Scroll"), this); - pauseLogScrollAct->setToolTip(tr("

Toggle scrolling of the log window. Scrolling can also be paused by selecting a log entry row.

")); - pauseLogScrollAct->setCheckable(true); - ui.btnLogPause->setDefaultAction(pauseLogScrollAct); - connect(pauseLogScrollAct, &QAction::triggered, this, [this](bool en) { if (!en) ui.logView->selectionModel()->clear(); }); // clear log view selection on "un-pause" - - QAction *clearLogWindowAct = new QAction(QIcon(QStringLiteral("delete.glyph")), tr("Clear Log Window"), this); - clearLogWindowAct->setToolTip(tr("Clear the log window.")); - ui.btnLogClear->setDefaultAction(clearLogWindowAct); - connect(clearLogWindowAct, &QAction::triggered, this, [this]() { d->logModel->clear(); }); - - QIcon wordWrapIcon(QStringLiteral("wrap_text.glyph")); - wordWrapIcon.addFile(QStringLiteral("notes.glyph"), QSize(), QIcon::Normal, QIcon::On); - QAction *wordWrapLogWindowAct = new QAction(wordWrapIcon, tr("Word Wrap"), this); - wordWrapLogWindowAct->setToolTip(tr("Toggle word wrapping of the log window.")); - wordWrapLogWindowAct->setCheckable(true); - wordWrapLogWindowAct->setChecked(true); - ui.btnLogWordWrap->setDefaultAction(wordWrapLogWindowAct); - connect(wordWrapLogWindowAct, &QAction::toggled, this, [this](bool chk) { ui.logView->setWordWrap(chk); ui.logView->resizeRowsToContents(); }); - - // connect the log model record added signal to make sure last record remains in view, unless scroll lock is enabled - connect(d->logModel, &LogRecordsModel::recordAdded, this, [=](const QModelIndex &i) { - // make sure log view scroll to bottom on insertions, unless a row is selected or scroll pause is set. - ui.logView->resizeRowToContents(i.row()); - if (!pauseLogScrollAct->isChecked() && !ui.logView->selectionModel()->hasSelection()) - ui.logView->scrollToBottom(/*i, QAbstractItemView::PositionAtBottom*/); - }); - // connect log viewer selection model to show pause button active while there is a selection - connect(ui.logView->selectionModel(), &QItemSelectionModel::selectionChanged, this, [=](const QItemSelection &sel, const QItemSelection&) { - pauseLogScrollAct->setChecked(!sel.isEmpty()); - }); + // Add column toggle and font size actions to the parent widget + ui.wEventsList->addAction(ui.eventsView->actionsMenu(ui.wRequests)->menuAction()); // Other UI-related actions - QAction *viewAct = new QAction(QIcon(QStringLiteral("grid_view.glyph")), tr("View"), this); QMenu *viewMenu = new QMenu(tr("View"), this); + viewMenu->setIcon(GLYPH_ICON("grid_view")); + //viewMenu->menuAction()->setShortcut(QKeySequence(Qt::AltModifier | Qt::Key_M)); + + MAKE_ACTION_NW_SC(docBrowserAct, tr("SimConnect SDK Docs Reference Browser"), tr("Reference"), "search", openDocsLookupWindow(), + tr("

Opens a window which allow searching through Simulator Variables, Key Events, and Unit types imported from online SimConnect SDK documentation.

"), + QKeySequence(Qt::AltModifier | Qt::Key_R)); + viewMenu->addAction(docBrowserAct); + +#define WIDGET_VIEW_TOGGLE_ACTION(T, W, V, K) {\ + QAction *act = new QAction(tr("Show %1 Form").arg(T), this); \ + act->setAutoRepeat(false); act->setCheckable(true); act->setChecked(V); \ + act->setShortcut(QKeySequence(Qt::AltModifier | Qt::Key_##K)); \ + W->addAction(act); W->setWindowTitle(T); W->setVisible(V); \ + connect(act, &QAction::toggled, W, &QWidget::setVisible); \ + d->formWidgets.append({T, W, act}); viewMenu->addAction(act); \ + } + WIDGET_VIEW_TOGGLE_ACTION(tr("Calculator Code"), ui.wCalcForm, true, C) + WIDGET_VIEW_TOGGLE_ACTION(tr("Variables"), ui.wVariables, true, V) + WIDGET_VIEW_TOGGLE_ACTION(tr("Lookup"), ui.wDataLookup, true, L) + WIDGET_VIEW_TOGGLE_ACTION(tr("Key Events"), ui.wKeyEvent, true, K) + WIDGET_VIEW_TOGGLE_ACTION(tr("API Command"), ui.wCommand, false, A) + WIDGET_VIEW_TOGGLE_ACTION(tr("Data Request Editor"), ui.wRequestForm, true, R) +#undef WIDGET_VIEW_TOGGLE_ACTION + viewMenu->addActions({ ui.dwRequests->toggleViewAction(), ui.dwEventsList->toggleViewAction(), ui.dwLog->toggleViewAction() }); - viewAct->setMenu(viewMenu); - QAction *styleAct = new QAction(QIcon(QStringLiteral("style.glyph")), tr("Toggle Dark/Light Theme"), this); + QAction *styleAct = new QAction(GLYPH_ICON("style"), tr("Toggle Dark/Light Theme"), this); + styleAct->setIconText(tr("Theme")); styleAct->setCheckable(true); styleAct->setShortcut(tr("Alt+D")); connect(styleAct, &QAction::triggered, &Utils::toggleAppStyle); - QAction *aboutAct = new QAction(QIcon(QStringLiteral("info.glyph")), tr("About"), this); + QAction *aboutAct = new QAction(GLYPH_ICON("info"), tr("About"), this); aboutAct->setShortcut(QKeySequence::HelpContents); connect(aboutAct, &QAction::triggered, this, [this]() {Utils::about(this); }); - QAction *projectLinkAct = new QAction(QIcon(QStringLiteral("IcoMoon-Free/github.glyph")), tr("Project Site"), this); + QAction *projectLinkAct = new QAction(GLYPH_ICON("IcoMoon-Free/github"), tr("Project Site"), this); connect(projectLinkAct, &QAction::triggered, this, [this]() { QDesktopServices::openUrl(QUrl(WSMCMND_PROJECT_URL)); }); // add all actions to this widget, for context menu and shortcut handling addActions({ - d->initAct, pingAct, d->connectAct, - Utils::separatorAction(this), removeRequestsAct, updateRequestsAct, saveRequestsAct, loadRequestsAct, - Utils::separatorAction(this), removeEventsAct, updateEventsAct, saveEventsAct, loadEventsAct, - Utils::separatorAction(this), pauseLogScrollAct, clearLogWindowAct, wordWrapLogWindowAct, - Utils::separatorAction(this), viewAct, styleAct, aboutAct, projectLinkAct + d->toggleConnAct, connectMenu->menuAction(), + Utils::separatorAction(this), docBrowserAct, viewMenu->menuAction(), styleAct, aboutAct, projectLinkAct }); @@ -1149,12 +1274,20 @@ WASimUI::WASimUI(QWidget *parent) : addToolBar(Qt::TopToolBarArea, toolbar); toolbar->setMovable(false); toolbar->setObjectName(QStringLiteral("TOOLBAR_MAIN")); - toolbar->setStyleSheet(QStringLiteral("QToolBar { border: 0; border-bottom: 1px solid palette(mid); spacing: 6px; } QToolBar::separator { background-color: palette(mid); width: 1px; padding: 0; margin: 6px 8px; }")); - toolbar->addActions({ d->initAct, pingAct, d->connectAct }); + toolbar->setStyleSheet(QStringLiteral( + "QToolBar { border: 0; border-bottom: 1px solid palette(mid); spacing: 6px; margin-left: 12px; }" + "QToolBar::separator { background-color: palette(mid); width: 1px; padding: 0; margin: 6px 8px; }" + )); + toolbar->addWidget(Utils::spacerWidget(Qt::Horizontal, 6)); + toolbar->addActions({ d->toggleConnAct }); toolbar->addSeparator(); - toolbar->addActions({ viewAct, styleAct, aboutAct, projectLinkAct }); + toolbar->addActions({ viewMenu->menuAction(), docBrowserAct, styleAct, aboutAct /*, projectLinkAct*/ }); // default toolbutton menu mode is lame - if (QToolButton *tb = qobject_cast(toolbar->widgetForAction(viewAct))) + if (QToolButton *tb = qobject_cast(toolbar->widgetForAction(d->toggleConnAct))) { + tb->setMenu(connectMenu); + tb->setPopupMode(QToolButton::MenuButtonPopup); + } + if (QToolButton *tb = qobject_cast(toolbar->widgetForAction(viewMenu->menuAction()))) tb->setPopupMode(QToolButton::InstantPopup); // Add the status widget to the toolbar, with spacers to right-align it with right padding. @@ -1174,17 +1307,31 @@ WASimUI::WASimUI(QWidget *parent) : // now restore any saved settings d->readSettings(); styleAct->setChecked(Utils::isDarkStyle()); + + // Say Hi! + d->logUiMessage("Hello!", CommandId::Ack, LogLevel::Info); } void WASimUI::onClientEvent(const ClientEvent &ev) { - d->clientStatus = ev.status; d->statWidget->setStatus(ev); - d->statWidget->setServerVersion(d->client->serverVersion()); - d->initAct->setChecked(+ev.status & +ClientStatus::SimConnected); - d->initAct->setText(d->initAct->isChecked() ? tr("Disconnect Simulator") : tr("Connect to Simulator") ); - d->connectAct->setChecked(+ev.status & +ClientStatus::Connected); - d->connectAct->setText(+d->connectAct->isChecked() ? tr("Disconnect Server") : tr("Connect to Server") ); + int simConnected = (+ev.status & +ClientStatus::SimConnected); + int isConnected = (+ev.status & +ClientStatus::Connected); + d->toggleConnAct->setChecked(simConnected && isConnected); + d->toggleConnAct->setText(simConnected && isConnected ? tr("Disconnect") : simConnected ? tr("Connect Server") : tr("Connect")); + d->initAct->setChecked(simConnected); + d->initAct->setText(simConnected ? tr("Disconnect Simulator") : tr("Connect to Simulator")); + d->connectAct->setChecked(isConnected); + d->connectAct->setText(isConnected ? tr("Disconnect Server") : tr("Connect to Server") ); + if ((+d->clientStatus & +ClientStatus::Connected) != isConnected) { + if (isConnected) + d->statWidget->setServerVersion(d->client->serverVersion()); + d->updateCalcCodeFormState(ui.cbCalculatorCode->currentText()); + d->updateLocalVarsFormState(); + d->toggleRequestButtonsState(); + d->toggleEventButtonsState(); + } + d->clientStatus = ev.status; } void WASimUI::onListResults(const ListResult &list) @@ -1194,6 +1341,7 @@ void WASimUI::onListResults(const ListResult &list) ui.cbLvars->clear(); for (const auto &pair : list.list) ui.cbLvars->addItem(QString::fromStdString(pair.second), pair.first); + ui.cbLvars->model()->sort(0); } void WASimUI::closeEvent(QCloseEvent *ev) diff --git a/src/WASimUI/WASimUI.h b/src/WASimUI/WASimUI.h index 045258a..14ca1cd 100644 --- a/src/WASimUI/WASimUI.h +++ b/src/WASimUI/WASimUI.h @@ -37,7 +37,6 @@ class WASimUI : public QMainWindow void commandResultReady(const WASimCommander::Command &c); void listResults(const WASimCommander::Client::ListResult &list); void dataResultReady(const WASimCommander::Client::DataRequestRecord &r); - void logMessageReady(const WASimCommander::LogRecord &r, quint8 src); protected: void closeEvent(QCloseEvent *) override; diff --git a/src/WASimUI/WASimUI.ui b/src/WASimUI/WASimUI.ui index b4eb07b..f3d0994 100644 --- a/src/WASimUI/WASimUI.ui +++ b/src/WASimUI/WASimUI.ui @@ -17,6 +17,9 @@ WASimUI + + Qt::ToolButtonTextBesideIcon + QMainWindow::AllowNestedDocks|QMainWindow::AllowTabbedDocks|QMainWindow::AnimatedDocks @@ -46,24 +49,19 @@ 8 - - + + + + Qt::ActionsContextMenu + - <p>Request Submission Form</p> -<p>Use this form to submit new or modified data requests to the WASimModule server. -Submitted requests will appear in the "Data Requests" window. Double-click on an existing request in the list to edit it in this form.</p> -<p> This form can also be partially pre-populated by using the "copy to request" buttons in the Calculator Event and Variables forms.</p> + <p>This form is for working with all types of variables. It can be used to get current values and also set values (on variables which allow that). The form adapts to the type of variable selected.</p> +<p>Variable definitions from this form can be copied to the Data Requests form and converted to a recurring data query.</p> - Data Request - - - Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft - - - false + Variables - + 4 @@ -77,484 +75,432 @@ Submitted requests will appear in the "Data Requests" window. Double-c 4 - 6 + 8 - - - - - - - 0 - 0 - - - - Update On: - - - - - - - Update Period - - - - - - - - 0 - 0 - - - - Interval: - - - + + + + + 0 + 0 + + + + + 75 + true + + + + Get + + + + + + + + 0 + 0 + + + + + + + + Set + + + Qt::ToolButtonIconOnly + + + false + + + + + + + 6 + + + 0 + + + 0 + + + 0 + + + 0 + - - - Update Interval (number of periods between updates, 0 for every period) - - - 999999999 - - - QAbstractSpinBox::DefaultStepType + + + true - - - - - + 0 0 - - ΔΕ: + + Qt::ClickFocus - - - - - Delta Epsilon, minimum change in value before request is updated (only for predefined value types Int/UInt/Float/Double). <p>Hold Ctrl and/or Shift keys to change stepping sizes.</p> - - - 7 + Result of a Variable Get request will appear here. - - -1.000000000000000 + + false - - 9999999.999997999519110 + + false - - 0.001000000000000 + + Variable Get result... - - QAbstractSpinBox::AdaptiveDecimalStepType + + true - + Qt::Horizontal - QSizePolicy::Preferred + QSizePolicy::Fixed - 50 - 10 + 24 + 20 - - - - false - - - - 0 - 0 - - - - Update the existing request record from current form entries. - - - Save - - - - - - - - 0 - 0 - - - - Add new request record from current form entries. - - - Add - - - - - - - 28 + + + + Get - - 0 + + Qt::ToolButtonIconOnly - - 0 + + + + + + + 0 + 0 + - + + + 75 + true + + + + Set + + + + + + + + 0 + 0 + + + + Qt::StrongFocus + + + The value to set the variable to. + + 8 - - 0 + + -4294967295.999899864196777 - - - - - 0 - 0 - - - - Request Type: - + + 4294969056.999899864196777 + + + QAbstractSpinBox::AdaptiveDecimalStepType + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 4 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 75 + true + + + + List + + + cbLvars + + + + + + + Qt::ClickFocus + + + <p>List of Local variables currently defined on the simulator (press Refresh button to (re)load)</p> + + + true + + + 25 + + + QComboBox::NoInsert + + + + + + + Refresh + + + Qt::ToolButtonIconOnly + + + false + + + + - + + + + 4 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 75 + true + + + + Name + + + cbVariableName + + + + + + + Qt::ClickFocus + + + <p>Enter the Variable name to use for the Get/Set command.</p> +<p>The Simulator Variable (SimVar) completion suggestions are loaded from imported SimConnect SDK documentation.</p> +<p>Press enter after entering text to save it in the list for future selection.</p> +<p>Saved items can be removed by right-clicking on them while the list is open.</p> + + + true + + + + + + + Lookup + + + Qt::ToolButtonIconOnly + + + false + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + - + 0 0 - - <p>"Named" variables can be of various types but are always accessed with a unique name or ID.</p> - - - Named Variable + + Qt::StrongFocus - + true - - bgrpRequestType - - + - + 0 0 - - <p>A cacluator code request can evaluate any kind of formula that returns a result.</p> - - - Calculator Code - - - false - - - bgrpRequestType - + + + 3 + + + 6 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + Idx: + + + sbGetSetSimVarIndex + + + + + + + SimVar Index (if any, zero is blank) + + + true + + + + + + + - - - - Qt::Horizontal - - - QSizePolicy::Expanding - - - - 40 - 20 - - - - - - - - 4 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - ID: - - - - - - - - 0 - 0 - - - - ID of request currently in the editor. - - - New - - - - - - - - - - - 6 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - - - 0 - 0 - - - - Variable Name or Calculator Code<p>(Save items by pressing Enter, Remove by right-clicking on them while the list is open.)</p> - - - true - - - - - - - 4 - - - 0 - - - - - - 0 - 0 - - - - Result: - - - - - - - Calculation Result Type - - - - - - - - - - 0 - 0 - - - - Size: - - - - - - - - 0 - 0 - - - - Value Size in Bytes or a preset type.<p>(Save items by pressing Enter, Remove by right-clicking on them while the list is open.)</p> - - - true - - - - - - - 4 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - Unit: - - - - - - - - 0 - 0 - - - - Optional Unit Name for Named Variables<p>(Save items by pressing Enter, Remove by right-clicking on them while the list is open.)</p> - - - true - - - - - - - - 0 - 0 - - - - Idx: - - - - - - - SimVar Index (if any, zero is blank) - - - true - - - - - - - - - - - - - - - <p>Form for triggering Key events with up to 5 data values. Events can be specified by name or ID.</p - - - Key Events - - - - 4 - - - 4 - - - 4 - - - 4 - - - 5 - - - + + 0 @@ -562,33 +508,46 @@ Submitted requests will appear in the "Data Requests" window. Double-c - Values: + Value: + + + dsbSetVarValue - - + + 0 0 - - Integer data for third event value. + + Unit: - - -999999999 + + cbSetVarUnitName - - 999999999 + + + + + + Set/Create + + + Qt::ToolButtonIconOnly + + + false - + - Send + Copy Qt::ToolButtonIconOnly @@ -596,48 +555,82 @@ Submitted requests will appear in the "Data Requests" window. Double-c - + + + Get/Create + + + Qt::ToolButtonIconOnly + + + + + + + + + + Qt::ActionsContextMenu + + + <p>Form for triggering Key events with up to 5 data values. Events can be specified by name or ID.</p + + + Key Events + + + + 4 + + + 4 + + + 4 + + + 4 + + + 5 + + + 0 0 - - Integer data for fifth event value. - - - -999999999 + + Values: - - 999999999 + + sbKeyEvent_v1 - - - - Qt::ClickFocus + + + + + 0 + 0 + - Event ID or Name or send. Names are listed in MSFS Event IDs reference, IDs can be found using the Lookup command. - - - true - - - 25 + Integer data for fourth event value. - - QComboBox::InsertAtTop + + -999999999 - - Name or ID + + 999999999 - - + + 0 @@ -645,7 +638,7 @@ Submitted requests will appear in the "Data Requests" window. Double-c - Integer data for second event value. + Integer data for first event value. -999999999 @@ -655,8 +648,8 @@ Submitted requests will appear in the "Data Requests" window. Double-c - - + + 0 @@ -664,7 +657,7 @@ Submitted requests will appear in the "Data Requests" window. Double-c - Integer data for fourth event value. + Integer data for fifth event value. -999999999 @@ -674,8 +667,8 @@ Submitted requests will appear in the "Data Requests" window. Double-c - - + + 0 @@ -683,7 +676,7 @@ Submitted requests will appear in the "Data Requests" window. Double-c - Integer data for first event value. + Integer data for second event value. -999999999 @@ -694,7 +687,7 @@ Submitted requests will appear in the "Data Requests" window. Double-c - + 0 @@ -709,18 +702,99 @@ Submitted requests will appear in the "Data Requests" window. Double-c + + + + + 0 + 0 + + + + Integer data for third event value. + + + -999999999 + + + 999999999 + + + + + + + Send + + + Qt::ToolButtonIconOnly + + + + + + + 3 + + + + + Qt::ClickFocus + + + <p>SimConnect Key Event name or ID to send..</p> +<p>The completion suggestions are loaded from imported SimConnect SDK documentation.</p> +<p>IDs can be found using the Lookup command.</p> +<p>Press enter after entering text to save it in the list for future selection.</p> +<p>Saved items can be removed by right-clicking on them while the list is open.</p> + + + true + + + 25 + + + QComboBox::InsertAtTop + + + Name or ID + + + + + + + Lookup + + + Qt::ToolButtonIconOnly + + + false + + + + + - - + + + + Qt::ActionsContextMenu + - <p>This form allows sending any arbitrary command to the WASimModule. This is for testing low-level API functions, not generally useful in most cases. Refer to API documentation for details.</p> + <p>This form allows performing various meta data retrival functions, for example to get numeric IDs of named variables/events, or to check for their existence.</p> - Send WASimCommander API Command + Data Lookup - + + + 6 + 4 @@ -733,132 +807,138 @@ Submitted requests will appear in the "Data Requests" window. Double-c 4 - - 3 - - - + + 0 0 + + Look up the ID of a named variable or unit. + - uData: + Lookup Type: + + + cbLookupItemType - - + + 0 0 - - sData: - - - - - - - Integer value for command's `uData` field. - - - -999999999 - - - 999999999 - - - - - - - - Double floating point value for command's `fData` field. <p>Hold Ctrl and/or Shift keys to change stepping sizes.</p> - - - 7 - - - 9999999.999997999519110 - - - 0.001000000000000 - - - QAbstractSpinBox::AdaptiveDecimalStepType + + + + Qt::ClickFocus - - - - - String value for command's `sData` member. + Variable or Unit name to look up. - - 512 + + true - - + + - Send + Lookup Qt::ToolButtonIconOnly + + false + - - + + 0 0 - - fData: - + + + 75 + true + + + + = + - - + + + + true + - + 0 0 + + Qt::ClickFocus + - <p>Send any command to the WASimModule server.</p><p>(Familiarity with the WASimCommander API is necessary for this to be particularly useful.)</p> + Result of an item Lookup will appear here. - - Command + + 16 + + + false + + + false + + + Lookup result... + + + true - - + + + + Qt::ActionsContextMenu + - <p>This form is for working with all types of variables. It can be used to get current values and also set values (on variables which allow that). The form adapts to the type of variable selected.</p> -<p>Variable definitions from this form can be copied to the Data Requests form and converted to a recurring data query.</p> + <p>Request Submission Form</p> +<p>Use this form to submit new or modified data requests to the WASimModule server. +Submitted requests will appear in the "Data Requests" window. Double-click on an existing request in the list to edit it in this form.</p> +<p> This form can also be partially pre-populated by using the "copy to request" buttons in the Calculator Event and Variables forms.</p> - Variables + Data Request + + + Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft + + + false - + 4 @@ -872,40 +952,183 @@ Submitted requests will appear in the "Data Requests" window. Double-c 4 - 8 + 6 - - - - - 0 - 0 - - - - Qt::StrongFocus - - - The value to set the variable to. - - - 8 - - - -4294967295.999899864196777 - - - 4294969056.999899864196777 - - - QAbstractSpinBox::AdaptiveDecimalStepType + + + + 4 - + + + + + 0 + 0 + + + + Update On: + + + cbPeriod + + + + + + + Update Period + + + + + + + + 0 + 0 + + + + Interval: + + + sbInterval + + + + + + + Update Interval (number of periods between updates, 0 for every period) + + + 999999999 + + + QAbstractSpinBox::DefaultStepType + + + + + + + + 0 + 0 + + + + ΔΕ: + + + dsbDeltaEpsilon + + + + + + + Delta Epsilon, minimum change in value before request is updated (only for predefined value types Int/UInt/Float/Double). <p>Hold Ctrl and/or Shift keys to change stepping sizes.</p> + + + 7 + + + -1.000000000000000 + + + 9999999.999997999519110 + + + 0.001000000000000 + + + QAbstractSpinBox::AdaptiveDecimalStepType + + + + + + + Qt::Horizontal + + + QSizePolicy::Preferred + + + + 50 + 10 + + + + + + + + + 0 + 0 + + + + Clear + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 20 + 1 + + + + + + + + false + + + + 0 + 0 + + + + Save + + + + + + + + 0 + 0 + + + + Add + + + + - - + + - 0 + 28 0 @@ -914,473 +1137,136 @@ Submitted requests will appear in the "Data Requests" window. Double-c 0 - 0 + 8 0 - - - - 6 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - true - - - - List - - - - - - - Qt::ClickFocus - - - Local Variables (press Refresh button to (re)load) - - - true - - - 25 - - - QComboBox::NoInsert - - - - - - - Refresh - - - Qt::ToolButtonIconOnly - - - false - - - - - - - - - - - 6 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - true - - - - Name - - - - - - - Qt::ClickFocus - - - Variable name - - - true - - - - + + + + 0 + 0 + + + + Request Type: + - - - - - - Get - - - Qt::ToolButtonIconOnly - - - - - - - Set - - - Qt::ToolButtonIconOnly - - - false - - - - - - - Set/Create - - - Qt::ToolButtonIconOnly - - - false - - - - - - - - 0 - 0 - - - - Unit: - - - - - - - Copy - - - Qt::ToolButtonIconOnly - - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - + - + 0 0 - - Qt::StrongFocus - - Optional Unit Name for the value. Leave blank to use the default units of the variable.<p>(Save items by pressing Enter, Remove by right-clicking on them while the list is open.)</p> + <p>"Named" variables can be of various types but are always accessed with a unique name or ID.</p> - + + Named Variable + + true + + bgrpRequestType + - + - + 0 0 - - - 3 - - - 6 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - Idx: - - - - - - - SimVar Index (if any, zero is blank) - - - true - - - - - - - - - - - - - - - - 0 - 0 - - - - - true - - - - Set - - - - - - - - 0 - 0 - - - - - true - - - - Get - - - - - - - - 0 - 0 - - - - Value: - - - - - - - - 0 - 0 - - - - - - - - 6 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - true - - - - 0 - 0 - - - - Qt::ClickFocus - - Result of a Variable Get request will appear here. + <p>A cacluator code request can evaluate any kind of formula that returns a result.</p> - - false + + Calculator Code - + false - - Variable Get result... - - - true - + + bgrpRequestType + - + Qt::Horizontal - QSizePolicy::Fixed + QSizePolicy::Expanding - 24 + 40 20 + + + + 4 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + ID: + + + + + + + + 0 + 0 + + + + ID of request currently in the editor. + + + New + + + + + - - - - - - - - 0 - 0 - - - - <p>This form is for evaluating RPN "calculator code" on the simulator via WASimModule. Evaluated code may or may not return a result of various types.</p> -<p>Calculator code which returns a result can be copied to Data Requests and turned into a recurring query. Code which performs some kind of action can be turned into a Registered Event which can later be triggered by name or ID. In either of these cases, the code will be pre-compiled into a more efficient format on the server so that subsequent evaluation will be more efficient.</p> - - - Evaluate Calculator Code - - - - 4 - - - 4 - - - 4 - - - 4 - - - 6 - - - - - Register - - - Qt::ToolButtonIconOnly - - - false - - - - - - - Calc - - - Qt::ToolButtonIconOnly - - - false - - - - - + + 6 @@ -1397,821 +1283,558 @@ Submitted requests will appear in the "Data Requests" window. Double-c 0 - - - true - + + + + - + 0 0 - - Qt::ClickFocus - - Calculator result, if any, will appear here. - - - false - - - false - - - Calculation result, if any, will appear here. + <p>Variable Name or Calculator Code for the Request.</p> +<p>The Simulator Variable completion suggestions are loaded from imported SimConnect SDK documentation.</p> +<p>Press enter after entering text to save it in the list for future selection.</p> +<p>Saved items can be removed by right-clicking on them while the list is open.</p> - + true - - - Qt::Horizontal - - - QSizePolicy::Fixed + + + 4 - - - 8 - 1 - + + 0 - + + 0 + + + 0 + + + 0 + + + + + Lookup + + + Qt::ToolButtonIconOnly + + + false + + + + + + + + 0 + 0 + + + + Idx: + + + sbSimVarIndex + + + + + + + SimVar Index (if any, zero is blank) + + + true + + + + + + + + + + + 0 + 0 + + + + Unit: + + + cbUnitName + + + + + + + + 0 + 0 + + + + true + + + + - - - - 0 - 0 - + + + 4 - - Event + + 0 - + + + + + 0 + 0 + + + + Result: + + + cbRequestCalcResultType + + + + + + + Calculation Result Type + + + + - + - + 0 0 - - <p>Optional specific name for this event.</p><p>If the custom event name contains a period (`.`) then it is used as-is. Otherwise "WASimCommander.&lt;client_name&gt;." will be prepended to the name.</p> - - - Name for registered event - - - - - - Save - - - Qt::ToolButtonIconOnly - - - false - - - - - - - - - - 0 - 0 - - - - Code: - - - - - - - Copy - - - Qt::ToolButtonIconOnly - - - - - - - Calculation Result Type - - - - - - - - 0 - 0 - - - - Calculator Code string <p>(Save items by pressing Enter, Remove by right-clicking on them while the list is open.)</p> - - - true - - - - - - - - 0 - 0 - - - - Result: - - - - - - - - - - <p>This form allows performing various meta data retrival functions, for example to get numeric IDs of named variables/events, or to check for their existence.</p> - - - Data Lookup - - - - 6 - - - 4 - - - 4 - - - 4 - - - 4 - - - - - - 0 - 0 - - - - Look up the ID of a named variable or unit. - - - Lookup Type: - - - - - - - - 0 - 0 - - - - - - - - Qt::ClickFocus - - - Variable or Unit name to look up. - - - true - - - - - - - Lookup - - - Qt::ToolButtonIconOnly - - - false - - - - - - - - 0 - 0 - - - - - true - - - - = - - - - - - - true - - - - 0 - 0 - - - - Qt::ClickFocus - - - Result of an item Lookup will appear here. - - - 16 - - - false - - - false - - - Lookup result... - - - true - - - - - - - - - - - - 0 - 0 - - - - Data Requests - - - 8 - - - - - 0 - 1 - - - - - 5 - - - 0 - - - 5 - - - 6 - - - 6 - - - 5 - - - - - Added Requests (double click to edit) - - - QAbstractItemView::NoEditTriggers - - - true - - - QAbstractItemView::SelectRows - - - Qt::ElideMiddle - - - QAbstractItemView::ScrollPerPixel - - - QAbstractItemView::ScrollPerPixel - - - Qt::DotLine - - - true - - - false - - - false - - - 20 - - - 60 - - - false - - - false - - - - - - - 6 - - - QLayout::SetMaximumSize - + Size: + + + cbValueSize + + + + + + + + 0 + 0 + + + + Value Size in Bytes or a preset type.<p>(Save items by pressing Enter, Remove by right-clicking on them while the list is open.)</p> + + + true + + + + + + + + + + + + + 0 + 0 + + + + Qt::ActionsContextMenu + + + <p>This form is for evaluating RPN "calculator code" on the simulator via WASimModule. Evaluated code may or may not return a result of various types.</p> +<p>Calculator code which returns a result can be copied to Data Requests and turned into a recurring query. Code which performs some kind of action can be turned into a Registered Event which can later be triggered by name or ID. In either of these cases, the code will be pre-compiled into a more efficient format on the server so that subsequent evaluation will be more efficient.</p> + + + Evaluate Calculator Code + + - 0 + 4 - 0 + 4 - 0 + 4 - 0 + 4 - - - - false - - - - 0 - 0 - - + + 6 + + + - Remove - - - - - - - false + Register - - - 0 - 0 - + + Qt::ToolButtonIconOnly - - Update + + false - - - - false - - - - 0 - 0 - - + + - Pause + Calc - - - - - - Qt::Horizontal + + Qt::ToolButtonIconOnly - - - 10 - 20 - + + false - + - - - - - 0 - 0 - + + + + 6 - - Load + + 0 - - - - - - false + + 0 - - - 0 - 0 - + + 0 - - Save + + 0 - - - - - - - - - - - 0 - 0 - - - - false - - - Log Output - - - 8 - - - - - 0 - 3 - - - - - 0 - - - 5 - - - 0 - - - 5 - - - 6 - - - - - - Courier New - 9 - - - - Log Records - - - QAbstractScrollArea::AdjustToContents - - - QAbstractItemView::NoEditTriggers - - - QAbstractItemView::SingleSelection - - - QAbstractItemView::SelectRows - - - - 16 - 16 - - - - Qt::ElideMiddle - - - QAbstractItemView::ScrollPerItem - - - QAbstractItemView::ScrollPerPixel - - - Qt::DotLine - - - false - - - false - - - false - - - 20 - - - 60 - - - false - - - false - - - true - - - false - - - - - - - 5 - - - 6 - - - 2 - - - + + + + true + + + + 0 + 0 + + + + Qt::ClickFocus + + + Calculator result, if any, will appear here. + + + false + + + false + + + Calculation result, if any, will appear here. + + + true + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 8 + 1 + + + + + + + + + 0 + 0 + + + + Event + + + leEventName + + + + + + + + 0 + 0 + + + + <p>Optional specific name for this event.</p><p>If the custom event name contains a period (`.`) then it is used as-is. Otherwise "WASimCommander.&lt;client_name&gt;." will be prepended to the name.</p> + + + Name for registered event + + + + + + + Save + + + Qt::ToolButtonIconOnly + + + false + + + + + + + - + 0 0 - Callback: - - - - - - - Pause + Code: - - Qt::ToolButtonIconOnly + + cbCalculatorCode - - + + - Server + Copy Qt::ToolButtonIconOnly - - false - - - - - UI - - - Qt::ToolButtonIconOnly - - - false + + + + Calculation Result Type - - + + - + 0 0 - - Console: + + Calculator Code string <p>(Save items by pressing Enter, Remove by right-clicking on them while the list is open.)</p> + + + true - - + + 0 0 - - <p>Set Server File Log Level (note that until/unless set via this control, the initial level is unknown).</p> - - - - - - Debug - - - Qt::ToolButtonIconOnly + Result: - - false + + cbCalcResultType - - + + + + + + + Qt::ActionsContextMenu + + + <p>This form allows sending any arbitrary command to the WASimModule. This is for testing low-level API functions, not generally useful in most cases. Refer to API documentation for details.</p> + + + Send WASimCommander API Command + + + + 4 + + + 4 + + + 4 + + + 4 + + + 3 + + + - + 0 0 - File: + uData: + + + sbCmdUData - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 10 - 20 - + + + + + 0 + 0 + - - - - - Info - - - Qt::ToolButtonIconOnly + sData: - - false + + leCmdSData - - - - Error + + + + Integer value for command's `uData` field. - - Qt::ToolButtonIconOnly + + -999999999 - - false + + 999999999 - - - - - 0 - 0 - - + + + + + - <p>Set Server Console Log Level (note that until/unless set via this control, the initial level is unknown).</p> + Double floating point value for command's `fData` field. <p>Hold Ctrl and/or Shift keys to change stepping sizes.</p> - - - - - - Warning + + 7 - - Qt::ToolButtonIconOnly + + 9999999.999997999519110 - - false + + 0.001000000000000 + + + QAbstractSpinBox::AdaptiveDecimalStepType - - - - Client - - - Qt::ToolButtonIconOnly + + + + String value for command's `sData` member. - - false + + 512 - - + + - Clear + Send Qt::ToolButtonIconOnly - - + + - + 0 0 - Client + fData: + + + sbCmdFData - - + + 0 @@ -2219,158 +1842,229 @@ Submitted requests will appear in the "Data Requests" window. Double-c - Set Server Callback Log Level + <p>Send any command to the WASimModule server.</p><p>(Familiarity with the WASimCommander API is necessary for this to be particularly useful.)</p> + + + Command + + + cbCommandId - - + + + + + + + + + 0 + 0 + + + + Data Requests + + + 8 + + + + + 0 + 1 + + + + Qt::ActionsContextMenu + + + + 5 + + + 0 + + + 5 + + + 6 + + + 6 + + + 5 + + + + + Added Requests (double click to edit) + + + + + + + 6 + + + QLayout::SetMaximumSize + + + 10 + + + 0 + + + 10 + + + 0 + + + + + false + - + 0 0 - File: + Remove - - + + + + false + 0 0 - - Set Client File Log Level + + Update - - + + Qt::Horizontal + + QSizePolicy::Fixed + - 4 + 40 20 - - - - - 0 - 0 - - - - Set Client Callback Log Level + + + + false - - - - 0 0 - - Set Client Console Log Level - - - - - - - - 0 - 0 - - - Server: + Pause - - - - WW + + + + Qt::Horizontal - - Qt::ToolButtonIconOnly + + + 10 + 20 + - + - - + + - + 0 0 - Console: + Load - - + + + + false + - + 0 0 - Callback: - - - - - - - Trace - - - Qt::ToolButtonIconOnly - - - false + Save - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 10 - 20 - - - - + + + + 0 + 0 + + + + false + + + Log Output + + + 8 + + + + + 0 + 3 + + + + Qt::ActionsContextMenu + + + @@ -2384,13 +2078,16 @@ Submitted requests will appear in the "Data Requests" window. Double-c 2 - + 0 0 + + Qt::ActionsContextMenu + 5 @@ -2411,7 +2108,7 @@ Submitted requests will appear in the "Data Requests" window. Double-c 5 - + Registered Events (double click to edit) @@ -2424,9 +2121,6 @@ Submitted requests will appear in the "Data Requests" window. Double-c QAbstractItemView::SelectRows - - Qt::ElideMiddle - QAbstractItemView::ScrollPerPixel @@ -2468,47 +2162,47 @@ Submitted requests will appear in the "Data Requests" window. Double-c 4 - 0 + 6 0 - 0 + 6 0 - + false - - - 0 - 0 - - Remove + + + 24 + 24 + + - + false - - - 0 - 0 - - Update + + + 24 + 24 + + @@ -2525,32 +2219,35 @@ Submitted requests will appear in the "Data Requests" window. Double-c - - - - 0 - 0 - - + Load + + + 24 + 24 + + + + QToolButton::InstantPopup + - + false - - - 0 - 0 - - Save + + + 24 + 24 + + @@ -2563,9 +2260,14 @@ Submitted requests will appear in the "Data Requests" window. Double-c - WASimUiNS::DeletableItemsComboBox + ActionPushButton + QPushButton +
ActionPushButton.h
+
+ + DeletableItemsComboBox QComboBox -
Widgets.h
+
DeletableItemsComboBox.h
WASimUiNS::CalculationTypeComboBox @@ -2587,21 +2289,11 @@ Submitted requests will appear in the "Data Requests" window. Double-c QComboBox
Widgets.h
- - ActionPushButton - QPushButton -
ActionPushButton.h
-
WASimUiNS::UnitTypeComboBox QComboBox
Widgets.h
- - WASimUiNS::LogLevelComboBox - QComboBox -
Widgets.h
-
WASimUiNS::LookupTypeComboBox QComboBox @@ -2612,7 +2304,86 @@ Submitted requests will appear in the "Data Requests" window. Double-c QComboBox
Widgets.h
+ + WASimUiNS::LogConsole + QWidget +
LogConsole.h
+ 1 +
+ + WASimUiNS::RequestsTableView + QTableView +
RequestsTableView.h
+
+ + BuddyLabel + QLabel +
BuddyLabel.h
+
+ + WASimUiNS::CustomTableView + QTableView +
CustomTableView.h
+
+ + cbCalculatorCode + cbCalcResultType + btnCalc + btnCopyCalcToRequest + leEventName + btnUpdateEvent + btnAddEvent + cbGetSetVarType + btnList + btnFindSimVar + cbSetVarUnitName + sbGetSetSimVarIndex + btnGetVar + btnGetCreate + btnCopyLVarToRequest + dsbSetVarValue + btnSetVar + btnSetCreate + cbLookupItemType + btnVarLookup + sbKeyEvent_v1 + sbKeyEvent_v2 + sbKeyEvent_v3 + sbKeyEvent_v4 + sbKeyEvent_v5 + btnKeyEventSend + cbCommandId + sbCmdUData + sbCmdFData + leCmdSData + btnCmdSend + rbRequestType_Named + rbRequestType_Calculated + cbVariableType + cbNameOrCode + sbSimVarIndex + cbUnitName + cbRequestCalcResultType + cbValueSize + cbPeriod + sbInterval + dsbDeltaEpsilon + btnClearRequest + btnUpdateRequest + btnAddRequest + requestsView + btnReqestsRemove + btnReqestsUpdate + btnReqestsPause + btnReqestsLoad + btnReqestsSave + eventsView + btnEventsRemove + btnEventsTransmit + btnEventsLoad + btnEventsSave + diff --git a/src/WASimUI/WASimUI.vcxproj b/src/WASimUI/WASimUI.vcxproj index 9716d0f..ca46442 100644 --- a/src/WASimUI/WASimUI.vcxproj +++ b/src/WASimUI/WASimUI.vcxproj @@ -31,18 +31,23 @@ Application v142 + Unicode Application v142 + Unicode + PGOptimize Application v142 + Unicode Application v142 + Unicode @@ -50,28 +55,28 @@ 5.12.12_msvc2017_64 - core;gui;widgets + core;sql;gui;widgets debug false - true + false 5.12.12_msvc2017_64 - core;gui;widgets + core;sql;gui;widgets release true false 5.12.12_msvc2017_64 - core;gui;widgets + core;sql;gui;widgets release true false 5.12.12_msvc2017_64 - core;gui;widgets + core;sql;gui;widgets debug true false @@ -104,17 +109,21 @@ true + $(ProjectDir)model;$(ProjectDir)widgets;$(IncludePath) true + $(ProjectDir)model;$(ProjectDir)widgets;$(IncludePath) true true + $(ProjectDir)model;$(ProjectDir)widgets;$(IncludePath) true true + $(ProjectDir)model;$(ProjectDir)widgets;$(IncludePath) @@ -160,6 +169,7 @@ true WSMCMND_API_STATIC;%(PreprocessorDefinitions) StdCall + Speed @@ -260,9 +270,16 @@ + + + - + + + + + @@ -271,6 +288,15 @@ true true + + Document + + + true + true + true + true + true true @@ -294,10 +320,24 @@ - + + + + + + + + + + + + + + + - - + + diff --git a/src/WASimUI/model/AlphanumComparer.h b/src/WASimUI/model/AlphanumComparer.h new file mode 100644 index 0000000..a1d74bf --- /dev/null +++ b/src/WASimUI/model/AlphanumComparer.h @@ -0,0 +1,109 @@ +#ifndef ALPHANUMCOMPARER_H +#define ALPHANUMCOMPARER_H + +/*! + * \brief Natural (alpha-num) sorting + * \author Litkevich Yuriy + * \see http://www.forum.crossplatform.ru/index.php?showtopic=6244&st=0&p=44752&#entry44752 + */ + + +#include + + +class AlphanumComparer +{ +public: + static bool lessThan(const QString &s1, const QString &s2) + { + return compare( s1, s2 ) < 0 ; + } + +private: + /*! + * \fn compare - compare two strings + * \param l - left sring. + * \param r - right string. + * + * \return + * lr - result more than zero; + * l=r - result is zero. + */ + static int compare(QString l, QString r) + { + enum Mode { STRING, NUMBER } mode = STRING; + + int size; + if ( l.size() < r.size() ) + size = l.size(); + else + size = r.size(); + + int i = 0; + + // runing throught both strings to position "size-1" + while( i < size) { + if ( mode == STRING ) { + QChar lchar, rchar; + bool ldigit, rdigit; + while( i < size ) { + lchar = l.at( i ); + rchar = r.at( i ); + ldigit = lchar.isDigit(); + rdigit = rchar.isDigit(); + // if both simbols is numbers, using numbers state + if ( ldigit && rdigit ) { + mode = NUMBER; + break; + } + if ( ldigit ) return -1; + if ( rdigit ) return +1; + // both simbols are letters + if ( lchar < rchar ) return -1; + if ( lchar > rchar ) return +1; + // simbols are equal + i++; + } + } else { //mode == NUMBER + unsigned long long lnum = 0, rnum = 0; + int li = i, ri = i; // local indexes + int ld = 0, rd = 0; // numbers + + // make left number + while ( li < l.size() ) { + ld = l.at( li ).digitValue(); + if ( ld < 0 ) break; + lnum = lnum*10 + ld; + li++; + } + + // make right number + while( ri < r.size() ) { + rd = r.at( ri ).digitValue(); + if ( rd < 0 ) break; + rnum = rnum*10 + rd; + ri++; + } + + long long delta = lnum - rnum; + if ( delta ) return delta; + + // numbers are equal + mode = STRING; + if ( li <= ri ) + i=li; + else + i=ri; + } + } + // this is for situation when both strings to position "size-1" equals + if ( i < r.size() ) return -1; + if (i < l.size() ) return +1; + + // strings are full equal + return 0; + } +}; + +#endif // ALPHANUMCOMPARER_H diff --git a/src/WASimUI/model/MultiColumnProxyModel.h b/src/WASimUI/model/MultiColumnProxyModel.h new file mode 100644 index 0000000..6a54167 --- /dev/null +++ b/src/WASimUI/model/MultiColumnProxyModel.h @@ -0,0 +1,262 @@ +/* +This file is part of the WASimCommander project. +https://github.com/mpaperno/WASimCommander + +Original version of multi-column filtering from ; Public domain; Modifications applied. +Original version multi-column sorting from ; GPL v3 license; Modifications applied. + +COPYRIGHT: (c) Maxim Paperno; All Rights Reserved. + +This file may be used under the terms of the GNU General Public License (GPL) +as published by the Free Software Foundation, either version 3 of the Licenses, +or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +A copy of the GNU GPL is included with this project +and is also available at . +*/ + +#pragma once + +#include +#include +#include +#include "AlphanumComparer.h" + +class MultiColumnProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT + + public: + using QSortFilterProxyModel::QSortFilterProxyModel; + + // Reimplemented to show sort icon and order as part of the heading title. + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole ) const override + { + if (orientation == Qt::Horizontal && role == Qt::DisplayRole && m_sortedColumns.contains(section)) { + const QString header = + sourceModel()->headerData(section, orientation).toString().append(m_sortedColumns.value(section).sortOrder == Qt::AscendingOrder ? " ▲" : " ▼"); + if (m_sortedColumns.count() == 1) + return header; + return header + ' ' + toSuperScript(m_sortedColumnsOrder.indexOf(section) + 1); + } + return QSortFilterProxyModel::headerData(section, orientation, role); + } + + void setUseAlphanumericSortingAlgo(bool use = true) { m_useAlphanumSort = use; } + bool usingAlphanumericSortingAlgo() const { return m_useAlphanumSort; } + + /// Return sort order as `Qt::SortOrder` enum value of the given column index, or -1 if the column isn't sorted. + int columnSortOrder (int column) const { + return m_sortedColumns.contains(column) ? m_sortedColumns.value(column).sortOrder : -1; + } + + /// Return column's position in the list of sorted columns, or -1 if the column isn't sorted. + int columnSortPosition (int column) const { + return m_sortedColumnsOrder.indexOf(column); + } + + /// Return count of sorted columns + int sortedColumnsCount () const { + return m_sortedColumnsOrder.count(); + } + + public Q_SLOTS: + + // Filtering + + virtual void setRegExpFilter(int col, const QRegExp& matcher, int role = Qt::DisplayRole) + { + if (matcher.isEmpty() || !matcher.isValid()) + return removeFilterFromColumn(col, role); + m_filters[(static_cast(col) << 32) | static_cast(role)] = matcher; + //qDebug() << col << matcher.pattern() << role; + invalidateFilter(); + } + + virtual void setStringPatternFilter(int col, QString pattern, int role) + { + if (pattern.isEmpty()) { + removeFilterFromColumn(col); + } + else if (pattern.startsWith('/') && pattern.endsWith('/') /*&& pattern.length() > 2*/) { + setRegExpFilter(col, QRegExp(pattern.mid(1, pattern.length() - 2), Qt::CaseInsensitive, QRegExp::RegExp)); + } + else { + if (!pattern.contains('*') && !pattern.contains('?') && !pattern.contains(']')) + pattern.prepend('*').append('*'); + setRegExpFilter(col, QRegExp(pattern, Qt::CaseInsensitive, QRegExp::WildcardUnix)); + } + } + + virtual void setDisplayRolePatternFilter(int col, QString pattern) { + setStringPatternFilter(col, pattern, Qt::DisplayRole); + } + + virtual void clearFilters() + { + m_filters.clear(); + invalidateFilter(); + } + + virtual void removeFilterFromColumn(int col, int role) + { + m_filters.remove((static_cast(col) << 32) | static_cast(role)); + invalidateFilter(); + } + + virtual void removeFilterFromColumn(int col) + { + for (auto i = m_filters.begin(); i != m_filters.end();) { + if ((i.key() >> 32) == col) + i = m_filters.erase(i); + else + ++i; + } + invalidateFilter(); + } + + // Sorting + + /// Sort by `column` number. If `isModifierPressed` is `true` and the column is not in the current sort list + /// then it is appended to the list using default Ascending order. If already in the list then the order is reversed. + /// If sorted a 3rd time, the column is removed from the list (assuming there is more than one). + /// The modifier is essentially ignored if only one column is being sorted on anyway. + /// If `isModifierPressed` is `false` then any other currently sorted columns are cleared and this column's order is either set to Ascending or is reversed, + /// depending on if it was already in the sort list or not. + /// If `order` is specified as `Qt::AscendingOrder` or `Qt::DescendingOrder`, then the order of sorting is forced instead of being reversed. + virtual void sortColumn(int column, bool isModifierPressed = false, qint8 order = -1) + { + if (isModifierPressed) { + if (m_sortedColumns.contains(column)) { + // remove sorted columns on 3rd click, but not if it's the last one. + if (m_sortedColumns.count() > 1 && m_sortedColumns.value(column).activations >= 2) + removeSortedColumn(column); + else if (order == -1) + changeSortDirection(column, 2); + else + setSortDirection(column, (Qt::SortOrder)order, 2); + } + else { + // append new sorted column + addSortedColumn(column, order == -1 ? Qt::AscendingOrder : (Qt::SortOrder)order); + } + } + else { + if (m_sortedColumns.contains(column) && order == -1) { + if (m_sortedColumns.count() == 1) { + // just change direction + changeSortDirection(column, 1); + } + else { + // need to remove all other columns first; save this column's order so it can be reversed afterwards. + Qt::SortOrder ord = reverseOrder(m_sortedColumns.value(column).sortOrder); + clearSortedColumns(); + addSortedColumn(column, ord); + } + } + else { + // column wasn't currently sorted on so just clear the rest and add the new one with default order + clearSortedColumns(); + addSortedColumn(column, order == -1 ? Qt::AscendingOrder : (Qt::SortOrder)order); + } + } + multisort(); + } + + /// Sorts by a single column in given order. + virtual void sortByColumn(int column, Qt::SortOrder order) { + clearSortedColumns(); + addSortedColumn(column, order); + multisort(); + } + + protected: + bool filterAcceptsRow(int source_row, const QModelIndex & source_parent) const override + { + for (int i = 0; i < sourceModel()->columnCount(source_parent); ++i) { + const QModelIndex currntIndex = sourceModel()->index(source_row, i, source_parent); + for (auto regExpIter = m_filters.constBegin(); regExpIter != m_filters.constEnd(); ++regExpIter) { + if (static_cast(regExpIter.key() >> 32) == i) { + if (regExpIter.value().indexIn(currntIndex.data(static_cast(regExpIter.key() & ((1i64 << 32) - 1))).toString().trimmed()) < 0) + return false; + } + } + } + return true; + } + + bool lessThan(const QModelIndex & left, const QModelIndex & right) const override { + if (m_useAlphanumSort) + return AlphanumComparer::lessThan(sourceModel()->data(left).toString(), sourceModel()->data(right).toString()); + return QSortFilterProxyModel::lessThan(left, right); + } + + virtual void multisort() { + // Perform the actual sort using superclass method in reverse order of currently sorted columns. + for(auto col = m_sortedColumnsOrder.crbegin(); col != m_sortedColumnsOrder.crend(); ++col) + sort(*col, m_sortedColumns.value(*col).sortOrder); + } + + private: + void addSortedColumn(int col, Qt::SortOrder order = Qt::AscendingOrder) { + m_sortedColumns.insert(col, { order, 1 }); + m_sortedColumnsOrder.append(col); + } + + void removeSortedColumn(int col) { + m_sortedColumns.remove(col); + m_sortedColumnsOrder.removeAll(col); + } + + inline void clearSortedColumns() { + m_sortedColumns.clear(); + m_sortedColumnsOrder.clear(); + } + + void changeSortDirection(int col, quint8 activations = 1) { + m_sortedColumns.insert(col, { reverseOrder(m_sortedColumns.value(col).sortOrder), activations }); + } + + void setSortDirection(int col, Qt::SortOrder order, quint8 activations = 1) { + m_sortedColumns.insert(col, { order, activations }); + } + + private: + QHash m_filters {}; + + struct SortTrack { + Qt::SortOrder sortOrder = Qt::AscendingOrder; + quint8 activations = 1; + }; + QHash m_sortedColumns; + QVector m_sortedColumnsOrder; + bool m_useAlphanumSort = true; + + static Qt::SortOrder reverseOrder(Qt::SortOrder ord) { + return ord == Qt::AscendingOrder ? Qt::DescendingOrder : Qt::AscendingOrder; + } + + static QString toSuperScript(int number) { + QString n = QString::number(number); + for (int i=0, e=n.length(); i < e; ++i) { + switch (n[i].toLatin1()) { + case '1': n.replace(i, 1, "¹"); break; + case '2': n.replace(i, 1, "²"); break; + case '3': n.replace(i, 1, "³"); break; + case '4': n.replace(i, 1, "⁴"); break; + case '5': n.replace(i, 1, "⁵"); break; + case '6': n.replace(i, 1, "⁶"); break; + case '7': n.replace(i, 1, "⁷"); break; + case '8': n.replace(i, 1, "⁸"); break; + case '9': n.replace(i, 1, "⁹"); break; + case '0': n.replace(i, 1, "⁰"); break; + } + } + return n; + } +}; diff --git a/src/WASimUI/resources/MSFS_SDK_Doc_Import.sqlite3 b/src/WASimUI/resources/MSFS_SDK_Doc_Import.sqlite3 new file mode 100644 index 0000000..bb0deac Binary files /dev/null and b/src/WASimUI/resources/MSFS_SDK_Doc_Import.sqlite3 differ diff --git a/src/WASimUI/ActionPushButton.cpp b/src/WASimUI/widgets/ActionPushButton.cpp similarity index 85% rename from src/WASimUI/ActionPushButton.cpp rename to src/WASimUI/widgets/ActionPushButton.cpp index 990b08a..4278b96 100644 --- a/src/WASimUI/ActionPushButton.cpp +++ b/src/WASimUI/widgets/ActionPushButton.cpp @@ -74,17 +74,15 @@ void ActionPushButton::paintEvent(QPaintEvent *e) QStylePainter p(this); QStyleOptionButton option; initStyleOption(&option); - if (!menu() && m_defaultAction && m_defaultAction->menu()) + if (!menu() && m_menuFromAction) option.features |= QStyleOptionButton::HasMenu; p.drawControl(QStyle::CE_PushButton, option); } void ActionPushButton::nextCheckState() { - if (!!m_defaultAction) - m_defaultAction->trigger(); - else - QPushButton::nextCheckState(); + if (!m_defaultAction && isCheckable()) + setChecked(!isChecked()); } void ActionPushButton::updateFromAction(QAction *action) @@ -123,8 +121,13 @@ void ActionPushButton::setDefaultAction(QAction *action) if (m_defaultAction == action) return; - if (m_menuFromAction && m_defaultAction->menu()) - disconnect(action->menu()->menuAction(), 0, this, 0); + bool hadDefault = !!m_defaultAction; + if (!!m_defaultAction) { + disconnect(this, &QPushButton::clicked, this, &ActionPushButton::onClicked); + if (m_menuFromAction && m_defaultAction->menu()) + disconnect(m_defaultAction->menu()->menuAction(), 0, this, 0); + } + m_menuFromAction = false; m_defaultAction = action; if (!action) @@ -133,6 +136,18 @@ void ActionPushButton::setDefaultAction(QAction *action) if (!actions().contains(action)) addAction(action); updateFromAction(action); + + connect(this, &QPushButton::clicked, this, &ActionPushButton::onClicked); +} + +void ActionPushButton::onClicked(bool checked) +{ + if (!m_defaultAction) + return; + if (isCheckable()) + m_defaultAction->setChecked(checked); + else + m_defaultAction->trigger(); } void ActionPushButton::onActionTriggered() diff --git a/src/WASimUI/ActionPushButton.h b/src/WASimUI/widgets/ActionPushButton.h similarity index 98% rename from src/WASimUI/ActionPushButton.h rename to src/WASimUI/widgets/ActionPushButton.h index ba72bca..260f31c 100644 --- a/src/WASimUI/ActionPushButton.h +++ b/src/WASimUI/widgets/ActionPushButton.h @@ -75,6 +75,7 @@ class ActionPushButton : public QPushButton private slots: void updateFromAction(QAction *action); + void onClicked(bool checked = false); void onActionTriggered(); private: diff --git a/src/WASimUI/widgets/BuddyLabel.h b/src/WASimUI/widgets/BuddyLabel.h new file mode 100644 index 0000000..67763a8 --- /dev/null +++ b/src/WASimUI/widgets/BuddyLabel.h @@ -0,0 +1,139 @@ +/* + BuddyLabel + https://github.com/mpaperno/maxLibQt + + COPYRIGHT: (c)2019 Maxim Paperno; All Right Reserved. + Contact: http://www.WorldDesign.com/contact + + LICENSE: + + Commercial License Usage + Licensees holding valid commercial licenses may use this file in + accordance with the terms contained in a written agreement between + you and the copyright holder. + + GNU General Public License Usage + Alternatively, this file may be used under the terms of the GNU + General Public License as published by the Free Software Foundation, + either version 3 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + A copy of the GNU General Public License is available at . +*/ + +#ifndef BUDDYLABEL_H +#define BUDDYLABEL_H + +#include +#include +#include +#include +#include +#include + +/*! + \brief The BuddyLabel class is a QLabel with enhanced "buddy" capabilities. + + It overrides the \c QLabel::setBuddy() method and, besides the usual shortcut handling provided by \c QLabel, + it adds mouse click handling and mirroring of the buddy's tool tip text. + + Mouse clicks are connected to the \c QWidget::setFocus slot. For \c QCheckBox it also connects to the \c click() slot so the box can be (un)checked by clicking on the label. + Mouse double-clicks are connected to \c QLineEdit::selectAll() on widgets which either are or have a \c QLineEdit (like \c QAbstractSpinBox and editable \c QComboBox). + Custom connections could be added by connecting to the \c clicked() and/or \c doubleClicked() signals, or inheriting and overriding the \c connectBuddy() virtual method. +*/ +class BuddyLabel : public QLabel +{ + Q_OBJECT + public: + using QLabel::QLabel; + + public slots: + //! Overrides the \c QLabel::setBuddy() method, which isn't virtual. Calls the base class implementation as well, so the shortcut mechanism still works. + void setBuddy(QWidget *buddy) + { + if (this->buddy()) { + this->buddy()->removeEventFilter(this); + disconnect(this->buddy()); + disconnectBuddy(this->buddy()); + } + + QLabel::setBuddy(buddy); + + if (!buddy) + return; + + setToolTip(buddy->toolTip()); + buddy->installEventFilter(this); + connectBuddy(buddy); + } + + signals: + //! Emitted when label is clicked with left mouse button (or something emulating one). + void clicked(); + //! Emitted when label is double-clicked with left mouse button (or something emulating one). + void doubleClicked(); + + protected: + //! Override this method for custom connections. + virtual void connectBuddy(QWidget *buddy) + { + // Single clicks + connect(this, &BuddyLabel::clicked, buddy, QOverload<>::of(&QWidget::setFocus)); + if (QCheckBox *cb = qobject_cast(buddy)) + connect(this, &BuddyLabel::clicked, cb, &QCheckBox::click); + + // Double clicks + if (QLineEdit *le = qobject_cast(buddy)) + connect(this, &BuddyLabel::doubleClicked, le, &QLineEdit::selectAll); + else if (QAbstractSpinBox *sb = qobject_cast(buddy)) + connect(this, &BuddyLabel::doubleClicked, sb, &QAbstractSpinBox::selectAll); + else if (QComboBox *cb = qobject_cast(buddy)) + if (cb->isEditable() && cb->lineEdit()) + connect(this, &BuddyLabel::doubleClicked, cb->lineEdit(), &QLineEdit::selectAll); + } + + //! Hook for custom disconnections. We already disconnect ourselves from all slots in \a buddy in the main handler. + virtual void disconnectBuddy(QWidget *buddy) { Q_UNUSED(buddy) } + + //! The filter monitors for tool tip changes on the buddy + bool eventFilter(QObject *obj, QEvent *ev) + { + if (ev->type() == QEvent::ToolTipChange && buddy() && obj == buddy()) + setToolTip(buddy()->toolTip()); + return false; + } + + void mousePressEvent(QMouseEvent *ev) + { + if (ev->button() == Qt::LeftButton) { + m_pressed = true; + ev->accept(); + } + QLabel::mousePressEvent(ev); + } + + void mouseReleaseEvent(QMouseEvent *ev) + { + if (m_pressed && rect().contains(ev->pos())) + emit clicked(); + m_pressed = false; + QLabel::mouseReleaseEvent(ev); + } + + void mouseDoubleClickEvent(QMouseEvent *ev) + { + if (ev->button() == Qt::LeftButton && rect().contains(ev->pos())) + emit doubleClicked(); + QLabel::mouseDoubleClickEvent(ev); + } + + private: + bool m_pressed = false; + Q_DISABLE_COPY(BuddyLabel) +}; + +#endif // BUDDYLABEL_H diff --git a/src/WASimUI/widgets/CustomTableView.h b/src/WASimUI/widgets/CustomTableView.h new file mode 100644 index 0000000..1d11c7f --- /dev/null +++ b/src/WASimUI/widgets/CustomTableView.h @@ -0,0 +1,294 @@ +/* +This file is part of the WASimCommander project. +https://github.com/mpaperno/WASimCommander + +COPYRIGHT: (c) Maxim Paperno; All Rights Reserved. + +This file may be used under the terms of the GNU General Public License (GPL) +as published by the Free Software Foundation, either version 3 of the Licenses, +or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +A copy of the GNU GPL is included with this project +and is also available at . +*/ + +#pragma once + +#include +#include +#include +//#include + +#include "MultisortTableView.h" +#include "FilterTableHeader.h" + +namespace WASimUiNS +{ + + class CustomTableView : public MultisortTableView + { + Q_OBJECT + + public: + CustomTableView(QWidget *parent) + : MultisortTableView(parent), + m_defaultFontSize{font().pointSize()}, + m_headerToggleMenu{new QMenu(tr("Toggle table columns"), this)}, + m_fontSizeMenu{new QMenu(tr("Adjust table font size"), this)}, + m_toggleFilterAction(new QAction(tr("Toggle column filters"), this)) + { + setObjectName(QStringLiteral("CustomTableView")); + + setContextMenuPolicy(Qt::ActionsContextMenu); + setEditTriggers(DoubleClicked | SelectedClicked | AnyKeyPressed); + setSelectionMode(ExtendedSelection); + setSelectionBehavior(SelectRows); + setVerticalScrollMode(ScrollPerItem); + setHorizontalScrollMode(ScrollPerPixel); + setGridStyle(Qt::DotLine); + setSortingEnabled(true); + setWordWrap(false); + setCornerButtonEnabled(false); + + verticalHeader()->setVisible(false); + verticalHeader()->setSectionResizeMode(QHeaderView::Fixed); + verticalHeader()->setMinimumSectionSize(10); + adjustRowSize(); + + FilterTableHeader *hdr = new FilterTableHeader(this); + setHorizontalHeader(hdr); + + m_headerToggleMenu->setIcon(QIcon(QStringLiteral("view_column.glyph"))); + m_fontSizeMenu->setIcon(QIcon(QStringLiteral("format_size.glyph"))); + + QIcon fltIcon(QStringLiteral("filter_list_off.glyph")); + fltIcon.addFile(QStringLiteral("filter_list.glyph"), QSize(), QIcon::Normal, QIcon::On); + m_toggleFilterAction->setIcon(fltIcon); + m_toggleFilterAction->setCheckable(true); + m_toggleFilterAction->setChecked(hdr->areFiltersVisible()); + connect(m_toggleFilterAction, &QAction::toggled, this, &CustomTableView::setFiltersVisible); + + QAction *plusAct = m_fontSizeMenu->addAction(QIcon("arrow_upward.glyph"), tr("Increase font size"), this, &CustomTableView::fontSizeInc); + plusAct->setShortcuts({ QKeySequence::ZoomIn, QKeySequence(Qt::ControlModifier | Qt::Key_Equal) }); + m_fontSizeMenu->addAction(QIcon("restart_alt.glyph"), tr("Reset font size"), this, &CustomTableView::fontSizeReset, QKeySequence(Qt::ControlModifier | Qt::Key_0)); + m_fontSizeMenu->addAction(QIcon("arrow_downward.glyph"), tr("Decrease font size"), this, &CustomTableView::fontSizeDec, QKeySequence::ZoomOut); + } + + QHeaderView *header() const { return horizontalHeader(); } + QAction *columnToggleMenuAction() const { return m_headerToggleMenu->menuAction(); } + QAction *fontSizeMenuAction() const { return m_fontSizeMenu->menuAction(); } + QAction *filterToggleAction() const { return m_toggleFilterAction; } + QMenu *actionsMenu(QWidget *parent) + { + QMenu *menu = new QMenu(tr("Table Header Options"), parent); + menu->setIcon(QIcon(QStringLiteral("table_rows.glyph"))); + menu->addAction(filterToggleAction()); + menu->addSeparator(); + menu->addAction(columnToggleMenuAction()); + menu->addSeparator(); + menu->addAction(fontSizeMenuAction()); + return menu; + } + + bool areFiltersVisible() const { + if (FilterTableHeader *fth = qobject_cast(header())) + return fth->areFiltersVisible(); + return false; + } + + QByteArray saveState() const { + QByteArray state; + QDataStream ds(&state, QIODevice::WriteOnly); + ds << header()->saveState(); + ds << font().pointSize(); + ds << areFiltersVisible(); + return state; + } + + public Q_SLOTS: + void setModel(QAbstractItemModel *model) override + { + MultisortTableView::setModel(model); + buildHeaderActions(); + } + + void setHorizontalHeader(QHeaderView *hdr) override + { + MultisortTableView::setHorizontalHeader(hdr); + setupHeader(); + } + + void moveColumn(int from, int to) const { horizontalHeader()->moveSection(from, to); } + + void setFiltersVisible(bool en = true) { + if (FilterTableHeader *fth = qobject_cast(header())) + fth->setFiltersVisible(en); + m_toggleFilterAction->setChecked(en); + } + + void setFilterFocus(int column) { + if (FilterTableHeader *fth = qobject_cast(header())) + fth->setFocusColumn(column); + } + + void setFilterText(int column, const QString& value){ + if (FilterTableHeader *fth = qobject_cast(header())) + fth->setFilter(column, value); + } + + void clearFilters() { + if (FilterTableHeader *fth = qobject_cast(header())) + fth->clearFilters(); + } + + void setFontSize(int pointSize) { + QFont f = font(); + if (pointSize > 3 && f.pointSize() != pointSize) { + f.setPointSize(pointSize); + setFont(f); + adjustRowSize(); + } + } + + void fontSizeReset() { + QFont f = font(); + if (f.pointSize() != m_defaultFontSize) + setFontSize(m_defaultFontSize); + } + void fontSizeInc() { + setFontSize(font().pointSize() + 1); + } + void fontSizeDec() { + if (font().pointSize() > 3) + setFontSize(font().pointSize() - 1); + } + + void adjustRowSize() { + verticalHeader()->setDefaultSectionSize(QFontMetrics(font()).lineSpacing() * 1.65); + } + + bool restoreState(const QByteArray &state) + { + if (!model() || state.isEmpty()) + return false; + + QByteArray hdrState; + int fontSize = m_defaultFontSize; + bool fltVis = areFiltersVisible(); + QHeaderView *hdr = horizontalHeader(); + + QDataStream ds(state); + if (!ds.atEnd()) { + ds >> hdrState; + if (!ds.atEnd()) + ds >> fontSize; + if (!ds.atEnd()) + ds >> fltVis; + } + else { + hdrState = state; + } + + setFontSize(fontSize); + hdr->restoreState(hdrState); + setFiltersVisible(fltVis); + + for (int i = 0; i < model()->columnCount() && i < hdr->actions().length(); ++i) + hdr->actions().at(i)->setChecked(!hdr->isSectionHidden(i)); + hdr->setSortIndicatorShown(false); // in case it was saved as "shown" for some reason + setSortingEnabled(isSortingEnabled()); // update sort indicator + return true; + } + + Q_SIGNALS: + void filterChanged(int column, QString value); + + private: + + void setupHeader() + { + QHeaderView *hdr = horizontalHeader(); + hdr->setCascadingSectionResizes(false); + hdr->setMinimumSectionSize(20); + hdr->setDefaultSectionSize(80); + hdr->setHighlightSections(false); + hdr->setStretchLastSection(true); + hdr->setSectionsMovable(true); + hdr->setSectionResizeMode(QHeaderView::Interactive); + hdr->setContextMenuPolicy(Qt::ActionsContextMenu); + hdr->setToolTip(tr( + "

" + "- CTRL-click to sort on multiple columns.
" + "- Right-click for menu to toggle column visibility.
" + "- Click-and-drag headings to re-arrange columns.
" + "- Double-click dividers to adjust column width to fit widest content.
" + "

" + )); + // make sure header has own font size so it doesn't change when grid font is changed + QFont f(font()); + f.setPointSize(m_defaultFontSize); + hdr->setFont(f); + + connect(hdr, &QHeaderView::sectionCountChanged, this, &CustomTableView::onSectionCountChanged, Qt::QueuedConnection); + if (FilterTableHeader *fth = qobject_cast(hdr)) + connect(fth, &FilterTableHeader::filterChanged, this, &CustomTableView::setDisplayRoleStringFilter); + } + + void onSectionCountChanged(int oldCnt, int newCnt) + { + //qDebug() << oldCnt << newCnt; + if (oldCnt != newCnt && newCnt != horizontalHeader()->actions().length()) + buildHeaderActions(); + } + + void onHeaderToggled(bool on) + { + if (QAction *act = qobject_cast(sender())) { + bool ok; + int id = act->property("col").toInt(&ok); + if (ok && id > -1) + horizontalHeader()->setSectionHidden(id, !on); + } + } + + void buildHeaderActions() + { + for (int i=0, e=m_headerToggleMenu->actions().length(); i < e; ++i) { + if (QAction *act = m_headerToggleMenu->actions().value(0, nullptr)) { + m_headerToggleMenu->removeAction(act); + disconnect(act, nullptr, this, nullptr); + act->deleteLater(); + } + } + + if (!model()) + return; + + QHeaderView *hdr = horizontalHeader(); + hdr->removeAction(m_toggleFilterAction); + + for (int i=0; i < model()->columnCount(); ++i) { + QAction *act = m_headerToggleMenu->addAction(model()->headerData(i, Qt::Horizontal, Qt::EditRole).toString(), this, &CustomTableView::onHeaderToggled); + act->setCheckable(true); + act->setChecked(!hdr->isSectionHidden(i)); + act->setProperty("col", i); + } + hdr->addActions(m_headerToggleMenu->actions()); + hdr->addAction(m_toggleFilterAction); + + if (FilterTableHeader *fth = qobject_cast(hdr)) + fth->generateFilters(model()->columnCount()); + } + + int m_defaultFontSize; + QMenu *m_headerToggleMenu; + QMenu *m_fontSizeMenu; + QAction *m_toggleFilterAction; + }; + +} diff --git a/src/WASimUI/DataComboBox.h b/src/WASimUI/widgets/DataComboBox.h similarity index 80% rename from src/WASimUI/DataComboBox.h rename to src/WASimUI/widgets/DataComboBox.h index 51e4a6f..6262cf8 100644 --- a/src/WASimUI/DataComboBox.h +++ b/src/WASimUI/widgets/DataComboBox.h @@ -53,7 +53,7 @@ class DataComboBox: public QComboBox explicit DataComboBox(QWidget *parent = nullptr): QComboBox(parent) { - connect(this, SIGNAL(currentIndexChanged(int)), this, SLOT(onCurrentIndexChanged(int))); + connect(this, QOverload::of(&QComboBox::currentIndexChanged), this, &DataComboBox::onCurrentIndexChanged); } //! The Role id to use for data operations unless otherwise specified. Default is \c Qt::UserRole. \sa setDefaultRole() @@ -69,25 +69,43 @@ class DataComboBox: public QComboBox return QComboBox::currentData(role); } + //! Adds an item like `QComboBox::addItem(const QString &, const QVariant &)` does but allows specifying the role. + void addItem(const QString &text, const QVariant &data, int role) { + QComboBox::addItem(text); + setItemData(count() - 1, data, role); + } + //! Add items with names from \a texts, with corresponding data from \a datas list, using the specified \a role. If \a role is \c -1 (default) then the \p defaultRole is used. void addItems(const QStringList &texts, const QVariantList &datas, int role = -1) { if (role < 0) role = m_role; - const int c = count(); - for (int i=0; i < texts.count(); ++i) { - addItem(texts.at(i)); - setItemData(i + c, datas.value(i, texts.at(i)), role); - } + for (int i=0; i < texts.count(); ++i) + addItem(texts.at(i), datas.value(i, texts.at(i)), role); } //! Add items with corresponding data from \a items map, using the specified \a role. If \a role is \c -1 (default) then the \p defaultRole is used. - void addItems(const QMap &items, int role = -1) + void addItems(const QMap &items, int role = -1) { + addItems(items.keys(), items.values(), role); + } + + //! Add items with names from \a texts, with corresponding data from \a datas list, using the specified \a role. If \a role is \c -1 (default) then the \p defaultRole is used. + void addItems(const QStringList &texts, const QStringList &datas, int role = -1) { + if (role < 0) + role = m_role; + for (int i=0; i < texts.count(); ++i) + addItem(texts.at(i), datas.value(i, texts.at(i)), role); + } + + //! Add items with corresponding data from \a items map, using the specified \a role. If \a role is \c -1 (default) then the \p defaultRole is used. + void addItems(const QMap &items, int role = -1) { addItems(items.keys(), items.values(), role); } - using QComboBox::addItems; // bring back the superclass version + // bring back the superclass versions + using QComboBox::addItem; + using QComboBox::addItems; public slots: //! Convenience slot for \c QComboBox::setCurrentIndex(findData(value, role, matchFlags)) diff --git a/src/WASimUI/widgets/DeletableItemsComboBox.h b/src/WASimUI/widgets/DeletableItemsComboBox.h new file mode 100644 index 0000000..839c424 --- /dev/null +++ b/src/WASimUI/widgets/DeletableItemsComboBox.h @@ -0,0 +1,380 @@ +/* +DeletableItemsComboBox +https://github.com/mpaperno/WASimCommander + +COPYRIGHT: (c)2022 Maxim Paperno; All Right Reserved. +Contact: http://www.WorldDesign.com/contact + +LICENSE: + +Commercial License Usage +Licensees holding valid commercial licenses may use this file in +accordance with the terms contained in a written agreement between +you and the copyright holder. + +GNU General Public License Usage +Alternatively, this file may be used under the terms of the GNU +General Public License as published by the Free Software Foundation, +either version 3 of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +A copy of the GNU General Public License is available at . +*/ + +#pragma once + +#include +#include +#include +#include +#include + +#include "DataComboBox.h" + +class DeletableItemsComboBox : public DataComboBox +{ + Q_OBJECT + Q_PROPERTY(QString placeholderText READ placeholderText WRITE setPlaceholderText) +public: + DeletableItemsComboBox(QWidget *p = nullptr) : DataComboBox(p) + { + DataComboBox::setEditable(true); + setInsertPolicy(InsertAtTop); + setSizeAdjustPolicy(AdjustToContents); + //setMinimumContentsLength(25); + setMaxVisibleItems(25); + setCurrentIndex(-1); + setToolTip(tr( + "

Press enter after entering text to save it in the list for future selection.

" + "

Saved items can be removed by right-clicking on them while the list is open.

" + )); + + m_defaultCompleter = completer(); + m_defaultCompleter->setParent(this); // Set a parent so QLineEdit doesn't delete it when removing. + m_completerFilter = m_defaultCompleter->filterMode(); + m_completerMode = m_defaultCompleter->completionMode(); + m_completerEnabled = true; + + setContextMenuPolicy(Qt::CustomContextMenu); + connect(this, &DataComboBox::customContextMenuRequested, this, &DeletableItemsComboBox::showContextMenu); + + connect(lineEdit(), &QWidget::destroyed, this, &DeletableItemsComboBox::updateCompleterOption, Qt::UniqueConnection); + + connect(this, &DeletableItemsComboBox::editTextChanged, this, [this](const QString &txt) { + if (txt.isEmpty()) + setCurrentIndex(-1); + }); + connect(view(), &QAbstractItemView::pressed, [this](const QModelIndex &idx) { + if (idx.isValid() && !idx.data(Qt::UserRole).isValid() && (QApplication::mouseButtons() & Qt::RightButton)) + model()->removeRow(idx.row()); + }); + + // Completer options actions/menu + QMenu *compMenu = new QMenu(this); + auto addAct = [=](const QString &t, int role, int d, const QString &i) { + QAction *a = compMenu->addAction(QIcon(i + ".glyph"), t); + a->setCheckable(true); + a->setData(int(d)); + a->setProperty("role", role); + compMenu->addAction(a); + return a; + }; + addAct(tr("Disable Suggestions"), 0, 1, "not_interested"); + compMenu->addSeparator(); + addAct(tr("Match Starts With"), 1, (int)Qt::MatchStartsWith, "start")->setChecked(m_completerFilter == Qt::MatchStartsWith); + addAct(tr("Match Contains"), 1, (int)Qt::MatchContains, "rotate=90/vertical_align_center")->setChecked(m_completerFilter == Qt::MatchContains); + addAct(tr("Match Ends With"), 1, (int)Qt::MatchEndsWith, "rotate=180/start")->setChecked(m_completerFilter == Qt::MatchEndsWith); + compMenu->addSeparator(); + addAct(tr("Show inline suggestions"), 2, (int)QCompleter::InlineCompletion, "Material Icons,9,5,50,1,1,0,0,0/title")->setChecked(m_completerMode == QCompleter::InlineCompletion); + addAct(tr("Show pop-up suggestions"), 2, (int)QCompleter::PopupCompletion, "IcoMoon-Free/option")->setChecked(m_completerMode == QCompleter::PopupCompletion); + addAct(tr("Show unfiltered pop-up suggestions"), 2, (int)QCompleter::UnfilteredPopupCompletion, "filter_list_off")->setChecked(m_completerMode == QCompleter::UnfilteredPopupCompletion); + + connect(compMenu, &QMenu::triggered, this, &DeletableItemsComboBox::onCompleterOptionAction); + + m_completerAction = new QAction(QIcon("manage_search.glyph"), tr("Typing Suggestions"), this); + m_completerAction->setToolTip(tr("Set options for the suggestions while you type are determined and presented.")); + m_completerAction->setMenu(compMenu); + addAction(compMenu->menuAction()); + } + + QAction *completerChoicesMenuAction() const { return m_completerAction->menu()->menuAction(); } + QString placeholderText() const { return lineEdit() ? lineEdit()->placeholderText() : ""; } + + const QStringList editedItems() const + { + QStringList ret; + if (!isEditable()) + return ret; + for (int i = 0, e = count(); i < e; ++i) { + if (!itemData(i).isValid() && !itemText(i).isEmpty()) + ret << itemText(i); + } + return ret; + } + + QByteArray saveState() const + { + QByteArray state; + QDataStream ds(&state, QIODevice::WriteOnly); + const QStringList items = editedItems(); + ds << items.count(); + for (const QString &itm : items) + ds << itm; + ds << (int)m_completerFilter << (int)m_completerMode << (int)m_completerEnabled; + return state; + } + + bool restoreState(const QByteArray &state) + { + if (state.isEmpty()) + return false; + + QDataStream ds(state); + // Restore any saved list items + int itemCount; + ds >> itemCount; + if (itemCount > 0 && !ds.atEnd()) { + QStringList items; + items.reserve(itemCount); + QString item; + for (int i = 0; i < itemCount && !ds.atEnd(); ++i) { + ds >> item; + items << item; + } + insertEditedItems(items); + } + // Restore completer options if there are any + if (!ds.atEnd()) { + int tmp; + ds >> tmp; + if (tmp != (int)m_completerFilter) + setCompleterFilterMode((Qt::MatchFlags)tmp); + ds >> tmp; + if (tmp != (int)m_completerMode) + setCompleterCompletionMode((QCompleter::CompletionMode)tmp); + if (!ds.atEnd()) { + ds >> tmp; + if ((bool)tmp != m_completerEnabled && !tmp) + setCompleterDisabled(); + } + } + + return true; + } + +public Q_SLOTS: + + void setClearButtonEnabled(bool enabled = true) { + if (lineEdit()) lineEdit()->setClearButtonEnabled(enabled); + } + + void setPlaceholderText(const QString &text) { + if (lineEdit()) + lineEdit()->setPlaceholderText(text); + } + + void setMaxLength(int length) { + if (lineEdit()) + lineEdit()->setMaxLength(length); + } + + void setCompleterOptionsButtonEnabled(bool en = true) + { + m_completerButtonEnabled = en; + + if (!lineEdit()) + return; + + if (!en) + lineEdit()->removeAction(m_completerAction); + else if (!lineEdit()->actions().contains(m_completerAction)) + lineEdit()->addAction(m_completerAction, QLineEdit::TrailingPosition); + } + + void setEditable(bool on = true) { + if (on != isEditable()) { + DataComboBox::setEditable(on); + onLineEditChange(); + } + } + + void setLineEdit(QLineEdit *le) { + DataComboBox::setLineEdit(le); + onLineEditChange(); + } + + // Reimplemented to save the last set completer for toggling it on/off. + // There's apparently no way to temporarily suspend a completer. + void setCompleter(QCompleter *c, bool setOptionsFromCompleter = false) + { + if (c == m_defaultCompleter) { + resetCompleter(); + return; + } + // Make sure the line editor doesn't own the completer otherwise it will delete it when removing. + if (c && (!c->parent() || c->parent() == lineEdit())) + c->setParent(this); + m_customCompleter = c; + if (m_completerEnabled || setOptionsFromCompleter) { + m_completerEnabled = true; + DataComboBox::setCompleter(c); + } + if (!c || setOptionsFromCompleter) { + updateCompleterOption(); + return; + } + if ((c = completer())) { + c->setFilterMode(m_completerFilter); + c->setCompletionMode(m_completerMode); + } + } + + // Reset completer back to the QComboBox default one. + void resetCompleter() { + DataComboBox::setCompleter(m_defaultCompleter); + updateCompleterOption(); + } + + // Sets the completer to either a custom one provided previously in `setCompleter()` or sets up the default completer. + void setCompleterEnabled(bool enabled = true) { + if (enabled) { + DataComboBox::setCompleter(!!m_customCompleter ? m_customCompleter : m_defaultCompleter); + updateCompleterOption(); + } + else { + setCompleterDisabled(); + } + } + + void setCompleterDisabled(bool disabled = true) { + if (disabled) { + DataComboBox::setCompleter(nullptr); + updateCompleterOption(); + } + else { + setCompleterEnabled(); + } + } + void setCompleterFilterMode(Qt::MatchFlags flags) + { + if (!completer()) + setCompleterEnabled(); + completer()->setFilterMode(flags); + updateCompleterOption(); + } + + void setCompleterCompletionMode(QCompleter::CompletionMode mode) + { + if (!completer()) + setCompleterEnabled(); + completer()->setCompletionMode(mode); + updateCompleterOption(); + } + + void setCompleterOptions(Qt::MatchFlags flags, QCompleter::CompletionMode mode, bool enableButton = true) { + setCompleterFilterMode(flags); + setCompleterCompletionMode(mode); + setCompleterOptionsButtonEnabled(enableButton); + } + + void insertEditedItems(const QStringList &items, InsertPolicy policy = NoInsert) + { + if (!isEditable() || insertPolicy() == NoInsert || !items.length()) + return; + if (policy == NoInsert) + policy = insertPolicy(); + + const int currIdx = currentIndex(); + if (policy == InsertAlphabetically) { + addItems(items); + model()->sort(0); + } + else { + int index = 0; + switch (policy) { + case InsertAtTop: + break; + case InsertAtBottom: + index = count(); + break; + case InsertAfterCurrent: + index = qMax(0, currentIndex()); + break; + case InsertBeforeCurrent: + index = qMax(0, currentIndex() - 1); + break; + } + QStringList sorted(items); + std::sort(sorted.begin(), sorted.end()); + insertItems(index, sorted); + } + if (currIdx == -1) + setCurrentIndex(currIdx); + } + +private Q_SLOTS: + void onLineEditChange() { + if (lineEdit()) { + setCompleterOptionsButtonEnabled(m_completerButtonEnabled); + connect(lineEdit(), &QWidget::destroyed, this, &DeletableItemsComboBox::updateCompleterOption, Qt::UniqueConnection); + } + updateCompleterOption(); + } + + void onCompleterOptionAction(QAction *act) { + const int role = act->property("role").toInt(); + if (role == 1) + setCompleterFilterMode((Qt::MatchFlags)act->data().toInt()); + else if (role == 2) + setCompleterCompletionMode((QCompleter::CompletionMode)act->data().toInt()); + else + setCompleterDisabled(); + } + + void updateCompleterOption() + { + if (completer()) { + m_completerEnabled = true; + m_completerFilter = completer()->filterMode(); + m_completerMode = completer()->completionMode(); + } + else { + m_completerEnabled = false; + m_completerAction->setEnabled(!!lineEdit()); + } + + for (QAction *a : m_completerAction->menu()->actions()) { + int role = a->property("role").toInt(); + int data = a->data().toInt(); + a->setChecked( + (role == 0 && (!m_completerEnabled || !completer())) || + (role == 1 && data == m_completerFilter && m_completerEnabled) || + (role == 2 && data == m_completerMode && m_completerEnabled) + ); + } + } + + void showContextMenu(const QPoint &pos) + { + if (!lineEdit()) + return; + QMenu* menu = lineEdit()->createStandardContextMenu(); + menu->addSeparator(); + menu->addAction(m_completerAction->menu()->menuAction()); + menu->exec(mapToGlobal(pos)); + } + +private: + QCompleter *m_defaultCompleter = nullptr; + QCompleter *m_customCompleter = nullptr; + QAction *m_completerAction = nullptr; + Qt::MatchFlags m_completerFilter; + QCompleter::CompletionMode m_completerMode; + bool m_completerEnabled; + bool m_completerButtonEnabled = false; +}; + diff --git a/src/WASimUI/widgets/FilterLineEdit.cpp b/src/WASimUI/widgets/FilterLineEdit.cpp new file mode 100644 index 0000000..f6cdae0 --- /dev/null +++ b/src/WASimUI/widgets/FilterLineEdit.cpp @@ -0,0 +1,157 @@ +/* +This file is part of the WASimCommander project. +https://github.com/mpaperno/WASimCommander + +Original version from ; Used under GPL v3 license; Modifications applied. +COPYRIGHT: (c) Maxim Paperno; All Rights Reserved. + +This file may be used under the terms of the GNU General Public License (GPL) +as published by the Free Software Foundation, either version 3 of the Licenses, +or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +A copy of the GNU GPL is included with this project +and is also available at . +*/ + +#include "FilterLineEdit.h" + +#include +#include +#include +#include + +FilterLineEdit::FilterLineEdit(QWidget* parent, std::vector* filters, int columnnum) : + QLineEdit(parent), + filterList(filters), + columnNumber(columnnum) +{ + setPlaceholderText(tr("Filter")); + setClearButtonEnabled(true); + setProperty("column", columnnum); // Store the column number for later use + + // Introduce a timer for delaying the signal triggered whenever the user changes the filter value. + // The idea here is that the textChanged() event isn't connected to the update filter slot directly anymore + // but instead there this timer mechanism in between: whenever the user changes the filter the delay timer + // is (re)started. As soon as the user stops typing the timer has a chance to trigger and call the + // delayedSignalTimerTriggered() method which then stops the timer and emits the delayed signal. + delaySignalTimer = new QTimer(this); + delaySignalTimer->setInterval(200); // This is the milliseconds of not-typing we want to wait before triggering + connect(this, &FilterLineEdit::textChanged, delaySignalTimer, QOverload<>::of(&QTimer::start)); + connect(delaySignalTimer, &QTimer::timeout, this, &FilterLineEdit::delayedSignalTimerTriggered); + + const QString help(tr( + "

These input fields allow you to perform quick filters in the currently selected table.

" + "All filters are case-insensitive. By default a search is performed anywhere in the column's value (*text*).
" + "The following operators are also supported:
" + "" + "" + "" + "" + "" + "" + "
*Matches zero or more of any characters.
?Matches any single character.
\\* or \\?Matches a literal asterisk or question mark (\"escapes\" it).
[...]Sets of characters can be represented in square brackets, similar to full regular expressions.
/regexp/Values matching the regular expression between the slashes (/).
" + )); + + setToolTip(help); + setWhatsThis(help); + + // Immediately emit the delayed filter value changed signal if the user presses the enter or the return key or + // the line edit widget loses focus + connect(this, &FilterLineEdit::editingFinished, this, &FilterLineEdit::delayedSignalTimerTriggered); + + // Prepare for adding the What's This information and filter helper actions to the context menu + setContextMenuPolicy(Qt::CustomContextMenu); + connect(this, &FilterLineEdit::customContextMenuRequested, this, &FilterLineEdit::showContextMenu); +} + +void FilterLineEdit::delayedSignalTimerTriggered() +{ + // Stop the timer first to avoid triggering in intervals + delaySignalTimer->stop(); + + // Only emit text changed signal if the text has actually changed in comparison to the last emitted signal. This is necessary + // because this method is also called whenever the line edit loses focus and not only when its text has definitely been changed. + if(text() != lastValue) + { + // Emit the delayed signal using the current value + emit delayedTextChanged(columnNumber, text()); + + // Remember this value for the next time + lastValue = text(); + } +} + +void FilterLineEdit::keyReleaseEvent(QKeyEvent* event) +{ + if (filterList) { + if (event->key() == Qt::Key_Tab) { + if (columnNumber < filterList->size() - 1) + filterList->at((size_t)columnNumber + 1)->setFocus(); + else + filterList->at(0)->setFocus(); + return; + } + else if (event->key() == Qt::Key_Backtab && columnNumber > 0) { + filterList->at((size_t)columnNumber - 1)->setFocus(); + return; + } + } + + QLineEdit::keyReleaseEvent(event); +} + +void FilterLineEdit::focusInEvent(QFocusEvent* event) +{ + QLineEdit::focusInEvent(event); + emit filterFocused(); +} + +void FilterLineEdit::clear() +{ + // When programmatically clearing the line edit's value make sure the effects are applied immediately, i.e. + // bypass the delayed signal timer + QLineEdit::clear(); + delayedSignalTimerTriggered(); +} + +void FilterLineEdit::setText(const QString& text) +{ + // When programmatically setting the line edit's value make sure the effects are applied immediately, i.e. + // bypass the delayed signal timer + QLineEdit::setText(text); + delayedSignalTimerTriggered(); +} + +void FilterLineEdit::setFilterHelper(const QString& filterOperator, const QString& operatorSuffix, bool clearCurrent) +{ + const QString txt(clearCurrent ? "" : text()); + setText(txt + filterOperator + "?" + operatorSuffix); + // Select the value for easy editing of the expression + setSelection(filterOperator.length() + txt.length(), 1); +} + +void FilterLineEdit::showContextMenu(const QPoint &pos) +{ + + // This has to be created here, otherwise the set of enabled options would not update accordingly. + QMenu* editContextMenu = createStandardContextMenu(); + editContextMenu->addSeparator(); + + QMenu* filterMenu = editContextMenu->addMenu(tr("Set Filter Expression")); + + filterMenu->addAction(QIcon("help_outline.glyph"), tr("What's This?"), this, [&]() { + QWhatsThis::showText(pos, whatsThis(), this); + }); + filterMenu->addSeparator(); + filterMenu->addAction(tr("Starts with..."), this, [&]() { setFilterHelper(QString (""), QString("*")); }); + filterMenu->addAction(tr("... ends with"), this, [&]() { setFilterHelper(QString ("*"), QString(""), false); }); + filterMenu->addAction(tr("In range [...]"), this, [&]() { setFilterHelper(QString ("["), QString("]")); }); + filterMenu->addAction(tr("Regular expression..."), this, [&]() { setFilterHelper(QString ("/"), QString ("/")); }); + + editContextMenu->exec(mapToGlobal(pos)); +} diff --git a/src/WASimUI/widgets/FilterLineEdit.h b/src/WASimUI/widgets/FilterLineEdit.h new file mode 100644 index 0000000..098e7bc --- /dev/null +++ b/src/WASimUI/widgets/FilterLineEdit.h @@ -0,0 +1,59 @@ +/* +This file is part of the WASimCommander project. +https://github.com/mpaperno/WASimCommander + +Original version from ; Used under GPL v3 license; Modifications applied. +COPYRIGHT: (c) Maxim Paperno; All Rights Reserved. + +This file may be used under the terms of the GNU General Public License (GPL) +as published by the Free Software Foundation, either version 3 of the Licenses, +or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +A copy of the GNU GPL is included with this project +and is also available at . +*/ + +#pragma once + +#include +#include + +class QTimer; +class QKeyEvent; + +class FilterLineEdit : public QLineEdit +{ + Q_OBJECT + +public: + explicit FilterLineEdit(QWidget* parent, std::vector* filters = nullptr, int columnnum = 0); + + // Override methods for programmatically changing the value of the line edit + void clear(); + void setText(const QString& text); + +Q_SIGNALS: + void delayedTextChanged(int column, QString text); + void filterFocused(); + +protected: + void keyReleaseEvent(QKeyEvent* event) override; + void focusInEvent(QFocusEvent* event) override; + +private Q_SLOTS: + void delayedSignalTimerTriggered(); + void setFilterHelper(const QString& filterOperator, const QString& operatorSuffix = QString(), bool clearCurrent = true); + void showContextMenu(const QPoint &pos); + +private: + std::vector* filterList; + int columnNumber; + QTimer* delaySignalTimer; + QString lastValue; + +}; diff --git a/src/WASimUI/widgets/FilterTableHeader.cpp b/src/WASimUI/widgets/FilterTableHeader.cpp new file mode 100644 index 0000000..e9c067a --- /dev/null +++ b/src/WASimUI/widgets/FilterTableHeader.cpp @@ -0,0 +1,139 @@ +/* +This file is part of the WASimCommander project. +https://github.com/mpaperno/WASimCommander + +Original version from ; Used under GPL v3 license; Modifications applied. +COPYRIGHT: (c) Maxim Paperno; All Rights Reserved. + +This file may be used under the terms of the GNU General Public License (GPL) +as published by the Free Software Foundation, either version 3 of the Licenses, +or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +A copy of the GNU GPL is included with this project +and is also available at . +*/ + +#include "FilterTableHeader.h" +#include "FilterLineEdit.h" + +#include +#include +#include + +FilterTableHeader::FilterTableHeader(QTableView* parent) : + QHeaderView(Qt::Horizontal, parent) +{ + // Do some connects: Basically just resize and reposition the input widgets whenever anything changes + connect(this, &FilterTableHeader::sectionResized, this, &FilterTableHeader::adjustPositions); + connect(this, &FilterTableHeader::sectionClicked, this, &FilterTableHeader::adjustPositions); + connect(parent->horizontalScrollBar(), &QScrollBar::valueChanged, this, &FilterTableHeader::adjustPositions); + connect(parent->verticalScrollBar(), &QScrollBar::valueChanged, this, &FilterTableHeader::adjustPositions); +} + +void FilterTableHeader::generateFilters(int number) +{ + // Delete all the current filter widgets + qDeleteAll(filterWidgets); + filterWidgets.clear(); + + // And generate a bunch of new ones + for(int i=0; i < number; ++i) + { + FilterLineEdit* l = new FilterLineEdit(this, &filterWidgets, i); + l->setVisible(m_filtersVisible); + // Set as focus proxy the first non-row-id visible filter-line. + if(!i) + setFocusProxy(l); + connect(l, &FilterLineEdit::delayedTextChanged, this, &FilterTableHeader::inputChanged); + connect(l, &FilterLineEdit::filterFocused, this, &FilterTableHeader::filterFocused); + filterWidgets.push_back(l); + } + + // Position them correctly + updateGeometries(); +} + +QSize FilterTableHeader::sizeHint() const +{ + // For the size hint just take the value of the standard implementation and add the height of a input widget to it if necessary + QSize s = QHeaderView::sizeHint(); + if(m_filtersVisible && filterWidgets.size()) + s.setHeight(s.height() + filterWidgets.at(0)->sizeHint().height() + 4); // The 4 adds just adds some extra space + return s; +} + +void FilterTableHeader::updateGeometries() +{ + // If there are any input widgets add a viewport margin to the header to generate some empty space for them which is not affected by scrolling + if(m_filtersVisible && filterWidgets.size()) + setViewportMargins(0, 0, 0, filterWidgets.at(0)->sizeHint().height()); + else + setViewportMargins(0, 0, 0, 0); + + // Now just call the parent implementation and reposition the input widgets + QHeaderView::updateGeometries(); + adjustPositions(); +} + +void FilterTableHeader::adjustPositions() +{ + // The two adds some extra space between the header label and the input widget + const int y = QHeaderView::sizeHint().height() + 2; + // Loop through all widgets + for(int i=0, e = (int)filterWidgets.size(); i < e; ++i) { + // Get the current widget, move it and resize it + QWidget* w = filterWidgets.at((size_t)i); + if (QApplication::layoutDirection() == Qt::RightToLeft) + w->move(width() - (sectionPosition(i) + sectionSize(i) - offset()), y); + else + w->move(sectionPosition(i) - offset(), y); + w->resize(sectionSize(i), w->sizeHint().height()); + } +} + +void FilterTableHeader::inputChanged(int col, const QString& new_value) +{ + //adjustPositions(); + // Just get the column number and the new value and send them to anybody interested in filter changes + emit filterChanged(col, new_value); +} + +void FilterTableHeader::clearFilters() +{ + for(FilterLineEdit* filterLineEdit : filterWidgets) + filterLineEdit->clear(); +} + +void FilterTableHeader::setFilter(int column, const QString& value) +{ + if(column < filterWidgets.size()) + filterWidgets.at(column)->setText(value); +} + +QString FilterTableHeader::filterValue(int column) const +{ + return filterWidgets[column]->text(); +} + +void FilterTableHeader::setFocusColumn(int column) +{ + if(column < filterWidgets.size()) + filterWidgets.at(column)->setFocus(Qt::FocusReason::TabFocusReason); +} + +void FilterTableHeader::setFiltersVisible(bool visible) +{ + if (m_filtersVisible == visible) + return; + m_filtersVisible = visible; + for(FilterLineEdit* filterLineEdit : filterWidgets) + filterLineEdit->setVisible(visible); + if (!visible) + clearFilters(); + updateGeometries(); +} diff --git a/src/WASimUI/widgets/FilterTableHeader.h b/src/WASimUI/widgets/FilterTableHeader.h new file mode 100644 index 0000000..34c3014 --- /dev/null +++ b/src/WASimUI/widgets/FilterTableHeader.h @@ -0,0 +1,64 @@ +/* +This file is part of the WASimCommander project. +https://github.com/mpaperno/WASimCommander + +Original version from ; Used under GPL v3 license; Modifications applied. +COPYRIGHT: (c) Maxim Paperno; All Rights Reserved. + +This file may be used under the terms of the GNU General Public License (GPL) +as published by the Free Software Foundation, either version 3 of the Licenses, +or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +A copy of the GNU GPL is included with this project +and is also available at . +*/ + +#ifndef FILTERTABLEHEADER_H +#define FILTERTABLEHEADER_H + +#include +#include + +class QTableView; +class FilterLineEdit; + +class FilterTableHeader : public QHeaderView +{ + Q_OBJECT + +public: + explicit FilterTableHeader(QTableView* parent = nullptr); + QSize sizeHint() const override; + bool hasFilters() const { return (filterWidgets.size() > 0); } + bool areFiltersVisible() const { return m_filtersVisible; } + QString filterValue(int column) const; + + public Q_SLOTS: + void generateFilters(int number); + void adjustPositions(); + void clearFilters(); + void setFilter(int column, const QString& value); + void setFocusColumn(int column); + void setFiltersVisible(bool visible = true); + + Q_SIGNALS: + void filterChanged(int column, QString value); + void filterFocused(); + + protected: + void updateGeometries() override; + + private Q_SLOTS: + void inputChanged(int col, const QString& new_value); + + private: + std::vector filterWidgets {}; + bool m_filtersVisible = false; +}; + +#endif diff --git a/src/WASimUI/widgets/MultisortTableView.h b/src/WASimUI/widgets/MultisortTableView.h new file mode 100644 index 0000000..5771a7a --- /dev/null +++ b/src/WASimUI/widgets/MultisortTableView.h @@ -0,0 +1,112 @@ +/* +This file is part of the WASimCommander project. +https://github.com/mpaperno/WASimCommander + +Original version from ; Used under GPL v3 license; Modifications applied. + +COPYRIGHT: (c) Maxim Paperno; All Rights Reserved. + +This file may be used under the terms of the GNU General Public License (GPL) +as published by the Free Software Foundation, either version 3 of the Licenses, +or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +A copy of the GNU GPL is included with this project +and is also available at . +*/ + +#pragma once + +#include +#include +#include +#include + +#include "MultiColumnProxyModel.h" + +class MultisortTableView : public QTableView +{ + Q_OBJECT + public: + explicit MultisortTableView(QWidget *parent = 0) : + QTableView(parent), + m_isSortingEnabled(false), + m_proxyModel(new MultiColumnProxyModel(this)), + m_modifier(Qt::ControlModifier) + { + QTableView::setSortingEnabled(false); // we do our own sorting + setupHeader(); + } + + /// Overrides parent method for internal handling of sorting state. + bool isSortingEnabled() const { return m_isSortingEnabled; } + /// Returns the custom proxy model used for sorting and filtering. + MultiColumnProxyModel *proxyModel() const { return m_proxyModel; } + + public Q_SLOTS: + /// Set key modifier to handle multicolumn sorting. + void setModifier(Qt::KeyboardModifier modifier) { m_modifier = modifier; } + + /// Overrides parent method for internal handling of sorting state. + void setSortingEnabled(bool enable) { + m_isSortingEnabled = enable; + if (enable) + m_proxyModel->sortColumn(horizontalHeader()->sortIndicatorSection(), false, (qint8)horizontalHeader()->sortIndicatorOrder()); + } + + virtual void setStringFilter(int col, QString pattern, int role = Qt::DisplayRole) { + m_proxyModel->setStringPatternFilter(col, pattern, role); + } + + virtual void setDisplayRoleStringFilter(int col, QString pattern) { + m_proxyModel->setStringPatternFilter(col, pattern, Qt::DisplayRole); + } + + virtual void setRegExpFilter(int col, const QRegExp& pattern, int role = Qt::DisplayRole) { + m_proxyModel->setRegExpFilter(col, pattern, Qt::DisplayRole); + } + + /// Overrides parent method for hooking into header's clicked signal and remove default sort indicator. + virtual void setHorizontalHeader(QHeaderView *hdr) { + QTableView::setHorizontalHeader(hdr); + setupHeader(); + } + + /// Overridden to use custom sorting model; + virtual void setModel(QAbstractItemModel *model) { + m_proxyModel->setSourceModel(model); + QTableView::setModel(m_proxyModel); + } + + private Q_SLOTS: + void onHeaderSectionrClicked(int column) { + if (m_isSortingEnabled) + m_proxyModel->sortColumn(column, (QApplication::keyboardModifiers() & m_modifier)); + //qDebug() << column << (Qt::SortOrder)m_proxyModel->columnSortOrder(column); + } + void onSortIndicatorChanged(int column, Qt::SortOrder order) { + //if (m_isSortingEnabled) + //m_proxyModel->sortColumn(column, (QApplication::keyboardModifiers() & m_modifier), (qint8)order); + //qDebug() << column << horizontalHeader()->sortIndicatorSection() << horizontalHeader()->sortIndicatorOrder(); + } + + void setupHeader() { + horizontalHeader()->setSortIndicatorShown(false); // we provide our own indicators + horizontalHeader()->setSectionsClickable(true); // make sure to receive click events so we can sort + connect(horizontalHeader(), &QHeaderView::sectionClicked, this, &MultisortTableView::onHeaderSectionrClicked); + //connect(horizontalHeader(), &QHeaderView::sortIndicatorChanged, this, &MultisortTableView::onSortIndicatorChanged); + } + + private: + // Sorting enable state + bool m_isSortingEnabled; + // ProxyModel to sorting columns + MultiColumnProxyModel *m_proxyModel; + // Modifier to handle multicolumn sorting + Qt::KeyboardModifier m_modifier; + +}; diff --git a/src/WASimUI/Widgets.h b/src/WASimUI/widgets/Widgets.h similarity index 83% rename from src/WASimUI/Widgets.h rename to src/WASimUI/widgets/Widgets.h index 703c484..4d7fcf4 100644 --- a/src/WASimUI/Widgets.h +++ b/src/WASimUI/widgets/Widgets.h @@ -19,17 +19,14 @@ and is also available at . #pragma once -#include -#include #include -#include #include #include -#include #include #include "client/WASimClient.h" #include "DataComboBox.h" +#include "DeletableItemsComboBox.h" #include "Utils.h" namespace WASimUiNS @@ -134,8 +131,11 @@ class CommandStatusWidget : public QWidget lo->setContentsMargins(8, 1, 10, 2); iconLabel = new QLabel(this); iconLabel->setFixedSize(iconSize, iconSize); + tsLabel = new QLabel(this); + tsLabel->setForegroundRole(QPalette::Link); textLabel = new QLabel(this); lo->addWidget(iconLabel, 0); + lo->addWidget(tsLabel, 0); lo->addWidget(textLabel, 1); icon.addFile("fg=green/thumb_up.glyph", QSize(), QIcon::Normal, QIcon::Off); @@ -154,11 +154,13 @@ class CommandStatusWidget : public QWidget const QIcon::Mode icnMode = resp.commandId == WSEnums::CommandId::Ack ? QIcon::Mode::Normal : QIcon::Mode::Active; iconLabel->setPixmap(icon.pixmap(iconSize, icnMode)); textLabel->setText(msg + details); + tsLabel->setText(QTime::currentTime().toString("[hh:mm:ss.zzz]")); } private: QIcon icon; QLabel *iconLabel; + QLabel *tsLabel; QLabel *textLabel; int iconSize; }; @@ -264,16 +266,16 @@ class VariableTypeComboBox : public DataComboBox public: VariableTypeComboBox(QWidget *p = nullptr) : DataComboBox(p) { - setToolTip(tr("Named variable type. Types marked with a * use Unit specifiers.")); + setToolTip(tr("Named variable type. Types marked with a * use Unit specifiers. Most 'L' vars will ignore the Unit (default is 'number').")); addItem(tr("A: SimVar *"), 'A'); - addItem(tr("B: Input"), 'B'); + //addItem(tr("B: Input"), 'B'); // only for gauge modules addItem(tr("C: GPS *"), 'C'); addItem(tr("E: Env. *"), 'E'); addItem(tr("H: HTML"), 'H'); //addItem(tr("I: Instr."), 'I'); // only for gauge modules addItem(tr("K: Key"), 'K'); - addItem(tr("L: Local"), 'L'); + addItem(tr("L: Local *"), 'L'); addItem(tr("M: Mouse"), 'M'); //addItem(tr("O: Comp."), 'O'); // only for gauge modules //addItem(tr("R: Resource"), 'R'); // strings only, can't be read with "Get" command or set at all @@ -284,84 +286,6 @@ class VariableTypeComboBox : public DataComboBox } }; - -class DeletableItemsComboBox : public DataComboBox -{ - Q_OBJECT - Q_PROPERTY(QString placeholderText READ placeholderText WRITE setPlaceholderText) -public: - DeletableItemsComboBox(QWidget *p = nullptr) : DataComboBox(p) - { - setEditable(true); - setInsertPolicy(InsertAtTop); - setSizeAdjustPolicy(AdjustToContents); - //setMinimumContentsLength(25); - setMaxVisibleItems(25); - setCurrentIndex(-1); - setToolTip(tr("Manually added text items (at top of list) can be removed by right-clicking on them while the list is open.")); - - connect(this, &DeletableItemsComboBox::editTextChanged, this, [this](const QString &txt) { - if (txt.isEmpty()) - setCurrentIndex(-1); - }); - connect(view(), &QAbstractItemView::pressed, [this](const QModelIndex &idx) { - if (idx.isValid() && !idx.data(Qt::UserRole).isValid() && (QApplication::mouseButtons() & Qt::RightButton)) - model()->removeRow(idx.row()); - }); - } - - void setClearButtonEnabled(bool enabled) { lineEdit()->setClearButtonEnabled(enabled); } - - const QStringList editedItems() const - { - QStringList ret; - if (!isEditable()) - return ret; - for (int i = 0, e = count(); i < e; ++i) { - if (!itemData(i).isValid() && !itemText(i).isEmpty()) - ret << itemText(i); - } - return ret; - } - - void insertEditedItems(const QStringList &items, InsertPolicy policy = NoInsert) - { - if (!isEditable() || insertPolicy() == NoInsert) - return; - if (policy == NoInsert) - policy = insertPolicy(); - int index = 0; - switch (policy) { - case InsertAtTop: - break; - case InsertAtBottom: - index = count(); - break; - case InsertAfterCurrent: - index = qMax(0, currentIndex()); - break; - case InsertBeforeCurrent: - index = qMax(0, currentIndex() - 1); - break; - } - const int currIdx = currentIndex(); - insertItems(index, items); - if (policy == InsertAlphabetically) - model()->sort(0); - if (currIdx == -1) - setCurrentIndex(currIdx); - } - - QString placeholderText() const { return lineEdit() ? lineEdit()->placeholderText() : ""; } - - void setPlaceholderText(const QString &text) - { - if (lineEdit()) - lineEdit()->setPlaceholderText(text); - } - -}; - class ValueSizeComboBox : public DeletableItemsComboBox { Q_OBJECT @@ -398,6 +322,13 @@ class UnitTypeComboBox : public DeletableItemsComboBox public: UnitTypeComboBox(QWidget *p = nullptr) : DeletableItemsComboBox(p) { + setToolTip(tr( + "

Unit Name for the value. For L vars this can be left blank to get the default value of the variable.

" + "

The completion suggestions are looked up from imported SimConnect SDK documentation unit types.

" + "

Unit types may also be saved for quick selection later by pressing Return after selecting one or typing it in.

" + "

Saved items can be removed by right-clicking on them while the list is open.

" + )); + setInsertPolicy(InsertAlphabetically); int i = 0; addItem(QStringLiteral("bar"), i++); @@ -430,6 +361,7 @@ class UnitTypeComboBox : public DeletableItemsComboBox addItem(QStringLiteral("psi"), i++); addItem(QStringLiteral("radians"), i++); addItem(QStringLiteral("rpm"), i++); + addItem(QStringLiteral("seconds"), i++); addItem(QStringLiteral("string"), i++); addItem(QStringLiteral("volts"), i++); addItem(QStringLiteral("Watts"), i++); diff --git a/src/include/WASimCommander.h b/src/include/WASimCommander.h index 9c4ed50..8fd7660 100644 --- a/src/include/WASimCommander.h +++ b/src/include/WASimCommander.h @@ -128,18 +128,18 @@ namespace WASimCommander { uint32_t requestId; ///< Unique ID for the request, subsequent use of this ID overwrites any previous request definition (but size may not grow). uint32_t valueSize; ///< Byte size of stored value; can also be one of the predefined DATA_TYPE_* constants. \sa WASimCommander::DATA_TYPE_INT8, etc - float deltaEpsilon = 0.0f; ///< Minimum change in numeric value required to trigger an update. The default of `0.0` is to send updates only if the value changes, but even on the smallest changes. + float deltaEpsilon; ///< Minimum change in numeric value required to trigger an update. The default of `0.0` is to send updates only if the value changes, but even on the smallest changes. /// Setting this to some positive value can be especially useful for high-precision floating-point numbers which may fluctuate within an insignifcant range, /// but may be used with any numeric value (for integer value types, only the integer part of the epsilon value is considered). /// Conversely, to send data updates _every time_ the value is read, and skip any comparison check altogether, set this to a negative value like `-1.0`. ///< \note For the positive epsilon settings to work, the `valueSize` must be set to one of the predefined `DATA_TYPE_*` constants. - uint32_t interval = 0; ///< How many `UpdatePeriod` period's should elapse between checks. eg. 500ms or 10 ticks. + uint32_t interval; ///< How many `UpdatePeriod` period's should elapse between checks. eg. 500ms or 10 ticks. /// Zero means to check at every `period`, `1` means every other `period`, etc. WSE::UpdatePeriod period; ///< How often to read/calculate this value. WSE::RequestType requestType; ///< Named variable or calculated value. WSE::CalcResultType calcResultType; ///< Expected calculator result type. uint8_t simVarIndex; ///< Some SimVars require an index for access, default is 0. - char varTypePrefix = 'L'; ///< Variable type prefix for named variables. Types: 'L' (local), 'A' (SimVar) and 'T' (Token, not an actual GaugeAPI prefix) are checked using respecitive GaugeAPI methods. + char varTypePrefix; ///< Variable type prefix for named variables. Types: 'L' (local), 'A' (SimVar) and 'T' (Token, not an actual GaugeAPI prefix) are checked using respecitive GaugeAPI methods. char nameOrCode[STRSZ_REQ] = {0}; ///< Variable name or full calculator string. char unitName[STRSZ_UNIT] = {0}; ///< Unit name for named variables (optional to override variable's default units). Only 'L' and 'A' variable types support unit specifiers. // 1088/1088 B (packed/unpacked), 8/16 B aligned @@ -152,9 +152,14 @@ namespace WASimCommander WSE::CalcResultType calcResultType = WSE::CalcResultType::Double, WSE::UpdatePeriod period = WSE::UpdatePeriod::Tick, const char * nameOrCode = nullptr, - const char * unitName = nullptr - ) - : requestId(requestId), valueSize(valueSize), period(period), requestType(requestType), calcResultType(calcResultType) + const char * unitName = nullptr, + char varTypePrefix = 'L', + float deltaEpsilon = 0.0f, + uint8_t interval = 0, + uint8_t simVarIndex = 0 + ) : + requestId(requestId), valueSize(valueSize), deltaEpsilon(deltaEpsilon), interval(interval), period(period), + requestType(requestType), calcResultType(calcResultType), simVarIndex(simVarIndex), varTypePrefix(varTypePrefix) { if (nameOrCode) setNameOrCode(nameOrCode); @@ -165,29 +170,18 @@ namespace WASimCommander /// Constructs a request for a named variable (`requestType = RequestType::Named`) with optional update period, interval, and epsilon values. explicit DataRequest(uint32_t requestId, char variableType, const char *variableName, uint32_t valueSize, WSE::UpdatePeriod period = WSE::UpdatePeriod::Tick, uint32_t interval = 0, float deltaEpsilon = 0.0f) : - requestId(requestId), valueSize(valueSize), deltaEpsilon(deltaEpsilon), interval(interval), period(period), requestType(WSE::RequestType::Named), varTypePrefix(variableType) - { - if (variableName) - setNameOrCode(variableName); - } + DataRequest(requestId, valueSize, WSE::RequestType::Named, WSE::CalcResultType::None, period, variableName, nullptr, variableType, deltaEpsilon, interval) + { } /// Constructs a request for a named Simulator Variable (`requestType = RequestType::Named` and `varTypePrefix = 'A'`) with optional update period, interval, and epsilon values. explicit DataRequest(uint32_t requestId, const char *simVarName, const char *unitName, uint8_t simVarIndex, uint32_t valueSize, WSE::UpdatePeriod period = WSE::UpdatePeriod::Tick, uint32_t interval = 0, float deltaEpsilon = 0.0f) : - requestId(requestId), valueSize(valueSize), deltaEpsilon(deltaEpsilon), interval(interval), period(period), requestType(WSE::RequestType::Named), simVarIndex(simVarIndex), varTypePrefix('A') - { - if (simVarName) - setNameOrCode(simVarName); - if (unitName) - setUnitName(unitName); - } + DataRequest(requestId, valueSize, WSE::RequestType::Named, WSE::CalcResultType::None, period, simVarName, unitName, 'A', deltaEpsilon, interval, simVarIndex) + { } /// Constructs a calculator code request (`requestType = RequestType::Calculated`) with optional update period, interval, and epsilon values. explicit DataRequest(uint32_t requestId, WSE::CalcResultType resultType, const char *calculatorCode, uint32_t valueSize, WSE::UpdatePeriod period = WSE::UpdatePeriod::Tick, uint32_t interval = 0, float deltaEpsilon = 0.0f) : - requestId(requestId), valueSize(valueSize), deltaEpsilon(deltaEpsilon), interval(interval), period(period), requestType(WSE::RequestType::Calculated), calcResultType(resultType) - { - if (calculatorCode) - setNameOrCode(calculatorCode); - } + DataRequest(requestId, valueSize, WSE::RequestType::Calculated, resultType, period, calculatorCode, nullptr, 'Q', deltaEpsilon, interval) + { } void setNameOrCode(const char *name) { setCharArrayValue(nameOrCode, STRSZ_REQ, name); } ///< Set the `nameOrCode` member using a const char array. void setUnitName(const char *name) { setCharArrayValue(unitName, STRSZ_UNIT, name); } ///< Set the `unitName` member using a const char array. diff --git a/src/include/client/WASimClient.h b/src/include/client/WASimClient.h index b82841f..566c1a7 100644 --- a/src/include/client/WASimClient.h +++ b/src/include/client/WASimClient.h @@ -154,11 +154,12 @@ static const HRESULT E_TIMEOUT = /*ERROR_TIMEOUT*/ 1460L | (/*FACILI /// The unit name is ignored for all other variable types, and the `unitId` field is preferred if it is greater than -1. int variableId = -1; ///< Numeric ID of the variable to get/set. Overrides the `variableName` field if greater than -1. Only 'A', 'L', 'T' variable types can be referenced by numeric IDs. int unitId = -1; ///< Numeric ID of the Unit type to use in the get/set command. Overrides the `unitName` field if greater than -1. See usage notes for `unitName` about applicable variable types. - uint8_t simVarIndex = 0; ///< Optional index number for SimVars ('A') which require them. If using named variables, yhe index can also be included in the variable name string (after a colon `:`, as would be used in a calculator string). + uint8_t simVarIndex = 0; ///< Optional index number for SimVars ('A') which require them. If using named variables, yhe index can also be included in the variable name string (after a colon `:`, as would be used in a calculator string). + bool createLVar = false; ///< This flag indicates that the L var should be created if it doesn't already exist in the simulator. This applies for both "Set" and "Get" commands. - /// Default constructor, with optional parameters for variable type, name, unit name and SimVar index. - explicit VariableRequest(char variableType = 'L', const std::string &variableName = std::string(), const std::string &unitName = std::string(), uint8_t simVarIndex = 0) : - variableType{variableType}, variableName{variableName}, unitName{unitName}, simVarIndex(simVarIndex) { } + /// Default constructor, with optional parameters for variable type, name, unit name, SimVar index and `createLVar` flag. + explicit VariableRequest(char variableType = 'L', const std::string &variableName = std::string(), const std::string &unitName = std::string(), uint8_t simVarIndex = 0, bool createVariable = false) : + variableType{variableType}, variableName{variableName}, unitName{unitName}, simVarIndex(simVarIndex), createLVar{createVariable} { } /// Construct a variable request using numeric variable and (optionally) unit IDs, and optional SimVar index. explicit VariableRequest(char variableType, int variableId, int unitId = -1, uint8_t simVarIndex = 0) : variableType{variableType}, variableId{variableId}, unitId{unitId}, simVarIndex(simVarIndex) { } @@ -168,9 +169,10 @@ static const HRESULT E_TIMEOUT = /*ERROR_TIMEOUT*/ 1460L | (/*FACILI /// Construct a variable request for a Simulator Variable ('A') using numeric variable and unit IDs, with optional index parameter. explicit VariableRequest(int simVarId, int unitId, uint8_t simVarIndex = 0) : variableType{'A'}, variableId{simVarId}, unitId{unitId}, simVarIndex(simVarIndex) { } - /// Construct a variable request for a Local variable ('L') with the given name. - explicit VariableRequest(const std::string &localVarName) : - variableType{'L'}, variableName{localVarName} { } + /// Construct a variable request for a Local variable ('L') with the given name. `createVariable` will create the L var on the simulator if it doesn't exist yet + /// (for "Get" as well as "Set" commands). An optional unit name can also be provided. + explicit VariableRequest(const std::string &localVarName, bool createVariable = false, const std::string &unitName = std::string()) : + variableType{'L'}, variableName{localVarName}, unitName{unitName}, createLVar{createVariable} { } /// Construct a variable request for a Local variable ('L') with the given numeric ID. explicit VariableRequest(int localVarId) : variableType{'L'}, variableId{localVarId} { } @@ -304,8 +306,10 @@ static const HRESULT E_TIMEOUT = /*ERROR_TIMEOUT*/ 1460L | (/*FACILI /// if a numeric result is requested, the string result will also be populated). /// \param pfResult A pointer to an initialized variable of `double` to store the result into if `resultType` is `Enums::CalcResultType::Double` or `Enums::CalcResultType::Integer`. /// \param psResult A string pointer to store the string result into. The string version is typically populated even for numeric type requests, but definitely for `Enums::CalcResultType::String` or `Enums::CalcResultType::Formatted` type requests. - /// \return `S_OK` on success, `E_FAIL` if the server returned Nak response, `E_NOT_CONNECTED` if not connected to server, or `E_TIMEOUT` on general server communication failure. - /// \note This method blocks until either the Server responds or the timeout has expired. + /// \return `S_OK` on success, `E_NOT_CONNECTED` if not connected to server; \n + /// If a result is expected, may also return `E_FAIL` if the server returned Nak response, or `E_TIMEOUT` on general server communication failure. + /// \note _If_ a result is expected (`resultType` != `Enums::CalcResultType::None`) then this method blocks until either the Server responds or the timeout has expired (see `defaultTimeout()`). + /// To request calculated results in a non-blocking fashion, use a data request instead. /// /// If you need to execute the same code multiple times, it would be more efficient to save the code as either a data request (for code returning values) or a registered event (for code not returning values). /// The advantage is that in those cases the calculator string is pre-compiled to byte code and saved once, then each invocation of the _Gauge API_ calculator functions uses the more efficient byte code version. @@ -317,20 +321,32 @@ static const HRESULT E_TIMEOUT = /*ERROR_TIMEOUT*/ 1460L | (/*FACILI // Variables accessors ------------------------------ /// Get a Variable value by name, with optional named unit type. This is primarily useful for local ('L') variables, SimVars ('A') and token variables ('T') which can be read via dedicated _Gauge API_ functions - /// (`get_named_variable_value()`/`get_named_variable_typed_value()`, `aircraft_varget()`, and `lookup_var()` respectively). Other variables types can also be set this way ('B', 'E', 'M', etc) but such requests are simply converted to a calculator string and - /// evaluated via the _Gauge API_ `execute_calculator_code()`. Using `WASimClient::executeCalculatorCode()` directly may be more efficient. + /// (`get_named_variable_value()`/`get_named_variable_typed_value()`, `aircraft_varget()`, and `lookup_var()` respectively). \n + /// Other variables types can also be set this way ('C', 'E', 'M', etc) but such requests are simply **converted to a calculator string** and evaluated via the _Gauge API_ `execute_calculator_code()`. \n + /// Likewise, requesting string-type variables using this method also ends up running a calculator expression on the server side. \n + /// In both cases, using `WASimClient::executeCalculatorCode()` directly may be more efficient. Also, unlike `executeCalculatorCode()`, this method will not return a string representation of a numeric value. /// \param variable See `VariableRequest` documentation for descriptions of the individual fields. - /// \param pfResult Pointer to a double precision variable to hold the result. + /// \param pfResult Pointer to a double precision variable to hold the numeric result. + /// \param psResult Pointer to a string type variable to hold a string-type result. See notes above regarding string types. /// \return `S_OK` on success, `E_INVALIDARG` on parameter validation errors, `E_NOT_CONNECTED` if not connected to server, `E_TIMEOUT` on server communication failure, or `E_FAIL` if server returns a Nak response. /// \note This method blocks until either the Server responds or the timeout has expired. /// \sa \refwcc{VariableRequest}, \refwce{CommandId::Get}, defaultTimeout(), setDefaultTimeout() - HRESULT getVariable(const VariableRequest &variable, double *pfResult); - /// A convenience version of getVariable(VariableRequest('L', variableName), pfResult). See `getVariable()` for details. + HRESULT getVariable(const VariableRequest &variable, double *pfResult, std::string *psResult = nullptr); + /// A convenience version of `getVariable(VariableRequest(variableName, false, unitName), pfResult)`. See `getVariable()` and `VariableRequest` for details. /// \param variableName Name of the local variable. /// \param pfResult Pointer to a double precision variable to hold the result. + /// \param unitName Optional unit specifier to use. Most Local vars do not specify a unit and default to a generic "number" type. /// \return `S_OK` on success, `E_INVALIDARG` on parameter validation errors, `E_NOT_CONNECTED` if not connected to server, `E_TIMEOUT` on server communication failure, or `E_FAIL` if server returns a Nak response. - /// \note This method blocks until either the Server responds or the timeout has expired. \sa defaultTimeout(), setDefaultTimeout() - HRESULT getLocalVariable(const std::string &variableName, double *pfResult); + /// \note This method blocks until either the Server responds or the timeout has expired. \sa \refwce{CommandId::Get}, defaultTimeout(), setDefaultTimeout() + HRESULT getLocalVariable(const std::string &variableName, double *pfResult, const std::string &unitName = std::string()); + /// Gets the value of a local variable just like `getLocalVariable()` but will also create the variable on the simulator if it doesn't already exist. + /// \param variableName Name of the local variable. + /// \param pfResult Pointer to a double precision variable to hold the result. + /// \param defaultValue The L var will be created on the simulator if it doesn't exist yet using this initial value (and this same value will be returned in `pfResult`). + /// \param unitName Optional unit specifier to use. Most Local vars do not specify a unit and default to a generic "number" type. + /// \return `S_OK` on success, `E_INVALIDARG` on parameter validation errors, `E_NOT_CONNECTED` if not connected to server, `E_TIMEOUT` on server communication failure, or `E_FAIL` if server returns a Nak response. + /// \note This method blocks until either the Server responds or the timeout has expired. \sa \refwce{CommandId::GetCreate}, defaultTimeout(), setDefaultTimeout() + HRESULT getOrCreateLocalVariable(const std::string &variableName, double *pfResult, double defaultValue = 0.0, const std::string &unitName = std::string()); /// Set a Variable value by name, with optional named unit type. Although any settable variable type can set this way, it is primarily useful for local (`L`) variables which can be set via dedicated _Gauge API_ functions /// (`set_named_variable_value()` and `set_named_variable_typed_value()`). Other variables types can also be set this way ('A', 'H", 'K', etc) but such requests are simply converted to a calculator string and @@ -340,28 +356,36 @@ static const HRESULT E_TIMEOUT = /*ERROR_TIMEOUT*/ 1460L | (/*FACILI /// \return `S_OK` on success, `E_INVALIDARG` on parameter validation errors, `E_NOT_CONNECTED` if not connected to server, or `E_FAIL` on general failure (unlikely). /// \sa \refwce{CommandId::Set} HRESULT setVariable(const VariableRequest &variable, const double value); - /// A convenience version of `setVariable(VariableRequest('L', variableName), value)`. See `setVariable()` for details. + /// A convenience version of `setVariable()` for Local variable types. Equivalent to `setVariable(VariableRequest(variableName, false, unitName), value)`. See `setVariable()` and `VariableRequest` for details. /// \param variableName Name of the local variable. /// \param value The value to set. + /// \param unitName Optional unit specifier to use. Most Local vars do not specify a unit and default to a generic "number" type. /// \return `S_OK` on success, `E_INVALIDARG` on parameter validation errors, `E_NOT_CONNECTED` if not connected to server, or `E_FAIL` on general failure (unlikely). - HRESULT setLocalVariable(const std::string &variableName, const double value); + /// \sa \refwce{CommandId::Set} + HRESULT setLocalVariable(const std::string &variableName, const double value, const std::string &unitName = std::string()); /// Set a Local Variable value by variable name, creating it first if it does not already exist. This first calls the `register_named_variable()` _Gauge API_ function to get the ID from the name, - /// which creates the variable if it doesn't exist. The returned ID (new or existing) is then used to set the value. Unit type cannot be specified when creating/using custom local variables in this fashion. - /// Use the `lookup()` method to check for the existence of a variable name. + /// which creates the variable if it doesn't exist. The returned ID (new or existing) is then used to set the value. Use the `lookup()` method to check for the existence of a variable name. + /// Equivalent to `setVariable(VariableRequest(variableName, true, unitName), value)`. See `setVariable()` and `VariableRequest` for details. /// \param variableName Name of the local variable. /// \param value The value to set. Becomes the intial value if the variable is created. + /// \param unitName Optional unit specifier to use. Most Local vars do not specify a unit and default to a generic "number" type. /// \return `S_OK` on success, `E_INVALIDARG` on parameter validation errors, `E_NOT_CONNECTED` if not connected to server, or `E_FAIL` on general failure (unlikely). - /// \sa \refwcc{VariableRequest}, \refwce{CommandId::SetCreate} - HRESULT setOrCreateLocalVariable(const std::string &variableName, const double value); + /// \sa \refwce{CommandId::SetCreate} + HRESULT setOrCreateLocalVariable(const std::string &variableName, const double value, const std::string &unitName = std::string()); // Data subscriptions ------------------------------- /// Add a new `WASimCommander::DataRequest` or update an existing one with the same `DataRequest::requestId`. If the client is not currently connected to the server, the request is queued until the next connection is established. /// \param request The `WASimCommander::DataRequest` structure to process. See `WASimCommander::DataRequest` documentation for details of the structure members. - /// \return `S_OK` on success, `E_INVALIDARG` if there is a problem with the `DataRequest` contents; If currently connected to the server, may also return `E_FAIL` if the server returned `Nak` response, or `E_TIMEOUT` on general server communication failure. - /// \note If currently connected to the server, this method will block until either the Server responds or the timeout has expired (see `defaultTimeout()`). + /// \param async `true` to wait for a response from the server before returning, or `false` (default) to wait for an `Ack`/`Nak` response. See return values and the Note below for more details. + /// \return `S_OK` on success, `E_INVALIDARG` if there is a problem with the `DataRequest` contents. \n + /// If currently connected to the server and `async` is `false`, may also return `E_FAIL` if the server returned `Nak` response, or `E_TIMEOUT` on general server communication failure. + /// \note If currently connected to the server and the `async` param is `false`, this method will block until either the Server responds or the timeout has expired (see `defaultTimeout()`). + /// \par Tracking async calls + /// To track the status of an async request, set a callback function with `setCommandResultCallback()`. The server should respond with an \refwce{CommandId::Ack} or \refwce{CommandId::Nak} + /// \refwc{Command} where the `uData` value is \refwce{CommandId::Subscribe} and the \refwc{Command::token} will be the `requestId` value from the given `request` struct. /// \sa \refwc{DataRequest} \refwce{CommandId::Subscribe}, removeDataRequest(), updateDataRequest() - HRESULT saveDataRequest(const DataRequest &request); + HRESULT saveDataRequest(const DataRequest &request, bool async = false); /// Remove a previously-added `DataRequest`. This clears the subscription and any tracking/meta data from both server and client sides. /// Using this method is effectively the same as calling `dataRequest()` with a `DataRequest` of type `RequestType::None`. /// \param requestId ID of the request to remove. @@ -387,7 +411,10 @@ static const HRESULT E_TIMEOUT = /*ERROR_TIMEOUT*/ 1460L | (/*FACILI /// Enables or disables all data request subscription updates at the same time. Use this to temporarily suspend value update checks when they are not needed, but may be again in the future. /// This is a lot more efficient than disconnecting and re-connecting to the server, since all the data requests need to be re-created upon every new connection (similar to SimConnect itself). - /// \return `S_OK` on success, `E_NOT_CONNECTED` if not connected to server. + /// \since{v1.2} + /// This method can be called while not connected to the server. In this case the setting is saved and sent to the server upon next connection, before sending any data request subscriptions. + /// This way updates could be suspended upon initial connection, then re-enabled when the data is actually needed. + /// \return `S_OK` on success; If currently connected to the server, may also return `E_TIMEOUT` on general server communication failure. HRESULT setDataRequestsPaused(bool paused) const; // Custom Event registration -------------------------- diff --git a/src/include/enums_impl.h b/src/include/enums_impl.h index 1bb8622..b4b0161 100644 --- a/src/include/enums_impl.h +++ b/src/include/enums_impl.h @@ -52,13 +52,16 @@ namespace WSMCMND_ENUM_NAMESPACE /// For example, a SimVar: ```uData = 'A'; sData = "PROP BETA:2,degrees";``` \n /// Other variables types can also be requested ('B', 'E', 'M', etc) but such requests are simply converted to a calculator string and processed as an `Exec` type command (using an `Exec` command directly may be more efficient).\n /// Result is returned with the `Ack` response in `fData` as a double-precision value. In case of failure a `Nak` is returned with possible error message in `sData`. - GetCreate, ///< Same as `Get` but creates the variable if it doesn't already exist (with `register_named_variable()` _Gauge API_). The returned value will be `0.0` (see `SetCreate` to assign a default value and `Lookup` to check if a variable exists). - /// **This only works with `L` (local) type variables.** + GetCreate, ///< Same as `Get` but creates a local 'L' variable if it doesn't already exist (with `register_named_variable()` _Gauge API_). Use `Lookup` command to check if a variable exists. + /// \n **Since v1.2:** If a variable is created, the value provided in `fData` will be used as the initial value of the variable, and will be returned as the result + /// (essentially providing a default value in this case). Previous versions would _not_ set a value (or unit type) on the variable after creating it and would return the default of `0.0`. \n + /// **Creating variables only works with `L` (local) types.** Since v1.2, for all other types this command will be handled the same as `Get`. Previous versions would return a `Nak`. Set, ///< Set a named local variable with optional unit type. `uData` is a char of the variable type, with default of 'L' for local vars. /// `sData` is the variable name or numeric ID (for local vars only), optionally followed by comma (`,`) and unit name (or numeric unit ID for local vars) (**no spaces**). The value to set is passed in `fData` member.\n /// For example, a SimVar: ```uData = 'A'; sData = "PROP RPM:2,rpm"; fData = 2200;```\n /// Other variables types can also be set this way ('A', 'H", 'K', etc) but such requests are simply converted to a calculator string and processed as an `Exec` type command (using an `Exec` command directly may be slightly more efficient). - SetCreate, ///< Same as `Set` but creates the variable first if it doesn't already exist (with `register_named_variable()` _Gauge API_). Use the `Lookup` command to check if a variable exists. **This only works with `L` (local) type variables.** + SetCreate, ///< Same as `Set` but creates a local 'L' variable if it doesn't already exist (with `register_named_variable()` _Gauge API_). Use the `Lookup` command to check if a variable exists. \n + /// **Creating variables only works with `L` (local) types.** Since v1.2, for all other types this command will be handled the same as `Get`. Previous versions would return a `Nak`. Exec, ///< Run calculator code contained in `sData` with `WASimCommander::CalcResultType` in `uData`. Result, if any, is returned with the `Ack` response, numeric types in `fData` and strings in `sData`. /// (Due to the way the _Gauge API_ calculator function works, a string result is typically also returned even when only a numeric result is requested.)\n\n /// In case of failure a `Nak` is returned with possible error message in `sData`. Note however that the _Gauge API_ functions often happily return a "success" status even when the actual thing you're trying to do fails. The only feedback diff --git a/src/include/wasim_version.h b/src/include/wasim_version.h index 9b87025..a115965 100644 --- a/src/include/wasim_version.h +++ b/src/include/wasim_version.h @@ -1,4 +1,4 @@ - /* +/* This file is part of the WASimCommander project. https://github.com/mpaperno/WASimCommander @@ -27,21 +27,21 @@ and are available at . #define WSMCMND_GUI_NAME "WASimUI" #define WSMCMND_VER_MAJOR 1 -#define WSMCMND_VER_MINOR 1 -#define WSMCMND_VER_PATCH 2 +#define WSMCMND_VER_MINOR 2 +#define WSMCMND_VER_PATCH 0 #define WSMCMND_VER_BUILD 0 // Git commit hash (top 8 bytes) -#define WSMCMND_VER_COMIT 0x0C321F25UL +#define WSMCMND_VER_COMIT 0x4A699DF9UL /// Version number in 32 bit "binary coded decimal", eg. 0x01230400 = 1.23.4.0 -#define WSMCMND_VERSION 0x01010200UL +#define WSMCMND_VERSION 0x01020000UL /// Possible version suffix, eg "-beta1" (can be blank for release versions) #define WSMCMND_VER_NAME "" /// Dotted version string Maj.Min.Pat.Bld, eg. "1.23.4.0" -#define WSMCMND_VERSION_STR "1.1.2.0" +#define WSMCMND_VERSION_STR "1.2.0.0" /// Dotted version string with possible suffix, eg. "1.23.4.0-beta1" -#define WSMCMND_VERSION_INFO "1.1.2.0" +#define WSMCMND_VERSION_INFO "1.2.0.0" /// Build date & time in ISO-8601 "Zulu Time" format, UTC -#define WSMCMND_BUILD_DATE "2023-02-23T09:43:21Z" +#define WSMCMND_BUILD_DATE "2023-11-01T18:50:07Z" #define WSMCMND_PROJECT_URL "https://github.com/mpaperno/WASimCommander" diff --git a/src/shared/utilities.h b/src/shared/utilities.h index 6208fce..625bbd6 100644 --- a/src/shared/utilities.h +++ b/src/shared/utilities.h @@ -85,10 +85,15 @@ namespace WASimCommander { } static bool isUnitBasedVariableType(const char type) { - static const std::vector VAR_TYPES_UNIT_BASED = { 'A', 'C', 'E', 'P' }; + static const std::vector VAR_TYPES_UNIT_BASED = { 'A', 'C', 'E', 'L', 'P' }; return find(VAR_TYPES_UNIT_BASED.cbegin(), VAR_TYPES_UNIT_BASED.cend(), type) != VAR_TYPES_UNIT_BASED.cend(); } + static bool isSettableVariableType(const char type) { + static const std::vector VAR_TYPES_SETTABLE = { 'A', 'C', 'H', 'K', 'L', 'Z' }; + return find(VAR_TYPES_SETTABLE.cbegin(), VAR_TYPES_SETTABLE.cend(), type) != VAR_TYPES_SETTABLE.cend(); + } + // returns actual byte size from given size which may be one of the SimConnect_AddToClientDataDefinition() dwSizeOrType constants static constexpr uint32_t getActualValueSize(DWORD dwSizeOrType) {