tahoma2d/toonz/sources/toonz/loadfoldercommand.cpp
2023-10-12 08:23:29 -04:00

600 lines
19 KiB
C++

#include "iocommand.h"
// Tnz6 includes
#include "tapp.h"
#include "menubarcommandids.h"
#include "loadfolderpopup.h"
// TnzQt includes
#include "toonzqt/dvdialog.h"
#include "toonzqt/gutil.h"
#include "toonzqt/validatedchoicedialog.h"
#include "toonzqt/menubarcommand.h"
// TnzLib includes
#include "toonz/tscenehandle.h"
#include "toonz/toonzscene.h"
#include "toonz/preferences.h"
// TnzCore includes
#include "tsystem.h"
#include "tfiletype.h"
// Qt includes
#include <QCoreApplication>
#include <QDir>
#include <QFileInfo>
#include <QRegExp>
#include <QButtonGroup>
#include <QRadioButton>
// boost includes
#include <boost/optional.hpp>
#include <boost/operators.hpp>
#include <boost/range.hpp>
#include <boost/iterator/transform_iterator.hpp>
#include <boost/range/algorithm/for_each.hpp>
#include <boost/range/algorithm/copy.hpp>
#include <boost/range/adaptor/filtered.hpp>
#include <boost/range/adaptor/adjacent_filtered.hpp>
#include <boost/range/adaptor/transformed.hpp>
#include <boost/utility/in_place_factory.hpp>
// STD includes
#include <set>
#include <algorithm>
//************************************************************************
// Local namespace structures
//************************************************************************
namespace {
typedef IoCmd::LoadResourceArguments LRArgs;
typedef TFilePath (*PathFunc)(const TFilePath &);
//--------------------------------------------------------------
struct FormatData {
QRegExp m_regExp;
PathFunc m_resourcePathFunc, m_componentPathFunc;
};
//************************************************************************
// ResourceData definition
//************************************************************************
struct Resource //! A single resource to be loaded.
{
struct Path : private boost::less_than_comparable<Path> //! Path locating a
//! resource.
{
TFilePath m_rootFp, //!< Selected root folder hosting the resource.
m_relFp; //!< Path representing the resource, \a relative
//! to \p m_rootFolder.
public:
Path(const TFilePath &rootPath, const TFilePath &relativeFilePath)
: m_rootFp(rootPath), m_relFp(relativeFilePath) {
assert(m_rootFp.isAbsolute());
assert(!m_relFp.isAbsolute());
}
TFilePath absoluteResourcePath() const { return m_rootFp + m_relFp; }
bool operator<(const Path &other) const {
return (m_rootFp < other.m_rootFp ||
(!(other.m_relFp < m_relFp) && m_relFp < other.m_relFp));
}
};
struct Component {
TFilePath m_srcFp; //!< Source path, <I>relative to parent folder</I>.
PathFunc m_pathFunc; //!< Possible callback function transforming source
//! paths into their imported counterparts.
public:
Component(const TFilePath &srcPath, PathFunc pathFunc)
: m_srcFp(srcPath), m_pathFunc(pathFunc) {
assert(m_srcFp.getParentDir() == TFilePath());
}
};
typedef std::vector<Component> CompSeq;
public:
Path m_path; //!< The level path.
CompSeq m_components; //!< File Paths for level components. The first path
//! is intended as the resource's file representant.
boost::optional<LevelOptions>
m_levelOptions; //!< Level Options to be loaded for a level resource.
public:
Resource(const Path &path) : m_path(path) {}
Resource(const TFilePath &rootPath, const TFilePath &relativeFilePath)
: m_path(rootPath, relativeFilePath) {}
Resource(const Path &path, const CompSeq &components)
: m_path(path), m_components(components) {}
};
//************************************************************************
// OverwriteDialog definition
//************************************************************************
class OverwriteDialog final : public DVGui::ValidatedChoiceDialog {
public:
enum Resolution {
NO_RESOLUTION,
CANCEL,
OVERWRITE,
SKIP,
};
struct Obj {
const TFilePath &m_dstDir;
Resource &m_rsrc;
};
public:
OverwriteDialog(QWidget *parent);
QString acceptResolution(void *obj, int resolution, bool applyToAll) override;
};
//************************************************************************
// Local namespace functions
//************************************************************************
TFilePath relativePath(const TFilePath &from, const TFilePath &to) {
return TFilePath(QDir(from.getQString()).relativeFilePath(to.getQString()));
}
//--------------------------------------------------------------
bool isLoadable(const TFilePath &resourcePath) {
TFileType::Type type = TFileType::getInfo(resourcePath);
return (type & TFileType::IMAGE || type & TFileType::LEVEL);
}
//--------------------------------------------------------------
template <typename RegStruct>
bool exactMatch(const RegStruct &regStruct, const TFilePath &fp) {
return regStruct.m_regExp.exactMatch(fp.getQString());
}
//==============================================================
TFilePath multiframeResourcePath(const TFilePath &fp) {
return fp.withFrame(TFrameId::EMPTY_FRAME);
}
//--------------------------------------------------------------
TFilePath retasComponentPath(const TFilePath &fp) {
std::wstring name = fp.getWideName();
// Assumes the name is Axxxx.tga and needs to be changed to A.xxxx.tga
if (name.size() > 4 && fp.getDots() != "..")
name.insert(name.size() - 4, 1, L'.');
return fp.withName(name);
}
//--------------------------------------------------------------
TFilePath retasResourcePath(const TFilePath &fp) {
return retasComponentPath(fp).withFrame(TFrameId::EMPTY_FRAME);
}
//--------------------------------------------------------------
static const FormatData l_formatDatas[] = {
{QRegExp(".+\\.[0-9]{4,4}.*\\..*"), &multiframeResourcePath, 0},
{QRegExp(".+[0-9]{4,4}\\.tga", Qt::CaseInsensitive), &retasResourcePath,
&retasComponentPath}};
//==============================================================
typedef std::pair<Resource::Path, int> RsrcKey;
typedef std::map<RsrcKey, Resource::CompSeq> RsrcMap;
auto const differentPath = [](const RsrcMap::value_type &a,
const RsrcMap::value_type &b) -> bool {
return (a.first.first < b.first.first) || (b.first.first < a.first.first);
};
struct buildResources_locals {
static bool isValid(const RsrcMap::value_type &rsrcVal) {
return (isLoadable(rsrcVal.first.first.m_relFp) && !rsrcVal.second.empty());
}
static Resource toResource(const RsrcMap::value_type &rsrcVal) {
return Resource(rsrcVal.first.first, rsrcVal.second);
}
struct MergeData {
QRegExp m_regExp; //!< Path regexp for file pattern recognition.
int m_componentIdx; //!< Starting index for components merging.
};
static void mergeInto(RsrcMap::iterator &rt, RsrcMap &rsrcMap) {
// NOTE: This algorithm works for 1-level-deep inclusions. I guess this is
// sufficient,
// for now.
static const std::string componentsTable[] = {"cln", "tpl", "hst"};
static const MergeData mergeTable[] = {
{QRegExp(".*\\.\\..*"), 0}, {QRegExp(".*\\.tlv"), 1}, {QRegExp(), 3}};
// Lookup rd's path in the mergeTable
const MergeData *mdt,
*mdEnd = mergeTable + boost::size(mergeTable) - 1; // Last item is fake
mdt = std::find_if(mergeTable, mdEnd, [&rt](const MergeData& mergeData){ return exactMatch(mergeData, rt->first.first.m_relFp); });
if (mdt != mdEnd) {
// Lookup every possible resource component to merge
const std::string *cBegin = componentsTable + mdt->m_componentIdx,
*cEnd = componentsTable + (mdt + 1)->m_componentIdx;
for (const std::string *ct = cBegin; ct != cEnd; ++ct) {
RsrcKey childKey(
Resource::Path(rt->first.first.m_rootFp,
rt->first.first.m_relFp.withNoFrame().withType(*ct)),
rt->first.second);
RsrcMap::iterator chrt = rsrcMap.find(childKey);
if (chrt != rsrcMap.end()) {
// Move every component into rsrc
rt->second.insert(rt->second.end(), chrt->second.begin(),
chrt->second.end());
chrt->second.clear();
}
}
}
}
static void assignLevelOptions(Resource &rsrc) {
assert(!rsrc.m_components.empty());
const Preferences &prefs = *Preferences::instance();
// Match level format against the level's *file representant*
int formatIdx = prefs.matchLevelFormat(rsrc.m_components.front().m_srcFp);
if (formatIdx >= 0)
rsrc.m_levelOptions = prefs.levelFormat(formatIdx).m_options;
}
}; // buildResources_locals
void buildResources(std::vector<Resource> &resources, const TFilePath &rootPath,
const TFilePath &subPath = TFilePath()) {
typedef buildResources_locals locals;
const TFilePath &folderPath = rootPath + subPath;
assert(QFileInfo(folderPath.getQString()).isDir());
// Extract loadable levels in the folder
QDir folderDir(folderPath.getQString());
{
const QStringList &files =
folderDir.entryList(QDir::Files); // Files only first
// Store every possible resource path
RsrcMap rsrcMap;
QStringList::const_iterator ft, fEnd = files.end();
for (ft = files.begin(); ft != fEnd; ++ft) {
const TFilePath &compPath = TFilePath(*ft); // Relative to folderPath
PathFunc componentFunc = 0;
TFilePath relPath = relativePath(rootPath, folderPath + compPath);
const FormatData *fdt,
*fdEnd = l_formatDatas + boost::size(l_formatDatas);
fdt = std::find_if(
l_formatDatas, fdEnd, [&relPath](const FormatData &formatData){ return exactMatch(formatData, relPath); });
if (fdt != fdEnd) {
relPath = fdt->m_resourcePathFunc(relPath);
componentFunc = fdt->m_componentPathFunc;
}
assert(!relPath.isEmpty());
RsrcKey rsrcKey(Resource::Path(rootPath, relPath),
int(fdt - l_formatDatas));
rsrcMap[rsrcKey].push_back(Resource::Component(compPath, componentFunc));
}
// Further let found resources merge with others they recognize as
// 'children'
RsrcMap::iterator rt, rEnd = rsrcMap.end();
for (rt = rsrcMap.begin(); rt != rEnd; ++rt) locals::mergeInto(rt, rsrcMap);
// Export valid data into the output resources collection
boost::copy(rsrcMap | boost::adaptors::filtered(locals::isValid) |
boost::adaptors::adjacent_filtered(
differentPath) // E.g. A.xxxx.tga and Axxxx.tga
| boost::adaptors::transformed(locals::toResource),
std::back_inserter(resources));
}
// Look for level options associated to each level
std::for_each(resources.begin(), resources.end(), locals::assignLevelOptions);
// Recursive on sub-folders
const QStringList &dirs =
folderDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
QStringList::const_iterator dt, dEnd = dirs.end();
for (dt = dirs.begin(); dt != dEnd; ++dt)
buildResources(resources, rootPath, subPath + TFilePath(*dt));
}
//--------------------------------------------------------------
TFilePath dstPath(const TFilePath &dstDir, const Resource::Component &comp) {
return dstDir +
(comp.m_pathFunc ? comp.m_pathFunc(comp.m_srcFp) : comp.m_srcFp);
}
//--------------------------------------------------------------
struct import_Locals {
const ToonzScene &m_scene;
std::unique_ptr<OverwriteDialog> m_overwriteDialog;
void switchToDst(Resource::Path &path) {
path.m_rootFp = m_scene.decodeFilePath(
m_scene.getImportedLevelPath(path.absoluteResourcePath())
.getParentDir() // E.g. +drawings/
+ path.m_rootFp.getWideName() // Root dir name
);
}
static void copy(const TFilePath &srcDir, const TFilePath &dstDir,
const Resource::Component &comp, bool overwrite) {
TSystem::copyFile(dstPath(dstDir, comp), srcDir + comp.m_srcFp, overwrite);
}
void import(Resource &rsrc) {
if (!m_scene.isExternPath(rsrc.m_path.m_rootFp)) return;
try {
// Build parent folder paths
const TFilePath &srcDir =
rsrc.m_path.absoluteResourcePath().getParentDir();
switchToDst(rsrc.m_path);
const TFilePath &dstDir =
rsrc.m_path.absoluteResourcePath().getParentDir();
// Make sure destination folder exists
TSystem::mkDir(dstDir);
// Find out whether a destination component file
// already exists
bool overwrite = false;
OverwriteDialog::Obj obj = {dstDir, rsrc};
switch (m_overwriteDialog->execute(&obj)) {
case OverwriteDialog::OVERWRITE:
overwrite = true;
break;
case OverwriteDialog::SKIP:
return;
}
// Perform resource copy
std::for_each(rsrc.m_components.begin(), rsrc.m_components.end(),
[&srcDir, &dstDir, &overwrite](const Resource::Component &comp){ copy(srcDir, dstDir, comp, overwrite); });
} catch (const TException &e) {
DVGui::error(QString::fromStdWString(e.getMessage()));
} catch (...) {
}
}
}; // import_Locals
void import(const ToonzScene &scene, std::vector<Resource> &resources,
IoCmd::LoadResourceArguments::ScopedBlock &sb) {
import_Locals locals = {scene, std::unique_ptr<OverwriteDialog>()};
// Setup import GUI
int r, rCount = resources.size();
DVGui::ProgressDialog *progressDialog = 0;
if (rCount > 1) {
progressDialog = &sb.progressDialog();
progressDialog->setModal(true);
progressDialog->setMinimum(0);
progressDialog->setMaximum(rCount);
progressDialog->show();
}
// Perform import
locals.m_overwriteDialog.reset(new OverwriteDialog(
progressDialog ? (QWidget *)progressDialog
: (QWidget *)TApp::instance()->getMainWindow()));
for (r = 0; r != rCount; ++r) {
Resource &rsrc = resources[r];
if (progressDialog) {
progressDialog->setLabelText(
DVGui::ProgressDialog::tr("Importing \"%1\"...")
.arg(rsrc.m_path.m_relFp.getQString()));
progressDialog->setValue(r);
QCoreApplication::processEvents();
}
locals.import(rsrc);
}
}
//************************************************************************
// OverwriteDialog implementation
//************************************************************************
OverwriteDialog::OverwriteDialog(QWidget *parent)
: ValidatedChoiceDialog(parent, ValidatedChoiceDialog::APPLY_TO_ALL) {
setWindowTitle(QObject::tr("Warning!", "OverwriteDialog"));
// Option 1: OVERWRITE
QRadioButton *radioButton = new QRadioButton;
radioButton->setText(QObject::tr("Overwrite", "OverwriteDialog"));
radioButton->setFixedHeight(20);
radioButton->setChecked(true); // initial option: OVERWRITE
m_buttonGroup->addButton(radioButton, OVERWRITE);
addWidget(radioButton);
// Option 2: SKIP
radioButton = new QRadioButton;
radioButton->setText(QObject::tr("Skip", "OverwriteDialog"));
radioButton->setFixedHeight(20);
m_buttonGroup->addButton(radioButton, SKIP);
addWidget(radioButton);
endVLayout();
layout()->setSizeConstraint(QLayout::SetFixedSize);
}
//--------------------------------------------------------------
QString OverwriteDialog::acceptResolution(void *obj_, int resolution,
bool applyToAll) {
struct locals {
static bool existsComponent(const TFilePath &dstDir,
const Resource::Component &comp) {
return QFile::exists(dstPath(dstDir, comp).getQString());
}
static bool existsResource(const TFilePath &dstDir, const Resource &rsrc) {
return std::any_of(rsrc.m_components.begin(), rsrc.m_components.end(),
[&dstDir](const Resource::Component &comp){ return existsComponent(dstDir, comp); });
}
}; // locals
const Obj &obj = *static_cast<Obj *>(obj_);
QString error;
if (resolution == NO_RESOLUTION) {
// Test existence of any resource components
if (locals::existsResource(obj.m_dstDir, obj.m_rsrc))
error = QObject::tr(
"File \"%1\" already exists.\nDo you want to overwrite it?",
"OverwriteDialog")
.arg(obj.m_rsrc.m_path.m_relFp.getQString());
}
return error;
}
} // namespace
//************************************************************************
// API functions
//************************************************************************
int IoCmd::loadResourceFolders(LoadResourceArguments &args,
LoadResourceArguments::ScopedBlock *sb) {
struct locals {
static LRArgs::ResourceData toResourceData(const Resource &rsrc) {
LRArgs::ResourceData rd(rsrc.m_path.absoluteResourcePath());
rd.m_options = rsrc.m_levelOptions;
return rd;
}
static bool isExternPath(const ToonzScene &scene,
const LRArgs::ResourceData &rd) {
return scene.isExternPath(rd.m_path);
}
}; // locals
boost::optional<LRArgs::ScopedBlock> sb_;
if (!sb) sb = (sb_ = boost::in_place()).get_ptr();
ToonzScene *scene = TApp::instance()->getCurrentScene()->getScene();
assert(scene);
// Loading options popup
// Deal with import decision
bool import = false;
{
if (std::any_of(
args.resourceDatas.begin(), args.resourceDatas.end(),
[scene](const LRArgs::ResourceData &rd){ return locals::isExternPath(*scene, rd); })) {
// Ask for data import in this case
int resolutionButton = DVGui::MsgBox(
QObject::tr("Selected folders don't belong to the current project.\n"
"Do you want to import them or load from their original "
"location?"),
QObject::tr("Load"), QObject::tr("Import"), QObject::tr("Cancel"));
enum { CLOSED, LOAD, IMPORT, CANCELED };
switch (resolutionButton) {
case CLOSED:
case CANCELED:
return 0;
case IMPORT:
import = true;
}
}
}
// Select resources to be loaded
std::vector<Resource> resources;
boost::for_each(args.resourceDatas,
[&resources](const LRArgs::ResourceData &resourceData){ buildResources(resources, resourceData.m_path, TFilePath()); });
// Import them if required
if (import) ::import(*scene, resources, *sb);
// Temporarily substitute args' import policy
struct SubstImportPolicy {
LRArgs &m_args;
LRArgs::ImportPolicy m_policy;
SubstImportPolicy(LRArgs &args, LRArgs::ImportPolicy policy)
: m_args(args), m_policy(args.importPolicy) {
args.importPolicy = policy;
}
~SubstImportPolicy() { m_args.importPolicy = m_policy; }
} substImportPolicy(args, import ? LRArgs::IMPORT : LRArgs::LOAD);
// Perform loading via loadResources()
args.resourceDatas.assign(
boost::make_transform_iterator(resources.begin(), locals::toResourceData),
boost::make_transform_iterator(resources.end(), locals::toResourceData));
return IoCmd::loadResources(args, false, sb);
}
//************************************************************************
// Command instantiation
//************************************************************************
OpenPopupCommandHandler<LoadFolderPopup> loadFolderCommand(MI_LoadFolder);