File indexing completed on 2025-02-02 04:11:33

0001 /*
0002  * SPDX-FileCopyrightText: 2019-2023 Mattia Basaglia <dev@dragon.best>
0003  *
0004  * SPDX-License-Identifier: GPL-3.0-or-later
0005  */
0006 
0007 #include "keyframe_transition_widget.hpp"
0008 #include <QPaintEvent>
0009 #include <QPainter>
0010 #include <QMouseEvent>
0011 #include <QPainterPath>
0012 
0013 #include "math/math.hpp"
0014 
0015 #include <QDebug>
0016 
0017 using namespace glaxnimate::gui;
0018 using namespace glaxnimate;
0019 
0020 class KeyframeTransitionWidget::Private
0021 {
0022 public:
0023     model::KeyframeTransition* target = nullptr;
0024     int selected_handle = 0;
0025     int highlighted_handle = 0;
0026     int handle_radius = 8;
0027     double y_margin = 0;
0028     double y_margin_value = 0;
0029     QPoint drag_start_mouse;
0030     QPoint drag_start_handle;
0031 
0032     bool hold() const
0033     {
0034         return target->hold();
0035     }
0036 
0037     const QPointF& point(int i) { return target->bezier().points()[i]; }
0038 
0039     double map_coord(double coord, double size, double margin)
0040     {
0041         return margin + (size - 2 * margin) * coord;
0042     }
0043 
0044     QPointF map_pt(const QPointF& p, int width, int height)
0045     {
0046         return QPointF(
0047             map_coord(p.x(), width, handle_radius),
0048             map_coord((1-p.y()), height, y_margin * height + handle_radius)
0049         );
0050     }
0051 
0052     QPointF mapped_point(int i, int width, int height)
0053     {
0054         return map_pt(point(i), width, height);
0055     }
0056 
0057     double unmap_coord(double coord, double size, double margin, double bound_margin)
0058     {
0059         return qBound(0. - bound_margin, (coord - margin) / (size - 2.0 * margin), 1. + bound_margin);
0060     }
0061 
0062     QPointF unmap_pt(const QPoint& p, int width, int height, bool clamp_y = false)
0063     {
0064         auto y = 1 - unmap_coord(p.y(), height, y_margin * height + handle_radius, y_margin_value);
0065         if ( clamp_y )
0066             y = qBound(0., y, 1.);
0067         return QPointF{unmap_coord(p.x(), width, 0, 0), y};
0068     }
0069 
0070 };
0071 
0072 
0073 KeyframeTransitionWidget::KeyframeTransitionWidget(QWidget* parent)
0074     : QWidget(parent), d(std::make_unique<Private>())
0075 {
0076     setMouseTracking(true);
0077     setFocusPolicy(Qt::StrongFocus);
0078 }
0079 
0080 KeyframeTransitionWidget::~KeyframeTransitionWidget() = default;
0081 
0082 void KeyframeTransitionWidget::set_target(model::KeyframeTransition* kft)
0083 {
0084     d->target = kft;
0085     auto margin = math::max(-kft->before().y(), kft->after().y() - 1);
0086     if ( margin > 0 )
0087     {
0088         d->y_margin_value = margin;
0089         d->y_margin = margin / (2 * margin + 1);
0090     }
0091 
0092     update();
0093 
0094     if ( kft )
0095     {
0096         Q_EMIT before_changed(kft->before_descriptive());
0097         Q_EMIT after_changed(kft->after_descriptive());
0098     }
0099 }
0100 
0101 void KeyframeTransitionWidget::paintEvent(QPaintEvent*)
0102 {
0103     QPainter painter(this);
0104 
0105 
0106     QRect bounds = rect();
0107     QRect center_rect = bounds.adjusted(
0108         d->handle_radius,
0109         d->handle_radius,
0110         -d->handle_radius,
0111         -d->handle_radius
0112     );
0113 
0114     QPalette::ColorGroup group = isEnabled() && d->target && !d->hold() ? QPalette::Active : QPalette::Disabled;
0115     painter.fillRect(bounds, palette().brush(group, QPalette::Window));
0116     painter.fillRect(center_rect, palette().brush(group, QPalette::Base));
0117 
0118     if ( d->y_margin > 0 )
0119     {
0120         painter.setPen(QPen(palette().brush(group, QPalette::Window), 2, Qt::DotLine));
0121         int y = bounds.top() + d->y_margin * bounds.height() + d->handle_radius;
0122         painter.drawLine(bounds.left(), y, bounds.right(), y);
0123         y = bounds.bottom() - d->y_margin * bounds.height() - d->handle_radius;
0124         painter.drawLine(bounds.left(), y, bounds.right(), y);
0125 
0126     }
0127 
0128     if ( d->target )
0129     {
0130         bool enabled = isEnabled() && !d->hold();
0131         painter.setRenderHint(QPainter::Antialiasing);
0132 
0133         QBrush fg = palette().brush(group, QPalette::Text);
0134         int w = width();
0135         int h = height();
0136         std::array<QPointF, 4> p = {
0137             d->mapped_point(0, w, h),
0138             d->mapped_point(1, w, h),
0139             d->mapped_point(2, w, h),
0140             d->mapped_point(3, w, h)
0141         };
0142 
0143         // Path
0144         QPainterPath pp(p[0]);
0145         pp.cubicTo(p[1], p[2], p[3]);
0146         painter.setBrush(Qt::NoBrush);
0147         painter.setPen(QPen(palette().brush(group, QPalette::Mid), 3));
0148         painter.drawPath(pp);
0149         painter.setPen(QPen(fg, 1));
0150         painter.drawPath(pp);
0151 
0152         // Connect handles
0153         QPen high_pen(palette().brush(group, QPalette::Highlight), 2);
0154         QPen low_pen(QPen(palette().brush(group, QPalette::Text), 2));
0155         painter.setBrush(Qt::NoBrush);
0156         painter.setPen(1 == d->selected_handle && enabled ? high_pen : low_pen);
0157         painter.drawLine(p[0], p[1]);
0158         painter.setPen(2 == d->selected_handle && enabled ? high_pen : low_pen);
0159         painter.drawLine(p[2], p[3]);
0160 
0161         // Fixed handles
0162         painter.setBrush(fg);
0163         painter.setPen(Qt::NoPen);
0164         painter.drawEllipse(p[0], d->handle_radius, d->handle_radius);
0165         painter.drawEllipse(p[3], d->handle_radius, d->handle_radius);
0166 
0167         // Draggable handles
0168         for ( int i = 1; i <= 2; i++ )
0169         {
0170             if ( i == d->selected_handle && enabled )
0171             {
0172                 painter.setBrush(palette().brush(group, QPalette::Highlight));
0173                 painter.setPen(Qt::transparent);
0174             }
0175             else if ( i == d->highlighted_handle && enabled )
0176             {
0177                 painter.setBrush(palette().brush(group, QPalette::HighlightedText));
0178                 painter.setPen(QPen(palette().brush(group, QPalette::Highlight), 1));
0179             }
0180             else
0181             {
0182                 painter.setBrush(palette().brush(group, QPalette::Base));
0183                 painter.setPen(QPen(palette().brush(group, QPalette::Text), 1));
0184             }
0185             painter.drawEllipse(p[i], d->handle_radius, d->handle_radius);
0186         }
0187     }
0188 }
0189 
0190 void KeyframeTransitionWidget::mousePressEvent(QMouseEvent* event)
0191 {
0192     QWidget::mousePressEvent(event);
0193     if ( !d->target || d->hold() )
0194         return;
0195 
0196     if ( event->button() == Qt::LeftButton )
0197     {
0198         event->accept();
0199         d->selected_handle = d->highlighted_handle;
0200         if ( d->selected_handle )
0201         {
0202             d->drag_start_mouse = event->pos();
0203             d->drag_start_handle = d->map_pt(d->point(d->selected_handle), width(), height()).toPoint();
0204         }
0205         update();
0206     }
0207 }
0208 
0209 void KeyframeTransitionWidget::mouseMoveEvent(QMouseEvent* event)
0210 {
0211     QWidget::mouseMoveEvent(event);
0212     if ( !d->target || d->hold() )
0213         return;
0214 
0215     if ( d->selected_handle )
0216     {
0217         QPoint pos = event->pos() - d->drag_start_mouse + d->drag_start_handle;
0218 
0219         bool clamp_y = event->modifiers() & Qt::ControlModifier;
0220 
0221         if ( d->selected_handle == 1 )
0222         {
0223             d->target->set_before(d->unmap_pt(pos, width(), height(), clamp_y));
0224             Q_EMIT before_changed(d->target->before_descriptive());
0225         }
0226         else if ( d->selected_handle == 2 )
0227         {
0228             d->target->set_after(d->unmap_pt(pos, width(), height(), clamp_y));
0229             Q_EMIT after_changed(d->target->after_descriptive());
0230         }
0231         update();
0232     }
0233     else if ( isEnabled() )
0234     {
0235         event->accept();
0236         QPointF p = d->unmap_pt(event->pos(), width(), height());
0237         double d1 = math::length_squared(d->point(1) - p);
0238         double d2 = math::length_squared(d->point(2) - p);
0239         d->highlighted_handle = d1 < d2 ? 1 : 2;
0240         update();
0241     }
0242 }
0243 
0244 void KeyframeTransitionWidget::mouseReleaseEvent(QMouseEvent* event)
0245 {
0246     QWidget::mouseReleaseEvent(event);
0247     if ( !d->target )
0248         return;
0249 
0250     if ( event->button() == Qt::LeftButton )
0251     {
0252         event->accept();
0253         d->selected_handle = 0;
0254         update();
0255     }
0256 }
0257 
0258 void KeyframeTransitionWidget::focusInEvent(QFocusEvent* event)
0259 {
0260     QWidget::focusInEvent(event);
0261 }
0262 
0263 void KeyframeTransitionWidget::focusOutEvent(QFocusEvent* event)
0264 {
0265     QWidget::focusOutEvent(event);
0266     d->selected_handle = 0;
0267     update();
0268 }
0269 
0270 void KeyframeTransitionWidget::leaveEvent(QEvent* event)
0271 {
0272     QWidget::leaveEvent(event);
0273     if ( !d->selected_handle )
0274     {
0275         d->highlighted_handle = 0;
0276         update();
0277     }
0278 }
0279 
0280 model::KeyframeTransition * KeyframeTransitionWidget::target() const
0281 {
0282     return d->target;
0283 }
0284 
0285 QSize KeyframeTransitionWidget::sizeHint() const
0286 {
0287     return QSize(300, 200);
0288 }
0289 
0290 void glaxnimate::gui::KeyframeTransitionWidget::set_y_margin(double margin)
0291 {
0292     d->y_margin = margin;
0293     update();
0294 }
0295