Files
oak-editor/app/widget/projectexplorer/projectexplorer.cpp
T
Mike-Solar 43599dba9e Add batch proxy menu to Project Explorer and persist Use Proxy state
The Project Explorer previously only had an old Pre-Cache submenu that
required choosing a sequence. Replace it with the same Proxy menu that
the timeline uses:

- Generate Proxy: batch-generates proxies for all selected footage items
  that have an enabled video stream, using ProxyManager and the current
  proxy settings.
- Use Proxy: checkable toggle that enables/disables proxy playback for the
  selected footage.
- Reveal Proxy / Delete Proxy: consistent with the timeline context menu.

Also make Footage::SaveCustom() write the <proxy> element whenever
proxy_enabled_ is true, even if proxy_path_ is empty, and load it back
in LoadCustom(). This preserves the user's Use Proxy preference across
project saves so it is restored on the next open.

Translation files updated with lupdate; zh_CN and zh_TW translations for
the new Project Explorer strings (and the matching timeline strings) are
filled in.
2026-07-13 10:19:30 +08:00

880 lines
24 KiB
C++

/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
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/>.
***/
#include "projectexplorer.h"
#include <QDebug>
#include <QDesktopServices>
#include <QDir>
#include <QFileDialog>
#include <QMessageBox>
#include <QProcess>
#include <QUrl>
#include <QVBoxLayout>
#include "common/define.h"
#include "core.h"
#include "dialog/footageproperties/footageproperties.h"
#include "dialog/sequence/sequence.h"
#include "projectexplorerundo.h"
#include "codec/proxymanager.h"
#include "task/taskmanager.h"
#include "widget/menu/menu.h"
#include "widget/menu/menushared.h"
#include "node/nodeundo.h"
#include "window/mainwindow/mainwindow.h"
#include "window/mainwindow/mainwindowundo.h"
#include "widget/timelinewidget/timelinewidget.h"
namespace olive
{
namespace {
QVector<Footage *> GetSelectedProxyFootage(const QVector<Node *> &items)
{
QVector<Footage *> footage;
for (Node *node : items) {
Footage *candidate = dynamic_cast<Footage *>(node);
if (!candidate || !candidate->GetFirstEnabledVideoStream().is_valid() ||
footage.contains(candidate)) {
continue;
}
footage.append(candidate);
}
return footage;
}
}
ProjectExplorer::ProjectExplorer(QWidget *parent)
: QWidget(parent)
, model_(this)
{
// Create layout
QVBoxLayout *layout = new QVBoxLayout(this);
layout->setSpacing(0);
layout->setContentsMargins(0, 0, 0, 0);
// Set up navigation bar
nav_bar_ = new ProjectExplorerNavigation(this);
connect(nav_bar_, &ProjectExplorerNavigation::SizeChanged, this,
&ProjectExplorer::SizeChangedSlot);
connect(nav_bar_, &ProjectExplorerNavigation::DirectoryUpClicked, this,
&ProjectExplorer::DirUpSlot);
layout->addWidget(nav_bar_);
// Set up stacked widget
stacked_widget_ = new QStackedWidget(this);
layout->addWidget(stacked_widget_);
// Set up sort filter proxy model
sort_model_.setSourceModel(&model_);
sort_model_.setFilterCaseSensitivity(Qt::CaseInsensitive);
sort_model_.setSortRole(ProjectViewModel::kInnerTextRole);
// Add tree view to stacked widget
tree_view_ = new ProjectExplorerTreeView(stacked_widget_);
tree_view_->setSortingEnabled(true);
tree_view_->sortByColumn(0, Qt::AscendingOrder);
tree_view_->setContextMenuPolicy(Qt::CustomContextMenu);
AddView(tree_view_);
// Add list view to stacked widget
list_view_ = new ProjectExplorerListView(stacked_widget_);
list_view_->setContextMenuPolicy(Qt::CustomContextMenu);
AddView(list_view_);
// Add icon view to stacked widget
icon_view_ = new ProjectExplorerIconView(stacked_widget_);
icon_view_->setContextMenuPolicy(Qt::CustomContextMenu);
AddView(icon_view_);
// Set default view to tree view
set_view_type(ProjectToolbar::TreeView);
// Set default icon size
SizeChangedSlot(kProjectIconSizeDefault);
connect(tree_view_, &ProjectExplorerTreeView::customContextMenuRequested,
this, &ProjectExplorer::ShowContextMenu);
connect(list_view_, &ProjectExplorerListView::customContextMenuRequested,
this, &ProjectExplorer::ShowContextMenu);
connect(icon_view_, &ProjectExplorerIconView::customContextMenuRequested,
this, &ProjectExplorer::ShowContextMenu);
UpdateNavBarText();
}
const ProjectToolbar::ViewType &ProjectExplorer::view_type() const
{
return view_type_;
}
void ProjectExplorer::set_view_type(ProjectToolbar::ViewType type)
{
view_type_ = type;
// Set widget based on view type
switch (view_type_) {
case ProjectToolbar::TreeView:
stacked_widget_->setCurrentWidget(tree_view_);
nav_bar_->setVisible(false);
break;
case ProjectToolbar::ListView:
stacked_widget_->setCurrentWidget(list_view_);
nav_bar_->setVisible(true);
break;
case ProjectToolbar::IconView:
stacked_widget_->setCurrentWidget(icon_view_);
nav_bar_->setVisible(true);
break;
}
}
void ProjectExplorer::Edit(Node *item)
{
CurrentView()->edit(
sort_model_.mapFromSource(model_.CreateIndexFromItem(item)));
}
void ProjectExplorer::AddView(QAbstractItemView *view)
{
view->setModel(&sort_model_);
view->setEditTriggers(QAbstractItemView::SelectedClicked);
connect(view, &QAbstractItemView::doubleClicked, this,
&ProjectExplorer::ItemDoubleClickedSlot);
connect(view->selectionModel(), &QItemSelectionModel::selectionChanged,
this, &ProjectExplorer::ViewSelectionChanged);
connect(view, SIGNAL(DoubleClickedEmptyArea()), this,
SLOT(ViewEmptyAreaDoubleClickedSlot()));
stacked_widget_->addWidget(view);
}
void ProjectExplorer::BrowseToFolder(const QModelIndex &index)
{
// Set appropriate views to this index
icon_view_->setRootIndex(index);
list_view_->setRootIndex(index);
// Set navbar text to folder's name
UpdateNavBarText();
// Set directory up enabled button based on whether we're in root or not
nav_bar_->set_dir_up_enabled(index.isValid());
}
int ProjectExplorer::ConfirmItemDeletion(Node *item)
{
QMessageBox msgbox(this);
msgbox.setWindowTitle(tr("Confirm Item Deletion"));
msgbox.setIcon(QMessageBox::Warning);
QStringList connected_nodes_names;
foreach (const Node::OutputConnection &connected,
item->output_connections()) {
if (!dynamic_cast<Folder *>(connected.second.node())) {
connected_nodes_names.append(
GetHumanReadableNodeName(connected.second.node()));
}
}
msgbox.setText(
tr("The item \"%1\" is currently connected to the following nodes:\n\n"
"%2\n\n"
"Are you sure you wish to delete this footage?")
.arg(GetHumanReadableNodeName(item),
connected_nodes_names.join('\n')));
// Set up buttons
msgbox.addButton(QMessageBox::Yes);
msgbox.addButton(QMessageBox::YesToAll);
msgbox.addButton(QMessageBox::No);
msgbox.addButton(QMessageBox::Cancel);
// Run messagebox
return msgbox.exec();
}
bool ProjectExplorer::DeleteItemsInternal(const QVector<Node *> &selected,
bool &check_if_item_is_in_use,
MultiUndoCommand *command)
{
for (int i = 0; i < selected.size(); i++) {
// Delete sequences first
Node *node = selected.at(i);
bool can_delete_item = true;
if (check_if_item_is_in_use) {
foreach (const Node::OutputConnection &oc,
node->output_connections()) {
Folder *folder_test = dynamic_cast<Folder *>(oc.second.node());
if (!folder_test) {
// This sequence outputs to SOMETHING, confirm the user if they want to delete this
int r = ConfirmItemDeletion(node);
switch (r) {
case QMessageBox::No:
can_delete_item = false;
break;
case QMessageBox::Cancel:
return false;
case QMessageBox::YesToAll:
check_if_item_is_in_use = false;
break;
}
}
}
}
if (can_delete_item) {
Sequence *sequence = dynamic_cast<Sequence *>(node);
if (sequence &&
Core::instance()->main_window()->IsSequenceOpen(sequence)) {
command->add_child(new CloseSequenceCommand(sequence));
}
if (node->folder()) {
command->add_child(
new Folder::RemoveElementCommand(node->folder(), node));
}
command->add_child(
new NodeRemoveWithExclusiveDependenciesAndDisconnect(node));
}
}
return true;
}
QString ProjectExplorer::GetHumanReadableNodeName(Node *node)
{
if (node->GetLabel().isEmpty()) {
return node->Name();
} else {
return tr("%1 (%2)").arg(node->GetLabel(), node->Name());
}
}
void ProjectExplorer::UpdateNavBarText()
{
QString absolute;
Folder *f = static_cast<Folder *>(
sort_model_.mapToSource(list_view_->rootIndex()).internalPointer());
while (f && f != project()->root()) {
absolute.prepend(QStringLiteral("%1 / ").arg(f->GetLabel()));
f = f->folder();
}
absolute.prepend(QStringLiteral("/ "));
nav_bar_->set_text(absolute);
}
QAbstractItemView *ProjectExplorer::CurrentView() const
{
return static_cast<QAbstractItemView *>(stacked_widget_->currentWidget());
}
void ProjectExplorer::ViewEmptyAreaDoubleClickedSlot()
{
emit DoubleClickedItem(nullptr);
}
void ProjectExplorer::ItemDoubleClickedSlot(const QModelIndex &index)
{
// Retrieve source item from index
Node *i =
static_cast<Node *>(sort_model_.mapToSource(index).internalPointer());
// If the item is a folder, browse to it
if (dynamic_cast<Folder *>(i) &&
(view_type() == ProjectToolbar::ListView ||
view_type() == ProjectToolbar::IconView)) {
BrowseToFolder(index);
}
// Emit a signal
emit DoubleClickedItem(i);
}
void ProjectExplorer::SizeChangedSlot(int s)
{
icon_view_->setGridSize(QSize(s, s));
list_view_->setIconSize(QSize(s, s));
}
void ProjectExplorer::DirUpSlot()
{
QModelIndex current_root = icon_view_->rootIndex();
if (current_root.isValid()) {
QModelIndex parent = current_root.parent();
BrowseToFolder(parent);
}
}
void ProjectExplorer::RenameSelectedItem()
{
auto indexes = CurrentView()->selectionModel()->selectedRows();
if (!indexes.empty()) {
CurrentView()->edit(indexes.first());
}
}
void ProjectExplorer::SetSearchFilter(const QString &s)
{
sort_model_.setFilterFixedString(s);
}
void ProjectExplorer::ShowContextMenu()
{
Menu menu;
Menu new_menu;
context_menu_items_ = SelectedItems();
if (context_menu_items_.isEmpty()) {
// Items to show if no items are selected
// "New" menu
new_menu.setTitle(tr("&New"));
MenuShared::instance()->AddItemsForNewMenu(&new_menu);
menu.addMenu(&new_menu);
// "Import" action
QAction *import_action = menu.addAction(tr("&Import..."));
connect(import_action, &QAction::triggered, Core::instance(),
&Core::DialogImportShow);
} else {
// Actions to add when only one item is selected
if (context_menu_items_.size() == 1) {
Node *context_menu_item = context_menu_items_.first();
if (dynamic_cast<Folder *>(context_menu_item)) {
QAction *open_in_new_tab =
menu.addAction(tr("Open in New Tab"));
connect(open_in_new_tab, &QAction::triggered, this,
&ProjectExplorer::OpenContextMenuItemInNewTab);
QAction *open_in_new_window =
menu.addAction(tr("Open in New Window"));
connect(open_in_new_window, &QAction::triggered, this,
&ProjectExplorer::OpenContextMenuItemInNewWindow);
} else if (dynamic_cast<Footage *>(context_menu_item)) {
QString reveal_text;
#if defined(Q_OS_WINDOWS)
reveal_text = tr("Reveal in Explorer");
#elif defined(Q_OS_MAC)
reveal_text = tr("Reveal in Finder");
#else
reveal_text = tr("Reveal in File Manager");
#endif
QAction *reveal_action = menu.addAction(reveal_text);
connect(reveal_action, &QAction::triggered, this,
&ProjectExplorer::RevealSelectedFootage);
QAction *replace_action = menu.addAction(tr("Replace Footage"));
connect(replace_action, &QAction::triggered, this,
&ProjectExplorer::ReplaceSelectedFootage);
}
menu.addSeparator();
}
bool all_items_are_footage = true;
bool all_items_have_video_streams = true;
bool all_items_are_footage_or_sequence = true;
foreach (Node *i, context_menu_items_) {
Footage *footage_cast_test = dynamic_cast<Footage *>(i);
Sequence *sequence_cast_test = dynamic_cast<Sequence *>(i);
if (footage_cast_test &&
!footage_cast_test->HasEnabledVideoStreams()) {
all_items_have_video_streams = false;
}
if (!footage_cast_test) {
all_items_are_footage = false;
}
if (!footage_cast_test && !sequence_cast_test) {
all_items_are_footage_or_sequence = false;
}
}
if (all_items_are_footage && all_items_have_video_streams) {
const QVector<Footage *> proxy_footage =
GetSelectedProxyFootage(context_menu_items_);
Menu *proxy_menu = new Menu(tr("Proxy"), &menu);
menu.addMenu(proxy_menu);
QAction *generate_proxy =
proxy_menu->addAction(tr("Generate Proxy"));
generate_proxy->setEnabled(!proxy_footage.isEmpty());
connect(generate_proxy, &QAction::triggered, this,
&ProjectExplorer::GenerateProxiesForSelectedFootage);
QAction *use_proxy = proxy_menu->addAction(tr("Use Proxy"));
use_proxy->setCheckable(true);
use_proxy->setEnabled(!proxy_footage.isEmpty());
use_proxy->setChecked(
!proxy_footage.isEmpty() &&
std::all_of(proxy_footage.cbegin(), proxy_footage.cend(),
[](const Footage *footage) {
return footage->proxy_enabled();
}));
connect(use_proxy, &QAction::triggered, this,
&ProjectExplorer::SetSelectedFootageProxyEnabled);
QAction *reveal_proxy =
proxy_menu->addAction(tr("Reveal Proxy"));
reveal_proxy->setEnabled(std::any_of(
proxy_footage.cbegin(), proxy_footage.cend(),
[](const Footage *footage) {
return !footage->proxy_path().isEmpty();
}));
connect(reveal_proxy, &QAction::triggered, this,
&ProjectExplorer::RevealProxyForSelectedFootage);
QAction *delete_proxy =
proxy_menu->addAction(tr("Delete Proxy"));
delete_proxy->setEnabled(std::any_of(
proxy_footage.cbegin(), proxy_footage.cend(),
[](const Footage *footage) {
return !footage->proxy_path().isEmpty();
}));
connect(delete_proxy, &QAction::triggered, this,
&ProjectExplorer::DeleteProxiesForSelectedFootage);
}
Q_UNUSED(all_items_are_footage_or_sequence)
if (context_menu_items_.size() == 1) {
menu.addSeparator();
auto rename_action = menu.addAction(tr("Rename"));
connect(rename_action, &QAction::triggered, this,
&ProjectExplorer::RenameSelectedItem);
}
auto delete_action = menu.addAction(tr("Delete"));
connect(delete_action, &QAction::triggered, this,
&ProjectExplorer::DeleteSelected);
if (context_menu_items_.size() == 1) {
menu.addSeparator();
QAction *properties_action = menu.addAction(tr("P&roperties"));
connect(properties_action, &QAction::triggered, this,
&ProjectExplorer::ShowItemPropertiesDialog);
}
}
menu.exec(QCursor::pos());
}
void ProjectExplorer::ShowItemPropertiesDialog()
{
Node *sel = context_menu_items_.first();
// FIXME: Support for multiple items
if (dynamic_cast<Footage *>(sel)) {
FootagePropertiesDialog fpd(this, static_cast<Footage *>(sel));
fpd.exec();
} else if (dynamic_cast<Folder *>(sel)) {
Core::instance()->LabelNodes(context_menu_items_);
} else if (dynamic_cast<Sequence *>(sel)) {
SequenceDialog sd(static_cast<Sequence *>(sel),
SequenceDialog::kExisting, this);
sd.exec();
}
}
void ProjectExplorer::RevealSelectedFootage()
{
Footage *footage = static_cast<Footage *>(context_menu_items_.first());
#if defined(Q_OS_WINDOWS)
// Explorer
QStringList args;
args << "/select," << QDir::toNativeSeparators(footage->filename());
QProcess::startDetached("explorer", args);
#elif defined(Q_OS_MAC)
QStringList args;
args << "-e";
args << "tell application \"Finder\"";
args << "-e";
args << "activate";
args << "-e";
args << "select POSIX file \"" + footage->filename() + "\"";
args << "-e";
args << "end tell";
QProcess::startDetached("osascript", args);
#else
QDesktopServices::openUrl(QUrl::fromLocalFile(
QFileInfo(footage->filename()).dir().absolutePath()));
#endif
}
void ProjectExplorer::ReplaceSelectedFootage()
{
Footage *footage = static_cast<Footage *>(context_menu_items_.first());
QString file = QFileDialog::getOpenFileName(
this, tr("Replace Footage"), QString(),
Core::FootageFileDialogFilter());
if (!file.isEmpty()) {
if (!Core::IsFootageExtensionAllowed(file)) {
QMessageBox::warning(
this, tr("Unsupported media"),
tr("This file type is not allowed by the current media type "
"filter."));
return;
}
auto p = new MultiUndoCommand();
// Change filename parameter
p->add_child(new NodeParamSetStandardValueCommand(
NodeKeyframeTrackReference(
NodeInput(footage, Footage::kFilenameInput)),
file));
if (QFileInfo(footage->filename()).fileName() == footage->GetLabel()) {
// Footage label == filename, change label too
p->add_child(
new NodeRenameCommand(footage, QFileInfo(file).fileName()));
}
Core::instance()->undo_stack()->push(p, tr("Replaced Footage"));
}
}
void ProjectExplorer::OpenContextMenuItemInNewTab()
{
Core::instance()->main_window()->OpenFolder(
static_cast<Folder *>(context_menu_items_.first()), false);
}
void ProjectExplorer::OpenContextMenuItemInNewWindow()
{
Core::instance()->main_window()->OpenFolder(
static_cast<Folder *>(context_menu_items_.first()), true);
}
void ProjectExplorer::GenerateProxiesForSelectedFootage()
{
if (!ProxyManager::instance() || !project()) {
qWarning() << "GenerateProxiesForSelectedFootage: ProxyManager or project unavailable";
return;
}
const QVector<Footage *> footage =
GetSelectedProxyFootage(context_menu_items_);
qDebug() << "GenerateProxiesForSelectedFootage: starting proxy generation for"
<< footage.size() << "footage item(s)";
for (Footage *item : footage) {
const VideoParams video = item->GetFirstEnabledVideoStream();
if (!video.is_valid()) {
qWarning() << "GenerateProxiesForSelectedFootage: skipping item with no valid video stream"
<< item->filename();
continue;
}
ProxyManager::ProxyParams params;
params.width = OLIVE_CONFIG("ProxyWidth").value<int>();
params.height = OLIVE_CONFIG("ProxyHeight").value<int>();
params.crf = OLIVE_CONFIG("ProxyCRF").value<int>();
params.preset = OLIVE_CONFIG("ProxyPreset").toString();
const ProxyManager::Proxy proxy =
ProxyManager::instance()->GetOrStartProxy(
item->project()->cache_path(), item->filename(),
video.stream_index(), params);
qDebug() << "GenerateProxiesForSelectedFootage: proxy state="
<< ProxyManager::ProxyStateToString(proxy.state)
<< "file=" << proxy.filename
<< "cache=" << item->project()->cache_path();
item->SetProxy(proxy.filename, proxy.state, video.stream_index(),
params.version, true);
item->InvalidateAll(Footage::kFilenameInput);
}
}
void ProjectExplorer::SetSelectedFootageProxyEnabled(bool enabled)
{
const QVector<Footage *> footage =
GetSelectedProxyFootage(context_menu_items_);
qDebug() << "ProjectExplorer::SetSelectedFootageProxyEnabled:" << enabled
<< "footage count=" << footage.size();
for (Footage *item : footage) {
if (item->proxy_path().isEmpty()) {
qDebug() << " skipping item with empty proxy path"
<< item->filename();
continue;
}
item->set_proxy_enabled(enabled);
item->InvalidateAll(Footage::kFilenameInput);
}
}
void ProjectExplorer::RevealProxyForSelectedFootage()
{
const QVector<Footage *> footage =
GetSelectedProxyFootage(context_menu_items_);
for (Footage *item : footage) {
if (item->proxy_path().isEmpty()) {
continue;
}
#if defined(Q_OS_WINDOWS)
QStringList args;
args << "/select," << QDir::toNativeSeparators(item->proxy_path());
QProcess::startDetached(QStringLiteral("explorer"), args);
#elif defined(Q_OS_MAC)
QStringList args;
args << "-e";
args << "tell application \"Finder\"";
args << "-e";
args << "activate";
args << "-e";
args << "select POSIX file \"" + item->proxy_path() + "\"";
args << "-e";
args << "end tell";
QProcess::startDetached(QStringLiteral("osascript"), args);
#else
QDesktopServices::openUrl(QUrl::fromLocalFile(
QFileInfo(item->proxy_path()).dir().absolutePath()));
#endif
}
}
void ProjectExplorer::DeleteProxiesForSelectedFootage()
{
const QVector<Footage *> footage =
GetSelectedProxyFootage(context_menu_items_);
for (Footage *item : footage) {
if (item->proxy_path().isEmpty()) {
continue;
}
QFile::remove(item->proxy_path());
item->ClearProxy();
item->InvalidateAll(Footage::kFilenameInput);
}
}
void ProjectExplorer::ViewSelectionChanged()
{
QItemSelectionModel *model = static_cast<QItemSelectionModel *>(sender());
QModelIndexList selection = model->selectedIndexes();
QVector<Node *> nodes;
foreach (const QModelIndex &index, selection) {
Node *sel = static_cast<Node *>(
sort_model_.mapToSource(index).internalPointer());
if (!nodes.contains(sel)) {
nodes.append(sel);
}
}
if (nodes.isEmpty()) {
nodes.append(get_root());
}
emit SelectionChanged(nodes);
}
Project *ProjectExplorer::project() const
{
return model_.project();
}
void ProjectExplorer::set_project(Project *p)
{
model_.set_project(p);
}
Folder *ProjectExplorer::get_root() const
{
QModelIndex root_index = sort_model_.mapToSource(tree_view_->rootIndex());
if (!root_index.isValid()) {
return project()->root();
}
return static_cast<Folder *>(root_index.internalPointer());
}
void ProjectExplorer::set_root(Folder *item)
{
QModelIndex index =
sort_model_.mapFromSource(model_.CreateIndexFromItem(item));
BrowseToFolder(index);
tree_view_->setRootIndex(index);
}
QVector<Node *> ProjectExplorer::SelectedItems() const
{
// Determine which view is active and get its selected indexes
QModelIndexList index_list =
CurrentView()->selectionModel()->selectedRows();
// Convert indexes to item objects
QVector<Node *> selected_items;
for (int i = 0; i < index_list.size(); i++) {
QModelIndex index = sort_model_.mapToSource(index_list.at(i));
Node *item = static_cast<Node *>(index.internalPointer());
selected_items.append(item);
}
return selected_items;
}
Folder *ProjectExplorer::GetSelectedFolder() const
{
if (project() == nullptr) {
return nullptr;
}
Folder *folder = nullptr;
// Get the selected items from the panel
QVector<Node *> selected_items = SelectedItems();
// Heuristic for finding the selected folder:
//
// - If `folder` is nullptr, we set the first folder we find. Either the item itself if it's a folder, or the
// item's parent.
// - Otherwise, if all folders found are the same, we'll use that to import into.
// - If more than one folder is found, we play it safe and import into the root folder
for (int i = 0; i < selected_items.size(); i++) {
Node *sel_item = selected_items.at(i);
// If this item is not a folder, presumably it's parent is
if (!dynamic_cast<Folder *>(sel_item)) {
sel_item = sel_item->folder();
}
if (folder == nullptr) {
// If the folder is nullptr, cache it as this folder
folder = static_cast<Folder *>(sel_item);
} else if (folder != sel_item) {
// If not, we've already cached a folder so we check if it's the same
// If it isn't, we "play it safe" and use the root folder
folder = nullptr;
break;
}
}
// If we didn't pick up a folder from the heuristic above for whatever reason, use root
if (folder == nullptr) {
folder = project()->root();
}
return folder;
}
ProjectViewModel *ProjectExplorer::model()
{
return &model_;
}
void ProjectExplorer::SelectAll()
{
CurrentView()->selectAll();
}
void ProjectExplorer::DeselectAll()
{
CurrentView()->selectionModel()->clearSelection();
}
void ProjectExplorer::DeleteSelected()
{
QVector<Node *> selected = SelectedItems();
if (selected.isEmpty()) {
return;
}
MultiUndoCommand *command = new MultiUndoCommand();
bool check_if_item_is_in_use = true;
if (DeleteItemsInternal(selected, check_if_item_is_in_use, command)) {
Core::instance()->undo_stack()->push(
command, tr("Deleted %1 Item(s)").arg(selected.size()));
} else {
delete command;
}
}
bool ProjectExplorer::SelectItem(Node *n, bool deselect_all_first)
{
if (deselect_all_first) {
DeselectAll();
}
QModelIndex index = model_.CreateIndexFromItem(n);
if (index.isValid()) {
index = sort_model_.mapFromSource(index);
QModelIndex parent = index.parent();
if (view_type() == ProjectToolbar::TreeView) {
// Expand all folders until this index is visible
while (parent.isValid()) {
tree_view_->expand(parent);
parent = parent.parent();
}
} else {
BrowseToFolder(parent);
}
CurrentView()->selectionModel()->select(
index, QItemSelectionModel::Select | QItemSelectionModel::Rows);
return true;
}
return false;
}
}