-
Notifications
You must be signed in to change notification settings - Fork 338
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow widgets to be rearranged by keyboard #3426
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,13 +20,18 @@ | |
using DevHome.Dashboard.TelemetryEvents; | ||
using DevHome.Dashboard.ViewModels; | ||
using DevHome.Telemetry; | ||
using Microsoft.UI.Dispatching; | ||
using Microsoft.UI.Input; | ||
using Microsoft.UI.Xaml; | ||
using Microsoft.UI.Xaml.Automation; | ||
using Microsoft.UI.Xaml.Controls; | ||
using Microsoft.UI.Xaml.Input; | ||
using Microsoft.Windows.Widgets; | ||
using Microsoft.Windows.Widgets.Hosts; | ||
using Serilog; | ||
using Windows.System; | ||
using Windows.UI.Core; | ||
|
||
using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue; | ||
krschau marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
namespace DevHome.Dashboard.Views; | ||
|
||
|
@@ -41,6 +46,7 @@ public partial class DashboardView : ToolPage, IDisposable | |
private readonly WidgetViewModelFactory _widgetViewModelFactory; | ||
|
||
private readonly SemaphoreSlim _pinnedWidgetsLock = new(1, 1); | ||
private readonly SemaphoreSlim _moveWidgetsLock = new(1, 1); | ||
|
||
private static DispatcherQueue _dispatcherQueue; | ||
private readonly ILocalSettingsService _localSettingsService; | ||
|
@@ -467,13 +473,13 @@ private async Task PinDefaultWidgetAsync(ComSafeWidgetDefinition defaultWidgetDe | |
[RelayCommand] | ||
public async Task GoToWidgetsInStoreAsync() | ||
{ | ||
if (Common.Helpers.RuntimeHelper.IsOnWindows11) | ||
if (RuntimeHelper.IsOnWindows11) | ||
{ | ||
await Windows.System.Launcher.LaunchUriAsync(new($"ms-windows-store://pdp/?productid={WidgetHelpers.WebExperiencePackPackageId}")); | ||
await Launcher.LaunchUriAsync(new($"ms-windows-store://pdp/?productid={WidgetHelpers.WebExperiencePackPackageId}")); | ||
} | ||
else | ||
{ | ||
await Windows.System.Launcher.LaunchUriAsync(new($"ms-windows-store://pdp/?productid={WidgetHelpers.WidgetServiceStorePackageId}")); | ||
await Launcher.LaunchUriAsync(new($"ms-windows-store://pdp/?productid={WidgetHelpers.WidgetServiceStorePackageId}")); | ||
} | ||
} | ||
|
||
|
@@ -817,15 +823,33 @@ private async void WidgetControl_Drop(object sender, DragEventArgs e) | |
|
||
var draggedWidgetViewModel = draggedObject as WidgetViewModel; | ||
|
||
await MoveWidgetAsync(draggedWidgetViewModel, draggedIndex, droppedIndex); | ||
|
||
_log.Debug($"Drop ended"); | ||
} | ||
|
||
private async Task MoveWidgetAsync(WidgetViewModel draggedWidgetViewModel, int draggedIndex, int droppedIndex) | ||
{ | ||
// Remove the moved widget then insert it back in the collection at the new location. If the dropped widget was | ||
// moved from a lower index to a higher one, removing the moved widget before inserting it will ensure that any | ||
// widgets between the starting and ending indices move up to replace the removed widget. If the widget was | ||
// moved from a higher index to a lower one, then the order of removal and insertion doesn't matter. | ||
ViewModel.PinnedWidgets.CollectionChanged -= OnPinnedWidgetsCollectionChangedAsync; | ||
|
||
ViewModel.PinnedWidgets.RemoveAt(draggedIndex); | ||
var size = await draggedWidgetViewModel.Widget.GetSizeAsync(); | ||
await InsertWidgetInPinnedWidgetsAsync(draggedWidgetViewModel.Widget, size, droppedIndex); | ||
|
||
// Doing these operations in different orders for different start/end indexes make the animation look a little better. | ||
if (draggedIndex < droppedIndex) | ||
{ | ||
ViewModel.PinnedWidgets.RemoveAt(draggedIndex); | ||
await InsertWidgetInPinnedWidgetsAsync(draggedWidgetViewModel.Widget, size, droppedIndex); | ||
} | ||
else | ||
{ | ||
await InsertWidgetInPinnedWidgetsAsync(draggedWidgetViewModel.Widget, size, droppedIndex); | ||
ViewModel.PinnedWidgets.RemoveAt(draggedIndex + 1); | ||
} | ||
|
||
await WidgetHelpers.SetPositionCustomStateAsync(draggedWidgetViewModel.Widget, droppedIndex); | ||
|
||
// Update the CustomState Position of any widgets that were moved. | ||
|
@@ -839,8 +863,70 @@ private async void WidgetControl_Drop(object sender, DragEventArgs e) | |
} | ||
|
||
ViewModel.PinnedWidgets.CollectionChanged += OnPinnedWidgetsCollectionChangedAsync; | ||
} | ||
|
||
_log.Debug($"Drop ended"); | ||
/// <summary> | ||
/// Handle keyboard shortcuts for moving widgets left and right. | ||
/// </summary> | ||
private async void HandleKeyUpAsync(object sender, KeyRoutedEventArgs e) | ||
{ | ||
_log.Debug($"Key up"); | ||
|
||
await _moveWidgetsLock.WaitAsync(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh I see, is this lock to prevent concurrent execution from manipulating shared resources? If yes, putting References: RelayCommand docs |
||
try | ||
{ | ||
var key = e.Key; | ||
_log.Debug($"e.Key = {key}"); | ||
if (e.Handled || !(key == VirtualKey.Left || key == VirtualKey.Right)) | ||
{ | ||
return; | ||
} | ||
|
||
var focusedItem = e.OriginalSource as GridViewItem; | ||
if (focusedItem == null || focusedItem.Content == null || focusedItem.Content is not WidgetViewModel widgetViewModel) | ||
krschau marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
return; | ||
} | ||
|
||
if (IsAltAndShiftPressed()) | ||
{ | ||
var index = ViewModel.PinnedWidgets.IndexOf(widgetViewModel); | ||
_log.Information($"Move widget {widgetViewModel.WidgetDisplayTitle} at index {index} {key}"); | ||
|
||
if (key == VirtualKey.Left && index > 0) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe this can be reduced to figuring out the new index in the if/else block. |
||
{ | ||
await MoveWidgetAsync(widgetViewModel, index, index - 1); | ||
await FocusManager.TryFocusAsync(WidgetGridView.ItemsPanelRoot.Children.ElementAt(index - 1), FocusState.Keyboard); | ||
_log.Debug($"Focus moved to index {index - 1}"); | ||
} | ||
else if (key == VirtualKey.Right && index < (ViewModel.PinnedWidgets.Count - 1)) | ||
{ | ||
// Setting focus before and after the move looks more natural than letting the focus move to the wrong element. | ||
await FocusManager.TryFocusAsync(WidgetGridView.ItemsPanelRoot.Children.ElementAt(index + 1), FocusState.Keyboard); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why the extra focus here, but not in the if case? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was nothing I could set focus on in the if case that would make the animation look less awkward. Someday I'd like better animations for both keyboard and the drag and drop scenario, but I'd rather unblock accessibility asap than hold anything up for that. |
||
await MoveWidgetAsync(widgetViewModel, index, index + 1); | ||
await FocusManager.TryFocusAsync(WidgetGridView.ItemsPanelRoot.Children.ElementAt(index + 1), FocusState.Keyboard); | ||
_log.Debug($"Focus moved to index {index + 1}"); | ||
} | ||
} | ||
|
||
e.Handled = true; | ||
} | ||
finally | ||
{ | ||
_moveWidgetsLock.Release(); | ||
} | ||
} | ||
|
||
private bool IsAltAndShiftPressed() | ||
{ | ||
var downState = CoreVirtualKeyStates.Down; | ||
|
||
// VirtualKeys "Menu" key is also the "Alt" key on the keyboard. | ||
var isAltKeyPressed = (InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Menu) & downState) == downState; | ||
var isShiftKeyPressed = (InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Shift) & downState) == downState; | ||
_log.Debug($"isAltKeyPressed = {isAltKeyPressed} isShiftKeyPressed = {isShiftKeyPressed}"); | ||
|
||
return isAltKeyPressed && isShiftKeyPressed; | ||
} | ||
|
||
public void Dispose() | ||
|
@@ -856,6 +942,7 @@ protected virtual void Dispose(bool disposing) | |
if (disposing) | ||
{ | ||
_pinnedWidgetsLock.Dispose(); | ||
_moveWidgetsLock.Dispose(); | ||
} | ||
|
||
_disposedValue = true; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would promoting this to a command prevent any unexpected concurrent asynchronous execution behavior?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because it's in a ItemsPanelTempate, I can't bind to the command 🙁
microsoft/microsoft-ui-xaml#2508