File indexing completed on 2024-12-15 04:01:00
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 "main_window.hpp" 0008 #include "ui_main_window.h" 0009 0010 #include <QMessageBox> 0011 #include <QPointer> 0012 #include <QScreen> 0013 #include <QMenu> 0014 #include <QStandardPaths> 0015 0016 #include "model/document.hpp" 0017 #include "model/shapes/fill.hpp" 0018 #include "model/shapes/stroke.hpp" 0019 0020 #include "io/glaxnimate/glaxnimate_format.hpp" 0021 #include "io/lottie/tgs_format.hpp" 0022 0023 #include "command/undo_macro_guard.hpp" 0024 #include "command/structure_commands.hpp" 0025 #include "command/animation_commands.hpp" 0026 0027 #include "graphics/document_scene.hpp" 0028 #include "tools/base.hpp" 0029 #include "item_models/document_node_model.hpp" 0030 #include "widgets/dialogs/import_export_dialog.hpp" 0031 #include "widgets/flow_layout.hpp" 0032 #include "widgets/docks/layer_view.hpp" 0033 #include "style/property_delegate.hpp" 0034 #include "utils/pseudo_mutex.hpp" 0035 #include "style/scroll_area_event_filter.hpp" 0036 #include "emoji/emoji_set.hpp" 0037 0038 0039 #include "android_file_picker.hpp" 0040 #include "format_selection_dialog.hpp" 0041 #include "document_opener.hpp" 0042 #include "sticker_pack_builder_dialog.hpp" 0043 #include "help_dialog.hpp" 0044 #include "timeline_slider.hpp" 0045 #include "better_toolbox_widget.hpp" 0046 0047 0048 using namespace glaxnimate::android; 0049 0050 class MainWindow::Private 0051 { 0052 public: 0053 MainWindow* parent; 0054 Ui::MainWindow ui; 0055 gui::graphics::DocumentScene scene; 0056 std::unique_ptr<model::Document> current_document; 0057 gui::tools::Tool* active_tool = nullptr; 0058 0059 QPointer<model::BrushStyle> main_brush; 0060 QPointer<model::BrushStyle> secondary_brush; 0061 model::Composition* comp = nullptr; 0062 QPointer<model::VisualNode> current_node; 0063 QPointer<model::Fill> current_fill; 0064 QPointer<model::Stroke> current_stroke; 0065 0066 io::Options export_options; 0067 bool current_document_has_file = false; 0068 AndroidFilePicker file_picker; 0069 FormatSelectionDialog format_selector; 0070 DocumentOpener document_opener; 0071 0072 QHBoxLayout* layout_tools = nullptr; 0073 QHBoxLayout* layout_actions = nullptr; 0074 QHBoxLayout* layout_edit_actions = nullptr; 0075 QAction* action_undo = nullptr; 0076 QAction* action_redo = nullptr; 0077 StickerPackBuilderDialog telegram_export_dialog; 0078 gui::style::PropertyDelegate property_delegate; 0079 QActionGroup *view_actions = nullptr; 0080 TimelineSlider* timeline_slider; 0081 std::vector<QSpacerItem*> toolbar_spacers; 0082 gui::LayerView* layer_view = nullptr; 0083 gui::item_models::DocumentNodeModel document_node_model; 0084 utils::PseudoMutex updating_selection; 0085 0086 Private(MainWindow* parent) 0087 : parent(parent), 0088 file_picker(parent), 0089 document_opener(parent) 0090 { 0091 ui.setupUi(parent); 0092 0093 timeline_slider = new TimelineSlider(ui.time_container); 0094 ui.time_container_layout->insertWidget(1, timeline_slider); 0095 0096 layout_actions = init_toolbar_layout(); 0097 ui.widget_actions->setLayout(layout_actions); 0098 0099 layout_tools = init_toolbar_layout(); 0100 ui.layout_tools_container->addLayout(layout_tools); 0101 0102 init_toolbar_tools(); 0103 init_toolbar_actions(); 0104 init_toolbar_edit(); 0105 0106 ui.canvas->set_tool_target(parent); 0107 ui.canvas->setScene(&scene); 0108 0109 ui.fill_style_widget->set_current_color(QColor("#3250b0")); 0110 ui.stroke_style_widget->set_color(QColor("#1d2848")); 0111 ui.stroke_style_widget->set_stroke_width(6); 0112 0113 connect( 0114 QGuiApplication::primaryScreen(), 0115 &QScreen::primaryOrientationChanged, 0116 parent, 0117 [this]{adjust_size();} 0118 ); 0119 0120 setup_document_new(); 0121 0122 export_options.path = default_save_path(); 0123 0124 connect(&file_picker, &glaxnimate::android::AndroidFilePicker::open_selected, parent, [this](const QUrl& url, bool is_import){ 0125 open_url(url, is_import); 0126 }); 0127 connect(&file_picker, &glaxnimate::android::AndroidFilePicker::save_selected, parent, 0128 [this](const QUrl& url, bool is_export){ 0129 save_url(url, is_export); 0130 }); 0131 0132 connect(&scene, &gui::graphics::DocumentScene::node_user_selected, parent, [this](const std::vector<model::VisualNode*>& selected, const std::vector<model::VisualNode*>& deselected){ 0133 this->parent->update_selection(selected, deselected); 0134 if ( !selected.empty() ) 0135 this->parent->set_current_document_node(selected.back()); 0136 else 0137 this->parent->set_current_document_node(nullptr); 0138 }); 0139 connect(layer_view, &gui::LayerView::selection_changed, parent, [this](const std::vector<model::VisualNode*>& selected, const std::vector<model::VisualNode*>& deselected){ 0140 this->parent->update_selection(selected, deselected); 0141 }); 0142 connect(layer_view, &gui::LayerView::current_node_changed, parent, [this](model::VisualNode* selected){ 0143 this->parent->set_current_document_node(selected); 0144 }); 0145 0146 (new gui::ScrollAreaEventFilter(ui.property_widget))->setParent(ui.property_widget); 0147 0148 int side_width = ui.fill_style_widget->sizeHint().width(); 0149 ui.stroke_style_widget->setMinimumWidth(side_width); 0150 ui.property_widget->setMinimumWidth(400); 0151 0152 adjust_size(); 0153 } 0154 0155 QHBoxLayout* init_toolbar_layout() 0156 { 0157 auto lay = new QHBoxLayout(); 0158 lay->setSpacing(0); 0159 lay->setMargin(0); 0160 return lay; 0161 } 0162 0163 void setup_document_new() 0164 { 0165 clear_document(); 0166 0167 current_document = std::make_unique<model::Document>(""); 0168 auto main = current_document->assets()->compositions->values.insert(std::make_unique<model::Composition>(current_document.get())); 0169 main->width.set(512); 0170 main->height.set(512); 0171 main->animation->last_frame.set(180); 0172 main->fps.set(60); 0173 0174 auto opts = current_document->io_options(); 0175 opts.format = io::glaxnimate::GlaxnimateFormat::instance(); 0176 opts.path = default_save_path(); 0177 current_document->set_io_options(opts); 0178 0179 auto layer = std::make_unique<model::Layer>(current_document.get()); 0180 layer->animation->last_frame.set(180); 0181 layer->name.set(layer->type_name_human()); 0182 QPointF pos( 0183 main->width.get() / 2.0, 0184 main->height.get() / 2.0 0185 ); 0186 layer->transform.get()->anchor_point.set(pos); 0187 layer->transform.get()->position.set(pos); 0188 0189 main->shapes.insert(std::move(layer), 0); 0190 0191 setup_document(); 0192 } 0193 0194 void clear_document() 0195 { 0196 if ( active_tool ) 0197 active_tool->close_document_event({ui.canvas, &scene, parent}); 0198 ui.play_controls->pause(); 0199 document_node_model.set_document(nullptr); 0200 scene.set_document(nullptr); 0201 action_redo->setEnabled(false); 0202 action_undo->setEnabled(false); 0203 clear_property_widgets(); 0204 ui.stroke_style_widget->set_current(nullptr); 0205 ui.fill_style_widget->set_current(nullptr); 0206 comp = nullptr; 0207 current_document.reset(); 0208 current_document_has_file = false; 0209 } 0210 0211 void setup_document() 0212 { 0213 scene.set_document(current_document.get()); 0214 document_node_model.set_document(current_document.get()); 0215 if ( current_document->assets()->compositions->values.empty() ) 0216 comp = current_document->assets()->compositions->values.insert(std::make_unique<model::Composition>(current_document.get())); 0217 else 0218 comp = current_document->assets()->compositions->values[0]; 0219 current_document->set_record_to_keyframe(true); 0220 0221 // Undo Redo 0222 connect(action_undo, &QAction::triggered, ¤t_document->undo_stack(), &QUndoStack::undo); 0223 connect(¤t_document->undo_stack(), &QUndoStack::canUndoChanged, action_undo, &QAction::setEnabled); 0224 connect(action_redo, &QAction::triggered, ¤t_document->undo_stack(), &QUndoStack::redo); 0225 connect(¤t_document->undo_stack(), &QUndoStack::canRedoChanged, action_redo, &QAction::setEnabled); 0226 0227 // play controls 0228 auto first_frame = comp->animation->first_frame.get(); 0229 auto last_frame = comp->animation->last_frame.get(); 0230 ui.play_controls->set_range(first_frame, last_frame); 0231 ui.play_controls->set_record_enabled(current_document->record_to_keyframe()); 0232 QObject::connect(comp->animation.get(), &model::AnimationContainer::first_frame_changed, ui.play_controls, [this](float frame){ui.play_controls->set_min(frame);}); 0233 QObject::connect(comp->animation.get(), &model::AnimationContainer::last_frame_changed, ui.play_controls, [this](float frame){ui.play_controls->set_max(frame);}); 0234 QObject::connect(comp, &model::Composition::fps_changed, ui.play_controls, &gui::FrameControlsWidget::set_fps); 0235 QObject::connect(ui.play_controls, &gui::FrameControlsWidget::frame_selected, current_document.get(), [this](int frame){current_document->set_current_time(frame);}); 0236 QObject::connect(current_document.get(), &model::Document::current_time_changed, ui.play_controls, [this](float frame){ui.play_controls->set_frame(frame);}); 0237 QObject::connect(current_document.get(), &model::Document::record_to_keyframe_changed, ui.play_controls, &gui::FrameControlsWidget::set_record_enabled); 0238 QObject::connect(ui.play_controls, &gui::FrameControlsWidget::record_toggled, current_document.get(), &model::Document::set_record_to_keyframe); 0239 0240 // slider 0241 timeline_slider->setMinimum(first_frame); 0242 timeline_slider->setMaximum(last_frame); 0243 timeline_slider->setValue(first_frame); 0244 QObject::connect(timeline_slider, &QAbstractSlider::valueChanged, current_document.get(), [this](int value){current_document->set_current_time(value);}); 0245 QObject::connect(current_document.get(), &model::Document::current_time_changed, timeline_slider, [this](float frame){timeline_slider->setValue(frame);}); 0246 0247 // Views 0248 layer_view->set_composition(comp); 0249 0250 } 0251 0252 void switch_tool(gui::tools::Tool* tool) 0253 { 0254 if ( !tool || tool == active_tool ) 0255 return; 0256 0257 if ( !tool->get_action()->isChecked() ) 0258 tool->get_action()->setChecked(true); 0259 /* 0260 if ( active_tool ) 0261 { 0262 for ( const auto& widget : tool_widgets[active_tool->id()] ) 0263 { 0264 widget->setVisible(false); 0265 widget->setEnabled(false); 0266 } 0267 0268 for ( const auto& action : tool_actions[active_tool->id()] ) 0269 { 0270 action->setEnabled(false); 0271 } 0272 } 0273 0274 0275 for ( const auto& widget : tool_widgets[tool->id()] ) 0276 { 0277 widget->setVisible(true); 0278 widget->setEnabled(true); 0279 } 0280 0281 for ( const auto& action : tool_actions[tool->id()] ) 0282 { 0283 action->setEnabled(true); 0284 } 0285 */ 0286 active_tool = tool; 0287 scene.set_active_tool(tool); 0288 ui.canvas->set_active_tool(tool); 0289 /* 0290 ui.tool_settings_widget->setCurrentWidget(tool->get_settings_widget()); 0291 if ( active_tool->group() == tools::Registry::Draw || active_tool->group() == tools::Registry::Shape ) 0292 widget_current_style->clear_gradients();*/ 0293 } 0294 0295 QToolButton* action_button(QAction* action) 0296 { 0297 auto btn = new QToolButton; 0298 btn->setDefaultAction(action); 0299 return btn; 0300 } 0301 0302 0303 QToolButton* action_button_exclusive_opt(QAction* action) 0304 { 0305 #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) 0306 auto btn = new QToolButton; 0307 btn->setIcon(action->icon()); 0308 btn->setText(action->text()); 0309 btn->setCheckable(action->isCheckable()); 0310 connect(action, &QAction::toggled, btn, &QAbstractButton::setChecked); 0311 0312 connect(btn, &QAbstractButton::clicked, action, [action](bool b){ 0313 if ( b ) 0314 action->trigger(); 0315 else 0316 action->setChecked(false); 0317 }); 0318 return btn; 0319 #else 0320 return action_button(action); 0321 #endif 0322 } 0323 0324 QMenu* action_menu(const QIcon& icon, const QString& label, QHBoxLayout* container) 0325 { 0326 QMenu *menu = new QMenu(parent); 0327 menu->setTitle(label); 0328 menu->setIcon(icon); 0329 0330 QToolButton* button = new QToolButton; 0331 button->setMenu(menu); 0332 button->setIcon(icon); 0333 button->setText(label); 0334 button->setPopupMode(QToolButton::InstantPopup); 0335 container->addWidget(button); 0336 0337 return menu; 0338 } 0339 0340 std::vector<QAction*> tool_actions(const gui::tools::Registry::mapped_type& group, QActionGroup *tool_actions, gui::tools::Tool*& to_activate, const gui::tools::Event& event) 0341 { 0342 std::vector<QAction*> ret; 0343 0344 for ( const auto& tool : group ) 0345 { 0346 QAction* action = tool.second->get_action(); 0347 ret.push_back(action); 0348 action->setParent(parent); 0349 action->setActionGroup(tool_actions); 0350 connect(action, &QAction::triggered, parent, &MainWindow::tool_triggered); 0351 0352 if ( !to_activate ) 0353 { 0354 to_activate = tool.second.get(); 0355 action->setChecked(true); 0356 } 0357 tool.second->retranslate(); 0358 tool.second->initialize(event); 0359 } 0360 0361 return ret; 0362 } 0363 0364 void init_toolbar_tools() 0365 { 0366 // Tool Actions 0367 QActionGroup *tool_actions_grp = new QActionGroup(parent); 0368 tool_actions_grp->setExclusive(true); 0369 0370 gui::tools::Event event{ui.canvas, &scene, parent}; 0371 gui::tools::Tool* to_activate = nullptr; 0372 0373 for ( auto action: tool_actions(gui::tools::Registry::instance()[gui::tools::Registry::Core], tool_actions_grp, to_activate, event) ) 0374 layout_tools->addWidget(action_button(action)); 0375 0376 std::map<int, const char*> icons = { 0377 {gui::tools::Registry::Draw, "draw-brush"}, 0378 {gui::tools::Registry::Shape, "shapes"}, 0379 }; 0380 0381 for ( const auto& grp : gui::tools::Registry::instance() ) 0382 { 0383 if ( grp.first == gui::tools::Registry::Core ) 0384 continue; 0385 0386 auto actions = tool_actions(grp.second, tool_actions_grp, to_activate, event); 0387 0388 if ( grp.first == gui::tools::Registry::Shape ) 0389 actions.push_back(emoji_tool_action()); 0390 0391 QIcon icon = QIcon::fromTheme(icons[grp.first]); 0392 QMenu* menu = action_menu(icon, "", layout_tools); 0393 for ( auto action: actions ) 0394 menu->addAction(action); 0395 } 0396 switch_tool(to_activate); 0397 0398 0399 // Views 0400 view_actions = new QActionGroup(parent); 0401 view_actions->setExclusive(true); 0402 0403 #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) 0404 view_actions->setExclusionPolicy(QActionGroup::ExclusionPolicy::ExclusiveOptional); 0405 #endif 0406 0407 toolbar_spacers.push_back(new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Minimum)); 0408 layout_tools->addItem(toolbar_spacers.back()); 0409 0410 layout_tools->addWidget(action_button_exclusive_opt(view_action( 0411 QIcon::fromTheme("player-time"), i18n("Timeline"), 0412 view_actions, ui.time_container, true 0413 ))); 0414 0415 layout_tools->addWidget(action_button_exclusive_opt(view_action( 0416 QIcon::fromTheme("fill-color"), i18n("Fill Style"), 0417 view_actions, ui.fill_style_widget 0418 ))); 0419 0420 layout_tools->addWidget(action_button_exclusive_opt(view_action( 0421 QIcon::fromTheme("object-stroke-style"), i18n("Stroke Style"), 0422 view_actions, ui.stroke_style_widget 0423 ))); 0424 } 0425 0426 void selection_move(command::ReorderCommand::SpecialPosition pos, const QString& msg) 0427 { 0428 auto sel = scene.cleaned_selection(); 0429 if ( sel.empty() ) 0430 return; 0431 0432 command::UndoMacroGuard guard(msg, current_document.get(), false); 0433 0434 for ( const auto& node : sel ) 0435 { 0436 auto shape = node->cast<model::ShapeElement>(); 0437 if ( !shape ) 0438 continue; 0439 0440 int position = pos; 0441 if ( !command::ReorderCommand::resolve_position(shape, position) ) 0442 continue; 0443 0444 guard.start(); 0445 node->push_command(new command::ReorderCommand(shape, pos)); 0446 } 0447 } 0448 0449 void selection_raise() 0450 { 0451 selection_move(command::ReorderCommand::MoveUp, i18n("Raise")); 0452 } 0453 0454 void selection_lower() 0455 { 0456 selection_move(command::ReorderCommand::MoveDown, i18n("Lower")); 0457 } 0458 0459 void init_toolbar_actions() 0460 { 0461 // Clipboard 0462 layout_actions->addWidget(action_button( 0463 document_action_public(QIcon::fromTheme("edit-cut"), i18n("Cut"), &MainWindow::cut) 0464 )); 0465 layout_actions->addWidget(action_button( 0466 document_action_public(QIcon::fromTheme("edit-copy"), i18n("Copy"), &MainWindow::copy) 0467 )); 0468 layout_actions->addWidget(action_button( 0469 document_action_public(QIcon::fromTheme("edit-paste"), i18n("Paste"), &MainWindow::paste) 0470 )); 0471 0472 layout_actions->addWidget(action_button( 0473 document_action_public(QIcon::fromTheme("edit-delete"), i18n("Delete Selected"), &MainWindow::delete_selected) 0474 )); 0475 0476 toolbar_spacers.push_back(new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Minimum)); 0477 layout_actions->addItem(toolbar_spacers.back()); 0478 0479 // Layer 0480 layout_actions->addWidget(action_button( 0481 document_action(QIcon::fromTheme("layer-raise"), i18n("Raise Above"), &Private::selection_raise) 0482 )); 0483 0484 layout_actions->addWidget(action_button( 0485 document_action(QIcon::fromTheme("layer-lower"), i18n("Lower Below"), &Private::selection_lower) 0486 )); 0487 0488 // Undo-redo 0489 action_undo = new QAction(QIcon::fromTheme("edit-undo"), i18n("Undo"), parent); 0490 layout_actions->addWidget(action_button(action_undo)); 0491 action_redo = new QAction(QIcon::fromTheme("edit-redo"), i18n("Redo"), parent); 0492 layout_actions->addWidget(action_button(action_redo)); 0493 } 0494 0495 void init_toolbar_edit() 0496 { 0497 layout_edit_actions = new QHBoxLayout(); 0498 layout_edit_actions->setMargin(0); 0499 layout_edit_actions->setSpacing(0); 0500 ui.widget_edit_actions->setLayout(layout_edit_actions); 0501 0502 0503 // Document actions 0504 layout_edit_actions->addWidget(action_button( 0505 document_action(QIcon::fromTheme("document-new"), i18n("New"), &Private::document_new) 0506 )); 0507 0508 QMenu* menu_open = action_menu(QIcon::fromTheme("document-open"), i18n("Open..."), layout_edit_actions); 0509 menu_open->addAction( 0510 document_action(QIcon::fromTheme("document-open"), i18n("Open"), &Private::document_open) 0511 ); 0512 menu_open->addAction( 0513 document_action(QIcon::fromTheme("document-import"), i18n("Import as Composition"), &Private::document_import) 0514 ); 0515 0516 QMenu* menu_save = action_menu(QIcon::fromTheme("document-save"), i18n("Save..."), layout_edit_actions); 0517 menu_save->addAction( 0518 document_action(QIcon::fromTheme("document-save"), i18n("Save"), &Private::document_save) 0519 ); 0520 menu_save->addAction( 0521 document_action(QIcon::fromTheme("document-save-as"), i18n("Save As"), &Private::document_save_as) 0522 ); 0523 menu_save->addAction( 0524 document_action(QIcon::fromTheme("document-export"), i18n("Export"), &Private::document_export) 0525 ); 0526 menu_save->addAction( 0527 document_action(QIcon::fromTheme("view-preview"), i18n("Save Frame as PNG"), &Private::document_frame_to_png) 0528 ); 0529 0530 layout_edit_actions->addWidget(action_button( 0531 document_action(QIcon::fromTheme("telegram"), i18n("Send to Telegram"), &Private::document_export_telegram) 0532 )); 0533 0534 // Spacer 0535 layout_edit_actions->addItem(new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Minimum)); 0536 0537 // Views 0538 layer_view = new gui::LayerView(parent); 0539 layer_view->setIconSize({80, 80}); 0540 ui.gridLayout->addWidget(layer_view, 1, 2, 2, 1); 0541 layer_view->set_base_model(&document_node_model); 0542 layout_edit_actions->addWidget(action_button_exclusive_opt(view_action( 0543 QIcon::fromTheme("dialog-layers"), i18n("Layers"), 0544 view_actions, layer_view 0545 ))); 0546 gui::ScrollAreaEventFilter::setup_scroller(layer_view); 0547 0548 layout_edit_actions->addWidget(action_button_exclusive_opt(view_action( 0549 QIcon::fromTheme("document-properties"), i18n("Advanced Properties"), 0550 view_actions, ui.property_widget 0551 ))); 0552 layer_view->setMinimumWidth(512); 0553 0554 auto help = new QAction(QIcon::fromTheme("question"), i18n("Help"), parent); 0555 layout_edit_actions->addWidget(action_button(help)); 0556 connect(help, &QAction::triggered, parent, [this]{ 0557 HelpDialog(parent).exec(); 0558 }); 0559 0560 /* 0561 // Toggler 0562 layout_tools->addWidget(action_button(view_action( 0563 QIcon::fromTheme("overflow-menu"), i18n("More Tools"), 0564 nullptr, ui.widget_actions, true 0565 ))); 0566 */ 0567 } 0568 0569 QAction* document_action(const QIcon& icon, const QString& text, void (Private::* func)()) 0570 { 0571 QAction* action = new QAction(icon, text, parent); 0572 connect(action, &QAction::triggered, parent, [this, func]{ 0573 (this->*func)(); 0574 }); 0575 return action; 0576 } 0577 0578 template<class Callback> 0579 QAction* document_action_public(const QIcon& icon, const QString& text, Callback func) 0580 { 0581 QAction* action = new QAction(icon, text, parent); 0582 connect(action, &QAction::triggered, parent, [this, func]{ 0583 (parent->*func)(); 0584 }); 0585 return action; 0586 } 0587 0588 QAction* view_action(const QIcon& icon, const QString& text, QActionGroup* group, 0589 QWidget* target, bool checked = false) 0590 { 0591 QAction* action = new QAction(icon, text, parent); 0592 action->setCheckable(true); 0593 action->setChecked(checked); 0594 target->setVisible(checked); 0595 action->setActionGroup(group); 0596 connect(action, &QAction::toggled, target, &QWidget::setVisible); 0597 0598 return action; 0599 } 0600 0601 bool save_document(bool force_dialog, bool export_opts) 0602 { 0603 io::Options opts = export_opts ? export_options : current_document->io_options(); 0604 0605 if ( export_opts || !opts.format || !opts.format->can_save() || !current_document_has_file || opts.filename.isEmpty() ) 0606 force_dialog = true; 0607 0608 if ( force_dialog ) 0609 { 0610 if ( export_opts || !opts.format ) 0611 { 0612 format_selector.setFocus(); 0613 if ( !format_selector.exec() ) 0614 return false; 0615 opts.format = format_selector.format(); 0616 } 0617 0618 QString suggestion; 0619 if ( !opts.filename.isEmpty() ) 0620 suggestion = opts.filename; 0621 else 0622 suggestion = i18n("Animation.%1", opts.format ? opts.format->extensions()[0] : "rawr"); 0623 0624 if ( file_picker.select_save(suggestion, export_opts) ) 0625 { 0626 if ( export_opts ) 0627 export_options = opts; 0628 else 0629 current_document->set_io_options(opts); 0630 0631 return true; 0632 } 0633 0634 gui::ImportExportDialog dialog(opts, parent); 0635 0636 if ( !dialog.export_dialog(comp) ) 0637 return false; 0638 0639 opts = dialog.io_options(); 0640 } 0641 0642 return save_url(QUrl(opts.filename), export_opts); 0643 } 0644 0645 void save_document_set_opts(const io::Options& opts, bool export_opts) 0646 { 0647 if ( export_opts ) 0648 { 0649 export_options = opts; 0650 } 0651 else 0652 { 0653 current_document->set_io_options(opts); 0654 current_document->undo_stack().setClean(); 0655 current_document_has_file = true; 0656 } 0657 } 0658 0659 bool close_document() 0660 { 0661 if ( current_document && !current_document->undo_stack().isClean() ) 0662 { 0663 QMessageBox warning(parent); 0664 warning.setWindowTitle(i18n("Closing Animation")); 0665 warning.setText(i18n("The animation has unsaved changes.\nDo you want to save your changes?")); 0666 warning.setInformativeText(current_document->filename()); 0667 warning.setStandardButtons(QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel); 0668 warning.setDefaultButton(QMessageBox::Save); 0669 warning.setIcon(QMessageBox::Warning); 0670 int result = warning.exec(); 0671 0672 if ( result == QMessageBox::Save ) 0673 { 0674 if ( !save_document(false, false) ) 0675 return false; 0676 } 0677 else if ( result == QMessageBox::Cancel ) 0678 { 0679 return false; 0680 } 0681 0682 // Prevent signals on the destructor 0683 current_document->undo_stack().clear(); 0684 } 0685 0686 if ( active_tool ) 0687 active_tool->close_document_event({ui.canvas, &scene, parent}); 0688 0689 clear_document(); 0690 0691 return true; 0692 } 0693 0694 void document_new() 0695 { 0696 if ( close_document() ) 0697 setup_document_new(); 0698 } 0699 0700 void document_open() 0701 { 0702 io::Options options = current_document->io_options(); 0703 0704 if ( close_document() ) 0705 { 0706 setup_document_new(); 0707 0708 if ( !file_picker.select_open(false) ) 0709 { 0710 // Ugly widget as fallback 0711 gui::ImportExportDialog dialog(options, ui.centralwidget->parentWidget()); 0712 if ( dialog.import_dialog() ) 0713 { 0714 options = dialog.io_options(); 0715 clear_document(); 0716 QFile file(options.filename); 0717 current_document = std::make_unique<model::Document>(options.filename); 0718 options.format->open(file, options.filename, current_document.get(), options.settings); 0719 current_document->set_io_options(options); 0720 0721 setup_document_open(); 0722 0723 } 0724 else 0725 { 0726 setup_document_new(); 0727 } 0728 } 0729 } 0730 } 0731 0732 void document_import() 0733 { 0734 if ( !file_picker.select_open(true) ) 0735 { 0736 // Ugly widget as fallback 0737 gui::ImportExportDialog dialog(current_document->io_options(), ui.centralwidget->parentWidget()); 0738 if ( dialog.import_dialog() ) 0739 { 0740 io::Options options = dialog.io_options(); 0741 0742 0743 model::Document imported(options.filename); 0744 QFile file(options.filename); 0745 bool ok = options.format->open(file, options.filename, &imported, options.settings); 0746 0747 if ( !ok ) 0748 { 0749 QMessageBox::warning(parent, i18n("Import File"), i18n("Could not import file")); 0750 return; 0751 } 0752 0753 parent->paste_document(&imported, i18n("Import File"), true); 0754 0755 } 0756 } 0757 } 0758 0759 void setup_document_open() 0760 { 0761 current_document_has_file = true; 0762 0763 export_options = current_document->io_options(); 0764 export_options.filename = ""; 0765 setup_document(); 0766 } 0767 0768 void document_save() 0769 { 0770 save_document(false, false); 0771 } 0772 0773 void document_save_as() 0774 { 0775 save_document(true, false); 0776 } 0777 0778 void document_export() 0779 { 0780 exporting_png = false; 0781 save_document(true, true); 0782 } 0783 0784 bool exporting_png = false; 0785 void document_frame_to_png() 0786 { 0787 exporting_png = true; 0788 if ( !file_picker.select_save(i18n("Frame %1.png", current_document->current_time()), true, "image/png") ) 0789 { 0790 exporting_png = false; 0791 } 0792 } 0793 0794 QDir default_save_path() 0795 { 0796 if ( file_picker.get_permissions() ) 0797 return QDir("/storage/emulated/0/Movies"); 0798 0799 return QStandardPaths::writableLocation(QStandardPaths::MoviesLocation); 0800 } 0801 0802 void document_export_telegram() 0803 { 0804 telegram_export_dialog.set_current_file(current_document.get()); 0805 telegram_export_dialog.exec(); 0806 } 0807 0808 void adjust_size() 0809 { 0810 int mins; 0811 qreal tool_layout_extent; 0812 0813 Qt::Orientation toolbar_orientation; 0814 QBoxLayout::Direction toolbar_direction; 0815 0816 QSize screen_size = QApplication::primaryScreen()->size(); 0817 0818 #ifndef Q_OS_ANDROID 0819 screen_size = parent->size(); 0820 #endif 0821 0822 std::pair<QSizePolicy::Policy, QSizePolicy::Policy> spacer_policy; 0823 0824 if ( screen_size.width() > screen_size.height() ) 0825 { 0826 toolbar_orientation = Qt::Vertical; 0827 toolbar_direction = QBoxLayout::TopToBottom; 0828 mins = screen_size.height() * 0.7; 0829 spacer_policy = {QSizePolicy::Minimum, QSizePolicy::Expanding}; 0830 } 0831 else 0832 { 0833 spacer_policy = {QSizePolicy::Expanding, QSizePolicy::Minimum}; 0834 toolbar_orientation = Qt::Horizontal; 0835 toolbar_direction = QBoxLayout::LeftToRight; 0836 mins = screen_size.width(); 0837 } 0838 0839 for ( const auto& spacer : toolbar_spacers ) 0840 spacer->changeSize(0, 0, spacer_policy.first, spacer_policy.second); 0841 0842 0843 int button_w = qFloor(mins / 9.); 0844 QSize button_size(button_w, button_w); 0845 0846 for ( QToolButton* btn : parent->findChildren<QToolButton*>() ) 0847 btn->setIconSize(button_size); 0848 0849 timeline_slider->setFixedHeight(button_w); 0850 timeline_slider->set_slider_size(screen_size.width() / 10); 0851 0852 ui.widget_tools_container_side->setVisible(false); 0853 ui.widget_tools_container_bottom->setVisible(false); 0854 0855 tool_layout_extent = button_w; 0856 0857 layout_actions->setDirection(toolbar_direction); 0858 layout_tools->setDirection(toolbar_direction); 0859 QSize tool_layout_size(tool_layout_extent, tool_layout_extent); 0860 // layout_tools->set_fixed_item_size(tool_layout_size); 0861 // layout_actions->set_fixed_item_size(tool_layout_size); 0862 0863 if ( toolbar_orientation == Qt::Horizontal ) 0864 { 0865 ui.widget_tools_container_bottom->setLayout(ui.layout_tools_container); 0866 ui.layout_tools_container->setDirection(QBoxLayout::TopToBottom); 0867 ui.widget_tools_container_bottom->setVisible(true); 0868 } 0869 else 0870 { 0871 ui.layout_tools_container->setDirection(QBoxLayout::RightToLeft); 0872 ui.widget_tools_container_side->setLayout(ui.layout_tools_container); 0873 ui.widget_tools_container_side->setVisible(true); 0874 } 0875 } 0876 0877 void open_url(const QUrl& url, bool is_import) 0878 { 0879 if ( !url.isValid() ) 0880 return; 0881 0882 if ( is_import ) 0883 { 0884 auto imported = document_opener.open(url); 0885 if ( imported ) 0886 parent->paste_document(imported.get(), i18n("Import File"), true); 0887 return; 0888 } 0889 0890 clear_document(); 0891 current_document = document_opener.open(url); 0892 if ( current_document ) 0893 setup_document_open(); 0894 else 0895 setup_document_new(); 0896 } 0897 0898 bool save_url(const QUrl& url, bool export_opts) 0899 { 0900 if ( !url.isValid() ) 0901 return false; 0902 0903 if ( export_opts && exporting_png ) 0904 { 0905 QImage pix(comp->size(), QImage::Format_RGBA8888); 0906 pix.fill(Qt::transparent); 0907 QPainter painter(&pix); 0908 painter.setRenderHint(QPainter::Antialiasing); 0909 comp->paint(&painter, current_document->current_time(), model::VisualNode::Render); 0910 painter.end(); 0911 QByteArray data; 0912 QBuffer buf(&data); 0913 buf.open(QIODevice::WriteOnly); 0914 pix.save(&buf, "PNG"); 0915 buf.close(); 0916 AndroidFilePicker::write_content_uri(url, data); 0917 exporting_png = false; 0918 return false; 0919 } 0920 0921 io::Options options = export_opts ? export_options : current_document->io_options(); 0922 if ( document_opener.save(url, comp, options) ) 0923 { 0924 save_document_set_opts(options, export_opts); 0925 return true; 0926 } 0927 0928 return false; 0929 } 0930 0931 void clear_property_widgets() 0932 { 0933 while ( QLayoutItem *child = ui.property_widget_layout->takeAt(0) ) 0934 { 0935 delete child->widget(); 0936 delete child; 0937 } 0938 } 0939 0940 void add_property_widgets_object(model::Object* node, BetterToolboxWidget* toolbox) 0941 { 0942 using Traits = model::PropertyTraits; 0943 std::vector<std::pair<QWidget*, model::BaseProperty*>> props; 0944 0945 for ( const auto& prop : node->properties() ) 0946 { 0947 auto traits = prop->traits(); 0948 if ( traits.type == Traits::ObjectReference || (traits.flags & (Traits::List|Traits::Hidden)) ) 0949 { 0950 continue; 0951 } 0952 else if ( traits.type == Traits::Object ) 0953 { 0954 add_property_widgets_object(prop->value().value<model::Object*>(), toolbox); 0955 } 0956 else if ( !(traits.flags & Traits::Visual) ) 0957 { 0958 continue; 0959 } 0960 else if ( auto wid = property_delegate.editor_from_property(prop, nullptr) ) 0961 { 0962 QWidget* prop_widget = new QWidget(); 0963 QVBoxLayout* lay = new QVBoxLayout(); 0964 prop_widget->setLayout(lay); 0965 lay->addWidget(wid); 0966 lay->setMargin(0); 0967 lay->setSpacing(0); 0968 wid->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); 0969 0970 if ( traits.flags & Traits::Animated ) 0971 { 0972 auto anim = static_cast<model::AnimatableBase*>(prop); 0973 QHBoxLayout* btnlay = new QHBoxLayout(); 0974 0975 auto btn_add_kf = new QToolButton(); 0976 btn_add_kf->setIcon(QIcon::fromTheme("keyframe-add")); 0977 btn_add_kf->setText(i18n("Add keyframe")); 0978 connect(btn_add_kf, &QToolButton::clicked, node, [anim]{ 0979 anim->add_smooth_keyframe_undoable(anim->time(), anim->value()); 0980 }); 0981 btn_add_kf->setIconSize(QSize(80, 80)); 0982 btnlay->addWidget(btn_add_kf); 0983 0984 auto btn_rm_kf = new QToolButton(); 0985 btn_rm_kf->setIcon(QIcon::fromTheme("keyframe-remove")); 0986 btn_rm_kf->setText(i18n("Remove keyframe")); 0987 connect(btn_rm_kf, &QToolButton::clicked, node, [anim]{ 0988 if ( anim->has_keyframe(anim->time()) ) 0989 { 0990 anim->object()->push_command( 0991 new command::RemoveKeyframeTime(anim, anim->time()) 0992 ); 0993 } 0994 }); 0995 btn_rm_kf->setIconSize(QSize(80, 80)); 0996 btnlay->addWidget(btn_rm_kf); 0997 0998 0999 auto btn_rm_kf_all = new QToolButton(); 1000 btn_rm_kf_all->setIcon(QIcon::fromTheme("edit-clear-all")); 1001 btn_rm_kf_all->setText(i18n("Clear Animations")); 1002 connect(btn_rm_kf, &QToolButton::clicked, node, [anim]{ 1003 if ( anim->animated() ) 1004 { 1005 anim->clear_keyframes_undoable(); 1006 } 1007 }); 1008 btn_rm_kf_all->setIconSize(QSize(80, 80)); 1009 btnlay->addWidget(btn_rm_kf_all); 1010 1011 1012 lay->addLayout(btnlay); 1013 } 1014 1015 props.emplace_back(wid, prop); 1016 property_delegate.set_editor_data(wid, prop); 1017 toolbox->addItem(prop_widget, prop->name()); 1018 } 1019 } 1020 connect(node, &model::Object::visual_property_changed, props[0].first, [props, this]{ 1021 for ( const auto& p : props ) 1022 property_delegate.set_editor_data(p.first, p.second); 1023 }); 1024 1025 } 1026 1027 void add_property_widgets(model::VisualNode* node, BetterToolboxWidget* toolbox) 1028 { 1029 BetterToolboxWidget* sub_toolbox = new BetterToolboxWidget(); 1030 int index = toolbox->count(); 1031 toolbox->addItem(sub_toolbox, node->tree_icon(), node->object_name()); 1032 1033 connect(node, &model::DocumentNode::name_changed, sub_toolbox, 1034 [toolbox, index](const QString& name){ 1035 toolbox->setItemText(index, name); 1036 } 1037 ); 1038 add_property_widgets_object(node, sub_toolbox); 1039 1040 if ( auto grp = node->cast<model::Group>() ) 1041 { 1042 for ( const auto& child : grp->shapes ) 1043 { 1044 if ( !child->is_instance<model::Group>() ) 1045 add_property_widgets(child.get(), toolbox); 1046 } 1047 } 1048 } 1049 1050 1051 void set_property_widgets(model::VisualNode* node) 1052 { 1053 clear_property_widgets(); 1054 if ( node ) 1055 { 1056 BetterToolboxWidget* toolbox = new BetterToolboxWidget(); 1057 toolbox->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); 1058 ui.property_widget_layout->addWidget(toolbox); 1059 add_property_widgets(node, toolbox); 1060 ui.property_widget->setMinimumWidth(toolbox->sizeHint().width() + 6); 1061 } 1062 } 1063 1064 QAction* emoji_tool_action() 1065 { 1066 QAction* action = new QAction(QIcon::fromTheme("smiley-shape"), i18n("Emoji")); 1067 connect(action, &QAction::triggered, parent, [this]{ import_emoji(); }); 1068 return action; 1069 } 1070 1071 void import_emoji() 1072 { 1073 qDebug() << "foo"; 1074 emoji::EmojiDialog& dialog = telegram_export_dialog.emoji_dialog(); 1075 dialog.show(); 1076 qDebug() << dialog.isVisible() << dialog.parentWidget(); 1077 if ( !dialog.exec() ) 1078 return; 1079 1080 QString filename = gui::GlaxnimateApp::instance()->data_file("emoji/svg/" + dialog.image_slug_format().slug(dialog.current_slug()) + ".svg"); 1081 1082 QFileInfo finfo(filename); 1083 io::Options options; 1084 options.format = io::IoRegistry::instance().from_extension(finfo.suffix(), io::ImportExport::Import); 1085 if ( !options.format ) 1086 return; 1087 1088 options.filename = filename; 1089 options.path = finfo.dir(); 1090 1091 model::Document imported(options.filename); 1092 QFile file(options.filename); 1093 1094 if ( !options.format->open(file, options.filename, &imported, options.settings) ) 1095 return; 1096 1097 parent->paste_document(&imported, i18n("Import Emoji"), true); 1098 } 1099 }; 1100 1101 MainWindow::MainWindow(QWidget *parent) : 1102 QMainWindow(parent), 1103 d(std::make_unique<Private>(this)) 1104 { 1105 } 1106 1107 MainWindow::~MainWindow() 1108 { 1109 } 1110 1111 void MainWindow::changeEvent(QEvent *e) 1112 { 1113 QMainWindow::changeEvent(e); 1114 switch (e->type()) { 1115 case QEvent::LanguageChange: 1116 d->ui.retranslateUi(this); 1117 1118 for ( const auto& grp : gui::tools::Registry::instance() ) 1119 { 1120 for ( const auto& tool : grp.second ) 1121 { 1122 tool.second->retranslate(); 1123 } 1124 } 1125 break; 1126 default: 1127 break; 1128 } 1129 } 1130 1131 glaxnimate::model::Document* MainWindow::document() const 1132 { 1133 return d->current_document.get(); 1134 } 1135 1136 1137 glaxnimate::model::Composition* MainWindow::current_composition() const 1138 { 1139 return d->comp; 1140 } 1141 1142 glaxnimate::model::VisualNode* MainWindow::current_document_node() const 1143 { 1144 return d->current_node; 1145 } 1146 1147 QColor MainWindow::current_color() const 1148 { 1149 return d->ui.fill_style_widget->current_color(); 1150 } 1151 1152 void MainWindow::set_current_color(const QColor& c) 1153 { 1154 d->ui.fill_style_widget->set_current_color(c); 1155 } 1156 1157 QColor MainWindow::secondary_color() const 1158 { 1159 return d->ui.stroke_style_widget->current_color(); 1160 } 1161 1162 void MainWindow::set_secondary_color(const QColor& c) 1163 { 1164 d->ui.stroke_style_widget->set_color(c); 1165 } 1166 1167 QPen MainWindow::current_pen_style() const 1168 { 1169 return d->ui.stroke_style_widget->pen_style(); 1170 } 1171 1172 qreal MainWindow::current_zoom() const 1173 { 1174 return d->ui.canvas->get_zoom_factor(); 1175 } 1176 1177 glaxnimate::model::BrushStyle* MainWindow::linked_brush_style(bool secondary) const 1178 { 1179 if ( secondary ) 1180 return d->secondary_brush; 1181 return d->main_brush; 1182 } 1183 1184 void MainWindow::set_current_document_node(model::VisualNode* node) 1185 { 1186 if ( node == d->current_node ) 1187 return; 1188 1189 d->current_node = node; 1190 d->current_fill = nullptr; 1191 d->current_stroke = nullptr; 1192 d->main_brush = nullptr; 1193 d->secondary_brush = nullptr; 1194 1195 if ( node ) 1196 { 1197 auto grp = node->cast<model::Group>(); 1198 if ( !grp ) 1199 grp = node->docnode_parent()->cast<model::Group>(); 1200 if ( grp ) 1201 { 1202 for ( const auto& sh : grp->shapes ) 1203 { 1204 if ( auto fill = sh->cast<model::Fill>() ) 1205 d->current_fill = fill; 1206 else if ( auto stroke = sh->cast<model::Stroke>() ) 1207 d->current_stroke = stroke; 1208 } 1209 } 1210 1211 if ( d->current_fill ) 1212 d->main_brush = d->current_fill->use.get(); 1213 if ( d->current_stroke ) 1214 d->secondary_brush = d->current_stroke->use.get(); 1215 } 1216 1217 d->ui.stroke_style_widget->set_current(d->current_stroke); 1218 d->ui.fill_style_widget->set_current(d->current_fill); 1219 1220 1221 d->set_property_widgets(node); 1222 1223 if ( sender() != d->layer_view ) 1224 d->layer_view->set_current_node(node); 1225 } 1226 1227 void MainWindow::set_current_composition(model::Composition* comp) 1228 { 1229 d->comp = comp ? comp : d->comp; 1230 d->scene.set_composition(comp); 1231 d->layer_view->set_composition(comp); 1232 } 1233 1234 void MainWindow::switch_tool(gui::tools::Tool* tool) 1235 { 1236 d->switch_tool(tool); 1237 } 1238 1239 std::vector<glaxnimate::model::VisualNode*> MainWindow::cleaned_selection() const 1240 { 1241 return d->scene.cleaned_selection(); 1242 } 1243 1244 std::vector<glaxnimate::io::mime::MimeSerializer *> MainWindow::supported_mimes() const 1245 { 1246 return { 1247 io::IoRegistry::instance().serializer_from_slug("glaxnimate") 1248 }; 1249 } 1250 1251 void MainWindow::tool_triggered(bool checked) 1252 { 1253 if ( checked ) 1254 d->switch_tool(static_cast<QAction*>(sender())->data().value<gui::tools::Tool*>()); 1255 } 1256 1257 void MainWindow::resizeEvent(QResizeEvent* e) 1258 { 1259 QMainWindow::resizeEvent(e); 1260 1261 #ifndef Q_OS_ANDROID 1262 d->adjust_size(); 1263 #endif 1264 } 1265 1266 void MainWindow::showEvent(QShowEvent *e) 1267 { 1268 QMainWindow::showEvent(e); 1269 d->adjust_size(); 1270 } 1271 1272 void MainWindow::set_selection(const std::vector<model::VisualNode*>& selected) 1273 { 1274 d->scene.user_select(selected, gui::graphics::DocumentScene::Replace); 1275 } 1276 1277 void MainWindow::update_selection(const std::vector<model::VisualNode *> &selected, const std::vector<model::VisualNode *> &deselected) 1278 { 1279 if ( d->updating_selection ) 1280 return; 1281 1282 std::unique_lock lock(d->updating_selection); 1283 1284 if ( sender() != &d->scene ) 1285 { 1286 d->scene.user_select(deselected, gui::graphics::DocumentScene::Remove); 1287 d->scene.user_select(selected, gui::graphics::DocumentScene::Append); 1288 } 1289 1290 if ( sender() != d->layer_view ) 1291 d->layer_view->update_selection(selected, deselected); 1292 } 1293 1294 glaxnimate::gui::item_models::DocumentNodeModel* MainWindow::model() const 1295 { 1296 return &d->document_node_model; 1297 } 1298 1299 void MainWindow::open_intent(const QUrl &uri) 1300 { 1301 if ( d->close_document() ) 1302 { 1303 d->open_url(uri, false); 1304 } 1305 }