From 0fa6afde7182c32919342bcf32a87403dd3d3f8e Mon Sep 17 00:00:00 2001 From: mphillippi7804 <62009085+mphillippi7804@users.noreply.github.com> Date: Fri, 16 Oct 2026 07:03:00 -0500 Subject: [PATCH 1/3] Create Multiline Text Panel.htpl Adds a text box that is capable of handling multiple lines of input such as a CDNU text box or the F16 Text Box. It has been tested --- .../Text Displays/Multiline Text Panel.htpl | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 Helios/Templates/Base/Text Displays/Multiline Text Panel.htpl diff --git a/Helios/Templates/Base/Text Displays/Multiline Text Panel.htpl b/Helios/Templates/Base/Text Displays/Multiline Text Panel.htpl new file mode 100644 index 000000000..0820194c8 --- /dev/null +++ b/Helios/Templates/Base/Text Displays/Multiline Text Panel.htpl @@ -0,0 +1,32 @@ + +Multiline Text Panel +Text Displays +Helios.Base.TextDisplay + + From 55fca333cf860af33701a34b9acab13205336826 Mon Sep 17 00:00:00 2001 From: mphillippi7804 <62009085+mphillippi7804@users.noreply.github.com> Date: Fri, 16 Oct 2026 07:17:45 -0500 Subject: [PATCH 2/3] Update Interfaces --- InterfaceFiles/Interfaces | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InterfaceFiles/Interfaces b/InterfaceFiles/Interfaces index 32d26e9bf..08dbaf305 160000 --- a/InterfaceFiles/Interfaces +++ b/InterfaceFiles/Interfaces @@ -1 +1 @@ -Subproject commit 32d26e9bf846881fa696cf802e789dab0d5f27d7 +Subproject commit 08dbaf305c68684ecb1bc4b55ed3f4f59ab078b8 From 2f9b2ec3b8c7a717f798a8f727c5f0d48661e969 Mon Sep 17 00:00:00 2001 From: mphillippi7804 <62009085+mphillippi7804@users.noreply.github.com> Date: Fri, 16 Oct 2026 07:37:39 -0500 Subject: [PATCH 3/3] Recommended Device addition The dual potentiometer will add to the general capabilities for profile building. Originally designed to be the solution to the F-16 IAS indicator dragging the Mach gauge with it, I had it adjusted for other stacked knob functions such as TACAN Controls, brightness controls on the display settings. It has the ability to scale the size of the knobs independently, and control which drags which if any. It also allows for simple selection of device rotation for reversing. --- Helios/Controls/DualPotentiometer.cs | 706 ++++++++++++++++++ .../DualPotentiometerAppearanceEditor.xaml | 77 ++ .../DualPotentiometerAppearanceEditor.xaml.cs | 73 ++ .../DualPotentiometerBehaviorEditor.xaml | 89 +++ .../DualPotentiometerBehaviorEditor.xaml.cs | 33 + Helios/Controls/DualPotentiometerRenderer.cs | 97 +++ 6 files changed, 1075 insertions(+) create mode 100644 Helios/Controls/DualPotentiometer.cs create mode 100644 Helios/Controls/DualPotentiometerAppearanceEditor.xaml create mode 100644 Helios/Controls/DualPotentiometerAppearanceEditor.xaml.cs create mode 100644 Helios/Controls/DualPotentiometerBehaviorEditor.xaml create mode 100644 Helios/Controls/DualPotentiometerBehaviorEditor.xaml.cs create mode 100644 Helios/Controls/DualPotentiometerRenderer.cs diff --git a/Helios/Controls/DualPotentiometer.cs b/Helios/Controls/DualPotentiometer.cs new file mode 100644 index 000000000..a5fa46997 --- /dev/null +++ b/Helios/Controls/DualPotentiometer.cs @@ -0,0 +1,706 @@ +// Copyright 2014 Craig Courtney +// Copyright 2026 Helios Contributors +// +// Helios is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Helios is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +using System; +using System.ComponentModel; +using System.Globalization; +using System.Windows; +using System.Windows.Input; +using System.Windows.Media; +using System.Xml; +using GadrocsWorkshop.Helios.ComponentModel; +using GadrocsWorkshop.Helios.Controls.Capabilities; +using GadrocsWorkshop.Helios.Windows.Controls; + +namespace GadrocsWorkshop.Helios.Controls +{ + /// + /// which potentiometer of a DualPotentiometer, if any, is carried around by the other one + /// + /// WARNING: these value names must never be changed, as they are serialized to Profile XML + /// + public enum DualPotentiometerFollowMode + { + [Description("Pot 2 follows Pot 1")] + Potentiometer2FollowsPotentiometer1, + + [Description("Pot 1 follows Pot 2")] + Potentiometer1FollowsPotentiometer2, + + [Description("Independent")] + None + } + + /// + /// Two concentric rotating images, each driven by its own input value in the same way + /// a potentiometer is (min value, max value, initial rotation, rotation travel). + /// + /// Potentiometer 1 is shown to the user as "Pot 1 (Lower)" and Potentiometer 2 as "Pot 2 (Upper)". + /// Either one can be carried around by the other while its own value turns it in either direction + /// on top of that (for example the F-16 airspeed pointer and the Mach dial that rides with it). + /// + /// The mouse wheel turns Potentiometer 2 while the pointer is over its image, and Potentiometer 1 + /// anywhere else on the control. + /// + [HeliosControl("Helios.Base.DualPotentiometer", "Dual Potentiometer", "Potentiometers", typeof(DualPotentiometerRenderer))] + public class DualPotentiometer : HeliosVisual, IConfigurableImageLocation, IRefreshableImage + { + private const string DefaultPotentiometer1Name = "pot 1"; + private const string DefaultPotentiometer2Name = "pot 2"; + + private bool _mouseWheelAction = true; + + private string _potentiometer1Name = DefaultPotentiometer1Name; + private string _potentiometer1Image = "{Helios}/Images/Knobs/knob1.png"; + private double _potentiometer1InitialValue; + private double _potentiometer1StepValue = 0.1d; + private double _potentiometer1MinValue; + private double _potentiometer1MaxValue = 1d; + private double _potentiometer1InitialRotation; + private double _potentiometer1RotationTravel = 360d; + private bool _potentiometer1CounterClockwise; + private double _potentiometer1Scale = 1d; + + private string _potentiometer2Name = DefaultPotentiometer2Name; + private string _potentiometer2Image = "{Helios}/Images/Knobs/knob1.png"; + private double _potentiometer2InitialValue; + private double _potentiometer2StepValue = 0.1d; + private double _potentiometer2MinValue; + private double _potentiometer2MaxValue = 1d; + private double _potentiometer2InitialRotation; + private double _potentiometer2RotationTravel = 360d; + private bool _potentiometer2CounterClockwise = true; + private double _potentiometer2Scale = 0.5d; + + private DualPotentiometerFollowMode _followMode = DualPotentiometerFollowMode.Potentiometer2FollowsPotentiometer1; + private bool _potentiometer2OnTop = true; + + private double _potentiometer1Rotation; + private double _potentiometer2Rotation; + + private readonly HeliosValue _potentiometer1Value; + private readonly HeliosValue _potentiometer2Value; + + public DualPotentiometer() + : base("Dual Potentiometer", new Size(200, 200)) + { + _potentiometer1Value = new HeliosValue(this, new BindingValue(0d), "", "potentiometer 1 value", "potentiometer 1 value", + "Current value of pot 1 (the lower pot).", "", BindingValueUnits.Numeric); + _potentiometer1Value.Execute += Potentiometer1Value_Execute; + Values.Add(_potentiometer1Value); + Actions.Add(_potentiometer1Value); + Triggers.Add(_potentiometer1Value); + + _potentiometer2Value = new HeliosValue(this, new BindingValue(0d), "", "potentiometer 2 value", "potentiometer 2 value", + "Current value of pot 2 (the upper pot).", "", BindingValueUnits.Numeric); + _potentiometer2Value.Execute += Potentiometer2Value_Execute; + Values.Add(_potentiometer2Value); + Actions.Add(_potentiometer2Value); + Triggers.Add(_potentiometer2Value); + + UpdateValueNames(); + + UpdateRotations(); + } + + #region Potentiometer 1 (lower) properties + + /// + /// Name of potentiometer 1 as it is shown in the bindings lists. Changing it does not break existing bindings. + /// + public string Potentiometer1Name + { + get => _potentiometer1Name; + set + { + string newValue = string.IsNullOrWhiteSpace(value) ? DefaultPotentiometer1Name : value.Trim(); + if (_potentiometer1Name.Equals(newValue)) + { + return; + } + string oldValue = _potentiometer1Name; + _potentiometer1Name = newValue; + UpdateValueNames(); + OnPropertyChanged(nameof(Potentiometer1Name), oldValue, newValue, true); + } + } + + public string Potentiometer1Image + { + get => _potentiometer1Image; + set => SetImage(ref _potentiometer1Image, value, nameof(Potentiometer1Image)); + } + + public double Potentiometer1InitialValue + { + get => _potentiometer1InitialValue; + set => SetConfig(ref _potentiometer1InitialValue, value, nameof(Potentiometer1InitialValue)); + } + + /// + /// Amount the value of this pot changes for each click of the mouse wheel + /// + public double Potentiometer1StepValue + { + get => _potentiometer1StepValue; + set => SetConfig(ref _potentiometer1StepValue, value, nameof(Potentiometer1StepValue)); + } + + public double Potentiometer1MinValue + { + get => _potentiometer1MinValue; + set => SetConfig(ref _potentiometer1MinValue, value, nameof(Potentiometer1MinValue)); + } + + public double Potentiometer1MaxValue + { + get => _potentiometer1MaxValue; + set => SetConfig(ref _potentiometer1MaxValue, value, nameof(Potentiometer1MaxValue)); + } + + /// + /// Rotation in degrees of potentiometer 1 when both pots are at their minimum value + /// + public double Potentiometer1InitialRotation + { + get => _potentiometer1InitialRotation; + set => SetConfig(ref _potentiometer1InitialRotation, value, nameof(Potentiometer1InitialRotation)); + } + + /// + /// Degrees potentiometer 1 turns between its minimum and maximum value + /// + public double Potentiometer1RotationTravel + { + get => _potentiometer1RotationTravel; + set => SetConfig(ref _potentiometer1RotationTravel, value, nameof(Potentiometer1RotationTravel)); + } + + public bool Potentiometer1CounterClockwise + { + get => _potentiometer1CounterClockwise; + set => SetConfig(ref _potentiometer1CounterClockwise, value, nameof(Potentiometer1CounterClockwise)); + } + + /// + /// Size of the potentiometer 1 image relative to the control (1 = fills the control) + /// + public double Potentiometer1Scale + { + get => _potentiometer1Scale; + set + { + if (SetConfig(ref _potentiometer1Scale, value, nameof(Potentiometer1Scale))) + { + Refresh(); + } + } + } + + /// + /// UI access to the current value of potentiometer 1; writes do not create Undo events + /// + public double Potentiometer1Value + { + get => _potentiometer1Value.Value.DoubleValue; + set + { + double oldValue = _potentiometer1Value.Value.DoubleValue; + if (oldValue.Equals(value)) + { + return; + } + _potentiometer1Value.SetValue(new BindingValue(value), BypassTriggers); + OnPropertyChanged(nameof(Potentiometer1Value), oldValue, value, false); + UpdateRotations(); + } + } + + #endregion + + #region Potentiometer 2 (upper) properties + + /// + /// Name of potentiometer 2 as it is shown in the bindings lists. Changing it does not break existing bindings. + /// + public string Potentiometer2Name + { + get => _potentiometer2Name; + set + { + string newValue = string.IsNullOrWhiteSpace(value) ? DefaultPotentiometer2Name : value.Trim(); + if (_potentiometer2Name.Equals(newValue)) + { + return; + } + string oldValue = _potentiometer2Name; + _potentiometer2Name = newValue; + UpdateValueNames(); + OnPropertyChanged(nameof(Potentiometer2Name), oldValue, newValue, true); + } + } + + public string Potentiometer2Image + { + get => _potentiometer2Image; + set => SetImage(ref _potentiometer2Image, value, nameof(Potentiometer2Image)); + } + + public double Potentiometer2InitialValue + { + get => _potentiometer2InitialValue; + set => SetConfig(ref _potentiometer2InitialValue, value, nameof(Potentiometer2InitialValue)); + } + + /// + /// Amount the value of this pot changes for each click of the mouse wheel + /// + public double Potentiometer2StepValue + { + get => _potentiometer2StepValue; + set => SetConfig(ref _potentiometer2StepValue, value, nameof(Potentiometer2StepValue)); + } + + public double Potentiometer2MinValue + { + get => _potentiometer2MinValue; + set => SetConfig(ref _potentiometer2MinValue, value, nameof(Potentiometer2MinValue)); + } + + public double Potentiometer2MaxValue + { + get => _potentiometer2MaxValue; + set => SetConfig(ref _potentiometer2MaxValue, value, nameof(Potentiometer2MaxValue)); + } + + /// + /// Rotation in degrees of potentiometer 2 when both pots are at their minimum value + /// + public double Potentiometer2InitialRotation + { + get => _potentiometer2InitialRotation; + set => SetConfig(ref _potentiometer2InitialRotation, value, nameof(Potentiometer2InitialRotation)); + } + + /// + /// Degrees potentiometer 2 turns, relative to whatever is carrying it, between its minimum and maximum value + /// + public double Potentiometer2RotationTravel + { + get => _potentiometer2RotationTravel; + set => SetConfig(ref _potentiometer2RotationTravel, value, nameof(Potentiometer2RotationTravel)); + } + + public bool Potentiometer2CounterClockwise + { + get => _potentiometer2CounterClockwise; + set => SetConfig(ref _potentiometer2CounterClockwise, value, nameof(Potentiometer2CounterClockwise)); + } + + /// + /// Size of the potentiometer 2 image relative to the control (1 = fills the control) + /// + public double Potentiometer2Scale + { + get => _potentiometer2Scale; + set + { + if (SetConfig(ref _potentiometer2Scale, value, nameof(Potentiometer2Scale))) + { + Refresh(); + } + } + } + + /// + /// Selects which potentiometer is carried around by the other one. The following potentiometer's own value + /// turns it relative to the potentiometer it follows. + /// + public DualPotentiometerFollowMode FollowMode + { + get => _followMode; + set => SetConfig(ref _followMode, value, nameof(FollowMode)); + } + + /// + /// If true, potentiometer 2 is drawn over potentiometer 1, otherwise under it + /// + public bool Potentiometer2OnTop + { + get => _potentiometer2OnTop; + set => SetConfig(ref _potentiometer2OnTop, value, nameof(Potentiometer2OnTop)); + } + + /// + /// UI access to the current value of potentiometer 2; writes do not create Undo events + /// + public double Potentiometer2Value + { + get => _potentiometer2Value.Value.DoubleValue; + set + { + double oldValue = _potentiometer2Value.Value.DoubleValue; + if (oldValue.Equals(value)) + { + return; + } + _potentiometer2Value.SetValue(new BindingValue(value), BypassTriggers); + OnPropertyChanged(nameof(Potentiometer2Value), oldValue, value, false); + UpdateRotations(); + } + } + + #endregion + + #region Rendered state + + /// + /// Current rotation in degrees at which potentiometer 1 is drawn, including any rotation it inherits from potentiometer 2 + /// + public double Potentiometer1Rotation => _potentiometer1Rotation; + + /// + /// Current rotation in degrees at which potentiometer 2 is drawn, including any rotation it inherits from potentiometer 1 + /// + public double Potentiometer2Rotation => _potentiometer2Rotation; + + #endregion + + #region Actions + + private void Potentiometer1Value_Execute(object action, HeliosActionEventArgs e) + { + _potentiometer1Value.SetValue(e.Value, e.BypassCascadingTriggers); + UpdateRotations(); + } + + private void Potentiometer2Value_Execute(object action, HeliosActionEventArgs e) + { + _potentiometer2Value.SetValue(e.Value, e.BypassCascadingTriggers); + UpdateRotations(); + } + + #endregion + + /// + /// Degrees a potentiometer has turned away from its initial rotation for the given value + /// + private static double Deflection(double value, double min, double max, double travel, bool counterClockwise) + { + double span = max - min; + if (span.Equals(0d) || double.IsNaN(value)) + { + return 0d; + } + double fraction = Math.Max(0d, Math.Min(1d, (value - min) / span)); + return (counterClockwise ? -1d : 1d) * fraction * travel; + } + + private void UpdateRotations() + { + double deflection1 = Deflection(_potentiometer1Value.Value.DoubleValue, _potentiometer1MinValue, _potentiometer1MaxValue, + _potentiometer1RotationTravel, _potentiometer1CounterClockwise); + double deflection2 = Deflection(_potentiometer2Value.Value.DoubleValue, _potentiometer2MinValue, _potentiometer2MaxValue, + _potentiometer2RotationTravel, _potentiometer2CounterClockwise); + + double rotation1 = _potentiometer1InitialRotation + deflection1 + + (_followMode == DualPotentiometerFollowMode.Potentiometer1FollowsPotentiometer2 ? deflection2 : 0d); + double rotation2 = _potentiometer2InitialRotation + deflection2 + + (_followMode == DualPotentiometerFollowMode.Potentiometer2FollowsPotentiometer1 ? deflection1 : 0d); + + if (!rotation1.Equals(_potentiometer1Rotation) || !rotation2.Equals(_potentiometer2Rotation)) + { + _potentiometer1Rotation = rotation1; + _potentiometer2Rotation = rotation2; + OnDisplayUpdate(); + } + } + + /// + /// Applies the potentiometer names to the names shown for the bindable values. The identifiers used + /// to save bindings in the profile are fixed, so bindings survive a rename. + /// + private void UpdateValueNames() + { + _potentiometer1Value.Name = _potentiometer1Name + " value"; + _potentiometer1Value.RecalculateName(); + _potentiometer2Value.Name = _potentiometer2Name + " value"; + _potentiometer2Value.RecalculateName(); + } + + private bool SetConfig(ref T field, T value, string propertyName) + { + if (Equals(field, value)) + { + return false; + } + T oldValue = field; + field = value; + OnPropertyChanged(propertyName, oldValue, value, true); + UpdateRotations(); + OnDisplayUpdate(); + return true; + } + + private void SetImage(ref string field, string value, string propertyName) + { + if (string.Equals(field, value)) + { + return; + } + string oldValue = field; + field = value; + OnPropertyChanged(propertyName, oldValue, value, true); + Refresh(); + } + + #region IConfigurableImageLocation + + public void ReplaceImageNames(string oldName, string newName) + { + Potentiometer1Image = ReplaceImageName(Potentiometer1Image, oldName, newName); + Potentiometer2Image = ReplaceImageName(Potentiometer2Image, oldName, newName); + } + + private static string ReplaceImageName(string image, string oldName, string newName) + { + return string.IsNullOrEmpty(image) ? image : string.IsNullOrEmpty(oldName) ? newName + image : image.Replace(oldName, newName); + } + + #endregion + + #region IRefreshableImage + + public override bool ConditionalImageRefresh(string imageName) + { + if ((Potentiometer1Image ?? "").ToLower().Replace("/", @"\") == imageName || + (Potentiometer2Image ?? "").ToLower().Replace("/", @"\") == imageName) + { + ImageRefresh = true; + Refresh(); + } + return ImageRefresh; + } + + #endregion + + public override void Reset() + { + base.Reset(); + + BeginTriggerBypass(true); + Potentiometer1Value = Potentiometer1InitialValue; + Potentiometer2Value = Potentiometer2InitialValue; + EndTriggerBypass(true); + UpdateRotations(); + } + + #region Mouse wheel + + /// + /// If true, the mouse wheel turns Pot 2 while the pointer is over the Pot 2 image and Pot 1 anywhere else + /// + public bool MouseWheelAction + { + get => _mouseWheelAction; + set => SetConfig(ref _mouseWheelAction, value, nameof(MouseWheelAction)); + } + + public override bool CanConsumeMouseWheel => _mouseWheelAction; + + public override void MouseWheel(int delta) + { + if (!_mouseWheelAction || delta == 0) + { + return; + } + + // NOTE: ignoring mouse wheel speed because unit is unclear, same as the other rotary controls + int pulses = delta < 1 ? -1 : 1; + if (IsPointerOverPotentiometer2()) + { + Potentiometer2Value = Stepped(Potentiometer2Value, pulses * _potentiometer2StepValue, _potentiometer2MinValue, _potentiometer2MaxValue); + } + else + { + Potentiometer1Value = Stepped(Potentiometer1Value, pulses * _potentiometer1StepValue, _potentiometer1MinValue, _potentiometer1MaxValue); + } + } + + private static double Stepped(double value, double change, double min, double max) + { + double low = Math.Min(min, max); + double high = Math.Max(min, max); + return Math.Round(Math.Max(low, Math.Min(high, value + change)), 5); + } + + /// + /// The mouse wheel notification does not include a location, so we ask WPF where the pointer is + /// relative to the view that is displaying this control, and then test that against the + /// area covered by the Pot 2 image. + /// + private bool IsPointerOverPotentiometer2() + { + DependencyObject element = Mouse.DirectlyOver as DependencyObject; + while (element != null) + { + if (element is HeliosVisualView view && ReferenceEquals(view.Visual, this)) + { + return IsOverPotentiometer2(Mouse.GetPosition(view)); + } + element = element is Visual || element is System.Windows.Media.Media3D.Visual3D + ? VisualTreeHelper.GetParent(element) + : null; + } + + // pointer location is not available, so treat it as being outside of Pot 2 + return false; + } + + /// location relative to the upper left corner of this control + /// true if the location is inside the ellipse covered by the Pot 2 image + private bool IsOverPotentiometer2(Point location) + { + double radiusX = Width * _potentiometer2Scale / 2d; + double radiusY = Height * _potentiometer2Scale / 2d; + if (radiusX <= 0d || radiusY <= 0d) + { + return false; + } + double x = (location.X - Width / 2d) / radiusX; + double y = (location.Y - Height / 2d) / radiusY; + return x * x + y * y <= 1d; + } + + #endregion + + // this control does not react to touch or mouse buttons + + public override void MouseDown(Point location) + { + // no code + } + + public override void MouseDrag(Point location) + { + // no code + } + + public override void MouseUp(Point location) + { + // no code + } + + #region Persistence + + public override void WriteXml(XmlWriter writer) + { + base.WriteXml(writer); + writer.WriteStartElement("Potentiometer1"); + WritePotentiometer(writer, Potentiometer1Name, Potentiometer1Image, Potentiometer1InitialValue, Potentiometer1MinValue, Potentiometer1MaxValue, + Potentiometer1InitialRotation, Potentiometer1RotationTravel, Potentiometer1CounterClockwise, Potentiometer1Scale, Potentiometer1StepValue); + writer.WriteEndElement(); + writer.WriteStartElement("Potentiometer2"); + WritePotentiometer(writer, Potentiometer2Name, Potentiometer2Image, Potentiometer2InitialValue, Potentiometer2MinValue, Potentiometer2MaxValue, + Potentiometer2InitialRotation, Potentiometer2RotationTravel, Potentiometer2CounterClockwise, Potentiometer2Scale, Potentiometer2StepValue); + writer.WriteElementString("OnTop", Potentiometer2OnTop.ToString(CultureInfo.InvariantCulture)); + writer.WriteEndElement(); + writer.WriteElementString("FollowMode", FollowMode.ToString()); + writer.WriteElementString("MouseWheel", MouseWheelAction.ToString(CultureInfo.InvariantCulture)); + } + + private static void WritePotentiometer(XmlWriter writer, string name, string image, double initialValue, double minValue, + double maxValue, double initialRotation, double rotationTravel, bool counterClockwise, double scale, double stepValue) + { + writer.WriteElementString("Name", name); + writer.WriteElementString("Image", image ?? ""); + writer.WriteElementString("InitialValue", initialValue.ToString(CultureInfo.InvariantCulture)); + writer.WriteElementString("MinValue", minValue.ToString(CultureInfo.InvariantCulture)); + writer.WriteElementString("MaxValue", maxValue.ToString(CultureInfo.InvariantCulture)); + writer.WriteElementString("InitialRotation", initialRotation.ToString(CultureInfo.InvariantCulture)); + writer.WriteElementString("RotationTravel", rotationTravel.ToString(CultureInfo.InvariantCulture)); + writer.WriteElementString("CounterClockwise", counterClockwise.ToString(CultureInfo.InvariantCulture)); + writer.WriteElementString("Scale", scale.ToString(CultureInfo.InvariantCulture)); + writer.WriteElementString("StepValue", stepValue.ToString(CultureInfo.InvariantCulture)); + } + + public override void ReadXml(XmlReader reader) + { + base.ReadXml(reader); + TypeConverter bc = TypeDescriptor.GetConverter(typeof(bool)); + + reader.ReadStartElement("Potentiometer1"); + if (reader.Name.Equals("Name")) + { + Potentiometer1Name = reader.ReadElementString("Name"); + } + Potentiometer1Image = reader.ReadElementString("Image"); + Potentiometer1InitialValue = ReadDouble(reader, "InitialValue"); + Potentiometer1MinValue = ReadDouble(reader, "MinValue"); + Potentiometer1MaxValue = ReadDouble(reader, "MaxValue"); + Potentiometer1InitialRotation = ReadDouble(reader, "InitialRotation"); + Potentiometer1RotationTravel = ReadDouble(reader, "RotationTravel"); + Potentiometer1CounterClockwise = (bool)bc.ConvertFromInvariantString(reader.ReadElementString("CounterClockwise")); + Potentiometer1Scale = ReadDouble(reader, "Scale"); + if (reader.Name.Equals("StepValue")) + { + Potentiometer1StepValue = ReadDouble(reader, "StepValue"); + } + reader.ReadEndElement(); + + reader.ReadStartElement("Potentiometer2"); + if (reader.Name.Equals("Name")) + { + Potentiometer2Name = reader.ReadElementString("Name"); + } + Potentiometer2Image = reader.ReadElementString("Image"); + Potentiometer2InitialValue = ReadDouble(reader, "InitialValue"); + Potentiometer2MinValue = ReadDouble(reader, "MinValue"); + Potentiometer2MaxValue = ReadDouble(reader, "MaxValue"); + Potentiometer2InitialRotation = ReadDouble(reader, "InitialRotation"); + Potentiometer2RotationTravel = ReadDouble(reader, "RotationTravel"); + Potentiometer2CounterClockwise = (bool)bc.ConvertFromInvariantString(reader.ReadElementString("CounterClockwise")); + Potentiometer2Scale = ReadDouble(reader, "Scale"); + if (reader.Name.Equals("StepValue")) + { + Potentiometer2StepValue = ReadDouble(reader, "StepValue"); + } + Potentiometer2OnTop = (bool)bc.ConvertFromInvariantString(reader.ReadElementString("OnTop")); + reader.ReadEndElement(); + if (reader.Name.Equals("FollowMode")) + { + FollowMode = (DualPotentiometerFollowMode)Enum.Parse(typeof(DualPotentiometerFollowMode), reader.ReadElementString("FollowMode")); + } + if (reader.Name.Equals("MouseWheel")) + { + MouseWheelAction = (bool)bc.ConvertFromInvariantString(reader.ReadElementString("MouseWheel")); + } + + BeginTriggerBypass(true); + Potentiometer1Value = Potentiometer1InitialValue; + Potentiometer2Value = Potentiometer2InitialValue; + EndTriggerBypass(true); + UpdateRotations(); + } + + private static double ReadDouble(XmlReader reader, string elementName) + { + return double.Parse(reader.ReadElementString(elementName), CultureInfo.InvariantCulture); + } + + #endregion + } +} diff --git a/Helios/Controls/DualPotentiometerAppearanceEditor.xaml b/Helios/Controls/DualPotentiometerAppearanceEditor.xaml new file mode 100644 index 000000000..f948452a6 --- /dev/null +++ b/Helios/Controls/DualPotentiometerAppearanceEditor.xaml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + Pot 1 (Lower) + + + + + + + + + + + Pot 2 (Upper) + + + + + + + + + + + + + + diff --git a/Helios/Controls/DualPotentiometerAppearanceEditor.xaml.cs b/Helios/Controls/DualPotentiometerAppearanceEditor.xaml.cs new file mode 100644 index 000000000..551d79a11 --- /dev/null +++ b/Helios/Controls/DualPotentiometerAppearanceEditor.xaml.cs @@ -0,0 +1,73 @@ +// Copyright 2014 Craig Courtney +// Copyright 2026 Helios Contributors +// +// Helios is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Helios is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +namespace GadrocsWorkshop.Helios.Controls +{ + using GadrocsWorkshop.Helios.ComponentModel; + using GadrocsWorkshop.Helios.Windows.Controls; + using System.Windows; + + /// + /// Interaction logic for DualPotentiometerAppearanceEditor.xaml + /// + [HeliosPropertyEditor("Helios.Base.DualPotentiometer", "Appearance")] + public partial class DualPotentiometerAppearanceEditor : HeliosPropertyEditor + { + public DualPotentiometerAppearanceEditor() + { + InitializeComponent(); + } + + // while a rotation slider is being adjusted, move the pots to the position that slider + // describes, so the effect can be seen in the editor + + private void Potentiometer1InitialRotation_GotFocus(object sender, RoutedEventArgs e) + { + if (Control is DualPotentiometer pots) + { + pots.Potentiometer1Value = pots.Potentiometer1MinValue; + pots.Potentiometer2Value = pots.Potentiometer2MinValue; + } + } + + private void Potentiometer1RotationTravel_GotFocus(object sender, RoutedEventArgs e) + { + if (Control is DualPotentiometer pots) + { + pots.Potentiometer1Value = pots.Potentiometer1MaxValue; + pots.Potentiometer2Value = pots.Potentiometer2MinValue; + } + } + + private void Potentiometer2InitialRotation_GotFocus(object sender, RoutedEventArgs e) + { + if (Control is DualPotentiometer pots) + { + pots.Potentiometer1Value = pots.Potentiometer1MinValue; + pots.Potentiometer2Value = pots.Potentiometer2MinValue; + } + } + + private void Potentiometer2RotationTravel_GotFocus(object sender, RoutedEventArgs e) + { + if (Control is DualPotentiometer pots) + { + pots.Potentiometer1Value = pots.Potentiometer1MinValue; + pots.Potentiometer2Value = pots.Potentiometer2MaxValue; + } + } + } +} diff --git a/Helios/Controls/DualPotentiometerBehaviorEditor.xaml b/Helios/Controls/DualPotentiometerBehaviorEditor.xaml new file mode 100644 index 000000000..31da1aa7e --- /dev/null +++ b/Helios/Controls/DualPotentiometerBehaviorEditor.xaml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Pot 1 (Lower) Value + + + + + + + + + + + Pot 2 (Upper) Value + + + + + + + + + + + Linkage + + + + + + diff --git a/Helios/Controls/DualPotentiometerBehaviorEditor.xaml.cs b/Helios/Controls/DualPotentiometerBehaviorEditor.xaml.cs new file mode 100644 index 000000000..0668a44b0 --- /dev/null +++ b/Helios/Controls/DualPotentiometerBehaviorEditor.xaml.cs @@ -0,0 +1,33 @@ +// Copyright 2014 Craig Courtney +// Copyright 2026 Helios Contributors +// +// Helios is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Helios is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +namespace GadrocsWorkshop.Helios.Controls +{ + using GadrocsWorkshop.Helios.ComponentModel; + using GadrocsWorkshop.Helios.Windows.Controls; + + /// + /// Interaction logic for DualPotentiometerBehaviorEditor.xaml + /// + [HeliosPropertyEditor("Helios.Base.DualPotentiometer", "Behavior")] + public partial class DualPotentiometerBehaviorEditor : HeliosPropertyEditor + { + public DualPotentiometerBehaviorEditor() + { + InitializeComponent(); + } + } +} diff --git a/Helios/Controls/DualPotentiometerRenderer.cs b/Helios/Controls/DualPotentiometerRenderer.cs new file mode 100644 index 000000000..178570325 --- /dev/null +++ b/Helios/Controls/DualPotentiometerRenderer.cs @@ -0,0 +1,97 @@ +// Copyright 2014 Craig Courtney +// Copyright 2026 Helios Contributors +// +// Helios is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Helios is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +namespace GadrocsWorkshop.Helios.Controls +{ + using System.Windows; + using System.Windows.Media; + + public class DualPotentiometerRenderer : HeliosVisualRenderer + { + private ImageSource _image1; + private ImageSource _image2; + private Rect _rect1; + private Rect _rect2; + private Point _center; + private readonly DrawingGroup _group = new DrawingGroup(); + + protected override void OnRender(DrawingContext drawingContext) + { + if (!(Visual is DualPotentiometer pots)) + { + return; + } + + DrawingContext ctx = _group.Open(); + + // keep the extent of the drawing equal to the control, so that it does not move + // or get rescaled as rotated images change the bounding box + ctx.DrawRectangle(Brushes.Transparent, null, new Rect(0d, 0d, pots.Width, pots.Height)); + + if (pots.Potentiometer2OnTop) + { + DrawPotentiometer(ctx, _image1, _rect1, pots.Potentiometer1Rotation); + DrawPotentiometer(ctx, _image2, _rect2, pots.Potentiometer2Rotation); + } + else + { + DrawPotentiometer(ctx, _image2, _rect2, pots.Potentiometer2Rotation); + DrawPotentiometer(ctx, _image1, _rect1, pots.Potentiometer1Rotation); + } + ctx.Close(); + DrawGroup(drawingContext, _group); + } + + private void DrawPotentiometer(DrawingContext ctx, ImageSource image, Rect rect, double rotation) + { + if (image == null) + { + return; + } + ctx.PushTransform(new RotateTransform(rotation, _center.X, _center.Y)); + ctx.DrawImage(image, rect); + ctx.Pop(); + } + + protected override void OnRefresh() + { + if (!(Visual is DualPotentiometer pots)) + { + _image1 = null; + _image2 = null; + return; + } + + IImageManager3 imageManager = ConfigManager.ImageManager as IImageManager3; + LoadImageOptions loadOptions = pots.ImageRefresh ? LoadImageOptions.ReloadIfChangedExternally : LoadImageOptions.None; + + _center = new Point(pots.Width / 2d, pots.Height / 2d); + _rect1 = CenteredRect(pots, pots.Potentiometer1Scale); + _rect2 = CenteredRect(pots, pots.Potentiometer2Scale); + + _image1 = string.IsNullOrEmpty(pots.Potentiometer1Image) ? null : imageManager?.LoadImage(pots.Potentiometer1Image, loadOptions); + _image2 = string.IsNullOrEmpty(pots.Potentiometer2Image) ? null : imageManager?.LoadImage(pots.Potentiometer2Image, loadOptions); + } + + private static Rect CenteredRect(DualPotentiometer pots, double scale) + { + scale = double.IsNaN(scale) || scale < 0d ? 0d : scale; + double width = pots.Width * scale; + double height = pots.Height * scale; + return new Rect((pots.Width - width) / 2d, (pots.Height - height) / 2d, width, height); + } + } +}