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 this.Text += " - development";
227 CreateAudioManager();
230 iNetworkManager = new NetworkManager();
231 iNetworkManager.OnConnectivityChanged += OnConnectivityChanged;
232 UpdateNetworkStatus();
235 iCecManager = new ConsumerElectronicControl();
236 OnWndProc += iCecManager.OnWndProc;
243 PopulateTreeViewEvents();
245 //Setup notification icon
248 //Setup recording notification
249 SetupRecordingNotification();
251 // To make sure start up with minimize to tray works
252 if (WindowState == FormWindowState.Minimized && Properties.Settings.Default.MinimizeToTray)
258 // When not debugging we want the screen to be empty until a client takes over
259 ClearLayout(iTableLayoutPanelCurrentClient);
261 // Display layout should be empty too
262 // TODO: Eventually we will need to dynamically create and destroy our client table layout
263 DoClearLayout(iTableLayoutPanelDisplay);
264 iTableLayoutPanelDisplay.ColumnCount = 1;
265 iTableLayoutPanelDisplay.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
266 iTableLayoutPanelDisplay.RowCount = 1;
267 iTableLayoutPanelDisplay.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
269 iTableLayoutPanelDisplay.SetRowSpan(iTableLayoutPanelCurrentClient, 1);
270 iTableLayoutPanelDisplay.SetColumnSpan(iTableLayoutPanelCurrentClient, 1);
271 iTableLayoutPanelDisplay.Controls.Add(iTableLayoutPanelCurrentClient,0,0);
273 // Is that needed? Why just in release?
274 iCurrentClientData = null;
276 //When developing we want at least one client for testing
277 StartNewClient("abcdefghijklmnopqrst-0123456789", "ABCDEFGHIJKLMNOPQRST-0123456789");
280 //Open display connection on start-up if needed
281 if (Properties.Settings.Default.DisplayConnectOnStartup)
283 OpenDisplayConnection();
286 //Start our server so that we can get client requests
289 //Register for HID events
290 RegisterHidDevices();
292 //Start Idle client if needed
293 if (Properties.Settings.Default.StartIdleClient)
300 private void CreateAudioManager()
302 iAudioManager = new AudioManager();
303 iAudioManager.Open(OnDefaultMultiMediaDeviceChanged, OnVolumeNotification);
304 UpdateAudioDeviceAndMasterVolumeThreadSafe();
307 private void DestroyAudioManager()
309 if (iAudioManager != null)
311 iAudioManager.Close();
312 iAudioManager = null;
319 /// <param name="sender"></param>
320 /// <param name="aEvent"></param>
321 public void OnDefaultMultiMediaDeviceChanged(object sender, DefaultDeviceChangedEventArgs aEvent)
323 if (aEvent.DataFlow == DataFlow.Render && aEvent.Role == Role.Multimedia)
325 ResetAudioManagerThreadSafe();
332 private void ResetAudioManagerThreadSafe()
336 //Not in the proper thread, invoke ourselves
337 BeginInvoke(new Action<FormMain>((sender) => { ResetAudioManagerThreadSafe(); }), this);
341 //Proper thread, go ahead
342 DestroyAudioManager();
343 CreateAudioManager();
348 /// Called when our display is opened.
350 /// <param name="aDisplay"></param>
351 private void OnDisplayOpened(Display aDisplay)
353 //Make sure we resume frame rendering
354 iSkipFrameRendering = false;
356 //Set our screen size now that our display is connected
357 //Our panelDisplay is the container of our tableLayoutPanel
358 //tableLayoutPanel will resize itself to fit the client size of our panelDisplay
359 //panelDisplay needs an extra 2 pixels for borders on each sides
360 //tableLayoutPanel will eventually be the exact size of our display
361 Size size = new Size(iDisplay.WidthInPixels() + 2, iDisplay.HeightInPixels() + 2);
362 iPanelDisplay.Size = size;
364 //Our display was just opened, update our UI
366 //Initiate asynchronous request
367 iDisplay.RequestFirmwareRevision();
370 UpdateMasterVolumeThreadSafe();
372 UpdateNetworkStatus();
375 //Testing icon in debug, no arm done if icon not supported
376 //iDisplay.SetIconStatus(Display.TMiniDisplayIconType.EMiniDisplayIconRecording, 0, 1);
377 //iDisplay.SetAllIconsStatus(2);
383 private static void AddActionsToTreeNode(TreeNode aParentNode, Ear.Object aObject)
385 foreach (Ear.Action a in aObject.Objects.OfType<Ear.Action>())
388 TreeNode actionNode = aParentNode.Nodes.Add(a.Brief());
390 //Use color from parent unless our action itself is disabled
391 actionNode.ForeColor = a.Enabled ? aParentNode.ForeColor : Color.DimGray;
393 AddActionsToTreeNode(actionNode,a);
401 /// <param name="aObject"></param>
402 /// <param name="aNode"></param>
403 private static TreeNode FindTreeNodeForEarObject(Ear.Object aObject, TreeNode aNode)
405 if (aNode.Tag == aObject)
410 foreach (TreeNode n in aNode.Nodes)
412 TreeNode found = FindTreeNodeForEarObject(aObject,n);
426 /// <param name="aObject"></param>
427 private void SelectEarObject(Ear.Object aObject)
429 foreach (TreeNode n in iTreeViewEvents.Nodes)
431 TreeNode found = FindTreeNodeForEarObject(aObject, n);
434 iTreeViewEvents.SelectedNode=found;
435 iTreeViewEvents.Focus();
442 /// Populate tree view with events and actions
444 private void PopulateTreeViewEvents(Ear.Object aSelectedObject=null)
447 iTreeViewEvents.Nodes.Clear();
448 //Populate registered events
449 foreach (Ear.Event e in Properties.Settings.Default.EarManager.Events)
451 //Create our event node
452 //Work out the name of our node
453 string eventNodeName = "";
454 if (!string.IsNullOrEmpty(e.Name))
456 //That event has a proper name, use it then
457 eventNodeName = $"{e.Name} - {e.Brief()}";
461 //Unnamed events just use brief
462 eventNodeName = e.Brief();
465 TreeNode eventNode = iTreeViewEvents.Nodes.Add(eventNodeName);
466 eventNode.Tag = e; //For easy access to our event
469 //Dim our nodes if disabled
470 eventNode.ForeColor = Color.DimGray;
473 //Add event description as child node
474 eventNode.Nodes.Add(e.AttributeDescription).ForeColor = eventNode.ForeColor;
475 //Create child node for actions root
476 TreeNode actionsNode = eventNode.Nodes.Add("Actions");
477 actionsNode.ForeColor = eventNode.ForeColor;
479 // Recursively add our actions for that event
480 AddActionsToTreeNode(actionsNode,e);
483 iTreeViewEvents.ExpandAll();
485 if (aSelectedObject != null)
487 SelectEarObject(aSelectedObject);
490 // Just to be safe in case the selection did not work
495 /// Called when our display is closed.
497 /// <param name="aDisplay"></param>
498 private void OnDisplayClosed(Display aDisplay)
500 //Our display was just closed, update our UI consequently
504 public void OnConnectivityChanged(NetworkManager aNetwork, NLM_CONNECTIVITY newConnectivity)
506 //Update network status
507 UpdateNetworkStatus();
511 /// Update our Network Status
513 private void UpdateNetworkStatus()
515 if (iDisplay.IsOpen())
517 iDisplay.SetIconOnOff(MiniDisplay.IconType.Internet,
518 iNetworkManager.NetworkListManager.IsConnectedToInternet);
519 iDisplay.SetIconOnOff(MiniDisplay.IconType.NetworkSignal, iNetworkManager.NetworkListManager.IsConnected);
524 int iLastNetworkIconIndex = 0;
525 int iUpdateCountSinceLastNetworkAnimation = 0;
530 private void UpdateNetworkSignal(DateTime aLastTickTime, DateTime aNewTickTime)
532 iUpdateCountSinceLastNetworkAnimation++;
533 iUpdateCountSinceLastNetworkAnimation = iUpdateCountSinceLastNetworkAnimation%4;
535 if (iDisplay.IsOpen() && iNetworkManager.NetworkListManager.IsConnected &&
536 iUpdateCountSinceLastNetworkAnimation == 0)
538 int iconCount = iDisplay.IconCount(MiniDisplay.IconType.NetworkSignal);
541 //Prevents div by zero and other undefined behavior
544 iLastNetworkIconIndex++;
545 iLastNetworkIconIndex = iLastNetworkIconIndex%(iconCount*2);
546 for (int i = 0; i < iconCount; i++)
548 if (i < iLastNetworkIconIndex && !(i == 0 && iLastNetworkIconIndex > 3) &&
549 !(i == 1 && iLastNetworkIconIndex > 4))
551 iDisplay.SetIconOn(MiniDisplay.IconType.NetworkSignal, i);
555 iDisplay.SetIconOff(MiniDisplay.IconType.NetworkSignal, i);
564 /// Receive volume change notification and reflect changes on our slider.
566 /// <param name="data"></param>
567 public void OnVolumeNotification(object sender, AudioEndpointVolumeCallbackEventArgs aEvent)
569 UpdateMasterVolumeThreadSafe();
573 /// Update master volume when user moves our slider.
575 /// <param name="sender"></param>
576 /// <param name="e"></param>
577 private void trackBarMasterVolume_Scroll(object sender, EventArgs e)
579 //Just like Windows Volume Mixer we unmute if the volume is adjusted
580 iAudioManager.Volume.IsMuted = false;
581 //Set volume level according to our volume slider new position
582 iAudioManager.Volume.MasterVolumeLevelScalar = trackBarMasterVolume.Value/100.0f;
587 /// Mute check box changed.
589 /// <param name="sender"></param>
590 /// <param name="e"></param>
591 private void checkBoxMute_CheckedChanged(object sender, EventArgs e)
593 iAudioManager.Volume.IsMuted = checkBoxMute.Checked;
598 /// Update master volume indicators based our current system states.
599 /// This typically includes volume levels and mute status.
601 private void UpdateMasterVolumeThreadSafe()
605 //Not in the proper thread, invoke ourselves
606 BeginInvoke(new Action<FormMain>((sender) => { UpdateMasterVolumeThreadSafe(); }), this);
610 //Update volume slider
611 float volumeLevelScalar = iAudioManager.Volume.MasterVolumeLevelScalar;
612 trackBarMasterVolume.Value = Convert.ToInt32(volumeLevelScalar*100);
613 //Update mute checkbox
614 checkBoxMute.Checked = iAudioManager.Volume.IsMuted;
616 //If our display connection is open we need to update its icons
617 if (iDisplay.IsOpen())
619 //First take care our our volume level icons
620 int volumeIconCount = iDisplay.IconCount(MiniDisplay.IconType.Volume);
621 if (volumeIconCount > 0)
623 //Compute current volume level from system level and the number of segments in our display volume bar.
624 //That tells us how many segments in our volume bar needs to be turned on.
625 float currentVolume = volumeLevelScalar*volumeIconCount;
626 int segmentOnCount = Convert.ToInt32(currentVolume);
627 //Check if our segment count was rounded up, this will later be used for half brightness segment
628 bool roundedUp = segmentOnCount > currentVolume;
630 for (int i = 0; i < volumeIconCount; i++)
632 if (i < segmentOnCount)
634 //If we are dealing with our last segment and our segment count was rounded up then we will use half brightness.
635 if (i == segmentOnCount - 1 && roundedUp)
638 iDisplay.SetIconStatus(MiniDisplay.IconType.Volume, i,
639 (iDisplay.IconStatusCount(MiniDisplay.IconType.Volume) - 1)/2);
644 iDisplay.SetIconStatus(MiniDisplay.IconType.Volume, i,
645 iDisplay.IconStatusCount(MiniDisplay.IconType.Volume) - 1);
650 iDisplay.SetIconStatus(MiniDisplay.IconType.Volume, i, 0);
655 //Take care of our mute icon
656 iDisplay.SetIconOnOff(MiniDisplay.IconType.Mute, iAudioManager.Volume.IsMuted);
666 private void UpdateAudioVisualization()
668 // No point if we don't have a current client
669 if (iCurrentClientData == null)
675 if (iAudioManager==null || iAudioManager.Spectrum==null || !iAudioManager.Spectrum.Update())
677 //Nothing changed no need to render
681 // Check if our current client has an Audio Visualizer field
682 // and render them as needed
683 foreach (DataField f in iCurrentClientData.View.Fields)
685 if (f is AudioVisualizerField)
687 AudioVisualizerField avf = (AudioVisualizerField)f;
688 Control ctrl = iTableLayoutPanelCurrentClient.GetControlFromPosition(avf.Column, avf.Row);
690 if (ctrl is PictureBox)
692 PictureBox pb = (PictureBox)ctrl;
693 if (iAudioManager.Spectrum.Render(pb.Image, Color.Black, Color.Black, Color.White, false))
706 private void UpdateAudioDeviceAndMasterVolumeThreadSafe()
710 //Not in the proper thread, invoke ourselves
711 BeginInvoke(new Action<FormMain>((sender) => { UpdateAudioDeviceAndMasterVolumeThreadSafe(); }), this);
715 //We are in the correct thread just go ahead.
720 iLabelDefaultAudioDevice.Text = iAudioManager.DefaultDevice.FriendlyName;
722 //Show our volume in our track bar
723 UpdateMasterVolumeThreadSafe();
726 trackBarMasterVolume.Enabled = true;
730 Debug.WriteLine("Exception thrown in UpdateAudioDeviceAndMasterVolume");
731 Debug.WriteLine(ex.ToString());
732 //Something went wrong S/PDIF device ca throw exception I guess
733 trackBarMasterVolume.Enabled = false;
740 private void PopulateDeviceTypes()
742 int count = Display.TypeCount();
744 for (int i = 0; i < count; i++)
746 comboBoxDisplayType.Items.Add(Display.TypeName((MiniDisplay.Type) i));
753 private void SetupTrayIcon()
755 iNotifyIcon.Icon = GetIcon("vfd.ico");
756 iNotifyIcon.Text = "Sharp Display Manager";
757 iNotifyIcon.Visible = true;
759 //Double click toggles visibility - typically brings up the application
760 iNotifyIcon.DoubleClick += delegate(object obj, EventArgs args)
765 //Adding a context menu, useful to be able to exit the application
766 ContextMenu contextMenu = new ContextMenu();
767 //Context menu item to toggle visibility
768 MenuItem hideShowItem = new MenuItem("Hide/Show");
769 hideShowItem.Click += delegate(object obj, EventArgs args)
773 contextMenu.MenuItems.Add(hideShowItem);
775 //Context menu item separator
776 contextMenu.MenuItems.Add(new MenuItem("-"));
778 //Context menu exit item
779 MenuItem exitItem = new MenuItem("Exit");
780 exitItem.Click += delegate(object obj, EventArgs args)
784 contextMenu.MenuItems.Add(exitItem);
786 iNotifyIcon.ContextMenu = contextMenu;
792 private void SetupRecordingNotification()
794 iRecordingNotification.Icon = GetIcon("record.ico");
795 iRecordingNotification.Text = "No recording";
796 iRecordingNotification.Visible = false;
800 /// Access icons from embedded resources.
802 /// <param name="aName"></param>
803 /// <returns></returns>
804 public static Icon GetIcon(string aName)
806 string[] names = Assembly.GetExecutingAssembly().GetManifestResourceNames();
807 foreach (string name in names)
809 //Find a resource name that ends with the given name
810 if (name.EndsWith(aName))
812 using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(name))
814 return new Icon(stream);
824 /// Set our current client.
825 /// This will take care of applying our client layout and set data fields.
827 /// <param name="aSessionId"></param>
828 void SetCurrentClient(string aSessionId, bool aForce = false)
830 if (aSessionId == iCurrentClientSessionId)
832 //Given client is already the current one.
833 //Don't bother changing anything then.
837 ClientData requestedClientData = iClients[aSessionId];
839 //Check when was the last time we switched to that client
840 if (iCurrentClientData != null)
842 //Do not switch client if priority of current client is higher
843 if (!aForce && requestedClientData.Priority < iCurrentClientData.Priority)
849 double lastSwitchToClientSecondsAgo = (DateTime.Now - iCurrentClientData.LastSwitchTime).TotalSeconds;
850 //TODO: put that hard coded value as a client property
851 //Clients should be able to define how often they can be interrupted
852 //Thus a background client can set this to zero allowing any other client to interrupt at any time
853 //We could also compute this delay by looking at the requests frequencies?
855 requestedClientData.Priority == iCurrentClientData.Priority &&
856 //Time sharing is only if clients have the same priority
857 (lastSwitchToClientSecondsAgo < 30)) //Make sure a client is on for at least 30 seconds
859 //Don't switch clients too often
864 //Set current client ID.
865 iCurrentClientSessionId = aSessionId;
866 //Set the time we last switched to that client
867 iClients[aSessionId].LastSwitchTime = DateTime.Now;
868 //Fetch and set current client data.
869 iCurrentClientData = requestedClientData;
870 //Apply layout and set data fields.
871 UpdateTableLayoutPanel(iCurrentClientData.View, iTableLayoutPanelCurrentClient);
874 private void buttonFont_Click(object sender, EventArgs e)
876 //fontDialog.ShowColor = true;
877 //fontDialog.ShowApply = true;
878 fontDialog.ShowEffects = true;
879 fontDialog.Font = cds.Font;
881 fontDialog.FixedPitchOnly = checkBoxFixedPitchFontOnly.Checked;
883 //fontDialog.ShowHelp = true;
885 //fontDlg.MaxSize = 40;
886 //fontDlg.MinSize = 22;
888 //fontDialog.Parent = this;
889 //fontDialog.StartPosition = FormStartPosition.CenterParent;
891 //DlgBox.ShowDialog(fontDialog);
893 //if (fontDialog.ShowDialog(this) != DialogResult.Cancel)
894 if (DlgBox.ShowDialog(fontDialog) != DialogResult.Cancel)
897 cds.Font = fontDialog.Font;
898 Properties.Settings.Default.Save();
900 //Set the fonts to all our labels in our layout
901 UpdateFonts(iTableLayoutPanelDisplay);
908 /// TODO: review this in respect to our logical font feature when we get there.
910 void CheckFontHeight()
912 //Show font height and width
913 labelFontHeight.Text = "Font height: " + cds.Font.Height;
914 float charWidth = IsFixedWidth(cds.Font);
915 if (charWidth == 0.0f)
917 labelFontWidth.Visible = false;
921 labelFontWidth.Visible = true;
922 labelFontWidth.Text = "Font width: " + charWidth;
925 MarqueeLabel label = null;
926 //Get the first label control we can find
927 foreach (Control ctrl in iTableLayoutPanelCurrentClient.Controls)
929 if (ctrl is MarqueeLabel)
931 label = (MarqueeLabel) ctrl;
936 //Now check font height and show a warning if needed.
937 if (label != null && label.Font.Height > label.Height)
939 labelWarning.Text = "WARNING: Selected font is too height by " + (label.Font.Height - label.Height) +
941 labelWarning.Visible = true;
945 labelWarning.Visible = false;
950 private void buttonCapture_Click(object sender, EventArgs e)
952 System.Drawing.Bitmap bmp = new System.Drawing.Bitmap(iTableLayoutPanelDisplay.Width, iTableLayoutPanelDisplay.Height);
953 iTableLayoutPanelDisplay.DrawToBitmap(bmp, iTableLayoutPanelDisplay.ClientRectangle);
954 //Bitmap bmpToSave = new Bitmap(bmp);
955 bmp.Save("D:\\capture.png");
958 string outputFileName = "d:\\capture.png";
959 using (MemoryStream memory = new MemoryStream())
961 using (FileStream fs = new FileStream(outputFileName, FileMode.OpenOrCreate, FileAccess.ReadWrite))
963 bmp.Save(memory, System.Drawing.Imaging.ImageFormat.Png);
964 byte[] bytes = memory.ToArray();
965 fs.Write(bytes, 0, bytes.Length);
972 private void CheckForRequestResults()
974 if (iDisplay.IsRequestPending())
976 switch (iDisplay.AttemptRequestCompletion())
978 case MiniDisplay.Request.FirmwareRevision:
979 toolStripStatusLabelConnect.Text += " v" + iDisplay.FirmwareRevision();
980 //Issue next request then
981 iDisplay.RequestPowerSupplyStatus();
984 case MiniDisplay.Request.PowerSupplyStatus:
985 if (iDisplay.PowerSupplyStatus())
987 toolStripStatusLabelPower.Text = "ON";
991 toolStripStatusLabelPower.Text = "OFF";
993 //Issue next request then
994 iDisplay.RequestDeviceId();
997 case MiniDisplay.Request.DeviceId:
998 toolStripStatusLabelConnect.Text += " - " + iDisplay.DeviceId();
999 //No more request to issue
1005 public static uint ColorWhiteIsOn(int aX, int aY, uint aPixel)
1007 if ((aPixel & 0x00FFFFFF) == 0x00FFFFFF)
1014 public static uint ColorUntouched(int aX, int aY, uint aPixel)
1019 public static uint ColorInversed(int aX, int aY, uint aPixel)
1024 public static uint ColorChessboard(int aX, int aY, uint aPixel)
1026 if ((aX%2 == 0) && (aY%2 == 0))
1030 else if ((aX%2 != 0) && (aY%2 != 0))
1038 public static int ScreenReversedX(System.Drawing.Bitmap aBmp, int aX)
1040 return aBmp.Width - aX - 1;
1043 public int ScreenReversedY(System.Drawing.Bitmap aBmp, int aY)
1045 return iBmp.Height - aY - 1;
1048 public int ScreenX(System.Drawing.Bitmap aBmp, int aX)
1053 public int ScreenY(System.Drawing.Bitmap aBmp, int aY)
1059 /// Select proper pixel delegates according to our current settings.
1061 private void SetupPixelDelegates()
1063 //Select our pixel processing routine
1064 if (cds.InverseColors)
1066 //iColorFx = ColorChessboard;
1067 iColorFx = ColorInversed;
1071 iColorFx = ColorWhiteIsOn;
1074 //Select proper coordinate translation functions
1075 //We used delegate/function pointer to support reverse screen without doing an extra test on each pixels
1076 if (cds.ReverseScreen)
1078 iScreenX = ScreenReversedX;
1079 iScreenY = ScreenReversedY;
1090 /// This is our timer tick responsible to perform our render
1091 /// TODO: Use a threading timer instead of a Windows form timer.
1093 /// <param name="sender"></param>
1094 /// <param name="e"></param>
1095 private void timer_Tick(object sender, EventArgs e)
1097 //Update our animations
1098 DateTime NewTickTime = DateTime.Now;
1100 UpdateNetworkSignal(LastTickTime, NewTickTime);
1102 // Update animation for all our marquees
1103 UpdateMarqueesAnimations(iTableLayoutPanelDisplay, LastTickTime, NewTickTime);
1105 // Update audio visualization
1106 UpdateAudioVisualization();
1108 //Update our display
1109 if (iDisplay.IsOpen())
1111 CheckForRequestResults();
1113 //Check if frame rendering is needed
1114 //Typically used when showing clock
1115 if (!iSkipFrameRendering)
1120 iBmp = new System.Drawing.Bitmap(iTableLayoutPanelDisplay.Width, iTableLayoutPanelDisplay.Height,
1121 PixelFormat.Format32bppArgb);
1122 iCreateBitmap = false;
1124 iTableLayoutPanelDisplay.DrawToBitmap(iBmp, iTableLayoutPanelDisplay.ClientRectangle);
1125 //iBmp.Save("D:\\capture.png");
1127 //Send it to our display
1128 for (int i = 0; i < iBmp.Width; i++)
1130 for (int j = 0; j < iBmp.Height; j++)
1134 //Get our processed pixel coordinates
1135 int x = iScreenX(iBmp, i);
1136 int y = iScreenY(iBmp, j);
1138 uint color = (uint) iBmp.GetPixel(i, j).ToArgb();
1139 //Apply color effects
1140 color = iColorFx(x, y, color);
1142 iDisplay.SetPixel(x, y, color);
1147 iDisplay.SwapBuffers();
1151 //Compute instant FPS
1152 toolStripStatusLabelFps.Text = (1.0/NewTickTime.Subtract(LastTickTime).TotalSeconds).ToString("F0") + " / " +
1153 (1000/iTimerDisplay.Interval).ToString() + " FPS";
1155 LastTickTime = NewTickTime;
1160 /// Update marquee animation children of the given control.
1162 static private void UpdateMarqueesAnimations(Control aControl, DateTime aLastTickTime, DateTime aNewTickTime)
1164 // For each our children
1165 foreach (Control ctrl in aControl.Controls)
1167 // Update animation if it is a marquee control
1168 if (ctrl is MarqueeLabel)
1170 ((MarqueeLabel)ctrl).UpdateAnimation(aLastTickTime, aNewTickTime);
1173 // Go one level deeper by recursion
1174 UpdateMarqueesAnimations(ctrl, aLastTickTime, aNewTickTime);
1179 /// Attempt to establish connection with our display hardware.
1181 private void OpenDisplayConnection()
1183 CloseDisplayConnection();
1185 if (!iDisplay.Open((MiniDisplay.Type) cds.DisplayType))
1188 toolStripStatusLabelConnect.Text = "Connection error";
1192 private void CloseDisplayConnection()
1194 //Status will be updated upon receiving the closed event
1196 if (iDisplay == null || !iDisplay.IsOpen())
1201 //Do not clear if we gave up on rendering already.
1202 //This means we will keep on displaying clock on MDM166AA for instance.
1203 if (!iSkipFrameRendering)
1206 iDisplay.SwapBuffers();
1209 iDisplay.SetAllIconsStatus(0); //Turn off all icons
1213 private void buttonOpen_Click(object sender, EventArgs e)
1215 OpenDisplayConnection();
1218 private void buttonClose_Click(object sender, EventArgs e)
1220 CloseDisplayConnection();
1223 private void buttonClear_Click(object sender, EventArgs e)
1226 iDisplay.SwapBuffers();
1229 private void buttonFill_Click(object sender, EventArgs e)
1232 iDisplay.SwapBuffers();
1235 private void trackBarBrightness_Scroll(object sender, EventArgs e)
1237 cds.Brightness = trackBarBrightness.Value;
1238 Properties.Settings.Default.Save();
1239 iDisplay.SetBrightness(trackBarBrightness.Value);
1245 /// CDS stands for Current Display Settings
1247 private DisplaySettings cds
1251 DisplaysSettings settings = Properties.Settings.Default.DisplaysSettings;
1252 if (settings == null)
1254 settings = new DisplaysSettings();
1256 Properties.Settings.Default.DisplaysSettings = settings;
1259 //Make sure all our settings have been created
1260 while (settings.Displays.Count <= Properties.Settings.Default.CurrentDisplayIndex)
1262 settings.Displays.Add(new DisplaySettings());
1265 DisplaySettings displaySettings = settings.Displays[Properties.Settings.Default.CurrentDisplayIndex];
1266 return displaySettings;
1271 /// Check if the given font has a fixed character pitch.
1273 /// <param name="ft"></param>
1274 /// <returns>0.0f if this is not a monospace font, otherwise returns the character width.</returns>
1275 public float IsFixedWidth(Font ft)
1277 Graphics g = CreateGraphics();
1278 char[] charSizes = new char[] {'i', 'a', 'Z', '%', '#', 'a', 'B', 'l', 'm', ',', '.'};
1279 float charWidth = g.MeasureString("I", ft, Int32.MaxValue, StringFormat.GenericTypographic).Width;
1281 bool fixedWidth = true;
1283 foreach (char c in charSizes)
1284 if (g.MeasureString(c.ToString(), ft, Int32.MaxValue, StringFormat.GenericTypographic).Width !=
1297 /// Update fonts in our control tree.
1299 /// <param name="aControls"></param>
1300 private void UpdateFonts(Control aControl)
1302 foreach (Control ctrl in aControl.Controls)
1304 if (ctrl is MarqueeLabel)
1306 MarqueeLabel marquee = (MarqueeLabel)ctrl;
1307 marquee.Font = cds.Font;
1316 /// Update marquees separator in our control tree.
1318 /// <param name="aControls"></param>
1319 private void UpdateMarqueesSeparator(Control aControl)
1321 foreach (Control ctrl in aControl.Controls)
1323 if (ctrl is MarqueeLabel)
1325 MarqueeLabel marquee = (MarqueeLabel)ctrl;
1326 marquee.Separator = cds.Separator;
1330 UpdateMarqueesSeparator(ctrl);
1335 /// Synchronize UI with settings
1337 private void UpdateStatus()
1340 checkBoxShowBorders.Checked = cds.ShowBorders;
1341 iTableLayoutPanelCurrentClient.CellBorderStyle = (cds.ShowBorders
1342 ? TableLayoutPanelCellBorderStyle.Single
1343 : TableLayoutPanelCellBorderStyle.None);
1345 //Set the proper font to each of our labels
1346 UpdateFonts(iTableLayoutPanelDisplay);
1349 //Check if "run on Windows startup" is enabled
1350 checkBoxAutoStart.Checked = iStartupManager.Startup;
1353 comboBoxHdmiPort.SelectedIndex = Properties.Settings.Default.CecHdmiPort - 1;
1355 //Mini Display settings
1356 checkBoxReverseScreen.Checked = cds.ReverseScreen;
1357 checkBoxInverseColors.Checked = cds.InverseColors;
1358 checkBoxShowVolumeLabel.Checked = cds.ShowVolumeLabel;
1359 checkBoxScaleToFit.Checked = cds.ScaleToFit;
1360 maskedTextBoxMinFontSize.Enabled = cds.ScaleToFit;
1361 labelMinFontSize.Enabled = cds.ScaleToFit;
1362 maskedTextBoxMinFontSize.Text = cds.MinFontSize.ToString();
1363 maskedTextBoxScrollingSpeed.Text = cds.ScrollingSpeedInPixelsPerSecond.ToString();
1364 comboBoxDisplayType.SelectedIndex = cds.DisplayType;
1365 iTimerDisplay.Interval = cds.TimerInterval;
1366 maskedTextBoxTimerInterval.Text = cds.TimerInterval.ToString();
1367 textBoxScrollLoopSeparator.Text = cds.Separator;
1369 SetupPixelDelegates();
1371 if (iDisplay.IsOpen())
1373 //We have a display connection
1374 //Reflect that in our UI
1377 iTableLayoutPanelDisplay.Enabled = true;
1378 iTableLayoutPanelCurrentClient.Enabled = true;
1379 iPanelDisplay.Enabled = true;
1381 //Only setup brightness if display is open
1382 trackBarBrightness.Minimum = iDisplay.MinBrightness();
1383 trackBarBrightness.Maximum = iDisplay.MaxBrightness();
1384 if (cds.Brightness < iDisplay.MinBrightness() || cds.Brightness > iDisplay.MaxBrightness())
1386 //Brightness out of range, this can occur when using auto-detect
1387 //Use max brightness instead
1388 trackBarBrightness.Value = iDisplay.MaxBrightness();
1389 iDisplay.SetBrightness(iDisplay.MaxBrightness());
1393 trackBarBrightness.Value = cds.Brightness;
1394 iDisplay.SetBrightness(cds.Brightness);
1397 //Try compute the steps to something that makes sense
1398 trackBarBrightness.LargeChange = Math.Max(1, (iDisplay.MaxBrightness() - iDisplay.MinBrightness())/5);
1399 trackBarBrightness.SmallChange = 1;
1402 buttonFill.Enabled = true;
1403 buttonClear.Enabled = true;
1404 buttonOpen.Enabled = false;
1405 buttonClose.Enabled = true;
1406 trackBarBrightness.Enabled = true;
1407 toolStripStatusLabelConnect.Text = "Connected - " + iDisplay.Vendor() + " - " + iDisplay.Product();
1408 //+ " - " + iDisplay.SerialNumber();
1410 if (iDisplay.SupportPowerOnOff())
1412 buttonPowerOn.Enabled = true;
1413 buttonPowerOff.Enabled = true;
1417 buttonPowerOn.Enabled = false;
1418 buttonPowerOff.Enabled = false;
1421 if (iDisplay.SupportClock())
1423 buttonShowClock.Enabled = true;
1424 buttonHideClock.Enabled = true;
1428 buttonShowClock.Enabled = false;
1429 buttonHideClock.Enabled = false;
1433 //Check if Volume Label is supported. To date only MDM166AA supports that crap :)
1434 checkBoxShowVolumeLabel.Enabled = iDisplay.IconCount(MiniDisplay.IconType.VolumeLabel) > 0;
1436 if (cds.ShowVolumeLabel)
1438 iDisplay.SetIconOn(MiniDisplay.IconType.VolumeLabel);
1442 iDisplay.SetIconOff(MiniDisplay.IconType.VolumeLabel);
1447 //Display connection not available
1448 //Reflect that in our UI
1450 //In debug start our timer even if we don't have a display connection
1453 //In production environment we don't need our timer if no display connection
1456 checkBoxShowVolumeLabel.Enabled = false;
1457 iTableLayoutPanelDisplay.Enabled = false;
1458 iTableLayoutPanelCurrentClient.Enabled = false;
1459 iPanelDisplay.Enabled = false;
1460 buttonFill.Enabled = false;
1461 buttonClear.Enabled = false;
1462 buttonOpen.Enabled = true;
1463 buttonClose.Enabled = false;
1464 trackBarBrightness.Enabled = false;
1465 buttonPowerOn.Enabled = false;
1466 buttonPowerOff.Enabled = false;
1467 buttonShowClock.Enabled = false;
1468 buttonHideClock.Enabled = false;
1469 toolStripStatusLabelConnect.Text = "Disconnected";
1470 toolStripStatusLabelPower.Text = "N/A";
1479 /// <param name="sender"></param>
1480 /// <param name="e"></param>
1481 private void checkBoxShowVolumeLabel_CheckedChanged(object sender, EventArgs e)
1483 cds.ShowVolumeLabel = checkBoxShowVolumeLabel.Checked;
1484 Properties.Settings.Default.Save();
1488 private void checkBoxShowBorders_CheckedChanged(object sender, EventArgs e)
1490 //Save our show borders setting
1491 iTableLayoutPanelCurrentClient.CellBorderStyle = (checkBoxShowBorders.Checked
1492 ? TableLayoutPanelCellBorderStyle.Single
1493 : TableLayoutPanelCellBorderStyle.None);
1494 cds.ShowBorders = checkBoxShowBorders.Checked;
1495 Properties.Settings.Default.Save();
1499 private void checkBoxAutoStart_CheckedChanged(object sender, EventArgs e)
1501 iStartupManager.Startup = checkBoxAutoStart.Checked;
1505 private void checkBoxReverseScreen_CheckedChanged(object sender, EventArgs e)
1507 //Save our reverse screen setting
1508 cds.ReverseScreen = checkBoxReverseScreen.Checked;
1509 Properties.Settings.Default.Save();
1510 SetupPixelDelegates();
1513 private void checkBoxInverseColors_CheckedChanged(object sender, EventArgs e)
1515 //Save our inverse colors setting
1516 cds.InverseColors = checkBoxInverseColors.Checked;
1517 Properties.Settings.Default.Save();
1518 SetupPixelDelegates();
1521 private void checkBoxScaleToFit_CheckedChanged(object sender, EventArgs e)
1523 //Save our scale to fit setting
1524 cds.ScaleToFit = checkBoxScaleToFit.Checked;
1525 Properties.Settings.Default.Save();
1527 labelMinFontSize.Enabled = cds.ScaleToFit;
1528 maskedTextBoxMinFontSize.Enabled = cds.ScaleToFit;
1531 private void MainForm_Resize(object sender, EventArgs e)
1533 if (WindowState == FormWindowState.Minimized)
1535 // To workaround our empty bitmap bug on Windows 7 we need to recreate our bitmap when the application is minimized
1536 // That's apparently not needed on Windows 10 but we better leave it in place.
1537 iCreateBitmap = true;
1541 private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
1544 iNetworkManager.Dispose();
1545 DestroyAudioManager();
1546 CloseDisplayConnection();
1548 e.Cancel = iClosing;
1551 public void StartServer()
1553 iServiceHost = new ServiceHost
1556 new Uri[] {new Uri("net.tcp://localhost:8001/")}
1559 iServiceHost.AddServiceEndpoint(typeof(IService), new NetTcpBinding(SecurityMode.None, true),
1561 iServiceHost.Open();
1564 public void StopServer()
1566 if (iClients.Count > 0 && !iClosing)
1570 BroadcastCloseEvent();
1575 MessageBox.Show("Force exit?", "Waiting for clients...", MessageBoxButtons.YesNo,
1576 MessageBoxIcon.Warning) == DialogResult.Yes)
1578 iClosing = false; //We make sure we force close if asked twice
1583 //We removed that as it often lags for some reason
1584 //iServiceHost.Close();
1591 public void BroadcastCloseEvent()
1593 Trace.TraceInformation("BroadcastCloseEvent - start");
1595 var inactiveClients = new List<string>();
1596 foreach (var client in iClients)
1598 //if (client.Key != eventData.ClientName)
1602 Trace.TraceInformation("BroadcastCloseEvent - " + client.Key);
1603 client.Value.Callback.OnCloseOrder( /*eventData*/);
1605 catch (Exception ex)
1607 inactiveClients.Add(client.Key);
1612 if (inactiveClients.Count > 0)
1614 foreach (var client in inactiveClients)
1616 iClients.Remove(client);
1617 Program.iFormMain.iTreeViewClients.Nodes.Remove(
1618 Program.iFormMain.iTreeViewClients.Nodes.Find(client, false)[0]);
1622 if (iClients.Count == 0)
1624 ClearLayout(iTableLayoutPanelCurrentClient);
1625 iCurrentClientData = null;
1630 /// Just remove all our fields.
1632 static private void ClearLayout(TableLayoutPanel aPanel)
1634 // For each loop did not work as calling Dispose on a control removes it from the collection.
1635 // We make sure every control are disposed of notably to turn off visualizer when no more needed.
1636 // That's the only way we found to make sure Control.Disposed is called in a timely fashion.
1637 // 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.
1638 // That's what happened with our MarqueeLabel until we fixed it's Dispose override.
1639 while (aPanel.Controls.Count>0)
1641 // Dispose our last item
1642 aPanel.Controls[aPanel.Controls.Count - 1].Dispose();
1645 DoClearLayout(aPanel);
1651 /// <param name="aPanel"></param>
1652 static private void DoClearLayout(TableLayoutPanel aPanel)
1654 aPanel.Controls.Clear();
1655 aPanel.RowStyles.Clear();
1656 aPanel.ColumnStyles.Clear();
1657 aPanel.RowCount = 0;
1658 aPanel.ColumnCount = 0;
1662 /// Just launch a demo client.
1664 private void StartNewClient(string aTopText = "", string aBottomText = "")
1666 Thread clientThread = new Thread(SharpDisplayClient.Program.MainWithParams);
1667 SharpDisplayClient.StartParams myParams = new SharpDisplayClient.StartParams(
1668 new Point(this.Right, this.Top), aTopText, aBottomText);
1669 clientThread.Start(myParams);
1674 /// Just launch our idle client.
1676 private void StartIdleClient(string aTopText = "", string aBottomText = "")
1678 Thread clientThread = new Thread(SharpDisplayClientIdle.Program.MainWithParams);
1679 SharpDisplayClientIdle.StartParams myParams =
1680 new SharpDisplayClientIdle.StartParams(new Point(this.Right, this.Top), aTopText, aBottomText);
1681 clientThread.Start(myParams);
1686 private void buttonStartClient_Click(object sender, EventArgs e)
1691 private void buttonSuspend_Click(object sender, EventArgs e)
1696 private void StartTimer()
1698 LastTickTime = DateTime.Now; //Reset timer to prevent jump
1699 iTimerDisplay.Enabled = true;
1700 UpdateSuspendButton();
1703 private void StopTimer()
1705 LastTickTime = DateTime.Now; //Reset timer to prevent jump
1706 iTimerDisplay.Enabled = false;
1707 UpdateSuspendButton();
1710 private void ToggleTimer()
1712 LastTickTime = DateTime.Now; //Reset timer to prevent jump
1713 iTimerDisplay.Enabled = !iTimerDisplay.Enabled;
1714 UpdateSuspendButton();
1717 private void UpdateSuspendButton()
1719 if (!iTimerDisplay.Enabled)
1721 buttonSuspend.Text = "Run";
1725 buttonSuspend.Text = "Pause";
1730 private void buttonCloseClients_Click(object sender, EventArgs e)
1732 BroadcastCloseEvent();
1735 private void treeViewClients_AfterSelect(object sender, TreeViewEventArgs e)
1737 //Root node must have at least one child
1738 if (e.Node.Nodes.Count == 0)
1743 //If the selected node is the root node of a client then switch to it
1744 string sessionId = e.Node.Nodes[0].Text; //First child of a root node is the sessionId
1745 if (iClients.ContainsKey(sessionId)) //Check that's actually what we are looking at
1747 //We have a valid session just switch to that client
1748 SetCurrentClient(sessionId, true);
1757 /// <param name="aSessionId"></param>
1758 /// <param name="aCallback"></param>
1759 public void AddClientThreadSafe(string aSessionId, ICallback aCallback)
1761 if (this.InvokeRequired)
1763 //Not in the proper thread, invoke ourselves
1764 AddClientDelegate d = new AddClientDelegate(AddClientThreadSafe);
1765 this.Invoke(d, new object[] {aSessionId, aCallback});
1769 //We are in the proper thread
1770 //Add this session to our collection of clients
1771 ClientData newClient = new ClientData(aSessionId, aCallback);
1772 Program.iFormMain.iClients.Add(aSessionId, newClient);
1773 //Add this session to our UI
1774 UpdateClientTreeViewNode(newClient);
1780 /// Find the client with the highest priority if any.
1782 /// <returns>Our highest priority client or null if not a single client is connected.</returns>
1783 public ClientData FindHighestPriorityClient()
1785 ClientData highestPriorityClient = null;
1786 foreach (var client in iClients)
1788 if (highestPriorityClient == null || client.Value.Priority > highestPriorityClient.Priority)
1790 highestPriorityClient = client.Value;
1794 return highestPriorityClient;
1800 /// <param name="aSessionId"></param>
1801 public void RemoveClientThreadSafe(string aSessionId)
1803 if (this.InvokeRequired)
1805 //Not in the proper thread, invoke ourselves
1806 RemoveClientDelegate d = new RemoveClientDelegate(RemoveClientThreadSafe);
1807 this.Invoke(d, new object[] {aSessionId});
1811 //We are in the proper thread
1812 //Remove this session from both client collection and UI tree view
1813 if (Program.iFormMain.iClients.Keys.Contains(aSessionId))
1815 Program.iFormMain.iClients.Remove(aSessionId);
1816 Program.iFormMain.iTreeViewClients.Nodes.Remove(
1817 Program.iFormMain.iTreeViewClients.Nodes.Find(aSessionId, false)[0]);
1818 //Update recording status too whenever a client is removed
1819 UpdateRecordingNotification();
1822 if (iCurrentClientSessionId == aSessionId)
1824 //The current client is closing
1825 iCurrentClientData = null;
1826 //Find the client with the highest priority and set it as current
1827 ClientData newCurrentClient = FindHighestPriorityClient();
1828 if (newCurrentClient != null)
1830 SetCurrentClient(newCurrentClient.SessionId, true);
1834 if (iClients.Count == 0)
1836 //Clear our screen when last client disconnects
1837 ClearLayout(iTableLayoutPanelCurrentClient);
1838 iCurrentClientData = null;
1842 //We were closing our form
1843 //All clients are now closed
1844 //Just resume our close operation
1855 /// <param name="aSessionId"></param>
1856 /// <param name="aLayout"></param>
1857 public void SetClientLayoutThreadSafe(string aSessionId, TableLayout aLayout)
1859 if (this.InvokeRequired)
1861 //Not in the proper thread, invoke ourselves
1862 Invoke(new Action<FormMain>((sender) => { SetClientLayoutThreadSafe(aSessionId, aLayout); }), this);
1866 ClientData client = iClients[aSessionId];
1873 // If we have a matching client and we want to change the client layout
1874 if (client.Target == Target.Client)
1876 //Don't change a thing if the layout is the same
1877 if (!client.View.Layout.IsSameAs(aLayout))
1879 Debug.Print("SetClientLayoutThreadSafe: Layout updated.");
1880 //Set our client layout then
1881 client.View.Layout = aLayout;
1882 //So that next time we update all our fields at ones
1883 client.HasNewLayout = true;
1884 //Layout has changed clear our fields then
1885 client.View.Fields.Clear();
1887 UpdateClientTreeViewNode(client);
1891 Debug.Print("SetClientLayoutThreadSafe: Layout has not changed.");
1894 else if (client.Target == Target.Display)
1896 // Mark our display layout has updated and wait for the fields.
1897 // Is display layout a property from our client?
1898 //iTableLayoutPanelDisplay. = aLayout;
1905 /// <param name="aSessionId"></param>
1906 /// <param name="aField"></param>
1907 public void SetClientFieldThreadSafe(string aSessionId, DataField aField)
1909 if (this.InvokeRequired)
1911 //Not in the proper thread, invoke ourselves
1912 Invoke(new Action<FormMain>((sender) => { SetClientFieldThreadSafe(aSessionId, aField); }), this);
1916 //We are in the proper thread
1917 //Call the non-thread-safe variant
1918 SetClientField(aSessionId, aField);
1926 /// Set a data field in the given client.
1928 /// <param name="aSessionId"></param>
1929 /// <param name="aField"></param>
1930 private void SetClientField(string aSessionId, DataField aField)
1932 //TODO: should check if the field actually changed?
1934 ClientData client = iClients[aSessionId];
1935 bool layoutChanged = false;
1936 bool contentChanged = true;
1938 //Fetch our field index
1939 int fieldIndex = client.View.FindSameFieldIndex(aField);
1943 //No corresponding field, just bail out
1947 //Keep our previous field in there
1948 DataField previousField = client.View.Fields[fieldIndex];
1949 //Just update that field then
1950 client.View.Fields[fieldIndex] = aField;
1952 if (!aField.IsTableField)
1954 //We are done then if that field is not in our table layout
1958 TableField tableField = (TableField) aField;
1960 if (previousField.IsSameLayout(aField))
1962 //If we are updating a field in our current client we need to update it in our panel
1963 if (aSessionId == iCurrentClientSessionId)
1965 Control ctrl = iTableLayoutPanelCurrentClient.GetControlFromPosition(tableField.Column, tableField.Row);
1966 if (aField.IsTextField && ctrl is MarqueeLabel)
1968 TextField textField = (TextField)aField;
1969 //Text field control already in place, just change the text
1970 MarqueeLabel label = (MarqueeLabel)ctrl;
1971 contentChanged = (label.Text != textField.Text || label.TextAlign != textField.Alignment);
1972 label.Text = textField.Text;
1973 label.TextAlign = textField.Alignment;
1975 else if (aField.IsBitmapField && ctrl is PictureBox)
1977 BitmapField bitmapField = (BitmapField)aField;
1978 contentChanged = true; //TODO: Bitmap comp or should we leave that to clients?
1979 //Bitmap field control already in place just change the bitmap
1980 PictureBox pictureBox = (PictureBox)ctrl;
1981 pictureBox.Image = bitmapField.Bitmap;
1983 else if (aField is AudioVisualizerField && ctrl is PictureBox)
1985 contentChanged = false; // Since nothing was changed
1989 layoutChanged = true;
1995 layoutChanged = true;
1998 //If either content or layout changed we need to update our tree view to reflect the changes
1999 if (contentChanged || layoutChanged)
2001 UpdateClientTreeViewNode(client);
2005 Debug.Print("Layout changed");
2006 //Our layout has changed, if we are already the current client we need to update our panel
2007 if (aSessionId == iCurrentClientSessionId)
2009 //Apply layout and set data fields.
2010 UpdateTableLayoutPanel(iCurrentClientData.View, iTableLayoutPanelCurrentClient);
2015 Debug.Print("Layout has not changed.");
2020 Debug.Print("WARNING: content and layout have not changed!");
2023 //When a client field is set we try switching to this client to present the new information to our user
2024 SetCurrentClient(aSessionId);
2030 /// <param name="aTexts"></param>
2031 public void SetClientFieldsThreadSafe(string aSessionId, System.Collections.Generic.IList<DataField> aFields)
2033 if (this.InvokeRequired)
2035 //Not in the proper thread, invoke ourselves
2036 Invoke(new Action<FormMain>((sender) => { SetClientFieldsThreadSafe(aSessionId, aFields); }), this);
2040 ClientData client = iClients[aSessionId];
2042 if (client.HasNewLayout)
2044 //TODO: Assert client.Count == 0
2045 //Our layout was just changed
2046 //Do some special handling to avoid re-creating our panel N times, once for each fields
2047 client.HasNewLayout = false;
2048 //Just set all our fields then
2049 client.View.Fields.AddRange(aFields);
2050 //Try switch to that client
2051 SetCurrentClient(aSessionId);
2053 //If we are updating the current client update our panel
2054 if (aSessionId == iCurrentClientSessionId)
2056 //Apply layout and set data fields.
2057 UpdateTableLayoutPanel(iCurrentClientData.View, iTableLayoutPanelCurrentClient);
2060 UpdateClientTreeViewNode(client);
2064 //Put each our text fields in a label control
2065 foreach (DataField field in aFields)
2067 SetClientField(aSessionId, field);
2076 /// <param name="aSessionId"></param>
2077 /// <param name="aName"></param>
2078 public void SetClientNameThreadSafe(string aSessionId, string aName)
2080 if (this.InvokeRequired)
2082 //Not in the proper thread, invoke ourselves
2083 Invoke(new Action<FormMain>((sender) => { SetClientNameThreadSafe(aSessionId, aName); }), this);
2087 //We are in the proper thread
2089 ClientData client = iClients[aSessionId];
2093 client.Name = aName;
2094 //Update our tree-view
2095 UpdateClientTreeViewNode(client);
2101 public void SetClientPriorityThreadSafe(string aSessionId, uint aPriority)
2103 if (this.InvokeRequired)
2105 //Not in the proper thread, invoke ourselves
2106 Invoke(new Action<FormMain>((sender) => { SetClientPriorityThreadSafe(aSessionId, aPriority); }), this);
2110 //We are in the proper thread
2112 ClientData client = iClients[aSessionId];
2116 client.Priority = aPriority;
2117 //Update our tree-view
2118 UpdateClientTreeViewNode(client);
2119 //Change our current client as per new priority
2120 ClientData newCurrentClient = FindHighestPriorityClient();
2121 if (newCurrentClient != null)
2123 SetCurrentClient(newCurrentClient.SessionId);
2132 /// <param name="aSessionId"></param>
2133 /// <param name="aPriority"></param>
2134 public void SetClientTargetThreadSafe(string aSessionId, Target aTarget)
2136 if (this.InvokeRequired)
2138 //Not in the proper thread, invoke ourselves
2139 Invoke(new Action<FormMain>((sender) => { SetClientTargetThreadSafe(aSessionId, aTarget); }), this);
2143 //We are in the proper thread
2145 ClientData client = iClients[aSessionId];
2149 client.Target = aTarget;
2150 //Update our tree-view
2151 //UpdateClientTreeViewNode(client);
2159 /// <param name="value"></param>
2160 /// <param name="maxChars"></param>
2161 /// <returns></returns>
2162 public static string Truncate(string value, int maxChars)
2164 return value.Length <= maxChars ? value : value.Substring(0, maxChars - 3) + "...";
2168 /// Update our recording notification.
2170 private void UpdateRecordingNotification()
2173 bool activeRecording = false;
2175 RecordingField recField = new RecordingField();
2176 foreach (var client in iClients)
2178 RecordingField rec = (RecordingField) client.Value.View.FindSameFieldAs(recField);
2179 if (rec != null && rec.IsActive)
2181 activeRecording = true;
2182 //Don't break cause we are collecting the names/texts.
2183 if (!String.IsNullOrEmpty(rec.Text))
2185 text += (rec.Text + "\n");
2189 //Not text for that recording, use client name instead
2190 text += client.Value.Name + " recording\n";
2196 //Update our text no matter what, can't have more than 63 characters otherwise it throws an exception.
2197 iRecordingNotification.Text = Truncate(text, 63);
2199 //Change visibility of notification if needed
2200 if (iRecordingNotification.Visible != activeRecording)
2202 iRecordingNotification.Visible = activeRecording;
2203 //Assuming the notification icon is in sync with our display icon
2204 //Take care of our REC icon
2205 if (iDisplay.IsOpen())
2207 iDisplay.SetIconOnOff(MiniDisplay.IconType.Recording, activeRecording);
2215 /// <param name="aClient"></param>
2216 private void UpdateClientTreeViewNode(ClientData aClient)
2218 Debug.Print("UpdateClientTreeViewNode");
2220 if (aClient == null)
2225 //Hook in record icon update too
2226 UpdateRecordingNotification();
2228 TreeNode node = null;
2229 //Check that our client node already exists
2230 //Get our client root node using its key which is our session ID
2231 TreeNode[] nodes = iTreeViewClients.Nodes.Find(aClient.SessionId, false);
2232 if (nodes.Count() > 0)
2234 //We already have a node for that client
2236 //Clear children as we are going to recreate them below
2241 //Client node does not exists create a new one
2242 iTreeViewClients.Nodes.Add(aClient.SessionId, aClient.SessionId);
2243 node = iTreeViewClients.Nodes.Find(aClient.SessionId, false)[0];
2249 if (!String.IsNullOrEmpty(aClient.Name))
2251 //We have a name, use it as text for our root node
2252 node.Text = aClient.Name;
2253 //Add a child with SessionId
2254 node.Nodes.Add(new TreeNode(aClient.SessionId));
2258 //No name, use session ID instead
2259 node.Text = aClient.SessionId;
2262 //Display client priority
2263 node.Nodes.Add(new TreeNode("Priority: " + aClient.Priority));
2265 if (aClient.View.Fields.Count > 0)
2267 //Create root node for our texts
2268 TreeNode textsRoot = new TreeNode("Fields");
2269 node.Nodes.Add(textsRoot);
2270 //For each text add a new entry
2271 foreach (DataField field in aClient.View.Fields)
2273 if (field.IsTextField)
2275 TextField textField = (TextField) field;
2276 textsRoot.Nodes.Add(new TreeNode("[Text]" + textField.Text));
2278 else if (field.IsBitmapField)
2280 textsRoot.Nodes.Add(new TreeNode("[Bitmap]"));
2282 else if (field is AudioVisualizerField)
2284 textsRoot.Nodes.Add(new TreeNode("[Audio Visualizer]"));
2286 else if (field.IsRecordingField)
2288 RecordingField recordingField = (RecordingField) field;
2289 textsRoot.Nodes.Add(new TreeNode("[Recording]" + recordingField.IsActive));
2299 /// Update our display table layout.
2300 /// Will instanciate every field control as defined by our client.
2301 /// Fields must be specified by rows from the left.
2303 /// <param name="aLayout"></param>
2304 private void UpdateTableLayoutPanel(SharpLib.Display.View aView, TableLayoutPanel aPanel)
2306 Debug.Print("UpdateTableLayoutPanel");
2315 TableLayout layout = aView.Layout;
2317 //First clean our current panel
2318 ClearLayout(aPanel);
2320 //Then recreate our rows...
2321 while (aPanel.RowCount < layout.Rows.Count)
2327 while (aPanel.ColumnCount < layout.Columns.Count)
2329 aPanel.ColumnCount++;
2333 for (int i = 0; i < aPanel.ColumnCount; i++)
2335 //Create our column styles
2336 aPanel.ColumnStyles.Add(layout.Columns[i]);
2339 for (int j = 0; j < aPanel.RowCount; j++)
2343 //Create our row styles
2344 aPanel.RowStyles.Add(layout.Rows[j]);
2354 foreach (DataField field in aView.Fields)
2356 if (!field.IsTableField)
2358 //That field is not taking part in our table layout skip it
2362 TableField tableField = (TableField) field;
2364 //Create a control corresponding to the field specified for that cell
2365 Control control = CreateControlForDataField(tableField);
2367 //Add newly created control to our table layout at the specified row and column
2368 aPanel.Controls.Add(control, tableField.Column, tableField.Row);
2369 //Make sure we specify column and row span for that new control
2370 aPanel.SetColumnSpan(control, tableField.ColumnSpan);
2371 aPanel.SetRowSpan(control, tableField.RowSpan);
2379 /// Check our type of data field and create corresponding control
2381 /// <param name="aField"></param>
2382 private Control CreateControlForDataField(DataField aField)
2384 Control control = null;
2385 if (aField.IsTextField)
2387 MarqueeLabel label = new SharpDisplayManager.MarqueeLabel();
2388 label.AutoEllipsis = true;
2389 label.AutoSize = true;
2390 label.BackColor = System.Drawing.Color.Transparent;
2391 label.Dock = System.Windows.Forms.DockStyle.Fill;
2392 label.Location = new System.Drawing.Point(1, 1);
2393 label.Margin = new System.Windows.Forms.Padding(0);
2394 label.Name = "marqueeLabel" + aField;
2395 label.OwnTimer = false;
2396 label.PixelsPerSecond = cds.ScrollingSpeedInPixelsPerSecond;
2397 label.Separator = cds.Separator;
2398 label.MinFontSize = cds.MinFontSize;
2399 label.ScaleToFit = cds.ScaleToFit;
2400 //control.Size = new System.Drawing.Size(254, 30);
2401 //control.TabIndex = 2;
2402 label.Font = cds.Font;
2404 TextField field = (TextField)aField;
2405 label.TextAlign = field.Alignment;
2406 label.UseCompatibleTextRendering = true;
2407 label.Text = field.Text;
2411 else if (aField.IsBitmapField)
2413 //Create picture box
2414 PictureBox picture = new PictureBox();
2415 picture.AutoSize = true;
2416 picture.BackColor = System.Drawing.Color.Transparent;
2417 picture.Dock = System.Windows.Forms.DockStyle.Fill;
2418 picture.Location = new System.Drawing.Point(1, 1);
2419 picture.Margin = new System.Windows.Forms.Padding(0);
2420 picture.Name = "pictureBox" + aField;
2422 BitmapField field = (BitmapField)aField;
2423 picture.Image = field.Bitmap;
2427 else if (aField is AudioVisualizerField)
2429 //Create picture box
2430 PictureBox picture = new PictureBox();
2431 picture.AutoSize = true;
2432 picture.BackColor = System.Drawing.Color.Transparent;
2433 picture.Dock = System.Windows.Forms.DockStyle.Fill;
2434 picture.Location = new System.Drawing.Point(1, 1);
2435 picture.Margin = new System.Windows.Forms.Padding(0);
2436 picture.Name = "pictureBox" + aField;
2438 // Make sure visualization is running
2439 if (iAudioManager != null) // When closing main form with multiple client audio manager can be null. I guess we should fix the core issue instead.
2441 iAudioManager.AddVisualizer();
2444 // Notify audio manager when we don't need audio visualizer anymore
2445 picture.Disposed += (sender, e) =>
2447 if (iAudioManager != null)
2449 // Make sure we stop visualization when not needed
2450 iAudioManager.RemoveVisualizer();
2454 // Create a new bitmap when control size changes
2455 picture.SizeChanged += (sender, e) =>
2457 // Somehow bitmap created when our from is invisible are not working
2458 // Mark our form visibility status
2459 bool visible = Visible;
2460 // Make sure it's visible
2462 // Adjust our bitmap size when control size changes
2463 picture.Image = new System.Drawing.Bitmap(picture.Width, picture.Height, PixelFormat.Format32bppArgb);
2464 // Restore our form visibility
2471 //TODO: Handle recording field?
2477 /// Called when the user selected a new display type.
2479 /// <param name="sender"></param>
2480 /// <param name="e"></param>
2481 private void comboBoxDisplayType_SelectedIndexChanged(object sender, EventArgs e)
2483 //Store the selected display type in our settings
2484 Properties.Settings.Default.CurrentDisplayIndex = comboBoxDisplayType.SelectedIndex;
2485 cds.DisplayType = comboBoxDisplayType.SelectedIndex;
2486 Properties.Settings.Default.Save();
2488 //Try re-opening the display connection if we were already connected.
2489 //Otherwise just update our status to reflect display type change.
2490 if (iDisplay.IsOpen())
2492 OpenDisplayConnection();
2500 private void maskedTextBoxTimerInterval_TextChanged(object sender, EventArgs e)
2502 if (maskedTextBoxTimerInterval.Text != "")
2504 int interval = Convert.ToInt32(maskedTextBoxTimerInterval.Text);
2508 iTimerDisplay.Interval = interval;
2509 cds.TimerInterval = iTimerDisplay.Interval;
2510 Properties.Settings.Default.Save();
2515 private void maskedTextBoxMinFontSize_TextChanged(object sender, EventArgs e)
2517 if (maskedTextBoxMinFontSize.Text != "")
2519 int minFontSize = Convert.ToInt32(maskedTextBoxMinFontSize.Text);
2521 if (minFontSize > 0)
2523 cds.MinFontSize = minFontSize;
2524 Properties.Settings.Default.Save();
2525 //We need to recreate our layout for that change to take effect
2526 if (iCurrentClientData != null)
2528 UpdateTableLayoutPanel(iCurrentClientData.View, iTableLayoutPanelCurrentClient);
2535 private void maskedTextBoxScrollingSpeed_TextChanged(object sender, EventArgs e)
2537 if (maskedTextBoxScrollingSpeed.Text != "")
2539 int scrollingSpeed = Convert.ToInt32(maskedTextBoxScrollingSpeed.Text);
2541 if (scrollingSpeed > 0)
2543 cds.ScrollingSpeedInPixelsPerSecond = scrollingSpeed;
2544 Properties.Settings.Default.Save();
2545 //We need to recreate our layout for that change to take effect
2546 if (iCurrentClientData != null)
2548 UpdateTableLayoutPanel(iCurrentClientData.View, iTableLayoutPanelCurrentClient);
2557 /// <param name="sender"></param>
2558 /// <param name="e"></param>
2559 private void textBoxScrollLoopSeparator_TextChanged(object sender, EventArgs e)
2561 cds.Separator = textBoxScrollLoopSeparator.Text;
2562 Properties.Settings.Default.Save();
2564 //Update our text fields
2565 UpdateMarqueesSeparator(iTableLayoutPanelDisplay);
2568 private void buttonPowerOn_Click(object sender, EventArgs e)
2573 private void buttonPowerOff_Click(object sender, EventArgs e)
2575 iDisplay.PowerOff();
2578 private void buttonShowClock_Click(object sender, EventArgs e)
2583 private void buttonHideClock_Click(object sender, EventArgs e)
2588 private void buttonUpdate_Click(object sender, EventArgs e)
2590 InstallUpdateSyncWithInfo();
2598 if (!iDisplay.IsOpen())
2603 //Devices like MDM166AA don't support windowing and frame rendering must be stopped while showing our clock
2604 iSkipFrameRendering = true;
2607 iDisplay.SwapBuffers();
2608 //Then show our clock
2609 iDisplay.ShowClock();
2617 if (!iDisplay.IsOpen())
2622 //Devices like MDM166AA don't support windowing and frame rendering must be stopped while showing our clock
2623 iSkipFrameRendering = false;
2624 iDisplay.HideClock();
2627 private void InstallUpdateSyncWithInfo()
2629 UpdateCheckInfo info = null;
2631 if (ApplicationDeployment.IsNetworkDeployed)
2633 ApplicationDeployment ad = ApplicationDeployment.CurrentDeployment;
2637 info = ad.CheckForDetailedUpdate();
2640 catch (DeploymentDownloadException dde)
2643 "The new version of the application cannot be downloaded at this time. \n\nPlease check your network connection, or try again later. Error: " +
2647 catch (InvalidDeploymentException ide)
2650 "Cannot check for a new version of the application. The ClickOnce deployment is corrupt. Please redeploy the application and try again. Error: " +
2654 catch (InvalidOperationException ioe)
2657 "This application cannot be updated. It is likely not a ClickOnce application. Error: " +
2662 if (info.UpdateAvailable)
2664 Boolean doUpdate = true;
2666 if (!info.IsUpdateRequired)
2669 MessageBox.Show("An update is available. Would you like to update the application now?",
2670 "Update Available", MessageBoxButtons.OKCancel);
2671 if (!(DialogResult.OK == dr))
2678 // Display a message that the application MUST reboot. Display the minimum required version.
2679 MessageBox.Show("This application has detected a mandatory update from your current " +
2680 "version to version " + info.MinimumRequiredVersion.ToString() +
2681 ". The application will now install the update and restart.",
2682 "Update Available", MessageBoxButtons.OK,
2683 MessageBoxIcon.Information);
2691 MessageBox.Show("The application has been upgraded, and will now restart.");
2692 Application.Restart();
2694 catch (DeploymentDownloadException dde)
2697 "Cannot install the latest version of the application. \n\nPlease check your network connection, or try again later. Error: " +
2705 MessageBox.Show("You are already running the latest version.", "Application up-to-date");
2714 private void SysTrayHideShow()
2720 WindowState = FormWindowState.Normal;
2725 /// Use to handle minimize events.
2727 /// <param name="sender"></param>
2728 /// <param name="e"></param>
2729 private void MainForm_SizeChanged(object sender, EventArgs e)
2731 if (WindowState == FormWindowState.Minimized && Properties.Settings.Default.MinimizeToTray)
2743 /// <param name="sender"></param>
2744 /// <param name="e"></param>
2745 private void tableLayoutPanel_SizeChanged(object sender, EventArgs e)
2747 //Our table layout size has changed which means our display size has changed.
2748 //We need to re-create our bitmap.
2749 iCreateBitmap = true;
2753 /// Broadcast messages to subscribers.
2755 /// <param name="message"></param>
2756 protected override void WndProc(ref Message aMessage)
2758 if (OnWndProc != null)
2760 OnWndProc(ref aMessage);
2763 base.WndProc(ref aMessage);
2766 private void iCheckBoxCecEnabled_CheckedChanged(object sender, EventArgs e)
2772 private void comboBoxHdmiPort_SelectedIndexChanged(object sender, EventArgs e)
2774 //Save CEC HDMI port
2775 Properties.Settings.Default.CecHdmiPort = Convert.ToByte(comboBoxHdmiPort.SelectedIndex);
2776 Properties.Settings.Default.CecHdmiPort++;
2777 Properties.Settings.Default.Save();
2785 private void ResetCec()
2787 if (iCecManager == null)
2789 //Thus skipping initial UI setup
2795 if (Properties.Settings.Default.CecEnabled)
2797 iCecManager.Start(Handle, "CEC",
2798 Properties.Settings.Default.CecHdmiPort);
2807 private async void ResetHarmonyAsync(bool aForceAuth=false)
2809 // ConnectAsync already if we have an existing session cookie
2810 if (Properties.Settings.Default.HarmonyEnabled)
2814 iButtonHarmonyConnect.Enabled = false;
2815 await ConnectHarmonyAsync(aForceAuth);
2817 catch (Exception ex)
2819 Trace.WriteLine("Exception thrown by ConnectHarmonyAsync");
2820 Trace.WriteLine(ex.ToString());
2824 iButtonHarmonyConnect.Enabled = true;
2832 /// <param name="sender"></param>
2833 /// <param name="e"></param>
2834 private void iButtonHarmonyConnect_Click(object sender, EventArgs e)
2836 // User is explicitaly trying to connect
2837 //Reset Harmony Hub connection forcing authentication
2838 ResetHarmonyAsync(true);
2844 /// <param name="sender"></param>
2845 /// <param name="e"></param>
2846 private void iCheckBoxHarmonyEnabled_CheckedChanged(object sender, EventArgs e)
2848 iButtonHarmonyConnect.Enabled = iCheckBoxHarmonyEnabled.Checked;
2854 private void SetupCecLogLevel()
2857 iCecManager.Client.LogLevel = 0;
2859 if (checkBoxCecLogError.Checked)
2860 iCecManager.Client.LogLevel |= (int) CecLogLevel.Error;
2862 if (checkBoxCecLogWarning.Checked)
2863 iCecManager.Client.LogLevel |= (int) CecLogLevel.Warning;
2865 if (checkBoxCecLogNotice.Checked)
2866 iCecManager.Client.LogLevel |= (int) CecLogLevel.Notice;
2868 if (checkBoxCecLogTraffic.Checked)
2869 iCecManager.Client.LogLevel |= (int) CecLogLevel.Traffic;
2871 if (checkBoxCecLogDebug.Checked)
2872 iCecManager.Client.LogLevel |= (int) CecLogLevel.Debug;
2874 iCecManager.Client.FilterOutPollLogs = checkBoxCecLogNoPoll.Checked;
2878 private void ButtonStartIdleClient_Click(object sender, EventArgs e)
2883 private void buttonClearLogs_Click(object sender, EventArgs e)
2885 richTextBoxLogs.Clear();
2888 private void checkBoxCecLogs_CheckedChanged(object sender, EventArgs e)
2898 /// Get the current event based on event tree view selection.
2900 /// <returns></returns>
2901 private Ear.Event CurrentEvent()
2903 //Walk up the tree from the selected node to find our event
2904 TreeNode node = iTreeViewEvents.SelectedNode;
2905 Ear.Event selectedEvent = null;
2906 while (node != null)
2908 if (node.Tag is Ear.Event)
2910 selectedEvent = (Ear.Event) node.Tag;
2916 return selectedEvent;
2920 /// Get the current action based on event tree view selection
2922 /// <returns></returns>
2923 private Ear.Action CurrentAction()
2925 TreeNode node = iTreeViewEvents.SelectedNode;
2926 if (node != null && node.Tag is Ear.Action)
2928 return (Ear.Action) node.Tag;
2937 /// <returns></returns>
2938 private Ear.Object CurrentEarObject()
2940 Ear.Action a = CurrentAction();
2941 Ear.Event e = CurrentEvent();
2952 /// Get the current action based on event tree view selection
2954 /// <returns></returns>
2955 private Ear.Object CurrentEarParent()
2957 TreeNode node = iTreeViewEvents.SelectedNode;
2958 if (node == null || node.Parent == null)
2963 if (node.Parent.Tag is Ear.Object)
2965 return (Ear.Object)node.Parent.Tag;
2968 if (node.Parent.Parent != null && node.Parent.Parent.Tag is Ear.Object)
2970 //Can be the case for events
2971 return (Ear.Object)node.Parent.Parent.Tag;
2981 /// <param name="sender"></param>
2982 /// <param name="e"></param>
2983 private void buttonActionAdd_Click(object sender, EventArgs e)
2985 Ear.Object parent = CurrentEarObject();
2986 if (parent == null )
2988 //We did not find a corresponding event or action
2992 FormEditObject<Ear.Action> ea = new FormEditObject<Ear.Action>();
2993 ea.Text = "Add action";
2994 DialogResult res = CodeProject.Dialog.DlgBox.ShowDialog(ea);
2995 if (res == DialogResult.OK)
2997 parent.Objects.Add(ea.Object);
2998 Properties.Settings.Default.Save();
2999 // We want to select the parent so that one can easily add another action to the same collection
3000 PopulateTreeViewEvents(parent);
3007 /// <param name="sender"></param>
3008 /// <param name="e"></param>
3009 private void buttonActionEdit_Click(object sender, EventArgs e)
3011 Ear.Action selectedAction = CurrentAction();
3012 Ear.Object parent = CurrentEarParent()
3014 if (parent == null || selectedAction == null)
3016 //We did not find a corresponding parent
3020 FormEditObject<Ear.Action> ea = new FormEditObject<Ear.Action>();
3021 ea.Text = "Edit action";
3022 ea.Object = selectedAction;
3023 int actionIndex = iTreeViewEvents.SelectedNode.Index;
3024 DialogResult res = CodeProject.Dialog.DlgBox.ShowDialog(ea);
3025 if (res == DialogResult.OK)
3027 //Make sure we keep the same children as before
3028 ea.Object.Objects = parent.Objects[actionIndex].Objects;
3030 parent.Objects[actionIndex]=ea.Object;
3031 //Save and rebuild our event tree view
3032 Properties.Settings.Default.Save();
3033 PopulateTreeViewEvents(ea.Object);
3040 /// <param name="sender"></param>
3041 /// <param name="e"></param>
3042 private void buttonActionDelete_Click(object sender, EventArgs e)
3044 Ear.Action action = CurrentAction();
3047 //Must select action node
3051 Properties.Settings.Default.EarManager.RemoveAction(action);
3052 Properties.Settings.Default.Save();
3053 PopulateTreeViewEvents();
3059 /// <param name="sender"></param>
3060 /// <param name="e"></param>
3061 private void buttonActionTest_Click(object sender, EventArgs e)
3063 Ear.Action a = CurrentAction();
3068 iTreeViewEvents.Focus();
3074 /// <param name="sender"></param>
3075 /// <param name="e"></param>
3076 private void buttonActionMoveUp_Click(object sender, EventArgs e)
3078 Ear.Action a = CurrentAction();
3080 //Action already at the top of the list
3081 iTreeViewEvents.SelectedNode.Index == 0)
3086 //Swap actions in event's action list
3087 Ear.Object parent = CurrentEarParent();
3088 int currentIndex = iTreeViewEvents.SelectedNode.Index;
3089 Ear.Action movingUp = parent.Objects[currentIndex] as Ear.Action;
3090 Ear.Action movingDown = parent.Objects[currentIndex-1] as Ear.Action;
3091 parent.Objects[currentIndex] = movingDown;
3092 parent.Objects[currentIndex-1] = movingUp;
3094 //Save and populate our tree again
3095 Properties.Settings.Default.Save();
3096 PopulateTreeViewEvents(a);
3102 /// <param name="sender"></param>
3103 /// <param name="e"></param>
3104 private void buttonActionMoveDown_Click(object sender, EventArgs e)
3106 Ear.Action a = CurrentAction();
3108 //Action already at the bottom of the list
3109 iTreeViewEvents.SelectedNode.Index == iTreeViewEvents.SelectedNode.Parent.Nodes.Count - 1)
3114 //Swap actions in event's action list
3115 Ear.Object parent = CurrentEarParent();
3116 int currentIndex = iTreeViewEvents.SelectedNode.Index;
3117 Ear.Action movingDown = parent.Objects[currentIndex] as Ear.Action;
3118 Ear.Action movingUp = parent.Objects[currentIndex + 1] as Ear.Action;
3119 parent.Objects[currentIndex] = movingUp;
3120 parent.Objects[currentIndex + 1] = movingDown;
3122 //Save and populate our tree again
3123 Properties.Settings.Default.Save();
3124 PopulateTreeViewEvents(a);
3131 /// <param name="sender"></param>
3132 /// <param name="e"></param>
3133 private void buttonEventTest_Click(object sender, EventArgs e)
3135 Ear.Event earEvent = CurrentEvent();
3136 if (earEvent != null)
3143 /// Manages events and actions buttons according to selected item in event tree.
3145 /// <param name="sender"></param>
3146 /// <param name="e"></param>
3147 private void iTreeViewEvents_AfterSelect(object sender, TreeViewEventArgs e)
3155 private void UpdateEventView()
3157 //One can always add an event
3158 buttonEventAdd.Enabled = true;
3160 //Enable buttons according to selected item
3161 buttonActionAdd.Enabled =
3162 buttonEventTest.Enabled =
3163 buttonEventDelete.Enabled =
3164 buttonEventEdit.Enabled =
3165 CurrentEvent() != null;
3167 Ear.Action currentAction = CurrentAction();
3168 //If an action is selected enable the following buttons
3169 buttonActionTest.Enabled =
3170 buttonActionDelete.Enabled =
3171 buttonActionMoveUp.Enabled =
3172 buttonActionMoveDown.Enabled =
3173 buttonActionEdit.Enabled =
3174 currentAction != null;
3176 if (currentAction != null)
3178 //If an action is selected enable move buttons if needed
3179 buttonActionMoveUp.Enabled = iTreeViewEvents.SelectedNode.Index != 0;
3180 buttonActionMoveDown.Enabled = iTreeViewEvents.SelectedNode.Index <
3181 iTreeViewEvents.SelectedNode.Parent.Nodes.Count - 1;
3188 /// <param name="sender"></param>
3189 /// <param name="e"></param>
3190 private void buttonEventAdd_Click(object sender, EventArgs e)
3192 FormEditObject<Ear.Event> ea = new FormEditObject<Ear.Event>();
3193 ea.Text = "Add event";
3194 DialogResult res = CodeProject.Dialog.DlgBox.ShowDialog(ea);
3195 if (res == DialogResult.OK)
3197 Properties.Settings.Default.EarManager.Events.Add(ea.Object);
3198 Properties.Settings.Default.Save();
3199 PopulateTreeViewEvents(ea.Object);
3206 /// <param name="sender"></param>
3207 /// <param name="e"></param>
3208 private void buttonEventDelete_Click(object sender, EventArgs e)
3210 Ear.Event currentEvent = CurrentEvent();
3211 if (currentEvent == null)
3213 //Must select action node
3217 Properties.Settings.Default.EarManager.Events.Remove(currentEvent);
3218 Properties.Settings.Default.Save();
3219 PopulateTreeViewEvents();
3225 /// <param name="sender"></param>
3226 /// <param name="e"></param>
3227 private void buttonEventEdit_Click(object sender, EventArgs e)
3229 Ear.Event selectedEvent = CurrentEvent();
3230 if (selectedEvent == null)
3232 //We did not find a corresponding event
3236 FormEditObject<Ear.Event> ea = new FormEditObject<Ear.Event>();
3237 ea.Text = "Edit event";
3238 ea.Object = selectedEvent;
3239 int index = iTreeViewEvents.SelectedNode.Index;
3240 DialogResult res = CodeProject.Dialog.DlgBox.ShowDialog(ea);
3241 if (res == DialogResult.OK)
3243 //Make sure we keep the same actions as before
3244 ea.Object.Objects = Properties.Settings.Default.EarManager.Events[index].Objects;
3246 Properties.Settings.Default.EarManager.Events[index] = ea.Object;
3247 //Save and rebuild our event tree view
3248 Properties.Settings.Default.Save();
3249 PopulateTreeViewEvents(ea.Object);
3256 /// <param name="sender"></param>
3257 /// <param name="e"></param>
3258 private void iTreeViewEvents_Leave(object sender, EventArgs e)
3260 //Make sure our event tree never looses focus
3261 ((TreeView) sender).Focus();
3265 /// Called whenever we loose connection with our HarmonyHub.
3267 /// <param name="aRequestWasCancelled"></param>
3268 private void HarmonyConnectionClosed(object aSender, bool aClosedByServer)
3270 if (aClosedByServer)
3272 //Try reconnect then
3273 #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
3274 BeginInvoke(new MethodInvoker(delegate () { ResetHarmonyAsync(); }));
3275 #pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
3281 int iHarmonyReconnectTries = 0;
3282 const int KHarmonyMaxReconnectTries = 10;
3287 /// <returns></returns>
3288 private async Task ConnectHarmonyAsync(bool aForceAuth=false)
3290 if (Program.HarmonyClient != null)
3292 await Program.HarmonyClient.CloseAsync();
3295 bool success = false;
3297 //Reset Harmony client & config
3298 Program.HarmonyClient = null;
3299 Program.HarmonyConfig = null;
3300 iTreeViewHarmony.Nodes.Clear();
3302 Trace.WriteLine("Harmony: Connecting... ");
3303 //First create our client and login
3304 //Tip: Set keep-alive to false when testing reconnection process
3305 Program.HarmonyClient = new HarmonyHub.Client(iTextBoxHarmonyHubAddress.Text, true);
3306 Program.HarmonyClient.OnConnectionClosed += HarmonyConnectionClosed;
3308 string authToken = Properties.Settings.Default.LogitechAuthToken;
3309 if (!string.IsNullOrEmpty(authToken) && !aForceAuth)
3311 Trace.WriteLine("Harmony: Reusing token: {0}", authToken);
3312 success = await Program.HarmonyClient.TryOpenAsync(authToken);
3315 if (!Program.HarmonyClient.IsReady || !success
3316 // Only first failure triggers new Harmony server AUTH
3317 // That's to avoid calling upon Logitech servers too often
3318 && iHarmonyReconnectTries == 0 )
3320 //We failed to connect using our token
3322 Trace.WriteLine("Harmony: Reseting authentication token!");
3323 Properties.Settings.Default.LogitechAuthToken = "";
3324 Properties.Settings.Default.Save();
3326 Trace.WriteLine("Harmony: Authenticating with Logitech servers...");
3327 success = await Program.HarmonyClient.TryOpenAsync();
3328 //Persist our authentication token in our setting
3331 Trace.WriteLine("Harmony: Saving authentication token.");
3332 Properties.Settings.Default.LogitechAuthToken = Program.HarmonyClient.Token;
3333 Properties.Settings.Default.Save();
3337 // I've seen this failing with "Policy lookup failed on server".
3338 Program.HarmonyConfig = await Program.HarmonyClient.TryGetConfigAsync();
3339 if (Program.HarmonyConfig == null)
3345 // So we now have our Harmony Configuration
3346 PopulateTreeViewHarmony(Program.HarmonyConfig);
3347 // Make sure harmony command actions are showing device name instead of device id
3348 PopulateTreeViewEvents(CurrentEarObject());
3351 // TODO: Consider putting the retry logic one level higher in ResetHarmonyAsync
3354 // See if we need to keep trying
3355 if (iHarmonyReconnectTries < KHarmonyMaxReconnectTries)
3357 iHarmonyReconnectTries++;
3358 Trace.WriteLine("Harmony: Failed to connect, try again: " + iHarmonyReconnectTries);
3359 await ConnectHarmonyAsync();
3363 Trace.WriteLine("Harmony: Failed to connect, giving up!");
3364 iHarmonyReconnectTries = 0;
3365 // TODO: Could use a data member as timer rather than a new instance.
3366 // Try that again in 5 minutes then.
3367 // Using Windows Form timer to make sure we run in the UI thread.
3368 System.Windows.Forms.Timer timer = new System.Windows.Forms.Timer();
3369 timer.Tick += async delegate (object sender, EventArgs e)
3371 // Stop our timer first as we won't need it anymore
3372 (sender as System.Windows.Forms.Timer).Stop();
3373 // Then try to connect again
3374 await ConnectHarmonyAsync();
3376 timer.Interval = 300000;
3382 // We are connected with a valid Harmony Configuration
3383 // Reset our tries counter then
3384 iHarmonyReconnectTries = 0;
3391 /// <param name="aConfig"></param>
3392 private void PopulateTreeViewHarmony(HarmonyHub.Config aConfig)
3394 iTreeViewHarmony.Nodes.Clear();
3396 foreach (HarmonyHub.Device device in aConfig.Devices)
3398 TreeNode deviceNode = iTreeViewHarmony.Nodes.Add(device.Id, $"{device.Label} ({device.DeviceTypeDisplayName}/{device.Model})");
3399 deviceNode.Tag = device;
3401 foreach (HarmonyHub.ControlGroup cg in device.ControlGroups)
3403 TreeNode cgNode = deviceNode.Nodes.Add(cg.Name);
3406 foreach (HarmonyHub.Function f in cg.Functions)
3408 TreeNode fNode = cgNode.Nodes.Add(f.Label);
3414 //treeViewConfig.ExpandAll();
3417 private async void iTreeViewHarmony_NodeMouseDoubleClick(object sender, TreeNodeMouseClickEventArgs e)
3419 //Upon function node double click we execute it
3420 var tag = e.Node.Tag as HarmonyHub.Function;
3421 if (tag != null && e.Node.Parent.Parent.Tag is HarmonyHub.Device)
3423 HarmonyHub.Function f = tag;
3424 HarmonyHub.Device d = (HarmonyHub.Device)e.Node.Parent.Parent.Tag;
3426 Trace.WriteLine($"Harmony: Sending {f.Label} to {d.Label}...");
3428 await Program.HarmonyClient.TrySendKeyPressAsync(d.Id, f.Action.Command);