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