Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prevent accidental properties edit by mouse move #3070

Merged
merged 2 commits into from
Nov 17, 2019

Conversation

SuslikV
Copy link
Contributor

@SuslikV SuslikV commented Nov 9, 2019

Set threshold for the first mouse move before change the values of properties and lock the row selection.

It pisses me off when I clicking properties and every single parameter tries to change its value. Also, it is not a funny when you drag mouse over the vertical list of properties and everything is changing.

I have plans to re-use the property selection to display some stuff related to the property value in time. So it had to work smooth and robust.

SuslikV added 2 commits November 9, 2019 10:28
Prevents accidental selection of the neighbor rows
Prevents accidental property edit by mouse click and insignificant
first move.

The mouse event may recalculate by UI update, so this change ensures
that user moves the mouse intentionally - to change the value, not
just thinking about -is the right property was pressed-, while holding
the mouse button down.
self.prev_row = row
self.lock_selection = True

if row is None:
Copy link
Member

Choose a reason for hiding this comment

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

Looks like row could be undefined at this point. Probably still need a declaration outside these if/else statements

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I googled a bit and it seems that the code like this is common in Python.

The None check (the later one) was left from the debugging and not a big deal right now. Feel free to remove it. I placed it just as reminder for myself when code wasn't complete yet.

By the way, the self.currentIndex().row() instead of the indexAt - works OK too with this QTableView.

Copy link
Contributor

Choose a reason for hiding this comment

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

row couldn't be undefined (meaning, it'll always have gotten set to either self.prev_row or self.indexAt(event.pos()).row() — the code is always going to go down one or the other of those paths), so that shouldn't be a worry and the check should be safe. The only concern is that it could, in fact, be None, if self.prev_row was also None.

That was actually going to be my only suggestion here: that self.prev_row be checked for a sensible value, before blindly setting row = self.prev_row.

Otherwise, if the code should ever happen to get into a state where self.lock_selection is True, BUT self.prev_row is None, then it's going to bail right out of the mouseMoveEvent without processing anything. row will get set to self.prev_row, which means now they're both None which then trips this condition. (In theory. But input events can and do get dropped or delivered out of order, so this isn't purely theoretical.)

And since (in the else path) self.prev_row gets blindly updated to the value of row without first checking if self.indexAt(event.pos()).row() evaluated to None, that possibility becomes even more real.

Still, that shouldn't really happen (or at least it should be rare), and even when it does, it'll reset itself when the mouseReleaseEvent() is processed. So, it would only cause a single drag operation to be ignored. (It's not a dire situation like my right-click change in #2997, where I had to use a timer to release the lock. But there, I knew for a fact that bad states could occur, and I'd get stuck like that indefinitely once it happened since there was no mechanism to reset the state. Here, mouseReleaseEvent() protects us from that scenario.)

But, it is still a short-term possibility. And the solution is simple, really:

if self.lock_selection and self.prev_row:
    row = self.prev_row
else:
    row = self.indexAt(event.pos()).row()
    self.prev_row = row
    self.lock_selection = True

That'll protect against ever trying to lock to a nonexistent self.prev_row.

(TBH, doing it that way self.lock_selection could be eliminated, really — a non-None value of self.prev_row is all that's really needed to control/signify the lock, and setting self.prev_row = None would take the place of self.lock_selection = False in the mouseReleaseEvent() code. But there's no harm in being a tiny bit more explicit, I figure.)

I don't think it's a big deal, but it's probably worth making that adjustment for defensive-coding purposes.

@jonoomph
Copy link
Member

Looking good! I like the concept!

@jonoomph
Copy link
Member

I'm gonna go ahead and merge this and test it. If we need to fix anything, we'll do it in a new PR. Thx @SuslikV!

@jonoomph jonoomph merged commit 69632c1 into OpenShot:develop Nov 17, 2019
@SuslikV SuslikV deleted the patch-7 branch November 18, 2019 14:15
@ferdnyc
Copy link
Contributor

ferdnyc commented Nov 19, 2019

I'd meant to comment on this one, it's a great fix and that mouse-dragging 'drift' thing was SO incredibly frustrating. Huge improvement having that fixed. Thanks @SuslikV !

self.prev_row = row
self.lock_selection = True

if row is None:
Copy link
Contributor

Choose a reason for hiding this comment

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

row couldn't be undefined (meaning, it'll always have gotten set to either self.prev_row or self.indexAt(event.pos()).row() — the code is always going to go down one or the other of those paths), so that shouldn't be a worry and the check should be safe. The only concern is that it could, in fact, be None, if self.prev_row was also None.

That was actually going to be my only suggestion here: that self.prev_row be checked for a sensible value, before blindly setting row = self.prev_row.

Otherwise, if the code should ever happen to get into a state where self.lock_selection is True, BUT self.prev_row is None, then it's going to bail right out of the mouseMoveEvent without processing anything. row will get set to self.prev_row, which means now they're both None which then trips this condition. (In theory. But input events can and do get dropped or delivered out of order, so this isn't purely theoretical.)

And since (in the else path) self.prev_row gets blindly updated to the value of row without first checking if self.indexAt(event.pos()).row() evaluated to None, that possibility becomes even more real.

Still, that shouldn't really happen (or at least it should be rare), and even when it does, it'll reset itself when the mouseReleaseEvent() is processed. So, it would only cause a single drag operation to be ignored. (It's not a dire situation like my right-click change in #2997, where I had to use a timer to release the lock. But there, I knew for a fact that bad states could occur, and I'd get stuck like that indefinitely once it happened since there was no mechanism to reset the state. Here, mouseReleaseEvent() protects us from that scenario.)

But, it is still a short-term possibility. And the solution is simple, really:

if self.lock_selection and self.prev_row:
    row = self.prev_row
else:
    row = self.indexAt(event.pos()).row()
    self.prev_row = row
    self.lock_selection = True

That'll protect against ever trying to lock to a nonexistent self.prev_row.

(TBH, doing it that way self.lock_selection could be eliminated, really — a non-None value of self.prev_row is all that's really needed to control/signify the lock, and setting self.prev_row = None would take the place of self.lock_selection = False in the mouseReleaseEvent() code. But there's no harm in being a tiny bit more explicit, I figure.)

I don't think it's a big deal, but it's probably worth making that adjustment for defensive-coding purposes.

Comment on lines +254 to +265
if self.previous_x == -1:
# Init previous_x for the first time
self.previous_x = event.x()
self.diff_length = 10
if abs(self.previous_x - event.x()) < self.diff_length:
# If threshold exceeded then allow any small incriment for the next time
# In few steps, because the first click may has any far position
self.diff_length -= 1
if self.diff_length < 0:
self.diff_length = 0
self.previous_x = event.x()
return
Copy link
Contributor

Choose a reason for hiding this comment

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

All of these lines are identical (effectively) to the code in the if min_max_range > 1000.0: side of the condition, and none of it depends on min_max_range in any way. That means it should really be outside of the condition entirely. By moving the code up before the condition, instead of copy-pasting it into both paths inside the condition, it can be processed up front, before we ever have to worry about the range of the parameter. Meaning:

            # For numeric values, apply percentage within parameter's allowable range
            if property_type in ["float", "int"] and property_name != "Track":

                # Start tracking previous position
                if self.previous_x == -1:
                    self.diff_length = 10
                    # Init previous_x for the first time
                    self.previous_x = event.x()

                # Calculate # of pixels dragged
                drag_diff = self.previous_x - event.x()

                # update previous x
                self.previous_x = event.x()

                # Ignore small initial movements
                if abs(drag_diff) < self.diff_length:
                    # Lower threshold to 0 incrementally, to guarantee it'll eventually be exceeded
                    self.diff_length = max(0, self.diff_length - 1)
                    return

                # Compute size of property's possible values range
                min_max_range = float(property_max) - float(property_min)

                if min_max_range < 1000.0:
                    # Small range - use cursor to calculate new value as percentage of total range
                    self.new_value = property_min + (min_max_range * cursor_value_percent)
                else:
                    # range is unreasonably long (such as position, start, end, etc.... which can be huge #'s)

                    # Get the current value and apply fixed adjustments in response to motion
                    self.new_value = QLocale().system().toDouble(self.selected_item.text())[0]

                    if drag_diff > 0:
                        # Move to the left by a small amount
                        self.new_value -= 0.50
                    elif drag_diff < 0:
                        # Move to the right by a small amount
                        self.new_value += 0.50

                # Clamp value between min and max (just incase user drags too big)
                self.new_value = max(property_min, self.new_value)
                self.new_value = min(property_max, self.new_value)

That code should work exactly the same as the merged version — I'm not saying there's any functional difference at all, in fact it's the whole point that there isn't.

Anyway, since this is already merged I'll submit the change as a new PR. I still think it makes sense to do even though it won't affect anything from a user perspective. It's purely a question of code cleanliness and maintainability. (If nothing else, it'll mean that the next time I have Codacy scan the repo, I won't have to face its scowl of disapproval because the "Code duplication" stat crept up by however-many lines of code.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants