2 // Copyright (C) 2014-2015 Stéphane Lenclud.
4 // This file is part of SharpDisplayManager.
6 // SharpDisplayManager is free software: you can redistribute it and/or modify
7 // it under the terms of the GNU General Public License as published by
8 // the Free Software Foundation, either version 3 of the License, or
9 // (at your option) any later version.
11 // SharpDisplayManager is distributed in the hope that it will be useful,
12 // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 // GNU General Public License for more details.
16 // You should have received a copy of the GNU General Public License
17 // along with SharpDisplayManager. If not, see <http://www.gnu.org/licenses/>.
21 using System.Collections.Generic;
22 using System.ComponentModel;
27 using System.Threading.Tasks;
28 using System.Windows.Forms;
30 using CodeProject.Dialog;
31 using System.Drawing.Imaging;
32 using System.ServiceModel;
33 using System.Threading;
34 using System.Diagnostics;
35 using System.Deployment.Application;
36 using System.Reflection;
37 using System.Runtime.InteropServices;
38 using System.Security;
44 using CSCore.CoreAudioAPI;
53 using SharpDisplayClient;
55 using MiniDisplayInterop;
56 using SharpLib.Display;
57 using Ear = SharpLib.Ear;
60 namespace SharpDisplayManager
63 public delegate uint ColorProcessingDelegate(int aX, int aY, uint aPixel);
65 public delegate int CoordinateTranslationDelegate(System.Drawing.Bitmap aBmp, int aInt);
67 //Delegates are used for our thread safe method
68 public delegate void AddClientDelegate(string aSessionId, ICallback aCallback);
70 public delegate void RemoveClientDelegate(string aSessionId);
72 public delegate void SetFieldDelegate(string SessionId, DataField aField);
74 public delegate void SetFieldsDelegate(string SessionId, System.Collections.Generic.IList<DataField> aFields);
76 public delegate void SetLayoutDelegate(string SessionId, TableLayout aLayout);
78 public delegate void SetClientNameDelegate(string aSessionId, string aName);
80 public delegate void SetClientPriorityDelegate(string aSessionId, uint aPriority);
82 public delegate void PlainUpdateDelegate();
84 public delegate void WndProcDelegate(ref Message aMessage);
87 /// Our Display manager main form
89 [System.ComponentModel.DesignerCategory("Form")]
90 public partial class FormMain : FormMainHid, IMMNotificationClient
92 //public Manager iManager = new Manager();
93 DateTime LastTickTime;
95 System.Drawing.Bitmap iBmp;
96 //TODO: Align that with what we did from Audio Visualizers bitmaps?
97 bool iCreateBitmap; //Workaround render to bitmap issues when minimized
98 ServiceHost iServiceHost;
99 // Our collection of clients sorted by session id.
100 public Dictionary<string, ClientData> iClients;
101 // The name of the client which informations are currently displayed.
102 public string iCurrentClientSessionId;
103 ClientData iCurrentClientData;
105 public bool iClosing;
107 public bool iSkipFrameRendering;
108 //Function pointer for pixel color filtering
109 ColorProcessingDelegate iColorFx;
110 //Function pointer for pixel X coordinate intercept
111 CoordinateTranslationDelegate iScreenX;
112 //Function pointer for pixel Y coordinate intercept
113 CoordinateTranslationDelegate iScreenY;
116 private MMDeviceEnumerator iMultiMediaDeviceEnumerator;
117 private MMDevice iMultiMediaDevice;
118 private AudioEndpointVolume iAudioEndpointVolume;
119 // Audio visualization
120 private WasapiCapture iSoundIn;
121 private IWaveSource iWaveSource;
122 private LineSpectrum iLineSpectrum;
125 private NetworkManager iNetworkManager;
128 /// CEC - Consumer Electronic Control.
129 /// Notably used to turn TV on and off as Windows broadcast monitor on and off notifications.
131 private ConsumerElectronicControl iCecManager;
134 /// Manage run when Windows startup option
136 private StartupManager iStartupManager;
139 /// System notification icon used to hide our application from the task bar.
141 private SharpLib.Notification.Control iNotifyIcon;
144 /// System recording notification icon.
146 private SharpLib.Notification.Control iRecordingNotification;
151 RichTextBoxTraceListener iWriter;
155 /// Allow user to receive window messages;
157 public event WndProcDelegate OnWndProc;
161 if (Properties.Settings.Default.EarManager == null)
163 //No actions in our settings yet
164 Properties.Settings.Default.EarManager = new EarManager();
168 // We loaded events and actions from our settings
169 // Internalizer apparently skips constructor so we need to initialize it here
170 // Though I reckon that should only be needed when loading an empty EAR manager I guess.
171 Properties.Settings.Default.EarManager.Construct();
173 iSkipFrameRendering = false;
175 iCurrentClientSessionId = "";
176 iCurrentClientData = null;
177 LastTickTime = DateTime.Now;
178 //Instantiate our display and register for events notifications
179 iDisplay = new Display();
180 iDisplay.OnOpened += OnDisplayOpened;
181 iDisplay.OnClosed += OnDisplayClosed;
183 iClients = new Dictionary<string, ClientData>();
184 iStartupManager = new StartupManager();
185 iNotifyIcon = new SharpLib.Notification.Control();
186 iRecordingNotification = new SharpLib.Notification.Control();
188 //Have our designer initialize its controls
189 InitializeComponent();
191 //Redirect console output
192 iWriter = new RichTextBoxTraceListener(richTextBoxLogs);
193 Trace.Listeners.Add(iWriter);
195 //Populate device types
196 PopulateDeviceTypes();
198 //Initial status update
201 //We have a bug when drawing minimized and reusing our bitmap
202 //Though I could not reproduce it on Windows 10
203 iBmp = new System.Drawing.Bitmap(iTableLayoutPanel.Width, iTableLayoutPanel.Height,
204 PixelFormat.Format32bppArgb);
205 iCreateBitmap = false;
207 //Minimize our window if desired
208 if (Properties.Settings.Default.StartMinimized)
210 WindowState = FormWindowState.Minimized;
218 /// <param name="sender"></param>
219 /// <param name="e"></param>
220 private void MainForm_Load(object sender, EventArgs e)
222 //Check if we are running a Click Once deployed application
223 if (ApplicationDeployment.IsNetworkDeployed)
225 //This is a proper Click Once installation, fetch and show our version number
226 this.Text += " - v" + ApplicationDeployment.CurrentDeployment.CurrentVersion;
230 //Not a proper Click Once installation, assuming development build then
231 this.Text += " - development";
235 iMultiMediaDeviceEnumerator = new MMDeviceEnumerator();
236 iMultiMediaDeviceEnumerator.RegisterEndpointNotificationCallback(this);
237 UpdateAudioDeviceAndMasterVolumeThreadSafe();
240 iNetworkManager = new NetworkManager();
241 iNetworkManager.OnConnectivityChanged += OnConnectivityChanged;
242 UpdateNetworkStatus();
245 iCecManager = new ConsumerElectronicControl();
246 OnWndProc += iCecManager.OnWndProc;
253 PopulateTreeViewEvents();
255 //Setup notification icon
258 //Setup recording notification
259 SetupRecordingNotification();
261 // To make sure start up with minimize to tray works
262 if (WindowState == FormWindowState.Minimized && Properties.Settings.Default.MinimizeToTray)
268 //When not debugging we want the screen to be empty until a client takes over
271 //When developing we want at least one client for testing
272 StartNewClient("abcdefghijklmnopqrst-0123456789", "ABCDEFGHIJKLMNOPQRST-0123456789");
275 //Open display connection on start-up if needed
276 if (Properties.Settings.Default.DisplayConnectOnStartup)
278 OpenDisplayConnection();
281 //Start our server so that we can get client requests
284 //Register for HID events
285 RegisterHidDevices();
287 //Start Idle client if needed
288 if (Properties.Settings.Default.StartIdleClient)
295 /// Called when our display is opened.
297 /// <param name="aDisplay"></param>
298 private void OnDisplayOpened(Display aDisplay)
300 //Make sure we resume frame rendering
301 iSkipFrameRendering = false;
303 //Set our screen size now that our display is connected
304 //Our panelDisplay is the container of our tableLayoutPanel
305 //tableLayoutPanel will resize itself to fit the client size of our panelDisplay
306 //panelDisplay needs an extra 2 pixels for borders on each sides
307 //tableLayoutPanel will eventually be the exact size of our display
308 Size size = new Size(iDisplay.WidthInPixels() + 2, iDisplay.HeightInPixels() + 2);
309 panelDisplay.Size = size;
311 //Our display was just opened, update our UI
313 //Initiate asynchronous request
314 iDisplay.RequestFirmwareRevision();
317 UpdateMasterVolumeThreadSafe();
319 UpdateNetworkStatus();
322 //Testing icon in debug, no arm done if icon not supported
323 //iDisplay.SetIconStatus(Display.TMiniDisplayIconType.EMiniDisplayIconRecording, 0, 1);
324 //iDisplay.SetAllIconsStatus(2);
330 private static void AddActionsToTreeNode(TreeNode aParentNode, Ear.Object aObject)
332 foreach (Ear.Action a in aObject.Objects.OfType<Ear.Action>())
335 TreeNode actionNode = aParentNode.Nodes.Add(a.Brief());
337 //Use color from parent unless our action itself is disabled
338 actionNode.ForeColor = a.Enabled ? aParentNode.ForeColor : Color.DimGray;
340 AddActionsToTreeNode(actionNode,a);
348 /// <param name="aObject"></param>
349 /// <param name="aNode"></param>
350 private static TreeNode FindTreeNodeForEarObject(Ear.Object aObject, TreeNode aNode)
352 if (aNode.Tag == aObject)
357 foreach (TreeNode n in aNode.Nodes)
359 TreeNode found = FindTreeNodeForEarObject(aObject,n);
373 /// <param name="aObject"></param>
374 private void SelectEarObject(Ear.Object aObject)
376 foreach (TreeNode n in iTreeViewEvents.Nodes)
378 TreeNode found = FindTreeNodeForEarObject(aObject, n);
381 iTreeViewEvents.SelectedNode=found;
382 iTreeViewEvents.Focus();
389 /// Populate tree view with events and actions
391 private void PopulateTreeViewEvents(Ear.Object aSelectedObject=null)
394 iTreeViewEvents.Nodes.Clear();
395 //Populate registered events
396 foreach (Ear.Event e in Properties.Settings.Default.EarManager.Events)
398 //Create our event node
399 //Work out the name of our node
400 string eventNodeName = "";
401 if (!string.IsNullOrEmpty(e.Name))
403 //That event has a proper name, use it then
404 eventNodeName = $"{e.Name} - {e.Brief()}";
408 //Unnamed events just use brief
409 eventNodeName = e.Brief();
412 TreeNode eventNode = iTreeViewEvents.Nodes.Add(eventNodeName);
413 eventNode.Tag = e; //For easy access to our event
416 //Dim our nodes if disabled
417 eventNode.ForeColor = Color.DimGray;
420 //Add event description as child node
421 eventNode.Nodes.Add(e.AttributeDescription).ForeColor = eventNode.ForeColor;
422 //Create child node for actions root
423 TreeNode actionsNode = eventNode.Nodes.Add("Actions");
424 actionsNode.ForeColor = eventNode.ForeColor;
426 // Recursively add our actions for that event
427 AddActionsToTreeNode(actionsNode,e);
430 iTreeViewEvents.ExpandAll();
432 if (aSelectedObject != null)
434 SelectEarObject(aSelectedObject);
437 // Just to be safe in case the selection did not work
442 /// Called when our display is closed.
444 /// <param name="aDisplay"></param>
445 private void OnDisplayClosed(Display aDisplay)
447 //Our display was just closed, update our UI consequently
451 public void OnConnectivityChanged(NetworkManager aNetwork, NLM_CONNECTIVITY newConnectivity)
453 //Update network status
454 UpdateNetworkStatus();
458 /// Update our Network Status
460 private void UpdateNetworkStatus()
462 if (iDisplay.IsOpen())
464 iDisplay.SetIconOnOff(MiniDisplay.IconType.Internet,
465 iNetworkManager.NetworkListManager.IsConnectedToInternet);
466 iDisplay.SetIconOnOff(MiniDisplay.IconType.NetworkSignal, iNetworkManager.NetworkListManager.IsConnected);
471 int iLastNetworkIconIndex = 0;
472 int iUpdateCountSinceLastNetworkAnimation = 0;
477 private void UpdateNetworkSignal(DateTime aLastTickTime, DateTime aNewTickTime)
479 iUpdateCountSinceLastNetworkAnimation++;
480 iUpdateCountSinceLastNetworkAnimation = iUpdateCountSinceLastNetworkAnimation%4;
482 if (iDisplay.IsOpen() && iNetworkManager.NetworkListManager.IsConnected &&
483 iUpdateCountSinceLastNetworkAnimation == 0)
485 int iconCount = iDisplay.IconCount(MiniDisplay.IconType.NetworkSignal);
488 //Prevents div by zero and other undefined behavior
491 iLastNetworkIconIndex++;
492 iLastNetworkIconIndex = iLastNetworkIconIndex%(iconCount*2);
493 for (int i = 0; i < iconCount; i++)
495 if (i < iLastNetworkIconIndex && !(i == 0 && iLastNetworkIconIndex > 3) &&
496 !(i == 1 && iLastNetworkIconIndex > 4))
498 iDisplay.SetIconOn(MiniDisplay.IconType.NetworkSignal, i);
502 iDisplay.SetIconOff(MiniDisplay.IconType.NetworkSignal, i);
511 /// Receive volume change notification and reflect changes on our slider.
513 /// <param name="data"></param>
514 public void OnVolumeNotificationThreadSafe(object sender, AudioEndpointVolumeCallbackEventArgs aEvent)
516 UpdateMasterVolumeThreadSafe();
520 /// Update master volume when user moves our slider.
522 /// <param name="sender"></param>
523 /// <param name="e"></param>
524 private void trackBarMasterVolume_Scroll(object sender, EventArgs e)
526 //Just like Windows Volume Mixer we unmute if the volume is adjusted
527 iAudioEndpointVolume.IsMuted = false;
528 //Set volume level according to our volume slider new position
529 iAudioEndpointVolume.MasterVolumeLevelScalar = trackBarMasterVolume.Value/100.0f;
534 /// Mute check box changed.
536 /// <param name="sender"></param>
537 /// <param name="e"></param>
538 private void checkBoxMute_CheckedChanged(object sender, EventArgs e)
540 iAudioEndpointVolume.IsMuted = checkBoxMute.Checked;
544 /// Device State Changed
546 public void OnDeviceStateChanged([MarshalAs(UnmanagedType.LPWStr)] string deviceId,
547 [MarshalAs(UnmanagedType.I4)] DeviceState newState)
554 public void OnDeviceAdded([MarshalAs(UnmanagedType.LPWStr)] string pwstrDeviceId)
561 public void OnDeviceRemoved([MarshalAs(UnmanagedType.LPWStr)] string deviceId)
566 /// Default Device Changed
568 public void OnDefaultDeviceChanged(DataFlow flow, Role role,
569 [MarshalAs(UnmanagedType.LPWStr)] string defaultDeviceId)
571 if (role == Role.Multimedia && flow == DataFlow.Render)
573 UpdateAudioDeviceAndMasterVolumeThreadSafe();
578 /// Property Value Changed
580 /// <param name="pwstrDeviceId"></param>
581 /// <param name="key"></param>
582 public void OnPropertyValueChanged([MarshalAs(UnmanagedType.LPWStr)] string pwstrDeviceId, PropertyKey key)
590 /// Update master volume indicators based our current system states.
591 /// This typically includes volume levels and mute status.
593 private void UpdateMasterVolumeThreadSafe()
595 if (this.InvokeRequired)
597 //Not in the proper thread, invoke ourselves
598 PlainUpdateDelegate d = new PlainUpdateDelegate(UpdateMasterVolumeThreadSafe);
599 this.Invoke(d, new object[] {});
603 //Update volume slider
604 float volumeLevelScalar = iAudioEndpointVolume.MasterVolumeLevelScalar;
605 trackBarMasterVolume.Value = Convert.ToInt32(volumeLevelScalar*100);
606 //Update mute checkbox
607 checkBoxMute.Checked = iAudioEndpointVolume.IsMuted;
609 //If our display connection is open we need to update its icons
610 if (iDisplay.IsOpen())
612 //First take care our our volume level icons
613 int volumeIconCount = iDisplay.IconCount(MiniDisplay.IconType.Volume);
614 if (volumeIconCount > 0)
616 //Compute current volume level from system level and the number of segments in our display volume bar.
617 //That tells us how many segments in our volume bar needs to be turned on.
618 float currentVolume = volumeLevelScalar*volumeIconCount;
619 int segmentOnCount = Convert.ToInt32(currentVolume);
620 //Check if our segment count was rounded up, this will later be used for half brightness segment
621 bool roundedUp = segmentOnCount > currentVolume;
623 for (int i = 0; i < volumeIconCount; i++)
625 if (i < segmentOnCount)
627 //If we are dealing with our last segment and our segment count was rounded up then we will use half brightness.
628 if (i == segmentOnCount - 1 && roundedUp)
631 iDisplay.SetIconStatus(MiniDisplay.IconType.Volume, i,
632 (iDisplay.IconStatusCount(MiniDisplay.IconType.Volume) - 1)/2);
637 iDisplay.SetIconStatus(MiniDisplay.IconType.Volume, i,
638 iDisplay.IconStatusCount(MiniDisplay.IconType.Volume) - 1);
643 iDisplay.SetIconStatus(MiniDisplay.IconType.Volume, i, 0);
648 //Take care of our mute icon
649 iDisplay.SetIconOnOff(MiniDisplay.IconType.Mute, iAudioEndpointVolume.IsMuted);
657 private void StartAudioVisualization()
659 StopAudioVisualization();
660 //Open the default device
661 iSoundIn = new WasapiLoopbackCapture();
662 //Our loopback capture opens the default render device by default so the following is not needed
663 //iSoundIn.Device = MMDeviceEnumerator.DefaultAudioEndpoint(DataFlow.Render, Role.Console);
664 iSoundIn.Initialize();
666 SoundInSource soundInSource = new SoundInSource(iSoundIn);
667 ISampleSource source = soundInSource.ToSampleSource();
669 const FftSize fftSize = FftSize.Fft2048;
670 //create a spectrum provider which provides fft data based on some input
671 BasicSpectrumProvider spectrumProvider = new BasicSpectrumProvider(source.WaveFormat.Channels, source.WaveFormat.SampleRate, fftSize);
673 //linespectrum and voiceprint3dspectrum used for rendering some fft data
674 //in oder to get some fft data, set the previously created spectrumprovider
675 iLineSpectrum = new LineSpectrum(fftSize)
677 SpectrumProvider = spectrumProvider,
682 ScalingStrategy = ScalingStrategy.Decibel
686 //the SingleBlockNotificationStream is used to intercept the played samples
687 var notificationSource = new SingleBlockNotificationStream(source);
688 //pass the intercepted samples as input data to the spectrumprovider (which will calculate a fft based on them)
689 notificationSource.SingleBlockRead += (s, a) => spectrumProvider.Add(a.Left, a.Right);
691 iWaveSource = notificationSource.ToWaveSource(16);
694 // We need to read from our source otherwise SingleBlockRead is never called and our spectrum provider is not populated
695 byte[] buffer = new byte[iWaveSource.WaveFormat.BytesPerSecond / 2];
696 soundInSource.DataAvailable += (s, aEvent) =>
699 while ((read = iWaveSource.Read(buffer, 0, buffer.Length)) > 0) ;
710 private void StopAudioVisualization()
713 if (iSoundIn != null)
719 if (iWaveSource != null)
721 iWaveSource.Dispose();
731 private void UpdateAudioVisualization()
733 // No point if we don't have a current client
734 if (iCurrentClientData == null)
740 if (!iLineSpectrum.Update())
742 //Nothing changed no need to render
746 // Check if our current client has an Audio Visualizer field
747 // and render them as needed
748 foreach (DataField f in iCurrentClientData.Fields)
750 if (f is AudioVisualizerField)
752 AudioVisualizerField avf = (AudioVisualizerField)f;
753 Control ctrl = iTableLayoutPanel.GetControlFromPosition(avf.Column, avf.Row);
755 if (ctrl is PictureBox)
757 PictureBox pb = (PictureBox)ctrl;
758 if (iLineSpectrum.Render(pb.Image, Color.Black, Color.Black, Color.White, false))
771 private void UpdateAudioDeviceAndMasterVolumeThreadSafe()
773 if (this.InvokeRequired)
775 //Not in the proper thread, invoke ourselves
776 PlainUpdateDelegate d = new PlainUpdateDelegate(UpdateAudioDeviceAndMasterVolumeThreadSafe);
777 this.Invoke(d, new object[] {});
781 //We are in the correct thread just go ahead.
784 //Get our master volume
785 iMultiMediaDevice = iMultiMediaDeviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia);
786 iAudioEndpointVolume = AudioEndpointVolume.FromDevice(iMultiMediaDevice);
789 labelDefaultAudioDevice.Text = iMultiMediaDevice.FriendlyName;
791 //Show our volume in our track bar
792 UpdateMasterVolumeThreadSafe();
794 //Register to get volume modifications
795 AudioEndpointVolumeCallback callback = new AudioEndpointVolumeCallback();
796 callback.NotifyRecived += OnVolumeNotificationThreadSafe;
797 // Do we need to unregister?
798 iAudioEndpointVolume.RegisterControlChangeNotify(callback);
800 StartAudioVisualization();
802 trackBarMasterVolume.Enabled = true;
806 Debug.WriteLine("Exception thrown in UpdateAudioDeviceAndMasterVolume");
807 Debug.WriteLine(ex.ToString());
808 //Something went wrong S/PDIF device ca throw exception I guess
809 trackBarMasterVolume.Enabled = false;
816 private void PopulateDeviceTypes()
818 int count = Display.TypeCount();
820 for (int i = 0; i < count; i++)
822 comboBoxDisplayType.Items.Add(Display.TypeName((MiniDisplay.Type) i));
829 private void SetupTrayIcon()
831 iNotifyIcon.Icon = GetIcon("vfd.ico");
832 iNotifyIcon.Text = "Sharp Display Manager";
833 iNotifyIcon.Visible = true;
835 //Double click toggles visibility - typically brings up the application
836 iNotifyIcon.DoubleClick += delegate(object obj, EventArgs args)
841 //Adding a context menu, useful to be able to exit the application
842 ContextMenu contextMenu = new ContextMenu();
843 //Context menu item to toggle visibility
844 MenuItem hideShowItem = new MenuItem("Hide/Show");
845 hideShowItem.Click += delegate(object obj, EventArgs args)
849 contextMenu.MenuItems.Add(hideShowItem);
851 //Context menu item separator
852 contextMenu.MenuItems.Add(new MenuItem("-"));
854 //Context menu exit item
855 MenuItem exitItem = new MenuItem("Exit");
856 exitItem.Click += delegate(object obj, EventArgs args)
860 contextMenu.MenuItems.Add(exitItem);
862 iNotifyIcon.ContextMenu = contextMenu;
868 private void SetupRecordingNotification()
870 iRecordingNotification.Icon = GetIcon("record.ico");
871 iRecordingNotification.Text = "No recording";
872 iRecordingNotification.Visible = false;
876 /// Access icons from embedded resources.
878 /// <param name="aName"></param>
879 /// <returns></returns>
880 public static Icon GetIcon(string aName)
882 string[] names = Assembly.GetExecutingAssembly().GetManifestResourceNames();
883 foreach (string name in names)
885 //Find a resource name that ends with the given name
886 if (name.EndsWith(aName))
888 using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(name))
890 return new Icon(stream);
900 /// Set our current client.
901 /// This will take care of applying our client layout and set data fields.
903 /// <param name="aSessionId"></param>
904 void SetCurrentClient(string aSessionId, bool aForce = false)
906 if (aSessionId == iCurrentClientSessionId)
908 //Given client is already the current one.
909 //Don't bother changing anything then.
913 ClientData requestedClientData = iClients[aSessionId];
915 //Check when was the last time we switched to that client
916 if (iCurrentClientData != null)
918 //Do not switch client if priority of current client is higher
919 if (!aForce && requestedClientData.Priority < iCurrentClientData.Priority)
925 double lastSwitchToClientSecondsAgo = (DateTime.Now - iCurrentClientData.LastSwitchTime).TotalSeconds;
926 //TODO: put that hard coded value as a client property
927 //Clients should be able to define how often they can be interrupted
928 //Thus a background client can set this to zero allowing any other client to interrupt at any time
929 //We could also compute this delay by looking at the requests frequencies?
931 requestedClientData.Priority == iCurrentClientData.Priority &&
932 //Time sharing is only if clients have the same priority
933 (lastSwitchToClientSecondsAgo < 30)) //Make sure a client is on for at least 30 seconds
935 //Don't switch clients too often
940 //Set current client ID.
941 iCurrentClientSessionId = aSessionId;
942 //Set the time we last switched to that client
943 iClients[aSessionId].LastSwitchTime = DateTime.Now;
944 //Fetch and set current client data.
945 iCurrentClientData = requestedClientData;
946 //Apply layout and set data fields.
947 UpdateTableLayoutPanel(iCurrentClientData);
950 private void buttonFont_Click(object sender, EventArgs e)
952 //fontDialog.ShowColor = true;
953 //fontDialog.ShowApply = true;
954 fontDialog.ShowEffects = true;
955 fontDialog.Font = cds.Font;
957 fontDialog.FixedPitchOnly = checkBoxFixedPitchFontOnly.Checked;
959 //fontDialog.ShowHelp = true;
961 //fontDlg.MaxSize = 40;
962 //fontDlg.MinSize = 22;
964 //fontDialog.Parent = this;
965 //fontDialog.StartPosition = FormStartPosition.CenterParent;
967 //DlgBox.ShowDialog(fontDialog);
969 //if (fontDialog.ShowDialog(this) != DialogResult.Cancel)
970 if (DlgBox.ShowDialog(fontDialog) != DialogResult.Cancel)
972 //Set the fonts to all our labels in our layout
973 foreach (Control ctrl in iTableLayoutPanel.Controls)
975 if (ctrl is MarqueeLabel)
977 ((MarqueeLabel) ctrl).Font = fontDialog.Font;
982 cds.Font = fontDialog.Font;
983 Properties.Settings.Default.Save();
992 void CheckFontHeight()
994 //Show font height and width
995 labelFontHeight.Text = "Font height: " + cds.Font.Height;
996 float charWidth = IsFixedWidth(cds.Font);
997 if (charWidth == 0.0f)
999 labelFontWidth.Visible = false;
1003 labelFontWidth.Visible = true;
1004 labelFontWidth.Text = "Font width: " + charWidth;
1007 MarqueeLabel label = null;
1008 //Get the first label control we can find
1009 foreach (Control ctrl in iTableLayoutPanel.Controls)
1011 if (ctrl is MarqueeLabel)
1013 label = (MarqueeLabel) ctrl;
1018 //Now check font height and show a warning if needed.
1019 if (label != null && label.Font.Height > label.Height)
1021 labelWarning.Text = "WARNING: Selected font is too height by " + (label.Font.Height - label.Height) +
1023 labelWarning.Visible = true;
1027 labelWarning.Visible = false;
1032 private void buttonCapture_Click(object sender, EventArgs e)
1034 System.Drawing.Bitmap bmp = new System.Drawing.Bitmap(iTableLayoutPanel.Width, iTableLayoutPanel.Height);
1035 iTableLayoutPanel.DrawToBitmap(bmp, iTableLayoutPanel.ClientRectangle);
1036 //Bitmap bmpToSave = new Bitmap(bmp);
1037 bmp.Save("D:\\capture.png");
1039 ((MarqueeLabel) iTableLayoutPanel.Controls[0]).Text = "Captured";
1042 string outputFileName = "d:\\capture.png";
1043 using (MemoryStream memory = new MemoryStream())
1045 using (FileStream fs = new FileStream(outputFileName, FileMode.OpenOrCreate, FileAccess.ReadWrite))
1047 bmp.Save(memory, System.Drawing.Imaging.ImageFormat.Png);
1048 byte[] bytes = memory.ToArray();
1049 fs.Write(bytes, 0, bytes.Length);
1056 private void CheckForRequestResults()
1058 if (iDisplay.IsRequestPending())
1060 switch (iDisplay.AttemptRequestCompletion())
1062 case MiniDisplay.Request.FirmwareRevision:
1063 toolStripStatusLabelConnect.Text += " v" + iDisplay.FirmwareRevision();
1064 //Issue next request then
1065 iDisplay.RequestPowerSupplyStatus();
1068 case MiniDisplay.Request.PowerSupplyStatus:
1069 if (iDisplay.PowerSupplyStatus())
1071 toolStripStatusLabelPower.Text = "ON";
1075 toolStripStatusLabelPower.Text = "OFF";
1077 //Issue next request then
1078 iDisplay.RequestDeviceId();
1081 case MiniDisplay.Request.DeviceId:
1082 toolStripStatusLabelConnect.Text += " - " + iDisplay.DeviceId();
1083 //No more request to issue
1089 public static uint ColorWhiteIsOn(int aX, int aY, uint aPixel)
1091 if ((aPixel & 0x00FFFFFF) == 0x00FFFFFF)
1098 public static uint ColorUntouched(int aX, int aY, uint aPixel)
1103 public static uint ColorInversed(int aX, int aY, uint aPixel)
1108 public static uint ColorChessboard(int aX, int aY, uint aPixel)
1110 if ((aX%2 == 0) && (aY%2 == 0))
1114 else if ((aX%2 != 0) && (aY%2 != 0))
1122 public static int ScreenReversedX(System.Drawing.Bitmap aBmp, int aX)
1124 return aBmp.Width - aX - 1;
1127 public int ScreenReversedY(System.Drawing.Bitmap aBmp, int aY)
1129 return iBmp.Height - aY - 1;
1132 public int ScreenX(System.Drawing.Bitmap aBmp, int aX)
1137 public int ScreenY(System.Drawing.Bitmap aBmp, int aY)
1143 /// Select proper pixel delegates according to our current settings.
1145 private void SetupPixelDelegates()
1147 //Select our pixel processing routine
1148 if (cds.InverseColors)
1150 //iColorFx = ColorChessboard;
1151 iColorFx = ColorInversed;
1155 iColorFx = ColorWhiteIsOn;
1158 //Select proper coordinate translation functions
1159 //We used delegate/function pointer to support reverse screen without doing an extra test on each pixels
1160 if (cds.ReverseScreen)
1162 iScreenX = ScreenReversedX;
1163 iScreenY = ScreenReversedY;
1174 /// This is our timer tick responsible to perform our render
1175 /// TODO: Use a threading timer instead of a Windows form timer.
1177 /// <param name="sender"></param>
1178 /// <param name="e"></param>
1179 private void timer_Tick(object sender, EventArgs e)
1181 //Update our animations
1182 DateTime NewTickTime = DateTime.Now;
1184 UpdateNetworkSignal(LastTickTime, NewTickTime);
1186 //Update animation for all our marquees
1187 foreach (Control ctrl in iTableLayoutPanel.Controls)
1189 if (ctrl is MarqueeLabel)
1191 ((MarqueeLabel) ctrl).UpdateAnimation(LastTickTime, NewTickTime);
1195 UpdateAudioVisualization();
1197 //Update our display
1198 if (iDisplay.IsOpen())
1200 CheckForRequestResults();
1202 //Check if frame rendering is needed
1203 //Typically used when showing clock
1204 if (!iSkipFrameRendering)
1209 iBmp = new System.Drawing.Bitmap(iTableLayoutPanel.Width, iTableLayoutPanel.Height,
1210 PixelFormat.Format32bppArgb);
1211 iCreateBitmap = false;
1213 iTableLayoutPanel.DrawToBitmap(iBmp, iTableLayoutPanel.ClientRectangle);
1214 //iBmp.Save("D:\\capture.png");
1216 //Send it to our display
1217 for (int i = 0; i < iBmp.Width; i++)
1219 for (int j = 0; j < iBmp.Height; j++)
1223 //Get our processed pixel coordinates
1224 int x = iScreenX(iBmp, i);
1225 int y = iScreenY(iBmp, j);
1227 uint color = (uint) iBmp.GetPixel(i, j).ToArgb();
1228 //Apply color effects
1229 color = iColorFx(x, y, color);
1231 iDisplay.SetPixel(x, y, color);
1236 iDisplay.SwapBuffers();
1240 //Compute instant FPS
1241 toolStripStatusLabelFps.Text = (1.0/NewTickTime.Subtract(LastTickTime).TotalSeconds).ToString("F0") + " / " +
1242 (1000/iTimerDisplay.Interval).ToString() + " FPS";
1244 LastTickTime = NewTickTime;
1249 /// Attempt to establish connection with our display hardware.
1251 private void OpenDisplayConnection()
1253 CloseDisplayConnection();
1255 if (!iDisplay.Open((MiniDisplay.Type) cds.DisplayType))
1258 toolStripStatusLabelConnect.Text = "Connection error";
1262 private void CloseDisplayConnection()
1264 //Status will be updated upon receiving the closed event
1266 if (iDisplay == null || !iDisplay.IsOpen())
1271 //Do not clear if we gave up on rendering already.
1272 //This means we will keep on displaying clock on MDM166AA for instance.
1273 if (!iSkipFrameRendering)
1276 iDisplay.SwapBuffers();
1279 iDisplay.SetAllIconsStatus(0); //Turn off all icons
1283 private void buttonOpen_Click(object sender, EventArgs e)
1285 OpenDisplayConnection();
1288 private void buttonClose_Click(object sender, EventArgs e)
1290 CloseDisplayConnection();
1293 private void buttonClear_Click(object sender, EventArgs e)
1296 iDisplay.SwapBuffers();
1299 private void buttonFill_Click(object sender, EventArgs e)
1302 iDisplay.SwapBuffers();
1305 private void trackBarBrightness_Scroll(object sender, EventArgs e)
1307 cds.Brightness = trackBarBrightness.Value;
1308 Properties.Settings.Default.Save();
1309 iDisplay.SetBrightness(trackBarBrightness.Value);
1315 /// CDS stands for Current Display Settings
1317 private DisplaySettings cds
1321 DisplaysSettings settings = Properties.Settings.Default.DisplaysSettings;
1322 if (settings == null)
1324 settings = new DisplaysSettings();
1326 Properties.Settings.Default.DisplaysSettings = settings;
1329 //Make sure all our settings have been created
1330 while (settings.Displays.Count <= Properties.Settings.Default.CurrentDisplayIndex)
1332 settings.Displays.Add(new DisplaySettings());
1335 DisplaySettings displaySettings = settings.Displays[Properties.Settings.Default.CurrentDisplayIndex];
1336 return displaySettings;
1341 /// Check if the given font has a fixed character pitch.
1343 /// <param name="ft"></param>
1344 /// <returns>0.0f if this is not a monospace font, otherwise returns the character width.</returns>
1345 public float IsFixedWidth(Font ft)
1347 Graphics g = CreateGraphics();
1348 char[] charSizes = new char[] {'i', 'a', 'Z', '%', '#', 'a', 'B', 'l', 'm', ',', '.'};
1349 float charWidth = g.MeasureString("I", ft, Int32.MaxValue, StringFormat.GenericTypographic).Width;
1351 bool fixedWidth = true;
1353 foreach (char c in charSizes)
1354 if (g.MeasureString(c.ToString(), ft, Int32.MaxValue, StringFormat.GenericTypographic).Width !=
1367 /// Synchronize UI with settings
1369 private void UpdateStatus()
1372 checkBoxShowBorders.Checked = cds.ShowBorders;
1373 iTableLayoutPanel.CellBorderStyle = (cds.ShowBorders
1374 ? TableLayoutPanelCellBorderStyle.Single
1375 : TableLayoutPanelCellBorderStyle.None);
1377 //Set the proper font to each of our labels
1378 foreach (MarqueeLabel ctrl in iTableLayoutPanel.Controls)
1380 ctrl.Font = cds.Font;
1384 //Check if "run on Windows startup" is enabled
1385 checkBoxAutoStart.Checked = iStartupManager.Startup;
1388 comboBoxHdmiPort.SelectedIndex = Properties.Settings.Default.CecHdmiPort - 1;
1390 //Mini Display settings
1391 checkBoxReverseScreen.Checked = cds.ReverseScreen;
1392 checkBoxInverseColors.Checked = cds.InverseColors;
1393 checkBoxShowVolumeLabel.Checked = cds.ShowVolumeLabel;
1394 checkBoxScaleToFit.Checked = cds.ScaleToFit;
1395 maskedTextBoxMinFontSize.Enabled = cds.ScaleToFit;
1396 labelMinFontSize.Enabled = cds.ScaleToFit;
1397 maskedTextBoxMinFontSize.Text = cds.MinFontSize.ToString();
1398 maskedTextBoxScrollingSpeed.Text = cds.ScrollingSpeedInPixelsPerSecond.ToString();
1399 comboBoxDisplayType.SelectedIndex = cds.DisplayType;
1400 iTimerDisplay.Interval = cds.TimerInterval;
1401 maskedTextBoxTimerInterval.Text = cds.TimerInterval.ToString();
1402 textBoxScrollLoopSeparator.Text = cds.Separator;
1404 SetupPixelDelegates();
1406 if (iDisplay.IsOpen())
1408 //We have a display connection
1409 //Reflect that in our UI
1412 iTableLayoutPanel.Enabled = true;
1413 panelDisplay.Enabled = true;
1415 //Only setup brightness if display is open
1416 trackBarBrightness.Minimum = iDisplay.MinBrightness();
1417 trackBarBrightness.Maximum = iDisplay.MaxBrightness();
1418 if (cds.Brightness < iDisplay.MinBrightness() || cds.Brightness > iDisplay.MaxBrightness())
1420 //Brightness out of range, this can occur when using auto-detect
1421 //Use max brightness instead
1422 trackBarBrightness.Value = iDisplay.MaxBrightness();
1423 iDisplay.SetBrightness(iDisplay.MaxBrightness());
1427 trackBarBrightness.Value = cds.Brightness;
1428 iDisplay.SetBrightness(cds.Brightness);
1431 //Try compute the steps to something that makes sense
1432 trackBarBrightness.LargeChange = Math.Max(1, (iDisplay.MaxBrightness() - iDisplay.MinBrightness())/5);
1433 trackBarBrightness.SmallChange = 1;
1436 buttonFill.Enabled = true;
1437 buttonClear.Enabled = true;
1438 buttonOpen.Enabled = false;
1439 buttonClose.Enabled = true;
1440 trackBarBrightness.Enabled = true;
1441 toolStripStatusLabelConnect.Text = "Connected - " + iDisplay.Vendor() + " - " + iDisplay.Product();
1442 //+ " - " + iDisplay.SerialNumber();
1444 if (iDisplay.SupportPowerOnOff())
1446 buttonPowerOn.Enabled = true;
1447 buttonPowerOff.Enabled = true;
1451 buttonPowerOn.Enabled = false;
1452 buttonPowerOff.Enabled = false;
1455 if (iDisplay.SupportClock())
1457 buttonShowClock.Enabled = true;
1458 buttonHideClock.Enabled = true;
1462 buttonShowClock.Enabled = false;
1463 buttonHideClock.Enabled = false;
1467 //Check if Volume Label is supported. To date only MDM166AA supports that crap :)
1468 checkBoxShowVolumeLabel.Enabled = iDisplay.IconCount(MiniDisplay.IconType.VolumeLabel) > 0;
1470 if (cds.ShowVolumeLabel)
1472 iDisplay.SetIconOn(MiniDisplay.IconType.VolumeLabel);
1476 iDisplay.SetIconOff(MiniDisplay.IconType.VolumeLabel);
1481 //Display connection not available
1482 //Reflect that in our UI
1484 //In debug start our timer even if we don't have a display connection
1487 //In production environment we don't need our timer if no display connection
1490 checkBoxShowVolumeLabel.Enabled = false;
1491 iTableLayoutPanel.Enabled = false;
1492 panelDisplay.Enabled = false;
1493 buttonFill.Enabled = false;
1494 buttonClear.Enabled = false;
1495 buttonOpen.Enabled = true;
1496 buttonClose.Enabled = false;
1497 trackBarBrightness.Enabled = false;
1498 buttonPowerOn.Enabled = false;
1499 buttonPowerOff.Enabled = false;
1500 buttonShowClock.Enabled = false;
1501 buttonHideClock.Enabled = false;
1502 toolStripStatusLabelConnect.Text = "Disconnected";
1503 toolStripStatusLabelPower.Text = "N/A";
1512 /// <param name="sender"></param>
1513 /// <param name="e"></param>
1514 private void checkBoxShowVolumeLabel_CheckedChanged(object sender, EventArgs e)
1516 cds.ShowVolumeLabel = checkBoxShowVolumeLabel.Checked;
1517 Properties.Settings.Default.Save();
1521 private void checkBoxShowBorders_CheckedChanged(object sender, EventArgs e)
1523 //Save our show borders setting
1524 iTableLayoutPanel.CellBorderStyle = (checkBoxShowBorders.Checked
1525 ? TableLayoutPanelCellBorderStyle.Single
1526 : TableLayoutPanelCellBorderStyle.None);
1527 cds.ShowBorders = checkBoxShowBorders.Checked;
1528 Properties.Settings.Default.Save();
1532 private void checkBoxAutoStart_CheckedChanged(object sender, EventArgs e)
1534 iStartupManager.Startup = checkBoxAutoStart.Checked;
1538 private void checkBoxReverseScreen_CheckedChanged(object sender, EventArgs e)
1540 //Save our reverse screen setting
1541 cds.ReverseScreen = checkBoxReverseScreen.Checked;
1542 Properties.Settings.Default.Save();
1543 SetupPixelDelegates();
1546 private void checkBoxInverseColors_CheckedChanged(object sender, EventArgs e)
1548 //Save our inverse colors setting
1549 cds.InverseColors = checkBoxInverseColors.Checked;
1550 Properties.Settings.Default.Save();
1551 SetupPixelDelegates();
1554 private void checkBoxScaleToFit_CheckedChanged(object sender, EventArgs e)
1556 //Save our scale to fit setting
1557 cds.ScaleToFit = checkBoxScaleToFit.Checked;
1558 Properties.Settings.Default.Save();
1560 labelMinFontSize.Enabled = cds.ScaleToFit;
1561 maskedTextBoxMinFontSize.Enabled = cds.ScaleToFit;
1564 private void MainForm_Resize(object sender, EventArgs e)
1566 if (WindowState == FormWindowState.Minimized)
1568 // To workaround our empty bitmap bug on Windows 7 we need to recreate our bitmap when the application is minimized
1569 // That's apparently not needed on Windows 10 but we better leave it in place.
1570 iCreateBitmap = true;
1574 private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
1576 //TODO: discard other CSCore audio objects
1577 StopAudioVisualization();
1579 iNetworkManager.Dispose();
1580 CloseDisplayConnection();
1582 e.Cancel = iClosing;
1585 public void StartServer()
1587 iServiceHost = new ServiceHost
1590 new Uri[] {new Uri("net.tcp://localhost:8001/")}
1593 iServiceHost.AddServiceEndpoint(typeof(IService), new NetTcpBinding(SecurityMode.None, true),
1595 iServiceHost.Open();
1598 public void StopServer()
1600 if (iClients.Count > 0 && !iClosing)
1604 BroadcastCloseEvent();
1609 MessageBox.Show("Force exit?", "Waiting for clients...", MessageBoxButtons.YesNo,
1610 MessageBoxIcon.Warning) == DialogResult.Yes)
1612 iClosing = false; //We make sure we force close if asked twice
1617 //We removed that as it often lags for some reason
1618 //iServiceHost.Close();
1622 public void BroadcastCloseEvent()
1624 Trace.TraceInformation("BroadcastCloseEvent - start");
1626 var inactiveClients = new List<string>();
1627 foreach (var client in iClients)
1629 //if (client.Key != eventData.ClientName)
1633 Trace.TraceInformation("BroadcastCloseEvent - " + client.Key);
1634 client.Value.Callback.OnCloseOrder( /*eventData*/);
1636 catch (Exception ex)
1638 inactiveClients.Add(client.Key);
1643 if (inactiveClients.Count > 0)
1645 foreach (var client in inactiveClients)
1647 iClients.Remove(client);
1648 Program.iFormMain.iTreeViewClients.Nodes.Remove(
1649 Program.iFormMain.iTreeViewClients.Nodes.Find(client, false)[0]);
1653 if (iClients.Count == 0)
1660 /// Just remove all our fields.
1662 private void ClearLayout()
1664 iTableLayoutPanel.Controls.Clear();
1665 iTableLayoutPanel.RowStyles.Clear();
1666 iTableLayoutPanel.ColumnStyles.Clear();
1667 iCurrentClientData = null;
1671 /// Just launch a demo client.
1673 private void StartNewClient(string aTopText = "", string aBottomText = "")
1675 Thread clientThread = new Thread(SharpDisplayClient.Program.MainWithParams);
1676 SharpDisplayClient.StartParams myParams = new SharpDisplayClient.StartParams(
1677 new Point(this.Right, this.Top), aTopText, aBottomText);
1678 clientThread.Start(myParams);
1683 /// Just launch our idle client.
1685 private void StartIdleClient(string aTopText = "", string aBottomText = "")
1687 Thread clientThread = new Thread(SharpDisplayClientIdle.Program.MainWithParams);
1688 SharpDisplayClientIdle.StartParams myParams =
1689 new SharpDisplayClientIdle.StartParams(new Point(this.Right, this.Top), aTopText, aBottomText);
1690 clientThread.Start(myParams);
1695 private void buttonStartClient_Click(object sender, EventArgs e)
1700 private void buttonSuspend_Click(object sender, EventArgs e)
1705 private void StartTimer()
1707 LastTickTime = DateTime.Now; //Reset timer to prevent jump
1708 iTimerDisplay.Enabled = true;
1709 UpdateSuspendButton();
1712 private void StopTimer()
1714 LastTickTime = DateTime.Now; //Reset timer to prevent jump
1715 iTimerDisplay.Enabled = false;
1716 UpdateSuspendButton();
1719 private void ToggleTimer()
1721 LastTickTime = DateTime.Now; //Reset timer to prevent jump
1722 iTimerDisplay.Enabled = !iTimerDisplay.Enabled;
1723 UpdateSuspendButton();
1726 private void UpdateSuspendButton()
1728 if (!iTimerDisplay.Enabled)
1730 buttonSuspend.Text = "Run";
1734 buttonSuspend.Text = "Pause";
1739 private void buttonCloseClients_Click(object sender, EventArgs e)
1741 BroadcastCloseEvent();
1744 private void treeViewClients_AfterSelect(object sender, TreeViewEventArgs e)
1746 //Root node must have at least one child
1747 if (e.Node.Nodes.Count == 0)
1752 //If the selected node is the root node of a client then switch to it
1753 string sessionId = e.Node.Nodes[0].Text; //First child of a root node is the sessionId
1754 if (iClients.ContainsKey(sessionId)) //Check that's actually what we are looking at
1756 //We have a valid session just switch to that client
1757 SetCurrentClient(sessionId, true);
1766 /// <param name="aSessionId"></param>
1767 /// <param name="aCallback"></param>
1768 public void AddClientThreadSafe(string aSessionId, ICallback aCallback)
1770 if (this.InvokeRequired)
1772 //Not in the proper thread, invoke ourselves
1773 AddClientDelegate d = new AddClientDelegate(AddClientThreadSafe);
1774 this.Invoke(d, new object[] {aSessionId, aCallback});
1778 //We are in the proper thread
1779 //Add this session to our collection of clients
1780 ClientData newClient = new ClientData(aSessionId, aCallback);
1781 Program.iFormMain.iClients.Add(aSessionId, newClient);
1782 //Add this session to our UI
1783 UpdateClientTreeViewNode(newClient);
1789 /// Find the client with the highest priority if any.
1791 /// <returns>Our highest priority client or null if not a single client is connected.</returns>
1792 public ClientData FindHighestPriorityClient()
1794 ClientData highestPriorityClient = null;
1795 foreach (var client in iClients)
1797 if (highestPriorityClient == null || client.Value.Priority > highestPriorityClient.Priority)
1799 highestPriorityClient = client.Value;
1803 return highestPriorityClient;
1809 /// <param name="aSessionId"></param>
1810 public void RemoveClientThreadSafe(string aSessionId)
1812 if (this.InvokeRequired)
1814 //Not in the proper thread, invoke ourselves
1815 RemoveClientDelegate d = new RemoveClientDelegate(RemoveClientThreadSafe);
1816 this.Invoke(d, new object[] {aSessionId});
1820 //We are in the proper thread
1821 //Remove this session from both client collection and UI tree view
1822 if (Program.iFormMain.iClients.Keys.Contains(aSessionId))
1824 Program.iFormMain.iClients.Remove(aSessionId);
1825 Program.iFormMain.iTreeViewClients.Nodes.Remove(
1826 Program.iFormMain.iTreeViewClients.Nodes.Find(aSessionId, false)[0]);
1827 //Update recording status too whenever a client is removed
1828 UpdateRecordingNotification();
1831 if (iCurrentClientSessionId == aSessionId)
1833 //The current client is closing
1834 iCurrentClientData = null;
1835 //Find the client with the highest priority and set it as current
1836 ClientData newCurrentClient = FindHighestPriorityClient();
1837 if (newCurrentClient != null)
1839 SetCurrentClient(newCurrentClient.SessionId, true);
1843 if (iClients.Count == 0)
1845 //Clear our screen when last client disconnects
1850 //We were closing our form
1851 //All clients are now closed
1852 //Just resume our close operation
1863 /// <param name="aSessionId"></param>
1864 /// <param name="aLayout"></param>
1865 public void SetClientLayoutThreadSafe(string aSessionId, TableLayout aLayout)
1867 if (this.InvokeRequired)
1869 //Not in the proper thread, invoke ourselves
1870 SetLayoutDelegate d = new SetLayoutDelegate(SetClientLayoutThreadSafe);
1871 this.Invoke(d, new object[] {aSessionId, aLayout});
1875 ClientData client = iClients[aSessionId];
1878 //Don't change a thing if the layout is the same
1879 if (!client.Layout.IsSameAs(aLayout))
1881 Debug.Print("SetClientLayoutThreadSafe: Layout updated.");
1882 //Set our client layout then
1883 client.Layout = aLayout;
1884 //So that next time we update all our fields at ones
1885 client.HasNewLayout = true;
1886 //Layout has changed clear our fields then
1887 client.Fields.Clear();
1889 UpdateClientTreeViewNode(client);
1893 Debug.Print("SetClientLayoutThreadSafe: Layout has not changed.");
1902 /// <param name="aSessionId"></param>
1903 /// <param name="aField"></param>
1904 public void SetClientFieldThreadSafe(string aSessionId, DataField aField)
1906 if (this.InvokeRequired)
1908 //Not in the proper thread, invoke ourselves
1909 SetFieldDelegate d = new SetFieldDelegate(SetClientFieldThreadSafe);
1910 this.Invoke(d, new object[] {aSessionId, aField});
1914 //We are in the proper thread
1915 //Call the non-thread-safe variant
1916 SetClientField(aSessionId, aField);
1924 /// Set a data field in the given client.
1926 /// <param name="aSessionId"></param>
1927 /// <param name="aField"></param>
1928 private void SetClientField(string aSessionId, DataField aField)
1930 //TODO: should check if the field actually changed?
1932 ClientData client = iClients[aSessionId];
1933 bool layoutChanged = false;
1934 bool contentChanged = true;
1936 //Fetch our field index
1937 int fieldIndex = client.FindSameFieldIndex(aField);
1941 //No corresponding field, just bail out
1945 //Keep our previous field in there
1946 DataField previousField = client.Fields[fieldIndex];
1947 //Just update that field then
1948 client.Fields[fieldIndex] = aField;
1950 if (!aField.IsTableField)
1952 //We are done then if that field is not in our table layout
1956 TableField tableField = (TableField) aField;
1958 if (previousField.IsSameLayout(aField))
1960 //If we are updating a field in our current client we need to update it in our panel
1961 if (aSessionId == iCurrentClientSessionId)
1963 Control ctrl = iTableLayoutPanel.GetControlFromPosition(tableField.Column, tableField.Row);
1964 if (aField.IsTextField && ctrl is MarqueeLabel)
1966 TextField textField = (TextField)aField;
1967 //Text field control already in place, just change the text
1968 MarqueeLabel label = (MarqueeLabel)ctrl;
1969 contentChanged = (label.Text != textField.Text || label.TextAlign != textField.Alignment);
1970 label.Text = textField.Text;
1971 label.TextAlign = textField.Alignment;
1973 else if (aField.IsBitmapField && ctrl is PictureBox)
1975 BitmapField bitmapField = (BitmapField)aField;
1976 contentChanged = true; //TODO: Bitmap comp or should we leave that to clients?
1977 //Bitmap field control already in place just change the bitmap
1978 PictureBox pictureBox = (PictureBox)ctrl;
1979 pictureBox.Image = bitmapField.Bitmap;
1981 else if (aField is AudioVisualizerField && ctrl is PictureBox)
1983 contentChanged = false; // Since nothing was changed
1987 layoutChanged = true;
1993 layoutChanged = true;
1996 //If either content or layout changed we need to update our tree view to reflect the changes
1997 if (contentChanged || layoutChanged)
1999 UpdateClientTreeViewNode(client);
2003 Debug.Print("Layout changed");
2004 //Our layout has changed, if we are already the current client we need to update our panel
2005 if (aSessionId == iCurrentClientSessionId)
2007 //Apply layout and set data fields.
2008 UpdateTableLayoutPanel(iCurrentClientData);
2013 Debug.Print("Layout has not changed.");
2018 Debug.Print("WARNING: content and layout have not changed!");
2021 //When a client field is set we try switching to this client to present the new information to our user
2022 SetCurrentClient(aSessionId);
2028 /// <param name="aTexts"></param>
2029 public void SetClientFieldsThreadSafe(string aSessionId, System.Collections.Generic.IList<DataField> aFields)
2031 if (this.InvokeRequired)
2033 //Not in the proper thread, invoke ourselves
2034 SetFieldsDelegate d = new SetFieldsDelegate(SetClientFieldsThreadSafe);
2035 this.Invoke(d, new object[] {aSessionId, aFields});
2039 ClientData client = iClients[aSessionId];
2041 if (client.HasNewLayout)
2043 //TODO: Assert client.Count == 0
2044 //Our layout was just changed
2045 //Do some special handling to avoid re-creating our panel N times, once for each fields
2046 client.HasNewLayout = false;
2047 //Just set all our fields then
2048 client.Fields.AddRange(aFields);
2049 //Try switch to that client
2050 SetCurrentClient(aSessionId);
2052 //If we are updating the current client update our panel
2053 if (aSessionId == iCurrentClientSessionId)
2055 //Apply layout and set data fields.
2056 UpdateTableLayoutPanel(iCurrentClientData);
2059 UpdateClientTreeViewNode(client);
2063 //Put each our text fields in a label control
2064 foreach (DataField field in aFields)
2066 SetClientField(aSessionId, field);
2075 /// <param name="aSessionId"></param>
2076 /// <param name="aName"></param>
2077 public void SetClientNameThreadSafe(string aSessionId, string aName)
2079 if (this.InvokeRequired)
2081 //Not in the proper thread, invoke ourselves
2082 SetClientNameDelegate d = new SetClientNameDelegate(SetClientNameThreadSafe);
2083 this.Invoke(d, new object[] {aSessionId, aName});
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 SetClientPriorityDelegate d = new SetClientPriorityDelegate(SetClientPriorityThreadSafe);
2107 this.Invoke(d, new object[] {aSessionId, aPriority});
2111 //We are in the proper thread
2113 ClientData client = iClients[aSessionId];
2117 client.Priority = aPriority;
2118 //Update our tree-view
2119 UpdateClientTreeViewNode(client);
2120 //Change our current client as per new priority
2121 ClientData newCurrentClient = FindHighestPriorityClient();
2122 if (newCurrentClient != null)
2124 SetCurrentClient(newCurrentClient.SessionId);
2133 /// <param name="value"></param>
2134 /// <param name="maxChars"></param>
2135 /// <returns></returns>
2136 public static string Truncate(string value, int maxChars)
2138 return value.Length <= maxChars ? value : value.Substring(0, maxChars - 3) + "...";
2142 /// Update our recording notification.
2144 private void UpdateRecordingNotification()
2147 bool activeRecording = false;
2149 RecordingField recField = new RecordingField();
2150 foreach (var client in iClients)
2152 RecordingField rec = (RecordingField) client.Value.FindSameFieldAs(recField);
2153 if (rec != null && rec.IsActive)
2155 activeRecording = true;
2156 //Don't break cause we are collecting the names/texts.
2157 if (!String.IsNullOrEmpty(rec.Text))
2159 text += (rec.Text + "\n");
2163 //Not text for that recording, use client name instead
2164 text += client.Value.Name + " recording\n";
2170 //Update our text no matter what, can't have more than 63 characters otherwise it throws an exception.
2171 iRecordingNotification.Text = Truncate(text, 63);
2173 //Change visibility of notification if needed
2174 if (iRecordingNotification.Visible != activeRecording)
2176 iRecordingNotification.Visible = activeRecording;
2177 //Assuming the notification icon is in sync with our display icon
2178 //Take care of our REC icon
2179 if (iDisplay.IsOpen())
2181 iDisplay.SetIconOnOff(MiniDisplay.IconType.Recording, activeRecording);
2189 /// <param name="aClient"></param>
2190 private void UpdateClientTreeViewNode(ClientData aClient)
2192 Debug.Print("UpdateClientTreeViewNode");
2194 if (aClient == null)
2199 //Hook in record icon update too
2200 UpdateRecordingNotification();
2202 TreeNode node = null;
2203 //Check that our client node already exists
2204 //Get our client root node using its key which is our session ID
2205 TreeNode[] nodes = iTreeViewClients.Nodes.Find(aClient.SessionId, false);
2206 if (nodes.Count() > 0)
2208 //We already have a node for that client
2210 //Clear children as we are going to recreate them below
2215 //Client node does not exists create a new one
2216 iTreeViewClients.Nodes.Add(aClient.SessionId, aClient.SessionId);
2217 node = iTreeViewClients.Nodes.Find(aClient.SessionId, false)[0];
2223 if (!String.IsNullOrEmpty(aClient.Name))
2225 //We have a name, use it as text for our root node
2226 node.Text = aClient.Name;
2227 //Add a child with SessionId
2228 node.Nodes.Add(new TreeNode(aClient.SessionId));
2232 //No name, use session ID instead
2233 node.Text = aClient.SessionId;
2236 //Display client priority
2237 node.Nodes.Add(new TreeNode("Priority: " + aClient.Priority));
2239 if (aClient.Fields.Count > 0)
2241 //Create root node for our texts
2242 TreeNode textsRoot = new TreeNode("Fields");
2243 node.Nodes.Add(textsRoot);
2244 //For each text add a new entry
2245 foreach (DataField field in aClient.Fields)
2247 if (field.IsTextField)
2249 TextField textField = (TextField) field;
2250 textsRoot.Nodes.Add(new TreeNode("[Text]" + textField.Text));
2252 else if (field.IsBitmapField)
2254 textsRoot.Nodes.Add(new TreeNode("[Bitmap]"));
2256 else if (field is AudioVisualizerField)
2258 textsRoot.Nodes.Add(new TreeNode("[Audio Visualizer]"));
2260 else if (field.IsRecordingField)
2262 RecordingField recordingField = (RecordingField) field;
2263 textsRoot.Nodes.Add(new TreeNode("[Recording]" + recordingField.IsActive));
2273 /// Update our table layout row styles to make sure each rows have similar height
2275 private void UpdateTableLayoutRowStyles()
2277 foreach (RowStyle rowStyle in iTableLayoutPanel.RowStyles)
2279 rowStyle.SizeType = SizeType.Percent;
2280 rowStyle.Height = 100/iTableLayoutPanel.RowCount;
2285 /// Update our display table layout.
2286 /// Will instanciated every field control as defined by our client.
2287 /// Fields must be specified by rows from the left.
2289 /// <param name="aLayout"></param>
2290 private void UpdateTableLayoutPanel(ClientData aClient)
2292 Debug.Print("UpdateTableLayoutPanel");
2294 if (aClient == null)
2301 TableLayout layout = aClient.Layout;
2303 //First clean our current panel
2304 iTableLayoutPanel.Controls.Clear();
2305 iTableLayoutPanel.RowStyles.Clear();
2306 iTableLayoutPanel.ColumnStyles.Clear();
2307 iTableLayoutPanel.RowCount = 0;
2308 iTableLayoutPanel.ColumnCount = 0;
2310 //Then recreate our rows...
2311 while (iTableLayoutPanel.RowCount < layout.Rows.Count)
2313 iTableLayoutPanel.RowCount++;
2317 while (iTableLayoutPanel.ColumnCount < layout.Columns.Count)
2319 iTableLayoutPanel.ColumnCount++;
2323 for (int i = 0; i < iTableLayoutPanel.ColumnCount; i++)
2325 //Create our column styles
2326 this.iTableLayoutPanel.ColumnStyles.Add(layout.Columns[i]);
2329 for (int j = 0; j < iTableLayoutPanel.RowCount; j++)
2333 //Create our row styles
2334 this.iTableLayoutPanel.RowStyles.Add(layout.Rows[j]);
2344 foreach (DataField field in aClient.Fields)
2346 if (!field.IsTableField)
2348 //That field is not taking part in our table layout skip it
2352 TableField tableField = (TableField) field;
2354 //Create a control corresponding to the field specified for that cell
2355 Control control = CreateControlForDataField(tableField);
2357 //Add newly created control to our table layout at the specified row and column
2358 iTableLayoutPanel.Controls.Add(control, tableField.Column, tableField.Row);
2359 //Make sure we specify column and row span for that new control
2360 iTableLayoutPanel.SetColumnSpan(control, tableField.ColumnSpan);
2361 iTableLayoutPanel.SetRowSpan(control, tableField.RowSpan);
2369 /// Check our type of data field and create corresponding control
2371 /// <param name="aField"></param>
2372 private Control CreateControlForDataField(DataField aField)
2374 Control control = null;
2375 if (aField.IsTextField)
2377 MarqueeLabel label = new SharpDisplayManager.MarqueeLabel();
2378 label.AutoEllipsis = true;
2379 label.AutoSize = true;
2380 label.BackColor = System.Drawing.Color.Transparent;
2381 label.Dock = System.Windows.Forms.DockStyle.Fill;
2382 label.Location = new System.Drawing.Point(1, 1);
2383 label.Margin = new System.Windows.Forms.Padding(0);
2384 label.Name = "marqueeLabel" + aField;
2385 label.OwnTimer = false;
2386 label.PixelsPerSecond = cds.ScrollingSpeedInPixelsPerSecond;
2387 label.Separator = cds.Separator;
2388 label.MinFontSize = cds.MinFontSize;
2389 label.ScaleToFit = cds.ScaleToFit;
2390 //control.Size = new System.Drawing.Size(254, 30);
2391 //control.TabIndex = 2;
2392 label.Font = cds.Font;
2394 TextField field = (TextField)aField;
2395 label.TextAlign = field.Alignment;
2396 label.UseCompatibleTextRendering = true;
2397 label.Text = field.Text;
2401 else if (aField.IsBitmapField)
2403 //Create picture box
2404 PictureBox picture = new PictureBox();
2405 picture.AutoSize = true;
2406 picture.BackColor = System.Drawing.Color.Transparent;
2407 picture.Dock = System.Windows.Forms.DockStyle.Fill;
2408 picture.Location = new System.Drawing.Point(1, 1);
2409 picture.Margin = new System.Windows.Forms.Padding(0);
2410 picture.Name = "pictureBox" + aField;
2412 BitmapField field = (BitmapField)aField;
2413 picture.Image = field.Bitmap;
2417 else if (aField is AudioVisualizerField)
2419 //Create picture box
2420 PictureBox picture = new PictureBox();
2421 picture.AutoSize = true;
2422 picture.BackColor = System.Drawing.Color.Transparent;
2423 picture.Dock = System.Windows.Forms.DockStyle.Fill;
2424 picture.Location = new System.Drawing.Point(1, 1);
2425 picture.Margin = new System.Windows.Forms.Padding(0);
2426 picture.Name = "pictureBox" + aField;
2427 picture.SizeChanged += (sender, e) =>
2429 // Somehow bitmap created when our from is invisible are not working
2430 // Mark our form visibility status
2431 bool visible = Visible;
2432 // Make sure it's visible
2434 // Adjust our bitmap size when control size changes
2435 picture.Image = new System.Drawing.Bitmap(picture.Width, picture.Height, PixelFormat.Format32bppArgb);
2436 // Restore our form visibility
2443 //TODO: Handle recording field?
2449 /// Called when the user selected a new display type.
2451 /// <param name="sender"></param>
2452 /// <param name="e"></param>
2453 private void comboBoxDisplayType_SelectedIndexChanged(object sender, EventArgs e)
2455 //Store the selected display type in our settings
2456 Properties.Settings.Default.CurrentDisplayIndex = comboBoxDisplayType.SelectedIndex;
2457 cds.DisplayType = comboBoxDisplayType.SelectedIndex;
2458 Properties.Settings.Default.Save();
2460 //Try re-opening the display connection if we were already connected.
2461 //Otherwise just update our status to reflect display type change.
2462 if (iDisplay.IsOpen())
2464 OpenDisplayConnection();
2472 private void maskedTextBoxTimerInterval_TextChanged(object sender, EventArgs e)
2474 if (maskedTextBoxTimerInterval.Text != "")
2476 int interval = Convert.ToInt32(maskedTextBoxTimerInterval.Text);
2480 iTimerDisplay.Interval = interval;
2481 cds.TimerInterval = iTimerDisplay.Interval;
2482 Properties.Settings.Default.Save();
2487 private void maskedTextBoxMinFontSize_TextChanged(object sender, EventArgs e)
2489 if (maskedTextBoxMinFontSize.Text != "")
2491 int minFontSize = Convert.ToInt32(maskedTextBoxMinFontSize.Text);
2493 if (minFontSize > 0)
2495 cds.MinFontSize = minFontSize;
2496 Properties.Settings.Default.Save();
2497 //We need to recreate our layout for that change to take effect
2498 UpdateTableLayoutPanel(iCurrentClientData);
2504 private void maskedTextBoxScrollingSpeed_TextChanged(object sender, EventArgs e)
2506 if (maskedTextBoxScrollingSpeed.Text != "")
2508 int scrollingSpeed = Convert.ToInt32(maskedTextBoxScrollingSpeed.Text);
2510 if (scrollingSpeed > 0)
2512 cds.ScrollingSpeedInPixelsPerSecond = scrollingSpeed;
2513 Properties.Settings.Default.Save();
2514 //We need to recreate our layout for that change to take effect
2515 UpdateTableLayoutPanel(iCurrentClientData);
2520 private void textBoxScrollLoopSeparator_TextChanged(object sender, EventArgs e)
2522 cds.Separator = textBoxScrollLoopSeparator.Text;
2523 Properties.Settings.Default.Save();
2525 //Update our text fields
2526 foreach (MarqueeLabel ctrl in iTableLayoutPanel.Controls)
2528 ctrl.Separator = cds.Separator;
2533 private void buttonPowerOn_Click(object sender, EventArgs e)
2538 private void buttonPowerOff_Click(object sender, EventArgs e)
2540 iDisplay.PowerOff();
2543 private void buttonShowClock_Click(object sender, EventArgs e)
2548 private void buttonHideClock_Click(object sender, EventArgs e)
2553 private void buttonUpdate_Click(object sender, EventArgs e)
2555 InstallUpdateSyncWithInfo();
2563 if (!iDisplay.IsOpen())
2568 //Devices like MDM166AA don't support windowing and frame rendering must be stopped while showing our clock
2569 iSkipFrameRendering = true;
2572 iDisplay.SwapBuffers();
2573 //Then show our clock
2574 iDisplay.ShowClock();
2582 if (!iDisplay.IsOpen())
2587 //Devices like MDM166AA don't support windowing and frame rendering must be stopped while showing our clock
2588 iSkipFrameRendering = false;
2589 iDisplay.HideClock();
2592 private void InstallUpdateSyncWithInfo()
2594 UpdateCheckInfo info = null;
2596 if (ApplicationDeployment.IsNetworkDeployed)
2598 ApplicationDeployment ad = ApplicationDeployment.CurrentDeployment;
2602 info = ad.CheckForDetailedUpdate();
2605 catch (DeploymentDownloadException dde)
2608 "The new version of the application cannot be downloaded at this time. \n\nPlease check your network connection, or try again later. Error: " +
2612 catch (InvalidDeploymentException ide)
2615 "Cannot check for a new version of the application. The ClickOnce deployment is corrupt. Please redeploy the application and try again. Error: " +
2619 catch (InvalidOperationException ioe)
2622 "This application cannot be updated. It is likely not a ClickOnce application. Error: " +
2627 if (info.UpdateAvailable)
2629 Boolean doUpdate = true;
2631 if (!info.IsUpdateRequired)
2634 MessageBox.Show("An update is available. Would you like to update the application now?",
2635 "Update Available", MessageBoxButtons.OKCancel);
2636 if (!(DialogResult.OK == dr))
2643 // Display a message that the application MUST reboot. Display the minimum required version.
2644 MessageBox.Show("This application has detected a mandatory update from your current " +
2645 "version to version " + info.MinimumRequiredVersion.ToString() +
2646 ". The application will now install the update and restart.",
2647 "Update Available", MessageBoxButtons.OK,
2648 MessageBoxIcon.Information);
2656 MessageBox.Show("The application has been upgraded, and will now restart.");
2657 Application.Restart();
2659 catch (DeploymentDownloadException dde)
2662 "Cannot install the latest version of the application. \n\nPlease check your network connection, or try again later. Error: " +
2670 MessageBox.Show("You are already running the latest version.", "Application up-to-date");
2679 private void SysTrayHideShow()
2685 WindowState = FormWindowState.Normal;
2690 /// Use to handle minimize events.
2692 /// <param name="sender"></param>
2693 /// <param name="e"></param>
2694 private void MainForm_SizeChanged(object sender, EventArgs e)
2696 if (WindowState == FormWindowState.Minimized && Properties.Settings.Default.MinimizeToTray)
2708 /// <param name="sender"></param>
2709 /// <param name="e"></param>
2710 private void tableLayoutPanel_SizeChanged(object sender, EventArgs e)
2712 //Our table layout size has changed which means our display size has changed.
2713 //We need to re-create our bitmap.
2714 iCreateBitmap = true;
2718 /// Broadcast messages to subscribers.
2720 /// <param name="message"></param>
2721 protected override void WndProc(ref Message aMessage)
2723 if (OnWndProc != null)
2725 OnWndProc(ref aMessage);
2728 base.WndProc(ref aMessage);
2731 private void iCheckBoxCecEnabled_CheckedChanged(object sender, EventArgs e)
2737 private void comboBoxHdmiPort_SelectedIndexChanged(object sender, EventArgs e)
2739 //Save CEC HDMI port
2740 Properties.Settings.Default.CecHdmiPort = Convert.ToByte(comboBoxHdmiPort.SelectedIndex);
2741 Properties.Settings.Default.CecHdmiPort++;
2742 Properties.Settings.Default.Save();
2750 private void ResetCec()
2752 if (iCecManager == null)
2754 //Thus skipping initial UI setup
2760 if (Properties.Settings.Default.CecEnabled)
2762 iCecManager.Start(Handle, "CEC",
2763 Properties.Settings.Default.CecHdmiPort);
2772 private async void ResetHarmonyAsync(bool aForceAuth=false)
2774 // ConnectAsync already if we have an existing session cookie
2775 if (Properties.Settings.Default.HarmonyEnabled)
2779 iButtonHarmonyConnect.Enabled = false;
2780 await ConnectHarmonyAsync(aForceAuth);
2782 catch (Exception ex)
2784 Trace.WriteLine("Exception thrown by ConnectHarmonyAsync");
2785 Trace.WriteLine(ex.ToString());
2789 iButtonHarmonyConnect.Enabled = true;
2797 /// <param name="sender"></param>
2798 /// <param name="e"></param>
2799 private void iButtonHarmonyConnect_Click(object sender, EventArgs e)
2801 // User is explicitaly trying to connect
2802 //Reset Harmony Hub connection forcing authentication
2803 ResetHarmonyAsync(true);
2809 /// <param name="sender"></param>
2810 /// <param name="e"></param>
2811 private void iCheckBoxHarmonyEnabled_CheckedChanged(object sender, EventArgs e)
2813 iButtonHarmonyConnect.Enabled = iCheckBoxHarmonyEnabled.Checked;
2819 private void SetupCecLogLevel()
2822 iCecManager.Client.LogLevel = 0;
2824 if (checkBoxCecLogError.Checked)
2825 iCecManager.Client.LogLevel |= (int) CecLogLevel.Error;
2827 if (checkBoxCecLogWarning.Checked)
2828 iCecManager.Client.LogLevel |= (int) CecLogLevel.Warning;
2830 if (checkBoxCecLogNotice.Checked)
2831 iCecManager.Client.LogLevel |= (int) CecLogLevel.Notice;
2833 if (checkBoxCecLogTraffic.Checked)
2834 iCecManager.Client.LogLevel |= (int) CecLogLevel.Traffic;
2836 if (checkBoxCecLogDebug.Checked)
2837 iCecManager.Client.LogLevel |= (int) CecLogLevel.Debug;
2839 iCecManager.Client.FilterOutPollLogs = checkBoxCecLogNoPoll.Checked;
2843 private void ButtonStartIdleClient_Click(object sender, EventArgs e)
2848 private void buttonClearLogs_Click(object sender, EventArgs e)
2850 richTextBoxLogs.Clear();
2853 private void checkBoxCecLogs_CheckedChanged(object sender, EventArgs e)
2863 /// Get the current event based on event tree view selection.
2865 /// <returns></returns>
2866 private Ear.Event CurrentEvent()
2868 //Walk up the tree from the selected node to find our event
2869 TreeNode node = iTreeViewEvents.SelectedNode;
2870 Ear.Event selectedEvent = null;
2871 while (node != null)
2873 if (node.Tag is Ear.Event)
2875 selectedEvent = (Ear.Event) node.Tag;
2881 return selectedEvent;
2885 /// Get the current action based on event tree view selection
2887 /// <returns></returns>
2888 private Ear.Action CurrentAction()
2890 TreeNode node = iTreeViewEvents.SelectedNode;
2891 if (node != null && node.Tag is Ear.Action)
2893 return (Ear.Action) node.Tag;
2902 /// <returns></returns>
2903 private Ear.Object CurrentEarObject()
2905 Ear.Action a = CurrentAction();
2906 Ear.Event e = CurrentEvent();
2917 /// Get the current action based on event tree view selection
2919 /// <returns></returns>
2920 private Ear.Object CurrentEarParent()
2922 TreeNode node = iTreeViewEvents.SelectedNode;
2923 if (node == null || node.Parent == null)
2928 if (node.Parent.Tag is Ear.Object)
2930 return (Ear.Object)node.Parent.Tag;
2933 if (node.Parent.Parent != null && node.Parent.Parent.Tag is Ear.Object)
2935 //Can be the case for events
2936 return (Ear.Object)node.Parent.Parent.Tag;
2946 /// <param name="sender"></param>
2947 /// <param name="e"></param>
2948 private void buttonActionAdd_Click(object sender, EventArgs e)
2950 Ear.Object parent = CurrentEarObject();
2951 if (parent == null )
2953 //We did not find a corresponding event or action
2957 FormEditObject<Ear.Action> ea = new FormEditObject<Ear.Action>();
2958 ea.Text = "Add action";
2959 DialogResult res = CodeProject.Dialog.DlgBox.ShowDialog(ea);
2960 if (res == DialogResult.OK)
2962 parent.Objects.Add(ea.Object);
2963 Properties.Settings.Default.Save();
2964 // We want to select the parent so that one can easily add another action to the same collection
2965 PopulateTreeViewEvents(parent);
2972 /// <param name="sender"></param>
2973 /// <param name="e"></param>
2974 private void buttonActionEdit_Click(object sender, EventArgs e)
2976 Ear.Action selectedAction = CurrentAction();
2977 Ear.Object parent = CurrentEarParent()
2979 if (parent == null || selectedAction == null)
2981 //We did not find a corresponding parent
2985 FormEditObject<Ear.Action> ea = new FormEditObject<Ear.Action>();
2986 ea.Text = "Edit action";
2987 ea.Object = selectedAction;
2988 int actionIndex = iTreeViewEvents.SelectedNode.Index;
2989 DialogResult res = CodeProject.Dialog.DlgBox.ShowDialog(ea);
2990 if (res == DialogResult.OK)
2992 //Make sure we keep the same children as before
2993 ea.Object.Objects = parent.Objects[actionIndex].Objects;
2995 parent.Objects[actionIndex]=ea.Object;
2996 //Save and rebuild our event tree view
2997 Properties.Settings.Default.Save();
2998 PopulateTreeViewEvents(ea.Object);
3005 /// <param name="sender"></param>
3006 /// <param name="e"></param>
3007 private void buttonActionDelete_Click(object sender, EventArgs e)
3009 Ear.Action action = CurrentAction();
3012 //Must select action node
3016 Properties.Settings.Default.EarManager.RemoveAction(action);
3017 Properties.Settings.Default.Save();
3018 PopulateTreeViewEvents();
3024 /// <param name="sender"></param>
3025 /// <param name="e"></param>
3026 private void buttonActionTest_Click(object sender, EventArgs e)
3028 Ear.Action a = CurrentAction();
3033 iTreeViewEvents.Focus();
3039 /// <param name="sender"></param>
3040 /// <param name="e"></param>
3041 private void buttonActionMoveUp_Click(object sender, EventArgs e)
3043 Ear.Action a = CurrentAction();
3045 //Action already at the top of the list
3046 iTreeViewEvents.SelectedNode.Index == 0)
3051 //Swap actions in event's action list
3052 Ear.Object parent = CurrentEarParent();
3053 int currentIndex = iTreeViewEvents.SelectedNode.Index;
3054 Ear.Action movingUp = parent.Objects[currentIndex] as Ear.Action;
3055 Ear.Action movingDown = parent.Objects[currentIndex-1] as Ear.Action;
3056 parent.Objects[currentIndex] = movingDown;
3057 parent.Objects[currentIndex-1] = movingUp;
3059 //Save and populate our tree again
3060 Properties.Settings.Default.Save();
3061 PopulateTreeViewEvents(a);
3067 /// <param name="sender"></param>
3068 /// <param name="e"></param>
3069 private void buttonActionMoveDown_Click(object sender, EventArgs e)
3071 Ear.Action a = CurrentAction();
3073 //Action already at the bottom of the list
3074 iTreeViewEvents.SelectedNode.Index == iTreeViewEvents.SelectedNode.Parent.Nodes.Count - 1)
3079 //Swap actions in event's action list
3080 Ear.Object parent = CurrentEarParent();
3081 int currentIndex = iTreeViewEvents.SelectedNode.Index;
3082 Ear.Action movingDown = parent.Objects[currentIndex] as Ear.Action;
3083 Ear.Action movingUp = parent.Objects[currentIndex + 1] as Ear.Action;
3084 parent.Objects[currentIndex] = movingUp;
3085 parent.Objects[currentIndex + 1] = movingDown;
3087 //Save and populate our tree again
3088 Properties.Settings.Default.Save();
3089 PopulateTreeViewEvents(a);
3096 /// <param name="sender"></param>
3097 /// <param name="e"></param>
3098 private void buttonEventTest_Click(object sender, EventArgs e)
3100 Ear.Event earEvent = CurrentEvent();
3101 if (earEvent != null)
3108 /// Manages events and actions buttons according to selected item in event tree.
3110 /// <param name="sender"></param>
3111 /// <param name="e"></param>
3112 private void iTreeViewEvents_AfterSelect(object sender, TreeViewEventArgs e)
3120 private void UpdateEventView()
3122 //One can always add an event
3123 buttonEventAdd.Enabled = true;
3125 //Enable buttons according to selected item
3126 buttonActionAdd.Enabled =
3127 buttonEventTest.Enabled =
3128 buttonEventDelete.Enabled =
3129 buttonEventEdit.Enabled =
3130 CurrentEvent() != null;
3132 Ear.Action currentAction = CurrentAction();
3133 //If an action is selected enable the following buttons
3134 buttonActionTest.Enabled =
3135 buttonActionDelete.Enabled =
3136 buttonActionMoveUp.Enabled =
3137 buttonActionMoveDown.Enabled =
3138 buttonActionEdit.Enabled =
3139 currentAction != null;
3141 if (currentAction != null)
3143 //If an action is selected enable move buttons if needed
3144 buttonActionMoveUp.Enabled = iTreeViewEvents.SelectedNode.Index != 0;
3145 buttonActionMoveDown.Enabled = iTreeViewEvents.SelectedNode.Index <
3146 iTreeViewEvents.SelectedNode.Parent.Nodes.Count - 1;
3153 /// <param name="sender"></param>
3154 /// <param name="e"></param>
3155 private void buttonEventAdd_Click(object sender, EventArgs e)
3157 FormEditObject<Ear.Event> ea = new FormEditObject<Ear.Event>();
3158 ea.Text = "Add event";
3159 DialogResult res = CodeProject.Dialog.DlgBox.ShowDialog(ea);
3160 if (res == DialogResult.OK)
3162 Properties.Settings.Default.EarManager.Events.Add(ea.Object);
3163 Properties.Settings.Default.Save();
3164 PopulateTreeViewEvents(ea.Object);
3171 /// <param name="sender"></param>
3172 /// <param name="e"></param>
3173 private void buttonEventDelete_Click(object sender, EventArgs e)
3175 Ear.Event currentEvent = CurrentEvent();
3176 if (currentEvent == null)
3178 //Must select action node
3182 Properties.Settings.Default.EarManager.Events.Remove(currentEvent);
3183 Properties.Settings.Default.Save();
3184 PopulateTreeViewEvents();
3190 /// <param name="sender"></param>
3191 /// <param name="e"></param>
3192 private void buttonEventEdit_Click(object sender, EventArgs e)
3194 Ear.Event selectedEvent = CurrentEvent();
3195 if (selectedEvent == null)
3197 //We did not find a corresponding event
3201 FormEditObject<Ear.Event> ea = new FormEditObject<Ear.Event>();
3202 ea.Text = "Edit event";
3203 ea.Object = selectedEvent;
3204 int index = iTreeViewEvents.SelectedNode.Index;
3205 DialogResult res = CodeProject.Dialog.DlgBox.ShowDialog(ea);
3206 if (res == DialogResult.OK)
3208 //Make sure we keep the same actions as before
3209 ea.Object.Objects = Properties.Settings.Default.EarManager.Events[index].Objects;
3211 Properties.Settings.Default.EarManager.Events[index] = ea.Object;
3212 //Save and rebuild our event tree view
3213 Properties.Settings.Default.Save();
3214 PopulateTreeViewEvents(ea.Object);
3221 /// <param name="sender"></param>
3222 /// <param name="e"></param>
3223 private void iTreeViewEvents_Leave(object sender, EventArgs e)
3225 //Make sure our event tree never looses focus
3226 ((TreeView) sender).Focus();
3230 /// Called whenever we loose connection with our HarmonyHub.
3232 /// <param name="aRequestWasCancelled"></param>
3233 private void HarmonyConnectionClosed(object aSender, bool aClosedByServer)
3235 if (aClosedByServer)
3237 //Try reconnect then
3238 #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
3239 BeginInvoke(new MethodInvoker(delegate () { ResetHarmonyAsync(); }));
3240 #pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
3246 int iHarmonyReconnectTries = 0;
3247 const int KHarmonyMaxReconnectTries = 10;
3252 /// <returns></returns>
3253 private async Task ConnectHarmonyAsync(bool aForceAuth=false)
3255 if (Program.HarmonyClient != null)
3257 await Program.HarmonyClient.CloseAsync();
3260 bool success = false;
3262 //Reset Harmony client & config
3263 Program.HarmonyClient = null;
3264 Program.HarmonyConfig = null;
3265 iTreeViewHarmony.Nodes.Clear();
3267 Trace.WriteLine("Harmony: Connecting... ");
3268 //First create our client and login
3269 //Tip: Set keep-alive to false when testing reconnection process
3270 Program.HarmonyClient = new HarmonyHub.Client(iTextBoxHarmonyHubAddress.Text, true);
3271 Program.HarmonyClient.OnConnectionClosed += HarmonyConnectionClosed;
3273 string authToken = Properties.Settings.Default.LogitechAuthToken;
3274 if (!string.IsNullOrEmpty(authToken) && !aForceAuth)
3276 Trace.WriteLine("Harmony: Reusing token: {0}", authToken);
3277 success = await Program.HarmonyClient.TryOpenAsync(authToken);
3280 if (!Program.HarmonyClient.IsReady || !success
3281 // Only first failure triggers new Harmony server AUTH
3282 // That's to avoid calling upon Logitech servers too often
3283 && iHarmonyReconnectTries == 0 )
3285 //We failed to connect using our token
3287 Trace.WriteLine("Harmony: Reseting authentication token!");
3288 Properties.Settings.Default.LogitechAuthToken = "";
3289 Properties.Settings.Default.Save();
3291 Trace.WriteLine("Harmony: Authenticating with Logitech servers...");
3292 success = await Program.HarmonyClient.TryOpenAsync();
3293 //Persist our authentication token in our setting
3296 Trace.WriteLine("Harmony: Saving authentication token.");
3297 Properties.Settings.Default.LogitechAuthToken = Program.HarmonyClient.Token;
3298 Properties.Settings.Default.Save();
3302 // I've seen this failing with "Policy lookup failed on server".
3303 Program.HarmonyConfig = await Program.HarmonyClient.TryGetConfigAsync();
3304 if (Program.HarmonyConfig == null)
3310 // So we now have our Harmony Configuration
3311 PopulateTreeViewHarmony(Program.HarmonyConfig);
3312 // Make sure harmony command actions are showing device name instead of device id
3313 PopulateTreeViewEvents(CurrentEarObject());
3316 // TODO: Consider putting the retry logic one level higher in ResetHarmonyAsync
3319 // See if we need to keep trying
3320 if (iHarmonyReconnectTries < KHarmonyMaxReconnectTries)
3322 iHarmonyReconnectTries++;
3323 Trace.WriteLine("Harmony: Failed to connect, try again: " + iHarmonyReconnectTries);
3324 await ConnectHarmonyAsync();
3328 Trace.WriteLine("Harmony: Failed to connect, giving up!");
3329 iHarmonyReconnectTries = 0;
3330 // TODO: Could use a data member as timer rather than a new instance.
3331 // Try that again in 5 minutes then.
3332 // Using Windows Form timer to make sure we run in the UI thread.
3333 System.Windows.Forms.Timer timer = new System.Windows.Forms.Timer();
3334 timer.Tick += async delegate (object sender, EventArgs e)
3336 // Stop our timer first as we won't need it anymore
3337 (sender as System.Windows.Forms.Timer).Stop();
3338 // Then try to connect again
3339 await ConnectHarmonyAsync();
3341 timer.Interval = 300000;
3347 // We are connected with a valid Harmony Configuration
3348 // Reset our tries counter then
3349 iHarmonyReconnectTries = 0;
3356 /// <param name="aConfig"></param>
3357 private void PopulateTreeViewHarmony(HarmonyHub.Config aConfig)
3359 iTreeViewHarmony.Nodes.Clear();
3361 foreach (HarmonyHub.Device device in aConfig.Devices)
3363 TreeNode deviceNode = iTreeViewHarmony.Nodes.Add(device.Id, $"{device.Label} ({device.DeviceTypeDisplayName}/{device.Model})");
3364 deviceNode.Tag = device;
3366 foreach (HarmonyHub.ControlGroup cg in device.ControlGroups)
3368 TreeNode cgNode = deviceNode.Nodes.Add(cg.Name);
3371 foreach (HarmonyHub.Function f in cg.Functions)
3373 TreeNode fNode = cgNode.Nodes.Add(f.Label);
3379 //treeViewConfig.ExpandAll();
3382 private async void iTreeViewHarmony_NodeMouseDoubleClick(object sender, TreeNodeMouseClickEventArgs e)
3384 //Upon function node double click we execute it
3385 var tag = e.Node.Tag as HarmonyHub.Function;
3386 if (tag != null && e.Node.Parent.Parent.Tag is HarmonyHub.Device)
3388 HarmonyHub.Function f = tag;
3389 HarmonyHub.Device d = (HarmonyHub.Device)e.Node.Parent.Parent.Tag;
3391 Trace.WriteLine($"Harmony: Sending {f.Label} to {d.Label}...");
3393 await Program.HarmonyClient.TrySendKeyPressAsync(d.Id, f.Action.Command);