All Classes Namespaces Functions Variables Enumerations Properties Pages
importimageseqdialog.cpp
1 /*
2 
3 Pencil2D - Traditional Animation Software
4 Copyright (C) 2005-2007 Patrick Corrieri & Pascal Naidon
5 Copyright (C) 2012-2020 Matthew Chiawen Chang
6 
7 This program is free software; you can redistribute it and/or
8 modify it under the terms of the GNU General Public License
9 as published by the Free Software Foundation; version 2 of the License.
10 
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15 
16 */
17 
18 #include "importimageseqdialog.h"
19 #include "ui_importimageseqoptions.h"
20 #include "ui_importimageseqpreview.h"
21 #include "util.h"
22 #include "app_util.h"
23 
24 #include "editor.h"
25 #include "predefinedsetmodel.h"
26 #include "layermanager.h"
27 #include "viewmanager.h"
28 
29 #include <QProgressDialog>
30 #include <QMessageBox>
31 #include <QDir>
32 #include <QtDebug>
33 #include <QDialogButtonBox>
34 #include <QPushButton>
35 
36 ImportImageSeqDialog::ImportImageSeqDialog(QWidget* parent, Mode mode, FileType fileType, ImportCriteria importCriteria) :
37  ImportExportDialog(parent, mode, fileType), mParent(parent), mImportCriteria(importCriteria), mFileType(fileType)
38 {
39 
40  uiOptionsBox = new Ui::ImportImageSeqOptions;
41  uiOptionsBox->setupUi(getOptionsGroupBox());
42 
43  uiGroupBoxPreview = new Ui::ImportImageSeqPreviewGroupBox;
44  uiGroupBoxPreview->setupUi(getPreviewGroupBox());
45 
46  if (importCriteria == ImportCriteria::PredefinedSet) {
47  setupPredefinedLayout();
48  } else {
49  setupLayout();
50  }
51 
52  getDialogButtonBox()->button(QDialogButtonBox::StandardButton::Ok)->setEnabled(false);
53 }
54 
55 void ImportImageSeqDialog::setupLayout()
56 {
57 
58  hideInstructionsLabel(true);
59 
60  if (mFileType == FileType::GIF) {
61  setWindowTitle(tr("Import Animated GIF"));
62  } else {
63  setWindowTitle(tr("Import image sequence"));
64  }
65 
66  connect(uiOptionsBox->spaceSpinBox, static_cast<void(QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, &ImportImageSeqDialog::setSpace);
67  connect(this, &ImportImageSeqDialog::filePathsChanged, this, &ImportImageSeqDialog::validateFiles);
68 }
69 
70 void ImportImageSeqDialog::setupPredefinedLayout()
71 {
72  setWindowTitle(tr("Import predefined keyframe set"));
73  setInstructionsLabel(tr("Select an image that matches the criteria: MyFile000.png, eg. Joe001.png \n"
74  "The importer will search and find images matching the same criteria. You can see the result in the preview box below."));
75  hideOptionsGroupBox(true);
76  hidePreviewGroupBox(false);
77 
78  connect(this, &ImportImageSeqDialog::filePathsChanged, this, &ImportImageSeqDialog::updatePreviewList);
79 }
80 
81 ImportImageSeqDialog::~ImportImageSeqDialog()
82 {
83  if (uiOptionsBox) {
84  delete uiOptionsBox;
85  }
86  if (uiGroupBoxPreview) {
87  delete uiGroupBoxPreview;
88  }
89 }
90 
91 int ImportImageSeqDialog::getSpace()
92 {
93  return uiOptionsBox->spaceSpinBox->value();
94 }
95 
96 void ImportImageSeqDialog::updatePreviewList(const QStringList& list)
97 {
98  Q_UNUSED(list)
99  if (mImportCriteria == ImportCriteria::PredefinedSet)
100  {
101  const PredefinedKeySet& keySet = generatePredefinedKeySet();
102 
103  Status status = Status::OK;
104  status = validateKeySet(keySet, list);
105 
106  QPushButton* okButton = getDialogButtonBox()->button(QDialogButtonBox::StandardButton::Ok);
107  if (status == Status::FAIL)
108  {
109  QMessageBox::warning(mParent,
110  status.title(),
111  status.description(),
114  okButton->setEnabled(false);
115  } else {
116  okButton->setEnabled(true);
117  }
118  setPreviewModel(keySet);
119  }
120 }
121 
122 const PredefinedKeySet ImportImageSeqDialog::generatePredefinedKeySet() const
123 {
124  PredefinedKeySet keySet;
125  const PredefinedKeySetParams& setParams = predefinedKeySetParams();
126 
127  const QStringList& filenames = setParams.filenames;
128  const int& digits = setParams.digits;
129  const QString& folderPath = setParams.folderPath;
130 
131  for (int i = 0; i < filenames.size(); i++)
132  {
133  const int& frameIndex = filenames[i].mid(setParams.dot - digits, digits).toInt();
134  const QString& absolutePath = folderPath + filenames[i];
135 
136  keySet.insert(frameIndex, absolutePath);
137  }
138  keySet.setLayerName(setParams.prefix);
139  return keySet;
140 }
141 
142 void ImportImageSeqDialog::setPreviewModel(const PredefinedKeySet& keySet)
143 {
144  PredefinedSetModel* previewModel = new PredefinedSetModel(nullptr, keySet);
145  uiGroupBoxPreview->tableView->setModel(previewModel);
146  uiGroupBoxPreview->tableView->setColumnWidth(0, 500);
147  uiGroupBoxPreview->tableView->setColumnWidth(1, 100);
148 }
149 
150 ImportExportDialog::Mode ImportImageSeqDialog::getMode()
151 {
152  return ImportExportDialog::Import;
153 }
154 
155 FileType ImportImageSeqDialog::getFileType()
156 {
157  return mFileType;
158 }
159 
160 void ImportImageSeqDialog::setSpace(int number)
161 {
162  QSignalBlocker b1(uiOptionsBox->spaceSpinBox);
163  uiOptionsBox->spaceSpinBox->setValue(number);
164 }
165 
166 void ImportImageSeqDialog::importArbitrarySequence()
167 {
168  QStringList files = getFilePaths();
169  int number = getSpace();
170 
171  // Show a progress dialog, as this can take a while if you have lots of images.
172  QProgressDialog progress(tr("Importing image sequence..."), tr("Abort"), 0, 100, mParent);
173  hideQuestionMark(progress);
174  progress.setWindowModality(Qt::WindowModal);
175  progress.show();
176 
177  int totalImagesToImport = files.count();
178  int imagesImportedSoFar = 0;
179  progress.setMaximum(totalImagesToImport);
180 
181  QString failedFiles;
182  bool failedImport = false;
183  for (const QString& strImgFile : files)
184  {
185  QString strImgFileLower = strImgFile.toLower();
186 
187  if (strImgFileLower.endsWith(".png") ||
188  strImgFileLower.endsWith(".jpg") ||
189  strImgFileLower.endsWith(".jpeg") ||
190  strImgFileLower.endsWith(".bmp") ||
191  strImgFileLower.endsWith(".tif") ||
192  strImgFileLower.endsWith(".tiff"))
193  {
194  mEditor->importImage(strImgFile);
195 
196  imagesImportedSoFar++;
197  progress.setValue(imagesImportedSoFar);
198  QApplication::processEvents(QEventLoop::ExcludeUserInputEvents); // Required to make progress bar update
199 
200  if (progress.wasCanceled())
201  {
202  break;
203  }
204  }
205  else
206  {
207  failedFiles += strImgFile + "\n";
208  if (!failedImport)
209  {
210  failedImport = true;
211  }
212  }
213 
214  for (int i = 1; i < number; i++)
215  {
216  mEditor->scrubForward();
217  }
218  }
219 
220  if (failedImport)
221  {
222  QMessageBox::warning(mParent,
223  tr("Warning"),
224  tr("Unable to import") + failedFiles,
227  }
228 
229 
230  emit notifyAnimationLengthChanged();
231  progress.close();
232 }
233 
234 const PredefinedKeySetParams ImportImageSeqDialog::predefinedKeySetParams() const
235 {
236  QString strFilePath = getFilePath();
237  PredefinedKeySetParams setParams;
238 
239  // local vars for testing file validity
240  int dot = strFilePath.lastIndexOf(".");
241  int slash = strFilePath.lastIndexOf("/");
242  QString fName = strFilePath.mid(slash + 1);
243  QString path = strFilePath.left(slash + 1);
244  QString digit = strFilePath.mid(slash + 1, dot - slash - 1);
245 
246  // Find number of digits (min: 1, max: digit.length - 1)
247  int digits = 0;
248  for (int i = digit.length() - 1; i > 0; i--)
249  {
250  if (digit.at(i).isDigit())
251  {
252  digits++;
253  }
254  else
255  {
256  break;
257  }
258  }
259 
260  if (digits < 1)
261  {
262  return setParams;
263  }
264 
265  digit = strFilePath.mid(dot - digits, digits);
266  QString prefix = strFilePath.mid(slash + 1, dot - slash - digits - 1);
267  QString suffix = strFilePath.mid(dot, strFilePath.length() - 1);
268 
269  QDir dir = strFilePath.left(strFilePath.lastIndexOf("/"));
271  if (sList.isEmpty()) { return setParams; }
272 
273  // List of files is not empty. Let's go find the relevant files
274  QStringList finalList;
275  int validLength = prefix.length() + digit.length() + suffix.length();
276  for (int i = 0; i < sList.size(); i++)
277  {
278  if (sList[i].startsWith(prefix) &&
279  sList[i].length() == validLength &&
280  sList[i].mid(sList[i].lastIndexOf(".") - digits, digits).toInt() > 0 &&
281  sList[i].endsWith(suffix))
282  {
283  finalList.append(sList[i]);
284  }
285  }
286  if (finalList.isEmpty()) { return setParams; }
287 
288  // List of relevant files is not empty. Let's validate them
289  dot = finalList[0].lastIndexOf(".");
290 
291  QStringList absolutePaths;
292  for (QString fileName : finalList) {
293  absolutePaths << path + fileName;
294  }
295 
296  setParams.dot = dot;
297  setParams.digits = digits;
298  setParams.filenames = finalList;
299  setParams.folderPath = path;
300  setParams.absolutePaths = absolutePaths;
301  setParams.prefix = prefix;
302  return setParams;
303 }
304 
305 void ImportImageSeqDialog::importPredefinedSet()
306 {
307  PredefinedKeySet keySet = generatePredefinedKeySet();
308 
309  // Show a progress dialog, as this can take a while if you have lots of images.
310  QProgressDialog progress(tr("Importing images..."), tr("Abort"), 0, 100, mParent);
311  hideQuestionMark(progress);
312  progress.setWindowModality(Qt::WindowModal);
313  progress.show();
314 
315  int totalImagesToImport = keySet.size();
316  int imagesImportedSoFar = 0;
317  progress.setMaximum(totalImagesToImport);
318 
319  mEditor->layers()->createBitmapLayer(keySet.layerName());
320 
321  for (int i = 0; i < keySet.size(); i++)
322  {
323  const int& frameIndex = keySet.keyFrameIndexAt(i);
324  const QString& filePath = keySet.filePathAt(i);
325 
326  mEditor->scrubTo(frameIndex);
327  bool ok = mEditor->importImage(filePath);
328  imagesImportedSoFar++;
329 
330  progress.setValue(imagesImportedSoFar);
331  QApplication::processEvents(QEventLoop::ExcludeUserInputEvents); // Required to make progress bar update
332 
333  if (progress.wasCanceled())
334  {
335  break;
336  }
337 
338  if (!ok) { return;}
339  }
340 
341  emit notifyAnimationLengthChanged();
342 }
343 
344 QStringList ImportImageSeqDialog::getFilePaths()
345 {
346  return ImportExportDialog::getFilePaths();
347 }
348 
349 Status ImportImageSeqDialog::validateKeySet(const PredefinedKeySet& keySet, const QStringList& filepaths)
350 {
351  QString msg = "";
352  QString failedPathsString;
353 
354  Status status = Status::OK;
355 
356  if (filepaths.isEmpty()) { status = Status::FAIL; }
357 
358  if (keySet.isEmpty())
359  {
360  status = Status::FAIL;
361  failedPathsString = QLocale().createSeparatedList(filepaths);
362  }
363 
364  if (status == Status::FAIL)
365  {
366  status.setTitle(tr("Invalid path"));
367  status.setDescription(QString(tr("The following file did not meet the criteria: \n%1 \n\nRead the instructions and try again")).arg(failedPathsString));
368  }
369 
370  return status;
371 }
372 
373 Status ImportImageSeqDialog::validateFiles(const QStringList &filepaths)
374 {
375  QString failedPathsString = "";
376 
377  Status status = Status::OK;
378 
379  if (filepaths.isEmpty()) { status = Status::FAIL; }
380 
381  for (int i = 0; i < filepaths.count(); i++)
382  {
383  QFileInfo file = filepaths.at(i);
384  if (!file.exists())
385  failedPathsString += filepaths.at(i) + "\n";
386  }
387 
388  if (!failedPathsString.isEmpty())
389  {
390  status = Status::FAIL;
391  status.setTitle(tr("Invalid path"));
392  status.setDescription(QString(tr("The following file(-s) did not meet the criteria: \n%1")).arg(failedPathsString));
393  }
394 
395  if (status == Status::OK)
396  {
397  getDialogButtonBox()->button(QDialogButtonBox::StandardButton::Ok)->setEnabled(true);
398  }
399  return status;
400 }
void setupUi(QWidget *widget)
int length() const const
bool isDigit() const const
const T & at(int i) const const
bool endsWith(const T &value) const const
int lastIndexOf(QStringView str, int from) const const
QString tr(const char *sourceText, const char *disambiguation, int n)
int size() const const
int lastIndexOf(QChar ch, int from, Qt::CaseSensitivity cs) const const
void valueChanged(int i)
void setEnabled(bool)
int count(const T &value) const const
void processEvents(QEventLoop::ProcessEventsFlags flags)
void append(const T &value)
bool isEmpty() const const
bool isEmpty() const const
bool endsWith(const QString &s, Qt::CaseSensitivity cs) const const
QString createSeparatedList(const QStringList &list) const const
QString toLower() const const
bool exists() const const
QString mid(int position, int n) const const
QStringList entryList(QDir::Filters filters, QDir::SortFlags sort) const const
const QChar at(int position) const const
void setWindowTitle(const QString &)
QList< T > mid(int pos, int length) const const
int length() const const
QString left(int n) const const
QPushButton * button(QDialogButtonBox::StandardButton which) const const
QMessageBox::StandardButton warning(QWidget *parent, const QString &title, const QString &text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton)
WindowModal
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)