Node Editor: Move All Selected Nodes when dragging #63994

Closed
opened 2019-04-29 20:14:01 +02:00 by William Reynish · 43 comments

In the Node Editor, it should be possible to move more than just one node by dragging, if more are selected.
Just like files on a desktop, all selected items should move along when dragging on one of them.

In the Node Editor, it should be possible to move more than just one node by dragging, if more are selected. Just like files on a desktop, all selected items should move along when dragging on one of them.
Bastien Montagne was assigned by William Reynish 2019-04-29 20:14:01 +02:00

Added subscriber: @WilliamReynish

Added subscriber: @WilliamReynish

Added subscriber: @DuarteRamos

Added subscriber: @DuarteRamos

Added subscriber: @ideasman42

Added subscriber: @ideasman42

Problem here is that the 'click' PRESS event will always be caught before the TWEAK one, so selection will always happen before we start translation.

Only simple solution I can see so far is to trigger selection on RELEASE (or CLICK), not on PRESS (tried, seems to work fine, but gives an annoying delay between click and actual selection, especially on laptops with touchpad fancy 'click' handling :( ).

Other possibility, much more radical, would be to 'eat' the click event altogether when we detect a tweak one. Am quiet unsure that is even possible though, at the very least not without a noticeable delay for the user.

Need some thoughts, also probably inputs from @ideasman42 on that one.

Problem here is that the 'click' PRESS event will always be caught before the TWEAK one, so selection will always happen before we start translation. Only simple solution I can see so far is to trigger selection on RELEASE (or CLICK), not on PRESS (tried, seems to work fine, but gives an annoying delay between click and actual selection, especially on laptops with touchpad fancy 'click' handling :( ). Other possibility, much more radical, would be to 'eat' the click event altogether when we detect a tweak one. Am quiet unsure that is even possible though, at the very least not without a noticeable delay for the user. Need some thoughts, also probably inputs from @ideasman42 on that one.

Would be interested to first consider where else this logic would apply
(where LMB tweak is used too).

  • Mask points.
  • Paint 'Curve' stroke method.
  • (Possibly Movie-Clip editor in the future?)

If we want this to work as many file-selectors do, we'd need to do as @mont29 suggests and run this on click instead of press.

We could make it feel more responsive we could.

  • Press sets the node active, then either.
    • Click de-selects all others.
    • Tweak moves everything.

... Even this could back-fire (with undo steps and having a click sometimes performing two operations, and the first press event having to pass-through so the click event is registered).

All things considered we're probably best off making select happen on release (exactly like how a file manager works), because supporting press events which pass-through complicates event handling.


Other possibility, much more radical, would be to 'eat' the click event altogether when we detect a tweak one.

Not sure what's meant here., when a tweak event is registered a click event wont be sent.

Would be interested to first consider where else this logic would apply (where LMB tweak is used too). - Mask points. - Paint 'Curve' stroke method. - (Possibly Movie-Clip editor in the future?) ---- If we want this to work as many file-selectors do, we'd need to do as @mont29 suggests and run this on click instead of press. We could make it feel more responsive we could. - Press sets the node active, then either. - Click de-selects all others. - Tweak moves everything. ... Even this could back-fire (with undo steps and having a click sometimes performing two operations, and the first press event having to pass-through so the click event is registered). All things considered we're probably best off making select happen on release (exactly like how a file manager works), because supporting press events which pass-through complicates event handling. ---- > Other possibility, much more radical, would be to 'eat' the click event altogether when we detect a tweak one. Not sure what's meant here., when a tweak event is registered a click event wont be sent.

Testing how it works with something like document files in the Mac Finder:

file_selecting.mov

As you can see, when you click on an item that is already selected, it doesn't deselect other selections. This way they are still able to register the click event immediately (mousedown) and don't have to wait for the mouseup event.

Testing how it works with something like document files in the Mac Finder: [file_selecting.mov](https://archive.blender.org/developer/F6998417/file_selecting.mov) As you can see, when you click on an item that is already selected, it doesn't deselect other selections. This way they are still able to register the click event immediately (mousedown) and don't have to wait for the mouseup event.

@WilliamReynish it does not really help to compare a filebrowser with a node editor (and Blender in general), the later is much more complex and allows for many different actions…

I’d agree with @ideasman42 here then, if we want that behavior then we just select on CLICK (or RELEASE) event.

Would not go into handling two different kind of events, this would make code even more tricky to follow, and more fragile too. Unless maybe we had a specific 'set active' operator for the 'PRESS' event, that would not register any undo steps? Still sounds cumbersome for little gain… :/

@WilliamReynish it does not really help to compare a filebrowser with a node editor (and Blender in general), the later is much more complex and allows for many different actions… I’d agree with @ideasman42 here then, if we want that behavior then we just select on CLICK (or RELEASE) event. Would not go into handling two different kind of events, this would make code even more tricky to follow, and more fragile too. Unless maybe we had a specific 'set active' operator for the 'PRESS' event, that would not register any undo steps? Still sounds cumbersome for little gain… :/

Btw, just realized that in 3DView at least, we do select on click, and not on PRESS event…

Btw, just realized that in 3DView at least, we do select on click, and not on PRESS event…

Added subscriber: @brecht

Added subscriber: @brecht

It actually depends if you use left or right click select. On press it feels more responsive so we kept it for right click, but it's incompatible with tools on left click select.

I'm not sure how we can put select on click instead of press for the node editor though? You want to be able to press to select and drag on a node immediately to move it?

It actually depends if you use left or right click select. On press it feels more responsive so we kept it for right click, but it's incompatible with tools on left click select. I'm not sure how we can put select on click instead of press for the node editor though? You want to be able to press to select and drag on a node immediately to move it?

@mont29: Well, the issue is identical: How can we handle click input so that you can both select and drag the selected items, without having to use slower mouse-up events? It's the exact same thing as selecting and moving files on your desktop.

They solve it that way, by making it not deselect others if you click on an item that was already selected. The same solution could work in Blender too.

@mont29: Well, the issue is identical: How can we handle click input so that you can both select and drag the selected items, without having to use slower mouse-up events? It's the exact same thing as selecting and moving files on your desktop. They solve it that way, by making it not deselect others if you click on an item that was already selected. The same solution could work in Blender too.

@brecht You want to be able to press to select and drag on a node immediately to move it? that’s kind of incompatible with behavior described in that task? Either we select, and then move on drag (what happens currently), or we give priority to drag, and only select on CLICK (i.e. when there was no drag detected).

Or we try to implement what @ideasman42 described (some kind of two-steps selection, with PRESS adding to current selection, and then RELEASE removing all selected items and selecting again the active one). With all the PASSTHROUGH and double-undo history issues… yuck. :/

Btw, #63996 looks like a very similar can of worms.

@WilliamReynish Sure thing, all problems can be solved. But if we have to rethink/rewrite half of our event/operator handling code to support that, this is not a task for me, and this is not a task for 2.80. I (again) have the feeling that a tremendous amount of time and energy needs to be put to fix that kind of issues (and am sorry, but those still look pretty low-priority to me, things that are 'nice to have', but by no mean critical issues). Nice if we can fix them simply and quickly, but otherwise I would not really bother too much.

And as a reminder, I was not even involved in that left-click-select project/decision, not at all. So I do not really feel like spending too much of my time trying to fix the cans of worms it unveils. I have other priorities and projects to tackle too. For me, either we go with the cheap 'CLICK' to select things, or we live with current behavior.

@brecht *You want to be able to press to select and drag on a node immediately to move it?* that’s kind of incompatible with behavior described in that task? Either we select, and then move on drag (what happens currently), or we give priority to drag, and only select on CLICK (i.e. when there was no drag detected). Or we try to implement what @ideasman42 described (some kind of two-steps selection, with PRESS adding to current selection, and then RELEASE removing all selected items and selecting again the active one). With all the PASSTHROUGH and double-undo history issues… yuck. :/ Btw, #63996 looks like a very similar can of worms. @WilliamReynish Sure thing, all problems can be solved. But if we have to rethink/rewrite half of our event/operator handling code to support that, this is not a task for me, and this is not a task for 2.80. I (again) have the feeling that a tremendous amount of time and energy needs to be put to fix that kind of issues (and am sorry, but those still look pretty low-priority to me, things that are 'nice to have', but by no mean critical issues). Nice if we can fix them simply and quickly, but otherwise I would not really bother too much. And as a reminder, I was not even involved in that left-click-select project/decision, not at all. So I do not really feel like spending too much of my time trying to fix the cans of worms it unveils. I have other priorities and projects to tackle too. For me, either we go with the cheap 'CLICK' to select things, or we live with current behavior.
Member

Added subscriber: @Harley

Added subscriber: @Harley
Member

@WilliamReynish: ...without having to use slower mouse-up events? It's the exact same thing as selecting and moving files on your desktop...

The trick with our desktops: The timing of the deselection of other items changes depending on whether the new item is selected or not.

If the new item is not already selected then the other items are deselected on mouse-down. But if the item is already selected then peers are only unselected on mouse-up. This is what allows selecting and dragging singles or groups without having to wait for mouse up.

> @WilliamReynish: ...without having to use slower mouse-up events? It's the exact same thing as selecting and moving files on your desktop... The trick with our desktops: The timing of the deselection of other items changes depending on whether the new item is selected or not. If the new item is not already selected then the other items are deselected on mouse-down. But if the item is already selected then peers are only unselected on mouse-up. This is what allows selecting and dragging singles or groups without having to wait for mouse up.

Ok, sure, it may be a bigger project to make selection more fundamentally different. I'm fine with any solution that works, even if it means using mouse-up events, if it makes it possible to move multiple nodes easily by simply dragging on your selection.

Ok, sure, it may be a bigger project to make selection more fundamentally different. I'm fine with any solution that works, even if it means using mouse-up events, if it makes it possible to move multiple nodes easily by simply dragging on your selection.

I think the way to solve it is actually to make changes to the select operator, so that clicking on selected items doesn't deselect others. Moving all selected items already works, so the issue is only that the select operator is first deselecting everything each time you click and drag.

I think the way to solve it is actually to make changes to the select operator, so that clicking on selected items doesn't deselect others. Moving all selected items already works, so the issue is only that the select operator is first deselecting everything each time you click and drag.

It still needs to deselect others when releasing the mouse button, if you haven't started dragging already. One way of doing that would be to make the select operator a modal operator that finishes as soon as you release the mouse button (deselecting others), or a tweak event is started (doing nothing).

To me this seems possible in a way that's isolated to the select operator and not interfering with other operators that need to run right after. But I'm not sure how much work it would be.

It still needs to deselect others when releasing the mouse button, if you haven't started dragging already. One way of doing that would be to make the select operator a modal operator that finishes as soon as you release the mouse button (deselecting others), or a tweak event is started (doing nothing). To me this seems possible in a way that's isolated to the select operator and not interfering with other operators that need to run right after. But I'm not sure how much work it would be.

@brecht Yes, that would be idea' - that it deselects others on mouse-up if the user didn't drag.

But IMO it would also be acceptable to do it in a simpler way so it just doesn't deselect others if you click on a selected item. Then you would have to click away to deselect first, or use the deselect operator. Not as nice, but eg the macOS Finder works this way and I never noticed it as being an issue.

@brecht Yes, that would be idea' - that it deselects others on mouse-up if the user didn't drag. But IMO it would also be acceptable to do it in a simpler way so it just doesn't deselect others if you click on a selected item. Then you would have to click away to deselect first, or use the deselect operator. Not as nice, but eg the macOS Finder works this way and I never noticed it as being an issue.

In #63994#670458, @brecht wrote:
One way of doing that would be to make the select operator a modal operator that finishes as soon as you release the mouse button (deselecting others), or a tweak event is started (doing nothing).

To me this seems possible in a way that's isolated to the select operator and not interfering with other operators that need to run right after. But I'm not sure how much work it would be.

Whats the advantage of this compared to having click for select, tweak for border select?

Both use the same threshold so it shouldn't be possible to move the cursor so it's moved too much for a click but not enough for a drag.

> In #63994#670458, @brecht wrote: > One way of doing that would be to make the select operator a modal operator that finishes as soon as you release the mouse button (deselecting others), or a tweak event is started (doing nothing). > > To me this seems possible in a way that's isolated to the select operator and not interfering with other operators that need to run right after. But I'm not sure how much work it would be. Whats the advantage of this compared to having click for select, tweak for border select? Both use the same threshold so it shouldn't be possible to move the cursor so it's moved too much for a click but not enough for a drag.

Whats the advantage of this compared to having click for select, tweak for border select?
Both use the same threshold so it shouldn't be possible to move the cursor so it's moved too much for a click but not enough for a drag.

The advantage is that you select and transform the node immediately. Otherwise you would have to click once to select, and then click again to transform the node.

> Whats the advantage of this compared to having click for select, tweak for border select? > Both use the same threshold so it shouldn't be possible to move the cursor so it's moved too much for a click but not enough for a drag. The advantage is that you select and transform the node immediately. Otherwise you would have to click once to select, and then click again to transform the node.

P966 Is an attempt to implement @brecht's suggestion, seems to work OK-ish.

Note that it relies on press/release events being the two triggers, think we can accept that hard-coded behavior here?

I remain pretty unhappy about that hack though. To have to add such complexity to a simple operator, just to work around (imho very bad) design decision to merge action and select buttons together… :(

[P966](https://archive.blender.org/developer/P966.txt) Is an attempt to implement @brecht's suggestion, seems to work OK-ish. Note that it relies on press/release events being the two triggers, think we can accept that hard-coded behavior here? I remain pretty unhappy about that hack though. To have to add such complexity to a simple operator, just to work around (imho very bad) design decision to merge action and select buttons together… :(

@mont29 Maybe I should have asked in here and not in the diff snippet:

This seems to work well!

Question: How generic is it? Could we re-use same logic for sequencer strips or NLA clips etc?

@mont29 Maybe I should have asked in here and not in the diff snippet: This seems to work well! Question: How generic is it? Could we re-use same logic for sequencer strips or NLA clips etc?

Lol… copy-pasting my answer from P966 here then. :P

Copy-pasting similar logic should work, yes, at least for similar selection (like in most 2D editors), would expect some complications if we tried to do so with e.g. 3DView selection (also we might have to do similar things there, cf. can of worms II, #63996). This is not a small change though, as pretty much all select ops do things a bit differently. Kind of like what I did for the 'deselect on nothing' change, but several times bigger every time…

My concern with that change is that it adds a whole other level of complexity over selection logic, which already has to deal with extend, select_all, etc. It also makes behavior in different kind of scenarii more complicated to predict (like, what if user adds a keymap using a totally different shortcut to select nodes? What about macros [e.g. just realized that current patch breaks the 'link to viewer node' macro, the one using sockets selection]? etc. etc.). That code can probably be refactored further to make it more easy to follow, but still… am not so happy with it. This adds technical debt for a bad reason imho. :|

Lol… copy-pasting my answer from [P966](https://archive.blender.org/developer/P966.txt) here then. :P Copy-pasting similar logic should work, yes, at least for similar selection (like in most 2D editors), would expect some complications if we tried to do so with e.g. 3DView selection (also we might have to do similar things there, cf. can of worms II, #63996). This is not a small change though, as pretty much all select ops do things a bit differently. Kind of like what I did for the 'deselect on nothing' change, but several times bigger every time… My concern with that change is that it adds a whole other level of complexity over selection logic, which already has to deal with extend, select_all, etc. It also makes behavior in different kind of scenarii more complicated to predict (like, what if user adds a keymap using a totally different shortcut to select nodes? What about macros [e.g. just realized that current patch breaks the 'link to viewer node' macro, the one using sockets selection]? etc. etc.). That code can probably be refactored further to make it more easy to follow, but still… am not so happy with it. This adds technical debt for a bad reason imho. :|

@mont29

Well, obviously I don't agree with*"for a bad reason"*. So many users are already finding Blender 2.8 much easier to use, and the ability to select and manipulate things more easily is a core reason why.

As for code complexity, I guess ideally we could somehow share this logic so it's not duplicated several places. We could also just start by adding this to the Node Editor only for 2.80, and then later we could refactor and generalize the logic to make it easier to re-use in other places.

@mont29 Well, obviously I don't agree with*"for a bad reason"*. So many users are already finding Blender 2.8 much easier to use, and the ability to select and manipulate things more easily is a core reason why. As for code complexity, I guess ideally we could somehow share this logic so it's not duplicated several places. We could also just start by adding this to the Node Editor only for 2.80, and then later we could refactor and generalize the logic to make it easier to re-use in other places.

Added subscriber: @BartekMoniewski

Added subscriber: @BartekMoniewski

Added subscriber: @SpectralVectors

Added subscriber: @SpectralVectors

Hello, I'm new here, but I wonder if this might be a helpful way to approach this:

Since dragging multiple nodes works as expected when they are contained within a frame, would it be possible to create an invisible frame around all selected nodes on mouse press, tweak to move it, then mouse release destroys the frame? (which the user never saw to begin with)

I did an experiment and replaced the Join Nodes - Keymapping (Alt+J by default, I think, I'm using my own keymap) with a Left Click, and while it didn't work if you selected a bunch of nodes and then clicked on one of them to drag, it DID work when you selected a bunch of nodes and clicked anywhere OTHER than a node.

So any misclicks ended up creating empty frames, perhaps that could be fixed with a check on mouse press, but that might then be back in the problem of adding latency.

Or, if they're invisible, creating a million nodes in the background that you never see.

Then again, maybe you could map a function that deletes all empty frames in addition to the select box frame on mouse release?

Just a thought.

Hello, I'm new here, but I wonder if this might be a helpful way to approach this: Since dragging multiple nodes works as expected when they are contained within a frame, would it be possible to create an invisible frame around all selected nodes on mouse press, tweak to move it, then mouse release destroys the frame? (which the user never saw to begin with) I did an experiment and replaced the Join Nodes - Keymapping (Alt+J by default, I think, I'm using my own keymap) with a Left Click, and while it didn't work if you selected a bunch of nodes and then clicked on one of them to drag, it DID work when you selected a bunch of nodes and clicked anywhere OTHER than a node. So any misclicks ended up creating empty frames, perhaps that could be fixed with a check on mouse press, but that might then be back in the problem of adding latency. Or, if they're invisible, creating a million nodes in the background that you never see. Then again, maybe you could map a function that deletes all empty frames in addition to the select box frame on mouse release? Just a thought.

@SpectralVectors That sounds even more complicated than the modal selection op solution?

@brecht @ideasman42 We should decide now what we do here. Afaics, we have two options:

  • Select on RELEASE, which solves present issue, is simple and safe, but makes selection experience a tad less nice for user, and prevents the 'click-drag' behavior to click on a node and move it in a single action.
  • Modal selection performing in two steps, which does everything user might expect, but makes selection code much more hairy than it already is (and hence prone to bugs).

I am a bit torn here, for me solution 1 is far better from a safety/'code quality'/maintainability point of view, while solution 2 is ideal from user point of view.

If we decide to take the risk of modal op solution, I would then like to do it as a separate operator, explicitly only modal, and explicitly not handling all extra cases (like extend selection, selection of sockets, etc.). That should keep code complexity to a more reasonable/maintainable level.

@SpectralVectors That sounds even more complicated than the modal selection op solution? @brecht @ideasman42 We should decide now what we do here. Afaics, we have two options: - Select on `RELEASE`, which solves present issue, is simple and safe, but makes selection experience a tad less nice for user, and prevents the 'click-drag' behavior to click on a node and move it in a single action. - Modal selection performing in two steps, which does everything user might expect, but makes selection code much more hairy than it already is (and hence prone to bugs). I am a bit torn here, for me solution 1 is far better from a safety/'code quality'/maintainability point of view, while solution 2 is ideal from user point of view. If we decide to take the risk of modal op solution, I would then like to do it as a separate operator, explicitly only modal, and explicitly **not** handling all extra cases (like extend selection, selection of sockets, etc.). That should keep code complexity to a more reasonable/maintainable level.

I think we should do 2), it's a nice improvement.

But there are some issues with the implementation that I found:

  • When the node is not initially selected, then dragging on it should not drag all other nodes. It should only do that when the node is already selected. File browsers work similar, and it keeps tweaking individual node positions quick (which I do much more often than moving multiple nodes). In this case the operator could finish immediately and not start the running modal.
  • When clicking and dragging a small distance (by accident), it seems to not deselect other nodes. The threshold should be the same as starting the transform. It may help to listen for EVT_TWEAK_L/M/R events instead of doing our own distance calculation with mouse events.

I'm not entirely sure making it a separate operator will reduce code complexity? We would have to see how that works out, but I fear it would complicate the keymaps. I do think that the socket selection should be handled separately, that could be in its own function, and if a socket is found it can skip running modal and finish immediately.

For the implementation, I guess storing active_item is not actually needed? As far as I can tell it's only used for asserts, so removing it could simplify the implementation.

I think we should do 2), it's a nice improvement. But there are some issues with the implementation that I found: * When the node is not initially selected, then dragging on it should not drag all other nodes. It should only do that when the node is already selected. File browsers work similar, and it keeps tweaking individual node positions quick (which I do much more often than moving multiple nodes). In this case the operator could finish immediately and not start the running modal. * When clicking and dragging a small distance (by accident), it seems to not deselect other nodes. The threshold should be the same as starting the transform. It may help to listen for `EVT_TWEAK_L/M/R` events instead of doing our own distance calculation with mouse events. I'm not entirely sure making it a separate operator will reduce code complexity? We would have to see how that works out, but I fear it would complicate the keymaps. I do think that the socket selection should be handled separately, that could be in its own function, and if a socket is found it can skip running modal and finish immediately. For the implementation, I guess storing `active_item` is not actually needed? As far as I can tell it's only used for asserts, so removing it could simplify the implementation.

In #63994#676045, @brecht wrote:
I think we should do 2), it's a nice improvement.

OK… Still not totally happy with it, but am happier with new version of patch (P966), so guess it’s OK. ;)

But there are some issues with the implementation that I found:

  • When the node is not initially selected, then dragging on it should not drag all other nodes. It should only do that when the node is already selected. File browsers work similar, and it keeps tweaking individual node positions quick (which I do much more often than moving multiple nodes). In this case the operator could finish immediately and not start the running modal.

Good point, fixed in new patch.

  • When clicking and dragging a small distance (by accident), it seems to not deselect other nodes. The threshold should be the same as starting the transform. It may help to listen for EVT_TWEAK_L/M/R events instead of doing our own distance calculation with mouse events.

Threshold is the same as starting tweak (see WM_EVENT_CLICK_TWEAK_THRESHOLD in wm_event_system.c). Also, EVT_TWEAK_L/M/R never reach our modal handler, not sure why (would assume WM event code does not always trigger them?). But previous code was buggy (using wrong coordinate values), new version of patch seems to work as expected here as well.

I'm not entirely sure making it a separate operator will reduce code complexity? We would have to see how that works out, but I fear it would complicate the keymaps. I do think that the socket selection should be handled separately, that could be in its own function, and if a socket is found it can skip running modal and finish immediately.

For the implementation, I guess storing active_item is not actually needed? As far as I can tell it's only used for asserts, so removing it could simplify the implementation.

Good points, new patch is much more satisfying on simplicity side imho (also made it more strict to only accept regular selection in modal 'two steps' mode, other options just get always executed in basic exec mode.

P966: (An Untitled Masterwork)

diff --git a/source/blender/editors/space_node/node_select.c b/source/blender/editors/space_node/node_select.c
index 07b01e576bc..681bf693246 100644
--- a/source/blender/editors/space_node/node_select.c
+++ b/source/blender/editors/space_node/node_select.c
@@ -435,13 +435,21 @@ static int node_mouse_select(Main *bmain,
                              const int mval[2],
                              const bool extend,
                              const bool socket_select,
-                             const bool deselect_all)
+                             const bool deselect_all,
+                             const bool do_first_step,
+                             const bool do_second_step)
 {
   bNode *node, *tnode;
   bNodeSocket *sock = NULL;
   bNodeSocket *tsock;
   float cursor[2];
-  bool selected = false;
+  int ret_value = OPERATOR_CANCELLED;
+
+  /* Handle two-steps selection (with deselection of already selected items as second step). */
+  const bool use_two_steps = do_first_step || do_second_step;
+
+  /* Two-steps modal selection is only allowed for basic selection. */
+  BLI_assert(!(extend || socket_select) || !use_two_steps);
 
   /* get mouse coordinates in view2d space */
   UI_view2d_region_to_view(&ar->v2d, mval- [x], mval- [x], &cursor- [x], &cursor[1]);
@@ -452,7 +460,7 @@ static int node_mouse_select(Main *bmain,
       /* NOTE: SOCK_IN does not take into account the extend case...
        * This feature is not really used anyway currently? */
       node_socket_toggle(node, sock, true);
-      selected = true;
+      ret_value = OPERATOR_FINISHED;
     }
     else if (node_find_indicated_socket(snode, &node, &sock, cursor, SOCK_OUT)) {
       if (sock->flag & SELECT) {
@@ -460,7 +468,7 @@ static int node_mouse_select(Main *bmain,
           node_socket_deselect(node, sock, true);
         }
         else {
-          selected = true;
+          ret_value = OPERATOR_FINISHED;
         }
       }
       else {
@@ -485,7 +493,7 @@ static int node_mouse_select(Main *bmain,
           }
         }
         node_socket_select(node, sock);
-        selected = true;
+        ret_value = OPERATOR_FINISHED;
       }
     }
   }
@@ -501,24 +509,43 @@ static int node_mouse_select(Main *bmain,
         if (!((node->flag & SELECT) && (node->flag & NODE_ACTIVE) == 0)) {
           node_toggle(node);
         }
-        selected = true;
+        ret_value = OPERATOR_FINISHED;
       }
     }
     else {
       if (node != NULL || deselect_all) {
-        for (tnode = snode->edittree->nodes.first; tnode; tnode = tnode->next) {
-          nodeSetSelected(tnode, false);
+        /* We want to deselect all other nodes in three cases:
+         *  - If 'deselect on nothing' is enabled, and we clicked on nothing.
+         *  - If we are in single-step selection, or in second step of two-steps selection.
+         *  - If we are in first step of two-steps selection, and node on which we clicked
+         *    was not selected (that way, click-drag on non-selected node will only move that one,
+         *    not also all other previously selected nodes).
+         */
+        if ((deselect_all && node == NULL) || (!use_two_steps || do_second_step) ||
+            (do_first_step && node != NULL && (node->flag & SELECT) == 0)) {
+          for (tnode = snode->edittree->nodes.first; tnode; tnode = tnode->next) {
+            if (tnode == node) {
+              continue;
+            }
+            nodeSetSelected(tnode, false);
+          }
+          if (node != NULL) {
+            nodeSetSelected(node, true);
+          }
+          /* In case we also do de-selection of previously selected nodes, we are done,
+           * no need to keep modal running. */
+          ret_value = OPERATOR_FINISHED;
         }
-        selected = true;
-        if (node != NULL) {
+        else if (node != NULL) {
           nodeSetSelected(node, true);
+          ret_value = do_first_step ? OPERATOR_RUNNING_MODAL : OPERATOR_FINISHED;
         }
       }
     }
   }
 
   /* update node order */
-  if (selected) {
+  if (ret_value != OPERATOR_CANCELLED) {
     if (node != NULL) {
       ED_node_set_active(bmain, snode->edittree, node);
     }
@@ -526,7 +553,7 @@ static int node_mouse_select(Main *bmain,
     ED_node_sort(snode->edittree);
   }
 
-  return selected;
+  return ret_value;
 }
 
 static int node_select_exec(bContext *C, wmOperator *op)
@@ -546,17 +573,90 @@ static int node_select_exec(bContext *C, wmOperator *op)
   const bool deselect_all = RNA_boolean_get(op->ptr, "deselect_all");
 
   /* perform the select */
-  if (node_mouse_select(bmain, snode, ar, mval, extend, socket_select, deselect_all)) {
+  const int ret_value = node_mouse_select(
+      bmain, snode, ar, mval, extend, socket_select, deselect_all, false, false);
+  if (ret_value & OPERATOR_FINISHED) {
     /* send notifiers */
     WM_event_add_notifier(C, NC_NODE | NA_SELECTED, NULL);
+  }
+  /* allow tweak event to work too */
+  return ret_value | OPERATOR_PASS_THROUGH;
+}
 
-    /* allow tweak event to work too */
-    return OPERATOR_FINISHED | OPERATOR_PASS_THROUGH;
+static int node_select_modal(bContext *C, wmOperator *op, const wmEvent *event)
+{
+  Main *bmain = CTX_data_main(C);
+  SpaceNode *snode = CTX_wm_space_node(C);
+  ARegion *ar = CTX_wm_region(C);
+
+  const short init_event_type = (short)POINTER_AS_INT(op->customdata);
+
+  /* get settings from RNA properties for operator */
+  int mval[2];
+  mval- [x] = RNA_int_get(op->ptr, "mouse_x");
+  mval- [x] = RNA_int_get(op->ptr, "mouse_y");
+
+  const bool extend = RNA_boolean_get(op->ptr, "extend");
+  /* always do socket_select when extending selection. */
+  const bool socket_select = extend || RNA_boolean_get(op->ptr, "socket_select");
+  const bool deselect_all = RNA_boolean_get(op->ptr, "deselect_all");
+
+  /* These cases are never modal. */
+  if (extend || socket_select) {
+    return node_select_exec(C, op);
   }
-  else {
-    /* allow tweak event to work too */
-    return OPERATOR_CANCELLED | OPERATOR_PASS_THROUGH;
+
+  if (init_event_type == 0) {
+    if (event->val == KM_PRESS) {
+      const int ret_value = node_mouse_select(
+          bmain, snode, ar, mval, extend, socket_select, deselect_all, true, false);
+
+      op->customdata = POINTER_FROM_INT((int)event->type);
+      if (ret_value & OPERATOR_RUNNING_MODAL) {
+        WM_event_add_modal_handler(C, op);
+      }
+      if (ret_value & (OPERATOR_RUNNING_MODAL | OPERATOR_FINISHED)) {
+        /* send notifiers */
+        WM_event_add_notifier(C, NC_NODE | NA_SELECTED, NULL);
+      }
+      return ret_value | OPERATOR_PASS_THROUGH;
+    }
+    else {
+      /* If we are in init phase, and cannot validate init of modal operations,
+       * just fall back to basic exec.
+       */
+      return node_select_exec(C, op);
+    }
+  }
+  else if (event->type == init_event_type && event->val == KM_RELEASE) {
+    const int ret_value = node_mouse_select(
+        bmain, snode, ar, mval, extend, socket_select, deselect_all, false, true);
+
+    if (ret_value & (OPERATOR_RUNNING_MODAL | OPERATOR_FINISHED)) {
+      /* send notifiers */
+      WM_event_add_notifier(C, NC_NODE | NA_SELECTED, NULL);
+    }
+
+    return ret_value | OPERATOR_PASS_THROUGH;
   }
+  else if (ELEM(event->type, MOUSEMOVE, INBETWEEN_MOUSEMOVE)) {
+    const int dx = mval- [x] - event->mval[0];
+    const int dy = mval- [x] - event->mval[1];
+    const float tweak_threshold = U.tweak_threshold * U.dpi_fac;
+    /* If user moves mouse more than defined threshold, we consider select operator as
+     * finished. Otherwise, it is still running until we get an 'release' event. In any
+     * case, we pass through event, but select op is not finished yet. */
+    if (abs(dx) + abs(dy) > tweak_threshold) {
+      return OPERATOR_FINISHED | OPERATOR_PASS_THROUGH;
+    }
+    else {
+      /* Important not to return anything other than PASS_THROUGH here,
+       * otherwise it prevents underlying tweak detection code to work properly. */
+      return OPERATOR_PASS_THROUGH;
+    }
+  }
+
+  return OPERATOR_FINISHED | OPERATOR_PASS_THROUGH;
 }
 
 static int node_select_invoke(bContext *C, wmOperator *op, const wmEvent *event)
@@ -564,7 +664,9 @@ static int node_select_invoke(bContext *C, wmOperator *op, const wmEvent *event)
   RNA_int_set(op->ptr, "mouse_x", event->mval[0]);
   RNA_int_set(op->ptr, "mouse_y", event->mval[1]);
 
-  return node_select_exec(C, op);
+  op->customdata = POINTER_FROM_INT(0);
+
+  return node_select_modal(C, op, event);
 }
 
 void NODE_OT_select(wmOperatorType *ot)
@@ -577,6 +679,7 @@ void NODE_OT_select(wmOperatorType *ot)
   /* api callbacks */
   ot->invoke = node_select_invoke;
   ot->exec = node_select_exec;
+  ot->modal = node_select_modal;
   ot->poll = ED_operator_node_active;
 
   /* flags */

> In #63994#676045, @brecht wrote: > I think we should do 2), it's a nice improvement. OK… Still not totally happy with it, but am happier with new version of patch ([P966](https://archive.blender.org/developer/P966.txt)), so guess it’s OK. ;) > But there are some issues with the implementation that I found: > * When the node is not initially selected, then dragging on it should not drag all other nodes. It should only do that when the node is already selected. File browsers work similar, and it keeps tweaking individual node positions quick (which I do much more often than moving multiple nodes). In this case the operator could finish immediately and not start the running modal. Good point, fixed in new patch. > * When clicking and dragging a small distance (by accident), it seems to not deselect other nodes. The threshold should be the same as starting the transform. It may help to listen for `EVT_TWEAK_L/M/R` events instead of doing our own distance calculation with mouse events. Threshold is the same as starting tweak (see `WM_EVENT_CLICK_TWEAK_THRESHOLD` in `wm_event_system.c`). Also, `EVT_TWEAK_L/M/R` never reach our modal handler, not sure why (would assume WM event code does not always trigger them?). But previous code was buggy (using wrong coordinate values), new version of patch seems to work as expected here as well. > I'm not entirely sure making it a separate operator will reduce code complexity? We would have to see how that works out, but I fear it would complicate the keymaps. I do think that the socket selection should be handled separately, that could be in its own function, and if a socket is found it can skip running modal and finish immediately. > > For the implementation, I guess storing `active_item` is not actually needed? As far as I can tell it's only used for asserts, so removing it could simplify the implementation. Good points, new patch is much more satisfying on simplicity side imho (also made it more strict to only accept regular selection in modal 'two steps' mode, other options just get always executed in basic `exec` mode. [P966: (An Untitled Masterwork)](https://archive.blender.org/developer/P966.txt) ``` diff --git a/source/blender/editors/space_node/node_select.c b/source/blender/editors/space_node/node_select.c index 07b01e576bc..681bf693246 100644 --- a/source/blender/editors/space_node/node_select.c +++ b/source/blender/editors/space_node/node_select.c @@ -435,13 +435,21 @@ static int node_mouse_select(Main *bmain, const int mval[2], const bool extend, const bool socket_select, - const bool deselect_all) + const bool deselect_all, + const bool do_first_step, + const bool do_second_step) { bNode *node, *tnode; bNodeSocket *sock = NULL; bNodeSocket *tsock; float cursor[2]; - bool selected = false; + int ret_value = OPERATOR_CANCELLED; + + /* Handle two-steps selection (with deselection of already selected items as second step). */ + const bool use_two_steps = do_first_step || do_second_step; + + /* Two-steps modal selection is only allowed for basic selection. */ + BLI_assert(!(extend || socket_select) || !use_two_steps); /* get mouse coordinates in view2d space */ UI_view2d_region_to_view(&ar->v2d, mval- [x], mval- [x], &cursor- [x], &cursor[1]); @@ -452,7 +460,7 @@ static int node_mouse_select(Main *bmain, /* NOTE: SOCK_IN does not take into account the extend case... * This feature is not really used anyway currently? */ node_socket_toggle(node, sock, true); - selected = true; + ret_value = OPERATOR_FINISHED; } else if (node_find_indicated_socket(snode, &node, &sock, cursor, SOCK_OUT)) { if (sock->flag & SELECT) { @@ -460,7 +468,7 @@ static int node_mouse_select(Main *bmain, node_socket_deselect(node, sock, true); } else { - selected = true; + ret_value = OPERATOR_FINISHED; } } else { @@ -485,7 +493,7 @@ static int node_mouse_select(Main *bmain, } } node_socket_select(node, sock); - selected = true; + ret_value = OPERATOR_FINISHED; } } } @@ -501,24 +509,43 @@ static int node_mouse_select(Main *bmain, if (!((node->flag & SELECT) && (node->flag & NODE_ACTIVE) == 0)) { node_toggle(node); } - selected = true; + ret_value = OPERATOR_FINISHED; } } else { if (node != NULL || deselect_all) { - for (tnode = snode->edittree->nodes.first; tnode; tnode = tnode->next) { - nodeSetSelected(tnode, false); + /* We want to deselect all other nodes in three cases: + * - If 'deselect on nothing' is enabled, and we clicked on nothing. + * - If we are in single-step selection, or in second step of two-steps selection. + * - If we are in first step of two-steps selection, and node on which we clicked + * was not selected (that way, click-drag on non-selected node will only move that one, + * not also all other previously selected nodes). + */ + if ((deselect_all && node == NULL) || (!use_two_steps || do_second_step) || + (do_first_step && node != NULL && (node->flag & SELECT) == 0)) { + for (tnode = snode->edittree->nodes.first; tnode; tnode = tnode->next) { + if (tnode == node) { + continue; + } + nodeSetSelected(tnode, false); + } + if (node != NULL) { + nodeSetSelected(node, true); + } + /* In case we also do de-selection of previously selected nodes, we are done, + * no need to keep modal running. */ + ret_value = OPERATOR_FINISHED; } - selected = true; - if (node != NULL) { + else if (node != NULL) { nodeSetSelected(node, true); + ret_value = do_first_step ? OPERATOR_RUNNING_MODAL : OPERATOR_FINISHED; } } } } /* update node order */ - if (selected) { + if (ret_value != OPERATOR_CANCELLED) { if (node != NULL) { ED_node_set_active(bmain, snode->edittree, node); } @@ -526,7 +553,7 @@ static int node_mouse_select(Main *bmain, ED_node_sort(snode->edittree); } - return selected; + return ret_value; } static int node_select_exec(bContext *C, wmOperator *op) @@ -546,17 +573,90 @@ static int node_select_exec(bContext *C, wmOperator *op) const bool deselect_all = RNA_boolean_get(op->ptr, "deselect_all"); /* perform the select */ - if (node_mouse_select(bmain, snode, ar, mval, extend, socket_select, deselect_all)) { + const int ret_value = node_mouse_select( + bmain, snode, ar, mval, extend, socket_select, deselect_all, false, false); + if (ret_value & OPERATOR_FINISHED) { /* send notifiers */ WM_event_add_notifier(C, NC_NODE | NA_SELECTED, NULL); + } + /* allow tweak event to work too */ + return ret_value | OPERATOR_PASS_THROUGH; +} - /* allow tweak event to work too */ - return OPERATOR_FINISHED | OPERATOR_PASS_THROUGH; +static int node_select_modal(bContext *C, wmOperator *op, const wmEvent *event) +{ + Main *bmain = CTX_data_main(C); + SpaceNode *snode = CTX_wm_space_node(C); + ARegion *ar = CTX_wm_region(C); + + const short init_event_type = (short)POINTER_AS_INT(op->customdata); + + /* get settings from RNA properties for operator */ + int mval[2]; + mval- [x] = RNA_int_get(op->ptr, "mouse_x"); + mval- [x] = RNA_int_get(op->ptr, "mouse_y"); + + const bool extend = RNA_boolean_get(op->ptr, "extend"); + /* always do socket_select when extending selection. */ + const bool socket_select = extend || RNA_boolean_get(op->ptr, "socket_select"); + const bool deselect_all = RNA_boolean_get(op->ptr, "deselect_all"); + + /* These cases are never modal. */ + if (extend || socket_select) { + return node_select_exec(C, op); } - else { - /* allow tweak event to work too */ - return OPERATOR_CANCELLED | OPERATOR_PASS_THROUGH; + + if (init_event_type == 0) { + if (event->val == KM_PRESS) { + const int ret_value = node_mouse_select( + bmain, snode, ar, mval, extend, socket_select, deselect_all, true, false); + + op->customdata = POINTER_FROM_INT((int)event->type); + if (ret_value & OPERATOR_RUNNING_MODAL) { + WM_event_add_modal_handler(C, op); + } + if (ret_value & (OPERATOR_RUNNING_MODAL | OPERATOR_FINISHED)) { + /* send notifiers */ + WM_event_add_notifier(C, NC_NODE | NA_SELECTED, NULL); + } + return ret_value | OPERATOR_PASS_THROUGH; + } + else { + /* If we are in init phase, and cannot validate init of modal operations, + * just fall back to basic exec. + */ + return node_select_exec(C, op); + } + } + else if (event->type == init_event_type && event->val == KM_RELEASE) { + const int ret_value = node_mouse_select( + bmain, snode, ar, mval, extend, socket_select, deselect_all, false, true); + + if (ret_value & (OPERATOR_RUNNING_MODAL | OPERATOR_FINISHED)) { + /* send notifiers */ + WM_event_add_notifier(C, NC_NODE | NA_SELECTED, NULL); + } + + return ret_value | OPERATOR_PASS_THROUGH; } + else if (ELEM(event->type, MOUSEMOVE, INBETWEEN_MOUSEMOVE)) { + const int dx = mval- [x] - event->mval[0]; + const int dy = mval- [x] - event->mval[1]; + const float tweak_threshold = U.tweak_threshold * U.dpi_fac; + /* If user moves mouse more than defined threshold, we consider select operator as + * finished. Otherwise, it is still running until we get an 'release' event. In any + * case, we pass through event, but select op is not finished yet. */ + if (abs(dx) + abs(dy) > tweak_threshold) { + return OPERATOR_FINISHED | OPERATOR_PASS_THROUGH; + } + else { + /* Important not to return anything other than PASS_THROUGH here, + * otherwise it prevents underlying tweak detection code to work properly. */ + return OPERATOR_PASS_THROUGH; + } + } + + return OPERATOR_FINISHED | OPERATOR_PASS_THROUGH; } static int node_select_invoke(bContext *C, wmOperator *op, const wmEvent *event) @@ -564,7 +664,9 @@ static int node_select_invoke(bContext *C, wmOperator *op, const wmEvent *event) RNA_int_set(op->ptr, "mouse_x", event->mval[0]); RNA_int_set(op->ptr, "mouse_y", event->mval[1]); - return node_select_exec(C, op); + op->customdata = POINTER_FROM_INT(0); + + return node_select_modal(C, op, event); } void NODE_OT_select(wmOperatorType *ot) @@ -577,6 +679,7 @@ void NODE_OT_select(wmOperatorType *ot) /* api callbacks */ ot->invoke = node_select_invoke; ot->exec = node_select_exec; + ot->modal = node_select_modal; ot->poll = ED_operator_node_active; /* flags */ ```

I think the patch works very well.

The code can be simplified further:
node-move.patch

Seems ok to commit.

I think the patch works very well. The code can be simplified further: [node-move.patch](https://archive.blender.org/developer/F7036358/node-move.patch) Seems ok to commit.

Ah yes, nice improvements, thanks.

Ah yes, nice improvements, thanks.

This issue was referenced by af088c2640

This issue was referenced by af088c26409503a53fdc0182038faa6574cfc29f

Changed status from 'Open' to: 'Resolved'

Changed status from 'Open' to: 'Resolved'
Member

Added subscriber: @JulianEisel

Added subscriber: @JulianEisel
Member

Digged into this today for #57918, but wondered if we wouldn't simplify this a lot by doing things the other way around:

  • Rather than making the select operators modal and letting them pass events through to the transform operator, make the transform operator invoke immediately, but letting it early exit and pass through events to the select operators.
  • If a tweak event is detected, the transform operator starts applying its changes and swallows all further events
  • If a release event is detected prior to a tweak one, transform is cancelled
  • Call selection once on mouse press event. If the click is on an unselected item, select it and deselect others, otherwise, do nothing.
  • On mouse release, call select operator again (or a different operator), letting it deselect items not under the cursor - note that this point is not reached if a tweak event triggered proper transform

This seemed almost too simple to be true, so I gave it a go. And: It appears to work just fine, with little changes in total, and the select operator changes are tiny.

P1121 adds drag-all-selected support to the sequencer. Note how few changes were needed in the select operator.

There a some small glitches to fix still (e.g. cursor changing on select), and we may dig deeper to make sure the transform code stays in valid states. But it seems like a very promising direction.
Am I missing something?

Digged into this today for #57918, but wondered if we wouldn't simplify this a lot by doing things the other way around: * Rather than making the select operators modal and letting them pass events through to the transform operator, make the transform operator invoke immediately, but letting it early exit and pass through events to the select operators. * If a tweak event is detected, the transform operator starts applying its changes and swallows all further events * If a release event is detected prior to a tweak one, transform is cancelled * Call selection once on mouse press event. If the click is on an unselected item, select it and deselect others, otherwise, do nothing. * On mouse release, call select operator again (or a different operator), letting it deselect items not under the cursor - note that this point is not reached if a tweak event triggered proper transform This seemed almost too simple to be true, so I gave it a go. And: It appears to work just fine, with little changes in total, and the select operator changes are tiny. [P1121](https://archive.blender.org/developer/P1121.txt) adds drag-all-selected support to the sequencer. Note how few changes were needed in the select operator. There a some small glitches to fix still (e.g. cursor changing on select), and we may dig deeper to make sure the transform code stays in valid states. But it seems like a very promising direction. Am I missing something?

A problem in this patch is that the select operator is registered twice and there are two undo pushes.

Ideally we would also not complicate the keymap with this, since editing keymaps is already complicated enough and this adds more obscure properties. The way it works in the node editor requires no keymap changes,

It also now actually starts the transform whenever you click to select. This shows up in the status bar, is potentially slow or might report some kind of warning. In the sequence editor, it's now clearing the sequencer cache when just selecting strips.

So I'm not sure about this approach, is it not possible to make the modal select thing more generic and reusable?

A problem in this patch is that the select operator is registered twice and there are two undo pushes. Ideally we would also not complicate the keymap with this, since editing keymaps is already complicated enough and this adds more obscure properties. The way it works in the node editor requires no keymap changes, It also now actually starts the transform whenever you click to select. This shows up in the status bar, is potentially slow or might report some kind of warning. In the sequence editor, it's now clearing the sequencer cache when just selecting strips. So I'm not sure about this approach, is it not possible to make the modal select thing more generic and reusable?
Member

Checking again on this, these issues seem to be solvable without too much hassle.

In #63994#786743, @brecht wrote:
A problem in this patch is that the select operator is registered twice and there are two undo pushes.

This can be addressed by making sure the operator returns OPERATOR_CANCELLED when it doesn't perform any operation. I think we'd have to do that with the modal select operators too.

Ideally we would also not complicate the keymap with this, since editing keymaps is already complicated enough and this adds more obscure properties. The way it works in the node editor requires no keymap changes,

Agree it's nice to avoid changes to the keymap for such things. But in the node editor implementation, there are hardcoded event checks, which are nice to avoid too.
I think we could also trigger the potential deselecting on release by making transform + deselecting a macro operator. Alternatively it might be a good idea to optionally let the transform operator trigger item deselection if it cancels before having applied any transformation. Again, not the nicest solution but solves this with less tweaks hacks in the keymap.

It also now actually starts the transform whenever you click to select. This shows up in the status bar, is potentially slow or might report some kind of warning. In the sequence editor, it's now clearing the sequencer cache when just selecting strips.

Although I'm not sure how well my previous patch handled this, the idea is that exactly this is prevented. The transform operator should just return OPERATOR_PASS_THROUGH until a tweak event it detected, without applying any change.

So I'm not sure about this approach, is it not possible to make the modal select thing more generic and reusable?

Well, the complaints about this approach from Bastien are still valid. The modal selection stuff makes selection even more complicated and contains hard coded event checks. I'm not sure if we can make this generic/reusable enough to work well. There's quite a difference between select operators.

This new (experimental!) patch fixes the issues mentioned: P1123: Sequencer: Drag-all-selected experiment (V2)

diff --git a/release/scripts/presets/keyconfig/keymap_data/blender_default.py b/release/scripts/presets/keyconfig/keymap_data/blender_default.py
index 1839dd5f322..2a8231ed1d2 100644
--- a/release/scripts/presets/keyconfig/keymap_data/blender_default.py
+++ b/release/scripts/presets/keyconfig/keymap_data/blender_default.py
@@ -2394,7 +2394,10 @@ def km_sequencer(params):
         ),
         ("sequencer.select", {"type": params.select_mouse, "value": 'PRESS'},
          {"properties": [("extend", False), ("deselect_all", True),
-                         ("linked_handle", False), ("left_right", 'NONE'), ("linked_time", False)]}),
+                         ("linked_handle", False), ("left_right", 'NONE'), ("linked_time", False), ("exit_if_selected", True)]}),
+        ("sequencer.select", {"type": params.select_mouse, "value": 'RELEASE'},
+         {"properties": [("extend", False), ("deselect_all", True),
+                         ("linked_handle", False), ("left_right", 'NONE'), ("linked_time", False), ("exit_if_selected", False)]}),
         ("sequencer.select", {"type": params.select_mouse, "value": 'PRESS', "shift": True},
          {"properties": [("extend", True), ("linked_handle", False), ("left_right", 'NONE'), ("linked_time", False)]}),
         ("sequencer.select", {"type": params.select_mouse, "value": 'PRESS', "alt": True},
@@ -2429,7 +2432,8 @@ def km_sequencer(params):
         ("wm.context_set_int", {"type": 'O', "value": 'PRESS'},
          {"properties": [("data_path", 'scene.sequence_editor.overlay_frame'), ("value", 0)]}),
         ("transform.seq_slide", {"type": 'G', "value": 'PRESS'}, None),
-        ("transform.seq_slide", {"type": params.select_tweak, "value": 'ANY'}, None),
+        ("transform.seq_slide", {"type": params.select_mouse, "value": 'PRESS'},
+         {"properties": [("wait_for_tweak", True)]}),
         ("transform.transform", {"type": 'E', "value": 'PRESS'},
          {"properties": [("mode", 'TIME_EXTEND')]}),
         ("marker.add", {"type": 'M', "value": 'PRESS'}, None),
diff --git a/source/blender/editors/space_sequencer/sequencer_select.c b/source/blender/editors/space_sequencer/sequencer_select.c
index affb6d3fd88..39cc353e197 100644
--- a/source/blender/editors/space_sequencer/sequencer_select.c
+++ b/source/blender/editors/space_sequencer/sequencer_select.c
@@ -328,6 +328,7 @@ static int sequencer_select_invoke(bContext *C, wmOperator *op, const wmEvent *e
   const bool deselect_all = RNA_boolean_get(op->ptr, "deselect_all");
   const bool linked_handle = RNA_boolean_get(op->ptr, "linked_handle");
   const bool linked_time = RNA_boolean_get(op->ptr, "linked_time");
+  const bool exit_if_selected = RNA_boolean_get(op->ptr, "exit_if_selected");
   int left_right = RNA_enum_get(op->ptr, "left_right");
 
   Sequence *seq, *neighbor, *act_orig;
@@ -342,6 +343,44 @@ static int sequencer_select_invoke(bContext *C, wmOperator *op, const wmEvent *e
 
   seq = find_nearest_seq(scene, v2d, &hand, event->mval);
 
+  if (exit_if_selected && seq && (seq->flag & SELECT)) {
+    /* Allow tweaks. */
+    return OPERATOR_CANCELLED | OPERATOR_PASS_THROUGH;
+  }
+  if (seq && (seq->flag & SELECT)) {
+    Sequence *iter_seq;
+    bool any_other_selected = false;
+
+    SEQP_BEGIN (ed, iter_seq) {
+      if ((iter_seq->flag & SELECT) && (iter_seq != seq)) {
+        any_other_selected = true;
+        break;
+      }
+    }
+    SEQ_END;
+
+    /* All we would do is selecting an item that is already selected. Exit. */
+    if (any_other_selected == false) {
+      return OPERATOR_CANCELLED | OPERATOR_PASS_THROUGH;
+    }
+  }
+  if (!seq) {
+    Sequence *iter_seq;
+    bool any_selected = false;
+
+    SEQP_BEGIN (ed, iter_seq) {
+      if (iter_seq->flag & SELECT) {
+        any_selected = true;
+        break;
+      }
+    }
+    SEQ_END;
+
+    if (any_selected == false) {
+      return OPERATOR_CANCELLED | OPERATOR_PASS_THROUGH;
+    }
+  }
+
   // XXX - not nice, Ctrl+RMB needs to do left_right only when not over a strip
   if (seq && linked_time && (left_right == SEQ_SELECT_LR_MOUSE)) {
     left_right = SEQ_SELECT_LR_NONE;
@@ -598,6 +637,12 @@ void SEQUENCER_OT_select(wmOperatorType *ot)
                "Select based on the current frame side the cursor is on");
   RNA_def_boolean(
       ot->srna, "linked_time", 0, "Linked Time", "Select other strips at the same time");
+  RNA_def_boolean(
+      ot->srna,
+      "exit_if_selected",
+      false,
+      "Exit if selected",
+      "Do not perform any selection change if the strip to be selected already is selected");
 }
 
 /* run recursively to select linked */
@@ -1038,7 +1083,8 @@ static const EnumPropertyItem sequencer_prop_select_grouped_types[] = {
      "EFFECT_LINK",
      0,
      "Effect/Linked",
-     "Other strips affected by the active one (sharing some time, and below or effect-assigned)"},
+     "Other strips affected by the active one (sharing some time, and below or "
+     "effect-assigned)"},
     {SEQ_SELECT_GROUP_OVERLAP, "OVERLAP", 0, "Overlap", "Overlapping time"},
     {0, NULL, 0, NULL, NULL},
 };
diff --git a/source/blender/editors/transform/transform.c b/source/blender/editors/transform/transform.c
index 67ea0f255fc..bff4d6fe71a 100644
--- a/source/blender/editors/transform/transform.c
+++ b/source/blender/editors/transform/transform.c
@@ -54,6 +54,7 @@
 #include "BKE_editmesh_bvh.h"
 #include "BKE_context.h"
 #include "BKE_constraint.h"
+#include "BKE_global.h"
 #include "BKE_particle.h"
 #include "BKE_unit.h"
 #include "BKE_scene.h"
@@ -832,13 +833,18 @@ enum {
   TFM_MODAL_INSERTOFS_TOGGLE_DIR = 27,
 };
 
-static bool transform_modal_item_poll(const wmOperator *op, int value)
+static int transform_modal_item_poll(const wmOperator *op, int value)
 {
   const TransInfo *t = op->customdata;
+
+  if (t->state == TRANS_WAITING) {
+    return KEYMAP_MODAL_RUN_ONLY;
+  }
+
   switch (value) {
     case TFM_MODAL_CANCEL: {
       if ((t->flag & T_RELEASE_CONFIRM) && ISMOUSE(t->launch_event)) {
-        return false;
+        return KEYMAP_MODAL_SKIP;
       }
       break;
     }
@@ -846,20 +852,20 @@ static bool transform_modal_item_poll(const wmOperator *op, int value)
     case TFM_MODAL_PROPSIZE_UP:
     case TFM_MODAL_PROPSIZE_DOWN: {
       if ((t->flag & T_PROP_EDIT) == 0) {
-        return false;
+        return KEYMAP_MODAL_SKIP;
       }
       break;
     }
     case TFM_MODAL_ADD_SNAP:
     case TFM_MODAL_REMOVE_SNAP: {
       if (t->spacetype != SPACE_VIEW3D) {
-        return false;
+        return KEYMAP_MODAL_SKIP;
       }
       else if (t->tsnap.mode & (SCE_SNAP_MODE_INCREMENT | SCE_SNAP_MODE_GRID)) {
-        return false;
+        return KEYMAP_MODAL_SKIP;
       }
       else if (!validSnap(t)) {
-        return false;
+        return KEYMAP_MODAL_SKIP;
       }
       break;
     }
@@ -870,43 +876,43 @@ static bool transform_modal_item_poll(const wmOperator *op, int value)
     case TFM_MODAL_PLANE_Y:
     case TFM_MODAL_PLANE_Z: {
       if (t->flag & T_NO_CONSTRAINT) {
-        return false;
+        return KEYMAP_MODAL_SKIP;
       }
       if (!ELEM(value, TFM_MODAL_AXIS_X, TFM_MODAL_AXIS_Y)) {
         if (t->flag & T_2D_EDIT) {
-          return false;
+          return KEYMAP_MODAL_SKIP;
         }
       }
       break;
     }
     case TFM_MODAL_CONS_OFF: {
       if ((t->con.mode & CON_APPLY) == 0) {
-        return false;
+        return KEYMAP_MODAL_SKIP;
       }
       break;
     }
     case TFM_MODAL_EDGESLIDE_UP:
     case TFM_MODAL_EDGESLIDE_DOWN: {
       if (t->mode != TFM_EDGE_SLIDE) {
-        return false;
+        return KEYMAP_MODAL_SKIP;
       }
       break;
     }
     case TFM_MODAL_INSERTOFS_TOGGLE_DIR: {
       if (t->spacetype != SPACE_NODE) {
-        return false;
+        return KEYMAP_MODAL_SKIP;
       }
       break;
     }
     case TFM_MODAL_AUTOIK_LEN_INC:
     case TFM_MODAL_AUTOIK_LEN_DEC: {
       if ((t->flag & T_AUTOIK) == 0) {
-        return false;
+        return KEYMAP_MODAL_SKIP;
       }
       break;
     }
   }
-  return true;
+  return KEYMAP_MODAL_RUN_AND_DRAW;
 }
 
 /* called in transform_ops.c, on each regeneration of keymaps */
@@ -1044,13 +1050,30 @@ static void transform_event_xyz_constraint(TransInfo *t, short key_type, char cm
   }
 }
 
-int transformEvent(TransInfo *t, const wmEvent *event)
+int transformEvent(TransInfo *t, wmOperator *op, const wmEvent *event)
 {
   char cmode = constraintModeToChar(t);
   bool handled = false;
   const int modifiers_prev = t->modifiers;
   const int mode_prev = t->mode;
 
+  if (t->state == TRANS_WAITING) {
+    if ((event->type == t->launch_event) && (event->val == KM_RELEASE)) {
+      t->state = TRANS_CANCEL;
+      return OPERATOR_CANCELLED | OPERATOR_PASS_THROUGH;
+    }
+    else if (ISTWEAK(event->type)) {
+      bContext *C = t->context;
+      initTransform(C, t, op, event, t->mode);
+      t->context = C;
+      G.moving = special_transform_moving(t);
+    }
+    else {
+      t->state = TRANS_WAITING;
+      return OPERATOR_PASS_THROUGH;
+    }
+  }
+
   t->redraw |= handleMouseInput(t, &t->mouse, event);
 
   /* Handle modal numinput events first, if already activated. */
@@ -1625,7 +1648,7 @@ int transformEvent(TransInfo *t, const wmEvent *event)
     WM_window_status_area_tag_redraw(CTX_wm_window(t->context));
   }
 
-  if (handled || t->redraw) {
+  if (handled || t->redraw || ELEM(t->state, TRANS_CONFIRM, TRANS_CANCEL)) {
     return 0;
   }
   else {
@@ -2323,6 +2346,17 @@ bool initTransform(bContext *C, TransInfo *t, wmOperator *op, const wmEvent *eve
 
   /* added initialize, for external calls to set stuff in TransInfo, like undo string */
 
+  if (op && (t->state != TRANS_WAITING) &&
+      ((prop = RNA_struct_find_property(op->ptr, "wait_for_tweak")) &&
+       RNA_property_is_set(op->ptr, prop))) {
+    if (RNA_property_boolean_get(op->ptr, prop)) {
+      t->mode = mode;
+      t->state = TRANS_WAITING;
+      t->launch_event = event ? WM_userdef_event_type_from_keymap_type(event->type) : -1;
+      return true;
+    }
+  }
+
   t->state = TRANS_STARTING;
 
   if ((prop = RNA_struct_find_property(op->ptr, "cursor_transform")) &&
@@ -2785,7 +2819,10 @@ int transformEnd(bContext *C, TransInfo *t)
 
   t->context = C;
 
-  if (t->state != TRANS_STARTING && t->state != TRANS_RUNNING) {
+  if (t->state == TRANS_WAITING) {
+    exit_code = 0;
+  }
+  else if (t->state != TRANS_STARTING && t->state != TRANS_RUNNING) {
     /* handle restoring objects */
     if (t->state == TRANS_CANCEL) {
       /* exception, edge slide transformed UVs too */
@@ -2797,7 +2834,10 @@ int transformEnd(bContext *C, TransInfo *t)
       }
 
       exit_code = OPERATOR_CANCELLED;
-      restoreTransObjects(t);  // calls recalcData()
+      if (t->depsgraph) { /* Just some silly NULL-check to ensure this is only called after correct
+                             initialization */
+        restoreTransObjects(t);  // calls recalcData()
+      }
     }
     else {
       if (t->flag & T_CLNOR_REBUILD) {
diff --git a/source/blender/editors/transform/transform.h b/source/blender/editors/transform/transform.h
index ff2afbc0cd7..ba9172cb55f 100644
--- a/source/blender/editors/transform/transform.h
+++ b/source/blender/editors/transform/transform.h
@@ -714,6 +714,7 @@ typedef struct TransInfo {
 
 /* transinfo->state */
 enum {
+  TRANS_WAITING = -1,
   TRANS_STARTING = 0,
   TRANS_RUNNING = 1,
   TRANS_CONFIRM = 2,
@@ -879,7 +880,7 @@ bool initTransform(struct bContext *C,
                    const struct wmEvent *event,
                    int mode);
 void saveTransform(struct bContext *C, struct TransInfo *t, struct wmOperator *op);
-int transformEvent(TransInfo *t, const struct wmEvent *event);
+int transformEvent(TransInfo *t, struct wmOperator *op, const struct wmEvent *event);
 void transformApply(struct bContext *C, TransInfo *t);
 int transformEnd(struct bContext *C, TransInfo *t);
 
diff --git a/source/blender/editors/transform/transform_convert.c b/source/blender/editors/transform/transform_convert.c
index 5862faaf667..2e3af5d9418 100644
--- a/source/blender/editors/transform/transform_convert.c
+++ b/source/blender/editors/transform/transform_convert.c
@@ -2332,7 +2332,10 @@ void special_aftertrans_update(bContext *C, TransInfo *t)
 
 int special_transform_moving(TransInfo *t)
 {
-  if (t->spacetype == SPACE_SEQ) {
+  if (t->state == TRANS_WAITING) {
+    /* Pass. */
+  }
+  else if (t->spacetype == SPACE_SEQ) {
     return G_TRANSFORM_SEQ;
   }
   else if (t->spacetype == SPACE_GRAPH) {
diff --git a/source/blender/editors/transform/transform_ops.c b/source/blender/editors/transform/transform_ops.c
index b2d8671fbce..e9b3c476f2a 100644
--- a/source/blender/editors/transform/transform_ops.c
+++ b/source/blender/editors/transform/transform_ops.c
@@ -413,7 +413,8 @@ static int transform_modal(bContext *C, wmOperator *op, const wmEvent *event)
 
   /* XXX insert keys are called here, and require context */
   t->context = C;
-  exit_code = transformEvent(t, event);
+  exit_code = transformEvent(t, op, event);
+  BLI_assert((t->state != TRANS_WAITING) || (exit_code & OPERATOR_PASS_THROUGH));
   t->context = NULL;
 
   /* XXX, workaround: active needs to be calculated before transforming,
@@ -430,9 +431,12 @@ static int transform_modal(bContext *C, wmOperator *op, const wmEvent *event)
 
   exit_code |= transformEnd(C, t);
 
-  if ((exit_code & OPERATOR_RUNNING_MODAL) == 0) {
+  if (t->state == TRANS_WAITING) {
+    /* pass */
+  }
+  else if ((exit_code & OPERATOR_RUNNING_MODAL) == 0) {
     transformops_exit(C, op);
-    exit_code &= ~OPERATOR_PASS_THROUGH; /* preventively remove passthrough */
+    //    exit_code &= ~OPERATOR_PASS_THROUGH; /* preventively remove passthrough */
   }
   else {
     if (mode_prev != t->mode) {
@@ -1159,6 +1163,16 @@ static void TRANSFORM_OT_seq_slide(struct wmOperatorType *ot)
 
   WM_operatortype_props_advanced_begin(ot);
 
+  /* Might want to support this in Transform_Properties(). */
+  prop = RNA_def_boolean(
+      ot->srna,
+      "wait_for_tweak",
+      0,
+      "Wait for Tweak Event",
+      "Let events pass to other operators and only start applying transformations when a tweak "
+      "event from the initial event source is recognized");
+  RNA_def_property_flag(prop, PROP_HIDDEN);
+
   Transform_Properties(ot, P_SNAP);
 }
 
diff --git a/source/blender/makesdna/DNA_windowmanager_types.h b/source/blender/makesdna/DNA_windowmanager_types.h
index 57c0a29382d..3a8807d92b7 100644
--- a/source/blender/makesdna/DNA_windowmanager_types.h
+++ b/source/blender/makesdna/DNA_windowmanager_types.h
@@ -406,7 +406,7 @@ typedef struct wmKeyMap {
   /* runtime */
   /** Verify if enabled in the current context, use #WM_keymap_poll instead of direct calls. */
   bool (*poll)(struct bContext *);
-  bool (*poll_modal_item)(const struct wmOperator *op, int value);
+  int (*poll_modal_item)(const struct wmOperator *op, int value);
 
   /** For modal, #EnumPropertyItem for now. */
   const void *modal_items;
@@ -424,6 +424,12 @@ enum {
   KEYMAP_TOOL = (1 << 7), /* keymap for active tool system */
 };
 
+enum {
+  KEYMAP_MODAL_SKIP = 0,
+  KEYMAP_MODAL_RUN_AND_DRAW = 1,
+  KEYMAP_MODAL_RUN_ONLY = 2,
+};
+
 /**
  * This is similar to addon-preferences,
  * however unlike add-ons key-config's aren't saved to disk.
diff --git a/source/blender/windowmanager/intern/wm_event_system.c b/source/blender/windowmanager/intern/wm_event_system.c
index 6b4327d5f44..92aeb1ce985 100644
--- a/source/blender/windowmanager/intern/wm_event_system.c
+++ b/source/blender/windowmanager/intern/wm_event_system.c
@@ -2065,7 +2065,8 @@ static wmKeyMapItem *wm_eventmatch_modal_keymap_items(const wmKeyMap *keymap,
 {
   for (wmKeyMapItem *kmi = keymap->items.first; kmi; kmi = kmi->next) {
     if (wm_eventmatch(event, kmi)) {
-      if ((keymap->poll_modal_item == NULL) || (keymap->poll_modal_item(op, kmi->propvalue))) {
+      if ((keymap->poll_modal_item == NULL) ||
+          (keymap->poll_modal_item(op, kmi->propvalue) != KEYMAP_MODAL_SKIP)) {
         return kmi;
       }
     }
@@ -5276,6 +5277,7 @@ bool WM_window_modal_keymap_status_draw(bContext *UNUSED(C), wmWindow *win, uiLa
     return false;
   }
   const EnumPropertyItem *items = keymap->modal_items;
+  bool any_drawn = false;
 
   uiLayout *row = uiLayoutRow(layout, true);
   for (int i = 0; items- [x].identifier; i++) {
@@ -5283,9 +5285,10 @@ bool WM_window_modal_keymap_status_draw(bContext *UNUSED(C), wmWindow *win, uiLa
       continue;
     }
     if ((keymap->poll_modal_item != NULL) &&
-        (keymap->poll_modal_item(op, items- [x].value) == false)) {
+        (keymap->poll_modal_item(op, items- [x].value) != KEYMAP_MODAL_RUN_AND_DRAW)) {
       continue;
     }
+    any_drawn = true;
 
     bool show_text = true;
 
@@ -5330,7 +5333,7 @@ bool WM_window_modal_keymap_status_draw(bContext *UNUSED(C), wmWindow *win, uiLa
       }
     }
   }
-  return true;
+  return any_drawn;
 }
 
 /** \} */
Checking again on this, these issues seem to be solvable without too much hassle. > In #63994#786743, @brecht wrote: > A problem in this patch is that the select operator is registered twice and there are two undo pushes. This can be addressed by making sure the operator returns `OPERATOR_CANCELLED` when it doesn't perform any operation. I think we'd have to do that with the modal select operators too. > Ideally we would also not complicate the keymap with this, since editing keymaps is already complicated enough and this adds more obscure properties. The way it works in the node editor requires no keymap changes, Agree it's nice to avoid changes to the keymap for such things. But in the node editor implementation, there are hardcoded event checks, which are nice to avoid too. I think we could also trigger the potential deselecting on release by making transform + deselecting a macro operator. Alternatively it might be a good idea to optionally let the transform operator trigger item deselection if it cancels before having applied any transformation. Again, not the nicest solution but solves this with less tweaks hacks in the keymap. > It also now actually starts the transform whenever you click to select. This shows up in the status bar, is potentially slow or might report some kind of warning. In the sequence editor, it's now clearing the sequencer cache when just selecting strips. Although I'm not sure how well my previous patch handled this, the idea is that exactly this is prevented. The transform operator should just return `OPERATOR_PASS_THROUGH` until a tweak event it detected, without applying any change. > So I'm not sure about this approach, is it not possible to make the modal select thing more generic and reusable? Well, the complaints about this approach from Bastien are still valid. The modal selection stuff makes selection even more complicated and contains hard coded event checks. I'm not sure if we can make this generic/reusable enough to work well. There's quite a difference between select operators. This new (experimental!) patch fixes the issues mentioned: [P1123: Sequencer: Drag-all-selected experiment (V2)](https://archive.blender.org/developer/P1123.txt) ``` diff --git a/release/scripts/presets/keyconfig/keymap_data/blender_default.py b/release/scripts/presets/keyconfig/keymap_data/blender_default.py index 1839dd5f322..2a8231ed1d2 100644 --- a/release/scripts/presets/keyconfig/keymap_data/blender_default.py +++ b/release/scripts/presets/keyconfig/keymap_data/blender_default.py @@ -2394,7 +2394,10 @@ def km_sequencer(params): ), ("sequencer.select", {"type": params.select_mouse, "value": 'PRESS'}, {"properties": [("extend", False), ("deselect_all", True), - ("linked_handle", False), ("left_right", 'NONE'), ("linked_time", False)]}), + ("linked_handle", False), ("left_right", 'NONE'), ("linked_time", False), ("exit_if_selected", True)]}), + ("sequencer.select", {"type": params.select_mouse, "value": 'RELEASE'}, + {"properties": [("extend", False), ("deselect_all", True), + ("linked_handle", False), ("left_right", 'NONE'), ("linked_time", False), ("exit_if_selected", False)]}), ("sequencer.select", {"type": params.select_mouse, "value": 'PRESS', "shift": True}, {"properties": [("extend", True), ("linked_handle", False), ("left_right", 'NONE'), ("linked_time", False)]}), ("sequencer.select", {"type": params.select_mouse, "value": 'PRESS', "alt": True}, @@ -2429,7 +2432,8 @@ def km_sequencer(params): ("wm.context_set_int", {"type": 'O', "value": 'PRESS'}, {"properties": [("data_path", 'scene.sequence_editor.overlay_frame'), ("value", 0)]}), ("transform.seq_slide", {"type": 'G', "value": 'PRESS'}, None), - ("transform.seq_slide", {"type": params.select_tweak, "value": 'ANY'}, None), + ("transform.seq_slide", {"type": params.select_mouse, "value": 'PRESS'}, + {"properties": [("wait_for_tweak", True)]}), ("transform.transform", {"type": 'E', "value": 'PRESS'}, {"properties": [("mode", 'TIME_EXTEND')]}), ("marker.add", {"type": 'M', "value": 'PRESS'}, None), diff --git a/source/blender/editors/space_sequencer/sequencer_select.c b/source/blender/editors/space_sequencer/sequencer_select.c index affb6d3fd88..39cc353e197 100644 --- a/source/blender/editors/space_sequencer/sequencer_select.c +++ b/source/blender/editors/space_sequencer/sequencer_select.c @@ -328,6 +328,7 @@ static int sequencer_select_invoke(bContext *C, wmOperator *op, const wmEvent *e const bool deselect_all = RNA_boolean_get(op->ptr, "deselect_all"); const bool linked_handle = RNA_boolean_get(op->ptr, "linked_handle"); const bool linked_time = RNA_boolean_get(op->ptr, "linked_time"); + const bool exit_if_selected = RNA_boolean_get(op->ptr, "exit_if_selected"); int left_right = RNA_enum_get(op->ptr, "left_right"); Sequence *seq, *neighbor, *act_orig; @@ -342,6 +343,44 @@ static int sequencer_select_invoke(bContext *C, wmOperator *op, const wmEvent *e seq = find_nearest_seq(scene, v2d, &hand, event->mval); + if (exit_if_selected && seq && (seq->flag & SELECT)) { + /* Allow tweaks. */ + return OPERATOR_CANCELLED | OPERATOR_PASS_THROUGH; + } + if (seq && (seq->flag & SELECT)) { + Sequence *iter_seq; + bool any_other_selected = false; + + SEQP_BEGIN (ed, iter_seq) { + if ((iter_seq->flag & SELECT) && (iter_seq != seq)) { + any_other_selected = true; + break; + } + } + SEQ_END; + + /* All we would do is selecting an item that is already selected. Exit. */ + if (any_other_selected == false) { + return OPERATOR_CANCELLED | OPERATOR_PASS_THROUGH; + } + } + if (!seq) { + Sequence *iter_seq; + bool any_selected = false; + + SEQP_BEGIN (ed, iter_seq) { + if (iter_seq->flag & SELECT) { + any_selected = true; + break; + } + } + SEQ_END; + + if (any_selected == false) { + return OPERATOR_CANCELLED | OPERATOR_PASS_THROUGH; + } + } + // XXX - not nice, Ctrl+RMB needs to do left_right only when not over a strip if (seq && linked_time && (left_right == SEQ_SELECT_LR_MOUSE)) { left_right = SEQ_SELECT_LR_NONE; @@ -598,6 +637,12 @@ void SEQUENCER_OT_select(wmOperatorType *ot) "Select based on the current frame side the cursor is on"); RNA_def_boolean( ot->srna, "linked_time", 0, "Linked Time", "Select other strips at the same time"); + RNA_def_boolean( + ot->srna, + "exit_if_selected", + false, + "Exit if selected", + "Do not perform any selection change if the strip to be selected already is selected"); } /* run recursively to select linked */ @@ -1038,7 +1083,8 @@ static const EnumPropertyItem sequencer_prop_select_grouped_types[] = { "EFFECT_LINK", 0, "Effect/Linked", - "Other strips affected by the active one (sharing some time, and below or effect-assigned)"}, + "Other strips affected by the active one (sharing some time, and below or " + "effect-assigned)"}, {SEQ_SELECT_GROUP_OVERLAP, "OVERLAP", 0, "Overlap", "Overlapping time"}, {0, NULL, 0, NULL, NULL}, }; diff --git a/source/blender/editors/transform/transform.c b/source/blender/editors/transform/transform.c index 67ea0f255fc..bff4d6fe71a 100644 --- a/source/blender/editors/transform/transform.c +++ b/source/blender/editors/transform/transform.c @@ -54,6 +54,7 @@ #include "BKE_editmesh_bvh.h" #include "BKE_context.h" #include "BKE_constraint.h" +#include "BKE_global.h" #include "BKE_particle.h" #include "BKE_unit.h" #include "BKE_scene.h" @@ -832,13 +833,18 @@ enum { TFM_MODAL_INSERTOFS_TOGGLE_DIR = 27, }; -static bool transform_modal_item_poll(const wmOperator *op, int value) +static int transform_modal_item_poll(const wmOperator *op, int value) { const TransInfo *t = op->customdata; + + if (t->state == TRANS_WAITING) { + return KEYMAP_MODAL_RUN_ONLY; + } + switch (value) { case TFM_MODAL_CANCEL: { if ((t->flag & T_RELEASE_CONFIRM) && ISMOUSE(t->launch_event)) { - return false; + return KEYMAP_MODAL_SKIP; } break; } @@ -846,20 +852,20 @@ static bool transform_modal_item_poll(const wmOperator *op, int value) case TFM_MODAL_PROPSIZE_UP: case TFM_MODAL_PROPSIZE_DOWN: { if ((t->flag & T_PROP_EDIT) == 0) { - return false; + return KEYMAP_MODAL_SKIP; } break; } case TFM_MODAL_ADD_SNAP: case TFM_MODAL_REMOVE_SNAP: { if (t->spacetype != SPACE_VIEW3D) { - return false; + return KEYMAP_MODAL_SKIP; } else if (t->tsnap.mode & (SCE_SNAP_MODE_INCREMENT | SCE_SNAP_MODE_GRID)) { - return false; + return KEYMAP_MODAL_SKIP; } else if (!validSnap(t)) { - return false; + return KEYMAP_MODAL_SKIP; } break; } @@ -870,43 +876,43 @@ static bool transform_modal_item_poll(const wmOperator *op, int value) case TFM_MODAL_PLANE_Y: case TFM_MODAL_PLANE_Z: { if (t->flag & T_NO_CONSTRAINT) { - return false; + return KEYMAP_MODAL_SKIP; } if (!ELEM(value, TFM_MODAL_AXIS_X, TFM_MODAL_AXIS_Y)) { if (t->flag & T_2D_EDIT) { - return false; + return KEYMAP_MODAL_SKIP; } } break; } case TFM_MODAL_CONS_OFF: { if ((t->con.mode & CON_APPLY) == 0) { - return false; + return KEYMAP_MODAL_SKIP; } break; } case TFM_MODAL_EDGESLIDE_UP: case TFM_MODAL_EDGESLIDE_DOWN: { if (t->mode != TFM_EDGE_SLIDE) { - return false; + return KEYMAP_MODAL_SKIP; } break; } case TFM_MODAL_INSERTOFS_TOGGLE_DIR: { if (t->spacetype != SPACE_NODE) { - return false; + return KEYMAP_MODAL_SKIP; } break; } case TFM_MODAL_AUTOIK_LEN_INC: case TFM_MODAL_AUTOIK_LEN_DEC: { if ((t->flag & T_AUTOIK) == 0) { - return false; + return KEYMAP_MODAL_SKIP; } break; } } - return true; + return KEYMAP_MODAL_RUN_AND_DRAW; } /* called in transform_ops.c, on each regeneration of keymaps */ @@ -1044,13 +1050,30 @@ static void transform_event_xyz_constraint(TransInfo *t, short key_type, char cm } } -int transformEvent(TransInfo *t, const wmEvent *event) +int transformEvent(TransInfo *t, wmOperator *op, const wmEvent *event) { char cmode = constraintModeToChar(t); bool handled = false; const int modifiers_prev = t->modifiers; const int mode_prev = t->mode; + if (t->state == TRANS_WAITING) { + if ((event->type == t->launch_event) && (event->val == KM_RELEASE)) { + t->state = TRANS_CANCEL; + return OPERATOR_CANCELLED | OPERATOR_PASS_THROUGH; + } + else if (ISTWEAK(event->type)) { + bContext *C = t->context; + initTransform(C, t, op, event, t->mode); + t->context = C; + G.moving = special_transform_moving(t); + } + else { + t->state = TRANS_WAITING; + return OPERATOR_PASS_THROUGH; + } + } + t->redraw |= handleMouseInput(t, &t->mouse, event); /* Handle modal numinput events first, if already activated. */ @@ -1625,7 +1648,7 @@ int transformEvent(TransInfo *t, const wmEvent *event) WM_window_status_area_tag_redraw(CTX_wm_window(t->context)); } - if (handled || t->redraw) { + if (handled || t->redraw || ELEM(t->state, TRANS_CONFIRM, TRANS_CANCEL)) { return 0; } else { @@ -2323,6 +2346,17 @@ bool initTransform(bContext *C, TransInfo *t, wmOperator *op, const wmEvent *eve /* added initialize, for external calls to set stuff in TransInfo, like undo string */ + if (op && (t->state != TRANS_WAITING) && + ((prop = RNA_struct_find_property(op->ptr, "wait_for_tweak")) && + RNA_property_is_set(op->ptr, prop))) { + if (RNA_property_boolean_get(op->ptr, prop)) { + t->mode = mode; + t->state = TRANS_WAITING; + t->launch_event = event ? WM_userdef_event_type_from_keymap_type(event->type) : -1; + return true; + } + } + t->state = TRANS_STARTING; if ((prop = RNA_struct_find_property(op->ptr, "cursor_transform")) && @@ -2785,7 +2819,10 @@ int transformEnd(bContext *C, TransInfo *t) t->context = C; - if (t->state != TRANS_STARTING && t->state != TRANS_RUNNING) { + if (t->state == TRANS_WAITING) { + exit_code = 0; + } + else if (t->state != TRANS_STARTING && t->state != TRANS_RUNNING) { /* handle restoring objects */ if (t->state == TRANS_CANCEL) { /* exception, edge slide transformed UVs too */ @@ -2797,7 +2834,10 @@ int transformEnd(bContext *C, TransInfo *t) } exit_code = OPERATOR_CANCELLED; - restoreTransObjects(t); // calls recalcData() + if (t->depsgraph) { /* Just some silly NULL-check to ensure this is only called after correct + initialization */ + restoreTransObjects(t); // calls recalcData() + } } else { if (t->flag & T_CLNOR_REBUILD) { diff --git a/source/blender/editors/transform/transform.h b/source/blender/editors/transform/transform.h index ff2afbc0cd7..ba9172cb55f 100644 --- a/source/blender/editors/transform/transform.h +++ b/source/blender/editors/transform/transform.h @@ -714,6 +714,7 @@ typedef struct TransInfo { /* transinfo->state */ enum { + TRANS_WAITING = -1, TRANS_STARTING = 0, TRANS_RUNNING = 1, TRANS_CONFIRM = 2, @@ -879,7 +880,7 @@ bool initTransform(struct bContext *C, const struct wmEvent *event, int mode); void saveTransform(struct bContext *C, struct TransInfo *t, struct wmOperator *op); -int transformEvent(TransInfo *t, const struct wmEvent *event); +int transformEvent(TransInfo *t, struct wmOperator *op, const struct wmEvent *event); void transformApply(struct bContext *C, TransInfo *t); int transformEnd(struct bContext *C, TransInfo *t); diff --git a/source/blender/editors/transform/transform_convert.c b/source/blender/editors/transform/transform_convert.c index 5862faaf667..2e3af5d9418 100644 --- a/source/blender/editors/transform/transform_convert.c +++ b/source/blender/editors/transform/transform_convert.c @@ -2332,7 +2332,10 @@ void special_aftertrans_update(bContext *C, TransInfo *t) int special_transform_moving(TransInfo *t) { - if (t->spacetype == SPACE_SEQ) { + if (t->state == TRANS_WAITING) { + /* Pass. */ + } + else if (t->spacetype == SPACE_SEQ) { return G_TRANSFORM_SEQ; } else if (t->spacetype == SPACE_GRAPH) { diff --git a/source/blender/editors/transform/transform_ops.c b/source/blender/editors/transform/transform_ops.c index b2d8671fbce..e9b3c476f2a 100644 --- a/source/blender/editors/transform/transform_ops.c +++ b/source/blender/editors/transform/transform_ops.c @@ -413,7 +413,8 @@ static int transform_modal(bContext *C, wmOperator *op, const wmEvent *event) /* XXX insert keys are called here, and require context */ t->context = C; - exit_code = transformEvent(t, event); + exit_code = transformEvent(t, op, event); + BLI_assert((t->state != TRANS_WAITING) || (exit_code & OPERATOR_PASS_THROUGH)); t->context = NULL; /* XXX, workaround: active needs to be calculated before transforming, @@ -430,9 +431,12 @@ static int transform_modal(bContext *C, wmOperator *op, const wmEvent *event) exit_code |= transformEnd(C, t); - if ((exit_code & OPERATOR_RUNNING_MODAL) == 0) { + if (t->state == TRANS_WAITING) { + /* pass */ + } + else if ((exit_code & OPERATOR_RUNNING_MODAL) == 0) { transformops_exit(C, op); - exit_code &= ~OPERATOR_PASS_THROUGH; /* preventively remove passthrough */ + // exit_code &= ~OPERATOR_PASS_THROUGH; /* preventively remove passthrough */ } else { if (mode_prev != t->mode) { @@ -1159,6 +1163,16 @@ static void TRANSFORM_OT_seq_slide(struct wmOperatorType *ot) WM_operatortype_props_advanced_begin(ot); + /* Might want to support this in Transform_Properties(). */ + prop = RNA_def_boolean( + ot->srna, + "wait_for_tweak", + 0, + "Wait for Tweak Event", + "Let events pass to other operators and only start applying transformations when a tweak " + "event from the initial event source is recognized"); + RNA_def_property_flag(prop, PROP_HIDDEN); + Transform_Properties(ot, P_SNAP); } diff --git a/source/blender/makesdna/DNA_windowmanager_types.h b/source/blender/makesdna/DNA_windowmanager_types.h index 57c0a29382d..3a8807d92b7 100644 --- a/source/blender/makesdna/DNA_windowmanager_types.h +++ b/source/blender/makesdna/DNA_windowmanager_types.h @@ -406,7 +406,7 @@ typedef struct wmKeyMap { /* runtime */ /** Verify if enabled in the current context, use #WM_keymap_poll instead of direct calls. */ bool (*poll)(struct bContext *); - bool (*poll_modal_item)(const struct wmOperator *op, int value); + int (*poll_modal_item)(const struct wmOperator *op, int value); /** For modal, #EnumPropertyItem for now. */ const void *modal_items; @@ -424,6 +424,12 @@ enum { KEYMAP_TOOL = (1 << 7), /* keymap for active tool system */ }; +enum { + KEYMAP_MODAL_SKIP = 0, + KEYMAP_MODAL_RUN_AND_DRAW = 1, + KEYMAP_MODAL_RUN_ONLY = 2, +}; + /** * This is similar to addon-preferences, * however unlike add-ons key-config's aren't saved to disk. diff --git a/source/blender/windowmanager/intern/wm_event_system.c b/source/blender/windowmanager/intern/wm_event_system.c index 6b4327d5f44..92aeb1ce985 100644 --- a/source/blender/windowmanager/intern/wm_event_system.c +++ b/source/blender/windowmanager/intern/wm_event_system.c @@ -2065,7 +2065,8 @@ static wmKeyMapItem *wm_eventmatch_modal_keymap_items(const wmKeyMap *keymap, { for (wmKeyMapItem *kmi = keymap->items.first; kmi; kmi = kmi->next) { if (wm_eventmatch(event, kmi)) { - if ((keymap->poll_modal_item == NULL) || (keymap->poll_modal_item(op, kmi->propvalue))) { + if ((keymap->poll_modal_item == NULL) || + (keymap->poll_modal_item(op, kmi->propvalue) != KEYMAP_MODAL_SKIP)) { return kmi; } } @@ -5276,6 +5277,7 @@ bool WM_window_modal_keymap_status_draw(bContext *UNUSED(C), wmWindow *win, uiLa return false; } const EnumPropertyItem *items = keymap->modal_items; + bool any_drawn = false; uiLayout *row = uiLayoutRow(layout, true); for (int i = 0; items- [x].identifier; i++) { @@ -5283,9 +5285,10 @@ bool WM_window_modal_keymap_status_draw(bContext *UNUSED(C), wmWindow *win, uiLa continue; } if ((keymap->poll_modal_item != NULL) && - (keymap->poll_modal_item(op, items- [x].value) == false)) { + (keymap->poll_modal_item(op, items- [x].value) != KEYMAP_MODAL_RUN_AND_DRAW)) { continue; } + any_drawn = true; bool show_text = true; @@ -5330,7 +5333,7 @@ bool WM_window_modal_keymap_status_draw(bContext *UNUSED(C), wmWindow *win, uiLa } } } - return true; + return any_drawn; } /** \} */ ```

This issue was referenced by 3b23685c7d

This issue was referenced by 3b23685c7dc16588bcc18045a1ba8c01671574c3

Well, the complaints about this approach from Bastien are still valid. The modal selection stuff makes selection even more complicated and contains hard coded event checks. I'm not sure if we can make this generic/reusable enough to work well. There's quite a difference between select operators.

There are no hardcoded event checks, it just checks the original event release + mouse move.

Here I did some refactoring to demonstrate what I mean.
3b23685c7d

Note how node_select_exec, node_select_modal, node_select_invoke no longer contain any node specific logic, which is all left to node_mouse_select.

> Well, the complaints about this approach from Bastien are still valid. The modal selection stuff makes selection even more complicated and contains hard coded event checks. I'm not sure if we can make this generic/reusable enough to work well. There's quite a difference between select operators. There are no hardcoded event checks, it just checks the original event release + mouse move. Here I did some refactoring to demonstrate what I mean. 3b23685c7d Note how `node_select_exec`, `node_select_modal`, `node_select_invoke` no longer contain any node specific logic, which is all left to `node_mouse_select`.

Note that if wait_to_deselect_others is made a hidden operator property, all that logic can be put in the exec function.

And then node definition looks something like this:

  /* api callbacks */
  ot->poll = ED_operator_node_active;
  ot->exec = node_select_exec;
  ot->invoke = WM_generic_select_invoke;
  ot->modal = WM_generic_select_modal;

  /* properties */
  WM_generic_select_properties(ot);
  RNA_def_boolean(ot->srna, "extend", false, "Extend", "");
  ...

Which is consistent with the way we do box select operators for example.

Note that if `wait_to_deselect_others` is made a hidden operator property, all that logic can be put in the `exec` function. And then node definition looks something like this: ``` /* api callbacks */ ot->poll = ED_operator_node_active; ot->exec = node_select_exec; ot->invoke = WM_generic_select_invoke; ot->modal = WM_generic_select_modal; /* properties */ WM_generic_select_properties(ot); RNA_def_boolean(ot->srna, "extend", false, "Extend", ""); ... ``` Which is consistent with the way we do box select operators for example.
Member

Added subscriber: @Mets

Added subscriber: @Mets

This issue was referenced by 926f7612fd

This issue was referenced by 926f7612fd75078f5fb802d165f9c25af0bdb0df
Sign in to join this conversation.
No Label
Interest
Alembic
Interest
Animation & Rigging
Interest
Asset Browser
Interest
Asset Browser Project Overview
Interest
Audio
Interest
Automated Testing
Interest
Blender Asset Bundle
Interest
BlendFile
Interest
Collada
Interest
Compatibility
Interest
Compositing
Interest
Core
Interest
Cycles
Interest
Dependency Graph
Interest
Development Management
Interest
EEVEE
Interest
EEVEE & Viewport
Interest
Freestyle
Interest
Geometry Nodes
Interest
Grease Pencil
Interest
ID Management
Interest
Images & Movies
Interest
Import Export
Interest
Line Art
Interest
Masking
Interest
Metal
Interest
Modeling
Interest
Modifiers
Interest
Motion Tracking
Interest
Nodes & Physics
Interest
OpenGL
Interest
Overlay
Interest
Overrides
Interest
Performance
Interest
Physics
Interest
Pipeline, Assets & IO
Interest
Platforms, Builds & Tests
Interest
Python API
Interest
Render & Cycles
Interest
Render Pipeline
Interest
Sculpt, Paint & Texture
Interest
Text Editor
Interest
Translations
Interest
Triaging
Interest
Undo
Interest
USD
Interest
User Interface
Interest
UV Editing
Interest
VFX & Video
Interest
Video Sequencer
Interest
Virtual Reality
Interest
Vulkan
Interest
Wayland
Interest
Workbench
Interest: X11
Legacy
Blender 2.8 Project
Legacy
Milestone 1: Basic, Local Asset Browser
Legacy
OpenGL Error
Meta
Good First Issue
Meta
Papercut
Meta
Retrospective
Meta
Security
Module
Animation & Rigging
Module
Core
Module
Development Management
Module
EEVEE & Viewport
Module
Grease Pencil
Module
Modeling
Module
Nodes & Physics
Module
Pipeline, Assets & IO
Module
Platforms, Builds & Tests
Module
Python API
Module
Render & Cycles
Module
Sculpt, Paint & Texture
Module
Triaging
Module
User Interface
Module
VFX & Video
Platform
FreeBSD
Platform
Linux
Platform
macOS
Platform
Windows
Priority
High
Priority
Low
Priority
Normal
Priority
Unbreak Now!
Status
Archived
Status
Confirmed
Status
Duplicate
Status
Needs Info from Developers
Status
Needs Information from User
Status
Needs Triage
Status
Resolved
Type
Bug
Type
Design
Type
Known Issue
Type
Patch
Type
Report
Type
To Do
No Milestone
No project
No Assignees
11 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: blender/blender#63994
No description provided.