FileBrowser: "Open containing folder" automatically selects file in the OS's file manager (#7700)

Introduces a new class FileRevealer to manage file selection across different platforms (Windows, macOS, Linux, and other *nix operating systems with xdg). Includes methods to open and select files or directories using the default file manager on the respective platform.

---------

Co-authored-by: Tres Finocchiaro <tres.finocchiaro@gmail.com>
Co-authored-by: Hyunjin Song <tteu.ingog@gmail.com>
Co-authored-by: Sotonye Atemie <sakertooth@gmail.com>
Co-authored-by: Dalton Messmer <messmer.dalton@gmail.com>
This commit is contained in:
Andrew Wiltshire
2025-03-02 00:38:51 +00:00
committed by GitHub
parent 050df381b0
commit 9159533814
5 changed files with 300 additions and 45 deletions

View File

@@ -195,8 +195,6 @@ private slots:
bool openInNewSampleTrack( lmms::gui::FileItem* item );
void sendToActiveInstrumentTrack( lmms::gui::FileItem* item );
void updateDirectory( QTreeWidgetItem * item );
void openContainingFolder( lmms::gui::FileItem* item );
} ;

77
include/FileRevealer.h Normal file
View File

@@ -0,0 +1,77 @@
/*
* FileRevealer.h - include file for FileRevealer
*
* Copyright (c) 2025 Andrew Wiltshire <aw1lt / at/ proton/ dot/me >
*
* This file is part of LMMS - https://lmms.io
*
* 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 2 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 (see COPYING); if not, write to the
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301 USA.
*
*/
#ifndef LMMS_FILE_REVEALER_H
#define LMMS_FILE_REVEALER_H
#include <QFileInfo>
namespace lmms {
/**
* @class FileRevealer
* @brief A utility class for revealing files and directories in the system's file manager.
*/
class FileRevealer
{
public:
/**
* @brief Retrieves the default file manager for the current platform.
* @return The name or command of the default file manager.
*/
static const QString& getDefaultFileManager();
/**
* @brief Opens the directory containing the specified file or folder in the file manager.
* @param item The QFileInfo object representing the file or folder.
*/
static void openDir(const QFileInfo item);
/**
* @brief Checks whether the file manager supports selecting a specific file.
* @return True if selection is supported, otherwise false.
*/
static const QStringList& getSelectCommand();
/**
* @brief Opens the file manager and selects the specified file if supported.
* @param item The QFileInfo object representing the file to reveal.
*/
static void reveal(const QFileInfo item);
private:
static bool s_canSelect;
protected:
/**
* @brief Determines if the given command supports the argument
* @param command The name of the file manager to check.
* @param arg The command line argument to parse for
* @return True if the file command the argument, otherwise false.
*/
static bool supportsArg(const QString& command, const QString& arg);
};
} // namespace lmms
#endif // LMMS_FILE_REVEALER_H

View File

@@ -14,6 +14,7 @@ SET(LMMS_SRCS
gui/EffectView.cpp
gui/embed.cpp
gui/FileBrowser.cpp
gui/FileRevealer.cpp
gui/GuiApplication.cpp
gui/LadspaControlView.cpp
gui/LfoControllerDialog.cpp

View File

@@ -26,15 +26,14 @@
#include "FileBrowser.h"
#include <QApplication>
#include <QDesktopServices>
#include <QDirIterator>
#include <QHBoxLayout>
#include <QKeyEvent>
#include <QLineEdit>
#include <QMdiArea>
#include <QMdiSubWindow>
#include <QMenu>
#include <QMessageBox>
#include <QProcess>
#include <QPushButton>
#include <QShortcut>
#include <QStringList>
@@ -45,7 +44,7 @@
#include "ConfigManager.h"
#include "DataFile.h"
#include "Engine.h"
#include "FileBrowser.h"
#include "FileRevealer.h"
#include "FileSearch.h"
#include "GuiApplication.h"
#include "ImportFilter.h"
@@ -624,28 +623,35 @@ void FileBrowserTreeWidget::focusOutEvent(QFocusEvent* fe)
QTreeWidget::focusOutEvent(fe);
}
void FileBrowserTreeWidget::contextMenuEvent(QContextMenuEvent * e )
void FileBrowserTreeWidget::contextMenuEvent(QContextMenuEvent* e)
{
auto file = dynamic_cast<FileItem*>(itemAt(e->pos()));
if( file != nullptr && file->isTrack() )
#ifdef LMMS_BUILD_APPLE
QString fileManager = tr("Finder");
#elif defined(LMMS_BUILD_WIN32)
QString fileManager = tr("Explorer");
#else
QString fileManager = tr("file manager");
#endif
QTreeWidgetItem* item = itemAt(e->pos());
if (item == nullptr) { return; } // program hangs when right-clicking on empty space otherwise
QMenu contextMenu(this);
switch (item->type())
{
QMenu contextMenu( this );
case TypeFileItem: {
auto file = dynamic_cast<FileItem*>(item);
contextMenu.addAction(
tr( "Send to active instrument-track" ),
[=, this]{ sendToActiveInstrumentTrack(file); }
);
if (file->isTrack())
{
contextMenu.addAction(
tr("Send to active instrument-track"), [=, this] { sendToActiveInstrumentTrack(file); });
contextMenu.addSeparator();
}
contextMenu.addSeparator();
contextMenu.addAction(
QIcon(embed::getIconPixmap("folder")),
tr("Open containing folder"),
[=, this]{ openContainingFolder(file); }
);
contextMenu.addAction(QIcon(embed::getIconPixmap("folder")), tr("Show in %1").arg(fileManager),
[=] { FileRevealer::reveal(file->fullName()); });
auto songEditorHeader = new QAction(tr("Song Editor"), nullptr);
songEditorHeader->setDisabled(true);
@@ -656,15 +662,21 @@ void FileBrowserTreeWidget::contextMenuEvent(QContextMenuEvent * e )
patternEditorHeader->setDisabled(true);
contextMenu.addAction(patternEditorHeader);
contextMenu.addActions( getContextActions(file, false) );
// We should only show the menu if it contains items
if (!contextMenu.isEmpty()) { contextMenu.exec( e->globalPos() ); }
break;
}
case TypeDirectoryItem: {
auto dir = dynamic_cast<Directory*>(item);
contextMenu.addAction(QIcon(embed::getIconPixmap("folder")), tr("Open in %1").arg(fileManager), [=] {
FileRevealer::openDir(dir->fullName());
});
break;
}
}
// Only show the menu if it contains items
if (!contextMenu.isEmpty()) { contextMenu.exec(e->globalPos()); }
}
QList<QAction*> FileBrowserTreeWidget::getContextActions(FileItem* file, bool songEditor)
{
QList<QAction*> result = QList<QAction*>();
@@ -991,22 +1003,6 @@ bool FileBrowserTreeWidget::openInNewSampleTrack(FileItem* item)
void FileBrowserTreeWidget::openContainingFolder(FileItem* item)
{
// Delegate to QDesktopServices::openUrl with the directory of the selected file. Please note that
// this will only open the directory but not select the file as this is much more complicated due
// to different implementations that are needed for different platforms (Linux/Windows/MacOS).
// Using QDesktopServices::openUrl seems to be the most simple cross platform way which uses
// functionality that's already available in Qt.
QFileInfo fileInfo(item->fullName());
QDesktopServices::openUrl(QUrl::fromLocalFile(fileInfo.dir().path()));
}
void FileBrowserTreeWidget::sendToActiveInstrumentTrack( FileItem* item )
{
// get all windows opened in the workspace

183
src/gui/FileRevealer.cpp Normal file
View File

@@ -0,0 +1,183 @@
/*
* FileRevealer.cpp - Helper file for cross platform file revealing
*
* Copyright (c) 2025 Andrew Wiltshire <aw1lt / at/ proton/ dot/me >
*
* This file is part of LMMS - https://lmms.io
*
* 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 2 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 (see COPYING); if not, write to the
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301 USA.
*
*/
#include "FileRevealer.h"
#include <QDesktopServices>
#include <QDebug>
#include <QDir>
#include <QProcess>
#include <QString>
#include <QRegularExpression>
#include <QUrl>
#include <optional>
#include "lmmsconfig.h"
namespace lmms {
bool FileRevealer::s_canSelect = false;
const QString& FileRevealer::getDefaultFileManager()
{
static std::optional<QString> fileManagerCache;
if (fileManagerCache.has_value()) { return fileManagerCache.value(); }
#if defined(LMMS_BUILD_WIN32)
fileManagerCache = "explorer";
#elif defined(LMMS_BUILD_APPLE)
fileManagerCache = "open";
#else
QString desktopEnv = qgetenv("XDG_CURRENT_DESKTOP").trimmed().toLower();
if (desktopEnv.contains("xfce"))
{
fileManagerCache = "exo-open";
return fileManagerCache.value();
}
QProcess process;
if (desktopEnv.contains("gnome"))
{
process.start("gio", {"mime", "inode/directory"});
}
else
{
process.start("xdg-mime", {"query", "default", "inode/directory"});
}
process.waitForFinished(3000);
QString fileManager = QString::fromUtf8(process.readAllStandardOutput()).toLower().trimmed();
if (fileManager.contains("inode/directory"))
{
// gio format: split on ":" or "\n", take second element
QStringList fileManagers = fileManager.split(QRegularExpression("[:\n]"), Qt::SkipEmptyParts);
if (fileManagers.length() >= 2)
{
fileManagers.removeFirst();
fileManager = fileManagers.first().trimmed();
}
else
{
// Fallback to something sane
fileManager = "xdg-open";
}
}
else
{
// xdg-mime format: split on ";", take the last non-empty element
QStringList fileManagers = fileManager.split(';', Qt::SkipEmptyParts);
if (!fileManagers.isEmpty())
{
// The highest priority file manager is last
fileManager = fileManagers.last();
}
}
if (fileManager.endsWith(".desktop")) { fileManager.chop(8); }
// If the fileManager contains dots (e.g., "org.kde.dolphin"), extract only the last part
fileManager = fileManager.section('.', -1);
fileManagerCache = fileManager;
#endif
qDebug() << "FileRevealer: Default app for inode/directory:" << fileManagerCache.value();
return fileManagerCache.value();
}
void FileRevealer::openDir(const QFileInfo item)
{
QString nativePath = QDir::toNativeSeparators(item.canonicalFilePath());
QProcess::startDetached(getDefaultFileManager(), {nativePath});
}
const QStringList& FileRevealer::getSelectCommand()
{
static std::optional<QStringList> selectCommandCache;
if (selectCommandCache.has_value()) { return selectCommandCache.value(); }
static const std::map<QString, QStringList> argMap = {
{"open", {"-R"}},
{"explorer", {"/select,"}},
{"nemo", {}},
{"thunar", {}},
{"exo-open", {"--launch", "FileManager"}},
};
// Skip calling "--help" for file managers that we know
for (const auto& [fileManager, arg] : argMap)
{
if (fileManager == getDefaultFileManager())
{
s_canSelect = true;
selectCommandCache = arg;
return selectCommandCache.value();
}
}
// Parse "<command> --help" and look for the "--select" for file managers that we don't know
if (supportsArg(getDefaultFileManager(), "--select"))
{
s_canSelect = true;
selectCommandCache = {"--select"};
return selectCommandCache.value();
}
// Fallback to empty list
selectCommandCache = {};
return selectCommandCache.value();
}
void FileRevealer::reveal(const QFileInfo item)
{
// Sets selectCommandCache, canSelect
const QStringList& selectCommand = getSelectCommand();
if (!s_canSelect)
{
QDesktopServices::openUrl(QUrl::fromLocalFile(item.canonicalPath()));
return;
}
QString path = QDir::toNativeSeparators(item.canonicalFilePath());
QStringList params;
if (!selectCommand.isEmpty()) { params << selectCommand; }
params << path;
QProcess::startDetached(getDefaultFileManager(), params);
}
bool FileRevealer::supportsArg(const QString& command, const QString& arg)
{
QProcess process;
process.start(command, {"--help"});
process.waitForFinished(3000);
QString output = process.readAllStandardOutput() + process.readAllStandardError();
return output.contains(arg);
}
} // namespace lmms