Loading files
/*
* LibrePCB - Professional EDA for everyone!
* Copyright (C) 2013 LibrePCB Developers, see AUTHORS.md for contributors.
* https://librepcb.org/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*******************************************************************************
* Includes
******************************************************************************/
#include "filesystemmodel.h"
#include "../utils/slinthelpers.h"
#include "quickaccessmodel.h"
#include <librepcb/core/exceptions.h>
#include <librepcb/core/fileio/fileutils.h>
#include <librepcb/core/project/project.h>
#include <librepcb/core/serialization/sexpression.h>
#include <librepcb/core/workspace/workspace.h>
#include <QtCore>
#include <QtWidgets>
/*******************************************************************************
* Namespace
******************************************************************************/
namespace librepcb {
namespace editor {
/*******************************************************************************
* Constructors / Destructor
******************************************************************************/
FileSystemModel::FileSystemModel(const Workspace& ws, const FilePath& root,
const QString& settingsPrefix,
QuickAccessModel* quickAccessModel,
QObject* parent) noexcept
: QObject(parent),
mWorkspace(ws),
mRoot(root),
mSettingsPrefix(settingsPrefix),
mQuickAccess(quickAccessModel) {
if (mQuickAccess) {
connect(mQuickAccess, &QuickAccessModel::favoriteProjectChanged, this,
&FileSystemModel::favoriteProjectChanged, Qt::QueuedConnection);
}
connect(&mWatcher, &QFileSystemWatcher::directoryChanged, this,
&FileSystemModel::directoryChanged);
// Restore expanded directories.
QSettings cs;
for (const QString& path : // NOLINT
cs.value(mSettingsPrefix % "/expanded").toStringList()) {
const FilePath fp = mRoot.getPathTo(path);
if (fp.isValid()) {
mExpandedDirs.insert(fp);
}
}
// Clean up non-existing expanded directories if there are many.
if (mExpandedDirs.count() > 100) {
qInfo()
<< "A lot of expanded directories in file system model, cleaning up...";
mExpandedDirs.removeIf(
[](const FilePath& fp) { return !fp.isExistingDir(); });
}
// Load directory.
expandDir(root, 0, 0);
}
FileSystemModel::~FileSystemModel() noexcept {
// Save expanded directories.
QSettings cs;
QStringList paths;
for (const FilePath& fp : std::as_const(mExpandedDirs)) {
paths.append(fp.toRelative(mRoot));
}
std::sort(paths.begin(), paths.end());
cs.setValue(mSettingsPrefix % "/expanded", paths);
}
/*******************************************************************************
* Implementations
******************************************************************************/
std::size_t FileSystemModel::row_count() const {
return mItems.size();
}
std::optional<ui::TreeViewItemData> FileSystemModel::row_data(
std::size_t i) const {
return (i < mItems.size()) ? std::optional(mItems.at(i)) : std::nullopt;
}
void FileSystemModel::set_row_data(std::size_t i,
const ui::TreeViewItemData& data) noexcept {
const FilePath fp(s2q(data.user_data));
if ((i < mItems.size()) && fp.isValid()) {
if ((!mItems.at(i).expanded) && data.expanded) {
expandDir(fp, i + 1, data.level + 1);
} else if (mItems.at(i).expanded && (!data.expanded)) {
collapseDir(fp, i + 1, data.level + 1);
}
if (mItems.at(i).supports_pinning && (mItems.at(i).pinned != data.pinned) &&
mQuickAccess) {
// if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0): Remove lambda.
const bool pinned = data.pinned;
QMetaObject::invokeMethod(
this,
[this, fp, pinned]() {
if (mQuickAccess) mQuickAccess->setFavoriteProject(fp, pinned);
},
Qt::QueuedConnection);
}
if (data.action != ui::TreeViewItemAction::None) {
// Run actions asynchronously to avoid complex nested function calls.
// if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0): Remove lambda.
const auto a = data.action;
QMetaObject::invokeMethod(
this, [this, fp, a]() { trigger(fp, a); }, Qt::QueuedConnection);
}
mItems.at(i) = data;
mItems.at(i).action = ui::TreeViewItemAction::None;
notify_row_changed(i);
}
}
/*******************************************************************************
* Private Methods
******************************************************************************/
void FileSystemModel::expandDir(const FilePath& fp, std::size_t index,
int level) noexcept {
QVector<std::size_t> childrenToBeExpanded;
QDir dir(fp.toStr());
dir.setFilter(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot);
dir.setSorting(QDir::Name | QDir::DirsFirst);
foreach (const QFileInfo& info, dir.entryInfoList()) {
const FilePath itemFp(info.absoluteFilePath());
const bool expand = info.isDir() && mExpandedDirs.contains(itemFp);
if (expand) {
childrenToBeExpanded.append(index);
}
const bool isProjectFile =
(itemFp.getSuffix() == "lpp") || (itemFp.getSuffix() == "lppz");
const bool isPinnable = isProjectFile && mQuickAccess;
const bool isProjectFolder = Project::isProjectDirectory(itemFp);
const bool isProjectFileOrFolder =
isProjectFolder || Project::isFilePathInsideProjectDirectory(itemFp);
QIcon icon;
if (isProjectFile) {
// TODO: For *.lppz, use zipped project icon.
icon = QIcon(":/img/app/librepcb.png");
} else if (isProjectFolder) {
icon = QIcon(":/img/places/project_folder.png");
} else if (itemFp.isExistingDir()) {
icon = QIcon(":/img/places/folder.png");
} else if (itemFp.isExistingFile()) {
icon = QIcon(":/img/places/file.png");
}
mItems.insert(
mItems.begin() + index,
ui::TreeViewItemData{
level, // Level
q2s(icon.pixmap(32)), // Icon
q2s(info.fileName()), // Text
slint::SharedString(), // Hint
q2s(itemFp.toStr()), // User data
isProjectFileOrFolder,
// Is project file or folder
info.isDir(), // Has children
expand, // Expanded
isPinnable, // Supports pinning
isPinnable && mQuickAccess->isFavoriteProject(itemFp), // Pinned
ui::TreeViewItemAction::None, // Action
});
notify_row_added(index, 1);
++index;
}
if (!mWatcher.addPath(fp.toStr())) {
qWarning() << "Failed to watch directory:" << fp.toNative();
}
if (fp != mRoot) {
mExpandedDirs.insert(fp);
}
// Expand children from bottom to top to keep indices valid.
std::reverse(childrenToBeExpanded.begin(), childrenToBeExpanded.end());
for (std::size_t i : childrenToBeExpanded) {
expandDir(FilePath(s2q(mItems.at(i).user_data)), i + 1,
mItems.at(i).level + 1);
}
}
void FileSystemModel::collapseDir(const FilePath& fp, std::size_t index,
int level) noexcept {
for (const QString& dir : mWatcher.directories()) { // NOLINT
const FilePath dirFp(dir);
if ((dirFp == fp) || dirFp.isLocatedInDir(fp)) {
if (!mWatcher.removePath(dir)) {
qWarning() << "Failed to unwatch directory:" << dirFp.toNative();
}
}
}
int childCount = 0;
for (std::size_t i = index;
(i < mItems.size()) && (mItems.at(i).level >= level); ++i) {
++childCount;
}
if (childCount > 0) {
mItems.erase(mItems.begin() + index, mItems.begin() + index + childCount);
notify_row_removed(index, childCount);
}
if (fp != mRoot) {
mExpandedDirs.remove(fp);
}
}
void FileSystemModel::directoryChanged(const QString& dir) noexcept {
const FilePath fp(dir);
qDebug() << "Watched directory changed:" << fp.toNative();
if (fp == mRoot) {
collapseDir(mRoot, 0, 0);
expandDir(mRoot, 0, 0);
} else {
for (std::size_t i = 0; i < mItems.size(); ++i) {
if (FilePath(s2q(mItems.at(i).user_data)) == fp) {
if (mItems.at(i).expanded) {
collapseDir(fp, i + 1, mItems.at(i).level + 1);
expandDir(fp, i + 1, mItems.at(i).level + 1);
}
break;
}
}
}
}
void FileSystemModel::favoriteProjectChanged(const FilePath& fp,
bool favorite) noexcept {
for (std::size_t i = 0; i < mItems.size(); ++i) {
if (mItems.at(i).supports_pinning &&
(FilePath(s2q(mItems.at(i).user_data)) == fp) &&
(mItems.at(i).pinned != favorite)) {
mItems.at(i).pinned = favorite;
notify_row_changed(i);
break;
}
}
}
void FileSystemModel::trigger(const FilePath& fp,
ui::TreeViewItemAction a) noexcept {
if (a == ui::TreeViewItemAction::Open) {
emit openFileTriggered(fp);
} else if (a == ui::TreeViewItemAction::NewFolder) {
QString n = QInputDialog::getText(qApp->activeWindow(), tr("New Folder"),
tr("Name:"));
n = FilePath::cleanFileName(n, FilePath::CleanFileNameOption::Default);
if (!n.isEmpty()) {
QDir(fp.toStr()).mkdir(n);
}
} else if (a == ui::TreeViewItemAction::NewProject) {
emit newProjectTriggered(fp);
} else if (a == ui::TreeViewItemAction::Delete) {
removeFileOrDirectory(fp);
} else {
qWarning() << "Unhandled action in FileSystemModel:" << static_cast<int>(a);
}
}
void FileSystemModel::removeFileOrDirectory(const FilePath& fp) noexcept {
if ((!fp.isValid()) || (!fp.isLocatedInDir(mRoot))) {
return;
}
const QMessageBox::StandardButton btn = QMessageBox::question(
qApp->activeWindow(), tr("Remove"),
tr("Are you really sure to remove following file or directory?\n\n"
"%1\n\nWarning: This cannot be undone!")
.arg(fp.toNative()));
if (btn != QMessageBox::Yes) {
return;
}
try {
if (fp.isExistingDir()) {
FileUtils::removeDirRecursively(fp);
} else {
FileUtils::removeFile(fp);
}
} catch (const Exception& e) {
QMessageBox::critical(qApp->activeWindow(), tr("Error"), e.getMsg());
}
}
/*******************************************************************************
* End of File
******************************************************************************/
} // namespace editor
} // namespace librepcb
f187d7f67This file is pinned to the selected release. Download the original to open it in your design tool.