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:
@@ -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
77
include/FileRevealer.h
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
183
src/gui/FileRevealer.cpp
Normal 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
|
||||
Reference in New Issue
Block a user