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

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 "glaxnimate_window_p.hpp"
0008 
0009 #include <queue>
0010 
0011 #include <QClipboard>
0012 #include <QImageReader>
0013 #include <QFileDialog>
0014 #include <QMimeData>
0015 #include <QMimeType>
0016 #include <QMimeDatabase>
0017 
0018 #include "app/settings/widget_builder.hpp"
0019 
0020 #include "command/shape_commands.hpp"
0021 #include "command/structure_commands.hpp"
0022 #include "command/undo_macro_guard.hpp"
0023 
0024 #include "model/shapes/image.hpp"
0025 #include "model/shapes/group.hpp"
0026 #include "model/shapes/path.hpp"
0027 #include "model/shapes/precomp_layer.hpp"
0028 #include "model/simple_visitor.hpp"
0029 #include "model/shapes/text.hpp"
0030 
0031 #include "settings/clipboard_settings.hpp"
0032 #include "widgets/dialogs/shape_parent_dialog.hpp"
0033 #include "widgets/shape_style/shape_style_preview_widget.hpp"
0034 
0035 #include "item_models/drag_data.hpp"
0036 
0037 model::Composition* GlaxnimateWindow::Private::current_composition()
0038 {
0039     return comp;
0040 }
0041 
0042 model::VisualNode* GlaxnimateWindow::Private::current_document_node()
0043 {
0044     if ( auto dn = ui.view_document_node->current_node() )
0045         return dn;
0046     return comp;
0047 }
0048 
0049 void GlaxnimateWindow::Private::layer_new_layer()
0050 {
0051     auto layer = std::make_unique<model::Layer>(current_document.get());
0052     layer->animation->last_frame.set(comp->animation->last_frame.get());
0053     QPointF pos = comp->rect().center();
0054     layer->transform.get()->anchor_point.set(pos);
0055     layer->transform.get()->position.set(pos);
0056     parent->layer_new_impl(std::move(layer));
0057 }
0058 
0059 void GlaxnimateWindow::Private::layer_new_fill()
0060 {
0061     auto layer = std::make_unique<model::Fill>(current_document.get());
0062     layer->color.set(ui.fill_style_widget->current_color());
0063     parent->layer_new_impl(std::move(layer));
0064 }
0065 
0066 void GlaxnimateWindow::Private::layer_new_stroke()
0067 {
0068     auto layer = std::make_unique<model::Stroke>(current_document.get());
0069     layer->set_pen_style(ui.stroke_style_widget->pen_style());
0070     parent->layer_new_impl(std::move(layer));
0071 }
0072 
0073 void GlaxnimateWindow::Private::layer_new_group()
0074 {
0075     auto layer = std::make_unique<model::Group>(current_document.get());
0076     QPointF pos = comp->rect().center();
0077     layer->transform.get()->anchor_point.set(pos);
0078     layer->transform.get()->position.set(pos);
0079     parent->layer_new_impl(std::move(layer));
0080 }
0081 
0082 void GlaxnimateWindow::Private::layer_delete()
0083 {
0084     auto current = parent->current_shape();
0085     if ( !current )
0086         return;
0087     parent->delete_shapes_impl(i18n("Delete %1", current->object_name()), {current});
0088 }
0089 
0090 void GlaxnimateWindow::Private::layer_duplicate()
0091 {
0092     auto current = parent->current_shape();
0093     if ( !current )
0094         return;
0095 
0096     auto cmd = command::duplicate_shape(current);
0097     current->push_command(cmd);
0098     set_current_document_node(cmd->object());
0099 }
0100 
0101 std::vector<model::VisualNode*> GlaxnimateWindow::Private::cleaned_selection()
0102 {
0103     return scene.cleaned_selection();
0104 }
0105 
0106 
0107 void GlaxnimateWindow::Private::duplicate_selection()
0108 {
0109     auto selection = cleaned_selection();
0110 
0111     if ( !selection.empty() )
0112     {
0113         std::vector<model::VisualNode*> duplicated;
0114         duplicated.reserve(selection.size());
0115 
0116         for ( const auto& node : selection )
0117         {
0118             if ( auto shape = node->cast<model::ShapeElement>() )
0119             {
0120                 auto cmd = command::duplicate_shape(shape);
0121                 current_document->push_command(cmd);
0122                 duplicated.push_back(cmd->object());
0123             }
0124         }
0125 
0126         scene.user_select(duplicated, graphics::DocumentScene::Replace);
0127     }
0128 }
0129 
0130 
0131 void GlaxnimateWindow::Private::move_current(command::ReorderCommand::SpecialPosition pos)
0132 {
0133     auto current = parent->current_shape();
0134     if ( !current )
0135         return;
0136     auto cmd = std::make_unique<command::ReorderCommand>(current, pos);
0137     if ( !cmd->has_action() )
0138         return;
0139     current->push_command(cmd.release());
0140 }
0141 
0142 void GlaxnimateWindow::Private::group_shapes()
0143 {
0144     auto data = command::GroupShapes::collect_shapes(cleaned_selection());
0145     if ( data.parent )
0146         current_document->push_command(
0147             new command::GroupShapes(data)
0148         );
0149 }
0150 
0151 void GlaxnimateWindow::Private::ungroup_shapes()
0152 {
0153     model::Group* group = qobject_cast<model::Group*>(current_document_node());
0154 
0155     if ( !group )
0156     {
0157         auto sp = parent->current_shape_container();
0158         if ( !sp )
0159             return;
0160         group = qobject_cast<model::Group*>(sp->object());
0161     }
0162 
0163     if ( group )
0164         current_document->push_command(new command::UngroupShapes(group));
0165 }
0166 
0167 
0168 void GlaxnimateWindow::Private::move_to()
0169 {
0170     auto sel = cleaned_selection();
0171     std::vector<model::ShapeElement*> shapes;
0172     shapes.reserve(sel.size());
0173     for ( const auto& node : sel )
0174     {
0175         if ( auto shape = qobject_cast<model::ShapeElement*>(node) )
0176             shapes.push_back(shape);
0177     }
0178 
0179     if ( shapes.empty() )
0180         return;
0181 
0182 
0183     if ( auto parent = ShapeParentDialog(&document_node_model, this->parent).get_shape_parent() )
0184     {
0185         command::UndoMacroGuard macro(i18n("Move Shapes"), current_document.get());
0186         for ( auto shape : shapes )
0187             if ( shape->owner() != parent )
0188                 shape->push_command(new command::MoveShape(shape, shape->owner(), parent, parent->size()));
0189     }
0190 }
0191 
0192 QStringList GlaxnimateWindow::Private::get_open_image_files(const QString& title, const QString& dir, QString* out_dir, bool multiple)
0193 {
0194     QFileDialog dialog(parent, title, dir);
0195     QStringList filters;
0196     QStringList all_ext;
0197 
0198     QMimeDatabase db;
0199     for ( const auto& baf : QImageReader::supportedMimeTypes() )
0200     {
0201         QMimeType mime(db.mimeTypeForName(baf));
0202         if ( mime.isValid() )
0203         {
0204             const QString patterns = mime.globPatterns().join(QLatin1Char(' '));
0205             all_ext += patterns;
0206             filters.push_back(mime.comment() + QLatin1String(" (") + patterns + QLatin1Char(')'));
0207         }
0208     }
0209     filters.push_front(i18n("All Supported files (%1)", all_ext.join(' ')));
0210     dialog.setNameFilters(filters);
0211 
0212     dialog.setAcceptMode(QFileDialog::AcceptOpen);
0213     dialog.setOption(QFileDialog::DontUseNativeDialog, !app::settings::get<bool>("open_save", "native_dialog"));
0214 
0215     if ( multiple )
0216         dialog.setFileMode(QFileDialog::ExistingFiles);
0217     else
0218         dialog.setFileMode(QFileDialog::ExistingFile);
0219 
0220     if ( dialog.exec() == QDialog::Rejected )
0221         return {};
0222 
0223     if ( out_dir )
0224         *out_dir = dialog.directory().path();
0225 
0226     return dialog.selectedFiles();
0227 }
0228 
0229 
0230 void GlaxnimateWindow::Private::import_image()
0231 {
0232     QString path = app::settings::get<QString>("open_save", "import_path");
0233     if ( path.isEmpty() )
0234         path = current_document->io_options().path.absolutePath();
0235 
0236     QStringList image_files = get_open_image_files(i18n("Import Image"), path, &path, true);
0237     if ( image_files.isEmpty() )
0238         return;
0239 
0240     app::settings::set("open_save", "import_path", path);
0241 
0242     /// \todo dialog asking whether to embed
0243     command::UndoMacroGuard macro(i18n("Import Image"), current_document.get());
0244 
0245     model::Image* select = nullptr;
0246 
0247     for ( const auto& image_file : image_files )
0248     {
0249         auto bitmap = std::make_unique<model::Bitmap>(current_document.get());
0250         bitmap->filename.set(image_file);
0251         if ( bitmap->pixmap().isNull() )
0252         {
0253             show_warning(i18n("Import Image"), i18n("Could not import image"));
0254             continue;
0255         }
0256 
0257         auto defs = current_document->assets();
0258         auto bmp_ptr = bitmap.get();
0259         current_document->push_command(new command::AddObject(&defs->images->values, std::move(bitmap), defs->images->values.size()));
0260 
0261         auto image = std::make_unique<model::Image>(current_document.get());
0262         image->image.set(bmp_ptr);
0263         QPointF p(bmp_ptr->pixmap().width() / 2.0, bmp_ptr->pixmap().height() / 2.0);
0264         image->transform->anchor_point.set(p);
0265         image->transform->position.set(p);
0266         auto comp = current_composition();
0267         select = image.get();
0268         image->name.set(QFileInfo(image_file).baseName());
0269         current_document->push_command(new command::AddShape(&comp->shapes, std::move(image), comp->shapes.size()));
0270     }
0271 
0272     if ( select )
0273         set_current_document_node(select);
0274 }
0275 
0276 template<class T>
0277 static void remove_assets(T& prop, int& count)
0278 {
0279     for ( int i = 0; i < prop.size();  )
0280     {
0281         if ( prop[i]->remove_if_unused(true) )
0282             count++;
0283         else
0284             i++;
0285     }
0286 }
0287 
0288 void GlaxnimateWindow::Private::cleanup_document()
0289 {
0290     command::UndoMacroGuard guard(i18n("Cleanup Document"), current_document.get());
0291     int count = 0;
0292 
0293     remove_assets(current_document->assets()->gradients->values, count);
0294     remove_assets(current_document->assets()->gradient_colors->values, count);
0295     remove_assets(current_document->assets()->colors->values, count);
0296     remove_assets(current_document->assets()->images->values, count);
0297 
0298     status_message(i18n("Removed %1 assets", count), 0);
0299 }
0300 
0301 void GlaxnimateWindow::Private::convert_to_path(const std::vector<model::ShapeElement*>& shapes, std::vector<model::ShapeElement*>* out)
0302 {
0303     if ( shapes.empty() )
0304         return;
0305 
0306     QString macro_name = i18n("Convert to path");
0307     if ( shapes.size() == 1 )
0308         macro_name = i18n("Convert %1 to path", (*shapes.begin())->name.get());
0309 
0310     std::unordered_map<model::Layer*, model::Layer*> converted_layers;
0311 
0312     command::UndoMacroGuard guard(macro_name, current_document.get(), false);
0313     for ( auto shape : shapes )
0314     {
0315         auto path = shape->to_path();
0316 
0317         if ( out )
0318             out->push_back(path.get());
0319 
0320         if ( path )
0321         {
0322             if ( auto lay = shape->cast<model::Layer>() )
0323                 converted_layers[lay] = static_cast<model::Layer*>(path.get());
0324 
0325             guard.start();
0326             current_document->push_command(
0327                 new command::AddObject<model::ShapeElement>(
0328                     shape->owner(),
0329                     std::move(path),
0330                     shape->position()
0331                 )
0332             );
0333             current_document->push_command(
0334                 new command::RemoveObject<model::ShapeElement>(shape, shape->owner())
0335             );
0336         }
0337     }
0338 
0339     // Maintain parenting of layers that have been converted
0340     for ( const auto& p : converted_layers )
0341     {
0342         if ( auto src_parent = p.first->parent.get() )
0343         {
0344             auto it = converted_layers.find(src_parent);
0345             if ( it != converted_layers.end() )
0346                 p.second->parent.set(it->second);
0347         }
0348     }
0349 }
0350 
0351 void GlaxnimateWindow::Private::to_path()
0352 {
0353     std::vector<model::ShapeElement*> shapes;
0354 
0355     for ( auto selected : scene.cleaned_selection() )
0356     {
0357         if ( selected->docnode_locked_recursive() )
0358             continue;
0359 
0360         if ( auto shape = selected->cast<model::ShapeElement>() )
0361         {
0362             if ( !shape->cast<model::Styler>() )
0363                 shapes.push_back(shape);
0364         }
0365     }
0366 
0367     convert_to_path(shapes, nullptr);
0368 }
0369 
0370 void GlaxnimateWindow::Private::switch_composition(model::Composition* new_comp, int i)
0371 {
0372     if ( i != ui.tab_bar->currentIndex() )
0373     {
0374         QSignalBlocker g(ui.tab_bar);
0375         ui.tab_bar->setCurrentIndex(i);
0376     }
0377 
0378     if ( comp )
0379     {
0380         int old_i = current_document->assets()->compositions->values.index_of(static_cast<model::Composition*>(comp));
0381         comp_selections[old_i].selection = scene.selection();
0382         if ( ui.view_document_node->currentIndex().isValid() )
0383             comp_selections[old_i].current = ui.view_document_node->current_node();
0384         else
0385             comp_selections[old_i].current = comp;
0386 
0387         QObject::disconnect(comp->animation.get(), &model::AnimationContainer::first_frame_changed, ui.play_controls, nullptr);
0388         QObject::disconnect(comp, &model::Composition::fps_changed, ui.play_controls, &FrameControlsWidget::set_fps);
0389     }
0390 
0391     comp = new_comp;
0392 
0393     ui.play_controls->set_range(comp->animation->first_frame.get(), comp->animation->last_frame.get());
0394     ui.play_controls->set_fps(comp->fps.get());
0395     ui.play_controls_2->set_range(comp->animation->first_frame.get(), comp->animation->last_frame.get());
0396     ui.play_controls_2->set_fps(comp->fps.get());
0397 
0398     QObject::connect(comp->animation.get(), &model::AnimationContainer::first_frame_changed, ui.play_controls, [this](float frame){ui.play_controls->set_min(frame);});
0399     QObject::connect(comp->animation.get(), &model::AnimationContainer::last_frame_changed, ui.play_controls, [this](float frame){ui.play_controls->set_max(frame);});
0400     QObject::connect(comp, &model::Composition::fps_changed, ui.play_controls, &FrameControlsWidget::set_fps);
0401 
0402     auto possible = current_document->comp_graph().possible_descendants(comp, current_document.get());
0403     std::set<model::Composition*> comps(possible.begin(), possible.end());
0404     for ( QAction* action : ui.menu_new_comp_layer->actions() )
0405         action->setEnabled(comps.count(action->data().value<model::Composition*>()));
0406 
0407     ui.view_document_node->set_composition(comp);
0408     ui.timeline_widget->set_composition(comp);
0409     scene.set_composition(comp);
0410     scene.user_select(comp_selections[i].selection, graphics::DocumentScene::Replace);
0411     auto current = comp_selections[i].current;
0412     ui.view_document_node->set_current_node(current);
0413 
0414     ui.canvas->viewport()->update();
0415 }
0416 
0417 void GlaxnimateWindow::Private::setup_composition(model::Composition* comp, int index)
0418 {
0419     CompState state;
0420     if ( !comp->shapes.empty() )
0421         state = comp->shapes[0];
0422     else
0423         state = comp;
0424 
0425     if ( index == -1 )
0426         index = comp_selections.size();
0427 
0428     comp_selections.insert(comp_selections.begin() + index, std::move(state));
0429 
0430     QAction* action = nullptr;
0431 
0432     ui.menu_new_comp_layer->setEnabled(true);
0433     action = new QAction(comp->instance_icon(), comp->object_name(), comp);
0434     if ( ui.menu_new_comp_layer->actions().empty() || index >= ui.menu_new_comp_layer->actions().size() )
0435         ui.menu_new_comp_layer->addAction(action);
0436     else
0437         ui.menu_new_comp_layer->insertAction(ui.menu_new_comp_layer->actions()[index-1], action);
0438     action->setData(QVariant::fromValue(comp));
0439 
0440     connect(comp, &model::DocumentNode::name_changed, action, [comp, action](){
0441         action->setText(comp->object_name());
0442     });
0443     connect(comp, &model::VisualNode::docnode_group_color_changed, action, [comp, action](){
0444         action->setIcon(comp->instance_icon());
0445     });
0446 
0447 }
0448 
0449 void GlaxnimateWindow::Private::add_composition()
0450 {
0451     auto old_comp = this->comp;
0452     std::unique_ptr<model::Composition> comp = std::make_unique<model::Composition>(current_document.get());
0453 
0454     auto lay = std::make_unique<model::Layer>(current_document.get());
0455     current_document->set_best_name(lay.get());
0456     lay->animation->first_frame.set(old_comp->animation->first_frame.get());
0457     lay->animation->last_frame.set(old_comp->animation->last_frame.get());
0458 
0459     comp->animation->first_frame.set(old_comp->animation->first_frame.get());
0460     comp->animation->last_frame.set(old_comp->animation->last_frame.get());
0461     comp->fps.set(old_comp->fps.get());
0462     comp->width.set(old_comp->width.get());
0463     comp->height.set(old_comp->height.get());
0464 
0465     QPointF center(comp->width.get() / 2, comp->height.get() / 2);
0466     lay->transform->anchor_point.set(center);
0467     lay->transform->position.set(center);
0468     comp->shapes.insert(std::move(lay));
0469 
0470     current_document->set_best_name(comp.get());
0471     current_document->push_command(new command::AddObject(&current_document->assets()->compositions->values, std::move(comp)));
0472     ui.tab_bar->setCurrentIndex(ui.tab_bar->count()-1);
0473 }
0474 
0475 void GlaxnimateWindow::Private::objects_to_new_composition(
0476     model::Composition* comp,
0477     const std::vector<model::VisualNode*>& objects,
0478     model::ObjectListProperty<model::ShapeElement>* layer_parent,
0479     int layer_index
0480 )
0481 {
0482     if ( objects.empty() )
0483         return;
0484 
0485     int new_comp_index = current_document->assets()->compositions->values.size();
0486     command::UndoMacroGuard guard(i18n("New Composition from Selection"), current_document.get());
0487 
0488     auto ucomp = std::make_unique<model::Composition>(current_document.get());
0489     model::Composition* new_comp = ucomp.get();
0490     new_comp->width.set(comp->width.get());
0491     new_comp->height.set(comp->height.get());
0492     new_comp->fps.set(comp->fps.get());
0493     new_comp->animation->first_frame.set(comp->animation->first_frame.get());
0494     new_comp->animation->last_frame.set(comp->animation->last_frame.get());
0495     if ( objects.size() > 1 || objects[0]->name.get().isEmpty() )
0496         current_document->set_best_name(new_comp);
0497     else
0498         new_comp->name.set(objects[0]->name.get());
0499     current_document->push_command(new command::AddObject(&current_document->assets()->compositions->values, std::move(ucomp)));
0500 
0501 
0502     for ( auto node : objects )
0503     {
0504         if ( auto shape = node->cast<model::ShapeElement>() )
0505             current_document->push_command(new command::MoveShape(
0506                 shape, shape->owner(), &new_comp->shapes, new_comp->shapes.size()
0507             ));
0508     }
0509 
0510     comp_selections.back().current = objects[0];
0511     comp_selections.back().selection = objects;
0512 
0513     auto pcl = std::make_unique<model::PreCompLayer>(current_document.get());
0514     pcl->composition.set(new_comp);
0515     pcl->size.set(new_comp->size());
0516     current_document->set_best_name(pcl.get());
0517     auto pcl_ptr = pcl.get();
0518     current_document->push_command(new command::AddShape(layer_parent, std::move(pcl), layer_index));
0519 
0520     switch_composition(new_comp, new_comp_index);
0521 
0522     int old_comp_index = current_document->assets()->compositions->values.index_of(static_cast<model::Composition*>(comp));
0523     comp_selections[old_comp_index] = pcl_ptr;
0524 }
0525 
0526 
0527 void GlaxnimateWindow::Private::on_remove_precomp(int index)
0528 {
0529     model::Composition* precomp = current_document->assets()->compositions->values[index];
0530 
0531     if ( precomp == comp )
0532     {
0533         auto& comps = current_document->assets()->compositions->values;
0534         if ( comps.empty() )
0535             add_composition();
0536 
0537         switch_composition(comps[qMax(comps.size(), index)], 0);
0538     }
0539 
0540     delete ui.menu_new_comp_layer->actions()[index];
0541     comp_selections.erase(comp_selections.begin()+index);
0542 }
0543 
0544 void GlaxnimateWindow::Private::layer_new_comp_action(QAction* action)
0545 {
0546     parent->layer_new_comp(action->data().value<model::Composition*>());
0547 }
0548 
0549 void GlaxnimateWindow::Private::shape_to_composition(model::ShapeElement* node)
0550 {
0551     if ( !node )
0552         return;
0553 
0554     auto parent = node->docnode_parent();
0555     if ( !parent )
0556         return;
0557 
0558     auto ancestor = parent;
0559     auto grand_ancestor = ancestor->docnode_parent();
0560     while ( grand_ancestor && !ancestor->is_instance<model::Composition>() )
0561     {
0562         ancestor = grand_ancestor;
0563         grand_ancestor = ancestor->docnode_parent();
0564     }
0565 
0566     auto owner_comp = ancestor->cast<model::Composition>();
0567     if ( !owner_comp )
0568         return;
0569 
0570     auto prop = parent->get_property("shapes");
0571     if ( !prop )
0572         return;
0573 
0574     auto shape_prop = static_cast<model::ObjectListProperty<model::ShapeElement>*>(prop);
0575     objects_to_new_composition(owner_comp, {node}, shape_prop, shape_prop->index_of(node));
0576 }
0577 
0578 QPointF GlaxnimateWindow::Private::align_point(const QRectF& rect, AlignDirection direction, AlignPosition position)
0579 {
0580     qreal x;
0581     qreal y;
0582 
0583     if ( direction == AlignDirection::Horizontal )
0584     {
0585         switch ( position )
0586         {
0587             case AlignPosition::Begin:  x = rect.left(); break;
0588             case AlignPosition::Center: x = rect.center().x(); break;
0589             case AlignPosition::End:    x = rect.right(); break;
0590         }
0591         y = rect.center().y();
0592     }
0593     else
0594     {
0595         switch ( position )
0596         {
0597             case AlignPosition::Begin:  y = rect.top(); break;
0598             case AlignPosition::Center: y = rect.center().y(); break;
0599             case AlignPosition::End:    y = rect.bottom(); break;
0600         }
0601         x = rect.center().x();
0602     }
0603 
0604     return {x, y};
0605 }
0606 
0607 namespace {
0608 
0609 struct AlignData
0610 {
0611     model::VisualNode* node;
0612     QTransform transform;
0613     QPointF bounds_point;
0614 };
0615 
0616 } // namespace
0617 
0618 void GlaxnimateWindow::Private::align(AlignDirection direction, AlignPosition position, bool outside)
0619 {
0620     std::vector<model::VisualNode*> selection = cleaned_selection();
0621 
0622     if ( selection.empty() )
0623         return;
0624 
0625     QRectF bounds;
0626 
0627     std::vector<AlignData> data;
0628     data.reserve(selection.size());
0629 
0630     auto bound_pos = position;
0631     if ( outside )
0632     {
0633         if ( bound_pos == AlignPosition::Begin )
0634             bound_pos = AlignPosition::End;
0635         else
0636             bound_pos = AlignPosition::Begin;
0637     }
0638 
0639     for ( const auto& item : selection )
0640     {
0641         auto t = item->time();
0642         QRectF local_bounds(item->local_bounding_rect(t));
0643         if ( !local_bounds.isValid() )
0644             continue;
0645 
0646         QTransform transform = item->transform_matrix(t);
0647         auto transformed_bounds = transform.map(local_bounds).boundingRect();
0648         data.push_back({item, transform.inverted(), align_point(transformed_bounds, direction, bound_pos)});
0649 
0650         if ( !bounds.isValid() )
0651             bounds = transformed_bounds;
0652         else
0653             bounds |= transformed_bounds;
0654     }
0655 
0656     QPointF reference;
0657 
0658     if ( ui.action_align_to_selection->isChecked() )
0659     {
0660         reference = align_point(bounds, direction, position);
0661     }
0662     else if ( ui.action_align_to_canvas->isChecked() )
0663     {
0664         reference = align_point(comp->rect(), direction, position);
0665     }
0666     else if ( ui.action_align_to_canvas_group->isChecked() )
0667     {
0668         reference = align_point(comp->rect(), direction, position);
0669         QPointF bounds_point = align_point(bounds, direction, bound_pos);
0670         for ( auto& item : data )
0671             item.bounds_point = bounds_point;
0672     }
0673 
0674     command::UndoMacroGuard guard(i18n("Align Selection"), current_document.get());
0675 
0676     for ( const auto& item : data )
0677     {
0678         QPointF target_point = reference;
0679         if ( direction == AlignDirection::Horizontal )
0680             target_point.setY(item.bounds_point.y());
0681         else
0682             target_point.setX(item.bounds_point.x());
0683 
0684         QPointF delta = item.transform.map(target_point) - item.transform.map(item.bounds_point);
0685 
0686         if ( auto path = item.node->cast<model::Path>() )
0687         {
0688             auto bezier = path->shape.get();
0689             for ( auto& point : bezier )
0690                 point.translate(delta);
0691             path->shape.set_undoable(QVariant::fromValue(bezier));
0692         }
0693         else if ( item.node->has("transform") )
0694         {
0695             auto m = item.node->local_transform_matrix(item.node->time());
0696             auto a = m.map(item.transform.map(target_point));
0697             auto b = m.map(item.transform.map(item.bounds_point));
0698             delta = a - b;
0699 
0700             auto trans = item.node->get("transform").value<model::Transform*>();
0701             auto point = trans->position.get() + delta;
0702             trans->position.set_undoable(point);
0703         }
0704         else if ( item.node->has("position") )
0705         {
0706             auto point = item.node->get("position").toPointF() + delta;
0707             item.node->get_property("position")->set_undoable(point);
0708         }
0709     }
0710 }
0711 
0712 void GlaxnimateWindow::Private::dropped(const QMimeData* data)
0713 {
0714     if ( data->hasFormat("application/x.glaxnimate-asset-uuid") )
0715     {
0716         command::UndoMacroGuard guard(i18n("Drop"), current_document.get(), false);
0717 
0718         item_models::DragDecoder<> decoder(data->data("application/x.glaxnimate-asset-uuid"), current_document.get());
0719 
0720         std::vector<model::VisualNode*> selection;
0721 
0722         auto cont = parent->current_shape_container();
0723         int position = cont->size();
0724 
0725         for ( auto asset : decoder )
0726         {
0727             std::unique_ptr<model::ShapeElement> element;
0728 
0729             if ( auto precomp = asset->cast<model::Composition>() )
0730             {
0731                 auto layer = std::make_unique<model::PreCompLayer>(current_document.get());
0732                 layer->composition.set(precomp);
0733                 layer->size.set(precomp->size());
0734                 element = std::move(layer);
0735             }
0736             else if ( auto bmp = asset->cast<model::Bitmap>() )
0737             {
0738                 auto image = std::make_unique<model::Image>(current_document.get());
0739                 image->image.set(bmp);
0740                 element = std::move(image);
0741             }
0742 
0743             if ( element )
0744             {
0745                 selection.push_back(element.get());
0746                 element->name.set(asset->name.get());
0747                 guard.start();
0748                 current_document->push_command(new command::AddShape(cont, std::move(element), position));
0749             }
0750         }
0751 
0752         if ( !selection.empty() )
0753             scene.user_select(selection, graphics::DocumentScene::Replace);
0754     }
0755 
0756 }
0757 
0758 static bool get_text(model::Group* group, std::vector<model::TextShape*>& shapes)
0759 {
0760     bool found = false;
0761 
0762     for ( const auto& node : group->shapes )
0763     {
0764         auto mo = node->metaObject();
0765 
0766         if ( mo->inherits(&model::TextShape::staticMetaObject) )
0767         {
0768             shapes.push_back(static_cast<model::TextShape*>(node.get()));
0769             found = true;
0770         }
0771         else if ( mo->inherits(&model::Group::staticMetaObject) )
0772         {
0773             found = get_text(static_cast<model::Group*>(node.get()), shapes) && found;
0774         }
0775     }
0776 
0777     return found;
0778 }
0779 
0780 void GlaxnimateWindow::Private::text_put_on_path()
0781 {
0782     std::vector<model::TextShape*> shapes;
0783     model::ShapeElement* path = nullptr;
0784 
0785     for ( const auto& node : scene.selection() )
0786     {
0787         auto mo = node->metaObject();
0788 
0789         if ( mo->inherits(&model::TextShape::staticMetaObject) )
0790         {
0791             shapes.push_back(static_cast<model::TextShape*>(node));
0792         }
0793         else if ( mo->inherits(&model::Group::staticMetaObject) )
0794         {
0795             if ( !get_text(static_cast<model::Group*>(node), shapes) )
0796                 path = static_cast<model::ShapeElement*>(node);
0797         }
0798         else if (
0799             mo->inherits(&model::ShapeElement::staticMetaObject) &&
0800             !mo->inherits(&model::Image::staticMetaObject) &&
0801             !mo->inherits(&model::PreCompLayer::staticMetaObject) &&
0802             !mo->inherits(&model::Styler::staticMetaObject)
0803         )
0804         {
0805             path = static_cast<model::ShapeElement*>(node);
0806         }
0807     }
0808 
0809     if ( shapes.empty() || !path )
0810         return;
0811 
0812     command::UndoMacroGuard guard(i18n("Put text on path"), current_document.get());
0813     for ( auto shape : shapes )
0814         shape->path.set_undoable(QVariant::fromValue(path));
0815 }
0816 
0817 void GlaxnimateWindow::Private::text_remove_from_path()
0818 {
0819     std::vector<model::TextShape*> shapes;
0820 
0821     for ( const auto& node : scene.selection() )
0822     {
0823         auto mo = node->metaObject();
0824 
0825         if ( mo->inherits(&model::TextShape::staticMetaObject) )
0826             shapes.push_back(static_cast<model::TextShape*>(node));
0827         else if ( mo->inherits(&model::Group::staticMetaObject) )
0828             get_text(static_cast<model::Group*>(node), shapes);
0829     }
0830 
0831     if ( shapes.empty() )
0832         return;
0833 
0834     command::UndoMacroGuard guard(i18n("Remove text from path"), current_document.get());
0835     for ( auto shape : shapes )
0836         shape->path.set_undoable({});
0837 }