/* timeline-widget.cpp - Implementation of the timeline widget 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 "timeline-widget.hpp" #include #include using namespace Gtk; using namespace std; using namespace boost; using namespace util; using namespace gui::widgets::timeline; namespace gui { namespace widgets { const int TimelineWidget::TrackPadding = 1; const int TimelineWidget::HeaderWidth = 150; const int TimelineWidget::HeaderIndentWidth = 10; const double TimelineWidget::ZoomIncrement = 1.25; const int64_t TimelineWidget::MaxScale = 30000000; TimelineWidget::TimelineWidget( shared_ptr source_sequence) : Table(2, 2), sequence(source_sequence), layoutHelper(*this), viewWindow(this, 0, 1), selectionStart(0), selectionEnd(0), playbackPeriodStart(0), playbackPeriodEnd(0), playbackPoint(GAVL_TIME_UNDEFINED), horizontalAdjustment(0, 0, 0), verticalAdjustment(0, 0, 0), horizontalScroll(horizontalAdjustment), verticalScroll(verticalAdjustment) { REQUIRE(sequence); body = new TimelineBody(*this); ENSURE(body != NULL); headerContainer = new TimelineHeaderContainer(*this); ENSURE(headerContainer != NULL); ruler = new TimelineRuler(*this); ENSURE(ruler != NULL); horizontalAdjustment.signal_value_changed().connect( sigc::mem_fun( this, &TimelineWidget::on_scroll) ); verticalAdjustment.signal_value_changed().connect( sigc::mem_fun( this, &TimelineWidget::on_scroll) ); body->signal_motion_notify_event().connect( sigc::mem_fun( this, &TimelineWidget::on_motion_in_body_notify_event) ); viewWindow.changed_signal().connect( sigc::mem_fun( this, &TimelineWidget::on_view_window_changed) ); viewWindow.set_time_scale(GAVL_TIME_SCALE / 200); set_selection(2000000, 4000000); update_tracks(); attach(*body, 1, 2, 1, 2, FILL|EXPAND, FILL|EXPAND); attach(*ruler, 1, 2, 0, 1, FILL|EXPAND, SHRINK); attach(*headerContainer, 0, 1, 1, 2, SHRINK, FILL|EXPAND); attach(horizontalScroll, 1, 2, 2, 3, FILL|EXPAND, SHRINK); attach(verticalScroll, 2, 3, 1, 2, SHRINK, FILL|EXPAND); set_tool(timeline::Arrow); // Receive notifications of changes to the tracks sequence->get_child_track_list().signal_changed().connect( sigc::mem_fun( this, &TimelineWidget::on_track_list_changed ) ); } TimelineWidget::~TimelineWidget() { // Destroy child widgets REQUIRE(body != NULL); if(body != NULL) body->unreference(); REQUIRE(headerContainer != NULL); if(headerContainer != NULL) headerContainer->unreference(); REQUIRE(ruler != NULL); if(ruler != NULL) ruler->unreference(); } /* ===== Data Access ===== */ TimelineViewWindow& TimelineWidget::get_view_window() { return viewWindow; } gavl_time_t TimelineWidget::get_selection_start() const { return selectionStart; } gavl_time_t TimelineWidget::get_selection_end() const { return selectionEnd; } void TimelineWidget::set_selection(gavl_time_t start, gavl_time_t end, bool reset_playback_period) { REQUIRE(ruler != NULL); REQUIRE(body != NULL); if(start < end) { selectionStart = start; selectionEnd = end; } else { // The selection is back-to-front, flip it round selectionStart = end; selectionEnd = start; } if(reset_playback_period) { playbackPeriodStart = selectionStart; playbackPeriodEnd = selectionEnd; } ruler->queue_draw(); body->queue_draw(); } gavl_time_t TimelineWidget::get_playback_period_start() const { return playbackPeriodStart; } gavl_time_t TimelineWidget::get_playback_period_end() const { return playbackPeriodEnd; } void TimelineWidget::set_playback_period(gavl_time_t start, gavl_time_t end) { REQUIRE(ruler != NULL); REQUIRE(body != NULL); if(start < end) { playbackPeriodStart = start; playbackPeriodEnd = end; } else { // The period is back-to-front, flip it round playbackPeriodStart = end; playbackPeriodEnd = start; } ruler->queue_draw(); body->queue_draw(); } void TimelineWidget::set_playback_point(gavl_time_t point) { playbackPoint = point; ruler->queue_draw(); body->queue_draw(); } gavl_time_t TimelineWidget::get_playback_point() const { return playbackPoint; } ToolType TimelineWidget::get_tool() const { REQUIRE(body != NULL); return body->get_tool(); } void TimelineWidget::set_tool(ToolType tool_type) { REQUIRE(body != NULL); body->set_tool(tool_type); } shared_ptr TimelineWidget::get_hovering_track() const { return hoveringTrack; } /* ===== Signals ===== */ sigc::signal TimelineWidget::mouse_hover_signal() const { return mouseHoverSignal; } sigc::signal TimelineWidget::playback_period_drag_released_signal() const { return playbackPeriodDragReleasedSignal; } sigc::signal > TimelineWidget::hovering_track_changed_signal() const { return hoveringTrackChangedSignal; } /* ===== Events ===== */ void TimelineWidget::on_scroll() { viewWindow.set_time_offset(horizontalAdjustment.get_value()); } void TimelineWidget::on_size_allocate(Allocation& allocation) { Widget::on_size_allocate(allocation); update_scroll(); } void TimelineWidget::on_view_window_changed() { REQUIRE(ruler != NULL); const int view_width = body->get_allocation().get_width(); horizontalAdjustment.set_page_size( viewWindow.get_time_scale() * view_width); horizontalAdjustment.set_value( viewWindow.get_time_offset()); } void TimelineWidget::on_add_track_command() { REQUIRE(sequence); // # TEST CODE sequence->get_child_track_list().push_back( shared_ptr(new model::ClipTrack())); } /* ===== Internals ===== */ void TimelineWidget::update_tracks() { REQUIRE(sequence); // Remove any tracks which are no longer present in the model remove_orphaned_tracks(); // Create timeline tracks from all the model tracks create_timeline_tracks(); // Update the header container REQUIRE(headerContainer != NULL); headerContainer->show_all_children(); headerContainer->update_headers(); // Update the layout helper layoutHelper.clone_tree_from_sequence(); layoutHelper.update_layout(); } void TimelineWidget::create_timeline_tracks() { REQUIRE(sequence); REQUIRE(headerContainer != NULL); REQUIRE(body != NULL); BOOST_FOREACH(shared_ptr child, sequence->get_child_tracks()) create_timeline_tracks_from_branch(child); } void TimelineWidget::create_timeline_tracks_from_branch( shared_ptr model_track) { REQUIRE(model_track); // Is a timeline UI track present in the map already? if(!contains(trackMap, model_track)) { // The timeline UI track is not present // We will need to create one trackMap[model_track] = create_timeline_track_from_model_track(model_track); } // Recurse to child tracks BOOST_FOREACH(shared_ptr child, model_track->get_child_tracks()) create_timeline_tracks_from_branch(child); } shared_ptr TimelineWidget::create_timeline_track_from_model_track( shared_ptr model_track) { REQUIRE(model_track); // Choose a corresponding timeline track class from the model track's // class if(typeid(*model_track) == typeid(model::ClipTrack)) return shared_ptr(new timeline::ClipTrack( *this, model_track)); else if(typeid(*model_track) == typeid(model::GroupTrack)) return shared_ptr(new timeline::GroupTrack( *this, dynamic_pointer_cast(model_track))); ASSERT(NULL); // Unknown track type; return shared_ptr(); } void TimelineWidget::remove_orphaned_tracks() { REQUIRE(sequence); REQUIRE(headerContainer != NULL); REQUIRE(body != NULL); std::map, boost::shared_ptr > orphan_track_map(trackMap); // Remove all tracks which are still present in the sequence BOOST_FOREACH(shared_ptr child, sequence->get_child_tracks()) search_orphaned_tracks_in_branch(child, orphan_track_map); // orphan_track_map now contains all the orphaned tracks // Remove them std::pair, shared_ptr > pair; BOOST_FOREACH( pair, orphan_track_map ) { ENSURE(pair.first); trackMap.erase(pair.first); } } void TimelineWidget::search_orphaned_tracks_in_branch( boost::shared_ptr model_track, std::map, boost::shared_ptr > &orphan_track_map) { REQUIRE(model_track); // Is the timeline UI still present? if(contains(orphan_track_map, model_track)) orphan_track_map.erase(model_track); // Recurse to child tracks BOOST_FOREACH(shared_ptr child, model_track->get_child_tracks()) search_orphaned_tracks_in_branch(child, orphan_track_map); } shared_ptr TimelineWidget::lookup_timeline_track( shared_ptr model_track) const { REQUIRE(sequence); REQUIRE(model_track); REQUIRE(model_track != sequence); // The sequence isn't really a track std::map, shared_ptr >:: const_iterator iterator = trackMap.find(model_track); if(iterator == trackMap.end()) { // The track is not present in the map // We are in an error condition if the timeline track is not found // - the timeline tracks must always be synchronous with the model // tracks. ENSURE(0); return shared_ptr(); } ENSURE(iterator->second != NULL); return iterator->second; } void TimelineWidget::on_layout_changed() { REQUIRE(headerContainer != NULL); REQUIRE(body != NULL); headerContainer->on_layout_changed(); body->queue_draw(); } void TimelineWidget::update_scroll() { REQUIRE(body != NULL); const Allocation body_allocation = body->get_allocation(); //----- Horizontal Scroll ------// // TEST CODE horizontalAdjustment.set_upper(1000 * GAVL_TIME_SCALE / 200); horizontalAdjustment.set_lower(-1000 * GAVL_TIME_SCALE / 200); // Set the page size horizontalAdjustment.set_page_size( viewWindow.get_time_scale() * body_allocation.get_width()); //----- Vertical Scroll -----// // Calculate the vertical length that can be scrolled: // the total height of all the tracks minus one screenful int y_scroll_length = layoutHelper.get_total_height() - body_allocation.get_height(); if(y_scroll_length < 0) y_scroll_length = 0; // If by resizing we're now over-scrolled, scroll back to // maximum distance if((int)verticalAdjustment.get_value() > y_scroll_length) verticalAdjustment.set_value(y_scroll_length); verticalAdjustment.set_upper(y_scroll_length); // Hide the scrollbar if no scrolling is possible #if 0 // Having this code included seems to cause a layout loop as the // window is shrunk if(y_scroll_length <= 0 && verticalScroll.is_visible()) verticalScroll.hide(); else if(y_scroll_length > 0 && !verticalScroll.is_visible()) verticalScroll.show(); #endif } int TimelineWidget::get_y_scroll_offset() const { return (int)verticalAdjustment.get_value(); } bool TimelineWidget::on_motion_in_body_notify_event(GdkEventMotion *event) { REQUIRE(event != NULL); ruler->set_mouse_chevron_offset(event->x); mouseHoverSignal.emit(viewWindow.x_to_time(event->x)); return true; } void TimelineWidget::on_track_list_changed() { update_tracks(); } void TimelineWidget::on_playback_period_drag_released() { playbackPeriodDragReleasedSignal.emit(); } void TimelineWidget::set_hovering_track( shared_ptr hovering_track) { hoveringTrack = hovering_track; hoveringTrackChangedSignal.emit(hovering_track); } } // namespace widgets } // namespace gui