Friday, May 30, 2008

Implementing Reorderable List

pitivi.timeline.composition.Composition provides the following relevant methods:
  • getSimpleSourcePosition
  • addSource
  • appendSource
  • moveSource
  • removeSource
  • insertSourceAfter
There are also these relevant signals:
  • condensed-list-changed
  • source-added
  • source-removed
The goal is to write code which uses
  1. calls insertSourceAfter() when an user drops item to timeline
  2. calls removeSource() when user deletes item from timeline
  3. calls moveSource() when user moves a source to a new position
  4. responds to the "source_added" signal by creating a new source widget and adding it to the timeline
  5. responds to the "source_removed" signal by removing its widget from the timeline
  6. responds to the "condensed_list_changed" signal by updating items to their new position
User Interaction: Two Approaches

When the user clicks on an item in the timeline, the source will be moved to the top layer of the canvas (so that it shows above other objects as it is moved. The user can then drag it freely in the x direction within the bounds of the canvas, but it will be completely constrained in the y direction.

If the object is moved beyond a certain threshold to the right or left, the position of the nearest object to the one being moved will be swapped (i.e. the order of the objects will change, but the object being dragged will still be under the control of the mouse). The operation finishes when the user releases the mouse.

Two ways to do this:
  1. When the threshold value is reached, simply call moveSource() to exchange the positions of both sources. The advantage of this approach is that the UI updates automatically when the composition emits the "condensed-list-changed" signal. One possibly negative side-effect is that the object the user is dragging will suddenly "jump" in to position. Another potential side-effect is that the "condensed-list-changed" signal might be delayed for some reason, leaving the timeline in a sorry state even after the user releases the mouse.
  2. When the threshold value is reached, only swap the sources visually. Save the proposed changes until after the user releases the mouse. Then call moveSource() to move the source. This avoids the "jumping" side effect, but introduces a different problem. The visual timeline and back end will be temporarily inconsistent. If the "consensed-list-changed" signal is delayed, then the user's changes are incorrectly displayed. We can get around this by automatically restoring the list to its original state after the user releases the mouse. This could also be confusing: the user sees their clips move into a new position, then move back to their original position after releasing the mouse, then move back to the new position after the signal arrives.
I guess I'll just have to pick one approach to try and see if my fears are really grounded. Any feedback would be appreciated.

My goal for my first commit is to come up with a drop-in replacement for the existing timeline. But, in the interest of being forward thinking, there's a design constraint here I almost forgot to consider: Transition widgets. Handling child widgets of different sizes and aspect ratios will not be so hard, but transitions are only supposed to go between two sources. How can I easily enforce this constraint? Maybe by fiddling with the threshold values, but remember that transitions are optional. I could use a mandatory space between sources, but to be honest I really hate that idea. I'll sleep on it for now. One thing at a time.


Laszlo said...

I had to solve the same problem in Jokosher. The different with our audio multi-track program is that audio clips cannot overlap, so if they try to drag the clip past another one it will hit against the side of the other clip and stop. Once they have moved the mouse more than halfway over the other clip, the clip being dragged will jump to the other side of it.

How this is implemented internally, is when the mouse drag starts, we move the widget, but we don't tell Jokosher core. That means, there are no updates, and only the widget that is being moved knows its position has changed.

As soon as the user releases the mouse, we send a move to Jokosher core, which updates the gnonlin start and stop properties and sends a "move" signal. The GUI will receive the move signal, and move the widget to its new position -- this would normally cause a jump which would confuse the user, but since that widget being moved is already in that position (it was dragged there by the mouse), it doesn't actually move at all.

This is how we avoid the jumping of widgets in Jokosher. I'm not sure how much of this applies because audio it a little different (we don't have transitions), but we are certainly solving similar problems.

brandon lewis said...

I agree it's very similar. I think this approach is fine so long as there will always be a signal emitted from the core. I guess my concern is the chaos which might ensue from a signal that is delayed or never emitted at all.

bilboed said...

signals are never delayed.

The following sequence happens:
(1) mouse event
(2) call core method
(3) core method does a modification
(4a) if modification is possible emit signal and return True
(4b) if modification is not possible, don't emit signal, but return False.

You callback for the signal emitted in 4a will do the drawing. You'll notice it's done before returning, in the same thread. It's all synchronous.