You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
libfm-qt-packaging/src/core/basicfilelauncher.cpp

346 lines
12 KiB

#include "basicfilelauncher.h"
#include "fileinfojob.h"
#include "mountoperation.h"
#include <gio/gdesktopappinfo.h>
#include <glib/gi18n.h>
#include <unordered_map>
#include <string>
#include <QObject>
#include <QEventLoop>
#include <QDebug>
namespace Fm {
BasicFileLauncher::BasicFileLauncher():
quickExec_{false} {
}
BasicFileLauncher::~BasicFileLauncher() {
}
bool BasicFileLauncher::launchFiles(const FileInfoList& fileInfos, GAppLaunchContext* ctx) {
std::unordered_map<std::string, FileInfoList> mimeTypeToFiles;
FileInfoList folderInfos;
FilePathList pathsToLaunch;
// classify files according to different mimetypes
for(auto& fileInfo : fileInfos) {
// qDebug("path: %s, target: %s", fileInfo->path().toString().get(), fileInfo->target().c_str());
if(fileInfo->isDir()) {
folderInfos.emplace_back(fileInfo);
}
else if(fileInfo->isMountable()) {
if(fileInfo->target().empty()) {
// the mountable is not yet mounted so we have no target URI.
GErrorPtr err{G_IO_ERROR, G_IO_ERROR_NOT_MOUNTED,
QObject::tr("The path is not mounted.")};
if(!showError(ctx, err, fileInfo->path(), fileInfo)) {
// the user fail to handle the error, skip this file.
continue;
}
// we do not have the target path in the FileInfo object.
// try to launch our path again to query the new file info later so we can get the mounted target URI.
pathsToLaunch.emplace_back(fileInfo->path());
}
else {
// we have the target path, launch it later
pathsToLaunch.emplace_back(FilePath::fromPathStr(fileInfo->target().c_str()));
}
}
else if(fileInfo->isDesktopEntry()) {
// launch the desktop entry
launchDesktopEntry(fileInfo, FilePathList{}, ctx);
}
else if(fileInfo->isExecutableType()) {
// directly execute the file
launchExecutable(fileInfo, ctx);
}
else if(fileInfo->isShortcut()) {
// for shortcuts, launch their targets instead
auto path = handleShortcut(fileInfo, ctx);
if(path.isValid()) {
pathsToLaunch.emplace_back(path);
}
}
else {
auto& mimeType = fileInfo->mimeType();
mimeTypeToFiles[mimeType->name()].emplace_back(fileInfo);
}
}
// open folders
if(!folderInfos.empty()) {
GErrorPtr err;
openFolder(ctx, folderInfos, err);
}
// open files of different mime-types with their default app
for(auto& typeFiles : mimeTypeToFiles) {
auto& mimeType = typeFiles.first;
auto& files = typeFiles.second;
GErrorPtr err;
GAppInfoPtr app{g_app_info_get_default_for_type(mimeType.c_str(), false), false};
if(!app) {
app = chooseApp(files, mimeType.c_str(), err);
}
if(app) {
launchWithApp(app.get(), files.paths(), ctx);
}
}
if(!pathsToLaunch.empty()) {
launchPaths(pathsToLaunch, ctx);
}
return true;
}
bool BasicFileLauncher::launchPaths(FilePathList paths, GAppLaunchContext* ctx) {
// FIXME: blocking with an event loop is not a good design :-(
QEventLoop eventLoop;
auto job = new FileInfoJob{paths};
job->setAutoDelete(false); // do not automatically delete the job since we want its results later.
GObjectPtr<GAppLaunchContext> ctxPtr{ctx};
QObject::connect(job, &FileInfoJob::finished,
[&eventLoop]() {
// exit the event loop when the job is done
eventLoop.exit();
});
// run the job in another thread to not block the UI
job->runAsync();
// blocking until the job is done with a event loop
eventLoop.exec();
// launch the file info
launchFiles(job->files(), ctx);
delete job;
return false;
}
GAppInfoPtr BasicFileLauncher::chooseApp(const FileInfoList& /* fileInfos */, const char* /*mimeType*/, GErrorPtr& /* err */) {
return GAppInfoPtr{};
}
bool BasicFileLauncher::openFolder(GAppLaunchContext* ctx, const FileInfoList& folderInfos, GErrorPtr& err) {
auto app = chooseApp(folderInfos, "inode/directory", err);
if(app) {
launchWithApp(app.get(), folderInfos.paths(), ctx);
}
else {
showError(ctx, err);
}
return false;
}
BasicFileLauncher::ExecAction BasicFileLauncher::askExecFile(const FileInfoPtr & /* file */) {
return ExecAction::DIRECT_EXEC;
}
bool BasicFileLauncher::showError(GAppLaunchContext* /* ctx */, GErrorPtr& /* err */, const FilePath& /* path */, const FileInfoPtr& /* info */) {
return false;
}
int BasicFileLauncher::ask(const char* /* msg */, char* const* /* btn_labels */, int default_btn) {
return default_btn;
}
bool BasicFileLauncher::launchWithApp(GAppInfo* app, const FilePathList& paths, GAppLaunchContext* ctx) {
GList* uris = nullptr;
for(auto& path : paths) {
auto uri = path.uri();
uris = g_list_prepend(uris, uri.release());
}
GErrorPtr err;
bool ret = bool(g_app_info_launch_uris(app, uris, ctx, &err));
g_list_foreach(uris, reinterpret_cast<GFunc>(g_free), nullptr);
g_list_free(uris);
if(!ret) {
// FIXME: show error for all files
showError(ctx, err, paths[0]);
}
return ret;
}
bool BasicFileLauncher::launchDesktopEntry(const FileInfoPtr &fileInfo, const FilePathList &paths, GAppLaunchContext* ctx) {
/* treat desktop entries as executables */
auto target = fileInfo->target();
CStrPtr filename;
const char* desktopEntryName = nullptr;
FilePathList shortcutTargetPaths;
if(fileInfo->isExecutableType()) {
auto act = quickExec_ ? ExecAction::DIRECT_EXEC : askExecFile(fileInfo);
switch(act) {
case ExecAction::EXEC_IN_TERMINAL:
case ExecAction::DIRECT_EXEC: {
if(fileInfo->isShortcut()) {
auto path = handleShortcut(fileInfo, ctx);
if(path.isValid()) {
shortcutTargetPaths.emplace_back(path);
}
}
else {
if(target.empty()) {
filename = fileInfo->path().localPath();
}
desktopEntryName = !target.empty() ? target.c_str() : filename.get();
}
break;
}
case ExecAction::OPEN_WITH_DEFAULT_APP:
return launchWithDefaultApp(fileInfo, ctx);
case ExecAction::CANCEL:
return false;
default:
return false;
}
}
/* make exception for desktop entries under menu */
else if(fileInfo->isNative() /* an exception */ ||
fileInfo->path().hasUriScheme("menu")) {
if(target.empty()) {
filename = fileInfo->path().localPath();
}
desktopEntryName = !target.empty() ? target.c_str() : filename.get();
}
if(desktopEntryName) {
return launchDesktopEntry(desktopEntryName, paths, ctx);
}
if(!shortcutTargetPaths.empty()) {
launchPaths(shortcutTargetPaths, ctx);
}
return false;
}
bool BasicFileLauncher::launchDesktopEntry(const char *desktopEntryName, const FilePathList &paths, GAppLaunchContext *ctx) {
bool ret = false;
GAppInfo* app;
/* Let GDesktopAppInfo try first. */
if(g_path_is_absolute(desktopEntryName)) {
app = G_APP_INFO(g_desktop_app_info_new_from_filename(desktopEntryName));
}
else {
app = G_APP_INFO(g_desktop_app_info_new(desktopEntryName));
}
/* we handle Type=Link in FmFileInfo so if GIO failed then
it cannot be launched in fact */
if(app) {
return launchWithApp(app, paths, ctx);
}
else {
QString msg = QObject::tr("Invalid desktop entry file: '%1'").arg(desktopEntryName);
GErrorPtr err{G_IO_ERROR, G_IO_ERROR_FAILED, msg};
showError(ctx, err);
}
return ret;
}
FilePath BasicFileLauncher::handleShortcut(const FileInfoPtr& fileInfo, GAppLaunchContext* ctx) {
auto target = fileInfo->target();
auto scheme = CStrPtr{g_uri_parse_scheme(target.c_str())};
if(scheme) {
// collect the uri schemes we support
if(strcmp(scheme.get(), "file") == 0
|| strcmp(scheme.get(), "trash") == 0
|| strcmp(scheme.get(), "network") == 0
|| strcmp(scheme.get(), "computer") == 0) {
return FilePath::fromUri(fileInfo->target().c_str());
}
else {
// ask gio to launch the default handler for the uri scheme
GAppInfoPtr app{g_app_info_get_default_for_uri_scheme(scheme.get()), false};
FilePathList uris{FilePath::fromUri(fileInfo->target().c_str())};
launchWithApp(app.get(), uris, ctx);
}
}
else {
// see it as a local path
return FilePath::fromLocalPath(fileInfo->target().c_str());
}
return FilePath();
}
bool BasicFileLauncher::launchExecutable(const FileInfoPtr &fileInfo, GAppLaunchContext* ctx) {
/* if it's an executable file, directly execute it. */
auto filename = fileInfo->path().localPath();
/* FIXME: we need to use eaccess/euidaccess here. */
if(g_file_test(filename.get(), G_FILE_TEST_IS_EXECUTABLE)) {
auto act = quickExec_ ? ExecAction::DIRECT_EXEC : askExecFile(fileInfo);
int flags = G_APP_INFO_CREATE_NONE;
switch(act) {
case ExecAction::EXEC_IN_TERMINAL:
flags |= G_APP_INFO_CREATE_NEEDS_TERMINAL;
/* Falls through. */
case ExecAction::DIRECT_EXEC: {
/* filename may contain spaces. Fix #3143296 */
CStrPtr quoted{g_shell_quote(filename.get())};
// FIXME: remove libfm dependency
GAppInfoPtr app{fm_app_info_create_from_commandline(quoted.get(), nullptr, GAppInfoCreateFlags(flags), nullptr)};
if(app) {
CStrPtr run_path{g_path_get_dirname(filename.get())};
CStrPtr cwd;
/* bug #3589641: scripts are ran from $HOME.
since GIO launcher is kinda ugly - it has
no means to set running directory so we
do workaround - change directory to it */
if(run_path && strcmp(run_path.get(), ".")) {
cwd = CStrPtr{g_get_current_dir()};
if(chdir(run_path.get()) != 0) {
cwd.reset();
// show errors
QString msg = QObject::tr("Cannot set working directory to '%1': %2").arg(run_path.get()).arg(g_strerror(errno));
GErrorPtr err{G_IO_ERROR, g_io_error_from_errno(errno), msg};
showError(ctx, err);
}
}
// FIXME: remove libfm dependency
GErrorPtr err;
if(!fm_app_info_launch(app.get(), nullptr, ctx, &err)) {
showError(ctx, err);
}
if(cwd) { /* return back */
if(chdir(cwd.get()) != 0) {
g_warning("fm_launch_files(): chdir() failed");
}
}
return true;
}
break;
}
case ExecAction::OPEN_WITH_DEFAULT_APP:
return launchWithDefaultApp(fileInfo, ctx);
case ExecAction::CANCEL:
default:
break;
}
}
return false;
}
bool BasicFileLauncher::launchWithDefaultApp(const FileInfoPtr &fileInfo, GAppLaunchContext* ctx) {
FileInfoList files;
files.emplace_back(fileInfo);
GErrorPtr err;
GAppInfoPtr app{g_app_info_get_default_for_type(fileInfo->mimeType()->name(), false), false};
if(app) {
return launchWithApp(app.get(), files.paths(), ctx);
}
else {
showError(ctx, err, fileInfo->path());
}
return false;
}
} // namespace Fm