diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..0bfd52a --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,27 @@ +name: Build Status + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + + runs-on: windows-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Setup .NET Core + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 3.1 + + - name: Install dependencies + run: dotnet restore ./streamdeck-gpu/streamdeck-gpu.sln + + - name: Build + run: dotnet build ./streamdeck-gpu/streamdeck-gpu.sln --configuration Release --no-restore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..426d76d --- /dev/null +++ b/.gitignore @@ -0,0 +1,398 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..82ce8e8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Nic Luckie + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..385f0ff --- /dev/null +++ b/README.md @@ -0,0 +1,99 @@ + +

streamdeck-gpu

+ +

+ + Logo + +

+ +

A Stream Deck plugin that displays the current GPU temperature.

+ +

Build Status

+ +
+ Table of Contents +
    +
  1. + About +
  2. +
  3. + Getting Started + +
  4. +
  5. Usage
  6. +
  7. Change Log
  8. +
  9. License
  10. +
  11. Acknowledgments
  12. +
+
+ +## About + +An open-source Stream Deck plugin, written in C#. It uses the [OpenHardwareMonitor](https://openhardwaremonitor.org/) library to monitor and display the current GPU temperature every second. + +## Getting Started + +### Prerequisites + +Before you begin, ensure the following are installed: + +- [.NET Framework 4.7.2](https://dotnet.microsoft.com/en-us/download/dotnet-framework/net472) or later +- [.NET Core 3.1 SDK](https://dotnet.microsoft.com/en-us/download/dotnet) or later +- [Visual Studio](https://visualstudio.microsoft.com/downloads/) +- [Stream Deck software](https://www.elgato.com/ca/en/s/downloads) + +### Installation + +1. Download the [latest release](https://github.com/nicolasluckie/streamdeck-gpu/releases/) and extract it. + +2. Quit Stream Deck. + +3. Copy the **"com.nicolasluckie.gpu.sdPlugin"** folder to **"C:\Users\User\AppData\Roaming\Elgato\StreamDeck\Plugins\"** + +4. Launch Stream Deck. You should now see the GPU plugin in the action menu on the right. + +5. To see plugin information open **Preferences > Plugins > GPU** + +### Building from Source + +1. Clone the repository to your local machine: + + ``` + git clone https://github.com/nicolasluckie/streamdeck-gpu.git + ``` + +2. Open the solution file (`streamdeck-gpu.sln`) in Visual Studio. + +3. Right-click on the solution in the Solution Explorer and select **"Restore NuGet Packages"** to download OpenHardwareMonitor and other dependencies. + +4. Right-click on the solution in the Solution Explorer and select **"Manage NuGet Packages for Solution"**, click the **"Select all packages"** checkbox, then click **"Update"** to update all dependencies. + +5. Build the solution by clicking on **"Build" > "Build Solution"** in the menu. + +6. Once the build is successful, the plugin will be available in the `bin/Debug` directory by default. You can change this to `bin/Release` in the build configuration settings. + +7. Follow steps 2-5 of the [Installation](#installation) instructions to load the plugin into Stream Deck. + +## Usage + +![usage](/usage.gif) + +## Change Log + +### Version 1.0.0 +- Initial commit + +## License + +[License](/LICENSE) + +## Acknowledgments + +Plugin template from [BarRaider/streamdeck-tools](https://github.com/BarRaider/streamdeck-tools) + +

(back to top)

diff --git a/streamdeck-gpu/barraider-sdtools/Attributes/FilenamePropertyAttribute.cs b/streamdeck-gpu/barraider-sdtools/Attributes/FilenamePropertyAttribute.cs new file mode 100644 index 0000000..725da6d --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Attributes/FilenamePropertyAttribute.cs @@ -0,0 +1,13 @@ +using System; + +namespace BarRaider.SdTools { + + /// + /// FilenamePropertyAttribute - Used to indicate the current property holds a file name. + /// This will allow StreamDeck Tools to strip the mandatory "C:\fakepath\" added by the SDK + /// + [Serializable] + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)] + public class FilenamePropertyAttribute : Attribute { + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Attributes/PluginActionIdAttribute.cs b/streamdeck-gpu/barraider-sdtools/Attributes/PluginActionIdAttribute.cs new file mode 100644 index 0000000..5e0eea2 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Attributes/PluginActionIdAttribute.cs @@ -0,0 +1,25 @@ +using System; + +namespace BarRaider.SdTools { + /// + /// PluginActionId attribute + /// Used to indicate the UUID in the manifest file that matches to this class + /// + [Serializable] + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] + public class PluginActionIdAttribute : Attribute { + + /// + /// UUID of the action + /// + public string ActionId { get; private set; } + + /// + /// Constructor - This attribute is used to indicate the UUID in the manifest file that matches to this class + /// + /// + public PluginActionIdAttribute(string ActionId) { + this.ActionId = ActionId; + } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/BRLogo_460.png b/streamdeck-gpu/barraider-sdtools/BRLogo_460.png new file mode 100644 index 0000000..7246dc7 Binary files /dev/null and b/streamdeck-gpu/barraider-sdtools/BRLogo_460.png differ diff --git a/streamdeck-gpu/barraider-sdtools/Backend/EncoderBase.cs b/streamdeck-gpu/barraider-sdtools/Backend/EncoderBase.cs new file mode 100644 index 0000000..f1193ab --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Backend/EncoderBase.cs @@ -0,0 +1,93 @@ +using BarRaider.SdTools.Payloads; + +namespace BarRaider.SdTools { + /// + /// Main abstract class your plugin should derive from for dials (not keys) + /// For keys use the KeyBase or KeyAndEncoderBase + /// Holds implementation for all the basic functions + /// If you're missing an event, you can register to it from the Connection.StreamDeckConnection object + /// + public abstract class EncoderBase : IEncoderPlugin { + /// + /// Called when the dial is rotated + /// + public abstract void DialRotate(DialRotatePayload payload); + + /// + /// Called when the Dial is pressed + /// + public abstract void DialDown(DialPayload payload); + + /// + /// Called when the Dial is released + /// + public abstract void DialUp(DialPayload payload); + + /// + /// Called when the touchpad (above the dials) is pressed + /// + public abstract void TouchPress(TouchpadPressPayload payload); + + /// + /// Called when the PropertyInspector has new settings + /// + /// + public abstract void ReceivedSettings(ReceivedSettingsPayload payload); + + /// + /// Called when GetGlobalSettings is called. + /// + /// + public abstract void ReceivedGlobalSettings(ReceivedGlobalSettingsPayload payload); + + /// + /// Called every second + /// Logic for displaying title/image can go here + /// + public abstract void OnTick(); + + /// + /// Abstract method Called when the plugin is disposed + /// + public abstract void Dispose(); + + /// + /// Main iDisposable Dispose function + /// + public void Destroy() { + Dispose(); + if (Connection != null) { + Connection.Dispose(); + } + } + + /// + /// Connection object which handles your communication with the Stream Deck app + /// + protected ISDConnection Connection { get; private set; } + + /// + /// Constructor for PluginBase. Receives the communication and plugin settings + /// Note that the settings object is not used by the base and should be consumed by the deriving class. + /// Usually, a private class inside the deriving class is created which stores the settings + /// Example for settings usage: + /// * if (payload.Settings == null || payload.Settings.Count == 0) + /// * { + /// * // Create default settings + /// * } + /// * else + /// * { + /// this.settings = payload.Settings.ToObject(); + /// * } + /// + /// + /// Communication module with Stream Deck + /// Plugin settings - NOTE: Not used in base class, should be consumed by deriving class +#pragma warning disable IDE0060 // Remove unused parameter + public EncoderBase(ISDConnection connection, InitialPayload payload) +#pragma warning restore IDE0060 // Remove unused parameter + { + Connection = connection; + } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Backend/ICommonPluginFunctions.cs b/streamdeck-gpu/barraider-sdtools/Backend/ICommonPluginFunctions.cs new file mode 100644 index 0000000..ea3e24a --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Backend/ICommonPluginFunctions.cs @@ -0,0 +1,31 @@ +using System; + +namespace BarRaider.SdTools { + /// + /// Common functions used by both keypad and dial plugins + /// + public interface ICommonPluginFunctions : IDisposable { + /// + /// Called when the PropertyInspector has new settings + /// + /// + void ReceivedSettings(ReceivedSettingsPayload payload); + + /// + /// Called when GetGlobalSettings is called. + /// + /// + void ReceivedGlobalSettings(ReceivedGlobalSettingsPayload payload); + + /// + /// Called every second + /// Logic for displaying title/image can go here + /// + void OnTick(); + + /// + /// Internal function used by StreamDeckTools to prevent memory leaks + /// + void Destroy(); + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Backend/IEncoderPlugin.cs b/streamdeck-gpu/barraider-sdtools/Backend/IEncoderPlugin.cs new file mode 100644 index 0000000..0601972 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Backend/IEncoderPlugin.cs @@ -0,0 +1,28 @@ +using BarRaider.SdTools.Payloads; + +namespace BarRaider.SdTools { + /// + /// Interface used to capture dial/encoder events + /// + public interface IEncoderPlugin : ICommonPluginFunctions { + /// + /// Called when the dial is rotated + /// + void DialRotate(DialRotatePayload payload); + + /// + /// Called when the Dial is pressed + /// + void DialDown(DialPayload payload); + + /// + /// Called when the Dial is released + /// + void DialUp(DialPayload payload); + + /// + /// Called when the touchpad (above the dials) is pressed + /// + void TouchPress(TouchpadPressPayload payload); + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Backend/IKeypadPlugin.cs b/streamdeck-gpu/barraider-sdtools/Backend/IKeypadPlugin.cs new file mode 100644 index 0000000..2bcf4ab --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Backend/IKeypadPlugin.cs @@ -0,0 +1,16 @@ +namespace BarRaider.SdTools { + /// + /// Interface used to capture key events + /// + public interface IKeypadPlugin : ICommonPluginFunctions { + /// + /// Called when a Stream Deck key is pressed + /// + void KeyPressed(KeyPayload payload); + + /// + /// Called when a Stream Deck key is released + /// + void KeyReleased(KeyPayload payload); + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Backend/ISDConnection.cs b/streamdeck-gpu/barraider-sdtools/Backend/ISDConnection.cs new file mode 100644 index 0000000..7621771 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Backend/ISDConnection.cs @@ -0,0 +1,231 @@ +using BarRaider.SdTools.Events; +using BarRaider.SdTools.Wrappers; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Threading.Tasks; + +namespace BarRaider.SdTools { + /// + /// Interface for a Stream Deck connection + /// + public interface ISDConnection : IDisposable { + #region Events + + /// + /// Event received by the plugin when the Property Inspector uses the sendToPlugin event. + /// + event EventHandler> OnSendToPlugin; + /// + /// Event received when the user changes the title or title parameters. + /// + event EventHandler> OnTitleParametersDidChange; + /// + /// Event received when a monitored application is terminated + /// + event EventHandler> OnApplicationDidTerminate; + /// + /// Event received when a monitored application is launched + /// + event EventHandler> OnApplicationDidLaunch; + /// + /// Event received when a device is unplugged from the computer + /// + event EventHandler> OnDeviceDidDisconnect; + /// + /// Event received when a device is plugged to the computer. + /// + event EventHandler> OnDeviceDidConnect; + /// + /// Event received when the Property Inspector appears in the Stream Deck software user interface, for example when selecting a new instance. + /// + event EventHandler> OnPropertyInspectorDidAppear; + /// + /// Event received when the Property Inspector for an instance is removed from the Stream Deck software user interface, for example when selecting a different instance. + /// + event EventHandler> OnPropertyInspectorDidDisappear; + /// + /// Event received when the computer wakes up + /// + event EventHandler> OnSystemDidWakeUp; + + #endregion + + #region Methods + + /// + /// Send settings to the PropertyInspector + /// + /// + /// + Task SendToPropertyInspectorAsync(JObject settings); + + /// + /// Persists your plugin settings + /// + /// + /// + Task SetSettingsAsync(JObject settings); + + /// + /// Persists your global plugin settings + /// + /// Settings to save globally + /// Boolean whether to also trigger a didReceiveGlobalSettings event. Default is true + /// + Task SetGlobalSettingsAsync(JObject settings, bool triggerDidReceiveGlobalSettings = true); + + /// + /// Persists your global plugin settings + /// + /// + Task GetGlobalSettingsAsync(); + + /// + /// Sets an image on the StreamDeck key. + /// + /// Base64 encoded image + /// A 0-based integer value representing the state of an action with multiple states. This is an optional parameter. If not specified, the title is set to all states. + /// Should image be sent even if it is identical to the one sent previously. Default is false + /// + Task SetImageAsync(string base64Image, int? state = null, bool forceSendToStreamdeck = false); + + /// + /// Sets an image on the StreamDeck key + /// + /// Image object + /// A 0-based integer value representing the state of an action with multiple states. This is an optional parameter. If not specified, the title is set to all states. + /// Should image be sent even if it is identical to the one sent previously. Default is false + /// + Task SetImageAsync(Image image, int? state = null, bool forceSendToStreamdeck = false); + + /// + /// Sets the default image for this state, as configured in the manifest + /// + /// + Task SetDefaultImageAsync(); + + /// + /// Sets a title on the StreamDeck key + /// + /// + /// A 0-based integer value representing the state of an action with multiple states. This is an optional parameter. If not specified, the title is set to all states. + /// + Task SetTitleAsync(string title, int? state = null); + + /// + /// Switches to one of the plugin's built-in profiles + /// + /// + /// + Task SwitchProfileAsync(string profileName); + + /// + /// Switches to one of the plugin's built-in profiles. Allows to choose which device to switch it on. + /// + /// + /// + /// + Task SwitchProfileAsync(string profileName, string deviceId); + + /// + /// Shows the Alert (Yellow Triangle) on the StreamDeck key + /// + /// + Task ShowAlert(); + + /// + /// Shows the Success (Green checkmark) on the StreamDeck key + /// + /// + Task ShowOk(); + + /// + /// Add a message to the Stream Deck log. This is the log located at: %appdata%\Elgato\StreamDeck\logs\StreamDeck0.log + /// + /// + /// + Task LogSDMessage(string message); + + /// + /// Gets the Stream Deck device's info + /// + /// + StreamDeckDeviceInfo DeviceInfo(); + + /// + /// Tells Stream Deck to return the current plugin settings via the ReceivedSettings function + /// + /// + Task GetSettingsAsync(); + + /// + /// Opens a URI in the user's browser + /// + /// + /// + Task OpenUrlAsync(string uri); + + /// + /// Opens a URI in the user's browser + /// + /// + /// + Task OpenUrlAsync(Uri uri); + + /// + /// Sets the plugin to a specific state which is pre-configured in the manifest file + /// + /// + /// + Task SetStateAsync(uint state); + + /// + /// Sets the values of touchpad layouts items + /// + /// Dictionary holding the layout item keys and values you want to change + /// + Task SetFeedbackAsync(Dictionary dictKeyValue); + + /// + /// Sets the value of a single touchpad layout item + /// + /// + Task SetFeedbackAsync(string layoutItemKey, string value); + + /// + /// Sets the values of touchpad layouts items using a preset JObject + /// + /// + /// + Task SetFeedbackAsync(JObject feedbackPayload); + + /// + /// Changes the current Stream Deck+ touch display layout + /// + /// + Task SetFeedbackLayoutAsync(string layout); + + #endregion + + /// + /// An opaque value identifying the plugin. This value is received during the Registration procedure + /// + [JsonIgnore] + String ContextId { get; } + + /// + /// An opaque value identifying the device the plugin is launched on. + /// + [JsonIgnore] + String DeviceId { get; } + + /// + /// StreamDeckConnection object, initialized based on the args received when launching the program + /// + [JsonIgnore] + Communication.StreamDeckConnection StreamDeckConnection { get; } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Backend/KeyAndEncoderBase.cs b/streamdeck-gpu/barraider-sdtools/Backend/KeyAndEncoderBase.cs new file mode 100644 index 0000000..6a242ab --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Backend/KeyAndEncoderBase.cs @@ -0,0 +1,104 @@ +using BarRaider.SdTools.Payloads; + +namespace BarRaider.SdTools { + + /// + /// Main abstract class your plugin should derive from for keys (not dials) + /// For dials use the EncoderBase or KeyAndEncoderBase + /// Holds implementation for all the basic functions + /// If you're missing an event, you can register to it from the Connection.StreamDeckConnection object + /// + public abstract class KeyAndEncoderBase : IKeypadPlugin, IEncoderPlugin { + /// + /// Called when the dial is rotated + /// + public abstract void DialRotate(DialRotatePayload payload); + + /// + /// Called when the Dial is pressed + /// + public abstract void DialDown(DialPayload payload); + + /// + /// Called when the Dial is released + /// + public abstract void DialUp(DialPayload payload); + + /// + /// Called when the touchpad (above the dials) is pressed + /// + public abstract void TouchPress(TouchpadPressPayload payload); + + /// + /// Called when a Stream Deck key is pressed + /// + public abstract void KeyPressed(KeyPayload payload); + + /// + /// Called when a Stream Deck key is released + /// + public abstract void KeyReleased(KeyPayload payload); + + /// + /// Called when the PropertyInspector has new settings + /// + /// + public abstract void ReceivedSettings(ReceivedSettingsPayload payload); + + /// + /// Called when GetGlobalSettings is called. + /// + /// + public abstract void ReceivedGlobalSettings(ReceivedGlobalSettingsPayload payload); + + /// + /// Called every second + /// Logic for displaying title/image can go here + /// + public abstract void OnTick(); + + /// + /// Abstract method Called when the plugin is disposed + /// + public abstract void Dispose(); + + /// + /// Main iDisposable Dispose function + /// + public void Destroy() { + Dispose(); + if (Connection != null) { + Connection.Dispose(); + } + } + + /// + /// Connection object which handles your communication with the Stream Deck app + /// + protected ISDConnection Connection { get; private set; } + + /// + /// Constructor for PluginBase. Receives the communication and plugin settings + /// Note that the settings object is not used by the base and should be consumed by the deriving class. + /// Usually, a private class inside the deriving class is created which stores the settings + /// Example for settings usage: + /// * if (payload.Settings == null || payload.Settings.Count == 0) + /// * { + /// * // Create default settings + /// * } + /// * else + /// * { + /// this.settings = payload.Settings.ToObject(); + /// * } + /// + /// + /// Communication module with Stream Deck + /// Plugin settings - NOTE: Not used in base class, should be consumed by deriving class +#pragma warning disable IDE0060 // Remove unused parameter + public KeyAndEncoderBase(ISDConnection connection, InitialPayload payload) +#pragma warning restore IDE0060 // Remove unused parameter + { + Connection = connection; + } + } +} \ No newline at end of file diff --git a/streamdeck-gpu/barraider-sdtools/Backend/KeypadBase.cs b/streamdeck-gpu/barraider-sdtools/Backend/KeypadBase.cs new file mode 100644 index 0000000..17fd761 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Backend/KeypadBase.cs @@ -0,0 +1,82 @@ +namespace BarRaider.SdTools { + + /// + /// Main abstract class your plugin should derive from for keys (not dials) + /// For dials use the EncoderBase or KeyAndEncoderBase + /// Holds implementation for all the basic functions + /// If you're missing an event, you can register to it from the Connection.StreamDeckConnection object + /// + public abstract class KeypadBase : IKeypadPlugin { + /// + /// Called when a Stream Deck key is pressed + /// + public abstract void KeyPressed(KeyPayload payload); + + /// + /// Called when a Stream Deck key is released + /// + public abstract void KeyReleased(KeyPayload payload); + + /// + /// Called when the PropertyInspector has new settings + /// + /// + public abstract void ReceivedSettings(ReceivedSettingsPayload payload); + + /// + /// Called when GetGlobalSettings is called. + /// + /// + public abstract void ReceivedGlobalSettings(ReceivedGlobalSettingsPayload payload); + + /// + /// Called every second + /// Logic for displaying title/image can go here + /// + public abstract void OnTick(); + + /// + /// Abstract method Called when the plugin is disposed + /// + public abstract void Dispose(); + + /// + /// Main iDisposable Dispose function + /// + public void Destroy() { + Dispose(); + if (Connection != null) { + Connection.Dispose(); + } + } + + /// + /// Connection object which handles your communication with the Stream Deck app + /// + protected ISDConnection Connection { get; private set; } + + /// + /// Constructor for PluginBase. Receives the communication and plugin settings + /// Note that the settings object is not used by the base and should be consumed by the deriving class. + /// Usually, a private class inside the deriving class is created which stores the settings + /// Example for settings usage: + /// * if (payload.Settings == null || payload.Settings.Count == 0) + /// * { + /// * // Create default settings + /// * } + /// * else + /// * { + /// this.settings = payload.Settings.ToObject(); + /// * } + /// + /// + /// Communication module with Stream Deck + /// Plugin settings - NOTE: Not used in base class, should be consumed by deriving class +#pragma warning disable IDE0060 // Remove unused parameter + public KeypadBase(ISDConnection connection, InitialPayload payload) +#pragma warning restore IDE0060 // Remove unused parameter + { + Connection = connection; + } + } +} \ No newline at end of file diff --git a/streamdeck-gpu/barraider-sdtools/Backend/PluginBase.cs b/streamdeck-gpu/barraider-sdtools/Backend/PluginBase.cs new file mode 100644 index 0000000..86cccd5 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Backend/PluginBase.cs @@ -0,0 +1,82 @@ +using System; + +namespace BarRaider.SdTools { + + /// + /// Obsolete! Use `KeypadBase` moving forward, or choose one of the other options: `EncoderBase`, `KeyAndEncoderBase` + /// + [Obsolete("PluginBase will be removed in next version. Use either 'KeypadBase' (if you don't support dials), 'EncoderBase' (for only dials), 'KeyAndEncoderBase' (for both keys and dials) instead")] + public abstract class PluginBase : IKeypadPlugin { + /// + /// Called when a Stream Deck key is pressed + /// + public abstract void KeyPressed(KeyPayload payload); + + /// + /// Called when a Stream Deck key is released + /// + public abstract void KeyReleased(KeyPayload payload); + + /// + /// Called when the PropertyInspector has new settings + /// + /// + public abstract void ReceivedSettings(ReceivedSettingsPayload payload); + + /// + /// Called when GetGlobalSettings is called. + /// + /// + public abstract void ReceivedGlobalSettings(ReceivedGlobalSettingsPayload payload); + + /// + /// Called every second + /// Logic for displaying title/image can go here + /// + public abstract void OnTick(); + + /// + /// Abstract method Called when the plugin is disposed + /// + public abstract void Dispose(); + + /// + /// Main iDisposable Dispose function + /// + public void Destroy() { + Dispose(); + if (Connection != null) { + Connection.Dispose(); + } + } + + /// + /// Connection object which handles your communication with the Stream Deck app + /// + protected ISDConnection Connection { get; private set; } + + /// + /// Constructor for PluginBase. Receives the communication and plugin settings + /// Note that the settings object is not used by the base and should be consumed by the deriving class. + /// Usually, a private class inside the deriving class is created which stores the settings + /// Example for settings usage: + /// * if (payload.Settings == null || payload.Settings.Count == 0) + /// * { + /// * // Create default settings + /// * } + /// * else + /// * { + /// this.settings = payload.Settings.ToObject(); + /// * } + /// + /// + /// Communication module with Stream Deck + /// Plugin settings - NOTE: Not used in base class, should be consumed by deriving class +#pragma warning disable IDE0060 // Remove unused parameter + public PluginBase(ISDConnection connection, InitialPayload payload) +#pragma warning restore IDE0060 // Remove unused parameter + { + Connection = connection; + } + } +} \ No newline at end of file diff --git a/streamdeck-gpu/barraider-sdtools/Backend/PluginContainer.cs b/streamdeck-gpu/barraider-sdtools/Backend/PluginContainer.cs new file mode 100644 index 0000000..0c099b3 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Backend/PluginContainer.cs @@ -0,0 +1,320 @@ +using BarRaider.SdTools.Communication; +using BarRaider.SdTools.Communication.SDEvents; +using BarRaider.SdTools.Payloads; +using BarRaider.SdTools.Wrappers; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; + +namespace BarRaider.SdTools { + class PluginContainer { + private const int STREAMDECK_INITIAL_CONNECTION_TIMEOUT_SECONDS = 60; + private StreamDeckConnection connection; + private readonly ManualResetEvent connectEvent = new ManualResetEvent(false); + private readonly ManualResetEvent disconnectEvent = new ManualResetEvent(false); + private readonly SemaphoreSlim instancesLock = new SemaphoreSlim(1); + private string pluginUUID = null; + private StreamDeckInfo deviceInfo; + + private static readonly Dictionary supportedActions = new Dictionary(); + + // Holds all instances of plugin + private static readonly Dictionary instances = new Dictionary(); + + public PluginContainer(PluginActionId[] supportedActionIds) { + foreach (PluginActionId action in supportedActionIds) { + supportedActions[action.ActionId] = action.PluginBaseType; + } + } + + public void Run(StreamDeckOptions options) { + pluginUUID = options.PluginUUID; + deviceInfo = options.DeviceInfo; + connection = new StreamDeckConnection(options.Port, options.PluginUUID, options.RegisterEvent); + + // Register for events + connection.OnConnected += Connection_OnConnected; + connection.OnDisconnected += Connection_OnDisconnected; + connection.OnKeyDown += Connection_OnKeyDown; + connection.OnKeyUp += Connection_OnKeyUp; + connection.OnWillAppear += Connection_OnWillAppear; + connection.OnWillDisappear += Connection_OnWillDisappear; + connection.OnDialRotate += Connection_OnDialRotate; + connection.OnDialDown += Connection_OnDialDown; + connection.OnDialUp += Connection_OnDialUp; + connection.OnTouchpadPress += Connection_OnTouchpadPress; + + // Settings changed + connection.OnDidReceiveSettings += Connection_OnDidReceiveSettings; + connection.OnDidReceiveGlobalSettings += Connection_OnDidReceiveGlobalSettings; + + // Start the connection + connection.Run(); + Logger.Instance.LogMessage(TracingLevel.DEBUG, $"Plugin Loaded: UUID: {pluginUUID} Device Info: {deviceInfo}"); + Logger.Instance.LogMessage(TracingLevel.INFO, $"Plugin version: {deviceInfo.Plugin.Version}"); + Logger.Instance.LogMessage(TracingLevel.INFO, "Connecting to Stream Deck..."); + + // Time to wait for initial connection + if (connectEvent.WaitOne(TimeSpan.FromSeconds(STREAMDECK_INITIAL_CONNECTION_TIMEOUT_SECONDS))) { + Logger.Instance.LogMessage(TracingLevel.INFO, "Connected to Stream Deck"); + + // Initialize GlobalSettings manager + GlobalSettingsManager.Instance.Initialize(connection); + + // We connected, loop every second until we disconnect + while (!disconnectEvent.WaitOne(TimeSpan.FromMilliseconds(1000))) { + RunTick(); + } + } + Logger.Instance.LogMessage(TracingLevel.INFO, "Plugin Disconnected - Exiting"); + } + + // Button pressed + private async void Connection_OnKeyDown(object sender, SDEventReceivedEventArgs e) { + await instancesLock.WaitAsync(); + try { +#if DEBUG + Logger.Instance.LogMessage(TracingLevel.DEBUG, $"Plugin Keydown: Context: {e.Event.Context} Action: {e.Event.Action} Payload: {e.Event.Payload?.ToStringEx()}"); +#endif + + if (instances.ContainsKey(e.Event.Context)) { + KeyPayload payload = new KeyPayload(e.Event.Payload.Coordinates, + e.Event.Payload.Settings, e.Event.Payload.State, e.Event.Payload.UserDesiredState, e.Event.Payload.IsInMultiAction); + if (instances[e.Event.Context] is IKeypadPlugin plugin) { + plugin.KeyPressed(payload); + } + else { + Logger.Instance.LogMessage(TracingLevel.ERROR, $"Keydown General Error: Could not convert {e.Event.Context} to IKeypadPlugin"); + } + } + } + finally { + instancesLock.Release(); + } + } + + // Button released + private async void Connection_OnKeyUp(object sender, SDEventReceivedEventArgs e) { + await instancesLock.WaitAsync(); + try { +#if DEBUG + Logger.Instance.LogMessage(TracingLevel.DEBUG, $"Plugin Keyup: Context: {e.Event.Context} Action: {e.Event.Action} Payload: {e.Event.Payload?.ToStringEx()}"); +#endif + + if (instances.ContainsKey(e.Event.Context)) { + KeyPayload payload = new KeyPayload(e.Event.Payload.Coordinates, + e.Event.Payload.Settings, e.Event.Payload.State, e.Event.Payload.UserDesiredState, e.Event.Payload.IsInMultiAction); + if (instances[e.Event.Context] is IKeypadPlugin plugin) { + plugin.KeyReleased(payload); + } + else { + Logger.Instance.LogMessage(TracingLevel.ERROR, $"Keyup General Error: Could not convert {e.Event.Context} to IKeypadPlugin"); + } + } + } + finally { + instancesLock.Release(); + } + } + + // Function runs every second, used to update UI + private async void RunTick() { + await instancesLock.WaitAsync(); + try { + foreach (KeyValuePair kvp in instances.ToArray()) { + kvp.Value.OnTick(); + } + } + finally { + instancesLock.Release(); + } + } + + // Action is loaded in the Stream Deck + private async void Connection_OnWillAppear(object sender, SDEventReceivedEventArgs e) { + SDConnection conn = new SDConnection(connection, pluginUUID, deviceInfo, e.Event.Action, e.Event.Context, e.Event.Device); + await instancesLock.WaitAsync(); + try { +#if DEBUG + Logger.Instance.LogMessage(TracingLevel.DEBUG, $"Plugin OnWillAppear: Context: {e.Event.Context} Action: {e.Event.Action} Payload: {e.Event.Payload?.ToStringEx()}"); +#endif + + if (supportedActions.ContainsKey(e.Event.Action)) { + try { + if (instances.ContainsKey(e.Event.Context) && instances[e.Event.Context] != null) { + Logger.Instance.LogMessage(TracingLevel.INFO, $"WillAppear called for already existing context {e.Event.Context} (might be inside a multi-action)"); + return; + } + InitialPayload payload = new InitialPayload(e.Event.Payload, deviceInfo); + instances[e.Event.Context] = (ICommonPluginFunctions)Activator.CreateInstance(supportedActions[e.Event.Action], conn, payload); + } + catch (Exception ex) { + Logger.Instance.LogMessage(TracingLevel.FATAL, $"Could not create instance of {supportedActions[e.Event.Action]} with context {e.Event.Context} - This may be due to an Exception raised in the constructor, or the class does not inherit PluginBase with the same constructor {ex}"); + } + } + else { + Logger.Instance.LogMessage(TracingLevel.WARN, $"No plugin found that matches action: {e.Event.Action}"); + } + } + finally { + instancesLock.Release(); + } + } + + private async void Connection_OnWillDisappear(object sender, SDEventReceivedEventArgs e) { + await instancesLock.WaitAsync(); + try { +#if DEBUG + Logger.Instance.LogMessage(TracingLevel.DEBUG, $"Plugin OnWillDisappear: Context: {e.Event.Context} Action: {e.Event.Action} Payload: {e.Event.Payload?.ToStringEx()}"); +#endif + + if (instances.ContainsKey(e.Event.Context)) { + instances[e.Event.Context].Destroy(); + instances.Remove(e.Event.Context); + } + } + finally { + instancesLock.Release(); + } + } + + // Settings updated + private async void Connection_OnDidReceiveSettings(object sender, SDEventReceivedEventArgs e) { + await instancesLock.WaitAsync(); + try { +#if DEBUG + Logger.Instance.LogMessage(TracingLevel.DEBUG, $"Plugin OnDidReceiveSettings: Context: {e.Event.Context} Action: {e.Event.Action} Payload: {e.Event.Payload?.ToStringEx()}"); +#endif + + if (instances.ContainsKey(e.Event.Context)) { + instances[e.Event.Context].ReceivedSettings(JObject.FromObject(e.Event.Payload).ToObject()); + } + } + finally { + instancesLock.Release(); + } + } + + // Global settings updated + private async void Connection_OnDidReceiveGlobalSettings(object sender, SDEventReceivedEventArgs e) { + await instancesLock.WaitAsync(); + try { +#if DEBUG + Logger.Instance.LogMessage(TracingLevel.DEBUG, $"Plugin OnDidReceiveGlobalSettings: Settings: {e.Event.Payload?.ToStringEx()}"); +#endif + + var globalSettings = JObject.FromObject(e.Event.Payload).ToObject(); + foreach (string key in instances.Keys) { + instances[key].ReceivedGlobalSettings(globalSettings); + } + } + finally { + instancesLock.Release(); + } + } + + private async void Connection_OnTouchpadPress(object sender, SDEventReceivedEventArgs e) { + await instancesLock.WaitAsync(); + try { +#if DEBUG + Logger.Instance.LogMessage(TracingLevel.DEBUG, $"TouchpadPress: Context: {e.Event.Context} Action: {e.Event.Action} Payload: {e.Event.Payload?.ToStringEx()}"); +#endif + + if (instances.ContainsKey(e.Event.Context)) { + TouchpadPressPayload payload = new TouchpadPressPayload(e.Event.Payload.Coordinates, e.Event.Payload.Settings, e.Event.Payload.Controller, e.Event.Payload.IsLongPress, e.Event.Payload.TapPosition); + if (instances[e.Event.Context] is IEncoderPlugin plugin) { + plugin.TouchPress(payload); + } + else { + Logger.Instance.LogMessage(TracingLevel.ERROR, $"TouchpadPress General Error: Could not convert {e.Event.Context} to IEncoderPlugin"); + } + } + } + finally { + instancesLock.Release(); + } + } + + // Dial Up + + private async void Connection_OnDialUp(object sender, SDEventReceivedEventArgs e) { + await instancesLock.WaitAsync(); + try { +#if DEBUG + Logger.Instance.LogMessage(TracingLevel.DEBUG, $"DialPress: Context: {e.Event.Context} Action: {e.Event.Action} Payload: {e.Event.Payload?.ToStringEx()}"); +#endif + + if (instances.ContainsKey(e.Event.Context)) { + DialPayload payload = new DialPayload(e.Event.Payload.Coordinates, e.Event.Payload.Settings, e.Event.Payload.Controller); + if (instances[e.Event.Context] is IEncoderPlugin plugin) { + plugin.DialUp(payload); + } + else { + Logger.Instance.LogMessage(TracingLevel.ERROR, $"DialDown General Error: Could not convert {e.Event.Context} to IEncoderPlugin"); + } + } + } + finally { + instancesLock.Release(); + } + } + + // Dial Down + private async void Connection_OnDialDown(object sender, SDEventReceivedEventArgs e) { + await instancesLock.WaitAsync(); + try { +#if DEBUG + Logger.Instance.LogMessage(TracingLevel.DEBUG, $"DialPress: Context: {e.Event.Context} Action: {e.Event.Action} Payload: {e.Event.Payload?.ToStringEx()}"); +#endif + + if (instances.ContainsKey(e.Event.Context)) { + DialPayload payload = new DialPayload(e.Event.Payload.Coordinates, e.Event.Payload.Settings, e.Event.Payload.Controller); + if (instances[e.Event.Context] is IEncoderPlugin plugin) { + plugin.DialDown(payload); + } + else { + Logger.Instance.LogMessage(TracingLevel.ERROR, $"DialDown General Error: Could not convert {e.Event.Context} to IEncoderPlugin"); + } + } + } + finally { + instancesLock.Release(); + } + } + + private async void Connection_OnDialRotate(object sender, SDEventReceivedEventArgs e) { + await instancesLock.WaitAsync(); + try { +#if DEBUG + Logger.Instance.LogMessage(TracingLevel.DEBUG, $"DialRotate: Context: {e.Event.Context} Action: {e.Event.Action} Payload: {e.Event.Payload?.ToStringEx()}"); +#endif + + if (instances.ContainsKey(e.Event.Context)) { + DialRotatePayload payload = new DialRotatePayload(e.Event.Payload.Coordinates, e.Event.Payload.Settings, e.Event.Payload.Controller, e.Event.Payload.Ticks, e.Event.Payload.IsDialPressed); + if (instances[e.Event.Context] is IEncoderPlugin plugin) { + plugin.DialRotate(payload); + } + else { + Logger.Instance.LogMessage(TracingLevel.ERROR, $"DialRotate General Error: Could not convert {e.Event.Context} to IEncoderPlugin"); + } + } + } + finally { + instancesLock.Release(); + } + } + + + + private void Connection_OnConnected(object sender, EventArgs e) { + connectEvent.Set(); + } + + private void Connection_OnDisconnected(object sender, EventArgs e) { + Logger.Instance.LogMessage(TracingLevel.INFO, "Disconnect event received"); + disconnectEvent.Set(); + } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Backend/SDConnection.cs b/streamdeck-gpu/barraider-sdtools/Backend/SDConnection.cs new file mode 100644 index 0000000..9de36f6 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Backend/SDConnection.cs @@ -0,0 +1,439 @@ +using BarRaider.SdTools.Communication; +using BarRaider.SdTools.Communication.SDEvents; +using BarRaider.SdTools.Events; +using BarRaider.SdTools.Payloads; +using BarRaider.SdTools.Wrappers; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Threading.Tasks; + +namespace BarRaider.SdTools { + /// + /// Connection object which handles your communication with the Stream Deck app + /// + public class SDConnection : ISDConnection { + #region Private Members + + private string previousImageHash = null; + + [JsonIgnore] + private readonly string actionId; + + /// + /// An opaque value identifying the plugin. Received as an argument when the executable was launched. + /// + [JsonIgnore] + private readonly string pluginUUID; + + /// + /// Holds information about the devices connected to the computer + /// + [JsonIgnore] + private readonly StreamDeckInfo deviceInfo; + + #endregion + + #region Public Events + + /// + /// Event received by the plugin when the Property Inspector uses the sendToPlugin event. + /// + public event EventHandler> OnSendToPlugin; + /// + /// Event received when the user changes the title or title parameters. + /// + public event EventHandler> OnTitleParametersDidChange; + /// + /// Event received when a monitored application is terminated + /// + public event EventHandler> OnApplicationDidTerminate; + /// + /// Event received when a monitored application is launched + /// + public event EventHandler> OnApplicationDidLaunch; + /// + /// Event received when a device is unplugged from the computer + /// + public event EventHandler> OnDeviceDidDisconnect; + /// + /// Event received when a device is plugged to the computer. + /// + public event EventHandler> OnDeviceDidConnect; + /// + /// Event received when the Property Inspector appears in the Stream Deck software user interface, for example when selecting a new instance. + /// + public event EventHandler> OnPropertyInspectorDidAppear; + /// + /// Event received when the Property Inspector for an instance is removed from the Stream Deck software user interface, for example when selecting a different instance. + /// + public event EventHandler> OnPropertyInspectorDidDisappear; + /// + /// Event received when the computer wakes up + /// + public event EventHandler> OnSystemDidWakeUp; + + #endregion + + #region Public Properties + + + /// + /// An opaque value identifying the plugin. This value is received during the Registration procedure + /// + [JsonIgnore] + public String ContextId { get; private set; } + + /// + /// An opaque value identifying the device the plugin is launched on. + /// + [JsonIgnore] + public String DeviceId { get; private set; } + + /// + /// StreamDeckConnection object, initialized based on the args received when launching the program + /// + [JsonIgnore] + public StreamDeckConnection StreamDeckConnection { get; private set; } + + #endregion + + /// + /// Public constructor, a StreamDeckConnection object is required along with the current action and context IDs + /// These will be used to correctly communicate with the StreamDeck App + /// + /// + /// + /// + /// + /// + /// /// + public SDConnection(StreamDeckConnection connection, string pluginUUID, StreamDeckInfo deviceInfo, string actionId, string contextId, string deviceId) { + StreamDeckConnection = connection; + this.pluginUUID = pluginUUID; + this.deviceInfo = deviceInfo; + this.actionId = actionId; + this.ContextId = contextId; + this.DeviceId = deviceId; + + StreamDeckConnection.OnSendToPlugin += Connection_OnSendToPlugin; + StreamDeckConnection.OnTitleParametersDidChange += Connection_OnTitleParametersDidChange; + StreamDeckConnection.OnApplicationDidTerminate += Connection_OnApplicationDidTerminate; + StreamDeckConnection.OnApplicationDidLaunch += Connection_OnApplicationDidLaunch; + StreamDeckConnection.OnDeviceDidDisconnect += Connection_OnDeviceDidDisconnect; + StreamDeckConnection.OnDeviceDidConnect += Connection_OnDeviceDidConnect; + StreamDeckConnection.OnPropertyInspectorDidAppear += Connection_OnPropertyInspectorDidAppear; + StreamDeckConnection.OnPropertyInspectorDidDisappear += Connection_OnPropertyInspectorDidDisappear; + StreamDeckConnection.OnSystemDidWakeUp += StreamDeckConnection_OnSystemDidWakeUp; + } + + #region Public Methods + + /// + /// Dispose (Destructor) function + /// + public void Dispose() { + StreamDeckConnection.OnSendToPlugin -= Connection_OnSendToPlugin; + StreamDeckConnection.OnTitleParametersDidChange -= Connection_OnTitleParametersDidChange; + StreamDeckConnection.OnApplicationDidTerminate -= Connection_OnApplicationDidTerminate; + StreamDeckConnection.OnApplicationDidLaunch -= Connection_OnApplicationDidLaunch; + StreamDeckConnection.OnDeviceDidDisconnect -= Connection_OnDeviceDidDisconnect; + StreamDeckConnection.OnDeviceDidConnect -= Connection_OnDeviceDidConnect; + StreamDeckConnection.OnPropertyInspectorDidAppear -= Connection_OnPropertyInspectorDidAppear; + StreamDeckConnection.OnPropertyInspectorDidDisappear -= Connection_OnPropertyInspectorDidDisappear; + StreamDeckConnection.OnSystemDidWakeUp -= StreamDeckConnection_OnSystemDidWakeUp; + } + + /// + /// Gets the Stream Deck device's info + /// + /// + public StreamDeckDeviceInfo DeviceInfo() { + if (deviceInfo == null || string.IsNullOrEmpty(DeviceId)) { + Logger.Instance.LogMessage(TracingLevel.ERROR, $"Could not get DeviceInfo for DeviceId: {DeviceId} Devices: {deviceInfo?.Devices?.Length}"); + return null; + } + + return deviceInfo.Devices.Where(d => d.Id == DeviceId).FirstOrDefault(); + } + + #endregion + + #region Public Requests + + /// + /// Send settings to the PropertyInspector + /// + /// + /// + public async Task SendToPropertyInspectorAsync(JObject settings) { + if (StreamDeckConnection != null && !String.IsNullOrEmpty(ContextId) && !String.IsNullOrEmpty(actionId)) { + await StreamDeckConnection.SendToPropertyInspectorAsync(actionId, settings, ContextId); + } + } + + /// + /// Persists your plugin settings + /// + /// + /// + public async Task SetSettingsAsync(JObject settings) { + if (StreamDeckConnection != null && !String.IsNullOrEmpty(ContextId) && !String.IsNullOrEmpty(actionId)) { + await StreamDeckConnection.SetSettingsAsync(settings, ContextId); + } + } + + /// + /// Persists your global plugin settings + /// + /// Settings to save globally + /// Boolean whether to also trigger a didReceiveGlobalSettings event. Default is true + /// + public async Task SetGlobalSettingsAsync(JObject settings, bool triggerDidReceiveGlobalSettings = true) { + if (StreamDeckConnection != null) { + await StreamDeckConnection.SetGlobalSettingsAsync(settings); + + if (triggerDidReceiveGlobalSettings) { + await GetGlobalSettingsAsync(); + } + } + } + + /// + /// Persists your global plugin settings + /// + /// + public async Task GetGlobalSettingsAsync() { + if (StreamDeckConnection != null) { + await StreamDeckConnection.GetGlobalSettingsAsync(); + } + } + + /// + /// Sets an image on the StreamDeck key. + /// + /// Base64 encoded image + /// A 0-based integer value representing the state of an action with multiple states. This is an optional parameter. If not specified, the title is set to all states. + /// Should image be sent even if it is identical to the one sent previously. Default is false + /// + public async Task SetImageAsync(string base64Image, int? state = null, bool forceSendToStreamdeck = false) { + string hash = Tools.StringToSHA512(base64Image); + if (forceSendToStreamdeck || hash != previousImageHash) { + previousImageHash = hash; + await StreamDeckConnection.SetImageAsync(base64Image, ContextId, SDKTarget.HardwareAndSoftware, state); + } + } + + /// + /// Sets an image on the StreamDeck key + /// + /// Image object + /// A 0-based integer value representing the state of an action with multiple states. This is an optional parameter. If not specified, the title is set to all states. + /// Should image be sent even if it is identical to the one sent previously. Default is false + /// + public async Task SetImageAsync(Image image, int? state = null, bool forceSendToStreamdeck = false) { + string hash = Tools.ImageToSHA512(image); + if (forceSendToStreamdeck || hash != previousImageHash) { + previousImageHash = hash; + await StreamDeckConnection.SetImageAsync(image, ContextId, SDKTarget.HardwareAndSoftware, state); + } + } + + /// + /// Sets the default image for this state, as configured in the manifest + /// + /// + public async Task SetDefaultImageAsync() { + await SetImageAsync((String)null); + } + + /// + /// Sets a title on the StreamDeck key + /// + /// + /// A 0-based integer value representing the state of an action with multiple states. This is an optional parameter. If not specified, the title is set to all states. + /// + public async Task SetTitleAsync(string title, int? state = null) { + await StreamDeckConnection.SetTitleAsync(title, ContextId, SDKTarget.HardwareAndSoftware, state); + } + + /// + /// Switches to one of the plugin's built-in profiles + /// + /// + /// + public async Task SwitchProfileAsync(string profileName) { + await StreamDeckConnection.SwitchToProfileAsync(this.DeviceId, profileName, this.pluginUUID); + } + + /// + /// Switches to one of the plugin's built-in profiles. Allows to choose which device to switch it on. + /// + /// + /// + /// + public async Task SwitchProfileAsync(string profileName, string deviceId) { + await StreamDeckConnection.SwitchToProfileAsync(deviceId, profileName, this.pluginUUID); + } + + /// + /// Shows the Alert (Yellow Triangle) on the StreamDeck key + /// + /// + public async Task ShowAlert() { + await StreamDeckConnection.ShowAlertAsync(ContextId); + } + + /// + /// Shows the Success (Green checkmark) on the StreamDeck key + /// + /// + public async Task ShowOk() { + await StreamDeckConnection.ShowOkAsync(ContextId); + } + + /// + /// Add a message to the Stream Deck log. This is the log located at: %appdata%\Elgato\StreamDeck\logs\StreamDeck0.log + /// + /// + /// + public async Task LogSDMessage(string message) { + await StreamDeckConnection.LogMessageAsync(message); + } + + /// + /// Tells Stream Deck to return the current plugin settings via the ReceivedSettings function + /// + /// + public async Task GetSettingsAsync() { + await StreamDeckConnection.GetSettingsAsync(ContextId); + } + + /// + /// Opens a URI in the user's browser + /// + /// + /// + public async Task OpenUrlAsync(string uri) { + await StreamDeckConnection.OpenUrlAsync(uri); + } + + /// + /// Opens a URI in the user's browser + /// + /// + /// + public async Task OpenUrlAsync(Uri uri) { + await StreamDeckConnection.OpenUrlAsync(uri); + } + + /// + /// Sets the plugin to a specific state which is pre-configured in the manifest file + /// + /// + /// + public async Task SetStateAsync(uint state) { + await StreamDeckConnection.SetStateAsync(state, ContextId); + } + + /// + /// Sets the values of touchpad layouts items + /// + /// + /// + public async Task SetFeedbackAsync(Dictionary dictKeyValues) { + await StreamDeckConnection.SetFeedbackAsync(dictKeyValues, ContextId); + } + + /// + /// Sets the value of a single touchpad layout item + /// + /// + public async Task SetFeedbackAsync(string layoutItemKey, string value) { + await StreamDeckConnection.SetFeedbackAsync(new Dictionary() { { layoutItemKey, value } }, ContextId); + } + + /// + /// Sets the value of a single touchpad layout item + /// + /// + public async Task SetFeedbackAsync(JObject feedbackPayload) { + await StreamDeckConnection.SetFeedbackAsync(feedbackPayload, ContextId); + } + + + /// + /// Changes the current Stream Deck+ touch display layout + /// + /// + public async Task SetFeedbackLayoutAsync(string layout) { + await StreamDeckConnection.SetFeedbackLayoutAsync(layout, ContextId); + } + + #endregion + + #region Event Wrappers + + private void Connection_OnPropertyInspectorDidDisappear(object sender, SDEventReceivedEventArgs e) { + if (e.Event.Context == ContextId) { + OnPropertyInspectorDidDisappear?.Invoke(this, new SDEventReceivedEventArgs(new PropertyInspectorDidDisappear(e.Event.Action, e.Event.Context, e.Event.Device))); + } + } + + private void Connection_OnPropertyInspectorDidAppear(object sender, SDEventReceivedEventArgs e) { + if (e.Event.Context == ContextId) { + OnPropertyInspectorDidAppear?.Invoke(this, new SDEventReceivedEventArgs(new PropertyInspectorDidAppear(e.Event.Action, e.Event.Context, e.Event.Device))); + } + } + + private void Connection_OnDeviceDidConnect(object sender, SDEventReceivedEventArgs e) { + OnDeviceDidConnect?.Invoke(this, new SDEventReceivedEventArgs(new DeviceDidConnect(e.Event.DeviceInfo))); + } + + private void Connection_OnDeviceDidDisconnect(object sender, SDEventReceivedEventArgs e) { + OnDeviceDidDisconnect?.Invoke(this, new SDEventReceivedEventArgs(new DeviceDidDisconnect(e.Event.Device))); + } + + private void Connection_OnApplicationDidTerminate(object sender, SDEventReceivedEventArgs e) { + OnApplicationDidTerminate?.Invoke(this, new SDEventReceivedEventArgs(new ApplicationDidTerminate(new Payloads.ApplicationPayload(e.Event.Payload.Application)))); + } + + private void Connection_OnApplicationDidLaunch(object sender, SDEventReceivedEventArgs e) { + OnApplicationDidLaunch?.Invoke(this, new SDEventReceivedEventArgs(new ApplicationDidLaunch(new Payloads.ApplicationPayload(e.Event.Payload.Application)))); + } + + private void Connection_OnTitleParametersDidChange(object sender, SDEventReceivedEventArgs e) { + if (e.Event.Context == ContextId) { + // Special case to take into account that TitleParameters arrives right after an OnWillAppear + if (OnTitleParametersDidChange == null) { + if (sender != this) { + Task.Run(async () => { + await Task.Delay(1000); + Connection_OnTitleParametersDidChange(this, e); + }); + } + return; + } + + var payload = e.Event.Payload; + var newPayload = new TitleParametersPayload(payload.Settings, payload.Coordinates, payload.State, payload.Title, payload.TitleParameters); + OnTitleParametersDidChange?.Invoke(this, new SDEventReceivedEventArgs(new TitleParametersDidChange(e.Event.Action, e.Event.Context, e.Event.Device, newPayload))); + } + } + + private void Connection_OnSendToPlugin(object sender, SDEventReceivedEventArgs e) { + if (e.Event.Context == ContextId) { + OnSendToPlugin?.Invoke(this, new SDEventReceivedEventArgs(new SendToPlugin(e.Event.Action, e.Event.Context, e.Event.Payload))); + } + } + + private void StreamDeckConnection_OnSystemDidWakeUp(object sender, SDEventReceivedEventArgs e) { + OnSystemDidWakeUp?.Invoke(this, new SDEventReceivedEventArgs(new SystemDidWakeUp())); + } + + #endregion + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Backend/SDWrapper.cs b/streamdeck-gpu/barraider-sdtools/Backend/SDWrapper.cs new file mode 100644 index 0000000..9ba2cd0 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Backend/SDWrapper.cs @@ -0,0 +1,89 @@ +using BarRaider.SdTools.Payloads; +using CommandLine; +using System; + +namespace BarRaider.SdTools { + /// + /// * Easy Configuration Instructions: + ///* 1. Use NuGet to get the following packages: + ///* CommandLineParser by gsscoder + ///* streamdeck-client-csharp by Shane DeSeranno + ///* Newtonsoft.Json by James Newton-King + ///* 2. Create a class that implements the IPluginable interface (which is located in BarRaider.SDTools), this will be your main plugin + ///* 3. Pass the type of the class to the main function + /// + public static class SDWrapper { + // Handles all the communication with the plugin + private static PluginContainer container; + + /// /************************************************************************ + /// * Initial configuration from TyrenDe's streamdeck-client-csharp example: + /// * https://github.com/TyrenDe/streamdeck-client-csharp + /// * and SaviorXTanren's MixItUp.StreamDeckPlugin: + /// * https://github.com/SaviorXTanren/mixer-mixitup/ + /// *************************************************************************/ + + + /// + /// Library's main initialization point. + /// Pass the args from your Main function. We'll handle the rest + /// + /// + public static void Run(string[] args) { + Run(args, Tools.AutoLoadPluginActions()); + } + + /// + /// Library's main initialization point. + /// Pass the args from your Main function and a list of supported PluginActionIds, the framework will handle the rest. + /// + /// + /// + private static void Run(string[] args, PluginActionId[] supportedActionIds) { + Logger.Instance.LogMessage(TracingLevel.INFO, $"Plugin [{GetExeName()}] Loading - {supportedActionIds.Length} Actions Found"); + System.AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionTrapper; + +#if DEBUG + Logger.Instance.LogMessage(TracingLevel.DEBUG, $"Plugin Loading - Args: {String.Join(" ", args)}"); +#endif + + // The command line args parser expects all args to use `--`, so, let's append + for (int count = 0; count < args.Length; count++) { + if (args[count].StartsWith("-") && !args[count].StartsWith("--")) { + args[count] = $"-{args[count]}"; + } + } + + Parser parser = new Parser((with) => { + with.EnableDashDash = true; + with.CaseInsensitiveEnumValues = true; + with.CaseSensitive = false; + with.IgnoreUnknownArguments = true; + with.HelpWriter = Console.Error; + }); + + ParserResult options = parser.ParseArguments(args); + options.WithParsed(o => RunPlugin(o, supportedActionIds)); + } + + + private static void RunPlugin(StreamDeckOptions options, PluginActionId[] supportedActionIds) { + container = new PluginContainer(supportedActionIds); + container.Run(options); + } + + private static void UnhandledExceptionTrapper(object sender, UnhandledExceptionEventArgs e) { + Logger.Instance.LogMessage(TracingLevel.FATAL, $"Unhandled Exception: {e.ExceptionObject}"); + } + + private static string GetExeName() { + try { + return System.IO.Path.GetFileNameWithoutExtension(System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName); + } + catch (Exception ex) { + Logger.Instance.LogMessage(TracingLevel.WARN, $"GetExeName failed {ex}"); + } + return String.Empty; + } + } +} \ No newline at end of file diff --git a/streamdeck-gpu/barraider-sdtools/Communication/IPayload.cs b/streamdeck-gpu/barraider-sdtools/Communication/IPayload.cs new file mode 100644 index 0000000..b2786dd --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Communication/IPayload.cs @@ -0,0 +1,4 @@ +namespace BarRaider.SdTools.Communication { + internal interface IPayload { + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Communication/Messages/EmptyPayload.cs b/streamdeck-gpu/barraider-sdtools/Communication/Messages/EmptyPayload.cs new file mode 100644 index 0000000..9a18d60 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Communication/Messages/EmptyPayload.cs @@ -0,0 +1,7 @@ +namespace BarRaider.SdTools.Communication.Messages { + /// + /// Empty payload in event + /// + public class EmptyPayload : IPayload { + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Communication/Messages/GetGlobalSettingsMessage.cs b/streamdeck-gpu/barraider-sdtools/Communication/Messages/GetGlobalSettingsMessage.cs new file mode 100644 index 0000000..d39b693 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Communication/Messages/GetGlobalSettingsMessage.cs @@ -0,0 +1,15 @@ +using Newtonsoft.Json; + +namespace BarRaider.SdTools.Communication.Messages { + internal class GetGlobalSettingsMessage : IMessage { + [JsonProperty("event")] + public string Event { get { return "getGlobalSettings"; } } + + [JsonProperty("context")] + public string Context { get; private set; } + + public GetGlobalSettingsMessage(string pluginUUID) { + this.Context = pluginUUID; + } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Communication/Messages/GetSettingsMessage.cs b/streamdeck-gpu/barraider-sdtools/Communication/Messages/GetSettingsMessage.cs new file mode 100644 index 0000000..2604822 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Communication/Messages/GetSettingsMessage.cs @@ -0,0 +1,15 @@ +using Newtonsoft.Json; + +namespace BarRaider.SdTools.Communication.Messages { + internal class GetSettingsMessage : IMessage { + [JsonProperty("event")] + public string Event { get { return "getSettings"; } } + + [JsonProperty("context")] + public string Context { get; private set; } + + public GetSettingsMessage(string context) { + this.Context = context; + } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Communication/Messages/IMessage.cs b/streamdeck-gpu/barraider-sdtools/Communication/Messages/IMessage.cs new file mode 100644 index 0000000..096dec0 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Communication/Messages/IMessage.cs @@ -0,0 +1,5 @@ +namespace BarRaider.SdTools.Communication.Messages { + internal interface IMessage { + string Event { get; } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Communication/Messages/LogMessage.cs b/streamdeck-gpu/barraider-sdtools/Communication/Messages/LogMessage.cs new file mode 100644 index 0000000..511c1df --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Communication/Messages/LogMessage.cs @@ -0,0 +1,24 @@ +using Newtonsoft.Json; + +namespace BarRaider.SdTools.Communication.Messages { + internal class LogMessage : IMessage { + [JsonProperty("event")] + public string Event { get { return "logMessage"; } } + + [JsonProperty("payload")] + public IPayload Payload { get; private set; } + + public LogMessage(string message) { + this.Payload = new PayloadClass(message); + } + + private class PayloadClass : IPayload { + [JsonProperty("message")] + public string Message { get; private set; } + + public PayloadClass(string message) { + this.Message = message; + } + } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Communication/Messages/OpenUrlMessage.cs b/streamdeck-gpu/barraider-sdtools/Communication/Messages/OpenUrlMessage.cs new file mode 100644 index 0000000..7f4b55d --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Communication/Messages/OpenUrlMessage.cs @@ -0,0 +1,25 @@ +using Newtonsoft.Json; +using System; + +namespace BarRaider.SdTools.Communication.Messages { + internal class OpenUrlMessage : IMessage { + [JsonProperty("event")] + public string Event { get { return "openUrl"; } } + + [JsonProperty("payload")] + public IPayload Payload { get; private set; } + + public OpenUrlMessage(Uri uri) { + this.Payload = new PayloadClass(uri); + } + + private class PayloadClass : IPayload { + [JsonProperty("url")] + public string Url { get; private set; } + + public PayloadClass(Uri uri) { + this.Url = uri.ToString(); + } + } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Communication/Messages/RegisterEventMessage.cs b/streamdeck-gpu/barraider-sdtools/Communication/Messages/RegisterEventMessage.cs new file mode 100644 index 0000000..9c514c7 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Communication/Messages/RegisterEventMessage.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace BarRaider.SdTools.Communication.Messages { + internal class RegisterEventMessage : IMessage { + [JsonProperty("event")] + public string Event { get; private set; } + + [JsonProperty("uuid")] + public string UUID { get; private set; } + + public RegisterEventMessage(string eventName, string uuid) { + this.Event = eventName; + this.UUID = uuid; + } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Communication/Messages/SendToPropertyInspectorMessage.cs b/streamdeck-gpu/barraider-sdtools/Communication/Messages/SendToPropertyInspectorMessage.cs new file mode 100644 index 0000000..3a44617 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Communication/Messages/SendToPropertyInspectorMessage.cs @@ -0,0 +1,24 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace BarRaider.SdTools.Communication.Messages { + internal class SendToPropertyInspectorMessage : IMessage { + [JsonProperty("event")] + public string Event { get { return "sendToPropertyInspector"; } } + + [JsonProperty("context")] + public string Context { get; private set; } + + [JsonProperty("payload")] + public JObject Payload { get; private set; } + + [JsonProperty("action")] + public string Action { get; private set; } + + public SendToPropertyInspectorMessage(string action, JObject data, string context) { + this.Context = context; + this.Payload = data; + this.Action = action; + } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Communication/Messages/SetFeedbackLayoutMessage.cs b/streamdeck-gpu/barraider-sdtools/Communication/Messages/SetFeedbackLayoutMessage.cs new file mode 100644 index 0000000..cc30d4c --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Communication/Messages/SetFeedbackLayoutMessage.cs @@ -0,0 +1,27 @@ +using Newtonsoft.Json; + +namespace BarRaider.SdTools.Communication.Messages { + internal class SetFeedbackLayoutMessage : IMessage { + [JsonProperty("event")] + public string Event { get { return "setFeedbackLayout"; } } + + [JsonProperty("context")] + public string Context { get; private set; } + + [JsonProperty("payload")] + public IPayload Payload { get; private set; } + + public SetFeedbackLayoutMessage(string layout, string context) { + this.Context = context; + this.Payload = new PayloadClass(layout); + } + + private class PayloadClass : IPayload { + [JsonProperty("layout")] + public string Layout { get; private set; } + public PayloadClass(string layout) { + this.Layout = layout; + } + } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Communication/Messages/SetFeedbackMessage.cs b/streamdeck-gpu/barraider-sdtools/Communication/Messages/SetFeedbackMessage.cs new file mode 100644 index 0000000..66418ab --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Communication/Messages/SetFeedbackMessage.cs @@ -0,0 +1,20 @@ +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace BarRaider.SdTools.Communication.Messages { + internal class SetFeedbackMessage : IMessage { + [JsonProperty("event")] + public string Event { get { return "setFeedback"; } } + + [JsonProperty("context")] + public string Context { get; private set; } + + [JsonProperty("payload")] + public Dictionary DictKeyValues { get; private set; } + + public SetFeedbackMessage(Dictionary dictKeyValues, string pluginUUID) { + this.Context = pluginUUID; + DictKeyValues = dictKeyValues; + } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Communication/Messages/SetFeedbackMessageEx.cs b/streamdeck-gpu/barraider-sdtools/Communication/Messages/SetFeedbackMessageEx.cs new file mode 100644 index 0000000..ac373c6 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Communication/Messages/SetFeedbackMessageEx.cs @@ -0,0 +1,20 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace BarRaider.SdTools.Communication.Messages { + internal class SetFeedbackMessageEx : IMessage { + [JsonProperty("event")] + public string Event { get { return "setFeedback"; } } + + [JsonProperty("context")] + public string Context { get; private set; } + + [JsonProperty("payload")] + public JObject Payload { get; private set; } + + public SetFeedbackMessageEx(JObject payload, string pluginUUID) { + this.Context = pluginUUID; + Payload = payload; + } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Communication/Messages/SetGlobalSettingsMessage.cs b/streamdeck-gpu/barraider-sdtools/Communication/Messages/SetGlobalSettingsMessage.cs new file mode 100644 index 0000000..33d3db5 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Communication/Messages/SetGlobalSettingsMessage.cs @@ -0,0 +1,20 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace BarRaider.SdTools.Communication.Messages { + internal class SetGlobalSettingsMessage : IMessage { + [JsonProperty("event")] + public string Event { get { return "setGlobalSettings"; } } + + [JsonProperty("context")] + public string Context { get; private set; } + + [JsonProperty("payload")] + public JObject Payload { get; private set; } + + public SetGlobalSettingsMessage(JObject settings, string pluginUUID) { + this.Context = pluginUUID; + this.Payload = settings; + } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Communication/Messages/SetImageMessage.cs b/streamdeck-gpu/barraider-sdtools/Communication/Messages/SetImageMessage.cs new file mode 100644 index 0000000..4c029d3 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Communication/Messages/SetImageMessage.cs @@ -0,0 +1,36 @@ +using Newtonsoft.Json; + +namespace BarRaider.SdTools.Communication.Messages { + internal class SetImageMessage : IMessage { + [JsonProperty("event")] + public string Event { get { return "setImage"; } } + + [JsonProperty("context")] + public string Context { get; private set; } + + [JsonProperty("payload")] + public IPayload Payload { get; private set; } + + public SetImageMessage(string base64Image, string context, SDKTarget target, int? state) { + this.Context = context; + this.Payload = new PayloadClass(base64Image, target, state); + } + + private class PayloadClass : IPayload { + [JsonProperty("image")] + public string Image { get; private set; } + + [JsonProperty("target")] + public SDKTarget Target { get; private set; } + + [JsonProperty("state", NullValueHandling = NullValueHandling.Ignore)] + public int? State { get; private set; } + + public PayloadClass(string image, SDKTarget target, int? state) { + this.Image = image; + this.Target = target; + this.State = state; + } + } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Communication/Messages/SetSettingsMessage.cs b/streamdeck-gpu/barraider-sdtools/Communication/Messages/SetSettingsMessage.cs new file mode 100644 index 0000000..e6b3341 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Communication/Messages/SetSettingsMessage.cs @@ -0,0 +1,20 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace BarRaider.SdTools.Communication.Messages { + internal class SetSettingsMessage : IMessage { + [JsonProperty("event")] + public string Event { get { return "setSettings"; } } + + [JsonProperty("context")] + public string Context { get; private set; } + + [JsonProperty("payload")] + public JObject Payload { get; private set; } + + public SetSettingsMessage(JObject settings, string context) { + this.Context = context; + this.Payload = settings; + } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Communication/Messages/SetStateMessage.cs b/streamdeck-gpu/barraider-sdtools/Communication/Messages/SetStateMessage.cs new file mode 100644 index 0000000..70a275c --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Communication/Messages/SetStateMessage.cs @@ -0,0 +1,28 @@ +using Newtonsoft.Json; + +namespace BarRaider.SdTools.Communication.Messages { + internal class SetStateMessage : IMessage { + [JsonProperty("event")] + public string Event { get { return "setState"; } } + + [JsonProperty("context")] + public string Context { get; private set; } + + [JsonProperty("payload")] + public IPayload Payload { get; private set; } + + public SetStateMessage(uint state, string context) { + this.Context = context; + this.Payload = new PayloadClass(state); + } + + private class PayloadClass : IPayload { + [JsonProperty("state")] + public uint State { get; private set; } + + public PayloadClass(uint state) { + this.State = state; + } + } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Communication/Messages/SetTitleMessage.cs b/streamdeck-gpu/barraider-sdtools/Communication/Messages/SetTitleMessage.cs new file mode 100644 index 0000000..9a3796d --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Communication/Messages/SetTitleMessage.cs @@ -0,0 +1,36 @@ +using Newtonsoft.Json; + +namespace BarRaider.SdTools.Communication.Messages { + internal class SetTitleMessage : IMessage { + [JsonProperty("event")] + public string Event { get { return "setTitle"; } } + + [JsonProperty("context")] + public string Context { get; private set; } + + [JsonProperty("payload")] + public IPayload Payload { get; private set; } + + public SetTitleMessage(string title, string context, SDKTarget target, int? state) { + this.Context = context; + this.Payload = new PayloadClass(title, target, state); + } + + private class PayloadClass : IPayload { + [JsonProperty("title")] + public string Title { get; private set; } + + [JsonProperty("target")] + public SDKTarget Target { get; private set; } + + [JsonProperty("state", NullValueHandling = NullValueHandling.Ignore)] + public int? State { get; private set; } + + public PayloadClass(string title, SDKTarget target, int? state) { + this.Title = title; + this.Target = target; + this.State = state; + } + } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Communication/Messages/ShowAlertMessage.cs b/streamdeck-gpu/barraider-sdtools/Communication/Messages/ShowAlertMessage.cs new file mode 100644 index 0000000..304a207 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Communication/Messages/ShowAlertMessage.cs @@ -0,0 +1,15 @@ +using Newtonsoft.Json; + +namespace BarRaider.SdTools.Communication.Messages { + internal class ShowAlertMessage : IMessage { + [JsonProperty("event")] + public string Event { get { return "showAlert"; } } + + [JsonProperty("context")] + public string Context { get; private set; } + + public ShowAlertMessage(string context) { + this.Context = context; + } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Communication/Messages/ShowOkMessage.cs b/streamdeck-gpu/barraider-sdtools/Communication/Messages/ShowOkMessage.cs new file mode 100644 index 0000000..db187d7 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Communication/Messages/ShowOkMessage.cs @@ -0,0 +1,15 @@ +using Newtonsoft.Json; + +namespace BarRaider.SdTools.Communication.Messages { + internal class ShowOkMessage : IMessage { + [JsonProperty("event")] + public string Event { get { return "showOk"; } } + + [JsonProperty("context")] + public string Context { get; private set; } + + public ShowOkMessage(string context) { + this.Context = context; + } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Communication/Messages/SwitchToProfileMessage.cs b/streamdeck-gpu/barraider-sdtools/Communication/Messages/SwitchToProfileMessage.cs new file mode 100644 index 0000000..ebeeaa2 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Communication/Messages/SwitchToProfileMessage.cs @@ -0,0 +1,37 @@ +using Newtonsoft.Json; + +namespace BarRaider.SdTools.Communication.Messages { + internal class SwitchToProfileMessage : IMessage { + [JsonProperty("event")] + public string Event { get { return "switchToProfile"; } } + + [JsonProperty("context")] + public string Context { get; private set; } + + [JsonProperty("device")] + public string Device { get; private set; } + + [JsonProperty("payload")] + public IPayload Payload { get; private set; } + + public SwitchToProfileMessage(string device, string profileName, string pluginUUID) { + this.Context = pluginUUID; + this.Device = device; + if (!string.IsNullOrEmpty(profileName)) { + this.Payload = new PayloadClass(profileName); + } + else { + this.Payload = new EmptyPayload(); + } + } + + private class PayloadClass : IPayload { + [JsonProperty("profile")] + public string Profile { get; private set; } + + public PayloadClass(string profile) { + this.Profile = profile; + } + } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/ApplicationDidLaunchEvent.cs b/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/ApplicationDidLaunchEvent.cs new file mode 100644 index 0000000..257eae9 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/ApplicationDidLaunchEvent.cs @@ -0,0 +1,15 @@ +using BarRaider.SdTools.Payloads; +using Newtonsoft.Json; + +namespace BarRaider.SdTools.Communication.SDEvents { + /// + /// Payload for ApplicationDidLaunch event + /// + public class ApplicationDidLaunchEvent : BaseEvent { + /// + /// Application information + /// + [JsonProperty("payload")] + public ApplicationPayload Payload { get; private set; } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/ApplicationDidTerminateEvent.cs b/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/ApplicationDidTerminateEvent.cs new file mode 100644 index 0000000..462c16e --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/ApplicationDidTerminateEvent.cs @@ -0,0 +1,15 @@ +using BarRaider.SdTools.Payloads; +using Newtonsoft.Json; + +namespace BarRaider.SdTools.Communication.SDEvents { + /// + /// Payload for ApplicationDidTerminate Event + /// + public class ApplicationDidTerminateEvent : BaseEvent { + /// + /// Application payload + /// + [JsonProperty("payload")] + public ApplicationPayload Payload { get; private set; } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/BaseEvent.cs b/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/BaseEvent.cs new file mode 100644 index 0000000..51bea2a --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/BaseEvent.cs @@ -0,0 +1,90 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; + +namespace BarRaider.SdTools.Communication.SDEvents { + /// + /// List of all supported event typs + /// + internal static class EventTypes { + public const string KeyDown = "keyDown"; + public const string KeyUp = "keyUp"; + public const string WillAppear = "willAppear"; + public const string WillDisappear = "willDisappear"; + public const string TitleParametersDidChange = "titleParametersDidChange"; + public const string DeviceDidConnect = "deviceDidConnect"; + public const string DeviceDidDisconnect = "deviceDidDisconnect"; + public const string ApplicationDidLaunch = "applicationDidLaunch"; + public const string ApplicationDidTerminate = "applicationDidTerminate"; + public const string SystemDidWakeUp = "systemDidWakeUp"; + public const string DidReceiveSettings = "didReceiveSettings"; + public const string DidReceiveGlobalSettings = "didReceiveGlobalSettings"; + public const string PropertyInspectorDidAppear = "propertyInspectorDidAppear"; + public const string PropertyInspectorDidDisappear = "propertyInspectorDidDisappear"; + public const string SendToPlugin = "sendToPlugin"; + public const string DialRotate = "dialRotate"; + public const string DialPress = "dialPress"; + public const string DialDown = "dialDown"; + public const string DialUp = "dialUp"; + public const string TouchpadPress = "touchTap"; + } + + /// + /// Base event that all the actual events derive from + /// + public abstract class BaseEvent { + private static readonly Dictionary eventsMap = new Dictionary + { + { EventTypes.KeyDown, typeof(KeyDownEvent) }, + { EventTypes.KeyUp, typeof(KeyUpEvent) }, + + { EventTypes.WillAppear, typeof(WillAppearEvent) }, + { EventTypes.WillDisappear, typeof(WillDisappearEvent) }, + + { EventTypes.TitleParametersDidChange, typeof(TitleParametersDidChangeEvent) }, + + { EventTypes.DeviceDidConnect, typeof(DeviceDidConnectEvent) }, + { EventTypes.DeviceDidDisconnect, typeof(DeviceDidDisconnectEvent) }, + + { EventTypes.ApplicationDidLaunch, typeof(ApplicationDidLaunchEvent) }, + { EventTypes.ApplicationDidTerminate, typeof(ApplicationDidTerminateEvent) }, + + { EventTypes.SystemDidWakeUp, typeof(SystemDidWakeUpEvent) }, + + { EventTypes.DidReceiveSettings, typeof(DidReceiveSettingsEvent) }, + { EventTypes.DidReceiveGlobalSettings, typeof(DidReceiveGlobalSettingsEvent) }, + + { EventTypes.PropertyInspectorDidAppear, typeof(PropertyInspectorDidAppearEvent) }, + { EventTypes.PropertyInspectorDidDisappear, typeof(PropertyInspectorDidDisappearEvent) }, + + { EventTypes.SendToPlugin, typeof(SendToPluginEvent) }, + + { EventTypes.DialRotate, typeof(DialRotateEvent) }, + { EventTypes.DialDown, typeof(DialDownEvent) }, + { EventTypes.DialUp, typeof(DialUpEvent) }, + { EventTypes.TouchpadPress, typeof(TouchpadPressEvent) }, + { EventTypes.DialPress, typeof(DialDownEvent) }, // Deprecated: Should be removed when event stops getting sent by SD + }; + + /// + /// Name of the event raised + /// + [JsonProperty("event")] + public string Event { get; set; } + + internal static BaseEvent Parse(string json) { + JObject jsonObject = JObject.Parse(json); + if (!jsonObject.ContainsKey("event")) { + return null; + } + + string eventType = jsonObject["event"].ToString(); + if (!eventsMap.ContainsKey(eventType)) { + return null; + } + + return JsonConvert.DeserializeObject(json, eventsMap[eventType]) as BaseEvent; + } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/DeviceDidConnectEvent.cs b/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/DeviceDidConnectEvent.cs new file mode 100644 index 0000000..77bb5b6 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/DeviceDidConnectEvent.cs @@ -0,0 +1,20 @@ +using Newtonsoft.Json; + +namespace BarRaider.SdTools.Communication.SDEvents { + /// + /// Payload for DeviceDidConnect Event + /// + public class DeviceDidConnectEvent : BaseEvent { + /// + /// UUID of device + /// + [JsonProperty("device")] + public string Device { get; private set; } + + /// + /// Information on the device connected + /// + [JsonProperty("deviceInfo")] + public StreamDeckDeviceInfo DeviceInfo { get; private set; } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/DeviceDidDisconnectEvent.cs b/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/DeviceDidDisconnectEvent.cs new file mode 100644 index 0000000..bf840fa --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/DeviceDidDisconnectEvent.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace BarRaider.SdTools.Communication.SDEvents { + /// + /// Payload for DeviceDidDisconnect Event + /// + public class DeviceDidDisconnectEvent : BaseEvent { + /// + /// UUID of device that was disconnected + /// + [JsonProperty("device")] + public string Device { get; private set; } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/DialDownEvent.cs b/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/DialDownEvent.cs new file mode 100644 index 0000000..3ce2d85 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/DialDownEvent.cs @@ -0,0 +1,33 @@ +using BarRaider.SdTools.Payloads; +using Newtonsoft.Json; + +namespace BarRaider.SdTools.Communication.SDEvents { + /// + /// Payload for Dial down event + /// + public class DialDownEvent : BaseEvent { + /// + /// Action Name + /// + [JsonProperty("action")] + public string Action { get; private set; } + + /// + /// Unique Action UUID + /// + [JsonProperty("context")] + public string Context { get; private set; } + + /// + /// Device UUID key was pressed on + /// + [JsonProperty("device")] + public string Device { get; private set; } + + /// + /// Information on dial status + /// + [JsonProperty("payload")] + public DialPayload Payload { get; private set; } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/DialRotateEvent.cs b/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/DialRotateEvent.cs new file mode 100644 index 0000000..d781074 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/DialRotateEvent.cs @@ -0,0 +1,33 @@ +using BarRaider.SdTools.Payloads; +using Newtonsoft.Json; + +namespace BarRaider.SdTools.Communication.SDEvents { + /// + /// Payload for dial rotation event + /// + public class DialRotateEvent : BaseEvent { + /// + /// Action Name + /// + [JsonProperty("action")] + public string Action { get; private set; } + + /// + /// Unique Action UUID + /// + [JsonProperty("context")] + public string Context { get; private set; } + + /// + /// Device UUID key was pressed on + /// + [JsonProperty("device")] + public string Device { get; private set; } + + /// + /// Information on dial rotation + /// + [JsonProperty("payload")] + public DialRotatePayload Payload { get; private set; } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/DialUpEvent.cs b/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/DialUpEvent.cs new file mode 100644 index 0000000..fc96916 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/DialUpEvent.cs @@ -0,0 +1,33 @@ +using BarRaider.SdTools.Payloads; +using Newtonsoft.Json; + +namespace BarRaider.SdTools.Communication.SDEvents { + /// + /// Payload for Dial up event + /// + public class DialUpEvent : BaseEvent { + /// + /// Action Name + /// + [JsonProperty("action")] + public string Action { get; private set; } + + /// + /// Unique Action UUID + /// + [JsonProperty("context")] + public string Context { get; private set; } + + /// + /// Device UUID key was pressed on + /// + [JsonProperty("device")] + public string Device { get; private set; } + + /// + /// Information on dial status + /// + [JsonProperty("payload")] + public DialPayload Payload { get; private set; } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/DidReceiveGlobalSettingsEvent.cs b/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/DidReceiveGlobalSettingsEvent.cs new file mode 100644 index 0000000..6313d0f --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/DidReceiveGlobalSettingsEvent.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace BarRaider.SdTools.Communication.SDEvents { + /// + /// Payload for DidReceiveGlobalSettings Event + /// + public class DidReceiveGlobalSettingsEvent : BaseEvent { + /// + /// Global Settings payload + /// + [JsonProperty("payload")] + public ReceivedGlobalSettingsPayload Payload { get; private set; } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/DidReceiveSettingsEvent.cs b/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/DidReceiveSettingsEvent.cs new file mode 100644 index 0000000..8ac6dc5 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/DidReceiveSettingsEvent.cs @@ -0,0 +1,32 @@ +using Newtonsoft.Json; + +namespace BarRaider.SdTools.Communication.SDEvents { + /// + /// Payload for DidReceiveSettings Event + /// + public class DidReceiveSettingsEvent : BaseEvent { + /// + /// Action Name + /// + [JsonProperty("action")] + public string Action { get; private set; } + + /// + /// Context (unique action UUID) + /// + [JsonProperty("context")] + public string Context { get; private set; } + + /// + /// Device UUID action is on + /// + [JsonProperty("device")] + public string Device { get; private set; } + + /// + /// Settings for action + /// + [JsonProperty("payload")] + public ReceivedSettingsPayload Payload { get; private set; } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/KeyDownEvent.cs b/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/KeyDownEvent.cs new file mode 100644 index 0000000..49cfba4 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/KeyDownEvent.cs @@ -0,0 +1,32 @@ +using Newtonsoft.Json; + +namespace BarRaider.SdTools.Communication.SDEvents { + /// + /// Payload for KeyDown event + /// + public class KeyDownEvent : BaseEvent { + /// + /// Action Name + /// + [JsonProperty("action")] + public string Action { get; private set; } + + /// + /// Unique Action UUID + /// + [JsonProperty("context")] + public string Context { get; private set; } + + /// + /// Device UUID key was pressed on + /// + [JsonProperty("device")] + public string Device { get; private set; } + + /// + /// Information on key + /// + [JsonProperty("payload")] + public KeyPayload Payload { get; private set; } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/KeyUpEvent.cs b/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/KeyUpEvent.cs new file mode 100644 index 0000000..4e14f7e --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/KeyUpEvent.cs @@ -0,0 +1,32 @@ +using Newtonsoft.Json; + +namespace BarRaider.SdTools.Communication.SDEvents { + /// + /// Payload for KeyUp event + /// + public class KeyUpEvent : BaseEvent { + /// + /// Action name + /// + [JsonProperty("action")] + public string Action { get; private set; } + + /// + /// Unique action UUID + /// + [JsonProperty("context")] + public string Context { get; private set; } + + /// + /// Stream Deck device UUID + /// + [JsonProperty("device")] + public string Device { get; private set; } + + /// + /// Key settings + /// + [JsonProperty("payload")] + public KeyPayload Payload { get; private set; } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/PropertyInspectorDidAppearEvent.cs b/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/PropertyInspectorDidAppearEvent.cs new file mode 100644 index 0000000..3026a34 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/PropertyInspectorDidAppearEvent.cs @@ -0,0 +1,26 @@ +using Newtonsoft.Json; + +namespace BarRaider.SdTools.Communication.SDEvents { + /// + /// Payload for PropertyInspectorDidAppearEvent event + /// + public class PropertyInspectorDidAppearEvent : BaseEvent { + /// + /// Action Name + /// + [JsonProperty("action")] + public string Action { get; private set; } + + /// + /// Unique Action UUID + /// + [JsonProperty("context")] + public string Context { get; private set; } + + /// + /// Stream Deck device UUID + /// + [JsonProperty("device")] + public string Device { get; private set; } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/PropertyInspectorDidDisappearEvent.cs b/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/PropertyInspectorDidDisappearEvent.cs new file mode 100644 index 0000000..b19fda7 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/PropertyInspectorDidDisappearEvent.cs @@ -0,0 +1,26 @@ +using Newtonsoft.Json; + +namespace BarRaider.SdTools.Communication.SDEvents { + /// + /// Payload for PropertyInspectorDidDisappearEvent event + /// + public class PropertyInspectorDidDisappearEvent : BaseEvent { + /// + /// Action Name + /// + [JsonProperty("action")] + public string Action { get; private set; } + + /// + /// Unique Action UUID + /// + [JsonProperty("context")] + public string Context { get; private set; } + + /// + /// Stream Deck device UUID + /// + [JsonProperty("device")] + public string Device { get; private set; } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/SendToPluginEvent.cs b/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/SendToPluginEvent.cs new file mode 100644 index 0000000..f44bc65 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/SendToPluginEvent.cs @@ -0,0 +1,27 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace BarRaider.SdTools.Communication.SDEvents { + /// + /// Payload for SendToPluginEvent event + /// + public class SendToPluginEvent : BaseEvent { + /// + /// Action Name + /// + [JsonProperty("action")] + public string Action { get; private set; } + + /// + /// Unique Action UUID + /// + [JsonProperty("context")] + public string Context { get; private set; } + + /// + /// Payload + /// + [JsonProperty("payload")] + public JObject Payload { get; private set; } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/SystemDidWakeUpEvent.cs b/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/SystemDidWakeUpEvent.cs new file mode 100644 index 0000000..70291c0 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/SystemDidWakeUpEvent.cs @@ -0,0 +1,7 @@ +namespace BarRaider.SdTools.Communication.SDEvents { + /// + /// Payload for SystemDidWakeUp event + /// + public class SystemDidWakeUpEvent : BaseEvent { + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/TitleParametersDidChangeEvent.cs b/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/TitleParametersDidChangeEvent.cs new file mode 100644 index 0000000..775ef3b --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/TitleParametersDidChangeEvent.cs @@ -0,0 +1,33 @@ +using BarRaider.SdTools.Payloads; +using Newtonsoft.Json; + +namespace BarRaider.SdTools.Communication.SDEvents { + /// + /// Payload for TitleParametersDidChangeEvent event + /// + public class TitleParametersDidChangeEvent : BaseEvent { + /// + /// Action Name + /// + [JsonProperty("action")] + public string Action { get; private set; } + + /// + /// Unique Action UUID + /// + [JsonProperty("context")] + public string Context { get; private set; } + + /// + /// Stream Deck device UUID + /// + [JsonProperty("device")] + public string Device { get; private set; } + + /// + /// Title settings + /// + [JsonProperty("payload")] + public TitleParametersPayload Payload { get; private set; } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/TouchpadPressEvent.cs b/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/TouchpadPressEvent.cs new file mode 100644 index 0000000..b4d9556 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/TouchpadPressEvent.cs @@ -0,0 +1,33 @@ +using BarRaider.SdTools.Payloads; +using Newtonsoft.Json; + +namespace BarRaider.SdTools.Communication.SDEvents { + /// + /// Payload for touchpad press + /// + public class TouchpadPressEvent : BaseEvent { + /// + /// Action Name + /// + [JsonProperty("action")] + public string Action { get; private set; } + + /// + /// Unique Action UUID + /// + [JsonProperty("context")] + public string Context { get; private set; } + + /// + /// Device UUID key was pressed on + /// + [JsonProperty("device")] + public string Device { get; private set; } + + /// + /// Information on touchpad press + /// + [JsonProperty("payload")] + public TouchpadPressPayload Payload { get; private set; } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/WillAppearEvent.cs b/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/WillAppearEvent.cs new file mode 100644 index 0000000..7fa6b7d --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/WillAppearEvent.cs @@ -0,0 +1,33 @@ +using BarRaider.SdTools.Payloads; +using Newtonsoft.Json; + +namespace BarRaider.SdTools.Communication.SDEvents { + /// + /// Payload for WillAppearEvent event + /// + public class WillAppearEvent : BaseEvent { + /// + /// Action Name + /// + [JsonProperty("action")] + public string Action { get; private set; } + + /// + /// Unique Action UUID + /// + [JsonProperty("context")] + public string Context { get; private set; } + + /// + /// Stream Deck device UUID + /// + [JsonProperty("device")] + public string Device { get; private set; } + + /// + /// Appearance settings + /// + [JsonProperty("payload")] + public AppearancePayload Payload { get; private set; } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/WillDisappearEvent.cs b/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/WillDisappearEvent.cs new file mode 100644 index 0000000..f5a2c95 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Communication/SDEvents/WillDisappearEvent.cs @@ -0,0 +1,33 @@ +using BarRaider.SdTools.Payloads; +using Newtonsoft.Json; + +namespace BarRaider.SdTools.Communication.SDEvents { + /// + /// Payload for WillDisappearEvent event + /// + public class WillDisappearEvent : BaseEvent { + /// + /// Action Name + /// + [JsonProperty("action")] + public string Action { get; private set; } + + /// + /// Unique Action UUID + /// + [JsonProperty("context")] + public string Context { get; private set; } + + /// + /// Stream Deck device UUID + /// + [JsonProperty("device")] + public string Device { get; private set; } + + /// + /// settings + /// + [JsonProperty("payload")] + public AppearancePayload Payload { get; private set; } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Communication/SDKTarget.cs b/streamdeck-gpu/barraider-sdtools/Communication/SDKTarget.cs new file mode 100644 index 0000000..d73c29e --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Communication/SDKTarget.cs @@ -0,0 +1,21 @@ +namespace BarRaider.SdTools.Communication { + /// + /// Target to send Title/Image to + /// + public enum SDKTarget : int { + /// + /// Send to both App and Device + /// + HardwareAndSoftware = 0, + + /// + /// Send only to device + /// + HardwareOnly = 1, + + /// + /// Send only to app + /// + SoftwareOnly = 2, + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Communication/StreamDeckConnection.cs b/streamdeck-gpu/barraider-sdtools/Communication/StreamDeckConnection.cs new file mode 100644 index 0000000..d018789 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Communication/StreamDeckConnection.cs @@ -0,0 +1,403 @@ +using BarRaider.SdTools.Communication.Messages; +using BarRaider.SdTools.Communication.SDEvents; +using BarRaider.SdTools.Wrappers; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace BarRaider.SdTools.Communication { + /// + /// Underlying object that communicates with the stream deck app + /// + public class StreamDeckConnection { + private const int BufferSize = 1024 * 1024; + + private ClientWebSocket webSocket; + private readonly SemaphoreSlim sendSocketSemaphore = new SemaphoreSlim(1); + private readonly CancellationTokenSource cancelTokenSource = new CancellationTokenSource(); + private readonly string registerEvent; + + /// + /// The port used to connect to the StreamDeck websocket + /// + public int Port { get; private set; } + + /// + /// This is the unique identifier used to communicate with the register StreamDeck plugin. + /// + public string UUID { get; private set; } + + #region Public Events + + /// + /// Raised when plugin is connected to stream deck app + /// + public event EventHandler OnConnected; + + /// + /// /// Raised when plugin is disconnected from stream deck app + /// + public event EventHandler OnDisconnected; + + /// + /// Raised when key is pushed down + /// + public event EventHandler> OnKeyDown; + + /// + /// Raised when key is released + /// + public event EventHandler> OnKeyUp; + + /// + /// Raised when the action is shown, main trigger for a PluginAction + /// + public event EventHandler> OnWillAppear; + + /// + /// Raised when the action is no longer shown, main trigger for Dispose of PluginAction + /// + public event EventHandler> OnWillDisappear; + + /// + /// Contains information on the Title and its style + /// + public event EventHandler> OnTitleParametersDidChange; + + /// + /// Raised when a Stream Deck device is connected to the PC + /// + public event EventHandler> OnDeviceDidConnect; + + /// + /// Raised when a Stream Deck device has disconnected from the PC + /// + public event EventHandler> OnDeviceDidDisconnect; + + /// + /// Raised when a monitored app is launched/active + /// + public event EventHandler> OnApplicationDidLaunch; + + /// + /// Raised when a monitored app is terminated + /// + public event EventHandler> OnApplicationDidTerminate; + + /// + /// Raised after the PC wakes up from sleep + /// + public event EventHandler> OnSystemDidWakeUp; + + /// + /// Raised when settings for the action are received + /// + public event EventHandler> OnDidReceiveSettings; + + /// + /// Raised when global settings for the entire plugin are received + /// + public event EventHandler> OnDidReceiveGlobalSettings; + + /// + /// Raised when the user is viewing the settings in the Stream Deck app + /// + public event EventHandler> OnPropertyInspectorDidAppear; + + /// + /// Raised when the user stops viewing the settings in the Stream Deck app + /// + public event EventHandler> OnPropertyInspectorDidDisappear; + + /// + /// Raised when a payload is sent to the plugin from the PI + /// + public event EventHandler> OnSendToPlugin; + + /// + /// Raised when a dial is rotated + /// + public event EventHandler> OnDialRotate; + + /// + /// Raised when a dial is down + /// + public event EventHandler> OnDialDown; + + /// + /// Raised when a dial is up + /// + public event EventHandler> OnDialUp; + + /// + /// Raised when the tochpad is pressed + /// + public event EventHandler> OnTouchpadPress; + + #endregion + + internal StreamDeckConnection(int port, string uuid, string registerEvent) { + this.Port = port; + this.UUID = uuid; + this.registerEvent = registerEvent; + } + + internal void Run() { + if (webSocket == null) { + webSocket = new ClientWebSocket(); + _ = this.RunAsync(); + } + } + + internal void Stop() { + cancelTokenSource.Cancel(); + } + + internal Task SendAsync(IMessage message) { + try { + return SendAsync(JsonConvert.SerializeObject(message)); + } + catch (Exception ex) { + Logger.Instance.LogMessage(TracingLevel.ERROR, $"SDTools SendAsync Exception: {ex}"); + } + return null; + } + + #region Requests + + internal Task SetTitleAsync(string title, string context, SDKTarget target, int? state) { + return SendAsync(new SetTitleMessage(title, context, target, state)); + } + + internal Task LogMessageAsync(string message) { + return SendAsync(new LogMessage(message)); + } + + internal Task SetImageAsync(Image image, string context, SDKTarget target, int? state) { + try { + using (MemoryStream memoryStream = new MemoryStream()) { + image.Save(memoryStream, ImageFormat.Png); + byte[] imageBytes = memoryStream.ToArray(); + + // Convert byte[] to Base64 String + string base64String = $"data:image/png;base64,{Convert.ToBase64String(imageBytes)}"; + return SetImageAsync(base64String, context, target, state); + } + } + catch (Exception ex) { + Logger.Instance.LogMessage(TracingLevel.ERROR, $"SetImageAsync Exception: {ex}"); + } + return null; + } + + internal Task SetImageAsync(string base64Image, string context, SDKTarget target, int? state) { + return SendAsync(new SetImageMessage(base64Image, context, target, state)); + } + + internal Task ShowAlertAsync(string context) { + return SendAsync(new ShowAlertMessage(context)); + } + + internal Task ShowOkAsync(string context) { + return SendAsync(new ShowOkMessage(context)); + } + + internal Task SetGlobalSettingsAsync(JObject settings) { + return SendAsync(new SetGlobalSettingsMessage(settings, this.UUID)); + } + + internal Task GetGlobalSettingsAsync() { + return SendAsync(new GetGlobalSettingsMessage(this.UUID)); + } + + internal Task SetSettingsAsync(JObject settings, string context) { + return SendAsync(new SetSettingsMessage(settings, context)); + } + + internal Task GetSettingsAsync(string context) { + return SendAsync(new GetSettingsMessage(context)); + } + + internal Task SetStateAsync(uint state, string context) { + return SendAsync(new SetStateMessage(state, context)); + } + + internal Task SendToPropertyInspectorAsync(string action, JObject data, string context) { + return SendAsync(new SendToPropertyInspectorMessage(action, data, context)); + } + + internal Task SwitchToProfileAsync(string device, string profileName, string context) { + return SendAsync(new SwitchToProfileMessage(device, profileName, context)); + } + internal Task OpenUrlAsync(string uri) { + return OpenUrlAsync(new Uri(uri)); + } + + internal Task OpenUrlAsync(Uri uri) { + return SendAsync(new OpenUrlMessage(uri)); + } + + internal Task SetFeedbackAsync(Dictionary dictKeyValues, string context) { + return SendAsync(new SetFeedbackMessage(dictKeyValues, context)); + } + + internal Task SetFeedbackAsync(JObject feedbackPayload, string context) { + return SendAsync(new SetFeedbackMessageEx(feedbackPayload, context)); + } + + internal Task SetFeedbackLayoutAsync(string layout, string context) { + return SendAsync(new SetFeedbackLayoutMessage(layout, context)); + } + + #endregion + + #region Private Methods + + private async Task SendAsync(string text) { + try { + if (webSocket != null) { + try { + await sendSocketSemaphore.WaitAsync(); + byte[] buffer = Encoding.UTF8.GetBytes(text); + await webSocket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, cancelTokenSource.Token); + } + finally { + sendSocketSemaphore.Release(); + } + } + } + catch (Exception ex) { + Logger.Instance.LogMessage(TracingLevel.FATAL, $"{this.GetType()} SendAsync Exception: {ex}"); + await DisconnectAsync(); + } + + } + + private async Task RunAsync() { + try { + await webSocket.ConnectAsync(new Uri($"ws://localhost:{this.Port}"), cancelTokenSource.Token); + if (webSocket.State != WebSocketState.Open) { + + Logger.Instance.LogMessage(TracingLevel.FATAL, $"{this.GetType()} RunAsync failed - Websocket not open {webSocket.State}"); + await DisconnectAsync(); + return; + } + + await SendAsync(new RegisterEventMessage(registerEvent, this.UUID)); + + OnConnected?.Invoke(this, new EventArgs()); + await ReceiveAsync(); + } + finally { + Logger.Instance.LogMessage(TracingLevel.INFO, $"{this.GetType()} RunAsync completed, shutting down"); + await DisconnectAsync(); + } + } + + private async Task ReceiveAsync() { + byte[] buffer = new byte[BufferSize]; + ArraySegment arrayBuffer = new ArraySegment(buffer); + StringBuilder textBuffer = new StringBuilder(BufferSize); + + try { + while (!cancelTokenSource.IsCancellationRequested && webSocket != null) { + WebSocketReceiveResult result = await webSocket.ReceiveAsync(arrayBuffer, cancelTokenSource.Token); + + if (result != null) { + if (result.MessageType == WebSocketMessageType.Close || + (result.CloseStatus != null && result.CloseStatus.HasValue && result.CloseStatus.Value != WebSocketCloseStatus.Empty)) { + return result.CloseStatus.GetValueOrDefault(); + } + else if (result.MessageType == WebSocketMessageType.Text) { + textBuffer.Append(Encoding.UTF8.GetString(buffer, 0, result.Count)); + if (result.EndOfMessage) { +#if DEBUG + Logger.Instance.LogMessage(TracingLevel.DEBUG, $"Incoming Message: {textBuffer}"); +#endif + + string strBuffer = textBuffer.ToString(); + textBuffer.Clear(); + BaseEvent evt = BaseEvent.Parse(strBuffer); + if (evt == null) { + Logger.Instance.LogMessage(TracingLevel.WARN, $"{this.GetType()} Unknown event received from Stream Deck: {strBuffer}"); + continue; + } + + try { + switch (evt.Event) { + case EventTypes.KeyDown: OnKeyDown?.Invoke(this, new SDEventReceivedEventArgs(evt as KeyDownEvent)); break; + case EventTypes.KeyUp: OnKeyUp?.Invoke(this, new SDEventReceivedEventArgs(evt as KeyUpEvent)); break; + case EventTypes.WillAppear: OnWillAppear?.Invoke(this, new SDEventReceivedEventArgs(evt as WillAppearEvent)); break; + case EventTypes.WillDisappear: OnWillDisappear?.Invoke(this, new SDEventReceivedEventArgs(evt as WillDisappearEvent)); break; + case EventTypes.TitleParametersDidChange: OnTitleParametersDidChange?.Invoke(this, new SDEventReceivedEventArgs(evt as TitleParametersDidChangeEvent)); break; + case EventTypes.DeviceDidConnect: OnDeviceDidConnect?.Invoke(this, new SDEventReceivedEventArgs(evt as DeviceDidConnectEvent)); break; + case EventTypes.DeviceDidDisconnect: OnDeviceDidDisconnect?.Invoke(this, new SDEventReceivedEventArgs(evt as DeviceDidDisconnectEvent)); break; + case EventTypes.ApplicationDidLaunch: OnApplicationDidLaunch?.Invoke(this, new SDEventReceivedEventArgs(evt as ApplicationDidLaunchEvent)); break; + case EventTypes.ApplicationDidTerminate: OnApplicationDidTerminate?.Invoke(this, new SDEventReceivedEventArgs(evt as ApplicationDidTerminateEvent)); break; + case EventTypes.SystemDidWakeUp: OnSystemDidWakeUp?.Invoke(this, new SDEventReceivedEventArgs(evt as SystemDidWakeUpEvent)); break; + case EventTypes.DidReceiveSettings: OnDidReceiveSettings?.Invoke(this, new SDEventReceivedEventArgs(evt as DidReceiveSettingsEvent)); break; + case EventTypes.DidReceiveGlobalSettings: OnDidReceiveGlobalSettings?.Invoke(this, new SDEventReceivedEventArgs(evt as DidReceiveGlobalSettingsEvent)); break; + case EventTypes.PropertyInspectorDidAppear: OnPropertyInspectorDidAppear?.Invoke(this, new SDEventReceivedEventArgs(evt as PropertyInspectorDidAppearEvent)); break; + case EventTypes.PropertyInspectorDidDisappear: OnPropertyInspectorDidDisappear?.Invoke(this, new SDEventReceivedEventArgs(evt as PropertyInspectorDidDisappearEvent)); break; + case EventTypes.SendToPlugin: OnSendToPlugin?.Invoke(this, new SDEventReceivedEventArgs(evt as SendToPluginEvent)); break; + case EventTypes.DialRotate: OnDialRotate?.Invoke(this, new SDEventReceivedEventArgs(evt as DialRotateEvent)); break; + case EventTypes.DialDown: OnDialDown?.Invoke(this, new SDEventReceivedEventArgs(evt as DialDownEvent)); break; + case EventTypes.DialUp: OnDialUp?.Invoke(this, new SDEventReceivedEventArgs(evt as DialUpEvent)); break; + case EventTypes.DialPress: /* Ignoring deprecated Stream Deck event;*/ break; + case EventTypes.TouchpadPress: OnTouchpadPress?.Invoke(this, new SDEventReceivedEventArgs(evt as TouchpadPressEvent)); break; + default: + Logger.Instance.LogMessage(TracingLevel.WARN, $"{this.GetType()} Unsupported Stream Deck event: {strBuffer}"); + break; + } + } + catch (Exception ex) { + Logger.Instance.LogMessage(TracingLevel.ERROR, $"{this.GetType()} Unhandled 3rd party exception when triggering {evt.Event} event. Exception: {ex}"); + } + + } + } + } + } + } + catch (Exception ex) { + Logger.Instance.LogMessage(TracingLevel.FATAL, $"{this.GetType()} ReceiveAsync Exception: {ex}"); + } + + return WebSocketCloseStatus.NormalClosure; + } + + private async Task DisconnectAsync() { + if (webSocket != null) { + ClientWebSocket socket = webSocket; + webSocket = null; + + try { + await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Disconnecting", cancelTokenSource.Token); + } + catch (Exception ex) { + Logger.Instance.LogMessage(TracingLevel.ERROR, $"{this.GetType()} DisconnectAsync failed to close connection. Exception: {ex}"); + } + + + try { + socket.Dispose(); + } + catch (Exception ex) { + Logger.Instance.LogMessage(TracingLevel.ERROR, $"{this.GetType()} DisconnectAsync failed to dispose websocket. Exception: {ex}"); + } + + OnDisconnected?.Invoke(this, EventArgs.Empty); + } + } + + #endregion + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Events/ApplicationDidLaunch.cs b/streamdeck-gpu/barraider-sdtools/Events/ApplicationDidLaunch.cs new file mode 100644 index 0000000..dd0b4ad --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Events/ApplicationDidLaunch.cs @@ -0,0 +1,23 @@ +using BarRaider.SdTools.Payloads; +using Newtonsoft.Json; + +namespace BarRaider.SdTools.Events { + /// + /// Payload for ApplicationDidLaunch event + /// + public class ApplicationDidLaunch { + /// + /// Payload + /// + [JsonProperty("payload")] + public ApplicationPayload Payload { get; private set; } + + /// + /// Constructor + /// + /// + public ApplicationDidLaunch(ApplicationPayload payload) { + Payload = payload; + } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Events/ApplicationDidTerminate.cs b/streamdeck-gpu/barraider-sdtools/Events/ApplicationDidTerminate.cs new file mode 100644 index 0000000..d461943 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Events/ApplicationDidTerminate.cs @@ -0,0 +1,23 @@ +using BarRaider.SdTools.Payloads; +using Newtonsoft.Json; + +namespace BarRaider.SdTools.Events { + /// + /// Payload for ApplicationDidTerminate event + /// + public class ApplicationDidTerminate { + /// + /// Payload + /// + [JsonProperty("payload")] + public ApplicationPayload Payload { get; private set; } + + /// + /// Constructor + /// + /// + public ApplicationDidTerminate(ApplicationPayload payload) { + Payload = payload; + } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Events/DeviceDidConnect.cs b/streamdeck-gpu/barraider-sdtools/Events/DeviceDidConnect.cs new file mode 100644 index 0000000..c12fb54 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Events/DeviceDidConnect.cs @@ -0,0 +1,29 @@ +using Newtonsoft.Json; + +namespace BarRaider.SdTools.Events { + /// + /// Payload for DeviceDidConnect event + /// + public class DeviceDidConnect { + /// + /// Device GUID + /// + [JsonProperty("device")] + public string Device { get; private set; } + + /// + /// Device Info + /// + [JsonProperty("deviceInfo")] + public StreamDeckDeviceInfo DeviceInfo { get; private set; } + + /// + /// Constructor + /// + /// + public DeviceDidConnect(StreamDeckDeviceInfo deviceInfo) { + Device = deviceInfo?.Id; + DeviceInfo = deviceInfo; + } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Events/DeviceDidDisconnect.cs b/streamdeck-gpu/barraider-sdtools/Events/DeviceDidDisconnect.cs new file mode 100644 index 0000000..4a243c4 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Events/DeviceDidDisconnect.cs @@ -0,0 +1,23 @@ +using Newtonsoft.Json; +using System; + +namespace BarRaider.SdTools.Events { + /// + /// Payload for DeviceDidDisconnect event + /// + public class DeviceDidDisconnect { + /// + /// Device GUID + /// + [JsonProperty("device")] + public string Device { get; private set; } + + /// + /// Constructor + /// + /// + public DeviceDidDisconnect(String device) { + Device = device; + } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Events/PropertyInspectorDidAppear.cs b/streamdeck-gpu/barraider-sdtools/Events/PropertyInspectorDidAppear.cs new file mode 100644 index 0000000..3956cd4 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Events/PropertyInspectorDidAppear.cs @@ -0,0 +1,38 @@ +using Newtonsoft.Json; + +namespace BarRaider.SdTools.Events { + /// + /// Payload for PropertyInspectorDidAppear event + /// + public class PropertyInspectorDidAppear { + /// + /// ActionId + /// + [JsonProperty("action")] + public string Action { get; private set; } + + /// + /// ContextId + /// + [JsonProperty("context")] + public string Context { get; private set; } + + /// + /// Device Guid + /// + [JsonProperty("device")] + public string Device { get; private set; } + + /// + /// Constructor + /// + /// + /// + /// + public PropertyInspectorDidAppear(string action, string context, string device) { + Action = action; + Context = context; + Device = device; + } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Events/PropertyInspectorDidDisappear.cs b/streamdeck-gpu/barraider-sdtools/Events/PropertyInspectorDidDisappear.cs new file mode 100644 index 0000000..585f2a7 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Events/PropertyInspectorDidDisappear.cs @@ -0,0 +1,38 @@ +using Newtonsoft.Json; + +namespace BarRaider.SdTools.Events { + /// + /// Payload for PropertyInspectorDidDisappear event + /// + public class PropertyInspectorDidDisappear { + /// + /// Action Id + /// + [JsonProperty("action")] + public string Action { get; private set; } + + /// + /// ContextId + /// + [JsonProperty("context")] + public string Context { get; private set; } + + /// + /// Device Guid + /// + [JsonProperty("device")] + public string Device { get; private set; } + + /// + /// Constructor + /// + /// + /// + /// + public PropertyInspectorDidDisappear(string action, string context, string device) { + Action = action; + Context = context; + Device = device; + } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Events/SendToPlugin.cs b/streamdeck-gpu/barraider-sdtools/Events/SendToPlugin.cs new file mode 100644 index 0000000..17e57a6 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Events/SendToPlugin.cs @@ -0,0 +1,39 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace BarRaider.SdTools.Events { + /// + /// Payload for SendToPlugin event + /// + public class SendToPlugin { + /// + /// ActionId + /// + [JsonProperty("action")] + public string Action { get; private set; } + + /// + /// ContextId + /// + [JsonProperty("context")] + public string Context { get; private set; } + + /// + /// Payload + /// + [JsonProperty("payload")] + public JObject Payload { get; private set; } + + /// + /// Constructor + /// + /// + /// + /// + public SendToPlugin(string action, string context, JObject payload) { + Action = action; + Context = context; + Payload = payload; + } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Events/SystemDidWakeUp.cs b/streamdeck-gpu/barraider-sdtools/Events/SystemDidWakeUp.cs new file mode 100644 index 0000000..e18f405 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Events/SystemDidWakeUp.cs @@ -0,0 +1,12 @@ +namespace BarRaider.SdTools.Events { + /// + /// Payload for SystemDidWakeUp event + /// + public class SystemDidWakeUp { + /// + /// Constructor + /// + public SystemDidWakeUp() { + } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Events/TitleParametersDidChange.cs b/streamdeck-gpu/barraider-sdtools/Events/TitleParametersDidChange.cs new file mode 100644 index 0000000..3fbd769 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Events/TitleParametersDidChange.cs @@ -0,0 +1,47 @@ +using BarRaider.SdTools.Payloads; +using Newtonsoft.Json; + +namespace BarRaider.SdTools.Events { + /// + /// Payload for TitleParametersDidChange event + /// + public class TitleParametersDidChange { + /// + /// Action Id + /// + [JsonProperty("action")] + public string Action { get; private set; } + + /// + /// Context Id + /// + [JsonProperty("context")] + public string Context { get; private set; } + + /// + /// Device Guid + /// + [JsonProperty("device")] + public string Device { get; private set; } + + /// + /// Payload + /// + [JsonProperty("payload")] + public TitleParametersPayload Payload { get; private set; } + + /// + /// Constructor + /// + /// + /// + /// + /// + public TitleParametersDidChange(string action, string context, string device, TitleParametersPayload payload) { + Action = action; + Context = context; + Device = device; + Payload = payload; + } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Payloads/AppearancePayload.cs b/streamdeck-gpu/barraider-sdtools/Payloads/AppearancePayload.cs new file mode 100644 index 0000000..11f9391 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Payloads/AppearancePayload.cs @@ -0,0 +1,39 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace BarRaider.SdTools.Payloads { + /// + /// Payload for Apperance settings + /// + public class AppearancePayload { + /// + /// Additional settings + /// + [JsonProperty("settings")] + public JObject Settings { get; private set; } + + /// + /// Coordinates of key pressed + /// + [JsonProperty("coordinates")] + public KeyCoordinates Coordinates { get; private set; } + + /// + /// State of key + /// + [JsonProperty("state")] + public uint State { get; private set; } + + /// + /// Is action in MultiAction + /// + [JsonProperty("isInMultiAction")] + public bool IsInMultiAction { get; private set; } + + /// + /// Controller which issued the event + /// + [JsonProperty("controller")] + public string Controller { get; private set; } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Payloads/ApplicationPayload.cs b/streamdeck-gpu/barraider-sdtools/Payloads/ApplicationPayload.cs new file mode 100644 index 0000000..511bece --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Payloads/ApplicationPayload.cs @@ -0,0 +1,22 @@ +using Newtonsoft.Json; + +namespace BarRaider.SdTools.Payloads { + /// + /// ApplicationPayload + /// + public class ApplicationPayload { + /// + /// Application Name + /// + [JsonProperty("application")] + public string Application { get; private set; } + + /// + /// Constructor + /// + /// + public ApplicationPayload(string application) { + Application = application; + } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Payloads/DialPayload.cs b/streamdeck-gpu/barraider-sdtools/Payloads/DialPayload.cs new file mode 100644 index 0000000..09da194 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Payloads/DialPayload.cs @@ -0,0 +1,44 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace BarRaider.SdTools.Payloads { + /// + /// Payload received when a dial is down or up + /// + public class DialPayload { + /// + /// Controller which issued the event + /// + [JsonProperty("controller")] + public string Controller { get; private set; } + + /// + /// Current event settings + /// + [JsonProperty("settings")] + public JObject Settings { get; private set; } + + /// + /// Coordinates of key on the stream deck + /// + [JsonProperty("coordinates")] + public KeyCoordinates Coordinates { get; private set; } + + /// + /// Constructor + /// + /// + /// + /// + public DialPayload(KeyCoordinates coordinates, JObject settings, string controller) { + Coordinates = coordinates; + Settings = settings; + Controller = controller; + } + + /// + /// Default constructor for serialization + /// + public DialPayload() { } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Payloads/DialRotatePayload.cs b/streamdeck-gpu/barraider-sdtools/Payloads/DialRotatePayload.cs new file mode 100644 index 0000000..7efd5c8 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Payloads/DialRotatePayload.cs @@ -0,0 +1,60 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace BarRaider.SdTools.Payloads { + /// + /// Payload received when a dial is rotated + /// + public class DialRotatePayload { + /// + /// Controller which issued the event + /// + [JsonProperty("controller")] + public string Controller { get; private set; } + + /// + /// Current event settings + /// + [JsonProperty("settings")] + public JObject Settings { get; private set; } + + /// + /// Coordinates of key on the stream deck + /// + [JsonProperty("coordinates")] + public KeyCoordinates Coordinates { get; private set; } + + /// + /// Number of ticks rotated. Positive is to the right, negative to the left + /// + [JsonProperty("ticks")] + public int Ticks { get; private set; } + + /// + /// Boolean whether the dial is currently pressed or not + /// + [JsonProperty("pressed")] + public bool IsDialPressed { get; private set; } + + /// + /// Constructor + /// + /// + /// + /// + /// + /// + public DialRotatePayload(KeyCoordinates coordinates, JObject settings, string controller, int ticks, bool isDialPressed) { + Coordinates = coordinates; + Settings = settings; + Controller = controller; + Ticks = ticks; + IsDialPressed = isDialPressed; + } + + /// + /// Default constructor for serialization + /// + public DialRotatePayload() { } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Payloads/InitialPayload.cs b/streamdeck-gpu/barraider-sdtools/Payloads/InitialPayload.cs new file mode 100644 index 0000000..511574d --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Payloads/InitialPayload.cs @@ -0,0 +1,60 @@ +using BarRaider.SdTools.Payloads; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace BarRaider.SdTools { + /// + /// Payload received during the plugin's constructor + /// + public class InitialPayload { + /// + /// Plugin instance's settings (set through Property Inspector) + /// + [JsonProperty("settings")] + public JObject Settings { get; private set; } + + /// + /// Plugin's physical location on the Stream Deck device + /// + [JsonProperty("coordinates")] + public KeyCoordinates Coordinates { get; private set; } + + /// + /// Current plugin state + /// + [JsonProperty("state")] + public uint State { get; private set; } + + /// + /// Is it in a Multiaction + /// + [JsonProperty("isInMultiAction")] + public bool IsInMultiAction { get; private set; } + + /// + /// The controller of the current action. Values include "Keypad" and "Encoder". + /// + [JsonProperty("controller")] + public string Controller { get; private set; } + + /// + /// Information regarding the Stream Deck hardware device + /// + [JsonProperty("deviceInfo", Required = Required.AllowNull)] + public StreamDeckInfo DeviceInfo { get; private set; } + + /// + /// Constructor + /// + /// + /// + public InitialPayload(AppearancePayload appearancePayload, StreamDeckInfo deviceInfo) { + Coordinates = appearancePayload.Coordinates; + Settings = appearancePayload.Settings; + State = appearancePayload.State; + IsInMultiAction = appearancePayload.IsInMultiAction; + Controller = appearancePayload.Controller; + DeviceInfo = deviceInfo; + } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Payloads/KeyPayload.cs b/streamdeck-gpu/barraider-sdtools/Payloads/KeyPayload.cs new file mode 100644 index 0000000..13f7ba2 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Payloads/KeyPayload.cs @@ -0,0 +1,60 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace BarRaider.SdTools { + /// + /// Payload received when a key is pressed or released + /// + public class KeyPayload { + /// + /// Current event settings + /// + [JsonProperty("settings")] + public JObject Settings { get; private set; } + + /// + /// Coordinates of key on the stream deck + /// + [JsonProperty("coordinates")] + public KeyCoordinates Coordinates { get; private set; } + + /// + /// Current key state + /// + [JsonProperty("state")] + public uint State { get; private set; } + + /// + /// Desired state + /// + [JsonProperty("userDesiredState")] + public uint UserDesiredState { get; private set; } + + /// + /// Is part of a multiAction + /// + [JsonProperty("isInMultiAction")] + public bool IsInMultiAction { get; private set; } + + /// + /// Constructor + /// + /// + /// + /// + /// + /// + public KeyPayload(KeyCoordinates coordinates, JObject settings, uint state, uint userDesiredState, bool isInMultiAction) { + Coordinates = coordinates; + Settings = settings; + State = state; + UserDesiredState = userDesiredState; + IsInMultiAction = isInMultiAction; + } + + /// + /// For Seralization + /// + public KeyPayload() { } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Payloads/ReceivedGlobalSettingsPayload.cs b/streamdeck-gpu/barraider-sdtools/Payloads/ReceivedGlobalSettingsPayload.cs new file mode 100644 index 0000000..0b87058 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Payloads/ReceivedGlobalSettingsPayload.cs @@ -0,0 +1,15 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace BarRaider.SdTools { + /// + /// Payload that holds all the settings in the ReceivedGlobalSettings event + /// + public class ReceivedGlobalSettingsPayload { + /// + /// Global settings object + /// + [JsonProperty("settings")] + public JObject Settings { get; private set; } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Payloads/ReceivedSettingsPayload.cs b/streamdeck-gpu/barraider-sdtools/Payloads/ReceivedSettingsPayload.cs new file mode 100644 index 0000000..0ef8d9b --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Payloads/ReceivedSettingsPayload.cs @@ -0,0 +1,27 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace BarRaider.SdTools { + /// + /// Payload that holds all the settings in the ReceivedSettings event + /// + public class ReceivedSettingsPayload { + /// + /// Action's settings + /// + [JsonProperty("settings")] + public JObject Settings { get; private set; } + + /// + /// Coordinates of the key pressed + /// + [JsonProperty("coordinates")] + public KeyCoordinates Coordinates { get; private set; } + + /// + /// Is event part of a multiaction + /// + [JsonProperty("isInMultiAction")] + public bool IsInMultiAction { get; private set; } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Payloads/StreamDeckOptions.cs b/streamdeck-gpu/barraider-sdtools/Payloads/StreamDeckOptions.cs new file mode 100644 index 0000000..4ff5388 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Payloads/StreamDeckOptions.cs @@ -0,0 +1,54 @@ +using CommandLine; +using Newtonsoft.Json.Linq; + +namespace BarRaider.SdTools.Payloads { + /// + /// Class holding all the information passed to the plugin when the program was launched + /// + public class StreamDeckOptions { + private StreamDeckInfo deviceInfo; + + /// + /// Port to communicate with the StreamDeck app + /// + [Option("port", Required = true, HelpText = "The websocket port to connect to", SetName = "port")] + public int Port { get; set; } + + /// + /// UUID of the plugin + /// + [Option("pluginUUID", Required = true, HelpText = "The UUID of the plugin")] + public string PluginUUID { get; set; } + + /// + /// Name of the event we should pass to the StreamDeck app to register + /// + [Option("registerEvent", Required = true, HelpText = "The event triggered when the plugin is registered?")] + public string RegisterEvent { get; set; } + + /// + /// Raw information in JSON format which we will parse into the DeviceInfo property + /// + [Option("info", Required = true, HelpText = "Extra JSON launch data")] + public string RawInfo { get; set; } + + /// + /// Information regarding the StreamDeck app and StreamDeck hardware which was parsed from the RawInfo JSON field. + /// + public StreamDeckInfo DeviceInfo { + get { + if (deviceInfo != null) { + return deviceInfo; + } + + if (RawInfo == null) { + return null; + } + + JObject obj = JObject.Parse(RawInfo); + deviceInfo = obj.ToObject(); + return deviceInfo; + } + } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Payloads/TitleParametersPayload.cs b/streamdeck-gpu/barraider-sdtools/Payloads/TitleParametersPayload.cs new file mode 100644 index 0000000..f495cbd --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Payloads/TitleParametersPayload.cs @@ -0,0 +1,84 @@ +using BarRaider.SdTools.Wrappers; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace BarRaider.SdTools.Payloads { + /// + /// Payload for TitleParametersDidChange Event + /// + public class TitleParametersPayload { + private TitleParameters titleParameters = null; + + /// + /// Settings JSON Object + /// + [JsonProperty("settings")] + public JObject Settings { get; private set; } + + /// + /// Key Coordinates + /// + [JsonProperty("coordinates")] + public KeyCoordinates Coordinates { get; private set; } + + /// + /// Key State + /// + [JsonProperty("state")] + public uint State { get; private set; } + + /// + /// Title + /// + [JsonProperty("title")] + public string Title { get; private set; } + + /// + /// Title Parameters + /// + [JsonIgnore] + public TitleParameters TitleParameters { + get { + if (titleParameters != null) { + return titleParameters; + } + + if (TitleParametersRaw != null) { + titleParameters = new TitleParameters(TitleParametersRaw.FontFamily, TitleParametersRaw.FontSize, TitleParametersRaw.FontStyle, TitleParametersRaw.FontUnderline, TitleParametersRaw.ShowTitle, TitleParametersRaw.TitleAlignment, TitleParametersRaw.TitleColor); + } + + return titleParameters; + } + private set { + titleParameters = value; + } + } + + /// + /// Raw Title Parameters (not as proper object) + /// + [JsonProperty("titleParameters")] + public TitleParametersRawPayload TitleParametersRaw { get; private set; } + + /// + /// Constructor + /// + /// + /// + /// + /// + /// + public TitleParametersPayload(JObject settings, KeyCoordinates coordinates, uint state, string title, TitleParameters titleParameters) { + Settings = settings; + Coordinates = coordinates; + State = state; + Title = title; + TitleParameters = titleParameters; + } + + /// + /// For Serilization + /// + public TitleParametersPayload() { } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Payloads/TitleParametersRawPayload.cs b/streamdeck-gpu/barraider-sdtools/Payloads/TitleParametersRawPayload.cs new file mode 100644 index 0000000..2bd01e4 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Payloads/TitleParametersRawPayload.cs @@ -0,0 +1,50 @@ +using Newtonsoft.Json; + +namespace BarRaider.SdTools.Payloads { + /// + /// Raw payload for TitleParametersRawPayload event (without objects) + /// + public class TitleParametersRawPayload { + /// + /// Name of font family + /// + [JsonProperty("fontFamily")] + public string FontFamily { get; private set; } + + /// + /// Size of font + /// + [JsonProperty("fontSize")] + public uint FontSize { get; private set; } + + /// + /// Style of font (bold, italic) + /// + [JsonProperty("fontStyle")] + public string FontStyle { get; private set; } + + /// + /// Is there an underling + /// + [JsonProperty("fontUnderline")] + public bool FontUnderline { get; private set; } + + /// + /// Should title be shown + /// + [JsonProperty("showTitle")] + public bool ShowTitle { get; private set; } + + /// + /// Alignment of title (top, middle, bottom) + /// + [JsonProperty("titleAlignment")] + public string TitleAlignment { get; private set; } + + /// + /// Color of title + /// + [JsonProperty("titleColor")] + public string TitleColor { get; private set; } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Payloads/TouchpadPressPayload.cs b/streamdeck-gpu/barraider-sdtools/Payloads/TouchpadPressPayload.cs new file mode 100644 index 0000000..e58c585 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Payloads/TouchpadPressPayload.cs @@ -0,0 +1,61 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace BarRaider.SdTools.Payloads { + /// + /// Payload received when the touchpad (above the dials) is pressed + /// + public class TouchpadPressPayload { + /// + /// Controller which issued the event + /// + [JsonProperty("controller")] + public string Controller { get; private set; } + + /// + /// Current event settings + /// + [JsonProperty("settings")] + public JObject Settings { get; private set; } + + /// + /// Coordinates of key on the stream deck + /// + [JsonProperty("coordinates")] + public KeyCoordinates Coordinates { get; private set; } + + /// + /// Boolean whether it was a long press or not + /// + [JsonProperty("hold")] + public bool IsLongPress { get; private set; } + + /// + /// Position on touchpad which was pressed + /// + [JsonProperty("tapPos")] + public int[] TapPosition { get; private set; } + + + /// + /// Constructor + /// + /// + /// + /// + /// + /// + public TouchpadPressPayload(KeyCoordinates coordinates, JObject settings, string controller, bool isLongPress, int[] tapPosition) { + Coordinates = coordinates; + Settings = settings; + Controller = controller; + IsLongPress = isLongPress; + TapPosition = tapPosition; + } + + /// + /// Default constructor for serialization + /// + public TouchpadPressPayload() { } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/StreamDeckInfo/DeviceType.cs b/streamdeck-gpu/barraider-sdtools/StreamDeckInfo/DeviceType.cs new file mode 100644 index 0000000..e9f697c --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/StreamDeckInfo/DeviceType.cs @@ -0,0 +1,46 @@ +namespace BarRaider.SdTools { + /// + /// Type of StreamDeck hardware device, currently two are supported (classic and mini) + /// + public enum DeviceType { + /// + /// StreamDeck classic with 15 keys + /// + StreamDeckClassic = 0, + + /// + /// StreamDeck mini with 6 keys + /// + StreamDeckMini = 1, + + /// + /// StreamDeck XL with 32 keys + /// + StreamDeckXL = 2, + + /// + /// StreamDeck Mobile version + /// + StreamDeckMobile = 3, + + /// + /// Corsair G-Keys version + /// + CorsairGKeys = 4, + + /// + /// Pedal + /// + StreamDeckPedal = 5, + + /// + /// Corsair CUE SDK (?) + /// + CorsairCueSDK = 6, + + /// + /// Stream Deck+ + /// + StreamDeckPlus = 7, + } +} diff --git a/streamdeck-gpu/barraider-sdtools/StreamDeckInfo/StreamDeckApplicationInfo.cs b/streamdeck-gpu/barraider-sdtools/StreamDeckInfo/StreamDeckApplicationInfo.cs new file mode 100644 index 0000000..05f89c4 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/StreamDeckInfo/StreamDeckApplicationInfo.cs @@ -0,0 +1,34 @@ +using Newtonsoft.Json; + +namespace BarRaider.SdTools { + /// + /// Holds general information on the StreamDeck App we're communicating with + /// + public class StreamDeckApplicationInfo { + /// + /// Current language of the StreamDeck app + /// + [JsonProperty(PropertyName = "language")] + public string Language { get; private set; } + + /// + /// OS Platform + /// + [JsonProperty(PropertyName = "platform")] + public string Platform { get; private set; } + + /// + /// Current version of the StreamDeck app + /// + [JsonProperty(PropertyName = "version")] + public string Version { get; private set; } + + /// + /// Shows class information as string + /// + /// + public override string ToString() { + return $"Language: {Language} Platform: {Platform} Version: {Version}"; + } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/StreamDeckInfo/StreamDeckDeviceInfo.cs b/streamdeck-gpu/barraider-sdtools/StreamDeckInfo/StreamDeckDeviceInfo.cs new file mode 100644 index 0000000..5a34d92 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/StreamDeckInfo/StreamDeckDeviceInfo.cs @@ -0,0 +1,46 @@ +using Newtonsoft.Json; + +namespace BarRaider.SdTools { + /// + /// Class which holds information on the StreamDeck hardware device + /// + public class StreamDeckDeviceInfo { + /// + /// Details on number of keys of the StreamDeck hardware device + /// + [JsonProperty(PropertyName = "size")] + public StreamDeckDeviceSize Size { get; private set; } + + /// + /// Type of StreamDeck hardware device + /// + [JsonProperty(PropertyName = "type")] + public DeviceType Type { get; private set; } + + /// + /// Id of the StreamDeck hardware device + /// + [JsonProperty(PropertyName = "id")] + public string Id { get; private set; } + + /// + /// Constructor + /// + /// + /// + /// + public StreamDeckDeviceInfo(StreamDeckDeviceSize size, DeviceType type, string deviceId) { + Size = size; + Type = type; + Id = deviceId; + } + + /// + /// Shows class information as string + /// + /// + public override string ToString() { + return $"Id: {Id} Type: {Type} Size: {Size?.ToString()}"; + } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/StreamDeckInfo/StreamDeckDeviceSize.cs b/streamdeck-gpu/barraider-sdtools/StreamDeckInfo/StreamDeckDeviceSize.cs new file mode 100644 index 0000000..1203ed9 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/StreamDeckInfo/StreamDeckDeviceSize.cs @@ -0,0 +1,38 @@ +using Newtonsoft.Json; + +namespace BarRaider.SdTools { + /// + /// Layout of the keys on the StreamDeck hardware device + /// + public class StreamDeckDeviceSize { + /// + /// Number of key rows on the StreamDeck hardware device + /// + [JsonProperty(PropertyName = "rows")] + public int Rows { get; private set; } + + /// + /// Number of key columns on the StreamDeck hardware device + /// + [JsonProperty(PropertyName = "columns")] + public int Cols { get; private set; } + + /// + /// Constructor + /// + /// + /// + public StreamDeckDeviceSize(int rows, int cols) { + Rows = rows; + Cols = cols; + } + + /// + /// Shows class information as string + /// + /// + public override string ToString() { + return $"Rows: {Rows} Columns: {Cols}"; + } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/StreamDeckInfo/StreamDeckInfo.cs b/streamdeck-gpu/barraider-sdtools/StreamDeckInfo/StreamDeckInfo.cs new file mode 100644 index 0000000..af231d6 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/StreamDeckInfo/StreamDeckInfo.cs @@ -0,0 +1,58 @@ +using Newtonsoft.Json; +using System.Text; + +namespace BarRaider.SdTools { + /// + /// Class which holds information on the StreamDeck app and StreamDeck hardware device that the plugin is communicating with + /// + public class StreamDeckInfo { + /// + /// Information on the StreamDeck App which we're communicating with + /// + [JsonProperty(PropertyName = "application")] + public StreamDeckApplicationInfo Application { get; private set; } + + /// + /// Information on the StreamDeck hardware device that the plugin is running on + /// + [JsonProperty(PropertyName = "devices")] + public StreamDeckDeviceInfo[] Devices { get; private set; } + + /// + /// Information on the Plugin we're currently running + /// + [JsonProperty(PropertyName = "plugin")] + public StreamDeckPluginInfo Plugin { get; private set; } + + /// + /// Device pixel ratio + /// + [JsonProperty(PropertyName = "devicePixelRatio")] + public int DevicePixelRatio { get; private set; } + + /// + /// Shows class information as string + /// + /// + public override string ToString() { + StringBuilder sb = new StringBuilder(); + if (Devices != null) { + sb.Append("Devices:\n"); + for (int device = 0; device < Devices.Length; device++) { + if (Devices[device] != null) { + sb.Append($"[{Devices[device]}]\n"); + } + } + } + + if (Application != null) { + sb.Append($"ApplicationInfo: {Application}\n"); + } + + if (Plugin != null) { + sb.Append($"PluginInfo: {Plugin}\n"); + } + return sb.ToString(); + } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/StreamDeckInfo/StreamDeckPluginInfo.cs b/streamdeck-gpu/barraider-sdtools/StreamDeckInfo/StreamDeckPluginInfo.cs new file mode 100644 index 0000000..459dba0 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/StreamDeckInfo/StreamDeckPluginInfo.cs @@ -0,0 +1,22 @@ +using Newtonsoft.Json; + +namespace BarRaider.SdTools { + /// + /// Holds general information on the StreamDeck App we're communicating with + /// + public class StreamDeckPluginInfo { + /// + /// Current version of the plugin + /// + [JsonProperty(PropertyName = "version")] + public string Version { get; private set; } + + /// + /// Shows class information as string + /// + /// + public override string ToString() { + return $"Version: {Version}"; + } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Tools/ExtensionMethods.cs b/streamdeck-gpu/barraider-sdtools/Tools/ExtensionMethods.cs new file mode 100644 index 0000000..b84d643 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Tools/ExtensionMethods.cs @@ -0,0 +1,298 @@ +using BarRaider.SdTools.Wrappers; +using System; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; +using System.IO; +using System.Text; + +namespace BarRaider.SdTools { + /// + /// Extension methods for various objects + /// + public static class ExtensionMethods { + #region Coordinates + /// + /// Checks if too KeyCoordinates match to the same key + /// + /// + /// + /// + public static bool IsCoordinatesSame(this KeyCoordinates coordinates, KeyCoordinates secondCoordinates) { + if (secondCoordinates == null) { + return false; + } + + return coordinates.Row == secondCoordinates.Row && coordinates.Column == secondCoordinates.Column; + } + + #endregion + + #region Brushes/Colors + + /// + /// Shows Color In Hex Format + /// + /// + /// + public static string ToHex(this Color color) { + return string.Format("#{0:X2}{1:X2}{2:X2}", color.R, color.G, color.B); + } + + /// + /// Shows Color in Hex format + /// + /// + /// + public static string ToHex(this Brush brush) { + if (brush is SolidBrush solidBrush) { + return solidBrush.Color.ToHex(); + } + return null; + } + + #endregion + + #region Image/Graphics + + /// + /// Converts an Image into a Byte Array + /// + /// + /// + public static byte[] ToByteArray(this Image image) { + using (var ms = new MemoryStream()) { + image.Save(ms, ImageFormat.Bmp); + return ms.ToArray(); + } + } + + /// + /// Convert a in-memory image object to Base64 format. Set the addHeaderPrefix to true, if this is sent to the SendImageAsync function + /// + /// + /// + /// + public static string ToBase64(this Image image, bool addHeaderPrefix) { + return Tools.ImageToBase64(image, addHeaderPrefix); + } + + /// + /// Draws a string on a Graphics object and returns the ending Y position of the string + /// + /// + /// + /// + /// + /// + /// + public static float DrawAndMeasureString(this Graphics graphics, string text, Font font, Brush brush, PointF position) { + SizeF stringSize = graphics.MeasureString(text, font); + graphics.DrawString(text, font, brush, position); + + return position.Y + stringSize.Height; + } + + /// + /// Returns the center X position of a string, given the image's max Width and Font information + /// + /// + /// + /// + /// + /// /// True/False - Does text fit image? False if text overflows + /// + /// + /// + public static float GetTextCenter(this Graphics graphics, string text, int imageWidth, Font font, out bool textFitsImage, int minIndentation = 0) { + SizeF stringSize = graphics.MeasureString(text, font); + float stringWidth = minIndentation; + textFitsImage = false; + if (stringSize.Width < imageWidth) { + textFitsImage = true; + stringWidth = Math.Abs((imageWidth - stringSize.Width)) / 2; + } + return stringWidth; + } + + /// + /// Returns the center X position of a string, given the image's max Width and Font information + /// + /// + /// + /// + /// + /// + /// + /// + public static float GetTextCenter(this Graphics graphics, string text, int imageWidth, Font font, int minIndentation = 0) { + return graphics.GetTextCenter(text, imageWidth, font, out _, minIndentation); + } + + /// + /// Returns the highest size of the given font in which the text fits the image + /// + /// + /// + /// + /// + /// /// + /// + public static float GetFontSizeWhereTextFitsImage(this Graphics graphics, string text, int imageWidth, Font font, int minimalFontSize = 6) { + bool textFitsImage; + float size = font.Size; + Font variableFont = new Font(font.Name, size, font.Style, GraphicsUnit.Pixel); + do { + graphics.GetTextCenter(text, imageWidth, variableFont, out textFitsImage); + if (!textFitsImage) { + variableFont.Dispose(); + size -= 0.5f; + variableFont = new Font(font.Name, size, font.Style, GraphicsUnit.Pixel); + } + } + while (!textFitsImage && size > minimalFontSize); + + variableFont.Dispose(); + return size; + } + + /// + /// Adds a text path to an existing Graphics object. Uses TitleParameters to emulate the Text settings in the Property Inspector + /// + /// + /// + /// + /// + /// + /// + public static void AddTextPath(this Graphics graphics, TitleParameters titleParameters, int imageHeight, int imageWidth, string text, int pixelsAlignment = 15) { + AddTextPath(graphics, titleParameters, imageHeight, imageWidth, text, Color.Black, 1, pixelsAlignment); + } + + /// + /// Adds a text path to an existing Graphics object. Uses TitleParameters to emulate the Text settings in the Property Inspector + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static void AddTextPath(this Graphics graphics, TitleParameters titleParameters, int imageHeight, int imageWidth, string text, Color strokeColor, float strokeThickness, int pixelsAlignment = 15) { + try { + if (titleParameters == null) { + Logger.Instance.LogMessage(TracingLevel.ERROR, $"AddTextPath: titleParameters is null"); + return; + } + + Font font = new Font(titleParameters.FontFamily, (float)titleParameters.FontSizeInPixelsScaledToDefaultImage, titleParameters.FontStyle, GraphicsUnit.Pixel); + Color color = titleParameters.TitleColor; + graphics.PageUnit = GraphicsUnit.Pixel; + float ratio = graphics.DpiY / imageWidth; + SizeF stringSize = graphics.MeasureString(text, font); + float textWidth = stringSize.Width * (1 - ratio); + float textHeight = stringSize.Height * (1 - ratio); + int stringWidth = 0; + if (textWidth < imageWidth) { + stringWidth = (int)(Math.Abs((imageWidth - textWidth)) / 2) - pixelsAlignment; + } + + int stringHeight = pixelsAlignment; // Top + if (titleParameters.VerticalAlignment == TitleVerticalAlignment.Middle) { + stringHeight = (imageHeight / 2) - pixelsAlignment; + } + else if (titleParameters.VerticalAlignment == TitleVerticalAlignment.Bottom) { + stringHeight = (int)(Math.Abs((imageHeight - textHeight)) - pixelsAlignment); + } + + Pen stroke = new Pen(strokeColor, strokeThickness); + GraphicsPath gpath = new GraphicsPath(); + gpath.AddString(text, + font.FontFamily, + (int)font.Style, + graphics.DpiY * font.SizeInPoints / imageWidth, + new Point(stringWidth, stringHeight), + new StringFormat()); + graphics.DrawPath(stroke, gpath); + graphics.FillPath(new SolidBrush(color), gpath); + } + catch (Exception ex) { + Logger.Instance.LogMessage(TracingLevel.ERROR, $"AddTextPath Exception {ex}"); + } + } + + #endregion + + #region String + + + /// + /// /// Truncates a string to the first maxSize characters. If maxSize is less than string length, original string will be returned + /// + /// String + /// Max size for string + /// + public static string Truncate(this string str, int maxSize) { + if (String.IsNullOrEmpty(str)) { + return null; + } + + if (maxSize < 1) { + return str; + } + + return str.Substring(0, Math.Min(Math.Max(0, maxSize), str.Length)); + } + + /// + /// Adds line breaks (\n) to the text to make sure it fits the key when using SetTitleAsync() + /// + /// + /// + /// + /// + /// + /// + public static string SplitToFitKey(this string str, TitleParameters titleParameters, int leftPaddingPixels = 3, int rightPaddingPixels = 3, int imageWidthPixels = 72) { + try { + if (titleParameters == null) { + return str; + } + + int padding = leftPaddingPixels + rightPaddingPixels; + Font font = new Font(titleParameters.FontFamily, (float)titleParameters.FontSizeInPoints, titleParameters.FontStyle, GraphicsUnit.Pixel); + StringBuilder finalString = new StringBuilder(); + StringBuilder currentLine = new StringBuilder(); + SizeF currentLineSize; + + using (Bitmap img = new Bitmap(imageWidthPixels, imageWidthPixels)) { + using (Graphics graphics = Graphics.FromImage(img)) { + for (int idx = 0; idx < str.Length; idx++) { + currentLine.Append(str[idx]); + currentLineSize = graphics.MeasureString(currentLine.ToString(), font); + if (currentLineSize.Width <= img.Width - padding) { + finalString.Append(str[idx]); + } + else // Overflow + { + finalString.Append("\n" + str[idx]); + currentLine = new StringBuilder(str[idx].ToString()); + } + } + } + } + + return finalString.ToString(); + } + catch (Exception ex) { + Logger.Instance.LogMessage(TracingLevel.ERROR, $"SplitStringToFit Exception: {ex}"); + return str; + } + } + + + #endregion + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Tools/GlobalSettingsManager.cs b/streamdeck-gpu/barraider-sdtools/Tools/GlobalSettingsManager.cs new file mode 100644 index 0000000..941ff1e --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Tools/GlobalSettingsManager.cs @@ -0,0 +1,126 @@ +using BarRaider.SdTools.Communication; +using BarRaider.SdTools.Communication.SDEvents; +using BarRaider.SdTools.Wrappers; +using Newtonsoft.Json.Linq; +using System; +using System.Threading.Tasks; + +namespace BarRaider.SdTools { + /// + /// Helper class which allows fetching the GlobalSettings of a plugin + /// + public class GlobalSettingsManager { + #region Private Static Members + + private static GlobalSettingsManager instance = null; + private static readonly object objLock = new object(); + + #endregion + + #region Private Members + + private const int GET_GLOBAL_SETTINGS_DELAY_MS = 300; + + private Communication.StreamDeckConnection connection; + private readonly System.Timers.Timer tmrGetGlobalSettings = new System.Timers.Timer(); + + #endregion + + #region Constructor + + /// + /// Returns singelton entry of GlobalSettingsManager + /// + public static GlobalSettingsManager Instance { + get { + if (instance != null) { + return instance; + } + + lock (objLock) { + if (instance == null) { + instance = new GlobalSettingsManager(); + } + return instance; + } + } + } + + private GlobalSettingsManager() { + tmrGetGlobalSettings.Interval = GET_GLOBAL_SETTINGS_DELAY_MS; + tmrGetGlobalSettings.Elapsed += TmrGetGlobalSettings_Elapsed; + tmrGetGlobalSettings.AutoReset = true; + } + + #endregion + + #region Public Methods + + /// + /// Event triggered when Global Settings are received + /// + public event EventHandler OnReceivedGlobalSettings; + + + internal void Initialize(StreamDeckConnection connection, int getGlobalSettingsDelayMs = GET_GLOBAL_SETTINGS_DELAY_MS) { + this.connection = connection; + this.connection.OnDidReceiveGlobalSettings += Connection_OnDidReceiveGlobalSettings; + + tmrGetGlobalSettings.Stop(); + tmrGetGlobalSettings.Interval = getGlobalSettingsDelayMs; + Logger.Instance.LogMessage(TracingLevel.INFO, "GlobalSettingsManager initialized"); + } + + /// + /// Command to request the Global Settings. Use the OnDidReceiveGlobalSSettings callback function to receive the Global Settings. + /// + /// + public void RequestGlobalSettings() { + if (connection == null) { + Logger.Instance.LogMessage(TracingLevel.ERROR, "GlobalSettingsManager::RequestGlobalSettings called while connection is null"); + return; + } + + Logger.Instance.LogMessage(TracingLevel.INFO, "GlobalSettingsManager::RequestGlobalSettings called"); + tmrGetGlobalSettings.Start(); + } + + /// + /// Sets the Global Settings for the plugin + /// + /// + /// + /// + public async Task SetGlobalSettings(JObject settings, bool triggerDidReceiveGlobalSettings = true) { + if (connection == null) { + Logger.Instance.LogMessage(TracingLevel.ERROR, "GlobalSettingsManager::SetGlobalSettings called while connection is null"); + return; + } + + Logger.Instance.LogMessage(TracingLevel.INFO, "GlobalSettingsManager::SetGlobalSettings called"); + await connection.SetGlobalSettingsAsync(settings); + + if (triggerDidReceiveGlobalSettings) { + tmrGetGlobalSettings.Start(); + } + } + + + #endregion + + #region Private Methods + + private void Connection_OnDidReceiveGlobalSettings(object sender, SDEventReceivedEventArgs e) { + OnReceivedGlobalSettings?.Invoke(this, JObject.FromObject(e.Event.Payload).ToObject()); + } + + private async void TmrGetGlobalSettings_Elapsed(object sender, System.Timers.ElapsedEventArgs e) { + tmrGetGlobalSettings.Stop(); + + Logger.Instance.LogMessage(TracingLevel.INFO, "GlobalSettingsManager::GetGlobalSettingsAsync triggered"); + await connection.GetGlobalSettingsAsync(); + } + + #endregion + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Tools/GraphicsTools.cs b/streamdeck-gpu/barraider-sdtools/Tools/GraphicsTools.cs new file mode 100644 index 0000000..cc223e5 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Tools/GraphicsTools.cs @@ -0,0 +1,247 @@ +using BarRaider.SdTools.Wrappers; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; +using System.Text; + +namespace BarRaider.SdTools { + /// + /// Library of tools used to manipulate graphics + /// + public static class GraphicsTools { + /// + /// Return a Color object based on the hex value + /// + /// + /// + public static Color ColorFromHex(string hexColor) { + return System.Drawing.ColorTranslator.FromHtml(hexColor); + } + + /// + /// Generates multiple shades based on an initial color, and number of stages/shades you want + /// + /// + /// + /// + /// + public static Color GenerateColorShades(string initialColor, int currentShade, int totalAmountOfShades) { + Color color = ColorFromHex(initialColor); + int a = color.A; + double r = color.R; + double g = color.G; + double b = color.B; + + // Try and increase the color in the last stage; + if (currentShade == totalAmountOfShades - 1) { + currentShade = 1; + } + + for (int idx = 0; idx < currentShade; idx++) { + r /= 2; + g /= 2; + b /= 2; + } + + return Color.FromArgb(a, (int)r, (int)g, (int)b); + } + + /// + /// Resizes an image while scaling + /// + /// + /// + /// + /// + public static Image ResizeImage(Image original, int newWidth, int newHeight) { + if (original == null) { + return null; + } + + int originalWidth = original.Width; + int originalHeight = original.Height; + + Image canvas = new Bitmap(newWidth, newHeight); + Graphics graphic = Graphics.FromImage(canvas); + + graphic.InterpolationMode = InterpolationMode.HighQualityBicubic; + graphic.SmoothingMode = SmoothingMode.HighQuality; + graphic.PixelOffsetMode = PixelOffsetMode.HighQuality; + graphic.CompositingQuality = CompositingQuality.HighQuality; + + // Figure out the ratio + double ratioX = (double)newWidth / (double)originalWidth; + double ratioY = (double)newHeight / (double)originalHeight; + // use whichever multiplier is smaller + double ratio = ratioX < ratioY ? ratioX : ratioY; + + // now we can get the new height and width + //int imgHeight = Convert.ToInt32(originalHeight * ratio); + //int imgWidth = Convert.ToInt32(originalWidth * ratio); + + // Now calculate the X,Y position of the upper-left corner + // (one of these will always be zero) + int posX = Convert.ToInt32((newWidth - (originalWidth * ratio)) / 2); + int posY = Convert.ToInt32((newHeight - (originalHeight * ratio)) / 2); + + graphic.Clear(Color.Black); // Padding + graphic.DrawImage(original, posX, posY, newWidth, newHeight); + + return canvas; + } + + /// + /// Extract a part of an Image + /// + /// + /// + /// + /// + /// + /// + public static Bitmap ExtractRectangle(Image image, int startX, int startY, int width, int height) { + Rectangle rec = new Rectangle(startX, startY, width, height); + using (Bitmap src = new Bitmap(image)) { + return src.Clone(rec, src.PixelFormat); + } + } + + /// + /// Creates a new image with different opacity + /// + /// + /// + /// + public static Image CreateOpacityImage(Image image, float opacity) { + try { + //create a Bitmap the size of the image provided + Bitmap bmp = new Bitmap(image.Width, image.Height); + + //create a graphics object from the image + using (Graphics gfx = Graphics.FromImage(bmp)) { + //create a color matrix object + ColorMatrix matrix = new ColorMatrix { + //set the opacity + Matrix33 = opacity + }; + + //create image attributes + ImageAttributes attributes = new ImageAttributes(); + + //set the color(opacity) of the image + attributes.SetColorMatrix(matrix, ColorMatrixFlag.Default, ColorAdjustType.Bitmap); + + //now draw the image + gfx.DrawImage(image, new Rectangle(0, 0, bmp.Width, bmp.Height), 0, 0, image.Width, image.Height, GraphicsUnit.Pixel, attributes); + } + return bmp; + } + catch (Exception ex) { + Logger.Instance.LogMessage(TracingLevel.ERROR, $"SetImageOpacity exception {ex}"); + return null; + } + } + + /// + /// Generates one (or more) images where each one has a few letters drawn on them based on the parameters. You can set number of letters and number of lines per key. + /// Use expandToNextImage to decide if you want only one Image returned or multiple if text is too long for one key + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static Image[] DrawMultiLinedText(string text, int currentTextPosition, int lettersPerLine, int numberOfLines, Font font, Color backgroundColor, Color textColor, bool expandToNextImage, PointF keyDrawStartingPosition) { + float currentWidth = keyDrawStartingPosition.X; + float currentHeight = keyDrawStartingPosition.Y; + int currentLine = 0; + List images = new List(); + Bitmap img = Tools.GenerateGenericKeyImage(out Graphics graphics); + images.Add(img); + + // Draw Background + var bgBrush = new SolidBrush(backgroundColor); + graphics.FillRectangle(bgBrush, 0, 0, img.Width, img.Height); + + float lineHeight = img.Height / numberOfLines; + if (numberOfLines == 1) { + currentHeight = img.Height / 2; // Align to middle + } + + float widthIncrement = img.Width / lettersPerLine; + for (int letter = currentTextPosition; letter < text.Length; letter++) { + // Check if I need to move back to the beginning of the key, but on a new line + if (letter > currentTextPosition && letter % lettersPerLine == 0) { + currentLine++; + if (currentLine >= numberOfLines) { + if (expandToNextImage) { + images.AddRange(DrawMultiLinedText(text, letter, lettersPerLine, numberOfLines, font, backgroundColor, textColor, expandToNextImage, keyDrawStartingPosition)); + } + break; + } + + currentHeight += lineHeight; + currentWidth = keyDrawStartingPosition.X; + } + + graphics.DrawString(text[letter].ToString(), font, new SolidBrush(textColor), new PointF(currentWidth, currentHeight)); + currentWidth += widthIncrement; + } + graphics.Dispose(); + return images.ToArray(); + } + + /// + /// Adds line breaks ('\n') to the string every time the text would overflow the image + /// + /// + /// + /// + /// + /// + /// + public static string WrapStringToFitImage(string str, TitleParameters titleParameters, int leftPaddingPixels = 5, int rightPaddingPixels = 5, int imageWidthPixels = 72) { + try { + if (titleParameters == null) { + return str; + } + + int padding = leftPaddingPixels + rightPaddingPixels; + Font font = new Font(titleParameters.FontFamily, (float)titleParameters.FontSizeInPixels, titleParameters.FontStyle, GraphicsUnit.Pixel); + StringBuilder finalString = new StringBuilder(); + StringBuilder currentLine = new StringBuilder(); + SizeF currentLineSize; + + using (Bitmap img = new Bitmap(imageWidthPixels, imageWidthPixels)) { + using (Graphics graphics = Graphics.FromImage(img)) { + for (int idx = 0; idx < str.Length; idx++) { + currentLine.Append(str[idx]); + currentLineSize = graphics.MeasureString(currentLine.ToString(), font); + if (currentLineSize.Width <= img.Width - padding) { + finalString.Append(str[idx]); + } + else // Overflow + { + finalString.Append("\n" + str[idx]); + currentLine = new StringBuilder(str[idx].ToString()); + } + } + } + } + + return finalString.ToString(); + } + catch (Exception ex) { + Logger.Instance.LogMessage(TracingLevel.ERROR, $"WrapStringToFitImage Exception: {ex}"); + return str; + } + } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Tools/Logger.cs b/streamdeck-gpu/barraider-sdtools/Tools/Logger.cs new file mode 100644 index 0000000..793d534 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Tools/Logger.cs @@ -0,0 +1,98 @@ +using NLog; + +namespace BarRaider.SdTools { + /// + /// Tracing levels used for Logger + /// + public enum TracingLevel { + /// + /// Debug level + /// + DEBUG, + + /// + /// Informational level + /// + INFO, + + /// + /// Warning level + /// + WARN, + + /// + /// Error level + /// + ERROR, + + /// + /// Fatal (highest) level + /// + FATAL + } + + /// + /// Log4Net logger helper class + /// + public class Logger { + private static Logger instance = null; + private static readonly object objLock = new object(); + + /// + /// Returns singelton entry of Log4Net logger + /// + public static Logger Instance { + get { + if (instance != null) { + return instance; + } + + lock (objLock) { + if (instance == null) { + instance = new Logger(); + } + return instance; + } + } + } + + private readonly NLog.Logger log = null; + private Logger() { + var config = new NLog.Config.LoggingConfiguration(); + var logfile = new NLog.Targets.FileTarget("logfile") { FileName = "pluginlog.log", ArchiveEvery = NLog.Targets.FileArchivePeriod.Day, MaxArchiveFiles = 3, ArchiveFileName = "archive/log.{###}.log", ArchiveNumbering = NLog.Targets.ArchiveNumberingMode.Rolling, Layout = "${longdate}|${level:uppercase=true}|${processname}|${threadid}|${message}" }; + config.AddRule(LogLevel.Debug, LogLevel.Fatal, logfile); + NLog.LogManager.Configuration = config; + log = LogManager.GetCurrentClassLogger(); + LogMessage(TracingLevel.DEBUG, "Logger Initialized"); + } + + /// + /// Add message to log with a specific severity level. + /// + /// + /// + public void LogMessage(TracingLevel Level, string Message) { + switch (Level) { + case TracingLevel.DEBUG: + log.Debug(Message); + break; + + case TracingLevel.INFO: + log.Info(Message); + break; + + case TracingLevel.WARN: + log.Warn(Message); + break; + + case TracingLevel.ERROR: + log.Error(Message); + break; + + case TracingLevel.FATAL: + log.Fatal(Message); + break; + } + } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Tools/PayloadExtensionMethods.cs b/streamdeck-gpu/barraider-sdtools/Tools/PayloadExtensionMethods.cs new file mode 100644 index 0000000..3c2084b --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Tools/PayloadExtensionMethods.cs @@ -0,0 +1,54 @@ +using BarRaider.SdTools.Payloads; + +namespace BarRaider.SdTools { + internal static class PayloadExtensionMethods { + internal static string ToStringEx(this ReceivedSettingsPayload rsp) { + if (rsp == null) { + return "ReceiveSettingsPayload is null!"; + } + return $"IsInMultiAction: {rsp.IsInMultiAction} Coordinates: ({rsp.Coordinates?.Row},{rsp.Coordinates?.Column}) Settings: {rsp.Settings}"; + } + + internal static string ToStringEx(this AppearancePayload ap) { + if (ap == null) { + return "AppearancePayload is null!"; + } + return $"State: {ap.State} IsInMultiAction: {ap.IsInMultiAction} Coordinates: ({ap.Coordinates?.Row},{ap.Coordinates?.Column}) Settings: {ap.Settings}"; + } + + internal static string ToStringEx(this KeyPayload kp) { + if (kp == null) { + return "KeyPayload is null!"; + } + return $"State: {kp.State} IsInMultiAction: {kp.IsInMultiAction} DesiredState: {kp.UserDesiredState} Coordinates: ({kp.Coordinates?.Row},{kp.Coordinates?.Column}) Settings: {kp.Settings}"; + } + + internal static string ToStringEx(this ReceivedGlobalSettingsPayload gsp) { + if (gsp == null) { + return "ReceiveGlobalSettingsPayload is null!"; + } + return $"Settings: {gsp.Settings}"; + } + + internal static string ToStringEx(this DialRotatePayload drp) { + if (drp == null) { + return "DialRotatePayload is null!"; + } + return $"Controller: {drp.Controller} Ticks: {drp.Ticks} Coordinates: ({drp.Coordinates?.Row},{drp.Coordinates?.Column}) Settings: {drp.Settings}"; + } + + internal static string ToStringEx(this DialPayload dpp) { + if (dpp == null) { + return "DialPressPayload is null!"; + } + return $"Controller: {dpp.Controller} Coordinates: ({dpp.Coordinates?.Row},{dpp.Coordinates?.Column}) Settings: {dpp.Settings}"; + } + + internal static string ToStringEx(this TouchpadPressPayload tpp) { + if (tpp == null) { + return "KeyPayload is null!"; + } + return $"Controller: {tpp.Controller} LongPress: {tpp.IsLongPress} Position: {(tpp.TapPosition?.Length == 2 ? tpp.TapPosition[0].ToString() + "," + tpp.TapPosition[1] : "Invalid")} Coordinates: ({tpp.Coordinates?.Row},{tpp.Coordinates?.Column}) Settings: {tpp.Settings}"; + } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Tools/RandomGenerator.cs b/streamdeck-gpu/barraider-sdtools/Tools/RandomGenerator.cs new file mode 100644 index 0000000..0b87d86 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Tools/RandomGenerator.cs @@ -0,0 +1,29 @@ +using System; + +namespace BarRaider.SdTools { + /// + /// Helper class for generating random numbers + /// + public static class RandomGenerator { + private static readonly Random random = new Random(); + + /// + /// Returns a non-negative random integer that is less than the specified maximum. + /// + /// + /// + public static int Next(int maxValue) { + return random.Next(maxValue); + } + + /// + /// Returns a random integer that is within a specified range. Value will be less than the specified maximum. + /// + /// + /// + /// + public static int Next(int minValue, int maxValue) { + return random.Next(minValue, maxValue); + } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Tools/Tools.cs b/streamdeck-gpu/barraider-sdtools/Tools/Tools.cs new file mode 100644 index 0000000..dffb70f --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Tools/Tools.cs @@ -0,0 +1,436 @@ +using BarRaider.SdTools.Wrappers; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; +using System.Drawing.Text; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Security.Cryptography; + +namespace BarRaider.SdTools { + /// + /// Set of common utilities used by various plugins + /// Currently the class mostly focuses on image-related functions that will be passed to the StreamDeck key + /// + public static class Tools { + private const string HEADER_PREFIX = "data:image/png;base64,"; + private const int CLASSIC_KEY_DEFAULT_HEIGHT = 72; + private const int CLASSIC_KEY_DEFAULT_WIDTH = 72; + private const int XL_KEY_DEFAULT_HEIGHT = 96; + private const int XL_KEY_DEFAULT_WIDTH = 96; + private const int GENERIC_KEY_IMAGE_SIZE = 144; + private const string FILENAME_NO_FILE_STRING = "No file..."; + + #region Image Related + + /// + /// Convert an image file to Base64 format. Set the addHeaderPrefix to true, if this is sent to the SendImageAsync function + /// + /// + /// + /// + public static string FileToBase64(string fileName, bool addHeaderPrefix) { + if (!File.Exists(fileName)) { + return null; + } + + using (Image image = Image.FromFile(fileName)) { + return ImageToBase64(image, addHeaderPrefix); + } + } + + /// + /// Convert a in-memory image object to Base64 format. Set the addHeaderPrefix to true, if this is sent to the SendImageAsync function + /// + /// + /// + /// + public static string ImageToBase64(Image image, bool addHeaderPrefix) { + if (image == null) { + return null; + } + + using (MemoryStream m = new MemoryStream()) { + image.Save(m, ImageFormat.Png); + byte[] imageBytes = m.ToArray(); + + // Convert byte[] to Base64 String + string base64String = Convert.ToBase64String(imageBytes); + return addHeaderPrefix ? HEADER_PREFIX + base64String : base64String; + } + } + + /// + /// Convert a base64 image string to an Image object + /// + /// + /// + public static Image Base64StringToImage(string base64String) { + try { + if (string.IsNullOrEmpty(base64String)) { + return null; + } + + // Remove header + if (base64String.Substring(0, HEADER_PREFIX.Length) == HEADER_PREFIX) { + base64String = base64String.Substring(HEADER_PREFIX.Length); + } + + byte[] imageBytes = Convert.FromBase64String(base64String); + using (MemoryStream m = new MemoryStream(imageBytes)) { + return Image.FromStream(m); + } + } + catch (Exception ex) { + Logger.Instance.LogMessage(TracingLevel.ERROR, $"Base64StringToImage Exception: {ex}"); + } + return null; + } + + /// + /// Gets the key default height in pixels. + /// To get the StreamDeckType use Connection.DeviceInfo() + /// + /// + /// + public static int GetKeyDefaultHeight(DeviceType streamDeckType) { + switch (streamDeckType) { + case DeviceType.StreamDeckClassic: + case DeviceType.StreamDeckMini: + case DeviceType.StreamDeckMobile: + return CLASSIC_KEY_DEFAULT_HEIGHT; + case DeviceType.StreamDeckXL: + return XL_KEY_DEFAULT_HEIGHT; + default: + Logger.Instance.LogMessage(TracingLevel.ERROR, $"SDTools GetKeyDefaultHeight Error: Invalid StreamDeckDeviceType: {streamDeckType}"); + break; + } + return 1; + } + + /// + /// Gets the key default width in pixels. + /// To get the StreamDeckType use Connection.DeviceInfo() + /// + /// + /// + public static int GetKeyDefaultWidth(DeviceType streamDeckType) { + switch (streamDeckType) { + case DeviceType.StreamDeckClassic: + case DeviceType.StreamDeckMini: + case DeviceType.StreamDeckMobile: + return CLASSIC_KEY_DEFAULT_WIDTH; + case DeviceType.StreamDeckXL: + return XL_KEY_DEFAULT_WIDTH; + default: + Logger.Instance.LogMessage(TracingLevel.ERROR, $"SDTools GetKeyDefaultHeight Error: Invalid StreamDeckDeviceType: {streamDeckType}"); + break; + } + return 1; + } + + /// + /// Generates an empty key bitmap with the default height and width. + /// New: To get the StreamDeckType use Connection.DeviceInfo() + /// + /// + /// + /// + public static Bitmap GenerateKeyImage(DeviceType streamDeckType, out Graphics graphics) { + int height = GetKeyDefaultHeight(streamDeckType); + int width = GetKeyDefaultWidth(streamDeckType); + + return GenerateKeyImage(height, width, out graphics); + } + + /// + /// Creates a key image that fits all Stream Decks + /// + /// + /// + public static Bitmap GenerateGenericKeyImage(out Graphics graphics) { + return GenerateKeyImage(GENERIC_KEY_IMAGE_SIZE, GENERIC_KEY_IMAGE_SIZE, out graphics); + } + + /// + /// Creates a key image based on given height and width + /// + /// + /// + /// + /// + private static Bitmap GenerateKeyImage(int height, int width, out Graphics graphics) { + try { + Bitmap bitmap = new Bitmap(width, height); + var brush = new SolidBrush(Color.Black); + + graphics = Graphics.FromImage(bitmap); + graphics.SmoothingMode = SmoothingMode.AntiAlias; + graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; + graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; + graphics.TextRenderingHint = TextRenderingHint.AntiAliasGridFit; + + //Fill background black + graphics.FillRectangle(brush, 0, 0, width, height); + return bitmap; + } + catch (Exception ex) { + Logger.Instance.LogMessage(TracingLevel.ERROR, $"SDTools GenerateKeyImage exception: {ex} Height: {height} Width: {width}"); + } + graphics = null; + return null; + } + + /// + /// Deprecated! Use AddTextPath on the Graphics extension method instead. + /// Adds a text path to an existing Graphics object. Uses TitleParser to emulate the Text settings in the Property Inspector + /// + /// + /// + /// + /// + /// + /// + [Obsolete("Use graphics.AddTextPath() extension method instead")] + public static void AddTextPathToGraphics(Graphics graphics, TitleParameters titleParameters, int imageHeight, int imageWidth, string text, int pixelsAlignment = 15) { + if (graphics != null) { + graphics.AddTextPath(titleParameters, imageHeight, imageWidth, text, pixelsAlignment); + } + } + + #endregion + + #region Filename Related + + /// + /// Extracts the actual filename from a file payload received from the Property Inspector + /// + /// + /// + public static string FilenameFromPayload(Newtonsoft.Json.Linq.JToken payload) { + return FilenameFromString((string)payload); + } + + private static string FilenameFromString(string filenameWithFakepath) { + if (string.IsNullOrEmpty(filenameWithFakepath)) { + return null; + } + + if (filenameWithFakepath == FILENAME_NO_FILE_STRING) { + return String.Empty; + } + + return Uri.UnescapeDataString(filenameWithFakepath.Replace("C:\\fakepath\\", "")); + } + + #endregion + + #region String Related + + /// + /// Converts a long to a human-readable string. Example: 54,265 => 54.27k + /// + /// + /// + public static string FormatNumber(long num) { + if (num >= 100000000) { + return (num / 1000000D).ToString("0.#M"); + } + if (num >= 1000000) { + return (num / 1000000D).ToString("0.##M"); + } + if (num >= 100000) { + return (num / 1000D).ToString("0.#k"); + } + if (num >= 10000) { + return (num / 1000D).ToString("0.##k"); + } + + return num.ToString("#,0"); + } + + //// Converts number in bytes to human-readable size (ex. "2 GB") + /// Size in bytes + /// Formatted human-readable string (ex. "2 MB") + public static string FormatBytes(double numberInBytes) { + var sizeCounter = 0; + var format = new[] { "{0:F0} B", "{0:F0} KB", "{0:F0} MB", "{0:F0} GB", "{0:F2} TB", "{0:F2} PB", "{0:F2} EB" }; + while (sizeCounter < format.Length && numberInBytes >= 1024d) { + numberInBytes = 100d * numberInBytes / 1024d / 100d; + sizeCounter++; + } + return String.Format(format[sizeCounter], numberInBytes); + } + + /// + /// OBSOLETE - Use String.SplitToFitKey() from SdTools.ExtensionMethods + /// + /// + /// + /// + /// + /// + /// + [Obsolete("Use String.SplitToFitKey(), now part of the SdTools.ExtensionMethods")] + public static string SplitStringToFit(string str, TitleParameters titleParameters, int leftPaddingPixels = 3, int rightPaddingPixels = 3, int imageWidthPixels = 72) { + return str.SplitToFitKey(titleParameters, leftPaddingPixels, rightPaddingPixels, imageWidthPixels); + } + + #endregion + + #region SHA512 + + /// + /// Returns SHA512 Hash from an image object + /// + /// + /// + public static string ImageToSHA512(Image image) { + if (image == null) { + return null; + } + + try { + using (MemoryStream ms = new MemoryStream()) { + image.Save(ms, ImageFormat.Png); + return BytesToSHA512(ms.ToArray()); + } + } + catch (Exception ex) { + Logger.Instance.LogMessage(TracingLevel.ERROR, $"ImageToSHA512 Exception: {ex}"); + } + return null; + } + + /// + /// Returns SHA512 Hash from a string + /// + /// + /// + public static string StringToSHA512(string str) { + if (str == null) { + return null; + } + return BytesToSHA512(System.Text.Encoding.UTF8.GetBytes(str)); + } + + /// + /// Returns SHA512 Hash from a byte stream + /// + /// + /// + public static string BytesToSHA512(byte[] byteStream) { + try { + using (SHA512CryptoServiceProvider sha512 = new SHA512CryptoServiceProvider()) { + byte[] hash = sha512.ComputeHash(byteStream); + return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); + } + } + catch (Exception ex) { + Logger.Instance.LogMessage(TracingLevel.ERROR, $"BytesToSHA512 Exception: {ex}"); + } + return null; + } + + #endregion + + #region JObject Related + + /// + /// Iterates through the fromJObject, finds the property that matches in the toSettings object, and sets the value from the fromJObject object + /// + /// + /// + /// + /// Number of properties updated + public static int AutoPopulateSettings(T toSettings, JObject fromJObject) { + Dictionary dicProperties = MatchPropertiesWithJsonProperty(toSettings); + int totalPopulated = 0; + + if (fromJObject != null) { + foreach (var prop in fromJObject) { + if (dicProperties.ContainsKey(prop.Key)) { + PropertyInfo info = dicProperties[prop.Key]; + + // Special handling for FilenameProperty + if (info.GetCustomAttributes(typeof(FilenamePropertyAttribute), true).Length > 0) { + string value = FilenameFromString((string)prop.Value); + info.SetValue(toSettings, value); + } + else { + info.SetValue(toSettings, Convert.ChangeType(prop.Value, info.PropertyType)); + } + totalPopulated++; + } + } + } + return totalPopulated; + } + + private static Dictionary MatchPropertiesWithJsonProperty(T obj) { + Dictionary dicProperties = new Dictionary(); + if (obj != null) { + PropertyInfo[] props = typeof(T).GetProperties(); + foreach (PropertyInfo prop in props) { + object[] attributes = prop.GetCustomAttributes(true); + foreach (object attr in attributes) { + if (attr is JsonPropertyAttribute jprop) { + dicProperties.Add(jprop.PropertyName, prop); + break; + } + } + } + } + + return dicProperties; + } + + #endregion + + #region Dials Related + + /// + /// Takes a custom range and recalculates the value on a scale from 0 to 100 + /// + /// + /// + /// + /// + public static int RangeToPercentage(int value, int originalMin, int originalMax) { + if (originalMax - originalMin == 0) // Prevent division by zero + { + return 0; + } + + return ((value - originalMin) * 100) / (originalMax - originalMin); + } + + #endregion + + #region Plugin Helper Classes + + /// + /// Uses the PluginActionId attribute on the various classes derived from PluginBase to find all the actions supported in this assembly + /// + /// + public static PluginActionId[] AutoLoadPluginActions() { + List actions = new List(); + + var pluginTypes = Assembly.GetEntryAssembly().GetTypes().Where(typ => typ.IsClass && typ.GetCustomAttributes(typeof(PluginActionIdAttribute), true).Length > 0).ToList(); + pluginTypes.ForEach(typ => { + if (typ.GetCustomAttributes(typeof(PluginActionIdAttribute), true).First() is PluginActionIdAttribute attr) { + actions.Add(new PluginActionId(attr.ActionId, typ)); + } + }); + + return actions.ToArray(); + } + + #endregion + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Wrappers/KeyCoordinates.cs b/streamdeck-gpu/barraider-sdtools/Wrappers/KeyCoordinates.cs new file mode 100644 index 0000000..8c70d26 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Wrappers/KeyCoordinates.cs @@ -0,0 +1,20 @@ +using Newtonsoft.Json; +namespace BarRaider.SdTools { + /// + /// Coordinates of the current key + /// + public class KeyCoordinates { + /// + /// Column of the current key + /// + [JsonProperty("column")] + public int Column { get; set; } + + /// + /// Row of the current key + /// + [JsonProperty("row")] + public int Row { get; set; } + } +} + diff --git a/streamdeck-gpu/barraider-sdtools/Wrappers/PluginActionId.cs b/streamdeck-gpu/barraider-sdtools/Wrappers/PluginActionId.cs new file mode 100644 index 0000000..0e79d03 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Wrappers/PluginActionId.cs @@ -0,0 +1,42 @@ +using System; + +namespace BarRaider.SdTools { + /// + /// This class associates a plugin UUID (which is indicated in the Manifest file), with the type of the implementation class. + /// The implementation class must be derived from the PluginBase class for this to work properly. + /// If the type passed does not derrive from PluginBase, a NotSupportedException will be thrown + /// + public class PluginActionId { + private Type pluginBaseType; + + /// + /// Action UUID as indicated in the manifest file + /// + public string ActionId { get; private set; } + + /// + /// Type of class that implemented this action. Must inherit PluginBase + /// + public Type PluginBaseType { + get { + return pluginBaseType; + } + private set { + if (value == null || (!typeof(IKeypadPlugin).IsAssignableFrom(value) && !typeof(IEncoderPlugin).IsAssignableFrom(value))) { + throw new NotSupportedException("Class type set to PluginBaseType does not inherit IKeypadPlugin or IEncoderPlugin"); + } + pluginBaseType = value; + } + } + + /// + /// PluginActionId constructor + /// + /// actionId is the UUID from the manifest file + /// Type of class that implemented this action. Must inherit PluginBase + public PluginActionId(string actionId, Type pluginBaseType) { + ActionId = actionId; + PluginBaseType = pluginBaseType; + } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Wrappers/SDEventReceivedEventArgs.cs b/streamdeck-gpu/barraider-sdtools/Wrappers/SDEventReceivedEventArgs.cs new file mode 100644 index 0000000..d7d6e8f --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Wrappers/SDEventReceivedEventArgs.cs @@ -0,0 +1,17 @@ +using System; + +namespace BarRaider.SdTools.Wrappers { + /// + /// Base (Generic) EventArgs used for events + /// + /// + public class SDEventReceivedEventArgs : EventArgs { + /// + /// Event Information + /// + public T Event { get; private set; } + internal SDEventReceivedEventArgs(T evt) { + this.Event = evt; + } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/Wrappers/TitleParameters.cs b/streamdeck-gpu/barraider-sdtools/Wrappers/TitleParameters.cs new file mode 100644 index 0000000..c03123f --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/Wrappers/TitleParameters.cs @@ -0,0 +1,173 @@ +using Newtonsoft.Json; +using System; +using System.Drawing; + +namespace BarRaider.SdTools.Wrappers { + /// + /// Enum for the alignment of the Title text on the key + /// + public enum TitleVerticalAlignment { + /// + /// Top Alignment + /// + Top, + + /// + /// Middle/Center Alignment + /// + Middle, + + /// + /// Bottom Alignment + /// + Bottom + } + + /// + /// Class holding all the Title Information set by a user in the Property Inspector + /// + public class TitleParameters { + #region Private Members + private const double POINTS_TO_PIXEL_CONVERT = 1.3; + private const int DEFAULT_IMAGE_SIZE_FONT_SCALE = 3; + private const string DEFAULT_FONT_FAMILY_NAME = "Verdana"; + #endregion + + /// + /// Title Color + /// + [JsonProperty("titleColor")] + public Color TitleColor { get; private set; } = Color.White; + + /// + /// Font Size in Points + /// + [JsonProperty("fontSize")] + public double FontSizeInPoints { get; private set; } = 10; + + /// + /// Font Size in Pixels + /// + [JsonIgnore] + public double FontSizeInPixels => Math.Round(FontSizeInPoints * POINTS_TO_PIXEL_CONVERT); + + /// + /// Font Size Scaled to Image + /// + [JsonIgnore] + public double FontSizeInPixelsScaledToDefaultImage => Math.Round(FontSizeInPixels * DEFAULT_IMAGE_SIZE_FONT_SCALE); + + /// + /// Font Family + /// + [JsonProperty("fontFamily")] + public FontFamily FontFamily { get; private set; } = new FontFamily(DEFAULT_FONT_FAMILY_NAME); + + /// + /// Font Style + /// + [JsonProperty("fontStyle")] + public FontStyle FontStyle { get; private set; } = FontStyle.Bold; + + /// + /// Should Title be shown + /// + [JsonProperty("showTitle")] + public bool ShowTitle { get; private set; } + + /// + /// Alignment position of the Title text on the key + /// + [JsonProperty("titleAlignment")] + public TitleVerticalAlignment VerticalAlignment { get; private set; } + + /// + /// Constructor + /// + /// + /// + /// + /// + /// + /// + public TitleParameters(FontFamily fontFamily, FontStyle fontStyle, double fontSize, Color titleColor, bool showTitle, TitleVerticalAlignment verticalAlignment) { + FontFamily = fontFamily; + FontStyle = fontStyle; + FontSizeInPoints = fontSize; + TitleColor = titleColor; + ShowTitle = showTitle; + VerticalAlignment = verticalAlignment; + } + + /// + /// Constructor + /// + /// + /// + /// + /// + /// + /// + /// + public TitleParameters(string fontFamily, uint fontSize, string fontStyle, bool fontUnderline, bool showTitle, string titleAlignment, string titleColor) { + ParsePayload(fontFamily, fontSize, fontStyle, fontUnderline, showTitle, titleAlignment, titleColor); + } + + + private void ParsePayload(string fontFamily, uint fontSize, string fontStyle, bool fontUnderline, bool showTitle, string titleAlignment, string titleColor) { + try { + ShowTitle = showTitle; + + // Color + if (!String.IsNullOrEmpty(titleColor)) { + TitleColor = ColorTranslator.FromHtml(titleColor); + } + + if (!String.IsNullOrEmpty(fontFamily)) { + FontFamily = new FontFamily(fontFamily); + } + + FontSizeInPoints = fontSize; + if (!String.IsNullOrEmpty(fontStyle)) { + switch (fontStyle.ToLowerInvariant()) { + case "regular": + FontStyle = FontStyle.Regular; + break; + case "bold": + FontStyle = FontStyle.Bold; + break; + case "italic": + FontStyle = FontStyle.Italic; + break; + case "bold italic": + FontStyle = FontStyle.Bold | FontStyle.Italic; + break; + default: + Logger.Instance.LogMessage(TracingLevel.WARN, $"{this.GetType()} Cannot parse Font Style: {fontStyle}"); + break; + } + } + if (fontUnderline) { + FontStyle |= FontStyle.Underline; + } + + if (!string.IsNullOrEmpty(titleAlignment)) { + switch (titleAlignment.ToLowerInvariant()) { + case "top": + VerticalAlignment = TitleVerticalAlignment.Top; + break; + case "bottom": + VerticalAlignment = TitleVerticalAlignment.Bottom; + break; + case "middle": + VerticalAlignment = TitleVerticalAlignment.Middle; + break; + } + } + } + catch (Exception ex) { + Logger.Instance.LogMessage(TracingLevel.ERROR, $"TitleParameters failed to parse payload {ex}"); + } + } + } +} diff --git a/streamdeck-gpu/barraider-sdtools/barraider-sdtools.csproj b/streamdeck-gpu/barraider-sdtools/barraider-sdtools.csproj new file mode 100644 index 0000000..1b5cce9 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/barraider-sdtools.csproj @@ -0,0 +1,58 @@ + + + netstandard2.0 + true + BarRaider + Stream Deck Tools by BarRaider + Start developing your Elgato StreamDeck plugin faster! +This library encapsulates all the overhead of setting up the framework, so that you can focus on writing the logic of the plugin itself. +[Samples for using the library are provided here: https://github.com/BarRaider/streamdeck-tools] +Using this library, you only need to derive from the abstract PluginBase class and add one line of code in your program.cs to have a working plugin. More info and working samples here: https://github.com/BarRaider/streamdeck-tools . +Feel free to contact me for more information: https://barraider.com + Copyright © BarRaider 2023 + https://github.com/BarRaider/streamdeck-tools/blob/master/LICENSE + https://github.com/BarRaider/streamdeck-tools + https://github.com/BarRaider/streamdeck-tools + StreamDeck Elgato Library Plugin Stream Deck Toolkit + StreamDeck-Tools + + 6.1.1.0 + 6.1.1.0 + 6.1.1 + 6.1.1 - Removed error being logged for DialPress events +- Support for new DialDown and DialUp events. +Removed support for deprecated DialPress event + BarRaider.SdTools + StreamDeckTools + BRLogo_460.png + + + + + + + + + streamdeck-tools.xml + + + streamdeck-tools.xml + + + + + + + + + + + + + + + True + + + + \ No newline at end of file diff --git a/streamdeck-gpu/barraider-sdtools/streamdeck-tools.xml b/streamdeck-gpu/barraider-sdtools/streamdeck-tools.xml new file mode 100644 index 0000000..40f7489 --- /dev/null +++ b/streamdeck-gpu/barraider-sdtools/streamdeck-tools.xml @@ -0,0 +1,2811 @@ + + + + StreamDeckTools + + + + + FilenamePropertyAttribute - Used to indicate the current property holds a file name. + This will allow StreamDeck Tools to strip the mandatory "C:\fakepath\" added by the SDK + + + + + PluginActionId attribute + Used to indicate the UUID in the manifest file that matches to this class + + + + + UUID of the action + + + + + Constructor - This attribute is used to indicate the UUID in the manifest file that matches to this class + + + + + + Main abstract class your plugin should derive from for dials (not keys) + For keys use the KeyBase or KeyAndEncoderBase + Holds implementation for all the basic functions + If you're missing an event, you can register to it from the Connection.StreamDeckConnection object + + + + + Called when the dial is rotated + + + + + Called when the Dial is pressed + + + + + Called when the Dial is released + + + + + Called when the touchpad (above the dials) is pressed + + + + + Called when the PropertyInspector has new settings + + + + + + Called when GetGlobalSettings is called. + + + + + + Called every second + Logic for displaying title/image can go here + + + + + Abstract method Called when the plugin is disposed + + + + + Main iDisposable Dispose function + + + + + Connection object which handles your communication with the Stream Deck app + + + + + Constructor for PluginBase. Receives the communication and plugin settings + Note that the settings object is not used by the base and should be consumed by the deriving class. + Usually, a private class inside the deriving class is created which stores the settings + Example for settings usage: + * if (payload.Settings == null || payload.Settings.Count == 0) + * { + * // Create default settings + * } + * else + * { + this.settings = payload.Settings.ToObject(); + * } + + + Communication module with Stream Deck + Plugin settings - NOTE: Not used in base class, should be consumed by deriving class + + + + Common functions used by both keypad and dial plugins + + + + + Called when the PropertyInspector has new settings + + + + + + Called when GetGlobalSettings is called. + + + + + + Called every second + Logic for displaying title/image can go here + + + + + Internal function used by StreamDeckTools to prevent memory leaks + + + + + Interface used to capture dial/encoder events + + + + + Called when the dial is rotated + + + + + Called when the Dial is pressed + + + + + Called when the Dial is released + + + + + Called when the touchpad (above the dials) is pressed + + + + + Interface used to capture key events + + + + + Called when a Stream Deck key is pressed + + + + + Called when a Stream Deck key is released + + + + + Interface for a Stream Deck connection + + + + + Event received by the plugin when the Property Inspector uses the sendToPlugin event. + + + + + Event received when the user changes the title or title parameters. + + + + + Event received when a monitored application is terminated + + + + + Event received when a monitored application is launched + + + + + Event received when a device is unplugged from the computer + + + + + Event received when a device is plugged to the computer. + + + + + Event received when the Property Inspector appears in the Stream Deck software user interface, for example when selecting a new instance. + + + + + Event received when the Property Inspector for an instance is removed from the Stream Deck software user interface, for example when selecting a different instance. + + + + + Event received when the computer wakes up + + + + + Send settings to the PropertyInspector + + + + + + + Persists your plugin settings + + + + + + + Persists your global plugin settings + + Settings to save globally + Boolean whether to also trigger a didReceiveGlobalSettings event. Default is true + + + + + Persists your global plugin settings + + + + + + Sets an image on the StreamDeck key. + + Base64 encoded image + A 0-based integer value representing the state of an action with multiple states. This is an optional parameter. If not specified, the title is set to all states. + Should image be sent even if it is identical to the one sent previously. Default is false + + + + + Sets an image on the StreamDeck key + + Image object + A 0-based integer value representing the state of an action with multiple states. This is an optional parameter. If not specified, the title is set to all states. + Should image be sent even if it is identical to the one sent previously. Default is false + + + + + Sets the default image for this state, as configured in the manifest + + + + + + Sets a title on the StreamDeck key + + + A 0-based integer value representing the state of an action with multiple states. This is an optional parameter. If not specified, the title is set to all states. + + + + + Switches to one of the plugin's built-in profiles + + + + + + + Switches to one of the plugin's built-in profiles. Allows to choose which device to switch it on. + + + + + + + + Shows the Alert (Yellow Triangle) on the StreamDeck key + + + + + + Shows the Success (Green checkmark) on the StreamDeck key + + + + + + Add a message to the Stream Deck log. This is the log located at: %appdata%\Elgato\StreamDeck\logs\StreamDeck0.log + + + + + + + Gets the Stream Deck device's info + + + + + + Tells Stream Deck to return the current plugin settings via the ReceivedSettings function + + + + + + Opens a URI in the user's browser + + + + + + + Opens a URI in the user's browser + + + + + + + Sets the plugin to a specific state which is pre-configured in the manifest file + + + + + + + Sets the values of touchpad layouts items + + Dictionary holding the layout item keys and values you want to change + + + + + Sets the value of a single touchpad layout item + + + + + + Sets the values of touchpad layouts items using a preset JObject + + + + + + + Changes the current Stream Deck+ touch display layout + + + + + + An opaque value identifying the plugin. This value is received during the Registration procedure + + + + + An opaque value identifying the device the plugin is launched on. + + + + + StreamDeckConnection object, initialized based on the args received when launching the program + + + + + Main abstract class your plugin should derive from for keys (not dials) + For dials use the EncoderBase or KeyAndEncoderBase + Holds implementation for all the basic functions + If you're missing an event, you can register to it from the Connection.StreamDeckConnection object + + + + + Called when the dial is rotated + + + + + Called when the Dial is pressed + + + + + Called when the Dial is released + + + + + Called when the touchpad (above the dials) is pressed + + + + + Called when a Stream Deck key is pressed + + + + + Called when a Stream Deck key is released + + + + + Called when the PropertyInspector has new settings + + + + + + Called when GetGlobalSettings is called. + + + + + + Called every second + Logic for displaying title/image can go here + + + + + Abstract method Called when the plugin is disposed + + + + + Main iDisposable Dispose function + + + + + Connection object which handles your communication with the Stream Deck app + + + + + Constructor for PluginBase. Receives the communication and plugin settings + Note that the settings object is not used by the base and should be consumed by the deriving class. + Usually, a private class inside the deriving class is created which stores the settings + Example for settings usage: + * if (payload.Settings == null || payload.Settings.Count == 0) + * { + * // Create default settings + * } + * else + * { + this.settings = payload.Settings.ToObject(); + * } + + + Communication module with Stream Deck + Plugin settings - NOTE: Not used in base class, should be consumed by deriving class + + + + Main abstract class your plugin should derive from for keys (not dials) + For dials use the EncoderBase or KeyAndEncoderBase + Holds implementation for all the basic functions + If you're missing an event, you can register to it from the Connection.StreamDeckConnection object + + + + + Called when a Stream Deck key is pressed + + + + + Called when a Stream Deck key is released + + + + + Called when the PropertyInspector has new settings + + + + + + Called when GetGlobalSettings is called. + + + + + + Called every second + Logic for displaying title/image can go here + + + + + Abstract method Called when the plugin is disposed + + + + + Main iDisposable Dispose function + + + + + Connection object which handles your communication with the Stream Deck app + + + + + Constructor for PluginBase. Receives the communication and plugin settings + Note that the settings object is not used by the base and should be consumed by the deriving class. + Usually, a private class inside the deriving class is created which stores the settings + Example for settings usage: + * if (payload.Settings == null || payload.Settings.Count == 0) + * { + * // Create default settings + * } + * else + * { + this.settings = payload.Settings.ToObject(); + * } + + + Communication module with Stream Deck + Plugin settings - NOTE: Not used in base class, should be consumed by deriving class + + + + Obsolete! Use `KeypadBase` moving forward, or choose one of the other options: `EncoderBase`, `KeyAndEncoderBase` + + + + + Called when a Stream Deck key is pressed + + + + + Called when a Stream Deck key is released + + + + + Called when the PropertyInspector has new settings + + + + + + Called when GetGlobalSettings is called. + + + + + + Called every second + Logic for displaying title/image can go here + + + + + Abstract method Called when the plugin is disposed + + + + + Main iDisposable Dispose function + + + + + Connection object which handles your communication with the Stream Deck app + + + + + Constructor for PluginBase. Receives the communication and plugin settings + Note that the settings object is not used by the base and should be consumed by the deriving class. + Usually, a private class inside the deriving class is created which stores the settings + Example for settings usage: + * if (payload.Settings == null || payload.Settings.Count == 0) + * { + * // Create default settings + * } + * else + * { + this.settings = payload.Settings.ToObject(); + * } + + + Communication module with Stream Deck + Plugin settings - NOTE: Not used in base class, should be consumed by deriving class + + + + Connection object which handles your communication with the Stream Deck app + + + + + An opaque value identifying the plugin. Received as an argument when the executable was launched. + + + + + Holds information about the devices connected to the computer + + + + + Event received by the plugin when the Property Inspector uses the sendToPlugin event. + + + + + Event received when the user changes the title or title parameters. + + + + + Event received when a monitored application is terminated + + + + + Event received when a monitored application is launched + + + + + Event received when a device is unplugged from the computer + + + + + Event received when a device is plugged to the computer. + + + + + Event received when the Property Inspector appears in the Stream Deck software user interface, for example when selecting a new instance. + + + + + Event received when the Property Inspector for an instance is removed from the Stream Deck software user interface, for example when selecting a different instance. + + + + + Event received when the computer wakes up + + + + + An opaque value identifying the plugin. This value is received during the Registration procedure + + + + + An opaque value identifying the device the plugin is launched on. + + + + + StreamDeckConnection object, initialized based on the args received when launching the program + + + + + Public constructor, a StreamDeckConnection object is required along with the current action and context IDs + These will be used to correctly communicate with the StreamDeck App + + + + + + + /// + + + + Dispose (Destructor) function + + + + + Gets the Stream Deck device's info + + + + + + Send settings to the PropertyInspector + + + + + + + Persists your plugin settings + + + + + + + Persists your global plugin settings + + Settings to save globally + Boolean whether to also trigger a didReceiveGlobalSettings event. Default is true + + + + + Persists your global plugin settings + + + + + + Sets an image on the StreamDeck key. + + Base64 encoded image + A 0-based integer value representing the state of an action with multiple states. This is an optional parameter. If not specified, the title is set to all states. + Should image be sent even if it is identical to the one sent previously. Default is false + + + + + Sets an image on the StreamDeck key + + Image object + A 0-based integer value representing the state of an action with multiple states. This is an optional parameter. If not specified, the title is set to all states. + Should image be sent even if it is identical to the one sent previously. Default is false + + + + + Sets the default image for this state, as configured in the manifest + + + + + + Sets a title on the StreamDeck key + + + A 0-based integer value representing the state of an action with multiple states. This is an optional parameter. If not specified, the title is set to all states. + + + + + Switches to one of the plugin's built-in profiles + + + + + + + Switches to one of the plugin's built-in profiles. Allows to choose which device to switch it on. + + + + + + + + Shows the Alert (Yellow Triangle) on the StreamDeck key + + + + + + Shows the Success (Green checkmark) on the StreamDeck key + + + + + + Add a message to the Stream Deck log. This is the log located at: %appdata%\Elgato\StreamDeck\logs\StreamDeck0.log + + + + + + + Tells Stream Deck to return the current plugin settings via the ReceivedSettings function + + + + + + Opens a URI in the user's browser + + + + + + + Opens a URI in the user's browser + + + + + + + Sets the plugin to a specific state which is pre-configured in the manifest file + + + + + + + Sets the values of touchpad layouts items + + + + + + + Sets the value of a single touchpad layout item + + + + + + Sets the value of a single touchpad layout item + + + + + + Changes the current Stream Deck+ touch display layout + + + + + + * Easy Configuration Instructions: + * 1. Use NuGet to get the following packages: + * CommandLineParser by gsscoder + * streamdeck-client-csharp by Shane DeSeranno + * Newtonsoft.Json by James Newton-King + * 2. Create a class that implements the IPluginable interface (which is located in BarRaider.SDTools), this will be your main plugin + * 3. Pass the type of the class to the main function + + + + /************************************************************************ + * Initial configuration from TyrenDe's streamdeck-client-csharp example: + * https://github.com/TyrenDe/streamdeck-client-csharp + * and SaviorXTanren's MixItUp.StreamDeckPlugin: + * https://github.com/SaviorXTanren/mixer-mixitup/ + *************************************************************************/ + + Library's main initialization point. + Pass the args from your Main function. We'll handle the rest + + + + + + Library's main initialization point. + Pass the args from your Main function and a list of supported PluginActionIds, the framework will handle the rest. + + + + + + + Empty payload in event + + + + + Payload for ApplicationDidLaunch event + + + + + Application information + + + + + Payload for ApplicationDidTerminate Event + + + + + Application payload + + + + + List of all supported event typs + + + + + Base event that all the actual events derive from + + + + + Name of the event raised + + + + + Payload for DeviceDidConnect Event + + + + + UUID of device + + + + + Information on the device connected + + + + + Payload for DeviceDidDisconnect Event + + + + + UUID of device that was disconnected + + + + + Payload for Dial down event + + + + + Action Name + + + + + Unique Action UUID + + + + + Device UUID key was pressed on + + + + + Information on dial status + + + + + Payload for dial rotation event + + + + + Action Name + + + + + Unique Action UUID + + + + + Device UUID key was pressed on + + + + + Information on dial rotation + + + + + Payload for Dial up event + + + + + Action Name + + + + + Unique Action UUID + + + + + Device UUID key was pressed on + + + + + Information on dial status + + + + + Payload for DidReceiveGlobalSettings Event + + + + + Global Settings payload + + + + + Payload for DidReceiveSettings Event + + + + + Action Name + + + + + Context (unique action UUID) + + + + + Device UUID action is on + + + + + Settings for action + + + + + Payload for KeyDown event + + + + + Action Name + + + + + Unique Action UUID + + + + + Device UUID key was pressed on + + + + + Information on key + + + + + Payload for KeyUp event + + + + + Action name + + + + + Unique action UUID + + + + + Stream Deck device UUID + + + + + Key settings + + + + + Payload for PropertyInspectorDidAppearEvent event + + + + + Action Name + + + + + Unique Action UUID + + + + + Stream Deck device UUID + + + + + Payload for PropertyInspectorDidDisappearEvent event + + + + + Action Name + + + + + Unique Action UUID + + + + + Stream Deck device UUID + + + + + Payload for SendToPluginEvent event + + + + + Action Name + + + + + Unique Action UUID + + + + + Payload + + + + + Payload for SystemDidWakeUp event + + + + + Payload for TitleParametersDidChangeEvent event + + + + + Action Name + + + + + Unique Action UUID + + + + + Stream Deck device UUID + + + + + Title settings + + + + + Payload for touchpad press + + + + + Action Name + + + + + Unique Action UUID + + + + + Device UUID key was pressed on + + + + + Information on touchpad press + + + + + Payload for WillAppearEvent event + + + + + Action Name + + + + + Unique Action UUID + + + + + Stream Deck device UUID + + + + + Appearance settings + + + + + Payload for WillDisappearEvent event + + + + + Action Name + + + + + Unique Action UUID + + + + + Stream Deck device UUID + + + + + settings + + + + + Target to send Title/Image to + + + + + Send to both App and Device + + + + + Send only to device + + + + + Send only to app + + + + + Underlying object that communicates with the stream deck app + + + + + The port used to connect to the StreamDeck websocket + + + + + This is the unique identifier used to communicate with the register StreamDeck plugin. + + + + + Raised when plugin is connected to stream deck app + + + + + /// Raised when plugin is disconnected from stream deck app + + + + + Raised when key is pushed down + + + + + Raised when key is released + + + + + Raised when the action is shown, main trigger for a PluginAction + + + + + Raised when the action is no longer shown, main trigger for Dispose of PluginAction + + + + + Contains information on the Title and its style + + + + + Raised when a Stream Deck device is connected to the PC + + + + + Raised when a Stream Deck device has disconnected from the PC + + + + + Raised when a monitored app is launched/active + + + + + Raised when a monitored app is terminated + + + + + Raised after the PC wakes up from sleep + + + + + Raised when settings for the action are received + + + + + Raised when global settings for the entire plugin are received + + + + + Raised when the user is viewing the settings in the Stream Deck app + + + + + Raised when the user stops viewing the settings in the Stream Deck app + + + + + Raised when a payload is sent to the plugin from the PI + + + + + Raised when a dial is rotated + + + + + Raised when a dial is down + + + + + Raised when a dial is up + + + + + Raised when the tochpad is pressed + + + + + Payload for ApplicationDidLaunch event + + + + + Payload + + + + + Constructor + + + + + + Payload for ApplicationDidTerminate event + + + + + Payload + + + + + Constructor + + + + + + Payload for DeviceDidConnect event + + + + + Device GUID + + + + + Device Info + + + + + Constructor + + + + + + Payload for DeviceDidDisconnect event + + + + + Device GUID + + + + + Constructor + + + + + + Payload for PropertyInspectorDidAppear event + + + + + ActionId + + + + + ContextId + + + + + Device Guid + + + + + Constructor + + + + + + + + Payload for PropertyInspectorDidDisappear event + + + + + Action Id + + + + + ContextId + + + + + Device Guid + + + + + Constructor + + + + + + + + Payload for SendToPlugin event + + + + + ActionId + + + + + ContextId + + + + + Payload + + + + + Constructor + + + + + + + + Payload for SystemDidWakeUp event + + + + + Constructor + + + + + Payload for TitleParametersDidChange event + + + + + Action Id + + + + + Context Id + + + + + Device Guid + + + + + Payload + + + + + Constructor + + + + + + + + + Payload for Apperance settings + + + + + Additional settings + + + + + Coordinates of key pressed + + + + + State of key + + + + + Is action in MultiAction + + + + + Controller which issued the event + + + + + ApplicationPayload + + + + + Application Name + + + + + Constructor + + + + + + Payload received when a dial is down or up + + + + + Controller which issued the event + + + + + Current event settings + + + + + Coordinates of key on the stream deck + + + + + Constructor + + + + + + + + Default constructor for serialization + + + + + Payload received when a dial is rotated + + + + + Controller which issued the event + + + + + Current event settings + + + + + Coordinates of key on the stream deck + + + + + Number of ticks rotated. Positive is to the right, negative to the left + + + + + Boolean whether the dial is currently pressed or not + + + + + Constructor + + + + + + + + + + Default constructor for serialization + + + + + Class holding all the information passed to the plugin when the program was launched + + + + + Port to communicate with the StreamDeck app + + + + + UUID of the plugin + + + + + Name of the event we should pass to the StreamDeck app to register + + + + + Raw information in JSON format which we will parse into the DeviceInfo property + + + + + Information regarding the StreamDeck app and StreamDeck hardware which was parsed from the RawInfo JSON field. + + + + + Payload for TitleParametersDidChange Event + + + + + Settings JSON Object + + + + + Key Coordinates + + + + + Key State + + + + + Title + + + + + Title Parameters + + + + + Raw Title Parameters (not as proper object) + + + + + Constructor + + + + + + + + + + For Serilization + + + + + Raw payload for TitleParametersRawPayload event (without objects) + + + + + Name of font family + + + + + Size of font + + + + + Style of font (bold, italic) + + + + + Is there an underling + + + + + Should title be shown + + + + + Alignment of title (top, middle, bottom) + + + + + Color of title + + + + + Payload received when the touchpad (above the dials) is pressed + + + + + Controller which issued the event + + + + + Current event settings + + + + + Coordinates of key on the stream deck + + + + + Boolean whether it was a long press or not + + + + + Position on touchpad which was pressed + + + + + Constructor + + + + + + + + + + Default constructor for serialization + + + + + Payload received during the plugin's constructor + + + + + Plugin instance's settings (set through Property Inspector) + + + + + Plugin's physical location on the Stream Deck device + + + + + Current plugin state + + + + + Is it in a Multiaction + + + + + The controller of the current action. Values include "Keypad" and "Encoder". + + + + + Information regarding the Stream Deck hardware device + + + + + Constructor + + + + + + + Payload received when a key is pressed or released + + + + + Current event settings + + + + + Coordinates of key on the stream deck + + + + + Current key state + + + + + Desired state + + + + + Is part of a multiAction + + + + + Constructor + + + + + + + + + + For Seralization + + + + + Payload that holds all the settings in the ReceivedGlobalSettings event + + + + + Global settings object + + + + + Payload that holds all the settings in the ReceivedSettings event + + + + + Action's settings + + + + + Coordinates of the key pressed + + + + + Is event part of a multiaction + + + + + Type of StreamDeck hardware device, currently two are supported (classic and mini) + + + + + StreamDeck classic with 15 keys + + + + + StreamDeck mini with 6 keys + + + + + StreamDeck XL with 32 keys + + + + + StreamDeck Mobile version + + + + + Corsair G-Keys version + + + + + Pedal + + + + + Corsair CUE SDK (?) + + + + + Stream Deck+ + + + + + Holds general information on the StreamDeck App we're communicating with + + + + + Current language of the StreamDeck app + + + + + OS Platform + + + + + Current version of the StreamDeck app + + + + + Shows class information as string + + + + + + Class which holds information on the StreamDeck hardware device + + + + + Details on number of keys of the StreamDeck hardware device + + + + + Type of StreamDeck hardware device + + + + + Id of the StreamDeck hardware device + + + + + Constructor + + + + + + + + Shows class information as string + + + + + + Layout of the keys on the StreamDeck hardware device + + + + + Number of key rows on the StreamDeck hardware device + + + + + Number of key columns on the StreamDeck hardware device + + + + + Constructor + + + + + + + Shows class information as string + + + + + + Class which holds information on the StreamDeck app and StreamDeck hardware device that the plugin is communicating with + + + + + Information on the StreamDeck App which we're communicating with + + + + + Information on the StreamDeck hardware device that the plugin is running on + + + + + Information on the Plugin we're currently running + + + + + Device pixel ratio + + + + + Shows class information as string + + + + + + Holds general information on the StreamDeck App we're communicating with + + + + + Current version of the plugin + + + + + Shows class information as string + + + + + + Extension methods for various objects + + + + + Checks if too KeyCoordinates match to the same key + + + + + + + + Shows Color In Hex Format + + + + + + + Shows Color in Hex format + + + + + + + Converts an Image into a Byte Array + + + + + + + Convert a in-memory image object to Base64 format. Set the addHeaderPrefix to true, if this is sent to the SendImageAsync function + + + + + + + + Draws a string on a Graphics object and returns the ending Y position of the string + + + + + + + + + + + Returns the center X position of a string, given the image's max Width and Font information + + + + + + /// True/False - Does text fit image? False if text overflows + + + + + + + Returns the center X position of a string, given the image's max Width and Font information + + + + + + + + + + + + Returns the highest size of the given font in which the text fits the image + + + + + + /// + + + + + Adds a text path to an existing Graphics object. Uses TitleParameters to emulate the Text settings in the Property Inspector + + + + + + + + + + + Adds a text path to an existing Graphics object. Uses TitleParameters to emulate the Text settings in the Property Inspector + + + + + + + + + + + + + /// Truncates a string to the first maxSize characters. If maxSize is less than string length, original string will be returned + + String + Max size for string + + + + + Adds line breaks (\n) to the text to make sure it fits the key when using SetTitleAsync() + + + + + + + + + + + Helper class which allows fetching the GlobalSettings of a plugin + + + + + Returns singelton entry of GlobalSettingsManager + + + + + Event triggered when Global Settings are received + + + + + Command to request the Global Settings. Use the OnDidReceiveGlobalSSettings callback function to receive the Global Settings. + + + + + + Sets the Global Settings for the plugin + + + + + + + + Library of tools used to manipulate graphics + + + + + Return a Color object based on the hex value + + + + + + + Generates multiple shades based on an initial color, and number of stages/shades you want + + + + + + + + + Resizes an image while scaling + + + + + + + + + Extract a part of an Image + + + + + + + + + + + Creates a new image with different opacity + + + + + + + + Generates one (or more) images where each one has a few letters drawn on them based on the parameters. You can set number of letters and number of lines per key. + Use expandToNextImage to decide if you want only one Image returned or multiple if text is too long for one key + + + + + + + + + + + + + + + Adds line breaks ('\n') to the string every time the text would overflow the image + + + + + + + + + + + Tracing levels used for Logger + + + + + Debug level + + + + + Informational level + + + + + Warning level + + + + + Error level + + + + + Fatal (highest) level + + + + + Log4Net logger helper class + + + + + Returns singelton entry of Log4Net logger + + + + + Add message to log with a specific severity level. + + + + + + + Helper class for generating random numbers + + + + + Returns a non-negative random integer that is less than the specified maximum. + + + + + + + Returns a random integer that is within a specified range. Value will be less than the specified maximum. + + + + + + + + Set of common utilities used by various plugins + Currently the class mostly focuses on image-related functions that will be passed to the StreamDeck key + + + + + Convert an image file to Base64 format. Set the addHeaderPrefix to true, if this is sent to the SendImageAsync function + + + + + + + + Convert a in-memory image object to Base64 format. Set the addHeaderPrefix to true, if this is sent to the SendImageAsync function + + + + + + + + Convert a base64 image string to an Image object + + + + + + + Gets the key default height in pixels. + To get the StreamDeckType use Connection.DeviceInfo() + + + + + + + Gets the key default width in pixels. + To get the StreamDeckType use Connection.DeviceInfo() + + + + + + + Generates an empty key bitmap with the default height and width. + New: To get the StreamDeckType use Connection.DeviceInfo() + + + + + + + + Creates a key image that fits all Stream Decks + + + + + + + Creates a key image based on given height and width + + + + + + + + + Deprecated! Use AddTextPath on the Graphics extension method instead. + Adds a text path to an existing Graphics object. Uses TitleParser to emulate the Text settings in the Property Inspector + + + + + + + + + + + Extracts the actual filename from a file payload received from the Property Inspector + + + + + + + Converts a long to a human-readable string. Example: 54,265 => 54.27k + + + + + + Size in bytes + Formatted human-readable string (ex. "2 MB") + + + + OBSOLETE - Use String.SplitToFitKey() from SdTools.ExtensionMethods + + + + + + + + + + + Returns SHA512 Hash from an image object + + + + + + + Returns SHA512 Hash from a string + + + + + + + Returns SHA512 Hash from a byte stream + + + + + + + Iterates through the fromJObject, finds the property that matches in the toSettings object, and sets the value from the fromJObject object + + + + + Number of properties updated + + + + Takes a custom range and recalculates the value on a scale from 0 to 100 + + + + + + + + + Uses the PluginActionId attribute on the various classes derived from PluginBase to find all the actions supported in this assembly + + + + + + Coordinates of the current key + + + + + Column of the current key + + + + + Row of the current key + + + + + This class associates a plugin UUID (which is indicated in the Manifest file), with the type of the implementation class. + The implementation class must be derived from the PluginBase class for this to work properly. + If the type passed does not derrive from PluginBase, a NotSupportedException will be thrown + + + + + Action UUID as indicated in the manifest file + + + + + Type of class that implemented this action. Must inherit PluginBase + + + + + PluginActionId constructor + + actionId is the UUID from the manifest file + Type of class that implemented this action. Must inherit PluginBase + + + + Base (Generic) EventArgs used for events + + + + + + Event Information + + + + + Enum for the alignment of the Title text on the key + + + + + Top Alignment + + + + + Middle/Center Alignment + + + + + Bottom Alignment + + + + + Class holding all the Title Information set by a user in the Property Inspector + + + + + Title Color + + + + + Font Size in Points + + + + + Font Size in Pixels + + + + + Font Size Scaled to Image + + + + + Font Family + + + + + Font Style + + + + + Should Title be shown + + + + + Alignment position of the Title text on the key + + + + + Constructor + + + + + + + + + + + Constructor + + + + + + + + + + + diff --git a/streamdeck-gpu/gpu/App.config b/streamdeck-gpu/gpu/App.config new file mode 100644 index 0000000..9395f24 --- /dev/null +++ b/streamdeck-gpu/gpu/App.config @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streamdeck-gpu/gpu/Images/categoryIcon.png b/streamdeck-gpu/gpu/Images/categoryIcon.png new file mode 100644 index 0000000..3ba1d6a Binary files /dev/null and b/streamdeck-gpu/gpu/Images/categoryIcon.png differ diff --git a/streamdeck-gpu/gpu/Images/categoryIcon@2x.png b/streamdeck-gpu/gpu/Images/categoryIcon@2x.png new file mode 100644 index 0000000..cf91ebe Binary files /dev/null and b/streamdeck-gpu/gpu/Images/categoryIcon@2x.png differ diff --git a/streamdeck-gpu/gpu/Images/icon.png b/streamdeck-gpu/gpu/Images/icon.png new file mode 100644 index 0000000..15b7243 Binary files /dev/null and b/streamdeck-gpu/gpu/Images/icon.png differ diff --git a/streamdeck-gpu/gpu/Images/icon@2x.png b/streamdeck-gpu/gpu/Images/icon@2x.png new file mode 100644 index 0000000..0ed07a9 Binary files /dev/null and b/streamdeck-gpu/gpu/Images/icon@2x.png differ diff --git a/streamdeck-gpu/gpu/Images/pluginAction.png b/streamdeck-gpu/gpu/Images/pluginAction.png new file mode 100644 index 0000000..3ba1d6a Binary files /dev/null and b/streamdeck-gpu/gpu/Images/pluginAction.png differ diff --git a/streamdeck-gpu/gpu/Images/pluginAction@2x.png b/streamdeck-gpu/gpu/Images/pluginAction@2x.png new file mode 100644 index 0000000..cf91ebe Binary files /dev/null and b/streamdeck-gpu/gpu/Images/pluginAction@2x.png differ diff --git a/streamdeck-gpu/gpu/Images/pluginIcon.png b/streamdeck-gpu/gpu/Images/pluginIcon.png new file mode 100644 index 0000000..3ba1d6a Binary files /dev/null and b/streamdeck-gpu/gpu/Images/pluginIcon.png differ diff --git a/streamdeck-gpu/gpu/Images/pluginIcon@2x.png b/streamdeck-gpu/gpu/Images/pluginIcon@2x.png new file mode 100644 index 0000000..cf91ebe Binary files /dev/null and b/streamdeck-gpu/gpu/Images/pluginIcon@2x.png differ diff --git a/streamdeck-gpu/gpu/Images/wood-bg.png b/streamdeck-gpu/gpu/Images/wood-bg.png new file mode 100644 index 0000000..729eb83 Binary files /dev/null and b/streamdeck-gpu/gpu/Images/wood-bg.png differ diff --git a/streamdeck-gpu/gpu/PluginActionGPU.cs b/streamdeck-gpu/gpu/PluginActionGPU.cs new file mode 100644 index 0000000..04add70 --- /dev/null +++ b/streamdeck-gpu/gpu/PluginActionGPU.cs @@ -0,0 +1,170 @@ +using BarRaider.SdTools; +using BarRaider.SdTools.Wrappers; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using OpenHardwareMonitor.Hardware; +using System; +using System.Drawing; +using System.Threading.Tasks; + + +namespace gpu { + [PluginActionId("com.nicolasluckie.gpu.gpu")] + public class PluginActionGPU : KeypadBase { + + // Create a computer object + static Computer computer = new Computer() { CPUEnabled = true, GPUEnabled = true, MainboardEnabled = true, RAMEnabled = true, HDDEnabled = true }; + + private class PluginSettings { + public static PluginSettings CreateDefaultSettings() { + PluginSettings instance = new PluginSettings { + OutputFileName = String.Empty, + InputString = String.Empty + }; + return instance; + } + + [FilenameProperty] + [JsonProperty(PropertyName = "outputFileName")] + public string OutputFileName { get; set; } + + [JsonProperty(PropertyName = "inputString")] + public string InputString { get; set; } + } + + private readonly PluginSettings settings; + + /// + /// 1. Initialize PluginActionGPU and settings + /// 2. Subscribe to various events + /// 3. Open the computer object to start monitoring hardware components + /// + /// The connection to Stream Deck. + /// The initial payload. + public PluginActionGPU(ISDConnection connection, InitialPayload payload) : base(connection, payload) { + if (payload.Settings == null || payload.Settings.Count == 0) { + this.settings = PluginSettings.CreateDefaultSettings(); + } + else { + this.settings = payload.Settings.ToObject(); + } + + Connection.OnApplicationDidLaunch += Connection_OnApplicationDidLaunch; + Connection.OnApplicationDidTerminate += Connection_OnApplicationDidTerminate; + Connection.OnDeviceDidConnect += Connection_OnDeviceDidConnect; + Connection.OnDeviceDidDisconnect += Connection_OnDeviceDidDisconnect; + Connection.OnPropertyInspectorDidAppear += Connection_OnPropertyInspectorDidAppear; + Connection.OnPropertyInspectorDidDisappear += Connection_OnPropertyInspectorDidDisappear; + Connection.OnSendToPlugin += Connection_OnSendToPlugin; + Connection.OnTitleParametersDidChange += Connection_OnTitleParametersDidChange; + computer.Open(); + } + + private void Connection_OnTitleParametersDidChange(object sender, BarRaider.SdTools.Wrappers.SDEventReceivedEventArgs e) { + } + + private void Connection_OnSendToPlugin(object sender, BarRaider.SdTools.Wrappers.SDEventReceivedEventArgs e) { + } + + private void Connection_OnPropertyInspectorDidDisappear(object sender, BarRaider.SdTools.Wrappers.SDEventReceivedEventArgs e) { + } + + private void Connection_OnPropertyInspectorDidAppear(object sender, BarRaider.SdTools.Wrappers.SDEventReceivedEventArgs e) { + } + + private void Connection_OnDeviceDidDisconnect(object sender, BarRaider.SdTools.Wrappers.SDEventReceivedEventArgs e) { + } + + private void Connection_OnDeviceDidConnect(object sender, BarRaider.SdTools.Wrappers.SDEventReceivedEventArgs e) { + } + + private void Connection_OnApplicationDidTerminate(object sender, BarRaider.SdTools.Wrappers.SDEventReceivedEventArgs e) { + } + + private void Connection_OnApplicationDidLaunch(object sender, BarRaider.SdTools.Wrappers.SDEventReceivedEventArgs e) { + } + + + /// + /// 1. Close the computer object to stop monitoring hardware components + /// 2. Free and release resources + /// + public override void Dispose() { + computer.Close(); + Connection.OnApplicationDidLaunch -= Connection_OnApplicationDidLaunch; + Connection.OnApplicationDidTerminate -= Connection_OnApplicationDidTerminate; + Connection.OnDeviceDidConnect -= Connection_OnDeviceDidConnect; + Connection.OnDeviceDidDisconnect -= Connection_OnDeviceDidDisconnect; + Connection.OnPropertyInspectorDidAppear -= Connection_OnPropertyInspectorDidAppear; + Connection.OnPropertyInspectorDidDisappear -= Connection_OnPropertyInspectorDidDisappear; + Connection.OnSendToPlugin -= Connection_OnSendToPlugin; + Connection.OnTitleParametersDidChange -= Connection_OnTitleParametersDidChange; + Logger.Instance.LogMessage(TracingLevel.INFO, $"Destructor called"); + } + + public async override void KeyPressed(KeyPayload payload) { + //Logger.Instance.LogMessage(TracingLevel.INFO, "Key Pressed"); + //TitleParameters tp = new TitleParameters(new FontFamily("Arial"), FontStyle.Bold, 20, Color.White, true, TitleVerticalAlignment.Middle); + //using (Image image = Tools.GenerateGenericKeyImage(out Graphics graphics)) { + // graphics.FillRectangle(new SolidBrush(Color.White), 0, 0, image.Width, image.Height); + // graphics.AddTextPath(tp, image.Height, image.Width, "Test"); + // graphics.Dispose(); + + // await Connection.SetImageAsync(image); + //} + } + + public async override void KeyReleased(KeyPayload payload) { + //TitleParameters tp = new TitleParameters(new FontFamily("Arial"), FontStyle.Bold, 20, Color.White, true, TitleVerticalAlignment.Middle); + //using (Image image = Tools.GenerateGenericKeyImage(out Graphics graphics)) { + // graphics.FillRectangle(new SolidBrush(Color.White), 0, 0, image.Width, image.Height); + // graphics.AddTextPath(tp, image.Height, image.Width, "Test", Color.Black, 7); + // graphics.Dispose(); + + // await Connection.SetImageAsync(image); + //} + } + + + /// + /// 1. Updates the hardware components + /// 2. Retrieves the GPU temperature + /// 3. Draws the temperature % on the key + /// + /// This function is called every second by the Stream Deck software. + /// It is not called if the plugin is not visible. + /// + public async override void OnTick() { + // Update all the hardware components to get accurate readings + foreach (IHardware hardware in computer.Hardware) { + hardware.Update(); + } + + // Get the GPU load % + int gpuTemp; + gpuTemp = (int)Math.Round(computer.Hardware[3].Sensors[0].Value ?? 0.0); + + TitleParameters tp = new TitleParameters(new FontFamily("Arial"), FontStyle.Bold, 24, Color.White, true, TitleVerticalAlignment.Middle); + using (Image image = Tools.GenerateGenericKeyImage(out Graphics graphics)) { + graphics.FillRectangle(new SolidBrush(Color.White), 0, 0, image.Width, image.Height); + Image img = Image.FromFile("Images/wood-bg.png"); + graphics.DrawImage(img, 0, 0, img.Width, img.Height); + graphics.AddTextPath(tp, image.Height, image.Width, gpuTemp + "°C", Color.Black, 7); + graphics.Dispose(); + + await Connection.SetImageAsync(image); + } + } + + public override void ReceivedSettings(ReceivedSettingsPayload payload) { + Tools.AutoPopulateSettings(settings, payload.Settings); + SaveSettings(); + } + + public override void ReceivedGlobalSettings(ReceivedGlobalSettingsPayload payload) { } + + private Task SaveSettings() { + return Connection.SetSettingsAsync(JObject.FromObject(settings)); + } + } +} \ No newline at end of file diff --git a/streamdeck-gpu/gpu/Program.cs b/streamdeck-gpu/gpu/Program.cs new file mode 100644 index 0000000..ba4d4c9 --- /dev/null +++ b/streamdeck-gpu/gpu/Program.cs @@ -0,0 +1,12 @@ +using BarRaider.SdTools; + +namespace gpu { + class Program { + static void Main(string[] args) { + // Uncomment this line of code to allow for debugging + //while (!System.Diagnostics.Debugger.IsAttached) { System.Threading.Thread.Sleep(100); } + + SDWrapper.Run(args); + } + } +} diff --git a/streamdeck-gpu/gpu/PropertyInspector/Sample.html b/streamdeck-gpu/gpu/PropertyInspector/Sample.html new file mode 100644 index 0000000..30946be --- /dev/null +++ b/streamdeck-gpu/gpu/PropertyInspector/Sample.html @@ -0,0 +1,25 @@ + + + + + + + + SamplePlugin Settings + + + + + + + + diff --git a/streamdeck-gpu/gpu/PropertyInspector/Sample.js b/streamdeck-gpu/gpu/PropertyInspector/Sample.js new file mode 100644 index 0000000..0ccc176 --- /dev/null +++ b/streamdeck-gpu/gpu/PropertyInspector/Sample.js @@ -0,0 +1,5 @@ +function resetCounter() { + var payload = {}; + payload.property_inspector = 'resetCounter'; + sendPayloadToPlugin(payload); +} \ No newline at end of file diff --git a/streamdeck-gpu/gpu/PropertyInspector/caret.svg b/streamdeck-gpu/gpu/PropertyInspector/caret.svg new file mode 100644 index 0000000..b69162a --- /dev/null +++ b/streamdeck-gpu/gpu/PropertyInspector/caret.svg @@ -0,0 +1,3 @@ + + + diff --git a/streamdeck-gpu/gpu/PropertyInspector/check.png b/streamdeck-gpu/gpu/PropertyInspector/check.png new file mode 100644 index 0000000..b5e9e62 Binary files /dev/null and b/streamdeck-gpu/gpu/PropertyInspector/check.png differ diff --git a/streamdeck-gpu/gpu/PropertyInspector/check.svg b/streamdeck-gpu/gpu/PropertyInspector/check.svg new file mode 100644 index 0000000..5b96af0 --- /dev/null +++ b/streamdeck-gpu/gpu/PropertyInspector/check.svg @@ -0,0 +1,3 @@ + + + diff --git a/streamdeck-gpu/gpu/PropertyInspector/elg_calendar.svg b/streamdeck-gpu/gpu/PropertyInspector/elg_calendar.svg new file mode 100644 index 0000000..157e01b --- /dev/null +++ b/streamdeck-gpu/gpu/PropertyInspector/elg_calendar.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streamdeck-gpu/gpu/PropertyInspector/elg_calendar_inv.svg b/streamdeck-gpu/gpu/PropertyInspector/elg_calendar_inv.svg new file mode 100644 index 0000000..4f8af68 --- /dev/null +++ b/streamdeck-gpu/gpu/PropertyInspector/elg_calendar_inv.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/streamdeck-gpu/gpu/PropertyInspector/rcheck.svg b/streamdeck-gpu/gpu/PropertyInspector/rcheck.svg new file mode 100644 index 0000000..af478ee --- /dev/null +++ b/streamdeck-gpu/gpu/PropertyInspector/rcheck.svg @@ -0,0 +1,3 @@ + + + diff --git a/streamdeck-gpu/gpu/PropertyInspector/sdpi.css b/streamdeck-gpu/gpu/PropertyInspector/sdpi.css new file mode 100644 index 0000000..ccd6ad4 --- /dev/null +++ b/streamdeck-gpu/gpu/PropertyInspector/sdpi.css @@ -0,0 +1,1469 @@ +.linkspan { + cursor: pointer; + color: #7397d2; + text-decoration: underline; +} + +html { + --sdpi-bgcolor: #2D2D2D; + --sdpi-background: #3D3D3D; + --sdpi-color: #d8d8d8; + --sdpi-bordercolor: #3a3a3a; + --sdpi-buttonbordercolor: #969696; + --sdpi-borderradius: 0px; + --sdpi-width: 224px; + --sdpi-fontweight: 600; + height: 100%; + width: 100%; + overflow: hidden; +} + +html, body { + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + font-size: 9pt; + background-color: var(--sdpi-bgcolor); + color: #9a9a9a; +} + +body { + height: 100%; + padding: 0; + overflow-x: hidden; + overflow-y: auto; + margin: 0; + -webkit-overflow-scrolling: touch; + -webkit-text-size-adjust: 100%; + -webkit-font-smoothing: antialiased; +} + +mark { + background-color: var(--sdpi-bgcolor); + color: var(--sdpi-color); +} + +hr, hr2 { + -webkit-margin-before: 1em; + -webkit-margin-after: 1em; + border-style: none; + background: var(--sdpi-background); + height: 1px; +} + +hr2, +.sdpi-heading { + display: flex; + flex-basis: 100%; + align-items: center; + color: inherit; + font-size: 9pt; + margin: 8px 0px; +} + +.sdpi-heading::before, +.sdpi-heading::after { + content: ""; + flex-grow: 1; + background: var(--sdpi-background); + height: 1px; + font-size: 0px; + line-height: 0px; + margin: 0px 16px; +} + +hr2 { + height: 2px; +} + +hr, hr2 { + margin-left:16px; + margin-right:16px; +} + +.sdpi-item-value, +option, +input, +select, +button { + font-size: 10pt; + font-weight: var(--sdpi-fontweight); + letter-spacing: var(--sdpi-letterspacing); +} + + + +.win .sdpi-item-value, +.win option, +.win input, +.win select, +.win button { + font-size: 11px; + font-style: normal; + letter-spacing: inherit; + font-weight: 100; +} + +.win button { + font-size: 12px; +} + +::-webkit-progress-value, +meter::-webkit-meter-optimum-value { + border-radius: 2px; + /* background: linear-gradient(#ccf, #99f 20%, #77f 45%, #77f 55%, #cdf); */ +} + +::-webkit-progress-bar, +meter::-webkit-meter-bar { + border-radius: 3px; + background: var(--sdpi-background); +} + +::-webkit-progress-bar:active, +meter::-webkit-meter-bar:active { + border-radius: 3px; + background: #222222; +} +::-webkit-progress-value:active, +meter::-webkit-meter-optimum-value:active { + background: #99f; +} + +progress, +progress.sdpi-item-value { + min-height: 5px !important; + height: 5px; + background-color: #303030; +} + +progress { + margin-top: 8px !important; + margin-bottom: 8px !important; +} + +.full progress, +progress.full { + margin-top: 3px !important; +} + +::-webkit-progress-inner-element { + background-color: transparent; +} + + +.sdpi-item[type="progress"] { + margin-top: 4px !important; + margin-bottom: 12px; + min-height: 15px; +} + +.sdpi-item-child.full:last-child { + margin-bottom: 4px; +} + +.tabs { + /** + * Setting display to flex makes this container lay + * out its children using flexbox, the exact same + * as in the above "Stepper input" example. + */ + display: flex; + + border-bottom: 1px solid #D7DBDD; +} + +.tab { + cursor: pointer; + padding: 5px 30px; + color: #16a2d7; + font-size: 9pt; + border-bottom: 2px solid transparent; +} + +.tab.is-tab-selected { + border-bottom-color: #4ebbe4; +} + +select { + -webkit-appearance: none; + -moz-appearance: none; + -o-appearance: none; + appearance: none; + background: url(caret.svg) no-repeat 97% center; +} + +label.sdpi-file-label, +input[type="button"], +input[type="submit"], +input[type="reset"], +input[type="file"], +input[type=file]::-webkit-file-upload-button, +button, +select { + color: var(--sdpi-color); + border: 1pt solid #303030; + font-size: 8pt; + background-color: var(--sdpi-background); + border-radius: var(--sdpi-borderradius); +} + +label.sdpi-file-label, +input[type="button"], +input[type="submit"], +input[type="reset"], +input[type="file"], +input[type=file]::-webkit-file-upload-button, +button { + border: 1pt solid var(--sdpi-buttonbordercolor); + border-radius: var(--sdpi-borderradius); + border-color: var(--sdpi-buttonbordercolor); + min-height: 23px !important; + height: 23px !important; + margin-right: 8px; +} + +input[type=number]::-webkit-inner-spin-button, +input[type=number]::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; +} + +input[type="file"] { + border-radius: var(--sdpi-borderradius); + max-width: 220px; +} + + +option { + height: 1.5em; + padding: 4px; +} + +/* SDPI */ + +.sdpi-wrapper { + overflow-x: hidden; +} + +.sdpi-item { + display: flex; + flex-direction: row; + min-height: 32px; + align-items: center; + margin-top: 2px; + max-width: 344px; +} + +.sdpi-item:first-child { + margin-top:1px; +} + +.sdpi-item:last-child { + margin-bottom: 0px; +} + +.sdpi-item > *:not(.sdpi-item-label):not(meter):not(details) { + min-height: 26px; + padding: 0px 4px 0px 4px; +} + +.sdpi-item > *:not(.sdpi-item-label.empty):not(meter) { + min-height: 26px; + padding: 0px 4px 0px 4px; +} + + +.sdpi-item-group { + padding: 0 !important; +} + +meter.sdpi-item-value { + margin-left: 6px; +} + +.sdpi-item[type="group"] { + display: block; + margin-top: 12px; + margin-bottom: 12px; + /* border: 1px solid white; */ + flex-direction: unset; + text-align: left; +} + +.sdpi-item[type="group"] > .sdpi-item-label, +.sdpi-item[type="group"].sdpi-item-label { + width: 96%; + text-align: left; + font-weight: 700; + margin-bottom: 4px; + padding-left: 4px; +} + +dl, +ul, +ol { + -webkit-margin-before: 0px; + -webkit-margin-after: 4px; + -webkit-padding-start: 1em; + max-height: 90px; + overflow-y: scroll; + cursor: pointer; + user-select: none; +} + +table.sdpi-item-value, +dl.sdpi-item-value, +ul.sdpi-item-value, +ol.sdpi-item-value { + -webkit-margin-before: 4px; + -webkit-margin-after: 8px; + -webkit-padding-start: 1em; + width: var(--sdpi-width); + text-align: center; +} + +table > caption { + margin: 2px; +} + +.list, +.sdpi-item[type="list"] { + align-items: baseline; +} + +.sdpi-item-label { + text-align: right; + flex: none; + width: 94px; + padding-right: 4px; + font-weight: 600; + -webkit-user-select: none; +} + +.win .sdpi-item-label, +.sdpi-item-label > small{ + font-weight: normal; +} + +.sdpi-item-label:after { + content: ": "; +} + +.sdpi-item-label.empty:after { + content: ""; +} + +.sdpi-test, +.sdpi-item-value { + flex: 1 0 0; + /* flex-grow: 1; + flex-shrink: 0; */ + margin-right: 14px; + margin-left: 4px; + justify-content: space-evenly; +} + +canvas.sdpi-item-value { + max-width: 144px; + max-height: 144px; + width: 144px; + height: 144px; + margin: 0 auto; + cursor: pointer; +} + +input.sdpi-item-value { + margin-left: 5px; +} + +.sdpi-item-value button, +button.sdpi-item-value { + margin-left: 7px; + margin-right: 19px; +} + +.sdpi-item-value.range { + margin-left: 0px; +} + +table, +dl.sdpi-item-value, +ul.sdpi-item-value, +ol.sdpi-item-value, +.sdpi-item-value > dl, +.sdpi-item-value > ul, +.sdpi-item-value > ol +{ + list-style-type: none; + list-style-position: outside; + margin-left: -4px; + margin-right: -4px; + padding: 4px; + border: 1px solid var(--sdpi-bordercolor); +} + +dl.sdpi-item-value, +ul.sdpi-item-value, +ol.sdpi-item-value, +.sdpi-item-value > ol { + list-style-type: none; + list-style-position: inside; + margin-left: 5px; + margin-right: 12px; + padding: 4px !important; +} + +ol.sdpi-item-value, +.sdpi-item-value > ol[listtype="none"] { + list-style-type: none; +} +ol.sdpi-item-value[type="decimal"], +.sdpi-item-value > ol[type="decimal"] { + list-style-type: decimal; +} + +ol.sdpi-item-value[type="decimal-leading-zero"], +.sdpi-item-value > ol[type="decimal-leading-zero"] { + list-style-type: decimal-leading-zero; +} + +ol.sdpi-item-value[type="lower-alpha"], +.sdpi-item-value > ol[type="lower-alpha"] { + list-style-type: lower-alpha; +} + +ol.sdpi-item-value[type="upper-alpha"], +.sdpi-item-value > ol[type="upper-alpha"] { + list-style-type: upper-alpha; +} + +ol.sdpi-item-value[type="upper-roman"], +.sdpi-item-value > ol[type="upper-roman"] { + list-style-type: upper-roman; +} + +ol.sdpi-item-value[type="lower-roman"], +.sdpi-item-value > ol[type="lower-roman"] { + list-style-type: upper-roman; +} + +tr:nth-child(even), +.sdpi-item-value > ul > li:nth-child(even), +.sdpi-item-value > ol > li:nth-child(even), +li:nth-child(even) { + background-color: rgba(0,0,0,.2) +} + +td:hover, +.sdpi-item-value > ul > li:hover:nth-child(even), +.sdpi-item-value > ol > li:hover:nth-child(even), +li:hover:nth-child(even), +li:hover { + background-color: rgba(255,255,255,.1); +} + +td.selected, +td.selected:hover, +li.selected:hover, +li.selected { + color: white; + background-color: #77f; +} + +tr { + border: 1px solid var(--sdpi-bordercolor); +} + +td { + border-right: 1px solid var(--sdpi-bordercolor); + -webkit-user-select: none; +} + +tr:last-child, +td:last-child { + border: none; +} + +.sdpi-item-value.select, +.sdpi-item-value > select { + margin-right: 13px; + margin-left: 4px; +} + +.sdpi-item-child, +.sdpi-item-group > .sdpi-item > input[type="color"] { + margin-top: 0.4em; + margin-right: 4px; +} + +.full, +.full *, +.sdpi-item-value.full, +.sdpi-item-child > full > *, +.sdpi-item-child.full, +.sdpi-item-child.full > *, +.full > .sdpi-item-child, +.full > .sdpi-item-child > *{ + display: flex; + flex: 1 1 0; + margin-bottom: 4px; + margin-left: 0px; + width: 100%; + + justify-content: space-evenly; +} + +.sdpi-item-group > .sdpi-item > input[type="color"] { + margin-top: 0px; +} + +::-webkit-calendar-picker-indicator:focus, +input[type=file]::-webkit-file-upload-button:focus, +button:focus, +textarea:focus, +input:focus, +select:focus, +option:focus, +details:focus, +summary:focus, +.custom-select select { + outline: none; +} + +summary { + cursor: default; + -webkit-user-select: none; +} + +.pointer, +summary .pointer { + cursor: pointer; +} + +details.message { + padding: 4px 18px 4px 12px; +} + +details.message summary { + font-size: 10pt; + font-weight: 600; + min-height: 18px; +} + +details.message:first-child { + margin-top: 4px; + margin-left: 0; + padding-left: 106px; +} + +details.message h1 { + text-align: left; +} + +.message > summary::-webkit-details-marker { + display: none; +} + +.info20, +.question, +.caution, +.info { + background-repeat: no-repeat; + background-position: 70px center; +} + +.info20 { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cpath fill='%23999' d='M10,20 C4.4771525,20 0,15.5228475 0,10 C0,4.4771525 4.4771525,0 10,0 C15.5228475,0 20,4.4771525 20,10 C20,15.5228475 15.5228475,20 10,20 Z M10,8 C8.8954305,8 8,8.84275812 8,9.88235294 L8,16.1176471 C8,17.1572419 8.8954305,18 10,18 C11.1045695,18 12,17.1572419 12,16.1176471 L12,9.88235294 C12,8.84275812 11.1045695,8 10,8 Z M10,3 C8.8954305,3 8,3.88165465 8,4.96923077 L8,5.03076923 C8,6.11834535 8.8954305,7 10,7 C11.1045695,7 12,6.11834535 12,5.03076923 L12,4.96923077 C12,3.88165465 11.1045695,3 10,3 Z'/%3E%3C/svg%3E%0A"); +} + +.info { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cpath fill='%23999' d='M10,18 C5.581722,18 2,14.418278 2,10 C2,5.581722 5.581722,2 10,2 C14.418278,2 18,5.581722 18,10 C18,14.418278 14.418278,18 10,18 Z M10,8 C9.44771525,8 9,8.42137906 9,8.94117647 L9,14.0588235 C9,14.5786209 9.44771525,15 10,15 C10.5522847,15 11,14.5786209 11,14.0588235 L11,8.94117647 C11,8.42137906 10.5522847,8 10,8 Z M10,5 C9.44771525,5 9,5.44082732 9,5.98461538 L9,6.01538462 C9,6.55917268 9.44771525,7 10,7 C10.5522847,7 11,6.55917268 11,6.01538462 L11,5.98461538 C11,5.44082732 10.5522847,5 10,5 Z'/%3E%3C/svg%3E%0A"); +} + +.info2 { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='15' height='15' viewBox='0 0 15 15'%3E%3Cpath fill='%23999' d='M7.5,15 C3.35786438,15 0,11.6421356 0,7.5 C0,3.35786438 3.35786438,0 7.5,0 C11.6421356,0 15,3.35786438 15,7.5 C15,11.6421356 11.6421356,15 7.5,15 Z M7.5,2 C6.67157287,2 6,2.66124098 6,3.47692307 L6,3.52307693 C6,4.33875902 6.67157287,5 7.5,5 C8.32842705,5 9,4.33875902 9,3.52307693 L9,3.47692307 C9,2.66124098 8.32842705,2 7.5,2 Z M5,6 L5,7.02155172 L6,7 L6,12 L5,12.0076778 L5,13 L10,13 L10,12 L9,12.0076778 L9,6 L5,6 Z'/%3E%3C/svg%3E%0A"); +} + +.sdpi-more-info { + background-image: linear-gradient(to right, #00000000 0%,#00000040 80%), url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3E%3Cpolygon fill='%23999' points='4 7 8 7 8 5 12 8 8 11 8 9 4 9'/%3E%3C/svg%3E%0A"); +} +.caution { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cpath fill='%23999' fill-rule='evenodd' d='M9.03952676,0.746646542 C9.57068894,-0.245797319 10.4285735,-0.25196227 10.9630352,0.746646542 L19.7705903,17.2030214 C20.3017525,18.1954653 19.8777595,19 18.8371387,19 L1.16542323,19 C0.118729947,19 -0.302490098,18.2016302 0.231971607,17.2030214 L9.03952676,0.746646542 Z M10,2.25584053 L1.9601405,17.3478261 L18.04099,17.3478261 L10,2.25584053 Z M10,5.9375 C10.531043,5.9375 10.9615385,6.37373537 10.9615385,6.91185897 L10.9615385,11.6923077 C10.9615385,12.2304313 10.531043,12.6666667 10,12.6666667 C9.46895697,12.6666667 9.03846154,12.2304313 9.03846154,11.6923077 L9.03846154,6.91185897 C9.03846154,6.37373537 9.46895697,5.9375 10,5.9375 Z M10,13.4583333 C10.6372516,13.4583333 11.1538462,13.9818158 11.1538462,14.6275641 L11.1538462,14.6641026 C11.1538462,15.3098509 10.6372516,15.8333333 10,15.8333333 C9.36274837,15.8333333 8.84615385,15.3098509 8.84615385,14.6641026 L8.84615385,14.6275641 C8.84615385,13.9818158 9.36274837,13.4583333 10,13.4583333 Z'/%3E%3C/svg%3E%0A"); +} + +.question { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cpath fill='%23999' d='M10,18 C5.581722,18 2,14.418278 2,10 C2,5.581722 5.581722,2 10,2 C14.418278,2 18,5.581722 18,10 C18,14.418278 14.418278,18 10,18 Z M6.77783203,7.65332031 C6.77783203,7.84798274 6.85929281,8.02888914 7.0222168,8.19604492 C7.18514079,8.36320071 7.38508996,8.44677734 7.62207031,8.44677734 C8.02409055,8.44677734 8.29703704,8.20768468 8.44091797,7.72949219 C8.59326248,7.27245865 8.77945854,6.92651485 8.99951172,6.69165039 C9.2195649,6.45678594 9.56233491,6.33935547 10.027832,6.33935547 C10.4256205,6.33935547 10.7006836,6.37695313 11.0021973,6.68847656 C11.652832,7.53271484 10.942627,8.472229 10.3750916,9.1321106 C9.80755615,9.79199219 8.29492188,11.9897461 10.027832,12.1347656 C10.4498423,12.1700818 10.7027991,11.9147157 10.7832031,11.4746094 C11.0021973,9.59857178 13.1254883,8.82415771 13.1254883,7.53271484 C13.1254883,7.07568131 12.9974785,6.65250846 12.7414551,6.26318359 C12.4854317,5.87385873 12.1225609,5.56600048 11.652832,5.33959961 C11.1831031,5.11319874 10.6414419,5 10.027832,5 C9.36767248,5 8.79004154,5.13541531 8.29492187,5.40625 C7.79980221,5.67708469 7.42317837,6.01879677 7.16503906,6.43139648 C6.90689975,6.8439962 6.77783203,7.25130007 6.77783203,7.65332031 Z M10.0099668,15 C10.2713191,15 10.5016601,14.9108147 10.7009967,14.7324415 C10.9003332,14.5540682 11,14.3088087 11,13.9966555 C11,13.7157177 10.9047629,13.4793767 10.7142857,13.2876254 C10.5238086,13.0958742 10.2890379,13 10.0099668,13 C9.72646591,13 9.48726565,13.0958742 9.2923588,13.2876254 C9.09745196,13.4793767 9,13.7157177 9,13.9966555 C9,14.313268 9.10077419,14.5596424 9.30232558,14.735786 C9.50387698,14.9119295 9.73975502,15 10.0099668,15 Z'/%3E%3C/svg%3E%0A"); +} + + +.sdpi-more-info { + position: fixed; + left: 0px; + right: 0px; + bottom: 0px; + min-height:16px; + padding-right: 16px; + text-align: right; + -webkit-touch-callout: none; + cursor: pointer; + user-select: none; + background-position: right center; + background-repeat: no-repeat; + border-radius: var(--sdpi-borderradius); + text-decoration: none; + color: var(--sdpi-color); +} + +.sdpi-more-info-button { + display: flex; + align-self: right; + margin-left: auto; + position: fixed; + right: 17px; + bottom: 0px; +} + +details a { + background-position: right !important; + min-height: 24px; + display: inline-block; + line-height: 24px; + padding-right: 28px; +} +input:not([type="range"]), +textarea { + -webkit-appearance: none; + background: var(--sdpi-background); + color: var(--sdpi-color); + font-weight: normal; + font-size: 9pt; + border: none; + margin-top: 2px; + margin-bottom: 2px; +} + +textarea + label { + display: flex; + justify-content: flex-end +} +input[type="radio"], +input[type="checkbox"] { + display: none; +} +input[type="radio"] + label, +input[type="checkbox"] + label { + font-size: 9pt; + color: var(--sdpi-color); + font-weight: normal; + margin-right: 8px; + -webkit-user-select: none; +} + +input[type="radio"] + label:after, +input[type="checkbox"] + label:after { + content: " " !important; +} + +.sdpi-item[type="radio"] > .sdpi-item-value, +.sdpi-item[type="checkbox"] > .sdpi-item-value { + padding-top: 2px; +} + +.sdpi-item[type="checkbox"] > .sdpi-item-value > * { + margin-top: 4px; +} + +.sdpi-item[type="checkbox"] .sdpi-item-child, +.sdpi-item[type="radio"] .sdpi-item-child { + display: inline-block; +} + +.sdpi-item[type="range"] .sdpi-item-value, +.sdpi-item[type="meter"] .sdpi-item-child, +.sdpi-item[type="progress"] .sdpi-item-child { + display: flex; +} + +.sdpi-item[type="range"] .sdpi-item-value { + min-height: 26px; +} + +.sdpi-item[type="range"] .sdpi-item-value span, +.sdpi-item[type="meter"] .sdpi-item-child span, +.sdpi-item[type="progress"] .sdpi-item-child span { + margin-top: -2px; + min-width: 8px; + text-align: right; + user-select: none; + cursor: pointer; +} + +.sdpi-item[type="range"] .sdpi-item-value span { + margin-top: 7px; + text-align: right; +} + +span + input[type="range"] { + display: flex; + max-width: 168px; + +} + +.sdpi-item[type="range"] .sdpi-item-value span:first-child, +.sdpi-item[type="meter"] .sdpi-item-child span:first-child, +.sdpi-item[type="progress"] .sdpi-item-child span:first-child { + margin-right: 4px; +} + +.sdpi-item[type="range"] .sdpi-item-value span:last-child, +.sdpi-item[type="meter"] .sdpi-item-child span:last-child, +.sdpi-item[type="progress"] .sdpi-item-child span:last-child { + margin-left: 4px; +} + +.reverse { + transform: rotate(180deg); +} + +.sdpi-item[type="meter"] .sdpi-item-child meter + span:last-child { + margin-left: -10px; +} + +.sdpi-item[type="progress"] .sdpi-item-child meter + span:last-child { + margin-left: -14px; +} + +.sdpi-item[type="radio"] > .sdpi-item-value > * { + margin-top: 2px; +} + +details { + padding: 8px 18px 8px 12px; + min-width: 86px; +} + +details > h4 { + border-bottom: 1px solid var(--sdpi-bordercolor); +} + +legend { + display: none; +} +.sdpi-item-value > textarea { + padding: 0px; + width: 227px; + margin-left: 1px; +} + +input[type="radio"] + label span, +input[type="checkbox"] + label span { + display: inline-block; + width: 16px; + height: 16px; + margin: 2px 4px 2px 0; + border-radius: 3px; + vertical-align: middle; + background: var(--sdpi-background); + cursor: pointer; + border: 1px solid rgb(0,0,0,.2); +} + +input[type="radio"] + label span { + border-radius: 100%; +} + +input[type="radio"]:checked + label span, +input[type="checkbox"]:checked + label span { + background-color: #77f; + background-image: url(check.svg); + background-repeat: no-repeat; + background-position: center center; + border: 1px solid rgb(0,0,0,.4); +} + +input[type="radio"]:active:checked + label span, +input[type="radio"]:active + label span, +input[type="checkbox"]:active:checked + label span, +input[type="checkbox"]:active + label span { + background-color: #303030; +} + +input[type="radio"]:checked + label span { + background-image: url(rcheck.svg); +} + + +/* +input[type="radio"] + label span { + background: url(buttons.png) -38px top no-repeat; +} + +input[type="radio"]:checked + label span { + background: url(buttons.png) -57px top no-repeat; +} +*/ + +input[type="range"] { + width: var(--sdpi-width); + height: 30px; + overflow: hidden; + cursor: pointer; + background: transparent !important; +} + +.sdpi-item > input[type="range"] { + margin-left: 8px; + max-width: var(--sdpi-width); + width: var(--sdpi-width); + padding: 0px; +} + +/* +input[type="range"], +input[type="range"]::-webkit-slider-runnable-track, +input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; +} +*/ + +input[type="range"]::-webkit-slider-runnable-track { + height: 5px; + background: #979797; + border-radius: 3px; + padding:0px !important; + border: 1px solid var(--sdpi-background); +} + +input[type="range"]::-webkit-slider-thumb { + position: relative; + -webkit-appearance: none; + background-color: var(--sdpi-color); + width: 12px; + height: 12px; + border-radius: 20px; + margin-top: -5px; + border: none; + +} +input[type="range" i]{ + margin: 0; +} + +input[type="range"]::-webkit-slider-thumb::before { + position: absolute; + content: ""; + height: 5px; /* equal to height of runnable track or 1 less */ + width: 500px; /* make this bigger than the widest range input element */ + left: -502px; /* this should be -2px - width */ + top: 8px; /* don't change this */ + background: #77f; +} + +input[type="color"] { + min-width: 32px; + min-height: 32px; + width: 32px; + height: 32px; + padding: 0; + background-color: var(--sdpi-bgcolor); + flex: none; +} + +::-webkit-color-swatch { + min-width: 24px; +} + +textarea { + height: 3em; + word-break: break-word; + line-height: 1.5em; +} + +.textarea { + padding: 0px !important; +} + +textarea { + width: 221px; /*98%;*/ + height: 96%; + min-height: 6em; + resize: none; + border-radius: var(--sdpi-borderradius); +} + +/* CAROUSEL */ + +.sdpi-item[type="carousel"]{ + +} + +.sdpi-item.card-carousel-wrapper, +.sdpi-item > .card-carousel-wrapper { + padding: 0; +} + + +.card-carousel-wrapper { + display: flex; + align-items: center; + justify-content: center; + margin: 12px auto; + color: #666a73; +} + +.card-carousel { + display: flex; + justify-content: center; + width: 278px; +} +.card-carousel--overflow-container { + overflow: hidden; +} +.card-carousel--nav__left, +.card-carousel--nav__right { + /* display: inline-block; */ + width: 12px; + height: 12px; + border-top: 2px solid #42b883; + border-right: 2px solid #42b883; + cursor: pointer; + margin: 0 4px; + transition: transform 150ms linear; +} +.card-carousel--nav__left[disabled], +.card-carousel--nav__right[disabled] { + opacity: 0.2; + border-color: black; +} +.card-carousel--nav__left { + transform: rotate(-135deg); +} +.card-carousel--nav__left:active { + transform: rotate(-135deg) scale(0.85); +} +.card-carousel--nav__right { + transform: rotate(45deg); +} +.card-carousel--nav__right:active { + transform: rotate(45deg) scale(0.85); +} +.card-carousel-cards { + display: flex; + transition: transform 150ms ease-out; + transform: translatex(0px); +} +.card-carousel-cards .card-carousel--card { + margin: 0 5px; + cursor: pointer; + /* box-shadow: 0 4px 15px 0 rgba(40, 44, 53, 0.06), 0 2px 2px 0 rgba(40, 44, 53, 0.08); */ + background-color: #fff; + border-radius: 4px; + z-index: 3; +} +.xxcard-carousel-cards .card-carousel--card:first-child { + margin-left: 0; +} +.xxcard-carousel-cards .card-carousel--card:last-child { + margin-right: 0; +} +.card-carousel-cards .card-carousel--card img { + vertical-align: bottom; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + transition: opacity 150ms linear; + width: 60px; +} +.card-carousel-cards .card-carousel--card img:hover { + opacity: 0.5; +} +.card-carousel-cards .card-carousel--card--footer { + border-top: 0; + max-width: 80px; + overflow: hidden; + display: flex; + height: 100%; + flex-direction: column; +} +.card-carousel-cards .card-carousel--card--footer p { + padding: 3px 0; + margin: 0; + margin-bottom: 2px; + font-size: 15px; + font-weight: 500; + color: #2c3e50; +} +.card-carousel-cards .card-carousel--card--footer p:nth-of-type(2) { + font-size: 12px; + font-weight: 300; + padding: 6px; + color: #666a73; +} + + +h1 { + font-size: 1.3em; + font-weight: 500; + text-align: center; + margin-bottom: 12px; +} + +::-webkit-datetime-edit { + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + background: url(elg_calendar_inv.svg) no-repeat left center; + padding-right: 1em; + padding-left: 25px; + background-position: 4px 0px; + } +::-webkit-datetime-edit-fields-wrapper { + + } +::-webkit-datetime-edit-text { padding: 0 0.3em; } +::-webkit-datetime-edit-month-field { } +::-webkit-datetime-edit-day-field {} +::-webkit-datetime-edit-year-field {} +::-webkit-inner-spin-button { + + /* display: none; */ + } +::-webkit-calendar-picker-indicator { + background: transparent; + font-size: 17px; +} + +::-webkit-calendar-picker-indicator:focus { + background-color: rgba(0,0,0,0.2); +} + +input[type="date"] { + -webkit-align-items: center; + display: -webkit-inline-flex; + font-family: monospace; + overflow: hidden; + padding: 0; + -webkit-padding-start: 1px; +} + +input::-webkit-datetime-edit { + -webkit-flex: 1; + -webkit-user-modify: read-only !important; + display: inline-block; + min-width: 0; + overflow: hidden; +} + +/* +input::-webkit-datetime-edit-fields-wrapper { + -webkit-user-modify: read-only !important; + display: inline-block; + padding: 1px 0; + white-space: pre; + +} +*/ + +/* +input[type="date"] { + background-color: red; + outline: none; +} + +input[type="date"]::-webkit-clear-button { + font-size: 18px; + height: 30px; + position: relative; +} + +input[type="date"]::-webkit-inner-spin-button { + height: 28px; +} + +input[type="date"]::-webkit-calendar-picker-indicator { + font-size: 15px; +} */ + +input[type="file"] { + opacity: 0; + display: none; +} + +.sdpi-item > input[type="file"] { + opacity: 1; + display: flex; +} + +input[type="file"] + span { + display: flex; + flex: 0 1 auto; + background-color: #0000ff50; +} + +label.sdpi-file-label { + cursor: pointer; + user-select: none; + display: inline-block; + min-height: 21px !important; + height: 21px !important; + line-height: 20px; + padding: 0px 4px; + margin: auto; + margin-right: 0px; + float:right; +} + +.sdpi-file-label > label:active, +.sdpi-file-label.file:active, +label.sdpi-file-label:active, +label.sdpi-file-info:active, +input[type="file"]::-webkit-file-upload-button:active, +button:active { + background-color: var(--sdpi-color); + color:#303030; +} + + +input:required:invalid, input:focus:invalid { + background: var(--sdpi-background) url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI5IiBoZWlnaHQ9IjkiIHZpZXdCb3g9IjAgMCA5IDkiPgogICAgPHBhdGggZmlsbD0iI0Q4RDhEOCIgZD0iTTQuNSwwIEM2Ljk4NTI4MTM3LC00LjU2NTM4NzgyZS0xNiA5LDIuMDE0NzE4NjMgOSw0LjUgQzksNi45ODUyODEzNyA2Ljk4NTI4MTM3LDkgNC41LDkgQzIuMDE0NzE4NjMsOSAzLjA0MzU5MTg4ZS0xNiw2Ljk4NTI4MTM3IDAsNC41IEMtMy4wNDM1OTE4OGUtMTYsMi4wMTQ3MTg2MyAyLjAxNDcxODYzLDQuNTY1Mzg3ODJlLTE2IDQuNSwwIFogTTQsMSBMNCw2IEw1LDYgTDUsMSBMNCwxIFogTTQuNSw4IEM0Ljc3NjE0MjM3LDggNSw3Ljc3NjE0MjM3IDUsNy41IEM1LDcuMjIzODU3NjMgNC43NzYxNDIzNyw3IDQuNSw3IEM0LjIyMzg1NzYzLDcgNCw3LjIyMzg1NzYzIDQsNy41IEM0LDcuNzc2MTQyMzcgNC4yMjM4NTc2Myw4IDQuNSw4IFoiLz4KICA8L3N2Zz4) no-repeat 98% center; +} + +input:required:valid { + background: var(--sdpi-background) url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI5IiBoZWlnaHQ9IjkiIHZpZXdCb3g9IjAgMCA5IDkiPjxwb2x5Z29uIGZpbGw9IiNEOEQ4RDgiIHBvaW50cz0iNS4yIDEgNi4yIDEgNi4yIDcgMy4yIDcgMy4yIDYgNS4yIDYiIHRyYW5zZm9ybT0icm90YXRlKDQwIDQuNjc3IDQpIi8+PC9zdmc+) no-repeat 98% center; +} + +.tooltip, +:tooltip, +:title { + color: yellow; +} + +[title]:hover { + display: flex; + align-items: center; + justify-content: center; +} + +[title]:hover::after { + content: ''; + position: absolute; + bottom: -1000px; + left: 8px; + display: none; + color: #fff; + border: 8px solid transparent; + border-bottom: 8px solid #000; +} +[title]:hover::before { +content: attr(title); + display: flex; + justify-content: center; + align-self: center; + padding: 6px 12px; + border-radius: 5px; + background: rgba(0,0,0,0.8); + color: var(--sdpi-color); + font-size: 9pt; + font-family: sans-serif; + opacity: 1; + position: absolute; + height: auto; + /* width: 50%; + left: 35%; */ + text-align: center; + bottom: 2px; + z-index: 100; + box-shadow: 0px 3px 6px rgba(0, 0, 0, .5); + /* box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); */ +} + +.sdpi-item-group.file { + width: 232px; + display: flex; + align-items: center; +} + +.sdpi-file-info { + overflow-wrap: break-word; + word-wrap: break-word; + hyphens: auto; + + min-width: 132px; + max-width: 144px; + max-height: 32px; + margin-top: 0px; + margin-left: 5px; + display: inline-block; + overflow: hidden; + padding: 6px 4px; + background-color: var(--sdpi-background); +} + + +::-webkit-scrollbar { + width: 8px; +} + +::-webkit-scrollbar-track { + -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); +} + +::-webkit-scrollbar-thumb { + background-color: #999999; + outline: 1px solid slategrey; + border-radius: 8px; +} + +a { + color: #7397d2; +} + +.testcontainer { + display: flex; + background-color: #0000ff20; + max-width: 400px; + height: 200px; + align-content: space-evenly; +} + +input[type=range] { + -webkit-appearance: none; + /* background-color: green; */ + height:6px; + margin-top: 12px; + z-index: 0; + overflow: visible; +} + +/* +input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + background-color: var(--sdpi-color); + width: 12px; + height: 12px; + border-radius: 20px; + margin-top: -6px; + border: none; +} */ + +:-webkit-slider-thumb { + -webkit-appearance: none; + background-color: var(--sdpi-color); + width: 16px; + height: 16px; + border-radius: 20px; + margin-top: -6px; + border: 1px solid #999999; +} + +.sdpi-item[type="range"] .sdpi-item-group { + display: flex; + flex-direction: column; +} + +.xxsdpi-item[type="range"] .sdpi-item-group input { + max-width: 204px; +} + +.sdpi-item[type="range"] .sdpi-item-group span { + margin-left: 0px !important; +} + +.sdpi-item[type="range"] .sdpi-item-group > .sdpi-item-child { + display: flex; + flex-direction: row; +} + +:disabled { + color: #993333; +} + +select, +select option { + color: var(--sdpi-color); +} + +select.disabled, +select option:disabled { + color: #fd9494; + font-style: italic; +} + +.runningAppsContainer { + display: none; +} + +/* debug +div { + background-color: rgba(64,128,255,0.2); +} +*/ + +.min80 > .sdpi-item-child { + min-width: 80px; +} + +.min100 > .sdpi-item-child { + min-width: 100px; +} + +.min120 > .sdpi-item-child { + min-width: 120px; +} + +.min140 > .sdpi-item-child { + min-width: 140px; +} + +.min160 > .sdpi-item-child { + min-width: 160px; +} + +.min200 > .sdpi-item-child { + min-width: 200px; +} + +.max40 { + flex-basis: 40%; + flex-grow: 0; +} + +.max30 { + flex-basis: 30%; + flex-grow: 0; +} + +.max20 { + flex-basis: 20%; + flex-grow: 0; +} + +.up20 { + margin-top: -20px; +} + +.alignCenter { + align-items: center; +} + +.alignTop { + align-items: flex-start; +} + +.alignBaseline { + align-items: baseline; +} + +.noMargins, +.noMargins *, +.noInnerMargins * { + margin: 0; + padding: 0; +} + +.hidden { + display: none; +} + +.icon-brighter, +.icon-darker, +.icon-warmer, +.icon-cooler { + margin-top: 5px !important; + min-width: 20px; + width: 20px; + background-repeat: no-repeat; +} + +.icon-brighter { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cg fill='%23999' fill-rule='evenodd'%3E%3Ccircle cx='10' cy='10' r='4'/%3E%3Cpath d='M14.8532861,7.77530426 C14.7173255,7.4682615 14.5540843,7.17599221 14.3666368,6.90157083 L16.6782032,5.5669873 L17.1782032,6.4330127 L14.8532861,7.77530426 Z M10.5,4.5414007 C10.2777625,4.51407201 10.051423,4.5 9.82179677,4.5 C9.71377555,4.5 9.60648167,4.50311409 9.5,4.50925739 L9.5,2 L10.5,2 L10.5,4.5414007 Z M5.38028092,6.75545367 C5.18389364,7.02383457 5.01124349,7.31068015 4.86542112,7.61289977 L2.82179677,6.4330127 L3.32179677,5.5669873 L5.38028092,6.75545367 Z M4.86542112,12.3871002 C5.01124349,12.6893198 5.18389364,12.9761654 5.38028092,13.2445463 L3.32179677,14.4330127 L2.82179677,13.5669873 L4.86542112,12.3871002 Z M9.5,15.4907426 C9.60648167,15.4968859 9.71377555,15.5 9.82179677,15.5 C10.051423,15.5 10.2777625,15.485928 10.5,15.4585993 L10.5,18 L9.5,18 L9.5,15.4907426 Z M14.3666368,13.0984292 C14.5540843,12.8240078 14.7173255,12.5317385 14.8532861,12.2246957 L17.1782032,13.5669873 L16.6782032,14.4330127 L14.3666368,13.0984292 Z'/%3E%3C/g%3E%3C/svg%3E"); +} +.icon-darker { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cg fill='%23999' fill-rule='evenodd'%3E%3Cpath d='M10 14C7.790861 14 6 12.209139 6 10 6 7.790861 7.790861 6 10 6 12.209139 6 14 7.790861 14 10 14 12.209139 12.209139 14 10 14zM10 13C11.6568542 13 13 11.6568542 13 10 13 8.34314575 11.6568542 7 10 7 8.34314575 7 7 8.34314575 7 10 7 11.6568542 8.34314575 13 10 13zM14.8532861 7.77530426C14.7173255 7.4682615 14.5540843 7.17599221 14.3666368 6.90157083L16.6782032 5.5669873 17.1782032 6.4330127 14.8532861 7.77530426zM10.5 4.5414007C10.2777625 4.51407201 10.051423 4.5 9.82179677 4.5 9.71377555 4.5 9.60648167 4.50311409 9.5 4.50925739L9.5 2 10.5 2 10.5 4.5414007zM5.38028092 6.75545367C5.18389364 7.02383457 5.01124349 7.31068015 4.86542112 7.61289977L2.82179677 6.4330127 3.32179677 5.5669873 5.38028092 6.75545367zM4.86542112 12.3871002C5.01124349 12.6893198 5.18389364 12.9761654 5.38028092 13.2445463L3.32179677 14.4330127 2.82179677 13.5669873 4.86542112 12.3871002zM9.5 15.4907426C9.60648167 15.4968859 9.71377555 15.5 9.82179677 15.5 10.051423 15.5 10.2777625 15.485928 10.5 15.4585993L10.5 18 9.5 18 9.5 15.4907426zM14.3666368 13.0984292C14.5540843 12.8240078 14.7173255 12.5317385 14.8532861 12.2246957L17.1782032 13.5669873 16.6782032 14.4330127 14.3666368 13.0984292z'/%3E%3C/g%3E%3C/svg%3E"); +} +.icon-warmer { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cg fill='%23999' fill-rule='evenodd'%3E%3Cpath d='M12.3247275 11.4890349C12.0406216 11.0007637 11.6761954 10.5649925 11.2495475 10.1998198 11.0890394 9.83238991 11 9.42659309 11 9 11 7.34314575 12.3431458 6 14 6 15.6568542 6 17 7.34314575 17 9 17 10.6568542 15.6568542 12 14 12 13.3795687 12 12.8031265 11.8116603 12.3247275 11.4890349zM17.6232392 11.6692284C17.8205899 11.4017892 17.9890383 11.1117186 18.123974 10.8036272L20.3121778 12.0669873 19.8121778 12.9330127 17.6232392 11.6692284zM18.123974 7.19637279C17.9890383 6.88828142 17.8205899 6.5982108 17.6232392 6.33077158L19.8121778 5.0669873 20.3121778 5.9330127 18.123974 7.19637279zM14.5 4.52746439C14.3358331 4.50931666 14.1690045 4.5 14 4.5 13.8309955 4.5 13.6641669 4.50931666 13.5 4.52746439L13.5 2 14.5 2 14.5 4.52746439zM13.5 13.4725356C13.6641669 13.4906833 13.8309955 13.5 14 13.5 14.1690045 13.5 14.3358331 13.4906833 14.5 13.4725356L14.5 16 13.5 16 13.5 13.4725356zM14 11C15.1045695 11 16 10.1045695 16 9 16 7.8954305 15.1045695 7 14 7 12.8954305 7 12 7.8954305 12 9 12 10.1045695 12.8954305 11 14 11zM9.5 11C10.6651924 11.4118364 11.5 12.5 11.5 14 11.5 16 10 17.5 8 17.5 6 17.5 4.5 16 4.5 14 4.5 12.6937812 5 11.5 6.5 11L6.5 7 9.5 7 9.5 11z'/%3E%3Cpath d='M12,14 C12,16.209139 10.209139,18 8,18 C5.790861,18 4,16.209139 4,14 C4,12.5194353 4.80439726,11.2267476 6,10.5351288 L6,4 C6,2.8954305 6.8954305,2 8,2 C9.1045695,2 10,2.8954305 10,4 L10,10.5351288 C11.1956027,11.2267476 12,12.5194353 12,14 Z M11,14 C11,12.6937812 10.1651924,11.5825421 9,11.1707057 L9,4 C9,3.44771525 8.55228475,3 8,3 C7.44771525,3 7,3.44771525 7,4 L7,11.1707057 C5.83480763,11.5825421 5,12.6937812 5,14 C5,15.6568542 6.34314575,17 8,17 C9.65685425,17 11,15.6568542 11,14 Z'/%3E%3C/g%3E%3C/svg%3E"); +} + +.icon-cooler { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cg fill='%23999' fill-rule='evenodd'%3E%3Cpath d='M10.4004569 11.6239517C10.0554735 10.9863849 9.57597206 10.4322632 9 9.99963381L9 9.7450467 9.53471338 9.7450467 10.8155381 8.46422201C10.7766941 8.39376637 10.7419749 8.32071759 10.7117062 8.2454012L9 8.2454012 9 6.96057868 10.6417702 6.96057868C10.6677696 6.86753378 10.7003289 6.77722682 10.7389179 6.69018783L9.44918707 5.40045694 9 5.40045694 9 4.34532219 9.32816127 4.34532219 9.34532219 2.91912025 10.4004569 2.91912025 10.4004569 4.53471338 11.6098599 5.74411634C11.7208059 5.68343597 11.8381332 5.63296451 11.9605787 5.59396526L11.9605787 3.8884898 10.8181818 2.74609294 11.5642748 2 12.5727518 3.00847706 13.5812289 2 14.3273218 2.74609294 13.2454012 3.82801356 13.2454012 5.61756719C13.3449693 5.65339299 13.4408747 5.69689391 13.5324038 5.74735625L14.7450467 4.53471338 14.7450467 2.91912025 15.8001815 2.91912025 15.8001815 4.34532219 17.2263834 4.34532219 17.2263834 5.40045694 15.6963166 5.40045694 14.4002441 6.69652946C14.437611 6.78161093 14.4692249 6.86979146 14.4945934 6.96057868L16.2570138 6.96057868 17.3994107 5.81818182 18.1455036 6.56427476 17.1370266 7.57275182 18.1455036 8.58122888 17.3994107 9.32732182 16.3174901 8.2454012 14.4246574 8.2454012C14.3952328 8.31861737 14.3616024 8.38969062 14.3240655 8.45832192L15.6107903 9.7450467 17.2263834 9.7450467 17.2263834 10.8001815 15.8001815 10.8001815 15.8001815 12.2263834 14.7450467 12.2263834 14.7450467 10.6963166 13.377994 9.32926387C13.3345872 9.34850842 13.2903677 9.36625331 13.2454012 9.38243281L13.2454012 11.3174901 14.3273218 12.3994107 13.5812289 13.1455036 12.5848864 12.1491612 11.5642748 13.1455036 10.8181818 12.3994107 11.9605787 11.2570138 11.9605787 9.40603474C11.8936938 9.38473169 11.828336 9.36000556 11.7647113 9.33206224L10.4004569 10.6963166 10.4004569 11.6239517zM12.75 8.5C13.3022847 8.5 13.75 8.05228475 13.75 7.5 13.75 6.94771525 13.3022847 6.5 12.75 6.5 12.1977153 6.5 11.75 6.94771525 11.75 7.5 11.75 8.05228475 12.1977153 8.5 12.75 8.5zM9.5 14C8.5 16.3333333 7.33333333 17.5 6 17.5 4.66666667 17.5 3.5 16.3333333 2.5 14L9.5 14z'/%3E%3Cpath d='M10,14 C10,16.209139 8.209139,18 6,18 C3.790861,18 2,16.209139 2,14 C2,12.5194353 2.80439726,11.2267476 4,10.5351288 L4,4 C4,2.8954305 4.8954305,2 6,2 C7.1045695,2 8,2.8954305 8,4 L8,10.5351288 C9.19560274,11.2267476 10,12.5194353 10,14 Z M9,14 C9,12.6937812 8.16519237,11.5825421 7,11.1707057 L7,4 C7,3.44771525 6.55228475,3 6,3 C5.44771525,3 5,3.44771525 5,4 L5,11.1707057 C3.83480763,11.5825421 3,12.6937812 3,14 C3,15.6568542 4.34314575,17 6,17 C7.65685425,17 9,15.6568542 9,14 Z'/%3E%3C/g%3E%3C/svg%3E"); +} + +/** +input[type=range].vVertical { + -webkit-appearance: none; + background-color: green; + margin-left: -60px; + width: 100px; + height:6px; + margin-top: 0px; + transform:rotate(90deg); + z-index: 0; + overflow: visible; +} + +input[type=range].vHorizon { + -webkit-appearance: none; + background-color: pink; + height: 10px; + width:200px; + +} + +.test2 { + background-color: #00ff0020; + display: flex; +} + + +.vertical.sdpi-item[type="range"] .sdpi-item-value { + display: block; +} + + +.vertical.sdpi-item:first-child, +.vertical { + margin-top: 12px; + margin-bottom: 16px; +} +.vertical > .sdpi-item-value { + margin-right: 16px; +} + +.vertical .sdpi-item-group { + width: 100%; + display: flex; + justify-content: space-evenly; +} + +.vertical input[type=range] { + height: 100px; + width: 21px; + -webkit-appearance: slider-vertical; + display: flex; + flex-flow: column; +} + +.vertical input[type="range"]::-webkit-slider-runnable-track { + height: auto; + width: 5px; +} + +.vertical input[type="range"]::-webkit-slider-thumb { + margin-top: 0px; + margin-left: -6px; +} + +.vertical .sdpi-item-value { + flex-flow: column; + align-items: flex-start; +} + +.vertical.sdpi-item[type="range"] .sdpi-item-value { + align-items: center; + margin-right: 16px; + text-align: center; +} + +.vertical.sdpi-item[type="range"] .sdpi-item-value span, +.vertical input[type="range"] .sdpi-item-value span { + text-align: center; + margin: 4px 0px; +} +*/ diff --git a/streamdeck-gpu/gpu/PropertyInspector/sdtools.common.js b/streamdeck-gpu/gpu/PropertyInspector/sdtools.common.js new file mode 100644 index 0000000..81ce8d6 --- /dev/null +++ b/streamdeck-gpu/gpu/PropertyInspector/sdtools.common.js @@ -0,0 +1,282 @@ +// sdtools.common.js v1.0 +var websocket = null, + uuid = null, + registerEventName = null, + actionInfo = {}, + inInfo = {}, + runningApps = [], + isQT = navigator.appVersion.includes('QtWebEngine'); + +function connectElgatoStreamDeckSocket(inPort, inUUID, inRegisterEvent, inInfo, inActionInfo) { + uuid = inUUID; + registerEventName = inRegisterEvent; + console.log(inUUID, inActionInfo); + actionInfo = JSON.parse(inActionInfo); // cache the info + inInfo = JSON.parse(inInfo); + websocket = new WebSocket('ws://127.0.0.1:' + inPort); + + addDynamicStyles(inInfo.colors); + + websocket.onopen = websocketOnOpen; + websocket.onmessage = websocketOnMessage; + + // Allow others to get notified that the websocket is created + var event = new Event('websocketCreate'); + document.dispatchEvent(event); + + loadConfiguration(actionInfo.payload.settings); +} + +function websocketOnOpen() { + var json = { + event: registerEventName, + uuid: uuid + }; + websocket.send(JSON.stringify(json)); + + // Notify the plugin that we are connected + sendValueToPlugin('propertyInspectorConnected', 'property_inspector'); +} + +function websocketOnMessage(evt) { + // Received message from Stream Deck + var jsonObj = JSON.parse(evt.data); + + if (jsonObj.event === 'sendToPropertyInspector') { + var payload = jsonObj.payload; + loadConfiguration(payload); + } + else if (jsonObj.event === 'didReceiveSettings') { + var payload = jsonObj.payload; + loadConfiguration(payload.settings); + } + else { + console.log("Unhandled websocketOnMessage: " + jsonObj.event); + } +} + +function loadConfiguration(payload) { + console.log('loadConfiguration'); + console.log(payload); + for (var key in payload) { + try { + var elem = document.getElementById(key); + if (elem.classList.contains("sdCheckbox")) { // Checkbox + elem.checked = payload[key]; + } + else if (elem.classList.contains("sdFile")) { // File + var elemFile = document.getElementById(elem.id + "Filename"); + elemFile.innerText = payload[key]; + if (!elemFile.innerText) { + elemFile.innerText = "No file..."; + } + } + else if (elem.classList.contains("sdList")) { // Dynamic dropdown + var textProperty = elem.getAttribute("sdListTextProperty"); + var valueProperty = elem.getAttribute("sdListValueProperty"); + var valueField = elem.getAttribute("sdValueField"); + + var items = payload[key]; + elem.options.length = 0; + + for (var idx = 0; idx < items.length; idx++) { + var opt = document.createElement('option'); + opt.value = items[idx][valueProperty]; + opt.text = items[idx][textProperty]; + elem.appendChild(opt); + } + elem.value = payload[valueField]; + } + else { // Normal value + elem.value = payload[key]; + } + console.log("Load: " + key + "=" + payload[key]); + } + catch (err) { + console.log("loadConfiguration failed for key: " + key + " - " + err); + } + } +} + +function setSettings() { + var payload = {}; + var elements = document.getElementsByClassName("sdProperty"); + + Array.prototype.forEach.call(elements, function (elem) { + var key = elem.id; + if (elem.classList.contains("sdCheckbox")) { // Checkbox + payload[key] = elem.checked; + } + else if (elem.classList.contains("sdFile")) { // File + var elemFile = document.getElementById(elem.id + "Filename"); + payload[key] = elem.value; + if (!elem.value) { + // Fetch innerText if file is empty (happens when we lose and regain focus to this key) + payload[key] = elemFile.innerText; + } + else { + // Set value on initial file selection + elemFile.innerText = elem.value; + } + } + else if (elem.classList.contains("sdList")) { // Dynamic dropdown + var valueField = elem.getAttribute("sdValueField"); + payload[valueField] = elem.value; + } + else { // Normal value + payload[key] = elem.value; + } + console.log("Save: " + key + "<=" + payload[key]); + }); + setSettingsToPlugin(payload); +} + +function setSettingsToPlugin(payload) { + if (websocket && (websocket.readyState === 1)) { + const json = { + 'event': 'setSettings', + 'context': uuid, + 'payload': payload + }; + websocket.send(JSON.stringify(json)); + var event = new Event('settingsUpdated'); + document.dispatchEvent(event); + } +} + +// Sends an entire payload to the sendToPlugin method +function sendPayloadToPlugin(payload) { + if (websocket && (websocket.readyState === 1)) { + const json = { + 'action': actionInfo['action'], + 'event': 'sendToPlugin', + 'context': uuid, + 'payload': payload + }; + websocket.send(JSON.stringify(json)); + } +} + +// Sends one value to the sendToPlugin method +function sendValueToPlugin(value, param) { + if (websocket && (websocket.readyState === 1)) { + const json = { + 'action': actionInfo['action'], + 'event': 'sendToPlugin', + 'context': uuid, + 'payload': { + [param]: value + } + }; + websocket.send(JSON.stringify(json)); + } +} + +function openWebsite() { + if (websocket && (websocket.readyState === 1)) { + const json = { + 'event': 'openUrl', + 'payload': { + 'url': 'https://BarRaider.github.io' + } + }; + websocket.send(JSON.stringify(json)); + } +} + +if (!isQT) { + document.addEventListener('DOMContentLoaded', function () { + initPropertyInspector(); + }); +} + +window.addEventListener('beforeunload', function (e) { + e.preventDefault(); + + // Notify the plugin we are about to leave + sendValueToPlugin('propertyInspectorWillDisappear', 'property_inspector'); + + // Don't set a returnValue to the event, otherwise Chromium with throw an error. +}); + +function initPropertyInspector() { + // Place to add functions +} + + +function addDynamicStyles(clrs) { + const node = document.getElementById('#sdpi-dynamic-styles') || document.createElement('style'); + if (!clrs.mouseDownColor) clrs.mouseDownColor = fadeColor(clrs.highlightColor, -100); + const clr = clrs.highlightColor.slice(0, 7); + const clr1 = fadeColor(clr, 100); + const clr2 = fadeColor(clr, 60); + const metersActiveColor = fadeColor(clr, -60); + + node.setAttribute('id', 'sdpi-dynamic-styles'); + node.innerHTML = ` + + input[type="radio"]:checked + label span, + input[type="checkbox"]:checked + label span { + background-color: ${clrs.highlightColor}; + } + + input[type="radio"]:active:checked + label span, + input[type="radio"]:active + label span, + input[type="checkbox"]:active:checked + label span, + input[type="checkbox"]:active + label span { + background-color: ${clrs.mouseDownColor}; + } + + input[type="radio"]:active + label span, + input[type="checkbox"]:active + label span { + background-color: ${clrs.buttonPressedBorderColor}; + } + + td.selected, + td.selected:hover, + li.selected:hover, + li.selected { + color: white; + background-color: ${clrs.highlightColor}; + } + + .sdpi-file-label > label:active, + .sdpi-file-label.file:active, + label.sdpi-file-label:active, + label.sdpi-file-info:active, + input[type="file"]::-webkit-file-upload-button:active, + button:active { + background-color: ${clrs.buttonPressedBackgroundColor}; + color: ${clrs.buttonPressedTextColor}; + border-color: ${clrs.buttonPressedBorderColor}; + } + + ::-webkit-progress-value, + meter::-webkit-meter-optimum-value { + background: linear-gradient(${clr2}, ${clr1} 20%, ${clr} 45%, ${clr} 55%, ${clr2}) + } + + ::-webkit-progress-value:active, + meter::-webkit-meter-optimum-value:active { + background: linear-gradient(${clr}, ${clr2} 20%, ${metersActiveColor} 45%, ${metersActiveColor} 55%, ${clr}) + } + `; + document.body.appendChild(node); +}; + +/** UTILITIES */ + +/* + Quick utility to lighten or darken a color (doesn't take color-drifting, etc. into account) + Usage: + fadeColor('#061261', 100); // will lighten the color + fadeColor('#200867'), -100); // will darken the color +*/ +function fadeColor(col, amt) { + const min = Math.min, max = Math.max; + const num = parseInt(col.replace(/#/g, ''), 16); + const r = min(255, max((num >> 16) + amt, 0)); + const g = min(255, max((num & 0x0000FF) + amt, 0)); + const b = min(255, max(((num >> 8) & 0x00FF) + amt, 0)); + return '#' + (g | (b << 8) | (r << 16)).toString(16).padStart(6, 0); +} diff --git a/streamdeck-gpu/gpu/gpu.csproj b/streamdeck-gpu/gpu/gpu.csproj new file mode 100644 index 0000000..e243124 --- /dev/null +++ b/streamdeck-gpu/gpu/gpu.csproj @@ -0,0 +1,120 @@ + + + Exe + net472 + false + com.nicolasluckie.gpu + AnyCPU;x64 + AnyCPU + gpu + + + Nicoals Luckie, BarRaider + 1.0.0 + com.nicolasluckie.gpu + pluginIcon@2x.png + + + bin\Release\com.nicolasluckie.gpu.sdPlugin\ + 4 + + + bin\Release\com.nicolasluckie.gpu.sdPlugin\ + 4 + + + bin\Debug\com.nicolasluckie.gpu.sdPlugin\ + 4 + + + bin\Debug\com.nicolasluckie.gpu.sdPlugin\ + 4 + + + + + + + + + + + PreserveNewest + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + + + + + + + + True + \ + + + True + \ + + + \ No newline at end of file diff --git a/streamdeck-gpu/gpu/manifest.json b/streamdeck-gpu/gpu/manifest.json new file mode 100644 index 0000000..dfd29a8 --- /dev/null +++ b/streamdeck-gpu/gpu/manifest.json @@ -0,0 +1,44 @@ +{ + "Actions": [ + { + "Icon": "Images/gpu", + "Name": "GPU", + "States": [ + { + "Image": "Images/pluginAction", + "TitleAlignment": "middle", + "FontSize": "12" + } + ], + "SupportedInMultiActions": true, + "Tooltip": "Created by Nic Luckie", + "UUID": "com.nicolasluckie.gpu.gpu", + "PropertyInspectorPath": "PropertyInspector/Sample.html" + } + ], + "Author": "Nicolas Luckie", + "Description": "Displays the current GPU temperature.", + "Name": "GPU", + "Icon": "Images/pluginIcon", + "URL": "https://nicluckie.ca/", + "Version": "1.0", + "CodePath": "com.nicolasluckie.gpu", + "Category": "GPU", + "CategoryIcon": "Images/categoryIcon", + "OS": [ + { + "Platform": "windows", + "MinimumVersion": "10" + } + ], + "SDKVersion": 2, + "Software": { + "MinimumVersion": "4.4.1" + }, + "ApplicationsToMonitor": { + "windows": [ + "notepad.exe", + "calc.exe" + ] + } +} diff --git a/streamdeck-gpu/samples.md b/streamdeck-gpu/samples.md new file mode 100644 index 0000000..50bd778 --- /dev/null +++ b/streamdeck-gpu/samples.md @@ -0,0 +1,10 @@ +## Samples of Github repositories using the StreamDeck-Tools framework + +List is sorted from basic to more complex plugins: +1. Stopwatch plugin: https://github.com/BarRaider/streamdeck-stopwatchdemo +2. Delayed Text Input plugin: https://github.com/BarRaider/streamdeck-delayedtext +3. Stock Ticker plugin: https://github.com/BarRaider/streamdeck-stockticker +4. VoiceMeeter Integration plugin: https://github.com/BarRaider/streamdeck-voicemeeter + + +**Author's website and contact information:** [https://barraider.github.io](https://barraider.github.io) diff --git a/streamdeck-gpu/streamdeck-gpu.sln b/streamdeck-gpu/streamdeck-gpu.sln new file mode 100644 index 0000000..c12a5a4 --- /dev/null +++ b/streamdeck-gpu/streamdeck-gpu.sln @@ -0,0 +1,41 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.4.33103.184 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "barraider-sdtools", "barraider-sdtools\barraider-sdtools.csproj", "{DABBD97D-6687-4CBD-A19E-AC9FFA3CEF03}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "gpu", "gpu\gpu.csproj", "{4635D874-69C0-4010-BE46-77EF92EB1553}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {DABBD97D-6687-4CBD-A19E-AC9FFA3CEF03}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DABBD97D-6687-4CBD-A19E-AC9FFA3CEF03}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DABBD97D-6687-4CBD-A19E-AC9FFA3CEF03}.Debug|x64.ActiveCfg = Debug|x64 + {DABBD97D-6687-4CBD-A19E-AC9FFA3CEF03}.Debug|x64.Build.0 = Debug|x64 + {DABBD97D-6687-4CBD-A19E-AC9FFA3CEF03}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DABBD97D-6687-4CBD-A19E-AC9FFA3CEF03}.Release|Any CPU.Build.0 = Release|Any CPU + {DABBD97D-6687-4CBD-A19E-AC9FFA3CEF03}.Release|x64.ActiveCfg = Release|x64 + {DABBD97D-6687-4CBD-A19E-AC9FFA3CEF03}.Release|x64.Build.0 = Release|x64 + {4635D874-69C0-4010-BE46-77EF92EB1553}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4635D874-69C0-4010-BE46-77EF92EB1553}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4635D874-69C0-4010-BE46-77EF92EB1553}.Debug|x64.ActiveCfg = Debug|x64 + {4635D874-69C0-4010-BE46-77EF92EB1553}.Debug|x64.Build.0 = Debug|x64 + {4635D874-69C0-4010-BE46-77EF92EB1553}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4635D874-69C0-4010-BE46-77EF92EB1553}.Release|Any CPU.Build.0 = Release|Any CPU + {4635D874-69C0-4010-BE46-77EF92EB1553}.Release|x64.ActiveCfg = Release|x64 + {4635D874-69C0-4010-BE46-77EF92EB1553}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {4FF0FD6C-650D-438B-9C5E-49C9259AE847} + EndGlobalSection +EndGlobal diff --git a/streamdeck-gpu/streamdeck-tools.xml b/streamdeck-gpu/streamdeck-tools.xml new file mode 100644 index 0000000..c0f8f89 --- /dev/null +++ b/streamdeck-gpu/streamdeck-tools.xml @@ -0,0 +1,1685 @@ + + + + StreamDeckTools + + + + + FilenamePropertyAttribute - Used to indicate the current property holds a file name. + This will allow StreamDeck Tools to strip the mandatory "C:\fakepath\" added by the SDK + + + + + PluginActionId attribute + Used to indicate the UUID in the manifest file that matches to this class + + + + + UUID of the action + + + + + Constructor - This attribute is used to indicate the UUID in the manifest file that matches to this class + + + + + + Interface for a Stream Deck connection + + + + + Event received by the plugin when the Property Inspector uses the sendToPlugin event. + + + + + Event received when the user changes the title or title parameters. + + + + + Event received when a monitored application is terminated + + + + + Event received when a monitored application is launched + + + + + Event received when a device is unplugged from the computer + + + + + Event received when a device is plugged to the computer. + + + + + Event received when the Property Inspector appears in the Stream Deck software user interface, for example when selecting a new instance. + + + + + Event received when the Property Inspector for an instance is removed from the Stream Deck software user interface, for example when selecting a different instance. + + + + + Send settings to the PropertyInspector + + + + + + + Persists your plugin settings + + + + + + + Persists your global plugin settings + + Settings to save globally + Boolean whether to also trigger a didReceiveGlobalSettings event. Default is true + + + + + Persists your global plugin settings + + + + + + Sets an image on the StreamDeck key. + + Base64 encoded image + A 0-based integer value representing the state of an action with multiple states. This is an optional parameter. If not specified, the title is set to all states. + Should image be sent even if it is identical to the one sent previously. Default is false + + + + + Sets an image on the StreamDeck key + + Image object + A 0-based integer value representing the state of an action with multiple states. This is an optional parameter. If not specified, the title is set to all states. + Should image be sent even if it is identical to the one sent previously. Default is false + + + + + Sets the default image for this state, as configured in the manifest + + + + + + Sets a title on the StreamDeck key + + + A 0-based integer value representing the state of an action with multiple states. This is an optional parameter. If not specified, the title is set to all states. + + + + + Switches to one of the plugin's built-in profiles + + + + + + + Switches to one of the plugin's built-in profiles. Allows to choose which device to switch it on. + + + + + + + + Shows the Alert (Yellow Triangle) on the StreamDeck key + + + + + + Shows the Success (Green checkmark) on the StreamDeck key + + + + + + Add a message to the Stream Deck log. This is the log located at: %appdata%\Elgato\StreamDeck\logs\StreamDeck0.log + + + + + + + Gets the Stream Deck device's info + + + + + + Tells Stream Deck to return the current plugin settings via the ReceivedSettings function + + + + + + Opens a URI in the user's browser + + + + + + + Opens a URI in the user's browser + + + + + + + Sets the plugin to a specific state which is pre-configured in the manifest file + + + + + + + An opaque value identifying the plugin. This value is received during the Registration procedure + + + + + An opaque value identifying the device the plugin is launched on. + + + + + StreamDeckConnection object, initialized based on the args received when launching the program + + + + + Connection object which handles your communication with the Stream Deck app + + + + + An opaque value identifying the plugin. Received as an argument when the executable was launched. + + + + + Holds information about the devices connected to the computer + + + + + Event received by the plugin when the Property Inspector uses the sendToPlugin event. + + + + + Event received when the user changes the title or title parameters. + + + + + Event received when a monitored application is terminated + + + + + Event received when a monitored application is launched + + + + + Event received when a device is unplugged from the computer + + + + + Event received when a device is plugged to the computer. + + + + + Event received when the Property Inspector appears in the Stream Deck software user interface, for example when selecting a new instance. + + + + + Event received when the Property Inspector for an instance is removed from the Stream Deck software user interface, for example when selecting a different instance. + + + + + Send settings to the PropertyInspector + + + + + + + Persists your plugin settings + + + + + + + Persists your global plugin settings + + Settings to save globally + Boolean whether to also trigger a didReceiveGlobalSettings event. Default is true + + + + + Persists your global plugin settings + + + + + + Sets an image on the StreamDeck key. + + Base64 encoded image + A 0-based integer value representing the state of an action with multiple states. This is an optional parameter. If not specified, the title is set to all states. + Should image be sent even if it is identical to the one sent previously. Default is false + + + + + Sets an image on the StreamDeck key + + Image object + A 0-based integer value representing the state of an action with multiple states. This is an optional parameter. If not specified, the title is set to all states. + Should image be sent even if it is identical to the one sent previously. Default is false + + + + + Sets the default image for this state, as configured in the manifest + + + + + + Sets a title on the StreamDeck key + + + A 0-based integer value representing the state of an action with multiple states. This is an optional parameter. If not specified, the title is set to all states. + + + + + Switches to one of the plugin's built-in profiles + + + + + + + Switches to one of the plugin's built-in profiles. Allows to choose which device to switch it on. + + + + + + + + Shows the Alert (Yellow Triangle) on the StreamDeck key + + + + + + Shows the Success (Green checkmark) on the StreamDeck key + + + + + + Add a message to the Stream Deck log. This is the log located at: %appdata%\Elgato\StreamDeck\logs\StreamDeck0.log + + + + + + + Gets the Stream Deck device's info + + + + + + Tells Stream Deck to return the current plugin settings via the ReceivedSettings function + + + + + + Opens a URI in the user's browser + + + + + + + Opens a URI in the user's browser + + + + + + + Sets the plugin to a specific state which is pre-configured in the manifest file + + + + + + + An opaque value identifying the plugin. This value is received during the Registration procedure + + + + + An opaque value identifying the device the plugin is launched on. + + + + + StreamDeckConnection object, initialized based on the args received when launching the program + + + + + Public constructor, a StreamDeckConnection object is required along with the current action and context IDs + These will be used to correctly communicate with the StreamDeck App + + + + + + + /// + + + + Dispose (Destructor) function + + + + + * Easy Configuration Instructions: + * 1. Use NuGet to get the following packages: + * CommandLineParser by gsscoder + * streamdeck-client-csharp by Shane DeSeranno + * Newtonsoft.Json by James Newton-King + * 2. Create a class that implements the IPluginable interface (which is located in BarRaider.SDTools), this will be your main plugin + * 3. Pass the type of the class to the main function + + + + /************************************************************************ + * Initial configuration from TyrenDe's streamdeck-client-csharp example: + * https://github.com/TyrenDe/streamdeck-client-csharp + * and SaviorXTanren's MixItUp.StreamDeckPlugin: + * https://github.com/SaviorXTanren/mixer-mixitup/ + *************************************************************************/ + + Obsolete! Use the new Run(string[]) overload + Library's main initialization point. + Pass the args from your Main function and a list of supported PluginActionIds, the framework will handle the rest. + + + + + + + Library's main initialization point. + Pass the args from your Main function. We'll handle the rest + + + + + + Payload for ApplicationDidLaunch event + + + + + Payload + + + + + Constructor + + + + + + Payload for ApplicationDidTerminate event + + + + + Payload + + + + + Constructor + + + + + + Payload for DeviceDidConnect event + + + + + Device GUID + + + + + Device Info + + + + + Constructor + + + + + + Payload for DeviceDidDisconnect event + + + + + Device GUID + + + + + Constructor + + + + + + Payload for PropertyInspectorDidAppear event + + + + + ActionId + + + + + ContextId + + + + + Device Guid + + + + + Constructor + + + + + + + + Payload for PropertyInspectorDidDisappear event + + + + + Action Id + + + + + ContextId + + + + + Device Guid + + + + + Constructor + + + + + + + + Payload for SendToPlugin event + + + + + ActionId + + + + + ContextId + + + + + Payload + + + + + Constructor + + + + + + + + Payload for TitleParametersDidChange event + + + + + Action Id + + + + + Context Id + + + + + Device Guid + + + + + Payload + + + + + Constructor + + + + + + + + + ApplicationPayload + + + + + Application Name + + + + + Constructor + + + + + + Payload for TitleParametersDidChange Event + + + + + Settings JSON Object + + + + + Key Coordinates + + + + + Key State + + + + + Title + + + + + Title Parameters + + + + + Constructor + + + + + + + + + + Payload received during the plugin's constructor + + + + + Plugin instance's settings (set through Property Inspector) + + + + + Plugin's physical location on the Stream Deck device + + + + + Current plugin state + + + + + Is it in a Multiaction + + + + + Information regarding the Stream Deck hardware device + + + + + Constructor + + + + + + + + + + Payload received when a key is pressed or released + + + + + Current plugin settings + + + + + Location of plugin on the stream deck + + + + + Current plugin state + + + + + Desired state + + + + + Is part of a multiAction + + + + + Constructor + + + + + + + + + + Payload that holds all the settings in the ReceivedGlobalSettings event + + + + + Global settings object + + + + + Payload that holds all the settings in the ReceivedSettings event + + + + + Action's settings + + + + + Coordinates of the key pressed + + + + + Is event part of a multiaction + + + + + Class holding all the information passed to the plugin when the program was launched + + + + + Port to communicate with the StreamDeck app + + + + + UUID of the plugin + + + + + Name of the event we should pass to the StreamDeck app to register + + + + + Raw information in JSON format which we will parse into the DeviceInfo property + + + + + Information regarding the StreamDeck app and StreamDeck hardware which was parsed from the RawInfo JSON field. + + + + + Holds general information on the StreamDeck App we're communicating with + + + + + Current language of the StreamDeck app + + + + + OS Platform + + + + + Current version of the StreamDeck app + + + + + Shows class information as string + + + + + + Type of StreamDeck hardware device, currently two are supported (classic and mini) + + + + + StreamDeck classic with 15 keys + + + + + StreamDeck mini with 6 keys + + + + + StreamDeck XL with 32 keys + + + + + StreamDeck Mobile version + + + + + Corsair G-Keys version + + + + + Class which holds information on the StreamDeck hardware device + + + + + Details on number of keys of the StreamDeck hardware device + + + + + Type of StreamDeck hardware device + + + + + Id of the StreamDeck hardware device + + + + + Constructor + + + + + + + + Shows class information as string + + + + + + Layout of the keys on the StreamDeck hardware device + + + + + Number of key rows on the StreamDeck hardware device + + + + + Number of key columns on the StreamDeck hardware device + + + + + Constructor + + + + + + + Shows class information as string + + + + + + Class which holds information on the StreamDeck app and StreamDeck hardware device that the plugin is communicating with + + + + + Information on the StreamDeck App which we're communicating with + + + + + Information on the StreamDeck hardware device that the plugin is running on + + + + + Information on the Plugin we're currently running + + + + + Device pixel ratio + + + + + Shows class information as string + + + + + + Holds general information on the StreamDeck App we're communicating with + + + + + Current version of the plugin + + + + + Shows class information as string + + + + + + Extension methods for various objects + + + + + Checks if too KeyCoordinates match to the same key + + + + + + + + Converts to a SDTools.KeyCoordinates + + + + + + + Converts to a SDTools StreamDeckDeviceInfo object + + + + + + + + Converts to an SDTools TitleParameters + + + + + + + Shows Color In Hex Format + + + + + + + Shows Color in Hex format + + + + + + + Converts an Image into a Byte Array + + + + + + + Draws a string on a Graphics object and returns the ending Y position of the string + + + + + + + + + + + Returns the center X position of a string, given the image's max Width and Font information + + + + + + /// True/False - Does text fit image? False if text overflows + + + + + + + Returns the center X position of a string, given the image's max Width and Font information + + + + + + + + + + + + Returns the highest size of the given font in which the text fits the image + + + + + + /// + + + + + Adds a text path to an existing Graphics object. Uses TitleParameters to emulate the Text settings in the Property Inspector + + + + + + + + + + + Adds a text path to an existing Graphics object. Uses TitleParameters to emulate the Text settings in the Property Inspector + + + + + + + + + + + + + /// Truncates a string to the first maxSize characters. If maxSize is less than string length, original string will be returned + + String + Max size for string + + + + + Adds line breaks (\n) to the text to make sure it fits the key when using SetTitleAsync() + + + + + + + + + + + Helper class which allows fetching the GlobalSettings of a plugin + + + + + Returns singelton entry of GlobalSettingsManager + + + + + Event triggered when Global Settings are received + + + + + Command to request the Global Settings. Use the OnDidReceiveGlobalSSettings callback function to receive the Global Settings. + + + + + + Sets the Global Settings for the plugin + + + + + + + + Library of tools used to manipulate graphics + + + + + Return a Color object based on the hex value + + + + + + + Generates multiple shades based on an initial color, and number of stages/shades you want + + + + + + + + + Resizes an image while scaling + + + + + + + + + Extract a part of an Image + + + + + + + + + + + Creates a new image with different opacity + + + + + + + + Generates one (or more) images where each one has a few letters drawn on them based on the parameters. You can set number of letters and number of lines per key. + Use expandToNextImage to decide if you want only one Image returned or multiple if text is too long for one key + + + + + + + + + + + + + + + Adds line breaks ('\n') to the string every time the text would overflow the image + + + + + + + + + + + Tracing levels used for Logger + + + + + Debug level + + + + + Informational level + + + + + Warning level + + + + + Error level + + + + + Fatal (highest) level + + + + + Log4Net logger helper class + + + + + Returns singelton entry of Log4Net logger + + + + + Add message to log with a specific severity level. + + + + + + + Main abstract class your plugin should derive from + Holds implementation for all the basic functions + If you're missing an event, you can register to it from the Connection.StreamDeckConnection object + + + + + Called when a Stream Deck key is pressed + + + + + Called when a Stream Deck key is released + + + + + Called when the PropertyInspector has new settings + + + + + + Called when GetGlobalSettings is called. + + + + + + Called every second + Logic for displaying title/image can go here + + + + + Abstract method Called when the plugin is disposed + + + + + Main iDisposable Dispose function + + + + + Connection object which handles your communication with the Stream Deck app + + + + + Constructor for PluginBase. Receives the communication and plugin settings + Note that the settings object is not used by the base and should be consumed by the deriving class. + Usually, a private class inside the deriving class is created which stores the settings + Example for settings usage: + * if (payload.Settings == null || payload.Settings.Count == 0) + * { + * // Create default settings + * } + * else + * { + this.settings = payload.Settings.ToObject(); + * } + + + Communication module with Stream Deck + Plugin settings - NOTE: Not used in base class, should be consumed by deriving class + + + + Helper class for generating random numbers + + + + + Returns a non-negative random integer that is less than the specified maximum. + + + + + + + Returns a random integer that is within a specified range. Value will be less than the specified maximum. + + + + + + + + Set of common utilities used by various plugins + Currently the class mostly focuses on image-related functions that will be passed to the StreamDeck key + + + + + Convert an image file to Base64 format. Set the addHeaderPrefix to true, if this is sent to the SendImageAsync function + + + + + + + + Convert a in-memory image object to Base64 format. Set the addHeaderPrefix to true, if this is sent to the SendImageAsync function + + + + + + + + Convert a base64 image string to an Image object + + + + + + + Gets the key default height in pixels. + To get the StreamDeckType use Connection.DeviceInfo() + + + + + + + Gets the key default width in pixels. + To get the StreamDeckType use Connection.DeviceInfo() + + + + + + + Generates an empty key bitmap with the default height and width. + New: To get the StreamDeckType use Connection.DeviceInfo() + + + + + + + + Creates a key image that fits all Stream Decks + + + + + + + Creates a key image based on given height and width + + + + + + + + + Deprecated! Use AddTextPath on the Graphics extension method instead. + Adds a text path to an existing Graphics object. Uses TitleParser to emulate the Text settings in the Property Inspector + + + + + + + + + + + Extracts the actual filename from a file payload received from the Property Inspector + + + + + + + Converts a long to a human-readable string. Example: 54,265 => 54.27k + + + + + + Size in bytes + Formatted human-readable string (ex. "2 MB") + + + + OBSOLETE - Use String.SplitToFitKey() from SdTools.ExtensionMethods + + + + + + + + + + + Returns SHA512 Hash from an image object + + + + + + + Returns SHA512 Hash from a string + + + + + + + Returns SHA512 Hash from a byte stream + + + + + + + Iterates through the fromJObject, finds the property that matches in the toSettings object, and sets the value from the fromJObject object + + + + + Number of properties updated + + + + Uses the PluginActionId attribute on the various classes derived from PluginBase to find all the actions supported in this assembly + + + + + + Coordinates of the current key + + + + + Column of the current key + + + + + Row of the current key + + + + + This class associates a plugin UUID (which is indicated in the Manifest file), with the type of the implementation class. + The implementation class must be derived from the PluginBase class for this to work properly. + If the type passed does not derrive from PluginBase, a NotSupportedException will be thrown + + + + + Action UUID as indicated in the manifest file + + + + + Type of class that implemented this action. Must inherit PluginBase + + + + + PluginActionId constructor + + actionId is the UUID from the manifest file + Type of class that implemented this action. Must inherit PluginBase + + + + Base (Generic) EventArgs used for events + + + + + + Event Information + + + + + Enum for the alignment of the Title text on the key + + + + + Top Alignment + + + + + Middle/Center Alignment + + + + + Bottom Alignment + + + + + Class holding all the Title Information set by a user in the Property Inspector + + + + + Title Color + + + + + Font Size in Points + + + + + Font Size in Pixels + + + + + Font Size Scaled to Image + + + + + Font Family + + + + + Font Style + + + + + Should Title be shown + + + + + Alignment position of the Title text on the key + + + + + Constructor + + + + + + + + + + + Constructor + + + + + + + + + + + diff --git a/usage.gif b/usage.gif new file mode 100644 index 0000000..60bcb43 Binary files /dev/null and b/usage.gif differ