Published v1.3.5.
Updated Harmony library.
Removed Harmony Logitech password and username.
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;
38 using NAudio.CoreAudioApi;
39 using NAudio.CoreAudioApi.Interfaces;
40 using System.Runtime.InteropServices;
41 using System.Security;
46 using SharpDisplayClient;
48 using MiniDisplayInterop;
49 using SharpLib.Display;
50 using Ear = SharpLib.Ear;
52 namespace SharpDisplayManager
55 public delegate uint ColorProcessingDelegate(int aX, int aY, uint aPixel);
57 public delegate int CoordinateTranslationDelegate(System.Drawing.Bitmap aBmp, int aInt);
59 //Delegates are used for our thread safe method
60 public delegate void AddClientDelegate(string aSessionId, ICallback aCallback);
62 public delegate void RemoveClientDelegate(string aSessionId);
64 public delegate void SetFieldDelegate(string SessionId, DataField aField);
66 public delegate void SetFieldsDelegate(string SessionId, System.Collections.Generic.IList<DataField> aFields);
68 public delegate void SetLayoutDelegate(string SessionId, TableLayout aLayout);
70 public delegate void SetClientNameDelegate(string aSessionId, string aName);
72 public delegate void SetClientPriorityDelegate(string aSessionId, uint aPriority);
74 public delegate void PlainUpdateDelegate();
76 public delegate void WndProcDelegate(ref Message aMessage);
79 /// Our Display manager main form
81 [System.ComponentModel.DesignerCategory("Form")]
82 public partial class FormMain : FormMainHid, IMMNotificationClient
84 //public Manager iManager = new Manager();
85 DateTime LastTickTime;
87 System.Drawing.Bitmap iBmp;
88 bool iCreateBitmap; //Workaround render to bitmap issues when minimized
89 ServiceHost iServiceHost;
90 // Our collection of clients sorted by session id.
91 public Dictionary<string, ClientData> iClients;
92 // The name of the client which informations are currently displayed.
93 public string iCurrentClientSessionId;
94 ClientData iCurrentClientData;
98 public bool iSkipFrameRendering;
99 //Function pointer for pixel color filtering
100 ColorProcessingDelegate iColorFx;
101 //Function pointer for pixel X coordinate intercept
102 CoordinateTranslationDelegate iScreenX;
103 //Function pointer for pixel Y coordinate intercept
104 CoordinateTranslationDelegate iScreenY;
106 private MMDeviceEnumerator iMultiMediaDeviceEnumerator;
107 private MMDevice iMultiMediaDevice;
109 private NetworkManager iNetworkManager;
112 /// CEC - Consumer Electronic Control.
113 /// Notably used to turn TV on and off as Windows broadcast monitor on and off notifications.
115 private ConsumerElectronicControl iCecManager;
118 /// Manage run when Windows startup option
120 private StartupManager iStartupManager;
123 /// System notification icon used to hide our application from the task bar.
125 private SharpLib.Notification.Control iNotifyIcon;
128 /// System recording notification icon.
130 private SharpLib.Notification.Control iRecordingNotification;
135 RichTextBoxTraceListener iWriter;
139 /// Allow user to receive window messages;
141 public event WndProcDelegate OnWndProc;
145 if (Properties.Settings.Default.EarManager == null)
147 //No actions in our settings yet
148 Properties.Settings.Default.EarManager = new EarManager();
152 // We loaded events and actions from our settings
153 // Internalizer apparently skips constructor so we need to initialize it here
154 // Though I reckon that should only be needed when loading an empty EAR manager I guess.
155 Properties.Settings.Default.EarManager.Construct();
157 iSkipFrameRendering = false;
159 iCurrentClientSessionId = "";
160 iCurrentClientData = null;
161 LastTickTime = DateTime.Now;
162 //Instantiate our display and register for events notifications
163 iDisplay = new Display();
164 iDisplay.OnOpened += OnDisplayOpened;
165 iDisplay.OnClosed += OnDisplayClosed;
167 iClients = new Dictionary<string, ClientData>();
168 iStartupManager = new StartupManager();
169 iNotifyIcon = new SharpLib.Notification.Control();
170 iRecordingNotification = new SharpLib.Notification.Control();
172 //Have our designer initialize its controls
173 InitializeComponent();
175 //Redirect console output
176 iWriter = new RichTextBoxTraceListener(richTextBoxLogs);
177 Trace.Listeners.Add(iWriter);
179 //Populate device types
180 PopulateDeviceTypes();
182 //Initial status update
185 //We have a bug when drawing minimized and reusing our bitmap
186 //Though I could not reproduce it on Windows 10
187 iBmp = new System.Drawing.Bitmap(iTableLayoutPanel.Width, iTableLayoutPanel.Height,
188 PixelFormat.Format32bppArgb);
189 iCreateBitmap = false;
191 //Minimize our window if desired
192 if (Properties.Settings.Default.StartMinimized)
194 WindowState = FormWindowState.Minimized;
202 /// <param name="sender"></param>
203 /// <param name="e"></param>
204 private void MainForm_Load(object sender, EventArgs e)
206 //Check if we are running a Click Once deployed application
207 if (ApplicationDeployment.IsNetworkDeployed)
209 //This is a proper Click Once installation, fetch and show our version number
210 this.Text += " - v" + ApplicationDeployment.CurrentDeployment.CurrentVersion;
214 //Not a proper Click Once installation, assuming development build then
215 this.Text += " - development";
219 iMultiMediaDeviceEnumerator = new MMDeviceEnumerator();
220 iMultiMediaDeviceEnumerator.RegisterEndpointNotificationCallback(this);
221 UpdateAudioDeviceAndMasterVolumeThreadSafe();
224 iNetworkManager = new NetworkManager();
225 iNetworkManager.OnConnectivityChanged += OnConnectivityChanged;
226 UpdateNetworkStatus();
229 iCecManager = new ConsumerElectronicControl();
230 OnWndProc += iCecManager.OnWndProc;
237 PopulateTreeViewEvents();
239 //Setup notification icon
242 //Setup recording notification
243 SetupRecordingNotification();
245 // To make sure start up with minimize to tray works
246 if (WindowState == FormWindowState.Minimized && Properties.Settings.Default.MinimizeToTray)
252 //When not debugging we want the screen to be empty until a client takes over
255 //When developing we want at least one client for testing
256 StartNewClient("abcdefghijklmnopqrst-0123456789", "ABCDEFGHIJKLMNOPQRST-0123456789");
259 //Open display connection on start-up if needed
260 if (Properties.Settings.Default.DisplayConnectOnStartup)
262 OpenDisplayConnection();
265 //Start our server so that we can get client requests
268 //Register for HID events
269 RegisterHidDevices();
271 //Start Idle client if needed
272 if (Properties.Settings.Default.StartIdleClient)
279 /// Called when our display is opened.
281 /// <param name="aDisplay"></param>
282 private void OnDisplayOpened(Display aDisplay)
284 //Make sure we resume frame rendering
285 iSkipFrameRendering = false;
287 //Set our screen size now that our display is connected
288 //Our panelDisplay is the container of our tableLayoutPanel
289 //tableLayoutPanel will resize itself to fit the client size of our panelDisplay
290 //panelDisplay needs an extra 2 pixels for borders on each sides
291 //tableLayoutPanel will eventually be the exact size of our display
292 Size size = new Size(iDisplay.WidthInPixels() + 2, iDisplay.HeightInPixels() + 2);
293 panelDisplay.Size = size;
295 //Our display was just opened, update our UI
297 //Initiate asynchronous request
298 iDisplay.RequestFirmwareRevision();
301 UpdateMasterVolumeThreadSafe();
303 UpdateNetworkStatus();
306 //Testing icon in debug, no arm done if icon not supported
307 //iDisplay.SetIconStatus(Display.TMiniDisplayIconType.EMiniDisplayIconRecording, 0, 1);
308 //iDisplay.SetAllIconsStatus(2);
314 private static void AddActionsToTreeNode(TreeNode aParentNode, Ear.Object aObject)
316 foreach (Ear.Action a in aObject.Objects.OfType<Ear.Action>())
319 TreeNode actionNode = aParentNode.Nodes.Add(a.Brief());
321 //Use color from parent unless our action itself is disabled
322 actionNode.ForeColor = a.Enabled ? aParentNode.ForeColor : Color.DimGray;
324 AddActionsToTreeNode(actionNode,a);
332 /// <param name="aObject"></param>
333 /// <param name="aNode"></param>
334 private static TreeNode FindTreeNodeForEarObject(Ear.Object aObject, TreeNode aNode)
336 if (aNode.Tag == aObject)
341 foreach (TreeNode n in aNode.Nodes)
343 TreeNode found = FindTreeNodeForEarObject(aObject,n);
357 /// <param name="aObject"></param>
358 private void SelectEarObject(Ear.Object aObject)
360 foreach (TreeNode n in iTreeViewEvents.Nodes)
362 TreeNode found = FindTreeNodeForEarObject(aObject, n);
365 iTreeViewEvents.SelectedNode=found;
366 iTreeViewEvents.Focus();
373 /// Populate tree view with events and actions
375 private void PopulateTreeViewEvents(Ear.Object aSelectedObject=null)
378 iTreeViewEvents.Nodes.Clear();
379 //Populate registered events
380 foreach (Ear.Event e in Properties.Settings.Default.EarManager.Events)
382 //Create our event node
383 //Work out the name of our node
384 string eventNodeName = "";
385 if (!string.IsNullOrEmpty(e.Name))
387 //That event has a proper name, use it then
388 eventNodeName = $"{e.Name} - {e.Brief()}";
392 //Unnamed events just use brief
393 eventNodeName = e.Brief();
396 TreeNode eventNode = iTreeViewEvents.Nodes.Add(eventNodeName);
397 eventNode.Tag = e; //For easy access to our event
400 //Dim our nodes if disabled
401 eventNode.ForeColor = Color.DimGray;
404 //Add event description as child node
405 eventNode.Nodes.Add(e.AttributeDescription).ForeColor = eventNode.ForeColor;
406 //Create child node for actions root
407 TreeNode actionsNode = eventNode.Nodes.Add("Actions");
408 actionsNode.ForeColor = eventNode.ForeColor;
410 // Recursively add our actions for that event
411 AddActionsToTreeNode(actionsNode,e);
414 iTreeViewEvents.ExpandAll();
416 if (aSelectedObject != null)
418 SelectEarObject(aSelectedObject);
421 // Just to be safe in case the selection did not work
426 /// Called when our display is closed.
428 /// <param name="aDisplay"></param>
429 private void OnDisplayClosed(Display aDisplay)
431 //Our display was just closed, update our UI consequently
435 public void OnConnectivityChanged(NetworkManager aNetwork, NLM_CONNECTIVITY newConnectivity)
437 //Update network status
438 UpdateNetworkStatus();
442 /// Update our Network Status
444 private void UpdateNetworkStatus()
446 if (iDisplay.IsOpen())
448 iDisplay.SetIconOnOff(MiniDisplay.IconType.Internet,
449 iNetworkManager.NetworkListManager.IsConnectedToInternet);
450 iDisplay.SetIconOnOff(MiniDisplay.IconType.NetworkSignal, iNetworkManager.NetworkListManager.IsConnected);
455 int iLastNetworkIconIndex = 0;
456 int iUpdateCountSinceLastNetworkAnimation = 0;
461 private void UpdateNetworkSignal(DateTime aLastTickTime, DateTime aNewTickTime)
463 iUpdateCountSinceLastNetworkAnimation++;
464 iUpdateCountSinceLastNetworkAnimation = iUpdateCountSinceLastNetworkAnimation%4;
466 if (iDisplay.IsOpen() && iNetworkManager.NetworkListManager.IsConnected &&
467 iUpdateCountSinceLastNetworkAnimation == 0)
469 int iconCount = iDisplay.IconCount(MiniDisplay.IconType.NetworkSignal);
472 //Prevents div by zero and other undefined behavior
475 iLastNetworkIconIndex++;
476 iLastNetworkIconIndex = iLastNetworkIconIndex%(iconCount*2);
477 for (int i = 0; i < iconCount; i++)
479 if (i < iLastNetworkIconIndex && !(i == 0 && iLastNetworkIconIndex > 3) &&
480 !(i == 1 && iLastNetworkIconIndex > 4))
482 iDisplay.SetIconOn(MiniDisplay.IconType.NetworkSignal, i);
486 iDisplay.SetIconOff(MiniDisplay.IconType.NetworkSignal, i);
495 /// Receive volume change notification and reflect changes on our slider.
497 /// <param name="data"></param>
498 public void OnVolumeNotificationThreadSafe(AudioVolumeNotificationData data)
500 UpdateMasterVolumeThreadSafe();
504 /// Update master volume when user moves our slider.
506 /// <param name="sender"></param>
507 /// <param name="e"></param>
508 private void trackBarMasterVolume_Scroll(object sender, EventArgs e)
510 //Just like Windows Volume Mixer we unmute if the volume is adjusted
511 iMultiMediaDevice.AudioEndpointVolume.Mute = false;
512 //Set volume level according to our volume slider new position
513 iMultiMediaDevice.AudioEndpointVolume.MasterVolumeLevelScalar = trackBarMasterVolume.Value/100.0f;
518 /// Mute check box changed.
520 /// <param name="sender"></param>
521 /// <param name="e"></param>
522 private void checkBoxMute_CheckedChanged(object sender, EventArgs e)
524 iMultiMediaDevice.AudioEndpointVolume.Mute = checkBoxMute.Checked;
528 /// Device State Changed
530 public void OnDeviceStateChanged([MarshalAs(UnmanagedType.LPWStr)] string deviceId,
531 [MarshalAs(UnmanagedType.I4)] DeviceState newState)
538 public void OnDeviceAdded([MarshalAs(UnmanagedType.LPWStr)] string pwstrDeviceId)
545 public void OnDeviceRemoved([MarshalAs(UnmanagedType.LPWStr)] string deviceId)
550 /// Default Device Changed
552 public void OnDefaultDeviceChanged(DataFlow flow, Role role,
553 [MarshalAs(UnmanagedType.LPWStr)] string defaultDeviceId)
555 if (role == Role.Multimedia && flow == DataFlow.Render)
557 UpdateAudioDeviceAndMasterVolumeThreadSafe();
562 /// Property Value Changed
564 /// <param name="pwstrDeviceId"></param>
565 /// <param name="key"></param>
566 public void OnPropertyValueChanged([MarshalAs(UnmanagedType.LPWStr)] string pwstrDeviceId, PropertyKey key)
574 /// Update master volume indicators based our current system states.
575 /// This typically includes volume levels and mute status.
577 private void UpdateMasterVolumeThreadSafe()
579 if (this.InvokeRequired)
581 //Not in the proper thread, invoke ourselves
582 PlainUpdateDelegate d = new PlainUpdateDelegate(UpdateMasterVolumeThreadSafe);
583 this.Invoke(d, new object[] {});
587 //Update volume slider
588 float volumeLevelScalar = iMultiMediaDevice.AudioEndpointVolume.MasterVolumeLevelScalar;
589 trackBarMasterVolume.Value = Convert.ToInt32(volumeLevelScalar*100);
590 //Update mute checkbox
591 checkBoxMute.Checked = iMultiMediaDevice.AudioEndpointVolume.Mute;
593 //If our display connection is open we need to update its icons
594 if (iDisplay.IsOpen())
596 //First take care our our volume level icons
597 int volumeIconCount = iDisplay.IconCount(MiniDisplay.IconType.Volume);
598 if (volumeIconCount > 0)
600 //Compute current volume level from system level and the number of segments in our display volume bar.
601 //That tells us how many segments in our volume bar needs to be turned on.
602 float currentVolume = volumeLevelScalar*volumeIconCount;
603 int segmentOnCount = Convert.ToInt32(currentVolume);
604 //Check if our segment count was rounded up, this will later be used for half brightness segment
605 bool roundedUp = segmentOnCount > currentVolume;
607 for (int i = 0; i < volumeIconCount; i++)
609 if (i < segmentOnCount)
611 //If we are dealing with our last segment and our segment count was rounded up then we will use half brightness.
612 if (i == segmentOnCount - 1 && roundedUp)
615 iDisplay.SetIconStatus(MiniDisplay.IconType.Volume, i,
616 (iDisplay.IconStatusCount(MiniDisplay.IconType.Volume) - 1)/2);
621 iDisplay.SetIconStatus(MiniDisplay.IconType.Volume, i,
622 iDisplay.IconStatusCount(MiniDisplay.IconType.Volume) - 1);
627 iDisplay.SetIconStatus(MiniDisplay.IconType.Volume, i, 0);
632 //Take care of our mute icon
633 iDisplay.SetIconOnOff(MiniDisplay.IconType.Mute, iMultiMediaDevice.AudioEndpointVolume.Mute);
641 private void UpdateAudioDeviceAndMasterVolumeThreadSafe()
643 if (this.InvokeRequired)
645 //Not in the proper thread, invoke ourselves
646 PlainUpdateDelegate d = new PlainUpdateDelegate(UpdateAudioDeviceAndMasterVolumeThreadSafe);
647 this.Invoke(d, new object[] {});
651 //We are in the correct thread just go ahead.
654 //Get our master volume
655 iMultiMediaDevice = iMultiMediaDeviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia);
657 labelDefaultAudioDevice.Text = iMultiMediaDevice.FriendlyName;
659 //Show our volume in our track bar
660 UpdateMasterVolumeThreadSafe();
662 //Register to get volume modifications
663 iMultiMediaDevice.AudioEndpointVolume.OnVolumeNotification += OnVolumeNotificationThreadSafe;
665 trackBarMasterVolume.Enabled = true;
669 Debug.WriteLine("Exception thrown in UpdateAudioDeviceAndMasterVolume");
670 Debug.WriteLine(ex.ToString());
671 //Something went wrong S/PDIF device ca throw exception I guess
672 trackBarMasterVolume.Enabled = false;
679 private void PopulateDeviceTypes()
681 int count = Display.TypeCount();
683 for (int i = 0; i < count; i++)
685 comboBoxDisplayType.Items.Add(Display.TypeName((MiniDisplay.Type) i));
692 private void SetupTrayIcon()
694 iNotifyIcon.Icon = GetIcon("vfd.ico");
695 iNotifyIcon.Text = "Sharp Display Manager";
696 iNotifyIcon.Visible = true;
698 //Double click toggles visibility - typically brings up the application
699 iNotifyIcon.DoubleClick += delegate(object obj, EventArgs args)
704 //Adding a context menu, useful to be able to exit the application
705 ContextMenu contextMenu = new ContextMenu();
706 //Context menu item to toggle visibility
707 MenuItem hideShowItem = new MenuItem("Hide/Show");
708 hideShowItem.Click += delegate(object obj, EventArgs args)
712 contextMenu.MenuItems.Add(hideShowItem);
714 //Context menu item separator
715 contextMenu.MenuItems.Add(new MenuItem("-"));
717 //Context menu exit item
718 MenuItem exitItem = new MenuItem("Exit");
719 exitItem.Click += delegate(object obj, EventArgs args)
723 contextMenu.MenuItems.Add(exitItem);
725 iNotifyIcon.ContextMenu = contextMenu;
731 private void SetupRecordingNotification()
733 iRecordingNotification.Icon = GetIcon("record.ico");
734 iRecordingNotification.Text = "No recording";
735 iRecordingNotification.Visible = false;
739 /// Access icons from embedded resources.
741 /// <param name="aName"></param>
742 /// <returns></returns>
743 public static Icon GetIcon(string aName)
745 string[] names = Assembly.GetExecutingAssembly().GetManifestResourceNames();
746 foreach (string name in names)
748 //Find a resource name that ends with the given name
749 if (name.EndsWith(aName))
751 using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(name))
753 return new Icon(stream);
763 /// Set our current client.
764 /// This will take care of applying our client layout and set data fields.
766 /// <param name="aSessionId"></param>
767 void SetCurrentClient(string aSessionId, bool aForce = false)
769 if (aSessionId == iCurrentClientSessionId)
771 //Given client is already the current one.
772 //Don't bother changing anything then.
776 ClientData requestedClientData = iClients[aSessionId];
778 //Check when was the last time we switched to that client
779 if (iCurrentClientData != null)
781 //Do not switch client if priority of current client is higher
782 if (!aForce && requestedClientData.Priority < iCurrentClientData.Priority)
788 double lastSwitchToClientSecondsAgo = (DateTime.Now - iCurrentClientData.LastSwitchTime).TotalSeconds;
789 //TODO: put that hard coded value as a client property
790 //Clients should be able to define how often they can be interrupted
791 //Thus a background client can set this to zero allowing any other client to interrupt at any time
792 //We could also compute this delay by looking at the requests frequencies?
794 requestedClientData.Priority == iCurrentClientData.Priority &&
795 //Time sharing is only if clients have the same priority
796 (lastSwitchToClientSecondsAgo < 30)) //Make sure a client is on for at least 30 seconds
798 //Don't switch clients too often
803 //Set current client ID.
804 iCurrentClientSessionId = aSessionId;
805 //Set the time we last switched to that client
806 iClients[aSessionId].LastSwitchTime = DateTime.Now;
807 //Fetch and set current client data.
808 iCurrentClientData = requestedClientData;
809 //Apply layout and set data fields.
810 UpdateTableLayoutPanel(iCurrentClientData);
813 private void buttonFont_Click(object sender, EventArgs e)
815 //fontDialog.ShowColor = true;
816 //fontDialog.ShowApply = true;
817 fontDialog.ShowEffects = true;
818 fontDialog.Font = cds.Font;
820 fontDialog.FixedPitchOnly = checkBoxFixedPitchFontOnly.Checked;
822 //fontDialog.ShowHelp = true;
824 //fontDlg.MaxSize = 40;
825 //fontDlg.MinSize = 22;
827 //fontDialog.Parent = this;
828 //fontDialog.StartPosition = FormStartPosition.CenterParent;
830 //DlgBox.ShowDialog(fontDialog);
832 //if (fontDialog.ShowDialog(this) != DialogResult.Cancel)
833 if (DlgBox.ShowDialog(fontDialog) != DialogResult.Cancel)
835 //Set the fonts to all our labels in our layout
836 foreach (Control ctrl in iTableLayoutPanel.Controls)
838 if (ctrl is MarqueeLabel)
840 ((MarqueeLabel) ctrl).Font = fontDialog.Font;
845 cds.Font = fontDialog.Font;
846 Properties.Settings.Default.Save();
855 void CheckFontHeight()
857 //Show font height and width
858 labelFontHeight.Text = "Font height: " + cds.Font.Height;
859 float charWidth = IsFixedWidth(cds.Font);
860 if (charWidth == 0.0f)
862 labelFontWidth.Visible = false;
866 labelFontWidth.Visible = true;
867 labelFontWidth.Text = "Font width: " + charWidth;
870 MarqueeLabel label = null;
871 //Get the first label control we can find
872 foreach (Control ctrl in iTableLayoutPanel.Controls)
874 if (ctrl is MarqueeLabel)
876 label = (MarqueeLabel) ctrl;
881 //Now check font height and show a warning if needed.
882 if (label != null && label.Font.Height > label.Height)
884 labelWarning.Text = "WARNING: Selected font is too height by " + (label.Font.Height - label.Height) +
886 labelWarning.Visible = true;
890 labelWarning.Visible = false;
895 private void buttonCapture_Click(object sender, EventArgs e)
897 System.Drawing.Bitmap bmp = new System.Drawing.Bitmap(iTableLayoutPanel.Width, iTableLayoutPanel.Height);
898 iTableLayoutPanel.DrawToBitmap(bmp, iTableLayoutPanel.ClientRectangle);
899 //Bitmap bmpToSave = new Bitmap(bmp);
900 bmp.Save("D:\\capture.png");
902 ((MarqueeLabel) iTableLayoutPanel.Controls[0]).Text = "Captured";
905 string outputFileName = "d:\\capture.png";
906 using (MemoryStream memory = new MemoryStream())
908 using (FileStream fs = new FileStream(outputFileName, FileMode.OpenOrCreate, FileAccess.ReadWrite))
910 bmp.Save(memory, System.Drawing.Imaging.ImageFormat.Png);
911 byte[] bytes = memory.ToArray();
912 fs.Write(bytes, 0, bytes.Length);
919 private void CheckForRequestResults()
921 if (iDisplay.IsRequestPending())
923 switch (iDisplay.AttemptRequestCompletion())
925 case MiniDisplay.Request.FirmwareRevision:
926 toolStripStatusLabelConnect.Text += " v" + iDisplay.FirmwareRevision();
927 //Issue next request then
928 iDisplay.RequestPowerSupplyStatus();
931 case MiniDisplay.Request.PowerSupplyStatus:
932 if (iDisplay.PowerSupplyStatus())
934 toolStripStatusLabelPower.Text = "ON";
938 toolStripStatusLabelPower.Text = "OFF";
940 //Issue next request then
941 iDisplay.RequestDeviceId();
944 case MiniDisplay.Request.DeviceId:
945 toolStripStatusLabelConnect.Text += " - " + iDisplay.DeviceId();
946 //No more request to issue
952 public static uint ColorWhiteIsOn(int aX, int aY, uint aPixel)
954 if ((aPixel & 0x00FFFFFF) == 0x00FFFFFF)
961 public static uint ColorUntouched(int aX, int aY, uint aPixel)
966 public static uint ColorInversed(int aX, int aY, uint aPixel)
971 public static uint ColorChessboard(int aX, int aY, uint aPixel)
973 if ((aX%2 == 0) && (aY%2 == 0))
977 else if ((aX%2 != 0) && (aY%2 != 0))
985 public static int ScreenReversedX(System.Drawing.Bitmap aBmp, int aX)
987 return aBmp.Width - aX - 1;
990 public int ScreenReversedY(System.Drawing.Bitmap aBmp, int aY)
992 return iBmp.Height - aY - 1;
995 public int ScreenX(System.Drawing.Bitmap aBmp, int aX)
1000 public int ScreenY(System.Drawing.Bitmap aBmp, int aY)
1006 /// Select proper pixel delegates according to our current settings.
1008 private void SetupPixelDelegates()
1010 //Select our pixel processing routine
1011 if (cds.InverseColors)
1013 //iColorFx = ColorChessboard;
1014 iColorFx = ColorInversed;
1018 iColorFx = ColorWhiteIsOn;
1021 //Select proper coordinate translation functions
1022 //We used delegate/function pointer to support reverse screen without doing an extra test on each pixels
1023 if (cds.ReverseScreen)
1025 iScreenX = ScreenReversedX;
1026 iScreenY = ScreenReversedY;
1037 /// This is our timer tick responsible to perform our render
1038 /// TODO: Use a threading timer instead of a Windows form timer.
1040 /// <param name="sender"></param>
1041 /// <param name="e"></param>
1042 private void timer_Tick(object sender, EventArgs e)
1044 //Update our animations
1045 DateTime NewTickTime = DateTime.Now;
1047 UpdateNetworkSignal(LastTickTime, NewTickTime);
1049 //Update animation for all our marquees
1050 foreach (Control ctrl in iTableLayoutPanel.Controls)
1052 if (ctrl is MarqueeLabel)
1054 ((MarqueeLabel) ctrl).UpdateAnimation(LastTickTime, NewTickTime);
1058 //Update our display
1059 if (iDisplay.IsOpen())
1061 CheckForRequestResults();
1063 //Check if frame rendering is needed
1064 //Typically used when showing clock
1065 if (!iSkipFrameRendering)
1070 iBmp = new System.Drawing.Bitmap(iTableLayoutPanel.Width, iTableLayoutPanel.Height,
1071 PixelFormat.Format32bppArgb);
1072 iCreateBitmap = false;
1074 iTableLayoutPanel.DrawToBitmap(iBmp, iTableLayoutPanel.ClientRectangle);
1075 //iBmp.Save("D:\\capture.png");
1077 //Send it to our display
1078 for (int i = 0; i < iBmp.Width; i++)
1080 for (int j = 0; j < iBmp.Height; j++)
1084 //Get our processed pixel coordinates
1085 int x = iScreenX(iBmp, i);
1086 int y = iScreenY(iBmp, j);
1088 uint color = (uint) iBmp.GetPixel(i, j).ToArgb();
1089 //Apply color effects
1090 color = iColorFx(x, y, color);
1092 iDisplay.SetPixel(x, y, color);
1097 iDisplay.SwapBuffers();
1101 //Compute instant FPS
1102 toolStripStatusLabelFps.Text = (1.0/NewTickTime.Subtract(LastTickTime).TotalSeconds).ToString("F0") + " / " +
1103 (1000/iTimerDisplay.Interval).ToString() + " FPS";
1105 LastTickTime = NewTickTime;
1110 /// Attempt to establish connection with our display hardware.
1112 private void OpenDisplayConnection()
1114 CloseDisplayConnection();
1116 if (!iDisplay.Open((MiniDisplay.Type) cds.DisplayType))
1119 toolStripStatusLabelConnect.Text = "Connection error";
1123 private void CloseDisplayConnection()
1125 //Status will be updated upon receiving the closed event
1127 if (iDisplay == null || !iDisplay.IsOpen())
1132 //Do not clear if we gave up on rendering already.
1133 //This means we will keep on displaying clock on MDM166AA for instance.
1134 if (!iSkipFrameRendering)
1137 iDisplay.SwapBuffers();
1140 iDisplay.SetAllIconsStatus(0); //Turn off all icons
1144 private void buttonOpen_Click(object sender, EventArgs e)
1146 OpenDisplayConnection();
1149 private void buttonClose_Click(object sender, EventArgs e)
1151 CloseDisplayConnection();
1154 private void buttonClear_Click(object sender, EventArgs e)
1157 iDisplay.SwapBuffers();
1160 private void buttonFill_Click(object sender, EventArgs e)
1163 iDisplay.SwapBuffers();
1166 private void trackBarBrightness_Scroll(object sender, EventArgs e)
1168 cds.Brightness = trackBarBrightness.Value;
1169 Properties.Settings.Default.Save();
1170 iDisplay.SetBrightness(trackBarBrightness.Value);
1176 /// CDS stands for Current Display Settings
1178 private DisplaySettings cds
1182 DisplaysSettings settings = Properties.Settings.Default.DisplaysSettings;
1183 if (settings == null)
1185 settings = new DisplaysSettings();
1187 Properties.Settings.Default.DisplaysSettings = settings;
1190 //Make sure all our settings have been created
1191 while (settings.Displays.Count <= Properties.Settings.Default.CurrentDisplayIndex)
1193 settings.Displays.Add(new DisplaySettings());
1196 DisplaySettings displaySettings = settings.Displays[Properties.Settings.Default.CurrentDisplayIndex];
1197 return displaySettings;
1202 /// Check if the given font has a fixed character pitch.
1204 /// <param name="ft"></param>
1205 /// <returns>0.0f if this is not a monospace font, otherwise returns the character width.</returns>
1206 public float IsFixedWidth(Font ft)
1208 Graphics g = CreateGraphics();
1209 char[] charSizes = new char[] {'i', 'a', 'Z', '%', '#', 'a', 'B', 'l', 'm', ',', '.'};
1210 float charWidth = g.MeasureString("I", ft, Int32.MaxValue, StringFormat.GenericTypographic).Width;
1212 bool fixedWidth = true;
1214 foreach (char c in charSizes)
1215 if (g.MeasureString(c.ToString(), ft, Int32.MaxValue, StringFormat.GenericTypographic).Width !=
1228 /// Synchronize UI with settings
1230 private void UpdateStatus()
1233 checkBoxShowBorders.Checked = cds.ShowBorders;
1234 iTableLayoutPanel.CellBorderStyle = (cds.ShowBorders
1235 ? TableLayoutPanelCellBorderStyle.Single
1236 : TableLayoutPanelCellBorderStyle.None);
1238 //Set the proper font to each of our labels
1239 foreach (MarqueeLabel ctrl in iTableLayoutPanel.Controls)
1241 ctrl.Font = cds.Font;
1245 //Check if "run on Windows startup" is enabled
1246 checkBoxAutoStart.Checked = iStartupManager.Startup;
1249 comboBoxHdmiPort.SelectedIndex = Properties.Settings.Default.CecHdmiPort - 1;
1251 //Mini Display settings
1252 checkBoxReverseScreen.Checked = cds.ReverseScreen;
1253 checkBoxInverseColors.Checked = cds.InverseColors;
1254 checkBoxShowVolumeLabel.Checked = cds.ShowVolumeLabel;
1255 checkBoxScaleToFit.Checked = cds.ScaleToFit;
1256 maskedTextBoxMinFontSize.Enabled = cds.ScaleToFit;
1257 labelMinFontSize.Enabled = cds.ScaleToFit;
1258 maskedTextBoxMinFontSize.Text = cds.MinFontSize.ToString();
1259 maskedTextBoxScrollingSpeed.Text = cds.ScrollingSpeedInPixelsPerSecond.ToString();
1260 comboBoxDisplayType.SelectedIndex = cds.DisplayType;
1261 iTimerDisplay.Interval = cds.TimerInterval;
1262 maskedTextBoxTimerInterval.Text = cds.TimerInterval.ToString();
1263 textBoxScrollLoopSeparator.Text = cds.Separator;
1265 SetupPixelDelegates();
1267 if (iDisplay.IsOpen())
1269 //We have a display connection
1270 //Reflect that in our UI
1273 iTableLayoutPanel.Enabled = true;
1274 panelDisplay.Enabled = true;
1276 //Only setup brightness if display is open
1277 trackBarBrightness.Minimum = iDisplay.MinBrightness();
1278 trackBarBrightness.Maximum = iDisplay.MaxBrightness();
1279 if (cds.Brightness < iDisplay.MinBrightness() || cds.Brightness > iDisplay.MaxBrightness())
1281 //Brightness out of range, this can occur when using auto-detect
1282 //Use max brightness instead
1283 trackBarBrightness.Value = iDisplay.MaxBrightness();
1284 iDisplay.SetBrightness(iDisplay.MaxBrightness());
1288 trackBarBrightness.Value = cds.Brightness;
1289 iDisplay.SetBrightness(cds.Brightness);
1292 //Try compute the steps to something that makes sense
1293 trackBarBrightness.LargeChange = Math.Max(1, (iDisplay.MaxBrightness() - iDisplay.MinBrightness())/5);
1294 trackBarBrightness.SmallChange = 1;
1297 buttonFill.Enabled = true;
1298 buttonClear.Enabled = true;
1299 buttonOpen.Enabled = false;
1300 buttonClose.Enabled = true;
1301 trackBarBrightness.Enabled = true;
1302 toolStripStatusLabelConnect.Text = "Connected - " + iDisplay.Vendor() + " - " + iDisplay.Product();
1303 //+ " - " + iDisplay.SerialNumber();
1305 if (iDisplay.SupportPowerOnOff())
1307 buttonPowerOn.Enabled = true;
1308 buttonPowerOff.Enabled = true;
1312 buttonPowerOn.Enabled = false;
1313 buttonPowerOff.Enabled = false;
1316 if (iDisplay.SupportClock())
1318 buttonShowClock.Enabled = true;
1319 buttonHideClock.Enabled = true;
1323 buttonShowClock.Enabled = false;
1324 buttonHideClock.Enabled = false;
1328 //Check if Volume Label is supported. To date only MDM166AA supports that crap :)
1329 checkBoxShowVolumeLabel.Enabled = iDisplay.IconCount(MiniDisplay.IconType.VolumeLabel) > 0;
1331 if (cds.ShowVolumeLabel)
1333 iDisplay.SetIconOn(MiniDisplay.IconType.VolumeLabel);
1337 iDisplay.SetIconOff(MiniDisplay.IconType.VolumeLabel);
1342 //Display connection not available
1343 //Reflect that in our UI
1345 //In debug start our timer even if we don't have a display connection
1348 //In production environment we don't need our timer if no display connection
1351 checkBoxShowVolumeLabel.Enabled = false;
1352 iTableLayoutPanel.Enabled = false;
1353 panelDisplay.Enabled = false;
1354 buttonFill.Enabled = false;
1355 buttonClear.Enabled = false;
1356 buttonOpen.Enabled = true;
1357 buttonClose.Enabled = false;
1358 trackBarBrightness.Enabled = false;
1359 buttonPowerOn.Enabled = false;
1360 buttonPowerOff.Enabled = false;
1361 buttonShowClock.Enabled = false;
1362 buttonHideClock.Enabled = false;
1363 toolStripStatusLabelConnect.Text = "Disconnected";
1364 toolStripStatusLabelPower.Text = "N/A";
1373 /// <param name="sender"></param>
1374 /// <param name="e"></param>
1375 private void checkBoxShowVolumeLabel_CheckedChanged(object sender, EventArgs e)
1377 cds.ShowVolumeLabel = checkBoxShowVolumeLabel.Checked;
1378 Properties.Settings.Default.Save();
1382 private void checkBoxShowBorders_CheckedChanged(object sender, EventArgs e)
1384 //Save our show borders setting
1385 iTableLayoutPanel.CellBorderStyle = (checkBoxShowBorders.Checked
1386 ? TableLayoutPanelCellBorderStyle.Single
1387 : TableLayoutPanelCellBorderStyle.None);
1388 cds.ShowBorders = checkBoxShowBorders.Checked;
1389 Properties.Settings.Default.Save();
1393 private void checkBoxAutoStart_CheckedChanged(object sender, EventArgs e)
1395 iStartupManager.Startup = checkBoxAutoStart.Checked;
1399 private void checkBoxReverseScreen_CheckedChanged(object sender, EventArgs e)
1401 //Save our reverse screen setting
1402 cds.ReverseScreen = checkBoxReverseScreen.Checked;
1403 Properties.Settings.Default.Save();
1404 SetupPixelDelegates();
1407 private void checkBoxInverseColors_CheckedChanged(object sender, EventArgs e)
1409 //Save our inverse colors setting
1410 cds.InverseColors = checkBoxInverseColors.Checked;
1411 Properties.Settings.Default.Save();
1412 SetupPixelDelegates();
1415 private void checkBoxScaleToFit_CheckedChanged(object sender, EventArgs e)
1417 //Save our scale to fit setting
1418 cds.ScaleToFit = checkBoxScaleToFit.Checked;
1419 Properties.Settings.Default.Save();
1421 labelMinFontSize.Enabled = cds.ScaleToFit;
1422 maskedTextBoxMinFontSize.Enabled = cds.ScaleToFit;
1425 private void MainForm_Resize(object sender, EventArgs e)
1427 if (WindowState == FormWindowState.Minimized)
1429 // To workaround our empty bitmap bug on Windows 7 we need to recreate our bitmap when the application is minimized
1430 // That's apparently not needed on Windows 10 but we better leave it in place.
1431 iCreateBitmap = true;
1435 private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
1438 iNetworkManager.Dispose();
1439 CloseDisplayConnection();
1441 e.Cancel = iClosing;
1444 public void StartServer()
1446 iServiceHost = new ServiceHost
1449 new Uri[] {new Uri("net.tcp://localhost:8001/")}
1452 iServiceHost.AddServiceEndpoint(typeof(IService), new NetTcpBinding(SecurityMode.None, true),
1454 iServiceHost.Open();
1457 public void StopServer()
1459 if (iClients.Count > 0 && !iClosing)
1463 BroadcastCloseEvent();
1468 MessageBox.Show("Force exit?", "Waiting for clients...", MessageBoxButtons.YesNo,
1469 MessageBoxIcon.Warning) == DialogResult.Yes)
1471 iClosing = false; //We make sure we force close if asked twice
1476 //We removed that as it often lags for some reason
1477 //iServiceHost.Close();
1481 public void BroadcastCloseEvent()
1483 Trace.TraceInformation("BroadcastCloseEvent - start");
1485 var inactiveClients = new List<string>();
1486 foreach (var client in iClients)
1488 //if (client.Key != eventData.ClientName)
1492 Trace.TraceInformation("BroadcastCloseEvent - " + client.Key);
1493 client.Value.Callback.OnCloseOrder( /*eventData*/);
1495 catch (Exception ex)
1497 inactiveClients.Add(client.Key);
1502 if (inactiveClients.Count > 0)
1504 foreach (var client in inactiveClients)
1506 iClients.Remove(client);
1507 Program.iFormMain.iTreeViewClients.Nodes.Remove(
1508 Program.iFormMain.iTreeViewClients.Nodes.Find(client, false)[0]);
1512 if (iClients.Count == 0)
1519 /// Just remove all our fields.
1521 private void ClearLayout()
1523 iTableLayoutPanel.Controls.Clear();
1524 iTableLayoutPanel.RowStyles.Clear();
1525 iTableLayoutPanel.ColumnStyles.Clear();
1526 iCurrentClientData = null;
1530 /// Just launch a demo client.
1532 private void StartNewClient(string aTopText = "", string aBottomText = "")
1534 Thread clientThread = new Thread(SharpDisplayClient.Program.MainWithParams);
1535 SharpDisplayClient.StartParams myParams = new SharpDisplayClient.StartParams(
1536 new Point(this.Right, this.Top), aTopText, aBottomText);
1537 clientThread.Start(myParams);
1542 /// Just launch our idle client.
1544 private void StartIdleClient(string aTopText = "", string aBottomText = "")
1546 Thread clientThread = new Thread(SharpDisplayClientIdle.Program.MainWithParams);
1547 SharpDisplayClientIdle.StartParams myParams =
1548 new SharpDisplayClientIdle.StartParams(new Point(this.Right, this.Top), aTopText, aBottomText);
1549 clientThread.Start(myParams);
1554 private void buttonStartClient_Click(object sender, EventArgs e)
1559 private void buttonSuspend_Click(object sender, EventArgs e)
1564 private void StartTimer()
1566 LastTickTime = DateTime.Now; //Reset timer to prevent jump
1567 iTimerDisplay.Enabled = true;
1568 UpdateSuspendButton();
1571 private void StopTimer()
1573 LastTickTime = DateTime.Now; //Reset timer to prevent jump
1574 iTimerDisplay.Enabled = false;
1575 UpdateSuspendButton();
1578 private void ToggleTimer()
1580 LastTickTime = DateTime.Now; //Reset timer to prevent jump
1581 iTimerDisplay.Enabled = !iTimerDisplay.Enabled;
1582 UpdateSuspendButton();
1585 private void UpdateSuspendButton()
1587 if (!iTimerDisplay.Enabled)
1589 buttonSuspend.Text = "Run";
1593 buttonSuspend.Text = "Pause";
1598 private void buttonCloseClients_Click(object sender, EventArgs e)
1600 BroadcastCloseEvent();
1603 private void treeViewClients_AfterSelect(object sender, TreeViewEventArgs e)
1605 //Root node must have at least one child
1606 if (e.Node.Nodes.Count == 0)
1611 //If the selected node is the root node of a client then switch to it
1612 string sessionId = e.Node.Nodes[0].Text; //First child of a root node is the sessionId
1613 if (iClients.ContainsKey(sessionId)) //Check that's actually what we are looking at
1615 //We have a valid session just switch to that client
1616 SetCurrentClient(sessionId, true);
1625 /// <param name="aSessionId"></param>
1626 /// <param name="aCallback"></param>
1627 public void AddClientThreadSafe(string aSessionId, ICallback aCallback)
1629 if (this.InvokeRequired)
1631 //Not in the proper thread, invoke ourselves
1632 AddClientDelegate d = new AddClientDelegate(AddClientThreadSafe);
1633 this.Invoke(d, new object[] {aSessionId, aCallback});
1637 //We are in the proper thread
1638 //Add this session to our collection of clients
1639 ClientData newClient = new ClientData(aSessionId, aCallback);
1640 Program.iFormMain.iClients.Add(aSessionId, newClient);
1641 //Add this session to our UI
1642 UpdateClientTreeViewNode(newClient);
1648 /// Find the client with the highest priority if any.
1650 /// <returns>Our highest priority client or null if not a single client is connected.</returns>
1651 public ClientData FindHighestPriorityClient()
1653 ClientData highestPriorityClient = null;
1654 foreach (var client in iClients)
1656 if (highestPriorityClient == null || client.Value.Priority > highestPriorityClient.Priority)
1658 highestPriorityClient = client.Value;
1662 return highestPriorityClient;
1668 /// <param name="aSessionId"></param>
1669 public void RemoveClientThreadSafe(string aSessionId)
1671 if (this.InvokeRequired)
1673 //Not in the proper thread, invoke ourselves
1674 RemoveClientDelegate d = new RemoveClientDelegate(RemoveClientThreadSafe);
1675 this.Invoke(d, new object[] {aSessionId});
1679 //We are in the proper thread
1680 //Remove this session from both client collection and UI tree view
1681 if (Program.iFormMain.iClients.Keys.Contains(aSessionId))
1683 Program.iFormMain.iClients.Remove(aSessionId);
1684 Program.iFormMain.iTreeViewClients.Nodes.Remove(
1685 Program.iFormMain.iTreeViewClients.Nodes.Find(aSessionId, false)[0]);
1686 //Update recording status too whenever a client is removed
1687 UpdateRecordingNotification();
1690 if (iCurrentClientSessionId == aSessionId)
1692 //The current client is closing
1693 iCurrentClientData = null;
1694 //Find the client with the highest priority and set it as current
1695 ClientData newCurrentClient = FindHighestPriorityClient();
1696 if (newCurrentClient != null)
1698 SetCurrentClient(newCurrentClient.SessionId, true);
1702 if (iClients.Count == 0)
1704 //Clear our screen when last client disconnects
1709 //We were closing our form
1710 //All clients are now closed
1711 //Just resume our close operation
1722 /// <param name="aSessionId"></param>
1723 /// <param name="aLayout"></param>
1724 public void SetClientLayoutThreadSafe(string aSessionId, TableLayout aLayout)
1726 if (this.InvokeRequired)
1728 //Not in the proper thread, invoke ourselves
1729 SetLayoutDelegate d = new SetLayoutDelegate(SetClientLayoutThreadSafe);
1730 this.Invoke(d, new object[] {aSessionId, aLayout});
1734 ClientData client = iClients[aSessionId];
1737 //Don't change a thing if the layout is the same
1738 if (!client.Layout.IsSameAs(aLayout))
1740 Debug.Print("SetClientLayoutThreadSafe: Layout updated.");
1741 //Set our client layout then
1742 client.Layout = aLayout;
1743 //So that next time we update all our fields at ones
1744 client.HasNewLayout = true;
1745 //Layout has changed clear our fields then
1746 client.Fields.Clear();
1748 UpdateClientTreeViewNode(client);
1752 Debug.Print("SetClientLayoutThreadSafe: Layout has not changed.");
1761 /// <param name="aSessionId"></param>
1762 /// <param name="aField"></param>
1763 public void SetClientFieldThreadSafe(string aSessionId, DataField aField)
1765 if (this.InvokeRequired)
1767 //Not in the proper thread, invoke ourselves
1768 SetFieldDelegate d = new SetFieldDelegate(SetClientFieldThreadSafe);
1769 this.Invoke(d, new object[] {aSessionId, aField});
1773 //We are in the proper thread
1774 //Call the non-thread-safe variant
1775 SetClientField(aSessionId, aField);
1783 /// Set a data field in the given client.
1785 /// <param name="aSessionId"></param>
1786 /// <param name="aField"></param>
1787 private void SetClientField(string aSessionId, DataField aField)
1789 //TODO: should check if the field actually changed?
1791 ClientData client = iClients[aSessionId];
1792 bool layoutChanged = false;
1793 bool contentChanged = true;
1795 //Fetch our field index
1796 int fieldIndex = client.FindSameFieldIndex(aField);
1800 //No corresponding field, just bail out
1804 //Keep our previous field in there
1805 DataField previousField = client.Fields[fieldIndex];
1806 //Just update that field then
1807 client.Fields[fieldIndex] = aField;
1809 if (!aField.IsTableField)
1811 //We are done then if that field is not in our table layout
1815 TableField tableField = (TableField) aField;
1817 if (previousField.IsSameLayout(aField))
1819 //If we are updating a field in our current client we need to update it in our panel
1820 if (aSessionId == iCurrentClientSessionId)
1822 Control ctrl = iTableLayoutPanel.GetControlFromPosition(tableField.Column, tableField.Row);
1823 if (aField.IsTextField && ctrl is MarqueeLabel)
1825 TextField textField = (TextField) aField;
1826 //Text field control already in place, just change the text
1827 MarqueeLabel label = (MarqueeLabel) ctrl;
1828 contentChanged = (label.Text != textField.Text || label.TextAlign != textField.Alignment);
1829 label.Text = textField.Text;
1830 label.TextAlign = textField.Alignment;
1832 else if (aField.IsBitmapField && ctrl is PictureBox)
1834 BitmapField bitmapField = (BitmapField) aField;
1835 contentChanged = true; //TODO: Bitmap comp or should we leave that to clients?
1836 //Bitmap field control already in place just change the bitmap
1837 PictureBox pictureBox = (PictureBox) ctrl;
1838 pictureBox.Image = bitmapField.Bitmap;
1842 layoutChanged = true;
1848 layoutChanged = true;
1851 //If either content or layout changed we need to update our tree view to reflect the changes
1852 if (contentChanged || layoutChanged)
1854 UpdateClientTreeViewNode(client);
1858 Debug.Print("Layout changed");
1859 //Our layout has changed, if we are already the current client we need to update our panel
1860 if (aSessionId == iCurrentClientSessionId)
1862 //Apply layout and set data fields.
1863 UpdateTableLayoutPanel(iCurrentClientData);
1868 Debug.Print("Layout has not changed.");
1873 Debug.Print("WARNING: content and layout have not changed!");
1876 //When a client field is set we try switching to this client to present the new information to our user
1877 SetCurrentClient(aSessionId);
1883 /// <param name="aTexts"></param>
1884 public void SetClientFieldsThreadSafe(string aSessionId, System.Collections.Generic.IList<DataField> aFields)
1886 if (this.InvokeRequired)
1888 //Not in the proper thread, invoke ourselves
1889 SetFieldsDelegate d = new SetFieldsDelegate(SetClientFieldsThreadSafe);
1890 this.Invoke(d, new object[] {aSessionId, aFields});
1894 ClientData client = iClients[aSessionId];
1896 if (client.HasNewLayout)
1898 //TODO: Assert client.Count == 0
1899 //Our layout was just changed
1900 //Do some special handling to avoid re-creating our panel N times, once for each fields
1901 client.HasNewLayout = false;
1902 //Just set all our fields then
1903 client.Fields.AddRange(aFields);
1904 //Try switch to that client
1905 SetCurrentClient(aSessionId);
1907 //If we are updating the current client update our panel
1908 if (aSessionId == iCurrentClientSessionId)
1910 //Apply layout and set data fields.
1911 UpdateTableLayoutPanel(iCurrentClientData);
1914 UpdateClientTreeViewNode(client);
1918 //Put each our text fields in a label control
1919 foreach (DataField field in aFields)
1921 SetClientField(aSessionId, field);
1930 /// <param name="aSessionId"></param>
1931 /// <param name="aName"></param>
1932 public void SetClientNameThreadSafe(string aSessionId, string aName)
1934 if (this.InvokeRequired)
1936 //Not in the proper thread, invoke ourselves
1937 SetClientNameDelegate d = new SetClientNameDelegate(SetClientNameThreadSafe);
1938 this.Invoke(d, new object[] {aSessionId, aName});
1942 //We are in the proper thread
1944 ClientData client = iClients[aSessionId];
1948 client.Name = aName;
1949 //Update our tree-view
1950 UpdateClientTreeViewNode(client);
1956 public void SetClientPriorityThreadSafe(string aSessionId, uint aPriority)
1958 if (this.InvokeRequired)
1960 //Not in the proper thread, invoke ourselves
1961 SetClientPriorityDelegate d = new SetClientPriorityDelegate(SetClientPriorityThreadSafe);
1962 this.Invoke(d, new object[] {aSessionId, aPriority});
1966 //We are in the proper thread
1968 ClientData client = iClients[aSessionId];
1972 client.Priority = aPriority;
1973 //Update our tree-view
1974 UpdateClientTreeViewNode(client);
1975 //Change our current client as per new priority
1976 ClientData newCurrentClient = FindHighestPriorityClient();
1977 if (newCurrentClient != null)
1979 SetCurrentClient(newCurrentClient.SessionId);
1988 /// <param name="value"></param>
1989 /// <param name="maxChars"></param>
1990 /// <returns></returns>
1991 public static string Truncate(string value, int maxChars)
1993 return value.Length <= maxChars ? value : value.Substring(0, maxChars - 3) + "...";
1997 /// Update our recording notification.
1999 private void UpdateRecordingNotification()
2002 bool activeRecording = false;
2004 RecordingField recField = new RecordingField();
2005 foreach (var client in iClients)
2007 RecordingField rec = (RecordingField) client.Value.FindSameFieldAs(recField);
2008 if (rec != null && rec.IsActive)
2010 activeRecording = true;
2011 //Don't break cause we are collecting the names/texts.
2012 if (!String.IsNullOrEmpty(rec.Text))
2014 text += (rec.Text + "\n");
2018 //Not text for that recording, use client name instead
2019 text += client.Value.Name + " recording\n";
2025 //Update our text no matter what, can't have more than 63 characters otherwise it throws an exception.
2026 iRecordingNotification.Text = Truncate(text, 63);
2028 //Change visibility of notification if needed
2029 if (iRecordingNotification.Visible != activeRecording)
2031 iRecordingNotification.Visible = activeRecording;
2032 //Assuming the notification icon is in sync with our display icon
2033 //Take care of our REC icon
2034 if (iDisplay.IsOpen())
2036 iDisplay.SetIconOnOff(MiniDisplay.IconType.Recording, activeRecording);
2044 /// <param name="aClient"></param>
2045 private void UpdateClientTreeViewNode(ClientData aClient)
2047 Debug.Print("UpdateClientTreeViewNode");
2049 if (aClient == null)
2054 //Hook in record icon update too
2055 UpdateRecordingNotification();
2057 TreeNode node = null;
2058 //Check that our client node already exists
2059 //Get our client root node using its key which is our session ID
2060 TreeNode[] nodes = iTreeViewClients.Nodes.Find(aClient.SessionId, false);
2061 if (nodes.Count() > 0)
2063 //We already have a node for that client
2065 //Clear children as we are going to recreate them below
2070 //Client node does not exists create a new one
2071 iTreeViewClients.Nodes.Add(aClient.SessionId, aClient.SessionId);
2072 node = iTreeViewClients.Nodes.Find(aClient.SessionId, false)[0];
2078 if (!String.IsNullOrEmpty(aClient.Name))
2080 //We have a name, use it as text for our root node
2081 node.Text = aClient.Name;
2082 //Add a child with SessionId
2083 node.Nodes.Add(new TreeNode(aClient.SessionId));
2087 //No name, use session ID instead
2088 node.Text = aClient.SessionId;
2091 //Display client priority
2092 node.Nodes.Add(new TreeNode("Priority: " + aClient.Priority));
2094 if (aClient.Fields.Count > 0)
2096 //Create root node for our texts
2097 TreeNode textsRoot = new TreeNode("Fields");
2098 node.Nodes.Add(textsRoot);
2099 //For each text add a new entry
2100 foreach (DataField field in aClient.Fields)
2102 if (field.IsTextField)
2104 TextField textField = (TextField) field;
2105 textsRoot.Nodes.Add(new TreeNode("[Text]" + textField.Text));
2107 else if (field.IsBitmapField)
2109 textsRoot.Nodes.Add(new TreeNode("[Bitmap]"));
2111 else if (field.IsRecordingField)
2113 RecordingField recordingField = (RecordingField) field;
2114 textsRoot.Nodes.Add(new TreeNode("[Recording]" + recordingField.IsActive));
2124 /// Update our table layout row styles to make sure each rows have similar height
2126 private void UpdateTableLayoutRowStyles()
2128 foreach (RowStyle rowStyle in iTableLayoutPanel.RowStyles)
2130 rowStyle.SizeType = SizeType.Percent;
2131 rowStyle.Height = 100/iTableLayoutPanel.RowCount;
2136 /// Update our display table layout.
2137 /// Will instanciated every field control as defined by our client.
2138 /// Fields must be specified by rows from the left.
2140 /// <param name="aLayout"></param>
2141 private void UpdateTableLayoutPanel(ClientData aClient)
2143 Debug.Print("UpdateTableLayoutPanel");
2145 if (aClient == null)
2152 TableLayout layout = aClient.Layout;
2154 //First clean our current panel
2155 iTableLayoutPanel.Controls.Clear();
2156 iTableLayoutPanel.RowStyles.Clear();
2157 iTableLayoutPanel.ColumnStyles.Clear();
2158 iTableLayoutPanel.RowCount = 0;
2159 iTableLayoutPanel.ColumnCount = 0;
2161 //Then recreate our rows...
2162 while (iTableLayoutPanel.RowCount < layout.Rows.Count)
2164 iTableLayoutPanel.RowCount++;
2168 while (iTableLayoutPanel.ColumnCount < layout.Columns.Count)
2170 iTableLayoutPanel.ColumnCount++;
2174 for (int i = 0; i < iTableLayoutPanel.ColumnCount; i++)
2176 //Create our column styles
2177 this.iTableLayoutPanel.ColumnStyles.Add(layout.Columns[i]);
2180 for (int j = 0; j < iTableLayoutPanel.RowCount; j++)
2184 //Create our row styles
2185 this.iTableLayoutPanel.RowStyles.Add(layout.Rows[j]);
2195 foreach (DataField field in aClient.Fields)
2197 if (!field.IsTableField)
2199 //That field is not taking part in our table layout skip it
2203 TableField tableField = (TableField) field;
2205 //Create a control corresponding to the field specified for that cell
2206 Control control = CreateControlForDataField(tableField);
2208 //Add newly created control to our table layout at the specified row and column
2209 iTableLayoutPanel.Controls.Add(control, tableField.Column, tableField.Row);
2210 //Make sure we specify column and row span for that new control
2211 iTableLayoutPanel.SetColumnSpan(control, tableField.ColumnSpan);
2212 iTableLayoutPanel.SetRowSpan(control, tableField.RowSpan);
2220 /// Check our type of data field and create corresponding control
2222 /// <param name="aField"></param>
2223 private Control CreateControlForDataField(DataField aField)
2225 Control control = null;
2226 if (aField.IsTextField)
2228 MarqueeLabel label = new SharpDisplayManager.MarqueeLabel();
2229 label.AutoEllipsis = true;
2230 label.AutoSize = true;
2231 label.BackColor = System.Drawing.Color.Transparent;
2232 label.Dock = System.Windows.Forms.DockStyle.Fill;
2233 label.Location = new System.Drawing.Point(1, 1);
2234 label.Margin = new System.Windows.Forms.Padding(0);
2235 label.Name = "marqueeLabel" + aField;
2236 label.OwnTimer = false;
2237 label.PixelsPerSecond = cds.ScrollingSpeedInPixelsPerSecond;
2238 label.Separator = cds.Separator;
2239 label.MinFontSize = cds.MinFontSize;
2240 label.ScaleToFit = cds.ScaleToFit;
2241 //control.Size = new System.Drawing.Size(254, 30);
2242 //control.TabIndex = 2;
2243 label.Font = cds.Font;
2245 TextField field = (TextField) aField;
2246 label.TextAlign = field.Alignment;
2247 label.UseCompatibleTextRendering = true;
2248 label.Text = field.Text;
2252 else if (aField.IsBitmapField)
2254 //Create picture box
2255 PictureBox picture = new PictureBox();
2256 picture.AutoSize = true;
2257 picture.BackColor = System.Drawing.Color.Transparent;
2258 picture.Dock = System.Windows.Forms.DockStyle.Fill;
2259 picture.Location = new System.Drawing.Point(1, 1);
2260 picture.Margin = new System.Windows.Forms.Padding(0);
2261 picture.Name = "pictureBox" + aField;
2263 BitmapField field = (BitmapField) aField;
2264 picture.Image = field.Bitmap;
2268 //TODO: Handle recording field?
2274 /// Called when the user selected a new display type.
2276 /// <param name="sender"></param>
2277 /// <param name="e"></param>
2278 private void comboBoxDisplayType_SelectedIndexChanged(object sender, EventArgs e)
2280 //Store the selected display type in our settings
2281 Properties.Settings.Default.CurrentDisplayIndex = comboBoxDisplayType.SelectedIndex;
2282 cds.DisplayType = comboBoxDisplayType.SelectedIndex;
2283 Properties.Settings.Default.Save();
2285 //Try re-opening the display connection if we were already connected.
2286 //Otherwise just update our status to reflect display type change.
2287 if (iDisplay.IsOpen())
2289 OpenDisplayConnection();
2297 private void maskedTextBoxTimerInterval_TextChanged(object sender, EventArgs e)
2299 if (maskedTextBoxTimerInterval.Text != "")
2301 int interval = Convert.ToInt32(maskedTextBoxTimerInterval.Text);
2305 iTimerDisplay.Interval = interval;
2306 cds.TimerInterval = iTimerDisplay.Interval;
2307 Properties.Settings.Default.Save();
2312 private void maskedTextBoxMinFontSize_TextChanged(object sender, EventArgs e)
2314 if (maskedTextBoxMinFontSize.Text != "")
2316 int minFontSize = Convert.ToInt32(maskedTextBoxMinFontSize.Text);
2318 if (minFontSize > 0)
2320 cds.MinFontSize = minFontSize;
2321 Properties.Settings.Default.Save();
2322 //We need to recreate our layout for that change to take effect
2323 UpdateTableLayoutPanel(iCurrentClientData);
2329 private void maskedTextBoxScrollingSpeed_TextChanged(object sender, EventArgs e)
2331 if (maskedTextBoxScrollingSpeed.Text != "")
2333 int scrollingSpeed = Convert.ToInt32(maskedTextBoxScrollingSpeed.Text);
2335 if (scrollingSpeed > 0)
2337 cds.ScrollingSpeedInPixelsPerSecond = scrollingSpeed;
2338 Properties.Settings.Default.Save();
2339 //We need to recreate our layout for that change to take effect
2340 UpdateTableLayoutPanel(iCurrentClientData);
2345 private void textBoxScrollLoopSeparator_TextChanged(object sender, EventArgs e)
2347 cds.Separator = textBoxScrollLoopSeparator.Text;
2348 Properties.Settings.Default.Save();
2350 //Update our text fields
2351 foreach (MarqueeLabel ctrl in iTableLayoutPanel.Controls)
2353 ctrl.Separator = cds.Separator;
2358 private void buttonPowerOn_Click(object sender, EventArgs e)
2363 private void buttonPowerOff_Click(object sender, EventArgs e)
2365 iDisplay.PowerOff();
2368 private void buttonShowClock_Click(object sender, EventArgs e)
2373 private void buttonHideClock_Click(object sender, EventArgs e)
2378 private void buttonUpdate_Click(object sender, EventArgs e)
2380 InstallUpdateSyncWithInfo();
2388 if (!iDisplay.IsOpen())
2393 //Devices like MDM166AA don't support windowing and frame rendering must be stopped while showing our clock
2394 iSkipFrameRendering = true;
2397 iDisplay.SwapBuffers();
2398 //Then show our clock
2399 iDisplay.ShowClock();
2407 if (!iDisplay.IsOpen())
2412 //Devices like MDM166AA don't support windowing and frame rendering must be stopped while showing our clock
2413 iSkipFrameRendering = false;
2414 iDisplay.HideClock();
2417 private void InstallUpdateSyncWithInfo()
2419 UpdateCheckInfo info = null;
2421 if (ApplicationDeployment.IsNetworkDeployed)
2423 ApplicationDeployment ad = ApplicationDeployment.CurrentDeployment;
2427 info = ad.CheckForDetailedUpdate();
2430 catch (DeploymentDownloadException dde)
2433 "The new version of the application cannot be downloaded at this time. \n\nPlease check your network connection, or try again later. Error: " +
2437 catch (InvalidDeploymentException ide)
2440 "Cannot check for a new version of the application. The ClickOnce deployment is corrupt. Please redeploy the application and try again. Error: " +
2444 catch (InvalidOperationException ioe)
2447 "This application cannot be updated. It is likely not a ClickOnce application. Error: " +
2452 if (info.UpdateAvailable)
2454 Boolean doUpdate = true;
2456 if (!info.IsUpdateRequired)
2459 MessageBox.Show("An update is available. Would you like to update the application now?",
2460 "Update Available", MessageBoxButtons.OKCancel);
2461 if (!(DialogResult.OK == dr))
2468 // Display a message that the application MUST reboot. Display the minimum required version.
2469 MessageBox.Show("This application has detected a mandatory update from your current " +
2470 "version to version " + info.MinimumRequiredVersion.ToString() +
2471 ". The application will now install the update and restart.",
2472 "Update Available", MessageBoxButtons.OK,
2473 MessageBoxIcon.Information);
2481 MessageBox.Show("The application has been upgraded, and will now restart.");
2482 Application.Restart();
2484 catch (DeploymentDownloadException dde)
2487 "Cannot install the latest version of the application. \n\nPlease check your network connection, or try again later. Error: " +
2495 MessageBox.Show("You are already running the latest version.", "Application up-to-date");
2504 private void SysTrayHideShow()
2510 WindowState = FormWindowState.Normal;
2515 /// Use to handle minimize events.
2517 /// <param name="sender"></param>
2518 /// <param name="e"></param>
2519 private void MainForm_SizeChanged(object sender, EventArgs e)
2521 if (WindowState == FormWindowState.Minimized && Properties.Settings.Default.MinimizeToTray)
2533 /// <param name="sender"></param>
2534 /// <param name="e"></param>
2535 private void tableLayoutPanel_SizeChanged(object sender, EventArgs e)
2537 //Our table layout size has changed which means our display size has changed.
2538 //We need to re-create our bitmap.
2539 iCreateBitmap = true;
2543 /// Broadcast messages to subscribers.
2545 /// <param name="message"></param>
2546 protected override void WndProc(ref Message aMessage)
2548 if (OnWndProc != null)
2550 OnWndProc(ref aMessage);
2553 base.WndProc(ref aMessage);
2556 private void iCheckBoxCecEnabled_CheckedChanged(object sender, EventArgs e)
2562 private void comboBoxHdmiPort_SelectedIndexChanged(object sender, EventArgs e)
2564 //Save CEC HDMI port
2565 Properties.Settings.Default.CecHdmiPort = Convert.ToByte(comboBoxHdmiPort.SelectedIndex);
2566 Properties.Settings.Default.CecHdmiPort++;
2567 Properties.Settings.Default.Save();
2575 private void ResetCec()
2577 if (iCecManager == null)
2579 //Thus skipping initial UI setup
2585 if (Properties.Settings.Default.CecEnabled)
2587 iCecManager.Start(Handle, "CEC",
2588 Properties.Settings.Default.CecHdmiPort);
2597 private async void ResetHarmonyAsync(bool aForceAuth=false)
2599 // ConnectAsync already if we have an existing session cookie
2600 if (Properties.Settings.Default.HarmonyEnabled)
2604 iButtonHarmonyConnect.Enabled = false;
2605 await ConnectHarmonyAsync(aForceAuth);
2607 catch (Exception ex)
2609 Trace.WriteLine("Exception thrown by ConnectHarmonyAsync");
2610 Trace.WriteLine(ex.ToString());
2614 iButtonHarmonyConnect.Enabled = true;
2622 /// <param name="sender"></param>
2623 /// <param name="e"></param>
2624 private void iButtonHarmonyConnect_Click(object sender, EventArgs e)
2626 // User is explicitaly trying to connect
2627 //Reset Harmony Hub connection forcing authentication
2628 ResetHarmonyAsync(true);
2634 /// <param name="sender"></param>
2635 /// <param name="e"></param>
2636 private void iCheckBoxHarmonyEnabled_CheckedChanged(object sender, EventArgs e)
2638 iButtonHarmonyConnect.Enabled = iCheckBoxHarmonyEnabled.Checked;
2644 private void SetupCecLogLevel()
2647 iCecManager.Client.LogLevel = 0;
2649 if (checkBoxCecLogError.Checked)
2650 iCecManager.Client.LogLevel |= (int) CecLogLevel.Error;
2652 if (checkBoxCecLogWarning.Checked)
2653 iCecManager.Client.LogLevel |= (int) CecLogLevel.Warning;
2655 if (checkBoxCecLogNotice.Checked)
2656 iCecManager.Client.LogLevel |= (int) CecLogLevel.Notice;
2658 if (checkBoxCecLogTraffic.Checked)
2659 iCecManager.Client.LogLevel |= (int) CecLogLevel.Traffic;
2661 if (checkBoxCecLogDebug.Checked)
2662 iCecManager.Client.LogLevel |= (int) CecLogLevel.Debug;
2664 iCecManager.Client.FilterOutPollLogs = checkBoxCecLogNoPoll.Checked;
2668 private void ButtonStartIdleClient_Click(object sender, EventArgs e)
2673 private void buttonClearLogs_Click(object sender, EventArgs e)
2675 richTextBoxLogs.Clear();
2678 private void checkBoxCecLogs_CheckedChanged(object sender, EventArgs e)
2688 /// Get the current event based on event tree view selection.
2690 /// <returns></returns>
2691 private Ear.Event CurrentEvent()
2693 //Walk up the tree from the selected node to find our event
2694 TreeNode node = iTreeViewEvents.SelectedNode;
2695 Ear.Event selectedEvent = null;
2696 while (node != null)
2698 if (node.Tag is Ear.Event)
2700 selectedEvent = (Ear.Event) node.Tag;
2706 return selectedEvent;
2710 /// Get the current action based on event tree view selection
2712 /// <returns></returns>
2713 private Ear.Action CurrentAction()
2715 TreeNode node = iTreeViewEvents.SelectedNode;
2716 if (node != null && node.Tag is Ear.Action)
2718 return (Ear.Action) node.Tag;
2727 /// <returns></returns>
2728 private Ear.Object CurrentEarObject()
2730 Ear.Action a = CurrentAction();
2731 Ear.Event e = CurrentEvent();
2742 /// Get the current action based on event tree view selection
2744 /// <returns></returns>
2745 private Ear.Object CurrentEarParent()
2747 TreeNode node = iTreeViewEvents.SelectedNode;
2748 if (node == null || node.Parent == null)
2753 if (node.Parent.Tag is Ear.Object)
2755 return (Ear.Object)node.Parent.Tag;
2758 if (node.Parent.Parent != null && node.Parent.Parent.Tag is Ear.Object)
2760 //Can be the case for events
2761 return (Ear.Object)node.Parent.Parent.Tag;
2771 /// <param name="sender"></param>
2772 /// <param name="e"></param>
2773 private void buttonActionAdd_Click(object sender, EventArgs e)
2775 Ear.Object parent = CurrentEarObject();
2776 if (parent == null )
2778 //We did not find a corresponding event or action
2782 FormEditObject<Ear.Action> ea = new FormEditObject<Ear.Action>();
2783 ea.Text = "Add action";
2784 DialogResult res = CodeProject.Dialog.DlgBox.ShowDialog(ea);
2785 if (res == DialogResult.OK)
2787 parent.Objects.Add(ea.Object);
2788 Properties.Settings.Default.Save();
2789 // We want to select the parent so that one can easily add another action to the same collection
2790 PopulateTreeViewEvents(parent);
2797 /// <param name="sender"></param>
2798 /// <param name="e"></param>
2799 private void buttonActionEdit_Click(object sender, EventArgs e)
2801 Ear.Action selectedAction = CurrentAction();
2802 Ear.Object parent = CurrentEarParent()
2804 if (parent == null || selectedAction == null)
2806 //We did not find a corresponding parent
2810 FormEditObject<Ear.Action> ea = new FormEditObject<Ear.Action>();
2811 ea.Text = "Edit action";
2812 ea.Object = selectedAction;
2813 int actionIndex = iTreeViewEvents.SelectedNode.Index;
2814 DialogResult res = CodeProject.Dialog.DlgBox.ShowDialog(ea);
2815 if (res == DialogResult.OK)
2817 //Make sure we keep the same children as before
2818 ea.Object.Objects = parent.Objects[actionIndex].Objects;
2820 parent.Objects[actionIndex]=ea.Object;
2821 //Save and rebuild our event tree view
2822 Properties.Settings.Default.Save();
2823 PopulateTreeViewEvents(ea.Object);
2830 /// <param name="sender"></param>
2831 /// <param name="e"></param>
2832 private void buttonActionDelete_Click(object sender, EventArgs e)
2834 Ear.Action action = CurrentAction();
2837 //Must select action node
2841 Properties.Settings.Default.EarManager.RemoveAction(action);
2842 Properties.Settings.Default.Save();
2843 PopulateTreeViewEvents();
2849 /// <param name="sender"></param>
2850 /// <param name="e"></param>
2851 private void buttonActionTest_Click(object sender, EventArgs e)
2853 Ear.Action a = CurrentAction();
2858 iTreeViewEvents.Focus();
2864 /// <param name="sender"></param>
2865 /// <param name="e"></param>
2866 private void buttonActionMoveUp_Click(object sender, EventArgs e)
2868 Ear.Action a = CurrentAction();
2870 //Action already at the top of the list
2871 iTreeViewEvents.SelectedNode.Index == 0)
2876 //Swap actions in event's action list
2877 Ear.Object parent = CurrentEarParent();
2878 int currentIndex = iTreeViewEvents.SelectedNode.Index;
2879 Ear.Action movingUp = parent.Objects[currentIndex] as Ear.Action;
2880 Ear.Action movingDown = parent.Objects[currentIndex-1] as Ear.Action;
2881 parent.Objects[currentIndex] = movingDown;
2882 parent.Objects[currentIndex-1] = movingUp;
2884 //Save and populate our tree again
2885 Properties.Settings.Default.Save();
2886 PopulateTreeViewEvents(a);
2892 /// <param name="sender"></param>
2893 /// <param name="e"></param>
2894 private void buttonActionMoveDown_Click(object sender, EventArgs e)
2896 Ear.Action a = CurrentAction();
2898 //Action already at the bottom of the list
2899 iTreeViewEvents.SelectedNode.Index == iTreeViewEvents.SelectedNode.Parent.Nodes.Count - 1)
2904 //Swap actions in event's action list
2905 Ear.Object parent = CurrentEarParent();
2906 int currentIndex = iTreeViewEvents.SelectedNode.Index;
2907 Ear.Action movingDown = parent.Objects[currentIndex] as Ear.Action;
2908 Ear.Action movingUp = parent.Objects[currentIndex + 1] as Ear.Action;
2909 parent.Objects[currentIndex] = movingUp;
2910 parent.Objects[currentIndex + 1] = movingDown;
2912 //Save and populate our tree again
2913 Properties.Settings.Default.Save();
2914 PopulateTreeViewEvents(a);
2921 /// <param name="sender"></param>
2922 /// <param name="e"></param>
2923 private void buttonEventTest_Click(object sender, EventArgs e)
2925 Ear.Event earEvent = CurrentEvent();
2926 if (earEvent != null)
2933 /// Manages events and actions buttons according to selected item in event tree.
2935 /// <param name="sender"></param>
2936 /// <param name="e"></param>
2937 private void iTreeViewEvents_AfterSelect(object sender, TreeViewEventArgs e)
2945 private void UpdateEventView()
2947 //One can always add an event
2948 buttonEventAdd.Enabled = true;
2950 //Enable buttons according to selected item
2951 buttonActionAdd.Enabled =
2952 buttonEventTest.Enabled =
2953 buttonEventDelete.Enabled =
2954 buttonEventEdit.Enabled =
2955 CurrentEvent() != null;
2957 Ear.Action currentAction = CurrentAction();
2958 //If an action is selected enable the following buttons
2959 buttonActionTest.Enabled =
2960 buttonActionDelete.Enabled =
2961 buttonActionMoveUp.Enabled =
2962 buttonActionMoveDown.Enabled =
2963 buttonActionEdit.Enabled =
2964 currentAction != null;
2966 if (currentAction != null)
2968 //If an action is selected enable move buttons if needed
2969 buttonActionMoveUp.Enabled = iTreeViewEvents.SelectedNode.Index != 0;
2970 buttonActionMoveDown.Enabled = iTreeViewEvents.SelectedNode.Index <
2971 iTreeViewEvents.SelectedNode.Parent.Nodes.Count - 1;
2978 /// <param name="sender"></param>
2979 /// <param name="e"></param>
2980 private void buttonEventAdd_Click(object sender, EventArgs e)
2982 FormEditObject<Ear.Event> ea = new FormEditObject<Ear.Event>();
2983 ea.Text = "Add event";
2984 DialogResult res = CodeProject.Dialog.DlgBox.ShowDialog(ea);
2985 if (res == DialogResult.OK)
2987 Properties.Settings.Default.EarManager.Events.Add(ea.Object);
2988 Properties.Settings.Default.Save();
2989 PopulateTreeViewEvents(ea.Object);
2996 /// <param name="sender"></param>
2997 /// <param name="e"></param>
2998 private void buttonEventDelete_Click(object sender, EventArgs e)
3000 Ear.Event currentEvent = CurrentEvent();
3001 if (currentEvent == null)
3003 //Must select action node
3007 Properties.Settings.Default.EarManager.Events.Remove(currentEvent);
3008 Properties.Settings.Default.Save();
3009 PopulateTreeViewEvents();
3015 /// <param name="sender"></param>
3016 /// <param name="e"></param>
3017 private void buttonEventEdit_Click(object sender, EventArgs e)
3019 Ear.Event selectedEvent = CurrentEvent();
3020 if (selectedEvent == null)
3022 //We did not find a corresponding event
3026 FormEditObject<Ear.Event> ea = new FormEditObject<Ear.Event>();
3027 ea.Text = "Edit event";
3028 ea.Object = selectedEvent;
3029 int index = iTreeViewEvents.SelectedNode.Index;
3030 DialogResult res = CodeProject.Dialog.DlgBox.ShowDialog(ea);
3031 if (res == DialogResult.OK)
3033 //Make sure we keep the same actions as before
3034 ea.Object.Objects = Properties.Settings.Default.EarManager.Events[index].Objects;
3036 Properties.Settings.Default.EarManager.Events[index] = ea.Object;
3037 //Save and rebuild our event tree view
3038 Properties.Settings.Default.Save();
3039 PopulateTreeViewEvents(ea.Object);
3046 /// <param name="sender"></param>
3047 /// <param name="e"></param>
3048 private void iTreeViewEvents_Leave(object sender, EventArgs e)
3050 //Make sure our event tree never looses focus
3051 ((TreeView) sender).Focus();
3055 /// Called whenever we loose connection with our HarmonyHub.
3057 /// <param name="aRequestWasCancelled"></param>
3058 private void HarmonyConnectionClosed(object aSender, bool aClosedByServer)
3060 if (aClosedByServer)
3062 //Try reconnect then
3063 #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
3064 BeginInvoke(new MethodInvoker(delegate () { ResetHarmonyAsync(); }));
3065 #pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
3071 int iHarmonyReconnectTries = 0;
3072 const int KHarmonyMaxReconnectTries = 10;
3077 /// <returns></returns>
3078 private async Task ConnectHarmonyAsync(bool aForceAuth=false)
3080 if (Program.HarmonyClient != null)
3082 await Program.HarmonyClient.CloseAsync();
3085 bool success = false;
3087 //Reset Harmony client & config
3088 Program.HarmonyClient = null;
3089 Program.HarmonyConfig = null;
3090 iTreeViewHarmony.Nodes.Clear();
3092 Trace.WriteLine("Harmony: Connecting... ");
3093 //First create our client and login
3094 //Tip: Set keep-alive to false when testing reconnection process
3095 Program.HarmonyClient = new HarmonyHub.Client(iTextBoxHarmonyHubAddress.Text, true);
3096 Program.HarmonyClient.OnConnectionClosed += HarmonyConnectionClosed;
3098 string authToken = Properties.Settings.Default.LogitechAuthToken;
3099 if (!string.IsNullOrEmpty(authToken) && !aForceAuth)
3101 Trace.WriteLine("Harmony: Reusing token: {0}", authToken);
3102 success = await Program.HarmonyClient.TryOpenAsync(authToken);
3105 if (!Program.HarmonyClient.IsReady || !success
3106 // Only first failure triggers new Harmony server AUTH
3107 // That's to avoid calling upon Logitech servers too often
3108 && iHarmonyReconnectTries == 0 )
3110 //We failed to connect using our token
3112 Trace.WriteLine("Harmony: Reseting authentication token!");
3113 Properties.Settings.Default.LogitechAuthToken = "";
3114 Properties.Settings.Default.Save();
3116 Trace.WriteLine("Harmony: Authenticating with Logitech servers...");
3117 success = await Program.HarmonyClient.TryOpenAsync();
3118 //Persist our authentication token in our setting
3121 Trace.WriteLine("Harmony: Saving authentication token.");
3122 Properties.Settings.Default.LogitechAuthToken = Program.HarmonyClient.Token;
3123 Properties.Settings.Default.Save();
3127 // I've seen this failing with "Policy lookup failed on server".
3128 Program.HarmonyConfig = await Program.HarmonyClient.TryGetConfigAsync();
3129 if (Program.HarmonyConfig == null)
3135 // So we now have our Harmony Configuration
3136 PopulateTreeViewHarmony(Program.HarmonyConfig);
3137 // Make sure harmony command actions are showing device name instead of device id
3138 PopulateTreeViewEvents(CurrentEarObject());
3141 // TODO: Consider putting the retry logic one level higher in ResetHarmonyAsync
3144 // See if we need to keep trying
3145 if (iHarmonyReconnectTries < KHarmonyMaxReconnectTries)
3147 iHarmonyReconnectTries++;
3148 Trace.WriteLine("Harmony: Failed to connect, try again: " + iHarmonyReconnectTries);
3149 await ConnectHarmonyAsync();
3153 Trace.WriteLine("Harmony: Failed to connect, giving up!");
3154 iHarmonyReconnectTries = 0;
3155 // TODO: Could use a data member as timer rather than a new instance.
3156 // Try that again in 5 minutes then.
3157 // Using Windows Form timer to make sure we run in the UI thread.
3158 System.Windows.Forms.Timer timer = new System.Windows.Forms.Timer();
3159 timer.Tick += async delegate (object sender, EventArgs e)
3161 // Stop our timer first as we won't need it anymore
3162 (sender as System.Windows.Forms.Timer).Stop();
3163 // Then try to connect again
3164 await ConnectHarmonyAsync();
3166 timer.Interval = 300000;
3172 // We are connected with a valid Harmony Configuration
3173 // Reset our tries counter then
3174 iHarmonyReconnectTries = 0;
3181 /// <param name="aConfig"></param>
3182 private void PopulateTreeViewHarmony(HarmonyHub.Config aConfig)
3184 iTreeViewHarmony.Nodes.Clear();
3186 foreach (HarmonyHub.Device device in aConfig.Devices)
3188 TreeNode deviceNode = iTreeViewHarmony.Nodes.Add(device.Id, $"{device.Label} ({device.DeviceTypeDisplayName}/{device.Model})");
3189 deviceNode.Tag = device;
3191 foreach (HarmonyHub.ControlGroup cg in device.ControlGroups)
3193 TreeNode cgNode = deviceNode.Nodes.Add(cg.Name);
3196 foreach (HarmonyHub.Function f in cg.Functions)
3198 TreeNode fNode = cgNode.Nodes.Add(f.Label);
3204 //treeViewConfig.ExpandAll();
3207 private async void iTreeViewHarmony_NodeMouseDoubleClick(object sender, TreeNodeMouseClickEventArgs e)
3209 //Upon function node double click we execute it
3210 var tag = e.Node.Tag as HarmonyHub.Function;
3211 if (tag != null && e.Node.Parent.Parent.Tag is HarmonyHub.Device)
3213 HarmonyHub.Function f = tag;
3214 HarmonyHub.Device d = (HarmonyHub.Device)e.Node.Parent.Parent.Tag;
3216 Trace.WriteLine($"Harmony: Sending {f.Label} to {d.Label}...");
3218 await Program.HarmonyClient.TrySendKeyPressAsync(d.Id, f.Action.Command);