Skip to content

Implement the shift click gesture to select several adjacent threads in the timeline#4138

Merged
julienw merged 9 commits into
firefox-devtools:mainfrom
julienw:support-shift-click-in-timeline
Aug 3, 2022
Merged

Implement the shift click gesture to select several adjacent threads in the timeline#4138
julienw merged 9 commits into
firefox-devtools:mainfrom
julienw:support-shift-click-in-timeline

Conversation

@julienw

@julienw julienw commented Jul 11, 2022

Copy link
Copy Markdown
Contributor

Gestures that work:

  • click some track, shift click some other track => threads in-between are selected. This works in both directions.
  • click some track, ctrl click some other track, shift + click a third track => the range is added to the initial selection
  • at load time, when there's just one selected track, it's possible to shift + click another track, the existing selected track will be used as the start of the range
  • for all of these gestures, after a shift-click, we can shift-click elsewhere, which should cancel the previous shift-click and select again.

Here is a deploy preview

Fixes #2710

Screencast_20220721_184835.mp4

@julienw julienw force-pushed the support-shift-click-in-timeline branch 4 times, most recently from b861724 to b6e8f9a Compare July 18, 2022 15:50
@codecov

codecov Bot commented Jul 18, 2022

Copy link
Copy Markdown

Codecov Report

Merging #4138 (61d33ae) into main (aefb4d3) will increase coverage by 0.13%.
The diff coverage is 94.20%.

❗ Current head 61d33ae differs from pull request most recent head 7caaf80. Consider uploading reports for the commit 7caaf80 to get more accurate results

@@            Coverage Diff             @@
##             main    #4138      +/-   ##
==========================================
+ Coverage   88.33%   88.46%   +0.13%     
==========================================
  Files         280      280              
  Lines       24409    24510     +101     
  Branches     6491     6533      +42     
==========================================
+ Hits        21561    21683     +122     
+ Misses       2645     2625      -20     
+ Partials      203      202       -1     
Impacted Files Coverage Δ
src/components/timeline/LocalTrack.js 73.17% <ø> (+1.21%) ⬆️
src/components/timeline/TrackThread.js 97.18% <92.85%> (+4.97%) ⬆️
src/actions/profile-view.js 87.18% <93.00%> (+3.90%) ⬆️
src/profile-logic/tracks.js 83.45% <94.11%> (+0.35%) ⬆️
src/components/timeline/GlobalTrack.js 70.64% <100.00%> (ø)
src/reducers/profile-view.js 96.21% <100.00%> (+0.35%) ⬆️
src/selectors/profile.js 97.00% <100.00%> (+<0.01%) ⬆️
src/utils/index.js 88.57% <100.00%> (ø)
src/components/app/ServiceWorkerManager.js 84.82% <0.00%> (-4.66%) ⬇️
src/index.js 0.00% <0.00%> (ø)
... and 6 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update aefb4d3...7caaf80. Read the comment docs.

@julienw julienw force-pushed the support-shift-click-in-timeline branch 2 times, most recently from 94e4d4c to ea45ec6 Compare July 21, 2022 16:45
@julienw julienw marked this pull request as ready for review July 21, 2022 16:45
@julienw julienw requested a review from canova July 21, 2022 16:45
@julienw julienw force-pushed the support-shift-click-in-timeline branch from ea45ec6 to 33fcee5 Compare July 21, 2022 16:55
@julienw julienw removed the request for review from canova July 21, 2022 16:57
@julienw julienw force-pushed the support-shift-click-in-timeline branch 5 times, most recently from 3fd5ecd to 5f305b2 Compare July 22, 2022 14:28
@julienw julienw requested a review from canova July 22, 2022 14:33
@mstange

mstange commented Jul 23, 2022

Copy link
Copy Markdown
Contributor

This works quite well!

I've noticed one case that I think can be tweaked: "Shift selection after Cmd selection"

  1. Have thread 1 selected.
  2. Cmd+click thread 3.
  3. Shift+click thread 5.

Expected selection: 1, 3, 4, 5 (this is what macOS Finder does)
Actual selection: 3, 4, 5 (everything outside of the new shift selection got discarded)

@mstange

mstange commented Jul 23, 2022

Copy link
Copy Markdown
Contributor

I also noticed some weirdness with the IPC tracks, but that's probably something that can be fixed as a follow-up.

For example, in the "Utility AudioDecoder" process, the "MediaPDecoder #1" thread is framed by two IPC tracks. Clicking one of those IPC tracks and then shift-clicking the other creates a selection which only contains the thread-track in between. Cmd+clicking the tracks individually has a different result.

@julienw

julienw commented Jul 23, 2022

Copy link
Copy Markdown
Contributor Author

This works quite well!

I've noticed one case that I think can be tweaked: "Shift selection after Cmd selection"

1. Have thread 1 selected.

2. Cmd+click thread 3.

3. Shift+click thread 5.

Expected selection: 1, 3, 4, 5 (this is what macOS Finder does) Actual selection: 3, 4, 5 (everything outside of the new shift selection got discarded)

Ah, that's interesting!
This works if you keep Ctrl pressed for the shift, but making it work without that would require saving the previous modifier, which sounds more work for this small use case...

I also noticed some weirdness with the IPC tracks, but that's probably something that can be fixed as a follow-up.

For example, in the "Utility AudioDecoder" process, the "MediaPDecoder #1" thread is framed by two IPC tracks. Clicking one of those IPC tracks and then shift-clicking the other creates a selection which only contains the thread-track in between. Cmd+clicking the tracks individually has a different result.

Ah that's interesting too! This is quite similar to the previous issue in that when we don't use Ctrl we replace the previous selection. I can see how it's weird in this case though and I'll think about how to fix it easily.

@julienw

julienw commented Jul 25, 2022

Copy link
Copy Markdown
Contributor Author

This works quite well!
I've noticed one case that I think can be tweaked: "Shift selection after Cmd selection"

1. Have thread 1 selected.

2. Cmd+click thread 3.

3. Shift+click thread 5.

Expected selection: 1, 3, 4, 5 (this is what macOS Finder does) Actual selection: 3, 4, 5 (everything outside of the new shift selection got discarded)

Ah, that's interesting! This works if you keep Ctrl pressed for the shift, but making it work without that would require saving the previous modifier, which sounds more work for this small use case...

Interestingly: libreoffice spreadsheets works like MacOS X' finder, but google spreadsheets works like my implementation.
Now I'm on the verge of implementing this :)

@julienw julienw removed the request for review from canova July 25, 2022 09:53
@julienw julienw force-pushed the support-shift-click-in-timeline branch from 5f305b2 to 1ab1c13 Compare July 26, 2022 16:51
@julienw

julienw commented Jul 26, 2022

Copy link
Copy Markdown
Contributor Author
 Ah that's interesting too! This is quite similar to the previous issue in that when we don't use Ctrl we replace the previous selection. I can see how it's weird in this case though and I'll think about how to fix it easily.

That one was easy to fix.

I'm trying to implement the other behavior.

@julienw julienw force-pushed the support-shift-click-in-timeline branch from 1ab1c13 to 964e667 Compare July 27, 2022 13:08
@mstange

mstange commented Jul 27, 2022

Copy link
Copy Markdown
Contributor

Now this case doesn't work as expected anymore:

1 is selected, Cmd 3, Shift 5, Shift 2, expected: 1, 2, 3

I haven't looked at your code at all, but I would implement it as follows:

type State = {|
  currentSelection: Set<TrackId>,
  selectionAtLastNonShiftClick: Set<TrackId>,
  clickedTrackAtLastNonShiftClick: TrackId,
|};

function onClick(state: State, clickedTrack: TrackId): State {
  const newSelection = new Set([clickedTrack]);
  return {
    currentSelection: newSelection,
    selectionAtLastNonShiftClick: newSelection,
    clickedTrackAtLastNonShiftClick: clickedTrack,
  }
}

function onCmdClick(state: State, clickedTrack: TrackId): State {
  const newSelection = new Set([...state.currentSelection, clickedTrack]);
  return {
    currentSelection: newSelection,
    selectionAtLastNonShiftClick: newSelection,
    clickedTrackAtLastNonShiftClick: clickedTrack,
  }
}

function onShiftClick(state: State, clickedTrack: TrackId): State {
  const shiftSelection = computeTracksBetween(state.clickedTrackAtLastNonShiftClick, clickedTrack);
  const newSelection = new Set([...state.selectionAtLastNonShiftClick, ...shiftSelection]);
  return {
    ...state,
    currentSelection: newSelection,
  }
}

@mstange

mstange commented Jul 27, 2022

Copy link
Copy Markdown
Contributor

Now this case doesn't work as expected anymore:

Oh, correction, it wasn't working at all before either. So I should rather say: Thank you for implementing the Cmd-then-shift behavior, but here's a case that can still be improved to match macOS Finder more closely.

@julienw julienw force-pushed the support-shift-click-in-timeline branch 2 times, most recently from 5b85355 to 61d33ae Compare July 27, 2022 16:21
@julienw

julienw commented Jul 27, 2022

Copy link
Copy Markdown
Contributor Author

Now this case doesn't work as expected anymore:

1 is selected, Cmd 3, Shift 5, Shift 2, expected: 1, 2, 3

I agree, I expect this to work too, and it doesn't. This works as expected without the Ctrl thing.

I haven't looked at your code at all, but I would implement it as follows:

type State = {|
  currentSelection: Set<TrackId>,
  selectionAtLastNonShiftClick: Set<TrackId>,
  clickedTrackAtLastNonShiftClick: TrackId,
|};

function onClick(state: State, clickedTrack: TrackId): State {
  const newSelection = new Set([clickedTrack]);
  return {
    currentSelection: newSelection,
    selectionAtLastNonShiftClick: newSelection,
    clickedTrackAtLastNonShiftClick: clickedTrack,
  }
}

function onCmdClick(state: State, clickedTrack: TrackId): State {
  const newSelection = new Set([...state.currentSelection, clickedTrack]);
  return {
    currentSelection: newSelection,
    selectionAtLastNonShiftClick: newSelection,
    clickedTrackAtLastNonShiftClick: clickedTrack,
  }
}

function onShiftClick(state: State, clickedTrack: TrackId): State {
  const shiftSelection = computeTracksBetween(state.clickedTrackAtLastNonShiftClick, clickedTrack);
  const newSelection = new Set([...state.selectionAtLastNonShiftClick, ...shiftSelection]);
  return {
    ...state,
    currentSelection: newSelection,
  }
}

This is pretty close to what I did but maybe I added piles on top of other piles and I could now possibly simplify this a bit. For example my code used to work in a quite stateless way initially, always recomputing the full range when shift was used and replacing the full selection, but then I added more state to control it. I like how you wrote that shift click reuse the previous selection, which builds on the assumption that the previous click either already replaced the previous selection or added to the previous selection, and I haven't thought of this idea before. So thanks!

@julienw

julienw commented Jul 27, 2022

Copy link
Copy Markdown
Contributor Author

OK, I know what my bug is, and what you propose (saving the selectionAtLastNonShiftClick) is indeed the right way to implement this. My code is always adding selections during this STR, while we want to replace what was added instead.
Thanks for the feedback!

@julienw julienw force-pushed the support-shift-click-in-timeline branch from 61d33ae to 3cc7a3c Compare July 29, 2022 15:39
invertCallstack,
selectedThreadIndexes,
} = this.props;
const modifiers = getTrackSelectionModifiers(event);

@julienw julienw Jul 29, 2022

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here it looks like it's a lot of changes, but if you look at the changes without the whitespace changes you'll see the code has just been reordered.

@julienw julienw force-pushed the support-shift-click-in-timeline branch 4 times, most recently from a92915d to 71f7278 Compare July 29, 2022 16:33
* information in the state. Because of all possible cases this isn't trivial.
* This will return null for tracks that are not selectable.
*/
function getInformationFromTrackReference(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This commit might be easier to follow with whitespace changes.

@julienw julienw requested a review from canova July 29, 2022 17:04
@julienw

julienw commented Jul 29, 2022

Copy link
Copy Markdown
Contributor Author

I reimplemented most of the patch following Markus' excellent suggestion (I wish I thought of that earlier :D), now I believe all mentioned use cases work as expected.

It should be easier to look commit by commit.

Comment on lines +691 to 703
let selectedTab =
clickedTrackInformation.relatedTab ?? getSelectedTab(getState());
const visibleTabs = getThreadSelectors(
clickedTrackInformation.relatedThreadIndex
).getUsefulTabs(getState());
if (!visibleTabs.includes(selectedTab)) {
// If the user switches to another track that doesn't have the current
// selectedTab then switch to the first tab.
selectedTab = visibleTabs[0];
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't sure what to do with the selectedTab, should we change it for all modes? Maybe we'd want to change it only for setOneTrackSelection... I thought it would be easy to change in a follow-up if needed.

@canova canova left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great, thanks for the feature and adding lots of tests! I've added a few nits mostly.

Comment thread src/utils/index.js Outdated
Comment thread src/actions/profile-view.js
Comment thread src/actions/profile-view.js Outdated
Comment thread src/actions/profile-view.js
Comment thread src/actions/profile-view.js
}
}

// Add the global track if it's not a virtual track and not out of the range.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By "virtual" you mean a global tack with a thread, right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"virtual track" means here a global track that is not a thread (such as what we get when importing sometimes).

But the comment says "add the global track if it's not virtual", maybe that's where you got confused?

Comment thread src/actions/profile-view.js
@julienw julienw force-pushed the support-shift-click-in-timeline branch from 71f7278 to 7caaf80 Compare August 3, 2022 14:01
@julienw

julienw commented Aug 3, 2022

Copy link
Copy Markdown
Contributor Author

Thanks so much for the review! I believe I handled all your comments and will merge now!

@julienw julienw enabled auto-merge August 3, 2022 14:05
@julienw julienw merged commit 192554a into firefox-devtools:main Aug 3, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement "shift click" behavior for selecting multiple tracks

3 participants