pencil2d / pencil

Pencil2D is an easy, intuitive tool to make 2D hand-drawn animations. Pencil2D is open source and cross-platform.
http://pencil2d.org
GNU General Public License v2.0
1.45k stars 272 forks source link

Maintain active layer track in view #1867

Closed HeCorr closed 2 months ago

HeCorr commented 2 months ago

Fixes #1858.

This change maintains the currently selected layer visible when the active layer changes, so that using the Up and Down arrow keys works as expected:

https://github.com/user-attachments/assets/26c97fea-6aa4-4d3b-a56c-f391d74162f4

It also works when the layer is far away from view, not just right above/below:

https://github.com/user-attachments/assets/8183b8cb-e94b-4c7d-8847-d7facfa76b48

Caveat

When clicking on a layer that is barely in view (which isn't considered as in view by the code), it is moved due to the sudden scroll as the cursor is still being held:

https://github.com/user-attachments/assets/2732bebc-9a8e-454e-aab0-612255185fbd

I thought of making it so that the layer is only selected upon mouse release but I'm willing to bet the code responsible for reordering layers decides which one to move based on which one is active, but I could be wrong.

Another potential solution would be to snap the timeline height so that it never shows half a layer:

2024-07-24 02-04-26 2024-07-24 02-06-26

HeCorr commented 2 months ago

Maybe instead of listening to currentLayerChanged which is emitted with both arrows and cursor clicks, I could emit a new signal from gotoPreviouslayer and gotoNextlayer, so that this only runs when using the arrows.

Or a simpler way, which I couldn't figure out is to directly listen to arrow key presses from timeline.cpp.

Or maybe even change the currentLayerChanged signal itself, adding an enum which specifies the source of the change (mouse or keyboard).

HeCorr commented 2 months ago

@MrStevns is the signature change of currentLayerChanged suggested above acceptable? If so I'm gonna give that a go soon.

MrStevns commented 2 months ago

Maybe instead of listening to currentLayerChanged which is emitted with both arrows and cursor clicks, I could emit a new signal from gotoPreviouslayer and gotoNextlayer, so that this only runs when using the arrows.

Or a simpler way, which I couldn't figure out is to directly listen to arrow key presses from timeline.cpp.

Or maybe even change the currentLayerChanged signal itself, adding an enum which specifies the source of the change (mouse or keyboard).

I'm not convinced that either of these will fix the problem. Why should changing the signature of currentLayerChanged change the behavior, when what's causing the problem is most likely the logic in timelinecells or the TimeLine::updateLayerView which controls the maximum amount of steps of the scrollbar?

edit: upon further inspection, it is indeed caused by the code in TimelineCells. For this to work properly, I think you'll have to add some logic that prevents it from doing the layer swapping on mouse release when you're not explicitly dragging.

HeCorr commented 2 months ago

Why should changing the signature of currentLayerChanged change the behavior?

Consider the following:

void TimeLine::currentLayerChanged(int layerIndex, LayerChangeSource source)

I can check whether source == LayerChangeSource::ArrowKey and only scroll if it's true, which would ignore mouse clicks (...unless clicking a half-visible layer already scrolls by default/outside this branch, which I honestly haven't tested).

For this to work properly, I think you'll have to add some logic that prevents it from doing the layer swapping on mouse release when you're not explicitly dragging.

I'll have to see what I can do about that.

HeCorr commented 2 months ago

Just checked on Nightly and indeed it does not scroll by default when clicking, which means my theoretical fix could work.

https://github.com/user-attachments/assets/8915e0ad-edc3-4cf1-a90a-a72ded4d6301

HeCorr commented 2 months ago

I did a quick test and it did work, but I ended up changing too many places that use the currentLayerChanged signal, so I'm going to try to create a new signal instead.

https://github.com/user-attachments/assets/7487f98b-f749-4184-bc0f-5a959252810e

MrStevns commented 2 months ago

Consider the following:

void TimeLine::currentLayerChanged(int layerIndex, LayerChangeSource source)

I can check whether source == LayerChangeSource::ArrowKey and only scroll if it's true, which would ignore mouse clicks (...unless clicking a half-visible layer already scrolls by default/outside this branch, which I honestly haven't tested).

We do not want to complicate the layer manager with what source; be it keyboard or mouse, is required to trigger an behavior that is meant for a GUI element.

It's a good thing that you're experimenting and that you've found a few solutions that works. However, I still think we should deal with this through the interface code, rather than adding an additional emitter logic to the LayerManager, when the source of the problem comes from the timeline logic.

I can understand if you're reluctant to add more code to the timeline cell logic, as it's a mess but that's imo. the correct way to fix this issue. Any other way is beating around the bush.

HeCorr commented 2 months ago

I can understand if you're reluctant to add more code to the timeline cell logic, as it's a mess

I wouldn't even know where to begin.

I understand that this simple fix is not the ideal solution but it works and unfortunately this is all I can provide at the moment.

MrStevns commented 2 months ago

I can understand if you're reluctant to add more code to the timeline cell logic, as it's a mess

I wouldn't even know where to begin.

I understand that this simple fix is not the ideal solution but it works and unfortunately this is all I can provide at the moment.

No worries, I took a stab at it which required minimal amount of changes to TimeLineCells. You can check out the commits yourself but the gist of it is that we've introduced a mScrollingVertically property so we can avoid moving the layers while actively scrolling. Once that's done, we still have one problem to solve though, which is how do we reset the value?

I figured that a timer that triggers after x milliseconds would do the trick. It will only trigger once we've scrolled one way or another, and only once.

After inspecting the TimeLine class myself I also noticed that we had a ´layerChanged` slot already, so I've refactored your logic into that method instead of having an additional signal.

Let me know what you think.

HeCorr commented 2 months ago

I personally don't understand why you would opt for a more complex implementation that uses a timer over simply emitting two signals and picking one to listen to depending on what makes the most sense.

Sure, it wasn't an elegant solution, but it was simple and worked pretty well. That being said, if the new solution works better for you then there's nothing I can do about it.

MrStevns commented 2 months ago

I get your point but what might seem like a simpler solution at first, doesn't make sense in the long run.

Your solution adds seemingly abysmal complexity to the core layer manager, an object that is tied to everything, to fix a specific scenario in a specific GUI component, while I add logic to the timeline, which only affects that specific component and lives in the APP part of the code base.

Consider this example. Another person writes their own app client for Pencil2D; possibly for mobile, and write a better timeline implementation. Why should they have to deal with this new signal, when it's a problem in our GUI code?

Small things like this add up over time and makes the code base less maintainable. Even though we're open source, doesn't mean we don't want the code to be as good as it can be.

It's not a rant nor critique, truthfully, I'm just trying to voice why I think it's important to fix bugs where they appear. :)