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 // Export
539 QString sCameraLayerName = dialog->getCameraLayerName();
540 LayerCamera* cameraLayer = static_cast<LayerCamera*>(mEditor->layers()->findLayerByName(sCameraLayerName, Layer::CAMERA));
541
542 QTransform view = cameraLayer->getViewAtFrame(mEditor->currentFrame());
543
544 bool bOK = mEditor->object()->exportIm(mEditor->currentFrame(),
545 view,
546 cameraLayer->getViewSize(),
547 exportSize,
548 filePath,
549 exportFormat,
550 true,
551 useTranparency);
552
553 if (!bOK)
554 {
555 QMessageBox::warning(mParent,
556 tr("Warning"),
557 tr("Unable to export image."),
558 QMessageBox::Ok);
559 return Status::FAIL;
560 }
561 return Status::OK;
562}
563
564void ActionCommands::flipSelectionX()
565{
566 bool flipVertical = false;
567 mEditor->flipSelection(flipVertical);
568}
569
570void ActionCommands::flipSelectionY()
571{
572 bool flipVertical = true;
573 mEditor->flipSelection(flipVertical);
574}
575
576void ActionCommands::selectAll()
577{
578 mEditor->selectAll();
579}
580
581void ActionCommands::deselectAll()
582{
583 mEditor->deselectAll();
584}
585
586void ActionCommands::ZoomIn()
587{
588 mEditor->view()->scaleUp();
589}
590
591void ActionCommands::ZoomOut()
592{
593 mEditor->view()->scaleDown();
594}
595
596void ActionCommands::rotateClockwise()
597{
598 // Rotation direction is inverted if view is flipped either vertically or horizontally
599 const float delta = mEditor->view()->isFlipHorizontal() == !mEditor->view()->isFlipVertical() ? -15.f : 15.f;
600 mEditor->view()->rotateRelative(delta);
601}
602
603void ActionCommands::rotateCounterClockwise()
604{
605 // Rotation direction is inverted if view is flipped either vertically or horizontally
606 const float delta = mEditor->view()->isFlipHorizontal() == !mEditor->view()->isFlipVertical() ? 15.f : -15.f;
607 mEditor->view()->rotateRelative(delta);
608}
609
610void ActionCommands::PlayStop()
611{
612 PlaybackManager* playback = mEditor->playback();
613 if (playback->isPlaying())
614 {
615 playback->stop();
616 }
617 else
618 {
619 playback->play();
620 }
621}
622
623void ActionCommands::GotoNextFrame()
624{
625 mEditor->scrubForward();
626}
627
628void ActionCommands::GotoPrevFrame()
629{
630 mEditor->scrubBackward();
631}
632
633void ActionCommands::GotoNextKeyFrame()
634{
635 mEditor->scrubNextKeyFrame();
636}
637
638void ActionCommands::GotoPrevKeyFrame()
639{
640 mEditor->scrubPreviousKeyFrame();
641}
642
643Status ActionCommands::addNewKey()
644{
645 // Sound keyframes should not be empty, so we try to import a sound instead
646 if (mEditor->layers()->currentLayer()->type() == Layer::SOUND)
647 {
648 return importSound(FileType::SOUND);
649 }
650
651 KeyFrame* key = mEditor->addNewKey();
652 Camera* cam = dynamic_cast<Camera*>(key);
653 if (cam)
654 {
655 mEditor->view()->forceUpdateViewTransform();
656 }
657
658 return Status::OK;
659}
660
661void ActionCommands::exposeSelectedFrames(int offset)
662{
663 Layer* currentLayer = mEditor->layers()->currentLayer();
664
665 bool hasSelectedFrames = currentLayer->hasAnySelectedFrames();
666
667 // Functionality to be able to expose the current frame without selecting
668 // A:
669 KeyFrame* key = currentLayer->getLastKeyFrameAtPosition(mEditor->currentFrame());
670 if (!hasSelectedFrames) {
671
672 if (key == nullptr) { return; }
673 currentLayer->setFrameSelected(key->pos(), true);
674 }
675
676 currentLayer->setExposureForSelectedFrames(offset);
677 emit mEditor->updateTimeLine();
678 emit mEditor->framesModified();
679
680 // Remember to deselect frame again so we don't show it being visually selected.
681 // B:
682 if (!hasSelectedFrames) {
683 currentLayer->setFrameSelected(key->pos(), false);
684 }
685}
686
687void ActionCommands::addExposureToSelectedFrames()
688{
689 exposeSelectedFrames(1);
690}
691
692void ActionCommands::subtractExposureFromSelectedFrames()
693{
694 exposeSelectedFrames(-1);
695}
696
697Status ActionCommands::insertKeyFrameAtCurrentPosition()
698{
699 Layer* currentLayer = mEditor->layers()->currentLayer();
700 int currentPosition = mEditor->currentFrame();
701
702 currentLayer->insertExposureAt(currentPosition);
703 return addNewKey();
704}
705
706void ActionCommands::removeSelectedFrames()
707{
708 Layer* currentLayer = mEditor->layers()->currentLayer();
709
710 if (!currentLayer->hasAnySelectedFrames()) { return; }
711
712 int ret = QMessageBox::warning(mParent,
713 tr("Remove selected frames", "Windows title of remove selected frames pop-up."),
714 tr("Are you sure you want to remove the selected frames? This action is irreversible currently!"),
715 QMessageBox::Ok | QMessageBox::Cancel,
716 QMessageBox::Ok);
717
718 if (ret != QMessageBox::Ok)
719 {
720 return;
721 }
722
723 for (int pos : currentLayer->selectedKeyFramesPositions()) {
724 currentLayer->removeKeyFrame(pos);
725 }
726 mEditor->layers()->notifyLayerChanged(currentLayer);
727}
728
729void ActionCommands::reverseSelectedFrames()
730{
731 Layer* currentLayer = mEditor->layers()->currentLayer();
732
733 if (!currentLayer->reverseOrderOfSelection()) {
734 return;
735 }
736
737 if (currentLayer->type() == Layer::CAMERA) {
738 mEditor->view()->forceUpdateViewTransform();
739 }
740 emit mEditor->framesModified();
741};
742
743void ActionCommands::removeKey()
744{
745 mEditor->removeKey();
746}
747
748void ActionCommands::duplicateLayer()
749{
750 LayerManager* layerMgr = mEditor->layers();
751 Layer* fromLayer = layerMgr->currentLayer();
752 int currFrame = mEditor->currentFrame();
753
754 Layer* toLayer = layerMgr->createLayer(fromLayer->type(), tr("%1 (copy)", "Default duplicate layer name").arg(fromLayer->name()));
755 fromLayer->foreachKeyFrame([&] (KeyFrame* key) {
756 key = key->clone();
757 toLayer->addOrReplaceKeyFrame(key->pos(), key);
758 if (toLayer->type() == Layer::SOUND)
759 {
760 mEditor->sound()->processSound(static_cast<SoundClip*>(key));
761 }
762 });
763 if (!fromLayer->keyExists(1)) {
764 toLayer->removeKeyFrame(1);
765 }
766 mEditor->scrubTo(currFrame);
767}
768
769void ActionCommands::duplicateKey()
770{
771 Layer* layer = mEditor->layers()->currentLayer();
772 if (layer == nullptr) return;
773 if (!layer->visible())
774 {
775 mEditor->getScribbleArea()->showLayerNotVisibleWarning();
776 return;
777 }
778
779 KeyFrame* key = layer->getKeyFrameAt(mEditor->currentFrame());
780 if (key == nullptr) return;
781
782 // Duplicating a selected keyframe is not handled properly.
783 // The desired behavior is to clear selection anyway so we just do that.
784 deselectAll();
785
786 KeyFrame* dupKey = key->clone();
787
788 int nextEmptyFrame = mEditor->currentFrame() + 1;
789 while (layer->keyExistsWhichCovers(nextEmptyFrame))
790 {
791 nextEmptyFrame += 1;
792 }
793
794 layer->addKeyFrame(nextEmptyFrame, dupKey);
795 mEditor->scrubTo(nextEmptyFrame);
796 emit mEditor->frameModified(nextEmptyFrame);
797
798 if (layer->type() == Layer::SOUND)
799 {
800 mEditor->sound()->processSound(dynamic_cast<SoundClip*>(dupKey));
801 showSoundClipWarningIfNeeded();
802 }
803
804 mEditor->layers()->notifyAnimationLengthChanged();
805 emit mEditor->layers()->currentLayerChanged(mEditor->layers()->currentLayerIndex()); // trigger timeline repaint.
806}
807
808void ActionCommands::moveFrameForward()
809{
810 Layer* layer = mEditor->layers()->currentLayer();
811 if (layer)
812 {
813 if (layer->moveKeyFrame(mEditor->currentFrame(), 1))
814 {
815 mEditor->scrubForward();
816 }
817 }
818 mEditor->layers()->notifyAnimationLengthChanged();
819 emit mEditor->framesModified();
820}
821
822void ActionCommands::moveFrameBackward()
823{
824 Layer* layer = mEditor->layers()->currentLayer();
825 if (layer)
826 {
827 if (layer->moveKeyFrame(mEditor->currentFrame(), -1))
828 {
829 mEditor->scrubBackward();
830 }
831 }
832 emit mEditor->framesModified();
833}
834
835Status ActionCommands::addNewBitmapLayer()
836{
837 bool ok;
838 QString text = QInputDialog::getText(nullptr, tr("Layer Properties"),
839 tr("Layer name:"), QLineEdit::Normal,
840 mEditor->layers()->nameSuggestLayer(tr("Bitmap Layer")), &ok);
841 if (ok && !text.isEmpty())
842 {
843 mEditor->layers()->createBitmapLayer(text);
844 }
845 return Status::OK;
846}
847
848Status ActionCommands::addNewVectorLayer()
849{
850 bool ok;
851 QString text = QInputDialog::getText(nullptr, tr("Layer Properties"),
852 tr("Layer name:"), QLineEdit::Normal,
853 mEditor->layers()->nameSuggestLayer(tr("Vector Layer")), &ok);
854 if (ok && !text.isEmpty())
855 {
856 mEditor->layers()->createVectorLayer(text);
857 }
858 return Status::OK;
859}
860
861Status ActionCommands::addNewCameraLayer()
862{
863 bool ok;
864 QString text = QInputDialog::getText(nullptr, tr("Layer Properties", "A popup when creating a new layer"),
865 tr("Layer name:"), QLineEdit::Normal,
866 mEditor->layers()->nameSuggestLayer(tr("Camera Layer")), &ok);
867 if (ok && !text.isEmpty())
868 {
869 mEditor->layers()->createCameraLayer(text);
870 }
871 return Status::OK;
872}
873
874Status ActionCommands::addNewSoundLayer()
875{
876 bool ok = false;
877 QString strLayerName = QInputDialog::getText(nullptr, tr("Layer Properties"),
878 tr("Layer name:"), QLineEdit::Normal,
879 mEditor->layers()->nameSuggestLayer(tr("Sound Layer")), &ok);
880 if (ok && !strLayerName.isEmpty())
881 {
882 Layer* layer = mEditor->layers()->createSoundLayer(strLayerName);
883 mEditor->layers()->setCurrentLayer(layer);
884 }
885 return Status::OK;
886}
887
888Status ActionCommands::deleteCurrentLayer()
889{
890 LayerManager* layerMgr = mEditor->layers();
891 QString strLayerName = layerMgr->currentLayer()->name();
892
893 if (!layerMgr->canDeleteLayer(mEditor->currentLayerIndex())) {
894 return Status::CANCELED;
895 }
896
897 int ret = QMessageBox::warning(mParent,
898 tr("Delete Layer", "Windows title of Delete current layer pop-up."),
899 tr("Are you sure you want to delete layer: %1? This cannot be undone.").arg(strLayerName),
900 QMessageBox::Ok | QMessageBox::Cancel,
901 QMessageBox::Ok);
902 if (ret == QMessageBox::Ok)
903 {
904 Status st = layerMgr->deleteLayer(mEditor->currentLayerIndex());
905 if (st == Status::ERROR_NEED_AT_LEAST_ONE_CAMERA_LAYER)
906 {
907 QMessageBox::information(mParent, "",
908 tr("Please keep at least one camera layer in project", "text when failed to delete camera layer"));
909 }
910 }
911 return Status::OK;
912}
913
914void ActionCommands::setLayerVisibilityIndex(int index)
915{
916 mEditor->setLayerVisibility(static_cast<LayerVisibility>(index));
917}
918
919void ActionCommands::changeKeyframeLineColor()
920{
921 if (mEditor->layers()->currentLayer()->type() == Layer::BITMAP &&
922 mEditor->layers()->currentLayer()->keyExists(mEditor->currentFrame()))
923 {
924 QRgb color = mEditor->color()->frontColor().rgb();
925 LayerBitmap* layer = static_cast<LayerBitmap*>(mEditor->layers()->currentLayer());
926 layer->getBitmapImageAtFrame(mEditor->currentFrame())->fillNonAlphaPixels(color);
927 mEditor->updateFrame();
928 }
929}
930
931void ActionCommands::changeallKeyframeLineColor()
932{
933 if (mEditor->layers()->currentLayer()->type() == Layer::BITMAP)
934 {
935 QRgb color = mEditor->color()->frontColor().rgb();
936 LayerBitmap* layer = static_cast<LayerBitmap*>(mEditor->layers()->currentLayer());
937 for (int i = layer->firstKeyFramePosition(); i <= layer->getMaxKeyFramePosition(); i++)
938 {
939 if (layer->keyExists(i))
940 layer->getBitmapImageAtFrame(i)->fillNonAlphaPixels(color);
941 }
942 mEditor->updateFrame();
943 }
944}
945
946
947void ActionCommands::resetAllTools()
948{
949 mEditor->tools()->resetAllTools();
950}
951
952void ActionCommands::help()
953{
954 QString url = "http://www.pencil2d.org/doc/";
955 QDesktopServices::openUrl(QUrl(url));
956}
957
958void ActionCommands::quickGuide()
959{
960 QString sDocPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
961 QString sCopyDest = QDir(sDocPath).filePath("pencil2d_quick_guide.pdf");
962
963 QFile quickGuideFile(":/app/pencil2d_quick_guide.pdf");
964 quickGuideFile.copy(sCopyDest);
965
966 QDesktopServices::openUrl(QUrl::fromLocalFile(sCopyDest));
967}
968
969void ActionCommands::website()
970{
971 QString url = "https://www.pencil2d.org/";
972 QDesktopServices::openUrl(QUrl(url));
973}
974
975void ActionCommands::forum()
976{
977 QString url = "https://discuss.pencil2d.org/";
978 QDesktopServices::openUrl(QUrl(url));
979}
980
981void ActionCommands::discord()
982{
983 QString url = "https://discord.gg/8FxdV2g";
984 QDesktopServices::openUrl(QUrl(url));
985}
986
987void ActionCommands::reportbug()
988{
989 QString url = "https://github.com/pencil2d/pencil/issues";
990 QDesktopServices::openUrl(QUrl(url));
991}
992
993void ActionCommands::checkForUpdates()
994{
995 CheckUpdatesDialog dialog;
996 dialog.startChecking();
997 dialog.exec();
998}
999
1000// This action is a temporary measure until we have an automated recover mechanism in place
1001void ActionCommands::openTemporaryDirectory()
1002{
1003 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);
1004 if (ret == QMessageBox::Ok)
1005 {
1006 QDesktopServices::openUrl(QUrl::fromLocalFile(QDir::temp().filePath("Pencil2D")));
1007 }
1008}
1009
1010void ActionCommands::about()
1011{
1012 AboutDialog* aboutBox = new AboutDialog(mParent);
1013 aboutBox->setAttribute(Qt::WA_DeleteOnClose);
1014 aboutBox->init();
1015 aboutBox->exec();
1016}
1017
1018void ActionCommands::showSoundClipWarningIfNeeded()
1019{
1020 int clipCount = mEditor->sound()->soundClipCount();
1021 if (clipCount >= MovieExporter::MAX_SOUND_FRAMES && !mSuppressSoundWarning) {
1022 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));
1023 mSuppressSoundWarning = true;
1024 } else {
1025 mSuppressSoundWarning = false;
1026 }
1027}
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:697
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::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 Jun 5 2025 14:06:43 for Pencil2D by doxygen 1.9.6 based on revision 4c63407997b2c03e5048716586dec6fbbb755173