/* * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or * its licensors. * * For complete copyright and license terms please see the LICENSE at the root of this * distribution (the "License"). All use of this software is governed by the License, * or, if provided, by the license below or the license accompanying this file. Do not * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * */ // Original file Copyright Crytek GMBH or its affiliates, used under license. #include #include "FolderTreeCtrl.h" #include #include #include #if defined (Q_OS_WIN) #include #endif #include #include #include #include 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(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 CFolderTreeCtrl::GetSelectedItems() const { QList 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; }