#include "timeline_view.h" #include "item_delegate.h" #include #include #include #include #include #include #include #include #include #include #include #include namespace LtsTimeline { const int TimelineView::s_textHeight = 20; TimelineView::TimelineView(QWidget *parent) : QTreeView(parent) , m_header(new TimelineHeaderView(this)) , m_dragHandler(new TimelineViewDragHandler(this)) , m_resizeHandler(new TimelineViewResizeHandler(this)) , m_selectHandler(new TimelineViewSelectHandler(this)) , m_trackHandler(new TimelineViewTrackHandler(this)) , m_logger(NGL::NGLLoggerBootstrap::getNGLRootLogger().getNGLLogger("Timeline.TimelineView")) { setAlternatingRowColors(true); setEditTriggers(QAbstractItemView::NoEditTriggers); setSelectionBehavior(QAbstractItemView::SelectItems); setSelectionMode(QAbstractItemView::NoSelection); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); setHeader(m_header); setAcceptDrops(true); } void TimelineView::setHeader(QHeaderView *header) { try { if (m_header && m_header != header) delete m_header; m_header = qobject_cast(header); QTreeView::setHeader(m_header); connect(m_header, &TimelineHeaderView::indicatorTimeChanged, this, [this]() { viewport()->update(); }); connect(m_header, &TimelineHeaderView::indicatorVisibilityChanged, this, [this]() { viewport()->update(); }); connect(m_header, &TimelineHeaderView::hoverPositionChanged, this, [this]() { if (m_model != nullptr) emit m_model->layoutChanged(); }); connect(m_header, &TimelineHeaderView::hoverVisibilityChanged, this, [this]() { viewport()->update(); }); } catch (...) { ERROR_LOG(m_logger) << "header view should be of type TimelineHeaderView"; } } void TimelineView::setModel(QAbstractItemModel *model) { m_model = model; QTreeView::setModel(model); setMouseTracking(true); m_timelineModel = qobject_cast(model); if (m_timelineModel != nullptr) return; m_proxyModel = qobject_cast(model); if (m_proxyModel != nullptr) m_timelineModel = qobject_cast(m_proxyModel->sourceModel()); if (m_timelineModel == nullptr) qWarning() << "TimelineSelectHandler:c_tor: model is not a QAbstractItemModel!"; Q_ASSERT(m_timelineModel != nullptr); } void TimelineView::setProxyModel(QAbstractProxyModel *proxy) noexcept(false) { if (!qobject_cast(proxy->sourceModel())) throw QStringLiteral("source model cannot be cast to TimelineModel"); m_model = qobject_cast(proxy->sourceModel()); m_proxyModel = proxy; QTreeView::setModel(m_proxyModel); } void TimelineView::setItemDelegate(QAbstractItemDelegate *delegate) { try { m_delegate = static_cast(delegate); QTreeView::setItemDelegate(m_delegate); } catch (...) { ERROR_LOG(m_logger) << "item delegate for TimelineView must be derived from ItemDelegate"; } } void TimelineView::setZoomFactor(ZoomFactor zoomFactor) { if (zoomFactor == m_zoomFactor) return; if (m_minZoomFactor > zoomFactor) { setZoomFactor(m_minZoomFactor); return; } if (m_upperZoomFactor < zoomFactor) { setZoomFactor(m_upperZoomFactor); return; } m_zoomFactor = zoomFactor; emit zoomChanged(m_zoomFactor); if (m_currentTime + m_zoomFactor > endTime()) setCurrentTime(endTime() - m_zoomFactor); else viewport()->update(); } void TimelineView::setMinimumZoomFactor(ZoomFactor zoomFactor) { if (zoomFactor == m_minZoomFactor) return; m_minZoomFactor = zoomFactor; if (m_zoomFactor < m_minZoomFactor) setZoomFactor(m_minZoomFactor); } void TimelineView::setMaximumZoomFactor(ZoomFactor zoomFactor) { if (zoomFactor == m_maxZoomFactor) return; m_maxZoomFactor = zoomFactor; if (m_upperZoomFactor > m_maxZoomFactor) m_upperZoomFactor = m_maxZoomFactor; if (m_zoomFactor > m_maxZoomFactor) setZoomFactor(m_maxZoomFactor); } void TimelineView::setStartTime(qint64 time) { if (time == m_startTime || time < 0) return; m_startTime = time; if (m_currentTime < m_startTime) setCurrentTime(m_startTime); if (m_duration > 0) if (m_currentTime > endTime() - m_zoomFactor) setCurrentTime(std::max(endTime() - m_zoomFactor, m_startTime)); emit rangeChanged(m_startTime, endTime()); } void TimelineView::setCurrentTime(qint64 time) { if (time == m_currentTime) return; if (time < m_startTime) time = m_startTime; if (m_duration > 0) if (time > endTime() - m_zoomFactor) time = std::max(endTime() - m_zoomFactor, m_startTime); if (time == m_currentTime) return; m_currentTime = time; emit currentTimeChanged(m_currentTime); viewport()->update(); } void TimelineView::setEndTime(qint64 time) { if (time == m_startTime + m_duration) return; if (time - m_startTime < m_minZoomFactor) time = m_startTime + m_minZoomFactor; setDuration(time - m_startTime); } void TimelineView::setDuration(int duration) { if (duration == m_duration) return; if (duration <= m_minZoomFactor) duration = m_minZoomFactor; m_duration = duration; emit rangeChanged(m_startTime, endTime()); ZoomFactor upperZoomFactor = Zoom::floor(duration); m_upperZoomFactor = std::min(upperZoomFactor, m_maxZoomFactor); if (duration < m_zoomFactor) setZoomFactor(m_upperZoomFactor); if (m_currentTime > endTime()) setCurrentTime(endTime() - m_zoomFactor); viewport()->update(); } void TimelineView::setRange(qint64 start, qint64 end) { if (start == m_startTime && end - start == m_duration) return; if (start > end) { m_startTime = end; m_duration = std::max(start - m_startTime, static_cast(m_minZoomFactor)); } else { m_startTime = start; m_duration = std::max(end - m_startTime, static_cast(m_minZoomFactor)); } if (m_currentTime < m_startTime) m_currentTime = m_startTime; if (m_duration > 0) if (m_currentTime > endTime() - m_zoomFactor) m_currentTime = std::max(endTime() - m_zoomFactor, m_startTime); ZoomFactor upperZoomFactor = Zoom::floor(m_duration); m_upperZoomFactor = upperZoomFactor > m_maxZoomFactor ? m_maxZoomFactor : upperZoomFactor; emit rangeChanged(m_startTime, endTime()); viewport()->update(); } void TimelineView::setCursorTextVisible(bool visible) { m_isCursorTextVisible = visible; viewport()->update(); } void TimelineView::setCursorText(const QString &text) { m_cursorText = text; viewport()->update(); } int TimelineView::toPixel(qint64 time) const { return m_header ? m_header->toPixel(time) : -1; } qint64 TimelineView::toTime(int pixel) const { return m_header ? m_header->toTime(pixel) : -1; } qint64 TimelineView::toDuration(int pixel) const { if (!m_header) return -1; int pos = m_header->sectionPosition(Section::eItems); return double(pixel - pos) / (geometry().width() - pos) * m_zoomFactor; } int TimelineView::fromDuration(qint64 time) const { if (!m_header) return -1; return (time - m_startTime) / m_zoomFactor * (geometry().width() - m_header->sectionPosition(Section::eItems)); } void TimelineView::clearSelectedItems() { m_selectedItemIds.clear(); emit selectedItemsChanged(m_selectedItemIds); viewport()->update(); } void TimelineView::selectAllItems() { if (m_model == nullptr) return; m_selectedItemIds.clear(); m_selectedItemIds = m_timelineModel->itemIds(); emit selectedItemsChanged(m_selectedItemIds); viewport()->update(); } void TimelineView::selectItem(int itemId, bool reset) { if (reset) m_selectedItemIds.clear(); else if (m_selectedItemIds.contains(itemId)) return; m_selectedItemIds.append(itemId); emit selectedItemsChanged(m_selectedItemIds); viewport()->update(); } void TimelineView::selectItems(const QVector &itemIds, bool reset) { if (reset) m_selectedItemIds.clear(); for (const int &itemId : qAsConst(itemIds)) if (!m_selectedItemIds.contains(itemId)) m_selectedItemIds.append(itemId); emit selectedItemsChanged(m_selectedItemIds); viewport()->update(); } void TimelineView::unselectItem(int itemId) { const int iItem = m_selectedItemIds.indexOf(itemId); if (iItem == -1) return; m_selectedItemIds.remove(iItem); emit selectedItemsChanged(m_selectedItemIds); viewport()->update(); } bool TimelineView::isCursorWithinRangeX(int x) const { if (m_header) return m_header->sectionPosition(Section::eItems) < x && x < geometry().right(); return false; } bool TimelineView::isCursorWithinRangeY(int y) const { return 0 < y && y < geometry().bottom(); } void TimelineView::dragEnterEvent(QDragEnterEvent *event) { if (m_dragHandler) m_dragHandler->dragEnterEvent(event); } void TimelineView::dragMoveEvent(QDragMoveEvent *event) { if (m_dragHandler) m_dragHandler->dragMoveEvent(event); } void TimelineView::dragLeaveEvent(QDragLeaveEvent *event) { if (m_dragHandler) m_dragHandler->dragLeaveEvent(event); } void TimelineView::dropEvent(QDropEvent *event) { if (m_dragHandler) m_dragHandler->dropEvent(event); } void TimelineView::mouseDoubleClickEvent(QMouseEvent *event) { if (m_trackHandler) m_trackHandler->mouseDoubleClickEvent(event); if (m_selectHandler) m_selectHandler->mouseDoubleClickEvent(event); } void TimelineView::mouseMoveEvent(QMouseEvent *event) { m_cursorPosition = event->pos(); const QModelIndex index(indexAt(event->pos())); if (m_header) { m_header->setHoverPosition(isCursorWithinRange(event->pos()) ? event->pos().x() : -1); if (m_header->mouseHandler()->isBusy()) { m_header->mouseHandler()->mouseMoveEvent(event); return; } if (cursor() == Qt::SizeHorCursor) { if (m_resizeHandler) { if (!m_resizeHandler->isActive()) setCursor(Qt::ArrowCursor); } else setCursor(Qt::ArrowCursor); } } if (!m_model) return; if (m_dragHandler && m_timelineModel->isEditable()) if (cursor().shape() == Qt::ClosedHandCursor || cursor().shape() == Qt::ForbiddenCursor) if (!m_dragHandler->m_dragItemIds.isEmpty() || !m_selectedItemIds.isEmpty()) { m_dragHandler->mouseMoveEvent(event); return; } if (m_resizeHandler && m_timelineModel->isEditable()) if (cursor().shape() == Qt::SizeHorCursor) if (m_resizeHandler->isActive()) { m_resizeHandler->mouseMoveEvent(event); return; } if (m_selectHandler) m_selectHandler->mouseMoveEvent(event); if (!index.isValid() || index.column() == Section::eActuator) { if (m_resizeHandler && m_timelineModel->isEditable()) if (!m_resizeHandler->isActive()) setCursor(QCursor(Qt::ArrowCursor)); event->ignore(); return; } if (!m_timelineModel->isEditable()) return; try { Track &track = m_timelineModel->track(index.data().toInt()); Item &item = track.itemAt(toTime(event->pos().x())); int startPos = toPixel(item.startTime()); int endPos = toPixel(item.startTime() + item.duration()); if (endPos - startPos < 15 || (startPos + 5 < event->pos().x() && event->pos().x() < endPos - 5)) setCursor(QCursor(Qt::ArrowCursor)); else setCursor(QCursor(Qt::SizeHorCursor)); } catch (...) { setCursor(QCursor(Qt::ArrowCursor)); } } void TimelineView::mousePressEvent(QMouseEvent *event) { if (!m_model) return; if (m_header) if (m_header->mouseHandler()->isBusy()) { m_header->mouseHandler()->mousePressEvent(event); return; } const QModelIndex index = indexAt(event->pos()); if (index.column() == Section::eActuator) { QTreeView::mousePressEvent(event); if (m_trackHandler) m_trackHandler->mousePressEvent(event); return; } if (m_timelineModel->isEditable() && cursor().shape() == Qt::SizeHorCursor) { if (m_resizeHandler) m_resizeHandler->mousePressEvent(event); } else { if (m_selectHandler) m_selectHandler->mousePressEvent(event); if (m_dragHandler && m_timelineModel->isEditable()) { m_dragHandler->mousePressEvent(event); if (m_dragHandler->isActive()) return; } if (event->button() == Qt::RightButton) emit customContextMenuRequested(event->pos()); } } void TimelineView::mouseReleaseEvent(QMouseEvent *event) { if (!m_model) return; if (m_header) if (m_header->mouseHandler()->isBusy()) { m_header->mouseHandler()->mouseReleaseEvent(event); return; } if (m_trackHandler) m_trackHandler->mouseReleaseEvent(event); if (m_dragHandler && m_timelineModel->isEditable()) if (m_dragHandler->isActive()) { m_dragHandler->mouseReleaseEvent(event); viewport()->update(); return; } if (m_resizeHandler && m_timelineModel->isEditable()) if (m_resizeHandler->isActive()) { m_resizeHandler->mouseReleaseEvent(event); viewport()->update(); return; } if (m_selectHandler) if (m_selectHandler->isActive() || !m_timelineModel->isEditable()) m_selectHandler->mouseReleaseEvent(event); viewport()->update(); } void TimelineView::keyPressEvent(QKeyEvent *event) { if (m_model == nullptr) return; if (m_timelineModel->isEditable()) { if (m_resizeHandler) if (m_resizeHandler->isActive()) return; if (m_dragHandler) m_dragHandler->keyPressEvent(event); } if (m_selectHandler) m_selectHandler->keyPressEvent(event); viewport()->update(); } void TimelineView::wheelEvent(QWheelEvent *event) { if (event->pos().y() <= m_header->geometry().bottom()) m_header->wheelEvent(event); else if (QGuiApplication::queryKeyboardModifiers() == Qt::ControlModifier) { if (event->angleDelta().y() < 0) setZoomFactor(Zoom::increment(m_zoomFactor)); else setZoomFactor(Zoom::decrement(m_zoomFactor)); } else QTreeView::wheelEvent(event); } void TimelineView::paintEvent(QPaintEvent *event) { Q_UNUSED(event); QWidget *viewPort = viewport(); QPainter painter(viewPort); painter.save(); QTreeView::paintEvent(event); if (m_dragHandler) { ItemDelegate *delegate = qobject_cast(itemDelegate()); if (delegate) delegate->paintDragItems(&painter); } painter.restore(); painter.save(); painter.setClipping(true); if (m_selectHandler) if (m_selectHandler->isRubberBandActive()) { QColor color = TimelineHeaderView::highlightColor(); painter.setPen(color); color.setAlpha(100); painter.setBrush(QBrush(color)); painter.drawRect(m_selectHandler->m_rubberBand); } if (m_header->isIndicatorVisible() && m_header->indicatorPosition() != -1 && m_header->indicatorPosition() >= m_header->sectionPosition(Section::eItems)) { painter.setPen(TimelineHeaderView::highlightColor()); painter.drawLine(m_header->indicatorPosition(), 0, m_header->indicatorPosition(), geometry().bottom()); } if (m_header->isHoverVisible() && m_header->hoverPosition() != -1) { painter.setPen(TimelineHeaderView::highlightColor()); painter.drawLine(m_header->hoverPosition(), 0, m_header->hoverPosition(), geometry().bottom()); if (m_isCursorTextVisible && !m_cursorText.isEmpty()) { painter.setBrush(QBrush(TimelineHeaderView::backgroundColor())); QRect rect = QRect( m_cursorPosition.x(), m_cursorPosition.y() - s_textHeight - 2, painter.fontMetrics().horizontalAdvance(m_cursorText) + 2, s_textHeight + 2 ); painter.drawRect(rect); painter.drawText(rect.adjusted(1, 1, -1, -1), Qt::AlignCenter, m_cursorText); } } if (m_header->isLiveIndicatorVisible() && m_header->liveIndicatorTime() != -1 && m_currentTime <= m_header->liveIndicatorTime() && m_header->liveIndicatorTime() <= m_currentTime + m_zoomFactor) { painter.setPen(m_header->liveIndicatorColor()); painter.drawLine(m_header->liveIndicatorPosition(), 0, m_header->liveIndicatorPosition(), geometry().bottom()); } painter.restore(); } }