You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
503 lines
13 KiB
C++
503 lines
13 KiB
C++
/*
|
|
* Copyright (c) Contributors to the Open 3D Engine Project
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
*
|
|
*/
|
|
|
|
|
|
#include "EditorDefs.h"
|
|
|
|
#include "FolderTreeCtrl.h"
|
|
|
|
// Qt
|
|
#include <QMenu>
|
|
#include <QSortFilterProxyModel>
|
|
#include <QStandardItemModel>
|
|
|
|
// AzQtComponents
|
|
#include <AzQtComponents/Utilities/DesktopUtilities.h> // for AzQtComponents::ShowFileOnDesktop
|
|
|
|
|
|
enum ETreeImage
|
|
{
|
|
eTreeImage_Folder = 0,
|
|
eTreeImage_File = 2
|
|
};
|
|
|
|
enum CustomRoles
|
|
{
|
|
IsFolderRole = Qt::UserRole
|
|
};
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
// CFolderTreeCtrl
|
|
//////////////////////////////////////////////////////////////////////////
|
|
CFolderTreeCtrl::CFolderTreeCtrl(const QStringList& folders, const QString& fileNameSpec,
|
|
const QString& rootName, bool bDisableMonitor, bool bFlatTree, QWidget* parent)
|
|
: QTreeView(parent)
|
|
, m_rootTreeItem(nullptr)
|
|
, m_folders(folders)
|
|
, m_fileNameSpec(fileNameSpec)
|
|
, m_rootName(rootName)
|
|
, m_bDisableMonitor(bDisableMonitor)
|
|
, m_bFlatStyle(bFlatTree)
|
|
{
|
|
init(folders, fileNameSpec, rootName, bDisableMonitor, bFlatTree);
|
|
}
|
|
|
|
CFolderTreeCtrl::CFolderTreeCtrl(QWidget* parent)
|
|
: QTreeView(parent)
|
|
, m_rootTreeItem(nullptr)
|
|
, m_bDisableMonitor(false)
|
|
, m_bFlatStyle(true)
|
|
|
|
{
|
|
}
|
|
|
|
|
|
void CFolderTreeCtrl::init(const QStringList& folders, const QString& fileNameSpec, const QString& rootName, bool bDisableMonitor /*= false*/, bool bFlatTree /*= true*/)
|
|
{
|
|
m_model = new QStandardItemModel(this);
|
|
m_proxyModel = new QSortFilterProxyModel(this);
|
|
m_proxyModel->setRecursiveFilteringEnabled(true);
|
|
|
|
m_proxyModel->setSourceModel(m_model);
|
|
setModel(m_proxyModel);
|
|
|
|
m_folders = folders;
|
|
m_fileNameSpec = fileNameSpec;
|
|
m_rootName = rootName;
|
|
m_bDisableMonitor = bDisableMonitor;
|
|
m_bFlatStyle = bFlatTree;
|
|
m_fileIcon = QIcon(":/TreeView/default-icon.svg");
|
|
m_folderIcon = QIcon(":/TreeView/folder-icon.svg");
|
|
|
|
for (auto item = m_folders.begin(), end = m_folders.end(); item != end; ++item)
|
|
{
|
|
(*item) = Path::RemoveBackslash(Path::ToUnixPath((*item)));
|
|
|
|
if (CFileUtil::PathExists(*item))
|
|
{
|
|
m_foldersSegments.insert(std::make_pair((*item), Path::SplitIntoSegments((*item)).size()));
|
|
}
|
|
else if (Path::IsFolder((*item).toLocal8Bit().constData()))
|
|
{
|
|
m_foldersSegments.insert(std::make_pair((*item), Path::SplitIntoSegments((*item)).size()));
|
|
}
|
|
else
|
|
{
|
|
(*item).clear();
|
|
}
|
|
}
|
|
|
|
setHeaderHidden(true);
|
|
|
|
QObject::connect(this, &QTreeView::doubleClicked, this, &CFolderTreeCtrl::OnIndexDoubleClicked);
|
|
|
|
InitTree();
|
|
|
|
setSortingEnabled(true);
|
|
}
|
|
|
|
QString CFolderTreeCtrl::GetPath(QStandardItem* item) const
|
|
{
|
|
CTreeItem* treeItem = static_cast<CTreeItem*>(item);
|
|
|
|
if (treeItem)
|
|
{
|
|
return treeItem->GetPath();
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
bool CFolderTreeCtrl::IsFolder(QStandardItem* item) const
|
|
{
|
|
return item->data(IsFolderRole).toBool();
|
|
}
|
|
|
|
bool CFolderTreeCtrl::IsFile(QStandardItem* item) const
|
|
{
|
|
return !IsFolder(item);
|
|
}
|
|
|
|
CFolderTreeCtrl::~CFolderTreeCtrl()
|
|
{
|
|
// Obliterate tree items before destroying the controls
|
|
m_rootTreeItem.reset(nullptr);
|
|
|
|
// Unsubscribe from file change notifications
|
|
if (!m_bDisableMonitor)
|
|
{
|
|
CFileChangeMonitor::Instance()->Unsubscribe(this);
|
|
}
|
|
}
|
|
|
|
void CFolderTreeCtrl::OnIndexDoubleClicked(const QModelIndex& index)
|
|
{
|
|
if (!m_proxyModel || !m_model)
|
|
{
|
|
return;
|
|
}
|
|
|
|
QStandardItem* item = GetSourceItemByIndex(index);
|
|
if (item)
|
|
{
|
|
Q_EMIT ItemDoubleClicked(item);
|
|
}
|
|
}
|
|
|
|
void CFolderTreeCtrl::OnFileMonitorChange(const SFileChangeInfo& rChange)
|
|
{
|
|
const QString filePath = Path::ToUnixPath(Path::GetRelativePath(rChange.filename));
|
|
for (auto item = m_folders.begin(), end = m_folders.end(); item != end; ++item)
|
|
{
|
|
// Only look for changes in folder
|
|
if (filePath.indexOf((*item)) != 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (rChange.changeType == rChange.eChangeType_Created || rChange.changeType == rChange.eChangeType_RenamedNewName)
|
|
{
|
|
if (CFileUtil::PathExists(filePath))
|
|
{
|
|
LoadTreeRec(filePath);
|
|
}
|
|
else
|
|
{
|
|
AddItem(filePath);
|
|
}
|
|
}
|
|
else if (rChange.changeType == rChange.eChangeType_Deleted || rChange.changeType == rChange.eChangeType_RenamedOldName)
|
|
{
|
|
RemoveItem(filePath);
|
|
}
|
|
}
|
|
}
|
|
|
|
void CFolderTreeCtrl::contextMenuEvent(QContextMenuEvent* e)
|
|
{
|
|
if (!m_model)
|
|
{
|
|
return;
|
|
}
|
|
|
|
auto index = indexAt(e->pos());
|
|
QStandardItem* item = GetSourceItemByIndex(index);
|
|
|
|
if (!item)
|
|
{
|
|
return;
|
|
}
|
|
|
|
const QString path = GetPath(item);
|
|
|
|
QMenu menu;
|
|
QAction* editAction = menu.addAction(tr("Edit"));
|
|
connect(editAction, &QAction::triggered, this, [=]()
|
|
{
|
|
this->Edit(path);
|
|
});
|
|
QAction* showInExplorerAction = menu.addAction(tr("Show In Explorer"));
|
|
connect(showInExplorerAction, &QAction::triggered, this, [=]()
|
|
{
|
|
this->ShowInExplorer(path);
|
|
});
|
|
menu.exec(QCursor::pos());
|
|
}
|
|
|
|
void CFolderTreeCtrl::InitTree()
|
|
{
|
|
m_rootTreeItem.reset(new CTreeItem(*this, m_rootName));
|
|
|
|
for (auto item = m_folders.begin(), end = m_folders.end(); item != end; ++item)
|
|
{
|
|
if (!(*item).isEmpty())
|
|
{
|
|
LoadTreeRec((*item));
|
|
}
|
|
}
|
|
|
|
if (!m_bDisableMonitor)
|
|
{
|
|
CFileChangeMonitor::Instance()->Subscribe(this);
|
|
}
|
|
|
|
|
|
expandAll();
|
|
}
|
|
|
|
void CFolderTreeCtrl::LoadTreeRec(const QString& currentFolder)
|
|
{
|
|
CFileEnum fileEnum;
|
|
QFileInfo fileData;
|
|
|
|
QString currentFolderSlash = Path::AddSlash(currentFolder);
|
|
QString targetFolder = currentFolder;
|
|
|
|
if (currentFolder.startsWith('@'))
|
|
{
|
|
char resolvedPath[AZ_MAX_PATH_LEN] = { 0 };
|
|
if (AZ::IO::FileIOBase::GetDirectInstance()->ResolvePath(currentFolder.toLocal8Bit().constData(), resolvedPath, AZ_MAX_PATH_LEN))
|
|
{
|
|
targetFolder = resolvedPath;
|
|
}
|
|
|
|
// update the base folder name
|
|
QStringList parts = Path::SplitIntoSegments(currentFolderSlash);
|
|
if (parts.size() > 1)
|
|
{
|
|
parts.removeFirst();
|
|
currentFolderSlash = Path::AddSlash(parts.join(QDir::separator()));
|
|
}
|
|
}
|
|
|
|
for (bool bFoundFile = fileEnum.StartEnumeration(targetFolder, "*", &fileData);
|
|
bFoundFile; bFoundFile = fileEnum.GetNextFile(&fileData))
|
|
{
|
|
const QString fileName = fileData.fileName();
|
|
|
|
// Have we found a folder?
|
|
if (fileData.isDir())
|
|
{
|
|
// Skip the parent folder entries
|
|
if (fileName == "." || fileName == "..")
|
|
{
|
|
continue;
|
|
}
|
|
|
|
LoadTreeRec(currentFolderSlash + fileName);
|
|
}
|
|
|
|
AddItem(currentFolderSlash + fileName);
|
|
}
|
|
}
|
|
|
|
void CFolderTreeCtrl::AddItem(const QString& path)
|
|
{
|
|
QString folder;
|
|
QString fileNameWithoutExtension;
|
|
QString ext;
|
|
|
|
Path::Split(path, folder, fileNameWithoutExtension, ext);
|
|
|
|
auto regex = QRegExp(m_fileNameSpec, Qt::CaseInsensitive, QRegExp::Wildcard);
|
|
if (regex.exactMatch(path))
|
|
{
|
|
CTreeItem* folderTreeItem = CreateFolderItems(folder);
|
|
folderTreeItem->AddChild(fileNameWithoutExtension, path, eTreeImage_File);
|
|
}
|
|
}
|
|
|
|
void CFolderTreeCtrl::RemoveItem(const QString& path)
|
|
{
|
|
if (!CFileUtil::FileExists(path))
|
|
{
|
|
auto findIter = m_pathToTreeItem.find(path);
|
|
|
|
if (findIter != m_pathToTreeItem.end())
|
|
{
|
|
CTreeItem* foundItem = findIter->second;
|
|
foundItem->Remove();
|
|
RemoveEmptyFolderItems(Path::GetPath(path));
|
|
}
|
|
}
|
|
}
|
|
|
|
CFolderTreeCtrl::CTreeItem* CFolderTreeCtrl::GetItem(const QString& path)
|
|
{
|
|
auto findIter = m_pathToTreeItem.find(path);
|
|
|
|
if (findIter == m_pathToTreeItem.end())
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
return findIter->second;
|
|
}
|
|
|
|
QStandardItem* CFolderTreeCtrl::GetSourceItemByIndex(const QModelIndex& index) const
|
|
{
|
|
if (!m_proxyModel || !m_model)
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
// Since our tree view has a proxy model to handle the sorting/filtering, any index
|
|
// found on the tree view (e.g. the selected index) needs to be mapped back to the source
|
|
// model to find the actual item.
|
|
auto sourceIndex = m_proxyModel->mapToSource(index);
|
|
return m_model->itemFromIndex(sourceIndex);
|
|
}
|
|
|
|
QString CFolderTreeCtrl::CalculateFolderFullPath(const QStringList& splittedFolder, int idx)
|
|
{
|
|
QString path;
|
|
for (int segIdx = 0; segIdx <= idx; ++segIdx)
|
|
{
|
|
if (segIdx != 0)
|
|
{
|
|
path.append(QLatin1Char('/'));
|
|
}
|
|
path.append(splittedFolder[segIdx]);
|
|
}
|
|
return path;
|
|
}
|
|
|
|
CFolderTreeCtrl::CTreeItem* CFolderTreeCtrl::CreateFolderItems(const QString& folder)
|
|
{
|
|
QStringList splittedFolder = Path::SplitIntoSegments(folder);
|
|
CTreeItem* currentTreeItem = m_rootTreeItem.get();
|
|
|
|
if (!m_bFlatStyle)
|
|
{
|
|
QString currentFolder;
|
|
QString fullpath;
|
|
const int splittedFoldersCount = splittedFolder.size();
|
|
for (int idx = 0; idx < splittedFoldersCount; ++idx)
|
|
{
|
|
currentFolder = Path::RemoveBackslash(splittedFolder[idx]);
|
|
fullpath = CalculateFolderFullPath(splittedFolder, idx);
|
|
|
|
CTreeItem* folderItem = GetItem(fullpath);
|
|
if (!folderItem)
|
|
{
|
|
currentTreeItem = currentTreeItem->AddChild(currentFolder, fullpath, eTreeImage_Folder);
|
|
}
|
|
else
|
|
{
|
|
currentTreeItem = folderItem;
|
|
}
|
|
}
|
|
}
|
|
|
|
return currentTreeItem;
|
|
}
|
|
|
|
void CFolderTreeCtrl::RemoveEmptyFolderItems(const QString& folder)
|
|
{
|
|
QStringList splittedFolder = Path::SplitIntoSegments(folder);
|
|
const int splittedFoldersCount = splittedFolder.size();
|
|
QString fullpath;
|
|
for (int idx = 0; idx < splittedFoldersCount; ++idx)
|
|
{
|
|
fullpath = CalculateFolderFullPath(splittedFolder, idx);
|
|
CTreeItem* folderItem = GetItem(fullpath);
|
|
if (!folderItem)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!folderItem->hasChildren())
|
|
{
|
|
folderItem->Remove();
|
|
}
|
|
}
|
|
}
|
|
|
|
void CFolderTreeCtrl::Edit(const QString& path)
|
|
{
|
|
CFileUtil::EditTextFile(QtUtil::ToString(path), 0, IFileUtil::FILE_TYPE_SCRIPT);
|
|
}
|
|
|
|
void CFolderTreeCtrl::ShowInExplorer(const QString& path)
|
|
{
|
|
QString absolutePath = QDir::currentPath();
|
|
|
|
CTreeItem* root = m_rootTreeItem.get();
|
|
CTreeItem* item = GetItem(path);
|
|
|
|
if (item != root)
|
|
{
|
|
absolutePath += QStringLiteral("/%1").arg(path);
|
|
}
|
|
|
|
AzQtComponents::ShowFileOnDesktop(absolutePath);
|
|
}
|
|
|
|
QIcon CFolderTreeCtrl::GetItemIcon(int image) const
|
|
{
|
|
return image == eTreeImage_File ? m_fileIcon : m_folderIcon;
|
|
}
|
|
|
|
QList<QStandardItem*> CFolderTreeCtrl::GetSelectedItems() const
|
|
{
|
|
QList<QStandardItem*> items;
|
|
|
|
for (auto index : selectedIndexes())
|
|
{
|
|
QStandardItem* item = GetSourceItemByIndex(index);
|
|
if (item)
|
|
{
|
|
items.append(item);
|
|
}
|
|
}
|
|
|
|
return items;
|
|
}
|
|
|
|
void CFolderTreeCtrl::SetSearchFilter(const QString& searchText)
|
|
{
|
|
if (m_proxyModel)
|
|
{
|
|
m_proxyModel->setFilterFixedString(searchText);
|
|
}
|
|
}
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
// CFolderTreeCtrl::CTreeItem
|
|
//////////////////////////////////////////////////////////////////////////
|
|
CFolderTreeCtrl::CTreeItem::CTreeItem(CFolderTreeCtrl& folderTreeCtrl, const QString& path)
|
|
: QStandardItem(folderTreeCtrl.GetItemIcon(eTreeImage_Folder), folderTreeCtrl.m_rootName)
|
|
, m_folderTreeCtrl(folderTreeCtrl)
|
|
, m_path(path)
|
|
{
|
|
setData(true, IsFolderRole);
|
|
|
|
m_folderTreeCtrl.m_model->invisibleRootItem()->appendRow(this);
|
|
m_folderTreeCtrl.m_pathToTreeItem[ m_path ] = this;
|
|
}
|
|
|
|
CFolderTreeCtrl::CTreeItem::CTreeItem(CFolderTreeCtrl& folderTreeCtrl, CFolderTreeCtrl::CTreeItem* parent,
|
|
const QString& name, const QString& path, const int image)
|
|
: QStandardItem(folderTreeCtrl.GetItemIcon(image), name)
|
|
, m_folderTreeCtrl(folderTreeCtrl)
|
|
, m_path(path)
|
|
{
|
|
parent->appendRow(this);
|
|
setData(image == eTreeImage_Folder, IsFolderRole);
|
|
|
|
m_folderTreeCtrl.m_pathToTreeItem[ m_path ] = this;
|
|
}
|
|
|
|
CFolderTreeCtrl::CTreeItem::~CTreeItem()
|
|
{
|
|
m_folderTreeCtrl.m_pathToTreeItem.erase(m_path);
|
|
}
|
|
|
|
void CFolderTreeCtrl::CTreeItem::Remove()
|
|
{
|
|
// Root can't be deleted this way
|
|
if (auto parentItem = parent())
|
|
{
|
|
int numRows = parentItem->rowCount();
|
|
for (int i = 0; i < numRows; ++i)
|
|
{
|
|
if (parentItem->child(i) == this)
|
|
{
|
|
parentItem->removeRow(i);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
CFolderTreeCtrl::CTreeItem* CFolderTreeCtrl::CTreeItem::AddChild(const QString& name, const QString& path, const int image)
|
|
{
|
|
CTreeItem* newItem = new CTreeItem(m_folderTreeCtrl, this, name, path, image);
|
|
return newItem;
|
|
}
|