/* timeline-layout-helper.cpp - Implementation of the timeline layout helper class Copyright (C) Lumiera.org 2008, Joel Holdsworth This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. * *****************************************************/ #include #include "timeline-layout-helper.hpp" #include "../timeline-widget.hpp" #include "../../model/sequence.hpp" #include "../../util/rectangle.hpp" using namespace Gtk; using namespace std; using namespace boost; using namespace lumiera; using namespace util; using namespace gui::util; namespace gui { namespace widgets { namespace timeline { TimelineLayoutHelper::TimelineLayoutHelper(TimelineWidget &owner) : timelineWidget(owner), totalHeight(0), dragBranchHeight(0), animating(false) { // Init draggingTrackIter into a non-dragging state draggingTrackIter.node = NULL; } void TimelineLayoutHelper::clone_tree_from_sequence() { const shared_ptr &sequence = timelineWidget.sequence; REQUIRE(sequence); layoutTree.clear(); TrackTree::iterator_base iterator = layoutTree.set_head(sequence); add_branch(iterator, sequence); } TimelineLayoutHelper::TrackTree& TimelineLayoutHelper::get_layout_tree() { return layoutTree; } void TimelineLayoutHelper::add_branch( TrackTree::iterator_base parent_iterator, shared_ptr parent) { BOOST_FOREACH(shared_ptr child, parent->get_child_tracks()) { TrackTree::iterator_base child_iterator = layoutTree.append_child(parent_iterator, child); add_branch(child_iterator, child); } } optional TimelineLayoutHelper::get_track_header_rect( boost::weak_ptr track) { if(contains(headerBoxes, track)) { Gdk::Rectangle rect(headerBoxes[track]); rect.set_y(rect.get_y() - timelineWidget.get_y_scroll_offset()); return optional(rect); } return optional(); } shared_ptr TimelineLayoutHelper::header_from_point(Gdk::Point point) { // Apply the scroll offset point.set_y(point.get_y() + timelineWidget.get_y_scroll_offset()); // Search the headers std::pair, Gdk::Rectangle> pair; BOOST_FOREACH( pair, headerBoxes ) { // Hit test the rectangle if(util::pt_in_rect(point, pair.second)) return shared_ptr(pair.first); } // No track was found - return an empty pointer return shared_ptr(); } boost::shared_ptr TimelineLayoutHelper::track_from_y(int y) { // Apply the scroll offset y += timelineWidget.get_y_scroll_offset(); // Search the tracks std::pair, Gdk::Rectangle> pair; BOOST_FOREACH( pair, headerBoxes ) { // Hit test the rectangle const Gdk::Rectangle &rect = pair.second; if(y >= rect.get_y() && y < rect.get_y() + rect.get_height()) return shared_ptr(pair.first); } // No track was found - return an empty pointer return shared_ptr(); } shared_ptr TimelineLayoutHelper::begin_dragging_track( const Gdk::Point &mouse_point) { shared_ptr dragging_track = header_from_point(mouse_point); if(!dragging_track) return shared_ptr(); dragPoint = Gdk::Point(mouse_point.get_x(), mouse_point.get_y() + timelineWidget.get_y_scroll_offset()); const Gdk::Rectangle &rect = headerBoxes[dragging_track]; dragStartOffset = Gdk::Point( dragPoint.get_x() - rect.get_x(), dragPoint.get_y() - rect.get_y()); const shared_ptr model_track = dragging_track->get_model_track(); draggingTrackIter = iterator_from_track(model_track); dragBranchHeight = measure_branch_height(draggingTrackIter); dropPoint.relation = None; return dragging_track; } void TimelineLayoutHelper::end_dragging_track(bool apply) { if(apply) apply_drop_to_model_tree(dropPoint); draggingTrackIter.node = NULL; clone_tree_from_sequence(); update_layout(); } bool TimelineLayoutHelper::is_dragging_track() const { return draggingTrackIter.node != NULL; } TimelineLayoutHelper::TrackTree::pre_order_iterator TimelineLayoutHelper::get_dragging_track_iter() const { return draggingTrackIter; } void TimelineLayoutHelper::drag_to_point(const Gdk::Point &mouse_point) { DropPoint drop; // begin_dragging_track must have been called before REQUIRE(is_dragging_track()); // Apply the scroll offset const Gdk::Point last_point(dragPoint); dragPoint = Gdk::Point(mouse_point.get_x(), mouse_point.get_y() + timelineWidget.get_y_scroll_offset()); // Get a test-point // We probe on the bottom edge of the dragging branch if the track is // being dragged downward, and on the top edge if it's being dragged // upward. Gdk::Point test_point(dragPoint.get_x(), dragPoint.get_y() - dragStartOffset.get_y()); if(last_point.get_y() > dragPoint.get_y()) test_point.set_y(test_point.get_y()); else test_point.set_y(test_point.get_y() + dragBranchHeight); // Search the headers TrackTree::pre_order_iterator iterator; for(iterator = ++layoutTree.begin(); iterator != layoutTree.end(); iterator++) { // Skip the dragging branch if(iterator == draggingTrackIter) iterator.skip_children(); else { // Do hit test drop = attempt_drop(iterator, test_point); if(drop.relation != None) break; } } // Did we get a drop point? if(drop.relation != None) { REQUIRE(*drop.target); shared_ptr target_timeline_track = lookup_timeline_track(*drop.target); apply_drop_to_layout_tree(drop); dropPoint = drop; if((drop.relation == FirstChild || drop.relation == LastChild) && !target_timeline_track->get_expanded()) { target_timeline_track->expand_collapse(Track::Expand); } } update_layout(); } int TimelineLayoutHelper::get_total_height() const { ENSURE(totalHeight >= 0); return totalHeight; } bool TimelineLayoutHelper::is_animating() const { return animating; } TimelineLayoutHelper::TrackTree::pre_order_iterator TimelineLayoutHelper::iterator_from_track( boost::shared_ptr model_track) { REQUIRE(model_track); TrackTree::pre_order_iterator iter; for(iter = layoutTree.begin(); iter != layoutTree.end(); iter++) { if(*iter == model_track) break; } return iter; } int TimelineLayoutHelper::measure_branch_height( TrackTree::iterator_base parent_iterator) { shared_ptr parent_track = lookup_timeline_track(*parent_iterator); int branch_height = parent_track->get_height() + TimelineWidget::TrackPadding; // Add the heights of child tracks if this parent is expanded if(parent_track->get_expanded()) { TrackTree::sibling_iterator iterator; for(iterator = layoutTree.begin(parent_iterator); iterator != layoutTree.end(parent_iterator); iterator++) { shared_ptr child_track = lookup_timeline_track(*iterator); branch_height += measure_branch_height(iterator); } } return branch_height; } void TimelineLayoutHelper::update_layout() { // Reset the animation state value, before it gets recalculated animating = false; // Clear previously cached layout headerBoxes.clear(); // Do the layout const int header_width = TimelineWidget::HeaderWidth; const int indent_width = TimelineWidget::HeaderIndentWidth; totalHeight = layout_headers_recursive(layoutTree.begin(), 0, header_width, indent_width, 0, true); // Signal that the layout has changed timelineWidget.on_layout_changed(); // Begin animating as necessary if(animating && !animationTimer) begin_animation(); } int TimelineLayoutHelper::layout_headers_recursive( TrackTree::iterator_base parent_iterator, const int branch_offset, const int header_width, const int indent_width, const int depth, const bool parent_expanded) { REQUIRE(depth >= 0); const bool dragging = is_dragging_track(); int child_offset = 0; TrackTree::sibling_iterator iterator; for(iterator = layoutTree.begin(parent_iterator); iterator != layoutTree.end(parent_iterator); iterator++) { Gdk::Rectangle rect; int track_height = 0; const shared_ptr &model_track = *iterator; REQUIRE(model_track); shared_ptr timeline_track = lookup_timeline_track(model_track); // Is this the root track of a dragging branch? bool being_dragged = false; if(dragging) being_dragged = (model_track == *draggingTrackIter); // Is the track going to be shown? if(parent_expanded) { // Calculate and store the box of the header track_height = timeline_track->get_height() + TimelineWidget::TrackPadding; const int indent = depth * indent_width; rect = Gdk::Rectangle( indent, // x branch_offset + child_offset, // y max( header_width - indent, 0 ), // width track_height); // height // Offset for the next header child_offset += track_height; // Is this header being dragged? if(being_dragged) rect.set_y(dragPoint.get_y() - dragStartOffset.get_y()); headerBoxes[timeline_track] = rect; } // Is the track animating? const bool is_track_animating = timeline_track->is_expand_animating(); animating |= is_track_animating; // Recurse to children const bool expand_child = (animating || timeline_track->get_expanded()) && parent_expanded; int child_branch_height = layout_headers_recursive( iterator, rect.get_y() + track_height, header_width, indent_width, depth + 1, expand_child); // Do collapse animation as necessary if(is_track_animating) { // Calculate the height of the area which will be // shown as expanded const float a = timeline_track->get_expand_animation_state(); child_branch_height *= a * a; const int y_limit = branch_offset + child_offset + child_branch_height; // Obscure tracks according to the animation state TrackTree::pre_order_iterator descendant_iterator(iterator); descendant_iterator++; TrackTree::sibling_iterator end(iterator); end++; for(descendant_iterator = layoutTree.begin(parent_iterator); descendant_iterator != end; descendant_iterator++) { const weak_ptr track = lookup_timeline_track(*descendant_iterator); const Gdk::Rectangle &rect = headerBoxes[track]; if(rect.get_y() + rect.get_height() > y_limit) headerBoxes.erase(track); } // Tick the track expand animation timeline_track->tick_expand_animation(); } child_offset += child_branch_height; } return child_offset; } shared_ptr TimelineLayoutHelper::lookup_timeline_track( shared_ptr model_track) { REQUIRE(model_track != NULL); shared_ptr timeline_track = timelineWidget.lookup_timeline_track(model_track); ENSURE(timeline_track); return timeline_track; } void TimelineLayoutHelper::begin_animation() { animationTimer = Glib::signal_idle().connect( sigc::mem_fun(this, &TimelineLayoutHelper::on_animation_tick), Glib::PRIORITY_DEFAULT); } bool TimelineLayoutHelper::on_animation_tick() { update_layout(); return animating; } TimelineLayoutHelper::DropPoint TimelineLayoutHelper::attempt_drop(TrackTree::pre_order_iterator target, const Gdk::Point &point) { // Lookup the tracks const shared_ptr model_track(*target); REQUIRE(model_track); const weak_ptr timeline_track = lookup_timeline_track(model_track); // Calculate coordinates const Gdk::Rectangle &rect = headerBoxes[timeline_track]; const int half_height = rect.get_height() / 2; const int y = rect.get_y(); const int y_mid = y + half_height; const int full_width = rect.get_x() + rect.get_width(); const int x_mid = rect.get_x() + rect.get_width() / 2; // Initialize the drop // By specifying relation = None, the default return value will signal // no drop-point was foind at point DropPoint drop = {target, None}; if(pt_in_rect(point, Gdk::Rectangle(0, y, full_width, half_height))) { // We're hovering over the upper half of the header drop.relation = Before; } else if(pt_in_rect(point, Gdk::Rectangle(0, y_mid, full_width, half_height))) { // We're hovering over the lower half of the header if(model_track->can_host_children()) { if(model_track->get_child_tracks().empty()) { // Is our track being dragged after this header? if(dragPoint.get_x() < x_mid) drop.relation = After; else drop.relation = FirstChild; } else drop.relation = LastChild; } else { // When this track cannot be a parent, the dragging track is // simply dropped after drop.relation = After; } } return drop; } void TimelineLayoutHelper::apply_drop_to_layout_tree( const TimelineLayoutHelper::DropPoint &drop) { switch(drop.relation) { case None: break; case Before: draggingTrackIter = layoutTree.move_before( drop.target, draggingTrackIter); break; case After: draggingTrackIter = layoutTree.move_after( drop.target, draggingTrackIter); break; case FirstChild: if(draggingTrackIter.node->parent != drop.target.node) { draggingTrackIter = layoutTree.move_ontop( layoutTree.prepend_child(drop.target), draggingTrackIter); } break; case LastChild: if(draggingTrackIter.node->parent != drop.target.node) { draggingTrackIter = layoutTree.move_ontop( layoutTree.append_child(drop.target), draggingTrackIter); } break; default: ASSERT(0); // Unexpected value of relation break; } } void TimelineLayoutHelper::apply_drop_to_model_tree( const TimelineLayoutHelper::DropPoint &drop) { if(drop.relation == None) return; // Freeze the timeline widget - it must be done manually later timelineWidget.freeze_update_tracks(); // Get the tracks shared_ptr &dragging_track = *draggingTrackIter; REQUIRE(dragging_track); REQUIRE(dragging_track != timelineWidget.sequence); shared_ptr &target_track = *drop.target; REQUIRE(target_track); REQUIRE(target_track != timelineWidget.sequence); // Detach the track from the old parent shared_ptr old_parent = timelineWidget.sequence->find_descendant_track_parent( dragging_track); REQUIRE(old_parent); // The track must have a parent old_parent->get_child_track_list().remove(dragging_track); if(drop.relation == Before || drop.relation == After) { // Find the new parent track shared_ptr new_parent = timelineWidget.sequence->find_descendant_track_parent( target_track); REQUIRE(new_parent); // The track must have a parent // Find the destination point observable_list< shared_ptr > &dest = new_parent->get_child_track_list(); list< shared_ptr >::iterator iter; for(iter = dest.begin(); iter != dest.end(); iter++) { if(*iter == target_track) break; } REQUIRE(iter != dest.end()); // The target must be // in the destination // We have to jump on 1 if we want to insert after if(drop.relation == After) iter++; // Insert at this point dest.insert(iter, dragging_track); } else if(drop.relation == FirstChild || drop.relation == LastChild) { shared_ptr new_parent = dynamic_pointer_cast( target_track); REQUIRE(new_parent); // The track must have a parent if(drop.relation == FirstChild) new_parent->get_child_track_list().push_front(dragging_track); else if(drop.relation == LastChild) new_parent->get_child_track_list().push_back(dragging_track); } else ASSERT(0); // Unexpected value of relation // Freeze the timeline widget - we will do it manually timelineWidget.freeze_update_tracks(); } } // namespace timeline } // namespace widgets } // namespace gui