Starting renaming to HTCIC.
Setup update and test.
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 WndProcDelegate(ref Message aMessage);
75 /// Our Display manager main form
77 [System.ComponentModel.DesignerCategory("Form")]
78 public partial class FormMain : FormMainHid
80 //public Manager iManager = new Manager();
81 DateTime LastTickTime;
83 System.Drawing.Bitmap iBmp;
84 //TODO: Align that with what we did from Audio Visualizers bitmaps?
85 bool iCreateBitmap; //Workaround render to bitmap issues when minimized
86 ServiceHost iServiceHost;
87 // Our collection of clients sorted by session id.
88 public Dictionary<string, ClientData> iClients;
89 // The name of the client which informations are currently displayed.
90 public string iCurrentClientSessionId;
91 ClientData iCurrentClientData;
93 /// Define our display view including layout and fields.
94 /// Display view should include one ClientField that will show on client View.
96 SharpLib.Display.View iDisplayView;
98 bool iHasNewDisplayLayout;
100 public bool iClosing;
102 public bool iSkipFrameRendering;
103 //Function pointer for pixel color filtering
104 ColorProcessingDelegate iColorFx;
105 //Function pointer for pixel X coordinate intercept
106 CoordinateTranslationDelegate iScreenX;
107 //Function pointer for pixel Y coordinate intercept
108 CoordinateTranslationDelegate iScreenY;
110 private AudioManager iAudioManager;
113 private NetworkManager iNetworkManager;
116 /// CEC - Consumer Electronic Control.
117 /// Notably used to turn TV on and off as Windows broadcast monitor on and off notifications.
119 private ConsumerElectronicControl iCecManager;
122 /// Manage run when Windows startup option
124 private StartupManager iStartupManager;
127 /// System notification icon used to hide our application from the task bar.
129 private SharpLib.Notification.Control iNotifyIcon;
132 /// System recording notification icon.
134 private SharpLib.Notification.Control iRecordingNotification;
139 RichTextBoxTraceListener iWriter;
143 /// Allow user to receive window messages;
145 public event WndProcDelegate OnWndProc;
149 if (Properties.Settings.Default.EarManager == null)
151 //No actions in our settings yet
152 Properties.Settings.Default.EarManager = new EarManager();
156 // We loaded events and actions from our settings
157 // Internalizer apparently skips constructor so we need to initialize it here
158 // Though I reckon that should only be needed when loading an empty EAR manager I guess.
159 Properties.Settings.Default.EarManager.Construct();
161 iSkipFrameRendering = false;
163 iCurrentClientSessionId = "";
164 iCurrentClientData = null;
165 LastTickTime = DateTime.Now;
166 //Instantiate our display and register for events notifications
167 iDisplay = new Display();
168 iDisplay.OnOpened += OnDisplayOpened;
169 iDisplay.OnClosed += OnDisplayClosed;
171 iClients = new Dictionary<string, ClientData>();
172 iStartupManager = new StartupManager();
173 iNotifyIcon = new SharpLib.Notification.Control();
174 iRecordingNotification = new SharpLib.Notification.Control();
175 iDisplayView = new SharpLib.Display.View();
176 // Default to a single field showing our client
177 iDisplayView.Layout = new TableLayout(1, 1);
178 iDisplayView.Fields.Add(new ClientField());
180 //Have our designer initialize its controls
181 InitializeComponent();
183 //Redirect console output
184 iWriter = new RichTextBoxTraceListener(richTextBoxLogs);
185 Trace.Listeners.Add(iWriter);
187 //Populate device types
188 PopulateDeviceTypes();
190 //Initial status update
193 //We have a bug when drawing minimized and reusing our bitmap
194 //Though I could not reproduce it on Windows 10
195 iBmp = new System.Drawing.Bitmap(iTableLayoutPanelDisplay.Width, iTableLayoutPanelDisplay.Height,
196 PixelFormat.Format32bppArgb);
197 iCreateBitmap = false;
199 //Minimize our window if desired
200 if (Properties.Settings.Default.StartMinimized)
202 WindowState = FormWindowState.Minimized;
210 /// <param name="sender"></param>
211 /// <param name="e"></param>
212 private void MainForm_Load(object sender, EventArgs e)
214 //Check if we are running a Click Once deployed application
215 if (ApplicationDeployment.IsNetworkDeployed)
217 //This is a proper Click Once installation, fetch and show our version number
218 this.Text += " - v" + ApplicationDeployment.CurrentDeployment.CurrentVersion;
222 //Not a proper Click Once installation, assuming development build then
223 var assembly = Assembly.GetExecutingAssembly();
224 var versionInfo = FileVersionInfo.GetVersionInfo(assembly.Location);
225 this.Text += " - v" + versionInfo.ProductVersion;
226 // Update not supported for non Click Once installation
227 buttonUpdate.Visible = false;
228 //this.Text += " - development";
232 CreateAudioManager();
235 iNetworkManager = new NetworkManager();
236 iNetworkManager.OnConnectivityChanged += OnConnectivityChanged;
237 UpdateNetworkStatus();
240 iCecManager = new ConsumerElectronicControl();
241 OnWndProc += iCecManager.OnWndProc;
248 PopulateTreeViewEvents();
250 //Setup notification icon
253 //Setup recording notification
254 SetupRecordingNotification();
256 // To make sure start up with minimize to tray works
257 if (WindowState == FormWindowState.Minimized && Properties.Settings.Default.MinimizeToTray)
263 // When not debugging we want the screen to be empty until a client takes over
264 ClearLayout(iTableLayoutPanelCurrentClient);
266 // Display layout should be empty too
267 // TODO: Eventually we will need to dynamically create and destroy our client table layout
268 DoClearLayout(iTableLayoutPanelDisplay);
269 iTableLayoutPanelDisplay.ColumnCount = 1;
270 iTableLayoutPanelDisplay.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
271 iTableLayoutPanelDisplay.RowCount = 1;
272 iTableLayoutPanelDisplay.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
274 iTableLayoutPanelDisplay.SetRowSpan(iTableLayoutPanelCurrentClient, 1);
275 iTableLayoutPanelDisplay.SetColumnSpan(iTableLayoutPanelCurrentClient, 1);
276 iTableLayoutPanelDisplay.Controls.Add(iTableLayoutPanelCurrentClient,0,0);
278 // Is that needed? Why just in release?
279 iCurrentClientData = null;
281 //When developing we want at least one client for testing
282 StartNewClient("abcdefghijklmnopqrst-0123456789", "ABCDEFGHIJKLMNOPQRST-0123456789");
285 //Open display connection on start-up if needed
286 if (Properties.Settings.Default.DisplayConnectOnStartup)
288 OpenDisplayConnection();
291 //Start our server so that we can get client requests
294 //Register for HID events
295 RegisterHidDevices();
297 //Start Idle client if needed
298 if (Properties.Settings.Default.StartIdleClient)
305 private void CreateAudioManager()
307 iAudioManager = new AudioManager();
308 iAudioManager.Open(OnDefaultMultiMediaDeviceChanged, OnVolumeNotification);
309 UpdateAudioDeviceAndMasterVolumeThreadSafe();
312 private void DestroyAudioManager()
314 if (iAudioManager != null)
316 iAudioManager.Close();
317 iAudioManager = null;
324 /// <param name="sender"></param>
325 /// <param name="aEvent"></param>
326 public void OnDefaultMultiMediaDeviceChanged(object sender, DefaultDeviceChangedEventArgs aEvent)
328 if (aEvent.DataFlow == DataFlow.Render && aEvent.Role == Role.Multimedia)
330 ResetAudioManagerThreadSafe();
337 private void ResetAudioManagerThreadSafe()
341 //Not in the proper thread, invoke ourselves
342 BeginInvoke(new Action<FormMain>((sender) => { ResetAudioManagerThreadSafe(); }), this);
346 //Proper thread, go ahead
347 DestroyAudioManager();
348 CreateAudioManager();
353 /// Called when our display is opened.
355 /// <param name="aDisplay"></param>
356 private void OnDisplayOpened(Display aDisplay)
358 //Make sure we resume frame rendering
359 iSkipFrameRendering = false;
361 //Set our screen size now that our display is connected
362 //Our panelDisplay is the container of our tableLayoutPanel
363 //tableLayoutPanel will resize itself to fit the client size of our panelDisplay
364 //panelDisplay needs an extra 2 pixels for borders on each sides
365 //tableLayoutPanel will eventually be the exact size of our display
366 Size size = new Size(iDisplay.WidthInPixels() + 2, iDisplay.HeightInPixels() + 2);
367 iPanelDisplay.Size = size;
369 //Our display was just opened, update our UI
371 //Initiate asynchronous request
372 iDisplay.RequestFirmwareRevision();
375 UpdateMasterVolumeThreadSafe();
377 UpdateNetworkStatus();
380 //Testing icon in debug, no arm done if icon not supported
381 //iDisplay.SetIconStatus(Display.TMiniDisplayIconType.EMiniDisplayIconRecording, 0, 1);
382 //iDisplay.SetAllIconsStatus(2);
388 private static void AddActionsToTreeNode(TreeNode aParentNode, Ear.Object aObject)
390 foreach (Ear.Action a in aObject.Objects.OfType<Ear.Action>())
393 TreeNode actionNode = aParentNode.Nodes.Add(a.Brief());
395 //Use color from parent unless our action itself is disabled
396 actionNode.ForeColor = a.Enabled ? aParentNode.ForeColor : Color.DimGray;
398 AddActionsToTreeNode(actionNode,a);
406 /// <param name="aObject"></param>
407 /// <param name="aNode"></param>
408 private static TreeNode FindTreeNodeForEarObject(Ear.Object aObject, TreeNode aNode)
410 if (aNode.Tag == aObject)
415 foreach (TreeNode n in aNode.Nodes)
417 TreeNode found = FindTreeNodeForEarObject(aObject,n);
431 /// <param name="aObject"></param>
432 private void SelectEarObject(Ear.Object aObject)
434 foreach (TreeNode n in iTreeViewEvents.Nodes)
436 TreeNode found = FindTreeNodeForEarObject(aObject, n);
439 iTreeViewEvents.SelectedNode=found;
440 iTreeViewEvents.Focus();
447 /// Populate tree view with events and actions
449 private void PopulateTreeViewEvents(Ear.Object aSelectedObject=null)
452 iTreeViewEvents.Nodes.Clear();
453 //Populate registered events
454 foreach (Ear.Event e in Properties.Settings.Default.EarManager.Events)
456 //Create our event node
457 //Work out the name of our node
458 string eventNodeName = "";
459 if (!string.IsNullOrEmpty(e.Name))
461 //That event has a proper name, use it then
462 eventNodeName = $"{e.Name} - {e.Brief()}";
466 //Unnamed events just use brief
467 eventNodeName = e.Brief();
470 TreeNode eventNode = iTreeViewEvents.Nodes.Add(eventNodeName);
471 eventNode.Tag = e; //For easy access to our event
474 //Dim our nodes if disabled
475 eventNode.ForeColor = Color.DimGray;
478 //Add event description as child node
479 eventNode.Nodes.Add(e.AttributeDescription).ForeColor = eventNode.ForeColor;
480 //Create child node for actions root
481 TreeNode actionsNode = eventNode.Nodes.Add("Actions");
482 actionsNode.ForeColor = eventNode.ForeColor;
484 // Recursively add our actions for that event
485 AddActionsToTreeNode(actionsNode,e);
488 iTreeViewEvents.ExpandAll();
490 if (aSelectedObject != null)
492 SelectEarObject(aSelectedObject);
495 // Just to be safe in case the selection did not work
500 /// Called when our display is closed.
502 /// <param name="aDisplay"></param>
503 private void OnDisplayClosed(Display aDisplay)
505 //Our display was just closed, update our UI consequently
509 public void OnConnectivityChanged(NetworkManager aNetwork, NLM_CONNECTIVITY newConnectivity)
511 //Update network status
512 UpdateNetworkStatus();
516 /// Update our Network Status
518 private void UpdateNetworkStatus()
520 if (iDisplay.IsOpen())
522 iDisplay.SetIconOnOff(MiniDisplay.IconType.Internet,
523 iNetworkManager.NetworkListManager.IsConnectedToInternet);
524 iDisplay.SetIconOnOff(MiniDisplay.IconType.NetworkSignal, iNetworkManager.NetworkListManager.IsConnected);
529 int iLastNetworkIconIndex = 0;
530 int iUpdateCountSinceLastNetworkAnimation = 0;
535 private void UpdateNetworkSignal(DateTime aLastTickTime, DateTime aNewTickTime)
537 iUpdateCountSinceLastNetworkAnimation++;
538 iUpdateCountSinceLastNetworkAnimation = iUpdateCountSinceLastNetworkAnimation%4;
540 if (iDisplay.IsOpen() && iNetworkManager.NetworkListManager.IsConnected &&
541 iUpdateCountSinceLastNetworkAnimation == 0)
543 int iconCount = iDisplay.IconCount(MiniDisplay.IconType.NetworkSignal);
546 //Prevents div by zero and other undefined behavior
549 iLastNetworkIconIndex++;
550 iLastNetworkIconIndex = iLastNetworkIconIndex%(iconCount*2);
551 for (int i = 0; i < iconCount; i++)
553 if (i < iLastNetworkIconIndex && !(i == 0 && iLastNetworkIconIndex > 3) &&
554 !(i == 1 && iLastNetworkIconIndex > 4))
556 iDisplay.SetIconOn(MiniDisplay.IconType.NetworkSignal, i);
560 iDisplay.SetIconOff(MiniDisplay.IconType.NetworkSignal, i);
569 /// Receive volume change notification and reflect changes on our slider.
571 /// <param name="data"></param>
572 public void OnVolumeNotification(object sender, AudioEndpointVolumeCallbackEventArgs aEvent)
574 UpdateMasterVolumeThreadSafe();
578 /// Update master volume when user moves our slider.
580 /// <param name="sender"></param>
581 /// <param name="e"></param>
582 private void trackBarMasterVolume_Scroll(object sender, EventArgs e)
584 //Just like Windows Volume Mixer we unmute if the volume is adjusted
585 iAudioManager.Volume.IsMuted = false;
586 //Set volume level according to our volume slider new position
587 iAudioManager.Volume.MasterVolumeLevelScalar = trackBarMasterVolume.Value/100.0f;
592 /// Mute check box changed.
594 /// <param name="sender"></param>
595 /// <param name="e"></param>
596 private void checkBoxMute_CheckedChanged(object sender, EventArgs e)
598 iAudioManager.Volume.IsMuted = checkBoxMute.Checked;
603 /// Update master volume indicators based our current system states.
604 /// This typically includes volume levels and mute status.
606 private void UpdateMasterVolumeThreadSafe()
610 //Not in the proper thread, invoke ourselves
611 BeginInvoke(new Action<FormMain>((sender) => { UpdateMasterVolumeThreadSafe(); }), this);
615 //Update volume slider
616 float volumeLevelScalar = iAudioManager.Volume.MasterVolumeLevelScalar;
617 trackBarMasterVolume.Value = Convert.ToInt32(volumeLevelScalar*100);
618 //Update mute checkbox
619 checkBoxMute.Checked = iAudioManager.Volume.IsMuted;
621 //If our display connection is open we need to update its icons
622 if (iDisplay.IsOpen())
624 //First take care our our volume level icons
625 int volumeIconCount = iDisplay.IconCount(MiniDisplay.IconType.Volume);
626 if (volumeIconCount > 0)
628 //Compute current volume level from system level and the number of segments in our display volume bar.
629 //That tells us how many segments in our volume bar needs to be turned on.
630 float currentVolume = volumeLevelScalar*volumeIconCount;
631 int segmentOnCount = Convert.ToInt32(currentVolume);
632 //Check if our segment count was rounded up, this will later be used for half brightness segment
633 bool roundedUp = segmentOnCount > currentVolume;
635 for (int i = 0; i < volumeIconCount; i++)
637 if (i < segmentOnCount)
639 //If we are dealing with our last segment and our segment count was rounded up then we will use half brightness.
640 if (i == segmentOnCount - 1 && roundedUp)
643 iDisplay.SetIconStatus(MiniDisplay.IconType.Volume, i,
644 (iDisplay.IconStatusCount(MiniDisplay.IconType.Volume) - 1)/2);
649 iDisplay.SetIconStatus(MiniDisplay.IconType.Volume, i,
650 iDisplay.IconStatusCount(MiniDisplay.IconType.Volume) - 1);
655 iDisplay.SetIconStatus(MiniDisplay.IconType.Volume, i, 0);
660 //Take care of our mute icon
661 iDisplay.SetIconOnOff(MiniDisplay.IconType.Mute, iAudioManager.Volume.IsMuted);
671 private void UpdateAudioVisualization()
673 // No point if we don't have a current client
674 if (iCurrentClientData == null)
680 if (iAudioManager==null || iAudioManager.Spectrum==null || !iAudioManager.Spectrum.Update())
682 //Nothing changed no need to render
686 // Check if our current client has an Audio Visualizer field
687 // and render them as needed
688 foreach (DataField f in iCurrentClientData.View.Fields)
690 if (f is AudioVisualizerField)
692 AudioVisualizerField avf = (AudioVisualizerField)f;
693 Control ctrl = iTableLayoutPanelCurrentClient.GetControlFromPosition(avf.Column, avf.Row);
695 if (ctrl is PictureBox)
697 PictureBox pb = (PictureBox)ctrl;
698 if (iAudioManager.Spectrum.Render(pb.Image, Color.Black, Color.Black, Color.White, false))
711 private void UpdateAudioDeviceAndMasterVolumeThreadSafe()
715 //Not in the proper thread, invoke ourselves
716 BeginInvoke(new Action<FormMain>((sender) => { UpdateAudioDeviceAndMasterVolumeThreadSafe(); }), this);
720 //We are in the correct thread just go ahead.
725 iLabelDefaultAudioDevice.Text = iAudioManager.DefaultDevice.FriendlyName;
727 //Show our volume in our track bar
728 UpdateMasterVolumeThreadSafe();
731 trackBarMasterVolume.Enabled = true;
735 Debug.WriteLine("Exception thrown in UpdateAudioDeviceAndMasterVolume");
736 Debug.WriteLine(ex.ToString());
737 //Something went wrong S/PDIF device ca throw exception I guess
738 trackBarMasterVolume.Enabled = false;
745 private void PopulateDeviceTypes()
747 int count = Display.TypeCount();
749 for (int i = 0; i < count; i++)
751 comboBoxDisplayType.Items.Add(Display.TypeName((MiniDisplay.Type) i));
758 private void SetupTrayIcon()
760 iNotifyIcon.Icon = GetIcon("vfd.ico");
761 iNotifyIcon.Text = "Sharp Display Manager";
762 iNotifyIcon.Visible = true;
764 //Double click toggles visibility - typically brings up the application
765 iNotifyIcon.DoubleClick += delegate(object obj, EventArgs args)
770 //Adding a context menu, useful to be able to exit the application
771 ContextMenu contextMenu = new ContextMenu();
772 //Context menu item to toggle visibility
773 MenuItem hideShowItem = new MenuItem("Hide/Show");
774 hideShowItem.Click += delegate(object obj, EventArgs args)
778 contextMenu.MenuItems.Add(hideShowItem);
780 //Context menu item separator
781 contextMenu.MenuItems.Add(new MenuItem("-"));
783 //Context menu exit item
784 MenuItem exitItem = new MenuItem("Exit");
785 exitItem.Click += delegate(object obj, EventArgs args)
789 contextMenu.MenuItems.Add(exitItem);
791 iNotifyIcon.ContextMenu = contextMenu;
797 private void SetupRecordingNotification()
799 iRecordingNotification.Icon = GetIcon("record.ico");
800 iRecordingNotification.Text = "No recording";
801 iRecordingNotification.Visible = false;
805 /// Access icons from embedded resources.
807 /// <param name="aName"></param>
808 /// <returns></returns>
809 public static Icon GetIcon(string aName)
811 string[] names = Assembly.GetExecutingAssembly().GetManifestResourceNames();
812 foreach (string name in names)
814 //Find a resource name that ends with the given name
815 if (name.EndsWith(aName))
817 using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(name))
819 return new Icon(stream);
829 /// Set our current client.
830 /// This will take care of applying our client layout and set data fields.
832 /// <param name="aSessionId"></param>
833 void SetCurrentClient(string aSessionId, bool aForce = false)
835 if (aSessionId == iCurrentClientSessionId)
837 //Given client is already the current one.
838 //Don't bother changing anything then.
842 ClientData requestedClientData = iClients[aSessionId];
844 //Check when was the last time we switched to that client
845 if (iCurrentClientData != null)
847 //Do not switch client if priority of current client is higher
848 if (!aForce && requestedClientData.Priority < iCurrentClientData.Priority)
854 double lastSwitchToClientSecondsAgo = (DateTime.Now - iCurrentClientData.LastSwitchTime).TotalSeconds;
855 //TODO: put that hard coded value as a client property
856 //Clients should be able to define how often they can be interrupted
857 //Thus a background client can set this to zero allowing any other client to interrupt at any time
858 //We could also compute this delay by looking at the requests frequencies?
860 requestedClientData.Priority == iCurrentClientData.Priority &&
861 //Time sharing is only if clients have the same priority
862 (lastSwitchToClientSecondsAgo < 30)) //Make sure a client is on for at least 30 seconds
864 //Don't switch clients too often
869 //Set current client ID.
870 iCurrentClientSessionId = aSessionId;
871 //Set the time we last switched to that client
872 iClients[aSessionId].LastSwitchTime = DateTime.Now;
873 //Fetch and set current client data.
874 iCurrentClientData = requestedClientData;
875 //Apply layout and set data fields.
876 UpdateTableLayoutPanel(iCurrentClientData.View, iTableLayoutPanelCurrentClient);
879 private void buttonFont_Click(object sender, EventArgs e)
881 //fontDialog.ShowColor = true;
882 //fontDialog.ShowApply = true;
883 fontDialog.ShowEffects = true;
884 fontDialog.Font = cds.Font;
886 fontDialog.FixedPitchOnly = checkBoxFixedPitchFontOnly.Checked;
888 //fontDialog.ShowHelp = true;
890 //fontDlg.MaxSize = 40;
891 //fontDlg.MinSize = 22;
893 //fontDialog.Parent = this;
894 //fontDialog.StartPosition = FormStartPosition.CenterParent;
896 //DlgBox.ShowDialog(fontDialog);
898 //if (fontDialog.ShowDialog(this) != DialogResult.Cancel)
899 if (DlgBox.ShowDialog(fontDialog) != DialogResult.Cancel)
902 cds.Font = fontDialog.Font;
903 Properties.Settings.Default.Save();
905 //Set the fonts to all our labels in our layout
906 UpdateFonts(iTableLayoutPanelDisplay);
913 /// TODO: review this in respect to our logical font feature when we get there.
915 void CheckFontHeight()
917 //Show font height and width
918 labelFontHeight.Text = "Font height: " + cds.Font.Height;
919 float charWidth = IsFixedWidth(cds.Font);
920 if (charWidth == 0.0f)
922 labelFontWidth.Visible = false;
926 labelFontWidth.Visible = true;
927 labelFontWidth.Text = "Font width: " + charWidth;
930 MarqueeLabel label = null;
931 //Get the first label control we can find
932 foreach (Control ctrl in iTableLayoutPanelCurrentClient.Controls)
934 if (ctrl is MarqueeLabel)
936 label = (MarqueeLabel) ctrl;
941 //Now check font height and show a warning if needed.
942 if (label != null && label.Font.Height > label.Height)
944 labelWarning.Text = "WARNING: Selected font is too height by " + (label.Font.Height - label.Height) +
946 labelWarning.Visible = true;
950 labelWarning.Visible = false;
955 private void buttonCapture_Click(object sender, EventArgs e)
957 System.Drawing.Bitmap bmp = new System.Drawing.Bitmap(iTableLayoutPanelDisplay.Width, iTableLayoutPanelDisplay.Height);
958 iTableLayoutPanelDisplay.DrawToBitmap(bmp, iTableLayoutPanelDisplay.ClientRectangle);
959 //Bitmap bmpToSave = new Bitmap(bmp);
960 bmp.Save("D:\\capture.png");
963 string outputFileName = "d:\\capture.png";
964 using (MemoryStream memory = new MemoryStream())
966 using (FileStream fs = new FileStream(outputFileName, FileMode.OpenOrCreate, FileAccess.ReadWrite))
968 bmp.Save(memory, System.Drawing.Imaging.ImageFormat.Png);
969 byte[] bytes = memory.ToArray();
970 fs.Write(bytes, 0, bytes.Length);
977 private void CheckForRequestResults()
979 if (iDisplay.IsRequestPending())
981 switch (iDisplay.AttemptRequestCompletion())
983 case MiniDisplay.Request.FirmwareRevision:
984 toolStripStatusLabelConnect.Text += " v" + iDisplay.FirmwareRevision();
985 //Issue next request then
986 iDisplay.RequestPowerSupplyStatus();
989 case MiniDisplay.Request.PowerSupplyStatus:
990 if (iDisplay.PowerSupplyStatus())
992 toolStripStatusLabelPower.Text = "ON";
996 toolStripStatusLabelPower.Text = "OFF";
998 //Issue next request then
999 iDisplay.RequestDeviceId();
1002 case MiniDisplay.Request.DeviceId:
1003 toolStripStatusLabelConnect.Text += " - " + iDisplay.DeviceId();
1004 //No more request to issue
1010 public static uint ColorWhiteIsOn(int aX, int aY, uint aPixel)
1012 if ((aPixel & 0x00FFFFFF) == 0x00FFFFFF)
1019 public static uint ColorUntouched(int aX, int aY, uint aPixel)
1024 public static uint ColorInversed(int aX, int aY, uint aPixel)
1029 public static uint ColorChessboard(int aX, int aY, uint aPixel)
1031 if ((aX%2 == 0) && (aY%2 == 0))
1035 else if ((aX%2 != 0) && (aY%2 != 0))
1043 public static int ScreenReversedX(System.Drawing.Bitmap aBmp, int aX)
1045 return aBmp.Width - aX - 1;
1048 public int ScreenReversedY(System.Drawing.Bitmap aBmp, int aY)
1050 return iBmp.Height - aY - 1;
1053 public int ScreenX(System.Drawing.Bitmap aBmp, int aX)
1058 public int ScreenY(System.Drawing.Bitmap aBmp, int aY)
1064 /// Select proper pixel delegates according to our current settings.
1066 private void SetupPixelDelegates()
1068 //Select our pixel processing routine
1069 if (cds.InverseColors)
1071 //iColorFx = ColorChessboard;
1072 iColorFx = ColorInversed;
1076 iColorFx = ColorWhiteIsOn;
1079 //Select proper coordinate translation functions
1080 //We used delegate/function pointer to support reverse screen without doing an extra test on each pixels
1081 if (cds.ReverseScreen)
1083 iScreenX = ScreenReversedX;
1084 iScreenY = ScreenReversedY;
1095 /// This is our timer tick responsible to perform our render
1096 /// TODO: Use a threading timer instead of a Windows form timer.
1098 /// <param name="sender"></param>
1099 /// <param name="e"></param>
1100 private void timer_Tick(object sender, EventArgs e)
1102 //Update our animations
1103 DateTime NewTickTime = DateTime.Now;
1105 UpdateNetworkSignal(LastTickTime, NewTickTime);
1107 // Update animation for all our marquees
1108 UpdateMarqueesAnimations(iTableLayoutPanelDisplay, LastTickTime, NewTickTime);
1110 // Update audio visualization
1111 UpdateAudioVisualization();
1113 //Update our display
1114 if (iDisplay.IsOpen())
1116 CheckForRequestResults();
1118 //Check if frame rendering is needed
1119 //Typically used when showing clock
1120 if (!iSkipFrameRendering)
1125 iBmp = new System.Drawing.Bitmap(iTableLayoutPanelDisplay.Width, iTableLayoutPanelDisplay.Height,
1126 PixelFormat.Format32bppArgb);
1127 iCreateBitmap = false;
1129 iTableLayoutPanelDisplay.DrawToBitmap(iBmp, iTableLayoutPanelDisplay.ClientRectangle);
1130 //iBmp.Save("D:\\capture.png");
1132 //Send it to our display
1133 for (int i = 0; i < iBmp.Width; i++)
1135 for (int j = 0; j < iBmp.Height; j++)
1139 //Get our processed pixel coordinates
1140 int x = iScreenX(iBmp, i);
1141 int y = iScreenY(iBmp, j);
1143 uint color = (uint) iBmp.GetPixel(i, j).ToArgb();
1144 //Apply color effects
1145 color = iColorFx(x, y, color);
1147 iDisplay.SetPixel(x, y, color);
1152 iDisplay.SwapBuffers();
1156 //Compute instant FPS
1157 toolStripStatusLabelFps.Text = (1.0/NewTickTime.Subtract(LastTickTime).TotalSeconds).ToString("F0") + " / " +
1158 (1000/iTimerDisplay.Interval).ToString() + " FPS";
1160 LastTickTime = NewTickTime;
1165 /// Update marquee animation children of the given control.
1167 static private void UpdateMarqueesAnimations(Control aControl, DateTime aLastTickTime, DateTime aNewTickTime)
1169 // For each our children
1170 foreach (Control ctrl in aControl.Controls)
1172 // Update animation if it is a marquee control
1173 if (ctrl is MarqueeLabel)
1175 ((MarqueeLabel)ctrl).UpdateAnimation(aLastTickTime, aNewTickTime);
1178 // Go one level deeper by recursion
1179 UpdateMarqueesAnimations(ctrl, aLastTickTime, aNewTickTime);
1184 /// Attempt to establish connection with our display hardware.
1186 private void OpenDisplayConnection()
1188 CloseDisplayConnection();
1190 if (!iDisplay.Open((MiniDisplay.Type) cds.DisplayType))
1193 toolStripStatusLabelConnect.Text = "Connection error";
1197 private void CloseDisplayConnection()
1199 //Status will be updated upon receiving the closed event
1201 if (iDisplay == null || !iDisplay.IsOpen())
1206 //Do not clear if we gave up on rendering already.
1207 //This means we will keep on displaying clock on MDM166AA for instance.
1208 if (!iSkipFrameRendering)
1211 iDisplay.SwapBuffers();
1214 iDisplay.SetAllIconsStatus(0); //Turn off all icons
1218 private void buttonOpen_Click(object sender, EventArgs e)
1220 OpenDisplayConnection();
1223 private void buttonClose_Click(object sender, EventArgs e)
1225 CloseDisplayConnection();
1228 private void buttonClear_Click(object sender, EventArgs e)
1231 iDisplay.SwapBuffers();
1234 private void buttonFill_Click(object sender, EventArgs e)
1237 iDisplay.SwapBuffers();
1240 private void trackBarBrightness_Scroll(object sender, EventArgs e)
1242 cds.Brightness = trackBarBrightness.Value;
1243 Properties.Settings.Default.Save();
1244 iDisplay.SetBrightness(trackBarBrightness.Value);
1250 /// CDS stands for Current Display Settings
1252 private DisplaySettings cds
1256 DisplaysSettings settings = Properties.Settings.Default.DisplaysSettings;
1257 if (settings == null)
1259 settings = new DisplaysSettings();
1261 Properties.Settings.Default.DisplaysSettings = settings;
1264 //Make sure all our settings have been created
1265 while (settings.Displays.Count <= Properties.Settings.Default.CurrentDisplayIndex)
1267 settings.Displays.Add(new DisplaySettings());
1270 DisplaySettings displaySettings = settings.Displays[Properties.Settings.Default.CurrentDisplayIndex];
1271 return displaySettings;
1276 /// Check if the given font has a fixed character pitch.
1278 /// <param name="ft"></param>
1279 /// <returns>0.0f if this is not a monospace font, otherwise returns the character width.</returns>
1280 public float IsFixedWidth(Font ft)
1282 Graphics g = CreateGraphics();
1283 char[] charSizes = new char[] {'i', 'a', 'Z', '%', '#', 'a', 'B', 'l', 'm', ',', '.'};
1284 float charWidth = g.MeasureString("I", ft, Int32.MaxValue, StringFormat.GenericTypographic).Width;
1286 bool fixedWidth = true;
1288 foreach (char c in charSizes)
1289 if (g.MeasureString(c.ToString(), ft, Int32.MaxValue, StringFormat.GenericTypographic).Width !=
1302 /// Update fonts in our control tree.
1304 /// <param name="aControls"></param>
1305 private void UpdateFonts(Control aControl)
1307 foreach (Control ctrl in aControl.Controls)
1309 if (ctrl is MarqueeLabel)
1311 MarqueeLabel marquee = (MarqueeLabel)ctrl;
1312 marquee.Font = cds.Font;
1321 /// Update marquees separator in our control tree.
1323 /// <param name="aControls"></param>
1324 private void UpdateMarqueesSeparator(Control aControl)
1326 foreach (Control ctrl in aControl.Controls)
1328 if (ctrl is MarqueeLabel)
1330 MarqueeLabel marquee = (MarqueeLabel)ctrl;
1331 marquee.Separator = cds.Separator;
1335 UpdateMarqueesSeparator(ctrl);
1340 /// Synchronize UI with settings
1342 private void UpdateStatus()
1345 checkBoxShowBorders.Checked = cds.ShowBorders;
1346 iTableLayoutPanelCurrentClient.CellBorderStyle = (cds.ShowBorders
1347 ? TableLayoutPanelCellBorderStyle.Single
1348 : TableLayoutPanelCellBorderStyle.None);
1350 //Set the proper font to each of our labels
1351 UpdateFonts(iTableLayoutPanelDisplay);
1354 //Check if "run on Windows startup" is enabled
1355 checkBoxAutoStart.Checked = iStartupManager.Startup;
1358 comboBoxHdmiPort.SelectedIndex = Properties.Settings.Default.CecHdmiPort - 1;
1360 //Mini Display settings
1361 checkBoxReverseScreen.Checked = cds.ReverseScreen;
1362 checkBoxInverseColors.Checked = cds.InverseColors;
1363 checkBoxShowVolumeLabel.Checked = cds.ShowVolumeLabel;
1364 checkBoxScaleToFit.Checked = cds.ScaleToFit;
1365 maskedTextBoxMinFontSize.Enabled = cds.ScaleToFit;
1366 labelMinFontSize.Enabled = cds.ScaleToFit;
1367 maskedTextBoxMinFontSize.Text = cds.MinFontSize.ToString();
1368 maskedTextBoxScrollingSpeed.Text = cds.ScrollingSpeedInPixelsPerSecond.ToString();
1369 comboBoxDisplayType.SelectedIndex = cds.DisplayType;
1370 iTimerDisplay.Interval = cds.TimerInterval;
1371 maskedTextBoxTimerInterval.Text = cds.TimerInterval.ToString();
1372 textBoxScrollLoopSeparator.Text = cds.Separator;
1374 SetupPixelDelegates();
1376 if (iDisplay.IsOpen())
1378 //We have a display connection
1379 //Reflect that in our UI
1382 iTableLayoutPanelDisplay.Enabled = true;
1383 iTableLayoutPanelCurrentClient.Enabled = true;
1384 iPanelDisplay.Enabled = true;
1386 //Only setup brightness if display is open
1387 trackBarBrightness.Minimum = iDisplay.MinBrightness();
1388 trackBarBrightness.Maximum = iDisplay.MaxBrightness();
1389 if (cds.Brightness < iDisplay.MinBrightness() || cds.Brightness > iDisplay.MaxBrightness())
1391 //Brightness out of range, this can occur when using auto-detect
1392 //Use max brightness instead
1393 trackBarBrightness.Value = iDisplay.MaxBrightness();
1394 iDisplay.SetBrightness(iDisplay.MaxBrightness());
1398 trackBarBrightness.Value = cds.Brightness;
1399 iDisplay.SetBrightness(cds.Brightness);
1402 //Try compute the steps to something that makes sense
1403 trackBarBrightness.LargeChange = Math.Max(1, (iDisplay.MaxBrightness() - iDisplay.MinBrightness())/5);
1404 trackBarBrightness.SmallChange = 1;
1407 buttonFill.Enabled = true;
1408 buttonClear.Enabled = true;
1409 buttonOpen.Enabled = false;
1410 buttonClose.Enabled = true;
1411 trackBarBrightness.Enabled = true;
1412 toolStripStatusLabelConnect.Text = "Connected - " + iDisplay.Vendor() + " - " + iDisplay.Product();
1413 //+ " - " + iDisplay.SerialNumber();
1415 if (iDisplay.SupportPowerOnOff())
1417 buttonPowerOn.Enabled = true;
1418 buttonPowerOff.Enabled = true;
1422 buttonPowerOn.Enabled = false;
1423 buttonPowerOff.Enabled = false;
1426 if (iDisplay.SupportClock())
1428 buttonShowClock.Enabled = true;
1429 buttonHideClock.Enabled = true;
1433 buttonShowClock.Enabled = false;
1434 buttonHideClock.Enabled = false;
1438 //Check if Volume Label is supported. To date only MDM166AA supports that crap :)
1439 checkBoxShowVolumeLabel.Enabled = iDisplay.IconCount(MiniDisplay.IconType.VolumeLabel) > 0;
1441 if (cds.ShowVolumeLabel)
1443 iDisplay.SetIconOn(MiniDisplay.IconType.VolumeLabel);
1447 iDisplay.SetIconOff(MiniDisplay.IconType.VolumeLabel);
1452 //Display connection not available
1453 //Reflect that in our UI
1455 //In debug start our timer even if we don't have a display connection
1458 //In production environment we don't need our timer if no display connection
1461 checkBoxShowVolumeLabel.Enabled = false;
1462 iTableLayoutPanelDisplay.Enabled = false;
1463 iTableLayoutPanelCurrentClient.Enabled = false;
1464 iPanelDisplay.Enabled = false;
1465 buttonFill.Enabled = false;
1466 buttonClear.Enabled = false;
1467 buttonOpen.Enabled = true;
1468 buttonClose.Enabled = false;
1469 trackBarBrightness.Enabled = false;
1470 buttonPowerOn.Enabled = false;
1471 buttonPowerOff.Enabled = false;
1472 buttonShowClock.Enabled = false;
1473 buttonHideClock.Enabled = false;
1474 toolStripStatusLabelConnect.Text = "Disconnected";
1475 toolStripStatusLabelPower.Text = "N/A";
1484 /// <param name="sender"></param>
1485 /// <param name="e"></param>
1486 private void checkBoxShowVolumeLabel_CheckedChanged(object sender, EventArgs e)
1488 cds.ShowVolumeLabel = checkBoxShowVolumeLabel.Checked;
1489 Properties.Settings.Default.Save();
1493 private void checkBoxShowBorders_CheckedChanged(object sender, EventArgs e)
1495 //Save our show borders setting
1496 iTableLayoutPanelCurrentClient.CellBorderStyle = (checkBoxShowBorders.Checked
1497 ? TableLayoutPanelCellBorderStyle.Single
1498 : TableLayoutPanelCellBorderStyle.None);
1499 cds.ShowBorders = checkBoxShowBorders.Checked;
1500 Properties.Settings.Default.Save();
1504 private void checkBoxAutoStart_CheckedChanged(object sender, EventArgs e)
1506 iStartupManager.Startup = checkBoxAutoStart.Checked;
1510 private void checkBoxReverseScreen_CheckedChanged(object sender, EventArgs e)
1512 //Save our reverse screen setting
1513 cds.ReverseScreen = checkBoxReverseScreen.Checked;
1514 Properties.Settings.Default.Save();
1515 SetupPixelDelegates();
1518 private void checkBoxInverseColors_CheckedChanged(object sender, EventArgs e)
1520 //Save our inverse colors setting
1521 cds.InverseColors = checkBoxInverseColors.Checked;
1522 Properties.Settings.Default.Save();
1523 SetupPixelDelegates();
1526 private void checkBoxScaleToFit_CheckedChanged(object sender, EventArgs e)
1528 //Save our scale to fit setting
1529 cds.ScaleToFit = checkBoxScaleToFit.Checked;
1530 Properties.Settings.Default.Save();
1532 labelMinFontSize.Enabled = cds.ScaleToFit;
1533 maskedTextBoxMinFontSize.Enabled = cds.ScaleToFit;
1536 private void MainForm_Resize(object sender, EventArgs e)
1538 if (WindowState == FormWindowState.Minimized)
1540 // To workaround our empty bitmap bug on Windows 7 we need to recreate our bitmap when the application is minimized
1541 // That's apparently not needed on Windows 10 but we better leave it in place.
1542 iCreateBitmap = true;
1546 private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
1549 iNetworkManager.Dispose();
1550 DestroyAudioManager();
1551 CloseDisplayConnection();
1553 e.Cancel = iClosing;
1556 public void StartServer()
1558 iServiceHost = new ServiceHost
1561 new Uri[] {new Uri("net.tcp://localhost:8001/")}
1564 iServiceHost.AddServiceEndpoint(typeof(IService), new NetTcpBinding(SecurityMode.None, true),
1566 iServiceHost.Open();
1569 public void StopServer()
1571 if (iClients.Count > 0 && !iClosing)
1575 BroadcastCloseEvent();
1580 MessageBox.Show("Force exit?", "Waiting for clients...", MessageBoxButtons.YesNo,
1581 MessageBoxIcon.Warning) == DialogResult.Yes)
1583 iClosing = false; //We make sure we force close if asked twice
1588 //We removed that as it often lags for some reason
1589 //iServiceHost.Close();
1596 public void BroadcastCloseEvent()
1598 Trace.TraceInformation("BroadcastCloseEvent - start");
1600 var inactiveClients = new List<string>();
1601 foreach (var client in iClients)
1603 //if (client.Key != eventData.ClientName)
1607 Trace.TraceInformation("BroadcastCloseEvent - " + client.Key);
1608 client.Value.Callback.OnCloseOrder( /*eventData*/);
1610 catch (Exception ex)
1612 inactiveClients.Add(client.Key);
1617 if (inactiveClients.Count > 0)
1619 foreach (var client in inactiveClients)
1621 iClients.Remove(client);
1622 Program.iFormMain.iTreeViewClients.Nodes.Remove(
1623 Program.iFormMain.iTreeViewClients.Nodes.Find(client, false)[0]);
1627 if (iClients.Count == 0)
1629 ClearLayout(iTableLayoutPanelCurrentClient);
1630 iCurrentClientData = null;
1635 /// Just remove all our fields.
1637 static private void ClearLayout(TableLayoutPanel aPanel)
1639 // For each loop did not work as calling Dispose on a control removes it from the collection.
1640 // We make sure every control are disposed of notably to turn off visualizer when no more needed.
1641 // That's the only way we found to make sure Control.Disposed is called in a timely fashion.
1642 // Though that loop is admetitly dangerous as if one of the control does not removes itself from the list we end up with infinite loop.
1643 // That's what happened with our MarqueeLabel until we fixed it's Dispose override.
1644 while (aPanel.Controls.Count>0)
1646 // Dispose our last item
1647 aPanel.Controls[aPanel.Controls.Count - 1].Dispose();
1650 DoClearLayout(aPanel);
1656 /// <param name="aPanel"></param>
1657 static private void DoClearLayout(TableLayoutPanel aPanel)
1659 aPanel.Controls.Clear();
1660 aPanel.RowStyles.Clear();
1661 aPanel.ColumnStyles.Clear();
1662 aPanel.RowCount = 0;
1663 aPanel.ColumnCount = 0;
1667 /// Just launch a demo client.
1669 private void StartNewClient(string aTopText = "", string aBottomText = "")
1671 Thread clientThread = new Thread(SharpDisplayClient.Program.MainWithParams);
1672 SharpDisplayClient.StartParams myParams = new SharpDisplayClient.StartParams(
1673 new Point(this.Right, this.Top), aTopText, aBottomText);
1674 clientThread.Start(myParams);
1679 /// Just launch our idle client.
1681 private void StartIdleClient(string aTopText = "", string aBottomText = "")
1683 Thread clientThread = new Thread(SharpDisplayClientIdle.Program.MainWithParams);
1684 SharpDisplayClientIdle.StartParams myParams =
1685 new SharpDisplayClientIdle.StartParams(new Point(this.Right, this.Top), aTopText, aBottomText);
1686 clientThread.Start(myParams);
1691 private void buttonStartClient_Click(object sender, EventArgs e)
1696 private void buttonSuspend_Click(object sender, EventArgs e)
1701 private void StartTimer()
1703 LastTickTime = DateTime.Now; //Reset timer to prevent jump
1704 iTimerDisplay.Enabled = true;
1705 UpdateSuspendButton();
1708 private void StopTimer()
1710 LastTickTime = DateTime.Now; //Reset timer to prevent jump
1711 iTimerDisplay.Enabled = false;
1712 UpdateSuspendButton();
1715 private void ToggleTimer()
1717 LastTickTime = DateTime.Now; //Reset timer to prevent jump
1718 iTimerDisplay.Enabled = !iTimerDisplay.Enabled;
1719 UpdateSuspendButton();
1722 private void UpdateSuspendButton()
1724 if (!iTimerDisplay.Enabled)
1726 buttonSuspend.Text = "Run";
1730 buttonSuspend.Text = "Pause";
1735 private void buttonCloseClients_Click(object sender, EventArgs e)
1737 BroadcastCloseEvent();
1740 private void treeViewClients_AfterSelect(object sender, TreeViewEventArgs e)
1742 //Root node must have at least one child
1743 if (e.Node.Nodes.Count == 0)
1748 //If the selected node is the root node of a client then switch to it
1749 string sessionId = e.Node.Nodes[0].Text; //First child of a root node is the sessionId
1750 if (iClients.ContainsKey(sessionId)) //Check that's actually what we are looking at
1752 //We have a valid session just switch to that client
1753 SetCurrentClient(sessionId, true);
1762 /// <param name="aSessionId"></param>
1763 /// <param name="aCallback"></param>
1764 public void AddClientThreadSafe(string aSessionId, ICallback aCallback)
1766 if (this.InvokeRequired)
1768 //Not in the proper thread, invoke ourselves
1769 AddClientDelegate d = new AddClientDelegate(AddClientThreadSafe);
1770 this.Invoke(d, new object[] {aSessionId, aCallback});
1774 //We are in the proper thread
1775 //Add this session to our collection of clients
1776 ClientData newClient = new ClientData(aSessionId, aCallback);
1777 Program.iFormMain.iClients.Add(aSessionId, newClient);
1778 //Add this session to our UI
1779 UpdateClientTreeViewNode(newClient);
1785 /// Find the client with the highest priority if any.
1787 /// <returns>Our highest priority client or null if not a single client is connected.</returns>
1788 public ClientData FindHighestPriorityClient()
1790 ClientData highestPriorityClient = null;
1791 foreach (var client in iClients)
1793 if (highestPriorityClient == null || client.Value.Priority > highestPriorityClient.Priority)
1795 highestPriorityClient = client.Value;
1799 return highestPriorityClient;
1805 /// <param name="aSessionId"></param>
1806 public void RemoveClientThreadSafe(string aSessionId)
1808 if (this.InvokeRequired)
1810 //Not in the proper thread, invoke ourselves
1811 RemoveClientDelegate d = new RemoveClientDelegate(RemoveClientThreadSafe);
1812 this.Invoke(d, new object[] {aSessionId});
1816 //We are in the proper thread
1817 //Remove this session from both client collection and UI tree view
1818 if (Program.iFormMain.iClients.Keys.Contains(aSessionId))
1820 Program.iFormMain.iClients.Remove(aSessionId);
1821 Program.iFormMain.iTreeViewClients.Nodes.Remove(
1822 Program.iFormMain.iTreeViewClients.Nodes.Find(aSessionId, false)[0]);
1823 //Update recording status too whenever a client is removed
1824 UpdateRecordingNotification();
1827 if (iCurrentClientSessionId == aSessionId)
1829 //The current client is closing
1830 iCurrentClientData = null;
1831 //Find the client with the highest priority and set it as current
1832 ClientData newCurrentClient = FindHighestPriorityClient();
1833 if (newCurrentClient != null)
1835 SetCurrentClient(newCurrentClient.SessionId, true);
1839 if (iClients.Count == 0)
1841 //Clear our screen when last client disconnects
1842 ClearLayout(iTableLayoutPanelCurrentClient);
1843 iCurrentClientData = null;
1847 //We were closing our form
1848 //All clients are now closed
1849 //Just resume our close operation
1860 /// <param name="aSessionId"></param>
1861 /// <param name="aLayout"></param>
1862 public void SetClientLayoutThreadSafe(string aSessionId, TableLayout aLayout)
1864 if (this.InvokeRequired)
1866 //Not in the proper thread, invoke ourselves
1867 Invoke(new Action<FormMain>((sender) => { SetClientLayoutThreadSafe(aSessionId, aLayout); }), this);
1871 ClientData client = iClients[aSessionId];
1878 // If we have a matching client and we want to change the client layout
1879 if (client.Target == Target.Client)
1881 //Don't change a thing if the layout is the same
1882 if (!client.View.Layout.IsSameAs(aLayout))
1884 Debug.Print("SetClientLayoutThreadSafe: Layout updated.");
1885 //Set our client layout then
1886 client.View.Layout = aLayout;
1887 //So that next time we update all our fields at ones
1888 client.HasNewLayout = true;
1889 //Layout has changed clear our fields then
1890 client.View.Fields.Clear();
1892 UpdateClientTreeViewNode(client);
1896 Debug.Print("SetClientLayoutThreadSafe: Layout has not changed.");
1899 else if (client.Target == Target.Display)
1901 // Mark our display layout has updated and wait for the fields.
1902 // Is display layout a property from our client?
1903 //iTableLayoutPanelDisplay. = aLayout;
1910 /// <param name="aSessionId"></param>
1911 /// <param name="aField"></param>
1912 public void SetClientFieldThreadSafe(string aSessionId, DataField aField)
1914 if (this.InvokeRequired)
1916 //Not in the proper thread, invoke ourselves
1917 Invoke(new Action<FormMain>((sender) => { SetClientFieldThreadSafe(aSessionId, aField); }), this);
1921 //We are in the proper thread
1922 //Call the non-thread-safe variant
1923 SetClientField(aSessionId, aField);
1931 /// Set a data field in the given client.
1933 /// <param name="aSessionId"></param>
1934 /// <param name="aField"></param>
1935 private void SetClientField(string aSessionId, DataField aField)
1937 //TODO: should check if the field actually changed?
1939 ClientData client = iClients[aSessionId];
1940 bool layoutChanged = false;
1941 bool contentChanged = true;
1943 //Fetch our field index
1944 int fieldIndex = client.View.FindSameFieldIndex(aField);
1948 //No corresponding field, just bail out
1952 //Keep our previous field in there
1953 DataField previousField = client.View.Fields[fieldIndex];
1954 //Just update that field then
1955 client.View.Fields[fieldIndex] = aField;
1957 if (!aField.IsTableField)
1959 //We are done then if that field is not in our table layout
1963 TableField tableField = (TableField) aField;
1965 if (previousField.IsSameLayout(aField))
1967 //If we are updating a field in our current client we need to update it in our panel
1968 if (aSessionId == iCurrentClientSessionId)
1970 Control ctrl = iTableLayoutPanelCurrentClient.GetControlFromPosition(tableField.Column, tableField.Row);
1971 if (aField.IsTextField && ctrl is MarqueeLabel)
1973 TextField textField = (TextField)aField;
1974 //Text field control already in place, just change the text
1975 MarqueeLabel label = (MarqueeLabel)ctrl;
1976 contentChanged = (label.Text != textField.Text || label.TextAlign != textField.Alignment);
1977 label.Text = textField.Text;
1978 label.TextAlign = textField.Alignment;
1980 else if (aField.IsBitmapField && ctrl is PictureBox)
1982 BitmapField bitmapField = (BitmapField)aField;
1983 contentChanged = true; //TODO: Bitmap comp or should we leave that to clients?
1984 //Bitmap field control already in place just change the bitmap
1985 PictureBox pictureBox = (PictureBox)ctrl;
1986 pictureBox.Image = bitmapField.Bitmap;
1988 else if (aField is AudioVisualizerField && ctrl is PictureBox)
1990 contentChanged = false; // Since nothing was changed
1994 layoutChanged = true;
2000 layoutChanged = true;
2003 //If either content or layout changed we need to update our tree view to reflect the changes
2004 if (contentChanged || layoutChanged)
2006 UpdateClientTreeViewNode(client);
2010 Debug.Print("Layout changed");
2011 //Our layout has changed, if we are already the current client we need to update our panel
2012 if (aSessionId == iCurrentClientSessionId)
2014 //Apply layout and set data fields.
2015 UpdateTableLayoutPanel(iCurrentClientData.View, iTableLayoutPanelCurrentClient);
2020 Debug.Print("Layout has not changed.");
2025 Debug.Print("WARNING: content and layout have not changed!");
2028 //When a client field is set we try switching to this client to present the new information to our user
2029 SetCurrentClient(aSessionId);
2035 /// <param name="aTexts"></param>
2036 public void SetClientFieldsThreadSafe(string aSessionId, System.Collections.Generic.IList<DataField> aFields)
2038 if (this.InvokeRequired)
2040 //Not in the proper thread, invoke ourselves
2041 Invoke(new Action<FormMain>((sender) => { SetClientFieldsThreadSafe(aSessionId, aFields); }), this);
2045 ClientData client = iClients[aSessionId];
2047 if (client.HasNewLayout)
2049 //TODO: Assert client.Count == 0
2050 //Our layout was just changed
2051 //Do some special handling to avoid re-creating our panel N times, once for each fields
2052 client.HasNewLayout = false;
2053 //Just set all our fields then
2054 client.View.Fields.AddRange(aFields);
2055 //Try switch to that client
2056 SetCurrentClient(aSessionId);
2058 //If we are updating the current client update our panel
2059 if (aSessionId == iCurrentClientSessionId)
2061 //Apply layout and set data fields.
2062 UpdateTableLayoutPanel(iCurrentClientData.View, iTableLayoutPanelCurrentClient);
2065 UpdateClientTreeViewNode(client);
2069 //Put each our text fields in a label control
2070 foreach (DataField field in aFields)
2072 SetClientField(aSessionId, field);
2081 /// <param name="aSessionId"></param>
2082 /// <param name="aName"></param>
2083 public void SetClientNameThreadSafe(string aSessionId, string aName)
2085 if (this.InvokeRequired)
2087 //Not in the proper thread, invoke ourselves
2088 Invoke(new Action<FormMain>((sender) => { SetClientNameThreadSafe(aSessionId, aName); }), this);
2092 //We are in the proper thread
2094 ClientData client = iClients[aSessionId];
2098 client.Name = aName;
2099 //Update our tree-view
2100 UpdateClientTreeViewNode(client);
2106 public void SetClientPriorityThreadSafe(string aSessionId, uint aPriority)
2108 if (this.InvokeRequired)
2110 //Not in the proper thread, invoke ourselves
2111 Invoke(new Action<FormMain>((sender) => { SetClientPriorityThreadSafe(aSessionId, aPriority); }), this);
2115 //We are in the proper thread
2117 ClientData client = iClients[aSessionId];
2121 client.Priority = aPriority;
2122 //Update our tree-view
2123 UpdateClientTreeViewNode(client);
2124 //Change our current client as per new priority
2125 ClientData newCurrentClient = FindHighestPriorityClient();
2126 if (newCurrentClient != null)
2128 SetCurrentClient(newCurrentClient.SessionId);
2137 /// <param name="aSessionId"></param>
2138 /// <param name="aPriority"></param>
2139 public void SetClientTargetThreadSafe(string aSessionId, Target aTarget)
2141 if (this.InvokeRequired)
2143 //Not in the proper thread, invoke ourselves
2144 Invoke(new Action<FormMain>((sender) => { SetClientTargetThreadSafe(aSessionId, aTarget); }), this);
2148 //We are in the proper thread
2150 ClientData client = iClients[aSessionId];
2154 client.Target = aTarget;
2155 //Update our tree-view
2156 //UpdateClientTreeViewNode(client);
2164 /// <param name="value"></param>
2165 /// <param name="maxChars"></param>
2166 /// <returns></returns>
2167 public static string Truncate(string value, int maxChars)
2169 return value.Length <= maxChars ? value : value.Substring(0, maxChars - 3) + "...";
2173 /// Update our recording notification.
2175 private void UpdateRecordingNotification()
2178 bool activeRecording = false;
2180 RecordingField recField = new RecordingField();
2181 foreach (var client in iClients)
2183 RecordingField rec = (RecordingField) client.Value.View.FindSameFieldAs(recField);
2184 if (rec != null && rec.IsActive)
2186 activeRecording = true;
2187 //Don't break cause we are collecting the names/texts.
2188 if (!String.IsNullOrEmpty(rec.Text))
2190 text += (rec.Text + "\n");
2194 //Not text for that recording, use client name instead
2195 text += client.Value.Name + " recording\n";
2201 //Update our text no matter what, can't have more than 63 characters otherwise it throws an exception.
2202 iRecordingNotification.Text = Truncate(text, 63);
2204 //Change visibility of notification if needed
2205 if (iRecordingNotification.Visible != activeRecording)
2207 iRecordingNotification.Visible = activeRecording;
2208 //Assuming the notification icon is in sync with our display icon
2209 //Take care of our REC icon
2210 if (iDisplay.IsOpen())
2212 iDisplay.SetIconOnOff(MiniDisplay.IconType.Recording, activeRecording);
2220 /// <param name="aClient"></param>
2221 private void UpdateClientTreeViewNode(ClientData aClient)
2223 Debug.Print("UpdateClientTreeViewNode");
2225 if (aClient == null)
2230 //Hook in record icon update too
2231 UpdateRecordingNotification();
2233 TreeNode node = null;
2234 //Check that our client node already exists
2235 //Get our client root node using its key which is our session ID
2236 TreeNode[] nodes = iTreeViewClients.Nodes.Find(aClient.SessionId, false);
2237 if (nodes.Count() > 0)
2239 //We already have a node for that client
2241 //Clear children as we are going to recreate them below
2246 //Client node does not exists create a new one
2247 iTreeViewClients.Nodes.Add(aClient.SessionId, aClient.SessionId);
2248 node = iTreeViewClients.Nodes.Find(aClient.SessionId, false)[0];
2254 if (!String.IsNullOrEmpty(aClient.Name))
2256 //We have a name, use it as text for our root node
2257 node.Text = aClient.Name;
2258 //Add a child with SessionId
2259 node.Nodes.Add(new TreeNode(aClient.SessionId));
2263 //No name, use session ID instead
2264 node.Text = aClient.SessionId;
2267 //Display client priority
2268 node.Nodes.Add(new TreeNode("Priority: " + aClient.Priority));
2270 if (aClient.View.Fields.Count > 0)
2272 //Create root node for our texts
2273 TreeNode textsRoot = new TreeNode("Fields");
2274 node.Nodes.Add(textsRoot);
2275 //For each text add a new entry
2276 foreach (DataField field in aClient.View.Fields)
2278 if (field.IsTextField)
2280 TextField textField = (TextField) field;
2281 textsRoot.Nodes.Add(new TreeNode("[Text]" + textField.Text));
2283 else if (field.IsBitmapField)
2285 textsRoot.Nodes.Add(new TreeNode("[Bitmap]"));
2287 else if (field is AudioVisualizerField)
2289 textsRoot.Nodes.Add(new TreeNode("[Audio Visualizer]"));
2291 else if (field.IsRecordingField)
2293 RecordingField recordingField = (RecordingField) field;
2294 textsRoot.Nodes.Add(new TreeNode("[Recording]" + recordingField.IsActive));
2304 /// Update our display table layout.
2305 /// Will instanciate every field control as defined by our client.
2306 /// Fields must be specified by rows from the left.
2308 /// <param name="aLayout"></param>
2309 private void UpdateTableLayoutPanel(SharpLib.Display.View aView, TableLayoutPanel aPanel)
2311 Debug.Print("UpdateTableLayoutPanel");
2320 TableLayout layout = aView.Layout;
2322 //First clean our current panel
2323 ClearLayout(aPanel);
2325 //Then recreate our rows...
2326 while (aPanel.RowCount < layout.Rows.Count)
2332 while (aPanel.ColumnCount < layout.Columns.Count)
2334 aPanel.ColumnCount++;
2338 for (int i = 0; i < aPanel.ColumnCount; i++)
2340 //Create our column styles
2341 aPanel.ColumnStyles.Add(layout.Columns[i]);
2344 for (int j = 0; j < aPanel.RowCount; j++)
2348 //Create our row styles
2349 aPanel.RowStyles.Add(layout.Rows[j]);
2359 foreach (DataField field in aView.Fields)
2361 if (!field.IsTableField)
2363 //That field is not taking part in our table layout skip it
2367 TableField tableField = (TableField) field;
2369 //Create a control corresponding to the field specified for that cell
2370 Control control = CreateControlForDataField(tableField);
2372 //Add newly created control to our table layout at the specified row and column
2373 aPanel.Controls.Add(control, tableField.Column, tableField.Row);
2374 //Make sure we specify column and row span for that new control
2375 aPanel.SetColumnSpan(control, tableField.ColumnSpan);
2376 aPanel.SetRowSpan(control, tableField.RowSpan);
2384 /// Check our type of data field and create corresponding control
2386 /// <param name="aField"></param>
2387 private Control CreateControlForDataField(DataField aField)
2389 Control control = null;
2390 if (aField.IsTextField)
2392 MarqueeLabel label = new SharpDisplayManager.MarqueeLabel();
2393 label.AutoEllipsis = true;
2394 label.AutoSize = true;
2395 label.BackColor = System.Drawing.Color.Transparent;
2396 label.Dock = System.Windows.Forms.DockStyle.Fill;
2397 label.Location = new System.Drawing.Point(1, 1);
2398 label.Margin = new System.Windows.Forms.Padding(0);
2399 label.Name = "marqueeLabel" + aField;
2400 label.OwnTimer = false;
2401 label.PixelsPerSecond = cds.ScrollingSpeedInPixelsPerSecond;
2402 label.Separator = cds.Separator;
2403 label.MinFontSize = cds.MinFontSize;
2404 label.ScaleToFit = cds.ScaleToFit;
2405 //control.Size = new System.Drawing.Size(254, 30);
2406 //control.TabIndex = 2;
2407 label.Font = cds.Font;
2409 TextField field = (TextField)aField;
2410 label.TextAlign = field.Alignment;
2411 label.UseCompatibleTextRendering = true;
2412 label.Text = field.Text;
2416 else if (aField.IsBitmapField)
2418 //Create picture box
2419 PictureBox picture = new PictureBox();
2420 picture.AutoSize = true;
2421 picture.BackColor = System.Drawing.Color.Transparent;
2422 picture.Dock = System.Windows.Forms.DockStyle.Fill;
2423 picture.Location = new System.Drawing.Point(1, 1);
2424 picture.Margin = new System.Windows.Forms.Padding(0);
2425 picture.Name = "pictureBox" + aField;
2427 BitmapField field = (BitmapField)aField;
2428 picture.Image = field.Bitmap;
2432 else if (aField is AudioVisualizerField)
2434 //Create picture box
2435 PictureBox picture = new PictureBox();
2436 picture.AutoSize = true;
2437 picture.BackColor = System.Drawing.Color.Transparent;
2438 picture.Dock = System.Windows.Forms.DockStyle.Fill;
2439 picture.Location = new System.Drawing.Point(1, 1);
2440 picture.Margin = new System.Windows.Forms.Padding(0);
2441 picture.Name = "pictureBox" + aField;
2443 // Make sure visualization is running
2444 if (iAudioManager != null) // When closing main form with multiple client audio manager can be null. I guess we should fix the core issue instead.
2446 iAudioManager.AddVisualizer();
2449 // Notify audio manager when we don't need audio visualizer anymore
2450 picture.Disposed += (sender, e) =>
2452 if (iAudioManager != null)
2454 // Make sure we stop visualization when not needed
2455 iAudioManager.RemoveVisualizer();
2459 // Create a new bitmap when control size changes
2460 picture.SizeChanged += (sender, e) =>
2462 // Somehow bitmap created when our from is invisible are not working
2463 // Mark our form visibility status
2464 bool visible = Visible;
2465 // Make sure it's visible
2467 // Adjust our bitmap size when control size changes
2468 picture.Image = new System.Drawing.Bitmap(picture.Width, picture.Height, PixelFormat.Format32bppArgb);
2469 // Restore our form visibility
2476 //TODO: Handle recording field?
2482 /// Called when the user selected a new display type.
2484 /// <param name="sender"></param>
2485 /// <param name="e"></param>
2486 private void comboBoxDisplayType_SelectedIndexChanged(object sender, EventArgs e)
2488 //Store the selected display type in our settings
2489 Properties.Settings.Default.CurrentDisplayIndex = comboBoxDisplayType.SelectedIndex;
2490 cds.DisplayType = comboBoxDisplayType.SelectedIndex;
2491 Properties.Settings.Default.Save();
2493 //Try re-opening the display connection if we were already connected.
2494 //Otherwise just update our status to reflect display type change.
2495 if (iDisplay.IsOpen())
2497 OpenDisplayConnection();
2505 private void maskedTextBoxTimerInterval_TextChanged(object sender, EventArgs e)
2507 if (maskedTextBoxTimerInterval.Text != "")
2509 int interval = Convert.ToInt32(maskedTextBoxTimerInterval.Text);
2513 iTimerDisplay.Interval = interval;
2514 cds.TimerInterval = iTimerDisplay.Interval;
2515 Properties.Settings.Default.Save();
2520 private void maskedTextBoxMinFontSize_TextChanged(object sender, EventArgs e)
2522 if (maskedTextBoxMinFontSize.Text != "")
2524 int minFontSize = Convert.ToInt32(maskedTextBoxMinFontSize.Text);
2526 if (minFontSize > 0)
2528 cds.MinFontSize = minFontSize;
2529 Properties.Settings.Default.Save();
2530 //We need to recreate our layout for that change to take effect
2531 if (iCurrentClientData != null)
2533 UpdateTableLayoutPanel(iCurrentClientData.View, iTableLayoutPanelCurrentClient);
2540 private void maskedTextBoxScrollingSpeed_TextChanged(object sender, EventArgs e)
2542 if (maskedTextBoxScrollingSpeed.Text != "")
2544 int scrollingSpeed = Convert.ToInt32(maskedTextBoxScrollingSpeed.Text);
2546 if (scrollingSpeed > 0)
2548 cds.ScrollingSpeedInPixelsPerSecond = scrollingSpeed;
2549 Properties.Settings.Default.Save();
2550 //We need to recreate our layout for that change to take effect
2551 if (iCurrentClientData != null)
2553 UpdateTableLayoutPanel(iCurrentClientData.View, iTableLayoutPanelCurrentClient);
2562 /// <param name="sender"></param>
2563 /// <param name="e"></param>
2564 private void textBoxScrollLoopSeparator_TextChanged(object sender, EventArgs e)
2566 cds.Separator = textBoxScrollLoopSeparator.Text;
2567 Properties.Settings.Default.Save();
2569 //Update our text fields
2570 UpdateMarqueesSeparator(iTableLayoutPanelDisplay);
2573 private void buttonPowerOn_Click(object sender, EventArgs e)
2578 private void buttonPowerOff_Click(object sender, EventArgs e)
2580 iDisplay.PowerOff();
2583 private void buttonShowClock_Click(object sender, EventArgs e)
2588 private void buttonHideClock_Click(object sender, EventArgs e)
2593 private void buttonUpdate_Click(object sender, EventArgs e)
2595 InstallUpdateSyncWithInfo();
2603 if (!iDisplay.IsOpen())
2608 //Devices like MDM166AA don't support windowing and frame rendering must be stopped while showing our clock
2609 iSkipFrameRendering = true;
2612 iDisplay.SwapBuffers();
2613 //Then show our clock
2614 iDisplay.ShowClock();
2622 if (!iDisplay.IsOpen())
2627 //Devices like MDM166AA don't support windowing and frame rendering must be stopped while showing our clock
2628 iSkipFrameRendering = false;
2629 iDisplay.HideClock();
2632 private void InstallUpdateSyncWithInfo()
2634 UpdateCheckInfo info = null;
2636 if (ApplicationDeployment.IsNetworkDeployed)
2638 ApplicationDeployment ad = ApplicationDeployment.CurrentDeployment;
2642 info = ad.CheckForDetailedUpdate();
2645 catch (DeploymentDownloadException dde)
2648 "The new version of the application cannot be downloaded at this time. \n\nPlease check your network connection, or try again later. Error: " +
2652 catch (InvalidDeploymentException ide)
2655 "Cannot check for a new version of the application. The ClickOnce deployment is corrupt. Please redeploy the application and try again. Error: " +
2659 catch (InvalidOperationException ioe)
2662 "This application cannot be updated. It is likely not a ClickOnce application. Error: " +
2667 if (info.UpdateAvailable)
2669 Boolean doUpdate = true;
2671 if (!info.IsUpdateRequired)
2674 MessageBox.Show("An update is available. Would you like to update the application now?",
2675 "Update Available", MessageBoxButtons.OKCancel);
2676 if (!(DialogResult.OK == dr))
2683 // Display a message that the application MUST reboot. Display the minimum required version.
2684 MessageBox.Show("This application has detected a mandatory update from your current " +
2685 "version to version " + info.MinimumRequiredVersion.ToString() +
2686 ". The application will now install the update and restart.",
2687 "Update Available", MessageBoxButtons.OK,
2688 MessageBoxIcon.Information);
2696 MessageBox.Show("The application has been upgraded, and will now restart.");
2697 Application.Restart();
2699 catch (DeploymentDownloadException dde)
2702 "Cannot install the latest version of the application. \n\nPlease check your network connection, or try again later. Error: " +
2710 MessageBox.Show("You are already running the latest version.", "Application up-to-date");
2719 private void SysTrayHideShow()
2725 WindowState = FormWindowState.Normal;
2730 /// Use to handle minimize events.
2732 /// <param name="sender"></param>
2733 /// <param name="e"></param>
2734 private void MainForm_SizeChanged(object sender, EventArgs e)
2736 if (WindowState == FormWindowState.Minimized && Properties.Settings.Default.MinimizeToTray)
2748 /// <param name="sender"></param>
2749 /// <param name="e"></param>
2750 private void tableLayoutPanel_SizeChanged(object sender, EventArgs e)
2752 //Our table layout size has changed which means our display size has changed.
2753 //We need to re-create our bitmap.
2754 iCreateBitmap = true;
2758 /// Broadcast messages to subscribers.
2760 /// <param name="message"></param>
2761 protected override void WndProc(ref Message aMessage)
2763 if (OnWndProc != null)
2765 OnWndProc(ref aMessage);
2768 base.WndProc(ref aMessage);
2771 private void iCheckBoxCecEnabled_CheckedChanged(object sender, EventArgs e)
2777 private void comboBoxHdmiPort_SelectedIndexChanged(object sender, EventArgs e)
2779 //Save CEC HDMI port
2780 Properties.Settings.Default.CecHdmiPort = Convert.ToByte(comboBoxHdmiPort.SelectedIndex);
2781 Properties.Settings.Default.CecHdmiPort++;
2782 Properties.Settings.Default.Save();
2790 private void ResetCec()
2792 if (iCecManager == null)
2794 //Thus skipping initial UI setup
2800 if (Properties.Settings.Default.CecEnabled)
2802 iCecManager.Start(Handle, "CEC",
2803 Properties.Settings.Default.CecHdmiPort);
2812 private async void ResetHarmonyAsync(bool aForceAuth=false)
2814 // ConnectAsync already if we have an existing session cookie
2815 if (Properties.Settings.Default.HarmonyEnabled)
2819 iButtonHarmonyConnect.Enabled = false;
2820 await ConnectHarmonyAsync(aForceAuth);
2822 catch (Exception ex)
2824 Trace.WriteLine("Exception thrown by ConnectHarmonyAsync");
2825 Trace.WriteLine(ex.ToString());
2829 iButtonHarmonyConnect.Enabled = true;
2837 /// <param name="sender"></param>
2838 /// <param name="e"></param>
2839 private void iButtonHarmonyConnect_Click(object sender, EventArgs e)
2841 // User is explicitaly trying to connect
2842 //Reset Harmony Hub connection forcing authentication
2843 ResetHarmonyAsync(true);
2849 /// <param name="sender"></param>
2850 /// <param name="e"></param>
2851 private void iCheckBoxHarmonyEnabled_CheckedChanged(object sender, EventArgs e)
2853 iButtonHarmonyConnect.Enabled = iCheckBoxHarmonyEnabled.Checked;
2859 private void SetupCecLogLevel()
2862 iCecManager.Client.LogLevel = 0;
2864 if (checkBoxCecLogError.Checked)
2865 iCecManager.Client.LogLevel |= (int) CecLogLevel.Error;
2867 if (checkBoxCecLogWarning.Checked)
2868 iCecManager.Client.LogLevel |= (int) CecLogLevel.Warning;
2870 if (checkBoxCecLogNotice.Checked)
2871 iCecManager.Client.LogLevel |= (int) CecLogLevel.Notice;
2873 if (checkBoxCecLogTraffic.Checked)
2874 iCecManager.Client.LogLevel |= (int) CecLogLevel.Traffic;
2876 if (checkBoxCecLogDebug.Checked)
2877 iCecManager.Client.LogLevel |= (int) CecLogLevel.Debug;
2879 iCecManager.Client.FilterOutPollLogs = checkBoxCecLogNoPoll.Checked;
2883 private void ButtonStartIdleClient_Click(object sender, EventArgs e)
2888 private void buttonClearLogs_Click(object sender, EventArgs e)
2890 richTextBoxLogs.Clear();
2893 private void checkBoxCecLogs_CheckedChanged(object sender, EventArgs e)
2903 /// Get the current event based on event tree view selection.
2905 /// <returns></returns>
2906 private Ear.Event CurrentEvent()
2908 //Walk up the tree from the selected node to find our event
2909 TreeNode node = iTreeViewEvents.SelectedNode;
2910 Ear.Event selectedEvent = null;
2911 while (node != null)
2913 if (node.Tag is Ear.Event)
2915 selectedEvent = (Ear.Event) node.Tag;
2921 return selectedEvent;
2925 /// Get the current action based on event tree view selection
2927 /// <returns></returns>
2928 private Ear.Action CurrentAction()
2930 TreeNode node = iTreeViewEvents.SelectedNode;
2931 if (node != null && node.Tag is Ear.Action)
2933 return (Ear.Action) node.Tag;
2942 /// <returns></returns>
2943 private Ear.Object CurrentEarObject()
2945 Ear.Action a = CurrentAction();
2946 Ear.Event e = CurrentEvent();
2957 /// Get the current action based on event tree view selection
2959 /// <returns></returns>
2960 private Ear.Object CurrentEarParent()
2962 TreeNode node = iTreeViewEvents.SelectedNode;
2963 if (node == null || node.Parent == null)
2968 if (node.Parent.Tag is Ear.Object)
2970 return (Ear.Object)node.Parent.Tag;
2973 if (node.Parent.Parent != null && node.Parent.Parent.Tag is Ear.Object)
2975 //Can be the case for events
2976 return (Ear.Object)node.Parent.Parent.Tag;
2986 /// <param name="sender"></param>
2987 /// <param name="e"></param>
2988 private void buttonActionAdd_Click(object sender, EventArgs e)
2990 Ear.Object parent = CurrentEarObject();
2991 if (parent == null )
2993 //We did not find a corresponding event or action
2997 FormEditObject<Ear.Action> ea = new FormEditObject<Ear.Action>();
2998 ea.Text = "Add action";
2999 DialogResult res = CodeProject.Dialog.DlgBox.ShowDialog(ea);
3000 if (res == DialogResult.OK)
3002 parent.Objects.Add(ea.Object);
3003 Properties.Settings.Default.Save();
3004 // We want to select the parent so that one can easily add another action to the same collection
3005 PopulateTreeViewEvents(parent);
3012 /// <param name="sender"></param>
3013 /// <param name="e"></param>
3014 private void buttonActionEdit_Click(object sender, EventArgs e)
3016 Ear.Action selectedAction = CurrentAction();
3017 Ear.Object parent = CurrentEarParent()
3019 if (parent == null || selectedAction == null)
3021 //We did not find a corresponding parent
3025 FormEditObject<Ear.Action> ea = new FormEditObject<Ear.Action>();
3026 ea.Text = "Edit action";
3027 ea.Object = selectedAction;
3028 int actionIndex = iTreeViewEvents.SelectedNode.Index;
3029 DialogResult res = CodeProject.Dialog.DlgBox.ShowDialog(ea);
3030 if (res == DialogResult.OK)
3032 //Make sure we keep the same children as before
3033 ea.Object.Objects = parent.Objects[actionIndex].Objects;
3035 parent.Objects[actionIndex]=ea.Object;
3036 //Save and rebuild our event tree view
3037 Properties.Settings.Default.Save();
3038 PopulateTreeViewEvents(ea.Object);
3045 /// <param name="sender"></param>
3046 /// <param name="e"></param>
3047 private void buttonActionDelete_Click(object sender, EventArgs e)
3049 Ear.Action action = CurrentAction();
3052 //Must select action node
3056 Properties.Settings.Default.EarManager.RemoveAction(action);
3057 Properties.Settings.Default.Save();
3058 PopulateTreeViewEvents();
3064 /// <param name="sender"></param>
3065 /// <param name="e"></param>
3066 private void buttonActionTest_Click(object sender, EventArgs e)
3068 Ear.Action a = CurrentAction();
3073 iTreeViewEvents.Focus();
3079 /// <param name="sender"></param>
3080 /// <param name="e"></param>
3081 private void buttonActionMoveUp_Click(object sender, EventArgs e)
3083 Ear.Action a = CurrentAction();
3085 //Action already at the top of the list
3086 iTreeViewEvents.SelectedNode.Index == 0)
3091 //Swap actions in event's action list
3092 Ear.Object parent = CurrentEarParent();
3093 int currentIndex = iTreeViewEvents.SelectedNode.Index;
3094 Ear.Action movingUp = parent.Objects[currentIndex] as Ear.Action;
3095 Ear.Action movingDown = parent.Objects[currentIndex-1] as Ear.Action;
3096 parent.Objects[currentIndex] = movingDown;
3097 parent.Objects[currentIndex-1] = movingUp;
3099 //Save and populate our tree again
3100 Properties.Settings.Default.Save();
3101 PopulateTreeViewEvents(a);
3107 /// <param name="sender"></param>
3108 /// <param name="e"></param>
3109 private void buttonActionMoveDown_Click(object sender, EventArgs e)
3111 Ear.Action a = CurrentAction();
3113 //Action already at the bottom of the list
3114 iTreeViewEvents.SelectedNode.Index == iTreeViewEvents.SelectedNode.Parent.Nodes.Count - 1)
3119 //Swap actions in event's action list
3120 Ear.Object parent = CurrentEarParent();
3121 int currentIndex = iTreeViewEvents.SelectedNode.Index;
3122 Ear.Action movingDown = parent.Objects[currentIndex] as Ear.Action;
3123 Ear.Action movingUp = parent.Objects[currentIndex + 1] as Ear.Action;
3124 parent.Objects[currentIndex] = movingUp;
3125 parent.Objects[currentIndex + 1] = movingDown;
3127 //Save and populate our tree again
3128 Properties.Settings.Default.Save();
3129 PopulateTreeViewEvents(a);
3136 /// <param name="sender"></param>
3137 /// <param name="e"></param>
3138 private void buttonEventTest_Click(object sender, EventArgs e)
3140 Ear.Event earEvent = CurrentEvent();
3141 if (earEvent != null)
3148 /// Manages events and actions buttons according to selected item in event tree.
3150 /// <param name="sender"></param>
3151 /// <param name="e"></param>
3152 private void iTreeViewEvents_AfterSelect(object sender, TreeViewEventArgs e)
3160 private void UpdateEventView()
3162 //One can always add an event
3163 buttonEventAdd.Enabled = true;
3165 //Enable buttons according to selected item
3166 buttonActionAdd.Enabled =
3167 buttonEventTest.Enabled =
3168 buttonEventDelete.Enabled =
3169 buttonEventEdit.Enabled =
3170 CurrentEvent() != null;
3172 Ear.Action currentAction = CurrentAction();
3173 //If an action is selected enable the following buttons
3174 buttonActionTest.Enabled =
3175 buttonActionDelete.Enabled =
3176 buttonActionMoveUp.Enabled =
3177 buttonActionMoveDown.Enabled =
3178 buttonActionEdit.Enabled =
3179 currentAction != null;
3181 if (currentAction != null)
3183 //If an action is selected enable move buttons if needed
3184 buttonActionMoveUp.Enabled = iTreeViewEvents.SelectedNode.Index != 0;
3185 buttonActionMoveDown.Enabled = iTreeViewEvents.SelectedNode.Index <
3186 iTreeViewEvents.SelectedNode.Parent.Nodes.Count - 1;
3193 /// <param name="sender"></param>
3194 /// <param name="e"></param>
3195 private void buttonEventAdd_Click(object sender, EventArgs e)
3197 FormEditObject<Ear.Event> ea = new FormEditObject<Ear.Event>();
3198 ea.Text = "Add event";
3199 DialogResult res = CodeProject.Dialog.DlgBox.ShowDialog(ea);
3200 if (res == DialogResult.OK)
3202 Properties.Settings.Default.EarManager.Events.Add(ea.Object);
3203 Properties.Settings.Default.Save();
3204 PopulateTreeViewEvents(ea.Object);
3211 /// <param name="sender"></param>
3212 /// <param name="e"></param>
3213 private void buttonEventDelete_Click(object sender, EventArgs e)
3215 Ear.Event currentEvent = CurrentEvent();
3216 if (currentEvent == null)
3218 //Must select action node
3222 Properties.Settings.Default.EarManager.Events.Remove(currentEvent);
3223 Properties.Settings.Default.Save();
3224 PopulateTreeViewEvents();
3230 /// <param name="sender"></param>
3231 /// <param name="e"></param>
3232 private void buttonEventEdit_Click(object sender, EventArgs e)
3234 Ear.Event selectedEvent = CurrentEvent();
3235 if (selectedEvent == null)
3237 //We did not find a corresponding event
3241 FormEditObject<Ear.Event> ea = new FormEditObject<Ear.Event>();
3242 ea.Text = "Edit event";
3243 ea.Object = selectedEvent;
3244 int index = iTreeViewEvents.SelectedNode.Index;
3245 DialogResult res = CodeProject.Dialog.DlgBox.ShowDialog(ea);
3246 if (res == DialogResult.OK)
3248 //Make sure we keep the same actions as before
3249 ea.Object.Objects = Properties.Settings.Default.EarManager.Events[index].Objects;
3251 Properties.Settings.Default.EarManager.Events[index] = ea.Object;
3252 //Save and rebuild our event tree view
3253 Properties.Settings.Default.Save();
3254 PopulateTreeViewEvents(ea.Object);
3261 /// <param name="sender"></param>
3262 /// <param name="e"></param>
3263 private void iTreeViewEvents_Leave(object sender, EventArgs e)
3265 //Make sure our event tree never looses focus
3266 ((TreeView) sender).Focus();
3270 /// Called whenever we loose connection with our HarmonyHub.
3272 /// <param name="aRequestWasCancelled"></param>
3273 private void HarmonyConnectionClosed(object aSender, bool aClosedByServer)
3275 if (aClosedByServer)
3277 //Try reconnect then
3278 #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
3279 BeginInvoke(new MethodInvoker(delegate () { ResetHarmonyAsync(); }));
3280 #pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
3286 int iHarmonyReconnectTries = 0;
3287 const int KHarmonyMaxReconnectTries = 10;
3292 /// <returns></returns>
3293 private async Task ConnectHarmonyAsync(bool aForceAuth=false)
3295 if (Program.HarmonyClient != null)
3297 await Program.HarmonyClient.CloseAsync();
3300 bool success = false;
3302 //Reset Harmony client & config
3303 Program.HarmonyClient = null;
3304 Program.HarmonyConfig = null;
3305 iTreeViewHarmony.Nodes.Clear();
3307 Trace.WriteLine("Harmony: Connecting... ");
3308 //First create our client and login
3309 //Tip: Set keep-alive to false when testing reconnection process
3310 Program.HarmonyClient = new HarmonyHub.Client(iTextBoxHarmonyHubAddress.Text, true);
3311 Program.HarmonyClient.OnConnectionClosed += HarmonyConnectionClosed;
3313 string authToken = Properties.Settings.Default.LogitechAuthToken;
3314 if (!string.IsNullOrEmpty(authToken) && !aForceAuth)
3316 Trace.WriteLine("Harmony: Reusing token: {0}", authToken);
3317 success = await Program.HarmonyClient.TryOpenAsync(authToken);
3320 if (!Program.HarmonyClient.IsReady || !success
3321 // Only first failure triggers new Harmony server AUTH
3322 // That's to avoid calling upon Logitech servers too often
3323 && iHarmonyReconnectTries == 0 )
3325 //We failed to connect using our token
3327 Trace.WriteLine("Harmony: Reseting authentication token!");
3328 Properties.Settings.Default.LogitechAuthToken = "";
3329 Properties.Settings.Default.Save();
3331 Trace.WriteLine("Harmony: Authenticating with Logitech servers...");
3332 success = await Program.HarmonyClient.TryOpenAsync();
3333 //Persist our authentication token in our setting
3336 Trace.WriteLine("Harmony: Saving authentication token.");
3337 Properties.Settings.Default.LogitechAuthToken = Program.HarmonyClient.Token;
3338 Properties.Settings.Default.Save();
3342 // I've seen this failing with "Policy lookup failed on server".
3343 Program.HarmonyConfig = await Program.HarmonyClient.TryGetConfigAsync();
3344 if (Program.HarmonyConfig == null)
3350 // So we now have our Harmony Configuration
3351 PopulateTreeViewHarmony(Program.HarmonyConfig);
3352 // Make sure harmony command actions are showing device name instead of device id
3353 PopulateTreeViewEvents(CurrentEarObject());
3356 // TODO: Consider putting the retry logic one level higher in ResetHarmonyAsync
3359 // See if we need to keep trying
3360 if (iHarmonyReconnectTries < KHarmonyMaxReconnectTries)
3362 iHarmonyReconnectTries++;
3363 Trace.WriteLine("Harmony: Failed to connect, try again: " + iHarmonyReconnectTries);
3364 await ConnectHarmonyAsync();
3368 Trace.WriteLine("Harmony: Failed to connect, giving up!");
3369 iHarmonyReconnectTries = 0;
3370 // TODO: Could use a data member as timer rather than a new instance.
3371 // Try that again in 5 minutes then.
3372 // Using Windows Form timer to make sure we run in the UI thread.
3373 System.Windows.Forms.Timer timer = new System.Windows.Forms.Timer();
3374 timer.Tick += async delegate (object sender, EventArgs e)
3376 // Stop our timer first as we won't need it anymore
3377 (sender as System.Windows.Forms.Timer).Stop();
3378 // Then try to connect again
3379 await ConnectHarmonyAsync();
3381 timer.Interval = 300000;
3387 // We are connected with a valid Harmony Configuration
3388 // Reset our tries counter then
3389 iHarmonyReconnectTries = 0;
3396 /// <param name="aConfig"></param>
3397 private void PopulateTreeViewHarmony(HarmonyHub.Config aConfig)
3399 iTreeViewHarmony.Nodes.Clear();
3401 foreach (HarmonyHub.Device device in aConfig.Devices)
3403 TreeNode deviceNode = iTreeViewHarmony.Nodes.Add(device.Id, $"{device.Label} ({device.DeviceTypeDisplayName}/{device.Model})");
3404 deviceNode.Tag = device;
3406 foreach (HarmonyHub.ControlGroup cg in device.ControlGroups)
3408 TreeNode cgNode = deviceNode.Nodes.Add(cg.Name);
3411 foreach (HarmonyHub.Function f in cg.Functions)
3413 TreeNode fNode = cgNode.Nodes.Add(f.Label);
3419 //treeViewConfig.ExpandAll();
3422 private async void iTreeViewHarmony_NodeMouseDoubleClick(object sender, TreeNodeMouseClickEventArgs e)
3424 //Upon function node double click we execute it
3425 var tag = e.Node.Tag as HarmonyHub.Function;
3426 if (tag != null && e.Node.Parent.Parent.Tag is HarmonyHub.Device)
3428 HarmonyHub.Function f = tag;
3429 HarmonyHub.Device d = (HarmonyHub.Device)e.Node.Parent.Parent.Tag;
3431 Trace.WriteLine($"Harmony: Sending {f.Label} to {d.Label}...");
3433 await Program.HarmonyClient.TrySendKeyPressAsync(d.Id, f.Action.Command);