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, &current_document->undo_stack(), &QUndoStack::undo);
0223         connect(&current_document->undo_stack(), &QUndoStack::canUndoChanged, action_undo, &QAction::setEnabled);
0224         connect(action_redo, &QAction::triggered, &current_document->undo_stack(), &QUndoStack::redo);
0225         connect(&current_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 }