/*
 *      fm-gtk-utils.c
 *
 *      Copyright 2009 PCMan <pcman@debian>
 *
 *      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; if not, write to the Free Software
 *      Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 *      MA 02110-1301, USA.
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <glib/gi18n-lib.h>
#include <gio/gdesktopappinfo.h>

#include "fm-gtk-utils.h"
#include "fm-file-ops-job.h"
#include "fm-progress-dlg.h"
#include "fm-path-entry.h"
#include "fm-app-chooser-dlg.h"

#include "fm-config.h"

static GtkDialog* 	_fm_get_user_input_dialog	(GtkWindow* parent, const char* title, const char* msg);
static gchar* 		_fm_user_input_dialog_run	(GtkDialog* dlg, GtkEntry *entry);

void fm_show_error(GtkWindow* parent, const char* msg)
{
    GtkWidget* dlg = gtk_message_dialog_new(parent, 0,
                                            GTK_MESSAGE_ERROR,
                                            GTK_BUTTONS_OK, msg);
    gtk_window_set_title((GtkWindow*)dlg, _("Error"));
    gtk_dialog_run((GtkDialog*)dlg);
    gtk_widget_destroy(dlg);
}

gboolean fm_yes_no(GtkWindow* parent, const char* question, gboolean default_yes)
{
    int ret;
    GtkWidget* dlg = gtk_message_dialog_new_with_markup(parent, 0,
                                GTK_MESSAGE_QUESTION, GTK_BUTTONS_YES_NO, question);
    gtk_dialog_set_default_response(GTK_DIALOG(dlg), default_yes ? GTK_RESPONSE_YES : GTK_RESPONSE_NO);
    ret = gtk_dialog_run((GtkDialog*)dlg);
    gtk_widget_destroy(dlg);
    return ret == GTK_RESPONSE_YES;
}

gboolean fm_ok_cancel(GtkWindow* parent, const char* question, gboolean default_ok)
{
    int ret;
    GtkWidget* dlg = gtk_message_dialog_new_with_markup(parent, 0,
                                GTK_MESSAGE_QUESTION, GTK_BUTTONS_OK_CANCEL, question);
    gtk_dialog_set_default_response(GTK_DIALOG(dlg), default_ok ? GTK_RESPONSE_OK : GTK_RESPONSE_CANCEL);
    ret = gtk_dialog_run((GtkDialog*)dlg);
    gtk_widget_destroy(dlg);
    return ret == GTK_RESPONSE_OK;
}

int fm_ask(GtkWindow* parent, const char* question, ...)
{
    int ret;
    va_list args;
    va_start (args, question);
    ret = fm_ask_valist(parent, question, args);
    va_end (args);
    return ret;
}

int fm_askv(GtkWindow* parent, const char* question, const char** options)
{
    int ret;
    guint id = 1;
    GtkWidget* dlg = gtk_message_dialog_new_with_markup(parent, 0,
                                GTK_MESSAGE_QUESTION, 0, question);
    /* FIXME: need to handle defualt button and alternative button
     * order problems. */
    while(*options)
    {
        /* FIXME: handle button image and stock buttons */
        GtkWidget* btn = gtk_dialog_add_button(GTK_DIALOG( dlg ), *options, id);
        ++options;
        ++id;
    }
    ret = gtk_dialog_run((GtkDialog*)dlg);
    if(ret >= 1)
        ret -= 1;
    else
        ret == -1;
    gtk_widget_destroy(dlg);
    return ret;
}

int fm_ask_valist(GtkWindow* parent, const char* question, va_list options)
{
    GArray* opts = g_array_sized_new(TRUE, TRUE, sizeof(char*), 6);
    gint ret;
    const char* opt = va_arg(options, const char*);
    while(opt)
    {
        g_array_append_val(opts, opt);
        opt = va_arg (options, const char *);
    }
    ret = fm_askv(parent, question, opts->data);
    g_array_free(opts, TRUE);
    return ret;
}



gchar* fm_get_user_input(GtkWindow* parent, const char* title, const char* msg, const char* default_text)
{
    GtkDialog* dlg = _fm_get_user_input_dialog( parent, title, msg);
    GtkWidget* entry = gtk_entry_new();
    gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE);

    if(default_text && default_text[0])
        gtk_entry_set_text(GTK_ENTRY( entry ), default_text);

    return _fm_user_input_dialog_run( dlg,  GTK_ENTRY( entry ) );
}

FmPath* fm_get_user_input_path(GtkWindow* parent, const char* title, const char* msg, FmPath* default_path)
{

    GtkDialog* dlg = _fm_get_user_input_dialog( parent, title, msg);
    GtkWidget* entry = gtk_entry_new();
    char *str, *path_str = NULL;
    FmPath* path;

    gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE);

    if(default_path)
    {
        path_str = fm_path_display_name(default_path, FALSE);
        gtk_entry_set_text(GTK_ENTRY( entry ), path_str);
    }

    str = _fm_user_input_dialog_run( dlg,  GTK_ENTRY( entry ) );
    path = fm_path_new(str);

    g_free(path_str);
    g_free(str);
    return path;
}


gchar* fm_get_user_input_rename(GtkWindow* parent, const char* title, const char* msg, const char* default_text)
{
    GtkDialog* dlg = _fm_get_user_input_dialog( parent, title, msg);
    GtkWidget* entry = gtk_entry_new();
    gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE);

    if(default_text && default_text[0])
    {
        gtk_entry_set_text(GTK_ENTRY( entry ), default_text);
        /* only select filename part without extension name. */
        if(default_text[1])
        {
            /* FIXME: handle the special case for *.tar.gz or *.tar.bz2
             * We should exam the file extension with g_content_type_guess, and
             * find out a longest valid extension name.
             * For example, the extension name of foo.tar.gz is .tar.gz, not .gz. */
            const char* dot = g_utf8_strrchr(default_text, -1, '.');
            if(dot)
                gtk_editable_select_region(GTK_EDITABLE(entry), 0, g_utf8_pointer_to_offset(default_text, dot));
            else
                gtk_editable_select_region(GTK_EDITABLE(entry), 0, -1);
            /*
            const char* dot = default_text;
            while( dot = g_utf8_strchr(dot + 1, -1, '.') )
            {
                gboolean uncertain;
                char* type = g_content_type_guess(dot-1, NULL, 0, &uncertain);
                if(!g_content_type_is_unknown(type))
                {
                    g_free(type);
                    gtk_editable_select_region(entry, 0, g_utf8_pointer_to_offset(default_text, dot));
                    break;
                }
                g_free(type);
            }
            */
        }
    }

    return _fm_user_input_dialog_run( dlg,  GTK_ENTRY( entry ) );
}

static GtkDialog* _fm_get_user_input_dialog(GtkWindow* parent, const char* title, const char* msg)
{
    GtkWidget* dlg = gtk_dialog_new_with_buttons(title, parent, GTK_DIALOG_NO_SEPARATOR,
                                GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
                                GTK_STOCK_OK, GTK_RESPONSE_OK, NULL);
    GtkWidget* label = gtk_label_new(msg);
    gtk_misc_set_alignment(GTK_MISC(label), 0.0, 0.5);

    gtk_dialog_set_alternative_button_order(GTK_DIALOG(dlg), GTK_RESPONSE_OK, GTK_RESPONSE_CANCEL, -1);
    gtk_box_set_spacing((GtkBox*)gtk_dialog_get_content_area(GTK_DIALOG(dlg)), 6);
    gtk_box_pack_start((GtkBox*)gtk_dialog_get_content_area(GTK_DIALOG(dlg)), label, FALSE, TRUE, 6);

    gtk_container_set_border_width(GTK_CONTAINER((GtkBox*)gtk_dialog_get_content_area(GTK_DIALOG(dlg))), 12);
    gtk_container_set_border_width(GTK_CONTAINER(dlg), 5);
    gtk_dialog_set_default_response(GTK_DIALOG(dlg), GTK_RESPONSE_OK);
    gtk_window_set_default_size(GTK_WINDOW(dlg), 480, -1);

    return dlg;
}

static gchar* _fm_user_input_dialog_run( GtkDialog* dlg, GtkEntry *entry)
{
    char* str = NULL;
    int sel_start, sel_end;
    gboolean has_sel;

    /* FIXME: this workaround is used to overcome bug of gtk+.
     * gtk+ seems to ignore select region and select all text for entry in dialog. */
    has_sel = gtk_editable_get_selection_bounds(GTK_EDITABLE(entry), &sel_start, &sel_end);
    gtk_box_pack_start(GTK_BOX( GTK_DIALOG(dlg)->vbox ), GTK_WIDGET( entry ), FALSE, TRUE, 6);
    gtk_widget_show_all(GTK_WIDGET(dlg));

    if(has_sel)
        gtk_editable_select_region(GTK_EDITABLE(entry), sel_start, sel_end);

    while(gtk_dialog_run(dlg) == GTK_RESPONSE_OK)
    {
        const char* pstr = gtk_entry_get_text(entry);
        if( pstr && *pstr )
        {
            str = g_strdup(pstr);
            break;
        }
    }
    gtk_widget_destroy(GTK_WIDGET(dlg));
    return str;
}

FmPath* fm_select_folder(GtkWindow* parent)
{
    FmPath* path;
    GtkFileChooser* chooser;
    chooser = (GtkFileChooser*)gtk_file_chooser_dialog_new(_("Please select a folder"),
                                        parent, GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER,
                                        GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
                                        GTK_STOCK_OK, GTK_RESPONSE_OK,
                                        NULL);
    gtk_dialog_set_alternative_button_order((GtkDialog*)chooser,
                                        GTK_RESPONSE_CANCEL,
                                        GTK_RESPONSE_OK, NULL);
    if( gtk_dialog_run((GtkDialog*)chooser) == GTK_RESPONSE_OK )
    {
        char* file = gtk_file_chooser_get_filename(chooser);
        if(!file)
            file = gtk_file_chooser_get_uri(chooser);
        path = fm_path_new(file);
        g_free(file);
    }
    else
        path = NULL;
    gtk_widget_destroy((GtkWidget*)chooser);
    return path;
}

typedef enum
{
    MOUNT_VOLUME,
    MOUNT_GFILE,
    UMOUNT_MOUNT,
    EJECT_MOUNT,
    EJECT_VOLUME
}MountAction;

struct MountData
{
    GMainLoop *loop;
    MountAction action;
    GError* err;
    gboolean ret;
};

static void on_mount_action_finished(GObject* src, GAsyncResult *res, gpointer user_data)
{
    struct MountData* data = user_data;

    switch(data->action)
    {
    case MOUNT_VOLUME:
        data->ret = g_volume_mount_finish(G_VOLUME(src), res, &data->err);
        break;
    case MOUNT_GFILE:
        data->ret = g_file_mount_enclosing_volume_finish(G_FILE(src), res, &data->err);
        break;
    case UMOUNT_MOUNT:
#if GLIB_CHECK_VERSION(2, 22, 0)
        data->ret = g_mount_unmount_with_operation_finish(G_MOUNT(src), res, &data->err);
#else
        data->ret = g_mount_unmount_finish(G_MOUNT(src), res, &data->err);
#endif
        break;
    case EJECT_MOUNT:
#if GLIB_CHECK_VERSION(2, 22, 0)
        data->ret = g_mount_eject_with_operation_finish(G_MOUNT(src), res, &data->err);
#else
        data->ret = g_mount_eject_finish(G_MOUNT(src), res, &data->err);
#endif
        break;
    case EJECT_VOLUME:
#if GLIB_CHECK_VERSION(2, 22, 0)
        data->ret = g_volume_eject_with_operation_finish(G_VOLUME(src), res, &data->err);
#else
        data->ret = g_volume_eject_finish(G_VOLUME(src), res, &data->err);
#endif
        break;
    }
    g_main_loop_quit(data->loop);
}

gboolean fm_do_mount(GtkWindow* parent, GObject* obj, MountAction action, gboolean interactive)
{
    gboolean ret;
    struct MountData* data = g_new0(struct MountData, 1);
    GMountOperation* op = interactive ? gtk_mount_operation_new(parent) : NULL;
    GCancellable* cancellable = g_cancellable_new();

    data->loop = g_main_loop_new (NULL, TRUE);
    data->action = action;

    switch(data->action)
    {
    case MOUNT_VOLUME:
        g_volume_mount(G_VOLUME(obj), 0, op, cancellable, on_mount_action_finished, data);
        break;
    case MOUNT_GFILE:
        g_file_mount_enclosing_volume(G_FILE(obj), 0, op, cancellable, on_mount_action_finished, data);
        break;
    case UMOUNT_MOUNT:
#if GLIB_CHECK_VERSION(2, 22, 0)
        g_mount_unmount_with_operation(G_MOUNT(obj), G_MOUNT_UNMOUNT_NONE, op, cancellable, on_mount_action_finished, data);
#else
        g_mount_unmount(G_MOUNT(obj), G_MOUNT_UNMOUNT_NONE, cancellable, on_mount_action_finished, data);
#endif
        break;
    case EJECT_MOUNT:
#if GLIB_CHECK_VERSION(2, 22, 0)
        g_mount_eject_with_operation(G_MOUNT(obj), G_MOUNT_UNMOUNT_NONE, op, cancellable, on_mount_action_finished, data);
#else
        g_mount_eject(G_MOUNT(obj), G_MOUNT_UNMOUNT_NONE, cancellable, on_mount_action_finished, data);
#endif
        break;
    case EJECT_VOLUME:
#if GLIB_CHECK_VERSION(2, 22, 0)
        g_volume_eject_with_operation(G_VOLUME(obj), G_MOUNT_UNMOUNT_NONE, op, cancellable, on_mount_action_finished, data);
#else
        g_volume_eject(G_VOLUME(obj), G_MOUNT_UNMOUNT_NONE, cancellable, on_mount_action_finished, data);
#endif
        break;
    }

    if (g_main_loop_is_running(data->loop))
    {
        GDK_THREADS_LEAVE();
        g_main_loop_run(data->loop);
        GDK_THREADS_ENTER();
    }
    g_main_loop_unref(data->loop);

    ret = data->ret;
    if(data->err)
    {
        if(interactive)
        {
            if(data->err->domain == G_IO_ERROR)
            {
                if(data->err->code == G_IO_ERROR_FAILED)
                {
                    /* Generate a more human-readable error message instead of using a gvfs one. */

                    /* The original error message is something like:
                     * Error unmounting: umount exited with exit code 1:
                     * helper failed with: umount: only root can unmount
                     * UUID=18cbf00c-e65f-445a-bccc-11964bdea05d from /media/sda4 */

                    /* Why they pass this back to us?
                     * This is not human-readable for the users at all. */

                    if(strstr(data->err->message, "only root can "))
                    {
                        g_debug("%s", data->err->message);
                        g_free(data->err->message);
                        data->err->message = g_strdup(_("Only system administrators have the permission to do this."));
                    }
                }
                else if(data->err->code == G_IO_ERROR_FAILED_HANDLED)
                    interactive = FALSE;
            }
            if(interactive)
                fm_show_error(parent, data->err->message);
        }
        g_error_free(data->err);
    }

    g_free(data);
    g_object_unref(cancellable);
    if(op)
        g_object_unref(op);
    return ret;
}

gboolean fm_mount_path(GtkWindow* parent, FmPath* path, gboolean interactive)
{
    GFile* gf = fm_path_to_gfile(path);
    gboolean ret = fm_do_mount(parent, G_OBJECT(gf), MOUNT_GFILE, interactive);
    g_object_unref(gf);
    return ret;
}

gboolean fm_mount_volume(GtkWindow* parent, GVolume* vol, gboolean interactive)
{
    return fm_do_mount(parent, G_OBJECT(vol), MOUNT_VOLUME, interactive);
}

gboolean fm_unmount_mount(GtkWindow* parent, GMount* mount, gboolean interactive)
{
    return fm_do_mount(parent, G_OBJECT(mount), UMOUNT_MOUNT, interactive);
}

gboolean fm_unmount_volume(GtkWindow* parent, GVolume* vol, gboolean interactive)
{
    GMount* mount = g_volume_get_mount(vol);
    gboolean ret;
    if(!mount)
        return FALSE;
    ret = fm_do_mount(parent, G_OBJECT(vol), UMOUNT_MOUNT, interactive);
    g_object_unref(mount);
    return ret;
}

gboolean fm_eject_mount(GtkWindow* parent, GMount* mount, gboolean interactive)
{
    return fm_do_mount(parent, G_OBJECT(mount), EJECT_MOUNT, interactive);
}

gboolean fm_eject_volume(GtkWindow* parent, GVolume* vol, gboolean interactive)
{
    return fm_do_mount(parent, G_OBJECT(vol), EJECT_VOLUME, interactive);
}


/* File operations */
/* FIXME: only show the progress dialog if the job isn't finished
 * in 1 sec. */

void fm_copy_files(FmPathList* files, FmPath* dest_dir)
{
	FmJob* job = fm_file_ops_job_new(FM_FILE_OP_COPY, files);
	fm_file_ops_job_set_dest(FM_FILE_OPS_JOB(job), dest_dir);
    fm_file_ops_job_run_with_progress(FM_FILE_OPS_JOB(job));
}

void fm_move_files(FmPathList* files, FmPath* dest_dir)
{
	FmJob* job = fm_file_ops_job_new(FM_FILE_OP_MOVE, files);
	fm_file_ops_job_set_dest(FM_FILE_OPS_JOB(job), dest_dir);
    fm_file_ops_job_run_with_progress(FM_FILE_OPS_JOB(job));
}

void fm_trash_files(FmPathList* files)
{
    if(!fm_config->confirm_del || fm_yes_no(NULL, _("Do you want to move the selected files to trash can?"), TRUE))
    {
        FmJob* job = fm_file_ops_job_new(FM_FILE_OP_TRASH, files);
        fm_file_ops_job_run_with_progress(FM_FILE_OPS_JOB(job));
    }
}

void fm_untrash_files(FmPathList* files)
{
    FmJob* job = fm_file_ops_job_new(FM_FILE_OP_UNTRASH, files);
    fm_file_ops_job_run_with_progress(FM_FILE_OPS_JOB(job));
}

static void fm_delete_files_internal(FmPathList* files)
{
    FmJob* job = fm_file_ops_job_new(FM_FILE_OP_DELETE, files);
    fm_file_ops_job_run_with_progress(FM_FILE_OPS_JOB(job));
}

void fm_delete_files(FmPathList* files)
{
    if(!fm_config->confirm_del || fm_yes_no(NULL, _("Do you want to delete the selected files?"), TRUE))
        fm_delete_files_internal(files);
}

void fm_trash_or_delete_files(FmPathList* files)
{
    if( !fm_list_is_empty(files) )
    {
        gboolean all_in_trash = TRUE;
        if(fm_config->use_trash)
        {
            GList* l = fm_list_peek_head_link(files);
            for(;l;l=l->next)
            {
                FmPath* path = FM_PATH(l->data);
                if(!fm_path_is_trash(path))
                    all_in_trash = FALSE;
            }
        }

        /* files already in trash:/// should only be deleted and cannot be trashed again. */
        if(fm_config->use_trash && !all_in_trash)
            fm_trash_files(files);
        else
            fm_delete_files(files);
    }
}

void fm_move_or_copy_files_to(FmPathList* files, gboolean is_move)
{
    FmPath* dest = fm_select_folder(NULL);
    if(dest)
    {
        if(is_move)
            fm_move_files(files, dest);
        else
            fm_copy_files(files, dest);
        fm_path_unref(dest);
    }
}


void fm_rename_file(FmPath* file)
{
    GFile* gf = fm_path_to_gfile(file), *parent, *dest;
    GError* err = NULL;
    gchar* new_name = fm_get_user_input_rename( NULL, _("Rename File"), _("Please enter a new name:"), file->name);
    if( !new_name )
        return;
    parent = g_file_get_parent(gf);
    dest = g_file_get_child(parent, new_name);
    g_object_unref(parent);
    if(!g_file_move(gf, dest,
                G_FILE_COPY_ALL_METADATA|
                G_FILE_COPY_NO_FALLBACK_FOR_MOVE|
                G_FILE_COPY_NOFOLLOW_SYMLINKS,
                NULL, /* make this cancellable later. */
                NULL, NULL, &err))
    {
        fm_show_error(NULL, err->message);
        g_error_free(err);
    }
    g_object_unref(dest);
    g_object_unref(gf);
}

void fm_empty_trash()
{
    if(fm_yes_no(NULL, _("Are you sure you want to empty the trash can?"), TRUE))
    {
        FmPathList* paths = fm_path_list_new();
        fm_list_push_tail(paths, fm_path_get_trash());
        fm_delete_files_internal(paths);
        fm_list_unref(paths);
    }
}

static GAppInfo* choose_app(GList* file_infos, FmMimeType* mime_type, gpointer user_data, GError** err)
{
    gpointer* data = (gpointer*)user_data;
    GtkWindow* parent = (GtkWindow*)data[0];
    return fm_choose_app_for_mime_type(parent, mime_type, mime_type != NULL);
}

static gboolean on_launch_error(GAppLaunchContext* ctx, GError* err, gpointer user_data)
{
    gpointer* data = (gpointer*)user_data;
    GtkWindow* parent = (GtkWindow*)data[0];
    fm_show_error(parent, err->message);
    return TRUE;
}

static gboolean on_open_folder(GAppLaunchContext* ctx, GList* folder_infos, gpointer user_data, GError** err)
{
    gpointer* data = (gpointer*)user_data;
    FmLaunchFolderFunc func = (FmLaunchFolderFunc)data[1];
    return func(ctx, folder_infos, data[2], err);
}

gboolean fm_launch_files_simple(GtkWindow* parent, GAppLaunchContext* ctx, GList* file_infos, FmLaunchFolderFunc func, gpointer user_data)
{
    FmFileLauncher launcher = {
        choose_app,
        on_open_folder,
        on_launch_error
    };
    gpointer data[] = {parent, func, user_data};
    GAppLaunchContext* _ctx = NULL;
    gboolean ret;
    if(ctx == NULL)
    {
        _ctx = ctx = gdk_app_launch_context_new();
        gdk_app_launch_context_set_screen(GDK_APP_LAUNCH_CONTEXT(ctx), parent ? gtk_widget_get_screen(GTK_WIDGET(parent)) : gdk_screen_get_default());
        gdk_app_launch_context_set_timestamp(GDK_APP_LAUNCH_CONTEXT(ctx), gtk_get_current_event_time());
        /* FIXME: how to handle gdk_app_launch_context_set_icon? */
    }
    ret = fm_launch_files(ctx, file_infos, &launcher, data);
    if(_ctx)
        g_object_unref(_ctx);
    return ret;
}

gboolean fm_launch_paths_simple(GtkWindow* parent, GAppLaunchContext* ctx, GList* paths, FmLaunchFolderFunc func, gpointer user_data)
{
    FmFileLauncher launcher = {
        choose_app,
        on_open_folder,
        on_launch_error
    };
    gpointer data[] = {parent, func, user_data};
    GAppLaunchContext* _ctx = NULL;
    gboolean ret;
    if(ctx == NULL)
    {
        _ctx = ctx = gdk_app_launch_context_new();
        gdk_app_launch_context_set_screen(GDK_APP_LAUNCH_CONTEXT(ctx), parent ? gtk_widget_get_screen(GTK_WIDGET(parent)) : gdk_screen_get_default());
        gdk_app_launch_context_set_timestamp(GDK_APP_LAUNCH_CONTEXT(ctx), gtk_get_current_event_time());
        /* FIXME: how to handle gdk_app_launch_context_set_icon? */
    }
    ret = fm_launch_paths(ctx, paths, &launcher, data);
    if(_ctx)
        g_object_unref(_ctx);
    return ret;
}

gboolean fm_launch_file_simple(GtkWindow* parent, GAppLaunchContext* ctx, FmFileInfo* file_info, FmLaunchFolderFunc func, gpointer user_data)
{
    gboolean ret;
    GList* files = g_list_prepend(NULL, file_info);
    ret = fm_launch_files_simple(parent, ctx, files, func, user_data);
    g_list_free(files);
    return ret;
}

gboolean fm_launch_path_simple(GtkWindow* parent, GAppLaunchContext* ctx, FmPath* path, FmLaunchFolderFunc func, gpointer user_data)
{
    gboolean ret;
    GList* files = g_list_prepend(NULL, path);
    ret = fm_launch_paths_simple(parent, ctx, files, func, user_data);
    g_list_free(files);
    return ret;
}
