1
2
3 #include "iocommand.h"
4
5 // Tnz6 includes
6 #include "tapp.h"
7 #include "menubarcommandids.h"
8 #include "loadfolderpopup.h"
9
10 // TnzQt includes
11 #include "toonzqt/dvdialog.h"
12 #include "toonzqt/gutil.h"
13 #include "toonzqt/validatedchoicedialog.h"
14 #include "toonzqt/menubarcommand.h"
15
16 // TnzLib includes
17 #include "toonz/tscenehandle.h"
18 #include "toonz/toonzscene.h"
19 #include "toonz/preferences.h"
20
21 // TnzCore includes
22 #include "tsystem.h"
23 #include "tfiletype.h"
24
25 // Qt includes
26 #include <QCoreApplication>
27 #include <QDir>
28 #include <QFileInfo>
29 #include <QRegExp>
30 #include <QButtonGroup>
31 #include <QRadioButton>
32
33 // boost includes
34 #include <boost/optional.hpp>
35 #include <boost/operators.hpp>
36 #include <boost/range.hpp>
37
38 #include <boost/bind.hpp>
39
40 #include <boost/iterator/transform_iterator.hpp>
41
42 #include <boost/range/algorithm/for_each.hpp>
43 #include <boost/range/algorithm/copy.hpp>
44 #include <boost/range/adaptor/filtered.hpp>
45 #include <boost/range/adaptor/adjacent_filtered.hpp>
46 #include <boost/range/adaptor/transformed.hpp>
47
48 #include <boost/utility/in_place_factory.hpp>
49
50 // STD includes
51 #include <set>
52 #include <algorithm>
53
54 //************************************************************************
55 // Local namespace structures
56 //************************************************************************
57
58 namespace {
59
60 typedef IoCmd::LoadResourceArguments LRArgs;
61
62 typedef TFilePath (*PathFunc)(const TFilePath &);
63
64 //--------------------------------------------------------------
65
66 struct FormatData {
67 QRegExp m_regExp;
68 PathFunc m_resourcePathFunc, m_componentPathFunc;
69 };
70
71 //************************************************************************
72 // ResourceData definition
73 //************************************************************************
74
75 struct Resource //! A single resource to be loaded.
76 {
77 struct Path : private boost::less_than_comparable<Path> //! Path locating a
78 //! resource.
79 {
80 TFilePath m_rootFp, //!< Selected root folder hosting the resource.
81 m_relFp; //!< Path representing the resource, \a relative
82 //! to \p m_rootFolder.
83 public:
Path__anon751b49ca0111::Resource::Path84 Path(const TFilePath &rootPath, const TFilePath &relativeFilePath)
85 : m_rootFp(rootPath), m_relFp(relativeFilePath) {
86 assert(m_rootFp.isAbsolute());
87 assert(!m_relFp.isAbsolute());
88 }
89
absoluteResourcePath__anon751b49ca0111::Resource::Path90 TFilePath absoluteResourcePath() const { return m_rootFp + m_relFp; }
91
operator <__anon751b49ca0111::Resource::Path92 bool operator<(const Path &other) const {
93 return (m_rootFp < other.m_rootFp ||
94 (!(other.m_relFp < m_relFp) && m_relFp < other.m_relFp));
95 }
96 };
97
98 struct Component {
99 TFilePath m_srcFp; //!< Source path, <I>relative to parent folder</I>.
100 PathFunc m_pathFunc; //!< Possible callback function transforming source
101 //! paths into their imported counterparts.
102 public:
Component__anon751b49ca0111::Resource::Component103 Component(const TFilePath &srcPath, PathFunc pathFunc)
104 : m_srcFp(srcPath), m_pathFunc(pathFunc) {
105 assert(m_srcFp.getParentDir() == TFilePath());
106 }
107 };
108
109 typedef std::vector<Component> CompSeq;
110
111 public:
112 Path m_path; //!< The level path.
113 CompSeq m_components; //!< File Paths for level components. The first path
114 //! is intended as the resource's file representant.
115 boost::optional<LevelOptions>
116 m_levelOptions; //!< Level Options to be loaded for a level resource.
117
118 public:
Resource__anon751b49ca0111::Resource119 Resource(const Path &path) : m_path(path) {}
Resource__anon751b49ca0111::Resource120 Resource(const TFilePath &rootPath, const TFilePath &relativeFilePath)
121 : m_path(rootPath, relativeFilePath) {}
Resource__anon751b49ca0111::Resource122 Resource(const Path &path, const CompSeq &components)
123 : m_path(path), m_components(components) {}
124 };
125
126 //************************************************************************
127 // OverwriteDialog definition
128 //************************************************************************
129
130 class OverwriteDialog final : public DVGui::ValidatedChoiceDialog {
131 public:
132 enum Resolution {
133 NO_RESOLUTION,
134 CANCEL,
135 OVERWRITE,
136 SKIP,
137 };
138
139 struct Obj {
140 const TFilePath &m_dstDir;
141 Resource &m_rsrc;
142 };
143
144 public:
145 OverwriteDialog(QWidget *parent);
146
147 QString acceptResolution(void *obj, int resolution, bool applyToAll) override;
148 };
149
150 //************************************************************************
151 // Local namespace functions
152 //************************************************************************
153
relativePath(const TFilePath & from,const TFilePath & to)154 TFilePath relativePath(const TFilePath &from, const TFilePath &to) {
155 return TFilePath(QDir(from.getQString()).relativeFilePath(to.getQString()));
156 }
157
158 //--------------------------------------------------------------
159
isLoadable(const TFilePath & resourcePath)160 bool isLoadable(const TFilePath &resourcePath) {
161 TFileType::Type type = TFileType::getInfo(resourcePath);
162
163 return (type & TFileType::IMAGE || type & TFileType::LEVEL);
164 }
165
166 //--------------------------------------------------------------
167
168 template <typename RegStruct>
exactMatch(const RegStruct & regStruct,const TFilePath & fp)169 bool exactMatch(const RegStruct ®Struct, const TFilePath &fp) {
170 return regStruct.m_regExp.exactMatch(fp.getQString());
171 }
172
173 //==============================================================
174
multiframeResourcePath(const TFilePath & fp)175 TFilePath multiframeResourcePath(const TFilePath &fp) {
176 return fp.withFrame(TFrameId::EMPTY_FRAME);
177 }
178
179 //--------------------------------------------------------------
180
retasComponentPath(const TFilePath & fp)181 TFilePath retasComponentPath(const TFilePath &fp) {
182 std::wstring name = fp.getWideName();
183
184 // Assumes the name is Axxxx.tga and needs to be changed to A.xxxx.tga
185 if (name.size() > 4 && fp.getDots() != "..")
186 name.insert(name.size() - 4, 1, L'.');
187
188 return fp.withName(name);
189 }
190
191 //--------------------------------------------------------------
192
retasResourcePath(const TFilePath & fp)193 TFilePath retasResourcePath(const TFilePath &fp) {
194 return retasComponentPath(fp).withFrame(TFrameId::EMPTY_FRAME);
195 }
196
197 //--------------------------------------------------------------
198
199 static const FormatData l_formatDatas[] = {
200 {QRegExp(".+\\.[0-9]{4,4}.*\\..*"), &multiframeResourcePath, 0},
201 {QRegExp(".+[0-9]{4,4}\\.tga", Qt::CaseInsensitive), &retasResourcePath,
202 &retasComponentPath}};
203
204 //==============================================================
205
206 typedef std::pair<Resource::Path, int> RsrcKey;
207 typedef std::map<RsrcKey, Resource::CompSeq> RsrcMap;
208
209 auto const differentPath = [](const RsrcMap::value_type &a,
__anon751b49ca0202(const RsrcMap::value_type &a, const RsrcMap::value_type &b) 210 const RsrcMap::value_type &b) -> bool {
211 return (a.first.first < b.first.first) || (b.first.first < a.first.first);
212 };
213
214 struct buildResources_locals {
isValid__anon751b49ca0111::buildResources_locals215 static bool isValid(const RsrcMap::value_type &rsrcVal) {
216 return (isLoadable(rsrcVal.first.first.m_relFp) && !rsrcVal.second.empty());
217 }
218
toResource__anon751b49ca0111::buildResources_locals219 static Resource toResource(const RsrcMap::value_type &rsrcVal) {
220 return Resource(rsrcVal.first.first, rsrcVal.second);
221 }
222
223 struct MergeData {
224 QRegExp m_regExp; //!< Path regexp for file pattern recognition.
225 int m_componentIdx; //!< Starting index for components merging.
226 };
227
mergeInto__anon751b49ca0111::buildResources_locals228 static void mergeInto(RsrcMap::iterator &rt, RsrcMap &rsrcMap) {
229 // NOTE: This algorithm works for 1-level-deep inclusions. I guess this is
230 // sufficient,
231 // for now.
232
233 static const std::string componentsTable[] = {"cln", "tpl", "hst"};
234
235 static const MergeData mergeTable[] = {
236 {QRegExp(".*\\.\\..*"), 0}, {QRegExp(".*\\.tlv"), 1}, {QRegExp(), 3}};
237
238 // Lookup rd's path in the mergeTable
239 const MergeData *mdt,
240 *mdEnd = mergeTable + boost::size(mergeTable) - 1; // Last item is fake
241
242 mdt = std::find_if(mergeTable, mdEnd,
243 boost::bind(exactMatch<MergeData>, _1,
244 boost::cref(rt->first.first.m_relFp)));
245
246 if (mdt != mdEnd) {
247 // Lookup every possible resource component to merge
248 const std::string *cBegin = componentsTable + mdt->m_componentIdx,
249 *cEnd = componentsTable + (mdt + 1)->m_componentIdx;
250
251 for (const std::string *ct = cBegin; ct != cEnd; ++ct) {
252 RsrcKey childKey(
253 Resource::Path(rt->first.first.m_rootFp,
254 rt->first.first.m_relFp.withNoFrame().withType(*ct)),
255 rt->first.second);
256
257 RsrcMap::iterator chrt = rsrcMap.find(childKey);
258
259 if (chrt != rsrcMap.end()) {
260 // Move every component into rsrc
261 rt->second.insert(rt->second.end(), chrt->second.begin(),
262 chrt->second.end());
263 chrt->second.clear();
264 }
265 }
266 }
267 }
268
assignLevelOptions__anon751b49ca0111::buildResources_locals269 static void assignLevelOptions(Resource &rsrc) {
270 assert(!rsrc.m_components.empty());
271
272 const Preferences &prefs = *Preferences::instance();
273
274 // Match level format against the level's *file representant*
275 int formatIdx = prefs.matchLevelFormat(rsrc.m_components.front().m_srcFp);
276
277 if (formatIdx >= 0)
278 rsrc.m_levelOptions = prefs.levelFormat(formatIdx).m_options;
279 }
280
281 }; // buildResources_locals
282
buildResources(std::vector<Resource> & resources,const TFilePath & rootPath,const TFilePath & subPath=TFilePath ())283 void buildResources(std::vector<Resource> &resources, const TFilePath &rootPath,
284 const TFilePath &subPath = TFilePath()) {
285 typedef buildResources_locals locals;
286
287 const TFilePath &folderPath = rootPath + subPath;
288 assert(QFileInfo(folderPath.getQString()).isDir());
289
290 // Extract loadable levels in the folder
291 QDir folderDir(folderPath.getQString());
292 {
293 const QStringList &files =
294 folderDir.entryList(QDir::Files); // Files only first
295
296 // Store every possible resource path
297 RsrcMap rsrcMap;
298
299 QStringList::const_iterator ft, fEnd = files.end();
300 for (ft = files.begin(); ft != fEnd; ++ft) {
301 const TFilePath &compPath = TFilePath(*ft); // Relative to folderPath
302 PathFunc componentFunc = 0;
303
304 TFilePath relPath = relativePath(rootPath, folderPath + compPath);
305
306 const FormatData *fdt,
307 *fdEnd = l_formatDatas + boost::size(l_formatDatas);
308 fdt = std::find_if(
309 l_formatDatas, fdEnd,
310 boost::bind(exactMatch<FormatData>, _1, boost::cref(relPath)));
311
312 if (fdt != fdEnd) {
313 relPath = fdt->m_resourcePathFunc(relPath);
314 componentFunc = fdt->m_componentPathFunc;
315 }
316
317 assert(!relPath.isEmpty());
318
319 RsrcKey rsrcKey(Resource::Path(rootPath, relPath),
320 int(fdt - l_formatDatas));
321 rsrcMap[rsrcKey].push_back(Resource::Component(compPath, componentFunc));
322 }
323
324 // Further let found resources merge with others they recognize as
325 // 'children'
326 RsrcMap::iterator rt, rEnd = rsrcMap.end();
327 for (rt = rsrcMap.begin(); rt != rEnd; ++rt) locals::mergeInto(rt, rsrcMap);
328
329 // Export valid data into the output resources collection
330 boost::copy(rsrcMap | boost::adaptors::filtered(locals::isValid) |
331 boost::adaptors::adjacent_filtered(
332 differentPath) // E.g. A.xxxx.tga and Axxxx.tga
333 | boost::adaptors::transformed(locals::toResource),
334 std::back_inserter(resources));
335 }
336
337 // Look for level options associated to each level
338 std::for_each(resources.begin(), resources.end(), locals::assignLevelOptions);
339
340 // Recursive on sub-folders
341 const QStringList &dirs =
342 folderDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
343
344 QStringList::const_iterator dt, dEnd = dirs.end();
345 for (dt = dirs.begin(); dt != dEnd; ++dt)
346 buildResources(resources, rootPath, subPath + TFilePath(*dt));
347 }
348
349 //--------------------------------------------------------------
350
dstPath(const TFilePath & dstDir,const Resource::Component & comp)351 TFilePath dstPath(const TFilePath &dstDir, const Resource::Component &comp) {
352 return dstDir +
353 (comp.m_pathFunc ? comp.m_pathFunc(comp.m_srcFp) : comp.m_srcFp);
354 }
355
356 //--------------------------------------------------------------
357
358 struct import_Locals {
359 const ToonzScene &m_scene;
360 std::unique_ptr<OverwriteDialog> m_overwriteDialog;
361
switchToDst__anon751b49ca0111::import_Locals362 void switchToDst(Resource::Path &path) {
363 path.m_rootFp = m_scene.decodeFilePath(
364 m_scene.getImportedLevelPath(path.absoluteResourcePath())
365 .getParentDir() // E.g. +drawings/
366 + path.m_rootFp.getWideName() // Root dir name
367 );
368 }
369
copy__anon751b49ca0111::import_Locals370 static void copy(const TFilePath &srcDir, const TFilePath &dstDir,
371 const Resource::Component &comp, bool overwrite) {
372 TSystem::copyFile(dstPath(dstDir, comp), srcDir + comp.m_srcFp, overwrite);
373 }
374
import__anon751b49ca0111::import_Locals375 void import(Resource &rsrc) {
376 if (!m_scene.isExternPath(rsrc.m_path.m_rootFp)) return;
377
378 try {
379 // Build parent folder paths
380 const TFilePath &srcDir =
381 rsrc.m_path.absoluteResourcePath().getParentDir();
382
383 switchToDst(rsrc.m_path);
384 const TFilePath &dstDir =
385 rsrc.m_path.absoluteResourcePath().getParentDir();
386
387 // Make sure destination folder exists
388 TSystem::mkDir(dstDir);
389
390 // Find out whether a destination component file
391 // already exists
392 bool overwrite = false;
393
394 OverwriteDialog::Obj obj = {dstDir, rsrc};
395 switch (m_overwriteDialog->execute(&obj)) {
396 case OverwriteDialog::OVERWRITE:
397 overwrite = true;
398 break;
399 case OverwriteDialog::SKIP:
400 return;
401 }
402
403 // Perform resource copy
404 std::for_each(rsrc.m_components.begin(), rsrc.m_components.end(),
405 boost::bind(copy, boost::cref(srcDir), boost::cref(dstDir),
406 _1, overwrite));
407 } catch (const TException &e) {
408 DVGui::error(QString::fromStdWString(e.getMessage()));
409 } catch (...) {
410 }
411 }
412
413 }; // import_Locals
414
import(const ToonzScene & scene,std::vector<Resource> & resources,IoCmd::LoadResourceArguments::ScopedBlock & sb)415 void import(const ToonzScene &scene, std::vector<Resource> &resources,
416 IoCmd::LoadResourceArguments::ScopedBlock &sb) {
417 import_Locals locals = {scene, std::unique_ptr<OverwriteDialog>()};
418
419 // Setup import GUI
420 int r, rCount = resources.size();
421
422 DVGui::ProgressDialog *progressDialog = 0;
423 if (rCount > 1) {
424 progressDialog = &sb.progressDialog();
425
426 progressDialog->setModal(true);
427 progressDialog->setMinimum(0);
428 progressDialog->setMaximum(rCount);
429 progressDialog->show();
430 }
431
432 // Perform import
433 locals.m_overwriteDialog.reset(new OverwriteDialog(
434 progressDialog ? (QWidget *)progressDialog
435 : (QWidget *)TApp::instance()->getMainWindow()));
436
437 for (r = 0; r != rCount; ++r) {
438 Resource &rsrc = resources[r];
439
440 if (progressDialog) {
441 progressDialog->setLabelText(
442 DVGui::ProgressDialog::tr("Importing \"%1\"...")
443 .arg(rsrc.m_path.m_relFp.getQString()));
444 progressDialog->setValue(r);
445
446 QCoreApplication::processEvents();
447 }
448
449 locals.import(rsrc);
450 }
451 }
452
453 //************************************************************************
454 // OverwriteDialog implementation
455 //************************************************************************
456
OverwriteDialog(QWidget * parent)457 OverwriteDialog::OverwriteDialog(QWidget *parent)
458 : ValidatedChoiceDialog(parent, ValidatedChoiceDialog::APPLY_TO_ALL) {
459 setWindowTitle(QObject::tr("Warning!", "OverwriteDialog"));
460
461 // Option 1: OVERWRITE
462 QRadioButton *radioButton = new QRadioButton;
463 radioButton->setText(QObject::tr("Overwrite", "OverwriteDialog"));
464 radioButton->setFixedHeight(20);
465 radioButton->setChecked(true); // initial option: OVERWRITE
466
467 m_buttonGroup->addButton(radioButton, OVERWRITE);
468 addWidget(radioButton);
469
470 // Option 2: SKIP
471 radioButton = new QRadioButton;
472 radioButton->setText(QObject::tr("Skip", "OverwriteDialog"));
473 radioButton->setFixedHeight(20);
474
475 m_buttonGroup->addButton(radioButton, SKIP);
476 addWidget(radioButton);
477
478 endVLayout();
479
480 layout()->setSizeConstraint(QLayout::SetFixedSize);
481 }
482
483 //--------------------------------------------------------------
484
acceptResolution(void * obj_,int resolution,bool applyToAll)485 QString OverwriteDialog::acceptResolution(void *obj_, int resolution,
486 bool applyToAll) {
487 struct locals {
488 static bool existsComponent(const TFilePath &dstDir,
489 const Resource::Component &comp) {
490 return QFile::exists(dstPath(dstDir, comp).getQString());
491 }
492
493 static bool existsResource(const TFilePath &dstDir, const Resource &rsrc) {
494 return std::any_of(rsrc.m_components.begin(), rsrc.m_components.end(),
495 boost::bind(existsComponent, boost::cref(dstDir), _1));
496 }
497 }; // locals
498
499 const Obj &obj = *static_cast<Obj *>(obj_);
500
501 QString error;
502
503 if (resolution == NO_RESOLUTION) {
504 // Test existence of any resource components
505 if (locals::existsResource(obj.m_dstDir, obj.m_rsrc))
506 error = QObject::tr(
507 "File \"%1\" already exists.\nDo you want to overwrite it?",
508 "OverwriteDialog")
509 .arg(obj.m_rsrc.m_path.m_relFp.getQString());
510 }
511
512 return error;
513 }
514
515 } // namespace
516
517 //************************************************************************
518 // API functions
519 //************************************************************************
520
loadResourceFolders(LoadResourceArguments & args,LoadResourceArguments::ScopedBlock * sb)521 int IoCmd::loadResourceFolders(LoadResourceArguments &args,
522 LoadResourceArguments::ScopedBlock *sb) {
523 struct locals {
524 static LRArgs::ResourceData toResourceData(const Resource &rsrc) {
525 LRArgs::ResourceData rd(rsrc.m_path.absoluteResourcePath());
526 rd.m_options = rsrc.m_levelOptions;
527
528 return rd;
529 }
530
531 static bool isExternPath(const ToonzScene &scene,
532 const LRArgs::ResourceData &rd) {
533 return scene.isExternPath(rd.m_path);
534 }
535 }; // locals
536
537 boost::optional<LRArgs::ScopedBlock> sb_;
538 if (!sb) sb = (sb_ = boost::in_place()).get_ptr();
539
540 ToonzScene *scene = TApp::instance()->getCurrentScene()->getScene();
541 assert(scene);
542
543 // Loading options popup
544
545 // Deal with import decision
546 bool import = false;
547 {
548 if (std::any_of(
549 args.resourceDatas.begin(), args.resourceDatas.end(),
550 boost::bind(locals::isExternPath, boost::cref(*scene), _1))) {
551 // Ask for data import in this case
552 int resolutionButton = DVGui::MsgBox(
553 QObject::tr("Selected folders don't belong to the current project.\n"
554 "Do you want to import them or load from their original "
555 "location?"),
556 QObject::tr("Load"), QObject::tr("Import"), QObject::tr("Cancel"));
557
558 enum { CLOSED, LOAD, IMPORT, CANCELED };
559 switch (resolutionButton) {
560 case CLOSED:
561 case CANCELED:
562 return 0;
563
564 case IMPORT:
565 import = true;
566 }
567 }
568 }
569
570 // Select resources to be loaded
571 std::vector<Resource> resources;
572
573 boost::for_each(
574 args.resourceDatas |
575 boost::adaptors::transformed(boost::bind<const TFilePath &>(
576 &LRArgs::ResourceData::m_path, _1)),
577 boost::bind(::buildResources, boost::ref(resources), _1, TFilePath()));
578
579 // Import them if required
580 if (import) ::import(*scene, resources, *sb);
581
582 // Temporarily substitute args' import policy
583 struct SubstImportPolicy {
584 LRArgs &m_args;
585 LRArgs::ImportPolicy m_policy;
586
587 SubstImportPolicy(LRArgs &args, LRArgs::ImportPolicy policy)
588 : m_args(args), m_policy(args.importPolicy) {
589 args.importPolicy = policy;
590 }
591
592 ~SubstImportPolicy() { m_args.importPolicy = m_policy; }
593
594 } substImportPolicy(args, import ? LRArgs::IMPORT : LRArgs::LOAD);
595
596 // Perform loading via loadResources()
597 args.resourceDatas.assign(
598 boost::make_transform_iterator(resources.begin(), locals::toResourceData),
599 boost::make_transform_iterator(resources.end(), locals::toResourceData));
600
601 return IoCmd::loadResources(args, false, sb);
602 }
603
604 //************************************************************************
605 // Command instantiation
606 //************************************************************************
607
608 OpenPopupCommandHandler<LoadFolderPopup> loadFolderCommand(MI_LoadFolder);
609