diff --git a/RetroBar/App.xaml.cs b/RetroBar/App.xaml.cs
index 76290d29..3ead6e7e 100644
--- a/RetroBar/App.xaml.cs
+++ b/RetroBar/App.xaml.cs
@@ -115,8 +115,11 @@ private ShellManager SetupManagedShell()
ShellConfig config = ShellManager.DefaultShellConfig;
config.PinnedNotifyIcons = Settings.Instance.NotifyIconBehaviors.Where(setting => setting.Behavior == NotifyIconBehavior.AlwaysShow).Select(setting => setting.Identifier).ToArray();
+ config.AutoStartTasksService = false;
- return new ShellManager(config);
+ ShellManager manager = new ShellManager(config);
+ manager.Tasks.Initialize(new TaskCategoryProvider(), config.MultiMonAwareTasksService);
+ return manager;
}
public void RestartApp()
diff --git a/RetroBar/Controls/TaskButton.xaml b/RetroBar/Controls/TaskButton.xaml
index 6eef4e63..3153232b 100644
--- a/RetroBar/Controls/TaskButton.xaml
+++ b/RetroBar/Controls/TaskButton.xaml
@@ -4,6 +4,7 @@
xmlns:converters="clr-namespace:RetroBar.Converters"
xmlns:controls="clr-namespace:RetroBar.Controls"
xmlns:utilities="clr-namespace:RetroBar.Utilities"
+ xmlns:managedShellTasks="clr-namespace:ManagedShell.WindowsTasks;assembly=ManagedShell.WindowsTasks"
Loaded="TaskButton_OnLoaded"
Unloaded="TaskButton_OnUnloaded">
@@ -12,8 +13,12 @@
+
+
+
diff --git a/RetroBar/Controls/TaskButton.xaml.cs b/RetroBar/Controls/TaskButton.xaml.cs
index 3a6f28d9..e13e6db6 100644
--- a/RetroBar/Controls/TaskButton.xaml.cs
+++ b/RetroBar/Controls/TaskButton.xaml.cs
@@ -1,10 +1,15 @@
using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
+using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
+using System.Windows.Media;
using System.Windows.Media.Animation;
using ManagedShell.Common.Helpers;
using ManagedShell.Interop;
@@ -33,22 +38,71 @@ public TaskList Host
private DelayedActivationHandler dragHandler;
private bool _isLoaded;
+ private INotifyCollectionChanged _subscribedCollection;
+ private List _subscribedWindows = new List();
+ private bool _isGroupMenuOpen;
+ private bool _allowOpenGroupMenu;
+
+ public static readonly DependencyProperty TasksProperty = DependencyProperty.Register("Tasks", typeof(IEnumerable), typeof(TaskButton), new PropertyMetadata(null, OnTasksChanged));
+
+ private static void OnTasksChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (d is TaskButton tb)
+ {
+ tb.UpdateDisplayIcon();
+ }
+ }
+
+ public IEnumerable Tasks
+ {
+ get => (IEnumerable)GetValue(TasksProperty);
+ set => SetValue(TasksProperty, value);
+ }
public TaskButton()
{
InitializeComponent();
SetStyle();
+ DataContextChanged += (s, e) => UpdateDisplayIcon();
}
private void SetStyle()
{
- MultiBinding multiBinding = new MultiBinding();
- multiBinding.Converter = StyleConverter;
+ ApplicationWindow.WindowState state = ApplicationWindow.WindowState.Inactive;
+
+ if (Tasks != null)
+ {
+ var windows = Tasks.OfType().ToList();
+ if (windows.Any(w => w.State == ApplicationWindow.WindowState.Active))
+ {
+ state = ApplicationWindow.WindowState.Active;
+ }
+ else if (windows.Any(w => w.State == ApplicationWindow.WindowState.Flashing))
+ {
+ state = ApplicationWindow.WindowState.Flashing;
+ }
+ }
+ else if (Window != null)
+ {
+ state = Window.State;
+ }
+
+ var fxStyle = this.FindResource("TaskButton") as Style;
+ if (state == ApplicationWindow.WindowState.Active)
+ {
+ fxStyle = this.FindResource("TaskButtonActive") as Style;
+ }
+ else if (state == ApplicationWindow.WindowState.Flashing)
+ {
+ fxStyle = this.FindResource("TaskButtonFlashing") as Style;
+ }
- multiBinding.Bindings.Add(new Binding { RelativeSource = RelativeSource.Self });
- multiBinding.Bindings.Add(new Binding("State"));
+ if (AppButton.ContextMenu?.IsOpen == true || _isGroupMenuOpen)
+ {
+ fxStyle = this.FindResource("TaskButtonActive") as Style;
+ }
- AppButton.SetBinding(StyleProperty, multiBinding);
+ AppButton.Style = fxStyle;
}
private void ScrollIntoView()
@@ -58,7 +112,21 @@ private void ScrollIntoView()
return;
}
- if (Window.State == ApplicationWindow.WindowState.Active)
+ ApplicationWindow.WindowState state = ApplicationWindow.WindowState.Inactive;
+ if (Tasks != null)
+ {
+ var windows = Tasks.OfType().ToList();
+ if (windows.Any(w => w.State == ApplicationWindow.WindowState.Active))
+ {
+ state = ApplicationWindow.WindowState.Active;
+ }
+ }
+ else
+ {
+ state = Window.State;
+ }
+
+ if (state == ApplicationWindow.WindowState.Active)
{
BringIntoView();
}
@@ -85,8 +153,45 @@ private void Animate()
private void TaskButton_OnLoaded(object sender, RoutedEventArgs e)
{
- Window = DataContext as ApplicationWindow;
+ _isLoaded = true;
+ if (DataContext is CollectionViewGroup group)
+ {
+ Window = group.Items.Count > 0 ? group.Items[0] as ApplicationWindow : null;
+ AppButton.DataContext = Window;
+
+ if (group.Items is INotifyCollectionChanged collectionChanged)
+ {
+ _subscribedCollection = collectionChanged;
+ _subscribedCollection.CollectionChanged -= GroupItems_CollectionChanged;
+ _subscribedCollection.CollectionChanged += GroupItems_CollectionChanged;
+ }
+ foreach (ApplicationWindow w in group.Items)
+ {
+ if (!_subscribedWindows.Contains(w))
+ {
+ w.PropertyChanged -= Window_PropertyChanged;
+ w.PropertyChanged += Window_PropertyChanged;
+ w.GetButtonRect -= Window_GetButtonRect;
+ w.GetButtonRect += Window_GetButtonRect;
+ _subscribedWindows.Add(w);
+ }
+ }
+ }
+ else
+ {
+ Window = DataContext as ApplicationWindow;
+ AppButton.DataContext = Window;
+ if (Window != null && !_subscribedWindows.Contains(Window))
+ {
+ Window.PropertyChanged -= Window_PropertyChanged;
+ Window.PropertyChanged += Window_PropertyChanged;
+ Window.GetButtonRect -= Window_GetButtonRect;
+ Window.GetButtonRect += Window_GetButtonRect;
+ _subscribedWindows.Add(Window);
+ }
+ }
+ UpdateDisplayIcon();
Settings.Instance.PropertyChanged += Settings_PropertyChanged;
dragHandler = new DelayedActivationHandler(() =>
@@ -94,18 +199,68 @@ private void TaskButton_OnLoaded(object sender, RoutedEventArgs e)
Window?.BringToFront();
});
- if (Window != null)
+ if (Settings.Instance.SlideTaskbarButtons && Host?.Host?.Orientation == Orientation.Horizontal)
{
- Window.GetButtonRect += Window_GetButtonRect;
- Window.PropertyChanged += Window_PropertyChanged;
+ Animate();
}
- if (Settings.Instance.SlideTaskbarButtons && Host?.Host?.Orientation == Orientation.Horizontal)
+ if (AppButton.ToolTip is ToolTip toolTip)
{
- Animate();
+ toolTip.CustomPopupPlacementCallback = new System.Windows.Controls.Primitives.CustomPopupPlacementCallback(ToolTipCustomPlacement);
}
_isLoaded = true;
+ SetStyle();
+ }
+
+ private System.Windows.Controls.Primitives.CustomPopupPlacement[] ToolTipCustomPlacement(Size popupSize, Size targetSize, Point offset)
+ {
+ double x = (targetSize.Width - popupSize.Width) / 2.0;
+ double y = -popupSize.Height - 5;
+ System.Windows.Controls.Primitives.PopupPrimaryAxis axis = System.Windows.Controls.Primitives.PopupPrimaryAxis.Horizontal;
+
+ if (Settings.Instance.Edge == ManagedShell.AppBar.AppBarEdge.Top)
+ {
+ y = targetSize.Height + 5;
+ }
+
+ return new System.Windows.Controls.Primitives.CustomPopupPlacement[] {
+ new System.Windows.Controls.Primitives.CustomPopupPlacement(new Point(x, y), axis)
+ };
+ }
+
+ private void GroupItems_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+ {
+ if (e.OldItems != null)
+ {
+ foreach (ApplicationWindow w in e.OldItems)
+ {
+ w.PropertyChanged -= Window_PropertyChanged;
+ w.GetButtonRect -= Window_GetButtonRect;
+ _subscribedWindows.Remove(w);
+ }
+ }
+ if (e.NewItems != null)
+ {
+ foreach (ApplicationWindow w in e.NewItems)
+ {
+ if (!_subscribedWindows.Contains(w))
+ {
+ w.PropertyChanged += Window_PropertyChanged;
+ w.GetButtonRect += Window_GetButtonRect;
+ _subscribedWindows.Add(w);
+ }
+ }
+ }
+ // Update the main Window binding to point to the remaining active window
+ if (DataContext is CollectionViewGroup group)
+ {
+ Window = group.Items.Count > 0 ? group.Items[0] as ApplicationWindow : null;
+ AppButton.DataContext = Window;
+ }
+
+ UpdateDisplayIcon();
+ SetStyle();
}
private void Window_GetButtonRect(ref NativeMethods.ShortRect rect)
@@ -128,10 +283,43 @@ private void Window_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "State")
{
- ScrollIntoView();
+ Dispatcher.InvokeAsync(() =>
+ {
+ ScrollIntoView();
+ SetStyle();
+ });
+ }
+ else if (e.PropertyName == "Icon")
+ {
+ Dispatcher.InvokeAsync(() =>
+ {
+ UpdateDisplayIcon();
+ });
}
}
+ private void UpdateDisplayIcon()
+ {
+ if (Window == null)
+ {
+ DisplayIcon = null;
+ return;
+ }
+
+ if (Tasks != null)
+ {
+ var windows = Tasks.OfType().ToList();
+ if (windows.Count > 1 && !string.IsNullOrEmpty(Window.WinFileName))
+ {
+ bool useLargeIcons = Settings.Instance.TaskbarScale > 1 || (Application.Current.FindResource("UseLargeIcons") as bool? ?? false);
+ DisplayIcon = IconImageConverter.GetImageFromAssociatedIcon(Window.WinFileName, useLargeIcons ? ManagedShell.Common.Enums.IconSize.Large : ManagedShell.Common.Enums.IconSize.Small);
+ return;
+ }
+ }
+
+ DisplayIcon = Window.Icon;
+ }
+
private void TaskButton_OnUnloaded(object sender, RoutedEventArgs e)
{
if (!_isLoaded)
@@ -142,17 +330,61 @@ private void TaskButton_OnUnloaded(object sender, RoutedEventArgs e)
Settings.Instance.PropertyChanged -= Settings_PropertyChanged;
dragHandler?.Dispose();
- if (Window != null)
+ if (_subscribedCollection != null)
{
- Window.GetButtonRect -= Window_GetButtonRect;
- Window.PropertyChanged -= Window_PropertyChanged;
+ _subscribedCollection.CollectionChanged -= GroupItems_CollectionChanged;
+ _subscribedCollection = null;
}
+ foreach (ApplicationWindow w in _subscribedWindows)
+ {
+ w.GetButtonRect -= Window_GetButtonRect;
+ w.PropertyChanged -= Window_PropertyChanged;
+ }
+ _subscribedWindows.Clear();
+
_isLoaded = false;
}
private void AppButton_OnContextMenuOpening(object sender, ContextMenuEventArgs e)
{
+ var windows = Tasks?.OfType().ToList();
+ bool isGroup = windows != null && windows.Count > 1;
+
+ if (isGroup)
+ {
+ RestoreMenuItem.Visibility = Visibility.Collapsed;
+ MoveMenuItem.Visibility = Visibility.Collapsed;
+ SizeMenuItem.Visibility = Visibility.Collapsed;
+ MinimizeMenuItem.Visibility = Visibility.Collapsed;
+ MaximizeMenuItem.Visibility = Visibility.Collapsed;
+ EndTaskMenuItem.Visibility = Visibility.Collapsed;
+ CloseMenuItem.Visibility = Visibility.Collapsed;
+ SingleSeparator.Visibility = Visibility.Collapsed;
+
+ MinimizeGroupMenuItem.Visibility = Visibility.Visible;
+ GroupSeparator.Visibility = Visibility.Visible;
+ CloseGroupMenuItem.Visibility = Visibility.Visible;
+
+ MinimizeGroupMenuItem.IsEnabled = windows.Any(w => w.CanMinimize && w.ShowStyle != NativeMethods.WindowShowStyle.ShowMinimized);
+ CloseGroupMenuItem.IsEnabled = true;
+ CloseGroupMenuItem.FontWeight = FontWeights.Normal;
+ return;
+ }
+
+ MinimizeGroupMenuItem.Visibility = Visibility.Collapsed;
+ GroupSeparator.Visibility = Visibility.Collapsed;
+ CloseGroupMenuItem.Visibility = Visibility.Collapsed;
+
+ RestoreMenuItem.Visibility = Visibility.Visible;
+ MoveMenuItem.Visibility = Visibility.Visible;
+ SizeMenuItem.Visibility = Visibility.Visible;
+ MinimizeMenuItem.Visibility = Visibility.Visible;
+ MaximizeMenuItem.Visibility = Visibility.Visible;
+ EndTaskMenuItem.Visibility = Settings.Instance.ShowEndTaskButton ? Visibility.Visible : Visibility.Collapsed;
+ CloseMenuItem.Visibility = Visibility.Visible;
+ SingleSeparator.Visibility = Visibility.Visible;
+
if (Window == null)
{
return;
@@ -178,6 +410,32 @@ private void AppButton_OnContextMenuOpening(object sender, ContextMenuEventArgs
SizeMenuItem.IsEnabled = wss == NativeMethods.WindowShowStyle.ShowNormal && (ws & (int)NativeMethods.WindowStyles.WS_MAXIMIZEBOX) != 0;
}
+ private void MinimizeGroupMenuItem_OnClick(object sender, RoutedEventArgs e)
+ {
+ if (Tasks != null)
+ {
+ foreach (ApplicationWindow win in Tasks.OfType())
+ {
+ if (win.CanMinimize)
+ {
+ win.Minimize();
+ }
+ }
+ }
+ }
+
+ private void CloseGroupMenuItem_OnClick(object sender, RoutedEventArgs e)
+ {
+ if (Tasks != null)
+ {
+ var windows = Tasks.OfType().ToList();
+ foreach (ApplicationWindow win in windows)
+ {
+ win.Close();
+ }
+ }
+ }
+
private void CloseMenuItem_OnClick(object sender, RoutedEventArgs e)
{
Window?.Close();
@@ -240,15 +498,106 @@ private void MaximizeMenuItem_OnClick(object sender, RoutedEventArgs e)
Window?.Maximize();
}
+
+
+ private void OpenGroupMenu(List windows)
+ {
+ ContextMenu groupMenu = new ContextMenu();
+
+ foreach (var window in windows)
+ {
+ MenuItem menuItem = new MenuItem();
+ menuItem.Header = window.Title;
+
+ Image icon = new Image();
+ icon.SetBinding(Image.SourceProperty, new Binding(nameof(window.Icon)) { Source = window });
+ icon.Width = 16;
+ icon.Height = 16;
+ menuItem.Icon = icon;
+
+ // We need a local copy of the window reference for the closure
+ var localWindow = window;
+ menuItem.Click += (s, ev) =>
+ {
+ if (localWindow.State == ApplicationWindow.WindowState.Active && localWindow.CanMinimize)
+ {
+ localWindow.Minimize();
+ }
+ else
+ {
+ localWindow.BringToFront();
+ }
+ };
+ groupMenu.Items.Add(menuItem);
+ }
+
+ groupMenu.PlacementTarget = AppButton;
+
+ if (Settings.Instance.Edge == ManagedShell.AppBar.AppBarEdge.Top)
+ groupMenu.Placement = System.Windows.Controls.Primitives.PlacementMode.Bottom;
+ else if (Settings.Instance.Edge == ManagedShell.AppBar.AppBarEdge.Left)
+ groupMenu.Placement = System.Windows.Controls.Primitives.PlacementMode.Right;
+ else if (Settings.Instance.Edge == ManagedShell.AppBar.AppBarEdge.Right)
+ groupMenu.Placement = System.Windows.Controls.Primitives.PlacementMode.Left;
+ else
+ groupMenu.Placement = System.Windows.Controls.Primitives.PlacementMode.Top;
+
+ // Bind TextRenderingMode to match other menus if possible
+ Binding textRenderingBinding = new Binding("AllowFontSmoothingMenu");
+ textRenderingBinding.Source = Settings.Instance;
+ textRenderingBinding.Converter = FindResource("menuTextRenderingModeConverter") as IValueConverter;
+ if (textRenderingBinding.Converter != null)
+ {
+ groupMenu.SetBinding(System.Windows.Media.TextOptions.TextRenderingModeProperty, textRenderingBinding);
+ }
+
+ groupMenu.Closed += (s, ev) =>
+ {
+ _isGroupMenuOpen = false;
+ SetStyle();
+ };
+
+ _isGroupMenuOpen = true;
+ groupMenu.IsOpen = true;
+ SetStyle();
+ }
+
private void AppButton_OnClick(object sender, RoutedEventArgs e)
{
- if (PressedWindowState == ApplicationWindow.WindowState.Active && Window?.CanMinimize == true)
+ if (!_allowOpenGroupMenu)
{
- Window?.Minimize();
+ return;
+ }
+
+ if (Tasks != null)
+ {
+ var windows = Tasks.OfType().ToList();
+ if (windows.Count > 1)
+ {
+ OpenGroupMenu(windows);
+ return;
+ }
+ }
+
+ // Fallback for single windows or ungrouped task buttons
+ ApplicationWindow targetWindow = Window;
+
+ if (Tasks != null)
+ {
+ var windows = Tasks.OfType().ToList();
+ if (windows.Count == 1)
+ {
+ targetWindow = windows[0];
+ }
+ }
+
+ if (PressedWindowState == ApplicationWindow.WindowState.Active && targetWindow?.CanMinimize == true)
+ {
+ targetWindow?.Minimize();
}
else
{
- Window?.BringToFront();
+ targetWindow?.BringToFront();
}
}
@@ -256,7 +605,8 @@ private void AppButton_OnPreviewMouseDown(object sender, MouseButtonEventArgs e)
{
if (e.ChangedButton == MouseButton.Left)
{
- PressedWindowState = Window.State;
+ PressedWindowState = Window?.State ?? ApplicationWindow.WindowState.Inactive;
+ _allowOpenGroupMenu = !_isGroupMenuOpen;
}
}
@@ -286,6 +636,13 @@ private void Settings_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
SetStyle();
}
+ else if (e.PropertyName == nameof(Settings.TaskbarScale))
+ {
+ Dispatcher.InvokeAsync(() =>
+ {
+ UpdateDisplayIcon();
+ });
+ }
}
private void AppButton_OnDragEnter(object sender, DragEventArgs e)
@@ -300,7 +657,15 @@ private void AppButton_OnDragLeave(object sender, DragEventArgs e)
private void ContextMenu_OpenedOrClosed(object sender, RoutedEventArgs e)
{
- BindingOperations.GetMultiBindingExpression(AppButton, StyleProperty).UpdateTarget();
+ SetStyle();
+ }
+
+ public static readonly DependencyProperty DisplayIconProperty = DependencyProperty.Register("DisplayIcon", typeof(ImageSource), typeof(TaskButton));
+
+ public ImageSource DisplayIcon
+ {
+ get => (ImageSource)GetValue(DisplayIconProperty);
+ set => SetValue(DisplayIconProperty, value);
}
}
}
\ No newline at end of file
diff --git a/RetroBar/Controls/TaskList.xaml b/RetroBar/Controls/TaskList.xaml
index 3747412d..98b45ebd 100644
--- a/RetroBar/Controls/TaskList.xaml
+++ b/RetroBar/Controls/TaskList.xaml
@@ -3,6 +3,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:RetroBar.Controls"
xmlns:converters="clr-namespace:RetroBar.Converters"
+ xmlns:managedShellTasks="clr-namespace:ManagedShell.WindowsTasks;assembly=ManagedShell.WindowsTasks"
xmlns:dd="urn:gong-wpf-dragdrop"
Loaded="TaskList_OnLoaded"
Unloaded="TaskList_OnUnloaded"
@@ -18,9 +19,9 @@
+ ConverterParameter="leading"
+ Path="Orientation"
+ RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=Window}" />
@@ -45,8 +47,20 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -57,8 +71,8 @@
-
-
+
+
diff --git a/RetroBar/Controls/TaskList.xaml.cs b/RetroBar/Controls/TaskList.xaml.cs
index fd23ec5b..05321ae1 100644
--- a/RetroBar/Controls/TaskList.xaml.cs
+++ b/RetroBar/Controls/TaskList.xaml.cs
@@ -3,7 +3,9 @@
using ManagedShell.Common.Helpers;
using RetroBar.Utilities;
using System;
+using System.Collections.Generic;
using System.ComponentModel;
+using System.Linq;
using System.Windows;
using System.Windows.Controls;
@@ -21,6 +23,7 @@ public partial class TaskList : UserControl
private double TaskButtonLeftMargin;
private double TaskButtonRightMargin;
private ICollectionView taskbarItems;
+ internal System.Collections.IList UnderlyingTasks => taskbarItems?.SourceCollection as System.Collections.IList;
public static DependencyProperty ButtonWidthProperty = DependencyProperty.Register(nameof(ButtonWidth), typeof(double), typeof(TaskList), new PropertyMetadata(new double()));
@@ -64,9 +67,12 @@ public Taskbar Host
set { SetValue(HostProperty, value); }
}
+ public TaskDropHandler DropHandler { get; set; }
+
public TaskList()
{
InitializeComponent();
+ DropHandler = new TaskDropHandler(this);
}
private void SetStyles()
@@ -96,16 +102,16 @@ private void TaskList_OnLoaded(object sender, RoutedEventArgs e)
private void SetTasksCollection()
{
- if (!isLoaded && Tasks != null && Host != null)
+ if (!isLoaded && Tasks?.GroupedWindows != null)
{
taskbarItems = Tasks.CreateGroupedWindowsCollection();
if (taskbarItems != null)
{
taskbarItems.CollectionChanged += GroupedWindows_CollectionChanged;
taskbarItems.Filter = Tasks_Filter;
- }
- TasksList.ItemsSource = taskbarItems;
+ UpdateTasksListItemsSource();
+ }
Settings.Instance.PropertyChanged += Settings_PropertyChanged;
Host.hotkeyManager.TaskbarHotkeyPressed += TaskList_TaskbarHotkeyPressed;
@@ -114,6 +120,63 @@ private void SetTasksCollection()
}
}
+ private void UpdateTasksListItemsSource()
+ {
+ if (taskbarItems == null) return;
+
+ if (Settings.Instance.GroupTaskbarButtons)
+ {
+ if (taskbarItems is System.ComponentModel.ICollectionViewLiveShaping taskbarItemsView)
+ {
+ taskbarItemsView.IsLiveGrouping = true;
+ if (!taskbarItemsView.LiveGroupingProperties.Contains("Category"))
+ {
+ taskbarItemsView.LiveGroupingProperties.Add("Category");
+ }
+ }
+
+ bool hasCategoryGroup = false;
+ foreach (var groupDesc in taskbarItems.GroupDescriptions)
+ {
+ if (groupDesc is System.Windows.Data.PropertyGroupDescription propDesc && propDesc.PropertyName == "Category")
+ {
+ hasCategoryGroup = true;
+ break;
+ }
+ }
+ if (!hasCategoryGroup)
+ {
+ taskbarItems.GroupDescriptions.Add(new System.Windows.Data.PropertyGroupDescription("Category"));
+ }
+
+ TasksList.ItemsSource = taskbarItems.Groups;
+ }
+ else
+ {
+ if (taskbarItems is System.ComponentModel.ICollectionViewLiveShaping taskbarItemsView)
+ {
+ taskbarItemsView.IsLiveGrouping = false;
+ taskbarItemsView.LiveGroupingProperties.Remove("Category");
+ }
+
+ System.Windows.Data.PropertyGroupDescription categoryGroupDesc = null;
+ foreach (var groupDesc in taskbarItems.GroupDescriptions)
+ {
+ if (groupDesc is System.Windows.Data.PropertyGroupDescription propDesc && propDesc.PropertyName == "Category")
+ {
+ categoryGroupDesc = propDesc;
+ break;
+ }
+ }
+ if (categoryGroupDesc != null)
+ {
+ taskbarItems.GroupDescriptions.Remove(categoryGroupDesc);
+ }
+
+ TasksList.ItemsSource = taskbarItems;
+ }
+ }
+
private static void TasksChangedCallback(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
if (sender is TaskList taskList && e.OldValue == null && e.NewValue != null)
@@ -124,7 +187,11 @@ private static void TasksChangedCallback(DependencyObject sender, DependencyProp
private void Settings_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
- if (e.PropertyName == nameof(Settings.MultiMonMode))
+ if (e.PropertyName == nameof(Settings.GroupTaskbarButtons))
+ {
+ UpdateTasksListItemsSource();
+ }
+ else if (e.PropertyName == nameof(Settings.MultiMonMode))
{
taskbarItems?.Refresh();
}
diff --git a/RetroBar/Controls/TaskThumbnail.xaml.cs b/RetroBar/Controls/TaskThumbnail.xaml.cs
index f5835334..a608e077 100644
--- a/RetroBar/Controls/TaskThumbnail.xaml.cs
+++ b/RetroBar/Controls/TaskThumbnail.xaml.cs
@@ -48,7 +48,15 @@ public IntPtr Handle
private IntPtr _thumbHandle;
- public static DependencyProperty SourceWindowHandleProperty = DependencyProperty.Register(nameof(SourceWindowHandle), typeof(IntPtr), typeof(TaskThumbnail), new PropertyMetadata(new IntPtr()));
+ public static DependencyProperty SourceWindowHandleProperty = DependencyProperty.Register(nameof(SourceWindowHandle), typeof(IntPtr), typeof(TaskThumbnail), new PropertyMetadata(new IntPtr(), OnSourceWindowHandleChanged));
+
+ private static void OnSourceWindowHandleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (d is TaskThumbnail thumbnail)
+ {
+ thumbnail.RegisterThumbnail();
+ }
+ }
public IntPtr SourceWindowHandle
{
@@ -85,7 +93,11 @@ public NativeMethods.Rect Rect
if (this == null)
return new NativeMethods.Rect(0, 0, 0, 0);
- var generalTransform = TransformToAncestor((System.Windows.Media.Visual)Parent);
+ var source = PresentationSource.FromVisual(this);
+ if (source == null || source.RootVisual == null)
+ return new NativeMethods.Rect(0, 0, 0, 0);
+
+ var generalTransform = TransformToAncestor(source.RootVisual);
var leftTopPoint = generalTransform.Transform(new Point(0, 0));
return new NativeMethods.Rect(
(int)(leftTopPoint.X * DpiScale),
@@ -190,17 +202,35 @@ private void UserControl_Unloaded(object sender, RoutedEventArgs e)
}
}
- private void UserControl_Loaded(object sender, RoutedEventArgs e)
+ private void RegisterThumbnail()
{
- DpiScale = PresentationSource.FromVisual(this).CompositionTarget.TransformToDevice.M11;
+ if (_thumbHandle != IntPtr.Zero)
+ {
+ NativeMethods.DwmUnregisterThumbnail(_thumbHandle);
+ _thumbHandle = IntPtr.Zero;
+ }
if (NativeMethods.DwmIsCompositionEnabled() && SourceWindowHandle != IntPtr.Zero && Handle != IntPtr.Zero && NativeMethods.DwmRegisterThumbnail(Handle, SourceWindowHandle, out _thumbHandle) == 0)
{
Refresh();
// once loaded, we need to refresh the thumbnail...
- _renderingHandler = (s, a) => Dispatcher.BeginInvoke(DispatcherPriority.Render, new Action(Refresh));
- CompositionTarget.Rendering += _renderingHandler;
+ if (_renderingHandler == null)
+ {
+ _renderingHandler = (s, a) => Dispatcher.BeginInvoke(DispatcherPriority.Render, new Action(Refresh));
+ CompositionTarget.Rendering += _renderingHandler;
+ }
}
+ }
+
+ private void UserControl_Loaded(object sender, RoutedEventArgs e)
+ {
+ var source = PresentationSource.FromVisual(this);
+ if (source?.CompositionTarget != null)
+ {
+ DpiScale = source.CompositionTarget.TransformToDevice.M11;
+ }
+
+ RegisterThumbnail();
_toolTipTimer.Start();
}
diff --git a/RetroBar/Converters/TaskGroupVisibilityConverter.cs b/RetroBar/Converters/TaskGroupVisibilityConverter.cs
new file mode 100644
index 00000000..57d8684d
--- /dev/null
+++ b/RetroBar/Converters/TaskGroupVisibilityConverter.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Collections;
+using System.Globalization;
+using System.Windows;
+using System.Windows.Data;
+
+namespace RetroBar.Converters
+{
+ public class TaskGroupVisibilityConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ int count = 0;
+ if (value is int i)
+ {
+ count = i;
+ }
+ else if (value is ICollection coll)
+ {
+ count = coll.Count;
+ }
+
+ return count > 1 ? Visibility.Visible : Visibility.Collapsed;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
\ No newline at end of file
diff --git a/RetroBar/Converters/TaskLabelConverter.cs b/RetroBar/Converters/TaskLabelConverter.cs
index ba176412..50331458 100644
--- a/RetroBar/Converters/TaskLabelConverter.cs
+++ b/RetroBar/Converters/TaskLabelConverter.cs
@@ -10,27 +10,63 @@ public class TaskLabelConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
- if (!(values[0] is string title &&
- values[1] is int progressValue &&
- values[2] is NativeMethods.TBPFLAG progressState))
+ if (values == null || values.Length == 0 || !(values[0] is string title))
{
return Binding.DoNothing;
}
+ string winFileDescription = values.Length > 1 ? values[1] as string : null;
+ string winFileName = values.Length > 2 ? values[2] as string : null;
+ int progressValue = values.Length > 3 && values[3] is int pv ? pv : 0;
+ NativeMethods.TBPFLAG progressState = values.Length > 4 && values[4] is NativeMethods.TBPFLAG ps ? ps : NativeMethods.TBPFLAG.TBPF_NOPROGRESS;
+
+ int taskCount = 0;
+ if (values.Length > 5)
+ {
+ if (values[5] is int i)
+ {
+ taskCount = i;
+ }
+ else if (values[5] is System.Collections.ICollection coll)
+ {
+ taskCount = coll.Count;
+ }
+ }
+
+ string displayTitle = title;
+ if (taskCount > 1)
+ {
+ if (!string.IsNullOrWhiteSpace(winFileDescription))
+ {
+ displayTitle = winFileDescription;
+ }
+ else if (!string.IsNullOrWhiteSpace(winFileName))
+ {
+ try
+ {
+ displayTitle = System.IO.Path.GetFileNameWithoutExtension(winFileName);
+ }
+ catch
+ {
+ displayTitle = title;
+ }
+ }
+ }
+
if (progressState == NativeMethods.TBPFLAG.TBPF_NOPROGRESS ||
progressState == NativeMethods.TBPFLAG.TBPF_INDETERMINATE ||
progressValue < 0)
{
- return title;
+ return displayTitle;
}
- if (title.Contains("%"))
+ if (displayTitle.Contains("%"))
{
// Window title may already contain progress percentage
- return title;
+ return displayTitle;
}
- return $"[{Math.Floor(progressValue / 65534.0 * 100)}%] {title}";
+ return $"[{Math.Floor(progressValue / 65534.0 * 100)}%] {displayTitle}";
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
diff --git a/RetroBar/Converters/TaskThumbnailItemsConverter.cs b/RetroBar/Converters/TaskThumbnailItemsConverter.cs
new file mode 100644
index 00000000..46989a00
--- /dev/null
+++ b/RetroBar/Converters/TaskThumbnailItemsConverter.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Windows.Data;
+using ManagedShell.WindowsTasks;
+
+namespace RetroBar.Converters
+{
+ public class TaskThumbnailItemsConverter : IMultiValueConverter
+ {
+ public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (values != null && values.Length > 1 && values[1] is IEnumerable tasks)
+ {
+ var list = tasks.OfType().ToList();
+ if (list.Count > 0)
+ {
+ return list;
+ }
+ }
+
+ if (values != null && values.Length > 0 && values[0] is ApplicationWindow win)
+ {
+ return new List { win };
+ }
+
+ return null;
+ }
+
+ public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
\ No newline at end of file
diff --git a/RetroBar/Converters/TaskToolTipConverter.cs b/RetroBar/Converters/TaskToolTipConverter.cs
new file mode 100644
index 00000000..8fc08ea7
--- /dev/null
+++ b/RetroBar/Converters/TaskToolTipConverter.cs
@@ -0,0 +1,56 @@
+using System;
+using System.Collections;
+using System.Globalization;
+using System.Windows.Data;
+
+namespace RetroBar.Converters
+{
+ public class TaskToolTipConverter : IMultiValueConverter
+ {
+ public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (values == null || values.Length == 0 || !(values[0] is string title))
+ {
+ return string.Empty;
+ }
+
+ string winFileDescription = values.Length > 1 ? values[1] as string : null;
+ string winFileName = values.Length > 2 ? values[2] as string : null;
+
+ int taskCount = 0;
+ if (values.Length > 3 && values[3] is ICollection coll)
+ {
+ taskCount = coll.Count;
+ }
+
+ if (taskCount > 1)
+ {
+ string programName = title;
+ if (!string.IsNullOrWhiteSpace(winFileDescription))
+ {
+ programName = winFileDescription;
+ }
+ else if (!string.IsNullOrWhiteSpace(winFileName))
+ {
+ try
+ {
+ programName = System.IO.Path.GetFileNameWithoutExtension(winFileName);
+ }
+ catch
+ {
+ programName = title;
+ }
+ }
+
+ return $"({taskCount}) {programName}";
+ }
+
+ return title;
+ }
+
+ public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
\ No newline at end of file
diff --git a/RetroBar/Converters/ToolTipPlacementConverter.cs b/RetroBar/Converters/ToolTipPlacementConverter.cs
index c50618a3..24856a6f 100644
--- a/RetroBar/Converters/ToolTipPlacementConverter.cs
+++ b/RetroBar/Converters/ToolTipPlacementConverter.cs
@@ -19,13 +19,9 @@ public object Convert(object[] value, Type targetType, object parameter, System.
{
return PlacementMode.Left;
}
- else if (Settings.Instance.Edge == AppBarEdge.Top)
- {
- return PlacementMode.Bottom;
- }
else
{
- return PlacementMode.Top;
+ return PlacementMode.Custom;
}
}
diff --git a/RetroBar/Languages/English.xaml b/RetroBar/Languages/English.xaml
index a7a56ffe..b1c8eb12 100644
--- a/RetroBar/Languages/English.xaml
+++ b/RetroBar/Languages/English.xaml
@@ -79,6 +79,7 @@
Check for u_pdates
Show E_xit option in right-click menu
Show End _task option in right-click menu
+ _Group similar taskbar buttons
Slid_e taskbar buttons
S_how seconds in the clock
Open custom themes folder
@@ -145,6 +146,9 @@
_Close
_End task
+ _Minimize Group
+ _Close Group
+
Show hidden icons
Hide
diff --git "a/RetroBar/Languages/espa\303\261ol.xaml" "b/RetroBar/Languages/espa\303\261ol.xaml"
index 2103891c..a5930d91 100644
--- "a/RetroBar/Languages/espa\303\261ol.xaml"
+++ "b/RetroBar/Languages/espa\303\261ol.xaml"
@@ -79,6 +79,7 @@
Comprobar _actualizaciones
Mostrar elemento "_Salir de RetroBar" en el menú contextual
Mostrar elemento "_Finalizar tarea" en el menú contextual
+ _Agrupar los botones similares de la barra de tareas
Deslizar _botones de la barra de tareas
Mostrar _segundos en el reloj
Abrir carpeta de temas personalizados
@@ -145,6 +146,9 @@
_Cerrar
_Finalizar tarea
+ _Minimizar grupo
+ C_errar grupo
+
Mostrar iconos ocultos
Ocultar
diff --git a/RetroBar/PropertiesWindow.xaml b/RetroBar/PropertiesWindow.xaml
index a4a54095..dd47d230 100644
--- a/RetroBar/PropertiesWindow.xaml
+++ b/RetroBar/PropertiesWindow.xaml
@@ -267,6 +267,10 @@
IsChecked="{Binding Source={x:Static Settings:Settings.Instance}, Path=ShowTaskThumbnails, UpdateSourceTrigger=PropertyChanged}">
+
+
+
diff --git a/RetroBar/Themes/Windows XP Blue.xaml b/RetroBar/Themes/Windows XP Blue.xaml
index df5b30da..62ecb27b 100644
--- a/RetroBar/Themes/Windows XP Blue.xaml
+++ b/RetroBar/Themes/Windows XP Blue.xaml
@@ -7,6 +7,7 @@
4
Normal
+
True
+
True
+
Set(ref _showTaskThumbnails, value);
}
+ private bool _groupTaskbarButtons = true;
+ public bool GroupTaskbarButtons
+ {
+ get => _groupTaskbarButtons;
+ set => Set(ref _groupTaskbarButtons, value);
+ }
+
private MultiMonOption _multiMonMode = MultiMonOption.AllTaskbars;
public MultiMonOption MultiMonMode
{
diff --git a/RetroBar/Utilities/TaskCategoryProvider.cs b/RetroBar/Utilities/TaskCategoryProvider.cs
new file mode 100644
index 00000000..8613af10
--- /dev/null
+++ b/RetroBar/Utilities/TaskCategoryProvider.cs
@@ -0,0 +1,26 @@
+using ManagedShell.WindowsTasks;
+
+namespace RetroBar.Utilities
+{
+ public class TaskCategoryProvider : ITaskCategoryProvider
+ {
+ public string GetCategory(ApplicationWindow window)
+ {
+ if (!string.IsNullOrEmpty(window.AppUserModelID))
+ return window.AppUserModelID;
+
+ if (!string.IsNullOrEmpty(window.WinFileName))
+ return window.WinFileName;
+
+ return window.ProcId?.ToString() ?? "Unknown";
+ }
+
+ public void SetCategoryChangeDelegate(TaskCategoryChangeDelegate changeDelegate)
+ {
+ }
+
+ public void Dispose()
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/RetroBar/Utilities/TaskDropHandler.cs b/RetroBar/Utilities/TaskDropHandler.cs
new file mode 100644
index 00000000..24bccebf
--- /dev/null
+++ b/RetroBar/Utilities/TaskDropHandler.cs
@@ -0,0 +1,135 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Windows.Data;
+using GongSolutions.Wpf.DragDrop;
+using ManagedShell.WindowsTasks;
+using RetroBar.Controls;
+
+namespace RetroBar.Utilities
+{
+ public class TaskDropHandler : IDropTarget
+ {
+ private TaskList _taskList;
+
+ public TaskDropHandler(TaskList taskList)
+ {
+ _taskList = taskList;
+ }
+
+ public void DragOver(IDropInfo dropInfo)
+ {
+ if (Settings.Instance.GroupTaskbarButtons && (dropInfo.Data is CollectionViewGroup || dropInfo.Data is ApplicationWindow))
+ {
+ dropInfo.DropTargetAdorner = DropTargetAdorners.Insert;
+ dropInfo.Effects = System.Windows.DragDropEffects.Move;
+ return;
+ }
+
+ DragDrop.DefaultDropHandler.DragOver(dropInfo);
+ }
+
+ public void Drop(IDropInfo dropInfo)
+ {
+ if (Settings.Instance.GroupTaskbarButtons && (dropInfo.Data is CollectionViewGroup || dropInfo.Data is ApplicationWindow))
+ {
+ var sourceItem = dropInfo.Data;
+ var targetItem = dropInfo.TargetItem;
+
+ if (sourceItem == targetItem || targetItem == null)
+ return;
+
+ // Safely get the underlying collection as IList instead of the Tasks object wrapper.
+ var tasksCollection = _taskList.UnderlyingTasks;
+ if (tasksCollection == null)
+ return;
+
+ List sourceWindows = new List();
+ if (sourceItem is CollectionViewGroup sourceGroup)
+ {
+ foreach (var item in sourceGroup.Items)
+ {
+ if (item is ApplicationWindow window)
+ sourceWindows.Add(window);
+ }
+ }
+ else if (sourceItem is ApplicationWindow window)
+ {
+ sourceWindows.Add(window);
+ }
+
+ int targetIndex = tasksCollection.Count; // Default to end
+ if (targetItem is CollectionViewGroup targetGroup)
+ {
+ int insertIndex = dropInfo.InsertIndex;
+ var groups = dropInfo.TargetCollection?.Cast