[Spice-devel] [PATCH spice-gtk 2/2] New file transfer API

Jonathon Jongsma jjongsma at redhat.com
Tue Sep 22 08:34:06 PDT 2015


Feel free to review this, but I just noticed that this patch broke the
gtk2 build. So there will need to be a few changes to fix that.



On Tue, 2015-09-22 at 10:26 -0500, Jonathon Jongsma wrote:
> There were several shortcomings to the existing file transfer API,
> particularly in terms of monitoring ongoing file transfers. The major
> issue is that spice_main_file_copy_async() allows you to pass an array
> of files, but the progress callback does not provide a way to
> identify which file the callback is associated with. This makes it
> nearly impossible for an application to monitor file transfers.
> 
> In addition, the SpiceDisplay widget automatically handles drag-and-drop
> actions on the widget, and initiates file transfers without allowing the
> application to specify a progress callback. So there's no way for an app
> to monitor file transfers that are initiated via drag and drop.
> 
> http://lists.freedesktop.org/archives/spice-devel/2015-September/021931.html
> has a more detailed explanation of the issues.
> 
> This change doesn't break the existing API, but adds some new API that
> will allow an application to monitor file transfer progress, even for
> transfers that are initiated within spice-gtk itself.
> 
> - A new public SpiceFileTransferTask object is added.
> - The SpiceMainChannel object gains a "new-file-transfer" signal that is
>   emitted whenever a new file transfer is initiated. The
>   SpiceFileTransferTask object is passed to the signal handler.
> - The application can retain this object and monitor its 'progress'
>   property to be notified when the progress of the file transfer
>   changes. The SpiceFileTransferTask::finished signal indicates when the
>   given file transfer has completed. The application can also cancel the
>   file transfer by calling the _cancel() method.
> 
> The 'spicy' test application has been updated to use this new API and
> display a simple dialog showing the progress of individual files.
> ---
>  doc/reference/spice-gtk-docs.xml     |   1 +
>  doc/reference/spice-gtk-sections.txt |  21 ++
>  doc/reference/spice-gtk.types        |   2 +
>  src/Makefile.am                      |   2 +
>  src/channel-main.c                   | 579 +++++++++++++++++++++++++++--------
>  src/channel-main.h                   |   1 +
>  src/map-file                         |   5 +
>  src/spice-file-transfer-task.h       |  58 ++++
>  src/spice-glib-sym-file              |   5 +
>  src/spicy.c                          | 153 +++++++++
>  10 files changed, 692 insertions(+), 135 deletions(-)
>  create mode 100644 src/spice-file-transfer-task.h
> 
> diff --git a/doc/reference/spice-gtk-docs.xml b/doc/reference/spice-gtk-docs.xml
> index 5faea74..2d2c1a4 100644
> --- a/doc/reference/spice-gtk-docs.xml
> +++ b/doc/reference/spice-gtk-docs.xml
> @@ -54,6 +54,7 @@
>        <xi:include href="xml/usb-device-manager.xml"/>
>        <xi:include href="xml/spice-util.xml"/>
>        <xi:include href="xml/spice-uri.xml"/>
> +      <xi:include href="xml/file-transfer-task.xml"/>
>      </chapter>
>  
>    </part>
> diff --git a/doc/reference/spice-gtk-sections.txt b/doc/reference/spice-gtk-sections.txt
> index 2f3e09e..32e16a0 100644
> --- a/doc/reference/spice-gtk-sections.txt
> +++ b/doc/reference/spice-gtk-sections.txt
> @@ -481,3 +481,24 @@ spice_webdav_channel_get_type
>  <SUBSECTION Private>
>  SpiceWebdavChannelPrivate
>  </SECTION>
> +
> +<SECTION>
> +<FILE>file-transfer-task</FILE>
> +<TITLE>SpiceFileTransferTask</TITLE>
> +SpiceFileTransferTask
> +SpiceFileTransferTaskClass
> +<SUBSECTION>
> +spice_file_transfer_task_get_progress
> +spice_file_transfer_task_get_filename
> +spice_file_transfer_task_cancel
> +<SUBSECTION Standard>
> +SPICE_FILE_TRANSFER_TASK
> +SPICE_IS_FILE_TRANSFER_TASK
> +SPICE_TYPE_FILE_TRANSFER_TASK
> +spice_file_transfer_task_get_type
> +SPICE_FILE_TRANSFER_TASK_CLASS
> +SPICE_IS_FILE_TRANSFER_TASK_CLASS
> +SPICE_FILE_TRANSFER_TASK_GET_CLASS
> +<SUBSECTION Private>
> +SpiceFileTransferTaskPrivate
> +</SECTION>
> diff --git a/doc/reference/spice-gtk.types b/doc/reference/spice-gtk.types
> index acd616d..e14ae1b 100644
> --- a/doc/reference/spice-gtk.types
> +++ b/doc/reference/spice-gtk.types
> @@ -20,6 +20,7 @@
>  #include "smartcard-manager.h"
>  #include "usb-device-manager.h"
>  #include "usb-device-widget.h"
> +#include "spice-file-transfer-task.h"
>  
>  spice_audio_get_type
>  spice_channel_event_get_type
> @@ -45,3 +46,4 @@ spice_usb_device_manager_get_type
>  spice_usb_device_widget_get_type
>  spice_port_channel_get_type
>  spice_webdav_channel_get_type
> +spice_file_transfer_task_get_type
> diff --git a/src/Makefile.am b/src/Makefile.am
> index cf02198..a58e414 100644
> --- a/src/Makefile.am
> +++ b/src/Makefile.am
> @@ -134,6 +134,7 @@ SPICE_GTK_SOURCES_COMMON =		\
>  	spice-gtk-session-priv.h	\
>  	spice-widget.c			\
>  	spice-widget-priv.h		\
> +	spice-file-transfer-task.h \
>  	vncdisplaykeymap.c		\
>  	vncdisplaykeymap.h		\
>  	spice-grabsequence.c		\
> @@ -315,6 +316,7 @@ libspice_client_glibinclude_HEADERS =	\
>  	channel-webdav.h		\
>  	usb-device-manager.h		\
>  	smartcard-manager.h		\
> +	spice-file-transfer-task.h	\
>  	$(NULL)
>  
>  nodist_libspice_client_glibinclude_HEADERS =	\
> diff --git a/src/channel-main.c b/src/channel-main.c
> index 9712191..216a698 100644
> --- a/src/channel-main.c
> +++ b/src/channel-main.c
> @@ -32,6 +32,7 @@
>  #include "spice-channel-priv.h"
>  #include "spice-session-priv.h"
>  #include "spice-audio-priv.h"
> +#include "spice-file-transfer-task.h"
>  
>  /**
>   * SECTION:channel-main
> @@ -56,8 +57,33 @@
>  
>  typedef struct spice_migrate spice_migrate;
>  
> +/**
> + * SECTION:file-transfer-task
> + * @short_description: Monitoring file transfers
> + * @title: File Transfer Task
> + * @section_id:
> + * @see_also: #SpiceMainChannel
> + * @stability: Stable
> + * @include: spice-file-transfer-task.h
> + *
> + * SpiceFileTransferTask is an object that represents a particular file
> + * transfer between the client and the guest. The properties and signals of the
> + * object can be used to monitor the status and result of the transfer. The
> + * Main Channel's #SpiceMainChannel::new-file-transfer signal will be emitted
> + * whenever a new file transfer task is initiated.
> + *
> + * Since: 0.30
> + */
> +G_DEFINE_TYPE(SpiceFileTransferTask, spice_file_transfer_task, G_TYPE_OBJECT)
> +
> +#define FILE_TRANSFER_TASK_PRIVATE(o) \
> +        (G_TYPE_INSTANCE_GET_PRIVATE((o), SPICE_TYPE_FILE_TRANSFER_TASK, SpiceFileTransferTaskPrivate))
> +
>  #define FILE_XFER_CHUNK_SIZE (VD_AGENT_MAX_DATA_SIZE * 32)
> -typedef struct SpiceFileXferTask {
> +struct _SpiceFileTransferTaskPrivate
> +
> +/* private */
> +{
>      uint32_t                       id;
>      gboolean                       pending;
>      GFile                          *file;
> @@ -69,13 +95,28 @@ typedef struct SpiceFileXferTask {
>      gpointer                       progress_callback_data;
>      GAsyncReadyCallback            callback;
>      gpointer                       user_data;
> -    char                           buffer[FILE_XFER_CHUNK_SIZE];
> +    char                           *buffer;
>      uint64_t                       read_bytes;
>      uint64_t                       file_size;
>      gint64                         start_time;
>      gint64                         last_update;
>      GError                         *error;
> -} SpiceFileXferTask;
> +};
> +
> +enum {
> +    PROP_TASK_ID = 1,
> +    PROP_TASK_CHANNEL,
> +    PROP_TASK_CANCELLABLE,
> +    PROP_TASK_FILE,
> +    PROP_TASK_PROGRESS,
> +};
> +
> +enum {
> +    SIGNAL_FINISHED,
> +    LAST_TASK_SIGNAL
> +};
> +
> +static guint task_signals[LAST_TASK_SIGNAL];
>  
>  typedef enum {
>      DISPLAY_UNDEFINED,
> @@ -169,6 +210,7 @@ enum {
>      SPICE_MAIN_CLIPBOARD_SELECTION_REQUEST,
>      SPICE_MAIN_CLIPBOARD_SELECTION_RELEASE,
>      SPICE_MIGRATION_STARTED,
> +    SPICE_MAIN_NEW_FILE_TRANSFER,
>      SPICE_MAIN_LAST_SIGNAL,
>  };
>  
> @@ -182,8 +224,8 @@ static void migrate_channel_event_cb(SpiceChannel *channel, SpiceChannelEvent ev
>                                       gpointer data);
>  static gboolean main_migrate_handshake_done(gpointer data);
>  static void spice_main_channel_send_migration_handshake(SpiceChannel *channel);
> -static void file_xfer_continue_read(SpiceFileXferTask *task);
> -static void file_xfer_completed(SpiceFileXferTask *task, GError *error);
> +static void file_xfer_continue_read(SpiceFileTransferTask *task);
> +static void spice_file_transfer_task_completed(SpiceFileTransferTask *self, GError *error);
>  static void file_xfer_flushed(SpiceMainChannel *channel, gboolean success);
>  static void spice_main_set_max_clipboard(SpiceMainChannel *self, gint max);
>  static void set_agent_connected(SpiceMainChannel *channel, gboolean connected);
> @@ -246,7 +288,8 @@ static void spice_main_channel_init(SpiceMainChannel *channel)
>  
>      c = channel->priv = SPICE_MAIN_CHANNEL_GET_PRIVATE(channel);
>      c->agent_msg_queue = g_queue_new();
> -    c->file_xfer_tasks = g_hash_table_new(g_direct_hash, g_direct_equal);
> +    c->file_xfer_tasks = g_hash_table_new_full(g_direct_hash, g_direct_equal,
> +                                               NULL, g_object_unref);
>      c->cancellable_volume_info = g_cancellable_new();
>  
>      spice_main_channel_reset_capabilties(SPICE_CHANNEL(channel));
> @@ -410,11 +453,11 @@ static void spice_main_channel_reset_agent(SpiceMainChannel *channel)
>  
>      tasks = g_hash_table_get_values(c->file_xfer_tasks);
>      for (l = tasks; l != NULL; l = l->next) {
> -        SpiceFileXferTask *task = (SpiceFileXferTask *)l->data;
> +        SpiceFileTransferTask *task = (SpiceFileTransferTask *)l->data;
>  
>          error = g_error_new(SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED,
>                              "Agent connection closed");
> -        file_xfer_completed(task, error);
> +        spice_file_transfer_task_completed(task, error);
>      }
>      g_list_free(tasks);
>      file_xfer_flushed(channel, FALSE);
> @@ -827,6 +870,28 @@ static void spice_main_channel_class_init(SpiceMainChannelClass *klass)
>                       1,
>                       G_TYPE_OBJECT);
>  
> +    /**
> +     * SpiceMainChannel::new-file-transfer:
> +     * @main: the #SpiceMainChannel that emitted the signal
> +     * @task: a #SpiceFileTransferTask
> +     *
> +     * This signal is emitted when a new file transfer task has been initiated
> +     * on this channel. Client applications may take a reference on the @task
> +     * object and use it to monitor the status of the file transfer task.
> +     *
> +     * Since: 0.30
> +     **/
> +    signals[SPICE_MAIN_NEW_FILE_TRANSFER] =
> +        g_signal_new("new-file-transfer",
> +                     G_OBJECT_CLASS_TYPE(gobject_class),
> +                     G_SIGNAL_RUN_LAST,
> +                     0,
> +                     NULL, NULL,
> +                     g_cclosure_marshal_VOID__OBJECT,
> +                     G_TYPE_NONE,
> +                     1,
> +                     G_TYPE_OBJECT);
> +
>      g_type_class_add_private(klass, sizeof(SpiceMainChannelPrivate));
>      channel_set_handlers(SPICE_CHANNEL_CLASS(klass));
>  }
> @@ -1690,31 +1755,16 @@ static void main_handle_agent_disconnected(SpiceChannel *channel, SpiceMsgIn *in
>      agent_stopped(SPICE_MAIN_CHANNEL(channel));
>  }
>  
> -static void file_xfer_task_free(SpiceFileXferTask *task)
> -{
> -    SpiceMainChannelPrivate *c;
> -
> -    g_return_if_fail(task != NULL);
> -
> -    c = task->channel->priv;
> -    g_hash_table_remove(c->file_xfer_tasks, GUINT_TO_POINTER(task->id));
> -
> -    g_clear_object(&task->channel);
> -    g_clear_object(&task->file);
> -    g_clear_object(&task->file_stream);
> -    g_free(task);
> -}
> -
>  /* main context */
>  static void file_xfer_close_cb(GObject      *object,
>                                 GAsyncResult *close_res,
>                                 gpointer      user_data)
>  {
>      GSimpleAsyncResult *res;
> -    SpiceFileXferTask *task;
> +    SpiceFileTransferTask *self;
>      GError *error = NULL;
>  
> -    task = user_data;
> +    self = user_data;
>  
>      if (object) {
>          GInputStream *stream = G_INPUT_STREAM(object);
> @@ -1728,23 +1778,23 @@ static void file_xfer_close_cb(GObject      *object,
>  
>      /* Notify to user that files have been transferred or something error
>         happened. */
> -    res = g_simple_async_result_new(G_OBJECT(task->channel),
> -                                    task->callback,
> -                                    task->user_data,
> +    res = g_simple_async_result_new(G_OBJECT(self->priv->channel),
> +                                    self->priv->callback,
> +                                    self->priv->user_data,
>                                      spice_main_file_copy_async);
> -    if (task->error) {
> -        g_simple_async_result_take_error(res, task->error);
> +    if (self->priv->error) {
> +        g_simple_async_result_take_error(res, self->priv->error);
>          g_simple_async_result_set_op_res_gboolean(res, FALSE);
>      } else {
>          g_simple_async_result_set_op_res_gboolean(res, TRUE);
>          if (spice_util_get_debug()) {
>              gint64 now = g_get_monotonic_time();
> -            gchar *basename = g_file_get_basename(task->file);
> -            double seconds = (double) (now - task->start_time) / G_TIME_SPAN_SECOND;
> -            gchar *file_size_str = g_format_size(task->file_size);
> -            gchar *transfer_speed_str = g_format_size(task->file_size / seconds);
> +            gchar *basename = g_file_get_basename(self->priv->file);
> +            double seconds = (double) (now - self->priv->start_time) / G_TIME_SPAN_SECOND;
> +            gchar *file_size_str = g_format_size(self->priv->file_size);
> +            gchar *transfer_speed_str = g_format_size(self->priv->file_size / seconds);
>  
> -            g_warn_if_fail(task->read_bytes == task->file_size);
> +            g_warn_if_fail(self->priv->read_bytes == self->priv->file_size);
>              SPICE_DEBUG("transferred file %s of %s size in %.1f seconds (%s/s)",
>                          basename, file_size_str, seconds, transfer_speed_str);
>  
> @@ -1756,21 +1806,21 @@ static void file_xfer_close_cb(GObject      *object,
>      g_simple_async_result_complete_in_idle(res);
>      g_object_unref(res);
>  
> -    file_xfer_task_free(task);
> +    g_object_unref(self);
>  }
>  
>  static void file_xfer_data_flushed_cb(GObject *source_object,
>                                        GAsyncResult *res,
>                                        gpointer user_data)
>  {
> -    SpiceFileXferTask *task = user_data;
> +    SpiceFileTransferTask *self = user_data;
>      SpiceMainChannel *channel = (SpiceMainChannel *)source_object;
>      GError *error = NULL;
>  
> -    task->pending = FALSE;
> +    self->priv->pending = FALSE;
>      file_xfer_flush_finish(channel, res, &error);
> -    if (error || task->error) {
> -        file_xfer_completed(task, error);
> +    if (error || self->priv->error) {
> +        spice_file_transfer_task_completed(self, error);
>          return;
>      }
>  
> @@ -1778,19 +1828,19 @@ static void file_xfer_data_flushed_cb(GObject *source_object,
>          const GTimeSpan interval = 20 * G_TIME_SPAN_SECOND;
>          gint64 now = g_get_monotonic_time();
>  
> -        if (interval < now - task->last_update) {
> -            gchar *basename = g_file_get_basename(task->file);
> -            task->last_update = now;
> +        if (interval < now - self->priv->last_update) {
> +            gchar *basename = g_file_get_basename(self->priv->file);
> +            self->priv->last_update = now;
>              SPICE_DEBUG("transferred %.2f%% of the file %s",
> -                        100.0 * task->read_bytes / task->file_size, basename);
> +                        100.0 * self->priv->read_bytes / self->priv->file_size, basename);
>              g_free(basename);
>          }
>      }
>  
> -    if (task->progress_callback) {
> +    if (self->priv->progress_callback) {
>          goffset read = 0;
>          goffset total = 0;
> -        SpiceMainChannel *main_channel = task->channel;
> +        SpiceMainChannel *main_channel = self->priv->channel;
>          GHashTableIter iter;
>          gpointer key, value;
>  
> @@ -1799,28 +1849,28 @@ static void file_xfer_data_flushed_cb(GObject *source_object,
>           * current transfers */
>          g_hash_table_iter_init(&iter, main_channel->priv->file_xfer_tasks);
>          while (g_hash_table_iter_next(&iter, &key, &value)) {
> -            SpiceFileXferTask *t = (SpiceFileXferTask *)value;
> -            read += t->read_bytes;
> -            total += t->file_size;
> +            SpiceFileTransferTask *t = (SpiceFileTransferTask *)value;
> +            read += t->priv->read_bytes;
> +            total += t->priv->file_size;
>          }
>  
> -        task->progress_callback(read, total, task->progress_callback_data);
> +        self->priv->progress_callback(read, total, self->priv->progress_callback_data);
>      }
>  
>      /* Read more data */
> -    file_xfer_continue_read(task);
> +    file_xfer_continue_read(self);
>  }
>  
> -static void file_xfer_queue(SpiceFileXferTask *task, int data_size)
> +static void file_xfer_queue(SpiceFileTransferTask *self, int data_size)
>  {
>      VDAgentFileXferDataMessage msg;
> -    SpiceMainChannel *channel = SPICE_MAIN_CHANNEL(task->channel);
> +    SpiceMainChannel *channel = SPICE_MAIN_CHANNEL(self->priv->channel);
>  
> -    msg.id = task->id;
> +    msg.id = self->priv->id;
>      msg.size = data_size;
>      agent_msg_queue_many(channel, VD_AGENT_FILE_XFER_DATA,
>                           &msg, sizeof(msg),
> -                         task->buffer, data_size, NULL);
> +                         self->priv->buffer, data_size, NULL);
>      spice_channel_wakeup(SPICE_CHANNEL(channel), FALSE);
>  }
>  
> @@ -1829,52 +1879,53 @@ static void file_xfer_read_cb(GObject *source_object,
>                                GAsyncResult *res,
>                                gpointer user_data)
>  {
> -    SpiceFileXferTask *task = user_data;
> -    SpiceMainChannel *channel = task->channel;
> +    SpiceFileTransferTask *self = user_data;
> +    SpiceMainChannel *channel = self->priv->channel;
>      gssize count;
>      GError *error = NULL;
>  
> -    task->pending = FALSE;
> -    count = g_input_stream_read_finish(G_INPUT_STREAM(task->file_stream),
> +    self->priv->pending = FALSE;
> +    count = g_input_stream_read_finish(G_INPUT_STREAM(self->priv->file_stream),
>                                         res, &error);
>      /* Check for pending earlier errors */
> -    if (task->error) {
> -        file_xfer_completed(task, error);
> +    if (self->priv->error) {
> +        spice_file_transfer_task_completed(self, error);
>          return;
>      }
>  
> -    if (count > 0 || task->file_size == 0) {
> -        task->read_bytes += count;
> -        file_xfer_queue(task, count);
> +    if (count > 0 || self->priv->file_size == 0) {
> +        self->priv->read_bytes += count;
> +        g_object_notify(G_OBJECT(self), "progress");
> +        file_xfer_queue(self, count);
>          if (count == 0)
>              return;
> -        file_xfer_flush_async(channel, task->cancellable,
> -                              file_xfer_data_flushed_cb, task);
> -        task->pending = TRUE;
> +        file_xfer_flush_async(channel, self->priv->cancellable,
> +                              file_xfer_data_flushed_cb, self);
> +        self->priv->pending = TRUE;
>      } else if (error) {
>          VDAgentFileXferStatusMessage msg = {
> -            .id = task->id,
> +            .id = self->priv->id,
>              .result = VD_AGENT_FILE_XFER_STATUS_ERROR,
>          };
> -        agent_msg_queue_many(task->channel, VD_AGENT_FILE_XFER_STATUS,
> +        agent_msg_queue_many(self->priv->channel, VD_AGENT_FILE_XFER_STATUS,
>                               &msg, sizeof(msg), NULL);
> -        spice_channel_wakeup(SPICE_CHANNEL(task->channel), FALSE);
> -        file_xfer_completed(task, error);
> +        spice_channel_wakeup(SPICE_CHANNEL(self->priv->channel), FALSE);
> +        spice_file_transfer_task_completed(self, error);
>      }
>      /* else EOF, do nothing (wait for VD_AGENT_FILE_XFER_STATUS from agent) */
>  }
>  
>  /* coroutine context */
> -static void file_xfer_continue_read(SpiceFileXferTask *task)
> +static void file_xfer_continue_read(SpiceFileTransferTask *self)
>  {
> -    g_input_stream_read_async(G_INPUT_STREAM(task->file_stream),
> -                              task->buffer,
> +    g_input_stream_read_async(G_INPUT_STREAM(self->priv->file_stream),
> +                              self->priv->buffer,
>                                FILE_XFER_CHUNK_SIZE,
>                                G_PRIORITY_DEFAULT,
> -                              task->cancellable,
> +                              self->priv->cancellable,
>                                file_xfer_read_cb,
> -                              task);
> -    task->pending = TRUE;
> +                              self);
> +    self->priv->pending = TRUE;
>  }
>  
>  /* coroutine context */
> @@ -1882,10 +1933,9 @@ static void file_xfer_handle_status(SpiceMainChannel *channel,
>                                      VDAgentFileXferStatusMessage *msg)
>  {
>      SpiceMainChannelPrivate *c = channel->priv;
> -    SpiceFileXferTask *task;
> +    SpiceFileTransferTask *task;
>      GError *error = NULL;
>  
> -
>      task = g_hash_table_lookup(c->file_xfer_tasks, GUINT_TO_POINTER(msg->id));
>      if (task == NULL) {
>          SPICE_DEBUG("cannot find task %d", msg->id);
> @@ -1896,7 +1946,7 @@ static void file_xfer_handle_status(SpiceMainChannel *channel,
>  
>      switch (msg->result) {
>      case VD_AGENT_FILE_XFER_STATUS_CAN_SEND_DATA:
> -        if (task->pending) {
> +        if (task->priv->pending) {
>              error = g_error_new(SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED,
>                             "transfer received CAN_SEND_DATA in pending state");
>              break;
> @@ -1912,7 +1962,7 @@ static void file_xfer_handle_status(SpiceMainChannel *channel,
>                              "some errors occurred in the spice agent");
>          break;
>      case VD_AGENT_FILE_XFER_STATUS_SUCCESS:
> -        if (task->pending)
> +        if (task->priv->pending)
>              error = g_error_new(SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED,
>                                  "transfer received success in pending state");
>          break;
> @@ -1923,7 +1973,7 @@ static void file_xfer_handle_status(SpiceMainChannel *channel,
>          break;
>      }
>  
> -    file_xfer_completed(task, error);
> +    spice_file_transfer_task_completed(task, error);
>  }
>  
>  /* any context: the message is not flushed immediately,
> @@ -2867,33 +2917,36 @@ void spice_main_set_display_enabled(SpiceMainChannel *channel, int id, gboolean
>      spice_main_update_display_enabled(channel, id, enabled, TRUE);
>  }
>  
> -static void file_xfer_completed(SpiceFileXferTask *task, GError *error)
> +static void spice_file_transfer_task_completed(SpiceFileTransferTask *self,
> +                                               GError *error)
>  {
>      /* In case of multiple errors we only report the first error */
> -    if (task->error)
> +    if (self->priv->error)
>          g_clear_error(&error);
>      if (error) {
>          SPICE_DEBUG("File %s xfer failed: %s",
> -                    g_file_get_path(task->file), error->message);
> -        task->error = error;
> +                    g_file_get_path(self->priv->file), error->message);
> +        self->priv->error = error;
>      }
>  
> -    if (task->pending)
> +    if (self->priv->pending)
>          return;
>  
> -    if (!task->file_stream) {
> -        file_xfer_close_cb(NULL, NULL, task);
> +    if (!self->priv->file_stream) {
> +        file_xfer_close_cb(NULL, NULL, self);
>          return;
>      }
>  
> -    g_input_stream_close_async(G_INPUT_STREAM(task->file_stream),
> +    g_input_stream_close_async(G_INPUT_STREAM(self->priv->file_stream),
>                                 G_PRIORITY_DEFAULT,
> -                               task->cancellable,
> +                               self->priv->cancellable,
>                                 file_xfer_close_cb,
> -                               task);
> -    task->pending = TRUE;
> +                               self);
> +    self->priv->pending = TRUE;
> +    g_signal_emit(self, task_signals[SIGNAL_FINISHED], 0, error);
>  }
>  
> +
>  static void file_xfer_info_async_cb(GObject *obj, GAsyncResult *res, gpointer data)
>  {
>      GFileInfo *info;
> @@ -2904,15 +2957,16 @@ static void file_xfer_info_async_cb(GObject *obj, GAsyncResult *res, gpointer da
>      VDAgentFileXferStartMessage msg;
>      gsize /*msg_size*/ data_len;
>      gchar *string;
> -    SpiceFileXferTask *task = (SpiceFileXferTask *)data;
> +    SpiceFileTransferTask *self = SPICE_FILE_TRANSFER_TASK(data);
>  
> -    task->pending = FALSE;
> +    self->priv->pending = FALSE;
>      info = g_file_query_info_finish(file, res, &error);
> -    if (error || task->error)
> +    if (error || self->priv->error)
>          goto failed;
>  
> -    task->file_size =
> +    self->priv->file_size =
>          g_file_info_get_attribute_uint64(info, G_FILE_ATTRIBUTE_STANDARD_SIZE);
> +    g_object_notify(G_OBJECT(self), "progress");
>      keyfile = g_key_file_new();
>  
>      /* File name */
> @@ -2920,7 +2974,7 @@ static void file_xfer_info_async_cb(GObject *obj, GAsyncResult *res, gpointer da
>      g_key_file_set_string(keyfile, "vdagent-file-xfer", "name", basename);
>      g_free(basename);
>      /* File size */
> -    g_key_file_set_uint64(keyfile, "vdagent-file-xfer", "size", task->file_size);
> +    g_key_file_set_uint64(keyfile, "vdagent-file-xfer", "size", self->priv->file_size);
>  
>      /* Save keyfile content to memory. TODO: more file attributions
>         need to be sent to guest */
> @@ -2930,41 +2984,45 @@ static void file_xfer_info_async_cb(GObject *obj, GAsyncResult *res, gpointer da
>          goto failed;
>  
>      /* Create file-xfer start message */
> -    msg.id = task->id;
> -    agent_msg_queue_many(task->channel, VD_AGENT_FILE_XFER_START,
> +    msg.id = self->priv->id;
> +    agent_msg_queue_many(self->priv->channel, VD_AGENT_FILE_XFER_START,
>                           &msg, sizeof(msg),
>                           string, data_len + 1, NULL);
>      g_free(string);
> -    spice_channel_wakeup(SPICE_CHANNEL(task->channel), FALSE);
> +    spice_channel_wakeup(SPICE_CHANNEL(self->priv->channel), FALSE);
>      return;
>  
>  failed:
> -    file_xfer_completed(task, error);
> +    spice_file_transfer_task_completed(self, error);
>  }
>  
>  static void file_xfer_read_async_cb(GObject *obj, GAsyncResult *res, gpointer data)
>  {
>      GFile *file = G_FILE(obj);
> -    SpiceFileXferTask *task = (SpiceFileXferTask *)data;
> +    SpiceFileTransferTask *self = SPICE_FILE_TRANSFER_TASK(data);
>      GError *error = NULL;
>  
> -    task->pending = FALSE;
> -    task->file_stream = g_file_read_finish(file, res, &error);
> -    if (error || task->error) {
> -        file_xfer_completed(task, error);
> +    self->priv->pending = FALSE;
> +    self->priv->file_stream = g_file_read_finish(file, res, &error);
> +    if (error || self->priv->error) {
> +        spice_file_transfer_task_completed(self, error);
>          return;
>      }
>  
> -    g_file_query_info_async(task->file,
> +    g_file_query_info_async(self->priv->file,
>                              G_FILE_ATTRIBUTE_STANDARD_SIZE,
>                              G_FILE_QUERY_INFO_NONE,
>                              G_PRIORITY_DEFAULT,
> -                            task->cancellable,
> +                            self->priv->cancellable,
>                              file_xfer_info_async_cb,
> -                            task);
> -    task->pending = TRUE;
> +                            self);
> +    self->priv->pending = TRUE;
>  }
>  
> +static SpiceFileTransferTask *spice_file_transfer_task_new(SpiceMainChannel *channel,
> +                                                           GFile *file,
> +                                                           GCancellable *cancellable);
> +
>  static void file_xfer_send_start_msg_async(SpiceMainChannel *channel,
>                                             GFile **files,
>                                             GFileCopyFlags flags,
> @@ -2975,39 +3033,41 @@ static void file_xfer_send_start_msg_async(SpiceMainChannel *channel,
>                                             gpointer user_data)
>  {
>      SpiceMainChannelPrivate *c = channel->priv;
> -    SpiceFileXferTask *task;
> -    static uint32_t xfer_id;    /* Used to identify task id */
> +    SpiceFileTransferTask *task;
>      gint i;
>  
>      for (i = 0; files[i] != NULL && !g_cancellable_is_cancelled(cancellable); i++) {
> -        task = g_malloc0(sizeof(SpiceFileXferTask));
> -        task->id = ++xfer_id;
> -        task->channel = g_object_ref(channel);
> -        task->file = g_object_ref(files[i]);
> -        task->flags = flags;
> -        task->cancellable = cancellable;
> -        task->progress_callback = progress_callback;
> -        task->progress_callback_data = progress_callback_data;
> -        task->callback = callback;
> -        task->user_data = user_data;
> -
> -        if (spice_util_get_debug()) {
> -            gchar *basename = g_file_get_basename(task->file);
> -            task->start_time = g_get_monotonic_time();
> -            task->last_update = task->start_time;
> -
> -            SPICE_DEBUG("transfer of file %s has started", basename);
> -            g_free(basename);
> -        }
> -        CHANNEL_DEBUG(task->channel, "Insert a xfer task:%d to task list", task->id);
> -        g_hash_table_insert(c->file_xfer_tasks, GUINT_TO_POINTER(task->id), task);
> +        GCancellable *task_cancellable = cancellable;
> +        /* if a cancellable object was not provided for the overall operation,
> +         * create a separate object for each file so that they can be cancelled
> +         * separately  */
> +        if (!task_cancellable)
> +            task_cancellable = g_cancellable_new();
> +
> +        task = spice_file_transfer_task_new(channel, files[i], task_cancellable);
> +        task->priv->flags = flags;
> +        task->priv->progress_callback = progress_callback;
> +        task->priv->progress_callback_data = progress_callback_data;
> +        task->priv->callback = callback;
> +        task->priv->user_data = user_data;
> +
> +        CHANNEL_DEBUG(channel, "Insert a xfer task:%d to task list",
> +                      task->priv->id);
> +        g_hash_table_insert(c->file_xfer_tasks,
> +                            GUINT_TO_POINTER(task->priv->id),
> +                            task);
> +        g_signal_emit(channel, signals[SPICE_MAIN_NEW_FILE_TRANSFER], 0, task);
>  
>          g_file_read_async(files[i],
>                            G_PRIORITY_DEFAULT,
>                            cancellable,
>                            file_xfer_read_async_cb,
>                            task);
> -        task->pending = TRUE;
> +        task->priv->pending = TRUE;
> +
> +        /* if we created a per-task cancellable above, free it */
> +        if (!cancellable)
> +            g_object_unref(task_cancellable);
>      }
>  }
>  
> @@ -3101,3 +3161,252 @@ gboolean spice_main_file_copy_finish(SpiceMainChannel *channel,
>  
>      return g_simple_async_result_get_op_res_gboolean(simple);
>  }
> +
> +
> +
> +static void
> +spice_file_transfer_task_get_property(GObject *object,
> +                                      guint property_id,
> +                                      GValue *value,
> +                                      GParamSpec *pspec)
> +{
> +    SpiceFileTransferTask *self = SPICE_FILE_TRANSFER_TASK(object);
> +
> +    switch (property_id)
> +    {
> +        case PROP_TASK_ID:
> +            g_value_set_uint(value, self->priv->id);
> +            break;
> +        case PROP_TASK_FILE:
> +            g_value_set_object(value, self->priv->file);
> +            break;
> +        case PROP_TASK_PROGRESS:
> +            g_value_set_double(value, spice_file_transfer_task_get_progress(self));
> +            break;
> +        default:
> +            G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
> +    }
> +}
> +
> +static void
> +spice_file_transfer_task_set_property(GObject *object,
> +                                      guint property_id,
> +                                      const GValue *value,
> +                                      GParamSpec *pspec)
> +{
> +    SpiceFileTransferTask *self = SPICE_FILE_TRANSFER_TASK(object);
> +
> +    switch (property_id)
> +    {
> +        case PROP_TASK_ID:
> +            self->priv->id = g_value_get_uint(value);
> +            break;
> +        case PROP_TASK_FILE:
> +            self->priv->file = g_value_dup_object(value);
> +            break;
> +        case PROP_TASK_CHANNEL:
> +            self->priv->channel = g_value_dup_object(value);
> +            break;
> +        case PROP_TASK_CANCELLABLE:
> +            self->priv->cancellable = g_value_dup_object(value);
> +            break;
> +        default:
> +            G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
> +    }
> +}
> +
> +static void
> +spice_file_transfer_task_dispose(GObject *object)
> +{
> +    SpiceFileTransferTask *self = SPICE_FILE_TRANSFER_TASK(object);
> +
> +    g_clear_object(&self->priv->file);
> +
> +    G_OBJECT_CLASS(spice_file_transfer_task_parent_class)->dispose(object);
> +}
> +
> +static void
> +spice_file_transfer_task_finalize(GObject *object)
> +{
> +    SpiceFileTransferTask *self = SPICE_FILE_TRANSFER_TASK(object);
> +
> +    g_free(self->priv->buffer);
> +
> +    G_OBJECT_CLASS(spice_file_transfer_task_parent_class)->finalize(object);
> +}
> +
> +static void
> +spice_file_transfer_task_constructed(GObject *object)
> +{
> +    SpiceFileTransferTask *self = SPICE_FILE_TRANSFER_TASK(object);
> +
> +    if (spice_util_get_debug()) {
> +        gchar *basename = g_file_get_basename(self->priv->file);
> +        self->priv->start_time = g_get_monotonic_time();
> +        self->priv->last_update = self->priv->start_time;
> +
> +        SPICE_DEBUG("transfer of file %s has started", basename);
> +        g_free(basename);
> +    }
> +}
> +
> +static void
> +spice_file_transfer_task_class_init(SpiceFileTransferTaskClass *klass)
> +{
> +    GObjectClass *object_class = G_OBJECT_CLASS(klass);
> +
> +    g_type_class_add_private(klass, sizeof(SpiceFileTransferTaskPrivate));
> +
> +    object_class->get_property = spice_file_transfer_task_get_property;
> +    object_class->set_property = spice_file_transfer_task_set_property;
> +    object_class->finalize = spice_file_transfer_task_finalize;
> +    object_class->dispose = spice_file_transfer_task_dispose;
> +    object_class->constructed = spice_file_transfer_task_constructed;
> +
> +    /**
> +     * SpiceFileTransferTask:id:
> +     *
> +     * The ID of the file transfer task
> +     **/
> +    g_object_class_install_property(object_class, PROP_TASK_ID,
> +                                    g_param_spec_uint("id",
> +                                                      "id",
> +                                                      "The id of the task",
> +                                                      0, G_MAXUINT, 0,
> +                                                      G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE |
> +                                                      G_PARAM_STATIC_STRINGS));
> +
> +    /**
> +     * SpiceFileTransferTask:channel:
> +     *
> +     * The main channel that owns the file transfer task
> +     **/
> +    g_object_class_install_property(object_class, PROP_TASK_CHANNEL,
> +                                    g_param_spec_object("channel",
> +                                                        "channel",
> +                                                        "The channel transferring the file",
> +                                                        SPICE_TYPE_MAIN_CHANNEL,
> +                                                        G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE |
> +                                                        G_PARAM_STATIC_STRINGS));
> +
> +    /**
> +     * SpiceFileTransferTask:cancellable:
> +     *
> +     * A cancellable object used to cancel the file transfer
> +     **/
> +    g_object_class_install_property(object_class, PROP_TASK_CANCELLABLE,
> +                                    g_param_spec_object("cancellable",
> +                                                        "cancellable",
> +                                                        "The object used to cancel the task",
> +                                                        G_TYPE_CANCELLABLE,
> +                                                        G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE |
> +                                                        G_PARAM_STATIC_STRINGS));
> +
> +    /**
> +     * SpiceFileTransferTask:file:
> +     *
> +     * The file that is being transferred in this file transfer task
> +     **/
> +    g_object_class_install_property(object_class, PROP_TASK_FILE,
> +                                    g_param_spec_object("file",
> +                                                        "File",
> +                                                        "The file being transferred",
> +                                                        G_TYPE_FILE,
> +                                                        G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE |
> +                                                        G_PARAM_STATIC_STRINGS));
> +
> +    /**
> +     * SpiceFileTransferTask:progress:
> +     *
> +     * The current state of the file transfer. This value indicates a
> +     * percentage, and ranges from 0 to 100. Listen for change notifications on
> +     * this property to be updated whenever the file transfer progress changes.
> +     **/
> +    g_object_class_install_property(object_class, PROP_TASK_PROGRESS,
> +                                    g_param_spec_double("progress",
> +                                                        "Progress",
> +                                                        "The percentage of the file transferred",
> +                                                        0.0, 100.0, 0.0,
> +                                                        G_PARAM_READABLE |
> +                                                        G_PARAM_STATIC_STRINGS));
> +
> +    /**
> +     * SpiceFileTransferTask::finished:
> +     * @task: the file transfer task that emitted the signal
> +     * @error: (transfer none): the error state of the transfer. Will be %NULL
> +     * if the file transfer was successful.
> +     *
> +     * The #SpiceFileTransferTask::finished signal is emitted when the file
> +     * transfer has completed transferring to the guest.
> +     **/
> +    task_signals[SIGNAL_FINISHED] = g_signal_new("finished", SPICE_TYPE_FILE_TRANSFER_TASK,
> +                                            G_SIGNAL_RUN_FIRST,
> +                                            0, NULL, NULL,
> +                                            g_cclosure_marshal_VOID__BOXED,
> +                                            G_TYPE_NONE, 1,
> +                                            G_TYPE_ERROR);
> +}
> +
> +static void
> +spice_file_transfer_task_init(SpiceFileTransferTask *self)
> +{
> +    self->priv = FILE_TRANSFER_TASK_PRIVATE(self);
> +    self->priv->buffer = g_malloc0(FILE_XFER_CHUNK_SIZE);
> +}
> +
> +SpiceFileTransferTask *
> +spice_file_transfer_task_new(SpiceMainChannel *channel, GFile *file, GCancellable *cancellable)
> +{
> +    static uint32_t xfer_id = 0;    /* Used to identify task id */
> +
> +    return g_object_new(SPICE_TYPE_FILE_TRANSFER_TASK,
> +                        "id", xfer_id++,
> +                        "file", file,
> +                        "channel", channel,
> +                        "cancellable", cancellable,
> +                        NULL);
> +}
> +
> +/**
> + * spice_file_transfer_task_get_progress:
> + * @self:
> + *
> + * Convenience function for retrieving the current progress of this file
> + * transfer task.
> + *
> + * Returns: A percentage value between 0 and 100
> + **/
> +double spice_file_transfer_task_get_progress(SpiceFileTransferTask *self)
> +{
> +    if (self->priv->file_size == 0)
> +        return 0.0;
> +
> +    return (double)self->priv->read_bytes / self->priv->file_size;
> +}
> +
> +/**
> + * spice_file_transfer_task_cancel:
> + * @self:
> + *
> + * Cancels the file transfer task. Note that depending on how the file transfer
> + * was initiated, multiple file transfer tasks may share a single
> + * #SpiceFileTransferTask::cancellable object, so canceling one task may result
> + * in the cancellation of other tasks.
> + **/
> +void spice_file_transfer_task_cancel(SpiceFileTransferTask *self)
> +{
> +    g_cancellable_cancel(self->priv->cancellable);
> +}
> +
> +/**
> + * spice_file_transfer_task_get_filename:
> + * @self:
> + *
> + * Gets the name of the file being transferred in this task
> + *
> + * Returns: (transfer none): The basename of the file
> + **/
> +char* spice_file_transfer_task_get_filename(SpiceFileTransferTask *self)
> +{
> +    return g_file_get_basename(self->priv->file);
> +}
> diff --git a/src/channel-main.h b/src/channel-main.h
> index 92875dc..332a71b 100644
> --- a/src/channel-main.h
> +++ b/src/channel-main.h
> @@ -19,6 +19,7 @@
>  #define __SPICE_CLIENT_MAIN_CHANNEL_H__
>  
>  #include "spice-channel.h"
> +#include "spice-file-transfer-task.h"
>  
>  G_BEGIN_DECLS
>  
> diff --git a/src/map-file b/src/map-file
> index a9abc61..92a9883 100644
> --- a/src/map-file
> +++ b/src/map-file
> @@ -33,6 +33,11 @@ spice_display_new_with_monitor;
>  spice_display_paste_from_guest;
>  spice_display_send_keys;
>  spice_display_set_grab_keys;
> +spice_file_transfer_task_cancel;
> +spice_file_transfer_task_get_filename;
> +spice_file_transfer_task_get_finished;
> +spice_file_transfer_task_get_progress;
> +spice_file_transfer_task_get_type;
>  spice_get_option_group;
>  spice_grab_sequence_as_string;
>  spice_grab_sequence_copy;
> diff --git a/src/spice-file-transfer-task.h b/src/spice-file-transfer-task.h
> new file mode 100644
> index 0000000..89960ce
> --- /dev/null
> +++ b/src/spice-file-transfer-task.h
> @@ -0,0 +1,58 @@
> +/*
> +   Copyright (C) 2010-2015 Red Hat, Inc.
> +
> +   This library is free software; you can redistribute it and/or
> +   modify it under the terms of the GNU Lesser General Public
> +   License as published by the Free Software Foundation; either
> +   version 2.1 of the License, or (at your option) any later version.
> +
> +   This library 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
> +   Lesser General Public License for more details.
> +
> +   You should have received a copy of the GNU Lesser General Public
> +   License along with this library; if not, see <http://www.gnu.org/licenses/>.
> +   */
> +
> +#ifndef __SPICE_FILE_TRANSFER_TASK_H__
> +#define __SPICE_FILE_TRANSFER_TASK_H__
> +
> +#include <gio/gio.h>
> +#include <spice/vd_agent.h>
> +
> +G_BEGIN_DECLS
> +
> +#define SPICE_TYPE_FILE_TRANSFER_TASK spice_file_transfer_task_get_type()
> +
> +#define SPICE_FILE_TRANSFER_TASK(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), SPICE_TYPE_FILE_TRANSFER_TASK, SpiceFileTransferTask))
> +#define SPICE_FILE_TRANSFER_TASK_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), SPICE_TYPE_FILE_TRANSFER_TASK, SpiceFileTransferTaskClass))
> +#define SPICE_IS_FILE_TRANSFER_TASK(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), SPICE_TYPE_FILE_TRANSFER_TASK))
> +#define SPICE_IS_FILE_TRANSFER_TASK_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), SPICE_TYPE_FILE_TRANSFER_TASK))
> +#define SPICE_FILE_TRANSFER_TASK_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), SPICE_TYPE_FILE_TRANSFER_TASK, SpiceFileTransferTaskClass))
> +
> +typedef struct _SpiceFileTransferTask SpiceFileTransferTask;
> +typedef struct _SpiceFileTransferTaskClass SpiceFileTransferTaskClass;
> +typedef struct _SpiceFileTransferTaskPrivate SpiceFileTransferTaskPrivate;
> +
> +struct _SpiceFileTransferTask
> +{
> +    GObject parent;
> +
> +    SpiceFileTransferTaskPrivate *priv;
> +};
> +
> +struct _SpiceFileTransferTaskClass
> +{
> +    GObjectClass parent_class;
> +};
> +
> +GType spice_file_transfer_task_get_type(void) G_GNUC_CONST;
> +
> +char* spice_file_transfer_task_get_filename(SpiceFileTransferTask *self);
> +void spice_file_transfer_task_cancel(SpiceFileTransferTask *self);
> +double spice_file_transfer_task_get_progress(SpiceFileTransferTask *self);
> +
> +G_END_DECLS
> +
> +#endif /* __SPICE_FILE_TRANSFER_TASK_H__ */
> diff --git a/src/spice-glib-sym-file b/src/spice-glib-sym-file
> index 1d62716..3817a46 100644
> --- a/src/spice-glib-sym-file
> +++ b/src/spice-glib-sym-file
> @@ -20,6 +20,11 @@ spice_client_error_quark
>  spice_cursor_channel_get_type
>  spice_display_channel_get_type
>  spice_display_get_primary
> +spice_file_transfer_task_cancel
> +spice_file_transfer_task_get_filename
> +spice_file_transfer_task_get_finished
> +spice_file_transfer_task_get_progress
> +spice_file_transfer_task_get_type
>  spice_get_option_group
>  spice_g_signal_connect_object
>  spice_inputs_button_press
> diff --git a/src/spicy.c b/src/spicy.c
> index f48a220..6d84d85 100644
> --- a/src/spicy.c
> +++ b/src/spicy.c
> @@ -91,6 +91,7 @@ G_DEFINE_TYPE (SpiceWindow, spice_window, G_TYPE_OBJECT);
>  #define CHANNELID_MAX 4
>  #define MONITORID_MAX 4
>  
> +
>  // FIXME: turn this into an object, get rid of fixed wins array, use
>  // signals to replace the various callback that iterate over wins array
>  struct spice_connection {
> @@ -104,6 +105,10 @@ struct spice_connection {
>      gboolean         agent_connected;
>      int              channels;
>      int              disconnecting;
> +
> +    /* key: SpiceFileTransferTask, value: TransferTaskWidgets */
> +    GHashTable *transfers;
> +    GtkWidget *transfer_dialog;
>  };
>  
>  static spice_connection *connection_new(void);
> @@ -1386,6 +1391,149 @@ static void port_data(SpicePortChannel *port,
>      }
>  }
>  
> +typedef struct {
> +    GtkWidget *vbox;
> +    GtkWidget *hbox;
> +    GtkWidget *progress;
> +    GtkWidget *label;
> +    GtkWidget *cancel;
> +} TransferTaskWidgets;
> +
> +static void transfer_update_progress(GObject *object,
> +                                     GParamSpec *pspec,
> +                                     gpointer user_data)
> +{
> +    spice_connection *conn = user_data;
> +    TransferTaskWidgets *widgets = g_hash_table_lookup(conn->transfers, object);
> +    g_return_if_fail(widgets);
> +    gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(widgets->progress),
> +                                  spice_file_transfer_task_get_progress(SPICE_FILE_TRANSFER_TASK(object)));
> +}
> +
> +static void transfer_task_finished(SpiceFileTransferTask *task, GError *error, spice_connection *conn)
> +{
> +    if (error)
> +        g_warning("%s", error->message);
> +    g_hash_table_remove(conn->transfers, task);
> +    if (!g_hash_table_size(conn->transfers))
> +        gtk_widget_hide(conn->transfer_dialog);
> +}
> +
> +static void dialog_response_cb(GtkDialog *dialog,
> +                               gint response_id,
> +                               gpointer user_data)
> +{
> +    spice_connection *conn = user_data;
> +    g_print("Reponse: %i\n", response_id);
> +
> +    if (response_id == GTK_RESPONSE_CANCEL) {
> +        GHashTableIter iter;
> +        gpointer key, value;
> +
> +        g_hash_table_iter_init(&iter, conn->transfers);
> +        while (g_hash_table_iter_next(&iter, &key, &value)) {
> +            SpiceFileTransferTask *task = key;
> +            spice_file_transfer_task_cancel(task);
> +        }
> +    }
> +
> +    //conn->transfer_dialog = NULL;
> +}
> +
> +void task_cancel_cb(GtkButton *button,
> +                    gpointer user_data)
> +{
> +    SpiceFileTransferTask *task = SPICE_FILE_TRANSFER_TASK(user_data);
> +    spice_file_transfer_task_cancel(task);
> +}
> +
> +TransferTaskWidgets *transfer_task_widgets_new(SpiceFileTransferTask *task)
> +{
> +    TransferTaskWidgets *widgets = g_new0(TransferTaskWidgets, 1);
> +
> +#if GTK_CHECK_VERSION(3,0,0)
> +    widgets->vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
> +    widgets->hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
> +#else
> +    widgets->vbox = gtk_vbox_new(FALSE, 0);
> +    widgets->hbox = gtk_hbox_new(FALSE, 6);
> +#endif
> +
> +    widgets->progress = gtk_progress_bar_new();
> +    widgets->label = gtk_label_new(spice_file_transfer_task_get_filename(task));
> +    widgets->cancel = gtk_button_new_from_icon_name(GTK_STOCK_CANCEL,
> +                                                    GTK_ICON_SIZE_SMALL_TOOLBAR);
> +
> +#if GTK_CHECK_VERSION(3,0,0)
> +    gtk_widget_set_halign(widgets->label, GTK_ALIGN_START);
> +    gtk_widget_set_valign(widgets->label, GTK_ALIGN_BASELINE);
> +    gtk_widget_set_valign(widgets->progress, GTK_ALIGN_CENTER);
> +    gtk_widget_set_hexpand(widgets->progress, TRUE);
> +    gtk_widget_set_valign(widgets->cancel, GTK_ALIGN_CENTER);
> +    gtk_widget_set_hexpand(widgets->progress, FALSE);
> +#endif
> +
> +    gtk_box_pack_start(GTK_BOX(widgets->hbox), widgets->progress,
> +                       TRUE, TRUE, 0);
> +    gtk_box_pack_start(GTK_BOX(widgets->hbox), widgets->cancel,
> +                       FALSE, TRUE, 0);
> +
> +    gtk_box_pack_start(GTK_BOX(widgets->vbox), widgets->label,
> +                       TRUE, TRUE, 0);
> +    gtk_box_pack_start(GTK_BOX(widgets->vbox), widgets->hbox,
> +                       TRUE, TRUE, 0);
> +
> +    g_signal_connect(widgets->cancel, "clicked",
> +                     G_CALLBACK(task_cancel_cb), task);
> +
> +    gtk_widget_show_all(widgets->vbox);
> +
> +    return widgets;
> +}
> +
> +void transfer_task_widgets_free(TransferTaskWidgets *widgets)
> +{
> +    /* child widgets will be destroyed automatically */
> +    gtk_widget_destroy(widgets->vbox);
> +    g_free(widgets);
> +}
> +
> +static void spice_connection_add_task(spice_connection *conn, SpiceFileTransferTask *task)
> +{
> +    TransferTaskWidgets *widgets;
> +    GtkWidget *content = NULL;
> +
> +    g_signal_connect(task, "notify::progress",
> +                     G_CALLBACK(transfer_update_progress), conn);
> +    g_signal_connect(task, "finished",
> +                     G_CALLBACK(transfer_task_finished), conn);
> +    if (!conn->transfer_dialog) {
> +        conn->transfer_dialog = gtk_dialog_new_with_buttons("File Transfers",
> +                                                            GTK_WINDOW(conn->wins[0]->toplevel), 0,
> +                                                            "Cancel", GTK_RESPONSE_CANCEL, NULL);
> +        gtk_dialog_set_default_response(GTK_DIALOG(conn->transfer_dialog),
> +                                        GTK_RESPONSE_CANCEL);
> +        gtk_window_set_resizable(GTK_WINDOW(conn->transfer_dialog), FALSE);
> +        g_signal_connect(conn->transfer_dialog, "response",
> +                         G_CALLBACK(dialog_response_cb), conn);
> +    }
> +    gtk_widget_show(conn->transfer_dialog);
> +    content = gtk_dialog_get_content_area(GTK_DIALOG(conn->transfer_dialog));
> +    gtk_container_set_border_width(GTK_CONTAINER(content), 12);
> +
> +    widgets = transfer_task_widgets_new(task);
> +    g_hash_table_insert(conn->transfers, g_object_ref(task), widgets);
> +    gtk_box_pack_start(GTK_BOX(content),
> +                       widgets->vbox, TRUE, TRUE, 6);
> +}
> +
> +static void new_file_transfer(SpiceMainChannel *main, SpiceFileTransferTask *task, gpointer user_data)
> +{
> +    spice_connection *conn = user_data;
> +    g_debug("new file transfer task");
> +    spice_connection_add_task(conn, task);
> +}
> +
>  static void channel_new(SpiceSession *s, SpiceChannel *channel, gpointer data)
>  {
>      spice_connection *conn = data;
> @@ -1404,6 +1552,8 @@ static void channel_new(SpiceSession *s, SpiceChannel *channel, gpointer data)
>                           G_CALLBACK(main_mouse_update), conn);
>          g_signal_connect(channel, "main-agent-update",
>                           G_CALLBACK(main_agent_update), conn);
> +        g_signal_connect(channel, "new-file-transfer",
> +                         G_CALLBACK(new_file_transfer), conn);
>          main_mouse_update(channel, conn);
>          main_agent_update(channel, conn);
>      }
> @@ -1515,6 +1665,9 @@ static spice_connection *connection_new(void)
>                           G_CALLBACK(usb_connect_failed), NULL);
>      }
>  
> +    conn->transfers = g_hash_table_new_full(g_direct_hash, g_direct_equal,
> +                                            g_object_unref,
> +                                            (GDestroyNotify)transfer_task_widgets_free);
>      connections++;
>      SPICE_DEBUG("%s (%d)", __FUNCTION__, connections);
>      return conn;




More information about the Spice-devel mailing list