diff --git a/include/FileBrowser.h b/include/FileBrowser.h index 9193da5e4..6c10ee763 100644 --- a/include/FileBrowser.h +++ b/include/FileBrowser.h @@ -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 ); - } ; diff --git a/include/FileRevealer.h b/include/FileRevealer.h new file mode 100644 index 000000000..feb6d1223 --- /dev/null +++ b/include/FileRevealer.h @@ -0,0 +1,77 @@ +/* + * FileRevealer.h - include file for FileRevealer + * + * Copyright (c) 2025 Andrew Wiltshire + * + * 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 + +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 diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 51f463832..f5e01a11c 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -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 diff --git a/src/gui/FileBrowser.cpp b/src/gui/FileBrowser.cpp index 8985a0475..5e8c84e33 100644 --- a/src/gui/FileBrowser.cpp +++ b/src/gui/FileBrowser.cpp @@ -26,15 +26,14 @@ #include "FileBrowser.h" #include -#include #include #include -#include #include #include #include #include #include +#include #include #include #include @@ -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(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(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(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 FileBrowserTreeWidget::getContextActions(FileItem* file, bool songEditor) { QList result = QList(); @@ -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 diff --git a/src/gui/FileRevealer.cpp b/src/gui/FileRevealer.cpp new file mode 100644 index 000000000..e93cc7aed --- /dev/null +++ b/src/gui/FileRevealer.cpp @@ -0,0 +1,183 @@ +/* + * FileRevealer.cpp - Helper file for cross platform file revealing + * + * Copyright (c) 2025 Andrew Wiltshire + * + * 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 +#include +#include +#include +#include +#include +#include +#include + +#include "lmmsconfig.h" + +namespace lmms { +bool FileRevealer::s_canSelect = false; + +const QString& FileRevealer::getDefaultFileManager() +{ + static std::optional 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 selectCommandCache; + + if (selectCommandCache.has_value()) { return selectCommandCache.value(); } + + static const std::map 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 " --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