Ground work for display layout support.
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);
260 iCurrentClientData = null;
262 //When developing we want at least one client for testing
263 StartNewClient("abcdefghijklmnopqrst-0123456789", "ABCDEFGHIJKLMNOPQRST-0123456789");
266 //Open display connection on start-up if needed
267 if (Properties.Settings.Default.DisplayConnectOnStartup)
269 OpenDisplayConnection();
272 //Start our server so that we can get client requests
275 //Register for HID events
276 RegisterHidDevices();
278 //Start Idle client if needed
279 if (Properties.Settings.Default.StartIdleClient)
286 private void CreateAudioManager()
288 iAudioManager = new AudioManager();
289 iAudioManager.Open(OnDefaultMultiMediaDeviceChanged, OnVolumeNotification);
290 UpdateAudioDeviceAndMasterVolumeThreadSafe();
293 private void DestroyAudioManager()
295 if (iAudioManager != null)
297 iAudioManager.Close();
298 iAudioManager = null;
305 /// <param name="sender"></param>
306 /// <param name="aEvent"></param>
307 public void OnDefaultMultiMediaDeviceChanged(object sender, DefaultDeviceChangedEventArgs aEvent)
309 if (aEvent.DataFlow == DataFlow.Render && aEvent.Role == Role.Multimedia)
311 ResetAudioManagerThreadSafe();
318 private void ResetAudioManagerThreadSafe()
322 //Not in the proper thread, invoke ourselves
323 BeginInvoke(new Action<FormMain>((sender) => { ResetAudioManagerThreadSafe(); }), this);
327 //Proper thread, go ahead
328 DestroyAudioManager();
329 CreateAudioManager();
334 /// Called when our display is opened.
336 /// <param name="aDisplay"></param>
337 private void OnDisplayOpened(Display aDisplay)
339 //Make sure we resume frame rendering
340 iSkipFrameRendering = false;
342 //Set our screen size now that our display is connected
343 //Our panelDisplay is the container of our tableLayoutPanel
344 //tableLayoutPanel will resize itself to fit the client size of our panelDisplay
345 //panelDisplay needs an extra 2 pixels for borders on each sides
346 //tableLayoutPanel will eventually be the exact size of our display
347 Size size = new Size(iDisplay.WidthInPixels() + 2, iDisplay.HeightInPixels() + 2);
348 iPanelDisplay.Size = size;
350 //Our display was just opened, update our UI
352 //Initiate asynchronous request
353 iDisplay.RequestFirmwareRevision();
356 UpdateMasterVolumeThreadSafe();
358 UpdateNetworkStatus();
361 //Testing icon in debug, no arm done if icon not supported
362 //iDisplay.SetIconStatus(Display.TMiniDisplayIconType.EMiniDisplayIconRecording, 0, 1);
363 //iDisplay.SetAllIconsStatus(2);
369 private static void AddActionsToTreeNode(TreeNode aParentNode, Ear.Object aObject)
371 foreach (Ear.Action a in aObject.Objects.OfType<Ear.Action>())
374 TreeNode actionNode = aParentNode.Nodes.Add(a.Brief());
376 //Use color from parent unless our action itself is disabled
377 actionNode.ForeColor = a.Enabled ? aParentNode.ForeColor : Color.DimGray;
379 AddActionsToTreeNode(actionNode,a);
387 /// <param name="aObject"></param>
388 /// <param name="aNode"></param>
389 private static TreeNode FindTreeNodeForEarObject(Ear.Object aObject, TreeNode aNode)
391 if (aNode.Tag == aObject)
396 foreach (TreeNode n in aNode.Nodes)
398 TreeNode found = FindTreeNodeForEarObject(aObject,n);
412 /// <param name="aObject"></param>
413 private void SelectEarObject(Ear.Object aObject)
415 foreach (TreeNode n in iTreeViewEvents.Nodes)
417 TreeNode found = FindTreeNodeForEarObject(aObject, n);
420 iTreeViewEvents.SelectedNode=found;
421 iTreeViewEvents.Focus();
428 /// Populate tree view with events and actions
430 private void PopulateTreeViewEvents(Ear.Object aSelectedObject=null)
433 iTreeViewEvents.Nodes.Clear();
434 //Populate registered events
435 foreach (Ear.Event e in Properties.Settings.Default.EarManager.Events)
437 //Create our event node
438 //Work out the name of our node
439 string eventNodeName = "";
440 if (!string.IsNullOrEmpty(e.Name))
442 //That event has a proper name, use it then
443 eventNodeName = $"{e.Name} - {e.Brief()}";
447 //Unnamed events just use brief
448 eventNodeName = e.Brief();
451 TreeNode eventNode = iTreeViewEvents.Nodes.Add(eventNodeName);
452 eventNode.Tag = e; //For easy access to our event
455 //Dim our nodes if disabled
456 eventNode.ForeColor = Color.DimGray;
459 //Add event description as child node
460 eventNode.Nodes.Add(e.AttributeDescription).ForeColor = eventNode.ForeColor;
461 //Create child node for actions root
462 TreeNode actionsNode = eventNode.Nodes.Add("Actions");
463 actionsNode.ForeColor = eventNode.ForeColor;
465 // Recursively add our actions for that event
466 AddActionsToTreeNode(actionsNode,e);
469 iTreeViewEvents.ExpandAll();
471 if (aSelectedObject != null)
473 SelectEarObject(aSelectedObject);
476 // Just to be safe in case the selection did not work
481 /// Called when our display is closed.
483 /// <param name="aDisplay"></param>
484 private void OnDisplayClosed(Display aDisplay)
486 //Our display was just closed, update our UI consequently
490 public void OnConnectivityChanged(NetworkManager aNetwork, NLM_CONNECTIVITY newConnectivity)
492 //Update network status
493 UpdateNetworkStatus();
497 /// Update our Network Status
499 private void UpdateNetworkStatus()
501 if (iDisplay.IsOpen())
503 iDisplay.SetIconOnOff(MiniDisplay.IconType.Internet,
504 iNetworkManager.NetworkListManager.IsConnectedToInternet);
505 iDisplay.SetIconOnOff(MiniDisplay.IconType.NetworkSignal, iNetworkManager.NetworkListManager.IsConnected);
510 int iLastNetworkIconIndex = 0;
511 int iUpdateCountSinceLastNetworkAnimation = 0;
516 private void UpdateNetworkSignal(DateTime aLastTickTime, DateTime aNewTickTime)
518 iUpdateCountSinceLastNetworkAnimation++;
519 iUpdateCountSinceLastNetworkAnimation = iUpdateCountSinceLastNetworkAnimation%4;
521 if (iDisplay.IsOpen() && iNetworkManager.NetworkListManager.IsConnected &&
522 iUpdateCountSinceLastNetworkAnimation == 0)
524 int iconCount = iDisplay.IconCount(MiniDisplay.IconType.NetworkSignal);
527 //Prevents div by zero and other undefined behavior
530 iLastNetworkIconIndex++;
531 iLastNetworkIconIndex = iLastNetworkIconIndex%(iconCount*2);
532 for (int i = 0; i < iconCount; i++)
534 if (i < iLastNetworkIconIndex && !(i == 0 && iLastNetworkIconIndex > 3) &&
535 !(i == 1 && iLastNetworkIconIndex > 4))
537 iDisplay.SetIconOn(MiniDisplay.IconType.NetworkSignal, i);
541 iDisplay.SetIconOff(MiniDisplay.IconType.NetworkSignal, i);
550 /// Receive volume change notification and reflect changes on our slider.
552 /// <param name="data"></param>
553 public void OnVolumeNotification(object sender, AudioEndpointVolumeCallbackEventArgs aEvent)
555 UpdateMasterVolumeThreadSafe();
559 /// Update master volume when user moves our slider.
561 /// <param name="sender"></param>
562 /// <param name="e"></param>
563 private void trackBarMasterVolume_Scroll(object sender, EventArgs e)
565 //Just like Windows Volume Mixer we unmute if the volume is adjusted
566 iAudioManager.Volume.IsMuted = false;
567 //Set volume level according to our volume slider new position
568 iAudioManager.Volume.MasterVolumeLevelScalar = trackBarMasterVolume.Value/100.0f;
573 /// Mute check box changed.
575 /// <param name="sender"></param>
576 /// <param name="e"></param>
577 private void checkBoxMute_CheckedChanged(object sender, EventArgs e)
579 iAudioManager.Volume.IsMuted = checkBoxMute.Checked;
584 /// Update master volume indicators based our current system states.
585 /// This typically includes volume levels and mute status.
587 private void UpdateMasterVolumeThreadSafe()
591 //Not in the proper thread, invoke ourselves
592 BeginInvoke(new Action<FormMain>((sender) => { UpdateMasterVolumeThreadSafe(); }), this);
596 //Update volume slider
597 float volumeLevelScalar = iAudioManager.Volume.MasterVolumeLevelScalar;
598 trackBarMasterVolume.Value = Convert.ToInt32(volumeLevelScalar*100);
599 //Update mute checkbox
600 checkBoxMute.Checked = iAudioManager.Volume.IsMuted;
602 //If our display connection is open we need to update its icons
603 if (iDisplay.IsOpen())
605 //First take care our our volume level icons
606 int volumeIconCount = iDisplay.IconCount(MiniDisplay.IconType.Volume);
607 if (volumeIconCount > 0)
609 //Compute current volume level from system level and the number of segments in our display volume bar.
610 //That tells us how many segments in our volume bar needs to be turned on.
611 float currentVolume = volumeLevelScalar*volumeIconCount;
612 int segmentOnCount = Convert.ToInt32(currentVolume);
613 //Check if our segment count was rounded up, this will later be used for half brightness segment
614 bool roundedUp = segmentOnCount > currentVolume;
616 for (int i = 0; i < volumeIconCount; i++)
618 if (i < segmentOnCount)
620 //If we are dealing with our last segment and our segment count was rounded up then we will use half brightness.
621 if (i == segmentOnCount - 1 && roundedUp)
624 iDisplay.SetIconStatus(MiniDisplay.IconType.Volume, i,
625 (iDisplay.IconStatusCount(MiniDisplay.IconType.Volume) - 1)/2);
630 iDisplay.SetIconStatus(MiniDisplay.IconType.Volume, i,
631 iDisplay.IconStatusCount(MiniDisplay.IconType.Volume) - 1);
636 iDisplay.SetIconStatus(MiniDisplay.IconType.Volume, i, 0);
641 //Take care of our mute icon
642 iDisplay.SetIconOnOff(MiniDisplay.IconType.Mute, iAudioManager.Volume.IsMuted);
652 private void UpdateAudioVisualization()
654 // No point if we don't have a current client
655 if (iCurrentClientData == null)
661 if (iAudioManager==null || iAudioManager.Spectrum==null || !iAudioManager.Spectrum.Update())
663 //Nothing changed no need to render
667 // Check if our current client has an Audio Visualizer field
668 // and render them as needed
669 foreach (DataField f in iCurrentClientData.View.Fields)
671 if (f is AudioVisualizerField)
673 AudioVisualizerField avf = (AudioVisualizerField)f;
674 Control ctrl = iTableLayoutPanelCurrentClient.GetControlFromPosition(avf.Column, avf.Row);
676 if (ctrl is PictureBox)
678 PictureBox pb = (PictureBox)ctrl;
679 if (iAudioManager.Spectrum.Render(pb.Image, Color.Black, Color.Black, Color.White, false))
692 private void UpdateAudioDeviceAndMasterVolumeThreadSafe()
696 //Not in the proper thread, invoke ourselves
697 BeginInvoke(new Action<FormMain>((sender) => { UpdateAudioDeviceAndMasterVolumeThreadSafe(); }), this);
701 //We are in the correct thread just go ahead.
706 iLabelDefaultAudioDevice.Text = iAudioManager.DefaultDevice.FriendlyName;
708 //Show our volume in our track bar
709 UpdateMasterVolumeThreadSafe();
712 trackBarMasterVolume.Enabled = true;
716 Debug.WriteLine("Exception thrown in UpdateAudioDeviceAndMasterVolume");
717 Debug.WriteLine(ex.ToString());
718 //Something went wrong S/PDIF device ca throw exception I guess
719 trackBarMasterVolume.Enabled = false;
726 private void PopulateDeviceTypes()
728 int count = Display.TypeCount();
730 for (int i = 0; i < count; i++)
732 comboBoxDisplayType.Items.Add(Display.TypeName((MiniDisplay.Type) i));
739 private void SetupTrayIcon()
741 iNotifyIcon.Icon = GetIcon("vfd.ico");
742 iNotifyIcon.Text = "Sharp Display Manager";
743 iNotifyIcon.Visible = true;
745 //Double click toggles visibility - typically brings up the application
746 iNotifyIcon.DoubleClick += delegate(object obj, EventArgs args)
751 //Adding a context menu, useful to be able to exit the application
752 ContextMenu contextMenu = new ContextMenu();
753 //Context menu item to toggle visibility
754 MenuItem hideShowItem = new MenuItem("Hide/Show");
755 hideShowItem.Click += delegate(object obj, EventArgs args)
759 contextMenu.MenuItems.Add(hideShowItem);
761 //Context menu item separator
762 contextMenu.MenuItems.Add(new MenuItem("-"));
764 //Context menu exit item
765 MenuItem exitItem = new MenuItem("Exit");
766 exitItem.Click += delegate(object obj, EventArgs args)
770 contextMenu.MenuItems.Add(exitItem);
772 iNotifyIcon.ContextMenu = contextMenu;
778 private void SetupRecordingNotification()
780 iRecordingNotification.Icon = GetIcon("record.ico");
781 iRecordingNotification.Text = "No recording";
782 iRecordingNotification.Visible = false;
786 /// Access icons from embedded resources.
788 /// <param name="aName"></param>
789 /// <returns></returns>
790 public static Icon GetIcon(string aName)
792 string[] names = Assembly.GetExecutingAssembly().GetManifestResourceNames();
793 foreach (string name in names)
795 //Find a resource name that ends with the given name
796 if (name.EndsWith(aName))
798 using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(name))
800 return new Icon(stream);
810 /// Set our current client.
811 /// This will take care of applying our client layout and set data fields.
813 /// <param name="aSessionId"></param>
814 void SetCurrentClient(string aSessionId, bool aForce = false)
816 if (aSessionId == iCurrentClientSessionId)
818 //Given client is already the current one.
819 //Don't bother changing anything then.
823 ClientData requestedClientData = iClients[aSessionId];
825 //Check when was the last time we switched to that client
826 if (iCurrentClientData != null)
828 //Do not switch client if priority of current client is higher
829 if (!aForce && requestedClientData.Priority < iCurrentClientData.Priority)
835 double lastSwitchToClientSecondsAgo = (DateTime.Now - iCurrentClientData.LastSwitchTime).TotalSeconds;
836 //TODO: put that hard coded value as a client property
837 //Clients should be able to define how often they can be interrupted
838 //Thus a background client can set this to zero allowing any other client to interrupt at any time
839 //We could also compute this delay by looking at the requests frequencies?
841 requestedClientData.Priority == iCurrentClientData.Priority &&
842 //Time sharing is only if clients have the same priority
843 (lastSwitchToClientSecondsAgo < 30)) //Make sure a client is on for at least 30 seconds
845 //Don't switch clients too often
850 //Set current client ID.
851 iCurrentClientSessionId = aSessionId;
852 //Set the time we last switched to that client
853 iClients[aSessionId].LastSwitchTime = DateTime.Now;
854 //Fetch and set current client data.
855 iCurrentClientData = requestedClientData;
856 //Apply layout and set data fields.
857 UpdateTableLayoutPanel(iCurrentClientData.View, iTableLayoutPanelCurrentClient);
860 private void buttonFont_Click(object sender, EventArgs e)
862 //fontDialog.ShowColor = true;
863 //fontDialog.ShowApply = true;
864 fontDialog.ShowEffects = true;
865 fontDialog.Font = cds.Font;
867 fontDialog.FixedPitchOnly = checkBoxFixedPitchFontOnly.Checked;
869 //fontDialog.ShowHelp = true;
871 //fontDlg.MaxSize = 40;
872 //fontDlg.MinSize = 22;
874 //fontDialog.Parent = this;
875 //fontDialog.StartPosition = FormStartPosition.CenterParent;
877 //DlgBox.ShowDialog(fontDialog);
879 //if (fontDialog.ShowDialog(this) != DialogResult.Cancel)
880 if (DlgBox.ShowDialog(fontDialog) != DialogResult.Cancel)
883 cds.Font = fontDialog.Font;
884 Properties.Settings.Default.Save();
886 //Set the fonts to all our labels in our layout
887 UpdateFonts(iTableLayoutPanelDisplay);
894 /// TODO: review this in respect to our logical font feature when we get there.
896 void CheckFontHeight()
898 //Show font height and width
899 labelFontHeight.Text = "Font height: " + cds.Font.Height;
900 float charWidth = IsFixedWidth(cds.Font);
901 if (charWidth == 0.0f)
903 labelFontWidth.Visible = false;
907 labelFontWidth.Visible = true;
908 labelFontWidth.Text = "Font width: " + charWidth;
911 MarqueeLabel label = null;
912 //Get the first label control we can find
913 foreach (Control ctrl in iTableLayoutPanelCurrentClient.Controls)
915 if (ctrl is MarqueeLabel)
917 label = (MarqueeLabel) ctrl;
922 //Now check font height and show a warning if needed.
923 if (label != null && label.Font.Height > label.Height)
925 labelWarning.Text = "WARNING: Selected font is too height by " + (label.Font.Height - label.Height) +
927 labelWarning.Visible = true;
931 labelWarning.Visible = false;
936 private void buttonCapture_Click(object sender, EventArgs e)
938 System.Drawing.Bitmap bmp = new System.Drawing.Bitmap(iTableLayoutPanelDisplay.Width, iTableLayoutPanelDisplay.Height);
939 iTableLayoutPanelDisplay.DrawToBitmap(bmp, iTableLayoutPanelDisplay.ClientRectangle);
940 //Bitmap bmpToSave = new Bitmap(bmp);
941 bmp.Save("D:\\capture.png");
944 string outputFileName = "d:\\capture.png";
945 using (MemoryStream memory = new MemoryStream())
947 using (FileStream fs = new FileStream(outputFileName, FileMode.OpenOrCreate, FileAccess.ReadWrite))
949 bmp.Save(memory, System.Drawing.Imaging.ImageFormat.Png);
950 byte[] bytes = memory.ToArray();
951 fs.Write(bytes, 0, bytes.Length);
958 private void CheckForRequestResults()
960 if (iDisplay.IsRequestPending())
962 switch (iDisplay.AttemptRequestCompletion())
964 case MiniDisplay.Request.FirmwareRevision:
965 toolStripStatusLabelConnect.Text += " v" + iDisplay.FirmwareRevision();
966 //Issue next request then
967 iDisplay.RequestPowerSupplyStatus();
970 case MiniDisplay.Request.PowerSupplyStatus:
971 if (iDisplay.PowerSupplyStatus())
973 toolStripStatusLabelPower.Text = "ON";
977 toolStripStatusLabelPower.Text = "OFF";
979 //Issue next request then
980 iDisplay.RequestDeviceId();
983 case MiniDisplay.Request.DeviceId:
984 toolStripStatusLabelConnect.Text += " - " + iDisplay.DeviceId();
985 //No more request to issue
991 public static uint ColorWhiteIsOn(int aX, int aY, uint aPixel)
993 if ((aPixel & 0x00FFFFFF) == 0x00FFFFFF)
1000 public static uint ColorUntouched(int aX, int aY, uint aPixel)
1005 public static uint ColorInversed(int aX, int aY, uint aPixel)
1010 public static uint ColorChessboard(int aX, int aY, uint aPixel)
1012 if ((aX%2 == 0) && (aY%2 == 0))
1016 else if ((aX%2 != 0) && (aY%2 != 0))
1024 public static int ScreenReversedX(System.Drawing.Bitmap aBmp, int aX)
1026 return aBmp.Width - aX - 1;
1029 public int ScreenReversedY(System.Drawing.Bitmap aBmp, int aY)
1031 return iBmp.Height - aY - 1;
1034 public int ScreenX(System.Drawing.Bitmap aBmp, int aX)
1039 public int ScreenY(System.Drawing.Bitmap aBmp, int aY)
1045 /// Select proper pixel delegates according to our current settings.
1047 private void SetupPixelDelegates()
1049 //Select our pixel processing routine
1050 if (cds.InverseColors)
1052 //iColorFx = ColorChessboard;
1053 iColorFx = ColorInversed;
1057 iColorFx = ColorWhiteIsOn;
1060 //Select proper coordinate translation functions
1061 //We used delegate/function pointer to support reverse screen without doing an extra test on each pixels
1062 if (cds.ReverseScreen)
1064 iScreenX = ScreenReversedX;
1065 iScreenY = ScreenReversedY;
1076 /// This is our timer tick responsible to perform our render
1077 /// TODO: Use a threading timer instead of a Windows form timer.
1079 /// <param name="sender"></param>
1080 /// <param name="e"></param>
1081 private void timer_Tick(object sender, EventArgs e)
1083 //Update our animations
1084 DateTime NewTickTime = DateTime.Now;
1086 UpdateNetworkSignal(LastTickTime, NewTickTime);
1088 // Update animation for all our marquees
1089 UpdateMarqueesAnimations(iTableLayoutPanelDisplay, LastTickTime, NewTickTime);
1091 // Update audio visualization
1092 UpdateAudioVisualization();
1094 //Update our display
1095 if (iDisplay.IsOpen())
1097 CheckForRequestResults();
1099 //Check if frame rendering is needed
1100 //Typically used when showing clock
1101 if (!iSkipFrameRendering)
1106 iBmp = new System.Drawing.Bitmap(iTableLayoutPanelDisplay.Width, iTableLayoutPanelDisplay.Height,
1107 PixelFormat.Format32bppArgb);
1108 iCreateBitmap = false;
1110 iTableLayoutPanelDisplay.DrawToBitmap(iBmp, iTableLayoutPanelDisplay.ClientRectangle);
1111 //iBmp.Save("D:\\capture.png");
1113 //Send it to our display
1114 for (int i = 0; i < iBmp.Width; i++)
1116 for (int j = 0; j < iBmp.Height; j++)
1120 //Get our processed pixel coordinates
1121 int x = iScreenX(iBmp, i);
1122 int y = iScreenY(iBmp, j);
1124 uint color = (uint) iBmp.GetPixel(i, j).ToArgb();
1125 //Apply color effects
1126 color = iColorFx(x, y, color);
1128 iDisplay.SetPixel(x, y, color);
1133 iDisplay.SwapBuffers();
1137 //Compute instant FPS
1138 toolStripStatusLabelFps.Text = (1.0/NewTickTime.Subtract(LastTickTime).TotalSeconds).ToString("F0") + " / " +
1139 (1000/iTimerDisplay.Interval).ToString() + " FPS";
1141 LastTickTime = NewTickTime;
1146 /// Update marquee animation children of the given control.
1148 static private void UpdateMarqueesAnimations(Control aControl, DateTime aLastTickTime, DateTime aNewTickTime)
1150 // For each our children
1151 foreach (Control ctrl in aControl.Controls)
1153 // Update animation if it is a marquee control
1154 if (ctrl is MarqueeLabel)
1156 ((MarqueeLabel)ctrl).UpdateAnimation(aLastTickTime, aNewTickTime);
1159 // Go one level deeper by recursion
1160 UpdateMarqueesAnimations(ctrl, aLastTickTime, aNewTickTime);
1165 /// Attempt to establish connection with our display hardware.
1167 private void OpenDisplayConnection()
1169 CloseDisplayConnection();
1171 if (!iDisplay.Open((MiniDisplay.Type) cds.DisplayType))
1174 toolStripStatusLabelConnect.Text = "Connection error";
1178 private void CloseDisplayConnection()
1180 //Status will be updated upon receiving the closed event
1182 if (iDisplay == null || !iDisplay.IsOpen())
1187 //Do not clear if we gave up on rendering already.
1188 //This means we will keep on displaying clock on MDM166AA for instance.
1189 if (!iSkipFrameRendering)
1192 iDisplay.SwapBuffers();
1195 iDisplay.SetAllIconsStatus(0); //Turn off all icons
1199 private void buttonOpen_Click(object sender, EventArgs e)
1201 OpenDisplayConnection();
1204 private void buttonClose_Click(object sender, EventArgs e)
1206 CloseDisplayConnection();
1209 private void buttonClear_Click(object sender, EventArgs e)
1212 iDisplay.SwapBuffers();
1215 private void buttonFill_Click(object sender, EventArgs e)
1218 iDisplay.SwapBuffers();
1221 private void trackBarBrightness_Scroll(object sender, EventArgs e)
1223 cds.Brightness = trackBarBrightness.Value;
1224 Properties.Settings.Default.Save();
1225 iDisplay.SetBrightness(trackBarBrightness.Value);
1231 /// CDS stands for Current Display Settings
1233 private DisplaySettings cds
1237 DisplaysSettings settings = Properties.Settings.Default.DisplaysSettings;
1238 if (settings == null)
1240 settings = new DisplaysSettings();
1242 Properties.Settings.Default.DisplaysSettings = settings;
1245 //Make sure all our settings have been created
1246 while (settings.Displays.Count <= Properties.Settings.Default.CurrentDisplayIndex)
1248 settings.Displays.Add(new DisplaySettings());
1251 DisplaySettings displaySettings = settings.Displays[Properties.Settings.Default.CurrentDisplayIndex];
1252 return displaySettings;
1257 /// Check if the given font has a fixed character pitch.
1259 /// <param name="ft"></param>
1260 /// <returns>0.0f if this is not a monospace font, otherwise returns the character width.</returns>
1261 public float IsFixedWidth(Font ft)
1263 Graphics g = CreateGraphics();
1264 char[] charSizes = new char[] {'i', 'a', 'Z', '%', '#', 'a', 'B', 'l', 'm', ',', '.'};
1265 float charWidth = g.MeasureString("I", ft, Int32.MaxValue, StringFormat.GenericTypographic).Width;
1267 bool fixedWidth = true;
1269 foreach (char c in charSizes)
1270 if (g.MeasureString(c.ToString(), ft, Int32.MaxValue, StringFormat.GenericTypographic).Width !=
1283 /// Update fonts in our control tree.
1285 /// <param name="aControls"></param>
1286 private void UpdateFonts(Control aControl)
1288 foreach (Control ctrl in aControl.Controls)
1290 if (ctrl is MarqueeLabel)
1292 MarqueeLabel marquee = (MarqueeLabel)ctrl;
1293 marquee.Font = cds.Font;
1302 /// Update marquees separator in our control tree.
1304 /// <param name="aControls"></param>
1305 private void UpdateMarqueesSeparator(Control aControl)
1307 foreach (Control ctrl in aControl.Controls)
1309 if (ctrl is MarqueeLabel)
1311 MarqueeLabel marquee = (MarqueeLabel)ctrl;
1312 marquee.Separator = cds.Separator;
1316 UpdateMarqueesSeparator(ctrl);
1321 /// Synchronize UI with settings
1323 private void UpdateStatus()
1326 checkBoxShowBorders.Checked = cds.ShowBorders;
1327 iTableLayoutPanelCurrentClient.CellBorderStyle = (cds.ShowBorders
1328 ? TableLayoutPanelCellBorderStyle.Single
1329 : TableLayoutPanelCellBorderStyle.None);
1331 //Set the proper font to each of our labels
1332 UpdateFonts(iTableLayoutPanelDisplay);
1335 //Check if "run on Windows startup" is enabled
1336 checkBoxAutoStart.Checked = iStartupManager.Startup;
1339 comboBoxHdmiPort.SelectedIndex = Properties.Settings.Default.CecHdmiPort - 1;
1341 //Mini Display settings
1342 checkBoxReverseScreen.Checked = cds.ReverseScreen;
1343 checkBoxInverseColors.Checked = cds.InverseColors;
1344 checkBoxShowVolumeLabel.Checked = cds.ShowVolumeLabel;
1345 checkBoxScaleToFit.Checked = cds.ScaleToFit;
1346 maskedTextBoxMinFontSize.Enabled = cds.ScaleToFit;
1347 labelMinFontSize.Enabled = cds.ScaleToFit;
1348 maskedTextBoxMinFontSize.Text = cds.MinFontSize.ToString();
1349 maskedTextBoxScrollingSpeed.Text = cds.ScrollingSpeedInPixelsPerSecond.ToString();
1350 comboBoxDisplayType.SelectedIndex = cds.DisplayType;
1351 iTimerDisplay.Interval = cds.TimerInterval;
1352 maskedTextBoxTimerInterval.Text = cds.TimerInterval.ToString();
1353 textBoxScrollLoopSeparator.Text = cds.Separator;
1355 SetupPixelDelegates();
1357 if (iDisplay.IsOpen())
1359 //We have a display connection
1360 //Reflect that in our UI
1363 iTableLayoutPanelDisplay.Enabled = true;
1364 iTableLayoutPanelCurrentClient.Enabled = true;
1365 iPanelDisplay.Enabled = true;
1367 //Only setup brightness if display is open
1368 trackBarBrightness.Minimum = iDisplay.MinBrightness();
1369 trackBarBrightness.Maximum = iDisplay.MaxBrightness();
1370 if (cds.Brightness < iDisplay.MinBrightness() || cds.Brightness > iDisplay.MaxBrightness())
1372 //Brightness out of range, this can occur when using auto-detect
1373 //Use max brightness instead
1374 trackBarBrightness.Value = iDisplay.MaxBrightness();
1375 iDisplay.SetBrightness(iDisplay.MaxBrightness());
1379 trackBarBrightness.Value = cds.Brightness;
1380 iDisplay.SetBrightness(cds.Brightness);
1383 //Try compute the steps to something that makes sense
1384 trackBarBrightness.LargeChange = Math.Max(1, (iDisplay.MaxBrightness() - iDisplay.MinBrightness())/5);
1385 trackBarBrightness.SmallChange = 1;
1388 buttonFill.Enabled = true;
1389 buttonClear.Enabled = true;
1390 buttonOpen.Enabled = false;
1391 buttonClose.Enabled = true;
1392 trackBarBrightness.Enabled = true;
1393 toolStripStatusLabelConnect.Text = "Connected - " + iDisplay.Vendor() + " - " + iDisplay.Product();
1394 //+ " - " + iDisplay.SerialNumber();
1396 if (iDisplay.SupportPowerOnOff())
1398 buttonPowerOn.Enabled = true;
1399 buttonPowerOff.Enabled = true;
1403 buttonPowerOn.Enabled = false;
1404 buttonPowerOff.Enabled = false;
1407 if (iDisplay.SupportClock())
1409 buttonShowClock.Enabled = true;
1410 buttonHideClock.Enabled = true;
1414 buttonShowClock.Enabled = false;
1415 buttonHideClock.Enabled = false;
1419 //Check if Volume Label is supported. To date only MDM166AA supports that crap :)
1420 checkBoxShowVolumeLabel.Enabled = iDisplay.IconCount(MiniDisplay.IconType.VolumeLabel) > 0;
1422 if (cds.ShowVolumeLabel)
1424 iDisplay.SetIconOn(MiniDisplay.IconType.VolumeLabel);
1428 iDisplay.SetIconOff(MiniDisplay.IconType.VolumeLabel);
1433 //Display connection not available
1434 //Reflect that in our UI
1436 //In debug start our timer even if we don't have a display connection
1439 //In production environment we don't need our timer if no display connection
1442 checkBoxShowVolumeLabel.Enabled = false;
1443 iTableLayoutPanelDisplay.Enabled = false;
1444 iTableLayoutPanelCurrentClient.Enabled = false;
1445 iPanelDisplay.Enabled = false;
1446 buttonFill.Enabled = false;
1447 buttonClear.Enabled = false;
1448 buttonOpen.Enabled = true;
1449 buttonClose.Enabled = false;
1450 trackBarBrightness.Enabled = false;
1451 buttonPowerOn.Enabled = false;
1452 buttonPowerOff.Enabled = false;
1453 buttonShowClock.Enabled = false;
1454 buttonHideClock.Enabled = false;
1455 toolStripStatusLabelConnect.Text = "Disconnected";
1456 toolStripStatusLabelPower.Text = "N/A";
1465 /// <param name="sender"></param>
1466 /// <param name="e"></param>
1467 private void checkBoxShowVolumeLabel_CheckedChanged(object sender, EventArgs e)
1469 cds.ShowVolumeLabel = checkBoxShowVolumeLabel.Checked;
1470 Properties.Settings.Default.Save();
1474 private void checkBoxShowBorders_CheckedChanged(object sender, EventArgs e)
1476 //Save our show borders setting
1477 iTableLayoutPanelCurrentClient.CellBorderStyle = (checkBoxShowBorders.Checked
1478 ? TableLayoutPanelCellBorderStyle.Single
1479 : TableLayoutPanelCellBorderStyle.None);
1480 cds.ShowBorders = checkBoxShowBorders.Checked;
1481 Properties.Settings.Default.Save();
1485 private void checkBoxAutoStart_CheckedChanged(object sender, EventArgs e)
1487 iStartupManager.Startup = checkBoxAutoStart.Checked;
1491 private void checkBoxReverseScreen_CheckedChanged(object sender, EventArgs e)
1493 //Save our reverse screen setting
1494 cds.ReverseScreen = checkBoxReverseScreen.Checked;
1495 Properties.Settings.Default.Save();
1496 SetupPixelDelegates();
1499 private void checkBoxInverseColors_CheckedChanged(object sender, EventArgs e)
1501 //Save our inverse colors setting
1502 cds.InverseColors = checkBoxInverseColors.Checked;
1503 Properties.Settings.Default.Save();
1504 SetupPixelDelegates();
1507 private void checkBoxScaleToFit_CheckedChanged(object sender, EventArgs e)
1509 //Save our scale to fit setting
1510 cds.ScaleToFit = checkBoxScaleToFit.Checked;
1511 Properties.Settings.Default.Save();
1513 labelMinFontSize.Enabled = cds.ScaleToFit;
1514 maskedTextBoxMinFontSize.Enabled = cds.ScaleToFit;
1517 private void MainForm_Resize(object sender, EventArgs e)
1519 if (WindowState == FormWindowState.Minimized)
1521 // To workaround our empty bitmap bug on Windows 7 we need to recreate our bitmap when the application is minimized
1522 // That's apparently not needed on Windows 10 but we better leave it in place.
1523 iCreateBitmap = true;
1527 private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
1530 iNetworkManager.Dispose();
1531 DestroyAudioManager();
1532 CloseDisplayConnection();
1534 e.Cancel = iClosing;
1537 public void StartServer()
1539 iServiceHost = new ServiceHost
1542 new Uri[] {new Uri("net.tcp://localhost:8001/")}
1545 iServiceHost.AddServiceEndpoint(typeof(IService), new NetTcpBinding(SecurityMode.None, true),
1547 iServiceHost.Open();
1550 public void StopServer()
1552 if (iClients.Count > 0 && !iClosing)
1556 BroadcastCloseEvent();
1561 MessageBox.Show("Force exit?", "Waiting for clients...", MessageBoxButtons.YesNo,
1562 MessageBoxIcon.Warning) == DialogResult.Yes)
1564 iClosing = false; //We make sure we force close if asked twice
1569 //We removed that as it often lags for some reason
1570 //iServiceHost.Close();
1577 public void BroadcastCloseEvent()
1579 Trace.TraceInformation("BroadcastCloseEvent - start");
1581 var inactiveClients = new List<string>();
1582 foreach (var client in iClients)
1584 //if (client.Key != eventData.ClientName)
1588 Trace.TraceInformation("BroadcastCloseEvent - " + client.Key);
1589 client.Value.Callback.OnCloseOrder( /*eventData*/);
1591 catch (Exception ex)
1593 inactiveClients.Add(client.Key);
1598 if (inactiveClients.Count > 0)
1600 foreach (var client in inactiveClients)
1602 iClients.Remove(client);
1603 Program.iFormMain.iTreeViewClients.Nodes.Remove(
1604 Program.iFormMain.iTreeViewClients.Nodes.Find(client, false)[0]);
1608 if (iClients.Count == 0)
1610 ClearLayout(iTableLayoutPanelCurrentClient);
1611 iCurrentClientData = null;
1616 /// Just remove all our fields.
1618 static private void ClearLayout(TableLayoutPanel aPanel)
1620 // For each loop did not work as calling Dispose on a control removes it from the collection.
1621 // We make sure every control are disposed of notably to turn off visualizer when no more needed.
1622 // That's the only way we found to make sure Control.Disposed is called in a timely fashion.
1623 // 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.
1624 // That's what happened with our MarqueeLabel until we fixed it's Dispose override.
1625 while (aPanel.Controls.Count>0)
1627 // Dispose our last item
1628 aPanel.Controls[aPanel.Controls.Count-1].Dispose();
1631 aPanel.Controls.Clear();
1632 aPanel.RowStyles.Clear();
1633 aPanel.ColumnStyles.Clear();
1634 aPanel.RowCount = 0;
1635 aPanel.ColumnCount = 0;
1639 /// Just launch a demo client.
1641 private void StartNewClient(string aTopText = "", string aBottomText = "")
1643 Thread clientThread = new Thread(SharpDisplayClient.Program.MainWithParams);
1644 SharpDisplayClient.StartParams myParams = new SharpDisplayClient.StartParams(
1645 new Point(this.Right, this.Top), aTopText, aBottomText);
1646 clientThread.Start(myParams);
1651 /// Just launch our idle client.
1653 private void StartIdleClient(string aTopText = "", string aBottomText = "")
1655 Thread clientThread = new Thread(SharpDisplayClientIdle.Program.MainWithParams);
1656 SharpDisplayClientIdle.StartParams myParams =
1657 new SharpDisplayClientIdle.StartParams(new Point(this.Right, this.Top), aTopText, aBottomText);
1658 clientThread.Start(myParams);
1663 private void buttonStartClient_Click(object sender, EventArgs e)
1668 private void buttonSuspend_Click(object sender, EventArgs e)
1673 private void StartTimer()
1675 LastTickTime = DateTime.Now; //Reset timer to prevent jump
1676 iTimerDisplay.Enabled = true;
1677 UpdateSuspendButton();
1680 private void StopTimer()
1682 LastTickTime = DateTime.Now; //Reset timer to prevent jump
1683 iTimerDisplay.Enabled = false;
1684 UpdateSuspendButton();
1687 private void ToggleTimer()
1689 LastTickTime = DateTime.Now; //Reset timer to prevent jump
1690 iTimerDisplay.Enabled = !iTimerDisplay.Enabled;
1691 UpdateSuspendButton();
1694 private void UpdateSuspendButton()
1696 if (!iTimerDisplay.Enabled)
1698 buttonSuspend.Text = "Run";
1702 buttonSuspend.Text = "Pause";
1707 private void buttonCloseClients_Click(object sender, EventArgs e)
1709 BroadcastCloseEvent();
1712 private void treeViewClients_AfterSelect(object sender, TreeViewEventArgs e)
1714 //Root node must have at least one child
1715 if (e.Node.Nodes.Count == 0)
1720 //If the selected node is the root node of a client then switch to it
1721 string sessionId = e.Node.Nodes[0].Text; //First child of a root node is the sessionId
1722 if (iClients.ContainsKey(sessionId)) //Check that's actually what we are looking at
1724 //We have a valid session just switch to that client
1725 SetCurrentClient(sessionId, true);
1734 /// <param name="aSessionId"></param>
1735 /// <param name="aCallback"></param>
1736 public void AddClientThreadSafe(string aSessionId, ICallback aCallback)
1738 if (this.InvokeRequired)
1740 //Not in the proper thread, invoke ourselves
1741 AddClientDelegate d = new AddClientDelegate(AddClientThreadSafe);
1742 this.Invoke(d, new object[] {aSessionId, aCallback});
1746 //We are in the proper thread
1747 //Add this session to our collection of clients
1748 ClientData newClient = new ClientData(aSessionId, aCallback);
1749 Program.iFormMain.iClients.Add(aSessionId, newClient);
1750 //Add this session to our UI
1751 UpdateClientTreeViewNode(newClient);
1757 /// Find the client with the highest priority if any.
1759 /// <returns>Our highest priority client or null if not a single client is connected.</returns>
1760 public ClientData FindHighestPriorityClient()
1762 ClientData highestPriorityClient = null;
1763 foreach (var client in iClients)
1765 if (highestPriorityClient == null || client.Value.Priority > highestPriorityClient.Priority)
1767 highestPriorityClient = client.Value;
1771 return highestPriorityClient;
1777 /// <param name="aSessionId"></param>
1778 public void RemoveClientThreadSafe(string aSessionId)
1780 if (this.InvokeRequired)
1782 //Not in the proper thread, invoke ourselves
1783 RemoveClientDelegate d = new RemoveClientDelegate(RemoveClientThreadSafe);
1784 this.Invoke(d, new object[] {aSessionId});
1788 //We are in the proper thread
1789 //Remove this session from both client collection and UI tree view
1790 if (Program.iFormMain.iClients.Keys.Contains(aSessionId))
1792 Program.iFormMain.iClients.Remove(aSessionId);
1793 Program.iFormMain.iTreeViewClients.Nodes.Remove(
1794 Program.iFormMain.iTreeViewClients.Nodes.Find(aSessionId, false)[0]);
1795 //Update recording status too whenever a client is removed
1796 UpdateRecordingNotification();
1799 if (iCurrentClientSessionId == aSessionId)
1801 //The current client is closing
1802 iCurrentClientData = null;
1803 //Find the client with the highest priority and set it as current
1804 ClientData newCurrentClient = FindHighestPriorityClient();
1805 if (newCurrentClient != null)
1807 SetCurrentClient(newCurrentClient.SessionId, true);
1811 if (iClients.Count == 0)
1813 //Clear our screen when last client disconnects
1814 ClearLayout(iTableLayoutPanelCurrentClient);
1815 iCurrentClientData = null;
1819 //We were closing our form
1820 //All clients are now closed
1821 //Just resume our close operation
1832 /// <param name="aSessionId"></param>
1833 /// <param name="aLayout"></param>
1834 public void SetClientLayoutThreadSafe(string aSessionId, TableLayout aLayout)
1836 if (this.InvokeRequired)
1838 //Not in the proper thread, invoke ourselves
1839 Invoke(new Action<FormMain>((sender) => { SetClientLayoutThreadSafe(aSessionId, aLayout); }), this);
1843 ClientData client = iClients[aSessionId];
1850 // If we have a matching client and we want to change the client layout
1851 if (client.Target == Target.Client)
1853 //Don't change a thing if the layout is the same
1854 if (!client.View.Layout.IsSameAs(aLayout))
1856 Debug.Print("SetClientLayoutThreadSafe: Layout updated.");
1857 //Set our client layout then
1858 client.View.Layout = aLayout;
1859 //So that next time we update all our fields at ones
1860 client.HasNewLayout = true;
1861 //Layout has changed clear our fields then
1862 client.View.Fields.Clear();
1864 UpdateClientTreeViewNode(client);
1868 Debug.Print("SetClientLayoutThreadSafe: Layout has not changed.");
1871 else if (client.Target == Target.Display)
1873 // Mark our display layout has updated and wait for the fields.
1874 // Is display layout a property from our client?
1875 //iTableLayoutPanelDisplay. = aLayout;
1882 /// <param name="aSessionId"></param>
1883 /// <param name="aField"></param>
1884 public void SetClientFieldThreadSafe(string aSessionId, DataField aField)
1886 if (this.InvokeRequired)
1888 //Not in the proper thread, invoke ourselves
1889 Invoke(new Action<FormMain>((sender) => { SetClientFieldThreadSafe(aSessionId, aField); }), this);
1893 //We are in the proper thread
1894 //Call the non-thread-safe variant
1895 SetClientField(aSessionId, aField);
1903 /// Set a data field in the given client.
1905 /// <param name="aSessionId"></param>
1906 /// <param name="aField"></param>
1907 private void SetClientField(string aSessionId, DataField aField)
1909 //TODO: should check if the field actually changed?
1911 ClientData client = iClients[aSessionId];
1912 bool layoutChanged = false;
1913 bool contentChanged = true;
1915 //Fetch our field index
1916 int fieldIndex = client.View.FindSameFieldIndex(aField);
1920 //No corresponding field, just bail out
1924 //Keep our previous field in there
1925 DataField previousField = client.View.Fields[fieldIndex];
1926 //Just update that field then
1927 client.View.Fields[fieldIndex] = aField;
1929 if (!aField.IsTableField)
1931 //We are done then if that field is not in our table layout
1935 TableField tableField = (TableField) aField;
1937 if (previousField.IsSameLayout(aField))
1939 //If we are updating a field in our current client we need to update it in our panel
1940 if (aSessionId == iCurrentClientSessionId)
1942 Control ctrl = iTableLayoutPanelCurrentClient.GetControlFromPosition(tableField.Column, tableField.Row);
1943 if (aField.IsTextField && ctrl is MarqueeLabel)
1945 TextField textField = (TextField)aField;
1946 //Text field control already in place, just change the text
1947 MarqueeLabel label = (MarqueeLabel)ctrl;
1948 contentChanged = (label.Text != textField.Text || label.TextAlign != textField.Alignment);
1949 label.Text = textField.Text;
1950 label.TextAlign = textField.Alignment;
1952 else if (aField.IsBitmapField && ctrl is PictureBox)
1954 BitmapField bitmapField = (BitmapField)aField;
1955 contentChanged = true; //TODO: Bitmap comp or should we leave that to clients?
1956 //Bitmap field control already in place just change the bitmap
1957 PictureBox pictureBox = (PictureBox)ctrl;
1958 pictureBox.Image = bitmapField.Bitmap;
1960 else if (aField is AudioVisualizerField && ctrl is PictureBox)
1962 contentChanged = false; // Since nothing was changed
1966 layoutChanged = true;
1972 layoutChanged = true;
1975 //If either content or layout changed we need to update our tree view to reflect the changes
1976 if (contentChanged || layoutChanged)
1978 UpdateClientTreeViewNode(client);
1982 Debug.Print("Layout changed");
1983 //Our layout has changed, if we are already the current client we need to update our panel
1984 if (aSessionId == iCurrentClientSessionId)
1986 //Apply layout and set data fields.
1987 UpdateTableLayoutPanel(iCurrentClientData.View, iTableLayoutPanelCurrentClient);
1992 Debug.Print("Layout has not changed.");
1997 Debug.Print("WARNING: content and layout have not changed!");
2000 //When a client field is set we try switching to this client to present the new information to our user
2001 SetCurrentClient(aSessionId);
2007 /// <param name="aTexts"></param>
2008 public void SetClientFieldsThreadSafe(string aSessionId, System.Collections.Generic.IList<DataField> aFields)
2010 if (this.InvokeRequired)
2012 //Not in the proper thread, invoke ourselves
2013 Invoke(new Action<FormMain>((sender) => { SetClientFieldsThreadSafe(aSessionId, aFields); }), this);
2017 ClientData client = iClients[aSessionId];
2019 if (client.HasNewLayout)
2021 //TODO: Assert client.Count == 0
2022 //Our layout was just changed
2023 //Do some special handling to avoid re-creating our panel N times, once for each fields
2024 client.HasNewLayout = false;
2025 //Just set all our fields then
2026 client.View.Fields.AddRange(aFields);
2027 //Try switch to that client
2028 SetCurrentClient(aSessionId);
2030 //If we are updating the current client update our panel
2031 if (aSessionId == iCurrentClientSessionId)
2033 //Apply layout and set data fields.
2034 UpdateTableLayoutPanel(iCurrentClientData.View, iTableLayoutPanelCurrentClient);
2037 UpdateClientTreeViewNode(client);
2041 //Put each our text fields in a label control
2042 foreach (DataField field in aFields)
2044 SetClientField(aSessionId, field);
2053 /// <param name="aSessionId"></param>
2054 /// <param name="aName"></param>
2055 public void SetClientNameThreadSafe(string aSessionId, string aName)
2057 if (this.InvokeRequired)
2059 //Not in the proper thread, invoke ourselves
2060 Invoke(new Action<FormMain>((sender) => { SetClientNameThreadSafe(aSessionId, aName); }), this);
2064 //We are in the proper thread
2066 ClientData client = iClients[aSessionId];
2070 client.Name = aName;
2071 //Update our tree-view
2072 UpdateClientTreeViewNode(client);
2078 public void SetClientPriorityThreadSafe(string aSessionId, uint aPriority)
2080 if (this.InvokeRequired)
2082 //Not in the proper thread, invoke ourselves
2083 Invoke(new Action<FormMain>((sender) => { SetClientPriorityThreadSafe(aSessionId, aPriority); }), this);
2087 //We are in the proper thread
2089 ClientData client = iClients[aSessionId];
2093 client.Priority = aPriority;
2094 //Update our tree-view
2095 UpdateClientTreeViewNode(client);
2096 //Change our current client as per new priority
2097 ClientData newCurrentClient = FindHighestPriorityClient();
2098 if (newCurrentClient != null)
2100 SetCurrentClient(newCurrentClient.SessionId);
2109 /// <param name="aSessionId"></param>
2110 /// <param name="aPriority"></param>
2111 public void SetClientTargetThreadSafe(string aSessionId, Target aTarget)
2113 if (this.InvokeRequired)
2115 //Not in the proper thread, invoke ourselves
2116 Invoke(new Action<FormMain>((sender) => { SetClientTargetThreadSafe(aSessionId, aTarget); }), this);
2120 //We are in the proper thread
2122 ClientData client = iClients[aSessionId];
2126 client.Target = aTarget;
2127 //Update our tree-view
2128 //UpdateClientTreeViewNode(client);
2136 /// <param name="value"></param>
2137 /// <param name="maxChars"></param>
2138 /// <returns></returns>
2139 public static string Truncate(string value, int maxChars)
2141 return value.Length <= maxChars ? value : value.Substring(0, maxChars - 3) + "...";
2145 /// Update our recording notification.
2147 private void UpdateRecordingNotification()
2150 bool activeRecording = false;
2152 RecordingField recField = new RecordingField();
2153 foreach (var client in iClients)
2155 RecordingField rec = (RecordingField) client.Value.View.FindSameFieldAs(recField);
2156 if (rec != null && rec.IsActive)
2158 activeRecording = true;
2159 //Don't break cause we are collecting the names/texts.
2160 if (!String.IsNullOrEmpty(rec.Text))
2162 text += (rec.Text + "\n");
2166 //Not text for that recording, use client name instead
2167 text += client.Value.Name + " recording\n";
2173 //Update our text no matter what, can't have more than 63 characters otherwise it throws an exception.
2174 iRecordingNotification.Text = Truncate(text, 63);
2176 //Change visibility of notification if needed
2177 if (iRecordingNotification.Visible != activeRecording)
2179 iRecordingNotification.Visible = activeRecording;
2180 //Assuming the notification icon is in sync with our display icon
2181 //Take care of our REC icon
2182 if (iDisplay.IsOpen())
2184 iDisplay.SetIconOnOff(MiniDisplay.IconType.Recording, activeRecording);
2192 /// <param name="aClient"></param>
2193 private void UpdateClientTreeViewNode(ClientData aClient)
2195 Debug.Print("UpdateClientTreeViewNode");
2197 if (aClient == null)
2202 //Hook in record icon update too
2203 UpdateRecordingNotification();
2205 TreeNode node = null;
2206 //Check that our client node already exists
2207 //Get our client root node using its key which is our session ID
2208 TreeNode[] nodes = iTreeViewClients.Nodes.Find(aClient.SessionId, false);
2209 if (nodes.Count() > 0)
2211 //We already have a node for that client
2213 //Clear children as we are going to recreate them below
2218 //Client node does not exists create a new one
2219 iTreeViewClients.Nodes.Add(aClient.SessionId, aClient.SessionId);
2220 node = iTreeViewClients.Nodes.Find(aClient.SessionId, false)[0];
2226 if (!String.IsNullOrEmpty(aClient.Name))
2228 //We have a name, use it as text for our root node
2229 node.Text = aClient.Name;
2230 //Add a child with SessionId
2231 node.Nodes.Add(new TreeNode(aClient.SessionId));
2235 //No name, use session ID instead
2236 node.Text = aClient.SessionId;
2239 //Display client priority
2240 node.Nodes.Add(new TreeNode("Priority: " + aClient.Priority));
2242 if (aClient.View.Fields.Count > 0)
2244 //Create root node for our texts
2245 TreeNode textsRoot = new TreeNode("Fields");
2246 node.Nodes.Add(textsRoot);
2247 //For each text add a new entry
2248 foreach (DataField field in aClient.View.Fields)
2250 if (field.IsTextField)
2252 TextField textField = (TextField) field;
2253 textsRoot.Nodes.Add(new TreeNode("[Text]" + textField.Text));
2255 else if (field.IsBitmapField)
2257 textsRoot.Nodes.Add(new TreeNode("[Bitmap]"));
2259 else if (field is AudioVisualizerField)
2261 textsRoot.Nodes.Add(new TreeNode("[Audio Visualizer]"));
2263 else if (field.IsRecordingField)
2265 RecordingField recordingField = (RecordingField) field;
2266 textsRoot.Nodes.Add(new TreeNode("[Recording]" + recordingField.IsActive));
2276 /// Update our display table layout.
2277 /// Will instanciated every field control as defined by our client.
2278 /// Fields must be specified by rows from the left.
2280 /// <param name="aLayout"></param>
2281 private void UpdateTableLayoutPanel(SharpLib.Display.View aView, TableLayoutPanel aPanel)
2283 Debug.Print("UpdateTableLayoutPanel");
2292 TableLayout layout = aView.Layout;
2294 //First clean our current panel
2295 ClearLayout(aPanel);
2297 //Then recreate our rows...
2298 while (aPanel.RowCount < layout.Rows.Count)
2304 while (aPanel.ColumnCount < layout.Columns.Count)
2306 aPanel.ColumnCount++;
2310 for (int i = 0; i < aPanel.ColumnCount; i++)
2312 //Create our column styles
2313 aPanel.ColumnStyles.Add(layout.Columns[i]);
2316 for (int j = 0; j < aPanel.RowCount; j++)
2320 //Create our row styles
2321 aPanel.RowStyles.Add(layout.Rows[j]);
2331 foreach (DataField field in aView.Fields)
2333 if (!field.IsTableField)
2335 //That field is not taking part in our table layout skip it
2339 TableField tableField = (TableField) field;
2341 //Create a control corresponding to the field specified for that cell
2342 Control control = CreateControlForDataField(tableField);
2344 //Add newly created control to our table layout at the specified row and column
2345 aPanel.Controls.Add(control, tableField.Column, tableField.Row);
2346 //Make sure we specify column and row span for that new control
2347 aPanel.SetColumnSpan(control, tableField.ColumnSpan);
2348 aPanel.SetRowSpan(control, tableField.RowSpan);
2356 /// Check our type of data field and create corresponding control
2358 /// <param name="aField"></param>
2359 private Control CreateControlForDataField(DataField aField)
2361 Control control = null;
2362 if (aField.IsTextField)
2364 MarqueeLabel label = new SharpDisplayManager.MarqueeLabel();
2365 label.AutoEllipsis = true;
2366 label.AutoSize = true;
2367 label.BackColor = System.Drawing.Color.Transparent;
2368 label.Dock = System.Windows.Forms.DockStyle.Fill;
2369 label.Location = new System.Drawing.Point(1, 1);
2370 label.Margin = new System.Windows.Forms.Padding(0);
2371 label.Name = "marqueeLabel" + aField;
2372 label.OwnTimer = false;
2373 label.PixelsPerSecond = cds.ScrollingSpeedInPixelsPerSecond;
2374 label.Separator = cds.Separator;
2375 label.MinFontSize = cds.MinFontSize;
2376 label.ScaleToFit = cds.ScaleToFit;
2377 //control.Size = new System.Drawing.Size(254, 30);
2378 //control.TabIndex = 2;
2379 label.Font = cds.Font;
2381 TextField field = (TextField)aField;
2382 label.TextAlign = field.Alignment;
2383 label.UseCompatibleTextRendering = true;
2384 label.Text = field.Text;
2388 else if (aField.IsBitmapField)
2390 //Create picture box
2391 PictureBox picture = new PictureBox();
2392 picture.AutoSize = true;
2393 picture.BackColor = System.Drawing.Color.Transparent;
2394 picture.Dock = System.Windows.Forms.DockStyle.Fill;
2395 picture.Location = new System.Drawing.Point(1, 1);
2396 picture.Margin = new System.Windows.Forms.Padding(0);
2397 picture.Name = "pictureBox" + aField;
2399 BitmapField field = (BitmapField)aField;
2400 picture.Image = field.Bitmap;
2404 else if (aField is AudioVisualizerField)
2406 //Create picture box
2407 PictureBox picture = new PictureBox();
2408 picture.AutoSize = true;
2409 picture.BackColor = System.Drawing.Color.Transparent;
2410 picture.Dock = System.Windows.Forms.DockStyle.Fill;
2411 picture.Location = new System.Drawing.Point(1, 1);
2412 picture.Margin = new System.Windows.Forms.Padding(0);
2413 picture.Name = "pictureBox" + aField;
2415 // Make sure visualization is running
2416 iAudioManager.AddVisualizer();
2418 // Notify audio manager when we don't need audio visualizer anymore
2419 picture.Disposed += (sender, e) =>
2421 if (iAudioManager != null)
2423 // Make sure we stop visualization when not needed
2424 iAudioManager.RemoveVisualizer();
2428 // Create a new bitmap when control size changes
2429 picture.SizeChanged += (sender, e) =>
2431 // Somehow bitmap created when our from is invisible are not working
2432 // Mark our form visibility status
2433 bool visible = Visible;
2434 // Make sure it's visible
2436 // Adjust our bitmap size when control size changes
2437 picture.Image = new System.Drawing.Bitmap(picture.Width, picture.Height, PixelFormat.Format32bppArgb);
2438 // Restore our form visibility
2445 //TODO: Handle recording field?
2451 /// Called when the user selected a new display type.
2453 /// <param name="sender"></param>
2454 /// <param name="e"></param>
2455 private void comboBoxDisplayType_SelectedIndexChanged(object sender, EventArgs e)
2457 //Store the selected display type in our settings
2458 Properties.Settings.Default.CurrentDisplayIndex = comboBoxDisplayType.SelectedIndex;
2459 cds.DisplayType = comboBoxDisplayType.SelectedIndex;
2460 Properties.Settings.Default.Save();
2462 //Try re-opening the display connection if we were already connected.
2463 //Otherwise just update our status to reflect display type change.
2464 if (iDisplay.IsOpen())
2466 OpenDisplayConnection();
2474 private void maskedTextBoxTimerInterval_TextChanged(object sender, EventArgs e)
2476 if (maskedTextBoxTimerInterval.Text != "")
2478 int interval = Convert.ToInt32(maskedTextBoxTimerInterval.Text);
2482 iTimerDisplay.Interval = interval;
2483 cds.TimerInterval = iTimerDisplay.Interval;
2484 Properties.Settings.Default.Save();
2489 private void maskedTextBoxMinFontSize_TextChanged(object sender, EventArgs e)
2491 if (maskedTextBoxMinFontSize.Text != "")
2493 int minFontSize = Convert.ToInt32(maskedTextBoxMinFontSize.Text);
2495 if (minFontSize > 0)
2497 cds.MinFontSize = minFontSize;
2498 Properties.Settings.Default.Save();
2499 //We need to recreate our layout for that change to take effect
2500 if (iCurrentClientData != null)
2502 UpdateTableLayoutPanel(iCurrentClientData.View, iTableLayoutPanelCurrentClient);
2509 private void maskedTextBoxScrollingSpeed_TextChanged(object sender, EventArgs e)
2511 if (maskedTextBoxScrollingSpeed.Text != "")
2513 int scrollingSpeed = Convert.ToInt32(maskedTextBoxScrollingSpeed.Text);
2515 if (scrollingSpeed > 0)
2517 cds.ScrollingSpeedInPixelsPerSecond = scrollingSpeed;
2518 Properties.Settings.Default.Save();
2519 //We need to recreate our layout for that change to take effect
2520 if (iCurrentClientData != null)
2522 UpdateTableLayoutPanel(iCurrentClientData.View, iTableLayoutPanelCurrentClient);
2531 /// <param name="sender"></param>
2532 /// <param name="e"></param>
2533 private void textBoxScrollLoopSeparator_TextChanged(object sender, EventArgs e)
2535 cds.Separator = textBoxScrollLoopSeparator.Text;
2536 Properties.Settings.Default.Save();
2538 //Update our text fields
2539 UpdateMarqueesSeparator(iTableLayoutPanelDisplay);
2542 private void buttonPowerOn_Click(object sender, EventArgs e)
2547 private void buttonPowerOff_Click(object sender, EventArgs e)
2549 iDisplay.PowerOff();
2552 private void buttonShowClock_Click(object sender, EventArgs e)
2557 private void buttonHideClock_Click(object sender, EventArgs e)
2562 private void buttonUpdate_Click(object sender, EventArgs e)
2564 InstallUpdateSyncWithInfo();
2572 if (!iDisplay.IsOpen())
2577 //Devices like MDM166AA don't support windowing and frame rendering must be stopped while showing our clock
2578 iSkipFrameRendering = true;
2581 iDisplay.SwapBuffers();
2582 //Then show our clock
2583 iDisplay.ShowClock();
2591 if (!iDisplay.IsOpen())
2596 //Devices like MDM166AA don't support windowing and frame rendering must be stopped while showing our clock
2597 iSkipFrameRendering = false;
2598 iDisplay.HideClock();
2601 private void InstallUpdateSyncWithInfo()
2603 UpdateCheckInfo info = null;
2605 if (ApplicationDeployment.IsNetworkDeployed)
2607 ApplicationDeployment ad = ApplicationDeployment.CurrentDeployment;
2611 info = ad.CheckForDetailedUpdate();
2614 catch (DeploymentDownloadException dde)
2617 "The new version of the application cannot be downloaded at this time. \n\nPlease check your network connection, or try again later. Error: " +
2621 catch (InvalidDeploymentException ide)
2624 "Cannot check for a new version of the application. The ClickOnce deployment is corrupt. Please redeploy the application and try again. Error: " +
2628 catch (InvalidOperationException ioe)
2631 "This application cannot be updated. It is likely not a ClickOnce application. Error: " +
2636 if (info.UpdateAvailable)
2638 Boolean doUpdate = true;
2640 if (!info.IsUpdateRequired)
2643 MessageBox.Show("An update is available. Would you like to update the application now?",
2644 "Update Available", MessageBoxButtons.OKCancel);
2645 if (!(DialogResult.OK == dr))
2652 // Display a message that the application MUST reboot. Display the minimum required version.
2653 MessageBox.Show("This application has detected a mandatory update from your current " +
2654 "version to version " + info.MinimumRequiredVersion.ToString() +
2655 ". The application will now install the update and restart.",
2656 "Update Available", MessageBoxButtons.OK,
2657 MessageBoxIcon.Information);
2665 MessageBox.Show("The application has been upgraded, and will now restart.");
2666 Application.Restart();
2668 catch (DeploymentDownloadException dde)
2671 "Cannot install the latest version of the application. \n\nPlease check your network connection, or try again later. Error: " +
2679 MessageBox.Show("You are already running the latest version.", "Application up-to-date");
2688 private void SysTrayHideShow()
2694 WindowState = FormWindowState.Normal;
2699 /// Use to handle minimize events.
2701 /// <param name="sender"></param>
2702 /// <param name="e"></param>
2703 private void MainForm_SizeChanged(object sender, EventArgs e)
2705 if (WindowState == FormWindowState.Minimized && Properties.Settings.Default.MinimizeToTray)
2717 /// <param name="sender"></param>
2718 /// <param name="e"></param>
2719 private void tableLayoutPanel_SizeChanged(object sender, EventArgs e)
2721 //Our table layout size has changed which means our display size has changed.
2722 //We need to re-create our bitmap.
2723 iCreateBitmap = true;
2727 /// Broadcast messages to subscribers.
2729 /// <param name="message"></param>
2730 protected override void WndProc(ref Message aMessage)
2732 if (OnWndProc != null)
2734 OnWndProc(ref aMessage);
2737 base.WndProc(ref aMessage);
2740 private void iCheckBoxCecEnabled_CheckedChanged(object sender, EventArgs e)
2746 private void comboBoxHdmiPort_SelectedIndexChanged(object sender, EventArgs e)
2748 //Save CEC HDMI port
2749 Properties.Settings.Default.CecHdmiPort = Convert.ToByte(comboBoxHdmiPort.SelectedIndex);
2750 Properties.Settings.Default.CecHdmiPort++;
2751 Properties.Settings.Default.Save();
2759 private void ResetCec()
2761 if (iCecManager == null)
2763 //Thus skipping initial UI setup
2769 if (Properties.Settings.Default.CecEnabled)
2771 iCecManager.Start(Handle, "CEC",
2772 Properties.Settings.Default.CecHdmiPort);
2781 private async void ResetHarmonyAsync(bool aForceAuth=false)
2783 // ConnectAsync already if we have an existing session cookie
2784 if (Properties.Settings.Default.HarmonyEnabled)
2788 iButtonHarmonyConnect.Enabled = false;
2789 await ConnectHarmonyAsync(aForceAuth);
2791 catch (Exception ex)
2793 Trace.WriteLine("Exception thrown by ConnectHarmonyAsync");
2794 Trace.WriteLine(ex.ToString());
2798 iButtonHarmonyConnect.Enabled = true;
2806 /// <param name="sender"></param>
2807 /// <param name="e"></param>
2808 private void iButtonHarmonyConnect_Click(object sender, EventArgs e)
2810 // User is explicitaly trying to connect
2811 //Reset Harmony Hub connection forcing authentication
2812 ResetHarmonyAsync(true);
2818 /// <param name="sender"></param>
2819 /// <param name="e"></param>
2820 private void iCheckBoxHarmonyEnabled_CheckedChanged(object sender, EventArgs e)
2822 iButtonHarmonyConnect.Enabled = iCheckBoxHarmonyEnabled.Checked;
2828 private void SetupCecLogLevel()
2831 iCecManager.Client.LogLevel = 0;
2833 if (checkBoxCecLogError.Checked)
2834 iCecManager.Client.LogLevel |= (int) CecLogLevel.Error;
2836 if (checkBoxCecLogWarning.Checked)
2837 iCecManager.Client.LogLevel |= (int) CecLogLevel.Warning;
2839 if (checkBoxCecLogNotice.Checked)
2840 iCecManager.Client.LogLevel |= (int) CecLogLevel.Notice;
2842 if (checkBoxCecLogTraffic.Checked)
2843 iCecManager.Client.LogLevel |= (int) CecLogLevel.Traffic;
2845 if (checkBoxCecLogDebug.Checked)
2846 iCecManager.Client.LogLevel |= (int) CecLogLevel.Debug;
2848 iCecManager.Client.FilterOutPollLogs = checkBoxCecLogNoPoll.Checked;
2852 private void ButtonStartIdleClient_Click(object sender, EventArgs e)
2857 private void buttonClearLogs_Click(object sender, EventArgs e)
2859 richTextBoxLogs.Clear();
2862 private void checkBoxCecLogs_CheckedChanged(object sender, EventArgs e)
2872 /// Get the current event based on event tree view selection.
2874 /// <returns></returns>
2875 private Ear.Event CurrentEvent()
2877 //Walk up the tree from the selected node to find our event
2878 TreeNode node = iTreeViewEvents.SelectedNode;
2879 Ear.Event selectedEvent = null;
2880 while (node != null)
2882 if (node.Tag is Ear.Event)
2884 selectedEvent = (Ear.Event) node.Tag;
2890 return selectedEvent;
2894 /// Get the current action based on event tree view selection
2896 /// <returns></returns>
2897 private Ear.Action CurrentAction()
2899 TreeNode node = iTreeViewEvents.SelectedNode;
2900 if (node != null && node.Tag is Ear.Action)
2902 return (Ear.Action) node.Tag;
2911 /// <returns></returns>
2912 private Ear.Object CurrentEarObject()
2914 Ear.Action a = CurrentAction();
2915 Ear.Event e = CurrentEvent();
2926 /// Get the current action based on event tree view selection
2928 /// <returns></returns>
2929 private Ear.Object CurrentEarParent()
2931 TreeNode node = iTreeViewEvents.SelectedNode;
2932 if (node == null || node.Parent == null)
2937 if (node.Parent.Tag is Ear.Object)
2939 return (Ear.Object)node.Parent.Tag;
2942 if (node.Parent.Parent != null && node.Parent.Parent.Tag is Ear.Object)
2944 //Can be the case for events
2945 return (Ear.Object)node.Parent.Parent.Tag;
2955 /// <param name="sender"></param>
2956 /// <param name="e"></param>
2957 private void buttonActionAdd_Click(object sender, EventArgs e)
2959 Ear.Object parent = CurrentEarObject();
2960 if (parent == null )
2962 //We did not find a corresponding event or action
2966 FormEditObject<Ear.Action> ea = new FormEditObject<Ear.Action>();
2967 ea.Text = "Add action";
2968 DialogResult res = CodeProject.Dialog.DlgBox.ShowDialog(ea);
2969 if (res == DialogResult.OK)
2971 parent.Objects.Add(ea.Object);
2972 Properties.Settings.Default.Save();
2973 // We want to select the parent so that one can easily add another action to the same collection
2974 PopulateTreeViewEvents(parent);
2981 /// <param name="sender"></param>
2982 /// <param name="e"></param>
2983 private void buttonActionEdit_Click(object sender, EventArgs e)
2985 Ear.Action selectedAction = CurrentAction();
2986 Ear.Object parent = CurrentEarParent()
2988 if (parent == null || selectedAction == null)
2990 //We did not find a corresponding parent
2994 FormEditObject<Ear.Action> ea = new FormEditObject<Ear.Action>();
2995 ea.Text = "Edit action";
2996 ea.Object = selectedAction;
2997 int actionIndex = iTreeViewEvents.SelectedNode.Index;
2998 DialogResult res = CodeProject.Dialog.DlgBox.ShowDialog(ea);
2999 if (res == DialogResult.OK)
3001 //Make sure we keep the same children as before
3002 ea.Object.Objects = parent.Objects[actionIndex].Objects;
3004 parent.Objects[actionIndex]=ea.Object;
3005 //Save and rebuild our event tree view
3006 Properties.Settings.Default.Save();
3007 PopulateTreeViewEvents(ea.Object);
3014 /// <param name="sender"></param>
3015 /// <param name="e"></param>
3016 private void buttonActionDelete_Click(object sender, EventArgs e)
3018 Ear.Action action = CurrentAction();
3021 //Must select action node
3025 Properties.Settings.Default.EarManager.RemoveAction(action);
3026 Properties.Settings.Default.Save();
3027 PopulateTreeViewEvents();
3033 /// <param name="sender"></param>
3034 /// <param name="e"></param>
3035 private void buttonActionTest_Click(object sender, EventArgs e)
3037 Ear.Action a = CurrentAction();
3042 iTreeViewEvents.Focus();
3048 /// <param name="sender"></param>
3049 /// <param name="e"></param>
3050 private void buttonActionMoveUp_Click(object sender, EventArgs e)
3052 Ear.Action a = CurrentAction();
3054 //Action already at the top of the list
3055 iTreeViewEvents.SelectedNode.Index == 0)
3060 //Swap actions in event's action list
3061 Ear.Object parent = CurrentEarParent();
3062 int currentIndex = iTreeViewEvents.SelectedNode.Index;
3063 Ear.Action movingUp = parent.Objects[currentIndex] as Ear.Action;
3064 Ear.Action movingDown = parent.Objects[currentIndex-1] as Ear.Action;
3065 parent.Objects[currentIndex] = movingDown;
3066 parent.Objects[currentIndex-1] = movingUp;
3068 //Save and populate our tree again
3069 Properties.Settings.Default.Save();
3070 PopulateTreeViewEvents(a);
3076 /// <param name="sender"></param>
3077 /// <param name="e"></param>
3078 private void buttonActionMoveDown_Click(object sender, EventArgs e)
3080 Ear.Action a = CurrentAction();
3082 //Action already at the bottom of the list
3083 iTreeViewEvents.SelectedNode.Index == iTreeViewEvents.SelectedNode.Parent.Nodes.Count - 1)
3088 //Swap actions in event's action list
3089 Ear.Object parent = CurrentEarParent();
3090 int currentIndex = iTreeViewEvents.SelectedNode.Index;
3091 Ear.Action movingDown = parent.Objects[currentIndex] as Ear.Action;
3092 Ear.Action movingUp = parent.Objects[currentIndex + 1] as Ear.Action;
3093 parent.Objects[currentIndex] = movingUp;
3094 parent.Objects[currentIndex + 1] = movingDown;
3096 //Save and populate our tree again
3097 Properties.Settings.Default.Save();
3098 PopulateTreeViewEvents(a);
3105 /// <param name="sender"></param>
3106 /// <param name="e"></param>
3107 private void buttonEventTest_Click(object sender, EventArgs e)
3109 Ear.Event earEvent = CurrentEvent();
3110 if (earEvent != null)
3117 /// Manages events and actions buttons according to selected item in event tree.
3119 /// <param name="sender"></param>
3120 /// <param name="e"></param>
3121 private void iTreeViewEvents_AfterSelect(object sender, TreeViewEventArgs e)
3129 private void UpdateEventView()
3131 //One can always add an event
3132 buttonEventAdd.Enabled = true;
3134 //Enable buttons according to selected item
3135 buttonActionAdd.Enabled =
3136 buttonEventTest.Enabled =
3137 buttonEventDelete.Enabled =
3138 buttonEventEdit.Enabled =
3139 CurrentEvent() != null;
3141 Ear.Action currentAction = CurrentAction();
3142 //If an action is selected enable the following buttons
3143 buttonActionTest.Enabled =
3144 buttonActionDelete.Enabled =
3145 buttonActionMoveUp.Enabled =
3146 buttonActionMoveDown.Enabled =
3147 buttonActionEdit.Enabled =
3148 currentAction != null;
3150 if (currentAction != null)
3152 //If an action is selected enable move buttons if needed
3153 buttonActionMoveUp.Enabled = iTreeViewEvents.SelectedNode.Index != 0;
3154 buttonActionMoveDown.Enabled = iTreeViewEvents.SelectedNode.Index <
3155 iTreeViewEvents.SelectedNode.Parent.Nodes.Count - 1;
3162 /// <param name="sender"></param>
3163 /// <param name="e"></param>
3164 private void buttonEventAdd_Click(object sender, EventArgs e)
3166 FormEditObject<Ear.Event> ea = new FormEditObject<Ear.Event>();
3167 ea.Text = "Add event";
3168 DialogResult res = CodeProject.Dialog.DlgBox.ShowDialog(ea);
3169 if (res == DialogResult.OK)
3171 Properties.Settings.Default.EarManager.Events.Add(ea.Object);
3172 Properties.Settings.Default.Save();
3173 PopulateTreeViewEvents(ea.Object);
3180 /// <param name="sender"></param>
3181 /// <param name="e"></param>
3182 private void buttonEventDelete_Click(object sender, EventArgs e)
3184 Ear.Event currentEvent = CurrentEvent();
3185 if (currentEvent == null)
3187 //Must select action node
3191 Properties.Settings.Default.EarManager.Events.Remove(currentEvent);
3192 Properties.Settings.Default.Save();
3193 PopulateTreeViewEvents();
3199 /// <param name="sender"></param>
3200 /// <param name="e"></param>
3201 private void buttonEventEdit_Click(object sender, EventArgs e)
3203 Ear.Event selectedEvent = CurrentEvent();
3204 if (selectedEvent == null)
3206 //We did not find a corresponding event
3210 FormEditObject<Ear.Event> ea = new FormEditObject<Ear.Event>();
3211 ea.Text = "Edit event";
3212 ea.Object = selectedEvent;
3213 int index = iTreeViewEvents.SelectedNode.Index;
3214 DialogResult res = CodeProject.Dialog.DlgBox.ShowDialog(ea);
3215 if (res == DialogResult.OK)
3217 //Make sure we keep the same actions as before
3218 ea.Object.Objects = Properties.Settings.Default.EarManager.Events[index].Objects;
3220 Properties.Settings.Default.EarManager.Events[index] = ea.Object;
3221 //Save and rebuild our event tree view
3222 Properties.Settings.Default.Save();
3223 PopulateTreeViewEvents(ea.Object);
3230 /// <param name="sender"></param>
3231 /// <param name="e"></param>
3232 private void iTreeViewEvents_Leave(object sender, EventArgs e)
3234 //Make sure our event tree never looses focus
3235 ((TreeView) sender).Focus();
3239 /// Called whenever we loose connection with our HarmonyHub.
3241 /// <param name="aRequestWasCancelled"></param>
3242 private void HarmonyConnectionClosed(object aSender, bool aClosedByServer)
3244 if (aClosedByServer)
3246 //Try reconnect then
3247 #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
3248 BeginInvoke(new MethodInvoker(delegate () { ResetHarmonyAsync(); }));
3249 #pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
3255 int iHarmonyReconnectTries = 0;
3256 const int KHarmonyMaxReconnectTries = 10;
3261 /// <returns></returns>
3262 private async Task ConnectHarmonyAsync(bool aForceAuth=false)
3264 if (Program.HarmonyClient != null)
3266 await Program.HarmonyClient.CloseAsync();
3269 bool success = false;
3271 //Reset Harmony client & config
3272 Program.HarmonyClient = null;
3273 Program.HarmonyConfig = null;
3274 iTreeViewHarmony.Nodes.Clear();
3276 Trace.WriteLine("Harmony: Connecting... ");
3277 //First create our client and login
3278 //Tip: Set keep-alive to false when testing reconnection process
3279 Program.HarmonyClient = new HarmonyHub.Client(iTextBoxHarmonyHubAddress.Text, true);
3280 Program.HarmonyClient.OnConnectionClosed += HarmonyConnectionClosed;
3282 string authToken = Properties.Settings.Default.LogitechAuthToken;
3283 if (!string.IsNullOrEmpty(authToken) && !aForceAuth)
3285 Trace.WriteLine("Harmony: Reusing token: {0}", authToken);
3286 success = await Program.HarmonyClient.TryOpenAsync(authToken);
3289 if (!Program.HarmonyClient.IsReady || !success
3290 // Only first failure triggers new Harmony server AUTH
3291 // That's to avoid calling upon Logitech servers too often
3292 && iHarmonyReconnectTries == 0 )
3294 //We failed to connect using our token
3296 Trace.WriteLine("Harmony: Reseting authentication token!");
3297 Properties.Settings.Default.LogitechAuthToken = "";
3298 Properties.Settings.Default.Save();
3300 Trace.WriteLine("Harmony: Authenticating with Logitech servers...");
3301 success = await Program.HarmonyClient.TryOpenAsync();
3302 //Persist our authentication token in our setting
3305 Trace.WriteLine("Harmony: Saving authentication token.");
3306 Properties.Settings.Default.LogitechAuthToken = Program.HarmonyClient.Token;
3307 Properties.Settings.Default.Save();
3311 // I've seen this failing with "Policy lookup failed on server".
3312 Program.HarmonyConfig = await Program.HarmonyClient.TryGetConfigAsync();
3313 if (Program.HarmonyConfig == null)
3319 // So we now have our Harmony Configuration
3320 PopulateTreeViewHarmony(Program.HarmonyConfig);
3321 // Make sure harmony command actions are showing device name instead of device id
3322 PopulateTreeViewEvents(CurrentEarObject());
3325 // TODO: Consider putting the retry logic one level higher in ResetHarmonyAsync
3328 // See if we need to keep trying
3329 if (iHarmonyReconnectTries < KHarmonyMaxReconnectTries)
3331 iHarmonyReconnectTries++;
3332 Trace.WriteLine("Harmony: Failed to connect, try again: " + iHarmonyReconnectTries);
3333 await ConnectHarmonyAsync();
3337 Trace.WriteLine("Harmony: Failed to connect, giving up!");
3338 iHarmonyReconnectTries = 0;
3339 // TODO: Could use a data member as timer rather than a new instance.
3340 // Try that again in 5 minutes then.
3341 // Using Windows Form timer to make sure we run in the UI thread.
3342 System.Windows.Forms.Timer timer = new System.Windows.Forms.Timer();
3343 timer.Tick += async delegate (object sender, EventArgs e)
3345 // Stop our timer first as we won't need it anymore
3346 (sender as System.Windows.Forms.Timer).Stop();
3347 // Then try to connect again
3348 await ConnectHarmonyAsync();
3350 timer.Interval = 300000;
3356 // We are connected with a valid Harmony Configuration
3357 // Reset our tries counter then
3358 iHarmonyReconnectTries = 0;
3365 /// <param name="aConfig"></param>
3366 private void PopulateTreeViewHarmony(HarmonyHub.Config aConfig)
3368 iTreeViewHarmony.Nodes.Clear();
3370 foreach (HarmonyHub.Device device in aConfig.Devices)
3372 TreeNode deviceNode = iTreeViewHarmony.Nodes.Add(device.Id, $"{device.Label} ({device.DeviceTypeDisplayName}/{device.Model})");
3373 deviceNode.Tag = device;
3375 foreach (HarmonyHub.ControlGroup cg in device.ControlGroups)
3377 TreeNode cgNode = deviceNode.Nodes.Add(cg.Name);
3380 foreach (HarmonyHub.Function f in cg.Functions)
3382 TreeNode fNode = cgNode.Nodes.Add(f.Label);
3388 //treeViewConfig.ExpandAll();
3391 private async void iTreeViewHarmony_NodeMouseDoubleClick(object sender, TreeNodeMouseClickEventArgs e)
3393 //Upon function node double click we execute it
3394 var tag = e.Node.Tag as HarmonyHub.Function;
3395 if (tag != null && e.Node.Parent.Parent.Tag is HarmonyHub.Device)
3397 HarmonyHub.Function f = tag;
3398 HarmonyHub.Device d = (HarmonyHub.Device)e.Node.Parent.Parent.Tag;
3400 Trace.WriteLine($"Harmony: Sending {f.Label} to {d.Label}...");
3402 await Program.HarmonyClient.TrySendKeyPressAsync(d.Id, f.Action.Command);