/*** Olive - Non-Linear Video Editor Copyright (C) 2021 Olive Team This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . ***/ #include "nodeviewitem.h" #include #include #include #include #include #include "common/flipmodifiers.h" #include "common/qtutils.h" #include "config/config.h" #include "core.h" #include "nodeview.h" #include "nodeviewscene.h" #include "nodeviewundo.h" #include "ui/colorcoding.h" #include "ui/icons/icons.h" #include "window/mainwindow/mainwindow.h" namespace olive { NodeViewItem::NodeViewItem(QGraphicsItem *parent) : QGraphicsRectItem(parent), node_(nullptr), expanded_(false), hide_titlebar_(false), highlighted_index_(-1), flow_dir_(NodeViewCommon::kLeftToRight), prevent_removing_(false) { // Set flags for this widget setFlag(QGraphicsItem::ItemIsMovable); setFlag(QGraphicsItem::ItemIsSelectable); setFlag(QGraphicsItem::ItemSendsGeometryChanges); // // We use font metrics to set all the UI measurements for DPI-awareness // // Set border width node_border_width_ = DefaultItemBorder(); int widget_width = DefaultItemWidth(); int widget_height = DefaultItemHeight(); title_bar_rect_ = QRectF(-widget_width/2, -widget_height/2, widget_width, widget_height); setRect(title_bar_rect_); output_triangle_.resize(3); } QPointF NodeViewItem::GetNodePosition() const { return ScreenToNodePoint(pos(), flow_dir_); } void NodeViewItem::SetNodePosition(const QPointF &pos) { cached_node_pos_ = pos; UpdateNodePosition(); } int NodeViewItem::DefaultTextPadding() { return QFontMetrics(QFont()).height() / 4; } int NodeViewItem::DefaultItemHeight() { return QFontMetrics(QFont()).height() + DefaultTextPadding() * 2; } int NodeViewItem::DefaultItemWidth() { return QtUtils::QFontMetricsWidth(QFontMetrics(QFont()), "HHHHHHHHHHHHHHHH");; } int NodeViewItem::DefaultItemBorder() { return QFontMetrics(QFont()).height() / 12; } QPointF NodeViewItem::NodeToScreenPoint(QPointF p, NodeViewCommon::FlowDirection direction) { switch (direction) { case NodeViewCommon::kLeftToRight: // NodeGraphs are always left-to-right internally, no need to translate break; case NodeViewCommon::kRightToLeft: // Invert X value p.setX(-p.x()); break; case NodeViewCommon::kTopToBottom: // Swap X/Y p = QPointF(p.y(), p.x()); break; case NodeViewCommon::kBottomToTop: // Swap X/Y and invert Y p = QPointF(p.y(), -p.x()); break; } // Multiply by item sizes for this direction p.setX(p.x() * DefaultItemHorizontalPadding(direction)); p.setY(p.y() * DefaultItemVerticalPadding(direction)); return p; } QPointF NodeViewItem::ScreenToNodePoint(QPointF p, NodeViewCommon::FlowDirection direction) { // Divide by item sizes for this direction p.setX(p.x() / DefaultItemHorizontalPadding(direction)); p.setY(p.y() / DefaultItemVerticalPadding(direction)); switch (direction) { case NodeViewCommon::kLeftToRight: // NodeGraphs are always left-to-right internally, no need to translate break; case NodeViewCommon::kRightToLeft: // Invert X value p.setX(-p.x()); break; case NodeViewCommon::kTopToBottom: // Swap X/Y p = QPointF(p.y(), p.x()); break; case NodeViewCommon::kBottomToTop: // Swap X/Y and invert Y p = QPointF(-p.y(), p.x()); break; } return p; } qreal NodeViewItem::DefaultItemHorizontalPadding(NodeViewCommon::FlowDirection dir) { if (NodeViewCommon::GetFlowOrientation(dir) == Qt::Horizontal) { return DefaultItemWidth() * 1.5; } else { return DefaultItemWidth() * 1.25; } } qreal NodeViewItem::DefaultItemVerticalPadding(NodeViewCommon::FlowDirection dir) { if (NodeViewCommon::GetFlowOrientation(dir) == Qt::Horizontal) { return DefaultItemHeight() * 1.5; } else { return DefaultItemHeight() * 2.0; } } qreal NodeViewItem::DefaultItemHorizontalPadding() const { return DefaultItemHorizontalPadding(flow_dir_); } qreal NodeViewItem::DefaultItemVerticalPadding() const { return DefaultItemVerticalPadding(flow_dir_); } void NodeViewItem::AddEdge(NodeViewEdge *edge) { edges_.append(edge); } void NodeViewItem::RemoveEdge(NodeViewEdge *edge) { edges_.removeOne(edge); } int NodeViewItem::GetIndexAt(QPointF pt) const { pt -= pos(); for (int i=0; iRetranslate(); foreach (const QString& input, node_->inputs()) { if (node_->IsInputConnectable(input)) { node_inputs_.append(input); } } } update(); } void NodeViewItem::SetExpanded(bool e, bool hide_titlebar) { if (node_inputs_.isEmpty() || (expanded_ == e && hide_titlebar_ == hide_titlebar)) { return; } expanded_ = e; hide_titlebar_ = hide_titlebar; if (expanded_ && !node_inputs_.isEmpty()) { // Create new rect QRectF new_rect = title_bar_rect_; if (hide_titlebar_) { new_rect.setHeight(new_rect.height() * node_inputs_.size()); } else { new_rect.setHeight(new_rect.height() * (node_inputs_.size() + 1)); } setRect(new_rect); } else { setRect(title_bar_rect_); } update(); ReadjustAllEdges(); } void NodeViewItem::ToggleExpanded() { SetExpanded(!IsExpanded()); } void NodeViewItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *) { // Use main window palette since the palette passed in `widget` is the NodeView palette which // has been slightly modified QPalette app_pal = Core::instance()->main_window()->palette(); // Draw background rect if expanded if (IsExpanded()) { painter->setPen(Qt::NoPen); painter->setBrush(app_pal.color(QPalette::Window)); painter->drawRect(rect()); painter->setPen(app_pal.color(QPalette::Text)); for (int i=0;ifillRect(input_rect, highlight_col); } painter->drawText(input_rect, Qt::AlignCenter, node_->GetInputName(node_inputs_.at(i))); } } // Draw the titlebar if (!hide_titlebar_ && node_) { painter->setPen(Qt::black); painter->setBrush(node_->brush(title_bar_rect_.top(), title_bar_rect_.bottom())); painter->drawRect(title_bar_rect_); painter->setPen(app_pal.color(QPalette::Text)); QString node_label = node_->GetLabel(); QString node_shortname = node_->ShortName(); if (node_label.isEmpty()) { // If this is a track and has no user-supplied label, generate an automatic one Track* track_cast_test = dynamic_cast(node_); if (track_cast_test) { node_label = Track::GetDefaultTrackName(track_cast_test->type(), track_cast_test->Index()); } } int icon_size = painter->fontMetrics().height()/2; if (node_label.isEmpty()) { // Draw shortname only DrawNodeTitle(painter, node_shortname, title_bar_rect_, Qt::AlignVCenter, icon_size, true); } else { int text_pad = DefaultTextPadding()/2; QRectF safe_label_bounds = title_bar_rect_.adjusted(text_pad, text_pad, -text_pad, -text_pad); QFont f; qreal font_sz = f.pointSizeF(); f.setPointSizeF(font_sz * 0.8); painter->setFont(f); DrawNodeTitle(painter, node_label, safe_label_bounds, Qt::AlignTop, icon_size, true); f.setPointSizeF(font_sz * 0.6); painter->setFont(f); DrawNodeTitle(painter, node_shortname, safe_label_bounds, Qt::AlignBottom, icon_size, false); } } // Draw final border QPen border_pen; border_pen.setWidth(node_border_width_); if (option->state & QStyle::State_Selected) { border_pen.setColor(app_pal.color(QPalette::Highlight)); } else { border_pen.setColor(Qt::black); } painter->setPen(border_pen); painter->setBrush(Qt::NoBrush); painter->drawRect(rect()); // Draw output triangle painter->setPen(Qt::NoPen); painter->setBrush(app_pal.color(QPalette::Text)); int triangle_sz = title_bar_rect_.height() / 2; int triangle_sz_half = triangle_sz / 2; switch (flow_dir_) { case NodeViewCommon::kLeftToRight: // Triangle pointing right output_triangle_[0] = QPointF(rect().right(), rect().center().y() - triangle_sz_half); output_triangle_[1] = QPointF(rect().right() + triangle_sz_half, rect().center().y()); output_triangle_[2] = QPointF(rect().right(), rect().center().y() + triangle_sz_half); break; case NodeViewCommon::kTopToBottom: // Triangle pointing down output_triangle_[0] = QPointF(rect().center().x() - triangle_sz_half, rect().bottom()); output_triangle_[1] = QPointF(rect().center().x(), rect().bottom() + triangle_sz_half); output_triangle_[2] = QPointF(rect().center().x() + triangle_sz_half, rect().bottom()); break; case NodeViewCommon::kBottomToTop: // Triangle pointing up output_triangle_[0] = QPointF(rect().center().x() - triangle_sz_half, rect().top()); output_triangle_[1] = QPointF(rect().center().x(), rect().top() - triangle_sz_half); output_triangle_[2] = QPointF(rect().center().x() + triangle_sz_half, rect().top()); break; case NodeViewCommon::kRightToLeft: // Triangle pointing left output_triangle_[0] = QPointF(rect().left(), rect().center().y() - triangle_sz_half); output_triangle_[1] = QPointF(rect().left() - triangle_sz_half, rect().center().y()); output_triangle_[2] = QPointF(rect().left(), rect().center().y() + triangle_sz_half); break; } painter->drawPolygon(output_triangle_); } void NodeViewItem::mousePressEvent(QGraphicsSceneMouseEvent *event) { event->setModifiers(FlipControlAndShiftModifiers(event->modifiers())); QGraphicsRectItem::mousePressEvent(event); } void NodeViewItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { event->setModifiers(FlipControlAndShiftModifiers(event->modifiers())); QGraphicsRectItem::mouseMoveEvent(event); } void NodeViewItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { event->setModifiers(FlipControlAndShiftModifiers(event->modifiers())); QGraphicsRectItem::mouseReleaseEvent(event); } void NodeViewItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) { QGraphicsRectItem::mouseDoubleClickEvent(event); if (!(event->modifiers() & Qt::ControlModifier)) { SetExpanded(!IsExpanded()); } } QVariant NodeViewItem::itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value) { if (change == ItemPositionHasChanged && node_) { ReadjustAllEdges(); } return QGraphicsItem::itemChange(change, value); } void NodeViewItem::ReadjustAllEdges() { foreach (NodeViewEdge* edge, edges_) { edge->Adjust(); } } void NodeViewItem::DrawNodeTitle(QPainter* painter, QString text, const QRectF& rect, Qt::Alignment vertical_align, int icon_size, bool draw_arrow) { QFontMetrics fm = painter->fontMetrics(); painter->setRenderHint(QPainter::SmoothPixmapTransform); // Draw right or down arrow based on expanded state int icon_padding = title_bar_rect_.height() / 2 - icon_size / 2; int icon_full_size = icon_size + icon_padding * 2; if (draw_arrow) { const QIcon& expand_icon = IsExpanded() ? icon::TriDown : icon::TriRight; int icon_size_scaled = icon_size * painter->transform().m11(); painter->drawPixmap(QRect(title_bar_rect_.x() + icon_padding, title_bar_rect_.y() + icon_padding, icon_size, icon_size), expand_icon.pixmap(QSize(icon_size_scaled, icon_size_scaled))); } // Calculate how much space we have for text int item_width = title_bar_rect_.width(); int max_text_width = item_width - DefaultTextPadding() * 2 - icon_full_size; int label_width = QtUtils::QFontMetricsWidth(fm, text); // Concatenate text if necessary (adds a "..." to the end and removes characters until the // string fits in the bounds) if (label_width > max_text_width) { QString concatenated; do { text.chop(1); concatenated = QCoreApplication::translate("NodeViewItem", "%1...").arg(text); } while ((label_width = QtUtils::QFontMetricsWidth(fm, concatenated)) > max_text_width); text = concatenated; } // Determine the text color (automatically calculate from node background color) painter->setPen(ColorCoding::GetUISelectorColor(node_->color())); // Determine X position (favors horizontal centering unless it'll overrun the arrow) QRectF text_rect = rect; Qt::Alignment text_align = Qt::AlignHCenter | vertical_align; int likely_x = item_width / 2 - label_width / 2; if (likely_x < icon_full_size) { text_rect.adjust(icon_full_size, 0, 0, 0); text_align = Qt::AlignLeft | vertical_align; } // Draw the text in a rect (the rect is sized around text already in the constructor) painter->drawText(text_rect, text_align, text); } void NodeViewItem::SetHighlightedIndex(int index) { if (highlighted_index_ == index) { return; } highlighted_index_ = index; update(); } QRectF NodeViewItem::GetInputRect(int index) const { QRectF r = title_bar_rect_; if (!hide_titlebar_) { index++; } if (IsExpanded()) { r.translate(0, r.height() * index); } return r; } QPointF NodeViewItem::GetInputPoint(const QString &input, int element, const QPointF& source_pos) const { return pos() + GetInputPointInternal(node_inputs_.indexOf(input), source_pos); } QPointF NodeViewItem::GetOutputPoint(const QString& output) const { switch (flow_dir_) { case NodeViewCommon::kLeftToRight: default: return pos() + QPointF(rect().right(), rect().center().y()); case NodeViewCommon::kRightToLeft: return pos() + QPointF(rect().left(), rect().center().y()); case NodeViewCommon::kTopToBottom: return pos() + QPointF(rect().center().x(), rect().bottom()); case NodeViewCommon::kBottomToTop: return pos() + QPointF(rect().center().x(), rect().top()); } } void NodeViewItem::SetFlowDirection(NodeViewCommon::FlowDirection dir) { flow_dir_ = dir; UpdateNodePosition(); } QPointF NodeViewItem::GetInputPointInternal(int index, const QPointF& source_pos) const { QRectF input_rect = GetInputRect(index); Qt::Orientation flow_orientation = NodeViewCommon::GetFlowOrientation(flow_dir_); if (flow_orientation == Qt::Horizontal || IsExpanded()) { if (flow_dir_ == NodeViewCommon::kLeftToRight || (flow_orientation == Qt::Vertical && source_pos.x() < pos().x())) { return QPointF(input_rect.left(), input_rect.center().y()); } else { return QPointF(input_rect.right(), input_rect.center().y()); } } else { if (flow_dir_ == NodeViewCommon::kTopToBottom) { return QPointF(input_rect.center().x(), input_rect.top()); } else { return QPointF(input_rect.center().x(), input_rect.bottom()); } } } void NodeViewItem::UpdateNodePosition() { setPos(NodeToScreenPoint(cached_node_pos_, flow_dir_)); } }