mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-07-01 07:59:36 +02:00
Compare commits
21 Commits
stable
...
dev/migrie
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0946ca3ac | ||
|
|
df2c146b30 | ||
|
|
8026df4d16 | ||
|
|
eb4792f942 | ||
|
|
8352afbb65 | ||
|
|
242ec2020c | ||
|
|
2927ffa8b7 | ||
|
|
f492cda5d5 | ||
|
|
e2248a6e1e | ||
|
|
20306ed599 | ||
|
|
a2be31e5f0 | ||
|
|
ca627134b3 | ||
|
|
4b8cbde9e6 | ||
|
|
0728481923 | ||
|
|
158e2f8d8a | ||
|
|
380489122a | ||
|
|
869a1f0560 | ||
|
|
3185267804 | ||
|
|
d2ceeccc6e | ||
|
|
ee014d06b8 | ||
|
|
2a2a6cc9f5 |
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
@@ -481,8 +481,9 @@ public sealed class CommandProviderWrapper : ICommandProviderContext
|
||||
this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs(-1));
|
||||
}
|
||||
|
||||
public void PinDockBand(string commandId, IServiceProvider serviceProvider, Dock.DockPinSide side = Dock.DockPinSide.Start, bool? showTitles = null, bool? showSubtitles = null, string? monitorDeviceId = null)
|
||||
public void PinDockBand(string commandId, IServiceProvider serviceProvider, bool withReload, Dock.DockPinSide side = Dock.DockPinSide.Start, bool? showTitles = null, bool? showSubtitles = null, string? monitorDeviceId = null)
|
||||
{
|
||||
Logger.LogDebug($"CommandProviderWrapper.PinDockBand(commandId): provider='{ProviderId}', commandId='{commandId}', withReload={withReload}, side={side}, monitor='{monitorDeviceId ?? "<global>"}'");
|
||||
var settingsService = serviceProvider.GetRequiredService<ISettingsService>();
|
||||
var settings = settingsService.Settings;
|
||||
var dockSettings = settings.DockSettings;
|
||||
@@ -535,7 +536,10 @@ public sealed class CommandProviderWrapper : ICommandProviderContext
|
||||
}
|
||||
|
||||
// Raise CommandsChanged so the TopLevelCommandManager reloads our commands
|
||||
this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs(-1));
|
||||
if (withReload)
|
||||
{
|
||||
this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs(-1));
|
||||
}
|
||||
}
|
||||
|
||||
private static void PinDockBandGlobal(ISettingsService settingsService, DockBandSettings bandSettings, Dock.DockPinSide side)
|
||||
@@ -610,7 +614,7 @@ public sealed class CommandProviderWrapper : ICommandProviderContext
|
||||
hotReload: false);
|
||||
}
|
||||
|
||||
public void UnpinDockBand(string commandId, IServiceProvider serviceProvider)
|
||||
public void UnpinDockBand(string commandId, IServiceProvider serviceProvider, bool withReload)
|
||||
{
|
||||
var settingsService = serviceProvider.GetRequiredService<ISettingsService>();
|
||||
settingsService.UpdateSettings(
|
||||
@@ -630,7 +634,10 @@ public sealed class CommandProviderWrapper : ICommandProviderContext
|
||||
hotReload: false);
|
||||
|
||||
// Raise CommandsChanged so the TopLevelCommandManager reloads our commands
|
||||
this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs(-1));
|
||||
if (withReload)
|
||||
{
|
||||
this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs(-1));
|
||||
}
|
||||
}
|
||||
|
||||
public ICommandProviderContext GetProviderContext() => this;
|
||||
@@ -639,8 +646,8 @@ public sealed class CommandProviderWrapper : ICommandProviderContext
|
||||
|
||||
public override int GetHashCode() => _commandProvider.GetHashCode();
|
||||
|
||||
private void CommandProvider_ItemsChanged(object sender, IItemsChangedEventArgs args) =>
|
||||
|
||||
private void CommandProvider_ItemsChanged(object sender, IItemsChangedEventArgs args)
|
||||
{
|
||||
// We don't want to handle this ourselves - we want the
|
||||
// TopLevelCommandManager to know about this, so they can remove
|
||||
// our old commands from their own list.
|
||||
@@ -648,6 +655,7 @@ public sealed class CommandProviderWrapper : ICommandProviderContext
|
||||
// In handling this, a call will be made to `LoadTopLevelCommands` to
|
||||
// retrieve the new items.
|
||||
this.CommandsChanged?.Invoke(this, args);
|
||||
}
|
||||
|
||||
internal void PinDockBand(TopLevelViewModel bandVm)
|
||||
{
|
||||
|
||||
@@ -228,6 +228,20 @@ public partial class ContextMenuViewModel : ObservableObject,
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised after a command is actually invoked (i.e. sent as a <see cref="PerformCommandMessage"/>)
|
||||
/// from this context menu. Not raised when the user navigates into a submenu.
|
||||
/// </summary>
|
||||
public event EventHandler<CommandItemViewModel>? CommandInvoked;
|
||||
|
||||
/// <summary>
|
||||
/// Raised immediately before the <see cref="PerformCommandMessage"/> is sent.
|
||||
/// Subscribers can decorate the message (for example, to attach an
|
||||
/// <see cref="PerformCommandMessage.OnBeforeShowConfirmation"/> callback).
|
||||
/// Not raised when the user navigates into a submenu.
|
||||
/// </summary>
|
||||
public event EventHandler<PerformCommandMessage>? CommandInvoking;
|
||||
|
||||
public ContextKeybindingResult InvokeCommand(CommandItemViewModel? command)
|
||||
{
|
||||
if (command is null)
|
||||
@@ -245,8 +259,11 @@ public partial class ContextMenuViewModel : ObservableObject,
|
||||
}
|
||||
else
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(command.Command.Model, command.Model));
|
||||
var message = new PerformCommandMessage(command.Command.Model, command.Model);
|
||||
CommandInvoking?.Invoke(this, message);
|
||||
WeakReferenceMessenger.Default.Send(message);
|
||||
UpdateContextItems();
|
||||
CommandInvoked?.Invoke(this, command);
|
||||
return ContextKeybindingResult.Hide;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,29 +66,23 @@ public sealed partial class DockViewModel : IDisposable
|
||||
{
|
||||
if (_isEditing)
|
||||
{
|
||||
Logger.LogDebug("Skipping DockBands_CollectionChanged during edit mode");
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.LogDebug("Starting DockBands_CollectionChanged");
|
||||
|
||||
// Refresh settings so newly pinned/unpinned bands are visible.
|
||||
// Pin/unpin operations save with hotReload:false (to avoid
|
||||
// double-updates), so _settings can be stale here.
|
||||
_settings = _settingsService.Settings.DockSettings;
|
||||
SetupBands();
|
||||
Logger.LogDebug("Ended DockBands_CollectionChanged");
|
||||
}
|
||||
|
||||
public void UpdateSettings(DockSettings settings)
|
||||
{
|
||||
if (_isEditing)
|
||||
{
|
||||
Logger.LogDebug("DockViewModel.UpdateSettings skipped (edit in progress)");
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.LogDebug($"DockViewModel.UpdateSettings");
|
||||
_settings = settings;
|
||||
SetupBands();
|
||||
}
|
||||
@@ -239,7 +233,6 @@ public sealed partial class DockViewModel : IDisposable
|
||||
|
||||
private void SetupBands()
|
||||
{
|
||||
Logger.LogDebug($"Setting up dock bands");
|
||||
var (start, center, end) = GetActiveBands();
|
||||
SetupBands(start, StartItems);
|
||||
SetupBands(center, CenterItems);
|
||||
@@ -258,7 +251,7 @@ public sealed partial class DockViewModel : IDisposable
|
||||
|
||||
if (topLevelCommand is null)
|
||||
{
|
||||
Logger.LogWarning($"Failed to find band {commandId}");
|
||||
Logger.LogWarning($"[DockDrop] DockViewModel.SetupBands: failed to find band command '{commandId}' (provider='{band.ProviderId}')");
|
||||
}
|
||||
|
||||
if (topLevelCommand is not null)
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// An <see cref="ObservableCollection{T}"/> of <see cref="TopLevelViewModel"/>
|
||||
/// that supports replacing the entire contents atomically with a single
|
||||
/// <see cref="NotifyCollectionChangedAction.Reset"/> notification.
|
||||
///
|
||||
/// <para>
|
||||
/// Using <see cref="ObservableCollection{T}"/>'s built-in Add/Remove/Insert
|
||||
/// mutations (or helpers like <c>ListHelpers.InPlaceUpdateList</c>) fires one
|
||||
/// <see cref="INotifyCollectionChanged.CollectionChanged"/> event per item
|
||||
/// mutation. The dock subscribes to that event and does a full rebuild for
|
||||
/// each, so a single provider reload (which can churn dozens of band entries)
|
||||
/// turns into dozens of full dock rebuilds.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// <see cref="ReplaceWith"/> bypasses the per-item notifications by mutating
|
||||
/// the protected <see cref="Collection{T}.Items"/> list directly and then
|
||||
/// raising one <c>Reset</c> at the end.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed partial class DockBandsCollection : ObservableCollection<TopLevelViewModel>
|
||||
{
|
||||
/// <summary>
|
||||
/// Replaces the contents of this collection with <paramref name="newItems"/>
|
||||
/// and raises exactly one <see cref="NotifyCollectionChangedAction.Reset"/>
|
||||
/// event (plus the standard <c>Count</c> / <c>Item[]</c> property change
|
||||
/// notifications). If the new contents are reference-equal to the current
|
||||
/// contents, no notification is raised.
|
||||
/// </summary>
|
||||
public void ReplaceWith(IEnumerable<TopLevelViewModel> newItems)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(newItems);
|
||||
|
||||
// Materialize once so we can compare and iterate without re-enumerating.
|
||||
var snapshot = newItems as IList<TopLevelViewModel> ?? [.. newItems];
|
||||
|
||||
// Cheap short-circuit: same length and same instances in the same
|
||||
// order means there is nothing to broadcast.
|
||||
if (SequenceReferenceEquals(Items, snapshot))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Items.Clear();
|
||||
for (var i = 0; i < snapshot.Count; i++)
|
||||
{
|
||||
Items.Add(snapshot[i]);
|
||||
}
|
||||
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(Count)));
|
||||
OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
|
||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
|
||||
}
|
||||
|
||||
private static bool SequenceReferenceEquals(IList<TopLevelViewModel> a, IList<TopLevelViewModel> b)
|
||||
{
|
||||
if (a.Count != b.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0; i < a.Count; i++)
|
||||
{
|
||||
if (!ReferenceEquals(a[i], b[i]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,15 @@ public record PerformCommandMessage
|
||||
|
||||
public bool TransientPage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional callback raised by <see cref="ShellViewModel"/> just before a
|
||||
/// <see cref="ShowConfirmationMessage"/> is dispatched for this command's
|
||||
/// result. Lets the sender prepare UI (for example, the dock uses this to
|
||||
/// open the cmdpal window anchored at the invoking dock item so that the
|
||||
/// confirmation dialog appears in the right place).
|
||||
/// </summary>
|
||||
public Action? OnBeforeShowConfirmation { get; set; }
|
||||
|
||||
public PerformCommandMessage(ExtensionObject<ICommand> command)
|
||||
{
|
||||
Command = command;
|
||||
|
||||
@@ -10,6 +10,7 @@ public record PinToDockMessage(
|
||||
string ProviderId,
|
||||
string CommandId,
|
||||
bool Pin,
|
||||
bool WithReload = true,
|
||||
DockPinSide Side = DockPinSide.Start,
|
||||
bool? ShowTitles = null,
|
||||
bool? ShowSubtitles = null,
|
||||
|
||||
@@ -386,7 +386,7 @@ public partial class ShellViewModel : ObservableObject,
|
||||
var result = invokable.Invoke(message.Context);
|
||||
|
||||
// But if it did succeed, we need to handle the result.
|
||||
UnsafeHandleCommandResult(result);
|
||||
UnsafeHandleCommandResult(result, message.OnBeforeShowConfirmation);
|
||||
|
||||
success = true;
|
||||
_handleInvokeTask = null;
|
||||
@@ -412,7 +412,7 @@ public partial class ShellViewModel : ObservableObject,
|
||||
}
|
||||
}
|
||||
|
||||
private void UnsafeHandleCommandResult(ICommandResult? result)
|
||||
private void UnsafeHandleCommandResult(ICommandResult? result, Action? onBeforeShowConfirmation = null)
|
||||
{
|
||||
if (result is null)
|
||||
{
|
||||
@@ -464,6 +464,17 @@ public partial class ShellViewModel : ObservableObject,
|
||||
{
|
||||
if (result.Args is IConfirmationArgs a)
|
||||
{
|
||||
// Give the original sender (e.g. the dock) a chance to
|
||||
// prepare UI before the confirmation dialog surfaces.
|
||||
try
|
||||
{
|
||||
onBeforeShowConfirmation?.Invoke();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CoreLogger.LogError(ex.ToString());
|
||||
}
|
||||
|
||||
WeakReferenceMessenger.Default.Send<ShowConfirmationMessage>(new(a));
|
||||
}
|
||||
|
||||
@@ -475,7 +486,7 @@ public partial class ShellViewModel : ObservableObject,
|
||||
if (result.Args is IToastArgs a)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<ShowToastMessage>(new(a.Message));
|
||||
UnsafeHandleCommandResult(a.Result);
|
||||
UnsafeHandleCommandResult(a.Result, onBeforeShowConfirmation);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
@@ -68,7 +68,12 @@ public sealed partial class TopLevelCommandManager : ObservableObject,
|
||||
|
||||
public ObservableCollection<TopLevelViewModel> TopLevelCommands { get; set; } = [];
|
||||
|
||||
public ObservableCollection<TopLevelViewModel> DockBands { get; set; } = [];
|
||||
// DockBands uses a custom collection so that bulk rewrites (see
|
||||
// UpdateCommandsForProvider) raise a single Reset notification instead of
|
||||
// one event per inserted/removed/moved item. The dock subscribes to this
|
||||
// collection and does a full rebuild per event, so collapsing the burst
|
||||
// here avoids dozens of redundant rebuilds for one provider reload.
|
||||
public DockBandsCollection DockBands { get; } = new();
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsLoading { get; private set; } = true;
|
||||
@@ -194,8 +199,10 @@ public sealed partial class TopLevelCommandManager : ObservableObject,
|
||||
// By all accounts, we're already on a background thread (the COM call
|
||||
// to handle the event shouldn't be on the main thread.). But just to
|
||||
// be sure we don't block the caller, hop off this thread
|
||||
private void CommandProvider_CommandsChanged(CommandProviderWrapper sender, IItemsChangedEventArgs args) =>
|
||||
private void CommandProvider_CommandsChanged(CommandProviderWrapper sender, IItemsChangedEventArgs args)
|
||||
{
|
||||
_ = Task.Run(async () => await UpdateCommandsForProvider(sender, args));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when a command provider raises its ItemsChanged event. We'll
|
||||
@@ -240,12 +247,14 @@ public sealed partial class TopLevelCommandManager : ObservableObject,
|
||||
|
||||
lock (_dockBandsLock)
|
||||
{
|
||||
// same idea for DockBands
|
||||
// Same idea as TopLevelCommands above, but we deliberately use
|
||||
// ReplaceWith so the dock only sees one CollectionChanged event
|
||||
// for the whole rewrite instead of one per item.
|
||||
List<TopLevelViewModel> dockClone = [.. DockBands];
|
||||
var dockStartIndex = FindIndexForFirstProviderItem(dockClone, sender.ProviderId);
|
||||
dockClone.RemoveAll(item => item.CommandProviderId == sender.ProviderId);
|
||||
dockClone.InsertRange(dockStartIndex, newBands);
|
||||
ListHelpers.InPlaceUpdateList(DockBands, dockClone);
|
||||
DockBands.ReplaceWith(dockClone);
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -726,13 +735,17 @@ public sealed partial class TopLevelCommandManager : ObservableObject,
|
||||
{
|
||||
if (message.Pin)
|
||||
{
|
||||
wrapper?.PinDockBand(message.CommandId, _serviceProvider, message.Side, message.ShowTitles, message.ShowSubtitles, message.MonitorDeviceId);
|
||||
wrapper?.PinDockBand(message.CommandId, _serviceProvider, message.WithReload, message.Side, message.ShowTitles, message.ShowSubtitles, message.MonitorDeviceId);
|
||||
}
|
||||
else
|
||||
{
|
||||
wrapper?.UnpinDockBand(message.CommandId, _serviceProvider);
|
||||
wrapper?.UnpinDockBand(message.CommandId, _serviceProvider, message.WithReload);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning($"[DockDrop] PinToDockMessage: no provider found for '{message.ProviderId}'");
|
||||
}
|
||||
}
|
||||
|
||||
public CommandProviderWrapper? LookupProvider(string providerId)
|
||||
|
||||
@@ -162,7 +162,10 @@ public partial class App : Application, IDisposable
|
||||
services.AddSingleton<ICommandProvider, ShellCommandsProvider>();
|
||||
services.AddSingleton<ICommandProvider, CalculatorCommandProvider>();
|
||||
services.AddSingleton<ICommandProvider>(files);
|
||||
services.AddSingleton<ICommandProvider, BookmarksCommandProvider>(_ => BookmarksCommandProvider.CreateWithDefaultStore());
|
||||
|
||||
var bookmarks = BookmarksCommandProvider.CreateWithDefaultStore();
|
||||
services.AddSingleton<ICommandProvider>(bookmarks);
|
||||
services.AddSingleton<IBookmarksManager>(bookmarks.BookmarksManager);
|
||||
|
||||
services.AddSingleton<ICommandProvider, WindowWalkerCommandsProvider>();
|
||||
services.AddSingleton<ICommandProvider, WebSearchCommandsProvider>();
|
||||
|
||||
@@ -206,9 +206,12 @@
|
||||
|
||||
<Grid
|
||||
x:Name="RootGrid"
|
||||
AllowDrop="True"
|
||||
Background="Transparent"
|
||||
BorderBrush="{ThemeResource SurfaceStrokeColorDefaultBrush}"
|
||||
BorderThickness="0,0,0,1"
|
||||
DragOver="RootGrid_DragOver"
|
||||
Drop="RootGrid_Drop"
|
||||
RightTapped="RootGrid_RightTapped">
|
||||
<!-- Dock content with Start / Center / End sections -->
|
||||
<local:DockContentControl
|
||||
|
||||
@@ -7,12 +7,14 @@ using System.Collections.Specialized;
|
||||
using System.Runtime.InteropServices;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Ext.Bookmarks;
|
||||
using Microsoft.CmdPal.UI.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Dock;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
@@ -21,6 +23,8 @@ using Microsoft.UI.Xaml.Media;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
using Windows.Foundation;
|
||||
|
||||
using RS_ = Microsoft.CmdPal.UI.Helpers.ResourceLoaderInstance;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Dock;
|
||||
|
||||
public sealed partial class DockControl : UserControl, IRecipient<CloseContextMenuMessage>, IRecipient<EnterDockEditModeMessage>, IRecipient<ExitDockEditModeMessage>, IRecipient<CrossMonitorBandDropMessage>
|
||||
@@ -30,7 +34,7 @@ public sealed partial class DockControl : UserControl, IRecipient<CloseContextMe
|
||||
internal DockViewModel ViewModel => _viewModel;
|
||||
|
||||
/// <summary>
|
||||
/// The HWND of the parent DockWindow that owns this control.
|
||||
/// Gets or sets the HWND of the parent DockWindow that owns this control.
|
||||
/// Used to target palette-show messages to the correct DockWindow in multi-monitor setups.
|
||||
/// </summary>
|
||||
internal IntPtr OwnerHwnd { get; set; }
|
||||
@@ -98,6 +102,11 @@ public sealed partial class DockControl : UserControl, IRecipient<CloseContextMe
|
||||
WeakReferenceMessenger.Default.Register<ExitDockEditModeMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<CrossMonitorBandDropMessage>(this);
|
||||
|
||||
ContextControl.ViewModel.CommandInvoked -= ContextMenu_CommandInvoked;
|
||||
ContextControl.ViewModel.CommandInvoked += ContextMenu_CommandInvoked;
|
||||
ContextControl.ViewModel.CommandInvoking -= ContextMenu_CommandInvoking;
|
||||
ContextControl.ViewModel.CommandInvoking += ContextMenu_CommandInvoking;
|
||||
|
||||
ViewModel.CenterItems.CollectionChanged -= CenterItems_CollectionChanged;
|
||||
ViewModel.CenterItems.CollectionChanged += CenterItems_CollectionChanged;
|
||||
|
||||
@@ -108,6 +117,9 @@ public sealed partial class DockControl : UserControl, IRecipient<CloseContextMe
|
||||
{
|
||||
WeakReferenceMessenger.Default.UnregisterAll(this);
|
||||
|
||||
ContextControl.ViewModel.CommandInvoked -= ContextMenu_CommandInvoked;
|
||||
ContextControl.ViewModel.CommandInvoking -= ContextMenu_CommandInvoking;
|
||||
|
||||
ViewModel.CenterItems.CollectionChanged -= CenterItems_CollectionChanged;
|
||||
|
||||
if (EditButtonsTeachingTip.IsOpen)
|
||||
@@ -291,10 +303,7 @@ public sealed partial class DockControl : UserControl, IRecipient<CloseContextMe
|
||||
if (sender is DockItemControl dockItem && dockItem.DataContext is DockBandViewModel band && dockItem.Tag is DockItemViewModel item)
|
||||
{
|
||||
// Use the center of the border as the point to open at
|
||||
var borderPos = dockItem.TransformToVisual(null).TransformPoint(new Point(0, 0));
|
||||
var borderCenter = new Point(
|
||||
borderPos.X + (dockItem.ActualWidth / 2),
|
||||
borderPos.Y + (dockItem.ActualHeight / 2));
|
||||
var borderCenter = GetDockItemCenter(dockItem);
|
||||
|
||||
InvokeItem(item, borderCenter);
|
||||
e.Handled = true;
|
||||
@@ -311,6 +320,11 @@ public sealed partial class DockControl : UserControl, IRecipient<CloseContextMe
|
||||
// Stores the band that was right-clicked for edit mode context menu
|
||||
private DockBandViewModel? _editModeContextBand;
|
||||
|
||||
// Position (in window coords) of the dock item whose context menu is currently
|
||||
// open, used to anchor the cmdpal palette when a Page command is invoked from
|
||||
// the context menu. Null when the open context menu is not anchored to a band.
|
||||
private Point? _bandContextMenuPalettePos;
|
||||
|
||||
private void BandItem_RightTapped(object sender, Microsoft.UI.Xaml.Input.RightTappedRoutedEventArgs e)
|
||||
{
|
||||
if (sender is DockItemControl dockItem && dockItem.DataContext is DockBandViewModel band && dockItem.Tag is DockItemViewModel item)
|
||||
@@ -348,6 +362,10 @@ public sealed partial class DockControl : UserControl, IRecipient<CloseContextMe
|
||||
// Normal mode - show the command context menu
|
||||
if (item.CanOpenContextMenu)
|
||||
{
|
||||
// Remember where to anchor the palette if the user picks a Page
|
||||
// command from the context menu.
|
||||
_bandContextMenuPalettePos = GetDockItemCenter(dockItem);
|
||||
|
||||
ContextControl.ViewModel.SelectedItem = item;
|
||||
ContextControl.ShowFilterBox = true;
|
||||
ContextControl.PrepareForOpen(GetDockContextMenuFilterLocation());
|
||||
@@ -392,17 +410,25 @@ public sealed partial class DockControl : UserControl, IRecipient<CloseContextMe
|
||||
private void InvokeItem(DockItemViewModel item, Point pos)
|
||||
{
|
||||
var command = item.Command;
|
||||
var hwnd = OwnerHwnd;
|
||||
try
|
||||
{
|
||||
PerformCommandMessage m = new(command.Model);
|
||||
m.WithAnimation = false;
|
||||
m.TransientPage = true;
|
||||
PerformCommandMessage m = new(command.Model)
|
||||
{
|
||||
WithAnimation = false,
|
||||
TransientPage = true,
|
||||
|
||||
// If the command is invokable and its result asks for a
|
||||
// confirmation dialog, surface the cmdpal window anchored at
|
||||
// this dock item before the dialog appears.
|
||||
OnBeforeShowConfirmation = () =>
|
||||
WeakReferenceMessenger.Default.Send<RequestShowPaletteAtMessage>(new(pos, hwnd)),
|
||||
};
|
||||
WeakReferenceMessenger.Default.Send(m);
|
||||
|
||||
var isPage = command.Model.Unsafe is not IInvokableCommand invokable;
|
||||
if (isPage)
|
||||
if (IsPageCommand(command.Model.Unsafe))
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<RequestShowPaletteAtMessage>(new(pos, OwnerHwnd));
|
||||
WeakReferenceMessenger.Default.Send<RequestShowPaletteAtMessage>(new(pos, hwnd));
|
||||
}
|
||||
}
|
||||
catch (COMException e)
|
||||
@@ -411,6 +437,59 @@ public sealed partial class DockControl : UserControl, IRecipient<CloseContextMe
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsPageCommand(ICommand? command)
|
||||
{
|
||||
// A Page command is one that's not directly invokable - selecting it
|
||||
// navigates into a page rather than performing an action in place.
|
||||
return command is not null and not IInvokableCommand;
|
||||
}
|
||||
|
||||
private static Point GetDockItemCenter(FrameworkElement dockItem)
|
||||
{
|
||||
var borderPos = dockItem.TransformToVisual(null).TransformPoint(new Point(0, 0));
|
||||
return new Point(
|
||||
borderPos.X + (dockItem.ActualWidth / 2),
|
||||
borderPos.Y + (dockItem.ActualHeight / 2));
|
||||
}
|
||||
|
||||
private void ContextMenu_CommandInvoked(object? sender, CommandItemViewModel command)
|
||||
{
|
||||
// The context menu just invoked a command. If it came from a dock band
|
||||
// (i.e. _bandContextMenuPalettePos is set) and the command is a Page,
|
||||
// open the cmdpal palette anchored at the dock item — mirroring what
|
||||
// a direct click on the band does.
|
||||
var pos = _bandContextMenuPalettePos;
|
||||
_bandContextMenuPalettePos = null;
|
||||
|
||||
if (pos is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsPageCommand(command.Command.Model.Unsafe))
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<RequestShowPaletteAtMessage>(new(pos.Value, OwnerHwnd));
|
||||
}
|
||||
}
|
||||
|
||||
private void ContextMenu_CommandInvoking(object? sender, PerformCommandMessage message)
|
||||
{
|
||||
// The context menu is about to dispatch a command. If it was opened
|
||||
// from a dock band, attach a callback so that an invokable command
|
||||
// whose result is a Confirm surfaces the cmdpal window anchored at the
|
||||
// dock item before the confirmation dialog appears.
|
||||
var pos = _bandContextMenuPalettePos;
|
||||
if (pos is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var hwnd = OwnerHwnd;
|
||||
var capturedPos = pos.Value;
|
||||
message.OnBeforeShowConfirmation = () =>
|
||||
WeakReferenceMessenger.Default.Send<RequestShowPaletteAtMessage>(new(capturedPos, hwnd));
|
||||
}
|
||||
|
||||
private void ContextMenuFlyout_Opened(object sender, object e)
|
||||
{
|
||||
// We need to wait until our flyout is opened to try and toss focus
|
||||
@@ -434,6 +513,10 @@ public sealed partial class DockControl : UserControl, IRecipient<CloseContextMe
|
||||
return;
|
||||
}
|
||||
|
||||
// This context menu is for the dock itself (not a band), so the palette
|
||||
// should not be opened on invocation.
|
||||
_bandContextMenuPalettePos = null;
|
||||
|
||||
var pos = e.GetPosition(null);
|
||||
var item = this.ViewModel.GetContextMenuForDock();
|
||||
if (item.HasMoreCommands)
|
||||
@@ -725,6 +808,102 @@ public sealed partial class DockControl : UserControl, IRecipient<CloseContextMe
|
||||
}
|
||||
}
|
||||
|
||||
private void RootGrid_DragOver(object sender, DragEventArgs e)
|
||||
{
|
||||
// Don't intercept internal band drag-drop during edit mode
|
||||
if (_draggedBand != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.DataView.Contains(StandardDataFormats.StorageItems) ||
|
||||
e.DataView.Contains(StandardDataFormats.Uri))
|
||||
{
|
||||
e.AcceptedOperation = DataPackageOperation.Link;
|
||||
e.DragUIOverride.Caption = RS_.GetString("Dock_DropFile_Caption");
|
||||
e.DragUIOverride.IsGlyphVisible = true;
|
||||
e.DragUIOverride.IsCaptionVisible = true;
|
||||
|
||||
// DON'T mark the event as handled - if you do, we won't get the Drop event.
|
||||
}
|
||||
}
|
||||
|
||||
private async void RootGrid_Drop(object sender, DragEventArgs e)
|
||||
{
|
||||
// Don't intercept internal band drag-drop during edit mode
|
||||
if (_draggedBand != null)
|
||||
{
|
||||
Logger.LogDebug("[DockDrop] RootGrid_Drop: ignoring (internal band drag in progress)");
|
||||
return;
|
||||
}
|
||||
|
||||
var hasStorageItems = e.DataView.Contains(StandardDataFormats.StorageItems);
|
||||
var hasUri = e.DataView.Contains(StandardDataFormats.Uri);
|
||||
|
||||
if (!hasStorageItems && !hasUri)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
|
||||
try
|
||||
{
|
||||
var bookmarksManager = App.Current.Services.GetService<IBookmarksManager>();
|
||||
if (bookmarksManager == null)
|
||||
{
|
||||
Logger.LogWarning("[DockDrop] IBookmarksManager service is not registered; cannot pin dropped item");
|
||||
return;
|
||||
}
|
||||
|
||||
var foundItem = false;
|
||||
if (hasStorageItems)
|
||||
{
|
||||
var items = await e.DataView.GetStorageItemsAsync();
|
||||
foreach (var item in items)
|
||||
{
|
||||
var path = item.Path;
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var name = Path.GetFileNameWithoutExtension(path);
|
||||
AddBookmarkAndPinToDock(bookmarksManager, name, path);
|
||||
foundItem = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (foundItem)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasUri)
|
||||
{
|
||||
var uri = await e.DataView.GetUriAsync();
|
||||
var url = uri.AbsoluteUri;
|
||||
var name = uri.Host;
|
||||
AddBookmarkAndPinToDock(bookmarksManager, name, url);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("[DockDrop] Error handling file drop on dock", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddBookmarkAndPinToDock(IBookmarksManager bookmarksManager, string name, string bookmarkValue)
|
||||
{
|
||||
var bookmark = bookmarksManager.Add(name, bookmarkValue);
|
||||
|
||||
// Make the command ID exactly the same as the ID it would have in the
|
||||
// top-level list, so that pinning to the dock from the top-level is seamless.
|
||||
var commandId = Ext.Bookmarks.Helpers.CommandIds.GetLaunchBookmarkItemId(bookmark.Id);
|
||||
Logger.LogDebug($"[DockDrop] Pinning dropped item '{name}' as bookmark id={bookmark.Id} (commandId='{commandId}')");
|
||||
WeakReferenceMessenger.Default.Send(new PinToDockMessage("Bookmarks", commandId, true, WithReload: false));
|
||||
}
|
||||
|
||||
public void Receive(CrossMonitorBandDropMessage message)
|
||||
{
|
||||
// Only match if this dock has a real monitor ID that matches the source.
|
||||
|
||||
@@ -1018,6 +1018,10 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<data name="Dock_EditMode_Unpin.Text" xml:space="preserve">
|
||||
<value>Unpin</value>
|
||||
</data>
|
||||
<data name="Dock_DropFile_Caption" xml:space="preserve">
|
||||
<value>Pin to Dock</value>
|
||||
<comment>Drag-over caption shown when dragging a file or shortcut onto the dock to bookmark and pin it</comment>
|
||||
</data>
|
||||
<data name="Dock_AddBand_NoCommandsAvailable.Text" xml:space="preserve">
|
||||
<value>All available bands are already pinned.</value>
|
||||
</data>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Diagnostics.Contracts;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Microsoft.CmdPal.Ext.Bookmarks.Helpers;
|
||||
using Microsoft.CmdPal.Ext.Bookmarks.Pages;
|
||||
using Microsoft.CmdPal.Ext.Bookmarks.Persistence;
|
||||
using Microsoft.CmdPal.Ext.Bookmarks.Services;
|
||||
@@ -24,6 +25,8 @@ public sealed partial class BookmarksCommandProvider : CommandProvider
|
||||
private readonly IBookmarkResolver _commandResolver;
|
||||
private readonly IBookmarkIconLocator _iconLocator = new IconLocator();
|
||||
|
||||
public IBookmarksManager BookmarksManager => _bookmarksManager;
|
||||
|
||||
private readonly ListItem _addNewItem;
|
||||
private readonly Lock _bookmarksLock = new();
|
||||
|
||||
@@ -127,4 +130,42 @@ public sealed partial class BookmarksCommandProvider : CommandProvider
|
||||
|
||||
[Pure]
|
||||
private ICommandItem[] BuildTopLevelCommandsUnsafe() => [_addNewItem, .. _bookmarks];
|
||||
|
||||
public override ICommandItem[]? GetDockBands()
|
||||
{
|
||||
BookmarkListItem[] bookmarks;
|
||||
|
||||
lock (_bookmarksLock)
|
||||
{
|
||||
// Here we're creating an entirely different set of items to return
|
||||
// as bands.
|
||||
//
|
||||
// These items will have the same ID, but bookmarks to directories
|
||||
// will have their default command be the "DirectoryPage", so that
|
||||
// clicking it will automatically open the palette with the files in
|
||||
// that dir.
|
||||
bookmarks = [.. _bookmarksManager.Bookmarks
|
||||
.Select(bookmark => new BookmarkListItem(
|
||||
bookmark,
|
||||
_bookmarksManager,
|
||||
_commandResolver,
|
||||
_iconLocator,
|
||||
_placeholderParser,
|
||||
asBand: true))];
|
||||
}
|
||||
|
||||
var bands = new List<ICommandItem>();
|
||||
|
||||
// Now take all those commands, and stick them into individual bands. We
|
||||
// don't want one band with all bookmarks, we want one band per
|
||||
// bookmark.
|
||||
foreach (var b in bookmarks)
|
||||
{
|
||||
var id = CommandIds.GetLaunchBookmarkItemId(b.BookmarkId);
|
||||
var wrapped = new WrappedDockItem(items: [b], id: id, displayTitle: b.Title);
|
||||
bands.Add(wrapped);
|
||||
}
|
||||
|
||||
return bands.Count > 0 ? bands.ToArray() : null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ internal sealed partial class BookmarksManager : IDisposable, IBookmarksManager
|
||||
public BookmarkData Add(string name, string bookmark)
|
||||
{
|
||||
var newBookmark = new BookmarkData(name, bookmark);
|
||||
Logger.LogDebug($"BookmarksManager.Add: created bookmark id={newBookmark.Id} name='{name}' value='{bookmark}'");
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Bookmarks.Helpers;
|
||||
|
||||
internal static class CommandIds
|
||||
public static class CommandIds
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns id of a command associated with a bookmark item. This id is for a command that launches the bookmark - regardless of whether
|
||||
|
||||
@@ -6,7 +6,7 @@ using Microsoft.CmdPal.Ext.Bookmarks.Persistence;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Bookmarks;
|
||||
|
||||
internal interface IBookmarksManager
|
||||
public interface IBookmarksManager
|
||||
{
|
||||
event Action<BookmarkData>? BookmarkAdded;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
@@ -26,6 +26,7 @@ internal sealed partial class BookmarkListItem : ListItem, IDisposable
|
||||
private readonly IPlaceholderParser _placeholderParser;
|
||||
private readonly SupersedingAsyncValueGate<BookmarkListItemReclassifyResult> _classificationGate;
|
||||
private readonly TaskCompletionSource _initializationTcs = new();
|
||||
private readonly bool _isBandItem;
|
||||
|
||||
private BookmarkData _bookmark;
|
||||
|
||||
@@ -37,12 +38,18 @@ internal sealed partial class BookmarkListItem : ListItem, IDisposable
|
||||
|
||||
public Guid BookmarkId => _bookmark.Id;
|
||||
|
||||
public BookmarkListItem(BookmarkData bookmark, IBookmarksManager bookmarksManager, IBookmarkResolver commandResolver, IBookmarkIconLocator iconLocator, IPlaceholderParser placeholderParser)
|
||||
public BookmarkListItem(
|
||||
BookmarkData bookmark,
|
||||
IBookmarksManager bookmarksManager,
|
||||
IBookmarkResolver commandResolver,
|
||||
IBookmarkIconLocator iconLocator,
|
||||
IPlaceholderParser placeholderParser,
|
||||
bool asBand = false)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(bookmark);
|
||||
ArgumentNullException.ThrowIfNull(bookmarksManager);
|
||||
ArgumentNullException.ThrowIfNull(commandResolver);
|
||||
|
||||
_isBandItem = asBand;
|
||||
_bookmark = bookmark;
|
||||
_bookmarksManager = bookmarksManager;
|
||||
_bookmarksManager.BookmarkUpdated += BookmarksManagerOnBookmarkUpdated;
|
||||
@@ -107,6 +114,23 @@ internal sealed partial class BookmarkListItem : ListItem, IDisposable
|
||||
BuildSpecificContextMenuItems(classification, contextMenu);
|
||||
AddCommonContextMenuItems(_bookmark, _bookmarksManager, bookmarkSavedHandler, contextMenu);
|
||||
|
||||
// If we're a band AND the classification kind was directory , then flip
|
||||
// the command and the first contextMenu item
|
||||
if (_isBandItem && classification.Kind == CommandKind.Directory && contextMenu.Count > 0)
|
||||
{
|
||||
var firstContextCommand = contextMenu[0] as CommandContextItem;
|
||||
if (firstContextCommand != null)
|
||||
{
|
||||
var browseCommand = firstContextCommand.Command;
|
||||
if (browseCommand != null)
|
||||
{
|
||||
contextMenu.RemoveAt(0);
|
||||
contextMenu.Insert(0, new CommandContextItem(command));
|
||||
command = browseCommand;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new BookmarkListItemReclassifyResult(
|
||||
command,
|
||||
title,
|
||||
@@ -155,7 +179,8 @@ internal sealed partial class BookmarkListItem : ListItem, IDisposable
|
||||
{
|
||||
case CommandKind.Directory:
|
||||
directoryPath = targetPath;
|
||||
contextMenu.Add(new CommandContextItem(new DirectoryPage(directoryPath))); // Browse
|
||||
var c = new DirectoryPage(directoryPath);
|
||||
contextMenu.Add(new CommandContextItem(c)); // Browse
|
||||
break;
|
||||
case CommandKind.FileExecutable:
|
||||
case CommandKind.FileDocument:
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CmdPal.Common.Helpers;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Data;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Properties;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
@@ -15,9 +17,10 @@ using Windows.Storage.Streams;
|
||||
#nullable enable
|
||||
namespace Microsoft.CmdPal.Ext.Indexer;
|
||||
|
||||
public sealed partial class DirectoryPage : ListPage
|
||||
public sealed partial class DirectoryPage : ListPage, IDisposable
|
||||
{
|
||||
private readonly string _path;
|
||||
private readonly SupersedingAsyncValueGate<IconInfo?> _iconReloadGate;
|
||||
|
||||
private List<IndexerListItem>? _directoryContents;
|
||||
|
||||
@@ -27,6 +30,28 @@ public sealed partial class DirectoryPage : ListPage
|
||||
Icon = Icons.FileExplorerIcon;
|
||||
Name = Resources.Indexer_Command_Browse;
|
||||
Title = path;
|
||||
|
||||
_iconReloadGate = new(
|
||||
async ct =>
|
||||
{
|
||||
var stream = await ThumbnailHelper.GetThumbnail(path);
|
||||
return stream is not null ? IconInfo.FromStream(stream) : null;
|
||||
},
|
||||
icon =>
|
||||
{
|
||||
if (icon is not null)
|
||||
{
|
||||
Icon = icon;
|
||||
}
|
||||
});
|
||||
|
||||
_ = _iconReloadGate.ExecuteAsync();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_iconReloadGate.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public override IListItem[] GetItems()
|
||||
|
||||
Reference in New Issue
Block a user