2 * mvSequencer.js Created on Oct 17, 2007
4 * All Metavid Wiki code is Released under the GPL2
5 * for more info visit http://metavid.org/wiki/Code
8 * @email mdale@wikimedia.org
10 * Further developed in open source development partnership with kaltura.
11 * more info at http://kaltura.com & http://kaltura.org
14 * is a basic embeddeble sequencer.
15 * extends the playlist with drag/drop/sortable/add/remove functionality
16 * editing of annotative content (mostly for wiki)
17 * enables more dynamic layouts
18 * exports back out to json or inline format
22 "mwe-menu_clipedit" : "Edit media",
23 "mwe-menu_transition" : "Transitions and effects",
24 "mwe-menu_cliplib" : "Add media",
25 "mwe-menu_resource_overview" : "Resource overview",
26 "mwe-menu_options" : "Options",
27 "mwe-loading_timeline" : "Loading timeline ...",
28 "mwe-loading_user_rights" : "Loading user rights ...",
29 "mwe-no_edit_permissions" : "You do not have permissions to save changes to this sequence",
30 "mwe-edit_clip" : "Edit clip",
31 "mwe-edit_save" : "Save sequence changes",
32 "mwe-saving_wait" : "Save in progress (please wait)",
33 "mwe-save_done" : "Save complete",
34 "mwe-edit_cancel" : "Cancel sequence edit",
35 "mwe-edit_cancel_confirm" : "Are you sure you want to cancel your edit? Changes will be lost.",
36 "mwe-zoom_in" : "Zoom in",
37 "mwe-zoom_out" : "Zoom out",
38 "mwe-cut_clip" : "Cut clips",
39 "mwe-expand_track" : "Expand track",
40 "mwe-collapse_track" : "Collapse track",
41 "mwe-play_from_position" : "Play from playline position",
42 "mwe-pixle2sec" : "pixels to seconds",
43 "mwe-rmclip" : "Remove clip",
44 "mwe-clip_in" : "clip in",
45 "mwe-clip_out" : "clip out",
46 "mwe-welcome_to_sequencer" : "<h3>Welcome to the sequencer demo<\/h3> Very <b>limited<\/b> functionality right now. Not much documentation yet either.",
47 "mwe-no_selected_resource" : "<h3>No resource selected<\/h3> Select a clip to enable editing.",
48 "mwe-error_edit_multiple" : "<h3>Multiple resources selected<\/h3> Select a single clip to edit it.",
49 "mwe-editor_options" : "Editor options",
50 "mwe-editor_mode" : "Editor mode",
51 "mwe-simple_editor_desc" : "simple editor (iMovie style)",
52 "mwe-advanced_editor_desc" : "advanced editor (Final Cut style)",
53 "mwe-other_options" : "Other options",
54 "mwe-contextmenu_opt" : "Enable context menus",
55 "mwe-sequencer_credit_line" : "Developed by <a href=\"http:\/\/kaltura.com\">Kaltura, Inc.<\/a> in partnership with the <a href=\"http:\/\/wikimediafoundation.org\/wiki\/Home\">Wikimedia Foundation<\/a> (<a href=\"#\">more information<\/a>)."
57 // used to set default values and validate the passed init object
58 var sequencerDefaultValues = {
60 instance_name:'mvSeq', // for now only one instance by name mvSeq is allowed
62 target_sequence_container:null,// text value (so that its a valid property)
63 target_form_text: null,
65 // what is our save mode:
66 // can save to 'api' url or 'form'
69 video_container_id:'mv_video_container',
74 sequence_tools_id:'mv_sequence_tools',
75 timeline_id:'mv_timeline',
79 // In pixel to second ratio ie 100pixles for every ~30seconds
82 // Default timeline duration in seconds
83 timeline_duration:500,
86 track_thumb_height:60,
89 // Default timeline mode: "story" (i-movie like) or "time" (finalCut like)
90 timeline_mode:'storyboard',
92 // How large are the i-movie type clips
93 track_clipThumb_height:80,
95 // Default time to subtract or add when adjusting clips.
98 // Default clipboard is empty:
99 clipboard:new Array(),
101 // Stores the clipboard edit token (if user has rights to edit their User page)
102 clipboardEditToken:null,
104 // Stores the sequence edit token (if user has rights to edit the current sequence)
105 sequenceEditToken:null,
107 // The time the sequence was last touched (grabbed at time of startup)
108 sequenceTouchedTime:null,
110 // the default config for the add media wizard
114 inline_playlist:'null',
115 inline_playlist_id:'null',
118 // The edit stack (so that you can "undo" edits)
119 edit_stack:new Array(),
125 var mvSequencer = function( iObj ) {
126 return this.init( iObj );
128 // Set up the mvSequencer object
129 mvSequencer.prototype = {
130 // The menu_items Object contains: default html, js setup/loader functions
135 'js': function( this_seq ) {
136 this_seq.doEditSelectedClip();
138 'click_js':function( this_seq ) {
139 this_seq.doEditSelectedClip();
144 'html' : '<h3>' + gM( 'mwe-menu_transition' ) + '</h3>',
145 'js':function( this_seq ) {
146 this_seq.doEditTransitionSelectedClip();
148 'click_js':function( this_seq ) {
149 // Highlight the transition of the selected clip:
150 this_seq.doEditTransitionSelectedClip();
155 'html': gM( 'mwe-loading_txt' ),
156 'js':function( this_seq ) {
157 // Load the search interface with sequence tool targets
159 'remoteSearchDriver',
160 'seqRemoteSearchDriver'
162 this_seq.mySearch = new seqRemoteSearchDriver( this_seq );
163 this_seq.mySearch.doInitDisplay();
169 'html' : '<h3>' + gM( 'mwe-menu_options' ) + '</h3>' +
170 gM( 'mwe-editor_mode' ) + '<br> ' +
171 '<blockquote><input type="radio" value="simple_editor" name="opt_editor">' +
172 gM( 'mwe-simple_editor_desc' ) + ' </blockquote>' +
173 '<blockquote><input type="radio" value="advanced_editor" name="opt_editor">' +
174 gM( 'mwe-advanced_editor_desc' ) + ' </blockquote>' +
175 gM( 'mwe-other_options' ) + '<br>' +
176 '<blockquote><input type="checkbox" value="contextmenu_opt" name="contextmenu_opt">' +
177 gM( 'mwe-contextmenu_opt' ) + ' </blockquote>',
178 'js':function( this_seq ) {
179 $j( '#options_ic input[value=\'simple_editor\']' ).attr( {
180 'checked':( this_seq.timeline_mode == 'storyboard' ) ? true:false
181 } ).click( function() {
182 this_seq.doSimpleTl();
184 $j( '#options_ic input[value=\'advanced_editor\']' ).attr( {
185 'checked':( this_seq.timeline_mode == 'time' ) ? true:false
186 } ).click( function() {
187 this_seq.doAdvancedTl();
189 // set up the options for context menus
194 // set up initial key states:
195 key_shift_down:false,
199 init:function( iObj ) {
200 // set up pointer to this_seq for current scope:
202 // set the default values:
203 for ( var i in sequencerDefaultValues ) {
204 this[ i ] = sequencerDefaultValues[i];
206 for ( var i in iObj ) {
207 // js_log('on '+ i + ' :' + iObj[i]);
208 if ( typeof sequencerDefaultValues[i] != 'undefined' ) { // make sure its a valid property
213 // check for sequence_container
214 if ( $j( this.target_sequence_container ).length === 0 ) {
215 js_log( "Error: missing target_sequence_container" );
219 // $j(this.target_sequence_container).css('position', 'relative');
220 this['base_width'] = $j( this.target_sequence_container ).width();
221 this['base_height'] = $j( this.target_sequence_container ).height();
223 // add the container divs (with basic layout ~universal~
224 $j( this.target_sequence_container ).html( '' +
225 '<div id="' + this.video_container_id + '" style="position:absolute;right:0px;top:0px;' +
226 'width:' + this.video_width + 'px;height:' + ( this.video_height + 54 ) + 'px;"/>' +
227 '<div id="' + this.timeline_id + '" class="ui-widget ui-widget-content ui-corner-all" style="position:absolute;' +
228 'left:0px;right:0px;top:' + ( this.video_height + 60 ) + 'px;bottom:20px;overflow:auto;">' +
229 gM( 'mwe-loading_timeline' ) + '</div>' +
230 '<div class="seq_status" style="position:absolute;left:0px;width:300px;"></div>' +
231 '<div class="seq_save_cancel" style="position:absolute;' +
232 'left:5px;bottom:0px;height:15px;">' +
233 gM( 'mwe-loading_user_rights' ) +
235 '<div class="about_editor" style="position:absolute;right:5px;bottom:0px;">' +
236 gM( 'mwe-sequencer_credit_line' ) +
238 '<div id="' + this.sequence_tools_id + '" style="position:absolute;' +
239 'left:0px;right:' + ( this.video_width + 10 ) + 'px;top:0px;height:' + ( this.video_height + 47 ) + 'px;"/>'
244 /*js_log('set: '+this.target_sequence_container + ' html to:'+ "\n"+
245 $j(this.target_sequence_container).html()
248 // first check if we got a cloned PL object:
249 // (when the editor is invoked with the plalylist already on the page)
250 /*if( this.plObj != 'null' ){
251 js_log('found plObj clone');
252 //extend with mvSeqPlayList object:
253 this.plObj = new mvSeqPlayList(this.plObj);
254 js_log('mvSeqPlayList added: ' + this.plObj.org_control_height );
255 $j('#'+this.video_container_id).get(0).attachNode( this.plObj );
256 this.plObj.getHTML();
257 this.checkReadyPlObj();
261 // else check for source based sequence editor (a clean page load of the editor)
262 if ( this.mv_pl_src != 'null' ) {
263 js_log( ' pl src:: ' + this.mv_pl_src );
264 var src_attr = ' src="' + this.mv_pl_src + '" ';
266 js_log( ' null playlist src .. (start empty) ' );
269 $j( '#' + this.video_container_id ).html( '<playlist ' + src_attr +
270 ' style="width:' + this.video_width + 'px;height:' + this.video_height + 'px;" ' +
271 ' sequencer="true" id="' + this.plObj_id + '" />' );
272 rewrite_by_id( this.plObj_id );
273 setTimeout( this.instance_name + '.checkReadyPlObj()', 25 );
275 updateSeqSaveButtons:function() {
277 if ( this.sequenceEditToken ) {
278 $j( this.target_sequence_container + ' .seq_save_cancel' ).html(
279 $j.btnHtml( gM( 'mwe-edit_save' ), 'seq_edit_save', 'close' ) + ' ' +
280 $j.btnHtml( gM( 'mwe-edit_cancel' ), 'seq_edit_cancel', 'close' )
283 $j( this.target_sequence_container + ' .seq_save_cancel' ).html( cancel_button + gM( 'mwe-no_edit_permissions' ) );
286 $j( this.target_sequence_container + ' .seq_edit_cancel' ).unbind().click( function() {
287 var x = window.confirm( gM( 'mwe-edit_cancel_confirm' ) );
289 _this.closeModEditor();
291 // close request canceled.
294 $j( this.target_sequence_container + ' .seq_edit_save' ).unbind().click( function() {
295 // pop up progress dialog ~requesting edit line summary~
296 // remove any other save dialog
297 $j( '#seq_save_dialog' ).remove();
298 $j( 'body' ).append( '<div id="seq_save_dialog" title="' + gM( 'mwe-edit_save' ) + '">' +
299 '<span class="mw-summary">' +
300 '<label for="seq_save_summary">Edit summary: </label>' +
302 '<input id="seq_save_summary" tabindex="1" maxlength="200" value="" size="30" name="seq_save_summary"/>' +
305 bConf[ gM( 'mwe-cancel' ) ] = function() {
306 $j( this ).dialog( 'close' );
308 bConf[ gM( 'mwe-edit_save' ) ] = function() {
311 'title' : _this.plObj.mTitle,
312 // the text is the sequence XML + the description
313 'text' : _this.getSeqOutputHLRDXML() + "\n" +
314 _this.plObj.wikiDesc,
315 'token' : _this.sequenceEditToken,
316 'summary' : $j( '#seq_save_summary' ).val()
318 // change to progress bar and save:
319 $j( '#seq_save_dialog' ).html( '<div class="progress" /><br>' +
320 gM( 'mwe-saving_wait' )
322 $j( '#seq_save_dialog .progress' ).progressbar( {
325 // run the Seq Save Request:
328 'url' : _this.getLocalApiUrl()
329 }, function( data ) {
330 $j( '#seq_save_dialog' ).html( gM( 'mwe-save_done' ) );
331 $j( '#seq_save_dialog' ).dialog( 'option',
335 window.location.reload();
337 "Do More Edits": function() {
338 $j( this ).dialog( "close" );
344 $j( '#seq_save_dialog' ).dialog( {
352 // display a menu item (hide the rest)
353 disp:function( item, dispCall ) {
354 js_log( 'menu_item disp: ' + item );
355 this.disp_menu_item = item;
356 // update the display and item state:
357 if ( this.menu_items[item] ) {
358 // update the tabs display:
360 $j( "#seq_menu" ).tabs( 'select', this.menu_items[item].inx );
362 this.menu_items[item].default = 1;
363 // do any click_js actions:getInsertControl
364 if ( this.menu_items[item].click_js )
365 this.menu_items[item].click_js( this );
368 // setup the menu items:
369 setupMenuItems:function() {
370 js_log( 'loadInitMenuItems' );
372 // do all the menu_items setup: @@we could defer this to once the menu item is requested
373 for ( var i in this.menu_items ) {
374 if ( this.menu_items[i].js )
375 this.menu_items[i].js( this );
378 renderTimeLine:function() {
379 // empty out the top level html:
380 $j( '#' + this.timeline_id ).html( '' );
381 // add html general for timeline
382 if ( this.timeline_mode == 'time' ) {
383 $j( '#' + this.timeline_id ).html( '' +
384 '<div id="' + this.timeline_id + '_left_cnt" class="mv_tl_left_cnt">' +
385 '<div id="' + this.timeline_id + '_head_control" style="position:absolute;top:0px;left:0px;right:0px;height:30px;">' +
386 '<a title="' + gM( 'mwe-play_from_position' ) + '" href="javascript:' + this.instance_name + '.play_jt()">' +
387 '<img style="width:16px;height:16px;border:0" src="' + mv_embed_path + 'images/control_play_blue.png">' +
389 '<a title="' + gM( 'mwe-zoom_in' ) + '" href="javascript:' + this.instance_name + '.zoom_in()">' +
390 '<img style="width:16px;height:16px;border:0" src="' + mv_embed_path + 'images/zoom_in.png">' +
392 '<a title="' + gM( 'mwe-zoom_out' ) + '" href="javascript:' + this.instance_name + '.zoom_out()">' +
393 '<img style="width:16px;height:16px;border:0" src="' + mv_embed_path + 'images/zoom_out.png">' +
395 '<a title="' + gM( 'mwe-cut_clip' ) + '" href="javascript:' + this.instance_name + '.cut_mode()">' +
396 '<img style="width:16px;height:16px;border:0" src="' + mv_embed_path + 'images/cut.png">' +
400 '<div id="' + this.timeline_id + '_tracks" class="mv_seq_tracks">' +
401 '<div id="' + this.timeline_id + '_head_jump" class="mv_head_jump" style="position:absolute;top:0px;left:0px;height:20px;"></div>' +
402 '<div id="' + this.timeline_id + '_playline" class="mv_playline"></div>' +
405 // add playlist hook to update timeline
406 this.plObj.update_tl_hook = this.instance_name + '.update_tl_hook';
410 for ( var i in this.plObj.tracks ) {
411 var track = this.plObj.tracks[i];
412 // js_log("on track: "+ i + ' t:'+ $j('#'+this.timeline_id+'_left_cnt').html() );
413 // set up track based on disp type
414 switch( track.disp_mode ) {
415 case 'timeline_thumb':
416 var track_height = 60;
417 var exc_img = 'opened';
418 var exc_action = 'close';
419 var exc_msg = gM( 'mwe-collapse_track' );
422 var track_height = 20;
423 var exc_img = 'closed';
424 var exc_action = 'open';
425 var exc_msg = gM( 'mwe-expand_track' );
429 $j( '#' + this.timeline_id + '_left_cnt' ).append(
430 '<div id="track_cnt_' + i + '" style="top:' + top_pos + 'px;height:' + track_height + 'px;" class="track_name">' +
431 '<a id="mv_exc_' + i + '" title="' + exc_msg + '" href="javascript:' + this_sq.instance_name + '.exc_track(' + i + ',\'' + exc_action + '\')">' +
432 '<img id="' + this_sq.timeline_id + '_close_expand" style="width:16px;height:16px;border:0" ' +
433 ' src="' + mv_embed_path + 'images/' + exc_img + '.png">' +
435 track.title + '</div>'
437 // also render the clips in the trackset container: (thumb or text view)
438 $j( '#' + this.timeline_id + '_tracks' ).append(
439 '<div id="container_track_' + i + '" style="top:' + top_pos + 'px;height:' + ( track_height + 2 ) + 'px;left:0px;right:0px;" class="container_track" />'
441 top_pos += track_height + 20;
444 if ( this.timeline_mode == 'storyboard' ) {
445 var top_pos = this.plObj.org_control_height;
447 for ( var i in this.plObj.tracks ) {
448 var track_height = this.track_clipThumb_height;
449 var timeline_id = this.timeline_id
450 // add in play box and container tracks
451 $j( '#' + timeline_id ).append( '' +
452 '<div id="interface_container_track_' + i + '" ' +
453 ' style="position:absolute;top:5px;height:' + ( track_height + 30 ) + 'px;left:10px;right:0px;"' +
455 '<div id="container_track_' + i + '" style="position:relative;top:0px;' +
456 'height:' + ( track_height + 30 ) + 'px;left:0px;right:0px;" class="container_track">' +
458 '<div id="' + timeline_id + '_playline" class="mv_story_playline">' +
459 '<div class="mv_playline_top"/>' +
463 top_pos += track_height + 20;
467 // once playlist is ready continue
468 checkReadyPlObj:function() {
469 // set up pointers from sequencer to pl obj
470 this.plObj = $j( '#' + this.plObj_id ).get( 0 );
471 // & from seq obj to sequencer
472 this.plObj.pSeq = this;
475 if ( ! this.plObj.loading )
478 // else keep checking for the playlist to be ready
479 if ( this.plObj.loading ) {
480 if ( this.plReadyTimeout == 200 ) {
481 js_error( 'error playlist never ready' );
483 this.plReadyTimeout++;
484 setTimeout( this.instance_name + '.checkReadyPlObj()', 25 );
488 getLocalApiUrl:function() {
489 return this.plObj.interface_url;
491 plReadyInit:function() {
493 js_log( 'plReadyInit' );
494 js_log( this.plObj );
495 // give the playlist a pointer to its parent seq:
496 this.plObj['seqObj'] = this;
498 // update playlist (if its empty right now)
499 if ( this.plObj.getClipCount() == 0 ) {
500 $j( '#' + this.plObj_id ).html( 'empty playlist' );
503 // propagate the edit tokens
504 // if on an edit page just grab from the form:
505 this.sequenceEditToken = $j( 'input[wpEditToken]' ).val();
507 if ( typeof this.sequenceEditToken == 'undefined' && this.getLocalApiUrl() != null ) {
508 get_mw_token( _this.plObj.mTitle, _this.getLocalApiUrl(),
511 _this.sequenceEditToken = token;
512 _this.updateSeqSaveButtons();
516 get_mw_token( _this.plObj.mTalk, _this.getLocalApiUrl(),
518 _this.clipboardEditToken = token;
521 // also grab permissions for sending clipboard commands to the server
523 // (calling the sequencer inline) try and get edit token via api call:
524 // (somewhat fragile way to get at the api... should move to config
525 /*var token_url = this.plObj.interface_url.replace(/index\.php/, 'api.php');
526 token_url += '?action=query&format=xml&prop=info&intoken=edit&titles=';
529 url: token_url + this_seq.plObj.mTitle,
530 success:function(data){
531 var pageElm = data.getElementsByTagName('page')[0];
532 if( $j(pageElm).attr('edittoken') ){
533 this_seq.sequenceEditToken = $j(pageElm).attr('edittoken');
538 // also grab permissions for sending clipboard commands to the server
541 url: token_url + this_seq.plObj.mTalk,
542 success:function(data){
543 var pageElm = data.getElementsByTagName('page')[0];
544 if( $j(pageElm).attr('edittoken') ){
545 this_seq.clipboardEditToken = $j(pageElm).attr('edittoken');
552 // Render the menu tabs::
553 var item_containers = '';
555 var selected_tab = 0;
557 var o = '<div id="seq_menu" style="width:100%;height:100%">';
559 for ( var tab_id in this.menu_items ) {
560 menu_item = this.menu_items[tab_id];
562 if ( menu_item.default ) {
564 _this.disp_menu_item = tab_id;
568 '<a id="mv_menu_item_' + tab_id + '" href="#' + tab_id + '_ic">' + gM( 'mwe-menu_' + tab_id ) + '</a>' +
571 tabc += '<div id="' + tab_id + '_ic" style="overflow:auto;height:268px;" >';
572 tabc += ( menu_item.html ) ? menu_item.html : '<h3>' + gM( 'mwe-menu_' + tab_id ) + '</h3>';
578 $j( '#' + this.sequence_tools_id ).html( o );
581 $j( "#seq_menu" ).tabs( {
582 selected:selected_tab,
583 select: function( event, ui ) {
584 _this.disp( $j( ui.tab ).attr( 'id' ).replace( 'mv_menu_item_', '' ), true );
587 } ).find( ".ui-tabs-nav" ).sortable( { axis : 'x' } );
590 // Render the timeline
591 this.renderTimeLine();
592 this.do_refresh_timeline();
594 // Load initial content into containers
595 this.setupMenuItems();
597 this.doFocusBindings();
599 // Set up key bidnings
600 $j( window ).keydown( function( e ) {
601 js_log( 'pushed down on:' + e.which );
603 _this.key_shift_down = true;
606 _this.key_ctrl_down = true;
608 if ( ( e.which == 67 && _this.key_ctrl_down ) && !_this.inputFocus )
609 _this.copySelectedClips();
611 if ( ( e.which == 88 && _this.key_ctrl_down ) && !_this.inputFocus )
612 _this.cutSelectedClips();
614 // Paste cips on v + ctrl while not focused on a text area:
615 if ( ( e.which == 86 && _this.key_ctrl_down ) && !_this.inputFocus )
616 _this.pasteClipBoardClips();
619 $j( window ).keyup( function( e ) {
620 js_log( 'key up on ' + e.which );
621 // User let go of "shift" turn off multi-select
623 _this.key_shift_down = false;
626 _this.key_ctrl_down = false;
628 // Escape key ( deselect )
630 _this.deselectClip();
633 // Backspace or Delete key while not focused on a text area:
634 if ( ( e.which == 8 || e.which == 46 ) && !_this.inputFocus )
635 _this.removeSelectedClips();
639 * Check all text nodes for focus
641 doFocusBindings:function() {
643 // if an input or text area has focus disable delete key binding
644 $j( "input,textarea" ).focus( function () {
645 js_log( "inputFocus:true" );
646 _this.inputFocus = true;
648 $j( "input,textarea" ).blur( function () {
649 js_log( "inputFocus:blur" );
650 _this.inputFocus = false;
654 * Update the timeline hook
656 update_tl_hook:function( jh_time_ms ) {
657 // Put into seconds scale:
658 var jh_time_sec_float = jh_time_ms / 1000;
659 // Render playline at given time
660 $j( '#' + this.timeline_id + '_playline' )
663 Math.round( jh_time_sec_float / this.timeline_scale ) + 'px'
665 // js_log('at time:'+ jh_time_sec + ' px:'+ Math.round(jh_time_sec_float/this.timeline_scale));
668 * Returns a xml or json representation of the current sequence
670 getSeqOutputJSON:function() {
671 js_log( 'json output:' );
674 * Gets the Sequence as a formated high level resource description xml string
677 getSeqOutputHLRDXML:function() {
678 var o = '<sequence_hlrd>' + "\n";
681 for ( var i in this.plObj.transitions ) {
682 if ( this.plObj.transitions[i] ) {
683 var tObj = this.plObj.transitions[i].getAttributeObj();
684 o += "\t\t<transition ";
685 for ( var j in tObj ) {
686 o += ' ' + j + '="' + tObj[j] + '"\n\t\t';
688 o += '/>' + "\n"; // transitions don't have children
695 // Output each track:
696 for ( var i in this.plObj.tracks ) {
697 var curTrack = this.plObj.tracks[i];
699 var tAttr = curTrack.getAttributeObj();
700 for ( var j in tAttr ) {
701 o += ' ' + j + '="' + tAttr[j] + '"\n\t\t\t';
704 for ( var k in curTrack.clips ) {
705 var curClip = curTrack.clips[k];
707 var cAttr = curClip.getAttributeObj();
709 for ( var j in cAttr ) {
710 var val = ( j == 'transIn' || j == 'transOut' ) ? cAttr[j].id : cAttr[j];
711 o += lt + j + '="' + val + '"';
714 o += ">\n" // close the clip
715 for ( var pName in curClip.params ) {
716 var pVal = curClip.params[pName];
717 o += "\t\t\t" + '<param name="' + pName + '">' + pVal + '</param>' + "\n";
719 o += "\t\t</ref>\n\n";
725 o += '</sequence_hlrd>';
730 * Takes a track index and a clip index, to get a clip Object.
731 * It then calls doEditClip with that clip Object.
733 editClip:function( track_inx, clip_inx ) {
734 var cObj = this.plObj.tracks[ track_inx ].clips[ clip_inx ];
735 this.doEditClip( cObj );
738 * Calls the doEditClip interface on the selected clip
739 * Handles cases where no clips are selected or multiple clips are selected.
741 doEditSelectedClip:function() {
742 js_log( "f:doEditSelectedClip:" );
743 // And only one clip selected
744 if ( $j( '.mv_selected_clip' ).length == 1 ) {
745 this.doEditClip( this.getClipFromSeqID( $j( '.mv_selected_clip' ).parent().attr( 'id' ) ) );
746 } else if ( $j( '.mv_selected_clip' ).length === 0 ) {
747 // No clip selected warning:
748 $j( '#clipedit_ic' ).html( gM( 'mwe-no_selected_resource' ) );
750 // Multiple clip selected warning:
751 $j( '#clipedit_ic' ).html( gM( 'mwe-error_edit_multiple' ) );
755 * Pulls up the edit transition interface for the selected clip
757 doEditTransitionSelectedClip:function() {
759 js_log( "f:doEditTransitionSelectedClip:" + $j( '.mv_selected_clip' ).length );
760 if ( $j( '.mv_selected_clip' ).length == 1 ) {
761 _this.doEditTransition( _this.getClipFromSeqID( $j( '.mv_selected_clip' ).parent().attr( 'id' ) ) );
762 } else if ( $j( '.mv_selected_clip' ).length === 0 ) {
763 // no clip selected warning:
764 $j( '#transition_ic' ).html( gM( 'mwe-no_selected_resource' ) );
766 // multiple clip selected warning:
767 $j( '#transition_ic' ).html( gM( 'mwe-error_edit_multiple' ) );
771 * Loads the transition edit javascript libs and
772 * displays the transition edit interface.
774 doEditTransition:function( cObj ) {
775 js_log( "sequence:doEditTransition" );
777 // Add a loading image
778 mv_get_loading_img( '#transitions_ic' );
783 // For some reason we lose scope in the options passed to mvTimedEffectsEdit
784 // so we re refrence the sequence here:
785 var localSeqRef = _this;
786 _this.myEffectEdit = new mvTimedEffectsEdit( {
788 'control_ct' : 'transition_ic',
794 * Updates the clip details div if edit resource is set
796 doEditClip:function( cObj ) {
797 js_log( 'seq:doEditClip' );
800 // Set default edit action
801 var edit_action = 'fileopts';
803 mv_get_loading_img( '#clipedit_ic' );
805 // Load the clipEdit library if not already loaded:
809 // Zero out the current editor:
810 _this.myClipEditor = { };
811 // Setup the cliploader options
812 _this.myClipEditor = new mvClipEdit( {
814 'control_ct' : 'clipedit_ic',
815 'clip_disp_ct' : cObj.id,
816 'edit_action' : edit_action,
818 'profile' : 'sequence'
824 * Save new clip segment
825 * FIXME this is just a stub
827 saveClipEdit:function() {
828 // saves the clip updates
832 * Closes the sequence and dereferences the global instance.
834 closeModEditor:function() {
835 // unset the sequencer
836 _global['mvSeq'] = null;
837 $j( this.target_sequence_container + ',.ui-widget-overlay' ).remove();
841 * Copies the selected clips to the server hosted "clipboard"
843 * FIXME need to support local clipboard for stand alone editing.
844 * FIXME this does not really work at all right now
846 copySelectedClips:function() {
848 // set all the selected clips
849 this.clipboard = new Array();
850 $j( '.mv_selected_clip' ).each( function() {
852 // Add each clip to the clip board:
853 var cur_clip = this_seq.getClipFromSeqID( $j( this ).parent().attr( 'id' ) );
854 this_seq.clipboard.push( cur_clip.getAttributeObj() );
858 // Upload clipboard to the server (if possible)
859 if ( mw.parseUri( document.URL ).host != mw.parseUri( this_seq.plObj.interface_url ).host ) {
860 js_log( 'error: presently we can\'t copy clips across domains' );
862 // FIXME we need to add an api entry point to store a "clipboard"
863 // right now this is dependent on a custom hook:
864 if ( this_seq.clipboardEditToken && this_seq.plObj.interface_url ) {
865 var req_url = this_seq.plObj.interface_url.replace( /api.php/, 'index.php' ) + '?action=ajax&rs=mv_seqtool_clipboard&rsargs[]=copy';
870 "clipboard_data": $j.toJSON( this_seq.clipboard ),
871 "clipboardEditToken": this_seq.clipboardEditToken
873 success:function( data ) {
874 js_log( 'did clipboard push ' + $j.toJSON( this_seq.clipboard ) );
878 js_log( 'error: no clipboardEditToken to uplaod clipboard to server' );
883 * Paste the clipboard clips into the sequence
885 pasteClipBoardClips:function() {
886 js_log( 'f:pasteClipBoardClips' );
887 // @@todo query the server for updated clipboard
888 // paste before the "current clip"
889 this.addClips( this.clipboard, this.plObj.cur_clip.order );
893 * Cut selected clips from the timeline
895 cutSelectedClips:function() {
896 this.copySelectedClips();
897 this.removeSelectedClips();
901 * Remove selected clips from the timeline
903 removeSelectedClips:function() {
904 var remove_clip_ary = new Array();
905 // Remove selected clips from display
906 $j( '.container_track .mv_selected_clip' ).each( function() {
907 // grab the track index from the id (assumes track_#_clip_#
908 remove_clip_ary.push ( $j( this ).parent().attr( 'id' ).replace( 'track_', '' ).replace( 'clip_', '' ).split( '_' ) );
910 if ( remove_clip_ary.length != 0 )
911 this.removeClips( remove_clip_ary );
913 // doEdit selected clips (updated selected resource)
914 // @@todo refresh menu of current
915 this.doEditSelectedClip();
918 * Add a clip to the timeline
920 addClip:function( clip, before_clip_pos, track_inx ) {
921 this.addClips( [clip], before_clip_pos, track_inx )
924 * add a single or set of clips
925 * to a given position and track_inx
927 addClips:function( clipSet, before_clip_pos, track_inx ) {
931 track_inx = this.plObj.default_track.inx;
933 if ( !before_clip_pos )
934 before_clip_pos = this.plObj.default_track.getClipCount();
936 js_log( "seq: add clip: at: " + before_clip_pos + ' in track: ' + track_inx );
937 var cur_pos = before_clip_pos;
939 $j.each( clipSet, function( inx, clipInitDom ) {
940 var mediaElement = document.createElement( 'ref' );
941 for ( var i in clipInitDom ) {
942 js_log( "set: " + i + ' to ' + clipInitDom[i] );
944 $j( mediaElement ).attr( i, clipInitDom[i] );
946 if ( this_seq.plObj.tryAddMedia( mediaElement, cur_pos, track_inx ) )
950 this.do_refresh_timeline();
954 * Removes Clips listed in the remove_clip_ary paramater
956 removeClips:function( remove_clip_ary ) {
958 var jselect = coma = '';
959 js_log( 'clip count before removal : ' + this_seq.plObj.default_track.clips.length + ' should remove ' + remove_clip_ary.length );
960 var afected_tracks = new Array();
961 // add order to track_clip before we start removing:
962 $j.each( remove_clip_ary, function( inx, track_clip ) {
963 remove_clip_ary[inx]['order'] = this_seq.plObj.tracks[ track_clip[0] ].clips[ track_clip[1] ].order;
965 $j.each( remove_clip_ary, function( inx, track_clip ) {
966 var track_inx = track_clip[0];
967 var clip_inx = track_clip[1];
968 var clip_rm_order = track_clip['order'];
969 js_log( 'remove t:' + track_inx + ' c:' + clip_inx + ' id:' + ' #track_' + track_inx + '_clip_' + clip_inx + ' order:' + clip_rm_order );
970 // remove the clips from the base tracks
971 for ( var i in this_seq.plObj.tracks[ track_inx ].clips ) {
972 cur_clip = this_seq.plObj.tracks[ track_inx ].clips[i]
973 if ( cur_clip.order == clip_rm_order ) {
974 this_seq.plObj.tracks[ track_clip[0] ].clips.splice( i, 1 );
977 // add track to affected track list:
978 afected_tracks[ track_inx ] = true;
979 jselect += coma + '#track_' + track_inx + '_clip_' + clip_inx;
983 $j.each( afected_tracks, function( track_inx, affected ) {
984 this_seq.plObj.tracks[track_inx].reOrderClips();
987 js_log( 'clip count after removal : ' + this_seq.plObj.default_track.clips.length );
988 // animate the removal (@@todo should be able to call the resulting fadeOut only once without a flag)
989 var done_with_refresh = false;
990 $j( jselect ).fadeOut( "slow", function() {
991 if ( !done_with_refresh )
992 this_seq.do_refresh_timeline();
993 done_with_refresh = true;
994 } ).empty(); // empty to remove any persistent bindings
996 doEdit:function( editObj ) {
997 // add the current editObj to the edit stack (should allow for "undo")
998 this.edit_stack.push( editObj );
999 // make the adjustments
1000 this.makeAdjustment( editObj );
1003 * takes adjust ment object with options:
1004 * track_inx, clip_inx, start, end delta
1006 makeAdjustment:function( e ) {
1008 case 'resize_start':
1009 this.plObj.tracks[e.track_inx].clips[e.clip_inx].doAdjust( 'start', e.delta );
1012 this.plObj.tracks[e.track_inx].clips[e.clip_inx].doAdjust( 'end', e.delta );
1015 js_log( 're render: ' + e.track_inx );
1016 // refresh the playlist after adjustment
1017 this.do_refresh_timeline();
1019 // @@todo set up key bindings for undo
1020 undoEdit:function() {
1021 var editObj = this.edit_stack.pop();
1025 exc_track:function( inx, req ) {
1027 if ( req == 'close' ) {
1028 $j( '#mv_exc_' + inx ).attr( 'href', 'javascript:' + this.instance_name + '.exc_track(' + inx + ',\'open\')' );
1029 $j( '#mv_exc_' + inx + ' > img' ).attr( 'src', mv_embed_path + 'images/closed.png' );
1030 $j( '#track_cnt_' + inx + ',#container_track_' + inx ).animate( { height:this.track_text_height }, "slow", '',
1032 this_seq.plObj.tracks[inx].disp_mode = 'text';
1033 this_seq.render_tracks( inx );
1035 } else if ( req == 'open' ) {
1036 $j( '#mv_exc_' + inx ).attr( 'href', 'javascript:' + this.instance_name + '.exc_track(' + inx + ',\'close\')' );
1037 $j( '#mv_exc_' + inx + ' > img' ).attr( 'src', mv_embed_path + 'images/opened.png' );
1038 $j( '#track_cnt_' + inx + ',#container_track_' + inx ).animate( { height:this.track_thumb_height }, "slow", '',
1040 this_seq.plObj.tracks[inx].disp_mode = 'timeline_thumb';
1041 this_seq.render_tracks( inx );
1047 add_track:function( inx, track ) {
1050 // toggle cut mode (change icon to cut)
1051 cut_mode:function() {
1052 js_log( 'do cut mode' );
1053 // add cut layer ontop of clips
1055 doAdvancedTl:function() {
1056 this.timeline_mode = 'time';
1057 this.renderTimeLine();
1058 this.do_refresh_timeline();
1061 doSimpleTl:function() {
1062 this.timeline_mode = 'storyboard';
1063 this.renderTimeLine();
1064 this.do_refresh_timeline();
1067 // renders updates the timeline based on the current scale
1068 render_tracks:function( track_inx ) {
1069 js_log( "f::render track: " + track_inx );
1070 var this_seq = this;
1071 // inject the tracks into the timeline (if not already there)
1072 for ( var track_id in this.plObj.tracks ) {
1073 if ( track_inx == track_id || typeof track_inx == 'undefined' ) {
1074 // empty out the track container:
1075 // $j('#container_track_'+track_id).empty();
1076 var track_html = droppable_html = '';
1077 // set up per track vars:
1078 var track = this.plObj.tracks[track_id];
1079 var cur_clip_time = 0;
1081 // set up some constants for timeline_mode == storyboard:
1082 if ( this.timeline_mode == 'storyboard' ) {
1083 var frame_width = Math.round( this.track_clipThumb_height * 1.3333333 );
1084 var container_width = frame_width + 60;
1088 for ( var j in track.clips ) {
1089 clip = track.clips[j];
1090 // var img = clip.getClipImg('icon');
1091 if ( this.timeline_mode == 'storyboard' ) {
1092 clip.left_px = j * container_width;
1093 clip.width_px = container_width;
1094 var base_id = 'track_' + track_id + '_clip_' + j;
1095 track_html += '<span id="' + base_id + '" ' +
1096 'class="mv_storyboard_container mv_clip_drag" ' +
1098 'left:' + clip.left_px + 'px;' +
1099 'height:' + ( this.track_clipThumb_height + 30 ) + 'px;' +
1100 'width:' + ( container_width ) + 'px;" >';
1101 track_html += clip.embed.renderTimelineThumbnail( {
1102 'width' : frame_width,
1103 'thumb_class' : 'mv_clip_thumb',
1104 'height':this.track_clipThumb_height,
1107 // render out edit button
1108 /*track_html+='<div class="clip_edit_button clip_edit_base clip_control"/>';*/
1110 // check if the clip has transitions
1114 if ( clip.transIn || clip.transOut ) {
1115 if ( clip.transIn && clip.transIn.getIconSrc )
1116 imsrc = clip.transIn.getIconSrc();
1117 // @@todo put transOut somewhere else
1118 if ( clip.transOut && clip.transOut.getIconSrc )
1119 imsrc = clip.transOut.getIconSrc();
1121 imgHtml = '<img style="width:32px;height:32px" src="' + imsrc + '" />';
1123 // render out transition edit box
1124 track_html += '<div id="tb_' + base_id + '" class="clip_trans_box">' +
1128 // render out adjustment text
1129 /*track_html+='<div id="' + base_id + '_adj' + '" class="mv_adj_text" style="top:'+ (this.track_clipThumb_height+10 )+'px;">'+
1130 '<span class="mv_adjust_click" onClick="'+this.instance_name+'.adjClipDur(' + track_id + ',' + j + ',\'-\')" /> - </span>'+
1131 ( (clip.getDuration() > 60 )? seconds2npt(clip.getDuration()): clip.getDuration() ) +
1132 '<span class="mv_adjust_click" onClick="'+this.instance_name+'.adjClipDur(' + track_id + ',' + j + ',\'+\')" /> + </span>'+
1135 track_html += '</span>';
1138 // do timeline_mode rendering:
1139 if ( this.timeline_mode == 'time' ) {
1140 clip.left_px = Math.round( cur_clip_time / this.timeline_scale );
1141 clip.width_px = Math.round( Math.round( clip.getDuration() ) / this.timeline_scale );
1142 clip.height_px = 60;
1143 js_log( 'at time:' + cur_clip_time + ' left: ' + clip.left_px + ' clip dur: ' + Math.round( clip.getDuration() ) + ' clip width:' + clip.width_px );
1145 // for every clip_width pixle output image
1146 if ( track.disp_mode == 'timeline_thumb' ) {
1147 track_html += '<span id="track_' + track_id + '_clip_' + j + '" ' +
1148 'class="mv_tl_clip mv_clip_drag" ' +
1150 'left:' + clip.left_px + 'px;' +
1151 'width:' + clip.width_px + 'px;' +
1152 'height:' + clip.height_px + 'px" >';
1153 track_html += this.render_clip_frames( clip );
1154 } else if ( track.disp_mode == 'text' ) {
1156 track_html += '<span id="track_' + track_id + '_clip_' + j + '" style="left:' + clip.left_px + 'px;' +
1157 'width:' + clip.width_px + 'px;background:' + clip.getColor() +
1158 '" class="mv_time_clip_text mv_clip_drag">' + clip.title;
1160 // add in per clip controls
1161 track_html += '<div title="' + gM( 'mwe-clip_in' ) + ' ' + clip.embed.start_ntp + '" class="ui-resizable-w ui-resizable-handle" style="width: 16px; height: 16px; left: 0px; top: 2px;background:url(\'' + mv_embed_path + 'images/application_side_contract.png\');" ></div>' + "\n";
1162 track_html += '<div title="' + gM( 'mwe-clip_out' ) + ' ' + clip.embed.end_ntp + '" class="ui-resizable-e ui-resizable-handle" style="width: 16px; height: 16px; right: 0px; top: 2px;background:url(\'' + mv_embed_path + 'images/application_side_expand.png\');" ></div>' + "\n";
1163 track_html += '<div title="' + gM( 'mwe-rmclip' ) + '" onClick="' + this.instance_name + '.removeClips(new Array([' + track_id + ',' + j + ']))" style="position:absolute;cursor:pointer;width: 16px; height: 16px; left: 0px; bottom:2px;background:url(\'' + mv_embed_path + 'images/delete.png\');"></div>' + "\n";
1164 track_html += '<span style="display:none;" class="mv_clip_stats"></span>';
1166 track_html += '</span>';
1167 // droppable_html+='<div id="dropBefore_'+i+'_c_'+j+'" class="mv_droppable" style="height:'+this.track_thumb_height+'px;left:'+clip.left_px+'px;width:'+Math.round(clip.width_px/2)+'px"></div>';
1168 // droppable_html+='<div id="dropAfter_'+i+'_c_'+j+'" class="mv_droppable" style="height:'+this.track_thumb_height+'px;left:'+(clip.left_px+Math.round(clip.width_px/2))+'px;width:'+(clip.width_px/2)+'px"></div>';
1169 cur_clip_time += Math.round( clip.getDuration() ); // increment cur_clip_time
1174 // js_log("new htmL for track i: "+track_id + ' html:'+track_html);
1175 $j( '#container_track_' + track_id ).html( track_html );
1177 // apply transition click action
1178 $j( '.clip_trans_box' ).click( function() {
1179 if ( $j( this ).hasClass( 'mv_selected_transition' ) ) {
1180 $j( this ).removeClass( 'mv_selected_transition' );
1181 this_seq.deselectClip( $j( this ).siblings( '.mv_clip_thumb' ).get( 0 ) );
1184 this_seq.deselectClip();
1185 $j( '.clip_trans_box' ).removeClass( 'mv_selected_transition' );
1186 $j( this ).addClass( "mv_selected_transition" );
1187 $j( this ).siblings( '.mv_clip_thumb' ).addClass( "mv_selected_clip" );
1188 var sClipObj = this_seq.getClipFromSeqID( $j( this ).parent().attr( 'id' ) );
1189 // jump to the current clip
1190 this_seq.plObj.updateCurrentClip( sClipObj );
1191 // display the transition edit tab:
1192 this_seq.disp( 'transition' );
1196 // apply edit button mouse over effect:
1197 $j( '.clip_edit_button' ).hover( function() {
1198 $j( this ).removeClass( "clip_edit_base" ).addClass( "clip_edit_over" );
1200 $j( this ).removeClass( "clip_edit_over" ).addClass( "clip_edit_base" );
1201 } ).click( function() {
1202 // deselect everything else:
1203 $j( '.mv_selected_clip' ).each( function( inx, selected_clip ) {
1204 this_seq.deselectClip( this );
1207 var sClipObj = this_seq.getClipFromSeqID( $j( this ).parent().attr( 'id' ) );
1208 this_seq.plObj.updateCurrentClip( sClipObj );
1209 // get the clip (siblings with mv_clip_thumb class)
1210 var cur_clip_elm = $j( this ).siblings( '.mv_clip_thumb' );
1211 // select the clip (add mv_selected_clip if not already selected)
1212 if ( ! $j( cur_clip_elm ).hasClass( "mv_selected_clip" ) ) {
1213 $j( cur_clip_elm ).addClass( 'mv_selected_clip' );
1214 $j( '#' + $j( cur_clip_elm ).parent().attr( "id" ) + '_adj' ).fadeIn( "fast" );
1216 // display the edit tab:
1217 this_seq.disp( 'clipedit' );
1218 // display edit dialog:
1219 this_seq.doEditClip( sClipObj );
1222 // apply onClick edit controls:
1223 $j( '.mv_clip_thumb' ).click( function() {
1224 var cur_clip_click = this;
1225 // if not in multi select mode remove all existing selections
1226 // (except for the current click which is handled down below)
1227 js_log( ' ks: ' + this_seq.key_shift_down + ' ctrl_down:' + this_seq.key_ctrl_down );
1228 if ( ! this_seq.key_shift_down && ! this_seq.key_ctrl_down ) {
1229 $j( '.mv_selected_clip' ).each( function( inx, selected_clip ) {
1230 if ( $j( this ).parent().attr( 'id' ) != $j( cur_clip_click ).parent().attr( 'id' )
1231 || ( $j( '.mv_selected_clip' ).length > 1 ) ) {
1232 this_seq.deselectClip( this );
1237 // jump to clip time
1238 var sClipObj = this_seq.getClipFromSeqID( $j( this ).parent().attr( 'id' ) );
1239 this_seq.plObj.updateCurrentClip( sClipObj );
1240 if ( $j( this ).hasClass( "mv_selected_clip" ) ) {
1241 $j( this ).removeClass( "mv_selected_clip" );
1242 $j( '#' + $j( this ).parent().attr( "id" ) + '_adj' ).fadeOut( "fast" );
1244 $j( this ).addClass( 'mv_selected_clip' );
1245 $j( '#' + $j( this ).parent().attr( "id" ) + '_adj' ).fadeIn( "fast" );
1247 // if shift select is down select the in-between clips
1248 if ( this_seq.key_shift_down ) {
1249 // get the min max of current selection (within the current track)
1251 var min_order = 999999999;
1252 $j( '.mv_selected_clip' ).each( function() {
1253 var cur_clip = this_seq.getClipFromSeqID( $j( this ).parent().attr( 'id' ) );
1255 if ( cur_clip.order < min_order )
1256 min_order = cur_clip.order;
1257 if ( cur_clip.order > max_order )
1258 max_order = cur_clip.order;
1260 // select all non-selected between max or min
1261 js_log( 'sOrder: ' + sClipObj.order + ' min:' + min_order + ' max:' + max_order );
1262 if ( sClipObj.order <= min_order ) {
1263 for ( var i = sClipObj.order; i <= max_order; i++ ) {
1264 $j( '#track_' + track_id + '_clip_' + i + ' > .mv_clip_thumb' ).addClass( 'mv_selected_clip' );
1267 if ( sClipObj.order >= max_order ) {
1268 for ( var i = min_order; i <= max_order; i++ ) {
1269 $j( '#track_' + track_id + '_clip_' + i + ' > .mv_clip_thumb' ).addClass( 'mv_selected_clip' );
1273 this_seq.doEditSelectedClip();
1275 // add in control for time based display
1277 if ( this.timeline_mode == 'time' ) {
1278 $j( '.ui-resizable-handle' ).mousedown( function() {
1279 js_log( 'hid: ' + $j( this ).attr( 'class' ) );
1280 this_seq.resize_mode = ( $j( this ).attr( 'class' ).indexOf( 'ui-resizable-e' ) != -1 ) ?
1281 'resize_end':'resize_start';
1284 var insert_key = 'na';
1286 // @@todo support multiple clips
1287 for ( var j in track.clips ) {
1288 $j( '#track_' + track_id + '_clip_' + j ).draggable( {
1290 containment:'#container_track_' + track_id,
1292 handle: ":not(.clip_control)",
1294 drag:function( e, ui ) {
1296 insert_key = this_seq.clipDragUpdate( ui, this );
1298 start:function( e, ui ) {
1299 js_log( 'start drag:' + this.id );
1300 // make sure we are ontop
1301 $j( this ).css( { top:'0px', zindex:10 } );
1303 stop:function( e, ui ) {
1304 $j( this ).css( { top:'0px', zindex:0 } );
1306 var id_parts = this.id.split( '_' );
1307 var track_inx = id_parts[1];
1308 var clip_inx = id_parts[3];
1309 var clips = this_seq.plObj.tracks[track_inx].clips;
1310 var cur_drag_clip = clips[clip_inx];
1312 if ( insert_key != 'na' && insert_key != 'end' ) {
1313 cur_drag_clip.order = insert_key - .5;
1314 } else if ( insert_key == 'end' ) {
1315 cur_drag_clip.order = clips.length;
1317 // reorder array based on new order
1318 clips.sort( sort_func );
1319 function sort_func( a, b ) {
1320 return a.order - b.order;
1322 // assign keys back to order:
1323 this_seq.plObj.tracks[track_inx].reOrderClips();
1325 this_seq.do_refresh_timeline();
1328 // add in resize hook if in time mode:
1329 if ( this.timeline_mode == 'time' ) {
1330 $j( '#track_' + track_id + '_clip_' + j ).resizable( {
1333 start: function( e, ui ) {
1334 // set border to red
1335 $j( this ).css( { 'border':'solid thin red' } );
1336 // fade In Time stats (end or start based on handle)
1337 // dragging east (adjusting end time)
1338 js_log( 'append to: ' + this.id );
1339 $j( '#' + this.id + ' > .mv_clip_stats' ).fadeIn( "fast" );
1341 stop: function( e, ui ) {
1342 js_log( 'stop resize' );
1344 $j( this ).css( 'border', 'solid thin white' );
1346 var clip_drag = this;
1347 $j( '#' + this.id + ' > .mv_clip_stats' ).fadeOut( "fast", function() {
1348 var id_parts = clip_drag.id.split( '_' );
1349 var track_inx = id_parts[1];
1350 var clip_inx = id_parts[3];
1353 type:this_seq.resize_mode,
1354 delta:this_seq.edit_delta,
1355 track_inx:track_inx,
1356 clip_inx:clip_inx } )
1359 resize: function( e, ui ) {
1360 // update time stats & render images:
1361 this_seq.update_clip_resize( this );
1366 $j( '#container_track_' + track_id ).width( Math.round( this.timeline_duration / this.timeline_scale ) );
1371 clipDragUpdate:function( ui, clipElm ) {
1372 var this_seq = this;
1374 var insert_key = 'na';
1375 // animate re-arrange by left position:
1376 // js_log('left: '+ui.position.left);
1377 // locate clip (based on clip duration not animate)
1378 var id_parts = clipElm.id.split( '_' );
1379 var track_inx = id_parts[1];
1380 var clip_inx = id_parts[3];
1381 var clips = this_seq.plObj.tracks[track_inx].clips;
1382 var cur_drag_clip = clips[clip_inx];
1383 var return_org = true;
1384 $j( clipElm ).css( 'zindex', 10 );
1385 // find out where we are inserting and set left border to solid red thick
1386 for ( var k in clips ) {
1387 if ( ui.position.left > clips[k].left_px &&
1388 ui.position.left < ( clips[k].left_px + clips[k].width_px ) ) {
1389 if ( clip_inx != k ) {
1390 // also make sure we are not where we started
1391 if ( k - 1 != clip_inx ) {
1392 $j( '#track_' + track_inx + '_clip_' + k ).css( 'border-left', 'solid thick red' );
1401 $j( '#track_' + track_inx + '_clip_' + k ).css( 'border-left', 'solid thin white' );
1404 // if greater than the last k insert after
1405 if ( ui.position.left > ( clips[k].left_px + clips[k].width_px ) &&
1407 $j( '#track_' + track_inx + '_clip_' + k ).css( 'border-right', 'solid thick red' );
1410 $j( '#track_' + track_inx + '_clip_' + k ).css( 'border-right', 'solid thin white' );
1414 deselectClip:function( clipElm ) {
1416 $j( '.mv_selected_clip' ).removeClass( "mv_selected_clip" );
1418 $j( clipElm ).removeClass( "mv_selected_clip" );
1419 // make sure the transition sibling is removed:
1420 $j( clipElm ).siblings( '.clip_trans_box' ).removeClass( 'mv_selected_transition' );
1421 $j( '#' + $j( clipElm ).parent().attr( "id" ) + '_adj' ).fadeOut( "fast" );
1424 getClipFromSeqID:function( clip_seq_id ) {
1425 js_log( 'get id from: ' + clip_seq_id );
1426 var ct = clip_seq_id.replace( 'track_', '' ).replace( 'clip_', '' ).split( '_' );
1427 return this.plObj.tracks[ ct[0] ].clips[ ct[1] ];
1429 // renders clip frames
1430 render_clip_frames:function( clip, frame_offset_count ) {
1431 js_log( 'f:render_clip_frames: ' + clip.id + ' foc:' + frame_offset_count );
1432 var clip_frames_html = '';
1433 var frame_width = Math.round( this.track_thumb_height * 1.3333333 );
1435 var pint = ( frame_offset_count == null ) ? 0:frame_offset_count * frame_width;
1437 // js_log("pinit: "+ pint+ ' < '+clip.width_px+' ++'+frame_width);
1438 for ( var p = pint; p < clip.width_px; p += frame_width ) {
1439 var clip_time = ( p == 0 ) ? 0:Math.round( p * this.timeline_scale );
1440 js_log( 'rendering clip frames: p:' + p + ' pts:' + ( p * this.timeline_scale ) + ' time:' + clip_time + ' height:' + this.track_thumb_height );
1441 clip_frames_html += clip.embed.renderTimelineThumbnail( {
1442 'width': frame_width,
1443 'thumb_class':'mv_tl_thumb',
1444 'height': this.track_thumb_height,
1445 'size' : "icon", // set size to "icon" preset
1449 js_log( 'render_clip_frames:' + clip_frames_html );
1450 return clip_frames_html;
1452 update_clip_resize:function( clip_element ) {
1453 // js_log('update_clip_resize');
1454 var this_seq = this;
1455 var id_parts = clip_element.id.split( '_' );
1456 track_inx = id_parts[1];
1457 clip_inx = id_parts[3];
1459 var clip = this.plObj.tracks[ track_inx ].clips[ clip_inx ];
1461 // would be nice if getting the width did not flicker the border
1462 // @@todo do a work around e in resize function has some screen based offset values
1463 clip.width_px = $j( clip_element ).width();
1464 var width_dif = clip.width_px - Math.round( Math.round( clip.getDuration() ) / this.timeline_scale );
1465 // var left_px = $j(clip_element).css('left');
1467 var new_clip_dur = Math.round( clip.width_px * this.timeline_scale );
1468 var clip_dif = ( new_clip_dur - clip.getDuration() );
1469 var clip_dif_str = ( clip_dif > 0 ) ? '+' + clip_dif:clip_dif;
1470 // set the edit global delta
1471 this.edit_delta = clip_dif;
1474 clip_desc += 'length: ' + seconds2npt( new_clip_dur ) + '(' + clip_dif_str + ')';
1475 if ( this_seq.resize_mode == 'resize_end' ) {
1477 var new_end = seconds2npt( npt2seconds( clip.embed.end_ntp ) + clip_dif );
1478 clip_desc += '<br>end time: ' + new_end;
1479 // also shift all the other clips (after the current)
1480 // js_log("track_inx: " + track_inx + ' clip inx:'+clip_inx);
1481 // $j('#container_track_'+track_inx+' > .mv_clip_drag :gt('+clip_inx+')').each(function(){
1482 $j( '#container_track_' + track_inx + ' > :gt(' + clip_inx + ')' ).each( function() {
1483 var move_id_parts = this.id.split( '_' );
1484 var move_clip = this_seq.plObj.tracks[move_id_parts[1]].clips[move_id_parts[3]];
1485 // js_log('should move:'+ this.id);
1486 $j( this ).css( 'left', move_clip.left_px + width_dif );
1489 // expanding left (resize_start)
1490 var new_start = seconds2npt( npt2seconds( clip.embed.start_ntp ) + clip_dif );
1491 clip_desc += '<br>start time: ' + new_start;
1494 // update clip stats:
1495 $j( '#' + clip_element.id + ' > .mv_clip_stats' ).html( clip_desc );
1496 var frame_width = Math.round( this.track_thumb_height * 1.3333333 );
1497 // check if we need to append some images:
1498 var frame_count = $j( '#' + clip_element.id + ' > img' ).length;
1499 if ( clip.width_px > ( frame_count * frame_width ) ) {
1500 // if dragging left append
1501 js_log( 'width_px:' + clip.width_px + ' framecount:' + frame_count + ' Xcw=' + ( frame_count * frame_width ) );
1502 $j( '#' + clip_element.id ).append( this.render_clip_frames( clip, frame_count ) );
1506 render_playheadhead_seeker:function() {
1507 js_log( 'render_playheadhead_seeker' );
1508 // render out time stamps and time "jump" links
1509 // first get total width
1511 // remove the old one if its still there
1512 $j( '#' + this.timeline_id + '_pl_control' ).remove();
1513 // render out a playlist clip wide and all the way to the right (only playhead and play button) (outside of timeline)
1514 $j( this.target_sequence_container ).append( '<div id="' + this.timeline_id + '_pl_control"' +
1515 ' style="position:absolute;top:' + ( this.plObj.height ) + 'px;' +
1516 'right:1px;width:' + this.plObj.width + 'px;height:' + this.plObj.org_control_height + '" ' +
1517 'class="' + this.plObj.ctrlBuilder.pClass + '"><div class="ui-widget ui-corner-bottom ui-state-default control-bar">' +
1518 this.plObj.getControlsHTML() +
1521 // update time and render out clip dividers .. should be used to show load progress
1522 this.plObj.updateBaseStatus();
1524 // once the controls are in the DOM add hooks:
1525 this.plObj.ctrlBuilder.addControlHooks( $j( '#' + this.timeline_id + '_pl_control' ) );
1527 // render out the "jump" div
1528 if ( this.timeline_mode == 'time' ) {
1529 /*$j('#'+this.timeline_id+'_head_jump').width(pixle_length);
1530 //output times every 50pixles
1532 //output time-desc every 50pixles and jump links every 10 pixles
1534 for(i=0;i<pixle_length;i+=10){
1535 out+='<div onclick="'+this.instance_name+'.jt('+i*this.timeline_scale+');"' +
1536 ' style="z-index:2;position:absolute;left:'+i+'px;width:10px;height:20px;top:0px;"></div>';
1538 out+='<span style="position:absolute;left:'+i+'px;">|'+seconds2npt(Math.round(i*this.timeline_scale))+'</span>';
1545 jt:function( jh_time ) {
1546 js_log( 'jt:' + jh_time );
1547 var this_seq = this;
1548 this.playline_time = jh_time;
1549 js_log( 'time: ' + seconds2npt( jh_time ) + ' ' + Math.round( jh_time / this.timeline_scale ) );
1550 // render playline at given time
1551 $j( '#' + this.timeline_id + '_playline' ).css( 'left', Math.round( jh_time / this.timeline_scale ) + 'px' );
1553 // update the thumb with the requested time:
1554 this.plObj.updateThumbTime( jh_time );
1556 // adjusts the current scale
1557 zoom_in:function() {
1558 this.timeline_scale = this.timeline_scale * .75;
1559 this.do_refresh_timeline();
1560 js_log( 'zoomed in:' + this.timeline_scale );
1562 zoom_out:function() {
1563 this.timeline_scale = this.timeline_scale * ( 1 + ( 1 / 3 ) );
1564 this.do_refresh_timeline();
1565 js_log( 'zoom out: ' + this.timeline_scale );
1567 do_refresh_timeline:function( preserve_selection ) {
1568 js_log( 'Sequencer:do_refresh_timeline()' );
1569 // @@todo should "lock" interface while refreshing timeline
1571 if ( preserve_selection ) {
1572 $j( '.mv_selected_clip' ).each( function() {
1573 pSelClips.push( $j( this ).parent().attr( 'id' ) );
1577 this.plObj.getDuration( true );
1579 this.plObj.getHTML();
1581 // this.render_playheadhead_seeker();
1582 this.render_tracks();
1583 this.jt( this.playline_time );
1585 if ( preserve_selection ) {
1586 for ( var i = 0; i < pSelClips.length; i++ ) {
1587 $j( '#' + pSelClips[i] + ' .mv_clip_thumb' ).addClass( 'mv_selected_clip' );
1593 /* extension to mvPlayList to support sequencer features properties */
1594 var mvSeqPlayList = function( element ) {
1595 return this.init( element );
1597 mvSeqPlayList.prototype = {
1598 init:function( element ) {
1599 var myPlObj = new mvPlayList( element );
1602 for ( var method in myPlObj ) {
1603 if ( typeof this[method] != 'undefined' ) {
1604 this[ 'parent_' + method ] = myPlObj[method];
1606 this[method] = myPlObj[method];
1610 this.org_control_height = this.pl_layout.control_height;
1611 // do specific mods:(controls and title are managed by the sequencer)
1612 this.pl_layout.title_bar_height = 0;
1613 this.pl_layout.control_height = 0;
1615 setSliderValue:function( perc ) {
1616 js_log( 'setSliderValue::' + perc );
1617 // get the track_clipThumb_height from parent mvSequencer
1618 var frame_width = Math.round( this.pSeq.track_clipThumb_height * 1.3333333 );
1619 var container_width = frame_width + 60;
1621 var perc_clip = this.cur_clip.embed.currentTime / this.cur_clip.getDuration();
1623 var left_px = parseInt( ( this.cur_clip.order * container_width ) + ( frame_width * perc_clip ) ) + 'px';
1624 js_log( "set " + perc + ' of cur_clip: ' + this.cur_clip.order + ' lp:' + left_px );
1627 // update the timeline playhead and
1628 $j( '#' + this.seqObj.timeline_id + '_playline' ).css( 'left', left_px );
1630 // pass update request to parent:
1631 this.parent_setSliderValue( perc );
1633 getControlsHTML:function() {
1634 // get controls from current clip add some playlist specific controls:
1635 this.cur_clip.embed.supports['prev_next'] = true;
1636 this.cur_clip.embed.supports['options'] = false;
1637 return ctrlBuilder.getControls( this.cur_clip.embed );
1639 // override renderDisplay
1640 renderDisplay:function() {
1641 js_log( 'mvSequence:renderDisplay' );
1642 // setup layout for title and dc_ clip container
1643 $j( this ).html( '<div id="dc_' + this.id + '" style="width:' + this.width + 'px;' +
1644 'height:' + ( this.height ) + 'px;position:relative;" />' );
1646 this.setupClipDisplay();