Pencil2D Animation
Download Community News Docs Contribute
  • Overview
  • Articles
  • Code
  •  
  • Class List
  • Class Index
  • Class Hierarchy
  • Class Members
  • File List
Loading...
Searching...
No Matches
  • app
  • src
actioncommands.cpp
1/*
2
3Pencil2D - Traditional Animation Software
4Copyright (C) 2012-2020 Matthew Chiawen Chang
5
6This program is free software; you can redistribute it and/or
7modify it under the terms of the GNU General Public License
8as published by the Free Software Foundation; version 2 of the License.
9
10This program is distributed in the hope that it will be useful,
11but WITHOUT ANY WARRANTY; without even the implied warranty of
12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13GNU General Public License for more details.
14
15*/
16
17#include "actioncommands.h"
18
19#include <QInputDialog>
20#include <QMessageBox>
21#include <QProgressDialog>
22#include <QApplication>
23#include <QDesktopServices>
24#include <QStandardPaths>
25#include <QFileDialog>
26
27#include "pencildef.h"
28#include "editor.h"
29#include "object.h"
30#include "viewmanager.h"
31#include "layermanager.h"
32#include "scribblearea.h"
33#include "toolmanager.h"
34#include "soundmanager.h"
35#include "playbackmanager.h"
36#include "colormanager.h"
37#include "preferencemanager.h"
38#include "selectionmanager.h"
39#include "util.h"
40#include "app_util.h"
41
42#include "layercamera.h"
43#include "layersound.h"
44#include "layerbitmap.h"
45#include "layervector.h"
46#include "bitmapimage.h"
47#include "vectorimage.h"
48#include "soundclip.h"
49#include "camera.h"
50
51#include "importimageseqdialog.h"
52#include "importpositiondialog.h"
53#include "movieimporter.h"
54#include "movieexporter.h"
55#include "filedialog.h"
56#include "exportmoviedialog.h"
57#include "exportimagedialog.h"
58#include "aboutdialog.h"
59#include "doubleprogressdialog.h"
60#include "checkupdatesdialog.h"
61#include "errordialog.h"
62
63
64ActionCommands::ActionCommands(QWidget* parent) : QObject(parent)
65{
66 mParent = parent;
67}
68
69ActionCommands::~ActionCommands() {}
70
71Status ActionCommands::importAnimatedImage()
72{
73 ImportImageSeqDialog fileDialog(mParent, ImportExportDialog::Import, FileType::ANIMATED_IMAGE);
74 fileDialog.exec();
75 if (fileDialog.result() != QDialog::Accepted)
76 {
77 return Status::CANCELED;
78 }
79 int frameSpacing = fileDialog.getSpace();
80 QString strImgFileLower = fileDialog.getFilePath();
81
82 ImportPositionDialog positionDialog(mEditor, mParent);
83 positionDialog.exec();
84 if (positionDialog.result() != QDialog::Accepted)
85 {
86 return Status::CANCELED;
87 }
88
89 // Show a progress dialog, as this could take a while if the gif is huge
90 QProgressDialog progressDialog(tr("Importing Animated Image..."), tr("Abort"), 0, 100, mParent);
91 hideQuestionMark(progressDialog);
92 progressDialog.setWindowModality(Qt::WindowModal);
93 progressDialog.show();
94
95 Status st = mEditor->importAnimatedImage(strImgFileLower, frameSpacing, [&progressDialog](int prog) {
96 progressDialog.setValue(prog);
97 QApplication::processEvents();
98 }, [&progressDialog]() {
99 return progressDialog.wasCanceled();
100 });
101
102 progressDialog.setValue(100);
103 progressDialog.close();
104
105 if (!st.ok())
106 {
107 ErrorDialog errorDialog(st.title(), st.description(), st.details().html());
108 errorDialog.exec();
109 return Status::SAFE;
110 }
111
112 return Status::OK;
113}
114
115Status ActionCommands::importMovieVideo()
116{
117 QString filePath = FileDialog::getOpenFileName(mParent, FileType::MOVIE);
118 if (filePath.isEmpty())
119 {
120 return Status::FAIL;
121 }
122
123 // Show a progress dialog, as this can take a while if you have lots of images.
124 QProgressDialog progressDialog(tr("Importing movie..."), tr("Abort"), 0, 100, mParent);
125 hideQuestionMark(progressDialog);
126 progressDialog.setWindowModality(Qt::WindowModal);
127 progressDialog.setMinimumWidth(250);
128 progressDialog.show();
129
130 QMessageBox information(mParent);
131 information.setIcon(QMessageBox::Warning);
132 information.setText(tr("You are importing a lot of frames, beware this could take some time. Are you sure you want to proceed?"));
133 information.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
134 information.setDefaultButton(QMessageBox::Yes);
135
136 MovieImporter importer(this);
137 importer.setCore(mEditor);
138
139 connect(&progressDialog, &QProgressDialog::canceled, &importer, &MovieImporter::cancel);
140
141 Status st = importer.run(filePath, mEditor->playback()->fps(), FileType::MOVIE, [&progressDialog](int prog) {
142 progressDialog.setValue(prog);
143 QApplication::processEvents();
144 }, [&progressDialog](QString progMessage) {
145 progressDialog.setLabelText(progMessage);
146 }, [&information]() {
147
148 int ret = information.exec();
149 return ret == QMessageBox::Yes;
150 });
151
152 if (!st.ok() && st != Status::CANCELED)
153 {
154 ErrorDialog errorDialog(st.title(), st.description(), st.details().html(), mParent);
155 errorDialog.exec();
156 return Status::SAFE;
157 }
158
159 mEditor->layers()->notifyAnimationLengthChanged();
160 emit mEditor->framesModified();
161
162 progressDialog.setValue(100);
163 progressDialog.close();
164
165 return Status::OK;
166}
167
168Status ActionCommands::importSound(FileType type)
169{
170 Layer* layer = mEditor->layers()->currentLayer();
171 if (layer == nullptr)
172 {
173 Q_ASSERT(layer);
174 return Status::FAIL;
175 }
176
177 if (layer->type() != Layer::SOUND)
178 {
179 QMessageBox msg;
180 msg.setText(tr("No sound layer exists as a destination for your import. Create a new sound layer?"));
181 msg.addButton(tr("Create sound layer"), QMessageBox::AcceptRole);
182 msg.addButton(tr("Don't create layer"), QMessageBox::RejectRole);
183
184 int buttonClicked = msg.exec();
185 if (buttonClicked != QMessageBox::AcceptRole)
186 {
187 return Status::SAFE;
188 }
189
190 // Create new sound layer.
191 bool ok = false;
192 QString strLayerName = QInputDialog::getText(mParent, tr("Layer Properties", "Dialog title on creating a sound layer"),
193 tr("Layer name:"), QLineEdit::Normal,
194 mEditor->layers()->nameSuggestLayer(tr("Sound Layer", "Default name on creating a sound layer")), &ok);
195 if (ok && !strLayerName.isEmpty())
196 {
197 Layer* newLayer = mEditor->layers()->createSoundLayer(strLayerName);
198 mEditor->layers()->setCurrentLayer(newLayer);
199 }
200 else
201 {
202 return Status::SAFE;
203 }
204 }
205
206 layer = mEditor->layers()->currentLayer();
207 Q_ASSERT(layer->type() == Layer::SOUND);
208
209 // Adding key before getting file name just to make sure the keyframe can be insterted
210 SoundClip* key = static_cast<SoundClip*>(mEditor->addNewKey());
211
212 if (key == nullptr)
213 {
214 // Probably tried to modify a hidden layer or something like that
215 // Let Editor handle the warnings
216 return Status::SAFE;
217 }
218
219 QString strSoundFile = FileDialog::getOpenFileName(mParent, type);
220
221 Status st = Status::FAIL;
222
223 if (strSoundFile.isEmpty())
224 {
225 st = Status::CANCELED;
226 }
227 else
228 {
229 // Convert even if it already is a WAV file to strip metadata that the
230 // DirectShow media player backend on Windows can't handle
231 st = convertSoundToWav(strSoundFile);
232 }
233
234 if (!st.ok())
235 {
236 mEditor->removeKey();
237 emit mEditor->layers()->currentLayerChanged(mEditor->layers()->currentLayerIndex()); // trigger timeline repaint.
238 } else {
239 showSoundClipWarningIfNeeded();
240 }
241
242 return st;
243}
244
245Status ActionCommands::convertSoundToWav(const QString& filePath)
246{
247 QProgressDialog progressDialog(tr("Importing sound..."), tr("Abort"), 0, 100, mParent);
248 hideQuestionMark(progressDialog);
249 progressDialog.setWindowModality(Qt::WindowModal);
250 progressDialog.show();
251
252 MovieImporter importer(this);
253 importer.setCore(mEditor);
254
255 Status st = importer.run(filePath, mEditor->playback()->fps(), FileType::SOUND, [&progressDialog](int prog) {
256 progressDialog.setValue(prog);
257 QApplication::processEvents();
258 }, [](QString progressMessage) {
259 Q_UNUSED(progressMessage)
260 // Not needed
261 }, []() {
262 return true;
263 });
264
265 connect(&progressDialog, &QProgressDialog::canceled, &importer, &MovieImporter::cancel);
266
267 if (!st.ok() && st != Status::CANCELED)
268 {
269 ErrorDialog errorDialog(st.title(), st.description(), st.details().html(), mParent);
270 errorDialog.exec();
271 }
272 return st;
273}
274
275Status ActionCommands::exportGif()
276{
277 // exporting gif
278 return exportMovie(true);
279}
280
281Status ActionCommands::exportMovie(bool isGif)
282{
283 FileType fileType = (isGif) ? FileType::GIF : FileType::MOVIE;
284
285 int clipCount = mEditor->sound()->soundClipCount();
286 if (fileType == FileType::MOVIE && clipCount >= MovieExporter::MAX_SOUND_FRAMES)
287 {
288 ErrorDialog errorDialog(tr("Something went wrong"), tr("You currently have a total of %1 sound clips. Due to current limitations, you will be unable to export any animation exceeding %2 sound clips. We recommend splitting up larger projects into multiple smaller project to stay within this limit.").arg(clipCount).arg(MovieExporter::MAX_SOUND_FRAMES), QString(), mParent);
289 errorDialog.exec();
290 return Status::FAIL;
291 }
292
293 ExportMovieDialog* dialog = new ExportMovieDialog(mParent, ImportExportDialog::Export, fileType);
294 OnScopeExit(dialog->deleteLater());
295
296 dialog->init();
297
298 std::vector< std::pair<QString, QSize> > camerasInfo;
299 auto cameraLayers = mEditor->object()->getLayersByType< LayerCamera >();
300 for (LayerCamera* i : cameraLayers)
301 {
302 camerasInfo.push_back(std::make_pair(i->name(), i->getViewSize()));
303 }
304
305 auto currLayer = mEditor->layers()->currentLayer();
306 if (currLayer->type() == Layer::CAMERA)
307 {
308 QString strName = currLayer->name();
309 auto it = std::find_if(camerasInfo.begin(), camerasInfo.end(),
310 [strName](std::pair<QString, QSize> p)
311 {
312 return p.first == strName;
313 });
314
315 Q_ASSERT(it != camerasInfo.end());
316
317 std::swap(camerasInfo[0], *it);
318 }
319
320 dialog->setCamerasInfo(camerasInfo);
321
322 int lengthWithSounds = mEditor->layers()->animationLength(true);
323 int length = mEditor->layers()->animationLength(false);
324
325 dialog->setDefaultRange(1, length, lengthWithSounds);
326 dialog->exec();
327
328 if (dialog->result() == QDialog::Rejected)
329 {
330 return Status::SAFE;
331 }
332 QString strMoviePath = dialog->getFilePath();
333
334 ExportMovieDesc desc;
335 desc.strFileName = strMoviePath;
336 desc.startFrame = dialog->getStartFrame();
337 desc.endFrame = dialog->getEndFrame();
338 desc.fps = mEditor->playback()->fps();
339 desc.exportSize = dialog->getExportSize();
340 desc.strCameraName = dialog->getSelectedCameraName();
341 desc.loop = dialog->getLoop();
342 desc.alpha = dialog->getTransparency();
343
344 DoubleProgressDialog progressDlg(mParent);
345 progressDlg.setWindowModality(Qt::WindowModal);
346 progressDlg.setWindowTitle(tr("Exporting movie"));
347 Qt::WindowFlags eFlags = Qt::Dialog | Qt::WindowTitleHint;
348 progressDlg.setWindowFlags(eFlags);
349 progressDlg.show();
350
351 MovieExporter ex;
352
353 connect(&progressDlg, &DoubleProgressDialog::canceled, [&ex]
354 {
355 ex.cancel();
356 });
357
358 // The start points and length for the current minor operation segment on the major progress bar
359 float minorStart, minorLength;
360
361 Status st = ex.run(mEditor->object(), desc,
362 [&progressDlg, &minorStart, &minorLength](float f, float final)
363 {
364 progressDlg.major->setValue(f);
365
366 minorStart = f;
367 minorLength = qMax(0.f, final - minorStart);
368
369 QApplication::processEvents();
370 },
371 [&progressDlg, &minorStart, &minorLength](float f) {
372 progressDlg.minor->setValue(f);
373
374 progressDlg.major->setValue(minorStart + f * minorLength);
375
376 QApplication::processEvents();
377 },
378 [&progressDlg](QString s) {
379 progressDlg.setStatus(s);
380 QApplication::processEvents();
381 }
382 );
383
384 if (st.ok())
385 {
386 if (QFile::exists(strMoviePath))
387 {
388 if (isGif) {
389 auto btn = QMessageBox::question(mParent, "Pencil2D",
390 tr("Finished. Open file location?"));
391
392 if (btn == QMessageBox::Yes)
393 {
394 QString path = dialog->getAbsolutePath();
395 QDesktopServices::openUrl(QUrl::fromLocalFile(path));
396 }
397 return Status::OK;
398 }
399 auto btn = QMessageBox::question(mParent, "Pencil2D",
400 tr("Finished. Open movie now?", "When movie export done."));
401 if (btn == QMessageBox::Yes)
402 {
403 QDesktopServices::openUrl(QUrl::fromLocalFile(strMoviePath));
404 }
405 }
406 else
407 {
408 ErrorDialog errorDialog(tr("Unknown export error"), tr("The export did not produce any errors, however we can't find the output file. Your export may not have completed successfully."), QString(), mParent);
409 errorDialog.exec();
410 }
411 }
412 else if(st != Status::CANCELED)
413 {
414 ErrorDialog errorDialog(st.title(), st.description(), st.details().html(), mParent);
415 errorDialog.exec();
416 }
417
418 return st;
419}
420
421Status ActionCommands::exportImageSequence()
422{
423 auto dialog = new ExportImageDialog(mParent, FileType::IMAGE_SEQUENCE);
424 OnScopeExit(dialog->deleteLater());
425
426 dialog->init();
427
428 std::vector< std::pair<QString, QSize> > camerasInfo;
429 auto cameraLayers = mEditor->object()->getLayersByType< LayerCamera >();
430 for (LayerCamera* i : cameraLayers)
431 {
432 camerasInfo.push_back(std::make_pair(i->name(), i->getViewSize()));
433 }
434
435 auto currLayer = mEditor->layers()->currentLayer();
436 if (currLayer->type() == Layer::CAMERA)
437 {
438 QString strName = currLayer->name();
439 auto it = std::find_if(camerasInfo.begin(), camerasInfo.end(),
440 [strName](std::pair<QString, QSize> p)
441 {
442 return p.first == strName;
443 });
444
445 Q_ASSERT(it != camerasInfo.end());
446 std::swap(camerasInfo[0], *it);
447 }
448 dialog->setCamerasInfo(camerasInfo);
449
450 int lengthWithSounds = mEditor->layers()->animationLength(true);
451 int length = mEditor->layers()->animationLength(false);
452
453 dialog->setDefaultRange(1, length, lengthWithSounds);
454
455 dialog->exec();
456
457 if (dialog->result() == QDialog::Rejected)
458 {
459 return Status::SAFE;
460 }
461
462 QString strFilePath = dialog->getFilePath();
463 QSize exportSize = dialog->getExportSize();
464 QString exportFormat = dialog->getExportFormat();
465 bool exportKeyframesOnly = dialog->getExportKeyframesOnly();
466 bool useTranparency = dialog->getTransparency();
467 int startFrame = dialog->getStartFrame();
468 int endFrame = dialog->getEndFrame();
469
470 QString sCameraLayerName = dialog->getCameraLayerName();
471 LayerCamera* cameraLayer = static_cast<LayerCamera*>(mEditor->layers()->findLayerByName(sCameraLayerName, Layer::CAMERA));
472
473 // Show a progress dialog, as this can take a while if you have lots of frames.
474 QProgressDialog progress(tr("Exporting image sequence..."), tr("Abort"), 0, 100, mParent);
475 hideQuestionMark(progress);
476 progress.setWindowModality(Qt::WindowModal);
477 progress.show();
478
479 mEditor->object()->exportFrames(startFrame, endFrame,
480 cameraLayer,
481 exportSize,
482 strFilePath,
483 exportFormat,
484 useTranparency,
485 exportKeyframesOnly,
486 mEditor->layers()->currentLayer()->name(),
487 true,
488 &progress,
489 100);
490
491 progress.close();
492
493 return Status::OK;
494}
495
496Status ActionCommands::exportImage()
497{
498 // Options
499 auto dialog = new ExportImageDialog(mParent, FileType::IMAGE);
500 OnScopeExit(dialog->deleteLater())
501
502 dialog->init();
503
504 std::vector< std::pair<QString, QSize> > camerasInfo;
505 auto cameraLayers = mEditor->object()->getLayersByType< LayerCamera >();
506 for (LayerCamera* i : cameraLayers)
507 {
508 camerasInfo.push_back(std::make_pair(i->name(), i->getViewSize()));
509 }
510
511 auto currLayer = mEditor->layers()->currentLayer();
512 if (currLayer->type() == Layer::CAMERA)
513 {
514 QString strName = currLayer->name();
515 auto it = std::find_if(camerasInfo.begin(), camerasInfo.end(),
516 [strName](std::pair<QString, QSize> p)
517 {
518 return p.first == strName;
519 });
520
521 Q_ASSERT(it != camerasInfo.end());
522 std::swap(camerasInfo[0], *it);
523 }
524 dialog->setCamerasInfo(camerasInfo);
525
526 dialog->exec();
527
528 if (dialog->result() == QDialog::Rejected)
529 {
530 return Status::SAFE;
531 }
532
533 QString filePath = dialog->getFilePath();
534 QSize exportSize = dialog->getExportSize();
535 QString exportFormat = dialog->getExportFormat();
536 bool useTranparency = dialog->getTransparency();
537
538 QString extension = "";
539 QString formatStr = exportFormat;
540 if (formatStr == "PNG" || formatStr == "png")
541 {
542 exportFormat = "PNG";
543 extension = ".png";
544 }
545 if (formatStr == "JPG" || formatStr == "jpg" || formatStr == "JPEG" || formatStr == "jpeg")
546 {
547 exportFormat = "JPG";
548 extension = ".jpg";
549 useTranparency = false; // JPG doesn't support transparency, so we have to include the background
550 }
551 if (formatStr == "TIFF" || formatStr == "tiff" || formatStr == "TIF" || formatStr == "tif")
552 {
553 exportFormat = "TIFF";
554 extension = ".tiff";
555 }
556 if (formatStr == "BMP" || formatStr == "bmp")
557 {
558 exportFormat = "BMP";
559 extension = ".bmp";
560 useTranparency = false;
561 }
562 if (formatStr == "WEBP" || formatStr == "webp") {
563 exportFormat = "WEBP";
564 extension = ".webp";
565 }
566 if (!filePath.endsWith(extension, Qt::CaseInsensitive))
567 {
568 filePath += extension;
569 }
570
571 // Export
572 QString sCameraLayerName = dialog->getCameraLayerName();
573 LayerCamera* cameraLayer = static_cast<LayerCamera*>(mEditor->layers()->findLayerByName(sCameraLayerName, Layer::CAMERA));
574
575 QTransform view = cameraLayer->getViewAtFrame(mEditor->currentFrame());
576
577 bool bOK = mEditor->object()->exportIm(mEditor->currentFrame(),
578 view,
579 cameraLayer->getViewSize(),
580 exportSize,
581 filePath,
582 exportFormat,
583 true,
584 useTranparency);
585
586 if (!bOK)
587 {
588 QMessageBox::warning(mParent,
589 tr("Warning"),
590 tr("Unable to export image."),
591 QMessageBox::Ok);
592 return Status::FAIL;
593 }
594 return Status::OK;
595}
596
597void ActionCommands::flipSelectionX()
598{
599 bool flipVertical = false;
600 mEditor->flipSelection(flipVertical);
601}
602
603void ActionCommands::flipSelectionY()
604{
605 bool flipVertical = true;
606 mEditor->flipSelection(flipVertical);
607}
608
609void ActionCommands::selectAll()
610{
611 mEditor->selectAll();
612}
613
614void ActionCommands::deselectAll()
615{
616 mEditor->deselectAll();
617}
618
619void ActionCommands::ZoomIn()
620{
621 mEditor->view()->scaleUp();
622}
623
624void ActionCommands::ZoomOut()
625{
626 mEditor->view()->scaleDown();
627}
628
629void ActionCommands::rotateClockwise()
630{
631 // Rotation direction is inverted if view is flipped either vertically or horizontally
632 const float delta = mEditor->view()->isFlipHorizontal() == !mEditor->view()->isFlipVertical() ? -15.f : 15.f;
633 mEditor->view()->rotateRelative(delta);
634}
635
636void ActionCommands::rotateCounterClockwise()
637{
638 // Rotation direction is inverted if view is flipped either vertically or horizontally
639 const float delta = mEditor->view()->isFlipHorizontal() == !mEditor->view()->isFlipVertical() ? 15.f : -15.f;
640 mEditor->view()->rotateRelative(delta);
641}
642
643void ActionCommands::PlayStop()
644{
645 PlaybackManager* playback = mEditor->playback();
646 if (playback->isPlaying())
647 {
648 playback->stop();
649 }
650 else
651 {
652 playback->play();
653 }
654}
655
656void ActionCommands::GotoNextFrame()
657{
658 mEditor->scrubForward();
659}
660
661void ActionCommands::GotoPrevFrame()
662{
663 mEditor->scrubBackward();
664}
665
666void ActionCommands::GotoNextKeyFrame()
667{
668 mEditor->scrubNextKeyFrame();
669}
670
671void ActionCommands::GotoPrevKeyFrame()
672{
673 mEditor->scrubPreviousKeyFrame();
674}
675
676Status ActionCommands::addNewKey()
677{
678 // Sound keyframes should not be empty, so we try to import a sound instead
679 if (mEditor->layers()->currentLayer()->type() == Layer::SOUND)
680 {
681 return importSound(FileType::SOUND);
682 }
683
684 KeyFrame* key = mEditor->addNewKey();
685 Camera* cam = dynamic_cast<Camera*>(key);
686 if (cam)
687 {
688 mEditor->view()->forceUpdateViewTransform();
689 }
690
691 return Status::OK;
692}
693
694void ActionCommands::exposeSelectedFrames(int offset)
695{
696 Layer* currentLayer = mEditor->layers()->currentLayer();
697
698 bool hasSelectedFrames = currentLayer->hasAnySelectedFrames();
699
700 // Functionality to be able to expose the current frame without selecting
701 // A:
702 KeyFrame* key = currentLayer->getLastKeyFrameAtPosition(mEditor->currentFrame());
703 if (!hasSelectedFrames) {
704
705 if (key == nullptr) { return; }
706 currentLayer->setFrameSelected(key->pos(), true);
707 }
708
709 currentLayer->setExposureForSelectedFrames(offset);
710 emit mEditor->updateTimeLine();
711 emit mEditor->framesModified();
712
713 // Remember to deselect frame again so we don't show it being visually selected.
714 // B:
715 if (!hasSelectedFrames) {
716 currentLayer->setFrameSelected(key->pos(), false);
717 }
718}
719
720void ActionCommands::addExposureToSelectedFrames()
721{
722 exposeSelectedFrames(1);
723}
724
725void ActionCommands::subtractExposureFromSelectedFrames()
726{
727 exposeSelectedFrames(-1);
728}
729
730Status ActionCommands::insertKeyFrameAtCurrentPosition()
731{
732 Layer* currentLayer = mEditor->layers()->currentLayer();
733 int currentPosition = mEditor->currentFrame();
734
735 currentLayer->insertExposureAt(currentPosition);
736 return addNewKey();
737}
738
739void ActionCommands::removeSelectedFrames()
740{
741 Layer* currentLayer = mEditor->layers()->currentLayer();
742
743 if (!currentLayer->hasAnySelectedFrames()) { return; }
744
745 int ret = QMessageBox::warning(mParent,
746 tr("Remove selected frames", "Windows title of remove selected frames pop-up."),
747 tr("Are you sure you want to remove the selected frames? This action is irreversible currently!"),
748 QMessageBox::Ok | QMessageBox::Cancel,
749 QMessageBox::Ok);
750
751 if (ret != QMessageBox::Ok)
752 {
753 return;
754 }
755
756 for (int pos : currentLayer->selectedKeyFramesPositions()) {
757 currentLayer->removeKeyFrame(pos);
758 }
759 mEditor->layers()->notifyLayerChanged(currentLayer);
760}
761
762void ActionCommands::reverseSelectedFrames()
763{
764 Layer* currentLayer = mEditor->layers()->currentLayer();
765
766 if (!currentLayer->reverseOrderOfSelection()) {
767 return;
768 }
769
770 if (currentLayer->type() == Layer::CAMERA) {
771 mEditor->view()->forceUpdateViewTransform();
772 }
773 emit mEditor->framesModified();
774};
775
776void ActionCommands::removeKey()
777{
778 mEditor->removeKey();
779}
780
781void ActionCommands::duplicateLayer()
782{
783 LayerManager* layerMgr = mEditor->layers();
784 Layer* fromLayer = layerMgr->currentLayer();
785 int currFrame = mEditor->currentFrame();
786
787 Layer* toLayer = layerMgr->createLayer(fromLayer->type(), tr("%1 (copy)", "Default duplicate layer name").arg(fromLayer->name()));
788 fromLayer->foreachKeyFrame([&] (KeyFrame* key) {
789 key = key->clone();
790 toLayer->addOrReplaceKeyFrame(key->pos(), key);
791 if (toLayer->type() == Layer::SOUND)
792 {
793 mEditor->sound()->processSound(static_cast<SoundClip*>(key));
794 }
795 });
796 if (!fromLayer->keyExists(1)) {
797 toLayer->removeKeyFrame(1);
798 }
799 mEditor->scrubTo(currFrame);
800}
801
802void ActionCommands::duplicateKey()
803{
804 Layer* layer = mEditor->layers()->currentLayer();
805 if (layer == nullptr) return;
806 if (!layer->visible())
807 {
808 mEditor->getScribbleArea()->showLayerNotVisibleWarning();
809 return;
810 }
811
812 KeyFrame* key = layer->getKeyFrameAt(mEditor->currentFrame());
813 if (key == nullptr) return;
814
815 // Duplicating a selected keyframe is not handled properly.
816 // The desired behavior is to clear selection anyway so we just do that.
817 deselectAll();
818
819 KeyFrame* dupKey = key->clone();
820
821 int nextEmptyFrame = mEditor->currentFrame() + 1;
822 while (layer->keyExistsWhichCovers(nextEmptyFrame))
823 {
824 nextEmptyFrame += 1;
825 }
826
827 layer->addKeyFrame(nextEmptyFrame, dupKey);
828 mEditor->scrubTo(nextEmptyFrame);
829 emit mEditor->frameModified(nextEmptyFrame);
830
831 if (layer->type() == Layer::SOUND)
832 {
833 mEditor->sound()->processSound(dynamic_cast<SoundClip*>(dupKey));
834 showSoundClipWarningIfNeeded();
835 }
836
837 mEditor->layers()->notifyAnimationLengthChanged();
838 emit mEditor->layers()->currentLayerChanged(mEditor->layers()->currentLayerIndex()); // trigger timeline repaint.
839}
840
841void ActionCommands::moveFrameForward()
842{
843 Layer* layer = mEditor->layers()->currentLayer();
844 if (layer)
845 {
846 if (layer->moveKeyFrame(mEditor->currentFrame(), 1))
847 {
848 mEditor->scrubForward();
849 }
850 }
851 mEditor->layers()->notifyAnimationLengthChanged();
852 emit mEditor->framesModified();
853}
854
855void ActionCommands::moveFrameBackward()
856{
857 Layer* layer = mEditor->layers()->currentLayer();
858 if (layer)
859 {
860 if (layer->moveKeyFrame(mEditor->currentFrame(), -1))
861 {
862 mEditor->scrubBackward();
863 }
864 }
865 emit mEditor->framesModified();
866}
867
868Status ActionCommands::addNewBitmapLayer()
869{
870 bool ok;
871 QString text = QInputDialog::getText(nullptr, tr("Layer Properties"),
872 tr("Layer name:"), QLineEdit::Normal,
873 mEditor->layers()->nameSuggestLayer(tr("Bitmap Layer")), &ok);
874 if (ok && !text.isEmpty())
875 {
876 mEditor->layers()->createBitmapLayer(text);
877 }
878 return Status::OK;
879}
880
881Status ActionCommands::addNewVectorLayer()
882{
883 bool ok;
884 QString text = QInputDialog::getText(nullptr, tr("Layer Properties"),
885 tr("Layer name:"), QLineEdit::Normal,
886 mEditor->layers()->nameSuggestLayer(tr("Vector Layer")), &ok);
887 if (ok && !text.isEmpty())
888 {
889 mEditor->layers()->createVectorLayer(text);
890 }
891 return Status::OK;
892}
893
894Status ActionCommands::addNewCameraLayer()
895{
896 bool ok;
897 QString text = QInputDialog::getText(nullptr, tr("Layer Properties", "A popup when creating a new layer"),
898 tr("Layer name:"), QLineEdit::Normal,
899 mEditor->layers()->nameSuggestLayer(tr("Camera Layer")), &ok);
900 if (ok && !text.isEmpty())
901 {
902 mEditor->layers()->createCameraLayer(text);
903 }
904 return Status::OK;
905}
906
907Status ActionCommands::addNewSoundLayer()
908{
909 bool ok = false;
910 QString strLayerName = QInputDialog::getText(nullptr, tr("Layer Properties"),
911 tr("Layer name:"), QLineEdit::Normal,
912 mEditor->layers()->nameSuggestLayer(tr("Sound Layer")), &ok);
913 if (ok && !strLayerName.isEmpty())
914 {
915 Layer* layer = mEditor->layers()->createSoundLayer(strLayerName);
916 mEditor->layers()->setCurrentLayer(layer);
917 }
918 return Status::OK;
919}
920
921Status ActionCommands::deleteCurrentLayer()
922{
923 LayerManager* layerMgr = mEditor->layers();
924 QString strLayerName = layerMgr->currentLayer()->name();
925
926 if (!layerMgr->canDeleteLayer(mEditor->currentLayerIndex())) {
927 return Status::CANCELED;
928 }
929
930 int ret = QMessageBox::warning(mParent,
931 tr("Delete Layer", "Windows title of Delete current layer pop-up."),
932 tr("Are you sure you want to delete layer: %1? This cannot be undone.").arg(strLayerName),
933 QMessageBox::Ok | QMessageBox::Cancel,
934 QMessageBox::Ok);
935 if (ret == QMessageBox::Ok)
936 {
937 Status st = layerMgr->deleteLayer(mEditor->currentLayerIndex());
938 if (st == Status::ERROR_NEED_AT_LEAST_ONE_CAMERA_LAYER)
939 {
940 QMessageBox::information(mParent, "",
941 tr("Please keep at least one camera layer in project", "text when failed to delete camera layer"));
942 }
943 }
944 return Status::OK;
945}
946
947void ActionCommands::setLayerVisibilityIndex(int index)
948{
949 mEditor->setLayerVisibility(static_cast<LayerVisibility>(index));
950}
951
952void ActionCommands::changeKeyframeLineColor()
953{
954 if (mEditor->layers()->currentLayer()->type() == Layer::BITMAP &&
955 mEditor->layers()->currentLayer()->keyExists(mEditor->currentFrame()))
956 {
957 QRgb color = mEditor->color()->frontColor().rgb();
958 LayerBitmap* layer = static_cast<LayerBitmap*>(mEditor->layers()->currentLayer());
959 layer->getBitmapImageAtFrame(mEditor->currentFrame())->fillNonAlphaPixels(color);
960 mEditor->updateFrame();
961 }
962}
963
964void ActionCommands::changeallKeyframeLineColor()
965{
966 if (mEditor->layers()->currentLayer()->type() == Layer::BITMAP)
967 {
968 QRgb color = mEditor->color()->frontColor().rgb();
969 LayerBitmap* layer = static_cast<LayerBitmap*>(mEditor->layers()->currentLayer());
970 for (int i = layer->firstKeyFramePosition(); i <= layer->getMaxKeyFramePosition(); i++)
971 {
972 if (layer->keyExists(i))
973 layer->getBitmapImageAtFrame(i)->fillNonAlphaPixels(color);
974 }
975 mEditor->updateFrame();
976 }
977}
978
979
980void ActionCommands::resetAllTools()
981{
982 mEditor->tools()->resetAllTools();
983}
984
985void ActionCommands::help()
986{
987 QString url = "http://www.pencil2d.org/doc/";
988 QDesktopServices::openUrl(QUrl(url));
989}
990
991void ActionCommands::quickGuide()
992{
993 QString sDocPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
994 QString sCopyDest = QDir(sDocPath).filePath("pencil2d_quick_guide.pdf");
995
996 QFile quickGuideFile(":/app/pencil2d_quick_guide.pdf");
997 quickGuideFile.copy(sCopyDest);
998
999 QDesktopServices::openUrl(QUrl::fromLocalFile(sCopyDest));
1000}
1001
1002void ActionCommands::website()
1003{
1004 QString url = "https://www.pencil2d.org/";
1005 QDesktopServices::openUrl(QUrl(url));
1006}
1007
1008void ActionCommands::forum()
1009{
1010 QString url = "https://discuss.pencil2d.org/";
1011 QDesktopServices::openUrl(QUrl(url));
1012}
1013
1014void ActionCommands::discord()
1015{
1016 QString url = "https://discord.gg/8FxdV2g";
1017 QDesktopServices::openUrl(QUrl(url));
1018}
1019
1020void ActionCommands::reportbug()
1021{
1022 QString url = "https://github.com/pencil2d/pencil/issues";
1023 QDesktopServices::openUrl(QUrl(url));
1024}
1025
1026void ActionCommands::checkForUpdates()
1027{
1028 CheckUpdatesDialog dialog;
1029 dialog.startChecking();
1030 dialog.exec();
1031}
1032
1033// This action is a temporary measure until we have an automated recover mechanism in place
1034void ActionCommands::openTemporaryDirectory()
1035{
1036 int ret = QMessageBox::warning(mParent, tr("Warning"), tr("The temporary directory is meant to be used only by Pencil2D. Do not modify it unless you know what you are doing."), QMessageBox::Cancel, QMessageBox::Ok);
1037 if (ret == QMessageBox::Ok)
1038 {
1039 QDesktopServices::openUrl(QUrl::fromLocalFile(QDir::temp().filePath("Pencil2D")));
1040 }
1041}
1042
1043void ActionCommands::about()
1044{
1045 AboutDialog* aboutBox = new AboutDialog(mParent);
1046 aboutBox->setAttribute(Qt::WA_DeleteOnClose);
1047 aboutBox->init();
1048 aboutBox->exec();
1049}
1050
1051void ActionCommands::showSoundClipWarningIfNeeded()
1052{
1053 int clipCount = mEditor->sound()->soundClipCount();
1054 if (clipCount >= MovieExporter::MAX_SOUND_FRAMES && !mSuppressSoundWarning) {
1055 QMessageBox::warning(mParent, tr("Warning"), tr("You currently have a total of %1 sound clips. Due to current limitations, you will be unable to export any animation exceeding %2 sound clips. We recommend splitting up larger projects into multiple smaller project to stay within this limit.").arg(clipCount).arg(MovieExporter::MAX_SOUND_FRAMES));
1056 mSuppressSoundWarning = true;
1057 } else {
1058 mSuppressSoundWarning = false;
1059 }
1060}
AboutDialog
Definition: aboutdialog.h:27
ActionCommands::insertKeyFrameAtCurrentPosition
Status insertKeyFrameAtCurrentPosition()
Will insert a keyframe at the current position and push connected frames to the right.
Definition: actioncommands.cpp:730
Camera
Definition: camera.h:25
CheckUpdatesDialog
Definition: checkupdatesdialog.h:31
ColorManager::frontColor
QColor frontColor(bool useIndexedColor=true)
frontColor
Definition: colormanager.cpp:61
DoubleProgressDialog
Definition: doubleprogressdialog.h:29
Editor::framesModified
void framesModified()
This should be emitted after modifying multiple frames.
Editor::addNewKey
KeyFrame * addNewKey()
Attempts to create a new keyframe at the current frame and layer.
Definition: editor.cpp:903
Editor::frameModified
void frameModified(int frameNumber)
This should be emitted after modifying the frame content.
Editor::updateFrame
void updateFrame()
Will call update() and update the canvas Only call this directly If you need the cache to be intact a...
Definition: editor.cpp:850
Editor::setLayerVisibility
void setLayerVisibility(LayerVisibility visibility)
The visibility value should match any of the VISIBILITY enum values.
Definition: editor.cpp:428
ErrorDialog
Definition: errordialog.h:28
ExportImageDialog
Definition: exportimagedialog.h:28
ExportMovieDialog
Definition: exportmoviedialog.h:30
FileDialog::getOpenFileName
static QString getOpenFileName(QWidget *parent, FileType fileType, const QString &caption=QString())
Shows a file dialog which allows the user to select a file to open.
Definition: filedialog.cpp:28
ImportImageSeqDialog
Definition: importimageseqdialog.h:46
ImportPositionDialog
Definition: importpositiondialog.h:31
KeyFrame
Definition: keyframe.h:30
LayerBitmap
Definition: layerbitmap.h:26
LayerCamera
Definition: layercamera.h:30
Layer
Definition: layer.h:33
Layer::selectedKeyFramesPositions
QList< int > selectedKeyFramesPositions() const
Get selected keyframe positions sorted by position.
Definition: layer.h:62
Layer::reverseOrderOfSelection
bool reverseOrderOfSelection()
Reverse order of selected frames.
Definition: layer.cpp:602
Layer::addKeyFrame
virtual bool addKeyFrame(int position, KeyFrame *pKeyFrame)
Adds a keyframe at the given position, unless one already exists.
Definition: layer.cpp:191
Layer::setExposureForSelectedFrames
void setExposureForSelectedFrames(int offset)
Add or subtract exposure from selected frames.
Definition: layer.cpp:512
Layer::insertExposureAt
bool insertExposureAt(int position)
Will insert an empty frame (exposure) after the given position.
Definition: layer.cpp:204
LayerManager
Definition: layermanager.h:31
LayerManager::animationLength
int animationLength(bool includeSounds=true)
Get the length of current project.
Definition: layermanager.cpp:371
LayerManager::createLayer
Layer * createLayer(Layer::LAYER_TYPE type, const QString &strLayerName)
Returns a new Layer with the given LAYER_TYPE.
Definition: layermanager.cpp:189
LayerManager::notifyAnimationLengthChanged
void notifyAnimationLengthChanged()
This should be emitted whenever the animation length frames, eg.
Definition: layermanager.cpp:405
MovieExporter
Definition: movieexporter.h:44
MovieExporter::run
Status run(const Object *obj, const ExportMovieDesc &desc, std::function< void(float, float)> majorProgress, std::function< void(float)> minorProgress, std::function< void(QString)> progressMessage)
Begin exporting the movie described by exportDesc.
Definition: movieexporter.cpp:78
MovieImporter
Definition: movieimporter.h:30
PlaybackManager
Definition: playbackmanager.h:30
SoundClip
Definition: soundclip.h:27
Status
Definition: pencilerror.h:40
QColor::rgb
QRgb rgb() const const
QCoreApplication::processEvents
void processEvents(QEventLoop::ProcessEventsFlags flags)
QDesktopServices::openUrl
bool openUrl(const QUrl &url)
QDialog::Accepted
Accepted
QDialog::exec
virtual int exec()
QDialog::result
int result() const const
QDir
QDir::filePath
QString filePath(const QString &fileName) const const
QDir::temp
QDir temp()
QFile
QInputDialog::getText
QString getText(QWidget *parent, const QString &title, const QString &label, QLineEdit::EchoMode mode, const QString &text, bool *ok, Qt::WindowFlags flags, Qt::InputMethodHints inputMethodHints)
QLineEdit::Normal
Normal
QMessageBox
QMessageBox::AcceptRole
AcceptRole
QMessageBox::Warning
Warning
QMessageBox::Yes
Yes
QMessageBox::addButton
void addButton(QAbstractButton *button, QMessageBox::ButtonRole role)
QMessageBox::exec
virtual int exec() override
QMessageBox::information
QMessageBox::StandardButton information(QWidget *parent, const QString &title, const QString &text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton)
QMessageBox::question
QMessageBox::StandardButton question(QWidget *parent, const QString &title, const QString &text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton)
QMessageBox::setText
void setText(const QString &text)
QMessageBox::warning
QMessageBox::StandardButton warning(QWidget *parent, const QString &title, const QString &text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton)
QObject
QObject::connect
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
QObject::deleteLater
void deleteLater()
QObject::tr
QString tr(const char *sourceText, const char *disambiguation, int n)
QProgressDialog
QProgressDialog::canceled
void canceled()
QSize
QStandardPaths::DocumentsLocation
DocumentsLocation
QStandardPaths::writableLocation
QString writableLocation(QStandardPaths::StandardLocation type)
QString
QString::endsWith
bool endsWith(const QString &s, Qt::CaseSensitivity cs) const const
QString::isEmpty
bool isEmpty() const const
Qt::CaseInsensitive
CaseInsensitive
Qt::WA_DeleteOnClose
WA_DeleteOnClose
Qt::WindowModal
WindowModal
Qt::WindowFlags
typedef WindowFlags
QTransform
QUrl
QUrl::fromLocalFile
QUrl fromLocalFile(const QString &localFile)
QWidget
QWidget::setAttribute
void setAttribute(Qt::WidgetAttribute attribute, bool on)
ExportMovieDesc
Definition: movieexporter.h:31
Generated on Tue Nov 18 2025 07:45:09 for Pencil2D by doxygen 1.9.6 based on revision d32017e1e1c82fd8b2e48782fe8069d83af428e2