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

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 <QTemporaryFile>
0010 #include <QDesktopServices>
0011 #include <QFileDialog>
0012 #include <QImageWriter>
0013 #include <QDropEvent>
0014 #include <QtConcurrent>
0015 #include <QEventLoop>
0016 #include <QLocalSocket>
0017 #include <QDataStream>
0018 #include <QSharedMemory>
0019 #include <QtGlobal>
0020 #include <QNetworkReply>
0021 
0022 
0023 #include "io/lottie/lottie_html_format.hpp"
0024 #include "io/svg/svg_renderer.hpp"
0025 #include "io/svg/svg_html_format.hpp"
0026 #include "io/glaxnimate/glaxnimate_format.hpp"
0027 #include "io/raster/raster_mime.hpp"
0028 #include "io/lottie/tgs_format.hpp"
0029 #include "io/lottie/validation.hpp"
0030 #include "io/rive/rive_html_format.hpp"
0031 #include "plugin/io.hpp"
0032 
0033 #include "model/visitor.hpp"
0034 #include "widgets/font/font_loader.hpp"
0035 
0036 #include "command/undo_macro_guard.hpp"
0037 #include "command/undo_macro_guard.hpp"
0038 
0039 #include "tools/base.hpp"
0040 
0041 #include "glaxnimate_app.hpp"
0042 #include "app_info.hpp"
0043 #include "widgets/dialogs/import_export_dialog.hpp"
0044 #include "widgets/dialogs/io_status_dialog.hpp"
0045 #include "widgets/shape_style/shape_style_preview_widget.hpp"
0046 #include "widgets/dialogs/stalefiles_dialog.hpp"
0047 
0048 template<class T>
0049 static void process_events(const QFuture<T>& promise)
0050 {
0051     while ( !promise.isFinished() )
0052     {
0053         qApp->processEvents(QEventLoop::ExcludeUserInputEvents|QEventLoop::WaitForMoreEvents, 10);
0054     }
0055 }
0056 
0057 void GlaxnimateWindow::Private::setup_document_ptr(std::unique_ptr<model::Document> doc)
0058 {
0059     if ( !close_document() )
0060         return;
0061 
0062     current_document = std::move(doc);
0063 
0064     do_setup_document();
0065 
0066     QDir path = app::settings::get<QString>("open_save", "path");
0067     auto opts = current_document->io_options();
0068     opts.path = path;
0069     current_document->set_io_options(opts);
0070 
0071     view_fit();
0072     if ( comp && !comp->shapes.empty() )
0073         ui.view_document_node->set_current_node(comp->shapes[0]);
0074 
0075     ui.timeline_widget->reset_view();
0076 }
0077 
0078 void GlaxnimateWindow::Private::do_setup_document()
0079 {
0080     current_document_has_file = false;
0081 
0082     // Composition
0083     comp = nullptr;
0084     connect(current_document->assets()->compositions.get(), &model::CompositionList::docnode_child_remove_end, parent, [this](model::DocumentNode*, int index){on_remove_precomp(index);});
0085     connect(current_document->assets()->compositions.get(), &model::CompositionList::precomp_added, parent, [this](model::Composition* node, int row){setup_composition(node, row);});
0086     ui.menu_new_comp_layer->setEnabled(false);
0087     for ( const auto& precomp : current_document->assets()->compositions->values )
0088         setup_composition(precomp.get());
0089 
0090     // Undo Redo
0091     parent->undo_group().addStack(&current_document->undo_stack());
0092     parent->undo_group().setActiveStack(&current_document->undo_stack());
0093 
0094     // Views
0095     document_node_model.set_document(current_document.get());
0096     ui.view_document_node->set_composition(comp);
0097     ui.timeline_widget->set_document(current_document.get());
0098     ui.timeline_widget->set_composition(comp);
0099     ui.view_assets->setRootIndex(asset_model.mapFromSource(document_node_model.node_index(current_document->assets()).siblingAtColumn(1)));
0100 
0101     property_model.set_document(current_document.get());
0102     property_model.set_object(comp);
0103     ui.tab_bar->set_document(current_document.get());
0104 
0105     scene.set_document(current_document.get());
0106 
0107     if ( !current_document->assets()->compositions->values.empty() )
0108         switch_composition(current_document->assets()->compositions->values[0], 0);
0109 
0110     ui.document_swatch_widget->set_document(current_document.get());
0111     ui.widget_gradients->set_document(current_document.get());
0112 
0113     // Scripting
0114     ui.console->clear_contexts();
0115     ui.console->set_global("document", QVariant::fromValue(current_document.get()));
0116 
0117     // Title
0118     QObject::connect(current_document.get(), &model::Document::filename_changed, parent, &GlaxnimateWindow::refresh_title);
0119     QObject::connect(&current_document->undo_stack(), &QUndoStack::cleanChanged, parent, &GlaxnimateWindow::refresh_title);
0120     refresh_title();
0121 
0122     // Playback
0123     ui.play_controls->set_record_enabled(false);
0124     ui.play_controls_2->set_record_enabled(false);
0125     ///...
0126     QObject::connect(ui.play_controls, &FrameControlsWidget::frame_selected, current_document.get(), [this](int frame){current_document->set_current_time(frame);});
0127     QObject::connect(current_document.get(), &model::Document::current_time_changed, ui.play_controls, [this](float frame){ui.play_controls->set_frame(frame);});
0128     QObject::connect(current_document.get(), &model::Document::record_to_keyframe_changed, ui.play_controls, &FrameControlsWidget::set_record_enabled);
0129     QObject::connect(current_document.get(), &model::Document::record_to_keyframe_changed, ui.play_controls_2, &FrameControlsWidget::set_record_enabled);
0130     QObject::connect(ui.play_controls, &FrameControlsWidget::record_toggled, current_document.get(), &model::Document::set_record_to_keyframe);
0131     QObject::connect(ui.play_controls_2, &FrameControlsWidget::record_toggled, current_document.get(), &model::Document::set_record_to_keyframe);
0132 
0133     widget_recording->setVisible(false);
0134     QObject::connect(current_document.get(), &model::Document::record_to_keyframe_changed, widget_recording, &QWidget::setVisible);
0135 
0136     // Export
0137     export_options = {};
0138     ui.action_export->setText(i18n("Export..."));
0139 }
0140 
0141 void GlaxnimateWindow::Private::setup_document_new(const QString& filename)
0142 {
0143     if ( !close_document() )
0144         return;
0145 
0146     current_document = std::make_unique<model::Document>(filename);
0147     current_document->assets()->add_comp_no_undo();
0148 
0149     do_setup_document();
0150     QUrl url = QUrl::fromLocalFile(i18n("Unsaved Animation"));
0151     autosave_file.setManagedFile(url);
0152     comp->name.set(comp->type_name_human());
0153     comp->width.set(app::settings::get<int>("defaults", "width"));
0154     comp->height.set(app::settings::get<int>("defaults", "height"));
0155     comp->fps.set(app::settings::get<float>("defaults", "fps"));
0156     float duration = app::settings::get<float>("defaults", "duration");
0157     int out_point = comp->fps.get() * duration;
0158     comp->animation->last_frame.set(out_point);
0159 
0160 
0161     auto layer = std::make_unique<model::Layer>(current_document.get());
0162     layer->animation->last_frame.set(out_point);
0163     layer->name.set(layer->type_name_human());
0164     QPointF pos(
0165         comp->width.get() / 2.0,
0166         comp->height.get() / 2.0
0167     );
0168     layer->transform.get()->anchor_point.set(pos);
0169     layer->transform.get()->position.set(pos);
0170     model::ShapeElement* ptr = layer.get();
0171     comp->shapes.insert(std::move(layer), 0);
0172 
0173     QDir path = app::settings::get<QString>("open_save", "path");
0174     auto opts = current_document->io_options();
0175     opts.path = path;
0176     current_document->set_io_options(opts);
0177 
0178     ui.view_document_node->set_current_node(ptr);
0179     ui.play_controls->set_range(0, out_point);
0180     view_fit();
0181 
0182     ui.timeline_widget->reset_view();
0183 }
0184 
0185 bool GlaxnimateWindow::Private::setup_document_open(QIODevice* file, const io::Options& options, bool is_file)
0186 {
0187     if ( !close_document() )
0188         return false;
0189 
0190     current_document = std::make_unique<model::Document>(options.filename);
0191 
0192     dialog_import_status->reset(options.format, options.filename);
0193     /*auto promise = QtConcurrent::run(
0194         [options, current_document=current_document.get()]{
0195             QFile file(options.filename);
0196             return options.format->open(file, options.filename, current_document, options.settings);
0197         });
0198 
0199     process_events(promise);
0200 
0201     bool ok = promise.result();*/
0202 
0203     current_document->set_io_options(options);
0204 
0205     bool ok = options.format->open(*file, options.filename, current_document.get(), options.settings);
0206 
0207     do_setup_document();
0208 
0209     if ( is_file )
0210     {
0211         current_document_has_file = true;
0212 
0213         app::settings::set<QString>("open_save", "path", options.path.absolutePath());
0214 
0215         if ( ok && !autosave_load )
0216             most_recent_file(options.filename);
0217     }
0218 
0219     view_fit();
0220 
0221 
0222     QUrl file_url = QUrl::fromLocalFile(options.filename);
0223     auto stale = KAutoSaveFile::staleFiles(file_url);
0224     autosave_file.setManagedFile(file_url);
0225 
0226     if ( !autosave_load && !stale.empty() )
0227     {
0228         // This ensures the stale file is not leaked if the user doesn't load it.
0229         stale[0]->setParent(current_document.get());
0230 
0231         WindowMessageWidget::Message msg{
0232             i18n("Looks like this file is being edited by another Glaxnimate instance or it was being edited when Glaxnimate crashed."),
0233             KMessageWidget::Information
0234         };
0235 
0236         msg.add_action(
0237             QIcon::fromTheme("document-close"),
0238             i18n("Close Document"),
0239             parent,
0240             &GlaxnimateWindow::document_new
0241         );
0242 
0243         msg.add_action(
0244             QIcon::fromTheme("document-revert"),
0245             i18n("Load Backup"),
0246             parent,
0247             [this, file=stale[0]]{ load_backup(file, true); }
0248         );
0249 
0250         for ( int i = 1; i < stale.size(); i++ )
0251             delete stale[i];
0252 
0253         ui.message_widget->queue_message(std::move(msg));
0254     }
0255 
0256     export_options = options;
0257     export_options.filename = "";
0258 
0259     ui.timeline_widget->reset_view();
0260 
0261     load_pending();
0262 
0263     return ok;
0264 }
0265 
0266 bool GlaxnimateWindow::Private::setup_document_open(const io::Options& options)
0267 {
0268     QFile file(options.filename);
0269     return setup_document_open(&file, options, true);
0270 }
0271 
0272 
0273 void GlaxnimateWindow::Private::refresh_title()
0274 {
0275     QString title = current_document->filename();
0276     if ( !current_document->undo_stack().isClean() )
0277         title += " *";
0278     parent->setWindowTitle(title);
0279 }
0280 
0281 bool GlaxnimateWindow::Private::close_document()
0282 {
0283     if ( current_document )
0284     {
0285         if ( !autosave_load ) {
0286             autosave_file.remove();
0287             autosave_file.releaseLock();
0288         }
0289 
0290         if ( !current_document->undo_stack().isClean() )
0291         {
0292             QMessageBox warning(parent);
0293             warning.setWindowTitle(i18n("Closing Animation"));
0294             warning.setText(i18n("The animation has unsaved changes.\nDo you want to save your changes?"));
0295             warning.setInformativeText(current_document->filename());
0296             warning.setStandardButtons(QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel);
0297             warning.setDefaultButton(QMessageBox::Save);
0298             warning.setIcon(QMessageBox::Warning);
0299             int result = warning.exec();
0300 
0301             if ( result == QMessageBox::Save )
0302             {
0303                 if ( !save_document(false, false) )
0304                     return false;
0305             }
0306             else if ( result == QMessageBox::Cancel )
0307             {
0308                 return false;
0309             }
0310 
0311             // Prevent signals on the destructor
0312             current_document->undo_stack().clear();
0313         }
0314     }
0315 
0316     if ( active_tool )
0317         active_tool->close_document_event({ui.canvas, &scene, parent});
0318 
0319     comp = nullptr;
0320     ui.stroke_style_widget->set_targets({});
0321     ui.stroke_style_widget->set_current(nullptr);
0322     ui.fill_style_widget->set_targets({});
0323     ui.fill_style_widget->set_current(nullptr);
0324     document_node_model.clear_document();
0325     property_model.clear_document();
0326     scene.clear_document();
0327     ui.timeline_widget->clear_document();
0328     ui.document_swatch_widget->set_document(nullptr);
0329     ui.widget_gradients->set_document(nullptr);
0330     ui.view_document_node->set_composition(nullptr);
0331     ui.tab_bar->set_document(nullptr);
0332 
0333     for ( const auto& stack : parent->undo_group().stacks() )
0334         parent->undo_group().removeStack(stack);
0335 
0336     ui.console->clear_contexts();
0337     ui.console->clear_output();
0338     ui.console->set_global("document", QVariant{});
0339 
0340     comp_selections.clear();
0341     ui.menu_new_comp_layer->clear();
0342 
0343     return true;
0344 }
0345 
0346 bool GlaxnimateWindow::Private::save_document(bool force_dialog, bool export_opts)
0347 {
0348     io::Options opts = export_opts ? export_options : current_document->io_options();
0349 
0350     if ( !opts.format || !opts.format->can_save() || !current_document_has_file || opts.filename.isEmpty() )
0351         force_dialog = true;
0352 
0353     if ( force_dialog )
0354     {
0355         ImportExportDialog dialog(opts, parent);
0356 
0357         if ( !dialog.export_dialog(comp) )
0358             return false;
0359 
0360         opts = dialog.io_options();
0361     }
0362 
0363     dialog_export_status->reset(opts.format, opts.filename);
0364 
0365     if ( !qobject_cast<plugin::IoFormat*>(opts.format) )
0366     {
0367         auto promise = QtConcurrent::run(
0368             [opts, comp=comp]{
0369                 QSaveFile file(opts.filename);
0370                 bool result = opts.format->save(file, opts.filename, comp, opts.settings);
0371 
0372                 if ( result )
0373                     file.commit();
0374 
0375                 return result;
0376             });
0377 
0378         process_events(promise);
0379 
0380         if ( !promise.result() )
0381             return false;
0382     }
0383     else
0384     {
0385         QSaveFile file(opts.filename);
0386         bool result = opts.format->save(file, opts.filename, comp, opts.settings);
0387 
0388         if ( !result )
0389             return false;
0390 
0391         file.commit();
0392     }
0393 
0394     if ( export_opts )
0395     {
0396         export_options = opts;
0397         ui.action_export->setText(i18n("Export to %1", QFileInfo(opts.filename).fileName()));
0398     }
0399     else
0400     {
0401         if ( opts.format->can_open() )
0402             most_recent_file(opts.filename);
0403         current_document->undo_stack().setClean();
0404         current_document_has_file = true;
0405         app::settings::set<QString>("open_save", "path", opts.path.absolutePath());
0406         current_document->set_io_options(opts);
0407 
0408         if ( !export_opts && !export_options.format )
0409             export_options.path = opts.path;
0410     }
0411 
0412     autosave_file.remove();
0413     autosave_file.setManagedFile(QUrl::fromLocalFile(opts.filename));
0414     return true;
0415 }
0416 
0417 void GlaxnimateWindow::Private::document_open()
0418 {
0419     io::Options options = current_document->io_options();
0420 
0421     ImportExportDialog dialog(options, ui.centralwidget->parentWidget());
0422     if ( dialog.import_dialog() )
0423         setup_document_open(dialog.io_options());
0424 }
0425 
0426 
0427 io::Options GlaxnimateWindow::Private::options_from_filename(const QString& filename, const QVariantMap& settings)
0428 {
0429     QFileInfo finfo(filename);
0430     if ( finfo.isFile() )
0431     {
0432         io::Options opts;
0433         opts.format = io::IoRegistry::instance().from_filename(filename, io::ImportExport::Import);
0434         opts.path = finfo.dir();
0435         opts.filename = filename;
0436         opts.settings = settings;
0437 
0438         if ( opts.format )
0439         {
0440             ImportExportDialog dialog(opts, ui.centralwidget->parentWidget());
0441             if ( dialog.options_dialog(opts.format->open_settings()) )
0442                 return dialog.io_options();
0443 
0444             return {};
0445         }
0446         else
0447         {
0448             show_warning(i18n("Open File"), i18n("No importer found for %1", filename));
0449         }
0450     }
0451     else
0452     {
0453         show_warning(i18n("Open File"), i18n("The file might have been moved or deleted\n%1", filename));
0454     }
0455 
0456     return {};
0457 }
0458 
0459 
0460 void GlaxnimateWindow::Private::document_open_from_filename(const QString& filename, const QVariantMap& settings)
0461 {
0462     io::Options opts = options_from_filename(filename, settings);
0463     if ( opts.format )
0464     {
0465         setup_document_open(opts);
0466         most_recent_file(filename);
0467     }
0468 }
0469 
0470 void GlaxnimateWindow::Private::drop_document(const QString& filename, bool as_comp)
0471 {
0472     auto options = options_from_filename(filename);
0473     if ( !options.format )
0474         return;
0475 
0476     model::Document imported(options.filename);
0477     QFile file(options.filename);
0478     bool ok = options.format->open(file, options.filename, &imported, options.settings);
0479     if ( !ok )
0480     {
0481         show_warning(i18n("Import File"), i18n("Could not import %1", options.filename));
0482         return;
0483     }
0484 
0485     parent->paste_document(&imported, i18n("Import File"), as_comp);
0486 }
0487 
0488 void GlaxnimateWindow::Private::document_reload()
0489 {
0490     if ( !current_document_has_file )
0491     {
0492         status_message(i18n("No file to reload from"));
0493         return;
0494     }
0495 
0496     auto options = current_document->io_options();
0497     setup_document_open(options);
0498 }
0499 
0500 
0501 void GlaxnimateWindow::Private::preview(io::ImportExport& exporter, const QVariantMap& options)
0502 {
0503     dialog_export_status->reset(&exporter, i18n("Web Preview"));
0504 
0505     auto promise = QtConcurrent::run(
0506         [&exporter, comp=comp, options]() -> QString {
0507             QTemporaryFile tempf(GlaxnimateApp::temp_path() + "/XXXXXX." + exporter.extensions()[0]);
0508             tempf.setAutoRemove(false);
0509             bool ok = tempf.open() && exporter.save(
0510                 tempf, tempf.fileName(), comp, options
0511             );
0512             if ( !ok )
0513                 return "";
0514             return tempf.fileName();
0515         });
0516 
0517     process_events(promise);
0518 
0519     QString path = promise.result();
0520 
0521     dialog_export_status->disconnect_import_export();
0522 
0523     if ( path.isEmpty() )
0524     {
0525         show_warning(i18n("Web Preview"), i18n("Could not create file"));
0526         return;
0527     }
0528 
0529     if ( !QDesktopServices::openUrl(QUrl::fromLocalFile(path)) )
0530     {
0531         show_warning(i18n("Web Preview"), i18n("Could not open browser"));
0532     }
0533 }
0534 
0535 void GlaxnimateWindow::Private::preview_lottie(const QString& renderer)
0536 {
0537     io::lottie::LottieHtmlFormat fmt;
0538     preview(fmt, {{"renderer", renderer}});
0539 }
0540 
0541 void GlaxnimateWindow::Private::preview_svg()
0542 {
0543     io::svg::SvgHtmlFormat fmt;
0544     preview(fmt, {});
0545 }
0546 
0547 void GlaxnimateWindow::Private::preview_rive()
0548 {
0549     io::rive::RiveHtmlFormat fmt;
0550     preview(fmt, {});
0551 }
0552 
0553 void GlaxnimateWindow::Private::save_frame_bmp()
0554 {
0555     int frame = current_document->current_time();
0556     QFileDialog fd(parent, i18n("Save Frame Image"));
0557     fd.setDirectory(app::settings::get<QString>("open_save", "render_path"));
0558     fd.setDefaultSuffix("png");
0559     fd.selectFile(i18n("Frame%1.png", frame));
0560     fd.setAcceptMode(QFileDialog::AcceptSave);
0561     fd.setFileMode(QFileDialog::AnyFile);
0562     fd.setOption(QFileDialog::DontUseNativeDialog, !app::settings::get<bool>("open_save", "native_dialog"));
0563 
0564     QString formats;
0565     for ( const auto& fmt : QImageWriter::supportedImageFormats() )
0566         formats += QString("*.%1 ").arg(QString::fromUtf8(fmt));
0567     fd.setNameFilter(i18n("Image files (%1)", formats));
0568 
0569     if ( fd.exec() == QDialog::Rejected )
0570         return;
0571 
0572     app::settings::set("open_save", "render_path", fd.directory().path());
0573 
0574     QImage image = io::raster::RasterMime().to_image({comp});
0575     if ( !image.save(fd.selectedFiles()[0]) )
0576         show_warning(i18n("Render Frame"), i18n("Could not save image"));
0577 }
0578 
0579 
0580 void GlaxnimateWindow::Private::save_frame_svg()
0581 {
0582     int frame = current_document->current_time();
0583     QFileDialog fd(parent, i18n("Save Frame Image"));
0584     fd.setDirectory(app::settings::get<QString>("open_save", "render_path"));
0585     fd.setDefaultSuffix("svg");
0586     fd.selectFile(i18n("Frame%1.svg", frame));
0587     fd.setAcceptMode(QFileDialog::AcceptSave);
0588     fd.setFileMode(QFileDialog::AnyFile);
0589     fd.setNameFilter(i18n("Scalable Vector Graphics (*.svg)"));
0590     fd.setOption(QFileDialog::DontUseNativeDialog, !app::settings::get<bool>("open_save", "native_dialog"));
0591 
0592     if ( fd.exec() == QDialog::Rejected )
0593         return;
0594 
0595     app::settings::set("open_save", "render_path", fd.directory().path());
0596 
0597     QFile file(fd.selectedFiles()[0]);
0598     if ( !file.open(QFile::WriteOnly) )
0599     {
0600         show_warning(i18n("Render Frame"), i18n("Could not save image"));
0601         return;
0602     }
0603 
0604     io::svg::SvgRenderer rend(io::svg::NotAnimated, io::svg::CssFontType::FontFace);
0605     rend.write_main(comp);
0606     rend.write(&file, true);
0607 }
0608 
0609 void GlaxnimateWindow::Private::validate_discord()
0610 {
0611     IoStatusDialog dialog(QIcon::fromTheme("discord"), "", false, parent);
0612     io::lottie::LottieFormat fmt;
0613     dialog.reset(&fmt, i18n("Validate Discord Sticker"));
0614     io::lottie::validate_discord(current_document.get(), comp, &fmt);
0615     dialog.show_errors(i18n("No issues found"), i18n("Some issues detected"));
0616     dialog.exec();
0617 }
0618 
0619 void GlaxnimateWindow::Private::validate_tgs()
0620 {
0621     IoStatusDialog dialog(QIcon::fromTheme("telegram"), "", false, parent);
0622     io::lottie::TgsFormat fmt;
0623     dialog.reset(&fmt, i18n("Validate Telegram Sticker"));
0624     fmt.validate(current_document.get(), comp);
0625     dialog.show_errors(i18n("No issues found"), i18n("Some issues detected"));
0626     dialog.exec();
0627 }
0628 
0629 void GlaxnimateWindow::Private::autosave_timer_start(int mins)
0630 {
0631     if ( mins == -1 )
0632         mins = app::settings::get<int>("open_save", "backup_frequency");
0633     autosave_timer_mins = mins;
0634     if ( autosave_timer_mins )
0635         autosave_timer = parent->startTimer(autosave_timer_mins * 1000 * 60);
0636 }
0637 
0638 void GlaxnimateWindow::Private::autosave(bool force)
0639 {
0640     if ( current_document && (force || (!current_document->undo_stack().isClean() && !autosave_load)) )
0641     {
0642         autosave_file.open(QIODevice::WriteOnly);
0643         io::glaxnimate::GlaxnimateFormat().save(autosave_file, autosave_file.fileName(), comp, {});
0644         autosave_file.close();
0645     }
0646 }
0647 
0648 void GlaxnimateWindow::Private::autosave_timer_load_settings()
0649 {
0650     int mins = app::settings::get<int>("open_save", "backup_frequency");
0651     if ( mins != autosave_timer_mins )
0652     {
0653         if ( autosave_timer )
0654             parent->killTimer(autosave_timer);
0655         autosave_timer = 0;
0656         autosave_timer_start(mins);
0657     }
0658 }
0659 
0660 void GlaxnimateWindow::Private::load_backup(KAutoSaveFile* file, bool io_options_from_current)
0661 {
0662     file->setParent(nullptr);
0663     std::unique_ptr<KAutoSaveFile> fptr(file);
0664 
0665     if ( !file->open(QIODevice::ReadOnly) )
0666     {
0667         show_warning(i18n("Load Backup"), i18n("Could not open the backup file"));
0668         return;
0669     }
0670 
0671     QFileInfo path(file->managedFile().toString());
0672     QDir dir = path.dir();
0673     if ( dir.isRoot() )
0674         dir = QDir();
0675 
0676     io::Options io_options_bak {
0677         io::glaxnimate::GlaxnimateFormat::instance(),
0678         dir,
0679         path.filePath(),
0680         {}
0681     };
0682 
0683     io::Options io_options;
0684     if ( io_options_from_current )
0685     {
0686         io_options = current_document->io_options();
0687     }
0688     else
0689     {
0690         io_options = io_options_bak;
0691         io_options.format = io::IoRegistry::instance().from_filename(path.filePath(), io::ImportExport::Import);
0692     }
0693 
0694     auto lock = autosave_load.get_lock();
0695     setup_document_open(file, io_options_bak, true);
0696     file->close();
0697     autosave_file.setManagedFile(file->managedFile());
0698     current_document->set_io_options(io_options);
0699     current_document->undo_stack().resetClean();
0700     autosave(true);
0701 }
0702 
0703 
0704 QString GlaxnimateWindow::Private::drop_event_data(QDropEvent* event)
0705 {
0706     const QMimeData* data = event->mimeData();
0707 
0708     if ( !data->hasUrls() )
0709        return {};
0710 
0711     for ( const auto& url : data->urls() )
0712     {
0713         if ( url.isLocalFile() )
0714         {
0715             QString filename = url.toLocalFile();
0716             QString extension = QFileInfo(filename).completeSuffix();
0717             if ( io::IoRegistry::instance().from_extension(extension, io::ImportExport::Import) )
0718                 return filename;
0719         }
0720     }
0721 
0722     return {};
0723 }
0724 
0725 void GlaxnimateWindow::Private::set_color_def(model::BrushStyle* def, bool secondary)
0726 {
0727     model::Styler* target;
0728     QString what;
0729     if ( secondary )
0730     {
0731         target = ui.stroke_style_widget->current();
0732         what = i18n("Stroke");
0733     }
0734     else
0735     {
0736         target = ui.fill_style_widget->current();
0737         what = i18n("Fill");
0738     }
0739 
0740     if ( target )
0741     {
0742         auto old = target->use.get();
0743 
0744         if ( !def )
0745         {
0746             command::UndoMacroGuard macro(i18n("Unlink %1 Color", what), current_document.get());
0747             if ( auto col = qobject_cast<model::NamedColor*>(target->use.get()) )
0748                 target->color.set_undoable(col->color.get());
0749             target->use.set_undoable(QVariant::fromValue(def));
0750             target->visible.set_undoable(false);
0751             if ( old )
0752                 old->remove_if_unused(false);
0753         }
0754         else
0755         {
0756             command::UndoMacroGuard macro(i18n("Link %1 Color", what), current_document.get());
0757             target->use.set_undoable(QVariant::fromValue(def));
0758             target->visible.set_undoable(true);
0759             if ( old )
0760                 old->remove_if_unused(false);
0761         }
0762     }
0763 
0764     set_brush_reference(def, secondary);
0765 }
0766 
0767 void GlaxnimateWindow::Private::set_brush_reference ( model::BrushStyle* sty, bool secondary )
0768 {
0769     if ( secondary )
0770         widget_current_style->set_stroke_ref(sty);
0771     else
0772         widget_current_style->set_fill_ref(sty);
0773 
0774     if ( qobject_cast<model::Gradient*>(sty) )
0775         sty = nullptr;
0776 
0777     if ( secondary )
0778         secondary_brush = sty;
0779     else
0780         main_brush = sty;
0781 
0782     style_change_event();
0783 }
0784 
0785 void GlaxnimateWindow::Private::style_change_event()
0786 {
0787     if ( active_tool )
0788         active_tool->shape_style_change_event({ui.canvas, &scene, parent});
0789 }
0790 
0791 
0792 void GlaxnimateWindow::Private::import_file()
0793 {
0794     io::Options options = current_document->io_options();
0795     QString path = app::settings::get<QString>("open_save", "import_path");
0796     if ( !path.isEmpty() )
0797         options.path.setPath(path);
0798 
0799     ImportExportDialog dialog(options, ui.centralwidget->parentWidget());
0800     if ( dialog.import_dialog() )
0801     {
0802         options = dialog.io_options();
0803         app::settings::set("open_save", "import_path", options.path.path());
0804         import_file(options);
0805     }
0806 
0807 }
0808 
0809 void GlaxnimateWindow::Private::import_file(const io::Options& options)
0810 {
0811     QFile file(options.filename);
0812     import_file(&file, options);
0813 }
0814 
0815 void GlaxnimateWindow::Private::import_file(QIODevice* file, const io::Options& options)
0816 {
0817     model::Document imported(options.filename);
0818 
0819     dialog_export_status->reset(options.format, options.filename);
0820     auto settings = options.settings;
0821     settings["default_time"] = comp->animation->last_frame.get();
0822     bool ok = options.format->open(*file, options.filename, &imported, settings);
0823     if ( !ok )
0824     {
0825         show_warning(i18n("Import File"), i18n("Could not import %1", options.filename));
0826         return;
0827     }
0828 
0829     /// \todo ask if comp
0830     parent->paste_document(&imported, i18n("Import File"), true);
0831 
0832     load_pending();
0833 }
0834 
0835 void GlaxnimateWindow::Private::import_file(const QString& filename, const QVariantMap& settings)
0836 {
0837     QFileInfo finfo(filename);
0838     io::Options opts;
0839     opts.settings = settings;
0840     opts.format = io::IoRegistry::instance().from_extension(finfo.suffix(), io::ImportExport::Import);
0841     if ( !opts.format )
0842         show_warning(i18n("Import File"), i18n("Could not import %1", filename));
0843     opts.filename = filename;
0844     opts.path = finfo.dir();
0845     import_file(opts);
0846 }
0847 
0848 void GlaxnimateWindow::Private::ipc_connect(const QString &name)
0849 {
0850     ipc_socket = qobject_make_unique<QLocalSocket>();
0851     ipc_stream = std::make_unique<QDataStream>(ipc_socket.get());
0852 #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
0853     ipc_stream->setVersion(QDataStream::Qt_5_15);
0854     QObject::connect(ipc_socket.get(), &QLocalSocket::errorOccurred, parent, &GlaxnimateWindow::ipc_error);
0855 #endif
0856     QObject::connect(ipc_socket.get(), &QLocalSocket::readyRead, parent, &GlaxnimateWindow::ipc_read);
0857     ipc_socket->connectToServer(name);
0858     ipc_memory = std::make_unique<QSharedMemory>(name);
0859 }
0860 
0861 static void on_font_loader_finished(glaxnimate::gui::font::FontLoader* loader)
0862 {
0863     if ( !loader->fonts().empty() )
0864     {
0865         auto document = static_cast<glaxnimate::model::Document*>(loader->parent());
0866         bool clear = document->undo_stack().count() == 0;
0867 
0868         glaxnimate::command::UndoMacroGuard guard(i18n("Download fonts"), document);
0869         for ( const auto& font : loader->fonts() )
0870             document->assets()->add_font(font);
0871         guard.finish();
0872 
0873         if ( clear )
0874             document->undo_stack().clear();
0875     }
0876     loader->deleteLater();
0877 }
0878 
0879 void glaxnimate::gui::GlaxnimateWindow::Private::load_pending()
0880 {
0881     auto font_loader = new glaxnimate::gui::font::FontLoader();
0882     font_loader->setParent(current_document.get());
0883     connect(
0884         font_loader, &gui::font::FontLoader::error, parent,
0885         [](const QString& msg){ app::log::Log("Font Loader").log(msg); }
0886     );
0887     font_loader->queue_pending(current_document.get());
0888     connect(
0889         font_loader, &gui::font::FontLoader::finished, current_document.get(),
0890         [font_loader]{on_font_loader_finished(font_loader);}
0891     );
0892     font_loader->load_queue();
0893 }
0894 
0895 void glaxnimate::gui::GlaxnimateWindow::Private::load_remote_document(const QUrl& url, io::Options options, bool open)
0896 {
0897     auto reply = http.get(QNetworkRequest(url));
0898     connect(reply, &QNetworkReply::finished, parent, [this, options, open, reply]() {
0899         if ( reply->error() )
0900         {
0901             auto code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
0902             QString message;
0903             if ( code.isValid() )
0904             {
0905                 auto reason = reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString();
0906                 message = i18n("HTTP Error %1 %2", code.toInt(), reason);
0907             }
0908             else
0909             {
0910                 message = i18n("Network Error");
0911             }
0912 
0913             show_warning(i18n("Load URL"), i18n("Could not load %1: %2", reply->url().toString(), message));
0914             return;
0915         }
0916 
0917         if ( open )
0918             setup_document_open(reply, options, false);
0919         else
0920             import_file(reply, options);
0921     });
0922 }
0923 
0924 void glaxnimate::gui::GlaxnimateWindow::Private::check_autosaves()
0925 {
0926     auto stale = KAutoSaveFile::allStaleFiles();
0927     if ( stale.empty() )
0928         return;
0929 
0930     WindowMessageWidget::Message msg{
0931         i18n("There are %1 auto-save files that can be restored.", stale.size()),
0932         KMessageWidget::Information
0933     };
0934 
0935     msg.add_action(QIcon::fromTheme("dialog-cancel"), i18n("Ignore"));
0936 
0937     msg.add_action(
0938         QIcon::fromTheme("document-revert"),
0939         i18n("Load Backup..."),
0940         parent,
0941         [this, stale]{ show_stale_autosave_list(stale); }
0942     );
0943 
0944     ui.message_widget->queue_message(std::move(msg));
0945 }
0946 
0947 void glaxnimate::gui::GlaxnimateWindow::Private::show_stale_autosave_list(const QList<KAutoSaveFile*>& stale)
0948 {
0949     StalefilesDialog dialog(stale, parent);
0950     if ( dialog.exec() == QDialog::Accepted && dialog.selected() )
0951     {
0952         dialog.cleanup(dialog.selected());
0953         load_backup(dialog.selected(), true);
0954     }
0955     else
0956     {
0957         dialog.cleanup(nullptr);
0958     }
0959 }