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; _______________________________________________ Spice-devel mailing list Spice-devel@xxxxxxxxxxxxxxxxxxxxx http://lists.freedesktop.org/mailman/listinfo/spice-devel