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

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 "node_menu.hpp"
0008 
0009 #include <QDesktopServices>
0010 #include <QInputDialog>
0011 #include <QActionGroup>
0012 
0013 #include <KLocalizedString>
0014 
0015 #include "model/shapes/group.hpp"
0016 #include "model/shapes/image.hpp"
0017 #include "model/shapes/precomp_layer.hpp"
0018 #include "model/shapes/fill.hpp"
0019 #include "model/shapes/stroke.hpp"
0020 #include "model/shapes/repeater.hpp"
0021 #include "model/shapes/trim.hpp"
0022 #include "model/shapes/text.hpp"
0023 #include "model/shapes/inflate_deflate.hpp"
0024 #include "model/shapes/round_corners.hpp"
0025 #include "model/shapes/offset_path.hpp"
0026 #include "model/shapes/zig_zag.hpp"
0027 
0028 #include "model/assets/assets.hpp"
0029 
0030 #include "command/structure_commands.hpp"
0031 #include "command/animation_commands.hpp"
0032 #include "command/property_commands.hpp"
0033 #include "command/shape_commands.hpp"
0034 #include "command/undo_macro_guard.hpp"
0035 
0036 #include "widgets/dialogs/shape_parent_dialog.hpp"
0037 
0038 using namespace glaxnimate::gui;
0039 using namespace glaxnimate;
0040 
0041 namespace {
0042 
0043 class ResetTransform
0044 {
0045 public:
0046     void operator()() const
0047     {
0048         doc->push_command(new command::SetMultipleAnimated(
0049             i18n("Reset Transform"),
0050             true,
0051             {
0052                 &trans->position,
0053                 &trans->scale,
0054                 &trans->rotation
0055             },
0056             trans->anchor_point.get(),
0057             QVector2D(1, 1),
0058             0
0059         ));
0060     }
0061 
0062     model::Document* doc;
0063     model::Transform* trans;
0064 };
0065 
0066 QAction* action_for_node(model::DocumentNode* node, model::DocumentNode* selected, QMenu* parent, QActionGroup* group)
0067 {
0068     QAction* act = new QAction(parent);
0069     if ( node )
0070     {
0071         act->setIcon(node->instance_icon());
0072         act->setText(node->object_name());
0073     }
0074     else
0075     {
0076         act->setIcon(QIcon::fromTheme("edit-none"));
0077         act->setText(i18n("None"));
0078     }
0079 
0080     act->setActionGroup(group);
0081     act->setCheckable(true);
0082     act->setChecked(node == selected);
0083     act->setData(QVariant::fromValue(node));
0084     parent->addAction(act);
0085     return act;
0086 }
0087 
0088 void move_action(
0089     NodeMenu* menu,
0090     model::ShapeElement* node,
0091     command::ReorderCommand::SpecialPosition pos
0092 )
0093 {
0094     QIcon icon;
0095     QString label;
0096 
0097     switch ( pos )
0098     {
0099         case command::ReorderCommand::MoveTop:
0100             icon = QIcon::fromTheme("layer-top");
0101             label = i18n("Move to Top");
0102             break;
0103         case command::ReorderCommand::MoveUp:
0104             icon = QIcon::fromTheme("layer-raise");
0105             label = i18n("Raise");
0106             break;
0107         case command::ReorderCommand::MoveDown:
0108             icon = QIcon::fromTheme("layer-lower");
0109             label = i18n("Lower");
0110             break;
0111         case command::ReorderCommand::MoveBottom:
0112             icon = QIcon::fromTheme("layer-bottom");
0113             label = i18n("Move to Bottom");
0114             break;
0115     }
0116 
0117     QAction* act = menu->addAction(icon, label, menu, [node, pos]{
0118         node->push_command(new command::ReorderCommand(node, pos));
0119     });
0120 
0121     int position = pos;
0122     if ( !command::ReorderCommand::resolve_position(node, position) )
0123         act->setEnabled(false);
0124 }
0125 
0126 QMenu* menu_ref_property(const QIcon& icon, const QString& text, QWidget* parent, model::ReferencePropertyBase* prop)
0127 {
0128     QMenu* menu = new QMenu(text, parent);
0129     menu->setIcon(icon);
0130     QActionGroup* group = new QActionGroup(parent);
0131     auto value = prop->value().value<model::DocumentNode*>();
0132     for ( auto other_lay : prop->valid_options() )
0133         action_for_node(other_lay, value, menu, group);
0134 
0135     QObject::connect(menu, &QMenu::triggered, parent, [prop](QAction* act){
0136         prop->object()->push_command(
0137             new command::SetPropertyValue(
0138                 prop,
0139                 prop->value(),
0140                 act->data(),
0141                 true
0142             )
0143         );
0144     });
0145 
0146     return menu;
0147 }
0148 
0149 template<class ToType>
0150 void convert_group_callback(int, ToType*){}
0151 
0152 template<class Callback, class ToType>
0153 void convert_group_callback(const Callback& callback, ToType* layer)
0154 {
0155     callback(layer);
0156 }
0157 
0158 template<class ToType> void convert_group_apply_settings(ToType*, model::Group*){}
0159 
0160 template<>
0161 void convert_group_apply_settings<model::Layer>(model::Layer* layer, model::Group* from)
0162 {
0163     auto comp = from->owner_composition();
0164     layer->animation->first_frame.set(comp->animation->first_frame.get());
0165     layer->animation->last_frame.set(comp->animation->last_frame.get());
0166 }
0167 
0168 template<class ToType, class Callback = int>
0169 class ConvertGroupType
0170 {
0171 public:
0172     ConvertGroupType(model::Group* from, Callback callback = {})
0173         : from(from), owner(static_cast<model::ShapeListProperty*>(from->docnode_parent()->get_property("shapes"))),
0174         callback(std::move(callback))
0175     {}
0176 
0177     void operator()() const
0178     {
0179         convert();
0180     }
0181 
0182     void convert() const
0183     {
0184         auto to = new ToType(from->document());
0185         std::unique_ptr<model::ShapeElement> uto(to);
0186 
0187         for ( auto prop : to->properties() )
0188         {
0189             if ( prop != &to->uuid && prop != &to->shapes )
0190             {
0191                 if ( auto src = from->get_property(prop->name()) )
0192                     prop->assign_from(src);
0193             }
0194         }
0195 
0196         if ( !from->is_instance<model::Layer>() )
0197             convert_group_apply_settings(to, from);
0198 
0199         command::UndoMacroGuard guard(i18n("Convert %1 to %2", from->name.get(), to->type_name_human()), from->document());
0200         from->push_command(new command::AddObject(owner, std::move(uto), owner->index_of(from)));
0201         std::vector<model::ShapeElement*> shapes;
0202         shapes.reserve(from->shapes.size());
0203         for ( const auto& shape : from->shapes )
0204             shapes.push_back(shape.get());
0205         for ( std::size_t i = 0; i < shapes.size(); i++ )
0206             from->push_command(new command::MoveObject(shapes[i], &from->shapes, &to->shapes, i));
0207         from->push_command(new command::RemoveObject<model::ShapeElement>(from, owner));
0208         convert_group_callback(callback, to);
0209     }
0210 
0211 private:
0212     model::Group* from;
0213     model::ShapeListProperty* owner;
0214     Callback callback;
0215 };
0216 
0217 void togglable_action(QMenu* menu, model::Property<bool>* prop, const QString& icon, const QString& label)
0218 {
0219     auto action = menu->addAction(QIcon::fromTheme(icon), label, menu, [prop](bool value){
0220         prop->set_undoable(value);
0221     });
0222     action->setCheckable(true);
0223     action->setChecked(prop->get());
0224 }
0225 
0226 template<class T>
0227 void add_child_action(QMenu* menu, model::Group* group)
0228 {
0229     menu->addAction(T::static_tree_icon(), T::static_type_name_human(), [group]{
0230         auto object = std::make_unique<T>(group->document());
0231         group->document()->set_best_name(object.get());
0232         group->push_command(new command::AddObject<model::ShapeElement>(&group->shapes, std::move(object), 0));
0233     });
0234 }
0235 
0236 void actions_group(QMenu* menu, GlaxnimateWindow* window, model::Group* group)
0237 {
0238     QMenu* menu_add = new QMenu(i18n("Add"), menu);
0239     menu_add->setIcon(QIcon::fromTheme("list-add"));
0240     menu->addAction(menu_add->menuAction());
0241     add_child_action<model::Fill>(menu_add, group);
0242     add_child_action<model::Stroke>(menu_add, group);
0243     menu_add->addSeparator();
0244     add_child_action<model::Trim>(menu_add, group);
0245     add_child_action<model::Repeater>(menu_add, group);
0246     add_child_action<model::InflateDeflate>(menu_add, group);
0247     add_child_action<model::RoundCorners>(menu_add, group);
0248     add_child_action<model::OffsetPath>(menu_add, group);
0249     add_child_action<model::ZigZag>(menu_add, group);
0250 
0251     menu->addSeparator();
0252 
0253     menu->addAction(QIcon::fromTheme("transform-move"), i18n("Reset Transform"), menu,
0254         ResetTransform{group->document(), group->transform.get()}
0255     );
0256     /// \todo better icon
0257     auto ao = menu->addAction(QIcon::fromTheme("path-reverse"), i18n("Auto Orient"), menu, [group](bool check) {
0258         group->auto_orient.set_undoable(check);
0259     });
0260     ao->setCheckable(true);
0261     ao->setChecked(group->auto_orient.get());
0262 
0263     model::Layer* lay = qobject_cast<model::Layer*>(group);
0264     if ( lay )
0265     {
0266         menu->addAction(QIcon::fromTheme("timeline-use-zone-on"), i18n("Span All Frames"), menu, [lay]{
0267             command::UndoMacroGuard guard(i18n("Span All Frames"), lay->document());
0268             lay->animation->first_frame.set_undoable(
0269                 lay->owner_composition()->animation->first_frame.get()
0270             );
0271             lay->animation->last_frame.set_undoable(
0272                 lay->owner_composition()->animation->last_frame.get()
0273             );
0274         });
0275 
0276         menu->addSeparator();
0277         menu->addAction(menu_ref_property(QIcon::fromTheme("go-parent-folder"), i18n("Parent"), menu, &lay->parent)->menuAction());
0278         menu->addAction(QIcon::fromTheme("object-group"), i18n("Convert to Group"), menu, ConvertGroupType<model::Group>(lay));
0279         menu->addAction(QIcon::fromTheme("component"), i18n("Precompose"), menu, [window, lay]{
0280             window->shape_to_composition(lay);
0281         });
0282 
0283         if ( !lay->mask->has_mask() )
0284         {
0285             menu->addAction(QIcon::fromTheme("path-mask-edit"), i18n("Convert to Mask"), menu, [lay]{
0286                 lay->mask->mask.set_undoable(true);
0287             });
0288         }
0289         else
0290         {
0291             menu->addAction(QIcon::fromTheme("path-mask-edit"), i18n("Remove Mask"), menu, [lay]{
0292                 lay->mask->mask.set_undoable(false);
0293             });
0294         }
0295     }
0296     else
0297     {
0298         menu->addSeparator();
0299 
0300         menu->addAction(QIcon::fromTheme("folder"), i18n("Convert to Layer"), menu, ConvertGroupType<model::Layer>(group));
0301         auto callback = [](model::Layer* lay){
0302             lay->mask->mask.set_undoable(true);
0303         };
0304         menu->addAction(QIcon::fromTheme("path-mask-edit"), i18n("Convert to Mask"), menu,
0305                         ConvertGroupType<model::Layer, decltype(callback)>(group, callback));
0306 
0307     }
0308 
0309     menu->addAction(QIcon::fromTheme("object-to-path"), i18n("Convert to Path"), menu, [window, group]{ window->convert_to_path(group);});
0310 }
0311 
0312 void actions_bitmap(QMenu* menu, GlaxnimateWindow* window, model::Bitmap* bmp, model::Image* shape)
0313 {
0314     menu->addAction(QIcon::fromTheme("mail-attachment-symbolic"), i18n("Embed"), menu, [bmp]{
0315             bmp->embed(true);
0316     })->setEnabled(bmp && !bmp->embedded());
0317 
0318     menu->addAction(QIcon::fromTheme("editimage"), i18n("Open with External Application"), menu, [bmp, window]{
0319         if ( !QDesktopServices::openUrl(bmp->to_url()) )
0320             window->warning(i18n("Could not find suitable application, check your system settings."));
0321     })->setEnabled(bmp);
0322 
0323 
0324     menu->addAction(QIcon::fromTheme("document-open"), i18n("From File..."), menu, [bmp, window, shape]{
0325         QString filename = window->get_open_image_file(i18n("Update Image"), bmp ? bmp->file_info().absolutePath() : "");
0326         if ( filename.isEmpty() )
0327             return;
0328 
0329         command::UndoMacroGuard macro(i18n("Update Image"), window->document());
0330         if ( bmp )
0331         {
0332             bmp->data.set_undoable(QByteArray());
0333             bmp->filename.set_undoable(filename);
0334         }
0335         else if ( shape )
0336         {
0337             auto img = window->document()->assets()->add_image_file(filename, false);
0338             if ( img )
0339                 shape->image.set_undoable(QVariant::fromValue(img));
0340         }
0341     });
0342 }
0343 
0344 void actions_image(QMenu* menu, GlaxnimateWindow* window, model::Image* image)
0345 {
0346     menu->addAction(QIcon::fromTheme("transform-move"), i18n("Reset Transform"), menu,
0347         ResetTransform{image->document(), image->transform.get()}
0348     );
0349 
0350     menu->addSeparator();
0351 
0352     menu->addAction(menu_ref_property(QIcon::fromTheme("folder-pictures"), i18n("Image"), menu, &image->image)->menuAction());
0353 
0354     actions_bitmap(menu, window, image->image.get(), image);
0355 
0356     menu->addAction(QIcon::fromTheme("bitmap-trace"), i18n("Trace Bitmap..."), menu, [image, window]{
0357         window->trace_dialog(image);
0358     });
0359 }
0360 
0361 void actions_precomp(QMenu* menu, GlaxnimateWindow*, model::PreCompLayer* lay)
0362 {
0363     menu->addAction(QIcon::fromTheme("transform-move"), i18n("Reset Transform"), menu,
0364         ResetTransform{lay->document(), lay->transform.get()}
0365     );
0366 
0367     menu->addAction(QIcon::fromTheme("edit-rename"), i18n("Rename from Composition"), menu, [lay]{
0368         if ( lay->composition.get() )
0369             lay->name.set_undoable(lay->composition->object_name());
0370     });
0371     menu->addAction(QIcon::fromTheme("archive-extract"), i18n("Decompose"), menu, [lay]{
0372         command::UndoMacroGuard guard(i18n("Decompose"), lay->document());
0373 
0374         auto comp = lay->composition.get();
0375 
0376         if ( comp )
0377         {
0378             int index = lay->owner()->index_of(lay);
0379             for ( const auto& child : comp->shapes )
0380             {
0381                 std::unique_ptr<model::ShapeElement> clone(static_cast<model::ShapeElement*>(child->clone().release()));
0382                 clone->refresh_uuid();
0383                 lay->push_command(new command::AddShape(lay->owner(), std::move(clone), ++index));
0384             }
0385         }
0386 
0387         lay->push_command(new command::RemoveShape(lay, lay->owner()));
0388 
0389         if ( comp && comp->users().empty() )
0390             lay->push_command(new command::RemoveObject(comp, &lay->document()->assets()->compositions->values));
0391     });
0392 }
0393 
0394 void actions_text(QMenu* menu, model::TextShape* text)
0395 {
0396     menu->addAction(QIcon::fromTheme("text-remove-from-path"), i18n("Remove from Path"), text, [text]{
0397         text->path.set_undoable(QVariant());
0398     })->setEnabled(text->path.get());
0399 }
0400 
0401 
0402 void time_stretch_dialog(model::Object* object, QWidget* parent)
0403 {
0404     QInputDialog dialog(parent);
0405     dialog.setInputMode(QInputDialog::DoubleInput);
0406     dialog.setDoubleMinimum(0.001);
0407     dialog.setDoubleMaximum(999);
0408     dialog.setDoubleValue(1);
0409     dialog.setDoubleDecimals(3);
0410     dialog.setWindowTitle(i18n("Stretch time"));
0411     dialog.setLabelText(i18n("Speed Multiplier"));
0412     if ( dialog.exec() )
0413         object->push_command(new command::StretchTimeCommand(object, 1/dialog.doubleValue()));
0414 }
0415 
0416 } // namespace
0417 
0418 
0419 
0420 NodeMenu::NodeMenu(model::DocumentNode* node, GlaxnimateWindow* window, QWidget* parent)
0421     : QMenu(node->object_name(), parent)
0422 {
0423     setIcon(node->tree_icon());
0424     addSection(node->tree_icon(), node->object_name());
0425 
0426     if ( auto visual = qobject_cast<model::VisualNode*>(node) )
0427     {
0428         togglable_action(this, &visual->visible, "view-visible", i18nc("@option:check", "Visible"));
0429         togglable_action(this, &visual->locked, "object-locked", i18nc("@option:check", "Locked"));
0430         addSeparator();
0431 
0432         if ( auto shape = qobject_cast<model::ShapeElement*>(node) )
0433         {
0434             addAction(QIcon::fromTheme("edit-delete-remove"), i18nc("@action:inmenu", "Delete"), this, [shape]{
0435                 shape->push_command(new command::RemoveShape(shape, shape->owner()));
0436             });
0437 
0438             addAction(QIcon::fromTheme("edit-duplicate"), i18nc("@action:inmenu", "Duplicate"), this, [shape, window]{
0439                 auto cmd = command::duplicate_shape(shape);
0440                 shape->push_command(cmd);
0441                 window->set_current_document_node(cmd->object());
0442             });
0443 
0444             addSeparator();
0445 
0446             move_action(this, shape, command::ReorderCommand::MoveTop);
0447             move_action(this, shape, command::ReorderCommand::MoveUp);
0448             move_action(this, shape, command::ReorderCommand::MoveDown);
0449             move_action(this, shape, command::ReorderCommand::MoveBottom);
0450 
0451             addAction(QIcon::fromTheme("selection-move-to-layer-above"), i18nc("@action:inmenu", "Move to..."), this, [shape, window]{
0452                 if ( auto parent = ShapeParentDialog(window->model(), window).get_shape_parent() )
0453                 {
0454                     if ( shape->owner() != parent )
0455                         shape->push_command(new command::MoveShape(shape, shape->owner(), parent, parent->size()));
0456                 }
0457             });
0458 
0459             addSeparator();
0460 
0461             if ( auto group = qobject_cast<model::Group*>(shape) )
0462             {
0463                 actions_group(this, window, group);
0464             }
0465             else if ( auto image = qobject_cast<model::Image*>(shape) )
0466             {
0467                 actions_image(this, window, image);
0468             }
0469             else if ( auto lay = qobject_cast<model::PreCompLayer*>(shape) )
0470             {
0471                 actions_precomp(this, window, lay);
0472             }
0473             else
0474             {
0475                 if ( auto text = shape->cast<model::TextShape>() )
0476                     actions_text(this, text);
0477 
0478                 addSeparator();
0479                 addAction(QIcon::fromTheme("object-to-path"), i18nc("@action:inmenu", "Convert to Path"), this, [window, shape]{ window->convert_to_path(shape);});
0480             }
0481         }
0482         else if ( qobject_cast<model::Composition*>(node) )
0483         {
0484             addAction(
0485                 window->create_layer_menu()->menuAction()
0486             );
0487         }
0488     }
0489     else if ( auto image = qobject_cast<model::Bitmap*>(node) )
0490     {
0491         actions_bitmap(this, window, image, nullptr);
0492     }
0493 
0494 
0495     addSeparator();
0496     addAction(QIcon::fromTheme("speedometer"), i18nc("@action:inmenu", "Change speed"), this, [node, parent]{ time_stretch_dialog(node, parent); });
0497 }