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