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 &regStruct, 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