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 "quickaccessmodel.h"
#include "../utils/slinthelpers.h"
#include <librepcb/core/exceptions.h>
#include <librepcb/core/fileio/fileutils.h>
#include <librepcb/core/serialization/sexpression.h>
#include <librepcb/core/workspace/workspace.h>
#include <QtCore>
/*******************************************************************************
* Namespace
******************************************************************************/
namespace librepcb {
namespace editor {
/*******************************************************************************
* Constructors / Destructor
******************************************************************************/
QuickAccessModel::QuickAccessModel(Workspace& ws, QObject* parent) noexcept
: QObject(parent),
mWorkspace(ws),
mRecentProjectsFp(ws.getDataPath().getPathTo("recent_projects.lp")),
mFavoriteProjectsFp(ws.getDataPath().getPathTo("favorite_projects.lp")),
mIcon(q2s(QIcon(":/img/app/librepcb.svg").pixmap(32))) {
load();
refreshItems();
// Refresh items when any directory of favorite/recent projects has been
// modified. However, delay the update a bit to avoid unnecessary operations
// when there are many file system changes within a short time period.
mWatcherTimer.setSingleShot(true);
connect(&mWatcher, &QFileSystemWatcher::directoryChanged, this,
[this]() { mWatcherTimer.start(500); });
connect(
&mWatcherTimer, &QTimer::timeout, this,
[this]() {
qDebug() << "Quick access directories modified, updating items...";
refreshItems();
},
Qt::QueuedConnection);
}
QuickAccessModel::~QuickAccessModel() noexcept {
}
/*******************************************************************************
* General Methods
******************************************************************************/
void QuickAccessModel::pushRecentProject(const FilePath& fp) noexcept {
if ((mRecentProjects.count() > 0) && (mRecentProjects.first() == fp)) {
// The filename is already on top of the list, so nothing to do here...
return;
}
// First remove it from the list, then add it to the top of the list.
mRecentProjects.removeAll(fp);
mRecentProjects.prepend(fp);
refreshItems();
saveRecentProjects();
}
void QuickAccessModel::discardRecentProject(const FilePath& fp) noexcept {
if (mRecentProjects.removeAll(fp) > 0) {
refreshItems();
saveRecentProjects();
}
}
void QuickAccessModel::setFavoriteProject(const FilePath& fp,
bool favorite) noexcept {
if ((favorite) && (!mFavoriteProjects.contains(fp))) {
mFavoriteProjects.append(fp);
refreshItems();
saveFavoriteProjects();
emit favoriteProjectChanged(fp, favorite);
} else if ((!favorite) && (mFavoriteProjects.removeAll(fp) > 0)) {
refreshItems();
saveFavoriteProjects();
emit favoriteProjectChanged(fp, favorite);
}
}
bool QuickAccessModel::isFavoriteProject(const FilePath& fp) const noexcept {
return mFavoriteProjects.contains(fp);
}
/*******************************************************************************
* Implementations
******************************************************************************/
std::size_t QuickAccessModel::row_count() const {
return mItems.size();
}
std::optional<ui::TreeViewItemData> QuickAccessModel::row_data(
std::size_t i) const {
return (i < mItems.size()) ? std::optional(mItems.at(i)) : std::nullopt;
}
void QuickAccessModel::set_row_data(std::size_t i,
const ui::TreeViewItemData& data) noexcept {
Q_UNUSED(i);
const FilePath fp(s2q(data.user_data));
if (fp.isValid()) {
setFavoriteProject(fp, data.pinned);
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);
}
}
}
/*******************************************************************************
* Private Methods
******************************************************************************/
void QuickAccessModel::load() noexcept {
try {
if (mRecentProjectsFp.isExistingFile()) {
const std::unique_ptr<const SExpression> root = SExpression::parse(
FileUtils::readFile(mRecentProjectsFp), mRecentProjectsFp);
foreach (const SExpression* child, root->getChildren("project")) {
const QString relPath = child->getChild("@0").getValue();
mRecentProjects.append(
FilePath::fromRelative(mWorkspace.getPath(), relPath));
}
}
} catch (const Exception& e) {
qWarning() << "Failed to read recent projects file:" << e.getMsg();
}
try {
if (mFavoriteProjectsFp.isExistingFile()) {
const std::unique_ptr<const SExpression> root = SExpression::parse(
FileUtils::readFile(mFavoriteProjectsFp), mFavoriteProjectsFp);
foreach (const SExpression* child, root->getChildren("project")) {
const QString relPath = child->getChild("@0").getValue();
mFavoriteProjects.append(
FilePath::fromRelative(mWorkspace.getPath(), relPath));
}
}
} catch (const Exception& e) {
qWarning() << "Failed to read favorite projects file:" << e.getMsg();
}
}
void QuickAccessModel::saveRecentProjects() noexcept {
try {
std::unique_ptr<SExpression> root =
SExpression::createList("librepcb_recent_projects");
foreach (const FilePath& filepath, mRecentProjects) {
root->ensureLineBreak();
root->appendChild("project", filepath.toRelative(mWorkspace.getPath()));
}
root->ensureLineBreak();
FileUtils::writeFile(mRecentProjectsFp, root->toByteArray()); // can throw
} catch (const Exception& e) {
qWarning() << "Failed to save recent projects file:" << e.getMsg();
}
}
void QuickAccessModel::saveFavoriteProjects() noexcept {
try {
std::unique_ptr<SExpression> root =
SExpression::createList("librepcb_favorite_projects");
foreach (const FilePath& filepath, mFavoriteProjects) {
root->ensureLineBreak();
root->appendChild("project", filepath.toRelative(mWorkspace.getPath()));
}
root->ensureLineBreak();
FileUtils::writeFile(mFavoriteProjectsFp,
root->toByteArray()); // can throw
} catch (const Exception& e) {
qWarning() << "Failed to save favorite projects file:" << e.getMsg();
}
}
void QuickAccessModel::refreshItems() noexcept {
QSet<FilePath> listedPaths;
std::vector<ui::TreeViewItemData> items;
for (const FilePath& fp : mRecentProjects + mFavoriteProjects) {
const bool favorite = mFavoriteProjects.contains(fp);
if (((listedPaths.count() < 5) || (favorite)) &&
(!listedPaths.contains(fp)) && fp.isExistingFile()) {
items.push_back(ui::TreeViewItemData{
0, // Level
mIcon, // Icon
q2s(fp.getFilename()), // Text
q2s(fp.toNative()), // Hint
q2s(fp.toStr()), // User data
true, // Is project file or folder
false, // Has children
false, // Expanded
true, // Supports pinning
favorite, // Pinned
ui::TreeViewItemAction::None, // Action
});
listedPaths.insert(fp);
}
}
setWatchedProjects(listedPaths);
const std::size_t oldCount = mItems.size();
if (items.size() < oldCount) {
notify_row_removed(items.size(), oldCount - items.size());
}
mItems = items;
if (items.size() > oldCount) {
notify_row_added(oldCount, items.size() - oldCount);
}
for (std::size_t i = 0; i < std::min(items.size(), oldCount); ++i) {
notify_row_changed(i);
}
}
static void addToWatchedDirs(const FilePath& fp, QSet<FilePath>& out,
int limit = 10) noexcept {
if (fp.isValid()) {
out.insert(fp);
if (limit > 0) {
addToWatchedDirs(fp.getParentDir(), out, limit - 1);
}
}
}
void QuickAccessModel::setWatchedProjects(
const QSet<FilePath>& projects) noexcept {
QSet<FilePath> toBeWatched;
for (const FilePath& fp : projects) {
addToWatchedDirs(fp.getParentDir(), toBeWatched);
}
QSet<FilePath> watched;
for (const QString& dir : mWatcher.directories()) {
watched.insert(FilePath(dir));
}
for (const FilePath& fp : watched - toBeWatched) {
mWatcher.removePath(fp.toStr());
}
for (const FilePath& fp : toBeWatched - watched) {
if (!mWatcher.addPath(fp.toStr())) {
qWarning() << "Failed to watch file:" << fp.toNative();
}
}
}
void QuickAccessModel::trigger(const FilePath& fp,
ui::TreeViewItemAction a) noexcept {
if (a == ui::TreeViewItemAction::Open) {
emit openFileTriggered(fp);
} else if (a == ui::TreeViewItemAction::Delete) {
setFavoriteProject(fp, false);
discardRecentProject(fp);
} else {
qWarning() << "Unhandled action in QuickAccessModel:"
<< static_cast<int>(a);
}
}
/*******************************************************************************
* End of File
******************************************************************************/
} // namespace editor
} // namespace librepcb
2955876e1This file is pinned to the selected release. Download the original to open it in your design tool.