Repository: microsoft/terminal
Pull request #18518: Feat preview color scheme on mouse hover
Author: login: eleadufresne
Labels: name: Product-Terminal
description: The new Windows Terminal.
name: Issue-Task
description: It's a feature request, but it doesn't really need a major design.
name: Area-CmdPal
description: Command Palette issues and features
Created: 2025-02-06T03:06:38Z | Merged: 2025-03-15T00:00:10Z

## Summary of the Pull Request

Allow users to preview color schemes by hovering over them with the mouse pointer in the Command Palette.


![Screenshot 2025-02-05 190055](https://github.com/user-attachments/assets/0f9008c5-299c-4f38-a5bb-ab3688af3e96)

## References and Relevant Issues

- This PR handles issue #18238. 
- This extends the previously closed issue #6689, which allowed the `UP` and `DOWN` arrows to trigger a preview of color schemes in the Command Palette.

## Detailed Description of the Pull Request / Additional comments

This works by attaching event handlers for `PointerEntered` and `PointerExited` to `ListViewItem` containers. When the mouse pointer moves into the item's bounding area, the `PreviewAction` handler is triggered to showcase the hovered color scheme. Conversely, when the mouse pointer leaves the item's area, the `PreviewAction` is executed on the selected item (generally from the `UP` and `DOWN` arrows). 

**Decisions I made, but I'm uncertain about:**

- ~~I coded my solution so that `PreviewAction` is called on the submenu items, as this is where the color schemes are located. I felt invoking the function for each element was unnecessary, but this approach may make it less flexible for future modifications.~~
- I'm attaching pointer event handlers when the data associated with the `ListViewItem` changes, but there might be a better approach.

**Important note:**

- This also provides previews for the other features that the `ActionPreviewHandler` handles, such as the background opacity of the terminal. 

## Validation Steps Performed

- Hover a color scheme, and it becomes the active one.
- Pressing `ESC` at any point to dismiss the command palette, and the scheme returns to the previous one.
- I did not add any additional test, though all existing ColorScheme tests passed.

## PR Checklist
- [x] Closes #18238
- [x] Tests added/passed
- [ ] Documentation updated
   - If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/terminal) and link it here: #xxx
- [ ] Schema updated (if necessary)


Linked issue #18238: Request: "Select color scheme..." mouse-hover should provide color-scheme preview
### Description of the new feature

This is a tiny quality-of-life improvement request:

CTRL + SHIFT + P   ----> "Select color scheme..."

We see now see a dropdown of all our color schemes.
The UP and DOWN arrows trigger a preview of the highlighted color-scheme.
But hovering the mouse over a color-scheme ...only results in a 'gentle highlight', no preview.

My request: hovering over a color-scheme should also trigger a preview in the current tab.

This may seem trivial, but it would be much appreciated.


### Proposed technical implementation details

_No response_
Linked issue comments: user: login: DHowett
created_at: 2024-11-27T14:45:34Z
body: You know, in retrospect it's surprising we missed this. Thank you!
author_association: MEMBER
user: login: eleadufresne
created_at: 2025-02-06T04:49:11Z
body: I drafted a PR for this feature; I'd appreciate if someone could take a look. 😊

CC: @DHowett
author_association: CONTRIBUTOR

Diff:
diff --git a/src/cascadia/TerminalApp/CommandPalette.cpp b/src/cascadia/TerminalApp/CommandPalette.cpp
index bcee25c72dd..6d5540703fc 100644
--- a/src/cascadia/TerminalApp/CommandPalette.cpp
+++ b/src/cascadia/TerminalApp/CommandPalette.cpp
@@ -530,6 +530,79 @@ namespace winrt::TerminalApp::implementation
         }
     }
 
+    // Method Description:
+    // - This event is called when the user's mouse pointer enters an individual
+    //   item from the list. We'll get the item that was hovered and "preview"
+    //   the command that the user hovered. To do that, we'll dispatch the switch
+    //   to tab command for this tab, but not dismiss the switcher.
+    //
+    // Arguments:
+    // - sender: the UI element that raised the event.
+    // Return Value:
+    // - <none>
+    void CommandPalette::_listItemPointerEntered(const winrt::Windows::Foundation::IInspectable& sender,
+                                                 const winrt::Windows::UI::Xaml::Input::PointerRoutedEventArgs& /*args*/)
+    {
+        // cancel any pending exit timer to prevent an unwanted preview revert
+        if (_pointerExitTimer)
+        {
+            _pointerExitTimer.Stop();
+        }
+
+        const auto listViewItem = sender.try_as<winrt::Windows::UI::Xaml::Controls::ListViewItem>();
+        if (_currentMode == CommandPaletteMode::ActionMode && listViewItem)
+        {
+            const auto enteredItem = listViewItem.Content();
+            if (const auto filteredCommand{ enteredItem.try_as<winrt::TerminalApp::FilteredCommand>() })
+            {
+                if (const auto actionPaletteItem{ filteredCommand.Item().try_as<winrt::TerminalApp::ActionPaletteItem>() })
+                {
+                    // immediately preview the hovered command
+                    PreviewAction.raise(*this, actionPaletteItem.Command());
+                }
+            }
+        }
+    }
+
+    // Method Description:
+    // - This event is called when the user's mouse pointer exits an individual
+    //   item from the list. We then revert to previewing the selected item rather
+    //   than the hovered one, using a short delay (via a DispatcherTimer) to smooth
+    //   transitions when rapidly moving between items.
+    //
+    // Arguments:
+    // - <none>
+    // Return Value:
+    // - <none>
+    void CommandPalette::_listItemPointerExited(const winrt::Windows::Foundation::IInspectable& /*sender*/,
+                                                const winrt::Windows::UI::Xaml::Input::PointerRoutedEventArgs& /*args*/)
+    {
+        // if there is no exit timer, create one
+        if (!_pointerExitTimer)
+        {
+            _pointerExitTimer = winrt::Windows::UI::Xaml::DispatcherTimer();
+            _pointerExitTimer.Interval(std::chrono::milliseconds(10));
+            _pointerExitTimer.Tick([this](auto const&, auto const&) {
+                // when the timer ticks, revert the preview to the selected command
+                const auto selectedCommand = _filteredActionsView().SelectedItem();
+                if (const auto filteredCommand{ selectedCommand.try_as<winrt::TerminalApp::FilteredCommand>() })
+                {
+                    if (_currentMode == CommandPaletteMode::ActionMode && filteredCommand)
+                    {
+                        if (const auto actionPaletteItem{ filteredCommand.Item().try_as<winrt::TerminalApp::ActionPaletteItem>() })
+                        {
+                            PreviewAction.raise(*this, actionPaletteItem.Command());
+                        }
+                    }
+                }
+                _pointerExitTimer.Stop();
+            });
+        }
+
+        // restart the timer
+        _pointerExitTimer.Start();
+    }
+
     void CommandPalette::_listItemSelectionChanged(const Windows::Foundation::IInspectable& /*sender*/, const Windows::UI::Xaml::Controls::SelectionChangedEventArgs& e)
     {
         // We don't care about...
@@ -1214,6 +1287,9 @@ namespace winrt::TerminalApp::implementation
 
         ParentCommandName(L"");
         _currentNestedCommands.Clear();
+
+        // revert any preview
+        _filteredActionsView().SelectedIndex(-1);
         PreviewAction.raise(*this, nullptr);
     }
 
@@ -1306,6 +1382,10 @@ namespace winrt::TerminalApp::implementation
         else
         {
             itemContainer.DataContext(args.Item());
+
+            // attach the pointer event handlers to the container
+            itemContainer.PointerEntered({ this, &CommandPalette::_listItemPointerEntered });
+            itemContainer.PointerExited({ this, &CommandPalette::_listItemPointerExited });
         }
     }
 
diff --git a/src/cascadia/TerminalApp/CommandPalette.h b/src/cascadia/TerminalApp/CommandPalette.h
index 58e34bb1f90..0da7ee13344 100644
--- a/src/cascadia/TerminalApp/CommandPalette.h
+++ b/src/cascadia/TerminalApp/CommandPalette.h
@@ -78,6 +78,8 @@ namespace winrt::TerminalApp::implementation
 
         Windows::Foundation::Collections::IVector<winrt::TerminalApp::FilteredCommand> _commandsToFilter();
 
+        winrt::Windows::UI::Xaml::DispatcherTimer _pointerExitTimer{ nullptr }; // timer to debounce pointer exit events (used to smooth preview transitions)
+
         bool _lastFilterTextWasEmpty{ true };
 
         void _populateCommands();
@@ -103,6 +105,10 @@ namespace winrt::TerminalApp::implementation
 
         void _listItemClicked(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::Controls::ItemClickEventArgs& e);
 
+        void _listItemPointerEntered(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::Input::PointerRoutedEventArgs& args);
+
+        void _listItemPointerExited(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::Input::PointerRoutedEventArgs& args);
+
         void _listItemSelectionChanged(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::Controls::SelectionChangedEventArgs& e);
 
         void _moveBackButtonClicked(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs&);


Reviews:
user: login: carlos-zamora
body: Looks and feels great! Just the one comment before approving. Thanks for doing this!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-02-20T18:29:06Z
user: login: carlos-zamora
body: (whoops, clicked the wrong button...)
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-02-20T18:29:27Z
user: login: eleadufresne
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-20T19:08:44Z
user: login: carlos-zamora
state: APPROVED
author_association: MEMBER
submitted_at: 2025-02-20T19:54:24Z
user: login: lhecker
body: One tiny nit, otherwise LGTM!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-11T16:17:57Z
user: login: eleadufresne
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T18:14:47Z
user: login: DHowett
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-14T22:08:04Z
user: login: eleadufresne
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T23:18:22Z
user: login: DHowett
body: This looks great! Thanks so much for doing it, and for being patient with us while we were a bit under-staffed. I can't wait to ship this!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-14T23:23:46Z

Review comments:
diff_hunk: @@ -530,6 +530,85 @@ namespace winrt::TerminalApp::implementation
         }
     }
 
+    // Method Description:
+    // - This event is called when the user's mouse pointer enters an individual
+    //   item from the list. We'll get the item that was hovered and "preview"
+    //   the command that the user hovered. To do that, we'll dispatch the switch
+    //   to tab command for this tab, but not dismiss the switcher.
+    //
+    // Arguments:
+    // - sender: the UI element that raised the event.
+    // Return Value:
+    // - <none>
+    void CommandPalette::_listItemPointerEntered(const winrt::Windows::Foundation::IInspectable& sender,
+                                                 const winrt::Windows::UI::Xaml::Input::PointerRoutedEventArgs& /*args*/)
+    {
+        // cancel any pending exit timer to prevent an unwanted preview revert
+        if (_pointerExitTimer)
+        {
+            _pointerExitTimer.Stop();
+        }
+
+        auto listViewItem = sender.try_as<winrt::Windows::UI::Xaml::Controls::ListViewItem>();
path: src/cascadia/TerminalApp/CommandPalette.cpp
user: login: carlos-zamora
body: ```suggestion
        const auto listViewItem = sender.try_as<winrt::Windows::UI::Xaml::Controls::ListViewItem>();
```
created_at: 2025-02-20T18:23:10Z
author_association: MEMBER
diff_hunk: @@ -530,6 +530,85 @@ namespace winrt::TerminalApp::implementation
         }
     }
 
+    // Method Description:
+    // - This event is called when the user's mouse pointer enters an individual
+    //   item from the list. We'll get the item that was hovered and "preview"
+    //   the command that the user hovered. To do that, we'll dispatch the switch
+    //   to tab command for this tab, but not dismiss the switcher.
+    //
+    // Arguments:
+    // - sender: the UI element that raised the event.
+    // Return Value:
+    // - <none>
+    void CommandPalette::_listItemPointerEntered(const winrt::Windows::Foundation::IInspectable& sender,
+                                                 const winrt::Windows::UI::Xaml::Input::PointerRoutedEventArgs& /*args*/)
+    {
+        // cancel any pending exit timer to prevent an unwanted preview revert
+        if (_pointerExitTimer)
+        {
+            _pointerExitTimer.Stop();
+        }
+
+        auto listViewItem = sender.try_as<winrt::Windows::UI::Xaml::Controls::ListViewItem>();
+        if (_currentMode == CommandPaletteMode::ActionMode && listViewItem)
+        {
+            const auto enteredItem = listViewItem.Content();
+            if (const auto filteredCommand{ enteredItem.try_as<winrt::TerminalApp::FilteredCommand>() })
+            {
+                // only preview submenu items (unsure about this as a requirement since for now we only preview color schemes)
+                if (_nestedActionStack.Size() > 0)
path: src/cascadia/TerminalApp/CommandPalette.cpp
user: login: carlos-zamora
body: Good callout! But nah, we can get rid of this if-statement.

The idea here really is to just allow actions to be previewable via the mouse, not just color schemes (and especially not just the ones that are nested).
created_at: 2025-02-20T18:26:48Z
author_association: MEMBER
diff_hunk: @@ -530,6 +530,85 @@ namespace winrt::TerminalApp::implementation
         }
     }
 
+    // Method Description:
+    // - This event is called when the user's mouse pointer enters an individual
+    //   item from the list. We'll get the item that was hovered and "preview"
+    //   the command that the user hovered. To do that, we'll dispatch the switch
+    //   to tab command for this tab, but not dismiss the switcher.
+    //
+    // Arguments:
+    // - sender: the UI element that raised the event.
+    // Return Value:
+    // - <none>
+    void CommandPalette::_listItemPointerEntered(const winrt::Windows::Foundation::IInspectable& sender,
+                                                 const winrt::Windows::UI::Xaml::Input::PointerRoutedEventArgs& /*args*/)
+    {
+        // cancel any pending exit timer to prevent an unwanted preview revert
+        if (_pointerExitTimer)
+        {
+            _pointerExitTimer.Stop();
+        }
+
+        auto listViewItem = sender.try_as<winrt::Windows::UI::Xaml::Controls::ListViewItem>();
+        if (_currentMode == CommandPaletteMode::ActionMode && listViewItem)
+        {
+            const auto enteredItem = listViewItem.Content();
+            if (const auto filteredCommand{ enteredItem.try_as<winrt::TerminalApp::FilteredCommand>() })
+            {
+                // only preview submenu items (unsure about this as a requirement since for now we only preview color schemes)
+                if (_nestedActionStack.Size() > 0)
path: src/cascadia/TerminalApp/CommandPalette.cpp
user: login: eleadufresne
body: Got it, that makes sense! I’ve gone ahead and taken out the if-statement. Thanks for the clarification 😊
created_at: 2025-02-20T19:08:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -530,6 +530,80 @@ namespace winrt::TerminalApp::implementation
         }
     }
 
+    // Method Description:
+    // - This event is called when the user's mouse pointer enters an individual
+    //   item from the list. We'll get the item that was hovered and "preview"
+    //   the command that the user hovered. To do that, we'll dispatch the switch
+    //   to tab command for this tab, but not dismiss the switcher.
+    //
+    // Arguments:
+    // - sender: the UI element that raised the event.
+    // Return Value:
+    // - <none>
+    void CommandPalette::_listItemPointerEntered(const winrt::Windows::Foundation::IInspectable& sender,
+                                                 const winrt::Windows::UI::Xaml::Input::PointerRoutedEventArgs& /*args*/)
+    {
+        // cancel any pending exit timer to prevent an unwanted preview revert
+        if (_pointerExitTimer)
+        {
+            _pointerExitTimer.Stop();
+        }
+
+        const auto listViewItem = sender.try_as<winrt::Windows::UI::Xaml::Controls::ListViewItem>();
+        if (_currentMode == CommandPaletteMode::ActionMode && listViewItem)
+        {
+            const auto enteredItem = listViewItem.Content();
+            if (const auto filteredCommand{ enteredItem.try_as<winrt::TerminalApp::FilteredCommand>() })
+            {
+                if (const auto actionPaletteItem{ filteredCommand.Item().try_as<winrt::TerminalApp::ActionPaletteItem>() })
+                {
+                    // immediately preview the hovered command
+                    PreviewAction.raise(*this, actionPaletteItem.Command());
+                }
+            }
+        }
+    }
+
+    // Method Description:
+    // - This event is called when the user's mouse pointer exits an individual
+    //   item from the list. We then revert to previewing the selected item rather
+    //   than the hovered one, using a short delay (via a DispatcherTimer) to smooth
+    //   transitions when rapidly moving between items.
+    //
+    // Arguments:
+    // - <none>
+    // Return Value:
+    // - <none>
+    void CommandPalette::_listItemPointerExited(const winrt::Windows::Foundation::IInspectable& /*sender*/,
+                                                const winrt::Windows::UI::Xaml::Input::PointerRoutedEventArgs& /*args*/)
+    {
+        // if there is no exit timer, create one
+        if (!_pointerExitTimer)
+        {
+            _pointerExitTimer = winrt::Windows::UI::Xaml::DispatcherTimer();
+            _pointerExitTimer.Interval(std::chrono::milliseconds(10));
+            _pointerExitTimer.Tick([this](auto const&, auto const&) {
+                // when the timer ticks, revert the preview to the selected command
+                const auto selectedCommand = _filteredActionsView().SelectedItem();
+                if (const auto filteredCommand{ selectedCommand.try_as<winrt::TerminalApp::FilteredCommand>() })
+                {
+                    if (_currentMode == CommandPaletteMode::ActionMode && filteredCommand)
+                    {
+                        if (const auto actionPaletteItem{ filteredCommand.Item().try_as<winrt::TerminalApp::ActionPaletteItem>() })
+                        {
+                            PreviewAction.raise(*this, actionPaletteItem.Command());
+                        }
+                    }
+                }
+                _pointerExitTimer.Stop();
+            });
+        }
+
+        // restart the timer
+        _pointerExitTimer.Stop();
+        _pointerExitTimer.Start();
path: src/cascadia/TerminalApp/CommandPalette.cpp
user: login: lhecker
body: FWIW I believe `Start` already resets the timer: https://learn.microsoft.com/en-us/uwp/api/windows.ui.xaml.dispatchertimer.start
created_at: 2025-03-11T16:17:40Z
author_association: MEMBER
diff_hunk: @@ -530,6 +530,80 @@ namespace winrt::TerminalApp::implementation
         }
     }
 
+    // Method Description:
+    // - This event is called when the user's mouse pointer enters an individual
+    //   item from the list. We'll get the item that was hovered and "preview"
+    //   the command that the user hovered. To do that, we'll dispatch the switch
+    //   to tab command for this tab, but not dismiss the switcher.
+    //
+    // Arguments:
+    // - sender: the UI element that raised the event.
+    // Return Value:
+    // - <none>
+    void CommandPalette::_listItemPointerEntered(const winrt::Windows::Foundation::IInspectable& sender,
+                                                 const winrt::Windows::UI::Xaml::Input::PointerRoutedEventArgs& /*args*/)
+    {
+        // cancel any pending exit timer to prevent an unwanted preview revert
+        if (_pointerExitTimer)
+        {
+            _pointerExitTimer.Stop();
+        }
+
+        const auto listViewItem = sender.try_as<winrt::Windows::UI::Xaml::Controls::ListViewItem>();
+        if (_currentMode == CommandPaletteMode::ActionMode && listViewItem)
+        {
+            const auto enteredItem = listViewItem.Content();
+            if (const auto filteredCommand{ enteredItem.try_as<winrt::TerminalApp::FilteredCommand>() })
+            {
+                if (const auto actionPaletteItem{ filteredCommand.Item().try_as<winrt::TerminalApp::ActionPaletteItem>() })
+                {
+                    // immediately preview the hovered command
+                    PreviewAction.raise(*this, actionPaletteItem.Command());
+                }
+            }
+        }
+    }
+
+    // Method Description:
+    // - This event is called when the user's mouse pointer exits an individual
+    //   item from the list. We then revert to previewing the selected item rather
+    //   than the hovered one, using a short delay (via a DispatcherTimer) to smooth
+    //   transitions when rapidly moving between items.
+    //
+    // Arguments:
+    // - <none>
+    // Return Value:
+    // - <none>
+    void CommandPalette::_listItemPointerExited(const winrt::Windows::Foundation::IInspectable& /*sender*/,
+                                                const winrt::Windows::UI::Xaml::Input::PointerRoutedEventArgs& /*args*/)
+    {
+        // if there is no exit timer, create one
+        if (!_pointerExitTimer)
+        {
+            _pointerExitTimer = winrt::Windows::UI::Xaml::DispatcherTimer();
+            _pointerExitTimer.Interval(std::chrono::milliseconds(10));
+            _pointerExitTimer.Tick([this](auto const&, auto const&) {
+                // when the timer ticks, revert the preview to the selected command
+                const auto selectedCommand = _filteredActionsView().SelectedItem();
+                if (const auto filteredCommand{ selectedCommand.try_as<winrt::TerminalApp::FilteredCommand>() })
+                {
+                    if (_currentMode == CommandPaletteMode::ActionMode && filteredCommand)
+                    {
+                        if (const auto actionPaletteItem{ filteredCommand.Item().try_as<winrt::TerminalApp::ActionPaletteItem>() })
+                        {
+                            PreviewAction.raise(*this, actionPaletteItem.Command());
+                        }
+                    }
+                }
+                _pointerExitTimer.Stop();
+            });
+        }
+
+        // restart the timer
+        _pointerExitTimer.Stop();
+        _pointerExitTimer.Start();
path: src/cascadia/TerminalApp/CommandPalette.cpp
user: login: eleadufresne
body: You're right, it does! Good catch, thanks 😊 I just removed the redundant call.
created_at: 2025-03-11T18:14:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1214,6 +1287,11 @@ namespace winrt::TerminalApp::implementation
 
         ParentCommandName(L"");
         _currentNestedCommands.Clear();
+
+        PreviewAction.raise(*this, nullptr);
+
+        // cancel previews triggered by mouse pointer events
+        _filteredActionsView().SelectedIndex(-1);
         PreviewAction.raise(*this, nullptr);
path: src/cascadia/TerminalApp/CommandPalette.cpp
user: login: DHowett
body: I'm surprised we're raising this twice - is it required to do it both before and after the selected index reset?
created_at: 2025-03-14T22:08:04Z
author_association: MEMBER
diff_hunk: @@ -1214,6 +1287,11 @@ namespace winrt::TerminalApp::implementation
 
         ParentCommandName(L"");
         _currentNestedCommands.Clear();
+
+        PreviewAction.raise(*this, nullptr);
+
+        // cancel previews triggered by mouse pointer events
+        _filteredActionsView().SelectedIndex(-1);
         PreviewAction.raise(*this, nullptr);
path: src/cascadia/TerminalApp/CommandPalette.cpp
user: login: eleadufresne
body: Oh, great catch! 😅 My understanding was that when we use the `UP/DOWN` arrows to generate the preview, the first call would switch back to the user's colour scheme, while the second call wouldn't do anything. On the other hand, if we trigger it with a mouse hover, the first call would revert to the colour from the `UP/DOWN` arrows, and the second call would go back to the user's colour scheme. 

But you're absolutely right; I didn't realize the first call was unnecessary. If we remove it, it will always revert to the user's colour scheme. Thanks so much for your patience!
created_at: 2025-03-14T23:18:22Z
author_association: CONTRIBUTOR

PR comments:
user: login: eleadufresne
created_at: 2025-02-06T03:09:59Z
body: @microsoft-github-policy-service agree
author_association: CONTRIBUTOR
user: login: eleadufresne
created_at: 2025-02-28T19:49:28Z
body: @DHowett can you please take a look when you have the chance? Thanks!
author_association: CONTRIBUTOR
user: login: DHowett
created_at: 2025-02-28T21:10:05Z
body: /azp run
author_association: MEMBER
user: login: azure-pipelines[bot]
created_at: 2025-02-28T21:10:15Z
body: <samp>
Azure Pipelines successfully started running 1 pipeline(s).<br>

</samp>
author_association: NONE
user: login: DHowett
created_at: 2025-02-28T21:10:19Z
body: @eleadufresne so sorry for the delay! I've been out sick this week, but I'm putting it at the top of my list
author_association: MEMBER
user: login: DHowett
created_at: 2025-02-28T21:11:48Z
body: ah, you may need to run the code formatter :)

i am dismayed that we are no longer allowed to run these things without team approval - it slows down the whole PR process.
author_association: MEMBER
user: login: eleadufresne
created_at: 2025-02-28T21:52:53Z
body: @DHowett Thanks for taking the time to look at it—especially while you’re not feeling great—and apologies for missing this in my last commit. I ran the formatter, so fingers crossed it passes all the checks this time!

By the way, can I run the pipeline myself now, or should it always be handled by someone from the team?
author_association: CONTRIBUTOR
user: login: DHowett
created_at: 2025-02-28T21:54:58Z
body: /azp run
author_association: MEMBER
user: login: azure-pipelines[bot]
created_at: 2025-02-28T21:55:08Z
body: <samp>
Azure Pipelines successfully started running 1 pipeline(s).<br>

</samp>
author_association: NONE
user: login: DHowett
created_at: 2025-02-28T21:55:21Z
body: unfortunately, it still needs a team member to approve each iteration
author_association: MEMBER
user: login: eleadufresne
created_at: 2025-02-28T22:03:30Z
body: Ah gotchu, thanks for the info!
author_association: CONTRIBUTOR
user: login: eleadufresne
created_at: 2025-02-28T23:58:01Z
body: @DHowett, looks like all checks have passed 😄 Thanks again for your help! Let me know if you need anything else before merging.
author_association: CONTRIBUTOR
user: login: DHowett
created_at: 2025-03-14T23:24:01Z
body: /azp run
author_association: MEMBER
user: login: azure-pipelines[bot]
created_at: 2025-03-14T23:24:11Z
body: <samp>
Azure Pipelines successfully started running 1 pipeline(s).<br>

</samp>
author_association: NONE
user: login: DHowett
created_at: 2025-03-14T23:25:07Z
body: > - I'm attaching pointer event handlers when the data associated with the `ListViewItem` changes, but there might be a better approach.

Honestly, probably not. This is fine with me - so I removed it from the final commit message!
author_association: MEMBER
user: login: eleadufresne
created_at: 2025-03-14T23:31:28Z
body: @DHowett No worries at all! It's been a great learning experience, and I appreciate the guidance. Always happy to help; I'm super excited to see this get merged 😄
author_association: CONTRIBUTOR