Skip to content

Commit 246d63e

Browse files
authored
Merge pull request #4113 from OpenShot/zoom-slider
New Zoom Slider Widget - Modernizing and simplifying timeline scrolling, panning, and zooming 🥳
2 parents 3a31113 + 4be3b27 commit 246d63e

File tree

12 files changed

+699
-160
lines changed

12 files changed

+699
-160
lines changed

src/classes/query.py

+5
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,11 @@ def get(**kwargs):
311311
""" Take any arguments given as filters, and find the first matching object """
312312
return QueryObject.get(Track, **kwargs)
313313

314+
def __lt__(self, other):
315+
return self.data.get('number', 0) < other.data.get('number', 0)
316+
317+
def __gt__(self, other):
318+
return self.data.get('number', 0) > other.data.get('number', 0)
314319

315320
class Effect(QueryObject):
316321
""" This class allows Effects to be queried, updated, and deleted from the project data. """

src/timeline/index.html

+3-3
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
<div class="playhead-line-small"></div>
5252
</div>
5353
<!-- Ruler extends beyond tracks area at least for a width of vertical scroll bar (or more, like 50px here) -->
54-
<canvas tl-ruler id="ruler" width="{{canvasMaxWidth(getTimelineWidth(1024) + 50)}}px" height="39"></canvas>
54+
<canvas tl-ruler id="ruler" width="{{canvasMaxWidth(getTimelineWidth(0) + 6)}}px" height="39"></canvas>
5555

5656
<!-- MARKERS -->
5757
<span class="ruler_marker" id="marker_for_{{marker.id}}">
@@ -76,9 +76,9 @@
7676
</div>
7777
<!-- TRACKS CONTAINER (right of screen) -->
7878
<div tl-scrollable-tracks id="scrolling_tracks">
79-
<div id="track-container" tl-track tl-multi-selectable style="width: {{getTimelineWidth(1024)}}px; padding-bottom: 2px;">
79+
<div id="track-container" tl-track tl-multi-selectable style="width: {{getTimelineWidth(0) - 6}}px; padding-bottom: 2px;">
8080
<!-- TRACKS -->
81-
<div ng-repeat="layer in project.layers.slice().reverse()" id="track_{{layer.number}}" ng-right-click="showTimelineMenu($event, layer.number)" class="{{getTrackStyle(layer.lock)}}" style="width:{{getTimelineWidth(1024)}}px;">
81+
<div ng-repeat="layer in project.layers.slice().reverse()" id="track_{{layer.number}}" ng-right-click="showTimelineMenu($event, layer.number)" class="{{getTrackStyle(layer.lock)}}" style="width:{{getTimelineWidth(0) - 6}}px;">
8282
</div>
8383

8484
<!-- CLIPS -->

src/timeline/js/controllers.js

+26-21
Original file line numberDiff line numberDiff line change
@@ -107,11 +107,6 @@ App.controller("TimelineCtrl", function ($scope) {
107107
var frames_per_second = $scope.project.fps.num / $scope.project.fps.den;
108108
var position_seconds = ((position_frames - 1) / frames_per_second);
109109

110-
// Center on the playhead if it has moved out of view and the timeline should follow it
111-
if ($scope.enable_playhead_follow && !$scope.isTimeVisible(position_seconds)) {
112-
$scope.centerOnTime(position_seconds);
113-
}
114-
115110
// Update internal scope (in seconds)
116111
$scope.movePlayhead(position_seconds);
117112
};
@@ -279,10 +274,19 @@ App.controller("TimelineCtrl", function ($scope) {
279274
});
280275

281276
// Scroll back to correct cursor time (minus the difference of the cursor location)
282-
var new_cursor_x = Math.round((cursor_time * $scope.pixelsPerSecond) - center_x);
277+
var new_cursor_x = Math.max(0, Math.round((cursor_time * $scope.pixelsPerSecond) - center_x));
278+
scrolling_tracks.scrollLeft(new_cursor_x + 1); // force scroll event
283279
scrolling_tracks.scrollLeft(new_cursor_x);
284280
};
285281

282+
// Change the scale and apply to scope
283+
$scope.setScroll = function (normalizedScrollValue) {
284+
var timeline_length = Math.min(32767, $scope.getTimelineWidth(0));
285+
var scrolling_tracks = $("#scrolling_tracks");
286+
var horz_scroll_offset = normalizedScrollValue * timeline_length;
287+
scrolling_tracks.scrollLeft(horz_scroll_offset);
288+
};
289+
286290
// Scroll the timeline horizontally of a certain amount (scrol_value)
287291
$scope.scrollLeft = function (scroll_value) {
288292
var scrolling_tracks = $("#scrolling_tracks");
@@ -948,22 +952,20 @@ App.controller("TimelineCtrl", function ($scope) {
948952
var scrolling_tracks = $("#scrolling_tracks");
949953
var vert_scroll_offset = scrolling_tracks.scrollTop();
950954

951-
$scope.$apply(function () {
952-
// Loop through each layer
953-
for (var layer_index = 0; layer_index < $scope.project.layers.length; layer_index++) {
954-
var layer = $scope.project.layers[layer_index];
955-
956-
// Find element on screen (bound to this layer)
957-
var layer_elem = $("#track_" + layer.number);
958-
if (layer_elem.offset()) {
959-
// Update the top offset
960-
layer.y = layer_elem.offset().top + vert_scroll_offset;
961-
}
955+
// Loop through each layer
956+
for (var layer_index = 0; layer_index < $scope.project.layers.length; layer_index++) {
957+
var layer = $scope.project.layers[layer_index];
958+
959+
// Find element on screen (bound to this layer)
960+
var layer_elem = $("#track_" + layer.number);
961+
if (layer_elem.offset()) {
962+
// Update the top offset
963+
layer.y = layer_elem.offset().top + vert_scroll_offset;
962964
}
963-
// Update playhead height
964-
$scope.playhead_height = $("#track-container").height();
965-
$(".playhead-line").height($scope.playhead_height);
966-
});
965+
}
966+
// Update playhead height
967+
$scope.playhead_height = $("#track-container").height();
968+
$(".playhead-line").height($scope.playhead_height);
967969
};
968970

969971
// Sort clips and transitions by position
@@ -1389,6 +1391,9 @@ App.controller("TimelineCtrl", function ($scope) {
13891391
// Re-index Layer Y values
13901392
$scope.updateLayerIndex();
13911393

1394+
// Force a scroll event (from 1 to 0, to send the geometry to zoom slider)
1395+
$("#scrolling_tracks").scrollLeft(1);
1396+
13921397
// Scroll to top/left when loading a project
13931398
$("#scrolling_tracks").animate({
13941399
scrollTop: 0,

src/timeline/js/directives/clip.js

+6-5
Original file line numberDiff line numberDiff line change
@@ -146,12 +146,13 @@ App.directive("tlClip", function ($timeout) {
146146
}
147147
// Resize timeline if it's too small to contain all clips
148148
scope.resizeTimeline();
149-
150-
// update clip in Qt (very important =)
151-
if (scope.Qt) {
152-
timeline.update_clip_data(JSON.stringify(scope.clip), true, true, false);
153-
}
154149
});
150+
151+
// update clip in Qt (very important =)
152+
if (scope.Qt) {
153+
timeline.update_clip_data(JSON.stringify(scope.clip), true, true, false);
154+
}
155+
155156
//resize the audio canvas to match the new clip width
156157
if (scope.clip.show_audio) {
157158
//redraw audio as the resize cleared the canvas

src/timeline/js/directives/ruler.js

+12-30
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,18 @@ App.directive("tlScrollableTracks", function () {
5353
$("#track_controls").scrollTop(element.scrollTop());
5454
$("#scrolling_ruler").scrollLeft(element.scrollLeft());
5555
$("#progress_container").scrollLeft(element.scrollLeft());
56+
57+
// Send scrollbar position to Qt
58+
if (scope.Qt) {
59+
// Calculate scrollbar positions (left and right edge of scrollbar)
60+
var timeline_length = Math.min(32767, scope.getTimelineWidth(0));
61+
var left_scrollbar_edge = scroll_left_pixels / timeline_length;
62+
var right_scrollbar_edge = (scroll_left_pixels + element.width()) / timeline_length;
63+
64+
// Send normalized scrollbar positions to Qt
65+
timeline.ScrollbarChanged([left_scrollbar_edge, right_scrollbar_edge, timeline_length, element.width()]);
66+
}
67+
5668
});
5769

5870
// Initialize panning when middle mouse is clicked
@@ -205,33 +217,3 @@ App.directive("tlRuler", function ($timeout) {
205217

206218
};
207219
});
208-
209-
210-
// The HTML5 canvas ruler
211-
App.directive("tlRulertime", function () {
212-
return {
213-
restrict: "A",
214-
link: function (scope, element, attrs) {
215-
//on click of the ruler canvas, jump playhead to the clicked spot
216-
element.on("mousedown", function () {
217-
var playhead_seconds = 0.0;
218-
// Update playhead
219-
scope.movePlayhead(playhead_seconds);
220-
scope.previewFrame(playhead_seconds);
221-
222-
});
223-
224-
// Move playhead to new position (if it's not currently being animated)
225-
element.on("mousemove", function (e) {
226-
if (e.which === 1 && !scope.playhead_animating) { // left button
227-
var playhead_seconds = 0.0;
228-
// Update playhead
229-
scope.movePlayhead(playhead_seconds);
230-
scope.previewFrame(playhead_seconds);
231-
}
232-
});
233-
234-
235-
}
236-
};
237-
});

src/timeline/js/directives/transition.js

+5-6
Original file line numberDiff line numberDiff line change
@@ -121,14 +121,13 @@ App.directive("tlTransition", function () {
121121
scope.transition.position = new_left;
122122
scope.transition.end -= delta_time;
123123
}
124-
125-
// update transition in Qt (very important =)
126-
if (scope.Qt) {
127-
timeline.update_transition_data(JSON.stringify(scope.transition), true, false);
128-
}
129-
130124
});
131125

126+
// update transition in Qt (very important =)
127+
if (scope.Qt) {
128+
timeline.update_transition_data(JSON.stringify(scope.transition), true, false);
129+
}
130+
132131
dragLoc = null;
133132

134133
},

src/timeline/media/css/main.css

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
html {
22
height: 100%;
33
}
4-
body {
4+
body {
55
height: 100%;
66
margin: 0;
77
background-repeat: no-repeat;
@@ -161,12 +161,12 @@ img {
161161
.snapping-line.ng-hide-add.ng-hide-add-active { opacity:0.0; }
162162

163163
/* Scrollbar style */
164-
::-webkit-scrollbar { width: 0.9em; height: 0.9em; }
164+
::-webkit-scrollbar { width: 6px; height: 6px; }
165165
::-webkit-scrollbar-thumb { background-color: #4b92ad; border-radius: 0.45em; }
166166

167167
::-webkit-scrollbar-track {
168168
background: #000000;
169-
box-shadow: inset 0 0 0.6em rgba(75,196,233,1.0);
169+
box-shadow: inset 0 0 0.6em rgba(75,196,233,1.0);
170170
border-radius: 0.45em;
171171
}
172172

src/windows/main_window.py

+43-27
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929

3030
import os
3131
import shutil
32-
import sys
3332
import webbrowser
3433
from copy import deepcopy
3534
from time import sleep
@@ -46,12 +45,11 @@
4645
QMessageBox, QDialog, QFileDialog, QInputDialog,
4746
QAction, QActionGroup, QSizePolicy,
4847
QStatusBar, QToolBar, QToolButton,
49-
QLineEdit, QSlider, QLabel, QComboBox, QTextEdit
48+
QLineEdit, QComboBox, QTextEdit
5049
)
5150

5251
from classes import exceptions, info, settings, qt_types, ui_util, updates
5352
from classes.app import get_app
54-
from classes.conversion import zoomToSeconds, secondsToZoom
5553
from classes.exporters.edl import export_edl
5654
from classes.exporters.final_cut_pro import export_xml
5755
from classes.importers.edl import import_edl
@@ -109,6 +107,13 @@ class MainWindow(updates.UpdateWatcher, QMainWindow):
109107
FileUpdated = pyqtSignal(str)
110108
CaptionTextUpdated = pyqtSignal(str, object)
111109
CaptionTextLoaded = pyqtSignal(str, object)
110+
TimelineZoom = pyqtSignal(float) # Signal to zoom into timeline from zoom slider
111+
TimelineScrolled = pyqtSignal(list) # Scrollbar changed signal from timeline
112+
TimelineScroll = pyqtSignal(float) # Signal to force scroll timeline to specific point
113+
TimelineCenter = pyqtSignal() # Signal to force center scroll on playhead
114+
SelectionAdded = pyqtSignal(str, str, bool) # Signal to add a selection
115+
SelectionRemoved = pyqtSignal(str, str) # Signal to remove a selection
116+
SelectionChanged = pyqtSignal() # Signal after selections have been changed (added/removed)
112117

113118
# Docks are closable, movable and floatable
114119
docks_frozen = False
@@ -1869,10 +1874,10 @@ def actionRemoveMarker_trigger(self):
18691874
m.delete()
18701875

18711876
def actionTimelineZoomIn_trigger(self):
1872-
self.sliderZoom.setValue(self.sliderZoom.value() - self.sliderZoom.singleStep())
1877+
self.sliderZoomWidget.zoomIn()
18731878

18741879
def actionTimelineZoomOut_trigger(self):
1875-
self.sliderZoom.setValue(self.sliderZoom.value() + self.sliderZoom.singleStep())
1880+
self.sliderZoomWidget.zoomOut()
18761881

18771882
def actionFullscreen_trigger(self):
18781883
# Toggle fullscreen state (current state mask XOR WindowFullScreen)
@@ -2287,6 +2292,9 @@ def addSelection(self, item_id, item_type, clear_existing=False):
22872292
self.show_property_type = item_type
22882293
self.show_property_timer.start()
22892294

2295+
# Notify UI that selection has been potentially changed
2296+
self.selection_timer.start()
2297+
22902298
# Remove from the selected items
22912299
def removeSelection(self, item_id, item_type):
22922300
# Remove existing selection (if any)
@@ -2318,8 +2326,14 @@ def removeSelection(self, item_id, item_type):
23182326
self.show_property_id = self.selected_effects[0]
23192327
self.show_property_type = item_type
23202328

2321-
# Change selected item in properties view
2329+
# Change selected item
23222330
self.show_property_timer.start()
2331+
self.selection_timer.start()
2332+
2333+
def emit_selection_signal(self):
2334+
"""Emit a signal for selection changed. Callback for selection timer."""
2335+
# Notify UI that selection has been potentially changed
2336+
self.SelectionChanged.emit()
23232337

23242338
def selected_files(self):
23252339
""" Return a list of File objects for the Project Files dock's selection """
@@ -2550,35 +2564,24 @@ def setup_toolbars(self):
25502564

25512565
# Hook up caption editor signal
25522566
self.captionTextEdit.textChanged.connect(self.captionTextEdit_TextChanged)
2553-
self.caption_save_timer = QTimer()
2567+
self.caption_save_timer = QTimer(self)
25542568
self.caption_save_timer.setInterval(100)
25552569
self.caption_save_timer.setSingleShot(True)
25562570
self.caption_save_timer.timeout.connect(self.caption_editor_save)
25572571
self.CaptionTextLoaded.connect(self.caption_editor_load)
25582572
self.caption_model_row = None
25592573

25602574
# Get project's initial zoom value
2561-
initial_scale = get_app().project.get("scale") or 15
2562-
# Round non-exponential scale down to next lowest power of 2
2563-
initial_zoom = secondsToZoom(initial_scale)
2564-
2565-
# Setup Zoom slider
2566-
self.sliderZoom = QSlider(Qt.Horizontal, self)
2567-
self.sliderZoom.setPageStep(1)
2568-
self.sliderZoom.setRange(0, 30)
2569-
self.sliderZoom.setValue(initial_zoom)
2570-
self.sliderZoom.setInvertedControls(True)
2571-
self.sliderZoom.resize(100, 16)
2572-
2573-
self.zoomScaleLabel = QLabel(
2574-
_("{} seconds").format(zoomToSeconds(self.sliderZoom.value()))
2575-
)
2575+
initial_scale = get_app().project.get("scale") or 15.0
2576+
2577+
# Setup Zoom Slider widget
2578+
from windows.views.zoom_slider import ZoomSlider
2579+
self.sliderZoomWidget = ZoomSlider(self)
2580+
self.sliderZoomWidget.setMinimumSize(200, 20)
2581+
self.sliderZoomWidget.setZoomFactor(initial_scale)
25762582

25772583
# add zoom widgets
2578-
self.timelineToolbar.addAction(self.actionTimelineZoomIn)
2579-
self.timelineToolbar.addWidget(self.sliderZoom)
2580-
self.timelineToolbar.addAction(self.actionTimelineZoomOut)
2581-
self.timelineToolbar.addWidget(self.zoomScaleLabel)
2584+
self.timelineToolbar.addWidget(self.sliderZoomWidget)
25822585

25832586
# Add timeline toolbar to web frame
25842587
self.frameWeb.addWidget(self.timelineToolbar)
@@ -2866,11 +2869,19 @@ def __init__(self, *args, mode=None):
28662869
# to update the property model hundreds of times)
28672870
self.show_property_id = None
28682871
self.show_property_type = None
2869-
self.show_property_timer = QTimer()
2872+
self.show_property_timer = QTimer(self)
28702873
self.show_property_timer.setInterval(100)
28712874
self.show_property_timer.setSingleShot(True)
28722875
self.show_property_timer.timeout.connect(self.show_property_timeout)
28732876

2877+
# Selection timer
2878+
# Timer to use a delay before emitting selection signal (to prevent a mass selection from trying
2879+
# to update the zoom slider widget hundreds of times)
2880+
self.selection_timer = QTimer(self)
2881+
self.selection_timer.setInterval(100)
2882+
self.selection_timer.setSingleShot(True)
2883+
self.selection_timer.timeout.connect(self.emit_selection_signal)
2884+
28742885
# Setup video preview QWidget
28752886
self.videoPreview = VideoWidget()
28762887
self.tabVideo.layout().insertWidget(0, self.videoPreview)
@@ -2886,6 +2897,7 @@ def __init__(self, *args, mode=None):
28862897
self.preview_parent = PreviewParent()
28872898
self.preview_parent.Init(self, self.timeline_sync.timeline, self.videoPreview)
28882899
self.preview_thread = self.preview_parent.worker
2900+
self.sliderZoomWidget.connect_playback()
28892901

28902902
# Set pause callback
28912903
self.PauseSignal.connect(self.handlePausedVideo)
@@ -2956,6 +2968,10 @@ def __init__(self, *args, mode=None):
29562968
# Connect OpenProject Signal
29572969
self.OpenProjectSignal.connect(self.open_project)
29582970

2971+
# Connect Selection signals
2972+
self.SelectionAdded.connect(self.addSelection)
2973+
self.SelectionRemoved.connect(self.removeSelection)
2974+
29592975
# Show window
29602976
if self.mode != "unittest":
29612977
self.show()

0 commit comments

Comments
 (0)