diff --git a/src/timeline2/model/timelinemodel.hpp b/src/timeline2/model/timelinemodel.hpp
index b12c4f687..6c919965c 100644
--- a/src/timeline2/model/timelinemodel.hpp
+++ b/src/timeline2/model/timelinemodel.hpp
@@ -1,786 +1,786 @@
/***************************************************************************
* Copyright (C) 2017 by Nicolas Carion *
* This file is part of Kdenlive. See www.kdenlive.org. *
* *
* 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) version 3 or any later version accepted by the *
* membership of KDE e.V. (or its successor approved by the membership *
* of KDE e.V.), which shall act as a proxy defined in Section 14 of *
* version 3 of the license. *
* *
* 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, see . *
***************************************************************************/
#ifndef TIMELINEMODEL_H
#define TIMELINEMODEL_H
#include "definitions.h"
#include "undohelper.hpp"
#include
#include
#include
#include
#include
#include
#include
#include
class AssetParameterModel;
class EffectStackModel;
class ClipModel;
class CompositionModel;
class DocUndoStack;
class GroupsModel;
class SnapModel;
class TimelineItemModel;
class TrackModel;
/* @brief This class represents a Timeline object, as viewed by the backend.
In general, the Gui associated with it will send modification queries (such as resize or move), and this class authorize them or not depending on the
validity of the modifications.
This class also serves to keep track of all objects. It holds pointers to all tracks and clips, and gives them unique IDs on creation. These Ids are used in
any interactions with the objects and have nothing to do with Melt IDs.
This is the entry point for any modifications that has to be made on an element. The dataflow beyond this entry point may vary, for example when the user
request a clip resize, the call is deferred to the clip itself, that check if there is enough data to extend by the requested amount, compute the new in and
out, and then asks the track if there is enough room for extension. To avoid any confusion on which function to call first, rembember to always call the
version in timeline. This is also required to generate the Undo/Redo operators
The undo/redo system is designed around lambda functions. Each time a function executes an elementary change to the model, it writes the corresponding
operation and its reverse, respectively in the redo and the undo lambdas. This way, if an operation fails for some reason, we can easily cancel the steps
that have been done so far without corrupting anything. The other advantage is that operations are easy to compose, and you get a undo/redo pair for free no
matter in which way you combine them.
Most of the modification functions are named requestObjectAction. Eg, if the object is a clip and we want to move it, we call requestClipMove. These
functions always return a bool indicating success, and when they return false they should guarantee than nothing has been modified. Most of the time, these
functions come in two versions: the first one is the entry point if you want to perform only the action (and not compose it with other actions). This version
will generally automatically push and Undo object on the Application stack, in case the user later wants to cancel the operation. It also generally goes the
extra mile to ensure the operation is done in a way that match the user's expectation: for example requestClipMove checks whether the clip belongs to a group
and in that case actually mouves the full group. The other version of the function, if it exists, is intended for composition (using the action as part of a
complex operation). It takes as input the undo/redo lambda corresponding to the action that is being performed and accumulates on them. Note that this
version does the minimal job: in the example of the requestClipMove, it will not move the full group if the clip is in a group.
Generally speaking, we don't check ahead of time if an action is going to succeed or not before applying it.
We just apply it naively, and if it fails at some point, we use the undo operator that we are constructing on the fly to revert what we have done so far.
For example, when we move a group of clips, we apply the move operation to all the clips inside this group (in the right order). If none fails, we are good,
otherwise we revert what we've already done.
This kind of behaviour frees us from the burden of simulating the actions before actually applying theme. This is a good thing because this simulation step
would be very sensitive to corruptions and small discrepancies, which we try to avoid at all cost.
It derives from AbstractItemModel (indirectly through TimelineItemModel) to provide the model to the QML interface. An itemModel is organized with row and
columns that contain the data. It can be hierarchical, meaning that a given index (row,column) can contain another level of rows and column.
Our organization is as follows: at the top level, each row contains a track. These rows are in the same order as in the actual timeline.
Then each of this row contains itself sub-rows that correspond to the clips.
Here the order of these sub-rows is unrelated to the chronological order of the clips,
but correspond to their Id order. For example, if you have three clips, with ids 12, 45 and 150, they will receive row index 0,1 and 2.
This is because the order actually doesn't matter since the clips are rendered based on their positions rather than their row order.
The id order has been chosen because it is consistent with a valid ordering of the clips.
The columns are never used, so the data is always in column 0
An ModelIndex in the ItemModel consists of a row number, a column number, and a parent index. In our case, tracks have always an empty parent, and the clip
have a track index as parent.
A ModelIndex can also store one additional integer, and we exploit this feature to store the unique ID of the object it corresponds to.
*/
class TimelineModel : public QAbstractItemModel_shared_from_this
{
Q_OBJECT
protected:
/* @brief this constructor should not be called. Call the static construct instead
*/
TimelineModel(Mlt::Profile *profile, std::weak_ptr undo_stack);
public:
friend class TrackModel;
template friend class MoveableItem;
friend class ClipModel;
friend class CompositionModel;
friend class GroupsModel;
friend class TimelineController;
friend struct TimelineFunctions;
/// Two level model: tracks and clips on track
enum {
NameRole = Qt::UserRole + 1,
ResourceRole, /// clip only
ServiceRole, /// clip only
StartRole, /// clip only
BinIdRole, /// clip only
TrackIdRole,
FakeTrackIdRole,
FakePositionRole,
MarkersRole, /// clip only
StatusRole, /// clip only
TypeRole, /// clip only
KeyframesRole,
DurationRole,
InPointRole, /// clip only
OutPointRole, /// clip only
FramerateRole, /// clip only
GroupedRole, /// clip only
HasAudio, /// clip only
CanBeAudioRole, /// clip only
CanBeVideoRole, /// clip only
IsDisabledRole, /// track only
IsAudioRole,
SortRole,
ShowKeyframesRole,
AudioLevelsRole, /// clip only
AudioChannelsRole, /// clip only
IsCompositeRole, /// track only
IsLockedRole, /// track only
HeightRole, /// track only
TrackTagRole, /// track only
FadeInRole, /// clip only
FadeOutRole, /// clip only
FileHashRole, /// clip only
SpeedRole, /// clip only
ReloadThumbRole, /// clip only
ItemATrack, /// composition only
ItemIdRole,
ThumbsFormatRole, /// track only
EffectNamesRole, // track and clip only
EffectsEnabledRole, // track and clip only
GrabbedRole, /// clip+composition only
TrackActiveRole, /// track only
AudioRecordRole /// track only
};
~TimelineModel() override;
Mlt::Tractor *tractor() const { return m_tractor.get(); }
/* @brief Load tracks from the current tractor, used on project opening
*/
void loadTractor();
/* @brief Returns the current tractor's producer, useful fo control seeking, playing, etc
*/
std::shared_ptr producer();
Mlt::Profile *getProfile();
/* @brief returns the number of tracks */
int getTracksCount() const;
/* @brief returns the track index (id) from its position */
int getTrackIndexFromPosition(int pos) const;
/* @brief returns the track index (id) from its position */
Q_INVOKABLE bool isAudioTrack(int trackId) const;
/* @brief returns the number of clips */
int getClipsCount() const;
/* @brief returns the number of compositions */
int getCompositionsCount() const;
/* @brief Returns the id of the track containing clip (-1 if it is not inserted)
@param clipId Id of the clip to test */
Q_INVOKABLE int getClipTrackId(int clipId) const;
/* @brief Returns the id of the track containing composition (-1 if it is not inserted)
@param clipId Id of the composition to test */
Q_INVOKABLE int getCompositionTrackId(int compoId) const;
/* @brief Convenience function that calls either of the previous ones based on item type*/
Q_INVOKABLE int getItemTrackId(int itemId) const;
Q_INVOKABLE int getCompositionPosition(int compoId) const;
int getCompositionPlaytime(int compoId) const;
/* Returns an item position, item can be clip or composition */
Q_INVOKABLE int getItemPosition(int itemId) const;
/* Returns an item duration, item can be clip or composition */
int getItemPlaytime(int itemId) const;
/* Returns the current speed of a clip */
double getClipSpeed(int clipId) const;
/* @brief Helper function to query the amount of free space around a clip
* @param clipId: the queried clip. If it is not inserted on a track, this functions returns 0
* @param after: if true, we return the blank after the clip, otherwise, before.
*/
int getBlankSizeNearClip(int clipId, bool after) const;
/* @brief if the clip belongs to a AVSplit group, then return the id of the other corresponding clip. Otherwise, returns -1 */
int getClipSplitPartner(int clipId) const;
/* @brief Helper function that returns true if the given ID corresponds to a clip */
- bool isClip(int id) const;
+ Q_INVOKABLE bool isClip(int id) const;
/* @brief Helper function that returns true if the given ID corresponds to a composition */
- bool isComposition(int id) const;
+ Q_INVOKABLE bool isComposition(int id) const;
/* @brief Helper function that returns true if the given ID corresponds to a track */
- bool isTrack(int id) const;
+ Q_INVOKABLE bool isTrack(int id) const;
/* @brief Helper function that returns true if the given ID corresponds to a group */
- bool isGroup(int id) const;
+ Q_INVOKABLE bool isGroup(int id) const;
/* @brief Given a composition Id, returns its underlying parameter model */
std::shared_ptr getCompositionParameterModel(int compoId) const;
/* @brief Given a clip Id, returns its underlying effect stack model */
std::shared_ptr getClipEffectStackModel(int clipId) const;
/* @brief Returns the position of clip (-1 if it is not inserted)
@param clipId Id of the clip to test
*/
Q_INVOKABLE int getClipPosition(int clipId) const;
Q_INVOKABLE bool addClipEffect(int clipId, const QString &effectId, bool notify = true);
Q_INVOKABLE bool addTrackEffect(int trackId, const QString &effectId);
bool removeFade(int clipId, bool fromStart);
Q_INVOKABLE bool copyClipEffect(int clipId, const QString &sourceId);
Q_INVOKABLE bool copyTrackEffect(int trackId, const QString &sourceId);
bool adjustEffectLength(int clipId, const QString &effectId, int duration, int initialDuration);
/* @brief Returns the closest snap point within snapDistance
*/
Q_INVOKABLE int suggestSnapPoint(int pos, int snapDistance);
/** @brief Return the previous track of same type as source trackId, or trackId if no track found */
Q_INVOKABLE int getPreviousTrackId(int trackId);
/** @brief Return the next track of same type as source trackId, or trackId if no track found */
Q_INVOKABLE int getNextTrackId(int trackId);
/* @brief Returns the in cut position of a clip
@param clipId Id of the clip to test
*/
int getClipIn(int clipId) const;
/* @brief Returns the clip state (audio/video only)
*/
PlaylistState::ClipState getClipState(int clipId) const;
/* @brief Returns the bin id of the clip master
@param clipId Id of the clip to test
*/
const QString getClipBinId(int clipId) const;
/* @brief Returns the duration of a clip
@param clipId Id of the clip to test
*/
int getClipPlaytime(int clipId) const;
/* @brief Returns the size of the clip's frame (widthxheight)
@param clipId Id of the clip to test
*/
QSize getClipFrameSize(int clipId) const;
/* @brief Returns the number of clips in a given track
@param trackId Id of the track to test
*/
int getTrackClipsCount(int trackId) const;
/* @brief Returns the number of compositions in a given track
@param trackId Id of the track to test
*/
int getTrackCompositionsCount(int trackId) const;
/* @brief Returns the position of the track in the order of the tracks
@param trackId Id of the track to test
*/
int getTrackPosition(int trackId) const;
/* @brief Returns the track's index in terms of mlt's internal representation
*/
int getTrackMltIndex(int trackId) const;
/* @brief Returns a sort position for tracks.
* @param separated: if true, the tracks will be sorted like: V2,V1,A1,A2
* Otherwise, the tracks will be sorted like V2,A2,V1,A1
*/
int getTrackSortValue(int trackId, bool separated) const;
/* @brief Returns the ids of the tracks below the given track in the order of the tracks
Returns an empty list if no track available
@param trackId Id of the track to test
*/
QList getLowerTracksId(int trackId, TrackType type = TrackType::AnyTrack) const;
/* @brief Returns the MLT track index of the video track just below the given track
@param trackId Id of the track to test
*/
int getPreviousVideoTrackPos(int trackId) const;
/* @brief Returns the Track id of the video track just below the given track
@param trackId Id of the track to test
*/
int getPreviousVideoTrackIndex(int trackId) const;
/* @brief Returns the Id of the corresponding audio track. If trackId corresponds to video1, this will return audio 1 and so on */
int getMirrorAudioTrackId(int trackId) const;
int getMirrorVideoTrackId(int trackId) const;
int getMirrorTrackId(int trackId) const;
/* @brief Move a clip to a specific position
This action is undoable
Returns true on success. If it fails, nothing is modified.
If the clip is not in inserted in a track yet, it gets inserted for the first time.
If the clip is in a group, the call is deferred to requestGroupMove
@param clipId is the ID of the clip
@param trackId is the ID of the target track
@param position is the position where we want to move
@param updateView if set to false, no signal is sent to qml
@param logUndo if set to false, no undo object is stored
*/
Q_INVOKABLE bool requestClipMove(int clipId, int trackId, int position, bool updateView = true, bool logUndo = true, bool invalidateTimeline = false);
/* @brief Move a composition to a specific position This action is undoable
Returns true on success. If it fails, nothing is modified. If the clip is
not in inserted in a track yet, it gets inserted for the first time. If
the clip is in a group, the call is deferred to requestGroupMove @param
transid is the ID of the composition @param trackId is the ID of the
track */
Q_INVOKABLE bool requestCompositionMove(int compoId, int trackId, int position, bool updateView = true, bool logUndo = true);
/* Same function, but accumulates undo and redo, and doesn't check
for group*/
bool requestClipMove(int clipId, int trackId, int position, bool updateView, bool invalidateTimeline, Fun &undo, Fun &redo);
bool requestCompositionMove(int transid, int trackId, int compositionTrack, int position, bool updateView, bool finalMove, Fun &undo, Fun &redo);
/* When timeline edit mode is insert or overwrite, we fake the move (as it will overlap existing clips, and only process the real move on drop */
bool requestFakeClipMove(int clipId, int trackId, int position, bool updateView, bool invalidateTimeline, Fun &undo, Fun &redo);
bool requestFakeClipMove(int clipId, int trackId, int position, bool updateView, bool logUndo, bool invalidateTimeline);
bool requestFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView = true, bool logUndo = true);
bool requestFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo,
bool allowViewRefresh = true);
/* @brief Given an intended move, try to suggest a more valid one
(accounting for snaps and missing UI calls)
@param clipId id of the clip to
move
@param trackId id of the target track
@param position target position
@param snapDistance the maximum distance for a snap result, -1 for no snapping
of the clip
@param dontRefreshMasterClip when false, no view refresh is attempted
*/
Q_INVOKABLE int suggestItemMove(int itemId, int trackId, int position, int cursorPosition, int snapDistance = -1);
Q_INVOKABLE int suggestClipMove(int clipId, int trackId, int position, int cursorPosition, int snapDistance = -1);
Q_INVOKABLE int suggestCompositionMove(int compoId, int trackId, int position, int cursorPosition, int snapDistance = -1);
/* @brief Request clip insertion at given position. This action is undoable
Returns true on success. If it fails, nothing is modified.
@param binClipId id of the clip in the bin
@param track Id of the track where to insert
@param position Requested position
@param ID return parameter of the id of the inserted clip
@param logUndo if set to false, no undo object is stored
@param refreshView whether the view should be refreshed
@param useTargets: if true, the Audio/video split will occur on the set targets. Otherwise, they will be computed as an offset from the middle line
*/
bool requestClipInsertion(const QString &binClipId, int trackId, int position, int &id, bool logUndo = true, bool refreshView = false,
bool useTargets = true);
/* Same function, but accumulates undo and redo*/
bool requestClipInsertion(const QString &binClipId, int trackId, int position, int &id, bool logUndo, bool refreshView, bool useTargets, Fun &undo,
Fun &redo);
protected:
/* @brief Creates a new clip instance without inserting it.
This action is undoable, returns true on success
@param binClipId: Bin id of the clip to insert
@param id: return parameter for the id of the newly created clip.
@param state: The desired clip state (original, audio/video only).
*/
bool requestClipCreation(const QString &binClipId, int &id, PlaylistState::ClipState state, double speed, Fun &undo, Fun &redo);
public:
/* @brief Deletes the given clip or composition from the timeline This
action is undoable Returns true on success. If it fails, nothing is
modified. If the clip/composition is in a group, the call is deferred to
requestGroupDeletion @param clipId is the ID of the clip/composition
@param logUndo if set to false, no undo object is stored */
Q_INVOKABLE bool requestItemDeletion(int clipId, bool logUndo = true);
/* Same function, but accumulates undo and redo*/
bool requestItemDeletion(int clipId, Fun &undo, Fun &redo);
/* @brief Move a group to a specific position
This action is undoable
Returns true on success. If it fails, nothing is modified.
If the clips in the group are not in inserted in a track yet, they get inserted for the first time.
@param clipId is the id of the clip that triggers the group move
@param groupId is the id of the group
@param delta_track is the delta applied to the track index
@param delta_pos is the requested position change
@param updateView if set to false, no signal is sent to qml for the clip clipId
@param logUndo if set to true, an undo object is created
@param allowViewRefresh if false, the view will never get updated (useful for suggestMove)
*/
bool requestGroupMove(int itemId, int groupId, int delta_track, int delta_pos, bool updateView = true, bool logUndo = true);
bool requestGroupMove(int itemId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo,
bool allowViewRefresh = true);
/* @brief Deletes all clips inside the group that contains the given clip.
This action is undoable
Note that if their is a hierarchy of groups, all of them will be deleted.
Returns true on success. If it fails, nothing is modified.
@param clipId is the id of the clip that triggers the group deletion
*/
Q_INVOKABLE bool requestGroupDeletion(int clipId, bool logUndo = true);
bool requestGroupDeletion(int clipId, Fun &undo, Fun &redo);
/* @brief Change the duration of an item (clip or composition)
This action is undoable
Returns the real size reached (can be different, if snapping occurs).
If it fails, nothing is modified, and -1 is returned
@param itemId is the ID of the item
@param size is the new size of the item
@param right is true if we change the right side of the item, false otherwise
@param logUndo if set to true, an undo object is created
@param snap if set to true, the resize order will be coerced to use the snapping grid
*/
Q_INVOKABLE int requestItemResize(int itemId, int size, bool right, bool logUndo = true, int snapDistance = -1, bool allowSingleResize = false);
/* Same function, but accumulates undo and redo and doesn't deal with snapping*/
bool requestItemResize(int itemId, int size, bool right, bool logUndo, Fun &undo, Fun &redo, bool blockUndo = false);
/* @brief Group together a set of ids
The ids are either a group ids or clip ids. The involved clip must already be inserted in a track
This action is undoable
Returns the group id on success, -1 if it fails and nothing is modified.
Typically, ids would be ids of clips, but for convenience, some of them can be ids of groups as well.
@param ids Set of ids to group
*/
int requestClipsGroup(const std::unordered_set &ids, bool logUndo = true, GroupType type = GroupType::Normal);
int requestClipsGroup(const std::unordered_set &ids, Fun &undo, Fun &redo, GroupType type = GroupType::Normal);
/* @brief Destruct the topmost group containing clip
This action is undoable
Returns true on success. If it fails, nothing is modified.
@param id of the clip to degroup (all clips belonging to the same group will be ungrouped as well)
*/
bool requestClipUngroup(int itemId, bool logUndo = true);
/* Same function, but accumulates undo and redo*/
bool requestClipUngroup(int itemId, Fun &undo, Fun &redo);
// convenience functions for several ids at the same time
bool requestClipsUngroup(const std::unordered_set &itemIds, bool logUndo = true);
/* @brief Create a track at given position
This action is undoable
Returns true on success. If it fails, nothing is modified.
@param Requested position (order). If set to -1, the track is inserted last.
@param id is a return parameter that holds the id of the resulting track (-1 on failure)
*/
bool requestTrackInsertion(int pos, int &id, const QString &trackName = QString(), bool audioTrack = false);
/* Same function, but accumulates undo and redo*/
bool requestTrackInsertion(int pos, int &id, const QString &trackName, bool audioTrack, Fun &undo, Fun &redo, bool updateView = true);
/* @brief Delete track with given id
This also deletes all the clips contained in the track.
This action is undoable
Returns true on success. If it fails, nothing is modified.
@param trackId id of the track to delete
*/
bool requestTrackDeletion(int trackId);
/* Same function, but accumulates undo and redo*/
bool requestTrackDeletion(int trackId, Fun &undo, Fun &redo);
/* @brief Get project duration
Returns the duration in frames
*/
int duration() const;
static int seekDuration; // Duration after project end where seeking is allowed
/* @brief Get all the elements of the same group as the given clip.
If there is a group hierarchy, only the topmost group is considered.
@param clipId id of the clip to test
*/
std::unordered_set getGroupElements(int clipId);
/* @brief Removes all the elements on the timeline (tracks and clips)
*/
bool requestReset(Fun &undo, Fun &redo);
/* @brief Updates the current the pointer to the current undo_stack
Must be called for example when the doc change
*/
void setUndoStack(std::weak_ptr undo_stack);
protected:
/* @brief Requests the best snapped position for a clip
@param pos is the clip's requested position
@param length is the clip's duration
@param pts snap points to ignore (for example currently moved clip)
@param snapDistance the maximum distance for a snap result, -1 for no snapping
@returns best snap position or -1 if no snap point is near
*/
int getBestSnapPos(int pos, int length, const std::vector &pts = std::vector(), int cursorPosition = 0, int snapDistance = -1);
public:
/* @brief Requests the next snapped point
@param pos is the current position
*/
int getNextSnapPos(int pos);
/* @brief Requests the previous snapped point
@param pos is the current position
*/
int getPreviousSnapPos(int pos);
/* @brief Add a new snap point
@param pos is the current position
*/
void addSnap(int pos);
/* @brief Remove snap point
@param pos is the current position
*/
void removeSnap(int pos);
/* @brief Request composition insertion at given position.
This action is undoable
Returns true on success. If it fails, nothing is modified.
@param transitionId Identifier of the Mlt transition to insert (as given by repository)
@param track Id of the track where to insert
@param position Requested position
@param length Requested initial length.
@param id return parameter of the id of the inserted composition
@param logUndo if set to false, no undo object is stored
*/
bool requestCompositionInsertion(const QString &transitionId, int trackId, int position, int length, std::unique_ptr transProps, int &id,
bool logUndo = true);
/* Same function, but accumulates undo and redo*/
bool requestCompositionInsertion(const QString &transitionId, int trackId, int compositionTrack, int position, int length,
std::unique_ptr transProps, int &id, Fun &undo, Fun &redo, bool finalMove = false);
/* @brief This function change the global (timeline-wise) enabled state of the effects
It disables/enables track and clip effects (recursively)
*/
void setTimelineEffectsEnabled(bool enabled);
/* @brief Get a timeline clip id by its position or -1 if not found
*/
int getClipByPosition(int trackId, int position) const;
/* @brief Get a timeline composition id by its starting position or -1 if not found
*/
int getCompositionByPosition(int trackId, int position) const;
/* @brief Returns a list of all items that are intersect with a given range.
* @param trackId is the id of the track for concerned items. Setting trackId to -1 returns items on all tracks
* @param start is the position where we the items should start
* @param end is the position after which items will not be selected, set to -1 to get all clips on track
* @param listCompositions if enabled, the list will also contains composition ids
*/
std::unordered_set getItemsInRange(int trackId, int start, int end = -1, bool listCompositions = true);
/* @brief Returns a list of all luma files used in the project
*/
QStringList extractCompositionLumas() const;
/* @brief Inform asset view of duration change
*/
virtual void adjustAssetRange(int clipId, int in, int out);
void requestClipReload(int clipId);
void requestClipUpdate(int clipId, const QVector &roles);
/** @brief define current edit mode (normal, insert, overwrite */
void setEditMode(TimelineMode::EditMode mode);
Q_INVOKABLE bool normalEdit() const;
/** @brief Returns the effectstack of a given clip. */
std::shared_ptr getClipEffectStack(int itemId);
std::shared_ptr getTrackEffectStackModel(int trackId);
/** @brief Add slowmotion effect to clip in timeline.
@param clipId id of the target clip
@param speed: speed in percentage. 100 corresponds to original speed, 50 to half the speed
This functions create an undo object and also apply the effect to the corresponding audio if there is any.
Returns true on success, false otherwise (and nothing is modified)
*/
bool requestClipTimeWarp(int clipId, double speed);
/* @brief Same function as above, but doesn't check for paired audio and accumulate undo/redo
*/
bool requestClipTimeWarp(int clipId, double speed, Fun &undo, Fun &redo);
void replugClip(int clipId);
/** @brief Refresh the tractor profile in case a change was requested. */
void updateProfile(Mlt::Profile *profile);
/** @brief Clear the current selection
@param onDeletion is true when the selection is cleared as a result of a deletion
*/
Q_INVOKABLE void requestClearSelection(bool onDeletion = false);
// same function with undo/redo accumulation
void requestClearSelection(bool onDeletion, Fun &undo, Fun &redo);
/** @brief Add the given item to the selection
If @param clear is true, the selection is first cleared
*/
Q_INVOKABLE void requestAddToSelection(int itemId, bool clear = false);
/** @brief Remove the given item from the selection */
Q_INVOKABLE void requestRemoveFromSelection(int itemId);
/** @brief Set the selection to the set of given ids */
bool requestSetSelection(const std::unordered_set &ids);
// same function with undo/redo
bool requestSetSelection(const std::unordered_set &ids, Fun &undo, Fun &redo);
/** @brief Returns a set containing all the items in the selection */
std::unordered_set getCurrentSelection() const;
protected:
/* @brief Register a new track. This is a call-back meant to be called from TrackModel
@param pos indicates the number of the track we are adding. If this is -1, then we add at the end.
*/
void registerTrack(std::shared_ptr track, int pos = -1, bool doInsert = true, bool reloadView = true);
/* @brief Register a new clip. This is a call-back meant to be called from ClipModel
*/
void registerClip(const std::shared_ptr &clip, bool registerProducer = false);
/* @brief Register a new composition. This is a call-back meant to be called from CompositionModel
*/
void registerComposition(const std::shared_ptr &composition);
/* @brief Register a new group. This is a call-back meant to be called from GroupsModel
*/
void registerGroup(int groupId);
/* @brief Deregister and destruct the track with given id.
@parame updateView Whether to send updates to the model. Must be false when called from a constructor/destructor
*/
Fun deregisterTrack_lambda(int id, bool updateView = false);
/* @brief Return a lambda that deregisters and destructs the clip with given id.
Note that the clip must already be deleted from its track and groups.
*/
Fun deregisterClip_lambda(int id);
/* @brief Return a lambda that deregisters and destructs the composition with given id.
*/
Fun deregisterComposition_lambda(int compoId);
/* @brief Deregister a group with given id
*/
void deregisterGroup(int id);
/* @brief Helper function to get a pointer to the track, given its id
*/
std::shared_ptr getTrackById(int trackId);
const std::shared_ptr getTrackById_const(int trackId) const;
/*@brief Helper function to get a pointer to a clip, given its id*/
std::shared_ptr getClipPtr(int clipId) const;
/*@brief Helper function to get a pointer to a composition, given its id*/
std::shared_ptr getCompositionPtr(int compoId) const;
/* @brief Returns next valid unique id to create an object
*/
static int getNextId();
/* @brief unplant and the replant all the compositions in the correct order
@param currentCompo is the id of a compo that have not yet been planted, if any. Otherwise send -1
*/
bool replantCompositions(int currentCompo, bool updateView);
/* @brief Unplant the composition with given Id */
bool unplantComposition(int compoId);
/* Same function but accumulates undo and redo, and doesn't check for group*/
bool requestClipDeletion(int clipId, Fun &undo, Fun &redo);
bool requestCompositionDeletion(int compositionId, Fun &undo, Fun &redo);
/** @brief Check tracks duration and update black track accordingly */
void updateDuration();
/** @brief Get a track tag (A1, V1, V2,...) through its id */
const QString getTrackTagById(int trackId) const;
/** @brief Attempt to make a clip move without ever updating the view */
bool requestClipMoveAttempt(int clipId, int trackId, int position);
public:
/* @brief Debugging function that checks consistency with Mlt objects */
bool checkConsistency();
protected:
/* @brief Refresh project monitor if cursor was inside range */
void checkRefresh(int start, int end);
/* @brief Send signal to require clearing effet/composition view */
void clearAssetView(int itemId);
bool m_blockRefresh;
signals:
/* @brief signal triggered by clearAssetView */
void requestClearAssetView(int);
void requestMonitorRefresh();
/* @brief signal triggered by track operations */
void invalidateZone(int in, int out);
/* @brief signal triggered when a track duration changed (insertion/deletion) */
void durationUpdated();
/* @brief Signal sent whenever the selection changes */
void selectionChanged();
protected:
std::unique_ptr m_tractor;
std::list> m_allTracks;
std::unordered_map>::iterator>
m_iteratorTable; // this logs the iterator associated which each track id. This allows easy access of a track based on its id.
std::unordered_map> m_allClips; // the keys are the clip id, and the values are the corresponding pointers
std::unordered_map>
m_allCompositions; // the keys are the composition id, and the values are the corresponding pointers
static int next_id; // next valid id to assign
std::unique_ptr m_groups;
std::shared_ptr m_snaps;
std::unordered_set m_allGroups; // ids of all the groups
std::weak_ptr m_undoStack;
Mlt::Profile *m_profile;
// The black track producer. Its length / out should always be adjusted to the projects's length
std::unique_ptr m_blackClip;
mutable QReadWriteLock m_lock; // This is a lock that ensures safety in case of concurrent access
bool m_timelineEffectsEnabled;
bool m_id; // id of the timeline itself
// id of the selection. If -1, there is no selection, if positive, then it might either be the id of the selection group, or the id of an individual
// item, or, finally, the id of a group which is not of type selection. The last case happens when the selection exactly matches an existing group
// (in that case we cannot further group it because the selection would have only one child, which is prohibited by design)
int m_currentSelection = -1;
// The index of the temporary overlay track in tractor, or -1 if not connected
int m_overlayTrackCount;
// The preferred audio target for clip insertion or -1 if not defined
int m_audioTarget;
// The preferred video target for clip insertion or -1 if not defined
int m_videoTarget;
// Timeline editing mode
TimelineMode::EditMode m_editMode;
// what follows are some virtual function that corresponds to the QML. They are implemented in TimelineItemModel
protected:
virtual void _beginRemoveRows(const QModelIndex &, int, int) = 0;
virtual void _beginInsertRows(const QModelIndex &, int, int) = 0;
virtual void _endRemoveRows() = 0;
virtual void _endInsertRows() = 0;
virtual void notifyChange(const QModelIndex &topleft, const QModelIndex &bottomright, bool start, bool duration, bool updateThumb) = 0;
virtual void notifyChange(const QModelIndex &topleft, const QModelIndex &bottomright, const QVector &roles) = 0;
virtual void notifyChange(const QModelIndex &topleft, const QModelIndex &bottomright, int role) = 0;
virtual QModelIndex makeClipIndexFromID(int) const = 0;
virtual QModelIndex makeCompositionIndexFromID(int) const = 0;
virtual QModelIndex makeTrackIndexFromID(int) const = 0;
virtual void _resetView() = 0;
};
#endif
diff --git a/src/timeline2/view/qml/timeline.qml b/src/timeline2/view/qml/timeline.qml
index ac8127e1c..dd96d274f 100644
--- a/src/timeline2/view/qml/timeline.qml
+++ b/src/timeline2/view/qml/timeline.qml
@@ -1,1383 +1,1384 @@
import QtQuick 2.6
import QtQml.Models 2.2
import QtQuick.Controls 1.4 as OLD
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import QtQuick.Dialogs 1.2
import Kdenlive.Controls 1.0
import QtQuick.Window 2.2
import 'Timeline.js' as Logic
Rectangle {
id: root
objectName: "timelineview"
SystemPalette { id: activePalette }
color: activePalette.window
property bool validMenu: false
property color textColor: activePalette.text
signal clipClicked()
signal mousePosChanged(int position)
signal zoomIn(bool onMouse)
signal zoomOut(bool onMouse)
FontMetrics {
id: fontMetrics
font.family: "Arial"
}
ClipMenu {
id: clipMenu
}
CompositionMenu {
id: compositionMenu
}
function fitZoom() {
return scrollView.width / (timeline.duration * 1.1)
}
function scrollPos() {
return scrollView.flickableItem.contentX
}
function goToStart(pos) {
scrollView.flickableItem.contentX = pos
}
function updatePalette() {
root.color = activePalette.window
root.textColor = activePalette.text
playhead.fillColor = activePalette.windowText
ruler.repaintRuler()
}
function moveSelectedTrack(offset) {
var cTrack = Logic.getTrackIndexFromId(timeline.activeTrack)
var newTrack = cTrack + offset
var max = tracksRepeater.count;
if (newTrack < 0) {
newTrack = max - 1;
} else if (newTrack >= max) {
newTrack = 0;
}
console.log('Setting curr tk: ', newTrack, 'MAX: ',max)
timeline.activeTrack = tracksRepeater.itemAt(newTrack).trackInternalId
}
function zoomByWheel(wheel) {
if (wheel.modifiers & Qt.AltModifier) {
if (wheel.angleDelta.x > 0) {
timeline.triggerAction('monitor_seek_snap_backward')
} else {
timeline.triggerAction('monitor_seek_snap_forward')
}
} else if (wheel.modifiers & Qt.ControlModifier) {
if (wheel.angleDelta.y > 0) {
root.zoomIn(true);
} else {
root.zoomOut(true);
}
} else {
var newScroll = Math.min(scrollView.flickableItem.contentX - wheel.angleDelta.y, timeline.fullDuration * root.timeScale - (scrollView.width - scrollView.__verticalScrollBar.width))
scrollView.flickableItem.contentX = Math.max(newScroll, 0)
}
wheel.accepted = true
}
function continuousScrolling(x) {
// This provides continuous scrolling at the left/right edges.
if (x > scrollView.flickableItem.contentX + scrollView.width - 50) {
scrollTimer.item = clip
scrollTimer.backwards = false
scrollTimer.start()
} else if (x < 50) {
scrollView.flickableItem.contentX = 0;
scrollTimer.stop()
} else if (x < scrollView.flickableItem.contentX + 50) {
scrollTimer.item = clip
scrollTimer.backwards = true
scrollTimer.start()
} else {
scrollTimer.stop()
}
}
function getTrackYFromId(a_track) {
return Logic.getTrackYFromId(a_track)
}
function getTrackYFromMltIndex(a_track) {
return Logic.getTrackYFromMltIndex(a_track)
}
function getTracksCount() {
return Logic.getTracksList()
}
function getMousePos() {
return (scrollView.flickableItem.contentX + tracksArea.mouseX) / timeline.scaleFactor
}
function getScrollPos() {
return scrollView.flickableItem.contentX
}
function setScrollPos(pos) {
return scrollView.flickableItem.contentX = pos
}
function getCopiedItemId() {
return copiedClip
}
function getMouseTrack() {
return Logic.getTrackIdFromPos(tracksArea.mouseY - ruler.height + scrollView.flickableItem.contentY)
}
function getTrackColor(audio, header) {
var col = activePalette.alternateBase
if (audio) {
col = Qt.tint(col, "#06FF00CC")
}
if (header) {
col = Qt.darker(col, 1.05)
}
return col
}
function clearDropData() {
clipBeingDroppedId = -1
droppedPosition = -1
droppedTrack = -1
scrollTimer.running = false
scrollTimer.stop()
}
function initDrag(itemObject, itemCoord, itemId, itemPos, itemTrack, isComposition) {
dragProxy.x = itemObject.modelStart * timeScale
dragProxy.y = itemCoord.y
dragProxy.width = itemObject.clipDuration * timeScale
dragProxy.height = itemCoord.height
dragProxy.masterObject = itemObject
dragProxy.draggedItem = itemId
dragProxy.sourceTrack = itemTrack
dragProxy.sourceFrame = itemPos
dragProxy.isComposition = isComposition
dragProxy.verticalOffset = isComposition ? itemObject.displayHeight : 0
}
function endDrag() {
dragProxy.draggedItem = -1
dragProxy.x = 0
dragProxy.y = 0
dragProxy.width = 0
dragProxy.height = 0
dragProxy.verticalOffset = 0
}
property int headerWidth: timeline.headerWidth()
property int activeTool: 0
property real baseUnit: fontMetrics.font.pointSize
property color selectedTrackColor: Qt.rgba(activePalette.highlight.r, activePalette.highlight.g, activePalette.highlight.b, 0.2)
property color frameColor: Qt.rgba(activePalette.shadow.r, activePalette.shadow.g, activePalette.shadow.b, 0.3)
property bool stopScrolling: false
property int duration: timeline.duration
property color audioColor: timeline.audioColor
property color videoColor: timeline.videoColor
property color neutralColor: timeline.neutralColor
property color groupColor: timeline.groupColor
property int clipBeingDroppedId: -1
property string clipBeingDroppedData
property int droppedPosition: -1
property int droppedTrack: -1
property int clipBeingMovedId: -1
property int spacerGroup: -1
property int spacerFrame: -1
property int spacerClickFrame: -1
property real timeScale: timeline.scaleFactor
property real snapping: timeline.snap ? 10 / Math.sqrt(timeScale) - 0.5 : -1
property var timelineSelection: timeline.selection
property int trackHeight
property int copiedClip: -1
property int zoomOnMouse: -1
property int viewActiveTrack: timeline.activeTrack
//onCurrentTrackChanged: timeline.selection = []
onTimeScaleChanged: {
if (root.zoomOnMouse >= 0) {
scrollView.flickableItem.contentX = Math.max(0, root.zoomOnMouse * timeline.scaleFactor - tracksArea.mouseX)
root.zoomOnMouse = -1
} else {
scrollView.flickableItem.contentX = Math.max(0, (timeline.seekPosition > -1 ? timeline.seekPosition : timeline.position) * timeline.scaleFactor - (scrollView.width / 2))
}
//root.snapping = timeline.snap ? 10 / Math.sqrt(root.timeScale) : -1
ruler.adjustStepSize()
if (dragProxy.draggedItem > -1 && dragProxy.masterObject) {
// update dragged item pos
dragProxy.masterObject.updateDrag()
}
}
onViewActiveTrackChanged: {
var tk = Logic.getTrackById(timeline.activeTrack)
if (tk.y < scrollView.flickableItem.contentY) {
scrollView.flickableItem.contentY = Math.max(0, tk.y - scrollView.height / 3)
} else if (tk.y + tk.height > scrollView.flickableItem.contentY + scrollView.viewport.height) {
scrollView.flickableItem.contentY = Math.min(trackHeaders.height - scrollView.height + scrollView.__horizontalScrollBar.height, tk.y - scrollView.height / 3)
}
}
DropArea { //Drop area for compositions
width: root.width - headerWidth
height: root.height - ruler.height
y: ruler.height
x: headerWidth
keys: 'kdenlive/composition'
onEntered: {
console.log("Trying to drop composition")
if (clipBeingMovedId == -1) {
console.log("No clip being moved")
var track = Logic.getTrackIdFromPos(drag.y + scrollView.flickableItem.contentY)
var frame = Math.round((drag.x + scrollView.flickableItem.contentX) / timeline.scaleFactor)
droppedPosition = frame
if (track >= 0 && !controller.isAudioTrack(track)) {
clipBeingDroppedData = drag.getDataAsString('kdenlive/composition')
console.log("Trying to insert",track, frame, clipBeingDroppedData)
clipBeingDroppedId = timeline.insertComposition(track, frame, clipBeingDroppedData, false)
console.log("id",clipBeingDroppedId)
continuousScrolling(drag.x + scrollView.flickableItem.contentX)
drag.acceptProposedAction()
} else {
drag.accepted = false
}
}
}
onPositionChanged: {
if (clipBeingMovedId == -1) {
var track = Logic.getTrackIdFromPos(drag.y + scrollView.flickableItem.contentY)
if (track !=-1) {
var frame = Math.round((drag.x + scrollView.flickableItem.contentX) / timeline.scaleFactor)
frame = controller.suggestSnapPoint(frame, Math.floor(root.snapping))
if (clipBeingDroppedId >= 0){
if (controller.isAudioTrack(track)) {
// Don't allow moving composition to an audio track
track = controller.getCompositionTrackId(clipBeingDroppedId)
}
controller.requestCompositionMove(clipBeingDroppedId, track, frame, true, false)
continuousScrolling(drag.x + scrollView.flickableItem.contentX)
} else if (!controller.isAudioTrack(track)) {
clipBeingDroppedData = drag.getDataAsString('kdenlive/composition')
clipBeingDroppedId = timeline.insertComposition(track, frame, clipBeingDroppedData , false)
continuousScrolling(drag.x + scrollView.flickableItem.contentX)
}
}
}
}
onExited:{
if (clipBeingDroppedId != -1) {
controller.requestItemDeletion(clipBeingDroppedId, false)
}
clearDropData()
}
onDropped: {
if (clipBeingDroppedId != -1) {
var frame = controller.getCompositionPosition(clipBeingDroppedId)
var track = controller.getCompositionTrackId(clipBeingDroppedId)
// we simulate insertion at the final position so that stored undo has correct value
controller.requestItemDeletion(clipBeingDroppedId, false)
timeline.insertNewComposition(track, frame, clipBeingDroppedData, true)
}
clearDropData()
}
}
DropArea { //Drop area for bin/clips
/** @brief local helper function to handle the insertion of multiple dragged items */
function insertAndMaybeGroup(track, frame, droppedData) {
var binIds = droppedData.split(";")
if (binIds.length == 0) {
return -1
}
var id = -1
if (binIds.length == 1) {
id = timeline.insertClip(timeline.activeTrack, frame, clipBeingDroppedData, false, true, false)
} else {
var ids = timeline.insertClips(timeline.activeTrack, frame, binIds, false, true, false)
// if the clip insertion succeeded, request the clips to be grouped
if (ids.length > 0) {
timeline.selectItems(ids)
id = ids[0]
}
}
return id
}
width: root.width - headerWidth
height: root.height - ruler.height
y: ruler.height
x: headerWidth
keys: 'kdenlive/producerslist'
onEntered: {
if (clipBeingMovedId == -1) {
//var track = Logic.getTrackIdFromPos(drag.y)
var track = Logic.getTrackIndexFromPos(drag.y + scrollView.flickableItem.contentY)
if (track >= 0 && track < tracksRepeater.count) {
var frame = Math.round((drag.x + scrollView.flickableItem.contentX) / timeline.scaleFactor)
droppedPosition = frame
timeline.activeTrack = tracksRepeater.itemAt(track).trackInternalId
//drag.acceptProposedAction()
clipBeingDroppedData = drag.getDataAsString('kdenlive/producerslist')
console.log('dropped data: ', clipBeingDroppedData)
clipBeingDroppedId = insertAndMaybeGroup(timeline.activeTrack, frame, clipBeingDroppedData)
continuousScrolling(drag.x + scrollView.flickableItem.contentX)
} else {
drag.accepted = false
}
}
}
onExited:{
if (clipBeingDroppedId != -1) {
controller.requestItemDeletion(clipBeingDroppedId, false)
}
clearDropData()
}
onPositionChanged: {
if (clipBeingMovedId == -1) {
var track = Logic.getTrackIndexFromPos(drag.y + scrollView.flickableItem.contentY)
if (track >= 0 && track < tracksRepeater.count) {
timeline.activeTrack = tracksRepeater.itemAt(track).trackInternalId
var frame = Math.round((drag.x + scrollView.flickableItem.contentX) / timeline.scaleFactor)
frame = controller.suggestSnapPoint(frame, Math.floor(root.snapping))
if (clipBeingDroppedId >= 0){
controller.requestClipMove(clipBeingDroppedId, timeline.activeTrack, frame, true, false, false)
continuousScrolling(drag.x + scrollView.flickableItem.contentX)
} else {
clipBeingDroppedId = insertAndMaybeGroup(timeline.activeTrack, frame, drag.getDataAsString('kdenlive/producerslist'), false, true)
continuousScrolling(drag.x + scrollView.flickableItem.contentX)
}
}
}
}
onDropped: {
if (clipBeingDroppedId != -1) {
var frame = controller.getClipPosition(clipBeingDroppedId)
var track = controller.getClipTrackId(clipBeingDroppedId)
/* We simulate insertion at the final position so that stored undo has correct value
* NOTE: even if dropping multiple clips, requesting the deletion of the first one is
* enough as internally it will request the group deletion
*/
controller.requestItemDeletion(clipBeingDroppedId, false)
var binIds = clipBeingDroppedData.split(";")
if (binIds.length == 1) {
timeline.insertClip(track, frame, clipBeingDroppedData, true, true, false)
} else {
timeline.insertClips(track, frame, binIds, true, true)
}
}
clearDropData()
}
}
OLD.Menu {
id: menu
property int clickedX
property int clickedY
onAboutToHide: {
timeline.ungrabHack()
}
OLD.MenuItem {
text: i18n('Paste')
iconName: 'edit-paste'
visible: copiedClip != -1
onTriggered: {
timeline.pasteItem()
}
}
OLD.MenuItem {
text: i18n('Insert Space')
onTriggered: {
var track = Logic.getTrackIdFromPos(menu.clickedY - ruler.height + scrollView.flickableItem.contentY)
var frame = Math.floor((menu.clickedX + scrollView.flickableItem.contentX) / timeline.scaleFactor)
timeline.insertSpace(track, frame);
}
}
OLD.MenuItem {
text: i18n('Remove Space On Active Track')
onTriggered: {
var track = Logic.getTrackIdFromPos(menu.clickedY - ruler.height + scrollView.flickableItem.contentY)
var frame = Math.floor((menu.clickedX + scrollView.flickableItem.contentX) / timeline.scaleFactor)
timeline.removeSpace(track, frame);
}
}
OLD.MenuItem {
text: i18n('Remove Space')
onTriggered: {
var track = Logic.getTrackIdFromPos(menu.clickedY - ruler.height + scrollView.flickableItem.contentY)
var frame = Math.floor((menu.clickedX + scrollView.flickableItem.contentX) / timeline.scaleFactor)
timeline.removeSpace(track, frame, true);
}
}
OLD.MenuItem {
id: addGuideMenu
text: i18n('Add Guide')
onTriggered: {
timeline.switchGuide(timeline.position);
}
}
OLD.MenuItem {
id: editGuideMenu
text: i18n('Edit Guide')
visible: false
onTriggered: {
timeline.editGuide(timeline.position);
}
}
AssetMenu {
title: i18n('Insert a composition...')
menuModel: transitionModel
isTransition: true
onAssetSelected: {
var track = Logic.getTrackIdFromPos(menu.clickedY - ruler.height + scrollView.flickableItem.contentY)
var frame = Math.round((menu.clickedX + scrollView.flickableItem.contentX) / timeline.scaleFactor)
var id = timeline.insertComposition(track, frame, assetId, true)
if (id == -1) {
compositionFail.open()
}
}
}
onAboutToShow: {
if (guidesModel.hasMarker(timeline.position)) {
// marker at timeline position
addGuideMenu.text = i18n('Remove Guide')
editGuideMenu.visible = true
} else {
addGuideMenu.text = i18n('Add Guide')
editGuideMenu.visible = false
}
console.log("pop menu")
}
}
OLD.Menu {
id: rulermenu
property int clickedX
property int clickedY
OLD.MenuItem {
id: addGuideMenu2
text: i18n('Add Guide')
onTriggered: {
timeline.switchGuide(timeline.position);
}
}
OLD.MenuItem {
id: editGuideMenu2
text: i18n('Edit Guide')
visible: false
onTriggered: {
timeline.editGuide(timeline.position);
}
}
OLD.MenuItem {
id: addProjectNote
text: i18n('Add Project Note')
onTriggered: {
timeline.triggerAction('add_project_note')
}
}
onAboutToShow: {
if (guidesModel.hasMarker(timeline.position)) {
// marker at timeline position
addGuideMenu2.text = i18n('Remove Guide')
editGuideMenu2.visible = true
} else {
addGuideMenu2.text = i18n('Add Guide')
editGuideMenu2.visible = false
}
console.log("pop menu")
}
}
MessageDialog {
id: compositionFail
title: i18n("Timeline error")
icon: StandardIcon.Warning
text: i18n("Impossible to add a composition at that position. There might not be enough space")
standardButtons: StandardButton.Ok
}
OLD.Menu {
id: headerMenu
property int trackId: -1
property int thumbsFormat: 0
property bool audioTrack: false
property bool recEnabled: false
onAboutToHide: {
timeline.ungrabHack()
}
OLD.MenuItem {
text: i18n('Add Track')
onTriggered: {
timeline.addTrack(timeline.activeTrack)
}
}
OLD.MenuItem {
text: i18n('Delete Track')
onTriggered: {
timeline.deleteTrack(timeline.activeTrack)
}
}
OLD.MenuItem {
visible: headerMenu.audioTrack
id: showRec
text: "Show Record Controls"
onTriggered: {
controller.setTrackProperty(headerMenu.trackId, "kdenlive:audio_rec", showRec.checked ? '1' : '0')
}
checkable: true
checked: headerMenu.recEnabled
}
OLD.Menu {
title: i18n('Track thumbnails')
visible: !headerMenu.audioTrack
OLD.ExclusiveGroup { id: thumbStyle }
OLD.MenuItem {
text: "In frame"
id: inFrame
onTriggered:controller.setTrackProperty(headerMenu.trackId, "kdenlive:thumbs_format", 2)
checkable: true
exclusiveGroup: thumbStyle
}
OLD.MenuItem {
text: "In / out frames"
id: inOutFrame
onTriggered:controller.setTrackProperty(headerMenu.trackId, "kdenlive:thumbs_format", 0)
checkable: true
checked: true
exclusiveGroup: thumbStyle
}
OLD.MenuItem {
text: "All frames"
id: allFrame
onTriggered:controller.setTrackProperty(headerMenu.trackId, "kdenlive:thumbs_format", 1)
checkable: true
exclusiveGroup: thumbStyle
}
OLD.MenuItem {
text: "No thumbnails"
id: noFrame
onTriggered:controller.setTrackProperty(headerMenu.trackId, "kdenlive:thumbs_format", 3)
checkable: true
exclusiveGroup: thumbStyle
}
onAboutToShow: {
switch(headerMenu.thumbsFormat) {
case 3:
noFrame.checked = true
break
case 2:
inFrame.checked = true
break
case 1:
allFrame.checked = true
break
default:
inOutFrame.checked = true
break
}
}
}
}
Row {
Column {
id: headerContainer
z: 1
Rectangle {
id: cornerstone
property bool selected: false
// Padding between toolbar and track headers.
width: headerWidth
height: ruler.height
color: 'transparent' //selected? shotcutBlue : activePalette.window
border.color: selected? 'red' : 'transparent'
border.width: selected? 1 : 0
z: 1
}
Flickable {
// Non-slider scroll area for the track headers.
id: headerFlick
contentY: scrollView.flickableItem.contentY
width: headerWidth
height: 100
interactive: false
MouseArea {
width: trackHeaders.width
height: trackHeaders.height
acceptedButtons: Qt.NoButton
onWheel: {
var newScroll = Math.min(scrollView.flickableItem.contentY - wheel.angleDelta.y, height - tracksArea.height + scrollView.__horizontalScrollBar.height + cornerstone.height)
scrollView.flickableItem.contentY = Math.max(newScroll, 0)
}
}
Column {
id: trackHeaders
spacing: 0
Repeater {
id: trackHeaderRepeater
model: multitrack
TrackHead {
trackName: model.name
thumbsFormat: model.thumbsFormat
trackTag: model.trackTag
isDisabled: model.disabled
isComposite: model.composite
isLocked: model.locked
isActive: model.trackActive
isAudio: model.audio
showAudioRecord: model.audioRecord
effectNames: model.effectNames
isStackEnabled: model.isStackEnabled
width: headerWidth
height: model.trackHeight
current: item === timeline.activeTrack
trackId: item
onIsLockedChanged: tracksRepeater.itemAt(index).isLocked = isLocked
collapsed: height <= collapsedHeight
onMyTrackHeightChanged: {
trackBaseRepeater.itemAt(index).height = myTrackHeight
tracksRepeater.itemAt(index).height = myTrackHeight
height = myTrackHeight
collapsed = height <= collapsedHeight
if (!collapsed) {
controller.setTrackProperty(trackId, "kdenlive:trackheight", myTrackHeight)
controller.setTrackProperty(trackId, "kdenlive:collapsed", "0")
} else {
controller.setTrackProperty(trackId, "kdenlive:collapsed", collapsedHeight)
}
// hack: change property to trigger transition adjustment
root.trackHeight = root.trackHeight === 1 ? 0 : 1
}
onClicked: {
timeline.activeTrack = tracksRepeater.itemAt(index).trackInternalId
console.log('track name: ',index, ' = ', model.name,'/',tracksRepeater.itemAt(index).trackInternalId)
//timeline.selectTrackHead(currentTrack)
}
}
}
}
Column {
id: trackHeadersResizer
spacing: 0
width: 5
Rectangle {
id: resizer
height: trackHeaders.height
width: 3
x: root.headerWidth - 2
color: 'red'
opacity: 0
Drag.active: headerMouseArea.drag.active
Drag.proposedAction: Qt.MoveAction
MouseArea {
id: headerMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.SizeHorCursor
drag.target: parent
drag.axis: Drag.XAxis
drag.minimumX: 2 * baseUnit
property double startX
property double originalX
drag.smoothed: false
onPressed: {
root.stopScrolling = true
}
onReleased: {
root.stopScrolling = false
parent.opacity = 0
}
onEntered: parent.opacity = 0.5
onExited: parent.opacity = 0
onPositionChanged: {
if (mouse.buttons === Qt.LeftButton) {
parent.opacity = 0.5
headerWidth = Math.max(10, mapToItem(null, x, y).x + 2)
timeline.setHeaderWidth(headerWidth)
}
}
}
}
}
}
}
MouseArea {
id: tracksArea
property real clickX
property real clickY
width: root.width - headerWidth
height: root.height
Keys.onDownPressed: {
root.moveSelectedTrack(1)
}
Keys.onUpPressed: {
root.moveSelectedTrack(-1)
}
// This provides continuous scrubbing and scimming at the left/right edges.
hoverEnabled: true
acceptedButtons: Qt.RightButton | Qt.LeftButton | Qt.MidButton
cursorShape: tracksArea.mouseY < ruler.height || root.activeTool === 0 ? Qt.ArrowCursor : root.activeTool === 1 ? Qt.IBeamCursor : Qt.SplitHCursor
onWheel: {
if (wheel.modifiers & Qt.AltModifier) {
// Alt + wheel = seek to next snap point
if (wheel.angleDelta.x > 0) {
timeline.triggerAction('monitor_seek_snap_backward')
} else {
timeline.triggerAction('monitor_seek_snap_forward')
}
} else {
var delta = wheel.modifiers & Qt.ShiftModifier ? timeline.fps() : 1
if (timeline.seekPosition > -1) {
timeline.seekPosition = Math.min(timeline.seekPosition - (wheel.angleDelta.y > 0 ? delta : -delta), timeline.fullDuration - 1)
} else {
timeline.seekPosition = Math.min(timeline.position - (wheel.angleDelta.y > 0 ? delta : -delta), timeline.fullDuration - 1)
}
timeline.position = timeline.seekPosition
}
}
onPressed: {
focus = true
if (mouse.buttons === Qt.MidButton || (root.activeTool == 0 && mouse.modifiers & Qt.ControlModifier)) {
clickX = mouseX
clickY = mouseY
return
}
if (root.activeTool === 0 && mouse.modifiers & Qt.ShiftModifier && mouse.y > ruler.height) {
// rubber selection
rubberSelect.x = mouse.x + tracksArea.x
rubberSelect.y = mouse.y
rubberSelect.originX = mouse.x
rubberSelect.originY = rubberSelect.y
rubberSelect.width = 0
rubberSelect.height = 0
} else if (mouse.button & Qt.LeftButton) {
if (dragProxy.draggedItem > -1) {
mouse.accepted = false
return
}
if (root.activeTool === 2 && mouse.y > ruler.height) {
// spacer tool
var y = mouse.y - ruler.height + scrollView.flickableItem.contentY
var frame = (scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor
var track = (mouse.modifiers & Qt.ControlModifier) ? tracksRepeater.itemAt(Logic.getTrackIndexFromPos(y)).trackInternalId : -1
spacerGroup = timeline.requestSpacerStartOperation(track, frame)
if (spacerGroup > -1) {
drag.axis = Drag.XAxis
Drag.active = true
Drag.proposedAction = Qt.MoveAction
spacerClickFrame = frame
spacerFrame = controller.getItemPosition(spacerGroup)
}
} else if (root.activeTool === 0 || mouse.y <= ruler.height) {
if (mouse.y > ruler.height) {
controller.requestClearSelection();
}
timeline.seekPosition = Math.min((scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor, timeline.fullDuration - 1)
timeline.position = timeline.seekPosition
} else if (root.activeTool === 1) {
// razor tool
var y = mouse.y - ruler.height + scrollView.flickableItem.contentY
timeline.cutClipUnderCursor((scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor, tracksRepeater.itemAt(Logic.getTrackIndexFromPos(y)).trackInternalId)
}
} else if (mouse.button & Qt.RightButton) {
menu.clickedX = mouse.x
menu.clickedY = mouse.y
if (mouse.y > ruler.height) {
timeline.activeTrack = tracksRepeater.itemAt(Logic.getTrackIndexFromPos(mouse.y - ruler.height + scrollView.flickableItem.contentY)).trackInternalId
menu.popup()
} else {
// ruler menu
rulermenu.popup()
}
}
}
property bool scim: false
onExited: {
scim = false
}
onPositionChanged: {
if (pressed && ((mouse.buttons === Qt.MidButton) || (mouse.buttons === Qt.LeftButton && root.activeTool == 0 && mouse.modifiers & Qt.ControlModifier))) {
var newScroll = Math.min(scrollView.flickableItem.contentX - (mouseX - clickX), timeline.fullDuration * root.timeScale - (scrollView.width - scrollView.__verticalScrollBar.width))
var vertScroll = Math.min(scrollView.flickableItem.contentY - (mouseY - clickY), trackHeaders.height - scrollView.height + scrollView.__horizontalScrollBar.height)
scrollView.flickableItem.contentX = Math.max(newScroll, 0)
scrollView.flickableItem.contentY = Math.max(vertScroll, 0)
clickX = mouseX
clickY = mouseY
return
}
if (dragProxy.draggedItem > -1) {
mouse.accepted = false
return
}
var mousePos = Math.max(0, Math.round((mouse.x + scrollView.flickableItem.contentX) / timeline.scaleFactor))
root.mousePosChanged(mousePos)
ruler.showZoneLabels = mouse.y < ruler.height
if (mouse.modifiers & Qt.ShiftModifier && mouse.buttons === Qt.LeftButton && root.activeTool === 0 && !rubberSelect.visible && rubberSelect.y > 0) {
// rubber selection
rubberSelect.visible = true
}
if (rubberSelect.visible) {
var newX = mouse.x
var newY = mouse.y
if (newX < rubberSelect.originX) {
rubberSelect.x = newX + tracksArea.x
rubberSelect.width = rubberSelect.originX - newX
} else {
rubberSelect.x = rubberSelect.originX + tracksArea.x
rubberSelect.width = newX - rubberSelect.originX
}
if (newY < rubberSelect.originY) {
rubberSelect.y = newY
rubberSelect.height = rubberSelect.originY - newY
} else {
rubberSelect.y = rubberSelect.originY
rubberSelect.height= newY - rubberSelect.originY
}
} else if (mouse.buttons === Qt.LeftButton) {
if (root.activeTool === 0 || mouse.y < ruler.height) {
timeline.seekPosition = Math.max(0, Math.min((scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor, timeline.fullDuration - 1))
timeline.position = timeline.seekPosition
} else if (root.activeTool === 2 && spacerGroup > -1) {
// Move group
var track = controller.getItemTrackId(spacerGroup)
var frame = Math.round((mouse.x + scrollView.flickableItem.contentX) / timeline.scaleFactor) + spacerFrame - spacerClickFrame
frame = controller.suggestItemMove(spacerGroup, track, frame, timeline.position, Math.floor(root.snapping))
continuousScrolling(mouse.x + scrollView.flickableItem.contentX)
}
scim = true
} else {
scim = false
if (root.activeTool === 1) {
cutLine.x = Math.floor((scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor) * timeline.scaleFactor - scrollView.flickableItem.contentX
if (mouse.modifiers & Qt.ShiftModifier) {
timeline.position = Math.floor((scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor)
}
}
}
}
onReleased: {
if (rubberSelect.visible) {
rubberSelect.visible = false
var y = rubberSelect.y - ruler.height + scrollView.flickableItem.contentY
var topTrack = Logic.getTrackIndexFromPos(Math.max(0, y))
var bottomTrack = Logic.getTrackIndexFromPos(y + rubberSelect.height)
if (bottomTrack >= topTrack) {
var t = []
for (var i = topTrack; i <= bottomTrack; i++) {
t.push(tracksRepeater.itemAt(i).trackInternalId)
}
var startFrame = (scrollView.flickableItem.contentX - tracksArea.x + rubberSelect.x) / timeline.scaleFactor
var endFrame = (scrollView.flickableItem.contentX - tracksArea.x + rubberSelect.x + rubberSelect.width) / timeline.scaleFactor
timeline.selectItems(t, startFrame, endFrame, mouse.modifiers & Qt.ControlModifier);
}
rubberSelect.y = -1
} else if (mouse.modifiers & Qt.ShiftModifier) {
// Shift click, process seek
timeline.seekPosition = Math.min((scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor, timeline.fullDuration - 1)
timeline.position = timeline.seekPosition
}
if (spacerGroup > -1) {
var frame = controller.getItemPosition(spacerGroup)
timeline.requestSpacerEndOperation(spacerGroup, spacerFrame, frame);
spacerClickFrame = -1
spacerFrame = -1
spacerGroup = -1
}
scim = false
}
Timer {
id: scrubTimer
interval: 25
repeat: true
running: parent.scim && parent.containsMouse
&& (parent.mouseX < 50 || parent.mouseX > parent.width - 50)
&& (timeline.position * timeline.scaleFactor >= 50)
onTriggered: {
if (parent.mouseX < 50)
timeline.seekPosition = Math.max(0, timeline.position - 10)
else
timeline.seekPosition = Math.min(timeline.position + 10, timeline.fullDuration - 1)
}
}
Column {
Flickable {
// Non-slider scroll area for the Ruler.
id: rulercontainer
width: root.width - headerWidth
height: fontMetrics.font.pixelSize * 2
contentX: scrollView.flickableItem.contentX
contentWidth: Math.max(parent.width, timeline.fullDuration * timeScale)
interactive: false
clip: true
Ruler {
id: ruler
width: rulercontainer.contentWidth
height: parent.height
Rectangle {
id: seekCursor
visible: timeline.seekPosition > -1
color: activePalette.highlight
width: 4
height: ruler.height
opacity: 0.5
x: timeline.seekPosition * timeline.scaleFactor
}
TimelinePlayhead {
id: playhead
visible: timeline.position > -1
height: baseUnit
width: baseUnit * 1.5
fillColor: activePalette.windowText
anchors.bottom: parent.bottom
x: timeline.position * timeline.scaleFactor - (width / 2)
}
}
}
OLD.ScrollView {
id: scrollView
width: root.width - headerWidth
height: root.height - ruler.height
y: ruler.height
// Click and drag should seek, not scroll the timeline view
flickableItem.interactive: false
clip: true
Rectangle {
id: tracksContainerArea
width: Math.max(scrollView.width - scrollView.__verticalScrollBar.width, timeline.fullDuration * timeScale)
height: trackHeaders.height
//Math.max(trackHeaders.height, scrollView.contentHeight - scrollView.__horizontalScrollBar.height)
color: root.color
Rectangle {
// Drag proxy, responsible for clip / composition move
id: dragProxy
x: 0
y: 0
width: 0
height: 0
property int draggedItem: -1
property int sourceTrack
property int sourceFrame
property bool isComposition
property int verticalOffset
property var masterObject
color: 'green'
opacity: 0.8
MouseArea {
id: dragProxyArea
anchors.fill: parent
drag.target: parent
drag.axis: Drag.XAxis
drag.smoothed: false
drag.minimumX: 0
property int dragFrame
property bool shiftClick: false
cursorShape: pressed ? Qt.ClosedHandCursor : Qt.OpenHandCursor
onPressed: {
console.log('+++++++++++++++++++ DRAG CLICKED +++++++++++++')
if (mouse.modifiers & Qt.ControlModifier) {
mouse.accepted = false
return
}
dragFrame = -1
timeline.activeTrack = dragProxy.sourceTrack
if (mouse.modifiers & Qt.ShiftModifier) {
if (timeline.selection.indexOf(dragProxy.draggedItem) == -1) {
console.log('ADD SELECTION: ', dragProxy.draggedItem)
controller.requestAddToSelection(dragProxy.draggedItem)
} else {
console.log('REMOVE SELECTION: ', dragProxy.draggedItem)
controller.requestRemoveFromSelection(dragProxy.draggedItem)
//endDrag()
shiftClick = true
return
}
shiftClick = true
} else {
if (timeline.selection.indexOf(dragProxy.draggedItem) == -1) {
controller.requestAddToSelection(dragProxy.draggedItem, /*clear=*/ true)
}
shiftClick = false
}
timeline.showAsset(dragProxy.draggedItem)
root.stopScrolling = true
clipBeingMovedId = dragProxy.draggedItem
if (dragProxy.draggedItem > -1) {
var tk = controller.getItemTrackId(dragProxy.draggedItem)
var x = controller.getItemPosition(dragProxy.draggedItem)
var posx = Math.round((parent.x)/ root.timeScale)
var clickAccepted = true
if (controller.normalEdit() && (tk != Logic.getTrackIdFromPos(parent.y) || x != posx)) {
console.log('INCORRECT DRAG, Trying to recover item: ', parent.y,' XPOS: ',x,'=',posx,'\n!!!!!!!!!!')
// Try to find correct item
var track = Logic.getTrackById(tk)
var container = track.children[0].children[0].children[0]
var tentativeClip = container.childAt(mouseX + parent.x, 5)
if (tentativeClip && tentativeClip.clipId) {
clickAccepted = true
dragProxy.draggedItem = tentativeClip.clipId
dragProxy.x = tentativeClip.x
dragProxy.y = tentativeClip.y
dragProxy.width = tentativeClip.width
dragProxy.height = tentativeClip.height
dragProxy.masterObject = tentativeClip
dragProxy.sourceTrack = tk
dragProxy.sourceFrame = tentativeClip.modelStart
dragProxy.isComposition = tentativeClip.isComposition
} else {
clickAccepted = false
mouse.accepted = false
dragProxy.draggedItem = -1
dragProxy.masterObject = undefined
parent.x = 0
parent.y = 0
parent.width = 0
parent.height = 0
}
}
if (clickAccepted && dragProxy.draggedItem != -1) {
focus = true;
dragProxy.masterObject.originalX = dragProxy.masterObject.x
dragProxy.masterObject.originalTrackId = dragProxy.masterObject.trackId
dragProxy.masterObject.forceActiveFocus();
}
} else {
mouse.accepted = false
parent.x = 0
parent.y = 0
parent.width = 0
parent.height = 0
}
}
onPositionChanged: {
- if (!shiftClick && dragProxy.draggedItem > -1 && mouse.buttons === Qt.LeftButton) {
+ // we have to check item validity in the controller, because they could have been deleted since the beginning of the drag
+ if (!shiftClick && dragProxy.draggedItem > -1 && mouse.buttons === Qt.LeftButton && (controller.isClip(dragProxy.draggedItem) || controller.isComposition(dragProxy.draggedItem))) {
continuousScrolling(mouse.x + parent.x)
var mapped = tracksContainerArea.mapFromItem(dragProxy, mouse.x, mouse.y).x
root.mousePosChanged(Math.round(mapped / timeline.scaleFactor))
var posx = Math.round((parent.x)/ root.timeScale)
var posy = Math.min(Math.max(0, mouse.y + parent.y - dragProxy.verticalOffset), tracksContainerArea.height)
var tId = Logic.getTrackIdFromPos(posy)
if (dragProxy.masterObject && tId == dragProxy.masterObject.trackId) {
if (posx == dragFrame) {
return
}
}
if (dragProxy.isComposition) {
dragFrame = controller.suggestCompositionMove(dragProxy.draggedItem, tId, posx, timeline.position, Math.floor(root.snapping))
timeline.activeTrack = timeline.getItemMovingTrack(dragProxy.draggedItem)
} else {
if (!controller.normalEdit() && dragProxy.masterObject.parent != dragContainer) {
var pos = dragProxy.masterObject.mapToGlobal(dragProxy.masterObject.x, dragProxy.masterObject.y);
dragProxy.masterObject.parent = dragContainer
pos = dragProxy.masterObject.mapFromGlobal(pos.x, pos.y)
dragProxy.masterObject.x = pos.x
dragProxy.masterObject.y = pos.y
console.log('bringing item to front')
}
dragFrame = controller.suggestClipMove(dragProxy.draggedItem, tId, posx, timeline.position, Math.floor(root.snapping))
timeline.activeTrack = timeline.getItemMovingTrack(dragProxy.draggedItem)
}
var delta = dragFrame - dragProxy.sourceFrame
if (delta != 0) {
var s = timeline.timecode(Math.abs(delta))
// remove leading zeroes
if (s.substring(0, 3) === '00:')
s = s.substring(3)
s = ((delta < 0)? '-' : (delta > 0)? '+' : '') + s
bubbleHelp.show(parent.x, ruler.height, s)
} else bubbleHelp.hide()
}
}
onReleased: {
clipBeingMovedId = -1
- if (!shiftClick && dragProxy.draggedItem > -1 && dragFrame > -1) {
+ if (!shiftClick && dragProxy.draggedItem > -1 && dragFrame > -1 && (controller.isClip(dragProxy.draggedItem) || controller.isComposition(dragProxy.draggedItem))) {
var tId = controller.getItemTrackId(dragProxy.draggedItem)
if (dragProxy.isComposition) {
controller.requestCompositionMove(dragProxy.draggedItem, dragProxy.sourceTrack, dragProxy.sourceFrame, true, false, false)
controller.requestCompositionMove(dragProxy.draggedItem, tId, dragFrame , true, true, true)
} else {
if (controller.normalEdit()) {
controller.requestClipMove(dragProxy.draggedItem, dragProxy.sourceTrack, dragProxy.sourceFrame, true, false, false)
controller.requestClipMove(dragProxy.draggedItem, tId, dragFrame , true, true, true)
} else {
// Fake move, only process final move
timeline.endFakeMove(dragProxy.draggedItem, dragFrame , true, true, true)
}
}
dragProxy.x = controller.getItemPosition(dragProxy.draggedItem) * timeline.scaleFactor
dragProxy.sourceFrame = dragFrame
bubbleHelp.hide()
}
}
onDoubleClicked: {
if (dragProxy.masterObject.keyframeModel) {
var newVal = (dragProxy.height - mouseY) / dragProxy.height
var newPos = Math.round(mouseX / timeScale) + dragProxy.masterObject.inPoint
timeline.addEffectKeyframe(dragProxy.draggedItem, newPos, newVal)
}
}
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
onWheel: zoomByWheel(wheel)
cursorShape: dragProxyArea.drag.active ? Qt.ClosedHandCursor : tracksArea.cursorShape
}
Column {
// These make the striped background for the tracks.
// It is important that these are not part of the track visual hierarchy;
// otherwise, the clips will be obscured by the Track's background.
Repeater {
model: multitrack
id: trackBaseRepeater
delegate: Rectangle {
width: tracksContainerArea.width
border.width: 1
border.color: root.frameColor
height: model.trackHeight
color: tracksRepeater.itemAt(index) ? ((tracksRepeater.itemAt(index).trackInternalId === timeline.activeTrack) ? Qt.tint(getTrackColor(tracksRepeater.itemAt(index).isAudio, false), selectedTrackColor) : getTrackColor(tracksRepeater.itemAt(index).isAudio, false)) : 'red'
}
}
}
Column {
id: tracksContainer
Repeater { id: tracksRepeater; model: trackDelegateModel }
Item {
id: dragContainer
}
Repeater { id: guidesRepeater; model: guidesDelegateModel }
}
Rectangle {
id: cursor
visible: timeline.position > -1
color: root.textColor
width: Math.max(1, 1 * timeline.scaleFactor)
opacity: (width > 2) ? 0.5 : 1
height: parent.height
x: timeline.position * timeline.scaleFactor
}
}
}
}
/*CornerSelectionShadow {
y: tracksRepeater.count ? tracksRepeater.itemAt(currentTrack).y + ruler.height - scrollView.flickableItem.contentY : 0
clip: timeline.selection.length ?
tracksRepeater.itemAt(currentTrack).clipAt(timeline.selection[0]) : null
opacity: clip && clip.x + clip.width < scrollView.flickableItem.contentX ? 1 : 0
}
CornerSelectionShadow {
y: tracksRepeater.count ? tracksRepeater.itemAt(currentTrack).y + ruler.height - scrollView.flickableItem.contentY : 0
clip: timeline.selection.length ?
tracksRepeater.itemAt(currentTrack).clipAt(timeline.selection[timeline.selection.length - 1]) : null
opacity: clip && clip.x > scrollView.flickableItem.contentX + scrollView.width ? 1 : 0
anchors.right: parent.right
mirrorGradient: true
}*/
Rectangle {
id: cutLine
visible: root.activeTool == 1 && tracksArea.mouseY > ruler.height
color: 'red'
width: Math.max(1, 1 * timeline.scaleFactor)
opacity: (width > 2) ? 0.5 : 1
height: root.height - scrollView.__horizontalScrollBar.height - ruler.height
x: 0
//x: timeline.position * timeline.scaleFactor - scrollView.flickableItem.contentX
y: ruler.height
}
}
}
Rectangle {
id: bubbleHelp
property alias text: bubbleHelpLabel.text
color: root.color //application.toolTipBaseColor
width: bubbleHelpLabel.width + 8
height: bubbleHelpLabel.height + 8
radius: 4
states: [
State { name: 'invisible'; PropertyChanges { target: bubbleHelp; opacity: 0} },
State { name: 'visible'; PropertyChanges { target: bubbleHelp; opacity: 0.8} }
]
state: 'invisible'
transitions: [
Transition {
from: 'invisible'
to: 'visible'
OpacityAnimator { target: bubbleHelp; duration: 200; easing.type: Easing.InOutQuad }
},
Transition {
from: 'visible'
to: 'invisible'
OpacityAnimator { target: bubbleHelp; duration: 200; easing.type: Easing.InOutQuad }
}
]
Label {
id: bubbleHelpLabel
color: activePalette.text //application.toolTipTextColor
anchors.centerIn: parent
font.pixelSize: root.baseUnit
}
function show(x, y, text) {
bubbleHelp.x = x + tracksArea.x - scrollView.flickableItem.contentX - bubbleHelpLabel.width
bubbleHelp.y = y + tracksArea.y - scrollView.flickableItem.contentY - bubbleHelpLabel.height
bubbleHelp.text = text
if (bubbleHelp.state !== 'visible')
bubbleHelp.state = 'visible'
}
function hide() {
bubbleHelp.state = 'invisible'
bubbleHelp.opacity = 0
}
}
Rectangle {
id: rubberSelect
property int originX
property int originY
y: -1
color: Qt.rgba(activePalette.highlight.r, activePalette.highlight.g, activePalette.highlight.b, 0.4)
border.color: activePalette.highlight
border.width: 1
visible: false
}
/*DropShadow {
source: bubbleHelp
anchors.fill: bubbleHelp
opacity: bubbleHelp.opacity
horizontalOffset: 3
verticalOffset: 3
radius: 8
color: '#80000000'
transparentBorder: true
fast: true
}*/
DelegateModel {
id: trackDelegateModel
model: multitrack
delegate: Track {
trackModel: multitrack
rootIndex: trackDelegateModel.modelIndex(index)
height: trackHeight
timeScale: timeline.scaleFactor
width: tracksContainerArea.width
isAudio: audio
trackThumbsFormat: thumbsFormat
isCurrentTrack: item === timeline.activeTrack
trackInternalId: item
z: tracksRepeater.count - index
}
}
DelegateModel {
id: guidesDelegateModel
model: guidesModel
Item {
id: guideRoot
z: 20
Rectangle {
id: guideBase
width: 1
height: tracksContainer.height
x: model.frame * timeScale;
color: model.color
}
Rectangle {
visible: mlabel.visible
opacity: 0.7
x: guideBase.x
y: mlabel.y
radius: 2
width: mlabel.width + 4
height: mlabel.height
color: model.color
MouseArea {
z: 10
anchors.fill: parent
acceptedButtons: Qt.LeftButton
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
property int startX
drag.axis: Drag.XAxis
drag.target: guideRoot
onPressed: {
drag.target = guideRoot
startX = guideRoot.x
}
onReleased: {
if (startX != guideRoot.x) {
timeline.moveGuide(model.frame, model.frame + guideRoot.x / timeline.scaleFactor)
}
drag.target = undefined
}
onPositionChanged: {
if (pressed) {
var frame = Math.round(model.frame + guideRoot.x / timeline.scaleFactor)
frame = controller.suggestSnapPoint(frame, root.snapping)
guideRoot.x = (frame - model.frame) * timeline.scaleFactor
}
}
drag.smoothed: false
onDoubleClicked: {
timeline.editGuide(model.frame)
drag.target = undefined
}
onClicked: timeline.position = guideBase.x / timeline.scaleFactor
}
}
Text {
id: mlabel
visible: timeline.showMarkers
text: model.comment
font.pixelSize: root.baseUnit
x: guideBase.x + 2
y: scrollView.flickableItem.contentY
color: 'white'
}
}
}
Connections {
target: timeline
onPositionChanged: if (!stopScrolling) Logic.scrollIfNeeded()
onFrameFormatChanged: ruler.adjustFormat()
onSelectionChanged: {
//cornerstone.selected = timeline.isMultitrackSelected()
if (dragProxy.draggedItem > -1 && !timeline.exists(dragProxy.draggedItem)) {
endDrag()
}
}
}
// This provides continuous scrolling at the left/right edges.
Timer {
id: scrollTimer
interval: 25
repeat: true
triggeredOnStart: true
property var item
property bool backwards
onTriggered: {
var delta = backwards? -10 : 10
if (item) item.x += delta
scrollView.flickableItem.contentX += delta
if (scrollView.flickableItem.contentX <= 0 || clipBeingMovedId == -1)
stop()
}
}
}