Adding support for enumerated action property edition.
2 // Copyright (C) 2014-2015 Stéphane Lenclud.
4 // This file is part of SharpDisplayManager.
6 // SharpDisplayManager is free software: you can redistribute it and/or modify
7 // it under the terms of the GNU General Public License as published by
8 // the Free Software Foundation, either version 3 of the License, or
9 // (at your option) any later version.
11 // SharpDisplayManager is distributed in the hope that it will be useful,
12 // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 // GNU General Public License for more details.
16 // You should have received a copy of the GNU General Public License
17 // along with SharpDisplayManager. If not, see <http://www.gnu.org/licenses/>.
21 using System.Collections.Generic;
22 using System.ComponentModel;
27 using System.Threading.Tasks;
28 using System.Windows.Forms;
30 using CodeProject.Dialog;
31 using System.Drawing.Imaging;
32 using System.ServiceModel;
33 using System.Threading;
34 using System.Diagnostics;
35 using System.Deployment.Application;
36 using System.Reflection;
38 using NAudio.CoreAudioApi;
39 using NAudio.CoreAudioApi.Interfaces;
40 using System.Runtime.InteropServices;
45 using SharpDisplayClient;
47 using MiniDisplayInterop;
48 using SharpLib.Display;
51 namespace SharpDisplayManager
54 public delegate uint ColorProcessingDelegate(int aX, int aY, uint aPixel);
55 public delegate int CoordinateTranslationDelegate(System.Drawing.Bitmap aBmp, int aInt);
56 //Delegates are used for our thread safe method
57 public delegate void AddClientDelegate(string aSessionId, ICallback aCallback);
58 public delegate void RemoveClientDelegate(string aSessionId);
59 public delegate void SetFieldDelegate(string SessionId, DataField aField);
60 public delegate void SetFieldsDelegate(string SessionId, System.Collections.Generic.IList<DataField> aFields);
61 public delegate void SetLayoutDelegate(string SessionId, TableLayout aLayout);
62 public delegate void SetClientNameDelegate(string aSessionId, string aName);
63 public delegate void SetClientPriorityDelegate(string aSessionId, uint aPriority);
64 public delegate void PlainUpdateDelegate();
65 public delegate void WndProcDelegate(ref Message aMessage);
68 /// Our Display manager main form
70 [System.ComponentModel.DesignerCategory("Form")]
71 public partial class MainForm : MainFormHid, IMMNotificationClient
73 //public ManagerEventAction iManager = new ManagerEventAction();
74 DateTime LastTickTime;
76 System.Drawing.Bitmap iBmp;
77 bool iCreateBitmap; //Workaround render to bitmap issues when minimized
78 ServiceHost iServiceHost;
79 // Our collection of clients sorted by session id.
80 public Dictionary<string, ClientData> iClients;
81 // The name of the client which informations are currently displayed.
82 public string iCurrentClientSessionId;
83 ClientData iCurrentClientData;
87 public bool iSkipFrameRendering;
88 //Function pointer for pixel color filtering
89 ColorProcessingDelegate iColorFx;
90 //Function pointer for pixel X coordinate intercept
91 CoordinateTranslationDelegate iScreenX;
92 //Function pointer for pixel Y coordinate intercept
93 CoordinateTranslationDelegate iScreenY;
95 private MMDeviceEnumerator iMultiMediaDeviceEnumerator;
96 private MMDevice iMultiMediaDevice;
98 private NetworkManager iNetworkManager;
101 /// CEC - Consumer Electronic Control.
102 /// Notably used to turn TV on and off as Windows broadcast monitor on and off notifications.
104 private ConsumerElectronicControl iCecManager;
107 /// Manage run when Windows startup option
109 private StartupManager iStartupManager;
112 /// System notification icon used to hide our application from the task bar.
114 private SharpLib.Notification.Control iNotifyIcon;
117 /// System recording notification icon.
119 private SharpLib.Notification.Control iRecordingNotification;
124 RichTextBoxTextWriter iWriter;
128 /// Allow user to receive window messages;
130 public event WndProcDelegate OnWndProc;
134 ManagerEventAction.Current = Properties.Settings.Default.Actions;
135 if (ManagerEventAction.Current == null)
137 //No actions in our settings yet
138 ManagerEventAction.Current = new ManagerEventAction();
139 Properties.Settings.Default.Actions = ManagerEventAction.Current;
143 //We loaded actions from our settings
144 //We need to hook them with corresponding events
145 ManagerEventAction.Current.Init();
147 iSkipFrameRendering = false;
149 iCurrentClientSessionId = "";
150 iCurrentClientData = null;
151 LastTickTime = DateTime.Now;
152 //Instantiate our display and register for events notifications
153 iDisplay = new Display();
154 iDisplay.OnOpened += OnDisplayOpened;
155 iDisplay.OnClosed += OnDisplayClosed;
157 iClients = new Dictionary<string, ClientData>();
158 iStartupManager = new StartupManager();
159 iNotifyIcon = new SharpLib.Notification.Control();
160 iRecordingNotification = new SharpLib.Notification.Control();
162 //Have our designer initialize its controls
163 InitializeComponent();
165 //Redirect console output
166 iWriter = new RichTextBoxTextWriter(richTextBoxLogs);
167 Console.SetOut(iWriter);
169 //Populate device types
170 PopulateDeviceTypes();
172 //Populate optical drives
173 PopulateOpticalDrives();
175 //Initial status update
178 //We have a bug when drawing minimized and reusing our bitmap
179 //Though I could not reproduce it on Windows 10
180 iBmp = new System.Drawing.Bitmap(iTableLayoutPanel.Width, iTableLayoutPanel.Height, PixelFormat.Format32bppArgb);
181 iCreateBitmap = false;
183 //Minimize our window if desired
184 if (Properties.Settings.Default.StartMinimized)
186 WindowState = FormWindowState.Minimized;
194 /// <param name="sender"></param>
195 /// <param name="e"></param>
196 private void MainForm_Load(object sender, EventArgs e)
198 //Check if we are running a Click Once deployed application
199 if (ApplicationDeployment.IsNetworkDeployed)
201 //This is a proper Click Once installation, fetch and show our version number
202 this.Text += " - v" + ApplicationDeployment.CurrentDeployment.CurrentVersion;
206 //Not a proper Click Once installation, assuming development build then
207 this.Text += " - development";
211 iMultiMediaDeviceEnumerator = new MMDeviceEnumerator();
212 iMultiMediaDeviceEnumerator.RegisterEndpointNotificationCallback(this);
213 UpdateAudioDeviceAndMasterVolumeThreadSafe();
216 iNetworkManager = new NetworkManager();
217 iNetworkManager.OnConnectivityChanged += OnConnectivityChanged;
218 UpdateNetworkStatus();
221 iCecManager = new ConsumerElectronicControl();
222 OnWndProc += iCecManager.OnWndProc;
226 PopulateEventsTreeView();
228 //Setup notification icon
231 //Setup recording notification
232 SetupRecordingNotification();
234 // To make sure start up with minimize to tray works
235 if (WindowState == FormWindowState.Minimized && Properties.Settings.Default.MinimizeToTray)
241 //When not debugging we want the screen to be empty until a client takes over
244 //When developing we want at least one client for testing
245 StartNewClient("abcdefghijklmnopqrst-0123456789","ABCDEFGHIJKLMNOPQRST-0123456789");
248 //Open display connection on start-up if needed
249 if (Properties.Settings.Default.DisplayConnectOnStartup)
251 OpenDisplayConnection();
254 //Start our server so that we can get client requests
257 //Register for HID events
258 RegisterHidDevices();
260 //Start Idle client if needed
261 if (Properties.Settings.Default.StartIdleClient)
268 /// Called when our display is opened.
270 /// <param name="aDisplay"></param>
271 private void OnDisplayOpened(Display aDisplay)
273 //Make sure we resume frame rendering
274 iSkipFrameRendering = false;
276 //Set our screen size now that our display is connected
277 //Our panelDisplay is the container of our tableLayoutPanel
278 //tableLayoutPanel will resize itself to fit the client size of our panelDisplay
279 //panelDisplay needs an extra 2 pixels for borders on each sides
280 //tableLayoutPanel will eventually be the exact size of our display
281 Size size = new Size(iDisplay.WidthInPixels() + 2, iDisplay.HeightInPixels() + 2);
282 panelDisplay.Size = size;
284 //Our display was just opened, update our UI
286 //Initiate asynchronous request
287 iDisplay.RequestFirmwareRevision();
290 UpdateMasterVolumeThreadSafe();
292 UpdateNetworkStatus();
295 //Testing icon in debug, no arm done if icon not supported
296 //iDisplay.SetIconStatus(Display.TMiniDisplayIconType.EMiniDisplayIconRecording, 0, 1);
297 //iDisplay.SetAllIconsStatus(2);
303 /// Populate tree view with events and actions
305 private void PopulateEventsTreeView()
307 //Disable action buttons
308 buttonAddAction.Enabled = false;
309 buttonDeleteAction.Enabled = false;
311 Event currentEvent = CurrentEvent();
314 iTreeViewEvents.Nodes.Clear();
315 //Populate registered events
316 foreach (string key in ManagerEventAction.Current.Events.Keys)
318 Event e = ManagerEventAction.Current.Events[key];
319 TreeNode eventNode = iTreeViewEvents.Nodes.Add(key,e.Name);
321 eventNode.Nodes.Add(key + ".Description", e.Description);
322 TreeNode actionsNodes = eventNode.Nodes.Add(key + ".Actions", "Actions");
324 // Add our actions for that event
325 foreach (SharpLib.Ear.Action a in e.Actions)
327 TreeNode actionNode = actionsNodes.Nodes.Add(a.Brief());
332 iTreeViewEvents.ExpandAll();
333 SelectEvent(currentEvent);
334 //Select the last action if any
335 if (iTreeViewEvents.SelectedNode!= null && iTreeViewEvents.SelectedNode.Nodes[1].GetNodeCount(false) > 0)
337 iTreeViewEvents.SelectedNode = iTreeViewEvents.SelectedNode.Nodes[1].Nodes[iTreeViewEvents.SelectedNode.Nodes[1].GetNodeCount(false)-1];
343 /// Called when our display is closed.
345 /// <param name="aDisplay"></param>
346 private void OnDisplayClosed(Display aDisplay)
348 //Our display was just closed, update our UI consequently
352 public void OnConnectivityChanged(NetworkManager aNetwork, NLM_CONNECTIVITY newConnectivity)
354 //Update network status
355 UpdateNetworkStatus();
359 /// Update our Network Status
361 private void UpdateNetworkStatus()
363 if (iDisplay.IsOpen())
365 iDisplay.SetIconOnOff(MiniDisplay.IconType.Internet, iNetworkManager.NetworkListManager.IsConnectedToInternet);
366 iDisplay.SetIconOnOff(MiniDisplay.IconType.NetworkSignal, iNetworkManager.NetworkListManager.IsConnected);
371 int iLastNetworkIconIndex = 0;
372 int iUpdateCountSinceLastNetworkAnimation = 0;
377 private void UpdateNetworkSignal(DateTime aLastTickTime, DateTime aNewTickTime)
379 iUpdateCountSinceLastNetworkAnimation++;
380 iUpdateCountSinceLastNetworkAnimation = iUpdateCountSinceLastNetworkAnimation % 4;
382 if (iDisplay.IsOpen() && iNetworkManager.NetworkListManager.IsConnected && iUpdateCountSinceLastNetworkAnimation==0)
384 int iconCount = iDisplay.IconCount(MiniDisplay.IconType.NetworkSignal);
387 //Prevents div by zero and other undefined behavior
390 iLastNetworkIconIndex++;
391 iLastNetworkIconIndex = iLastNetworkIconIndex % (iconCount*2);
392 for (int i=0;i<iconCount;i++)
394 if (i < iLastNetworkIconIndex && !(i == 0 && iLastNetworkIconIndex > 3) && !(i == 1 && iLastNetworkIconIndex > 4))
396 iDisplay.SetIconOn(MiniDisplay.IconType.NetworkSignal, i);
400 iDisplay.SetIconOff(MiniDisplay.IconType.NetworkSignal, i);
409 /// Receive volume change notification and reflect changes on our slider.
411 /// <param name="data"></param>
412 public void OnVolumeNotificationThreadSafe(AudioVolumeNotificationData data)
414 UpdateMasterVolumeThreadSafe();
418 /// Update master volume when user moves our slider.
420 /// <param name="sender"></param>
421 /// <param name="e"></param>
422 private void trackBarMasterVolume_Scroll(object sender, EventArgs e)
424 //Just like Windows Volume Mixer we unmute if the volume is adjusted
425 iMultiMediaDevice.AudioEndpointVolume.Mute = false;
426 //Set volume level according to our volume slider new position
427 iMultiMediaDevice.AudioEndpointVolume.MasterVolumeLevelScalar = trackBarMasterVolume.Value / 100.0f;
432 /// Mute check box changed.
434 /// <param name="sender"></param>
435 /// <param name="e"></param>
436 private void checkBoxMute_CheckedChanged(object sender, EventArgs e)
438 iMultiMediaDevice.AudioEndpointVolume.Mute = checkBoxMute.Checked;
442 /// Device State Changed
444 public void OnDeviceStateChanged([MarshalAs(UnmanagedType.LPWStr)] string deviceId, [MarshalAs(UnmanagedType.I4)] DeviceState newState){}
449 public void OnDeviceAdded([MarshalAs(UnmanagedType.LPWStr)] string pwstrDeviceId) { }
454 public void OnDeviceRemoved([MarshalAs(UnmanagedType.LPWStr)] string deviceId) { }
457 /// Default Device Changed
459 public void OnDefaultDeviceChanged(DataFlow flow, Role role, [MarshalAs(UnmanagedType.LPWStr)] string defaultDeviceId)
461 if (role == Role.Multimedia && flow == DataFlow.Render)
463 UpdateAudioDeviceAndMasterVolumeThreadSafe();
468 /// Property Value Changed
470 /// <param name="pwstrDeviceId"></param>
471 /// <param name="key"></param>
472 public void OnPropertyValueChanged([MarshalAs(UnmanagedType.LPWStr)] string pwstrDeviceId, PropertyKey key){}
478 /// Update master volume indicators based our current system states.
479 /// This typically includes volume levels and mute status.
481 private void UpdateMasterVolumeThreadSafe()
483 if (this.InvokeRequired)
485 //Not in the proper thread, invoke ourselves
486 PlainUpdateDelegate d = new PlainUpdateDelegate(UpdateMasterVolumeThreadSafe);
487 this.Invoke(d, new object[] { });
491 //Update volume slider
492 float volumeLevelScalar = iMultiMediaDevice.AudioEndpointVolume.MasterVolumeLevelScalar;
493 trackBarMasterVolume.Value = Convert.ToInt32(volumeLevelScalar * 100);
494 //Update mute checkbox
495 checkBoxMute.Checked = iMultiMediaDevice.AudioEndpointVolume.Mute;
497 //If our display connection is open we need to update its icons
498 if (iDisplay.IsOpen())
500 //First take care our our volume level icons
501 int volumeIconCount = iDisplay.IconCount(MiniDisplay.IconType.Volume);
502 if (volumeIconCount > 0)
504 //Compute current volume level from system level and the number of segments in our display volume bar.
505 //That tells us how many segments in our volume bar needs to be turned on.
506 float currentVolume = volumeLevelScalar * volumeIconCount;
507 int segmentOnCount = Convert.ToInt32(currentVolume);
508 //Check if our segment count was rounded up, this will later be used for half brightness segment
509 bool roundedUp = segmentOnCount > currentVolume;
511 for (int i = 0; i < volumeIconCount; i++)
513 if (i < segmentOnCount)
515 //If we are dealing with our last segment and our segment count was rounded up then we will use half brightness.
516 if (i == segmentOnCount - 1 && roundedUp)
519 iDisplay.SetIconStatus(MiniDisplay.IconType.Volume, i, (iDisplay.IconStatusCount(MiniDisplay.IconType.Volume) - 1) / 2);
524 iDisplay.SetIconStatus(MiniDisplay.IconType.Volume, i, iDisplay.IconStatusCount(MiniDisplay.IconType.Volume) - 1);
529 iDisplay.SetIconStatus(MiniDisplay.IconType.Volume, i, 0);
534 //Take care of our mute icon
535 iDisplay.SetIconOnOff(MiniDisplay.IconType.Mute, iMultiMediaDevice.AudioEndpointVolume.Mute);
543 private void UpdateAudioDeviceAndMasterVolumeThreadSafe()
545 if (this.InvokeRequired)
547 //Not in the proper thread, invoke ourselves
548 PlainUpdateDelegate d = new PlainUpdateDelegate(UpdateAudioDeviceAndMasterVolumeThreadSafe);
549 this.Invoke(d, new object[] { });
553 //We are in the correct thread just go ahead.
556 //Get our master volume
557 iMultiMediaDevice = iMultiMediaDeviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia);
559 labelDefaultAudioDevice.Text = iMultiMediaDevice.FriendlyName;
561 //Show our volume in our track bar
562 UpdateMasterVolumeThreadSafe();
564 //Register to get volume modifications
565 iMultiMediaDevice.AudioEndpointVolume.OnVolumeNotification += OnVolumeNotificationThreadSafe;
567 trackBarMasterVolume.Enabled = true;
571 Debug.WriteLine("Exception thrown in UpdateAudioDeviceAndMasterVolume");
572 Debug.WriteLine(ex.ToString());
573 //Something went wrong S/PDIF device ca throw exception I guess
574 trackBarMasterVolume.Enabled = false;
581 private void PopulateDeviceTypes()
583 int count = Display.TypeCount();
585 for (int i = 0; i < count; i++)
587 comboBoxDisplayType.Items.Add(Display.TypeName((MiniDisplay.Type)i));
594 private void PopulateOpticalDrives()
596 //Reset our list of drives
597 comboBoxOpticalDrives.Items.Clear();
598 comboBoxOpticalDrives.Items.Add("None");
600 //Go through each drives on our system and collected the optical ones in our list
601 DriveInfo[] allDrives = DriveInfo.GetDrives();
602 foreach (DriveInfo d in allDrives)
604 Debug.WriteLine("Drive " + d.Name);
605 Debug.WriteLine(" Drive type: {0}", d.DriveType);
607 if (d.DriveType==DriveType.CDRom)
609 //This is an optical drive, add it now
610 comboBoxOpticalDrives.Items.Add(d.Name.Substring(0,2));
618 /// <returns></returns>
619 public string OpticalDriveToEject()
621 return comboBoxOpticalDrives.SelectedItem.ToString();
629 private void SetupTrayIcon()
631 iNotifyIcon.Icon = GetIcon("vfd.ico");
632 iNotifyIcon.Text = "Sharp Display Manager";
633 iNotifyIcon.Visible = true;
635 //Double click toggles visibility - typically brings up the application
636 iNotifyIcon.DoubleClick += delegate(object obj, EventArgs args)
641 //Adding a context menu, useful to be able to exit the application
642 ContextMenu contextMenu = new ContextMenu();
643 //Context menu item to toggle visibility
644 MenuItem hideShowItem = new MenuItem("Hide/Show");
645 hideShowItem.Click += delegate(object obj, EventArgs args)
649 contextMenu.MenuItems.Add(hideShowItem);
651 //Context menu item separator
652 contextMenu.MenuItems.Add(new MenuItem("-"));
654 //Context menu exit item
655 MenuItem exitItem = new MenuItem("Exit");
656 exitItem.Click += delegate(object obj, EventArgs args)
660 contextMenu.MenuItems.Add(exitItem);
662 iNotifyIcon.ContextMenu = contextMenu;
668 private void SetupRecordingNotification()
670 iRecordingNotification.Icon = GetIcon("record.ico");
671 iRecordingNotification.Text = "No recording";
672 iRecordingNotification.Visible = false;
676 /// Access icons from embedded resources.
678 /// <param name="aName"></param>
679 /// <returns></returns>
680 public static Icon GetIcon(string aName)
682 string[] names = Assembly.GetExecutingAssembly().GetManifestResourceNames();
683 foreach (string name in names)
685 //Find a resource name that ends with the given name
686 if (name.EndsWith(aName))
688 using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(name))
690 return new Icon(stream);
700 /// Set our current client.
701 /// This will take care of applying our client layout and set data fields.
703 /// <param name="aSessionId"></param>
704 void SetCurrentClient(string aSessionId, bool aForce=false)
706 if (aSessionId == iCurrentClientSessionId)
708 //Given client is already the current one.
709 //Don't bother changing anything then.
713 ClientData requestedClientData = iClients[aSessionId];
715 //Check when was the last time we switched to that client
716 if (iCurrentClientData != null)
718 //Do not switch client if priority of current client is higher
719 if (!aForce && requestedClientData.Priority < iCurrentClientData.Priority)
725 double lastSwitchToClientSecondsAgo = (DateTime.Now - iCurrentClientData.LastSwitchTime).TotalSeconds;
726 //TODO: put that hard coded value as a client property
727 //Clients should be able to define how often they can be interrupted
728 //Thus a background client can set this to zero allowing any other client to interrupt at any time
729 //We could also compute this delay by looking at the requests frequencies?
731 requestedClientData.Priority == iCurrentClientData.Priority && //Time sharing is only if clients have the same priority
732 (lastSwitchToClientSecondsAgo < 30)) //Make sure a client is on for at least 30 seconds
734 //Don't switch clients too often
739 //Set current client ID.
740 iCurrentClientSessionId = aSessionId;
741 //Set the time we last switched to that client
742 iClients[aSessionId].LastSwitchTime = DateTime.Now;
743 //Fetch and set current client data.
744 iCurrentClientData = requestedClientData;
745 //Apply layout and set data fields.
746 UpdateTableLayoutPanel(iCurrentClientData);
749 private void buttonFont_Click(object sender, EventArgs e)
751 //fontDialog.ShowColor = true;
752 //fontDialog.ShowApply = true;
753 fontDialog.ShowEffects = true;
754 fontDialog.Font = cds.Font;
756 fontDialog.FixedPitchOnly = checkBoxFixedPitchFontOnly.Checked;
758 //fontDialog.ShowHelp = true;
760 //fontDlg.MaxSize = 40;
761 //fontDlg.MinSize = 22;
763 //fontDialog.Parent = this;
764 //fontDialog.StartPosition = FormStartPosition.CenterParent;
766 //DlgBox.ShowDialog(fontDialog);
768 //if (fontDialog.ShowDialog(this) != DialogResult.Cancel)
769 if (DlgBox.ShowDialog(fontDialog) != DialogResult.Cancel)
771 //Set the fonts to all our labels in our layout
772 foreach (Control ctrl in iTableLayoutPanel.Controls)
774 if (ctrl is MarqueeLabel)
776 ((MarqueeLabel)ctrl).Font = fontDialog.Font;
781 cds.Font = fontDialog.Font;
782 Properties.Settings.Default.Save();
791 void CheckFontHeight()
793 //Show font height and width
794 labelFontHeight.Text = "Font height: " + cds.Font.Height;
795 float charWidth = IsFixedWidth(cds.Font);
796 if (charWidth == 0.0f)
798 labelFontWidth.Visible = false;
802 labelFontWidth.Visible = true;
803 labelFontWidth.Text = "Font width: " + charWidth;
806 MarqueeLabel label = null;
807 //Get the first label control we can find
808 foreach (Control ctrl in iTableLayoutPanel.Controls)
810 if (ctrl is MarqueeLabel)
812 label = (MarqueeLabel)ctrl;
817 //Now check font height and show a warning if needed.
818 if (label != null && label.Font.Height > label.Height)
820 labelWarning.Text = "WARNING: Selected font is too height by " + (label.Font.Height - label.Height) + " pixels!";
821 labelWarning.Visible = true;
825 labelWarning.Visible = false;
830 private void buttonCapture_Click(object sender, EventArgs e)
832 System.Drawing.Bitmap bmp = new System.Drawing.Bitmap(iTableLayoutPanel.Width, iTableLayoutPanel.Height);
833 iTableLayoutPanel.DrawToBitmap(bmp, iTableLayoutPanel.ClientRectangle);
834 //Bitmap bmpToSave = new Bitmap(bmp);
835 bmp.Save("D:\\capture.png");
837 ((MarqueeLabel)iTableLayoutPanel.Controls[0]).Text = "Captured";
840 string outputFileName = "d:\\capture.png";
841 using (MemoryStream memory = new MemoryStream())
843 using (FileStream fs = new FileStream(outputFileName, FileMode.OpenOrCreate, FileAccess.ReadWrite))
845 bmp.Save(memory, System.Drawing.Imaging.ImageFormat.Png);
846 byte[] bytes = memory.ToArray();
847 fs.Write(bytes, 0, bytes.Length);
854 private void CheckForRequestResults()
856 if (iDisplay.IsRequestPending())
858 switch (iDisplay.AttemptRequestCompletion())
860 case MiniDisplay.Request.FirmwareRevision:
861 toolStripStatusLabelConnect.Text += " v" + iDisplay.FirmwareRevision();
862 //Issue next request then
863 iDisplay.RequestPowerSupplyStatus();
866 case MiniDisplay.Request.PowerSupplyStatus:
867 if (iDisplay.PowerSupplyStatus())
869 toolStripStatusLabelPower.Text = "ON";
873 toolStripStatusLabelPower.Text = "OFF";
875 //Issue next request then
876 iDisplay.RequestDeviceId();
879 case MiniDisplay.Request.DeviceId:
880 toolStripStatusLabelConnect.Text += " - " + iDisplay.DeviceId();
881 //No more request to issue
887 public static uint ColorWhiteIsOn(int aX, int aY, uint aPixel)
889 if ((aPixel & 0x00FFFFFF) == 0x00FFFFFF)
896 public static uint ColorUntouched(int aX, int aY, uint aPixel)
901 public static uint ColorInversed(int aX, int aY, uint aPixel)
906 public static uint ColorChessboard(int aX, int aY, uint aPixel)
908 if ((aX % 2 == 0) && (aY % 2 == 0))
912 else if ((aX % 2 != 0) && (aY % 2 != 0))
920 public static int ScreenReversedX(System.Drawing.Bitmap aBmp, int aX)
922 return aBmp.Width - aX - 1;
925 public int ScreenReversedY(System.Drawing.Bitmap aBmp, int aY)
927 return iBmp.Height - aY - 1;
930 public int ScreenX(System.Drawing.Bitmap aBmp, int aX)
935 public int ScreenY(System.Drawing.Bitmap aBmp, int aY)
941 /// Select proper pixel delegates according to our current settings.
943 private void SetupPixelDelegates()
945 //Select our pixel processing routine
946 if (cds.InverseColors)
948 //iColorFx = ColorChessboard;
949 iColorFx = ColorInversed;
953 iColorFx = ColorWhiteIsOn;
956 //Select proper coordinate translation functions
957 //We used delegate/function pointer to support reverse screen without doing an extra test on each pixels
958 if (cds.ReverseScreen)
960 iScreenX = ScreenReversedX;
961 iScreenY = ScreenReversedY;
971 //This is our timer tick responsible to perform our render
972 private void timer_Tick(object sender, EventArgs e)
974 //Not ideal cause this has nothing to do with display render
977 //Update our animations
978 DateTime NewTickTime = DateTime.Now;
980 UpdateNetworkSignal(LastTickTime, NewTickTime);
982 //Update animation for all our marquees
983 foreach (Control ctrl in iTableLayoutPanel.Controls)
985 if (ctrl is MarqueeLabel)
987 ((MarqueeLabel)ctrl).UpdateAnimation(LastTickTime, NewTickTime);
992 if (iDisplay.IsOpen())
994 CheckForRequestResults();
996 //Check if frame rendering is needed
997 //Typically used when showing clock
998 if (!iSkipFrameRendering)
1003 iBmp = new System.Drawing.Bitmap(iTableLayoutPanel.Width, iTableLayoutPanel.Height, PixelFormat.Format32bppArgb);
1004 iCreateBitmap = false;
1006 iTableLayoutPanel.DrawToBitmap(iBmp, iTableLayoutPanel.ClientRectangle);
1007 //iBmp.Save("D:\\capture.png");
1009 //Send it to our display
1010 for (int i = 0; i < iBmp.Width; i++)
1012 for (int j = 0; j < iBmp.Height; j++)
1016 //Get our processed pixel coordinates
1017 int x = iScreenX(iBmp, i);
1018 int y = iScreenY(iBmp, j);
1020 uint color = (uint)iBmp.GetPixel(i, j).ToArgb();
1021 //Apply color effects
1022 color = iColorFx(x, y, color);
1024 iDisplay.SetPixel(x, y, color);
1029 iDisplay.SwapBuffers();
1033 //Compute instant FPS
1034 toolStripStatusLabelFps.Text = (1.0/NewTickTime.Subtract(LastTickTime).TotalSeconds).ToString("F0") + " / " + (1000/timer.Interval).ToString() + " FPS";
1036 LastTickTime = NewTickTime;
1041 /// Attempt to establish connection with our display hardware.
1043 private void OpenDisplayConnection()
1045 CloseDisplayConnection();
1047 if (!iDisplay.Open((MiniDisplay.Type)cds.DisplayType))
1050 toolStripStatusLabelConnect.Text = "Connection error";
1054 private void CloseDisplayConnection()
1056 //Status will be updated upon receiving the closed event
1058 if (iDisplay == null || !iDisplay.IsOpen())
1063 //Do not clear if we gave up on rendering already.
1064 //This means we will keep on displaying clock on MDM166AA for instance.
1065 if (!iSkipFrameRendering)
1068 iDisplay.SwapBuffers();
1071 iDisplay.SetAllIconsStatus(0); //Turn off all icons
1075 private void buttonOpen_Click(object sender, EventArgs e)
1077 OpenDisplayConnection();
1080 private void buttonClose_Click(object sender, EventArgs e)
1082 CloseDisplayConnection();
1085 private void buttonClear_Click(object sender, EventArgs e)
1088 iDisplay.SwapBuffers();
1091 private void buttonFill_Click(object sender, EventArgs e)
1094 iDisplay.SwapBuffers();
1097 private void trackBarBrightness_Scroll(object sender, EventArgs e)
1099 cds.Brightness = trackBarBrightness.Value;
1100 Properties.Settings.Default.Save();
1101 iDisplay.SetBrightness(trackBarBrightness.Value);
1107 /// CDS stands for Current Display Settings
1109 private DisplaySettings cds
1113 DisplaysSettings settings = Properties.Settings.Default.DisplaysSettings;
1114 if (settings == null)
1116 settings = new DisplaysSettings();
1118 Properties.Settings.Default.DisplaysSettings = settings;
1121 //Make sure all our settings have been created
1122 while (settings.Displays.Count <= Properties.Settings.Default.CurrentDisplayIndex)
1124 settings.Displays.Add(new DisplaySettings());
1127 DisplaySettings displaySettings = settings.Displays[Properties.Settings.Default.CurrentDisplayIndex];
1128 return displaySettings;
1133 /// Check if the given font has a fixed character pitch.
1135 /// <param name="ft"></param>
1136 /// <returns>0.0f if this is not a monospace font, otherwise returns the character width.</returns>
1137 public float IsFixedWidth(Font ft)
1139 Graphics g = CreateGraphics();
1140 char[] charSizes = new char[] { 'i', 'a', 'Z', '%', '#', 'a', 'B', 'l', 'm', ',', '.' };
1141 float charWidth = g.MeasureString("I", ft, Int32.MaxValue, StringFormat.GenericTypographic).Width;
1143 bool fixedWidth = true;
1145 foreach (char c in charSizes)
1146 if (g.MeasureString(c.ToString(), ft, Int32.MaxValue, StringFormat.GenericTypographic).Width != charWidth)
1158 /// Synchronize UI with settings
1160 private void UpdateStatus()
1163 checkBoxShowBorders.Checked = cds.ShowBorders;
1164 iTableLayoutPanel.CellBorderStyle = (cds.ShowBorders ? TableLayoutPanelCellBorderStyle.Single : TableLayoutPanelCellBorderStyle.None);
1166 //Set the proper font to each of our labels
1167 foreach (MarqueeLabel ctrl in iTableLayoutPanel.Controls)
1169 ctrl.Font = cds.Font;
1173 //Check if "run on Windows startup" is enabled
1174 checkBoxAutoStart.Checked = iStartupManager.Startup;
1176 checkBoxConnectOnStartup.Checked = Properties.Settings.Default.DisplayConnectOnStartup;
1177 checkBoxMinimizeToTray.Checked = Properties.Settings.Default.MinimizeToTray;
1178 checkBoxStartMinimized.Checked = Properties.Settings.Default.StartMinimized;
1179 iCheckBoxStartIdleClient.Checked = Properties.Settings.Default.StartIdleClient;
1180 labelStartFileName.Text = Properties.Settings.Default.StartFileName;
1183 //Try find our drive in our drive list
1184 int opticalDriveItemIndex=0;
1185 bool driveNotFound = true;
1186 string opticalDriveToEject=Properties.Settings.Default.OpticalDriveToEject;
1187 foreach (object item in comboBoxOpticalDrives.Items)
1189 if (opticalDriveToEject == item.ToString())
1191 comboBoxOpticalDrives.SelectedIndex = opticalDriveItemIndex;
1192 driveNotFound = false;
1195 opticalDriveItemIndex++;
1200 //We could not find the drive we had saved.
1201 //Select "None" then.
1202 comboBoxOpticalDrives.SelectedIndex = 0;
1206 checkBoxCecEnabled.Checked = Properties.Settings.Default.CecEnabled;
1207 comboBoxHdmiPort.SelectedIndex = Properties.Settings.Default.CecHdmiPort - 1;
1209 //Mini Display settings
1210 checkBoxReverseScreen.Checked = cds.ReverseScreen;
1211 checkBoxInverseColors.Checked = cds.InverseColors;
1212 checkBoxShowVolumeLabel.Checked = cds.ShowVolumeLabel;
1213 checkBoxScaleToFit.Checked = cds.ScaleToFit;
1214 maskedTextBoxMinFontSize.Enabled = cds.ScaleToFit;
1215 labelMinFontSize.Enabled = cds.ScaleToFit;
1216 maskedTextBoxMinFontSize.Text = cds.MinFontSize.ToString();
1217 maskedTextBoxScrollingSpeed.Text = cds.ScrollingSpeedInPixelsPerSecond.ToString();
1218 comboBoxDisplayType.SelectedIndex = cds.DisplayType;
1219 timer.Interval = cds.TimerInterval;
1220 maskedTextBoxTimerInterval.Text = cds.TimerInterval.ToString();
1221 textBoxScrollLoopSeparator.Text = cds.Separator;
1223 SetupPixelDelegates();
1225 if (iDisplay.IsOpen())
1227 //We have a display connection
1228 //Reflect that in our UI
1231 iTableLayoutPanel.Enabled = true;
1232 panelDisplay.Enabled = true;
1234 //Only setup brightness if display is open
1235 trackBarBrightness.Minimum = iDisplay.MinBrightness();
1236 trackBarBrightness.Maximum = iDisplay.MaxBrightness();
1237 if (cds.Brightness < iDisplay.MinBrightness() || cds.Brightness > iDisplay.MaxBrightness())
1239 //Brightness out of range, this can occur when using auto-detect
1240 //Use max brightness instead
1241 trackBarBrightness.Value = iDisplay.MaxBrightness();
1242 iDisplay.SetBrightness(iDisplay.MaxBrightness());
1246 trackBarBrightness.Value = cds.Brightness;
1247 iDisplay.SetBrightness(cds.Brightness);
1250 //Try compute the steps to something that makes sense
1251 trackBarBrightness.LargeChange = Math.Max(1, (iDisplay.MaxBrightness() - iDisplay.MinBrightness()) / 5);
1252 trackBarBrightness.SmallChange = 1;
1255 buttonFill.Enabled = true;
1256 buttonClear.Enabled = true;
1257 buttonOpen.Enabled = false;
1258 buttonClose.Enabled = true;
1259 trackBarBrightness.Enabled = true;
1260 toolStripStatusLabelConnect.Text = "Connected - " + iDisplay.Vendor() + " - " + iDisplay.Product();
1261 //+ " - " + iDisplay.SerialNumber();
1263 if (iDisplay.SupportPowerOnOff())
1265 buttonPowerOn.Enabled = true;
1266 buttonPowerOff.Enabled = true;
1270 buttonPowerOn.Enabled = false;
1271 buttonPowerOff.Enabled = false;
1274 if (iDisplay.SupportClock())
1276 buttonShowClock.Enabled = true;
1277 buttonHideClock.Enabled = true;
1281 buttonShowClock.Enabled = false;
1282 buttonHideClock.Enabled = false;
1286 //Check if Volume Label is supported. To date only MDM166AA supports that crap :)
1287 checkBoxShowVolumeLabel.Enabled = iDisplay.IconCount(MiniDisplay.IconType.VolumeLabel)>0;
1289 if (cds.ShowVolumeLabel)
1291 iDisplay.SetIconOn(MiniDisplay.IconType.VolumeLabel);
1295 iDisplay.SetIconOff(MiniDisplay.IconType.VolumeLabel);
1300 //Display connection not available
1301 //Reflect that in our UI
1303 //In debug start our timer even if we don't have a display connection
1306 //In production environment we don't need our timer if no display connection
1309 checkBoxShowVolumeLabel.Enabled = false;
1310 iTableLayoutPanel.Enabled = false;
1311 panelDisplay.Enabled = false;
1312 buttonFill.Enabled = false;
1313 buttonClear.Enabled = false;
1314 buttonOpen.Enabled = true;
1315 buttonClose.Enabled = false;
1316 trackBarBrightness.Enabled = false;
1317 buttonPowerOn.Enabled = false;
1318 buttonPowerOff.Enabled = false;
1319 buttonShowClock.Enabled = false;
1320 buttonHideClock.Enabled = false;
1321 toolStripStatusLabelConnect.Text = "Disconnected";
1322 toolStripStatusLabelPower.Text = "N/A";
1331 /// <param name="sender"></param>
1332 /// <param name="e"></param>
1333 private void checkBoxShowVolumeLabel_CheckedChanged(object sender, EventArgs e)
1335 cds.ShowVolumeLabel = checkBoxShowVolumeLabel.Checked;
1336 Properties.Settings.Default.Save();
1340 private void checkBoxShowBorders_CheckedChanged(object sender, EventArgs e)
1342 //Save our show borders setting
1343 iTableLayoutPanel.CellBorderStyle = (checkBoxShowBorders.Checked ? TableLayoutPanelCellBorderStyle.Single : TableLayoutPanelCellBorderStyle.None);
1344 cds.ShowBorders = checkBoxShowBorders.Checked;
1345 Properties.Settings.Default.Save();
1349 private void checkBoxConnectOnStartup_CheckedChanged(object sender, EventArgs e)
1351 //Save our connect on startup setting
1352 Properties.Settings.Default.DisplayConnectOnStartup = checkBoxConnectOnStartup.Checked;
1353 Properties.Settings.Default.Save();
1356 private void checkBoxMinimizeToTray_CheckedChanged(object sender, EventArgs e)
1358 //Save our "Minimize to tray" setting
1359 Properties.Settings.Default.MinimizeToTray = checkBoxMinimizeToTray.Checked;
1360 Properties.Settings.Default.Save();
1364 private void checkBoxStartMinimized_CheckedChanged(object sender, EventArgs e)
1366 //Save our "Start minimized" setting
1367 Properties.Settings.Default.StartMinimized = checkBoxStartMinimized.Checked;
1368 Properties.Settings.Default.Save();
1371 private void checkBoxStartIdleClient_CheckedChanged(object sender, EventArgs e)
1373 Properties.Settings.Default.StartIdleClient = iCheckBoxStartIdleClient.Checked;
1374 Properties.Settings.Default.Save();
1377 private void checkBoxAutoStart_CheckedChanged(object sender, EventArgs e)
1379 iStartupManager.Startup = checkBoxAutoStart.Checked;
1383 private void checkBoxReverseScreen_CheckedChanged(object sender, EventArgs e)
1385 //Save our reverse screen setting
1386 cds.ReverseScreen = checkBoxReverseScreen.Checked;
1387 Properties.Settings.Default.Save();
1388 SetupPixelDelegates();
1391 private void checkBoxInverseColors_CheckedChanged(object sender, EventArgs e)
1393 //Save our inverse colors setting
1394 cds.InverseColors = checkBoxInverseColors.Checked;
1395 Properties.Settings.Default.Save();
1396 SetupPixelDelegates();
1399 private void checkBoxScaleToFit_CheckedChanged(object sender, EventArgs e)
1401 //Save our scale to fit setting
1402 cds.ScaleToFit = checkBoxScaleToFit.Checked;
1403 Properties.Settings.Default.Save();
1405 labelMinFontSize.Enabled = cds.ScaleToFit;
1406 maskedTextBoxMinFontSize.Enabled = cds.ScaleToFit;
1409 private void MainForm_Resize(object sender, EventArgs e)
1411 if (WindowState == FormWindowState.Minimized)
1413 // To workaround our empty bitmap bug on Windows 7 we need to recreate our bitmap when the application is minimized
1414 // That's apparently not needed on Windows 10 but we better leave it in place.
1415 iCreateBitmap = true;
1419 private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
1422 iNetworkManager.Dispose();
1423 CloseDisplayConnection();
1425 e.Cancel = iClosing;
1428 public void StartServer()
1430 iServiceHost = new ServiceHost
1433 new Uri[] { new Uri("net.tcp://localhost:8001/") }
1436 iServiceHost.AddServiceEndpoint(typeof(IService), new NetTcpBinding(SecurityMode.None, true), "DisplayService");
1437 iServiceHost.Open();
1440 public void StopServer()
1442 if (iClients.Count > 0 && !iClosing)
1446 BroadcastCloseEvent();
1450 if (MessageBox.Show("Force exit?", "Waiting for clients...", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) == DialogResult.Yes)
1452 iClosing = false; //We make sure we force close if asked twice
1457 //We removed that as it often lags for some reason
1458 //iServiceHost.Close();
1462 public void BroadcastCloseEvent()
1464 Trace.TraceInformation("BroadcastCloseEvent - start");
1466 var inactiveClients = new List<string>();
1467 foreach (var client in iClients)
1469 //if (client.Key != eventData.ClientName)
1473 Trace.TraceInformation("BroadcastCloseEvent - " + client.Key);
1474 client.Value.Callback.OnCloseOrder(/*eventData*/);
1476 catch (Exception ex)
1478 inactiveClients.Add(client.Key);
1483 if (inactiveClients.Count > 0)
1485 foreach (var client in inactiveClients)
1487 iClients.Remove(client);
1488 Program.iMainForm.iTreeViewClients.Nodes.Remove(Program.iMainForm.iTreeViewClients.Nodes.Find(client, false)[0]);
1492 if (iClients.Count==0)
1499 /// Just remove all our fields.
1501 private void ClearLayout()
1503 iTableLayoutPanel.Controls.Clear();
1504 iTableLayoutPanel.RowStyles.Clear();
1505 iTableLayoutPanel.ColumnStyles.Clear();
1506 iCurrentClientData = null;
1510 /// Just launch a demo client.
1512 private void StartNewClient(string aTopText = "", string aBottomText = "")
1514 Thread clientThread = new Thread(SharpDisplayClient.Program.MainWithParams);
1515 SharpDisplayClient.StartParams myParams = new SharpDisplayClient.StartParams(new Point(this.Right, this.Top),aTopText,aBottomText);
1516 clientThread.Start(myParams);
1521 /// Just launch our idle client.
1523 private void StartIdleClient(string aTopText = "", string aBottomText = "")
1525 Thread clientThread = new Thread(SharpDisplayIdleClient.Program.MainWithParams);
1526 SharpDisplayIdleClient.StartParams myParams = new SharpDisplayIdleClient.StartParams(new Point(this.Right, this.Top), aTopText, aBottomText);
1527 clientThread.Start(myParams);
1532 private void buttonStartClient_Click(object sender, EventArgs e)
1537 private void buttonSuspend_Click(object sender, EventArgs e)
1542 private void StartTimer()
1544 LastTickTime = DateTime.Now; //Reset timer to prevent jump
1545 timer.Enabled = true;
1546 UpdateSuspendButton();
1549 private void StopTimer()
1551 LastTickTime = DateTime.Now; //Reset timer to prevent jump
1552 timer.Enabled = false;
1553 UpdateSuspendButton();
1556 private void ToggleTimer()
1558 LastTickTime = DateTime.Now; //Reset timer to prevent jump
1559 timer.Enabled = !timer.Enabled;
1560 UpdateSuspendButton();
1563 private void UpdateSuspendButton()
1567 buttonSuspend.Text = "Run";
1571 buttonSuspend.Text = "Pause";
1576 private void buttonCloseClients_Click(object sender, EventArgs e)
1578 BroadcastCloseEvent();
1581 private void treeViewClients_AfterSelect(object sender, TreeViewEventArgs e)
1583 //Root node must have at least one child
1584 if (e.Node.Nodes.Count == 0)
1589 //If the selected node is the root node of a client then switch to it
1590 string sessionId=e.Node.Nodes[0].Text; //First child of a root node is the sessionId
1591 if (iClients.ContainsKey(sessionId)) //Check that's actually what we are looking at
1593 //We have a valid session just switch to that client
1594 SetCurrentClient(sessionId,true);
1603 /// <param name="aSessionId"></param>
1604 /// <param name="aCallback"></param>
1605 public void AddClientThreadSafe(string aSessionId, ICallback aCallback)
1607 if (this.InvokeRequired)
1609 //Not in the proper thread, invoke ourselves
1610 AddClientDelegate d = new AddClientDelegate(AddClientThreadSafe);
1611 this.Invoke(d, new object[] { aSessionId, aCallback });
1615 //We are in the proper thread
1616 //Add this session to our collection of clients
1617 ClientData newClient = new ClientData(aSessionId, aCallback);
1618 Program.iMainForm.iClients.Add(aSessionId, newClient);
1619 //Add this session to our UI
1620 UpdateClientTreeViewNode(newClient);
1626 /// Find the client with the highest priority if any.
1628 /// <returns>Our highest priority client or null if not a single client is connected.</returns>
1629 public ClientData FindHighestPriorityClient()
1631 ClientData highestPriorityClient = null;
1632 foreach (var client in iClients)
1634 if (highestPriorityClient == null || client.Value.Priority > highestPriorityClient.Priority)
1636 highestPriorityClient = client.Value;
1640 return highestPriorityClient;
1646 /// <param name="aSessionId"></param>
1647 public void RemoveClientThreadSafe(string aSessionId)
1649 if (this.InvokeRequired)
1651 //Not in the proper thread, invoke ourselves
1652 RemoveClientDelegate d = new RemoveClientDelegate(RemoveClientThreadSafe);
1653 this.Invoke(d, new object[] { aSessionId });
1657 //We are in the proper thread
1658 //Remove this session from both client collection and UI tree view
1659 if (Program.iMainForm.iClients.Keys.Contains(aSessionId))
1661 Program.iMainForm.iClients.Remove(aSessionId);
1662 Program.iMainForm.iTreeViewClients.Nodes.Remove(Program.iMainForm.iTreeViewClients.Nodes.Find(aSessionId, false)[0]);
1663 //Update recording status too whenever a client is removed
1664 UpdateRecordingNotification();
1667 if (iCurrentClientSessionId == aSessionId)
1669 //The current client is closing
1670 iCurrentClientData = null;
1671 //Find the client with the highest priority and set it as current
1672 ClientData newCurrentClient = FindHighestPriorityClient();
1673 if (newCurrentClient!=null)
1675 SetCurrentClient(newCurrentClient.SessionId, true);
1679 if (iClients.Count == 0)
1681 //Clear our screen when last client disconnects
1686 //We were closing our form
1687 //All clients are now closed
1688 //Just resume our close operation
1699 /// <param name="aSessionId"></param>
1700 /// <param name="aLayout"></param>
1701 public void SetClientLayoutThreadSafe(string aSessionId, TableLayout aLayout)
1703 if (this.InvokeRequired)
1705 //Not in the proper thread, invoke ourselves
1706 SetLayoutDelegate d = new SetLayoutDelegate(SetClientLayoutThreadSafe);
1707 this.Invoke(d, new object[] { aSessionId, aLayout });
1711 ClientData client = iClients[aSessionId];
1714 //Don't change a thing if the layout is the same
1715 if (!client.Layout.IsSameAs(aLayout))
1717 Debug.Print("SetClientLayoutThreadSafe: Layout updated.");
1718 //Set our client layout then
1719 client.Layout = aLayout;
1720 //So that next time we update all our fields at ones
1721 client.HasNewLayout = true;
1722 //Layout has changed clear our fields then
1723 client.Fields.Clear();
1725 UpdateClientTreeViewNode(client);
1729 Debug.Print("SetClientLayoutThreadSafe: Layout has not changed.");
1738 /// <param name="aSessionId"></param>
1739 /// <param name="aField"></param>
1740 public void SetClientFieldThreadSafe(string aSessionId, DataField aField)
1742 if (this.InvokeRequired)
1744 //Not in the proper thread, invoke ourselves
1745 SetFieldDelegate d = new SetFieldDelegate(SetClientFieldThreadSafe);
1746 this.Invoke(d, new object[] { aSessionId, aField });
1750 //We are in the proper thread
1751 //Call the non-thread-safe variant
1752 SetClientField(aSessionId, aField);
1760 /// Set a data field in the given client.
1762 /// <param name="aSessionId"></param>
1763 /// <param name="aField"></param>
1764 private void SetClientField(string aSessionId, DataField aField)
1766 //TODO: should check if the field actually changed?
1768 ClientData client = iClients[aSessionId];
1769 bool layoutChanged = false;
1770 bool contentChanged = true;
1772 //Fetch our field index
1773 int fieldIndex = client.FindSameFieldIndex(aField);
1777 //No corresponding field, just bail out
1781 //Keep our previous field in there
1782 DataField previousField = client.Fields[fieldIndex];
1783 //Just update that field then
1784 client.Fields[fieldIndex] = aField;
1786 if (!aField.IsTableField)
1788 //We are done then if that field is not in our table layout
1792 TableField tableField = (TableField) aField;
1794 if (previousField.IsSameLayout(aField))
1796 //If we are updating a field in our current client we need to update it in our panel
1797 if (aSessionId == iCurrentClientSessionId)
1799 Control ctrl=iTableLayoutPanel.GetControlFromPosition(tableField.Column, tableField.Row);
1800 if (aField.IsTextField && ctrl is MarqueeLabel)
1802 TextField textField=(TextField)aField;
1803 //Text field control already in place, just change the text
1804 MarqueeLabel label = (MarqueeLabel)ctrl;
1805 contentChanged = (label.Text != textField.Text || label.TextAlign != textField.Alignment);
1806 label.Text = textField.Text;
1807 label.TextAlign = textField.Alignment;
1809 else if (aField.IsBitmapField && ctrl is PictureBox)
1811 BitmapField bitmapField = (BitmapField)aField;
1812 contentChanged = true; //TODO: Bitmap comp or should we leave that to clients?
1813 //Bitmap field control already in place just change the bitmap
1814 PictureBox pictureBox = (PictureBox)ctrl;
1815 pictureBox.Image = bitmapField.Bitmap;
1819 layoutChanged = true;
1825 layoutChanged = true;
1828 //If either content or layout changed we need to update our tree view to reflect the changes
1829 if (contentChanged || layoutChanged)
1831 UpdateClientTreeViewNode(client);
1835 Debug.Print("Layout changed");
1836 //Our layout has changed, if we are already the current client we need to update our panel
1837 if (aSessionId == iCurrentClientSessionId)
1839 //Apply layout and set data fields.
1840 UpdateTableLayoutPanel(iCurrentClientData);
1845 Debug.Print("Layout has not changed.");
1850 Debug.Print("WARNING: content and layout have not changed!");
1853 //When a client field is set we try switching to this client to present the new information to our user
1854 SetCurrentClient(aSessionId);
1860 /// <param name="aTexts"></param>
1861 public void SetClientFieldsThreadSafe(string aSessionId, System.Collections.Generic.IList<DataField> aFields)
1863 if (this.InvokeRequired)
1865 //Not in the proper thread, invoke ourselves
1866 SetFieldsDelegate d = new SetFieldsDelegate(SetClientFieldsThreadSafe);
1867 this.Invoke(d, new object[] { aSessionId, aFields });
1871 ClientData client = iClients[aSessionId];
1873 if (client.HasNewLayout)
1875 //TODO: Assert client.Count == 0
1876 //Our layout was just changed
1877 //Do some special handling to avoid re-creating our panel N times, once for each fields
1878 client.HasNewLayout = false;
1879 //Just set all our fields then
1880 client.Fields.AddRange(aFields);
1881 //Try switch to that client
1882 SetCurrentClient(aSessionId);
1884 //If we are updating the current client update our panel
1885 if (aSessionId == iCurrentClientSessionId)
1887 //Apply layout and set data fields.
1888 UpdateTableLayoutPanel(iCurrentClientData);
1891 UpdateClientTreeViewNode(client);
1895 //Put each our text fields in a label control
1896 foreach (DataField field in aFields)
1898 SetClientField(aSessionId, field);
1907 /// <param name="aSessionId"></param>
1908 /// <param name="aName"></param>
1909 public void SetClientNameThreadSafe(string aSessionId, string aName)
1911 if (this.InvokeRequired)
1913 //Not in the proper thread, invoke ourselves
1914 SetClientNameDelegate d = new SetClientNameDelegate(SetClientNameThreadSafe);
1915 this.Invoke(d, new object[] { aSessionId, aName });
1919 //We are in the proper thread
1921 ClientData client = iClients[aSessionId];
1925 client.Name = aName;
1926 //Update our tree-view
1927 UpdateClientTreeViewNode(client);
1933 public void SetClientPriorityThreadSafe(string aSessionId, uint aPriority)
1935 if (this.InvokeRequired)
1937 //Not in the proper thread, invoke ourselves
1938 SetClientPriorityDelegate d = new SetClientPriorityDelegate(SetClientPriorityThreadSafe);
1939 this.Invoke(d, new object[] { aSessionId, aPriority });
1943 //We are in the proper thread
1945 ClientData client = iClients[aSessionId];
1949 client.Priority = aPriority;
1950 //Update our tree-view
1951 UpdateClientTreeViewNode(client);
1952 //Change our current client as per new priority
1953 ClientData newCurrentClient = FindHighestPriorityClient();
1954 if (newCurrentClient!=null)
1956 SetCurrentClient(newCurrentClient.SessionId);
1965 /// <param name="value"></param>
1966 /// <param name="maxChars"></param>
1967 /// <returns></returns>
1968 public static string Truncate(string value, int maxChars)
1970 return value.Length <= maxChars ? value : value.Substring(0, maxChars-3) + "...";
1974 /// Update our recording notification.
1976 private void UpdateRecordingNotification()
1979 bool activeRecording = false;
1981 RecordingField recField=new RecordingField();
1982 foreach (var client in iClients)
1984 RecordingField rec=(RecordingField)client.Value.FindSameFieldAs(recField);
1985 if (rec!=null && rec.IsActive)
1987 activeRecording = true;
1988 //Don't break cause we are collecting the names/texts.
1989 if (!String.IsNullOrEmpty(rec.Text))
1991 text += (rec.Text + "\n");
1995 //Not text for that recording, use client name instead
1996 text += client.Value.Name + " recording\n";
2002 //Update our text no matter what, can't have more than 63 characters otherwise it throws an exception.
2003 iRecordingNotification.Text = Truncate(text,63);
2005 //Change visibility of notification if needed
2006 if (iRecordingNotification.Visible != activeRecording)
2008 iRecordingNotification.Visible = activeRecording;
2009 //Assuming the notification icon is in sync with our display icon
2010 //Take care of our REC icon
2011 if (iDisplay.IsOpen())
2013 iDisplay.SetIconOnOff(MiniDisplay.IconType.Recording, activeRecording);
2021 /// <param name="aClient"></param>
2022 private void UpdateClientTreeViewNode(ClientData aClient)
2024 Debug.Print("UpdateClientTreeViewNode");
2026 if (aClient == null)
2031 //Hook in record icon update too
2032 UpdateRecordingNotification();
2034 TreeNode node = null;
2035 //Check that our client node already exists
2036 //Get our client root node using its key which is our session ID
2037 TreeNode[] nodes = iTreeViewClients.Nodes.Find(aClient.SessionId, false);
2038 if (nodes.Count()>0)
2040 //We already have a node for that client
2042 //Clear children as we are going to recreate them below
2047 //Client node does not exists create a new one
2048 iTreeViewClients.Nodes.Add(aClient.SessionId, aClient.SessionId);
2049 node = iTreeViewClients.Nodes.Find(aClient.SessionId, false)[0];
2055 if (!String.IsNullOrEmpty(aClient.Name))
2057 //We have a name, use it as text for our root node
2058 node.Text = aClient.Name;
2059 //Add a child with SessionId
2060 node.Nodes.Add(new TreeNode(aClient.SessionId));
2064 //No name, use session ID instead
2065 node.Text = aClient.SessionId;
2068 //Display client priority
2069 node.Nodes.Add(new TreeNode("Priority: " + aClient.Priority));
2071 if (aClient.Fields.Count > 0)
2073 //Create root node for our texts
2074 TreeNode textsRoot = new TreeNode("Fields");
2075 node.Nodes.Add(textsRoot);
2076 //For each text add a new entry
2077 foreach (DataField field in aClient.Fields)
2079 if (field.IsTextField)
2081 TextField textField = (TextField)field;
2082 textsRoot.Nodes.Add(new TreeNode("[Text]" + textField.Text));
2084 else if (field.IsBitmapField)
2086 textsRoot.Nodes.Add(new TreeNode("[Bitmap]"));
2088 else if (field.IsRecordingField)
2090 RecordingField recordingField = (RecordingField)field;
2091 textsRoot.Nodes.Add(new TreeNode("[Recording]" + recordingField.IsActive));
2101 /// Update our table layout row styles to make sure each rows have similar height
2103 private void UpdateTableLayoutRowStyles()
2105 foreach (RowStyle rowStyle in iTableLayoutPanel.RowStyles)
2107 rowStyle.SizeType = SizeType.Percent;
2108 rowStyle.Height = 100 / iTableLayoutPanel.RowCount;
2113 /// Update our display table layout.
2114 /// Will instanciated every field control as defined by our client.
2115 /// Fields must be specified by rows from the left.
2117 /// <param name="aLayout"></param>
2118 private void UpdateTableLayoutPanel(ClientData aClient)
2120 Debug.Print("UpdateTableLayoutPanel");
2122 if (aClient == null)
2129 TableLayout layout = aClient.Layout;
2131 //First clean our current panel
2132 iTableLayoutPanel.Controls.Clear();
2133 iTableLayoutPanel.RowStyles.Clear();
2134 iTableLayoutPanel.ColumnStyles.Clear();
2135 iTableLayoutPanel.RowCount = 0;
2136 iTableLayoutPanel.ColumnCount = 0;
2138 //Then recreate our rows...
2139 while (iTableLayoutPanel.RowCount < layout.Rows.Count)
2141 iTableLayoutPanel.RowCount++;
2145 while (iTableLayoutPanel.ColumnCount < layout.Columns.Count)
2147 iTableLayoutPanel.ColumnCount++;
2151 for (int i = 0; i < iTableLayoutPanel.ColumnCount; i++)
2153 //Create our column styles
2154 this.iTableLayoutPanel.ColumnStyles.Add(layout.Columns[i]);
2157 for (int j = 0; j < iTableLayoutPanel.RowCount; j++)
2161 //Create our row styles
2162 this.iTableLayoutPanel.RowStyles.Add(layout.Rows[j]);
2172 foreach (DataField field in aClient.Fields)
2174 if (!field.IsTableField)
2176 //That field is not taking part in our table layout skip it
2180 TableField tableField = (TableField)field;
2182 //Create a control corresponding to the field specified for that cell
2183 Control control = CreateControlForDataField(tableField);
2185 //Add newly created control to our table layout at the specified row and column
2186 iTableLayoutPanel.Controls.Add(control, tableField.Column, tableField.Row);
2187 //Make sure we specify column and row span for that new control
2188 iTableLayoutPanel.SetColumnSpan(control, tableField.ColumnSpan);
2189 iTableLayoutPanel.SetRowSpan(control, tableField.RowSpan);
2197 /// Check our type of data field and create corresponding control
2199 /// <param name="aField"></param>
2200 private Control CreateControlForDataField(DataField aField)
2202 Control control=null;
2203 if (aField.IsTextField)
2205 MarqueeLabel label = new SharpDisplayManager.MarqueeLabel();
2206 label.AutoEllipsis = true;
2207 label.AutoSize = true;
2208 label.BackColor = System.Drawing.Color.Transparent;
2209 label.Dock = System.Windows.Forms.DockStyle.Fill;
2210 label.Location = new System.Drawing.Point(1, 1);
2211 label.Margin = new System.Windows.Forms.Padding(0);
2212 label.Name = "marqueeLabel" + aField;
2213 label.OwnTimer = false;
2214 label.PixelsPerSecond = cds.ScrollingSpeedInPixelsPerSecond;
2215 label.Separator = cds.Separator;
2216 label.MinFontSize = cds.MinFontSize;
2217 label.ScaleToFit = cds.ScaleToFit;
2218 //control.Size = new System.Drawing.Size(254, 30);
2219 //control.TabIndex = 2;
2220 label.Font = cds.Font;
2222 TextField field = (TextField)aField;
2223 label.TextAlign = field.Alignment;
2224 label.UseCompatibleTextRendering = true;
2225 label.Text = field.Text;
2229 else if (aField.IsBitmapField)
2231 //Create picture box
2232 PictureBox picture = new PictureBox();
2233 picture.AutoSize = true;
2234 picture.BackColor = System.Drawing.Color.Transparent;
2235 picture.Dock = System.Windows.Forms.DockStyle.Fill;
2236 picture.Location = new System.Drawing.Point(1, 1);
2237 picture.Margin = new System.Windows.Forms.Padding(0);
2238 picture.Name = "pictureBox" + aField;
2240 BitmapField field = (BitmapField)aField;
2241 picture.Image = field.Bitmap;
2245 //TODO: Handle recording field?
2251 /// Called when the user selected a new display type.
2253 /// <param name="sender"></param>
2254 /// <param name="e"></param>
2255 private void comboBoxDisplayType_SelectedIndexChanged(object sender, EventArgs e)
2257 //Store the selected display type in our settings
2258 Properties.Settings.Default.CurrentDisplayIndex = comboBoxDisplayType.SelectedIndex;
2259 cds.DisplayType = comboBoxDisplayType.SelectedIndex;
2260 Properties.Settings.Default.Save();
2262 //Try re-opening the display connection if we were already connected.
2263 //Otherwise just update our status to reflect display type change.
2264 if (iDisplay.IsOpen())
2266 OpenDisplayConnection();
2274 private void maskedTextBoxTimerInterval_TextChanged(object sender, EventArgs e)
2276 if (maskedTextBoxTimerInterval.Text != "")
2278 int interval = Convert.ToInt32(maskedTextBoxTimerInterval.Text);
2282 timer.Interval = interval;
2283 cds.TimerInterval = timer.Interval;
2284 Properties.Settings.Default.Save();
2289 private void maskedTextBoxMinFontSize_TextChanged(object sender, EventArgs e)
2291 if (maskedTextBoxMinFontSize.Text != "")
2293 int minFontSize = Convert.ToInt32(maskedTextBoxMinFontSize.Text);
2295 if (minFontSize > 0)
2297 cds.MinFontSize = minFontSize;
2298 Properties.Settings.Default.Save();
2299 //We need to recreate our layout for that change to take effect
2300 UpdateTableLayoutPanel(iCurrentClientData);
2306 private void maskedTextBoxScrollingSpeed_TextChanged(object sender, EventArgs e)
2308 if (maskedTextBoxScrollingSpeed.Text != "")
2310 int scrollingSpeed = Convert.ToInt32(maskedTextBoxScrollingSpeed.Text);
2312 if (scrollingSpeed > 0)
2314 cds.ScrollingSpeedInPixelsPerSecond = scrollingSpeed;
2315 Properties.Settings.Default.Save();
2316 //We need to recreate our layout for that change to take effect
2317 UpdateTableLayoutPanel(iCurrentClientData);
2322 private void textBoxScrollLoopSeparator_TextChanged(object sender, EventArgs e)
2324 cds.Separator = textBoxScrollLoopSeparator.Text;
2325 Properties.Settings.Default.Save();
2327 //Update our text fields
2328 foreach (MarqueeLabel ctrl in iTableLayoutPanel.Controls)
2330 ctrl.Separator = cds.Separator;
2335 private void buttonPowerOn_Click(object sender, EventArgs e)
2340 private void buttonPowerOff_Click(object sender, EventArgs e)
2342 iDisplay.PowerOff();
2345 private void buttonShowClock_Click(object sender, EventArgs e)
2350 private void buttonHideClock_Click(object sender, EventArgs e)
2355 private void buttonUpdate_Click(object sender, EventArgs e)
2357 InstallUpdateSyncWithInfo();
2365 if (!iDisplay.IsOpen())
2370 //Devices like MDM166AA don't support windowing and frame rendering must be stopped while showing our clock
2371 iSkipFrameRendering = true;
2374 iDisplay.SwapBuffers();
2375 //Then show our clock
2376 iDisplay.ShowClock();
2384 if (!iDisplay.IsOpen())
2389 //Devices like MDM166AA don't support windowing and frame rendering must be stopped while showing our clock
2390 iSkipFrameRendering = false;
2391 iDisplay.HideClock();
2394 private void InstallUpdateSyncWithInfo()
2396 UpdateCheckInfo info = null;
2398 if (ApplicationDeployment.IsNetworkDeployed)
2400 ApplicationDeployment ad = ApplicationDeployment.CurrentDeployment;
2404 info = ad.CheckForDetailedUpdate();
2407 catch (DeploymentDownloadException dde)
2409 MessageBox.Show("The new version of the application cannot be downloaded at this time. \n\nPlease check your network connection, or try again later. Error: " + dde.Message);
2412 catch (InvalidDeploymentException ide)
2414 MessageBox.Show("Cannot check for a new version of the application. The ClickOnce deployment is corrupt. Please redeploy the application and try again. Error: " + ide.Message);
2417 catch (InvalidOperationException ioe)
2419 MessageBox.Show("This application cannot be updated. It is likely not a ClickOnce application. Error: " + ioe.Message);
2423 if (info.UpdateAvailable)
2425 Boolean doUpdate = true;
2427 if (!info.IsUpdateRequired)
2429 DialogResult dr = MessageBox.Show("An update is available. Would you like to update the application now?", "Update Available", MessageBoxButtons.OKCancel);
2430 if (!(DialogResult.OK == dr))
2437 // Display a message that the application MUST reboot. Display the minimum required version.
2438 MessageBox.Show("This application has detected a mandatory update from your current " +
2439 "version to version " + info.MinimumRequiredVersion.ToString() +
2440 ". The application will now install the update and restart.",
2441 "Update Available", MessageBoxButtons.OK,
2442 MessageBoxIcon.Information);
2450 MessageBox.Show("The application has been upgraded, and will now restart.");
2451 Application.Restart();
2453 catch (DeploymentDownloadException dde)
2455 MessageBox.Show("Cannot install the latest version of the application. \n\nPlease check your network connection, or try again later. Error: " + dde);
2462 MessageBox.Show("You are already running the latest version.", "Application up-to-date");
2471 private void SysTrayHideShow()
2477 WindowState = FormWindowState.Normal;
2482 /// Use to handle minimize events.
2484 /// <param name="sender"></param>
2485 /// <param name="e"></param>
2486 private void MainForm_SizeChanged(object sender, EventArgs e)
2488 if (WindowState == FormWindowState.Minimized && Properties.Settings.Default.MinimizeToTray)
2500 /// <param name="sender"></param>
2501 /// <param name="e"></param>
2502 private void tableLayoutPanel_SizeChanged(object sender, EventArgs e)
2504 //Our table layout size has changed which means our display size has changed.
2505 //We need to re-create our bitmap.
2506 iCreateBitmap = true;
2512 /// <param name="sender"></param>
2513 /// <param name="e"></param>
2514 private void buttonSelectFile_Click(object sender, EventArgs e)
2516 //openFileDialog1.InitialDirectory = "c:\\";
2517 //openFileDialog.Filter = "EXE files (*.exe)|*.exe|All files (*.*)|*.*";
2518 //openFileDialog.FilterIndex = 1;
2519 openFileDialog.RestoreDirectory = true;
2521 if (DlgBox.ShowDialog(openFileDialog) == DialogResult.OK)
2523 labelStartFileName.Text = openFileDialog.FileName;
2524 Properties.Settings.Default.StartFileName = openFileDialog.FileName;
2525 Properties.Settings.Default.Save();
2532 /// <param name="sender"></param>
2533 /// <param name="e"></param>
2534 private void comboBoxOpticalDrives_SelectedIndexChanged(object sender, EventArgs e)
2536 //Save the optical drive the user selected for ejection
2537 Properties.Settings.Default.OpticalDriveToEject = comboBoxOpticalDrives.SelectedItem.ToString();
2538 Properties.Settings.Default.Save();
2545 private void LogsUpdate()
2547 if (iWriter != null)
2555 /// Broadcast messages to subscribers.
2557 /// <param name="message"></param>
2558 protected override void WndProc(ref Message aMessage)
2562 if (OnWndProc!=null)
2564 OnWndProc(ref aMessage);
2567 base.WndProc(ref aMessage);
2570 private void checkBoxCecEnabled_CheckedChanged(object sender, EventArgs e)
2572 //Save CEC enabled status
2573 Properties.Settings.Default.CecEnabled = checkBoxCecEnabled.Checked;
2574 Properties.Settings.Default.Save();
2579 private void comboBoxHdmiPort_SelectedIndexChanged(object sender, EventArgs e)
2581 //Save CEC HDMI port
2582 Properties.Settings.Default.CecHdmiPort = Convert.ToByte(comboBoxHdmiPort.SelectedIndex);
2583 Properties.Settings.Default.CecHdmiPort++;
2584 Properties.Settings.Default.Save();
2592 private void ResetCec()
2594 if (iCecManager==null)
2596 //Thus skipping initial UI setup
2602 if (Properties.Settings.Default.CecEnabled)
2604 iCecManager.Start(Handle, "CEC",
2605 Properties.Settings.Default.CecHdmiPort);
2614 private void SetupCecLogLevel()
2617 iCecManager.Client.LogLevel = 0;
2619 if (checkBoxCecLogError.Checked)
2620 iCecManager.Client.LogLevel |= (int)CecLogLevel.Error;
2622 if (checkBoxCecLogWarning.Checked)
2623 iCecManager.Client.LogLevel |= (int)CecLogLevel.Warning;
2625 if (checkBoxCecLogNotice.Checked)
2626 iCecManager.Client.LogLevel |= (int)CecLogLevel.Notice;
2628 if (checkBoxCecLogTraffic.Checked)
2629 iCecManager.Client.LogLevel |= (int)CecLogLevel.Traffic;
2631 if (checkBoxCecLogDebug.Checked)
2632 iCecManager.Client.LogLevel |= (int)CecLogLevel.Debug;
2634 iCecManager.Client.FilterOutPollLogs = checkBoxCecLogNoPoll.Checked;
2638 private void ButtonStartIdleClient_Click(object sender, EventArgs e)
2643 private void buttonClearLogs_Click(object sender, EventArgs e)
2645 richTextBoxLogs.Clear();
2648 private void checkBoxCecLogs_CheckedChanged(object sender, EventArgs e)
2657 /// <param name="aEvent"></param>
2658 private void SelectEvent(Event aEvent)
2665 string key = aEvent.GetType().Name;
2666 TreeNode[] res=iTreeViewEvents.Nodes.Find(key, false);
2669 iTreeViewEvents.SelectedNode = res[0];
2670 iTreeViewEvents.Focus();
2677 /// Get the current event based on event tree view selection.
2679 /// <returns></returns>
2680 private Event CurrentEvent()
2682 //Walk up the tree from the selected node to find our event
2683 TreeNode node = iTreeViewEvents.SelectedNode;
2684 Event selectedEvent = null;
2685 while (node != null)
2687 if (node.Tag is Event)
2689 selectedEvent = (Event)node.Tag;
2695 return selectedEvent;
2699 /// Get the current action based on event tree view selection
2701 /// <returns></returns>
2702 private SharpLib.Ear.Action CurrentAction()
2704 TreeNode node = iTreeViewEvents.SelectedNode;
2705 if (node != null && node.Tag is SharpLib.Ear.Action)
2707 return (SharpLib.Ear.Action) node.Tag;
2716 /// <param name="sender"></param>
2717 /// <param name="e"></param>
2718 private void buttonAddAction_Click(object sender, EventArgs e)
2720 Event selectedEvent = CurrentEvent();
2721 if (selectedEvent == null)
2723 //We did not find a corresponding event
2727 FormEditAction ea = new FormEditAction();
2728 DialogResult res = CodeProject.Dialog.DlgBox.ShowDialog(ea);
2729 if (res == DialogResult.OK)
2731 selectedEvent.Actions.Add(ea.Action);
2732 Properties.Settings.Default.Actions = ManagerEventAction.Current;
2733 Properties.Settings.Default.Save();
2734 PopulateEventsTreeView();
2741 /// <param name="sender"></param>
2742 /// <param name="e"></param>
2743 private void buttonDeleteAction_Click(object sender, EventArgs e)
2746 SharpLib.Ear.Action action = CurrentAction();
2749 //Must select action node
2753 ManagerEventAction.Current.RemoveAction(action);
2754 Properties.Settings.Default.Actions = ManagerEventAction.Current;
2755 Properties.Settings.Default.Save();
2756 PopulateEventsTreeView();
2759 private void iTreeViewEvents_AfterSelect(object sender, TreeViewEventArgs e)
2761 //Enable buttons according to selected item
2762 buttonAddAction.Enabled = CurrentEvent() != null;
2763 buttonDeleteAction.Enabled = CurrentAction() != null;