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