Created Audio Manager class.
Clean up CScore audio usage.
Fixing broken audio device change handler.
Fixed various audio Dispose deadlock due to Invoke usage.
Thus now using BeginInvoke instead.
2 // Copyright (C) 2014-2015 Stéphane Lenclud.
4 // This file is part of SharpDisplayManager.
6 // SharpDisplayManager is free software: you can redistribute it and/or modify
7 // it under the terms of the GNU General Public License as published by
8 // the Free Software Foundation, either version 3 of the License, or
9 // (at your option) any later version.
11 // SharpDisplayManager is distributed in the hope that it will be useful,
12 // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 // GNU General Public License for more details.
16 // You should have received a copy of the GNU General Public License
17 // along with SharpDisplayManager. If not, see <http://www.gnu.org/licenses/>.
21 using System.Collections.Generic;
22 using System.ComponentModel;
27 using System.Threading.Tasks;
28 using System.Windows.Forms;
30 using CodeProject.Dialog;
31 using System.Drawing.Imaging;
32 using System.ServiceModel;
33 using System.Threading;
34 using System.Diagnostics;
35 using System.Deployment.Application;
36 using System.Reflection;
37 using System.Runtime.InteropServices;
38 using System.Security;
44 using CSCore.CoreAudioAPI;
53 using SharpDisplayClient;
55 using MiniDisplayInterop;
56 using SharpLib.Display;
57 using Ear = SharpLib.Ear;
60 namespace SharpDisplayManager
63 public delegate uint ColorProcessingDelegate(int aX, int aY, uint aPixel);
65 public delegate int CoordinateTranslationDelegate(System.Drawing.Bitmap aBmp, int aInt);
67 //Delegates are used for our thread safe method
68 public delegate void AddClientDelegate(string aSessionId, ICallback aCallback);
70 public delegate void RemoveClientDelegate(string aSessionId);
72 public delegate void SetFieldDelegate(string SessionId, DataField aField);
74 public delegate void SetFieldsDelegate(string SessionId, System.Collections.Generic.IList<DataField> aFields);
76 public delegate void SetLayoutDelegate(string SessionId, TableLayout aLayout);
78 public delegate void SetClientNameDelegate(string aSessionId, string aName);
80 public delegate void SetClientPriorityDelegate(string aSessionId, uint aPriority);
82 public delegate void WndProcDelegate(ref Message aMessage);
85 /// Our Display manager main form
87 [System.ComponentModel.DesignerCategory("Form")]
88 public partial class FormMain : FormMainHid
90 //public Manager iManager = new Manager();
91 DateTime LastTickTime;
93 System.Drawing.Bitmap iBmp;
94 //TODO: Align that with what we did from Audio Visualizers bitmaps?
95 bool iCreateBitmap; //Workaround render to bitmap issues when minimized
96 ServiceHost iServiceHost;
97 // Our collection of clients sorted by session id.
98 public Dictionary<string, ClientData> iClients;
99 // The name of the client which informations are currently displayed.
100 public string iCurrentClientSessionId;
101 ClientData iCurrentClientData;
103 public bool iClosing;
105 public bool iSkipFrameRendering;
106 //Function pointer for pixel color filtering
107 ColorProcessingDelegate iColorFx;
108 //Function pointer for pixel X coordinate intercept
109 CoordinateTranslationDelegate iScreenX;
110 //Function pointer for pixel Y coordinate intercept
111 CoordinateTranslationDelegate iScreenY;
113 private AudioManager iAudioManager;
116 private NetworkManager iNetworkManager;
119 /// CEC - Consumer Electronic Control.
120 /// Notably used to turn TV on and off as Windows broadcast monitor on and off notifications.
122 private ConsumerElectronicControl iCecManager;
125 /// Manage run when Windows startup option
127 private StartupManager iStartupManager;
130 /// System notification icon used to hide our application from the task bar.
132 private SharpLib.Notification.Control iNotifyIcon;
135 /// System recording notification icon.
137 private SharpLib.Notification.Control iRecordingNotification;
142 RichTextBoxTraceListener iWriter;
146 /// Allow user to receive window messages;
148 public event WndProcDelegate OnWndProc;
152 if (Properties.Settings.Default.EarManager == null)
154 //No actions in our settings yet
155 Properties.Settings.Default.EarManager = new EarManager();
159 // We loaded events and actions from our settings
160 // Internalizer apparently skips constructor so we need to initialize it here
161 // Though I reckon that should only be needed when loading an empty EAR manager I guess.
162 Properties.Settings.Default.EarManager.Construct();
164 iSkipFrameRendering = false;
166 iCurrentClientSessionId = "";
167 iCurrentClientData = null;
168 LastTickTime = DateTime.Now;
169 //Instantiate our display and register for events notifications
170 iDisplay = new Display();
171 iDisplay.OnOpened += OnDisplayOpened;
172 iDisplay.OnClosed += OnDisplayClosed;
174 iClients = new Dictionary<string, ClientData>();
175 iStartupManager = new StartupManager();
176 iNotifyIcon = new SharpLib.Notification.Control();
177 iRecordingNotification = new SharpLib.Notification.Control();
179 //Have our designer initialize its controls
180 InitializeComponent();
182 //Redirect console output
183 iWriter = new RichTextBoxTraceListener(richTextBoxLogs);
184 Trace.Listeners.Add(iWriter);
186 //Populate device types
187 PopulateDeviceTypes();
189 //Initial status update
192 //We have a bug when drawing minimized and reusing our bitmap
193 //Though I could not reproduce it on Windows 10
194 iBmp = new System.Drawing.Bitmap(iTableLayoutPanel.Width, iTableLayoutPanel.Height,
195 PixelFormat.Format32bppArgb);
196 iCreateBitmap = false;
198 //Minimize our window if desired
199 if (Properties.Settings.Default.StartMinimized)
201 WindowState = FormWindowState.Minimized;
209 /// <param name="sender"></param>
210 /// <param name="e"></param>
211 private void MainForm_Load(object sender, EventArgs e)
213 //Check if we are running a Click Once deployed application
214 if (ApplicationDeployment.IsNetworkDeployed)
216 //This is a proper Click Once installation, fetch and show our version number
217 this.Text += " - v" + ApplicationDeployment.CurrentDeployment.CurrentVersion;
221 //Not a proper Click Once installation, assuming development build then
222 this.Text += " - development";
226 CreateAudioManager();
229 iNetworkManager = new NetworkManager();
230 iNetworkManager.OnConnectivityChanged += OnConnectivityChanged;
231 UpdateNetworkStatus();
234 iCecManager = new ConsumerElectronicControl();
235 OnWndProc += iCecManager.OnWndProc;
242 PopulateTreeViewEvents();
244 //Setup notification icon
247 //Setup recording notification
248 SetupRecordingNotification();
250 // To make sure start up with minimize to tray works
251 if (WindowState == FormWindowState.Minimized && Properties.Settings.Default.MinimizeToTray)
257 //When not debugging we want the screen to be empty until a client takes over
260 //When developing we want at least one client for testing
261 StartNewClient("abcdefghijklmnopqrst-0123456789", "ABCDEFGHIJKLMNOPQRST-0123456789");
264 //Open display connection on start-up if needed
265 if (Properties.Settings.Default.DisplayConnectOnStartup)
267 OpenDisplayConnection();
270 //Start our server so that we can get client requests
273 //Register for HID events
274 RegisterHidDevices();
276 //Start Idle client if needed
277 if (Properties.Settings.Default.StartIdleClient)
284 private void CreateAudioManager()
286 iAudioManager = new AudioManager();
287 iAudioManager.Open(OnDefaultMultiMediaDeviceChanged, OnVolumeNotification);
288 UpdateAudioDeviceAndMasterVolumeThreadSafe();
291 private void DestroyAudioManager()
293 if (iAudioManager != null)
295 iAudioManager.Close();
296 iAudioManager = null;
303 /// <param name="sender"></param>
304 /// <param name="aEvent"></param>
305 public void OnDefaultMultiMediaDeviceChanged(object sender, DefaultDeviceChangedEventArgs aEvent)
307 if (aEvent.DataFlow == DataFlow.Render && aEvent.Role == Role.Multimedia)
309 ResetAudioManagerThreadSafe();
316 private void ResetAudioManagerThreadSafe()
320 //Not in the proper thread, invoke ourselves
321 BeginInvoke(new Action<FormMain>((sender) => { ResetAudioManagerThreadSafe(); }), this);
325 //Proper thread, go ahead
326 DestroyAudioManager();
327 CreateAudioManager();
332 /// Called when our display is opened.
334 /// <param name="aDisplay"></param>
335 private void OnDisplayOpened(Display aDisplay)
337 //Make sure we resume frame rendering
338 iSkipFrameRendering = false;
340 //Set our screen size now that our display is connected
341 //Our panelDisplay is the container of our tableLayoutPanel
342 //tableLayoutPanel will resize itself to fit the client size of our panelDisplay
343 //panelDisplay needs an extra 2 pixels for borders on each sides
344 //tableLayoutPanel will eventually be the exact size of our display
345 Size size = new Size(iDisplay.WidthInPixels() + 2, iDisplay.HeightInPixels() + 2);
346 panelDisplay.Size = size;
348 //Our display was just opened, update our UI
350 //Initiate asynchronous request
351 iDisplay.RequestFirmwareRevision();
354 UpdateMasterVolumeThreadSafe();
356 UpdateNetworkStatus();
359 //Testing icon in debug, no arm done if icon not supported
360 //iDisplay.SetIconStatus(Display.TMiniDisplayIconType.EMiniDisplayIconRecording, 0, 1);
361 //iDisplay.SetAllIconsStatus(2);
367 private static void AddActionsToTreeNode(TreeNode aParentNode, Ear.Object aObject)
369 foreach (Ear.Action a in aObject.Objects.OfType<Ear.Action>())
372 TreeNode actionNode = aParentNode.Nodes.Add(a.Brief());
374 //Use color from parent unless our action itself is disabled
375 actionNode.ForeColor = a.Enabled ? aParentNode.ForeColor : Color.DimGray;
377 AddActionsToTreeNode(actionNode,a);
385 /// <param name="aObject"></param>
386 /// <param name="aNode"></param>
387 private static TreeNode FindTreeNodeForEarObject(Ear.Object aObject, TreeNode aNode)
389 if (aNode.Tag == aObject)
394 foreach (TreeNode n in aNode.Nodes)
396 TreeNode found = FindTreeNodeForEarObject(aObject,n);
410 /// <param name="aObject"></param>
411 private void SelectEarObject(Ear.Object aObject)
413 foreach (TreeNode n in iTreeViewEvents.Nodes)
415 TreeNode found = FindTreeNodeForEarObject(aObject, n);
418 iTreeViewEvents.SelectedNode=found;
419 iTreeViewEvents.Focus();
426 /// Populate tree view with events and actions
428 private void PopulateTreeViewEvents(Ear.Object aSelectedObject=null)
431 iTreeViewEvents.Nodes.Clear();
432 //Populate registered events
433 foreach (Ear.Event e in Properties.Settings.Default.EarManager.Events)
435 //Create our event node
436 //Work out the name of our node
437 string eventNodeName = "";
438 if (!string.IsNullOrEmpty(e.Name))
440 //That event has a proper name, use it then
441 eventNodeName = $"{e.Name} - {e.Brief()}";
445 //Unnamed events just use brief
446 eventNodeName = e.Brief();
449 TreeNode eventNode = iTreeViewEvents.Nodes.Add(eventNodeName);
450 eventNode.Tag = e; //For easy access to our event
453 //Dim our nodes if disabled
454 eventNode.ForeColor = Color.DimGray;
457 //Add event description as child node
458 eventNode.Nodes.Add(e.AttributeDescription).ForeColor = eventNode.ForeColor;
459 //Create child node for actions root
460 TreeNode actionsNode = eventNode.Nodes.Add("Actions");
461 actionsNode.ForeColor = eventNode.ForeColor;
463 // Recursively add our actions for that event
464 AddActionsToTreeNode(actionsNode,e);
467 iTreeViewEvents.ExpandAll();
469 if (aSelectedObject != null)
471 SelectEarObject(aSelectedObject);
474 // Just to be safe in case the selection did not work
479 /// Called when our display is closed.
481 /// <param name="aDisplay"></param>
482 private void OnDisplayClosed(Display aDisplay)
484 //Our display was just closed, update our UI consequently
488 public void OnConnectivityChanged(NetworkManager aNetwork, NLM_CONNECTIVITY newConnectivity)
490 //Update network status
491 UpdateNetworkStatus();
495 /// Update our Network Status
497 private void UpdateNetworkStatus()
499 if (iDisplay.IsOpen())
501 iDisplay.SetIconOnOff(MiniDisplay.IconType.Internet,
502 iNetworkManager.NetworkListManager.IsConnectedToInternet);
503 iDisplay.SetIconOnOff(MiniDisplay.IconType.NetworkSignal, iNetworkManager.NetworkListManager.IsConnected);
508 int iLastNetworkIconIndex = 0;
509 int iUpdateCountSinceLastNetworkAnimation = 0;
514 private void UpdateNetworkSignal(DateTime aLastTickTime, DateTime aNewTickTime)
516 iUpdateCountSinceLastNetworkAnimation++;
517 iUpdateCountSinceLastNetworkAnimation = iUpdateCountSinceLastNetworkAnimation%4;
519 if (iDisplay.IsOpen() && iNetworkManager.NetworkListManager.IsConnected &&
520 iUpdateCountSinceLastNetworkAnimation == 0)
522 int iconCount = iDisplay.IconCount(MiniDisplay.IconType.NetworkSignal);
525 //Prevents div by zero and other undefined behavior
528 iLastNetworkIconIndex++;
529 iLastNetworkIconIndex = iLastNetworkIconIndex%(iconCount*2);
530 for (int i = 0; i < iconCount; i++)
532 if (i < iLastNetworkIconIndex && !(i == 0 && iLastNetworkIconIndex > 3) &&
533 !(i == 1 && iLastNetworkIconIndex > 4))
535 iDisplay.SetIconOn(MiniDisplay.IconType.NetworkSignal, i);
539 iDisplay.SetIconOff(MiniDisplay.IconType.NetworkSignal, i);
548 /// Receive volume change notification and reflect changes on our slider.
550 /// <param name="data"></param>
551 public void OnVolumeNotification(object sender, AudioEndpointVolumeCallbackEventArgs aEvent)
553 UpdateMasterVolumeThreadSafe();
557 /// Update master volume when user moves our slider.
559 /// <param name="sender"></param>
560 /// <param name="e"></param>
561 private void trackBarMasterVolume_Scroll(object sender, EventArgs e)
563 //Just like Windows Volume Mixer we unmute if the volume is adjusted
564 iAudioManager.Volume.IsMuted = false;
565 //Set volume level according to our volume slider new position
566 iAudioManager.Volume.MasterVolumeLevelScalar = trackBarMasterVolume.Value/100.0f;
571 /// Mute check box changed.
573 /// <param name="sender"></param>
574 /// <param name="e"></param>
575 private void checkBoxMute_CheckedChanged(object sender, EventArgs e)
577 iAudioManager.Volume.IsMuted = checkBoxMute.Checked;
582 /// Update master volume indicators based our current system states.
583 /// This typically includes volume levels and mute status.
585 private void UpdateMasterVolumeThreadSafe()
589 //Not in the proper thread, invoke ourselves
590 BeginInvoke(new Action<FormMain>((sender) => { UpdateMasterVolumeThreadSafe(); }), this);
594 //Update volume slider
595 float volumeLevelScalar = iAudioManager.Volume.MasterVolumeLevelScalar;
596 trackBarMasterVolume.Value = Convert.ToInt32(volumeLevelScalar*100);
597 //Update mute checkbox
598 checkBoxMute.Checked = iAudioManager.Volume.IsMuted;
600 //If our display connection is open we need to update its icons
601 if (iDisplay.IsOpen())
603 //First take care our our volume level icons
604 int volumeIconCount = iDisplay.IconCount(MiniDisplay.IconType.Volume);
605 if (volumeIconCount > 0)
607 //Compute current volume level from system level and the number of segments in our display volume bar.
608 //That tells us how many segments in our volume bar needs to be turned on.
609 float currentVolume = volumeLevelScalar*volumeIconCount;
610 int segmentOnCount = Convert.ToInt32(currentVolume);
611 //Check if our segment count was rounded up, this will later be used for half brightness segment
612 bool roundedUp = segmentOnCount > currentVolume;
614 for (int i = 0; i < volumeIconCount; i++)
616 if (i < segmentOnCount)
618 //If we are dealing with our last segment and our segment count was rounded up then we will use half brightness.
619 if (i == segmentOnCount - 1 && roundedUp)
622 iDisplay.SetIconStatus(MiniDisplay.IconType.Volume, i,
623 (iDisplay.IconStatusCount(MiniDisplay.IconType.Volume) - 1)/2);
628 iDisplay.SetIconStatus(MiniDisplay.IconType.Volume, i,
629 iDisplay.IconStatusCount(MiniDisplay.IconType.Volume) - 1);
634 iDisplay.SetIconStatus(MiniDisplay.IconType.Volume, i, 0);
639 //Take care of our mute icon
640 iDisplay.SetIconOnOff(MiniDisplay.IconType.Mute, iAudioManager.Volume.IsMuted);
650 private void UpdateAudioVisualization()
652 // No point if we don't have a current client
653 if (iCurrentClientData == null)
659 if (iAudioManager==null || !iAudioManager.Spectrum.Update())
661 //Nothing changed no need to render
665 // Check if our current client has an Audio Visualizer field
666 // and render them as needed
667 foreach (DataField f in iCurrentClientData.Fields)
669 if (f is AudioVisualizerField)
671 AudioVisualizerField avf = (AudioVisualizerField)f;
672 Control ctrl = iTableLayoutPanel.GetControlFromPosition(avf.Column, avf.Row);
674 if (ctrl is PictureBox)
676 PictureBox pb = (PictureBox)ctrl;
677 if (iAudioManager.Spectrum.Render(pb.Image, Color.Black, Color.Black, Color.White, false))
690 private void UpdateAudioDeviceAndMasterVolumeThreadSafe()
694 //Not in the proper thread, invoke ourselves
695 BeginInvoke(new Action<FormMain>((sender) => { UpdateAudioDeviceAndMasterVolumeThreadSafe(); }), this);
699 //We are in the correct thread just go ahead.
704 iLabelDefaultAudioDevice.Text = iAudioManager.DefaultDevice.FriendlyName;
706 //Show our volume in our track bar
707 UpdateMasterVolumeThreadSafe();
710 trackBarMasterVolume.Enabled = true;
714 Debug.WriteLine("Exception thrown in UpdateAudioDeviceAndMasterVolume");
715 Debug.WriteLine(ex.ToString());
716 //Something went wrong S/PDIF device ca throw exception I guess
717 trackBarMasterVolume.Enabled = false;
724 private void PopulateDeviceTypes()
726 int count = Display.TypeCount();
728 for (int i = 0; i < count; i++)
730 comboBoxDisplayType.Items.Add(Display.TypeName((MiniDisplay.Type) i));
737 private void SetupTrayIcon()
739 iNotifyIcon.Icon = GetIcon("vfd.ico");
740 iNotifyIcon.Text = "Sharp Display Manager";
741 iNotifyIcon.Visible = true;
743 //Double click toggles visibility - typically brings up the application
744 iNotifyIcon.DoubleClick += delegate(object obj, EventArgs args)
749 //Adding a context menu, useful to be able to exit the application
750 ContextMenu contextMenu = new ContextMenu();
751 //Context menu item to toggle visibility
752 MenuItem hideShowItem = new MenuItem("Hide/Show");
753 hideShowItem.Click += delegate(object obj, EventArgs args)
757 contextMenu.MenuItems.Add(hideShowItem);
759 //Context menu item separator
760 contextMenu.MenuItems.Add(new MenuItem("-"));
762 //Context menu exit item
763 MenuItem exitItem = new MenuItem("Exit");
764 exitItem.Click += delegate(object obj, EventArgs args)
768 contextMenu.MenuItems.Add(exitItem);
770 iNotifyIcon.ContextMenu = contextMenu;
776 private void SetupRecordingNotification()
778 iRecordingNotification.Icon = GetIcon("record.ico");
779 iRecordingNotification.Text = "No recording";
780 iRecordingNotification.Visible = false;
784 /// Access icons from embedded resources.
786 /// <param name="aName"></param>
787 /// <returns></returns>
788 public static Icon GetIcon(string aName)
790 string[] names = Assembly.GetExecutingAssembly().GetManifestResourceNames();
791 foreach (string name in names)
793 //Find a resource name that ends with the given name
794 if (name.EndsWith(aName))
796 using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(name))
798 return new Icon(stream);
808 /// Set our current client.
809 /// This will take care of applying our client layout and set data fields.
811 /// <param name="aSessionId"></param>
812 void SetCurrentClient(string aSessionId, bool aForce = false)
814 if (aSessionId == iCurrentClientSessionId)
816 //Given client is already the current one.
817 //Don't bother changing anything then.
821 ClientData requestedClientData = iClients[aSessionId];
823 //Check when was the last time we switched to that client
824 if (iCurrentClientData != null)
826 //Do not switch client if priority of current client is higher
827 if (!aForce && requestedClientData.Priority < iCurrentClientData.Priority)
833 double lastSwitchToClientSecondsAgo = (DateTime.Now - iCurrentClientData.LastSwitchTime).TotalSeconds;
834 //TODO: put that hard coded value as a client property
835 //Clients should be able to define how often they can be interrupted
836 //Thus a background client can set this to zero allowing any other client to interrupt at any time
837 //We could also compute this delay by looking at the requests frequencies?
839 requestedClientData.Priority == iCurrentClientData.Priority &&
840 //Time sharing is only if clients have the same priority
841 (lastSwitchToClientSecondsAgo < 30)) //Make sure a client is on for at least 30 seconds
843 //Don't switch clients too often
848 //Set current client ID.
849 iCurrentClientSessionId = aSessionId;
850 //Set the time we last switched to that client
851 iClients[aSessionId].LastSwitchTime = DateTime.Now;
852 //Fetch and set current client data.
853 iCurrentClientData = requestedClientData;
854 //Apply layout and set data fields.
855 UpdateTableLayoutPanel(iCurrentClientData);
858 private void buttonFont_Click(object sender, EventArgs e)
860 //fontDialog.ShowColor = true;
861 //fontDialog.ShowApply = true;
862 fontDialog.ShowEffects = true;
863 fontDialog.Font = cds.Font;
865 fontDialog.FixedPitchOnly = checkBoxFixedPitchFontOnly.Checked;
867 //fontDialog.ShowHelp = true;
869 //fontDlg.MaxSize = 40;
870 //fontDlg.MinSize = 22;
872 //fontDialog.Parent = this;
873 //fontDialog.StartPosition = FormStartPosition.CenterParent;
875 //DlgBox.ShowDialog(fontDialog);
877 //if (fontDialog.ShowDialog(this) != DialogResult.Cancel)
878 if (DlgBox.ShowDialog(fontDialog) != DialogResult.Cancel)
880 //Set the fonts to all our labels in our layout
881 foreach (Control ctrl in iTableLayoutPanel.Controls)
883 if (ctrl is MarqueeLabel)
885 ((MarqueeLabel) ctrl).Font = fontDialog.Font;
890 cds.Font = fontDialog.Font;
891 Properties.Settings.Default.Save();
900 void CheckFontHeight()
902 //Show font height and width
903 labelFontHeight.Text = "Font height: " + cds.Font.Height;
904 float charWidth = IsFixedWidth(cds.Font);
905 if (charWidth == 0.0f)
907 labelFontWidth.Visible = false;
911 labelFontWidth.Visible = true;
912 labelFontWidth.Text = "Font width: " + charWidth;
915 MarqueeLabel label = null;
916 //Get the first label control we can find
917 foreach (Control ctrl in iTableLayoutPanel.Controls)
919 if (ctrl is MarqueeLabel)
921 label = (MarqueeLabel) ctrl;
926 //Now check font height and show a warning if needed.
927 if (label != null && label.Font.Height > label.Height)
929 labelWarning.Text = "WARNING: Selected font is too height by " + (label.Font.Height - label.Height) +
931 labelWarning.Visible = true;
935 labelWarning.Visible = false;
940 private void buttonCapture_Click(object sender, EventArgs e)
942 System.Drawing.Bitmap bmp = new System.Drawing.Bitmap(iTableLayoutPanel.Width, iTableLayoutPanel.Height);
943 iTableLayoutPanel.DrawToBitmap(bmp, iTableLayoutPanel.ClientRectangle);
944 //Bitmap bmpToSave = new Bitmap(bmp);
945 bmp.Save("D:\\capture.png");
947 ((MarqueeLabel) iTableLayoutPanel.Controls[0]).Text = "Captured";
950 string outputFileName = "d:\\capture.png";
951 using (MemoryStream memory = new MemoryStream())
953 using (FileStream fs = new FileStream(outputFileName, FileMode.OpenOrCreate, FileAccess.ReadWrite))
955 bmp.Save(memory, System.Drawing.Imaging.ImageFormat.Png);
956 byte[] bytes = memory.ToArray();
957 fs.Write(bytes, 0, bytes.Length);
964 private void CheckForRequestResults()
966 if (iDisplay.IsRequestPending())
968 switch (iDisplay.AttemptRequestCompletion())
970 case MiniDisplay.Request.FirmwareRevision:
971 toolStripStatusLabelConnect.Text += " v" + iDisplay.FirmwareRevision();
972 //Issue next request then
973 iDisplay.RequestPowerSupplyStatus();
976 case MiniDisplay.Request.PowerSupplyStatus:
977 if (iDisplay.PowerSupplyStatus())
979 toolStripStatusLabelPower.Text = "ON";
983 toolStripStatusLabelPower.Text = "OFF";
985 //Issue next request then
986 iDisplay.RequestDeviceId();
989 case MiniDisplay.Request.DeviceId:
990 toolStripStatusLabelConnect.Text += " - " + iDisplay.DeviceId();
991 //No more request to issue
997 public static uint ColorWhiteIsOn(int aX, int aY, uint aPixel)
999 if ((aPixel & 0x00FFFFFF) == 0x00FFFFFF)
1006 public static uint ColorUntouched(int aX, int aY, uint aPixel)
1011 public static uint ColorInversed(int aX, int aY, uint aPixel)
1016 public static uint ColorChessboard(int aX, int aY, uint aPixel)
1018 if ((aX%2 == 0) && (aY%2 == 0))
1022 else if ((aX%2 != 0) && (aY%2 != 0))
1030 public static int ScreenReversedX(System.Drawing.Bitmap aBmp, int aX)
1032 return aBmp.Width - aX - 1;
1035 public int ScreenReversedY(System.Drawing.Bitmap aBmp, int aY)
1037 return iBmp.Height - aY - 1;
1040 public int ScreenX(System.Drawing.Bitmap aBmp, int aX)
1045 public int ScreenY(System.Drawing.Bitmap aBmp, int aY)
1051 /// Select proper pixel delegates according to our current settings.
1053 private void SetupPixelDelegates()
1055 //Select our pixel processing routine
1056 if (cds.InverseColors)
1058 //iColorFx = ColorChessboard;
1059 iColorFx = ColorInversed;
1063 iColorFx = ColorWhiteIsOn;
1066 //Select proper coordinate translation functions
1067 //We used delegate/function pointer to support reverse screen without doing an extra test on each pixels
1068 if (cds.ReverseScreen)
1070 iScreenX = ScreenReversedX;
1071 iScreenY = ScreenReversedY;
1082 /// This is our timer tick responsible to perform our render
1083 /// TODO: Use a threading timer instead of a Windows form timer.
1085 /// <param name="sender"></param>
1086 /// <param name="e"></param>
1087 private void timer_Tick(object sender, EventArgs e)
1089 //Update our animations
1090 DateTime NewTickTime = DateTime.Now;
1092 UpdateNetworkSignal(LastTickTime, NewTickTime);
1094 //Update animation for all our marquees
1095 foreach (Control ctrl in iTableLayoutPanel.Controls)
1097 if (ctrl is MarqueeLabel)
1099 ((MarqueeLabel) ctrl).UpdateAnimation(LastTickTime, NewTickTime);
1103 UpdateAudioVisualization();
1105 //Update our display
1106 if (iDisplay.IsOpen())
1108 CheckForRequestResults();
1110 //Check if frame rendering is needed
1111 //Typically used when showing clock
1112 if (!iSkipFrameRendering)
1117 iBmp = new System.Drawing.Bitmap(iTableLayoutPanel.Width, iTableLayoutPanel.Height,
1118 PixelFormat.Format32bppArgb);
1119 iCreateBitmap = false;
1121 iTableLayoutPanel.DrawToBitmap(iBmp, iTableLayoutPanel.ClientRectangle);
1122 //iBmp.Save("D:\\capture.png");
1124 //Send it to our display
1125 for (int i = 0; i < iBmp.Width; i++)
1127 for (int j = 0; j < iBmp.Height; j++)
1131 //Get our processed pixel coordinates
1132 int x = iScreenX(iBmp, i);
1133 int y = iScreenY(iBmp, j);
1135 uint color = (uint) iBmp.GetPixel(i, j).ToArgb();
1136 //Apply color effects
1137 color = iColorFx(x, y, color);
1139 iDisplay.SetPixel(x, y, color);
1144 iDisplay.SwapBuffers();
1148 //Compute instant FPS
1149 toolStripStatusLabelFps.Text = (1.0/NewTickTime.Subtract(LastTickTime).TotalSeconds).ToString("F0") + " / " +
1150 (1000/iTimerDisplay.Interval).ToString() + " FPS";
1152 LastTickTime = NewTickTime;
1157 /// Attempt to establish connection with our display hardware.
1159 private void OpenDisplayConnection()
1161 CloseDisplayConnection();
1163 if (!iDisplay.Open((MiniDisplay.Type) cds.DisplayType))
1166 toolStripStatusLabelConnect.Text = "Connection error";
1170 private void CloseDisplayConnection()
1172 //Status will be updated upon receiving the closed event
1174 if (iDisplay == null || !iDisplay.IsOpen())
1179 //Do not clear if we gave up on rendering already.
1180 //This means we will keep on displaying clock on MDM166AA for instance.
1181 if (!iSkipFrameRendering)
1184 iDisplay.SwapBuffers();
1187 iDisplay.SetAllIconsStatus(0); //Turn off all icons
1191 private void buttonOpen_Click(object sender, EventArgs e)
1193 OpenDisplayConnection();
1196 private void buttonClose_Click(object sender, EventArgs e)
1198 CloseDisplayConnection();
1201 private void buttonClear_Click(object sender, EventArgs e)
1204 iDisplay.SwapBuffers();
1207 private void buttonFill_Click(object sender, EventArgs e)
1210 iDisplay.SwapBuffers();
1213 private void trackBarBrightness_Scroll(object sender, EventArgs e)
1215 cds.Brightness = trackBarBrightness.Value;
1216 Properties.Settings.Default.Save();
1217 iDisplay.SetBrightness(trackBarBrightness.Value);
1223 /// CDS stands for Current Display Settings
1225 private DisplaySettings cds
1229 DisplaysSettings settings = Properties.Settings.Default.DisplaysSettings;
1230 if (settings == null)
1232 settings = new DisplaysSettings();
1234 Properties.Settings.Default.DisplaysSettings = settings;
1237 //Make sure all our settings have been created
1238 while (settings.Displays.Count <= Properties.Settings.Default.CurrentDisplayIndex)
1240 settings.Displays.Add(new DisplaySettings());
1243 DisplaySettings displaySettings = settings.Displays[Properties.Settings.Default.CurrentDisplayIndex];
1244 return displaySettings;
1249 /// Check if the given font has a fixed character pitch.
1251 /// <param name="ft"></param>
1252 /// <returns>0.0f if this is not a monospace font, otherwise returns the character width.</returns>
1253 public float IsFixedWidth(Font ft)
1255 Graphics g = CreateGraphics();
1256 char[] charSizes = new char[] {'i', 'a', 'Z', '%', '#', 'a', 'B', 'l', 'm', ',', '.'};
1257 float charWidth = g.MeasureString("I", ft, Int32.MaxValue, StringFormat.GenericTypographic).Width;
1259 bool fixedWidth = true;
1261 foreach (char c in charSizes)
1262 if (g.MeasureString(c.ToString(), ft, Int32.MaxValue, StringFormat.GenericTypographic).Width !=
1275 /// Synchronize UI with settings
1277 private void UpdateStatus()
1280 checkBoxShowBorders.Checked = cds.ShowBorders;
1281 iTableLayoutPanel.CellBorderStyle = (cds.ShowBorders
1282 ? TableLayoutPanelCellBorderStyle.Single
1283 : TableLayoutPanelCellBorderStyle.None);
1285 //Set the proper font to each of our labels
1286 foreach (MarqueeLabel ctrl in iTableLayoutPanel.Controls)
1288 ctrl.Font = cds.Font;
1292 //Check if "run on Windows startup" is enabled
1293 checkBoxAutoStart.Checked = iStartupManager.Startup;
1296 comboBoxHdmiPort.SelectedIndex = Properties.Settings.Default.CecHdmiPort - 1;
1298 //Mini Display settings
1299 checkBoxReverseScreen.Checked = cds.ReverseScreen;
1300 checkBoxInverseColors.Checked = cds.InverseColors;
1301 checkBoxShowVolumeLabel.Checked = cds.ShowVolumeLabel;
1302 checkBoxScaleToFit.Checked = cds.ScaleToFit;
1303 maskedTextBoxMinFontSize.Enabled = cds.ScaleToFit;
1304 labelMinFontSize.Enabled = cds.ScaleToFit;
1305 maskedTextBoxMinFontSize.Text = cds.MinFontSize.ToString();
1306 maskedTextBoxScrollingSpeed.Text = cds.ScrollingSpeedInPixelsPerSecond.ToString();
1307 comboBoxDisplayType.SelectedIndex = cds.DisplayType;
1308 iTimerDisplay.Interval = cds.TimerInterval;
1309 maskedTextBoxTimerInterval.Text = cds.TimerInterval.ToString();
1310 textBoxScrollLoopSeparator.Text = cds.Separator;
1312 SetupPixelDelegates();
1314 if (iDisplay.IsOpen())
1316 //We have a display connection
1317 //Reflect that in our UI
1320 iTableLayoutPanel.Enabled = true;
1321 panelDisplay.Enabled = true;
1323 //Only setup brightness if display is open
1324 trackBarBrightness.Minimum = iDisplay.MinBrightness();
1325 trackBarBrightness.Maximum = iDisplay.MaxBrightness();
1326 if (cds.Brightness < iDisplay.MinBrightness() || cds.Brightness > iDisplay.MaxBrightness())
1328 //Brightness out of range, this can occur when using auto-detect
1329 //Use max brightness instead
1330 trackBarBrightness.Value = iDisplay.MaxBrightness();
1331 iDisplay.SetBrightness(iDisplay.MaxBrightness());
1335 trackBarBrightness.Value = cds.Brightness;
1336 iDisplay.SetBrightness(cds.Brightness);
1339 //Try compute the steps to something that makes sense
1340 trackBarBrightness.LargeChange = Math.Max(1, (iDisplay.MaxBrightness() - iDisplay.MinBrightness())/5);
1341 trackBarBrightness.SmallChange = 1;
1344 buttonFill.Enabled = true;
1345 buttonClear.Enabled = true;
1346 buttonOpen.Enabled = false;
1347 buttonClose.Enabled = true;
1348 trackBarBrightness.Enabled = true;
1349 toolStripStatusLabelConnect.Text = "Connected - " + iDisplay.Vendor() + " - " + iDisplay.Product();
1350 //+ " - " + iDisplay.SerialNumber();
1352 if (iDisplay.SupportPowerOnOff())
1354 buttonPowerOn.Enabled = true;
1355 buttonPowerOff.Enabled = true;
1359 buttonPowerOn.Enabled = false;
1360 buttonPowerOff.Enabled = false;
1363 if (iDisplay.SupportClock())
1365 buttonShowClock.Enabled = true;
1366 buttonHideClock.Enabled = true;
1370 buttonShowClock.Enabled = false;
1371 buttonHideClock.Enabled = false;
1375 //Check if Volume Label is supported. To date only MDM166AA supports that crap :)
1376 checkBoxShowVolumeLabel.Enabled = iDisplay.IconCount(MiniDisplay.IconType.VolumeLabel) > 0;
1378 if (cds.ShowVolumeLabel)
1380 iDisplay.SetIconOn(MiniDisplay.IconType.VolumeLabel);
1384 iDisplay.SetIconOff(MiniDisplay.IconType.VolumeLabel);
1389 //Display connection not available
1390 //Reflect that in our UI
1392 //In debug start our timer even if we don't have a display connection
1395 //In production environment we don't need our timer if no display connection
1398 checkBoxShowVolumeLabel.Enabled = false;
1399 iTableLayoutPanel.Enabled = false;
1400 panelDisplay.Enabled = false;
1401 buttonFill.Enabled = false;
1402 buttonClear.Enabled = false;
1403 buttonOpen.Enabled = true;
1404 buttonClose.Enabled = false;
1405 trackBarBrightness.Enabled = false;
1406 buttonPowerOn.Enabled = false;
1407 buttonPowerOff.Enabled = false;
1408 buttonShowClock.Enabled = false;
1409 buttonHideClock.Enabled = false;
1410 toolStripStatusLabelConnect.Text = "Disconnected";
1411 toolStripStatusLabelPower.Text = "N/A";
1420 /// <param name="sender"></param>
1421 /// <param name="e"></param>
1422 private void checkBoxShowVolumeLabel_CheckedChanged(object sender, EventArgs e)
1424 cds.ShowVolumeLabel = checkBoxShowVolumeLabel.Checked;
1425 Properties.Settings.Default.Save();
1429 private void checkBoxShowBorders_CheckedChanged(object sender, EventArgs e)
1431 //Save our show borders setting
1432 iTableLayoutPanel.CellBorderStyle = (checkBoxShowBorders.Checked
1433 ? TableLayoutPanelCellBorderStyle.Single
1434 : TableLayoutPanelCellBorderStyle.None);
1435 cds.ShowBorders = checkBoxShowBorders.Checked;
1436 Properties.Settings.Default.Save();
1440 private void checkBoxAutoStart_CheckedChanged(object sender, EventArgs e)
1442 iStartupManager.Startup = checkBoxAutoStart.Checked;
1446 private void checkBoxReverseScreen_CheckedChanged(object sender, EventArgs e)
1448 //Save our reverse screen setting
1449 cds.ReverseScreen = checkBoxReverseScreen.Checked;
1450 Properties.Settings.Default.Save();
1451 SetupPixelDelegates();
1454 private void checkBoxInverseColors_CheckedChanged(object sender, EventArgs e)
1456 //Save our inverse colors setting
1457 cds.InverseColors = checkBoxInverseColors.Checked;
1458 Properties.Settings.Default.Save();
1459 SetupPixelDelegates();
1462 private void checkBoxScaleToFit_CheckedChanged(object sender, EventArgs e)
1464 //Save our scale to fit setting
1465 cds.ScaleToFit = checkBoxScaleToFit.Checked;
1466 Properties.Settings.Default.Save();
1468 labelMinFontSize.Enabled = cds.ScaleToFit;
1469 maskedTextBoxMinFontSize.Enabled = cds.ScaleToFit;
1472 private void MainForm_Resize(object sender, EventArgs e)
1474 if (WindowState == FormWindowState.Minimized)
1476 // To workaround our empty bitmap bug on Windows 7 we need to recreate our bitmap when the application is minimized
1477 // That's apparently not needed on Windows 10 but we better leave it in place.
1478 iCreateBitmap = true;
1482 private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
1485 iNetworkManager.Dispose();
1486 DestroyAudioManager();
1487 CloseDisplayConnection();
1489 e.Cancel = iClosing;
1492 public void StartServer()
1494 iServiceHost = new ServiceHost
1497 new Uri[] {new Uri("net.tcp://localhost:8001/")}
1500 iServiceHost.AddServiceEndpoint(typeof(IService), new NetTcpBinding(SecurityMode.None, true),
1502 iServiceHost.Open();
1505 public void StopServer()
1507 if (iClients.Count > 0 && !iClosing)
1511 BroadcastCloseEvent();
1516 MessageBox.Show("Force exit?", "Waiting for clients...", MessageBoxButtons.YesNo,
1517 MessageBoxIcon.Warning) == DialogResult.Yes)
1519 iClosing = false; //We make sure we force close if asked twice
1524 //We removed that as it often lags for some reason
1525 //iServiceHost.Close();
1529 public void BroadcastCloseEvent()
1531 Trace.TraceInformation("BroadcastCloseEvent - start");
1533 var inactiveClients = new List<string>();
1534 foreach (var client in iClients)
1536 //if (client.Key != eventData.ClientName)
1540 Trace.TraceInformation("BroadcastCloseEvent - " + client.Key);
1541 client.Value.Callback.OnCloseOrder( /*eventData*/);
1543 catch (Exception ex)
1545 inactiveClients.Add(client.Key);
1550 if (inactiveClients.Count > 0)
1552 foreach (var client in inactiveClients)
1554 iClients.Remove(client);
1555 Program.iFormMain.iTreeViewClients.Nodes.Remove(
1556 Program.iFormMain.iTreeViewClients.Nodes.Find(client, false)[0]);
1560 if (iClients.Count == 0)
1567 /// Just remove all our fields.
1569 private void ClearLayout()
1571 iTableLayoutPanel.Controls.Clear();
1572 iTableLayoutPanel.RowStyles.Clear();
1573 iTableLayoutPanel.ColumnStyles.Clear();
1574 iCurrentClientData = null;
1578 /// Just launch a demo client.
1580 private void StartNewClient(string aTopText = "", string aBottomText = "")
1582 Thread clientThread = new Thread(SharpDisplayClient.Program.MainWithParams);
1583 SharpDisplayClient.StartParams myParams = new SharpDisplayClient.StartParams(
1584 new Point(this.Right, this.Top), aTopText, aBottomText);
1585 clientThread.Start(myParams);
1590 /// Just launch our idle client.
1592 private void StartIdleClient(string aTopText = "", string aBottomText = "")
1594 Thread clientThread = new Thread(SharpDisplayClientIdle.Program.MainWithParams);
1595 SharpDisplayClientIdle.StartParams myParams =
1596 new SharpDisplayClientIdle.StartParams(new Point(this.Right, this.Top), aTopText, aBottomText);
1597 clientThread.Start(myParams);
1602 private void buttonStartClient_Click(object sender, EventArgs e)
1607 private void buttonSuspend_Click(object sender, EventArgs e)
1612 private void StartTimer()
1614 LastTickTime = DateTime.Now; //Reset timer to prevent jump
1615 iTimerDisplay.Enabled = true;
1616 UpdateSuspendButton();
1619 private void StopTimer()
1621 LastTickTime = DateTime.Now; //Reset timer to prevent jump
1622 iTimerDisplay.Enabled = false;
1623 UpdateSuspendButton();
1626 private void ToggleTimer()
1628 LastTickTime = DateTime.Now; //Reset timer to prevent jump
1629 iTimerDisplay.Enabled = !iTimerDisplay.Enabled;
1630 UpdateSuspendButton();
1633 private void UpdateSuspendButton()
1635 if (!iTimerDisplay.Enabled)
1637 buttonSuspend.Text = "Run";
1641 buttonSuspend.Text = "Pause";
1646 private void buttonCloseClients_Click(object sender, EventArgs e)
1648 BroadcastCloseEvent();
1651 private void treeViewClients_AfterSelect(object sender, TreeViewEventArgs e)
1653 //Root node must have at least one child
1654 if (e.Node.Nodes.Count == 0)
1659 //If the selected node is the root node of a client then switch to it
1660 string sessionId = e.Node.Nodes[0].Text; //First child of a root node is the sessionId
1661 if (iClients.ContainsKey(sessionId)) //Check that's actually what we are looking at
1663 //We have a valid session just switch to that client
1664 SetCurrentClient(sessionId, true);
1673 /// <param name="aSessionId"></param>
1674 /// <param name="aCallback"></param>
1675 public void AddClientThreadSafe(string aSessionId, ICallback aCallback)
1677 if (this.InvokeRequired)
1679 //Not in the proper thread, invoke ourselves
1680 AddClientDelegate d = new AddClientDelegate(AddClientThreadSafe);
1681 this.Invoke(d, new object[] {aSessionId, aCallback});
1685 //We are in the proper thread
1686 //Add this session to our collection of clients
1687 ClientData newClient = new ClientData(aSessionId, aCallback);
1688 Program.iFormMain.iClients.Add(aSessionId, newClient);
1689 //Add this session to our UI
1690 UpdateClientTreeViewNode(newClient);
1696 /// Find the client with the highest priority if any.
1698 /// <returns>Our highest priority client or null if not a single client is connected.</returns>
1699 public ClientData FindHighestPriorityClient()
1701 ClientData highestPriorityClient = null;
1702 foreach (var client in iClients)
1704 if (highestPriorityClient == null || client.Value.Priority > highestPriorityClient.Priority)
1706 highestPriorityClient = client.Value;
1710 return highestPriorityClient;
1716 /// <param name="aSessionId"></param>
1717 public void RemoveClientThreadSafe(string aSessionId)
1719 if (this.InvokeRequired)
1721 //Not in the proper thread, invoke ourselves
1722 RemoveClientDelegate d = new RemoveClientDelegate(RemoveClientThreadSafe);
1723 this.Invoke(d, new object[] {aSessionId});
1727 //We are in the proper thread
1728 //Remove this session from both client collection and UI tree view
1729 if (Program.iFormMain.iClients.Keys.Contains(aSessionId))
1731 Program.iFormMain.iClients.Remove(aSessionId);
1732 Program.iFormMain.iTreeViewClients.Nodes.Remove(
1733 Program.iFormMain.iTreeViewClients.Nodes.Find(aSessionId, false)[0]);
1734 //Update recording status too whenever a client is removed
1735 UpdateRecordingNotification();
1738 if (iCurrentClientSessionId == aSessionId)
1740 //The current client is closing
1741 iCurrentClientData = null;
1742 //Find the client with the highest priority and set it as current
1743 ClientData newCurrentClient = FindHighestPriorityClient();
1744 if (newCurrentClient != null)
1746 SetCurrentClient(newCurrentClient.SessionId, true);
1750 if (iClients.Count == 0)
1752 //Clear our screen when last client disconnects
1757 //We were closing our form
1758 //All clients are now closed
1759 //Just resume our close operation
1770 /// <param name="aSessionId"></param>
1771 /// <param name="aLayout"></param>
1772 public void SetClientLayoutThreadSafe(string aSessionId, TableLayout aLayout)
1774 if (this.InvokeRequired)
1776 //Not in the proper thread, invoke ourselves
1777 SetLayoutDelegate d = new SetLayoutDelegate(SetClientLayoutThreadSafe);
1778 this.Invoke(d, new object[] {aSessionId, aLayout});
1782 ClientData client = iClients[aSessionId];
1785 //Don't change a thing if the layout is the same
1786 if (!client.Layout.IsSameAs(aLayout))
1788 Debug.Print("SetClientLayoutThreadSafe: Layout updated.");
1789 //Set our client layout then
1790 client.Layout = aLayout;
1791 //So that next time we update all our fields at ones
1792 client.HasNewLayout = true;
1793 //Layout has changed clear our fields then
1794 client.Fields.Clear();
1796 UpdateClientTreeViewNode(client);
1800 Debug.Print("SetClientLayoutThreadSafe: Layout has not changed.");
1809 /// <param name="aSessionId"></param>
1810 /// <param name="aField"></param>
1811 public void SetClientFieldThreadSafe(string aSessionId, DataField aField)
1813 if (this.InvokeRequired)
1815 //Not in the proper thread, invoke ourselves
1816 SetFieldDelegate d = new SetFieldDelegate(SetClientFieldThreadSafe);
1817 this.Invoke(d, new object[] {aSessionId, aField});
1821 //We are in the proper thread
1822 //Call the non-thread-safe variant
1823 SetClientField(aSessionId, aField);
1831 /// Set a data field in the given client.
1833 /// <param name="aSessionId"></param>
1834 /// <param name="aField"></param>
1835 private void SetClientField(string aSessionId, DataField aField)
1837 //TODO: should check if the field actually changed?
1839 ClientData client = iClients[aSessionId];
1840 bool layoutChanged = false;
1841 bool contentChanged = true;
1843 //Fetch our field index
1844 int fieldIndex = client.FindSameFieldIndex(aField);
1848 //No corresponding field, just bail out
1852 //Keep our previous field in there
1853 DataField previousField = client.Fields[fieldIndex];
1854 //Just update that field then
1855 client.Fields[fieldIndex] = aField;
1857 if (!aField.IsTableField)
1859 //We are done then if that field is not in our table layout
1863 TableField tableField = (TableField) aField;
1865 if (previousField.IsSameLayout(aField))
1867 //If we are updating a field in our current client we need to update it in our panel
1868 if (aSessionId == iCurrentClientSessionId)
1870 Control ctrl = iTableLayoutPanel.GetControlFromPosition(tableField.Column, tableField.Row);
1871 if (aField.IsTextField && ctrl is MarqueeLabel)
1873 TextField textField = (TextField)aField;
1874 //Text field control already in place, just change the text
1875 MarqueeLabel label = (MarqueeLabel)ctrl;
1876 contentChanged = (label.Text != textField.Text || label.TextAlign != textField.Alignment);
1877 label.Text = textField.Text;
1878 label.TextAlign = textField.Alignment;
1880 else if (aField.IsBitmapField && ctrl is PictureBox)
1882 BitmapField bitmapField = (BitmapField)aField;
1883 contentChanged = true; //TODO: Bitmap comp or should we leave that to clients?
1884 //Bitmap field control already in place just change the bitmap
1885 PictureBox pictureBox = (PictureBox)ctrl;
1886 pictureBox.Image = bitmapField.Bitmap;
1888 else if (aField is AudioVisualizerField && ctrl is PictureBox)
1890 contentChanged = false; // Since nothing was changed
1894 layoutChanged = true;
1900 layoutChanged = true;
1903 //If either content or layout changed we need to update our tree view to reflect the changes
1904 if (contentChanged || layoutChanged)
1906 UpdateClientTreeViewNode(client);
1910 Debug.Print("Layout changed");
1911 //Our layout has changed, if we are already the current client we need to update our panel
1912 if (aSessionId == iCurrentClientSessionId)
1914 //Apply layout and set data fields.
1915 UpdateTableLayoutPanel(iCurrentClientData);
1920 Debug.Print("Layout has not changed.");
1925 Debug.Print("WARNING: content and layout have not changed!");
1928 //When a client field is set we try switching to this client to present the new information to our user
1929 SetCurrentClient(aSessionId);
1935 /// <param name="aTexts"></param>
1936 public void SetClientFieldsThreadSafe(string aSessionId, System.Collections.Generic.IList<DataField> aFields)
1938 if (this.InvokeRequired)
1940 //Not in the proper thread, invoke ourselves
1941 SetFieldsDelegate d = new SetFieldsDelegate(SetClientFieldsThreadSafe);
1942 this.Invoke(d, new object[] {aSessionId, aFields});
1946 ClientData client = iClients[aSessionId];
1948 if (client.HasNewLayout)
1950 //TODO: Assert client.Count == 0
1951 //Our layout was just changed
1952 //Do some special handling to avoid re-creating our panel N times, once for each fields
1953 client.HasNewLayout = false;
1954 //Just set all our fields then
1955 client.Fields.AddRange(aFields);
1956 //Try switch to that client
1957 SetCurrentClient(aSessionId);
1959 //If we are updating the current client update our panel
1960 if (aSessionId == iCurrentClientSessionId)
1962 //Apply layout and set data fields.
1963 UpdateTableLayoutPanel(iCurrentClientData);
1966 UpdateClientTreeViewNode(client);
1970 //Put each our text fields in a label control
1971 foreach (DataField field in aFields)
1973 SetClientField(aSessionId, field);
1982 /// <param name="aSessionId"></param>
1983 /// <param name="aName"></param>
1984 public void SetClientNameThreadSafe(string aSessionId, string aName)
1986 if (this.InvokeRequired)
1988 //Not in the proper thread, invoke ourselves
1989 SetClientNameDelegate d = new SetClientNameDelegate(SetClientNameThreadSafe);
1990 this.Invoke(d, new object[] {aSessionId, aName});
1994 //We are in the proper thread
1996 ClientData client = iClients[aSessionId];
2000 client.Name = aName;
2001 //Update our tree-view
2002 UpdateClientTreeViewNode(client);
2008 public void SetClientPriorityThreadSafe(string aSessionId, uint aPriority)
2010 if (this.InvokeRequired)
2012 //Not in the proper thread, invoke ourselves
2013 SetClientPriorityDelegate d = new SetClientPriorityDelegate(SetClientPriorityThreadSafe);
2014 this.Invoke(d, new object[] {aSessionId, aPriority});
2018 //We are in the proper thread
2020 ClientData client = iClients[aSessionId];
2024 client.Priority = aPriority;
2025 //Update our tree-view
2026 UpdateClientTreeViewNode(client);
2027 //Change our current client as per new priority
2028 ClientData newCurrentClient = FindHighestPriorityClient();
2029 if (newCurrentClient != null)
2031 SetCurrentClient(newCurrentClient.SessionId);
2040 /// <param name="value"></param>
2041 /// <param name="maxChars"></param>
2042 /// <returns></returns>
2043 public static string Truncate(string value, int maxChars)
2045 return value.Length <= maxChars ? value : value.Substring(0, maxChars - 3) + "...";
2049 /// Update our recording notification.
2051 private void UpdateRecordingNotification()
2054 bool activeRecording = false;
2056 RecordingField recField = new RecordingField();
2057 foreach (var client in iClients)
2059 RecordingField rec = (RecordingField) client.Value.FindSameFieldAs(recField);
2060 if (rec != null && rec.IsActive)
2062 activeRecording = true;
2063 //Don't break cause we are collecting the names/texts.
2064 if (!String.IsNullOrEmpty(rec.Text))
2066 text += (rec.Text + "\n");
2070 //Not text for that recording, use client name instead
2071 text += client.Value.Name + " recording\n";
2077 //Update our text no matter what, can't have more than 63 characters otherwise it throws an exception.
2078 iRecordingNotification.Text = Truncate(text, 63);
2080 //Change visibility of notification if needed
2081 if (iRecordingNotification.Visible != activeRecording)
2083 iRecordingNotification.Visible = activeRecording;
2084 //Assuming the notification icon is in sync with our display icon
2085 //Take care of our REC icon
2086 if (iDisplay.IsOpen())
2088 iDisplay.SetIconOnOff(MiniDisplay.IconType.Recording, activeRecording);
2096 /// <param name="aClient"></param>
2097 private void UpdateClientTreeViewNode(ClientData aClient)
2099 Debug.Print("UpdateClientTreeViewNode");
2101 if (aClient == null)
2106 //Hook in record icon update too
2107 UpdateRecordingNotification();
2109 TreeNode node = null;
2110 //Check that our client node already exists
2111 //Get our client root node using its key which is our session ID
2112 TreeNode[] nodes = iTreeViewClients.Nodes.Find(aClient.SessionId, false);
2113 if (nodes.Count() > 0)
2115 //We already have a node for that client
2117 //Clear children as we are going to recreate them below
2122 //Client node does not exists create a new one
2123 iTreeViewClients.Nodes.Add(aClient.SessionId, aClient.SessionId);
2124 node = iTreeViewClients.Nodes.Find(aClient.SessionId, false)[0];
2130 if (!String.IsNullOrEmpty(aClient.Name))
2132 //We have a name, use it as text for our root node
2133 node.Text = aClient.Name;
2134 //Add a child with SessionId
2135 node.Nodes.Add(new TreeNode(aClient.SessionId));
2139 //No name, use session ID instead
2140 node.Text = aClient.SessionId;
2143 //Display client priority
2144 node.Nodes.Add(new TreeNode("Priority: " + aClient.Priority));
2146 if (aClient.Fields.Count > 0)
2148 //Create root node for our texts
2149 TreeNode textsRoot = new TreeNode("Fields");
2150 node.Nodes.Add(textsRoot);
2151 //For each text add a new entry
2152 foreach (DataField field in aClient.Fields)
2154 if (field.IsTextField)
2156 TextField textField = (TextField) field;
2157 textsRoot.Nodes.Add(new TreeNode("[Text]" + textField.Text));
2159 else if (field.IsBitmapField)
2161 textsRoot.Nodes.Add(new TreeNode("[Bitmap]"));
2163 else if (field is AudioVisualizerField)
2165 textsRoot.Nodes.Add(new TreeNode("[Audio Visualizer]"));
2167 else if (field.IsRecordingField)
2169 RecordingField recordingField = (RecordingField) field;
2170 textsRoot.Nodes.Add(new TreeNode("[Recording]" + recordingField.IsActive));
2180 /// Update our table layout row styles to make sure each rows have similar height
2182 private void UpdateTableLayoutRowStyles()
2184 foreach (RowStyle rowStyle in iTableLayoutPanel.RowStyles)
2186 rowStyle.SizeType = SizeType.Percent;
2187 rowStyle.Height = 100/iTableLayoutPanel.RowCount;
2192 /// Update our display table layout.
2193 /// Will instanciated every field control as defined by our client.
2194 /// Fields must be specified by rows from the left.
2196 /// <param name="aLayout"></param>
2197 private void UpdateTableLayoutPanel(ClientData aClient)
2199 Debug.Print("UpdateTableLayoutPanel");
2201 if (aClient == null)
2208 TableLayout layout = aClient.Layout;
2210 //First clean our current panel
2211 iTableLayoutPanel.Controls.Clear();
2212 iTableLayoutPanel.RowStyles.Clear();
2213 iTableLayoutPanel.ColumnStyles.Clear();
2214 iTableLayoutPanel.RowCount = 0;
2215 iTableLayoutPanel.ColumnCount = 0;
2217 //Then recreate our rows...
2218 while (iTableLayoutPanel.RowCount < layout.Rows.Count)
2220 iTableLayoutPanel.RowCount++;
2224 while (iTableLayoutPanel.ColumnCount < layout.Columns.Count)
2226 iTableLayoutPanel.ColumnCount++;
2230 for (int i = 0; i < iTableLayoutPanel.ColumnCount; i++)
2232 //Create our column styles
2233 this.iTableLayoutPanel.ColumnStyles.Add(layout.Columns[i]);
2236 for (int j = 0; j < iTableLayoutPanel.RowCount; j++)
2240 //Create our row styles
2241 this.iTableLayoutPanel.RowStyles.Add(layout.Rows[j]);
2251 foreach (DataField field in aClient.Fields)
2253 if (!field.IsTableField)
2255 //That field is not taking part in our table layout skip it
2259 TableField tableField = (TableField) field;
2261 //Create a control corresponding to the field specified for that cell
2262 Control control = CreateControlForDataField(tableField);
2264 //Add newly created control to our table layout at the specified row and column
2265 iTableLayoutPanel.Controls.Add(control, tableField.Column, tableField.Row);
2266 //Make sure we specify column and row span for that new control
2267 iTableLayoutPanel.SetColumnSpan(control, tableField.ColumnSpan);
2268 iTableLayoutPanel.SetRowSpan(control, tableField.RowSpan);
2276 /// Check our type of data field and create corresponding control
2278 /// <param name="aField"></param>
2279 private Control CreateControlForDataField(DataField aField)
2281 Control control = null;
2282 if (aField.IsTextField)
2284 MarqueeLabel label = new SharpDisplayManager.MarqueeLabel();
2285 label.AutoEllipsis = true;
2286 label.AutoSize = true;
2287 label.BackColor = System.Drawing.Color.Transparent;
2288 label.Dock = System.Windows.Forms.DockStyle.Fill;
2289 label.Location = new System.Drawing.Point(1, 1);
2290 label.Margin = new System.Windows.Forms.Padding(0);
2291 label.Name = "marqueeLabel" + aField;
2292 label.OwnTimer = false;
2293 label.PixelsPerSecond = cds.ScrollingSpeedInPixelsPerSecond;
2294 label.Separator = cds.Separator;
2295 label.MinFontSize = cds.MinFontSize;
2296 label.ScaleToFit = cds.ScaleToFit;
2297 //control.Size = new System.Drawing.Size(254, 30);
2298 //control.TabIndex = 2;
2299 label.Font = cds.Font;
2301 TextField field = (TextField)aField;
2302 label.TextAlign = field.Alignment;
2303 label.UseCompatibleTextRendering = true;
2304 label.Text = field.Text;
2308 else if (aField.IsBitmapField)
2310 //Create picture box
2311 PictureBox picture = new PictureBox();
2312 picture.AutoSize = true;
2313 picture.BackColor = System.Drawing.Color.Transparent;
2314 picture.Dock = System.Windows.Forms.DockStyle.Fill;
2315 picture.Location = new System.Drawing.Point(1, 1);
2316 picture.Margin = new System.Windows.Forms.Padding(0);
2317 picture.Name = "pictureBox" + aField;
2319 BitmapField field = (BitmapField)aField;
2320 picture.Image = field.Bitmap;
2324 else if (aField is AudioVisualizerField)
2326 //Create picture box
2327 PictureBox picture = new PictureBox();
2328 picture.AutoSize = true;
2329 picture.BackColor = System.Drawing.Color.Transparent;
2330 picture.Dock = System.Windows.Forms.DockStyle.Fill;
2331 picture.Location = new System.Drawing.Point(1, 1);
2332 picture.Margin = new System.Windows.Forms.Padding(0);
2333 picture.Name = "pictureBox" + aField;
2334 picture.SizeChanged += (sender, e) =>
2336 // Somehow bitmap created when our from is invisible are not working
2337 // Mark our form visibility status
2338 bool visible = Visible;
2339 // Make sure it's visible
2341 // Adjust our bitmap size when control size changes
2342 picture.Image = new System.Drawing.Bitmap(picture.Width, picture.Height, PixelFormat.Format32bppArgb);
2343 // Restore our form visibility
2350 //TODO: Handle recording field?
2356 /// Called when the user selected a new display type.
2358 /// <param name="sender"></param>
2359 /// <param name="e"></param>
2360 private void comboBoxDisplayType_SelectedIndexChanged(object sender, EventArgs e)
2362 //Store the selected display type in our settings
2363 Properties.Settings.Default.CurrentDisplayIndex = comboBoxDisplayType.SelectedIndex;
2364 cds.DisplayType = comboBoxDisplayType.SelectedIndex;
2365 Properties.Settings.Default.Save();
2367 //Try re-opening the display connection if we were already connected.
2368 //Otherwise just update our status to reflect display type change.
2369 if (iDisplay.IsOpen())
2371 OpenDisplayConnection();
2379 private void maskedTextBoxTimerInterval_TextChanged(object sender, EventArgs e)
2381 if (maskedTextBoxTimerInterval.Text != "")
2383 int interval = Convert.ToInt32(maskedTextBoxTimerInterval.Text);
2387 iTimerDisplay.Interval = interval;
2388 cds.TimerInterval = iTimerDisplay.Interval;
2389 Properties.Settings.Default.Save();
2394 private void maskedTextBoxMinFontSize_TextChanged(object sender, EventArgs e)
2396 if (maskedTextBoxMinFontSize.Text != "")
2398 int minFontSize = Convert.ToInt32(maskedTextBoxMinFontSize.Text);
2400 if (minFontSize > 0)
2402 cds.MinFontSize = minFontSize;
2403 Properties.Settings.Default.Save();
2404 //We need to recreate our layout for that change to take effect
2405 UpdateTableLayoutPanel(iCurrentClientData);
2411 private void maskedTextBoxScrollingSpeed_TextChanged(object sender, EventArgs e)
2413 if (maskedTextBoxScrollingSpeed.Text != "")
2415 int scrollingSpeed = Convert.ToInt32(maskedTextBoxScrollingSpeed.Text);
2417 if (scrollingSpeed > 0)
2419 cds.ScrollingSpeedInPixelsPerSecond = scrollingSpeed;
2420 Properties.Settings.Default.Save();
2421 //We need to recreate our layout for that change to take effect
2422 UpdateTableLayoutPanel(iCurrentClientData);
2427 private void textBoxScrollLoopSeparator_TextChanged(object sender, EventArgs e)
2429 cds.Separator = textBoxScrollLoopSeparator.Text;
2430 Properties.Settings.Default.Save();
2432 //Update our text fields
2433 foreach (MarqueeLabel ctrl in iTableLayoutPanel.Controls)
2435 ctrl.Separator = cds.Separator;
2440 private void buttonPowerOn_Click(object sender, EventArgs e)
2445 private void buttonPowerOff_Click(object sender, EventArgs e)
2447 iDisplay.PowerOff();
2450 private void buttonShowClock_Click(object sender, EventArgs e)
2455 private void buttonHideClock_Click(object sender, EventArgs e)
2460 private void buttonUpdate_Click(object sender, EventArgs e)
2462 InstallUpdateSyncWithInfo();
2470 if (!iDisplay.IsOpen())
2475 //Devices like MDM166AA don't support windowing and frame rendering must be stopped while showing our clock
2476 iSkipFrameRendering = true;
2479 iDisplay.SwapBuffers();
2480 //Then show our clock
2481 iDisplay.ShowClock();
2489 if (!iDisplay.IsOpen())
2494 //Devices like MDM166AA don't support windowing and frame rendering must be stopped while showing our clock
2495 iSkipFrameRendering = false;
2496 iDisplay.HideClock();
2499 private void InstallUpdateSyncWithInfo()
2501 UpdateCheckInfo info = null;
2503 if (ApplicationDeployment.IsNetworkDeployed)
2505 ApplicationDeployment ad = ApplicationDeployment.CurrentDeployment;
2509 info = ad.CheckForDetailedUpdate();
2512 catch (DeploymentDownloadException dde)
2515 "The new version of the application cannot be downloaded at this time. \n\nPlease check your network connection, or try again later. Error: " +
2519 catch (InvalidDeploymentException ide)
2522 "Cannot check for a new version of the application. The ClickOnce deployment is corrupt. Please redeploy the application and try again. Error: " +
2526 catch (InvalidOperationException ioe)
2529 "This application cannot be updated. It is likely not a ClickOnce application. Error: " +
2534 if (info.UpdateAvailable)
2536 Boolean doUpdate = true;
2538 if (!info.IsUpdateRequired)
2541 MessageBox.Show("An update is available. Would you like to update the application now?",
2542 "Update Available", MessageBoxButtons.OKCancel);
2543 if (!(DialogResult.OK == dr))
2550 // Display a message that the application MUST reboot. Display the minimum required version.
2551 MessageBox.Show("This application has detected a mandatory update from your current " +
2552 "version to version " + info.MinimumRequiredVersion.ToString() +
2553 ". The application will now install the update and restart.",
2554 "Update Available", MessageBoxButtons.OK,
2555 MessageBoxIcon.Information);
2563 MessageBox.Show("The application has been upgraded, and will now restart.");
2564 Application.Restart();
2566 catch (DeploymentDownloadException dde)
2569 "Cannot install the latest version of the application. \n\nPlease check your network connection, or try again later. Error: " +
2577 MessageBox.Show("You are already running the latest version.", "Application up-to-date");
2586 private void SysTrayHideShow()
2592 WindowState = FormWindowState.Normal;
2597 /// Use to handle minimize events.
2599 /// <param name="sender"></param>
2600 /// <param name="e"></param>
2601 private void MainForm_SizeChanged(object sender, EventArgs e)
2603 if (WindowState == FormWindowState.Minimized && Properties.Settings.Default.MinimizeToTray)
2615 /// <param name="sender"></param>
2616 /// <param name="e"></param>
2617 private void tableLayoutPanel_SizeChanged(object sender, EventArgs e)
2619 //Our table layout size has changed which means our display size has changed.
2620 //We need to re-create our bitmap.
2621 iCreateBitmap = true;
2625 /// Broadcast messages to subscribers.
2627 /// <param name="message"></param>
2628 protected override void WndProc(ref Message aMessage)
2630 if (OnWndProc != null)
2632 OnWndProc(ref aMessage);
2635 base.WndProc(ref aMessage);
2638 private void iCheckBoxCecEnabled_CheckedChanged(object sender, EventArgs e)
2644 private void comboBoxHdmiPort_SelectedIndexChanged(object sender, EventArgs e)
2646 //Save CEC HDMI port
2647 Properties.Settings.Default.CecHdmiPort = Convert.ToByte(comboBoxHdmiPort.SelectedIndex);
2648 Properties.Settings.Default.CecHdmiPort++;
2649 Properties.Settings.Default.Save();
2657 private void ResetCec()
2659 if (iCecManager == null)
2661 //Thus skipping initial UI setup
2667 if (Properties.Settings.Default.CecEnabled)
2669 iCecManager.Start(Handle, "CEC",
2670 Properties.Settings.Default.CecHdmiPort);
2679 private async void ResetHarmonyAsync(bool aForceAuth=false)
2681 // ConnectAsync already if we have an existing session cookie
2682 if (Properties.Settings.Default.HarmonyEnabled)
2686 iButtonHarmonyConnect.Enabled = false;
2687 await ConnectHarmonyAsync(aForceAuth);
2689 catch (Exception ex)
2691 Trace.WriteLine("Exception thrown by ConnectHarmonyAsync");
2692 Trace.WriteLine(ex.ToString());
2696 iButtonHarmonyConnect.Enabled = true;
2704 /// <param name="sender"></param>
2705 /// <param name="e"></param>
2706 private void iButtonHarmonyConnect_Click(object sender, EventArgs e)
2708 // User is explicitaly trying to connect
2709 //Reset Harmony Hub connection forcing authentication
2710 ResetHarmonyAsync(true);
2716 /// <param name="sender"></param>
2717 /// <param name="e"></param>
2718 private void iCheckBoxHarmonyEnabled_CheckedChanged(object sender, EventArgs e)
2720 iButtonHarmonyConnect.Enabled = iCheckBoxHarmonyEnabled.Checked;
2726 private void SetupCecLogLevel()
2729 iCecManager.Client.LogLevel = 0;
2731 if (checkBoxCecLogError.Checked)
2732 iCecManager.Client.LogLevel |= (int) CecLogLevel.Error;
2734 if (checkBoxCecLogWarning.Checked)
2735 iCecManager.Client.LogLevel |= (int) CecLogLevel.Warning;
2737 if (checkBoxCecLogNotice.Checked)
2738 iCecManager.Client.LogLevel |= (int) CecLogLevel.Notice;
2740 if (checkBoxCecLogTraffic.Checked)
2741 iCecManager.Client.LogLevel |= (int) CecLogLevel.Traffic;
2743 if (checkBoxCecLogDebug.Checked)
2744 iCecManager.Client.LogLevel |= (int) CecLogLevel.Debug;
2746 iCecManager.Client.FilterOutPollLogs = checkBoxCecLogNoPoll.Checked;
2750 private void ButtonStartIdleClient_Click(object sender, EventArgs e)
2755 private void buttonClearLogs_Click(object sender, EventArgs e)
2757 richTextBoxLogs.Clear();
2760 private void checkBoxCecLogs_CheckedChanged(object sender, EventArgs e)
2770 /// Get the current event based on event tree view selection.
2772 /// <returns></returns>
2773 private Ear.Event CurrentEvent()
2775 //Walk up the tree from the selected node to find our event
2776 TreeNode node = iTreeViewEvents.SelectedNode;
2777 Ear.Event selectedEvent = null;
2778 while (node != null)
2780 if (node.Tag is Ear.Event)
2782 selectedEvent = (Ear.Event) node.Tag;
2788 return selectedEvent;
2792 /// Get the current action based on event tree view selection
2794 /// <returns></returns>
2795 private Ear.Action CurrentAction()
2797 TreeNode node = iTreeViewEvents.SelectedNode;
2798 if (node != null && node.Tag is Ear.Action)
2800 return (Ear.Action) node.Tag;
2809 /// <returns></returns>
2810 private Ear.Object CurrentEarObject()
2812 Ear.Action a = CurrentAction();
2813 Ear.Event e = CurrentEvent();
2824 /// Get the current action based on event tree view selection
2826 /// <returns></returns>
2827 private Ear.Object CurrentEarParent()
2829 TreeNode node = iTreeViewEvents.SelectedNode;
2830 if (node == null || node.Parent == null)
2835 if (node.Parent.Tag is Ear.Object)
2837 return (Ear.Object)node.Parent.Tag;
2840 if (node.Parent.Parent != null && node.Parent.Parent.Tag is Ear.Object)
2842 //Can be the case for events
2843 return (Ear.Object)node.Parent.Parent.Tag;
2853 /// <param name="sender"></param>
2854 /// <param name="e"></param>
2855 private void buttonActionAdd_Click(object sender, EventArgs e)
2857 Ear.Object parent = CurrentEarObject();
2858 if (parent == null )
2860 //We did not find a corresponding event or action
2864 FormEditObject<Ear.Action> ea = new FormEditObject<Ear.Action>();
2865 ea.Text = "Add action";
2866 DialogResult res = CodeProject.Dialog.DlgBox.ShowDialog(ea);
2867 if (res == DialogResult.OK)
2869 parent.Objects.Add(ea.Object);
2870 Properties.Settings.Default.Save();
2871 // We want to select the parent so that one can easily add another action to the same collection
2872 PopulateTreeViewEvents(parent);
2879 /// <param name="sender"></param>
2880 /// <param name="e"></param>
2881 private void buttonActionEdit_Click(object sender, EventArgs e)
2883 Ear.Action selectedAction = CurrentAction();
2884 Ear.Object parent = CurrentEarParent()
2886 if (parent == null || selectedAction == null)
2888 //We did not find a corresponding parent
2892 FormEditObject<Ear.Action> ea = new FormEditObject<Ear.Action>();
2893 ea.Text = "Edit action";
2894 ea.Object = selectedAction;
2895 int actionIndex = iTreeViewEvents.SelectedNode.Index;
2896 DialogResult res = CodeProject.Dialog.DlgBox.ShowDialog(ea);
2897 if (res == DialogResult.OK)
2899 //Make sure we keep the same children as before
2900 ea.Object.Objects = parent.Objects[actionIndex].Objects;
2902 parent.Objects[actionIndex]=ea.Object;
2903 //Save and rebuild our event tree view
2904 Properties.Settings.Default.Save();
2905 PopulateTreeViewEvents(ea.Object);
2912 /// <param name="sender"></param>
2913 /// <param name="e"></param>
2914 private void buttonActionDelete_Click(object sender, EventArgs e)
2916 Ear.Action action = CurrentAction();
2919 //Must select action node
2923 Properties.Settings.Default.EarManager.RemoveAction(action);
2924 Properties.Settings.Default.Save();
2925 PopulateTreeViewEvents();
2931 /// <param name="sender"></param>
2932 /// <param name="e"></param>
2933 private void buttonActionTest_Click(object sender, EventArgs e)
2935 Ear.Action a = CurrentAction();
2940 iTreeViewEvents.Focus();
2946 /// <param name="sender"></param>
2947 /// <param name="e"></param>
2948 private void buttonActionMoveUp_Click(object sender, EventArgs e)
2950 Ear.Action a = CurrentAction();
2952 //Action already at the top of the list
2953 iTreeViewEvents.SelectedNode.Index == 0)
2958 //Swap actions in event's action list
2959 Ear.Object parent = CurrentEarParent();
2960 int currentIndex = iTreeViewEvents.SelectedNode.Index;
2961 Ear.Action movingUp = parent.Objects[currentIndex] as Ear.Action;
2962 Ear.Action movingDown = parent.Objects[currentIndex-1] as Ear.Action;
2963 parent.Objects[currentIndex] = movingDown;
2964 parent.Objects[currentIndex-1] = movingUp;
2966 //Save and populate our tree again
2967 Properties.Settings.Default.Save();
2968 PopulateTreeViewEvents(a);
2974 /// <param name="sender"></param>
2975 /// <param name="e"></param>
2976 private void buttonActionMoveDown_Click(object sender, EventArgs e)
2978 Ear.Action a = CurrentAction();
2980 //Action already at the bottom of the list
2981 iTreeViewEvents.SelectedNode.Index == iTreeViewEvents.SelectedNode.Parent.Nodes.Count - 1)
2986 //Swap actions in event's action list
2987 Ear.Object parent = CurrentEarParent();
2988 int currentIndex = iTreeViewEvents.SelectedNode.Index;
2989 Ear.Action movingDown = parent.Objects[currentIndex] as Ear.Action;
2990 Ear.Action movingUp = parent.Objects[currentIndex + 1] as Ear.Action;
2991 parent.Objects[currentIndex] = movingUp;
2992 parent.Objects[currentIndex + 1] = movingDown;
2994 //Save and populate our tree again
2995 Properties.Settings.Default.Save();
2996 PopulateTreeViewEvents(a);
3003 /// <param name="sender"></param>
3004 /// <param name="e"></param>
3005 private void buttonEventTest_Click(object sender, EventArgs e)
3007 Ear.Event earEvent = CurrentEvent();
3008 if (earEvent != null)
3015 /// Manages events and actions buttons according to selected item in event tree.
3017 /// <param name="sender"></param>
3018 /// <param name="e"></param>
3019 private void iTreeViewEvents_AfterSelect(object sender, TreeViewEventArgs e)
3027 private void UpdateEventView()
3029 //One can always add an event
3030 buttonEventAdd.Enabled = true;
3032 //Enable buttons according to selected item
3033 buttonActionAdd.Enabled =
3034 buttonEventTest.Enabled =
3035 buttonEventDelete.Enabled =
3036 buttonEventEdit.Enabled =
3037 CurrentEvent() != null;
3039 Ear.Action currentAction = CurrentAction();
3040 //If an action is selected enable the following buttons
3041 buttonActionTest.Enabled =
3042 buttonActionDelete.Enabled =
3043 buttonActionMoveUp.Enabled =
3044 buttonActionMoveDown.Enabled =
3045 buttonActionEdit.Enabled =
3046 currentAction != null;
3048 if (currentAction != null)
3050 //If an action is selected enable move buttons if needed
3051 buttonActionMoveUp.Enabled = iTreeViewEvents.SelectedNode.Index != 0;
3052 buttonActionMoveDown.Enabled = iTreeViewEvents.SelectedNode.Index <
3053 iTreeViewEvents.SelectedNode.Parent.Nodes.Count - 1;
3060 /// <param name="sender"></param>
3061 /// <param name="e"></param>
3062 private void buttonEventAdd_Click(object sender, EventArgs e)
3064 FormEditObject<Ear.Event> ea = new FormEditObject<Ear.Event>();
3065 ea.Text = "Add event";
3066 DialogResult res = CodeProject.Dialog.DlgBox.ShowDialog(ea);
3067 if (res == DialogResult.OK)
3069 Properties.Settings.Default.EarManager.Events.Add(ea.Object);
3070 Properties.Settings.Default.Save();
3071 PopulateTreeViewEvents(ea.Object);
3078 /// <param name="sender"></param>
3079 /// <param name="e"></param>
3080 private void buttonEventDelete_Click(object sender, EventArgs e)
3082 Ear.Event currentEvent = CurrentEvent();
3083 if (currentEvent == null)
3085 //Must select action node
3089 Properties.Settings.Default.EarManager.Events.Remove(currentEvent);
3090 Properties.Settings.Default.Save();
3091 PopulateTreeViewEvents();
3097 /// <param name="sender"></param>
3098 /// <param name="e"></param>
3099 private void buttonEventEdit_Click(object sender, EventArgs e)
3101 Ear.Event selectedEvent = CurrentEvent();
3102 if (selectedEvent == null)
3104 //We did not find a corresponding event
3108 FormEditObject<Ear.Event> ea = new FormEditObject<Ear.Event>();
3109 ea.Text = "Edit event";
3110 ea.Object = selectedEvent;
3111 int index = iTreeViewEvents.SelectedNode.Index;
3112 DialogResult res = CodeProject.Dialog.DlgBox.ShowDialog(ea);
3113 if (res == DialogResult.OK)
3115 //Make sure we keep the same actions as before
3116 ea.Object.Objects = Properties.Settings.Default.EarManager.Events[index].Objects;
3118 Properties.Settings.Default.EarManager.Events[index] = ea.Object;
3119 //Save and rebuild our event tree view
3120 Properties.Settings.Default.Save();
3121 PopulateTreeViewEvents(ea.Object);
3128 /// <param name="sender"></param>
3129 /// <param name="e"></param>
3130 private void iTreeViewEvents_Leave(object sender, EventArgs e)
3132 //Make sure our event tree never looses focus
3133 ((TreeView) sender).Focus();
3137 /// Called whenever we loose connection with our HarmonyHub.
3139 /// <param name="aRequestWasCancelled"></param>
3140 private void HarmonyConnectionClosed(object aSender, bool aClosedByServer)
3142 if (aClosedByServer)
3144 //Try reconnect then
3145 #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
3146 BeginInvoke(new MethodInvoker(delegate () { ResetHarmonyAsync(); }));
3147 #pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
3153 int iHarmonyReconnectTries = 0;
3154 const int KHarmonyMaxReconnectTries = 10;
3159 /// <returns></returns>
3160 private async Task ConnectHarmonyAsync(bool aForceAuth=false)
3162 if (Program.HarmonyClient != null)
3164 await Program.HarmonyClient.CloseAsync();
3167 bool success = false;
3169 //Reset Harmony client & config
3170 Program.HarmonyClient = null;
3171 Program.HarmonyConfig = null;
3172 iTreeViewHarmony.Nodes.Clear();
3174 Trace.WriteLine("Harmony: Connecting... ");
3175 //First create our client and login
3176 //Tip: Set keep-alive to false when testing reconnection process
3177 Program.HarmonyClient = new HarmonyHub.Client(iTextBoxHarmonyHubAddress.Text, true);
3178 Program.HarmonyClient.OnConnectionClosed += HarmonyConnectionClosed;
3180 string authToken = Properties.Settings.Default.LogitechAuthToken;
3181 if (!string.IsNullOrEmpty(authToken) && !aForceAuth)
3183 Trace.WriteLine("Harmony: Reusing token: {0}", authToken);
3184 success = await Program.HarmonyClient.TryOpenAsync(authToken);
3187 if (!Program.HarmonyClient.IsReady || !success
3188 // Only first failure triggers new Harmony server AUTH
3189 // That's to avoid calling upon Logitech servers too often
3190 && iHarmonyReconnectTries == 0 )
3192 //We failed to connect using our token
3194 Trace.WriteLine("Harmony: Reseting authentication token!");
3195 Properties.Settings.Default.LogitechAuthToken = "";
3196 Properties.Settings.Default.Save();
3198 Trace.WriteLine("Harmony: Authenticating with Logitech servers...");
3199 success = await Program.HarmonyClient.TryOpenAsync();
3200 //Persist our authentication token in our setting
3203 Trace.WriteLine("Harmony: Saving authentication token.");
3204 Properties.Settings.Default.LogitechAuthToken = Program.HarmonyClient.Token;
3205 Properties.Settings.Default.Save();
3209 // I've seen this failing with "Policy lookup failed on server".
3210 Program.HarmonyConfig = await Program.HarmonyClient.TryGetConfigAsync();
3211 if (Program.HarmonyConfig == null)
3217 // So we now have our Harmony Configuration
3218 PopulateTreeViewHarmony(Program.HarmonyConfig);
3219 // Make sure harmony command actions are showing device name instead of device id
3220 PopulateTreeViewEvents(CurrentEarObject());
3223 // TODO: Consider putting the retry logic one level higher in ResetHarmonyAsync
3226 // See if we need to keep trying
3227 if (iHarmonyReconnectTries < KHarmonyMaxReconnectTries)
3229 iHarmonyReconnectTries++;
3230 Trace.WriteLine("Harmony: Failed to connect, try again: " + iHarmonyReconnectTries);
3231 await ConnectHarmonyAsync();
3235 Trace.WriteLine("Harmony: Failed to connect, giving up!");
3236 iHarmonyReconnectTries = 0;
3237 // TODO: Could use a data member as timer rather than a new instance.
3238 // Try that again in 5 minutes then.
3239 // Using Windows Form timer to make sure we run in the UI thread.
3240 System.Windows.Forms.Timer timer = new System.Windows.Forms.Timer();
3241 timer.Tick += async delegate (object sender, EventArgs e)
3243 // Stop our timer first as we won't need it anymore
3244 (sender as System.Windows.Forms.Timer).Stop();
3245 // Then try to connect again
3246 await ConnectHarmonyAsync();
3248 timer.Interval = 300000;
3254 // We are connected with a valid Harmony Configuration
3255 // Reset our tries counter then
3256 iHarmonyReconnectTries = 0;
3263 /// <param name="aConfig"></param>
3264 private void PopulateTreeViewHarmony(HarmonyHub.Config aConfig)
3266 iTreeViewHarmony.Nodes.Clear();
3268 foreach (HarmonyHub.Device device in aConfig.Devices)
3270 TreeNode deviceNode = iTreeViewHarmony.Nodes.Add(device.Id, $"{device.Label} ({device.DeviceTypeDisplayName}/{device.Model})");
3271 deviceNode.Tag = device;
3273 foreach (HarmonyHub.ControlGroup cg in device.ControlGroups)
3275 TreeNode cgNode = deviceNode.Nodes.Add(cg.Name);
3278 foreach (HarmonyHub.Function f in cg.Functions)
3280 TreeNode fNode = cgNode.Nodes.Add(f.Label);
3286 //treeViewConfig.ExpandAll();
3289 private async void iTreeViewHarmony_NodeMouseDoubleClick(object sender, TreeNodeMouseClickEventArgs e)
3291 //Upon function node double click we execute it
3292 var tag = e.Node.Tag as HarmonyHub.Function;
3293 if (tag != null && e.Node.Parent.Parent.Tag is HarmonyHub.Device)
3295 HarmonyHub.Function f = tag;
3296 HarmonyHub.Device d = (HarmonyHub.Device)e.Node.Parent.Parent.Tag;
3298 Trace.WriteLine($"Harmony: Sending {f.Label} to {d.Label}...");
3300 await Program.HarmonyClient.TrySendKeyPressAsync(d.Id, f.Action.Command);