diff --git a/Dev14/Dev14.csproj b/Dev14/Dev14.csproj index 8dcd0b8..8c953db 100644 --- a/Dev14/Dev14.csproj +++ b/Dev14/Dev14.csproj @@ -53,6 +53,8 @@ + + Form @@ -175,12 +177,15 @@ False + + - - ..\packages\WakaTime.Shared.ExtensionUtils.4.0.0\lib\netstandard2.0\WakaTime.Shared.ExtensionUtils.dll + + + ..\packages\WakaTime.Shared.ExtensionUtils.4.2.0\lib\netstandard2.0\WakaTime.Shared.ExtensionUtils.dll diff --git a/Dev14/ExtensionUtils/StatusbarControl.cs b/Dev14/ExtensionUtils/StatusbarControl.cs new file mode 100644 index 0000000..32ec74c --- /dev/null +++ b/Dev14/ExtensionUtils/StatusbarControl.cs @@ -0,0 +1,57 @@ +using Microsoft.VisualStudio.Shell; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; + +namespace WakaTime.ExtensionUtils +{ + internal class StatusbarControl : TextBlock + { + private const string Icon = "🕑"; + + private readonly Brush _normalBackground = new SolidColorBrush(Colors.Transparent); + private readonly Brush _hoverBackground = new SolidColorBrush(Colors.White) { Opacity = 0.2 }; + + public StatusbarControl() + { + Text = Icon; + Foreground = new SolidColorBrush(Colors.White); + Background = _normalBackground; + + VerticalAlignment = VerticalAlignment.Center; + Margin = new Thickness(7, 0, 7, 0); + Padding = new Thickness(7, 0, 7, 0); + + MouseEnter += (s, e) => + { + Cursor = Cursors.Hand; + Background = _hoverBackground; + }; + + MouseLeave += (s, e) => + { + Cursor = Cursors.Arrow; + Background = _normalBackground; + }; + + MouseLeftButtonUp += (s, e) => + { + // Open WakaTime in browser + System.Diagnostics.Process.Start("https://wakatime.com/"); + }; + } + + public void SetText(string text) + { + ThreadHelper.ThrowIfNotOnUIThread(); + Text = string.IsNullOrEmpty(text) ? Icon : $"{Icon} {text}"; + } + + public void SetToolTip(string toolTip) + { + ThreadHelper.ThrowIfNotOnUIThread(); + ToolTip = toolTip; + } + } +} diff --git a/Dev14/ExtensionUtils/StatusbarInjector.cs b/Dev14/ExtensionUtils/StatusbarInjector.cs new file mode 100644 index 0000000..3032925 --- /dev/null +++ b/Dev14/ExtensionUtils/StatusbarInjector.cs @@ -0,0 +1,87 @@ +using Microsoft.VisualStudio.Shell; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using Task = System.Threading.Tasks.Task; + +namespace WakaTime.ExtensionUtils +{ + internal static class StatusbarInjector + { + private static Panel _panel; + + private static DependencyObject FindChild(DependencyObject parent, string childName) + { + if (parent == null) + { + return null; + } + + int childrenCount = VisualTreeHelper.GetChildrenCount(parent); + + for (int i = 0; i < childrenCount; i++) + { + DependencyObject child = VisualTreeHelper.GetChild(parent, i); + + if (child is FrameworkElement frameworkElement && frameworkElement.Name == childName) + { + return frameworkElement; + } + + child = FindChild(child, childName); + + if (child != null) + { + return child; + } + } + + return null; + } + + private static async Task EnsureUIAsync() + { + while (_panel is null) + { + _panel = FindChild(Application.Current.MainWindow, "StatusBarPanel") as DockPanel; + if (_panel is null) + { + // Start window is showing. Need to wait for status bar render. + await Task.Delay(5000); + } + } + } + + public static async Task InjectControlAsync(FrameworkElement element) + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + await EnsureUIAsync(); + + // Place the element to the left of all elements on the right side of the status bar + element.SetValue(DockPanel.DockProperty, Dock.Right); + int index = GetLastDockRightElementIndex(_panel.Children) + 1; + _panel.Children.Insert(index, element); + } + + private static int GetLastDockRightElementIndex(UIElementCollection elements) + { + int lastDockRightElementIndex = 0; + + int elementCount = elements.Count; + for (int i = 0; i < elementCount; i++) + { + object element = elements[i]; + + if (element is DependencyObject dependencyObject && + dependencyObject.GetValue(DockPanel.DockProperty) is Dock dockProperty && + dockProperty == Dock.Right) + { + lastDockRightElementIndex = i; + } + } + + return lastDockRightElementIndex; + } + } +} diff --git a/Dev14/WakaTimePackage.cs b/Dev14/WakaTimePackage.cs index addbdeb..ecd5697 100644 --- a/Dev14/WakaTimePackage.cs +++ b/Dev14/WakaTimePackage.cs @@ -50,6 +50,7 @@ public sealed class WakaTimePackage : AsyncPackage private SettingsForm _settingsForm; private bool _isBuildRunning; private string _solutionName; + private StatusbarControl _statusbarControl; /// /// Initialization of the package; this method is called right after the package is sited, so this is the place @@ -95,12 +96,24 @@ private async Task InitializeAsync(CancellationToken cancellationToken) try { - await _wakatime.InitializeAsync(); + // Initialize _wakatime in background, which may take several seconds + Task wakaTimeInitializationTask = _wakatime.InitializeAsync(); // When initialized asynchronously, the current thread may be a background thread at this point. // Do any initialization that requires the UI thread after switching to the UI thread. await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + // Inject control to status bar + _statusbarControl = new StatusbarControl(); + _statusbarControl.SetText("Initializing..."); + _statusbarControl.SetToolTip("WakaTime: Initializing..."); + await StatusbarInjector.InjectControlAsync(_statusbarControl); + + // Wait for _wakatime to complete initialization,and display today's coding time on status bar + await wakaTimeInitializationTask; + UpdateTimeOnStatusbarControl(_wakatime.TotalTimeToday, _wakatime.TotalTimeTodayDetailed); + _wakatime.TotalTimeTodayUpdated += WakatimeTotalTimeTodayUpdated; + // Visual Studio Events _docEvents = _dte.Events.DocumentEvents; _windowEvents = _dte.Events.WindowEvents; @@ -380,6 +393,30 @@ private void TextEditorEventsLineChanged(TextPoint startPoint, TextPoint endPoin _logger.Error("TextEditorEventsLineChanged", ex); } } + + private void WakatimeTotalTimeTodayUpdated(object sender, TotalTimeTodayUpdatedEventArgs e) + { + _ = JoinableTaskFactory.RunAsync(async () => + { + await JoinableTaskFactory.SwitchToMainThreadAsync(); + UpdateTimeOnStatusbarControl(e.TotalTimeToday, e.TotalTimeTodayDetailed); + }); + } + + private void UpdateTimeOnStatusbarControl(string totalTimeToday, string totalTimeTodayDetailed) + { + ThreadHelper.ThrowIfNotOnUIThread(); + + string text = string.IsNullOrEmpty(totalTimeToday) ? "WakaTime" : totalTimeToday; + _statusbarControl?.SetText(text); + + string toolTip = "WakaTime: Today's coding time"; + if (!string.IsNullOrEmpty(totalTimeTodayDetailed)) + { + toolTip += Environment.NewLine + totalTimeTodayDetailed; + } + _statusbarControl?.SetToolTip(toolTip); + } } internal static class CoreAssembly diff --git a/Dev14/packages.config b/Dev14/packages.config index 26858ec..ea7b271 100644 --- a/Dev14/packages.config +++ b/Dev14/packages.config @@ -19,5 +19,5 @@ - + \ No newline at end of file diff --git a/Dev16/Dev16.csproj b/Dev16/Dev16.csproj index 3f1b891..d1d8b10 100644 --- a/Dev16/Dev16.csproj +++ b/Dev16/Dev16.csproj @@ -54,6 +54,8 @@ + + Form @@ -93,7 +95,7 @@ all - 4.0.0 + 4.2.0 @@ -118,12 +120,15 @@ + + + diff --git a/Dev16/ExtensionUtils/StatusbarControl.cs b/Dev16/ExtensionUtils/StatusbarControl.cs new file mode 100644 index 0000000..32ec74c --- /dev/null +++ b/Dev16/ExtensionUtils/StatusbarControl.cs @@ -0,0 +1,57 @@ +using Microsoft.VisualStudio.Shell; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; + +namespace WakaTime.ExtensionUtils +{ + internal class StatusbarControl : TextBlock + { + private const string Icon = "🕑"; + + private readonly Brush _normalBackground = new SolidColorBrush(Colors.Transparent); + private readonly Brush _hoverBackground = new SolidColorBrush(Colors.White) { Opacity = 0.2 }; + + public StatusbarControl() + { + Text = Icon; + Foreground = new SolidColorBrush(Colors.White); + Background = _normalBackground; + + VerticalAlignment = VerticalAlignment.Center; + Margin = new Thickness(7, 0, 7, 0); + Padding = new Thickness(7, 0, 7, 0); + + MouseEnter += (s, e) => + { + Cursor = Cursors.Hand; + Background = _hoverBackground; + }; + + MouseLeave += (s, e) => + { + Cursor = Cursors.Arrow; + Background = _normalBackground; + }; + + MouseLeftButtonUp += (s, e) => + { + // Open WakaTime in browser + System.Diagnostics.Process.Start("https://wakatime.com/"); + }; + } + + public void SetText(string text) + { + ThreadHelper.ThrowIfNotOnUIThread(); + Text = string.IsNullOrEmpty(text) ? Icon : $"{Icon} {text}"; + } + + public void SetToolTip(string toolTip) + { + ThreadHelper.ThrowIfNotOnUIThread(); + ToolTip = toolTip; + } + } +} diff --git a/Dev16/ExtensionUtils/StatusbarInjector.cs b/Dev16/ExtensionUtils/StatusbarInjector.cs new file mode 100644 index 0000000..3032925 --- /dev/null +++ b/Dev16/ExtensionUtils/StatusbarInjector.cs @@ -0,0 +1,87 @@ +using Microsoft.VisualStudio.Shell; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using Task = System.Threading.Tasks.Task; + +namespace WakaTime.ExtensionUtils +{ + internal static class StatusbarInjector + { + private static Panel _panel; + + private static DependencyObject FindChild(DependencyObject parent, string childName) + { + if (parent == null) + { + return null; + } + + int childrenCount = VisualTreeHelper.GetChildrenCount(parent); + + for (int i = 0; i < childrenCount; i++) + { + DependencyObject child = VisualTreeHelper.GetChild(parent, i); + + if (child is FrameworkElement frameworkElement && frameworkElement.Name == childName) + { + return frameworkElement; + } + + child = FindChild(child, childName); + + if (child != null) + { + return child; + } + } + + return null; + } + + private static async Task EnsureUIAsync() + { + while (_panel is null) + { + _panel = FindChild(Application.Current.MainWindow, "StatusBarPanel") as DockPanel; + if (_panel is null) + { + // Start window is showing. Need to wait for status bar render. + await Task.Delay(5000); + } + } + } + + public static async Task InjectControlAsync(FrameworkElement element) + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + await EnsureUIAsync(); + + // Place the element to the left of all elements on the right side of the status bar + element.SetValue(DockPanel.DockProperty, Dock.Right); + int index = GetLastDockRightElementIndex(_panel.Children) + 1; + _panel.Children.Insert(index, element); + } + + private static int GetLastDockRightElementIndex(UIElementCollection elements) + { + int lastDockRightElementIndex = 0; + + int elementCount = elements.Count; + for (int i = 0; i < elementCount; i++) + { + object element = elements[i]; + + if (element is DependencyObject dependencyObject && + dependencyObject.GetValue(DockPanel.DockProperty) is Dock dockProperty && + dockProperty == Dock.Right) + { + lastDockRightElementIndex = i; + } + } + + return lastDockRightElementIndex; + } + } +} diff --git a/Dev16/WakaTimePackage.cs b/Dev16/WakaTimePackage.cs index 8e9341a..4b6c206 100644 --- a/Dev16/WakaTimePackage.cs +++ b/Dev16/WakaTimePackage.cs @@ -51,6 +51,7 @@ public sealed class WakaTimePackage : AsyncPackage private SettingsForm _settingsForm; private bool _isBuildRunning; private string _solutionName; + private StatusbarControl _statusbarControl; /// /// Initialization of the package; this method is called right after the package is sited, so this is the place @@ -96,12 +97,24 @@ private async Task InitializeAsync(CancellationToken cancellationToken) try { - await _wakatime.InitializeAsync(); + // Initialize _wakatime in background, which may take several seconds + Task wakaTimeInitializationTask = _wakatime.InitializeAsync(); // When initialized asynchronously, the current thread may be a background thread at this point. // Do any initialization that requires the UI thread after switching to the UI thread. await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + // Inject control to status bar + _statusbarControl = new StatusbarControl(); + _statusbarControl.SetText("Initializing..."); + _statusbarControl.SetToolTip("WakaTime: Initializing..."); + await StatusbarInjector.InjectControlAsync(_statusbarControl); + + // Wait for _wakatime to complete initialization,and display today's coding time on status bar + await wakaTimeInitializationTask; + UpdateTimeOnStatusbarControl(_wakatime.TotalTimeToday, _wakatime.TotalTimeTodayDetailed); + _wakatime.TotalTimeTodayUpdated += WakatimeTotalTimeTodayUpdated; + // Visual Studio Events _docEvents = _dte.Events.DocumentEvents; _windowEvents = _dte.Events.WindowEvents; @@ -381,6 +394,30 @@ private void TextEditorEventsLineChanged(TextPoint startPoint, TextPoint endPoin _logger.Error("TextEditorEventsLineChanged", ex); } } + + private void WakatimeTotalTimeTodayUpdated(object sender, TotalTimeTodayUpdatedEventArgs e) + { + _ = JoinableTaskFactory.RunAsync(async () => + { + await JoinableTaskFactory.SwitchToMainThreadAsync(); + UpdateTimeOnStatusbarControl(e.TotalTimeToday, e.TotalTimeTodayDetailed); + }); + } + + private void UpdateTimeOnStatusbarControl(string totalTimeToday, string totalTimeTodayDetailed) + { + ThreadHelper.ThrowIfNotOnUIThread(); + + string text = string.IsNullOrEmpty(totalTimeToday) ? "WakaTime" : totalTimeToday; + _statusbarControl?.SetText(text); + + string toolTip = "WakaTime: Today's coding time"; + if (!string.IsNullOrEmpty(totalTimeTodayDetailed)) + { + toolTip += Environment.NewLine + totalTimeTodayDetailed; + } + _statusbarControl?.SetToolTip(toolTip); + } } internal static class CoreAssembly diff --git a/Dev17/Dev17.csproj b/Dev17/Dev17.csproj index a58cd32..638f842 100644 --- a/Dev17/Dev17.csproj +++ b/Dev17/Dev17.csproj @@ -62,6 +62,8 @@ + + Form @@ -89,12 +91,15 @@ + + + @@ -105,7 +110,7 @@ all - 4.0.0 + 4.2.0 diff --git a/Dev17/ExtensionUtils/StatusbarControl.cs b/Dev17/ExtensionUtils/StatusbarControl.cs new file mode 100644 index 0000000..32ec74c --- /dev/null +++ b/Dev17/ExtensionUtils/StatusbarControl.cs @@ -0,0 +1,57 @@ +using Microsoft.VisualStudio.Shell; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; + +namespace WakaTime.ExtensionUtils +{ + internal class StatusbarControl : TextBlock + { + private const string Icon = "🕑"; + + private readonly Brush _normalBackground = new SolidColorBrush(Colors.Transparent); + private readonly Brush _hoverBackground = new SolidColorBrush(Colors.White) { Opacity = 0.2 }; + + public StatusbarControl() + { + Text = Icon; + Foreground = new SolidColorBrush(Colors.White); + Background = _normalBackground; + + VerticalAlignment = VerticalAlignment.Center; + Margin = new Thickness(7, 0, 7, 0); + Padding = new Thickness(7, 0, 7, 0); + + MouseEnter += (s, e) => + { + Cursor = Cursors.Hand; + Background = _hoverBackground; + }; + + MouseLeave += (s, e) => + { + Cursor = Cursors.Arrow; + Background = _normalBackground; + }; + + MouseLeftButtonUp += (s, e) => + { + // Open WakaTime in browser + System.Diagnostics.Process.Start("https://wakatime.com/"); + }; + } + + public void SetText(string text) + { + ThreadHelper.ThrowIfNotOnUIThread(); + Text = string.IsNullOrEmpty(text) ? Icon : $"{Icon} {text}"; + } + + public void SetToolTip(string toolTip) + { + ThreadHelper.ThrowIfNotOnUIThread(); + ToolTip = toolTip; + } + } +} diff --git a/Dev17/ExtensionUtils/StatusbarInjector.cs b/Dev17/ExtensionUtils/StatusbarInjector.cs new file mode 100644 index 0000000..3032925 --- /dev/null +++ b/Dev17/ExtensionUtils/StatusbarInjector.cs @@ -0,0 +1,87 @@ +using Microsoft.VisualStudio.Shell; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using Task = System.Threading.Tasks.Task; + +namespace WakaTime.ExtensionUtils +{ + internal static class StatusbarInjector + { + private static Panel _panel; + + private static DependencyObject FindChild(DependencyObject parent, string childName) + { + if (parent == null) + { + return null; + } + + int childrenCount = VisualTreeHelper.GetChildrenCount(parent); + + for (int i = 0; i < childrenCount; i++) + { + DependencyObject child = VisualTreeHelper.GetChild(parent, i); + + if (child is FrameworkElement frameworkElement && frameworkElement.Name == childName) + { + return frameworkElement; + } + + child = FindChild(child, childName); + + if (child != null) + { + return child; + } + } + + return null; + } + + private static async Task EnsureUIAsync() + { + while (_panel is null) + { + _panel = FindChild(Application.Current.MainWindow, "StatusBarPanel") as DockPanel; + if (_panel is null) + { + // Start window is showing. Need to wait for status bar render. + await Task.Delay(5000); + } + } + } + + public static async Task InjectControlAsync(FrameworkElement element) + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + await EnsureUIAsync(); + + // Place the element to the left of all elements on the right side of the status bar + element.SetValue(DockPanel.DockProperty, Dock.Right); + int index = GetLastDockRightElementIndex(_panel.Children) + 1; + _panel.Children.Insert(index, element); + } + + private static int GetLastDockRightElementIndex(UIElementCollection elements) + { + int lastDockRightElementIndex = 0; + + int elementCount = elements.Count; + for (int i = 0; i < elementCount; i++) + { + object element = elements[i]; + + if (element is DependencyObject dependencyObject && + dependencyObject.GetValue(DockPanel.DockProperty) is Dock dockProperty && + dockProperty == Dock.Right) + { + lastDockRightElementIndex = i; + } + } + + return lastDockRightElementIndex; + } + } +} diff --git a/Dev17/WakaTimePackage.cs b/Dev17/WakaTimePackage.cs index cdd38d4..367a53f 100644 --- a/Dev17/WakaTimePackage.cs +++ b/Dev17/WakaTimePackage.cs @@ -51,6 +51,7 @@ public sealed class WakaTimePackage : AsyncPackage private SettingsForm _settingsForm; private bool _isBuildRunning; private string _solutionName; + private StatusbarControl _statusbarControl; /// /// Initialization of the package; this method is called right after the package is sited, so this is the place @@ -96,12 +97,24 @@ private async Task InitializeAsync(CancellationToken cancellationToken) try { - await _wakatime.InitializeAsync(); + // Initialize _wakatime in background, which may take several seconds + Task wakaTimeInitializationTask = _wakatime.InitializeAsync(); // When initialized asynchronously, the current thread may be a background thread at this point. // Do any initialization that requires the UI thread after switching to the UI thread. await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + // Inject control to status bar + _statusbarControl = new StatusbarControl(); + _statusbarControl.SetText("Initializing..."); + _statusbarControl.SetToolTip("WakaTime: Initializing..."); + await StatusbarInjector.InjectControlAsync(_statusbarControl); + + // Wait for _wakatime to complete initialization,and display today's coding time on status bar + await wakaTimeInitializationTask; + UpdateTimeOnStatusbarControl(_wakatime.TotalTimeToday, _wakatime.TotalTimeTodayDetailed); + _wakatime.TotalTimeTodayUpdated += WakatimeTotalTimeTodayUpdated; + // Visual Studio Events _docEvents = _dte.Events.DocumentEvents; _windowEvents = _dte.Events.WindowEvents; @@ -381,6 +394,30 @@ private void TextEditorEventsLineChanged(TextPoint startPoint, TextPoint endPoin _logger.Error("TextEditorEventsLineChanged", ex); } } + + private void WakatimeTotalTimeTodayUpdated(object sender, TotalTimeTodayUpdatedEventArgs e) + { + _ = JoinableTaskFactory.RunAsync(async () => + { + await JoinableTaskFactory.SwitchToMainThreadAsync(); + UpdateTimeOnStatusbarControl(e.TotalTimeToday, e.TotalTimeTodayDetailed); + }); + } + + private void UpdateTimeOnStatusbarControl(string totalTimeToday, string totalTimeTodayDetailed) + { + ThreadHelper.ThrowIfNotOnUIThread(); + + string text = string.IsNullOrEmpty(totalTimeToday) ? "WakaTime" : totalTimeToday; + _statusbarControl?.SetText(text); + + string toolTip = "WakaTime: Today's coding time"; + if (!string.IsNullOrEmpty(totalTimeTodayDetailed)) + { + toolTip += Environment.NewLine + totalTimeTodayDetailed; + } + _statusbarControl?.SetToolTip(toolTip); + } } internal static class CoreAssembly