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