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 }