Warning, file /graphics/glaxnimate/src/gui/tools/select_tool.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).

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 "base.hpp"
0008 
0009 #include <variant>
0010 
0011 #include "model/shapes/path.hpp"
0012 #include "command/structure_commands.hpp"
0013 #include "command/animation_commands.hpp"
0014 #include "math/geom.hpp"
0015 
0016 #ifndef Q_OS_ANDROID
0017     #include "widgets/dialogs/glaxnimate_window.hpp"
0018     #include "widgets/menus/node_menu.hpp"
0019     #include "handle_menu.hpp"
0020 #endif
0021 
0022 namespace glaxnimate::gui::tools {
0023 
0024 class SelectTool : public Tool
0025 {
0026 public:
0027     QString id() const override { return "select"; }
0028     QIcon icon() const override { return QIcon::fromTheme("edit-select"); }
0029     QString name() const override { return i18n("Select"); }
0030     QKeySequence key_sequence() const override { return QKeySequence(i18n("F1"), QKeySequence::PortableText); }
0031     static int static_group() noexcept { return Registry::Core; }
0032     int group() const noexcept override { return static_group(); }
0033 
0034 private:
0035     enum DragMode
0036     {
0037         None,
0038         Click,
0039         RubberBand,
0040         ForwardEvents,
0041         DrawSelect,
0042         DragObject,
0043     };
0044 
0045     struct DragObjectData
0046     {
0047         template<class Prop>
0048         struct PropData
0049         {
0050             Prop* property;
0051             typename Prop::value_type start_value;
0052         };
0053         using Variant = std::variant<
0054             PropData<model::AnimatedProperty<QPointF>>,
0055             PropData<model::AnimatedProperty<math::bezier::Bezier>>
0056         >;
0057 
0058         template<class T>
0059         DragObjectData(model::VisualNode* node, T* property, const QPointF& scene_pos)
0060         : transform(node->docnode_fuzzy_parent()->transform_matrix(node->time()).inverted()),
0061           data(PropData<T>{property, property->get()}),
0062           start_point(transform.map(scene_pos))
0063         {}
0064 
0065         static void push(model::VisualNode* node, const QPointF& scene_pos, std::vector<DragObjectData>& out)
0066         {
0067             if ( auto prop = node->get_property("position") )
0068                 out.push_back(DragObjectData(node, static_cast<model::AnimatedProperty<QPointF>*>(prop), scene_pos));
0069             if ( auto prop = node->get_property("transform") )
0070                 out.push_back(DragObjectData(
0071                     node,
0072                     &static_cast<model::SubObjectProperty<model::Transform>*>(prop)->get()->position,
0073                     scene_pos)
0074                 );
0075             else if ( auto shape = qobject_cast<model::Path*>(node) )
0076                 out.push_back(DragObjectData(node, &shape->shape, scene_pos));
0077         }
0078 
0079         void drag(const QPointF& dragged_to, command::SetMultipleAnimated* cmd) const
0080         {
0081             QPointF delta = transform.map(dragged_to) - start_point;
0082 
0083 
0084             if ( data.index() == 0 )
0085             {
0086                 cmd->push_property(std::get<0>(data).property, std::get<0>(data).start_value + delta);
0087                 return;
0088             }
0089 
0090             math::bezier::Bezier new_bezier = std::get<1>(data).start_value;
0091             for ( auto& point : new_bezier )
0092                 point.translate(delta);
0093 
0094             cmd->push_property(std::get<1>(data).property, QVariant::fromValue(new_bezier));
0095         }
0096 
0097         model::Document* doc() const
0098         {
0099             if ( data.index() == 0 )
0100                 return std::get<0>(data).property->object()->document();
0101             return std::get<1>(data).property->object()->document();
0102         }
0103 
0104         QTransform transform;
0105         Variant data;
0106         QPointF start_point;
0107     };
0108 
0109     void do_drag(QPointF scene_pos, Qt::KeyboardModifiers mods, bool commit)
0110     {
0111         if ( drag_data.empty() )
0112             return;
0113 
0114         rubber_p2 = scene_pos;
0115         if ( mods & Qt::ControlModifier )
0116         {
0117             std::array<QPointF, 2> directions = {QPointF(1, 0), QPointF(0, 1)};
0118             QPointF best;
0119             qreal min_dist = std::numeric_limits<qreal>::max();
0120             for ( const auto& dir : directions )
0121             {
0122                 auto p = math::line_closest_point(rubber_p1, rubber_p1 + dir, scene_pos);
0123                 qreal dist = math::length_squared(p - scene_pos);
0124                 if ( dist < min_dist )
0125                 {
0126                     min_dist = dist;
0127                     best = p;
0128                 }
0129             }
0130             scene_pos = best;
0131         }
0132 
0133         auto cmd = new command::SetMultipleAnimated(i18n("Drag"), commit);
0134         model::Document* doc = drag_data[0].doc();
0135 
0136         for ( const auto& dragger : drag_data )
0137             dragger.drag(scene_pos, cmd);
0138 
0139         doc->push_command(cmd);
0140     }
0141 
0142     void mouse_press(const MouseEvent& event) override
0143     {
0144         if ( event.press_button == Qt::LeftButton )
0145         {
0146             drag_data.clear();
0147 
0148             if ( event.modifiers() & Qt::AltModifier )
0149             {
0150                 drag_mode = DrawSelect;
0151                 draw_path.moveTo(event.scene_pos);
0152                 return;
0153             }
0154 
0155             drag_mode = Click;
0156             rubber_p1 = event.pos();
0157 
0158             auto selection_mode = event.modifiers() & Qt::ControlModifier ? SelectionMode::Shape : SelectionMode::Group;
0159             auto clicked_on = under_mouse(event, true, selection_mode);
0160             if ( clicked_on.handle )
0161             {
0162                 drag_mode = ForwardEvents;
0163                 event.forward_to_scene();
0164                 return;
0165             }
0166             else if ( !clicked_on.nodes.empty() )
0167             {
0168                 rubber_p1 = event.scene_pos;
0169                 replace_selection = nullptr;
0170 
0171                 bool drag_selection = false;
0172 
0173                 for ( auto node : clicked_on.nodes )
0174                 {
0175                     if ( event.scene->is_descendant_of_selection(node->node()) )
0176                     {
0177                         drag_selection = true;
0178                         break;
0179                     }
0180                 }
0181 
0182                 if ( drag_selection )
0183                 {
0184                     for ( auto node : event.scene->cleaned_selection() )
0185                         DragObjectData::push(node, event.scene_pos, drag_data);
0186                 }
0187                 else
0188                 {
0189                     replace_selection = clicked_on.nodes[0]->node();
0190                     DragObjectData::push(clicked_on.nodes[0]->node(), event.scene_pos, drag_data);
0191                 }
0192             }
0193 
0194         }
0195     }
0196 
0197     void mouse_move(const MouseEvent& event) override
0198     {
0199         if ( event.press_button == Qt::LeftButton )
0200         {
0201             switch ( drag_mode )
0202             {
0203                 case None:
0204                     break;
0205                 case ForwardEvents:
0206                     event.forward_to_scene();
0207                     break;
0208                 case DrawSelect:
0209                     draw_path.lineTo(event.scene_pos);
0210                     break;
0211                 case Click:
0212                     if ( !drag_data.empty() )
0213                     {
0214                         if ( replace_selection )
0215                         {
0216                             event.scene->user_select({replace_selection}, graphics::DocumentScene::Replace);
0217                             replace_selection = nullptr;
0218                         }
0219                         drag_mode = DragObject;
0220                     }
0221                     else
0222                     {
0223                         drag_mode = RubberBand;
0224                     }
0225                     mouse_move(event);
0226                     break;
0227                 case RubberBand:
0228                     rubber_p2 = event.pos();
0229                     break;
0230                 case DragObject:
0231                     do_drag(event.scene_pos, event.modifiers(), false);
0232                     break;
0233             }
0234         }
0235     }
0236 
0237     void complex_select(const MouseEvent& event, const std::vector<graphics::DocumentNodeGraphicsItem*>& items)
0238     {
0239 
0240         auto mode = graphics::DocumentScene::Replace;
0241         if ( event.modifiers() & Qt::ShiftModifier )
0242             mode = graphics::DocumentScene::Append;
0243 
0244         auto selection_mode = event.modifiers() & Qt::ControlModifier ? SelectionMode::Shape : SelectionMode::Group;
0245 
0246         std::vector<model::VisualNode*> selection;
0247         for ( auto item : items )
0248         {
0249             if ( item->node()->docnode_selectable() && item->selection_mode() >= selection_mode )
0250                 selection.push_back(item->node());
0251         }
0252 
0253         event.scene->user_select(selection, mode);
0254     }
0255 
0256     void mouse_release(const MouseEvent& event) override
0257     {
0258         if ( event.button() == Qt::LeftButton )
0259         {
0260             switch ( drag_mode )
0261             {
0262                 case None:
0263                     break;
0264                 case ForwardEvents:
0265                     event.forward_to_scene();
0266                     break;
0267                 case DrawSelect:
0268                     draw_path.lineTo(event.scene_pos);
0269                     complex_select(event, event.scene->nodes(draw_path, event.view->viewportTransform()));
0270                     draw_path = {};
0271                     event.view->viewport()->update();
0272                     break;
0273                 case RubberBand:
0274                     rubber_p2 = event.pos();
0275                     complex_select(event, event.scene->nodes(
0276                         event.view->mapToScene(QRect(rubber_p1.toPoint(), rubber_p2.toPoint()).normalized()),
0277                         event.view->viewportTransform(),
0278                         Qt::ContainsItemShape
0279                     ));
0280                     drag_mode = None;
0281                     event.view->viewport()->update();
0282                     break;
0283                 case Click:
0284                 {
0285                     replace_selection = nullptr;
0286 
0287                     std::vector<model::VisualNode*> selection;
0288 
0289                     auto selection_mode = event.modifiers() & Qt::ControlModifier ? SelectionMode::Shape : SelectionMode::Group;
0290                     auto nodes = under_mouse(event, true, selection_mode).nodes;
0291                     if ( !nodes.empty() )
0292                         selection.push_back(nodes[0]->node());
0293 
0294                     auto mode = graphics::DocumentScene::Replace;
0295                     if ( event.modifiers() & Qt::ShiftModifier )
0296                         mode = graphics::DocumentScene::Toggle;
0297 
0298                     event.scene->user_select(selection, mode);
0299                 }
0300                 break;
0301                 case DragObject:
0302                     do_drag(event.scene_pos, event.modifiers(), true);
0303                     break;
0304             }
0305 
0306             replace_selection = nullptr;
0307             drag_data.clear();
0308             drag_mode = None;
0309         }
0310         else if ( event.press_button == Qt::RightButton )
0311         {
0312             context_menu(event);
0313         }
0314     }
0315 
0316     void mouse_double_click(const MouseEvent& event) override
0317     {
0318         edit_clicked(event);
0319     }
0320 
0321     void key_press(const KeyEvent& event) override
0322     {
0323         if ( drag_mode == DragObject && event.key() == Qt::Key_Control )
0324         {
0325             do_drag(rubber_p2, event.modifiers(), false);
0326         }
0327     }
0328 
0329 
0330     void key_release(const KeyEvent& event) override
0331     {
0332         if ( drag_mode == None && (event.key() == Qt::Key_Backspace) )
0333         {
0334             event.window->delete_selected();
0335             event.accept();
0336         }
0337         else if ( drag_mode == DragObject && event.key() == Qt::Key_Control )
0338         {
0339             do_drag(rubber_p2, event.modifiers(), false);
0340         }
0341     }
0342 
0343     void paint(const PaintEvent& event) override
0344     {
0345         if ( !selected_shapes.empty() )
0346         {
0347             QColor select_color = event.view->palette().color(QPalette::Highlight);
0348             QPen pen(select_color, 1);
0349             event.painter->setPen(pen);
0350             event.painter->setBrush(Qt::NoBrush);
0351             for ( auto shape : selected_shapes )
0352             {
0353                 QPainterPath p;
0354                 shape->to_bezier(shape->time()).add_to_painter_path(p);
0355                 QTransform trans = shape->transform_matrix(shape->time()) * event.view->viewportTransform();
0356                 event.painter->drawPath(trans.map(p));
0357             }
0358         }
0359 
0360         if ( drag_mode == DrawSelect )
0361         {
0362             event.painter->setTransform(event.view->viewportTransform());
0363             event.painter->setBrush(Qt::transparent);
0364             QPen pen(event.view->palette().color(QPalette::Highlight), 2);
0365             pen.setCosmetic(true);
0366             event.painter->setPen(pen);
0367             event.painter->drawPath(draw_path);
0368         }
0369         else if ( drag_mode == RubberBand )
0370         {
0371             event.painter->setBrush(Qt::transparent);
0372             QColor select_color = event.view->palette().color(QPalette::Highlight);
0373             QPen pen(select_color, 1);
0374             pen.setCosmetic(true);
0375             event.painter->setPen(pen);
0376             select_color.setAlpha(128);
0377             event.painter->setBrush(select_color);
0378             event.painter->drawRect(QRectF(rubber_p1, rubber_p2));
0379         }
0380     }
0381 
0382     void enable_event(const Event&) override { selected_shapes.clear(); }
0383     void disable_event(const Event&) override { selected_shapes.clear(); }
0384 
0385     QCursor cursor() override { return Qt::ArrowCursor; }
0386 
0387     void context_menu(const MouseEvent& event)
0388     {
0389 #ifndef MOBILE_UI
0390         auto window = static_cast<GlaxnimateWindow*>(event.window);
0391 
0392         auto targets = under_mouse(event, true, graphics::DocumentNodeGraphicsItem::None);
0393 
0394         QMenu menu;
0395         auto undo_stack = &window->undo_group();
0396         menu.addAction(
0397             QIcon::fromTheme("edit-undo"),
0398             i18n("Undo %1", undo_stack->undoText()),
0399             undo_stack, &QUndoGroup::undo
0400         )->setEnabled(undo_stack->canUndo());
0401         menu.addAction(
0402             QIcon::fromTheme("edit-redo"),
0403             i18n("Redo %1", undo_stack->redoText()),
0404             undo_stack, &QUndoGroup::redo
0405         )->setEnabled(undo_stack->canRedo());
0406 
0407         menu.addSeparator();
0408         menu.addAction(QIcon::fromTheme("edit-cut"), i18n("Cut"),
0409                        window, &GlaxnimateWindow::cut);
0410         menu.addAction(QIcon::fromTheme("edit-copy"), i18n("Copy"),
0411                        window, &GlaxnimateWindow::copy);
0412         menu.addAction(QIcon::fromTheme("edit-paste"), i18n("Paste"),
0413                        window, &GlaxnimateWindow::paste);
0414 
0415         menu.addSeparator();
0416         menu.addAction(QIcon::fromTheme("edit-delete-remove"), i18n("Delete"),
0417                        window, &GlaxnimateWindow::delete_selected);
0418 
0419 
0420         menu.addSeparator();
0421         menu.addAction(QIcon::fromTheme("object-group"), i18n("Group Shapes"),
0422                        window, &GlaxnimateWindow::group_shapes);
0423 
0424         menu.addAction(QIcon::fromTheme("object-ungroup"), i18n("Ungroup Shapes"),
0425                        window, &GlaxnimateWindow::ungroup_shapes);
0426 
0427         menu.addSeparator();
0428         menu.addAction(QIcon::fromTheme("selection-move-to-layer-above"), i18n("Move to..."),
0429                        window, &GlaxnimateWindow::move_to);
0430 
0431 
0432         menu.addSeparator();
0433 
0434         model::DocumentNode* preferred = window->current_shape();
0435 
0436         for ( auto item : targets.nodes )
0437         {
0438             auto obj_menu = new NodeMenu(item->node(), window, &menu);
0439             if ( item->node() == preferred )
0440                 preferred = nullptr;
0441             if ( obj_menu->actions().size() > 1 )
0442                 menu.addAction(obj_menu->menuAction());
0443             else
0444                 delete obj_menu;
0445         }
0446 
0447         if ( preferred )
0448             menu.addAction((new NodeMenu(preferred, window, &menu))->menuAction());
0449 
0450         if ( targets.handle )
0451             add_property_menu_actions(&menu, targets.handle, event.window);
0452 
0453 
0454         menu.exec(QCursor::pos());
0455 #else
0456         Q_UNUSED(event);
0457 #endif
0458     }
0459 
0460     QWidget* on_create_widget() override
0461     {
0462         return new QWidget();
0463     }
0464 
0465     void on_selected(graphics::DocumentScene * scene, model::VisualNode * node) override
0466     {
0467         if ( node->has("transform") || node->has("position") || node->is_instance<model::Composition>() )
0468         {
0469             scene->show_editors(node);
0470         }
0471         else if ( auto shape = node->cast<model::Shape>() )
0472         {
0473             selected_shapes.push_back(shape);
0474             scene->invalidate(
0475                 shape->transform_matrix(shape->time())
0476                 .map(shape->local_bounding_rect(shape->time()))
0477                 .boundingRect()
0478             );
0479         }
0480     }
0481 
0482     void on_deselected(graphics::DocumentScene * scene, model::VisualNode * node) override
0483     {
0484         Tool::on_deselected(scene, node);
0485 
0486         if ( auto shape = node->cast<model::Shape>() )
0487         {
0488             selected_shapes.erase(std::remove(selected_shapes.begin(), selected_shapes.end(), shape), selected_shapes.end());
0489             scene->invalidate(
0490                 shape->transform_matrix(shape->time())
0491                 .map(shape->local_bounding_rect(shape->time()))
0492                 .boundingRect()
0493             );
0494         }
0495     }
0496 
0497     void close_document_event(const glaxnimate::gui::tools::Event &) override
0498     {
0499         drag_data.clear();
0500         replace_selection = nullptr;
0501         selected_shapes.clear();
0502     }
0503 
0504     DragMode drag_mode;
0505     QPainterPath draw_path;
0506     QPointF rubber_p1;
0507     QPointF rubber_p2;
0508     std::vector<DragObjectData> drag_data;
0509     model::VisualNode* replace_selection = nullptr;
0510     std::vector<model::Shape*> selected_shapes;
0511 
0512     static Autoreg<SelectTool> autoreg;
0513 };
0514 
0515 
0516 tools::Autoreg<tools::SelectTool> tools::SelectTool::autoreg{max_priority};
0517 
0518 } // namespace glaxnimate::gui::tools