/* * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or * its licensors. * * For complete copyright and license terms please see the LICENSE at the root of this * distribution (the "License"). All use of this software is governed by the License, * or, if provided, by the license below or the license accompanying this file. Do not * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * */ // Original file Copyright Crytek GMBH or its affiliates, used under license. #include "StdAfx.h" #include "TrackViewDopeSheetBase.h" #include "Controls/ReflectedPropertyControl/ReflectedPropertyCtrl.h" #include "TrackViewDialog.h" #include "AnimationContext.h" #include "TVCustomizeTrackColorsDlg.h" #include "TrackViewAnimNode.h" #include "TrackViewTrack.h" #include "TrackViewSequence.h" #include #include #include #include #include #include #include #include #include "Clipboard.h" #include #include #include #include #include #include #include #include #if defined(Q_OS_WIN) #include #endif #define EDIT_DISABLE_GRAY_COLOR QColor(128, 128, 128) #define KEY_TEXT_COLOR QColor(0, 0, 50) #define INACTIVE_TEXT_COLOR QColor(128, 128, 128) namespace { const int kMarginForMagnetSnapping = 10; const unsigned int kDefaultTrackHeight = 16; } enum ETVMouseMode { eTVMouseMode_None = 0, eTVMouseMode_Select = 1, eTVMouseMode_Move, eTVMouseMode_Clone, eTVMouseMode_DragTime, eTVMouseMode_DragStartMarker, eTVMouseMode_DragEndMarker, eTVMouseMode_Paste, eTVMouseMode_SelectWithinTime, eTVMouseMode_StartTimeAdjust, eTVMouseMode_EndTimeAdjust }; ////////////////////////////////////////////////////////////////////////// CTrackViewDopeSheetBase::CTrackViewDopeSheetBase(QWidget* parent) : QWidget(parent) { m_bkgrBrush = palette().color(QPalette::Window); m_bkgrBrushEmpty = QColor(190, 190, 190); m_timeBkgBrush = QColor(0xE0, 0xE0, 0xE0); m_timeHighlightBrush = QColor(0xFF, 0x0, 0x0); m_selectedBrush = QColor(200, 200, 230); m_visibilityBrush = QColor(120, 120, 255); m_selectTrackBrush = QColor(100, 190, 255); m_timeScale = 1.0f; m_ticksStep = 10; m_bZoomDrag = false; m_bMoveDrag = false; m_leftOffset = 30; m_scrollOffset = QPoint(0, 0); m_mouseMode = eTVMouseMode_None; m_currentTime = 0.0f; m_storedTime = m_currentTime; m_rcSelect = QRect(0, 0, 0, 0); m_rubberBand = 0; m_scrollBar = new QScrollBar(Qt::Horizontal, this); connect(m_scrollBar, &QScrollBar::valueChanged, this, &CTrackViewDopeSheetBase::OnHScroll); m_keyTimeOffset = 0; m_currCursor = QCursor(Qt::ArrowCursor); m_mouseActionMode = eTVActionMode_MoveKey; m_scrollMin = 0; m_scrollMax = 1000; m_descriptionFont = QFont(QStringLiteral("Verdana"), 7); m_bCursorWasInKey = false; m_bJustSelected = false; m_snappingMode = eSnappingMode_SnapNone; m_snapFrameTime = 0.033333f; m_bMouseMovedAfterRButtonDown = false; m_stashedRecordModeWhileTimeDragging = false; m_tickDisplayMode = eTVTickMode_InSeconds; m_bEditLock = false; m_bFastRedraw = false; m_pLastTrackSelectedOnSpot = NULL; m_wndPropsOnSpot = nullptr; #ifdef DEBUG m_redrawCount = 0; #endif m_bKeysMoved = false; ComputeFrameSteps(GetVisibleRange()); m_crsLeftRight = Qt::SizeHorCursor; m_crsAddKey = CMFCUtils::LoadCursor(IDC_ARROW_ADDKEY); m_crsCross = CMFCUtils::LoadCursor(IDC_POINTER_OBJHIT); m_crsAdjustLR = CMFCUtils::LoadCursor(IDC_LEFTRIGHT); setMouseTracking(true); setFocusPolicy(Qt::StrongFocus); m_colorUpdateTrack = nullptr; m_colorUpdateKeyTime = 0; } ////////////////////////////////////////////////////////////////////////// CTrackViewDopeSheetBase::~CTrackViewDopeSheetBase() { HideKeyPropertyCtrlOnSpot(); GetIEditor()->GetAnimation()->RemoveListener(this); } ////////////////////////////////////////////////////////////////////////// int CTrackViewDopeSheetBase::TimeToClient(float time) const { int x = m_leftOffset - m_scrollOffset.x() + (time * m_timeScale); return x; } ////////////////////////////////////////////////////////////////////////// Range CTrackViewDopeSheetBase::GetVisibleRange() const { Range r; r.start = (m_scrollOffset.x() - m_leftOffset) / m_timeScale; r.end = r.start + (m_rcClient.width()) / m_timeScale; Range extendedTimeRange(0.0f, m_timeRange.end); r = extendedTimeRange & r; return r; } ////////////////////////////////////////////////////////////////////////// Range CTrackViewDopeSheetBase::GetTimeRange(const QRect& rc) const { Range r; r.start = (rc.left() - m_leftOffset + m_scrollOffset.x()) / m_timeScale; r.end = r.start + (rc.width()) / m_timeScale; r.start = TickSnap(r.start); r.end = TickSnap(r.end); // Intersect range with global time range. r = m_timeRange & r; return r; } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::SetTimeRange(float start, float end) { if (m_timeMarked.start < start) { m_timeMarked.start = start; } if (m_timeMarked.end > end) { m_timeMarked.end = end; } m_timeRange.Set(start, end); SetHorizontalExtent(-m_leftOffset, m_timeRange.end * m_timeScale - m_leftOffset); } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::SetTimeScale(float timeScale, float fAnchorTime) { const double fOldOffset = -fAnchorTime * m_timeScale; const double fOldScale = m_timeScale; timeScale = std::max(timeScale, 0.001f); timeScale = std::min(timeScale, 100000.0f); m_timeScale = timeScale; int steps = 0; if (GetTickDisplayMode() == eTVTickMode_InSeconds) { m_ticksStep = 10; } else if (GetTickDisplayMode() == eTVTickMode_InFrames) { m_ticksStep = 1 / m_snapFrameTime; } else { assert(0); } double fPixelsPerTick; do { fPixelsPerTick = (1.0 / m_ticksStep) * (double)m_timeScale; if (fPixelsPerTick < 6.0) { m_ticksStep /= 2; } if (m_ticksStep <= 0) { m_ticksStep = 1; break; } steps++; } while (fPixelsPerTick < 6.0 && steps < 100); steps = 0; do { fPixelsPerTick = (1.0 / m_ticksStep) * (double)m_timeScale; if (fPixelsPerTick >= 12.0) { m_ticksStep *= 2; } if (m_ticksStep <= 0) { m_ticksStep = 1; break; } steps++; } while (fPixelsPerTick >= 12.0 && steps < 100); float fCurrentOffset = -fAnchorTime * m_timeScale; m_scrollOffset.rx() += fOldOffset - fCurrentOffset; m_scrollBar->setValue(m_scrollOffset.x()); update(); SetHorizontalExtent(-m_leftOffset, m_timeRange.end * m_timeScale); ComputeFrameSteps(GetVisibleRange()); OnHScroll(); } void CTrackViewDopeSheetBase::showEvent(QShowEvent* event) { QWidget::showEvent(event); GetIEditor()->GetAnimation()->AddListener(this); } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::resizeEvent(QResizeEvent* event) { QWidget::resizeEvent(event); m_rcClient = rect(); m_offscreenBitmap = QPixmap(m_rcClient.width(), m_rcClient.height()); m_offscreenBitmap.fill(Qt::transparent); m_rcTimeline = rect(); m_rcTimeline.setHeight(kDefaultTrackHeight); m_rcSummary = m_rcTimeline; m_rcSummary.setTop(m_rcTimeline.bottom()); m_rcSummary.setBottom(m_rcSummary.top() + 8); SetHorizontalExtent(m_scrollMin, m_scrollMax); m_scrollBar->setGeometry(0, height() - m_scrollBar->sizeHint().height(), width(), m_scrollBar->sizeHint().height()); QToolTip::hideText(); } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::wheelEvent(QWheelEvent* event) { CTrackViewSequence* pSequence = GetIEditor()->GetAnimation()->GetSequence(); if (!pSequence) { event->ignore(); return; } float z = (event->angleDelta().y() > 0) ? (m_timeScale * 1.25f) : (m_timeScale * 0.8f); // Use m_mouseOverPos to get the local position in the timeline view instead of // event->pos() which seems to include the variable left panel of the view that // lists the tracks. float fAnchorTime = TimeFromPointUnsnapped(m_mouseOverPos); SetTimeScale(z, fAnchorTime); event->accept(); } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::OnHScroll() { // Get the current position of scroll box. int curpos = m_scrollBar->value(); m_scrollOffset.setX(curpos); update(); } int CTrackViewDopeSheetBase::GetScrollPos() const { return m_scrollBar->value(); } ////////////////////////////////////////////////////////////////////////// double CTrackViewDopeSheetBase::GetTickTime() const { if (GetTickDisplayMode() == eTVTickMode_InFrames) { return m_fFrameTickStep; } else { return 1.0f / m_ticksStep; } } ////////////////////////////////////////////////////////////////////////// float CTrackViewDopeSheetBase::TickSnap(float time) const { double tickTime = GetTickTime(); double t = floor(((double)time / tickTime) + 0.5); t *= tickTime; return t; } ////////////////////////////////////////////////////////////////////////// float CTrackViewDopeSheetBase::TimeFromPoint(const QPoint& point) const { int x = point.x() - m_leftOffset + m_scrollOffset.x(); double t = (double)x / m_timeScale; return (float)TickSnap(t); } ////////////////////////////////////////////////////////////////////////// float CTrackViewDopeSheetBase::TimeFromPointUnsnapped(const QPoint& point) const { int x = point.x() - m_leftOffset + m_scrollOffset.x(); double t = (double)x / m_timeScale; return t; } void CTrackViewDopeSheetBase::mousePressEvent(QMouseEvent* event) { switch (event->button()) { case Qt::LeftButton: OnLButtonDown(event->modifiers(), event->pos()); break; case Qt::RightButton: OnRButtonDown(event->modifiers(), event->pos()); break; case Qt::MiddleButton: OnMButtonDown(event->modifiers(), event->pos()); break; default: break; } } void CTrackViewDopeSheetBase::mouseReleaseEvent(QMouseEvent* event) { switch (event->button()) { case Qt::LeftButton: OnLButtonUp(event->modifiers(), event->pos()); break; case Qt::RightButton: OnRButtonUp(event->modifiers(), event->pos()); break; case Qt::MiddleButton: OnMButtonUp(event->modifiers(), event->pos()); break; default: break; } } void CTrackViewDopeSheetBase::mouseDoubleClickEvent(QMouseEvent* event) { switch (event->button()) { case Qt::LeftButton: OnLButtonDblClk(event->modifiers(), event->pos()); break; default: break; } } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::OnLButtonDown(Qt::KeyboardModifiers modifiers, const QPoint& point) { CTrackViewSequence* sequence = GetIEditor()->GetAnimation()->GetSequence(); if (!sequence) { return; } HideKeyPropertyCtrlOnSpot(); if (m_rcTimeline.contains(point)) { m_mouseDownPos = point; // Clicked inside timeline. m_mouseMode = eTVMouseMode_DragTime; // If mouse over selected key, change cursor to left-right arrows. SetMouseCursor(m_crsLeftRight); m_stashedRecordModeWhileTimeDragging = GetIEditor()->GetAnimation()->IsRecordMode(); GetIEditor()->GetAnimation()->SetRecording(false); // disable recording while dragging time SetCurrTime(TimeFromPoint(point)); return; } if (m_bEditLock) { m_mouseDownPos = point; return; } if (m_mouseMode == eTVMouseMode_Paste) { m_mouseMode = eTVMouseMode_None; CTrackViewAnimNode* animNode = GetAnimNodeFromPoint(m_mouseOverPos); CTrackViewTrack* pTrack = GetTrackFromPoint(m_mouseOverPos); if (animNode) { AzToolsFramework::ScopedUndoBatch undoBatch("Paste Keys"); sequence->DeselectAllKeys(); sequence->PasteKeysFromClipboard(animNode, pTrack, ComputeSnappedMoveOffset()); undoBatch.MarkEntityDirty(sequence->GetSequenceComponentEntityId()); } SetMouseCursor(Qt::ArrowCursor); OnCaptureChanged(); return; } m_mouseDownPos = point; // The summary region is used for moving already selected keys. if (m_rcSummary.contains(point)) { CTrackViewKeyBundle selectedKeys = sequence->GetSelectedKeys(); if (selectedKeys.GetKeyCount() > 0) { // Move/Clone Key Undo Begin GetIEditor()->BeginUndo(); StoreMementoForTracksWithSelectedKeys(); m_keyTimeOffset = 0; m_mouseMode = eTVMouseMode_Move; SetMouseCursor(m_crsLeftRight); return; } } bool bStart = false; CTrackViewKeyHandle keyHandle = CheckCursorOnStartEndTimeAdjustBar(point, bStart); if (keyHandle.IsValid()) { return LButtonDownOnTimeAdjustBar(point, keyHandle, bStart); } keyHandle = FirstKeyFromPoint(point); if (!keyHandle.IsValid()) { keyHandle = DurationKeyFromPoint(point); } if (keyHandle.IsValid()) { return LButtonDownOnKey(point, keyHandle, modifiers); } if (m_mouseActionMode == eTVActionMode_AddKeys) { AddKeys(point, modifiers & Qt::ShiftModifier); return; } if (modifiers & Qt::ShiftModifier) { m_mouseMode = eTVMouseMode_SelectWithinTime; } else { m_mouseMode = eTVMouseMode_Select; } } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::OnLButtonUp(Qt::KeyboardModifiers modifiers, const QPoint& point) { CTrackViewSequence* pSequence = GetIEditor()->GetAnimation()->GetSequence(); if (!pSequence) { return; } if (m_mouseMode == eTVMouseMode_Select) { // Check if any key are selected. m_rcSelect.translate(-m_scrollOffset); SelectKeys(m_rcSelect, modifiers & Qt::ControlModifier); m_rcSelect = QRect(); m_rubberBand->deleteLater(); m_rubberBand = 0; } else if (m_mouseMode == eTVMouseMode_SelectWithinTime) { m_rcSelect.translate(-m_scrollOffset); SelectAllKeysWithinTimeFrame(m_rcSelect, modifiers & Qt::ControlModifier); m_rcSelect = QRect(); m_rubberBand->deleteLater(); m_rubberBand = 0; } else if (m_mouseMode == eTVMouseMode_DragTime) { SetMouseCursor(Qt::ArrowCursor); // Notify that time was explicitly set GetIEditor()->GetAnimation()->TimeChanged(TimeFromPoint(point)); if (m_stashedRecordModeWhileTimeDragging) { GetIEditor()->GetAnimation()->SetRecording(true); // re-enable recording that was disabled while dragging time m_stashedRecordModeWhileTimeDragging = false; // reset stashed value } } else if (m_mouseMode == eTVMouseMode_Paste) { SetMouseCursor(Qt::ArrowCursor); } OnCaptureChanged(); m_keyTimeOffset = 0; m_keyForTimeAdjust = CTrackViewKeyHandle(); AcceptUndo(); update(); } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::OnLButtonDblClk(Qt::KeyboardModifiers modifiers, const QPoint& point) { CTrackViewSequence* sequence = GetIEditor()->GetAnimation()->GetSequence(); if (!sequence || m_rcTimeline.contains(point) || m_bEditLock) { return; } CTrackViewKeyHandle keyHandle = FirstKeyFromPoint(point); if (!keyHandle.IsValid()) { keyHandle = DurationKeyFromPoint(point); } else { CTrackViewTrack* pTrack = GetTrackFromPoint(point); if (pTrack) { CTrackViewSequenceNotificationContext context(sequence); AzToolsFramework::ScopedUndoBatch undoBatch("Select key"); const std::vector beforeKeyState = sequence->SaveKeyStates(); sequence->DeselectAllKeys(); keyHandle.Select(true); const std::vector afterKeyState = sequence->SaveKeyStates(); if (beforeKeyState != afterKeyState) { undoBatch.MarkEntityDirty(sequence->GetSequenceComponentEntityId()); } m_keyTimeOffset = 0; if (pTrack->GetValueType() == AnimValueType::RGB) { // bring up color picker EditSelectedColorKey(pTrack); } else if (pTrack->GetValueType() != AnimValueType::Bool) { // Edit On Spot is blank (not useful) for boolean tracks so we disable dbl-clicking to bring it up for boolean tracks const QPoint p = QCursor::pos(); bool bKeyChangeInSameTrack = m_pLastTrackSelectedOnSpot && pTrack == m_pLastTrackSelectedOnSpot; m_pLastTrackSelectedOnSpot = pTrack; ShowKeyPropertyCtrlOnSpot(p.x(), p.y(), sequence->GetSelectedKeys().GetKeyCount() > 1, bKeyChangeInSameTrack); } } return; } const bool bTryAddKeysInGroup = modifiers & Qt::ShiftModifier; AddKeys(point, bTryAddKeysInGroup); m_mouseMode = eTVMouseMode_None; } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::OnMButtonDown(Qt::KeyboardModifiers modifiers, const QPoint& point) { OnRButtonDown(modifiers, point); } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::OnMButtonUp(Qt::KeyboardModifiers modifiers, const QPoint& point) { OnRButtonUp(modifiers, point); } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::OnRButtonDown(Qt::KeyboardModifiers modifiers, const QPoint& point) { CTrackViewSequence* pSequence = GetIEditor()->GetAnimation()->GetSequence(); if (!pSequence) { return; } HideKeyPropertyCtrlOnSpot(); m_bCursorWasInKey = false; m_bMouseMovedAfterRButtonDown = false; if (m_rcTimeline.contains(point)) { // Clicked inside timeline. // adjust markers. int nMarkerStart = TimeToClient(m_timeMarked.start); int nMarkerEnd = TimeToClient(m_timeMarked.end); if ((abs(point.x() - nMarkerStart)) < (abs(point.x() - nMarkerEnd))) { SetStartMarker(TimeFromPoint(point)); m_mouseMode = eTVMouseMode_DragStartMarker; } else { SetEndMarker(TimeFromPoint(point)); m_mouseMode = eTVMouseMode_DragEndMarker; } return; } m_mouseDownPos = point; if (modifiers & Qt::ShiftModifier) // alternative zoom { m_bZoomDrag = true; return; } CTrackViewKeyHandle keyHandle = FirstKeyFromPoint(point); if (!keyHandle.IsValid()) { keyHandle = DurationKeyFromPoint(point); } if (keyHandle.IsValid()) { m_bCursorWasInKey = true; CTrackViewNode* pNode = GetNodeFromPoint(point); CTrackViewTrack* pTrack = static_cast(pNode); keyHandle.Select(true); m_keyTimeOffset = 0; update(); // Show a little pop-up menu for copy & delete. QMenu menu; CTrackViewKeyBundle selectedKeys = pSequence->GetSelectedKeys(); const bool bEnableEditOnSpot = ((pTrack && pTrack->GetValueType() != AnimValueType::Bool) && (selectedKeys.GetKeyCount() > 0 && selectedKeys.AreAllKeysOfSameType())); QAction* actionEditOnSpot = menu.addAction(tr("Edit On Spot")); actionEditOnSpot->setEnabled(bEnableEditOnSpot); menu.addSeparator(); QAction* actionCopy = menu.addAction(tr("Copy")); menu.addSeparator(); QAction* actionDelete = menu.addAction(tr("Delete")); const QPoint p = QCursor::pos(); QAction* action = menu.exec(p); if (action == actionEditOnSpot) { bool bKeyChangeInSameTrack = m_pLastTrackSelectedOnSpot && selectedKeys.GetKeyCount() == 1 && selectedKeys.GetKey(0).GetTrack() == m_pLastTrackSelectedOnSpot; if (selectedKeys.GetKeyCount() == 1) { m_pLastTrackSelectedOnSpot = selectedKeys.GetKey(0).GetTrack(); } else { m_pLastTrackSelectedOnSpot = NULL; } ShowKeyPropertyCtrlOnSpot(p.x(), p.y(), selectedKeys.GetKeyCount() > 1, bKeyChangeInSameTrack); } else if (action == actionCopy) { pSequence->CopyKeysToClipboard(true, false); } else if (action == actionDelete) { CUndo undo("Delete Keys"); pSequence->DeleteSelectedKeys(); } } else { m_bMoveDrag = true; } } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::OnRButtonUp(Qt::KeyboardModifiers modifiers, const QPoint& point) { CTrackViewSequence* pSequence = GetIEditor()->GetAnimation()->GetSequence(); if (!pSequence) { return; } m_bZoomDrag = false; m_bMoveDrag = false; OnCaptureChanged(); m_mouseMode = eTVMouseMode_None; if (!m_bCursorWasInKey) { const bool bHasCopiedKey = (GetKeysInClickboard() != NULL); if (bHasCopiedKey && m_bMouseMovedAfterRButtonDown == false) // Once moved, it means the user wanted to scroll, so no paste pop-up. { // Show a little pop-up menu for paste. QMenu menu; QAction* actionPaste = menu.addAction(tr("Paste")); QAction* action = menu.exec(QCursor::pos()); if (action == actionPaste) { StartPasteKeys(); } } } } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::CancelDrag() { AcceptUndo(); m_mouseMode = eTVMouseMode_None; } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::mouseMoveEvent(QMouseEvent* event) { CTrackViewSequence* pSequence = GetIEditor()->GetAnimation()->GetSequence(); if (!pSequence) { return; } // To prevent the key moving while selecting if (m_bJustSelected) { m_bJustSelected = false; return; } // For some drags, make sure the left mouse button is still down. // If you drag off the window, and press the right mouse button, // and *then* release the left mouse button, QT will never tell us // about the release event. bool leftButtonPressed = event->buttons() & Qt::LeftButton; m_bMouseMovedAfterRButtonDown = true; m_mouseOverPos = event->pos(); if (m_bZoomDrag && (event->modifiers() & Qt::ShiftModifier)) { float fAnchorTime = TimeFromPointUnsnapped(m_mouseDownPos); SetTimeScale(m_timeScale * (1.0f + (event->pos().x() - m_mouseDownPos.x()) * 0.0025f), fAnchorTime); m_mouseDownPos = event->pos(); return; } else { m_bZoomDrag = false; } if (m_bMoveDrag) { m_scrollOffset.setX(qBound(m_scrollMin, m_scrollOffset.x() + m_mouseDownPos.x() - event->pos().x(), m_scrollMax)); m_mouseDownPos = event->pos(); // Set the new position of the thumb (scroll box). m_scrollBar->setValue(m_scrollOffset.x()); update(); SetMouseCursor(m_crsLeftRight); return; } if (m_mouseMode == eTVMouseMode_Select || m_mouseMode == eTVMouseMode_SelectWithinTime) { MouseMoveSelect(event->pos()); } else if (m_mouseMode == eTVMouseMode_Move) { if (leftButtonPressed) { MouseMoveMove(event->pos(), event->modifiers()); } else { CancelDrag(); } } else if (m_mouseMode == eTVMouseMode_Clone) { pSequence->CloneSelectedKeys(); m_mouseMode = eTVMouseMode_Move; } else if (m_mouseMode == eTVMouseMode_DragTime) { if (leftButtonPressed) { MouseMoveDragTime(event->pos(), event->modifiers()); } else { CancelDrag(); } } else if (m_mouseMode == eTVMouseMode_DragStartMarker) { if (leftButtonPressed) { MouseMoveDragStartMarker(event->pos(), event->modifiers()); } else { CancelDrag(); } } else if (m_mouseMode == eTVMouseMode_DragEndMarker) { if (leftButtonPressed) { MouseMoveDragEndMarker(event->pos(), event->modifiers()); } else { CancelDrag(); } } else if (m_mouseMode == eTVMouseMode_Paste) { update(); } else if (m_mouseMode == eTVMouseMode_StartTimeAdjust) { if (leftButtonPressed) { MouseMoveStartEndTimeAdjust(event->pos(), true); } else { CancelDrag(); } } else if (m_mouseMode == eTVMouseMode_EndTimeAdjust) { if (leftButtonPressed) { MouseMoveStartEndTimeAdjust(event->pos(), false); } else { CancelDrag(); } } else { ////////////////////////////////////////////////////////////////////////// if (m_mouseActionMode == eTVActionMode_AddKeys) { SetMouseCursor(m_crsAddKey); } else { MouseMoveOver(event->pos()); } } } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::paintEvent(QPaintEvent* event) { QPainter painter(this); { // In case of the fast-redraw mode, just draw the saved bitmap. // Otherwise, actually redraw all things. // This mode is helpful when playing a sequence if the sequence has a lot of keys. if (!m_bFastRedraw) { QLinearGradient gradient(rect().topLeft(), rect().bottomLeft()); gradient.setColorAt(0, QColor(250, 250, 250)); gradient.setColorAt(1, QColor(220, 220, 220)); painter.fillRect(rect(), gradient); if (GetIEditor()->GetAnimation()->GetSequence()) { if (m_bEditLock) { painter.fillRect(event->rect(), EDIT_DISABLE_GRAY_COLOR); } DrawControl(&painter, event->rect()); } } } if (GetIEditor()->GetAnimation()->GetSequence()) { // Drawing the timeline is handled separately. In other words, it's not saved to the 'm_offscreenBitmap'. // This is for the fast-redraw mode mentioned above. DrawTimeline(&painter, event->rect()); } #ifdef DEBUG painter.setFont(m_descriptionFont); painter.setPen(QColor(255, 255, 255)); painter.setBrush(QColor(0, 0, 0)); const QString redrawCountStr = QString::fromLatin1("Redraw Count: %1").arg(m_redrawCount); QRect redrawCountRect(0, 0, 150, 20); QRect bounds; painter.drawText(redrawCountRect, Qt::AlignLeft | Qt::TextSingleLine, redrawCountStr, &bounds); painter.fillRect(bounds, Qt::black); painter.drawText(redrawCountRect, Qt::AlignLeft | Qt::TextSingleLine, redrawCountStr); ++m_redrawCount; #endif } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::SelectAllKeysWithinTimeFrame(const QRect& rc, const bool bMultiSelection) { CTrackViewSequence* sequence = GetIEditor()->GetAnimation()->GetSequence(); if (!sequence) { return; } AzToolsFramework::ScopedUndoBatch undoBatch("Select keys"); const std::vector beforeKeyState = sequence->SaveKeyStates(); if (!bMultiSelection) { sequence->DeselectAllKeys(); } // put selection rectangle from client to track space. QRect trackRect = rc; trackRect.translate(m_scrollOffset); Range selTime = GetTimeRange(trackRect); CTrackViewTrackBundle tracks = sequence->GetAllTracks(); CTrackViewSequenceNotificationContext context(sequence); for (int i = 0; i < tracks.GetCount(); ++i) { CTrackViewTrack* pTrack = tracks.GetTrack(i); // Check which keys we intersect. for (int j = 0; j < pTrack->GetKeyCount(); j++) { CTrackViewKeyHandle keyHandle = pTrack->GetKey(j); const float time = keyHandle.GetTime(); if (selTime.IsInside(time)) { keyHandle.Select(true); } } } const std::vector afterKeyState = sequence->SaveKeyStates(); if (beforeKeyState != afterKeyState) { undoBatch.MarkEntityDirty(sequence->GetSequenceComponentEntityId()); } } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::SetMouseCursor(const QCursor& cursor) { m_currCursor = cursor; setCursor(m_currCursor); } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::SetCurrTime(float time) { if (time < m_timeRange.start) { time = m_timeRange.start; } if (time > m_timeRange.end) { time = m_timeRange.end; } GetIEditor()->GetAnimation()->SetTime(time); } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::OnTimeChanged(float newTime) { int x1 = TimeToClient(m_currentTime); int x2 = TimeToClient(newTime); m_currentTime = newTime; m_bFastRedraw = true; const QRect rc(QPoint(x1 - 3, m_rcClient.top()), QPoint(x1 + 4, m_rcClient.bottom())); update(rc); const QRect rc1(QPoint(x2 - 3, m_rcClient.top()), QPoint(x2 + 4, m_rcClient.bottom())); update(rc1); m_bFastRedraw = false; } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::SetStartMarker(float fTime) { m_timeMarked.start = fTime; if (m_timeMarked.start < m_timeRange.start) { m_timeMarked.start = m_timeRange.start; } if (m_timeMarked.start > m_timeRange.end) { m_timeMarked.start = m_timeRange.end; } if (m_timeMarked.start > m_timeMarked.end) { m_timeMarked.end = m_timeMarked.start; } GetIEditor()->GetAnimation()->SetMarkers(m_timeMarked); update(); } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::SetEndMarker(float fTime) { m_timeMarked.end = fTime; if (m_timeMarked.end < m_timeRange.start) { m_timeMarked.end = m_timeRange.start; } if (m_timeMarked.end > m_timeRange.end) { m_timeMarked.end = m_timeRange.end; } if (m_timeMarked.start > m_timeMarked.end) { m_timeMarked.start = m_timeMarked.end; } GetIEditor()->GetAnimation()->SetMarkers(m_timeMarked); update(); } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::SetMouseActionMode(ETVActionMode mode) { m_mouseActionMode = mode; if (mode == eTVActionMode_AddKeys) { setCursor(m_crsAddKey); } } ////////////////////////////////////////////////////////////////////////// CTrackViewNode* CTrackViewDopeSheetBase::GetNodeFromPointRec(CTrackViewNode* pCurrentNode, const QPoint& point) { QRect currentNodeRect = GetNodeRect(pCurrentNode); if (currentNodeRect.top() > point.y()) { return nullptr; } if (currentNodeRect.bottom() >= point.y()) { return pCurrentNode; } if (pCurrentNode->GetExpanded()) { unsigned int childCount = pCurrentNode->GetChildCount(); for (unsigned int i = 0; i < childCount; ++i) { CTrackViewNode* pFoundNode = GetNodeFromPointRec(pCurrentNode->GetChild(i), point); if (pFoundNode) { return pFoundNode; } } } return nullptr; } ////////////////////////////////////////////////////////////////////////// CTrackViewNode* CTrackViewDopeSheetBase::GetNodeFromPoint(const QPoint& point) { CTrackViewSequence* pSequence = GetIEditor()->GetAnimation()->GetSequence(); return GetNodeFromPointRec(pSequence, point); } ////////////////////////////////////////////////////////////////////////// CTrackViewAnimNode* CTrackViewDopeSheetBase::GetAnimNodeFromPoint(const QPoint& point) { CTrackViewNode* pNode = GetNodeFromPoint(point); if (pNode) { if (pNode->GetNodeType() == eTVNT_Track) { CTrackViewTrack* pTrack = static_cast(pNode); return static_cast(pTrack->GetAnimNode()); } else if (pNode->GetNodeType() == eTVNT_AnimNode) { return static_cast(pNode); } } return nullptr; } ////////////////////////////////////////////////////////////////////////// CTrackViewTrack* CTrackViewDopeSheetBase::GetTrackFromPoint(const QPoint& point) { CTrackViewNode* pNode = GetNodeFromPoint(point); if (pNode && pNode->GetNodeType() == eTVNT_Track) { return static_cast(pNode); } return nullptr; } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::SetHorizontalExtent(int min, int max) { m_scrollMin = min; m_scrollMax = max; m_scrollBar->setPageStep(m_rcClient.width() / 2); m_scrollBar->setRange(min, max - m_scrollBar->pageStep() * 2 + m_leftOffset); }; ////////////////////////////////////////////////////////////////////////// XmlNodeRef CTrackViewDopeSheetBase::GetKeysInClickboard() { CClipboard clip(this); if (clip.IsEmpty()) { return NULL; } if (clip.GetTitle() != "Track view keys") { return NULL; } XmlNodeRef copyNode = clip.Get(); if (copyNode == NULL || strcmp(copyNode->getTag(), "CopyKeysNode")) { return NULL; } int nNumTracksToPaste = copyNode->getChildCount(); if (nNumTracksToPaste == 0) { return NULL; } return copyNode; } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::StartPasteKeys() { m_clipboardKeys = GetKeysInClickboard(); if (m_clipboardKeys) { m_mouseMode = eTVMouseMode_Paste; // If mouse over selected key, change cursor to left-right arrows. SetMouseCursor(m_crsLeftRight); m_mouseDownPos = m_mouseOverPos; } } void CTrackViewDopeSheetBase::keyPressEvent(QKeyEvent* event) { CTrackViewSequence* sequence = GetIEditor()->GetAnimation()->GetSequence(); if (!sequence) { return; } // HAVE TO INCLUDE CASES FOR THESE IN THE ShortcutOverride handler in ::event() below if (event->matches(QKeySequence::Delete)) { CUndo undo("Delete Keys"); sequence->DeleteSelectedKeys(); return; } if (event->key() == Qt::Key_Up || event->key() == Qt::Key_Down || event->key() == Qt::Key_Right || event->key() == Qt::Key_Left) { CTrackViewKeyBundle keyBundle = sequence->GetSelectedKeys(); CTrackViewKeyHandle keyHandle = keyBundle.GetSingleSelectedKey(); if (keyHandle.IsValid()) { switch (event->key()) { case Qt::Key_Up: keyHandle = keyHandle.GetAboveKey(); break; case Qt::Key_Down: keyHandle = keyHandle.GetBelowKey(); break; case Qt::Key_Right: keyHandle = keyHandle.GetNextKey(); break; case Qt::Key_Left: keyHandle = keyHandle.GetPrevKey(); break; } if (keyHandle.IsValid()) { CTrackViewSequenceNotificationContext context(sequence); const std::vector beforeKeyState = sequence->SaveKeyStates(); AzToolsFramework::ScopedUndoBatch undoBatch("Select Key"); sequence->DeselectAllKeys(); keyHandle.Select(true); const std::vector afterKeyState = sequence->SaveKeyStates(); if (beforeKeyState != afterKeyState) { undoBatch.MarkEntityDirty(sequence->GetSequenceComponentEntityId()); } } } return; } if (event->matches(QKeySequence::Copy)) { sequence->CopyKeysToClipboard(true, false); } else if (event->matches(QKeySequence::Paste)) { StartPasteKeys(); } else if (event->matches(QKeySequence::Undo)) { GetIEditor()->Undo(); } else if (event->matches(QKeySequence::Redo)) { GetIEditor()->Redo(); } else { return QWidget::keyPressEvent(event); } } bool CTrackViewDopeSheetBase::event(QEvent* e) { if (e->type() == QEvent::ShortcutOverride) { // since we respond to the following things, let Qt know so that shortcuts don't override us bool respondsToEvent = false; QKeyEvent* keyEvent = static_cast(e); switch (keyEvent->key()) { case Qt::Key_Delete: case Qt::Key_Up: case Qt::Key_Down: case Qt::Key_Left: case Qt::Key_Right: respondsToEvent = true; break; default: respondsToEvent = keyEvent->matches(QKeySequence::Copy) || keyEvent->matches(QKeySequence::Paste) || keyEvent->matches(QKeySequence::Undo) || keyEvent->matches(QKeySequence::Redo); break; } if (respondsToEvent) { e->accept(); return true; } } return QWidget::event(e); } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::ShowKeyTooltip(CTrackViewKeyHandle& keyHandle, const QPoint& point) { if (m_lastTooltipPos == point) { return; } m_lastTooltipPos = point; const float time = keyHandle.GetTime(); const char* desc = keyHandle.GetDescription(); float duration = keyHandle.GetDuration(); QString tipText; if (GetTickDisplayMode() == eTVTickMode_InSeconds) { tipText = tr("%1, {%2}").arg(time, 0, 'f', 3).arg(desc); } else { tipText = tr("%1, {%2}").arg(ftoi(time / m_snapFrameTime)).arg(desc); } QToolTip::showText(point, tipText); } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::OnCaptureChanged() { AcceptUndo(); m_bZoomDrag = false; m_bMoveDrag = false; } ////////////////////////////////////////////////////////////////////////// bool CTrackViewDopeSheetBase::IsOkToAddKeyHere(const CTrackViewTrack* pTrack, float time) const { const float timeEpsilon = 0.05f; for (int i = 0; i < pTrack->GetKeyCount(); ++i) { const CTrackViewKeyConstHandle& keyHandle = pTrack->GetKey(i); if (keyHandle.GetTime() == time) { return false; } } return true; } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::MouseMoveSelect(const QPoint& point) { SetMouseCursor(Qt::ArrowCursor); QRect rc(m_mouseDownPos, point); rc = rc.normalized(); QRect rcClient = rect(); rc = rc.intersected(rcClient); if (m_rubberBand == nullptr) { m_rubberBand = new QRubberBand(QRubberBand::Rectangle, this); } m_rubberBand->show(); if (m_mouseMode == eTVMouseMode_SelectWithinTime) { rc.setTop(m_rcClient.top()); rc.setBottom(m_rcClient.bottom()); } m_rcSelect = rc; m_rubberBand->setGeometry(m_rcSelect); } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::MouseMoveStartEndTimeAdjust(const QPoint& p, bool bStart) { CTrackViewSequence* sequence = GetIEditor()->GetAnimation()->GetSequence(); if (!sequence) { return; } SetMouseCursor(m_crsAdjustLR); const QPoint point(qBound(m_rcClient.left(), p.x(), m_rcClient.right()), p.y()); const QPoint ofs = point - m_mouseDownPos; CTrackViewKeyHandle& keyHandle = m_keyForTimeAdjust; // TODO: Refactor this Time Range Key stuff. ICharacterKey characterKey; AZ::IAssetBlendKey assetBlendKey; ITimeRangeKey* timeRangeKey = nullptr; if (keyHandle.GetTrack()->GetValueType() == AnimValueType::AssetBlend) { keyHandle.GetKey(&assetBlendKey); timeRangeKey = &assetBlendKey; } else { // This will work for both character & time range keys because // ICharacterKey is derived from ITimeRangeKey. Not the most beautiful code. keyHandle.GetKey(&characterKey); timeRangeKey = &characterKey; } float& timeToAdjust = bStart ? timeRangeKey->m_startTime : timeRangeKey->m_endTime; // Undo the last offset. timeToAdjust += -m_keyTimeOffset; // Apply a new offset. m_keyTimeOffset = (ofs.x() / m_timeScale) * timeRangeKey->m_speed; timeToAdjust += m_keyTimeOffset; // Check the validity. if (bStart) { if (timeToAdjust < 0) { timeToAdjust = 0; } else if (timeToAdjust > timeRangeKey->GetValidEndTime()) { timeToAdjust = timeRangeKey->GetValidEndTime(); } } else { float endTime = AZStd::min(timeRangeKey->GetValidEndTime(), timeRangeKey->m_duration); if (timeToAdjust < timeRangeKey->m_startTime) { timeToAdjust = timeRangeKey->m_startTime; } else if (timeToAdjust > endTime) { timeToAdjust = endTime; } } keyHandle.SetKey(timeRangeKey); update(); } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::MouseMoveMove(const QPoint& p, Qt::KeyboardModifiers modifiers) { CTrackViewSequence* pSequence = GetIEditor()->GetAnimation()->GetSequence(); CTrackViewSequenceNotificationContext context(pSequence); SetMouseCursor(m_crsLeftRight); const QPoint point(qBound(m_rcClient.left(), p.x(), m_rcClient.right()), p.y()); // Reset tracks to their initial state before starting the move for (auto iter = m_trackMementos.begin(); iter != m_trackMementos.end(); ++iter) { CTrackViewTrack* pTrack = iter->first; const TrackMemento& trackMemento = iter->second; pTrack->RestoreFromMemento(trackMemento.m_memento); const unsigned int numKeys = trackMemento.m_keySelectionStates.size(); for (unsigned int i = 0; i < numKeys; ++i) { pTrack->GetKey(i).Select(trackMemento.m_keySelectionStates[i]); } } CTrackViewKeyHandle keyHandle = FirstKeyFromPoint(m_mouseDownPos); if (!keyHandle.IsValid()) { keyHandle = DurationKeyFromPoint(m_mouseDownPos); } float oldTime; if (keyHandle.IsValid()) { oldTime = keyHandle.GetTime(); } else { oldTime = TimeFromPointUnsnapped(m_mouseDownPos); } QPoint ofs = point - m_mouseDownPos; float timeOffset = ofs.x() / m_timeScale; float newTime = oldTime + timeOffset; // Snap it, if necessary. ESnappingMode snappingMode = GetKeyModifiedSnappingMode(); if (snappingMode == eSnappingMode_SnapFrame) { snappingMode = m_snappingMode; } if (snappingMode == eSnappingMode_SnapMagnet) { newTime = MagnetSnap(newTime, GetAnimNodeFromPoint(m_mouseOverPos)); } else if (snappingMode == eSnappingMode_SnapTick) { newTime = TickSnap(newTime); } else if (snappingMode == eSnappingMode_SnapFrame) { newTime = FrameSnap(newTime); } Range extendedTimeRange(0.0f, m_timeRange.end); extendedTimeRange.ClipValue(newTime); timeOffset = newTime - oldTime; // Re-compute the time offset using snapped & clipped 'newTime'. if (timeOffset == 0.0f) { return; } m_bKeysMoved = true; if (m_mouseActionMode == eTVActionMode_ScaleKey) { float tscale = 0.005f; float tofs = ofs.x() * tscale; tofs = pSequence->ClipTimeOffsetForScaling(1 + tofs) - 1; // Offset all selected keys by this offset. pSequence->ScaleSelectedKeys(1 + tofs); m_keyTimeOffset = tofs; } else { // Offset all selected keys by this offset. if (m_mouseActionMode == eTVActionMode_SlideKey) { timeOffset = pSequence->ClipTimeOffsetForSliding(timeOffset); pSequence->SlideKeys(timeOffset); } else { timeOffset = pSequence->ClipTimeOffsetForOffsetting(timeOffset); pSequence->OffsetSelectedKeys(timeOffset); } if (CheckVirtualKey(Qt::Key_Menu)) { CTrackViewKeyBundle selectedKeys = pSequence->GetSelectedKeys(); CTrackViewKeyHandle selectedKey = selectedKeys.GetSingleSelectedKey(); if (selectedKey.IsValid()) { GetIEditor()->GetAnimation()->SetTime(selectedKey.GetTime()); } } m_keyTimeOffset = timeOffset; } // The time of the selected keys has likely just changed. OnKeySelectionChanged() so the // UI elements of the key properties control will update. pSequence->OnKeySelectionChanged(); } void CTrackViewDopeSheetBase::MouseMoveDragTime(const QPoint& point, Qt::KeyboardModifiers modifiers) { const QPoint p(qBound(m_rcClient.left(), point.x(), m_rcClient.right()), qBound(m_rcClient.top(), point.y(), m_rcClient.bottom())); float time = TimeFromPointUnsnapped(p); m_timeRange.ClipValue(time); bool bSnap = (modifiers & Qt::ControlModifier); if (bSnap) { time = TickSnap(time); } SetCurrTime(time); } void CTrackViewDopeSheetBase::MouseMoveDragStartMarker(const QPoint& point, Qt::KeyboardModifiers modifiers) { const QPoint p(qBound(m_rcClient.left(), point.x(), m_rcClient.right()), qBound(m_rcClient.top(), point.y(), m_rcClient.bottom())); bool bNoSnap = (modifiers & Qt::ControlModifier); float time = TimeFromPointUnsnapped(p); m_timeRange.ClipValue(time); if (!bNoSnap) { time = TickSnap(time); } SetStartMarker(time); } void CTrackViewDopeSheetBase::MouseMoveDragEndMarker(const QPoint& point, Qt::KeyboardModifiers modifiers) { const QPoint p(qBound(m_rcClient.left(), point.x(), m_rcClient.right()), qBound(m_rcClient.top(), point.y(), m_rcClient.bottom())); bool bNoSnap = (modifiers & Qt::ControlModifier); float time = TimeFromPointUnsnapped(p); m_timeRange.ClipValue(time); if (!bNoSnap) { time = TickSnap(time); } SetEndMarker(time); } void CTrackViewDopeSheetBase::MouseMoveOver(const QPoint& point) { // No mouse mode. SetMouseCursor(Qt::ArrowCursor); bool bStart = false; CTrackViewKeyHandle keyHandle = CheckCursorOnStartEndTimeAdjustBar(point, bStart); if (keyHandle.IsValid()) { SetMouseCursor(m_crsAdjustLR); return; } keyHandle = FirstKeyFromPoint(point); if (!keyHandle.IsValid()) { keyHandle = DurationKeyFromPoint(point); } if (keyHandle.IsValid()) { CTrackViewTrack* pTrack = GetTrackFromPoint(point); if (pTrack && keyHandle.IsSelected()) { // If mouse over selected key, change cursor to left-right arrows. SetMouseCursor(m_crsLeftRight); } else { SetMouseCursor(m_crsCross); } if (pTrack) { ShowKeyTooltip(keyHandle, mapToGlobal(point)); } } else { QToolTip::hideText(); } } float CTrackViewDopeSheetBase::MagnetSnap(float newTime, const CTrackViewAnimNode* pNode) const { CTrackViewSequence* pSequence = GetIEditor()->GetAnimation()->GetSequence(); if (!pSequence) { return newTime; } CTrackViewKeyBundle keys = pSequence->GetKeysInTimeRange(newTime - kMarginForMagnetSnapping / m_timeScale, newTime + kMarginForMagnetSnapping / m_timeScale); if (keys.GetKeyCount() > 0) { // By default, just use the first key that belongs to the time range as a magnet. newTime = keys.GetKey(0).GetTime(); // But if there is an in-range key in a sibling track, use it instead. // Here a 'sibling' means a track that belongs to a same node. for (int i = 0; i < keys.GetKeyCount(); ++i) { CTrackViewKeyHandle keyHandle = keys.GetKey(i); if (keyHandle.GetTrack()->GetAnimNode() == pNode) { newTime = keyHandle.GetTime(); break; } } } return newTime; } ////////////////////////////////////////////////////////////////////////// float CTrackViewDopeSheetBase::FrameSnap(float time) const { double t = floor((double)time / m_snapFrameTime + 0.5); t = t * m_snapFrameTime; return t; } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::ShowKeyPropertyCtrlOnSpot(int x, int y, bool bMultipleKeysSelected, bool bKeyChangeInSameTrack) { if (m_keyPropertiesDlg == NULL) { return; } if (m_wndPropsOnSpot == nullptr) { m_wndPropsOnSpot = new ReflectedPropertyControl(this); m_wndPropsOnSpot->Setup(true, 150); m_wndPropsOnSpot->setWindowFlags(Qt::CustomizeWindowHint | Qt::Popup | Qt::WindowStaysOnTopHint); m_wndPropsOnSpot->SetStoreUndoByItems(false); bKeyChangeInSameTrack = false; } if (bKeyChangeInSameTrack) { m_wndPropsOnSpot->ClearSelection(); m_wndPropsOnSpot->ReloadValues(); } else { m_keyPropertiesDlg->PopulateVariables(m_wndPropsOnSpot); } m_wndPropsOnSpot->show(); m_wndPropsOnSpot->move(x, y); m_wndPropsOnSpot->ExpandAll(); QTimer::singleShot(0, [this]() { m_wndPropsOnSpot->resize(m_wndPropsOnSpot->sizeHint()); }); } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::HideKeyPropertyCtrlOnSpot() { if (m_wndPropsOnSpot) { m_wndPropsOnSpot->hide(); m_wndPropsOnSpot->ClearSelection(); } } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::SetScrollOffset(int hpos) { m_scrollBar->setValue(hpos); m_scrollOffset.setX(hpos); update(); } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::LButtonDownOnTimeAdjustBar(const QPoint& point, CTrackViewKeyHandle& keyHandle, bool bStart) { CTrackViewSequence* sequence = GetIEditor()->GetAnimation()->GetSequence(); m_keyTimeOffset = 0; m_keyForTimeAdjust = keyHandle; GetIEditor()->BeginUndo(); if (bStart) { m_mouseMode = eTVMouseMode_StartTimeAdjust; } else { // TODO: Refactor this Time Range Key stuff. ICharacterKey characterKey; AZ::IAssetBlendKey assetBlendKey; ITimeRangeKey* timeRangeKey = nullptr; if (keyHandle.GetTrack()->GetValueType() == AnimValueType::AssetBlend) { keyHandle.GetKey(&assetBlendKey); timeRangeKey = &assetBlendKey; } else { // This will work for both character & time range keys because // ICharacterKey is derived from ITimeRangeKey. Not the most beautiful code. keyHandle.GetKey(&characterKey); timeRangeKey = &characterKey; } // In case of the end time, make it have a valid (not zero) // end time, first. if (timeRangeKey->m_endTime == 0) { timeRangeKey->m_endTime = timeRangeKey->m_duration; keyHandle.SetKey(timeRangeKey); } m_mouseMode = eTVMouseMode_EndTimeAdjust; } SetMouseCursor(m_crsAdjustLR); } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::LButtonDownOnKey(const QPoint& point, CTrackViewKeyHandle& keyHandle, Qt::KeyboardModifiers modifiers) { CTrackViewSequence* sequence = GetIEditor()->GetAnimation()->GetSequence(); AZ_Assert(sequence, "Expected a valid sequence."); if (!sequence) { return; } if (!keyHandle.IsSelected() && !(modifiers & Qt::ControlModifier)) { CTrackViewSequenceNotificationContext context(sequence); AzToolsFramework::ScopedUndoBatch undoBatch("Select keys"); const std::vector beforeKeyState = sequence->SaveKeyStates(); sequence->DeselectAllKeys(); m_bJustSelected = true; m_keyTimeOffset = 0; keyHandle.Select(true); ChangeSequenceTrackSelection(sequence, keyHandle.GetTrack()); const std::vector afterKeyState = sequence->SaveKeyStates(); if (beforeKeyState != afterKeyState) { undoBatch.MarkEntityDirty(sequence->GetSequenceComponentEntityId()); } } else { GetIEditor()->CancelUndo(); } // Move/Clone Key Undo Begin GetIEditor()->BeginUndo(); StoreMementoForTracksWithSelectedKeys(); if (modifiers & Qt::ShiftModifier) { m_mouseMode = eTVMouseMode_Clone; SetMouseCursor(m_crsLeftRight); } else { m_mouseMode = eTVMouseMode_Move; SetMouseCursor(m_crsLeftRight); } update(); } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::ChangeSequenceTrackSelection(CTrackViewSequence* sequenceWithTrack, CTrackViewTrack* trackToSelect) const { // Deselect all currently selected tracks that aren't the keyHandle's Track, then ensure the trackToSelect is selected CTrackViewTrackBundle prevSelectedTracks; prevSelectedTracks = sequenceWithTrack->GetSelectedTracks(); for (int i = 0; i < prevSelectedTracks.GetCount(); i++) { CTrackViewTrack* prevSelectedTrack = prevSelectedTracks.GetTrack(i); if (prevSelectedTrack != trackToSelect) { prevSelectedTrack->SetSelected(false); } } trackToSelect->SetSelected(true); } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::ChangeSequenceTrackSelection(CTrackViewSequence* sequence, CTrackViewTrackBundle tracksToSelect, bool multiTrackSelection) const { if (!multiTrackSelection) { // Deselect any tracks not in the tracksToSelect bundle CTrackViewTrackBundle prevSelectedTracks; prevSelectedTracks = sequence->GetSelectedTracks(); for (int i = prevSelectedTracks.GetCount(); --i >= 0; ) { bool deselectTrack = true; CTrackViewTrack* prevSelectedTrack = prevSelectedTracks.GetTrack(i); for (int j = tracksToSelect.GetCount(); --j >= 0; ) { CTrackViewTrack* selectTrackCandidate = tracksToSelect.GetTrack(j); if (selectTrackCandidate == prevSelectedTrack) { // selectTrackCandidate is already selected tracksToSelect.RemoveTrack(selectTrackCandidate); deselectTrack = false; break; } } if (deselectTrack) { prevSelectedTrack->SetSelected(false); } } } // Add remaining tracks in tracksToSelect bundle to track selection for (int j = tracksToSelect.GetCount(); --j >= 0; ) { tracksToSelect.GetTrack(j)->SetSelected(true); } } ////////////////////////////////////////////////////////////////////////// bool CTrackViewDopeSheetBase::CreateColorKey(CTrackViewTrack* pTrack, float keyTime) { bool keyCreated = false; Vec3 vColor(0, 0, 0); pTrack->GetValue(keyTime, vColor); const AZ::Color defaultColor(clamp_tpl(FloatToIntRet(vColor.x), 0, 255), clamp_tpl(FloatToIntRet(vColor.y), 0, 255), clamp_tpl(FloatToIntRet(vColor.z), 0, 255), 255); m_colorUpdateTrack = pTrack; m_colorUpdateKeyTime = keyTime; AzQtComponents::ColorPicker dlg(AzQtComponents::ColorPicker::Configuration::RGB, QString(), this); dlg.setWindowTitle(tr("Select Color")); dlg.setCurrentColor(defaultColor); dlg.setSelectedColor(defaultColor); // Allows live update of colors on key creation QObject::connect(&dlg, &AzQtComponents::ColorPicker::currentColorChanged, this, &CTrackViewDopeSheetBase::OnCurrentColorChange); if (dlg.exec() == QDialog::Accepted) { const AZ::Color col = dlg.currentColor(); ColorF colArray(col.GetR8(), col.GetG8(), col.GetB8(), col.GetA8()); CTrackViewSequence* sequence = pTrack->GetSequence(); if (nullptr != sequence) { CTrackViewSequenceNotificationContext context(sequence); AzToolsFramework::ScopedUndoBatch undoBatch("Set Key"); const unsigned int numChildNodes = pTrack->GetChildCount(); for (int subTrackIndex = 0; subTrackIndex < numChildNodes; ++subTrackIndex) { CTrackViewTrack* subTrack = static_cast(pTrack->GetChild(subTrackIndex)); if (IsOkToAddKeyHere(subTrack, keyTime)) { CTrackViewKeyHandle newKey = subTrack->CreateKey(keyTime); I2DBezierKey bezierKey; newKey.GetKey(&bezierKey); bezierKey.value = Vec2(keyTime, colArray[subTrackIndex]); newKey.SetKey(&bezierKey); keyCreated = true; } } undoBatch.MarkEntityDirty(sequence->GetSequenceComponentEntityId()); } } //Remove added keys for live update else { const unsigned int numChildNodes = m_colorUpdateTrack->GetChildCount(); for (int subTrackIndex = 0; subTrackIndex < numChildNodes; ++subTrackIndex) { CTrackViewTrack* subTrack = static_cast(m_colorUpdateTrack->GetChild(subTrackIndex)); CTrackViewKeyHandle subTrackKey = subTrack->GetKeyByTime(m_colorUpdateKeyTime); if (subTrackKey.IsValid()) { subTrackKey.Delete(); } } } return keyCreated; } void CTrackViewDopeSheetBase::OnCurrentColorChange(const AZ::Color& color) { // This is while the color picker is up // so we want to update the property but not store an undo UpdateColorKey(AzQtComponents::toQColor(color), false); } void CTrackViewDopeSheetBase::UpdateColorKey(const QColor& color, bool addToUndo) { ColorF colArray(color.red(), color.green(), color.blue(), color.alpha()); CTrackViewSequence* sequence = m_colorUpdateTrack->GetSequence(); if (nullptr != sequence) { CTrackViewSequenceNotificationContext context(sequence); if (addToUndo) { AzToolsFramework::ScopedUndoBatch undoBatch("Set Key"); UpdateColorKeyHelper(colArray); undoBatch.MarkEntityDirty(sequence->GetSequenceComponentEntityId()); } else { UpdateColorKeyHelper(colArray); } // We want this to take affect now if (!addToUndo) { GetIEditor()->GetAnimation()->ForceAnimation(); } } } void CTrackViewDopeSheetBase::UpdateColorKeyHelper(const ColorF& color) { const unsigned int numChildNodes = m_colorUpdateTrack->GetChildCount(); for (int subTrackIndex = 0; subTrackIndex < numChildNodes; ++subTrackIndex) { CTrackViewTrack* subTrack = static_cast(m_colorUpdateTrack->GetChild(subTrackIndex)); CTrackViewKeyHandle subTrackKey = subTrack->GetKeyByTime(m_colorUpdateKeyTime); I2DBezierKey bezierKey; if (subTrackKey.IsValid()) { subTrackKey.GetKey(&bezierKey); } else { // no valid key found at this time - create Key subTrackKey = subTrack->CreateKey(m_colorUpdateKeyTime); subTrackKey.GetKey(&bezierKey); } bezierKey.value.x = m_colorUpdateKeyTime; bezierKey.value.y = color[subTrackIndex]; subTrackKey.SetKey(&bezierKey); } } void CTrackViewDopeSheetBase::EditSelectedColorKey(CTrackViewTrack* pTrack) { if (pTrack->IsCompoundTrack()) { CTrackViewKeyBundle selectedKeyBundle = pTrack->GetSelectedKeys(); if (selectedKeyBundle.GetKeyCount()) { m_colorUpdateTrack = pTrack; // init with the first selected key color m_colorUpdateKeyTime = selectedKeyBundle.GetKey(0).GetTime(); Vec3 color; pTrack->GetValue(m_colorUpdateKeyTime, color); const AZ::Color defaultColor(clamp_tpl(FloatToIntRet(color.x), 0, 255), clamp_tpl(FloatToIntRet(color.y), 0, 255), clamp_tpl(FloatToIntRet(color.z), 0, 255), 255); AzQtComponents::ColorPicker picker(AzQtComponents::ColorPicker::Configuration::RGB); picker.setWindowTitle(tr("Select Color")); picker.setCurrentColor(defaultColor); picker.setSelectedColor(defaultColor); QObject::connect(&picker, &AzQtComponents::ColorPicker::currentColorChanged, this, &CTrackViewDopeSheetBase::OnCurrentColorChange); if (picker.exec() == QDialog::Accepted) { const AZ::Color col = picker.currentColor(); // Moved bulk of method into helper to handle matching logic in QT callback and undo redo cases UpdateColorKey(AzQtComponents::toQColor(col), true); } else { // We canceled out of the color picker, revert to color held before opening it UpdateColorKey(AzQtComponents::toQColor(defaultColor), false); } } } } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::AcceptUndo() { if (CUndo::IsRecording()) { const QPoint mousePos = mapFromGlobal(QCursor::pos()); CTrackViewSequence* sequence = GetIEditor()->GetAnimation()->GetSequence(); if (m_mouseMode == eTVMouseMode_Paste) { GetIEditor()->CancelUndo(); } else if (m_mouseMode == eTVMouseMode_Move || m_mouseMode == eTVMouseMode_Clone) { if (sequence && m_bKeysMoved) { GetIEditor()->CancelUndo(); // Keys Moved, mark the sequence dirty to get an AZ undo event. AzToolsFramework::ScopedUndoBatch undoBatch("Move/Clone Keys"); undoBatch.MarkEntityDirty(sequence->GetSequenceComponentEntityId()); } else { GetIEditor()->CancelUndo(); } } else if (m_mouseMode == eTVMouseMode_StartTimeAdjust || m_mouseMode == eTVMouseMode_EndTimeAdjust) { if (sequence) { GetIEditor()->CancelUndo(); AzToolsFramework::ScopedUndoBatch undoBatch("Adjust Start/End Time of an Animation Key"); undoBatch.MarkEntityDirty(sequence->GetSequenceComponentEntityId()); } else { GetIEditor()->CancelUndo(); } } } m_mouseMode = eTVMouseMode_None; m_trackMementos.clear(); } ////////////////////////////////////////////////////////////////////////// float CTrackViewDopeSheetBase::ComputeSnappedMoveOffset() { // Compute time offset const QPoint currentMousePos(qBound(m_rcClient.left(), m_mouseOverPos.x(), m_rcClient.right()), m_mouseOverPos.y()); float time0 = TimeFromPointUnsnapped(m_mouseDownPos); float time = TimeFromPointUnsnapped(currentMousePos); if (GetKeyModifiedSnappingMode() == eSnappingMode_SnapTick) { time0 = TickSnap(time0); time = TickSnap(time); } return time - time0; } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::AddKeys(const QPoint& point, const bool bTryAddKeysInGroup) { CTrackViewSequence* sequence = GetIEditor()->GetAnimation()->GetSequence(); if (!sequence) { return; } // Add keys here. CTrackViewTrack* pTrack = GetTrackFromPoint(point); if (!pTrack) { return; } CTrackViewSequenceNotificationContext context(sequence); CTrackViewAnimNode* pNode = pTrack->GetAnimNode(); float keyTime = TimeFromPoint(point); bool inRange = m_timeRange.IsInside(keyTime); if (pTrack && inRange) { bool keyCreated = false; if (bTryAddKeysInGroup && pNode->GetParentNode()) // Add keys in group { CTrackViewTrackBundle tracksInGroup = pNode->GetTracksByParam(pTrack->GetParameterType()); for (int i = 0; i < (int)tracksInGroup.GetCount(); ++i) { CTrackViewTrack* pCurrTrack = tracksInGroup.GetTrack(i); if (pCurrTrack->GetChildCount() == 0) // A simple track { if (IsOkToAddKeyHere(pCurrTrack, keyTime)) { AzToolsFramework::ScopedUndoBatch undoBatch("Create Key"); pCurrTrack->CreateKey(keyTime); undoBatch.MarkEntityDirty(sequence->GetSequenceComponentEntityId()); keyCreated = true; } } else // A compound track { for (int k = 0; k < pCurrTrack->GetChildCount(); ++k) { CTrackViewTrack* pSubTrack = static_cast(pCurrTrack->GetChild(k)); if (IsOkToAddKeyHere(pSubTrack, keyTime)) { AzToolsFramework::ScopedUndoBatch undoBatch("Create Key"); pSubTrack->CreateKey(keyTime); undoBatch.MarkEntityDirty(sequence->GetSequenceComponentEntityId()); keyCreated = true; } } } } } else if (pTrack->GetChildCount() == 0) // A simple track { if (IsOkToAddKeyHere(pTrack, keyTime)) { AzToolsFramework::ScopedUndoBatch undoBatch("Create Key"); pTrack->CreateKey(keyTime); undoBatch.MarkEntityDirty(sequence->GetSequenceComponentEntityId()); keyCreated = true; } } else // A compound track { if (pTrack->GetValueType() == AnimValueType::RGB) { keyCreated = CreateColorKey(pTrack, keyTime); } else { AzToolsFramework::ScopedUndoBatch undoBatch("Create Key"); for (int i = 0; i < pTrack->GetChildCount(); ++i) { CTrackViewTrack* pSubTrack = static_cast(pTrack->GetChild(i)); if (IsOkToAddKeyHere(pSubTrack, keyTime)) { pSubTrack->CreateKey(keyTime); keyCreated = true; } } undoBatch.MarkEntityDirty(sequence->GetSequenceComponentEntityId()); } } } } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::DrawControl(QPainter* painter, const QRect& rcUpdate) { CTrackViewSequence* pSequence = GetIEditor()->GetAnimation()->GetSequence(); DrawNodesRecursive(pSequence, painter, rcUpdate); DrawSummary(painter, rcUpdate); DrawSelectedKeyIndicators(painter); if (m_mouseMode == eTVMouseMode_Paste) { // If in paste mode draw keys that are in clipboard DrawClipboardKeys(painter, QRect()); } } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::DrawNodesRecursive(CTrackViewNode* pNode, QPainter* painter, const QRect& rcUpdate) { const QRect rect = GetNodeRect(pNode); if (!rect.isEmpty()) { switch (pNode->GetNodeType()) { case eTVNT_AnimNode: DrawNodeTrack(static_cast(pNode), painter, rect); break; case eTVNT_Track: DrawTrack(static_cast(pNode), painter, rect); break; } } if (pNode->GetExpanded()) { unsigned int numChildren = pNode->GetChildCount(); for (unsigned int i = 0; i < numChildren; ++i) { DrawNodesRecursive(pNode->GetChild(i), painter, rcUpdate); } } } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::DrawTicks(QPainter* painter, const QRect& rc, Range& timeRange) { // Draw time ticks every tick step seconds. const QPen dkgray(QColor(90, 90, 90)); const QPen ltgray(QColor(120, 120, 120)); const QPen prevPen = painter->pen(); painter->setPen(dkgray); Range VisRange = GetVisibleRange(); int nNumberTicks = 10; if (GetTickDisplayMode() == eTVTickMode_InFrames) { nNumberTicks = 8; } double start = TickSnap(timeRange.start); double step = 1.0 / m_ticksStep; for (double t = 0.0f; t <= timeRange.end + step; t += step) { double st = TickSnap(t); if (st > timeRange.end) { st = timeRange.end; } if (st < VisRange.start) { continue; } if (st > VisRange.end) { break; } int x = TimeToClient(st); if (x < 0) { continue; } int k = RoundFloatToInt(st * m_ticksStep); if (k % nNumberTicks == 0) { if (st >= start) { painter->setPen(Qt::black); } else { painter->setPen(dkgray); } painter->drawLine(x, rc.bottom() - 1, x, rc.bottom() - 5); painter->setPen(dkgray); } else { if (st >= start) { painter->setPen(dkgray); } else { painter->setPen(ltgray); } painter->drawLine(x, rc.bottom() - 1, x, rc.bottom() - 3); } } painter->setPen(prevPen); } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::DrawTrack(CTrackViewTrack* pTrack, QPainter* painter, const QRect& trackRect) { CTrackViewSequence* pSequence = GetIEditor()->GetAnimation()->GetSequence(); const QPen prevPen = painter->pen(); painter->setPen(QColor(120, 120, 120)); painter->drawLine(trackRect.bottomLeft(), trackRect.bottomRight()); painter->setPen(prevPen); QRect rcInner = trackRect; rcInner.setLeft(max(trackRect.left(), m_leftOffset - m_scrollOffset.x())); rcInner.setRight(min(trackRect.right(), (m_scrollMax + m_scrollMin) - m_scrollOffset.x() + m_leftOffset * 2)); bool bLightAnimationSetActive = pSequence->GetFlags() & IAnimSequence::eSeqFlags_LightAnimationSet; if (bLightAnimationSetActive && pTrack->GetKeyCount() > 0) { // In the case of the light animation set, the time of of the last key // determines the end of the track. float lastKeyTime = pTrack->GetKey(pTrack->GetKeyCount() - 1).GetTime(); rcInner.setRight(min(rcInner.right(), TimeToClient(lastKeyTime))); } QRect rcInnerDraw(QPoint(rcInner.left() - 6, rcInner.top()), QPoint(rcInner.right() + 6, rcInner.bottom())); QColor trackColor = CTVCustomizeTrackColorsDlg::GetTrackColor(pTrack->GetParameterType()); if (pTrack->HasCustomColor()) { ColorB customColor = pTrack->GetCustomColor(); trackColor = QColor(customColor.r, customColor.g, customColor.b); } // For the case of tracks belonging to an inactive director node, // changes the track color to a custom one. const QColor colorForDisabled = CTVCustomizeTrackColorsDlg::GetColorForDisabledTracks(); const QColor colorForMuted = CTVCustomizeTrackColorsDlg::GetColorForMutedTracks(); CTrackViewAnimNode* pDirectorNode = pTrack->GetDirector(); if (!pDirectorNode->IsActiveDirector()) { trackColor = colorForDisabled; } // A disabled/muted track or any track in a disabled node also uses a custom color. CTrackViewAnimNode* animNode = pTrack->GetAnimNode(); bool bTrackDisabled = pTrack->GetFlags() & IAnimTrack::eAnimTrackFlags_Disabled; bool bTrackMuted = pTrack->GetFlags() & IAnimTrack::eAnimTrackFlags_Muted; bool bTrackInvalid = !pTrack->IsSubTrack() && !animNode->IsParamValid(pTrack->GetParameterType()); bool bTrackInDisabledNode = animNode->AreFlagsSetOnNodeOrAnyParent(eAnimNodeFlags_Disabled); if (bTrackDisabled || bTrackInDisabledNode || bTrackInvalid) { trackColor = colorForDisabled; } else if (bTrackMuted) { trackColor = colorForMuted; } const QRect rc = rcInnerDraw.adjusted(0, 1, 0, 0); const EAnimCurveType trackType = pTrack->GetCurveType(); if (trackType == eAnimCurveType_TCBFloat || trackType == eAnimCurveType_TCBQuat || trackType == eAnimCurveType_TCBVector) { trackColor = QColor(245, 80, 70); } if (pTrack->IsSelected()) { QLinearGradient gradient(rc.topLeft(), rc.bottomLeft()); gradient.setColorAt(0, trackColor); gradient.setColorAt(1, QColor(trackColor.red() / 2, trackColor.green() / 2, trackColor.blue() / 2)); painter->fillRect(rc, gradient); } else if (pTrack->GetValueType() == AnimValueType::RGB && pTrack->GetKeyCount() > 0) { DrawColorGradient(painter, rc, pTrack); } else { painter->fillRect(rc, trackColor); } // Left outside QRect rcOutside = trackRect; rcOutside.setRight(rcInnerDraw.left() - 1); rcOutside.adjust(1, 1, -1, 0); QLinearGradient gradient(rcOutside.topLeft(), rcOutside.bottomLeft()); gradient.setColorAt(0, QColor(210, 210, 210)); gradient.setColorAt(1, QColor(180, 180, 180)); painter->fillRect(rcOutside, gradient); // Right outside. rcOutside = trackRect; rcOutside.setLeft(rcInnerDraw.right() + 1); rcOutside.adjust(1, 1, -1, 0); gradient = QLinearGradient(rcOutside.topLeft(), rcOutside.bottomLeft()); gradient.setColorAt(0, QColor(210, 210, 210)); gradient.setColorAt(1, QColor(180, 180, 180)); painter->fillRect(rcOutside, gradient); // Get time range of update rectangle. Range timeRange = GetTimeRange(trackRect); // Draw tick marks in time range. DrawTicks(painter, rcInner, timeRange); // Draw special track features AnimValueType trackValueType = pTrack->GetValueType(); CAnimParamType trackParamType = pTrack->GetParameterType(); if (trackValueType == AnimValueType::Bool) { // If this track is bool Track draw bars where track is true DrawBoolTrack(timeRange, painter, pTrack, rc); } else if (trackValueType == AnimValueType::Select) { // If this track is Select Track draw bars to show where selection is active. DrawSelectTrack(timeRange, painter, pTrack, rc); } else if (trackParamType == AnimParamType::Sequence) { // If this track is Sequence Track draw bars to show where sequence is active. DrawSequenceTrack(timeRange, painter, pTrack, rc); } else if (trackParamType == AnimParamType::Goto) { // if this track is GoTo Track, draw an arrow to indicate jump position. DrawGoToTrackArrow(pTrack, painter, rc); } // Draw keys in time range. DrawKeys(pTrack, painter, rcInner, timeRange); } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::DrawSelectTrack(const Range& timeRange, QPainter* painter, CTrackViewTrack* pTrack, const QRect& rc) { int x0 = TimeToClient(timeRange.start); float t0 = timeRange.start; const QBrush prevBrush = painter->brush(); painter->setBrush(m_selectTrackBrush); const int numKeys = pTrack->GetKeyCount(); for (int i = 0; i < numKeys; ++i) { const CTrackViewKeyHandle& keyHandle = pTrack->GetKey(i); ISelectKey selectKey; keyHandle.GetKey(&selectKey); if (!selectKey.szSelection.empty() || selectKey.cameraAzEntityId.IsValid()) { float time = keyHandle.GetTime(); float nextTime = timeRange.end; if (i < numKeys - 1) { nextTime = pTrack->GetKey(i + 1).GetTime(); } time = clamp_tpl(time, timeRange.start, timeRange.end); nextTime = clamp_tpl(nextTime, timeRange.start, timeRange.end); int x0 = TimeToClient(time); float fBlendTime = selectKey.fBlendTime; int blendTimeEnd = 0; if (fBlendTime > 0.0f && fBlendTime < (nextTime - time)) { blendTimeEnd = TimeToClient(nextTime); nextTime -= fBlendTime; } int x = TimeToClient(nextTime); if (x != x0) { QLinearGradient gradient(x0, rc.top() + 1, x0, rc.bottom()); gradient.setColorAt(0, Qt::white); gradient.setColorAt(1, QColor(100, 190, 255)); painter->fillRect(QRect(QPoint(x0, rc.top() + 1), QPoint(x, rc.bottom())), gradient); } if (fBlendTime > 0.0f) { QLinearGradient gradient(x, rc.top() + 1, x, rc.bottom()); gradient.setColorAt(0, Qt::white); gradient.setColorAt(1, QColor(0, 115, 230)); painter->fillRect(QRect(QPoint(x, rc.top() + 1), QPoint(blendTimeEnd, rc.bottom())), gradient); } } } painter->setBrush(prevBrush); } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::DrawBoolTrack(const Range& timeRange, QPainter* painter, CTrackViewTrack* pTrack, const QRect& rc) { int x0 = TimeToClient(timeRange.start); float t0 = timeRange.start; QRect trackRect; const QBrush prevBrush = painter->brush(); painter->setBrush(m_visibilityBrush); const int numKeys = pTrack->GetKeyCount(); for (int i = 0; i < numKeys; ++i) { const CTrackViewKeyHandle& keyHandle = pTrack->GetKey(i); const float time = keyHandle.GetTime(); if (time < timeRange.start) { continue; } if (time > timeRange.end) { break; } int x = TimeToClient(time); bool val = false; pTrack->GetValue(time - 0.001f, val); if (val) { QLinearGradient gradient(x0, rc.top() + 4, x0, rc.bottom() - 4); gradient.setColorAt(0, QColor(250, 250, 250)); gradient.setColorAt(1, QColor(0, 80, 255)); painter->fillRect(QRect(QPoint(x0, rc.top() + 4), QPoint(x, rc.bottom() - 4)), gradient); } t0 = time; x0 = x; } int x = TimeToClient(timeRange.end); bool val = false; pTrack->GetValue(timeRange.end - 0.001f, val); if (val) { QLinearGradient gradient(x0, rc.top() + 4, x0, rc.bottom() - 4); gradient.setColorAt(0, QColor(250, 250, 250)); gradient.setColorAt(1, QColor(0, 80, 255)); painter->fillRect(QRect(QPoint(x0, rc.top() + 4), QPoint(x, rc.bottom() - 4)), gradient); } painter->setBrush(prevBrush); } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::DrawSequenceTrack(const Range& timeRange, QPainter* painter, CTrackViewTrack* pTrack, const QRect& rc) { int x0 = TimeToClient(timeRange.start); float t0 = timeRange.start; const QBrush prevBrush = painter->brush(); painter->setBrush(m_selectTrackBrush); const int numKeys = pTrack->GetKeyCount(); for (int i = 0; i < numKeys - 1; ++i) { const CTrackViewKeyHandle& keyHandle = pTrack->GetKey(i); ISequenceKey sequenceKey; keyHandle.GetKey(&sequenceKey); if (sequenceKey.sequenceEntityId.IsValid()) { float time = keyHandle.GetTime(); float nextTime = timeRange.end; if (i < numKeys - 1) { nextTime = pTrack->GetKey(i + 1).GetTime(); } time = clamp_tpl(time, timeRange.start, timeRange.end); nextTime = clamp_tpl(nextTime, timeRange.start, timeRange.end); int x0 = TimeToClient(time); int x = TimeToClient(nextTime); if (x != x0) { const QColor startColour(100, 190, 255); const QColor endColour(250, 250, 250); QLinearGradient gradient(x0, rc.top() + 1, x0, rc.bottom()); gradient.setColorAt(0, startColour); gradient.setColorAt(1, endColour); painter->fillRect(QRect(QPoint(x0, rc.top() + 1), QPoint(x, rc.bottom())), gradient); } } } painter->setBrush(prevBrush); } ////////////////////////////////////////////////////////////////////////// bool CTrackViewDopeSheetBase::CompareKeyHandleByTime(const CTrackViewKeyHandle &a, const CTrackViewKeyHandle &b) { return a.GetTime() < b.GetTime(); } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::DrawKeys(CTrackViewTrack* pTrack, QPainter* painter, QRect& rect, Range& timeRange) { int numKeys = pTrack->GetKeyCount(); const QFont prevFont = painter->font(); painter->setFont(m_descriptionFont); painter->setPen(KEY_TEXT_COLOR); int prevKeyPixel = -10000; const int kDefaultWidthForDescription = 200; const int kSmallMargin = 10; FixedDynArray drawnKeyTimes; drawnKeyTimes.set(ArrayT((float*)alloca(numKeys * sizeof(float)), numKeys)); AZStd::vector sortedKeys; sortedKeys.reserve(numKeys); for (int i = 0; i < numKeys; ++i) { sortedKeys.push_back(pTrack->GetKey(i)); } AZStd::sort(sortedKeys.begin(), sortedKeys.end(), CompareKeyHandleByTime); // Draw keys. for (int i = 0; i < numKeys; ++i) { CTrackViewKeyHandle keyHandle = sortedKeys[i]; const float time = keyHandle.GetTime(); if (!stl::push_back_unique(drawnKeyTimes, time)) { continue; } int x = TimeToClient(time); if (x - kSmallMargin > rect.right()) { continue; } int x1 = x + kDefaultWidthForDescription; CTrackViewKeyHandle nextKey = keyHandle.GetNextKey(); int nextKeyIndex = i + 1; // Skip over next keys that have the same time as the current key. // If they have the same time it means they are keys from sub tracks // in a compound track at the same time. while (nextKeyIndex < numKeys && AZ::IsClose(sortedKeys[nextKeyIndex].GetTime(), time, AZ::g_fltEps)) { nextKeyIndex++; } if (nextKeyIndex < numKeys) { CTrackViewKeyHandle nextKey = sortedKeys[nextKeyIndex]; x1 = TimeToClient(nextKey.GetTime()) - kSmallMargin; } if (x1 > x + kSmallMargin) // Enough space for description text or duration bar { // Get info about that key. const char* pDescription = keyHandle.GetDescription(); const float duration = keyHandle.GetDuration(); int xlast = x; if (duration > 0) { xlast = TimeToClient(time + duration); } if (xlast + kSmallMargin < rect.left()) { continue; } if (duration > 0) { DrawKeyDuration(pTrack, painter, rect, i); } if (pDescription && pDescription[0] != 0) { char keydesc[1024]; bool bSelectedAndBeingMoved = m_mouseMode == eTVMouseMode_Move && keyHandle.IsSelected(); if (bSelectedAndBeingMoved) { // Show its time or frame number additionally. if (GetTickDisplayMode() == eTVTickMode_InSeconds) { sprintf_s(keydesc, "%.3f, {", time); } else { sprintf_s(keydesc, "%d, {", ftoi(time / m_snapFrameTime)); } } else { cry_strcpy(keydesc, "{"); } cry_strcat(keydesc, pDescription); cry_strcat(keydesc, "}"); // Draw key description text. // Find next key. const QRect textRect(QPoint(x + 10, rect.top()), QPoint(x1, rect.bottom())); painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter | Qt::TextSingleLine, painter->fontMetrics().elidedText(keydesc, Qt::ElideRight, textRect.width())); } } if (x < 0) { continue; } if (pTrack->GetChildCount() == 0 // At compound tracks, keys are all green. && abs(x - prevKeyPixel) < 2) { // If multiple keys on the same time. painter->drawPixmap(QPoint(x - 6, rect.top() + 2), QPixmap(":/Trackview/trackview_keys_02.png")); } else { if (keyHandle.IsSelected()) { painter->drawPixmap(QPoint(x - 6, rect.top() + 2), QPixmap(":/Trackview/trackview_keys_01.png")); } else { painter->drawPixmap(QPoint(x - 6, rect.top() + 2), QPixmap(":/Trackview/trackview_keys_00.png")); } } prevKeyPixel = x; } painter->setFont(prevFont); } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::DrawClipboardKeys(QPainter* painter, const QRect& rc) { CTrackViewSequence* pSequence = GetIEditor()->GetAnimation()->GetSequence(); const float timeOffset = ComputeSnappedMoveOffset(); // Get node & track under cursor CTrackViewAnimNode* animNode = GetAnimNodeFromPoint(m_mouseOverPos); CTrackViewTrack* pTrack = GetTrackFromPoint(m_mouseOverPos); auto matchedLocations = pSequence->GetMatchedPasteLocations(m_clipboardKeys, animNode, pTrack); for (size_t i = 0; i < matchedLocations.size(); ++i) { auto& matchedLocation = matchedLocations[i]; CTrackViewTrack* pMatchedTrack = matchedLocation.first; XmlNodeRef trackNode = matchedLocation.second; if (pMatchedTrack->IsCompoundTrack()) { // Both child counts should be the same, but make sure const unsigned int numSubTrack = std::min(pMatchedTrack->GetChildCount(), (unsigned int)trackNode->getChildCount()); for (unsigned int subTrackIndex = 0; subTrackIndex < numSubTrack; ++subTrackIndex) { CTrackViewTrack* pSubTrack = static_cast(pMatchedTrack->GetChild(subTrackIndex)); XmlNodeRef subTrackNode = trackNode->getChild(subTrackIndex); DrawTrackClipboardKeys(painter, pSubTrack, subTrackNode, timeOffset); // Also draw to parent track. This is intentional DrawTrackClipboardKeys(painter, pMatchedTrack, subTrackNode, timeOffset); } } else { DrawTrackClipboardKeys(painter, pMatchedTrack, trackNode, timeOffset); } } } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::DrawTrackClipboardKeys(QPainter* painter, CTrackViewTrack* pTrack, XmlNodeRef trackNode, const float timeOffset) { const QPen prevPen = painter->pen(); painter->setPen(Qt::green); const QRect trackRect = GetNodeRect(pTrack); const int numKeysToPaste = trackNode->getChildCount(); for (int i = 0; i < numKeysToPaste; ++i) { XmlNodeRef keyNode = trackNode->getChild(i); float time; if (keyNode->getAttr("time", time)) { int x = TimeToClient(time + timeOffset); painter->drawPixmap(QPoint(x - 6, trackRect.top() + 2), QPixmap(":/Trackview/trackview_keys_03.png")); painter->drawLine(x, m_rcClient.top(), x, m_rcClient.bottom()); } } painter->setPen(prevPen); } ////////////////////////////////////////////////////////////////////////// CTrackViewKeyHandle CTrackViewDopeSheetBase::FirstKeyFromPoint(const QPoint& point) { CTrackViewTrack* pTrack = GetTrackFromPoint(point); if (!pTrack) { return CTrackViewKeyHandle(); } float t1 = TimeFromPointUnsnapped(QPoint(point.x() - 4, point.y())); float t2 = TimeFromPointUnsnapped(QPoint(point.x() + 4, point.y())); int numKeys = pTrack->GetKeyCount(); for (int i = 0; i < numKeys; ++i) { const CTrackViewKeyHandle& keyHandle = pTrack->GetKey(i); float time = keyHandle.GetTime(); if (time >= t1 && time <= t2) { return keyHandle; } } return CTrackViewKeyHandle(); } ////////////////////////////////////////////////////////////////////////// CTrackViewKeyHandle CTrackViewDopeSheetBase::DurationKeyFromPoint(const QPoint& point) { CTrackViewTrack* pTrack = GetTrackFromPoint(point); if (!pTrack) { return CTrackViewKeyHandle(); } float t = TimeFromPointUnsnapped(point); int numKeys = pTrack->GetKeyCount(); // Iterate in a reverse order to prioritize later nodes. for (int i = numKeys - 1; i >= 0; --i) { const CTrackViewKeyHandle& keyHandle = pTrack->GetKey(i); const float time = keyHandle.GetTime(); const float duration = keyHandle.GetDuration(); if (t >= time && t <= time + duration) { return keyHandle; } } return CTrackViewKeyHandle(); } ////////////////////////////////////////////////////////////////////////// CTrackViewKeyHandle CTrackViewDopeSheetBase::CheckCursorOnStartEndTimeAdjustBar(const QPoint& point, bool& bStart) { CTrackViewTrack* pTrack = GetTrackFromPoint(point); if (!pTrack || (pTrack->GetParameterType() != AnimParamType::Animation && pTrack->GetParameterType() != AnimParamType::TimeRanges && pTrack->GetValueType() != AnimValueType::CharacterAnim && pTrack->GetValueType() != AnimValueType::AssetBlend)) { return CTrackViewKeyHandle(); } int numKeys = pTrack->GetKeyCount(); for (int i = 0; i < numKeys; ++i) { const CTrackViewKeyHandle& keyHandle = pTrack->GetKey(i); if (!keyHandle.IsSelected()) { continue; } const float time = keyHandle.GetTime(); const float duration = keyHandle.GetDuration(); if (duration == 0) { continue; } // TODO: Refactor this Time Range Key stuff. ICharacterKey characterKey; AZ::IAssetBlendKey assetBlendKey; ITimeRangeKey* timeRangeKey = nullptr; if (pTrack->GetValueType() == AnimValueType::AssetBlend) { keyHandle.GetKey(&assetBlendKey); timeRangeKey = &assetBlendKey; } else { // This will work for both character & time range keys because // ICharacterKey is derived from ITimeRangeKey. Not the most beautiful code. keyHandle.GetKey(&characterKey); timeRangeKey = &characterKey; } int stime = TimeToClient(time); int etime = TimeToClient(time + AZStd::min(timeRangeKey->GetValidEndTime(), timeRangeKey->m_duration)); if (point.x() >= stime - 3 && point.x() <= stime) { bStart = true; return keyHandle; } else if (point.x() >= etime && point.x() <= etime + 3) { bStart = false; return keyHandle; } } return CTrackViewKeyHandle(); } ////////////////////////////////////////////////////////////////////////// int CTrackViewDopeSheetBase::NumKeysFromPoint(const QPoint& point) { CTrackViewTrack* pTrack = GetTrackFromPoint(point); if (!pTrack) { return -1; } float t1 = TimeFromPointUnsnapped(QPoint(point.x() - 4, point.y())); float t2 = TimeFromPointUnsnapped(QPoint(point.x() + 4, point.y())); int count = 0; int numKeys = pTrack->GetKeyCount(); for (int i = 0; i < numKeys; ++i) { const CTrackViewKeyHandle& keyHandle = pTrack->GetKey(i); const float time = keyHandle.GetTime(); if (time >= t1 && time <= t2) { ++count; } } return count; } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::SelectKeys(const QRect& rc, const bool bMultiSelection) { CTrackViewSequence* sequence = GetIEditor()->GetAnimation()->GetSequence(); AZ_Assert(sequence != nullptr, "sequence should never be nullptr here"); if (!sequence) { return; } AzToolsFramework::ScopedUndoBatch undoBatch("Select Keys"); const std::vector beforeKeyState = sequence->SaveKeyStates(); CTrackViewSequenceNotificationContext context(sequence); if (!bMultiSelection) { sequence->DeselectAllKeys(); } // put selection rectangle from client to track space. const QRect rci = rc.translated(m_scrollOffset); Range selTime = GetTimeRange(rci); CTrackViewTrackBundle tracks = sequence->GetAllTracks(); // note the tracks to select for the keyHandles selected CTrackViewTrackBundle tracksToSelect; for (int i = 0; i < tracks.GetCount(); ++i) { CTrackViewTrack* pTrack = tracks.GetTrack(i); QRect trackRect = GetNodeRect(pTrack); // Decrease item rectangle a bit. trackRect.adjust(4, 4, -4, -4); // Check if item rectangle intersects with selection rectangle in y axis. if ((trackRect.top() >= rc.top() && trackRect.top() <= rc.bottom()) || (trackRect.bottom() >= rc.top() && trackRect.bottom() <= rc.bottom()) || (rc.top() >= trackRect.top() && rc.top() <= trackRect.bottom()) || (rc.bottom() >= trackRect.top() && rc.bottom() <= trackRect.bottom())) { // Check which keys we intersect. for (int j = 0; j < pTrack->GetKeyCount(); j++) { CTrackViewKeyHandle keyHandle = pTrack->GetKey(j); const float time = keyHandle.GetTime(); if (selTime.IsInside(time)) { keyHandle.Select(true); tracksToSelect.AppendTrack(pTrack); } } } } ChangeSequenceTrackSelection(sequence, tracksToSelect, bMultiSelection); const std::vector afterKeyState = sequence->SaveKeyStates(); if (beforeKeyState != afterKeyState) { undoBatch.MarkEntityDirty(sequence->GetSequenceComponentEntityId()); } } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::SetTickDisplayMode(ETVTickMode mode) { m_tickDisplayMode = mode; SetTimeScale(GetTimeScale(), 0); // for refresh } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::SetSnapFPS(UINT fps) { m_snapFrameTime = (fps == 0) ? 0.033333f : (1.0f / float(fps)); } ////////////////////////////////////////////////////////////////////////// ESnappingMode CTrackViewDopeSheetBase::GetKeyModifiedSnappingMode() { ESnappingMode snappingMode = m_snappingMode; if (qApp->keyboardModifiers() & Qt::ControlModifier) { snappingMode = eSnappingMode_SnapNone; } else if (qApp->keyboardModifiers() & Qt::ShiftModifier) { snappingMode = eSnappingMode_SnapMagnet; } else if (qApp->keyboardModifiers() & Qt::AltModifier) { snappingMode = eSnappingMode_SnapFrame; } return snappingMode; } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::DrawSelectedKeyIndicators(QPainter* painter) { CTrackViewSequence* pSequence = GetIEditor()->GetAnimation()->GetSequence(); const QPen prevPen = painter->pen(); painter->setPen(Qt::green); CTrackViewKeyBundle keys = pSequence->GetSelectedKeys(); for (int i = 0; i < keys.GetKeyCount(); ++i) { const CTrackViewKeyHandle& keyHandle = keys.GetKey(i); int x = TimeToClient(keyHandle.GetTime()); painter->drawLine(x, m_rcClient.top(), x, m_rcClient.bottom()); } painter->setPen(prevPen); } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::ComputeFrameSteps(const Range& visRange) { float fNbFrames = fabsf ((visRange.end - visRange.start) / m_snapFrameTime); float afStepTable [4] = { 1.0f, 0.5f, 0.2f, 0.1f }; bool bDone = false; float fFact = 1.0f; unsigned int nStepIdx = 0; for (unsigned int nAttempts = 0; nAttempts < 10 && !bDone; ++nAttempts) { bool bLess = true; for (nStepIdx = 0; nStepIdx < 4; ++nStepIdx) { float fFactNbFrames = fNbFrames / (afStepTable[nStepIdx] * fFact); if (fFactNbFrames >= 3 && fFactNbFrames <= 9) { bDone = true; break; } else { bLess = (fFactNbFrames < 3); } } if (!bDone) { fFact *= (bLess) ? 0.1f : 10.0f; } } float nBIntermediateTicks = 5; m_fFrameLabelStep = fFact * afStepTable[nStepIdx]; if (TimeToClient(m_fFrameLabelStep) - TimeToClient(0) > 1300) { nBIntermediateTicks = 10; } m_fFrameTickStep = m_fFrameLabelStep * double (m_snapFrameTime) / double(nBIntermediateTicks); } void CTrackViewDopeSheetBase::DrawTimeLineInFrames(QPainter* painter, const QRect& rc, const QColor& lineCol, const QColor& textCol, double step) { float fFramesPerSec = 1.0f / m_snapFrameTime; float fInvFrameLabelStep = 1.0f / m_fFrameLabelStep; Range VisRange = GetVisibleRange(); const Range& timeRange = m_timeRange; const QPen ltgray(QColor(90, 90, 90)); const QPen black(textCol); for (double t = TickSnap(timeRange.start); t <= timeRange.end + m_fFrameTickStep; t += m_fFrameTickStep) { double st = t; if (st > timeRange.end) { st = timeRange.end; } if (st < VisRange.start) { continue; } if (st > VisRange.end) { break; } if (st < m_timeRange.start || st > m_timeRange.end) { continue; } const int x = TimeToClient(st); float fFrame = st * fFramesPerSec; float fFrameScaled = fFrame * fInvFrameLabelStep; if (fabsf(fFrameScaled - RoundFloatToInt(fFrameScaled)) < 0.001f) { painter->setPen(black); painter->drawLine(x, rc.bottom() - 2, x, rc.bottom() - 14); painter->drawText(x + 2, rc.top(), QString::number(fFrame)); painter->setPen(ltgray); } else { painter->drawLine(x, rc.bottom() - 2, x, rc.bottom() - 6); } } } void CTrackViewDopeSheetBase::DrawTimeLineInSeconds(QPainter* painter, const QRect& rc, const QColor& lineCol, const QColor& textCol, double step) { Range VisRange = GetVisibleRange(); const Range& timeRange = m_timeRange; int nNumberTicks = 10; const QPen ltgray(QColor(90, 90, 90)); const QPen black(textCol); for (double t = TickSnap(timeRange.start); t <= timeRange.end + step; t += step) { double st = TickSnap(t); if (st > timeRange.end) { st = timeRange.end; } if (st < VisRange.start) { continue; } if (st > VisRange.end) { break; } if (st < m_timeRange.start || st > m_timeRange.end) { continue; } int x = TimeToClient(st); int k = RoundFloatToInt(st * m_ticksStep); if (k % nNumberTicks == 0) { painter->setPen(black); painter->drawLine(x, rc.bottom() - 2, x, rc.bottom() - 14); painter->drawText(x + 2, rc.top(), QString::number(st)); painter->setPen(ltgray); } else { painter->drawLine(x, rc.bottom() - 2, x, rc.bottom() - 6); } } } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::DrawTimeline(QPainter* painter, const QRect& rcUpdate) { bool recording = GetIEditor()->GetAnimation()->IsRecording(); QColor lineCol(255, 0, 255); const QColor textCol(Qt::black); const QColor dkgrayCol(90, 90, 90); const QColor ltgrayCol(150, 150, 150); if (recording) { lineCol = Qt::red; } // Draw vertical line showing current time. { int x = TimeToClient(m_currentTime); if (x > m_rcClient.left() && x < m_rcClient.right()) { const QPen prevPen = painter->pen(); painter->setPen(lineCol); painter->drawLine(x, 0, x, m_rcClient.bottom()); painter->setPen(prevPen); } } const QRect rc = m_rcTimeline; if (!rc.intersects(rcUpdate)) { return; } QLinearGradient gradient(rc.topLeft(), rc.bottomLeft()); gradient.setColorAt(0, QColor(250, 250, 250)); gradient.setColorAt(1, QColor(180, 180, 180)); painter->fillRect(rc, gradient); const QPen prevPen = painter->pen(); const QPen dkgray(dkgrayCol); const QPen ltgray(ltgrayCol); const QPen black(textCol); const QPen redpen(lineCol); // Draw time ticks every tick step seconds. const Range& timeRange = m_timeRange; painter->setPen(dkgray); double step = 1.0 / double(m_ticksStep); if (GetTickDisplayMode() == eTVTickMode_InFrames) { DrawTimeLineInFrames(painter, rc, lineCol, textCol, step); } else if (GetTickDisplayMode() == eTVTickMode_InSeconds) { DrawTimeLineInSeconds(painter, rc, lineCol, textCol, step); } else { assert (0); } // Draw time markers. int x; x = TimeToClient(m_timeMarked.start); painter->drawPixmap(QPoint(x, m_rcTimeline.bottom() - 9), QPixmap(":/Trackview/marker/bmp00016_01.png")); x = TimeToClient(m_timeMarked.end); painter->drawPixmap(QPoint(x - 7, m_rcTimeline.bottom() - 9), QPixmap(":/Trackview/marker/bmp00016_00.png")); painter->setPen(redpen); x = TimeToClient(m_currentTime); painter->setBrush(Qt::NoBrush); painter->drawRect(QRect(QPoint(x - 3, rc.top()), QPoint(x + 3, rc.bottom()))); painter->setPen(redpen); painter->drawLine(x, rc.top(), x, rc.bottom()); painter->setPen(prevPen); } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::DrawSummary(QPainter* painter, const QRect& rcUpdate) { CTrackViewSequence* pSequence = GetIEditor()->GetAnimation()->GetSequence(); const QColor lineCol = Qt::black; const QColor fillCol(150, 100, 220); const QRect rc = m_rcSummary; if (!rc.intersects(rcUpdate)) { return; } painter->fillRect(rc, fillCol); const QPen prevPen = painter->pen(); painter->setPen(QPen(lineCol, 3)); Range timeRange = m_timeRange; // Draw a short thick line at each place where there is a key in any tracks. CTrackViewKeyBundle keys = pSequence->GetAllKeys(); for (int i = 0; i < keys.GetKeyCount(); ++i) { const CTrackViewKeyHandle& keyHandle = keys.GetKey(i); int x = TimeToClient(keyHandle.GetTime()); painter->drawLine(x, rc.bottom() - 2, x, rc.top() + 2); } painter->setPen(prevPen); } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::DrawNodeTrack(CTrackViewAnimNode* animNode, QPainter* painter, const QRect& trackRect) { const QFont prevFont = painter->font(); painter->setFont(m_descriptionFont); CTrackViewAnimNode* pDirectorNode = animNode->GetDirector(); if (pDirectorNode->GetNodeType() != eTVNT_Sequence && !pDirectorNode->IsActiveDirector()) { painter->setPen(INACTIVE_TEXT_COLOR); } else { painter->setPen(KEY_TEXT_COLOR); } const QRect textRect = trackRect.adjusted(4, 0, -4, 0); QString sAnimNodeName = animNode->GetName(); const bool hasObsoleteTrack = animNode->HasObsoleteTrack(); if (hasObsoleteTrack) { painter->setPen(QColor(245, 80, 70)); sAnimNodeName += tr(": Some of the sub-tracks contains obsoleted TCB splines (marked in red), thus cannot be copied or pasted."); } painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter | Qt::TextSingleLine, painter->fontMetrics().elidedText(sAnimNodeName, Qt::ElideRight, textRect.width())); painter->setFont(prevFont); } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::DrawGoToTrackArrow(CTrackViewTrack* pTrack, QPainter* painter, const QRect& rc) { int numKeys = pTrack->GetKeyCount(); const QColor colorLine(150, 150, 150); const QColor colorHeader(50, 50, 50); const int tickness = 2; const int halfMargin = (rc.height() - tickness) / 2; for (int i = 0; i < numKeys; ++i) { const CTrackViewKeyHandle& keyHandle = pTrack->GetKey(i); IDiscreteFloatKey discreteFloatKey; keyHandle.GetKey(&discreteFloatKey); int arrowStart = TimeToClient(discreteFloatKey.time); int arrowEnd = TimeToClient(discreteFloatKey.m_fValue); if (discreteFloatKey.m_fValue < 0.f) { continue; } // draw arrow body line if (arrowStart < arrowEnd) { painter->fillRect(QRect(QPoint(arrowStart, rc.top() + halfMargin), QPoint(arrowEnd, rc.bottom() - halfMargin)), colorLine); } else if (arrowStart > arrowEnd) { painter->fillRect(QRect(QPoint(arrowEnd, rc.top() + halfMargin), QPoint(arrowStart, rc.bottom() - halfMargin)), colorLine); } // draw arrow head if (arrowStart != arrowEnd) { painter->fillRect(QRect(QPoint(arrowEnd, rc.top() + 2), QPoint(arrowEnd + 1, rc.bottom() - 2)), colorHeader); } } } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::DrawKeyDuration(CTrackViewTrack* pTrack, QPainter* painter, const QRect& rc, int keyIndex) { const CTrackViewKeyHandle& keyHandle = pTrack->GetKey(keyIndex); const float time = keyHandle.GetTime(); const float duration = keyHandle.GetDuration(); int x = TimeToClient(time); // Draw key duration. float endt = min(time + duration, m_timeRange.end); int x1 = TimeToClient(endt); if (x1 < 0) { if (x > 0) { x1 = rc.right(); } } const QBrush prevBrush = painter->brush(); painter->setBrush(m_visibilityBrush); QColor colorFrom(120, 120, 255); if (pTrack->GetParameterType() == AnimParamType::Sound) // If it is a sound key { ISoundKey soundKey; keyHandle.GetKey(&soundKey); colorFrom.setRgbF(soundKey.customColor.x, soundKey.customColor.y, soundKey.customColor.z); } QLinearGradient gradient(x, rc.top() + 3, x, rc.bottom() - 3); gradient.setColorAt(0, colorFrom); gradient.setColorAt(1, QColor(250, 250, 250)); const int width = x1 + 1 - x; painter->fillRect(QRect(x, rc.top() + 3, width, rc.height() - 3), gradient); painter->setBrush(prevBrush); painter->drawLine(x1, rc.top(), x1, rc.bottom()); bool typeHasAnimBox = pTrack->GetParameterType() == AnimParamType::Animation || pTrack->GetParameterType() == AnimParamType::TimeRanges || pTrack->GetValueType() == AnimValueType::CharacterAnim || pTrack->GetValueType() == AnimValueType::AssetBlend; // If it is a selected animation track, draw the whole animation box (in green) // and two adjust bars (in red) for start/end time each, too. if (keyHandle.IsSelected() && typeHasAnimBox) { // Draw the whole animation box. // TODO: Refactor this Time Range Key stuff. ICharacterKey characterKey; AZ::IAssetBlendKey assetBlendKey; ITimeRangeKey* timeRangeKey = nullptr; if (pTrack->GetValueType() == AnimValueType::AssetBlend) { keyHandle.GetKey(&assetBlendKey); timeRangeKey = &assetBlendKey; } else { // This will work for both character & time range keys because // ICharacterKey is derived from ITimeRangeKey. Not the most beautiful code. keyHandle.GetKey(&characterKey); timeRangeKey = &characterKey; } int startX = TimeToClient(time - timeRangeKey->m_startTime / timeRangeKey->m_speed); int endX = TimeToClient(time + (timeRangeKey->m_duration - timeRangeKey->m_startTime) / timeRangeKey->m_speed); const QPen prevPen = painter->pen(); painter->setPen(Qt::green); painter->drawLine(startX, rc.top(), endX, rc.top()); painter->drawLine(endX, rc.top(), endX, rc.bottom()); painter->drawLine(endX, rc.bottom(), startX, rc.bottom()); painter->drawLine(startX, rc.bottom(), startX, rc.top()); // Draw two adjust bars. int durationX = TimeToClient(time + AZStd::min(timeRangeKey->GetValidEndTime(), timeRangeKey->m_duration)); painter->setPen(QPen(Qt::red, 3)); painter->drawLine(x - 2, rc.top(), x - 2, rc.bottom()); painter->drawLine(durationX + 2, rc.top(), durationX + 2, rc.bottom()); painter->setPen(prevPen); } } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::DrawColorGradient(QPainter* painter, const QRect& rc, const CTrackViewTrack* pTrack) { const QPen pOldPen = painter->pen(); for (int x = rc.left(); x < rc.right(); ++x) { // This is really slow. Is there a better way? Vec3 vColor(0, 0, 0); pTrack->GetValue(TimeFromPointUnsnapped(QPoint(x, rc.top())), vColor); painter->setPen(ColorLinearToGamma(vColor / 255.0f)); painter->drawLine(x, rc.top(), x, rc.bottom()); } painter->setPen(pOldPen); } ////////////////////////////////////////////////////////////////////////// QRect CTrackViewDopeSheetBase::GetNodeRect(const CTrackViewNode* pNode) const { CTrackViewNodesCtrl::CRecord* pRecord = m_pNodesCtrl->GetNodeRecord(pNode); if (pRecord && pRecord->IsVisible()) { QRect recordRect = pRecord->GetRect(); return QRect(0, recordRect.top(), m_rcClient.width(), recordRect.height()); } return QRect(); } ////////////////////////////////////////////////////////////////////////// void CTrackViewDopeSheetBase::StoreMementoForTracksWithSelectedKeys() { CTrackViewSequence* pSequence = GetIEditor()->GetAnimation()->GetSequence(); CTrackViewKeyBundle selectedKeys = pSequence->GetSelectedKeys(); m_trackMementos.clear(); // Construct the set of tracks that have selected keys std::set tracks; const unsigned int numKeys = selectedKeys.GetKeyCount(); for (int keyIndex = 0; keyIndex < numKeys; ++keyIndex) { CTrackViewKeyHandle keyHandle = selectedKeys.GetKey(keyIndex); tracks.insert(keyHandle.GetTrack()); } // For each of those tracks store an undo object for (auto iter = tracks.begin(); iter != tracks.end(); ++iter) { CTrackViewTrack* pTrack = *iter; TrackMemento trackMemento; trackMemento.m_memento = pTrack->GetMemento(); const unsigned int numKeys = pTrack->GetKeyCount(); for (unsigned int i = 0; i < numKeys; ++i) { trackMemento.m_keySelectionStates.push_back(pTrack->GetKey(i).IsSelected()); } m_trackMementos[pTrack] = trackMemento; } }