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

0001 /*
0002  * SPDX-FileCopyrightText: 2019-2023 Mattia Basaglia <dev@dragon.best>
0003  *
0004  * SPDX-License-Identifier: GPL-3.0-or-later
0005  */
0006 
0007 #include "glaxnimate_window_p.hpp"
0008 
0009 #include <QComboBox>
0010 #include <QLabel>
0011 #include <QScrollBar>
0012 #include <QInputDialog>
0013 #include <QLabel>
0014 #include <QPushButton>
0015 #include <QActionGroup>
0016 #include <QScreen>
0017 #include <QDialogButtonBox>
0018 
0019 #include <KHelpMenu>
0020 #include <KActionCollection>
0021 #include <KShortcutsDialog>
0022 
0023 #include "app/settings/keyboard_shortcuts.hpp"
0024 
0025 #include "tools/base.hpp"
0026 #include "model/shapes/group.hpp"
0027 #include "model/shapes/image.hpp"
0028 #include "model/shapes/repeater.hpp"
0029 #include "model/shapes/trim.hpp"
0030 #include "model/shapes/inflate_deflate.hpp"
0031 #include "model/shapes/round_corners.hpp"
0032 #include "model/shapes/offset_path.hpp"
0033 #include "model/shapes/zig_zag.hpp"
0034 #include "io/io_registry.hpp"
0035 
0036 #include "widgets/dialogs/io_status_dialog.hpp"
0037 #include "widgets/dialogs/about_dialog.hpp"
0038 #include "widgets/dialogs/resize_dialog.hpp"
0039 #include "widgets/dialogs/timing_dialog.hpp"
0040 #include "widgets/dialogs/document_metadata_dialog.hpp"
0041 #include "widgets/dialogs/trace_dialog.hpp"
0042 #include "widgets/dialogs/startup_dialog.hpp"
0043 #include "widgets/lottiefiles/lottiefiles_search_dialog.hpp"
0044 
0045 #include "widgets/view_transform_widget.hpp"
0046 #include "widgets/flow_layout.hpp"
0047 #include "widgets/menus/node_menu.hpp"
0048 #include "widgets/shape_style/shape_style_preview_widget.hpp"
0049 
0050 #include "style/better_elide_delegate.hpp"
0051 #include "tools/edit_tool.hpp"
0052 #include "plugin/action.hpp"
0053 #include "glaxnimate_app.hpp"
0054 #include "settings/toolbar_settings.hpp"
0055 #include "settings/document_templates.hpp"
0056 #include "emoji/emoji_set_dialog.hpp"
0057 
0058 using namespace glaxnimate::gui;
0059 
0060 
0061 static QToolButton* action_button(QAction* action, QWidget* parent)
0062 {
0063     auto button = new ScalableButton(parent);
0064     button->setDefaultAction(action);
0065     button->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
0066     button->resize(16, 16);
0067     button->setMaximumSize(64, 64);
0068     return button;
0069 }
0070 
0071 static void action_combo(QComboBox* box, QAction* action)
0072 {
0073     int index = box->count();
0074     box->addItem(action->icon(), action->text(), QVariant::fromValue(action));
0075     QObject::connect(action, &QAction::triggered, box, [index, box]{
0076         box->setCurrentIndex(index);
0077     });
0078     QObject::connect(action, &QAction::changed, box, [index, box, action]{
0079         box->setItemIcon(index, action->icon());
0080         box->setItemText(index, action->text());
0081     });
0082 }
0083 
0084 void GlaxnimateWindow::Private::setupUi(bool restore_state, bool debug, GlaxnimateWindow* parent)
0085 {
0086     this->parent = parent;
0087     parent->createGUI();
0088     ui.setupUi(parent);
0089 
0090     init_actions();
0091 
0092     tools::Tool* to_activate = init_tools_ui();
0093 
0094     init_item_views();
0095 
0096     // Tool buttons
0097     ui.btn_layer_add->setMenu(ui.menu_new_layer);
0098 
0099     init_status_bar();
0100 
0101     // Graphics scene
0102     ui.canvas->setScene(&scene);
0103     ui.canvas->set_tool_target(parent);
0104     connect(&scene, &graphics::DocumentScene::node_user_selected, parent, &GlaxnimateWindow::scene_selection_changed);
0105     connect(ui.canvas, &Canvas::dropped, parent, [this](const QMimeData* d){dropped(d);});
0106 
0107     // Dialogs
0108     dialog_import_status = new IoStatusDialog(QIcon::fromTheme("document-open"), i18n("Open File"), false, parent);
0109     dialog_export_status = new IoStatusDialog(QIcon::fromTheme("document-save"), i18n("Save File"), false, parent);
0110     about_dialog = new AboutDialog(parent);
0111 
0112     // Only enabled on debug because it isn't fully integrated yet
0113     if ( debug )
0114     {
0115         KHelpMenu *help_menu = new KHelpMenu(parent);
0116         ui.menu_bar->addMenu(help_menu->menu());
0117         connect(help_menu, &KHelpMenu::showAboutApplication, about_dialog, &QWidget::show);
0118     }
0119 
0120     // Recent files
0121     recent_files = app::settings::get<QStringList>("open_save", "recent_files");
0122     ui.action_open_last->setEnabled(!recent_files.isEmpty());
0123     reload_recent_menu();
0124     connect(ui.menu_open_recent, &QMenu::triggered, parent, &GlaxnimateWindow::document_open_recent);
0125 
0126     // Docks
0127     init_docks();
0128 
0129     // Menus
0130     init_menus();
0131 
0132     // Debug menu
0133     if ( debug )
0134         init_debug();
0135 
0136     // Initialize tools
0137     init_tools(to_activate);
0138 
0139     // Restore state
0140     // NOTE: keep at the end so we do this once all the widgets are in their default spots
0141     if ( restore_state )
0142         init_restore_state();
0143 
0144 
0145     // Toolbar settings
0146     parent->setToolButtonStyle(settings::ToolbarSettingsGroup::button_style);
0147     parent->setIconSize(settings::ToolbarSettingsGroup::icon_size());
0148     ui.toolbar_tools->setIconSize(settings::ToolbarSettingsGroup::tool_icon_size());
0149 }
0150 
0151 template<class T>
0152 void GlaxnimateWindow::Private::add_modifier_menu_action(QMenu* menu)
0153 {
0154     menu->addAction(T::static_tree_icon(), T::static_type_name_human(), [this]{
0155         auto layer = std::make_unique<T>(current_document.get());
0156         parent->layer_new_impl(std::move(layer));
0157     })->setObjectName("action_new_" + T::static_class_name().toLower());
0158 }
0159 
0160 void GlaxnimateWindow::Private::init_actions()
0161 {
0162     // Standard Shortcuts
0163     ui.action_new->setShortcut(QKeySequence::New);
0164     ui.action_open->setShortcut(QKeySequence::Open);
0165     ui.action_close->setShortcut(QKeySequence::Close);
0166     ui.action_reload->setShortcut(QKeySequence("Ctrl+F5", QKeySequence::PortableText));
0167     ui.action_save->setShortcut(QKeySequence::Save);
0168     ui.action_save_as->setShortcut(QKeySequence::SaveAs);
0169     ui.action_quit->setShortcut(QKeySequence::Quit);
0170     ui.action_copy->setShortcut(QKeySequence::Copy);
0171     ui.action_cut->setShortcut(QKeySequence::Cut);
0172     ui.action_paste->setShortcut(QKeySequence::Paste);
0173     ui.action_paste_as_composition->setShortcut(QKeySequence("Ctrl+Shift+V", QKeySequence::PortableText));
0174     ui.action_select_all->setShortcut(QKeySequence::SelectAll);
0175     ui.action_undo->setShortcut(QKeySequence::Undo);
0176     ui.action_redo->setShortcut(QKeySequence::Redo);
0177     ui.action_group->setShortcut(QKeySequence("Ctrl+G", QKeySequence::PortableText));
0178     ui.action_ungroup->setShortcut(QKeySequence("Ctrl+Shift+G", QKeySequence::PortableText));
0179     ui.action_open_last->setShortcut(QKeySequence("Ctrl+Shift+O", QKeySequence::PortableText));
0180     ui.action_import_image->setShortcut(QKeySequence("Ctrl+I", QKeySequence::PortableText));
0181     ui.action_import->setShortcut(QKeySequence("Ctrl+Shift+I", QKeySequence::PortableText));
0182     ui.action_node_remove->setShortcut(QKeySequence("Del", QKeySequence::PortableText));
0183     ui.action_delete->setShortcut(QKeySequence("Del", QKeySequence::PortableText));
0184     ui.action_export->setShortcut(QKeySequence("Ctrl+E", QKeySequence::PortableText));
0185     ui.action_export_as->setShortcut(QKeySequence("Ctrl+Shift+E", QKeySequence::PortableText));
0186     ui.action_frame_prev->setShortcut(QKeySequence("Left", QKeySequence::PortableText));
0187     ui.action_frame_next->setShortcut(QKeySequence("Right", QKeySequence::PortableText));
0188     ui.action_duplicate->setShortcut(QKeySequence("Ctrl+D", QKeySequence::PortableText));
0189     ui.action_play->setShortcut(QKeySequence("Space", QKeySequence::PortableText));
0190 
0191     // Actions
0192     connect(ui.action_copy, &QAction::triggered, parent, &GlaxnimateWindow::copy);
0193     connect(ui.action_paste, &QAction::triggered, parent, &GlaxnimateWindow::paste);
0194     connect(ui.action_paste_as_composition, &QAction::triggered, parent, [this]{parent->paste_as_composition();});
0195     connect(ui.action_cut, &QAction::triggered, parent, &GlaxnimateWindow::cut);
0196     connect(ui.action_duplicate, &QAction::triggered, parent, &GlaxnimateWindow::duplicate_selection);
0197     connect(ui.action_reload, &QAction::triggered, parent, &GlaxnimateWindow::document_reload);
0198     connect(ui.action_raise_to_top, &QAction::triggered, parent, &GlaxnimateWindow::layer_top);
0199     connect(ui.action_raise, &QAction::triggered, parent, &GlaxnimateWindow::layer_raise);
0200     connect(ui.action_lower, &QAction::triggered, parent, &GlaxnimateWindow::layer_lower);
0201     connect(ui.action_lower_to_bottom, &QAction::triggered, parent, &GlaxnimateWindow::layer_bottom);
0202     connect(ui.action_group, &QAction::triggered, parent, &GlaxnimateWindow::group_shapes);
0203     connect(ui.action_ungroup, &QAction::triggered, parent, &GlaxnimateWindow::ungroup_shapes);
0204     connect(ui.action_quit, &QAction::triggered, parent, &GlaxnimateWindow::close);
0205     connect(ui.action_move_to, &QAction::triggered, parent, &GlaxnimateWindow::move_to);
0206     connect(ui.action_validate_tgs, &QAction::triggered, parent, &GlaxnimateWindow::validate_tgs);
0207     connect(ui.action_validate_discord, &QAction::triggered, parent, [this]{validate_discord();});
0208     connect(ui.action_resize, &QAction::triggered, parent, [this]{ ResizeDialog(this->parent).resize_composition(comp); });
0209     connect(ui.action_donate, &QAction::triggered, parent, &GlaxnimateWindow::help_donate);
0210     connect(ui.action_new_layer, &QAction::triggered, parent, [this]{layer_new_layer();});
0211     connect(ui.action_new_group, &QAction::triggered, parent, [this]{layer_new_group();});
0212     connect(ui.action_new_fill, &QAction::triggered, parent, [this]{layer_new_fill();});
0213     connect(ui.action_new_stroke, &QAction::triggered, parent, [this]{layer_new_stroke();});
0214     connect(ui.action_open_last, &QAction::triggered, parent, [this]{
0215         if ( !recent_files.isEmpty() )
0216         {
0217             // Avoid references to recent_files
0218             QString filename = recent_files[0];
0219             document_open_from_filename(filename);
0220         }
0221     });
0222     add_modifier_menu_action<model::Repeater>(ui.menu_new_layer);
0223     add_modifier_menu_action<model::Trim>(ui.menu_new_layer);
0224     add_modifier_menu_action<model::InflateDeflate>(ui.menu_new_layer);
0225     add_modifier_menu_action<model::RoundCorners>(ui.menu_new_layer);
0226     add_modifier_menu_action<model::OffsetPath>(ui.menu_new_layer);
0227     add_modifier_menu_action<model::ZigZag>(ui.menu_new_layer);
0228     connect(ui.action_import_image, &QAction::triggered, parent, [this]{import_image();});
0229     connect(ui.action_delete, &QAction::triggered, parent, &GlaxnimateWindow::delete_selected);
0230     connect(ui.action_export, &QAction::triggered, parent, &GlaxnimateWindow::document_export);
0231     connect(ui.action_export_as, &QAction::triggered, parent, &GlaxnimateWindow::document_export_as);
0232     connect(ui.action_export_sequence, &QAction::triggered, parent, &GlaxnimateWindow::document_export_sequence);
0233     connect(ui.action_document_cleanup, &QAction::triggered, parent, [this]{cleanup_document();});
0234     connect(ui.action_timing, &QAction::triggered, parent, [this]{
0235         TimingDialog(comp, this->parent).exec();
0236     });
0237 
0238     connect(ui.action_play, &QAction::triggered, ui.play_controls, &FrameControlsWidget::toggle_play);
0239     connect(ui.play_controls, &FrameControlsWidget::play_started, parent, [this]{
0240         ui.action_play->setText(i18n("Pause"));
0241         ui.action_play->setIcon(QIcon::fromTheme("media-playback-pause"));
0242     });
0243     connect(ui.play_controls, &FrameControlsWidget::play_stopped, parent, [this]{
0244         ui.action_play->setText(i18n("Play"));
0245         ui.action_play->setIcon(QIcon::fromTheme("media-playback-start"));
0246     });
0247     connect(ui.play_controls, &FrameControlsWidget::record_toggled, ui.action_record, &QAction::setChecked);
0248     connect(ui.action_record, &QAction::triggered, ui.play_controls, &FrameControlsWidget::set_record_enabled);
0249     connect(ui.action_frame_first, &QAction::triggered, ui.play_controls, &FrameControlsWidget::go_first);
0250     connect(ui.action_frame_prev, &QAction::triggered, ui.play_controls, &FrameControlsWidget::go_prev);
0251     connect(ui.action_frame_next, &QAction::triggered, ui.play_controls, &FrameControlsWidget::go_next);
0252     connect(ui.action_frame_last, &QAction::triggered, ui.play_controls, &FrameControlsWidget::go_last);
0253     connect(ui.play_controls, &FrameControlsWidget::loop_changed, ui.action_play_loop, &QAction::setChecked);
0254     connect(ui.action_play_loop, &QAction::triggered, ui.play_controls, &FrameControlsWidget::set_loop);
0255 
0256     connect(ui.play_controls,   &FrameControlsWidget::min_changed,    ui.play_controls_2, &FrameControlsWidget::set_min);
0257     connect(ui.play_controls,   &FrameControlsWidget::max_changed,    ui.play_controls_2, &FrameControlsWidget::set_max);
0258     connect(ui.play_controls,   &FrameControlsWidget::fps_changed,    ui.play_controls_2, &FrameControlsWidget::set_fps);
0259     connect(ui.play_controls,   &FrameControlsWidget::play_started,   ui.play_controls_2, &FrameControlsWidget::play);
0260     connect(ui.play_controls,   &FrameControlsWidget::play_stopped,   ui.play_controls_2, &FrameControlsWidget::pause);
0261     connect(ui.play_controls,   &FrameControlsWidget::loop_changed,   ui.play_controls_2, &FrameControlsWidget::set_loop);
0262     connect(ui.play_controls,   &FrameControlsWidget::frame_selected, ui.play_controls_2, &FrameControlsWidget::set_frame);
0263     connect(ui.play_controls_2, &FrameControlsWidget::play_started,   ui.play_controls,   &FrameControlsWidget::play);
0264     connect(ui.play_controls_2, &FrameControlsWidget::play_stopped,   ui.play_controls,   &FrameControlsWidget::pause);
0265     connect(ui.play_controls_2, &FrameControlsWidget::loop_changed,   ui.play_controls,   &FrameControlsWidget::set_loop);
0266     connect(ui.play_controls_2, &FrameControlsWidget::frame_selected, ui.play_controls,   &FrameControlsWidget::set_frame);
0267     connect(ui.play_controls, &FrameControlsWidget::min_changed, ui.scroll_time, &QSlider::setMinimum);
0268     connect(ui.play_controls, &FrameControlsWidget::max_changed, ui.scroll_time, &QSlider::setMaximum);
0269     connect(ui.play_controls, &FrameControlsWidget::frame_selected, ui.scroll_time, &QSlider::setValue);
0270     connect(ui.scroll_time, &QSlider::valueChanged, ui.play_controls_2, &FrameControlsWidget::frame_selected);
0271 
0272     connect(ui.action_metadata, &QAction::triggered, parent, [this]{
0273         DocumentMetadataDialog(current_document.get(), this->parent).exec();
0274     });
0275     connect(ui.action_trace_bitmap, &QAction::triggered, parent, [this]{
0276         trace_dialog(parent->current_shape());
0277     });
0278     connect(ui.action_object_to_path, &QAction::triggered, parent, [this]{to_path();});
0279     connect(ui.action_lottie_preview, &QAction::triggered, parent, [this]{preview_lottie("svg");});
0280     connect(ui.action_lottie_canvas_preview, &QAction::triggered, parent, [this]{preview_lottie("canvas");});
0281     connect(ui.action_svg_preview, &QAction::triggered, parent, [this]{preview_svg();});
0282     connect(ui.action_rive_preview, &QAction::triggered, parent, [this]{preview_rive();});
0283     connect(ui.action_new_precomp, &QAction::triggered, parent, [this]{add_composition();});
0284     connect(ui.action_new_precomp_selection, &QAction::triggered, parent, [this]{
0285         objects_to_new_composition(comp, cleaned_selection(), &comp->shapes, -1);
0286     });
0287     connect(ui.menu_new_comp_layer, &QMenu::triggered, parent, [this](QAction* act){layer_new_comp_action(act);});
0288     connect(ui.action_align_hor_left_out,   &QAction::triggered, parent, [this]{align(AlignDirection::Horizontal, AlignPosition::Begin,  true);});
0289     connect(ui.action_align_hor_left,       &QAction::triggered, parent, [this]{align(AlignDirection::Horizontal, AlignPosition::Begin,  false);});
0290     connect(ui.action_align_hor_center,     &QAction::triggered, parent, [this]{align(AlignDirection::Horizontal, AlignPosition::Center, false);});
0291     connect(ui.action_align_hor_right,      &QAction::triggered, parent, [this]{align(AlignDirection::Horizontal, AlignPosition::End,    false);});
0292     connect(ui.action_align_hor_right_out,  &QAction::triggered, parent, [this]{align(AlignDirection::Horizontal, AlignPosition::End,    true);});
0293     connect(ui.action_align_vert_top_out,   &QAction::triggered, parent, [this]{align(AlignDirection::Vertical,   AlignPosition::Begin,  true);});
0294     connect(ui.action_align_vert_top,       &QAction::triggered, parent, [this]{align(AlignDirection::Vertical,   AlignPosition::Begin,  false);});
0295     connect(ui.action_align_vert_center,    &QAction::triggered, parent, [this]{align(AlignDirection::Vertical,   AlignPosition::Center, false);});
0296     connect(ui.action_align_vert_bottom,    &QAction::triggered, parent, [this]{align(AlignDirection::Vertical,   AlignPosition::End,    false);});
0297     connect(ui.action_align_vert_bottom_out,&QAction::triggered, parent, [this]{align(AlignDirection::Vertical,   AlignPosition::End,    true);});
0298     connect(ui.action_import, &QAction::triggered, parent, [this]{import_file();});
0299     connect(ui.action_flip_view, &QAction::triggered, ui.canvas, &Canvas::flip_horizontal);
0300     connect(ui.action_text_put_on_path, &QAction::triggered, parent, [this]{text_put_on_path();});
0301     connect(ui.action_text_remove_from_path, &QAction::triggered, parent, [this]{text_remove_from_path();});
0302     connect(ui.action_insert_emoji, &QAction::triggered, parent, [this]{insert_emoji();});
0303     connect(ui.action_open_lottiefiles, &QAction::triggered, parent, [this]{import_from_lottiefiles();});
0304     connect(ui.action_shortcuts, &QAction::triggered, parent, [this]{
0305         KShortcutsDialog::showDialog(parent->actionCollection(), KShortcutsEditor::LetterShortcutsAllowed, this->parent);
0306     });
0307 
0308     // Undo Redo
0309     QObject::connect(ui.action_redo, &QAction::triggered, &parent->undo_group(), &QUndoGroup::redo);
0310     QObject::connect(&parent->undo_group(), &QUndoGroup::canRedoChanged, ui.action_redo, &QAction::setEnabled);
0311     QObject::connect(&parent->undo_group(), &QUndoGroup::redoTextChanged, ui.action_redo, [this](const QString& s){
0312         ui.action_redo->setText(i18n("Redo %1", s));
0313     });
0314     QObject::connect(ui.action_undo, &QAction::triggered, &parent->undo_group(), &QUndoGroup::undo);
0315     QObject::connect(&parent->undo_group(), &QUndoGroup::canUndoChanged, ui.action_undo, &QAction::setEnabled);
0316     QObject::connect(&parent->undo_group(), &QUndoGroup::undoTextChanged, ui.action_undo, [this](const QString& s){
0317         ui.action_undo->setText(i18n("Undo %1", s));
0318     });
0319     ui.view_undo->setGroup(&parent->undo_group());
0320 
0321 #ifdef Q_OS_WIN32
0322     // Can't get emoji_data.cpp to compile on windows for qt6 for some reason
0323     // the compiler errors out without message
0324     ui.action_insert_emoji->setEnabled(false);
0325 #endif
0326 
0327     parent->actionCollection()->addAssociatedWidget(parent);
0328     for ( auto action : parent->findChildren<QAction*>() )
0329     {
0330         if ( !action->isSeparator() && !action->objectName().isEmpty() )
0331         {
0332             parent->actionCollection()->addAction(action->objectName(), action);
0333             parent->actionCollection()->setDefaultShortcut(action, action->shortcut());
0334         }
0335     }
0336 }
0337 
0338 tools::Tool* GlaxnimateWindow::Private::init_tools_ui()
0339 {
0340     // Tool Actions
0341     QActionGroup *tool_actions = new QActionGroup(parent);
0342     tool_actions->setExclusive(true);
0343 
0344     dock_tools_layout = new FlowLayout();
0345     ui.dock_tools_layout_parent->insertLayout(0, dock_tools_layout);
0346     tools::Tool* to_activate = nullptr;
0347     for ( const auto& grp : tools::Registry::instance() )
0348     {
0349         for ( const auto& tool : grp.second )
0350         {
0351             QAction* action = tool.second->get_action();
0352             action->setParent(parent);
0353             ui.menu_tools->addAction(action);
0354             action->setActionGroup(tool_actions);
0355             ScalableButton *button = tool.second->get_button();
0356             connect(action, &QAction::triggered, parent, &GlaxnimateWindow::tool_triggered);
0357 
0358             ui.toolbar_tools->addAction(action);
0359 
0360             button->resize(16, 16);
0361             dock_tools_layout->addWidget(button);
0362 
0363             QWidget* widget = tool.second->get_settings_widget();
0364             widget->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
0365             ui.tool_settings_widget->addWidget(widget);
0366 
0367             if ( !to_activate )
0368             {
0369                 to_activate = tool.second.get();
0370                 action->setChecked(true);
0371             }
0372         }
0373         ui.menu_tools->addSeparator();
0374         ui.toolbar_tools->addSeparator();
0375     }
0376 
0377     ui.toolbar_node->setVisible(false);
0378     ui.toolbar_node->setEnabled(false);
0379 
0380     /// \todo Have some way of creating/connecting actions from the tools
0381     this->tool_actions["select"] = {
0382         ui.action_delete,
0383     };
0384     tool_widgets["edit"] = {
0385         ui.toolbar_node
0386     };
0387     this->tool_actions["edit"] = {
0388         ui.action_node_remove,
0389         ui.action_node_type_corner,
0390         ui.action_node_type_smooth,
0391         ui.action_node_type_symmetric,
0392         ui.action_segment_lines,
0393         ui.action_segment_curve,
0394         ui.action_node_add,
0395         ui.action_node_dissolve,
0396     };
0397     tools::EditTool* edit_tool = static_cast<tools::EditTool*>(tools::Registry::instance().tool("edit"));
0398     connect(ui.action_node_type_corner, &QAction::triggered, parent, [edit_tool]{
0399         edit_tool->selection_set_vertex_type(math::bezier::Corner);
0400     });
0401     connect(ui.action_node_type_smooth, &QAction::triggered, parent, [edit_tool]{
0402         edit_tool->selection_set_vertex_type(math::bezier::Smooth);
0403     });
0404     connect(ui.action_node_type_symmetric, &QAction::triggered, parent, [edit_tool]{
0405         edit_tool->selection_set_vertex_type(math::bezier::Symmetrical);
0406     });
0407     connect(ui.action_node_remove, &QAction::triggered, parent, [edit_tool]{
0408         edit_tool->selection_delete();
0409     });
0410     connect(ui.action_segment_lines, &QAction::triggered, parent, [edit_tool]{
0411         edit_tool->selection_straighten();
0412     });
0413     connect(ui.action_segment_curve, &QAction::triggered, parent, [edit_tool]{
0414         edit_tool->selection_curve();
0415     });
0416     connect(ui.action_node_add, &QAction::triggered, parent, [edit_tool]{
0417         edit_tool->add_point_mode();
0418     });
0419     connect(ui.action_node_dissolve, &QAction::triggered, parent, [edit_tool]{
0420         edit_tool->selection_dissolve();
0421     });
0422     connect(edit_tool, &tools::EditTool::gradient_stop_changed, ui.fill_style_widget, &FillStyleWidget::set_gradient_stop);
0423     connect(edit_tool, &tools::EditTool::gradient_stop_changed, ui.stroke_style_widget, &StrokeStyleWidget::set_gradient_stop);
0424 
0425     return to_activate;
0426 }
0427 
0428 void GlaxnimateWindow::Private::init_item_views()
0429 {
0430     // Item views
0431     ui.view_document_node->set_base_model(&document_node_model);
0432     QObject::connect(ui.view_document_node, &LayerView::current_node_changed,
0433                         parent, &GlaxnimateWindow::document_treeview_current_changed);
0434 
0435     ui.view_document_node->setContextMenuPolicy(Qt::CustomContextMenu);
0436     QObject::connect(ui.view_document_node, &QWidget::customContextMenuRequested, parent,
0437         [this](const QPoint& pos){
0438             auto index = ui.view_document_node->indexAt(pos);
0439             if ( auto node = ui.view_document_node->node(index) )
0440                 NodeMenu(node, this->parent, this->parent).exec(ui.view_document_node->mapToGlobal(pos));
0441         }
0442     );
0443 
0444     ui.view_properties->setModel(&property_model);
0445     ui.view_properties->setItemDelegateForColumn(item_models::PropertyModelSingle::ColumnValue, &property_delegate);
0446     ui.view_properties->header()->setSectionResizeMode(item_models::PropertyModelSingle::ColumnName, QHeaderView::ResizeToContents);
0447     ui.view_properties->header()->setSectionResizeMode(item_models::PropertyModelSingle::ColumnValue, QHeaderView::Stretch);
0448 
0449     connect(ui.view_document_node, &LayerView::selection_changed, parent, &GlaxnimateWindow::document_treeview_selection_changed);
0450 
0451     ui.timeline_widget->set_controller(parent);
0452 
0453     asset_model.setSourceModel(&document_node_model);
0454     ui.view_assets->setModel(&asset_model);
0455     ui.view_assets->header()->setSectionResizeMode(item_models::DocumentNodeModel::ColumnName-1, QHeaderView::Stretch);
0456     ui.view_assets->header()->hideSection(item_models::DocumentNodeModel::ColumnVisible-1);
0457     ui.view_assets->header()->hideSection(item_models::DocumentNodeModel::ColumnLocked-1);
0458     ui.view_assets->header()->setSectionResizeMode(item_models::DocumentNodeModel::ColumnUsers-1, QHeaderView::ResizeToContents);
0459     connect(ui.view_assets, &CustomTreeView::customContextMenuRequested, parent, [this](const QPoint& pos){
0460         auto node = document_node_model.node(asset_model.mapToSource(ui.view_assets->indexAt(pos)));
0461         if ( node )
0462             NodeMenu(node, this->parent, this->parent).exec(ui.view_assets->mapToGlobal(pos));
0463     });
0464 
0465 
0466     connect(ui.timeline_widget, &CompoundTimelineWidget::current_node_changed, parent, [this](model::VisualNode* node){
0467         timeline_current_node_changed(node);
0468     });
0469     connect(ui.timeline_widget, &CompoundTimelineWidget::selection_changed, parent, [this](const std::vector<model::VisualNode*>& selected,  const std::vector<model::VisualNode*>& deselected){
0470         selection_changed(selected, deselected);
0471     });
0472 }
0473 
0474 static QWidget* status_bar_separator()
0475 {
0476     QFrame *line = new QFrame();
0477     line->setFrameShape(QFrame::VLine);
0478     line->setFrameShadow(QFrame::Sunken);
0479     return line;
0480 }
0481 
0482 void GlaxnimateWindow::Private::init_status_bar()
0483 {
0484     // Recording
0485     widget_recording = new QWidget();
0486     ui.status_bar->addPermanentWidget(widget_recording);
0487     QHBoxLayout* lay = new QHBoxLayout;
0488     widget_recording->setLayout(lay);
0489     lay->setContentsMargins(0, 0, 0, 0);
0490     widget_recording->setVisible(false);
0491 
0492     QLabel* label_recording_icon = new QLabel();
0493     label_recording_icon->setPixmap(QIcon::fromTheme("media-record").pixmap(ui.status_bar->height()));
0494     lay->addWidget(label_recording_icon);
0495 
0496     label_recording = new QLabel();
0497     label_recording->setText(i18n("Recording Keyframes"));
0498     lay->addWidget(label_recording);
0499 
0500     lay->addWidget(status_bar_separator());
0501 
0502     // X: ... Y: ...
0503     label_mouse_pos = new QLabel();
0504     ui.status_bar->addPermanentWidget(label_mouse_pos);
0505     QFont font;
0506     font.setFamily("monospace");
0507     font.setStyleHint(QFont::Monospace);
0508     label_mouse_pos->setFont(font);
0509     mouse_moved(QPointF(0, 0));
0510     connect(ui.canvas, &Canvas::mouse_moved, parent, [this](const QPointF& p){mouse_moved(p);});
0511 
0512     ui.status_bar->addPermanentWidget(status_bar_separator());
0513 
0514     // Current Style
0515     widget_current_style = new ShapeStylePreviewWidget();
0516     ui.status_bar->addPermanentWidget(widget_current_style);
0517     widget_current_style->setFixedSize(ui.status_bar->height(), ui.status_bar->height());
0518     connect(ui.fill_style_widget, &FillStyleWidget::current_color_changed,
0519             widget_current_style, &ShapeStylePreviewWidget::set_fill_color);
0520     connect(ui.stroke_style_widget, &StrokeStyleWidget::color_changed,
0521             widget_current_style, &ShapeStylePreviewWidget::set_stroke_color);
0522     ui.status_bar->addPermanentWidget(status_bar_separator());
0523     widget_current_style->set_fill_color(ui.fill_style_widget->current_color());
0524     widget_current_style->set_stroke_color(ui.stroke_style_widget->current_color());
0525 
0526     // Transform widget
0527     view_trans_widget = new ViewTransformWidget(ui.status_bar);
0528     ui.status_bar->addPermanentWidget(view_trans_widget);
0529     connect(view_trans_widget, &ViewTransformWidget::zoom_changed, ui.canvas, &Canvas::set_zoom);
0530     connect(ui.canvas, &Canvas::zoomed, view_trans_widget, &ViewTransformWidget::set_zoom);
0531     connect(view_trans_widget, &ViewTransformWidget::zoom_in, ui.canvas, &Canvas::zoom_in);
0532     connect(view_trans_widget, &ViewTransformWidget::zoom_out, ui.canvas, &Canvas::zoom_out);
0533     connect(view_trans_widget, &ViewTransformWidget::angle_changed, ui.canvas, &Canvas::set_rotation);
0534     connect(ui.canvas, &Canvas::rotated, view_trans_widget, &ViewTransformWidget::set_angle);
0535     connect(view_trans_widget, &ViewTransformWidget::view_fit, parent, &GlaxnimateWindow::view_fit);
0536     connect(view_trans_widget, &ViewTransformWidget::flip_view, ui.action_flip_view, &QAction::trigger);
0537 }
0538 
0539 void GlaxnimateWindow::Private::init_docks()
0540 {
0541     // Scripting
0542     connect(ui.console, &ScriptConsole::error, parent, [this](const QString& plugin, const QString& message){
0543         show_warning(plugin, message, app::log::Error);
0544     });
0545     ui.console->set_global("window", QVariant::fromValue(parent));
0546     init_plugins();
0547 
0548     // Logs
0549     log_model.populate(GlaxnimateApp::instance()->log_lines());
0550     ui.view_logs->setModel(&log_model);
0551     ui.view_logs->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
0552     ui.view_logs->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
0553     ui.view_logs->horizontalHeader()->setSectionResizeMode(3, QHeaderView::Stretch);
0554     auto del = new style::BetterElideDelegate(Qt::ElideLeft, ui.view_logs);
0555     ui.view_logs->setItemDelegateForColumn(2, del);
0556 
0557     // Swatches
0558     palette_model.setSearchPaths(app::Application::instance()->data_paths_unchecked("palettes"));
0559     palette_model.setSavePath(app::Application::instance()->writable_data_path("palettes"));
0560     palette_model.load();
0561     ui.fill_style_widget->set_palette_model(&palette_model);
0562     ui.stroke_style_widget->set_palette_model(&palette_model);
0563     ui.document_swatch_widget->set_palette_model(&palette_model);
0564 
0565     connect(ui.document_swatch_widget, &DocumentSwatchWidget::needs_new_color, [this]{
0566         ui.document_swatch_widget->add_new_color(ui.fill_style_widget->current_color());
0567     });
0568     connect(ui.document_swatch_widget, &DocumentSwatchWidget::current_color_def, [this](model::BrushStyle* sty){
0569         set_color_def(sty, false);
0570     });
0571     connect(ui.document_swatch_widget, &DocumentSwatchWidget::secondary_color_def, [this](model::BrushStyle* sty){
0572         set_color_def(sty, true);
0573     });
0574 
0575     // Gradients
0576     ui.widget_gradients->set_window(parent);
0577         connect(ui.widget_gradients, &GradientListWidget::selected, parent, [this](model::BrushStyle* sty, bool secondary){
0578             set_brush_reference(sty, secondary);
0579     });
0580 
0581     // Fill/Stroke
0582     connect(ui.fill_style_widget, &FillStyleWidget::current_color_changed, parent, [this]{style_change_event();});
0583     connect(ui.fill_style_widget, &FillStyleWidget::secondary_color_changed, parent, [this]{style_change_event();});
0584     connect(ui.stroke_style_widget, &StrokeStyleWidget::pen_style_changed, parent, [this]{style_change_event();});
0585 
0586     // Tab bar
0587     connect(ui.tab_bar, &CompositionTabBar::switch_composition, parent, &GlaxnimateWindow::switch_composition);
0588     connect(ui.timeline_widget, &CompoundTimelineWidget::switch_composition, parent, &GlaxnimateWindow::switch_composition);
0589 
0590     // Align
0591     ui.separator_align_relative_to->setSeparator(true);
0592     ui.separator_align_horizontal->setSeparator(true);
0593     ui.separator_align_vertical->setSeparator(true);
0594     QActionGroup *align_relative = new QActionGroup(parent);
0595     align_relative->setExclusive(true);
0596     ui.action_align_to_canvas->setActionGroup(align_relative);
0597     ui.action_align_to_selection->setActionGroup(align_relative);
0598     ui.action_align_to_canvas_group->setActionGroup(align_relative);
0599 
0600     auto combo_align_to = new QComboBox(ui.dock_align->widget());
0601     combo_align_to->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
0602     ui.dock_align_grid->addWidget(combo_align_to, 0, 0, 1, 3);
0603 
0604     action_combo(combo_align_to, ui.action_align_to_selection);
0605     action_combo(combo_align_to, ui.action_align_to_canvas);
0606     action_combo(combo_align_to, ui.action_align_to_canvas_group);
0607     connect(combo_align_to, qOverload<int>(&QComboBox::currentIndexChanged), parent, [combo_align_to](int i){
0608         combo_align_to->itemData(i).value<QAction*>()->setChecked(true);
0609     });
0610 
0611     int row = 1;
0612     ui.dock_align_grid->addWidget(action_button(ui.action_align_hor_left, ui.dock_align->widget()),         row, 0);
0613     ui.dock_align_grid->addWidget(action_button(ui.action_align_hor_center, ui.dock_align->widget()),       row, 1);
0614     ui.dock_align_grid->addWidget(action_button(ui.action_align_hor_right, ui.dock_align->widget()),        row, 2);
0615     row++;
0616     ui.dock_align_grid->addWidget(action_button(ui.action_align_hor_left_out, ui.dock_align->widget()),     row, 0);
0617     ui.dock_align_grid->addWidget(action_button(ui.action_align_hor_right_out, ui.dock_align->widget()),    row, 2);
0618     row++;
0619     ui.dock_align_grid->addWidget(action_button(ui.action_align_vert_top, ui.dock_align->widget()),         row, 0);
0620     ui.dock_align_grid->addWidget(action_button(ui.action_align_vert_center, ui.dock_align->widget()),      row, 1);
0621     ui.dock_align_grid->addWidget(action_button(ui.action_align_vert_bottom, ui.dock_align->widget()),      row, 2);
0622     row++;
0623     ui.dock_align_grid->addWidget(action_button(ui.action_align_vert_top_out, ui.dock_align->widget()),     row, 0);
0624     ui.dock_align_grid->addWidget(action_button(ui.action_align_vert_bottom_out, ui.dock_align->widget()),  row, 2);
0625     row++;
0626     ui.dock_align_grid->addItem(new QSpacerItem(0, 0, QSizePolicy::Minimum, QSizePolicy::Expanding),        row, 0);
0627     ui.dock_align->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
0628 
0629     // Layout
0630     QActionGroup *layout_actions = new QActionGroup(parent);
0631     layout_actions->setExclusive(true);
0632     for ( const auto& action : ui.menu_layout->actions() )
0633     {
0634         action->setActionGroup(layout_actions);
0635         connect(action, &QAction::triggered, parent, [this]{layout_update();});
0636     }
0637 
0638     auto preset = LayoutPreset(app::settings::get<int>("ui", "layout"));
0639 
0640     switch ( preset )
0641     {
0642         case LayoutPreset::Unknown:
0643         case LayoutPreset::Auto:
0644             layout_auto();
0645             break;
0646         case LayoutPreset::Wide:
0647             layout_wide();
0648             break;
0649         case LayoutPreset::Compact:
0650             layout_compact();
0651             break;
0652         case LayoutPreset::Medium:
0653             layout_medium();
0654             break;
0655         case LayoutPreset::Custom:
0656             layout_auto();
0657             app::settings::set("ui", "layout", int(LayoutPreset::Custom));
0658             ui.action_layout_custom->setChecked(true);
0659             break;
0660     }
0661 }
0662 
0663 void GlaxnimateWindow::Private::layout_update()
0664 {
0665     if ( ui.action_layout_wide->isChecked() )
0666         layout_wide();
0667     else if ( ui.action_layout_compact->isChecked() )
0668         layout_compact();
0669     else if ( ui.action_layout_custom->isChecked() )
0670         layout_custom();
0671     else if ( ui.action_layout_medium->isChecked() )
0672         layout_medium();
0673     else
0674         layout_auto();
0675 }
0676 
0677 void GlaxnimateWindow::Private::layout_medium()
0678 {
0679     // Bottom
0680     parent->addDockWidget(Qt::BottomDockWidgetArea, ui.dock_timeline);
0681     parent->tabifyDockWidget(ui.dock_timeline, ui.dock_properties);
0682     parent->tabifyDockWidget(ui.dock_properties, ui.dock_script_console);
0683     parent->tabifyDockWidget(ui.dock_script_console, ui.dock_logs);
0684     parent->tabifyDockWidget(ui.dock_logs, ui.dock_time_slider);
0685     ui.dock_timeline->raise();
0686     ui.dock_time_slider->setVisible(false);
0687     ui.dock_timeline->setVisible(true);
0688     ui.dock_properties->setVisible(true);
0689 
0690     parent->addDockWidget(Qt::BottomDockWidgetArea, ui.dock_snippets);
0691 
0692     // Bottom Right
0693     parent->addDockWidget(Qt::BottomDockWidgetArea, ui.dock_layers);
0694     parent->tabifyDockWidget(ui.dock_layers, ui.dock_gradients);
0695     parent->tabifyDockWidget(ui.dock_gradients, ui.dock_swatches);
0696     parent->tabifyDockWidget(ui.dock_swatches, ui.dock_assets);
0697     ui.dock_gradients->raise();
0698     ui.dock_gradients->setVisible(true);
0699     ui.dock_assets->setVisible(false);
0700     ui.dock_swatches->setVisible(false);
0701     ui.dock_layers->setVisible(true);
0702 
0703 
0704     // Right
0705     parent->addDockWidget(Qt::RightDockWidgetArea, ui.dock_colors);
0706     parent->tabifyDockWidget(ui.dock_colors, ui.dock_stroke);
0707     parent->tabifyDockWidget(ui.dock_stroke, ui.dock_undo);
0708     ui.dock_colors->raise();
0709     ui.dock_colors->setVisible(true);
0710     ui.dock_stroke->setVisible(true);
0711     ui.dock_undo->setVisible(true);
0712 
0713 
0714     // Left
0715     parent->addDockWidget(Qt::LeftDockWidgetArea, ui.dock_tools);
0716 
0717     parent->addDockWidget(Qt::LeftDockWidgetArea, ui.dock_tool_options);
0718     parent->tabifyDockWidget(ui.dock_tool_options, ui.dock_align);
0719     ui.dock_tool_options->raise();
0720     ui.dock_tool_options->setVisible(true);
0721     ui.dock_align->setVisible(true);
0722 
0723     // Resize
0724     parent->resizeDocks({ui.dock_snippets}, {1}, Qt::Horizontal);
0725     parent->resizeDocks({ui.dock_layers}, {1}, Qt::Horizontal);
0726     parent->resizeDocks({ui.dock_tools}, {200}, Qt::Horizontal);
0727     parent->resizeDocks({ui.dock_tool_options, ui.dock_align, ui.dock_tools}, {1, 1, 4000}, Qt::Vertical);
0728     parent->resizeDocks({ui.dock_timeline}, {300}, Qt::Vertical);
0729     ui.dock_script_console->setVisible(false);
0730     ui.dock_logs->setVisible(false);
0731     ui.dock_tools->setVisible(false);
0732     ui.dock_snippets->setVisible(false);
0733 
0734     // Resize parent to have a reasonable default size
0735     parent->resize(1440, 900);
0736 
0737     app::settings::set("ui", "layout", int(LayoutPreset::Medium));
0738     ui.action_layout_medium->setChecked(true);
0739 }
0740 
0741 void GlaxnimateWindow::Private::layout_wide()
0742 {
0743     // Bottom
0744     parent->addDockWidget(Qt::BottomDockWidgetArea, ui.dock_timeline);
0745     parent->tabifyDockWidget(ui.dock_timeline, ui.dock_properties);
0746     parent->tabifyDockWidget(ui.dock_properties, ui.dock_script_console);
0747     parent->tabifyDockWidget(ui.dock_script_console, ui.dock_logs);
0748     parent->tabifyDockWidget(ui.dock_logs, ui.dock_time_slider);
0749     ui.dock_timeline->raise();
0750     ui.dock_time_slider->setVisible(false);
0751     ui.dock_timeline->setVisible(true);
0752     ui.dock_properties->setVisible(true);
0753 
0754     parent->addDockWidget(Qt::BottomDockWidgetArea, ui.dock_snippets);
0755 
0756     // Bottom Right
0757     parent->addDockWidget(Qt::BottomDockWidgetArea, ui.dock_layers);
0758     parent->tabifyDockWidget(ui.dock_layers, ui.dock_gradients);
0759     parent->tabifyDockWidget(ui.dock_gradients, ui.dock_swatches);
0760     parent->tabifyDockWidget(ui.dock_swatches, ui.dock_assets);
0761     ui.dock_gradients->raise();
0762     ui.dock_gradients->setVisible(true);
0763     ui.dock_assets->setVisible(false);
0764     ui.dock_swatches->setVisible(false);
0765     ui.dock_layers->setVisible(true);
0766 
0767 
0768     // Right
0769     parent->addDockWidget(Qt::RightDockWidgetArea, ui.dock_colors);
0770     parent->tabifyDockWidget(ui.dock_colors, ui.dock_stroke);
0771     parent->tabifyDockWidget(ui.dock_stroke, ui.dock_undo);
0772     ui.dock_colors->raise();
0773     ui.dock_colors->setVisible(true);
0774     ui.dock_stroke->setVisible(true);
0775     ui.dock_undo->setVisible(true);
0776 
0777 
0778     // Left
0779     parent->addDockWidget(Qt::LeftDockWidgetArea, ui.dock_tools);
0780 
0781     parent->addDockWidget(Qt::LeftDockWidgetArea, ui.dock_tool_options);
0782     parent->addDockWidget(Qt::LeftDockWidgetArea, ui.dock_align);
0783     ui.dock_tool_options->setVisible(true);
0784     ui.dock_align->setVisible(true);
0785 
0786     // Resize
0787     parent->resizeDocks({ui.dock_snippets}, {1}, Qt::Horizontal);
0788     parent->resizeDocks({ui.dock_layers}, {1}, Qt::Horizontal);
0789     parent->resizeDocks({ui.dock_tools}, {200}, Qt::Horizontal);
0790     parent->resizeDocks({ui.dock_tool_options, ui.dock_align, ui.dock_tools}, {1, 1, 4000}, Qt::Vertical);
0791     parent->resizeDocks({ui.dock_timeline}, {1080/3}, Qt::Vertical);
0792     ui.dock_script_console->setVisible(false);
0793     ui.dock_logs->setVisible(false);
0794     ui.dock_tools->setVisible(false);
0795     ui.dock_snippets->setVisible(false);
0796 
0797     // Resize parent to have a reasonable default size
0798     parent->resize(1920, 1080);
0799 
0800     app::settings::set("ui", "layout", int(LayoutPreset::Wide));
0801     ui.action_layout_wide->setChecked(true);
0802 }
0803 
0804 void GlaxnimateWindow::Private::layout_compact()
0805 {
0806     // Bottom
0807     parent->addDockWidget(Qt::BottomDockWidgetArea, ui.dock_time_slider);
0808     parent->tabifyDockWidget(ui.dock_time_slider, ui.dock_timeline);
0809     parent->tabifyDockWidget(ui.dock_timeline, ui.dock_script_console);
0810     parent->tabifyDockWidget(ui.dock_script_console, ui.dock_logs);
0811     ui.dock_time_slider->raise();
0812     ui.dock_time_slider->setVisible(true);
0813     ui.dock_timeline->setVisible(false);
0814     ui.dock_logs->setVisible(false);
0815 
0816     parent->addDockWidget(Qt::BottomDockWidgetArea, ui.dock_snippets);
0817 
0818     // Right
0819     parent->addDockWidget(Qt::RightDockWidgetArea, ui.dock_colors);
0820     parent->tabifyDockWidget(ui.dock_colors, ui.dock_stroke);
0821     parent->tabifyDockWidget(ui.dock_stroke, ui.dock_layers);
0822     parent->tabifyDockWidget(ui.dock_layers, ui.dock_properties);
0823     parent->tabifyDockWidget(ui.dock_properties, ui.dock_undo);
0824     parent->tabifyDockWidget(ui.dock_undo, ui.dock_gradients);
0825     parent->tabifyDockWidget(ui.dock_gradients, ui.dock_swatches);
0826     parent->tabifyDockWidget(ui.dock_swatches, ui.dock_assets);
0827     ui.dock_colors->raise();
0828     ui.dock_colors->setVisible(true);
0829     ui.dock_stroke->setVisible(true);
0830     ui.dock_gradients->setVisible(false);
0831     ui.dock_assets->setVisible(false);
0832     ui.dock_swatches->setVisible(false);
0833     ui.dock_undo->setVisible(false);
0834     ui.dock_properties->setVisible(true);
0835     ui.dock_layers->setVisible(true);
0836 
0837     // Left
0838     parent->addDockWidget(Qt::LeftDockWidgetArea, ui.dock_tools);
0839 
0840     parent->addDockWidget(Qt::LeftDockWidgetArea, ui.dock_tool_options);
0841     parent->tabifyDockWidget(ui.dock_tool_options, ui.dock_align);
0842     ui.dock_tool_options->raise();
0843     ui.dock_tool_options->setVisible(true);
0844     ui.dock_align->setVisible(true);
0845 
0846     // Resize
0847     parent->resizeDocks({ui.dock_tool_options, ui.dock_align, ui.dock_tools}, {1, 1, 4000}, Qt::Vertical);
0848     parent->resizeDocks({ui.dock_time_slider}, {64}, Qt::Vertical);
0849     ui.dock_script_console->setVisible(false);
0850     ui.dock_logs->setVisible(false);
0851     ui.dock_tools->setVisible(false);
0852     ui.dock_snippets->setVisible(false);
0853 
0854     // Resize parent to have a reasonable default size
0855     parent->resize(1366, 768);
0856 
0857     app::settings::set("ui", "layout", int(LayoutPreset::Compact));
0858     ui.action_layout_compact->setChecked(true);
0859 }
0860 
0861 void GlaxnimateWindow::Private::layout_auto()
0862 {
0863     auto real_estate = qApp->primaryScreen()->availableSize();
0864     if ( real_estate.width() >= 1920 && real_estate.height() >= 1080 )
0865         layout_wide();
0866     else if ( real_estate.width() >= 1440 && real_estate.height() >= 900 )
0867         layout_medium();
0868     else
0869         layout_compact();
0870 
0871     app::settings::set("ui", "layout", int(LayoutPreset::Auto));
0872     ui.action_layout_automatic->setChecked(true);
0873 }
0874 
0875 void GlaxnimateWindow::Private::layout_custom()
0876 {
0877     init_restore_state();
0878     app::settings::set("ui", "layout", int(LayoutPreset::Custom));
0879     ui.action_layout_custom->setChecked(true);
0880 }
0881 
0882 void GlaxnimateWindow::Private::init_template_menu()
0883 {
0884     ui.menu_new_from_template->clear();
0885 
0886     for ( const auto& templ : settings::DocumentTemplates::instance().templates() )
0887         ui.menu_new_from_template->addAction(settings::DocumentTemplates::instance().create_action(templ, ui.menu_new_from_template));
0888 }
0889 
0890 void GlaxnimateWindow::Private::init_menus()
0891 {
0892     // Menu Views
0893     for ( QDockWidget* wid : parent->findChildren<QDockWidget*>() )
0894     {
0895         QAction* act = wid->toggleViewAction();
0896         act->setIcon(wid->windowIcon());
0897         ui.menu_views->addAction(act);
0898         wid->setStyle(&dock_style);
0899     }
0900 
0901     // Menu Toolbars
0902     for ( QToolBar* wid : parent->findChildren<QToolBar*>() )
0903     {
0904         QAction* act = wid->toggleViewAction();
0905         ui.menu_toolbars->addAction(act);
0906         wid->setStyle(&dock_style);
0907     }
0908 
0909     // Load keyboard shortcuts
0910     GlaxnimateApp::instance()->shortcuts()->add_menu(ui.menu_file);
0911     GlaxnimateApp::instance()->shortcuts()->add_menu(ui.menu_edit);
0912     GlaxnimateApp::instance()->shortcuts()->add_menu(ui.menu_tools);
0913     GlaxnimateApp::instance()->shortcuts()->add_menu(ui.menu_view);
0914     GlaxnimateApp::instance()->shortcuts()->add_menu(ui.menu_views);
0915     GlaxnimateApp::instance()->shortcuts()->add_menu(ui.menu_document);
0916     GlaxnimateApp::instance()->shortcuts()->add_menu(ui.menu_render_single_frame);
0917     GlaxnimateApp::instance()->shortcuts()->add_menu(ui.menu_playback);
0918     GlaxnimateApp::instance()->shortcuts()->add_menu(ui.menu_layers);
0919     GlaxnimateApp::instance()->shortcuts()->add_menu(ui.menu_new_layer);
0920     GlaxnimateApp::instance()->shortcuts()->add_menu(ui.menu_object);
0921     GlaxnimateApp::instance()->shortcuts()->add_menu(ui.menu_path);
0922     GlaxnimateApp::instance()->shortcuts()->add_menu(ui.menu_text);
0923     GlaxnimateApp::instance()->shortcuts()->add_menu(ui.menu_plugins);
0924     GlaxnimateApp::instance()->shortcuts()->add_menu(ui.menu_help);
0925 
0926     // Menu Templates
0927     init_template_menu();
0928 
0929     connect(&settings::DocumentTemplates::instance(), &settings::DocumentTemplates::loaded, parent, [this]{
0930         init_template_menu();
0931     });
0932 
0933     connect(&settings::DocumentTemplates::instance(), &settings::DocumentTemplates::create_from, parent,
0934         [this](const settings::DocumentTemplate& templ){
0935             if ( !close_document() )
0936                 return;
0937 
0938             bool ok = false;
0939             setup_document_ptr(templ.create(&ok));
0940             if ( !ok )
0941                 show_warning(i18n("New from Template"), i18n("Could not load template"));
0942         }
0943     );
0944 
0945     connect(ui.action_save_as_template, &QAction::triggered, parent, [this]{
0946         bool ok = true;
0947 
0948         QString old_name = comp->name.get();
0949         QString name = QInputDialog::getText(parent, i18n("Save as Template"), i18n("Name"), QLineEdit::Normal, old_name, &ok);
0950         if ( !ok )
0951             return;
0952 
0953         comp->name.set(name);
0954         if ( !settings::DocumentTemplates::instance().save_as_template(current_document.get()) )
0955             show_warning(i18n("Save as Template"), i18n("Could not save template"));
0956         comp->name.set(old_name);
0957     });
0958 }
0959 
0960 
0961 void GlaxnimateWindow::Private::init_tools(tools::Tool* to_activate)
0962 {
0963     tools::Event event{ui.canvas, &scene, parent};
0964     for ( const auto& grp : tools::Registry::instance() )
0965     {
0966         for ( const auto& tool : grp.second )
0967         {
0968             tool.second->retranslate();
0969             tool.second->initialize(event);
0970 
0971             if ( to_activate == tool.second.get() )
0972                 switch_tool(tool.second.get());
0973         }
0974     }
0975 }
0976 
0977 void GlaxnimateWindow::Private::init_restore_state()
0978 {
0979     parent->restoreState(app::settings::get<QByteArray>("ui", "window_state"));
0980     ui.timeline_widget->load_state(app::settings::get<QByteArray>("ui", "timeline_splitter"));
0981     parent->restoreGeometry(app::settings::get<QByteArray>("ui", "window_geometry"));
0982 
0983     // Hide tool widgets, as they might get shown by restoreState
0984     ui.toolbar_node->setVisible(false);
0985     ui.toolbar_node->setEnabled(false);
0986 }
0987 
0988 void GlaxnimateWindow::Private::retranslateUi(QMainWindow* parent)
0989 {
0990     ui.retranslateUi(parent);
0991     label_recording->setText(i18n("Recording Keyframes"));
0992 
0993     ui.action_undo->setText(i18n("Undo %1", current_document->undo_stack().undoText()));
0994     ui.action_redo->setText(i18n("Redo %1", current_document->undo_stack().redoText()));
0995 
0996     for ( const auto& grp : tools::Registry::instance() )
0997         for ( const auto& tool : grp.second )
0998             tool.second->retranslate();
0999 }
1000 
1001 void GlaxnimateWindow::Private::view_fit()
1002 {
1003     ui.canvas->view_fit();
1004 }
1005 
1006 void GlaxnimateWindow::Private::reload_recent_menu()
1007 {
1008     QIcon icon(GlaxnimateApp::instance()->data_file("images/glaxnimate-file.svg"));
1009     ui.menu_open_recent->clear();
1010     for ( const auto& recent : recent_files )
1011     {
1012         QAction* act = new QAction(icon, QFileInfo(recent).fileName(), ui.menu_open_recent);
1013         act->setToolTip(recent);
1014         act->setData(recent);
1015         ui.menu_open_recent->addAction(act);
1016     }
1017 }
1018 
1019 void GlaxnimateWindow::Private::most_recent_file(const QString& s)
1020 {
1021     recent_files.removeAll(s);
1022     recent_files.push_front(s);
1023     ui.action_open_last->setEnabled(true);
1024 
1025     int max = app::settings::get<int>("open_save", "max_recent_files");
1026     if ( recent_files.size() > max )
1027         recent_files.erase(recent_files.begin() + max, recent_files.end());
1028 
1029     reload_recent_menu();
1030 }
1031 
1032 void GlaxnimateWindow::Private::show_warning(const QString& title, const QString& message, app::log::Severity icon)
1033 {
1034     KMessageWidget::MessageType message_type;
1035     switch ( icon )
1036     {
1037         case app::log::Severity::Info: message_type = KMessageWidget::Information; break;
1038         case app::log::Severity::Warning: message_type = KMessageWidget::Warning; break;
1039         case app::log::Severity::Error: message_type = KMessageWidget::Error; break;
1040     }
1041     ui.message_widget->queue_message({message, message_type});
1042     app::log::Log(title).log(message, icon);
1043 }
1044 
1045 void GlaxnimateWindow::Private::help_about()
1046 {
1047     about_dialog->show();
1048 }
1049 
1050 void GlaxnimateWindow::Private::shutdown()
1051 {
1052     app::settings::set("ui", "window_geometry", parent->saveGeometry());
1053     app::settings::set("ui", "window_state", parent->saveState());
1054     app::settings::set("ui", "timeline_splitter", ui.timeline_widget->save_state());
1055     app::settings::set("open_save", "recent_files", recent_files);
1056 
1057     ui.fill_style_widget->save_settings();
1058     ui.stroke_style_widget->save_settings();
1059 
1060     ui.console->save_settings();
1061     ui.console->clear_contexts();
1062 }
1063 
1064 
1065 void GlaxnimateWindow::Private::switch_tool(tools::Tool* tool)
1066 {
1067     if ( !tool || tool == active_tool )
1068         return;
1069 
1070     if ( !tool->get_action()->isChecked() )
1071         tool->get_action()->setChecked(true);
1072 
1073     if ( active_tool )
1074     {
1075         for ( const auto& widget : tool_widgets[active_tool->id()] )
1076         {
1077             widget->setVisible(false);
1078             widget->setEnabled(false);
1079         }
1080 
1081         for ( const auto& action : tool_actions[active_tool->id()] )
1082         {
1083             action->setEnabled(false);
1084         }
1085     }
1086 
1087 
1088     for ( const auto& widget : tool_widgets[tool->id()] )
1089     {
1090         widget->setVisible(true);
1091         widget->setEnabled(true);
1092     }
1093 
1094     for ( const auto& action : tool_actions[tool->id()] )
1095     {
1096         action->setEnabled(true);
1097     }
1098 
1099     active_tool = tool;
1100     scene.set_active_tool(tool);
1101     ui.canvas->set_active_tool(tool);
1102 
1103     if ( auto old_wid = ui.tool_settings_widget->currentWidget() )
1104         old_wid->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
1105 
1106     auto new_wid = tool->get_settings_widget();
1107     new_wid->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
1108     ui.tool_settings_widget->setCurrentWidget(new_wid);
1109     if ( active_tool->group() == tools::Registry::Draw || active_tool->group() == tools::Registry::Shape )
1110         widget_current_style->clear_gradients();
1111 }
1112 
1113 void GlaxnimateWindow::Private::switch_tool_action(QAction* action)
1114 {
1115     switch_tool(action->data().value<tools::Tool*>());
1116 }
1117 
1118 void GlaxnimateWindow::Private::status_message(const QString& message, int duration)
1119 {
1120     ui.status_bar->showMessage(message, duration);
1121 }
1122 
1123 void GlaxnimateWindow::Private::trace_dialog(model::DocumentNode* object)
1124 {
1125     model::Image* bmp = nullptr;
1126     if ( object )
1127     {
1128         bmp = object->cast<model::Image>();
1129     }
1130 
1131     if ( !bmp )
1132     {
1133         for ( const auto& sel : scene.selection() )
1134         {
1135             if ( auto image = sel->cast<model::Image>() )
1136             {
1137                 if ( bmp )
1138                 {
1139                     show_warning(i18n("Trace Bitmap"), i18n("Only select one image"), app::log::Info);
1140                     return;
1141                 }
1142                 bmp = image;
1143             }
1144         }
1145 
1146         if ( !bmp )
1147         {
1148             show_warning(i18n("Trace Bitmap"), i18n("You need to select an image to trace"), app::log::Info);
1149             return;
1150         }
1151     }
1152 
1153     if ( !bmp->image.get() )
1154     {
1155         show_warning(i18n("Trace Bitmap"), i18n("You selected an image with no data"), app::log::Info);
1156         return;
1157     }
1158 
1159     TraceDialog dialog(bmp, parent);
1160     dialog.exec();
1161     if ( auto created = dialog.created() )
1162         ui.view_document_node->set_current_node(created);
1163 }
1164 
1165 
1166 void GlaxnimateWindow::Private::init_plugins()
1167 {
1168     auto& par = plugin::PluginActionRegistry::instance();
1169     for ( auto act : par.enabled() )
1170     {
1171         ui.menu_plugins->addAction(par.make_qaction(act));
1172     }
1173 
1174     connect(&par, &plugin::PluginActionRegistry::action_added, parent, [this](plugin::ActionService* action, plugin::ActionService* before) {
1175         QAction* insert = nullptr;
1176         for ( auto act : ui.menu_plugins->actions() )
1177         {
1178             if ( act->data().value<plugin::ActionService*>() == before )
1179             {
1180                 insert = act;
1181                 break;
1182             }
1183         }
1184         QAction* qaction = plugin::PluginActionRegistry::instance().make_qaction(action);
1185         ui.menu_plugins->insertAction(insert, qaction);
1186 
1187         app::settings::ShortcutGroup* group = GlaxnimateApp::instance()->shortcuts()->find_group(ui.menu_plugins->menuAction()->iconText());
1188         if (group)
1189             group->actions.push_back(GlaxnimateApp::instance()->shortcuts()->add_action(qaction));
1190     });
1191 
1192     connect(&par, &plugin::PluginActionRegistry::action_removed, parent, [](plugin::ActionService* action) {
1193         QString slug = "action_plugin_" + action->plugin()->data().name.toLower() + "_" + action->label.toLower();
1194         app::settings::ShortcutAction* act = GlaxnimateApp::instance()->shortcuts()->action(slug);
1195         GlaxnimateApp::instance()->shortcuts()->remove_action(act);
1196     });
1197 
1198     connect(
1199         &plugin::PluginRegistry::instance(),
1200         &plugin::PluginRegistry::loaded,
1201         ui.console,
1202         &ScriptConsole::clear_contexts
1203     );
1204 
1205     plugin::PluginRegistry::instance().set_executor(ui.console);
1206 }
1207 
1208 void GlaxnimateWindow::Private::mouse_moved(const QPointF& pos)
1209 {
1210     label_mouse_pos->setText(ki18n("X: %1 Y: %2").subs(pos.x(), 8, 'f', 3).subs(pos.y(), 8, 'f', 3).toString());
1211 }
1212 
1213 void GlaxnimateWindow::Private::show_startup_dialog()
1214 {
1215     if ( !app::settings::get<bool>("ui", "startup_dialog") )
1216         return;
1217 
1218     StartupDialog dialog(parent);
1219     connect(&dialog, &StartupDialog::open_recent, parent, &GlaxnimateWindow::document_open);
1220     connect(&dialog, &StartupDialog::open_browse, parent, &GlaxnimateWindow::document_open_dialog);
1221     if ( dialog.exec() )
1222         setup_document_ptr(dialog.create());
1223 }
1224 
1225 
1226 void GlaxnimateWindow::Private::drop_file(const QString& file)
1227 {
1228     QDialog dialog(parent);
1229     dialog.setWindowTitle(i18n("Drop File"));
1230     QIcon icon = QIcon::fromTheme("dialog-question");
1231     dialog.setWindowIcon(icon);
1232     QVBoxLayout lay;
1233     dialog.setLayout(&lay);
1234 
1235     QHBoxLayout lay1;
1236     QLabel label_icon;
1237     label_icon.setPixmap(icon.pixmap(64));
1238     lay1.addWidget(&label_icon);
1239     QLabel label_text;
1240     label_text.setText(i18n("Add to current file?"));
1241     label_text.setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
1242     lay1.addWidget(&label_text);
1243     lay.addLayout(&lay1);
1244 
1245     QHBoxLayout lay2;
1246     QPushButton btn1(i18n("Add as Object"));
1247     lay2.addWidget(&btn1);
1248     connect(&btn1, &QPushButton::clicked, &dialog, [&dialog, this, &file]{
1249         drop_document(file, false);
1250         dialog.accept();
1251     });
1252     QPushButton btn2(i18n("Add as Composition"));
1253     lay2.addWidget(&btn2);
1254     connect(&btn2, &QPushButton::clicked, &dialog, [&dialog, this, &file]{
1255         drop_document(file, true);
1256         dialog.accept();
1257     });
1258     QPushButton btn3(i18n("Open"));
1259     lay2.addWidget(&btn3);
1260     connect(&btn3, &QPushButton::clicked, &dialog, [&dialog, this, &file]{
1261         document_open_from_filename(file);
1262         dialog.accept();
1263     });
1264     lay.addLayout(&lay2);
1265 
1266     dialog.exec();
1267 }
1268 
1269 void GlaxnimateWindow::Private::insert_emoji()
1270 {
1271     emoji::EmojiSetDialog dialog;
1272     if ( !dialog.exec() || dialog.selected_svg().isEmpty() )
1273         return;
1274     import_file(dialog.selected_svg(), {{"forced_size", comp->size()}});
1275 }
1276 
1277 void GlaxnimateWindow::Private::import_from_lottiefiles()
1278 {
1279     LottieFilesSearchDialog dialog;
1280     if ( !dialog.exec() )
1281         return;
1282 
1283     io::Options options;
1284     options.format = io::IoRegistry::instance().from_slug("lottie");
1285     options.filename = dialog.selected_name() + ".json";
1286 
1287     load_remote_document(dialog.selected_url(), options, dialog.result() == LottieFilesSearchDialog::Open);
1288 }
1289 
1290 QVariant GlaxnimateWindow::Private::choose_option(const QString& label, const QVariantMap& options, const QString& title) const
1291 {
1292     QDialog dialog(parent);
1293     if ( !title.isEmpty() )
1294         dialog.setWindowTitle(title);
1295 
1296     QVBoxLayout* layout = new QVBoxLayout();
1297     dialog.setLayout(layout);
1298 
1299     QLabel qlabel;
1300     qlabel.setText(label);
1301     layout->addWidget(&qlabel);
1302 
1303     QComboBox box;
1304     layout->addWidget(&box);
1305     int count = 0;
1306     for ( auto it = options.begin(); it != options.end(); ++it )
1307     {
1308         box.insertItem(count++, it.key(), *it);
1309     }
1310 
1311     layout->addItem(new QSpacerItem(0, 0, QSizePolicy::Minimum, QSizePolicy::Expanding));
1312 
1313     QDialogButtonBox buttons;
1314     buttons.addButton(QDialogButtonBox::Ok);
1315     buttons.addButton(QDialogButtonBox::Cancel);
1316     connect(&buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
1317     connect(&buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
1318     layout->addWidget(&buttons);
1319 
1320     if ( !dialog.exec() )
1321         return {};
1322 
1323     return box.currentData();
1324 }