/**
 * inf_text_gtk_viewport_set_active_user:
 * @viewport: A #InfTextGtkViewport.
 * @user: A user from @viewport's user table, or %NULL.
 *
 * Sets the user for which perspective to draw the viewport. The cursor
 * position for teh active user is not draws since it is assumed that the
 * viewport's "real" scrollbars match the active user's position.
 */
void
inf_text_gtk_viewport_set_active_user(InfTextGtkViewport* viewport,
                                      InfTextUser* user)
{
  InfTextGtkViewportPrivate* priv;
  InfTextUser* active_user;

  g_return_if_fail(INF_TEXT_GTK_IS_VIEWPORT(viewport));
  g_return_if_fail(user == NULL || INF_TEXT_IS_USER(user));

  priv = INF_TEXT_GTK_VIEWPORT_PRIVATE(viewport);
  g_return_if_fail(
    user == NULL ||
    inf_user_table_lookup_user_by_id(
      priv->user_table,
      inf_user_get_id(INF_USER(user))
    ) == INF_USER(user)
  );

  if(priv->active_user != NULL)
  {
    active_user = priv->active_user;
    priv->active_user = NULL;

    inf_text_gtk_viewport_user_added(viewport, active_user);
  }

  if(user != NULL)
  {
    inf_text_gtk_viewport_user_removed(viewport, user);
  }

  priv->active_user = user;
  g_object_notify(G_OBJECT(viewport), "active-user");
}
Esempio n. 2
0
static void
inf_user_table_check_local_cb(GObject* object,
                              GParamSpec* pspec,
                              gpointer user_data)
{
  InfUserTable* user_table;
  InfUserTablePrivate* priv;
  GSList* item;

  user_table = INF_USER_TABLE(user_data);
  priv = INF_USER_TABLE_PRIVATE(user_table);
  item = g_slist_find(priv->locals, INF_USER(object));

  if(inf_user_table_is_local(INF_USER(object)) && item == NULL)
  {
    g_signal_emit(
      G_OBJECT(user_table),
      user_table_signals[ADD_LOCAL_USER],
      0,
      INF_USER(object)
    );
  }
  
  if(!inf_user_table_is_local(INF_USER(object)) && item != NULL)
  {
    g_signal_emit(
      G_OBJECT(user_table),
      user_table_signals[REMOVE_LOCAL_USER],
      0,
      INF_USER(object)
    );
  }
}
Esempio n. 3
0
void TextBuffer::eraseText( unsigned int pos,
    unsigned int len,
    User *user )
{
    qDebug() << "erasing text:" << slice(pos, len)->text() << pos << len;
    inf_text_buffer_text_erased( INF_TEXT_BUFFER(gobject()),
        pos, slice(pos, len)->infChunk(), INF_USER(user->gobject()) );
    inf_text_buffer_erase_text( INF_TEXT_BUFFER(gobject()),
        pos, len, INF_USER(user->gobject()) );
}
Esempio n. 4
0
void TextBuffer::insertChunk( unsigned int pos,
    const TextChunk &chunk,
    User *user )
{
    InfTextBufferIface* iface = INF_TEXT_BUFFER_GET_IFACE(INF_TEXT_BUFFER(gobject()));
    inf_text_buffer_text_inserted( INF_TEXT_BUFFER(gobject()),
        pos, chunk.infChunk(), INF_USER(user->gobject()) );
    inf_text_buffer_insert_chunk( INF_TEXT_BUFFER(gobject()),
        pos, chunk.infChunk(),
        INF_USER(user->gobject()) );
}
Esempio n. 5
0
void Gobby::ChatSessionView::set_active_user(InfUser* user)
{
	g_assert(
		user == NULL ||
		inf_user_table_lookup_user_by_id(
			inf_session_get_user_table(INF_SESSION(m_session)),
			inf_user_get_id(INF_USER(user)))
		== INF_USER(user));

	inf_gtk_chat_set_active_user(m_chat, user);
	active_user_changed(user);
}
Esempio n. 6
0
void TextBuffer::insertText( unsigned int pos,
    const QByteArray &data,
    unsigned int length,
    User *user )
{
    inf_text_buffer_insert_text( INF_TEXT_BUFFER(gobject()),
        pos, data.data(), data.length(), length,
        INF_USER(user->gobject()) );
    // TODO test me, I don't think this is used anywhere in kobby
    QInfinity::TextChunk chunk( encoding() );
    chunk.insertText( 0, data, data.length(), user->id() );
    inf_text_buffer_text_inserted( INF_TEXT_BUFFER(gobject()),
        pos, chunk.infChunk(), INF_USER(user->gobject()) );
}
static void
inf_text_default_delete_operation_apply(InfAdoptedOperation* operation,
                                        InfAdoptedUser* by,
                                        InfBuffer* buffer)
{
  InfTextDefaultDeleteOperationPrivate* priv;

  g_assert(INF_TEXT_IS_DEFAULT_DELETE_OPERATION(operation));
  g_assert(INF_TEXT_IS_BUFFER(buffer));

  priv = INF_TEXT_DEFAULT_DELETE_OPERATION_PRIVATE(operation);

#ifdef DELETE_OPERATION_CHECK_TEXT_MATCH
  g_assert(
    inf_text_default_delete_operation_text_match(
      INF_TEXT_DEFAULT_DELETE_OPERATION(operation),
      INF_TEXT_BUFFER(buffer)
    )
  );
#endif /* DELETE_OPERATION_CHECK_TEXT_MATCH */

  inf_text_buffer_erase_text(
    INF_TEXT_BUFFER(buffer),
    priv->position,
    inf_text_chunk_get_length(priv->chunk),
    INF_USER(by)
  );
}
Esempio n. 8
0
static void
inf_user_get_property(GObject* object,
                      guint prop_id,
                      GValue* value,
                      GParamSpec* pspec)
{
  InfUser* user;
  InfUserPrivate* priv;

  user = INF_USER(object);
  priv = INF_USER_PRIVATE(user);

  switch(prop_id)
  {
  case PROP_ID:
    g_value_set_uint(value, priv->id);
    break;
  case PROP_NAME:
    g_value_set_string(value, priv->name);
    break;
  case PROP_STATUS:
    g_value_set_enum(value, priv->status);
    break;
  case PROP_FLAGS:
    g_value_set_flags(value, priv->flags);
    break;
  case PROP_CONNECTION:
    g_value_set_object(value, G_OBJECT(priv->connection));
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
    break;
  }
}
Esempio n. 9
0
static void
inf_user_table_dispose_foreach_func(gpointer key,
                                    gpointer value,
                                    gpointer user_data)
{
  inf_user_table_unref_user(INF_USER_TABLE(user_data), INF_USER(value));
}
static void
inf_text_gtk_viewport_user_removed(InfTextGtkViewport* viewport,
                                   InfTextUser* user)
{
  InfTextGtkViewportPrivate* priv;
  InfTextGtkViewportUser* viewport_user;

  priv = INF_TEXT_GTK_VIEWPORT_PRIVATE(viewport);

  if(user == priv->active_user)
  {
    priv->active_user = NULL;
    g_object_notify(G_OBJECT(viewport), "active-user");
  }
  else
  {
    inf_signal_handlers_disconnect_by_func(
      user,
      G_CALLBACK(inf_text_gtk_viewport_user_notify_status_cb),
      viewport
    );

    if(inf_user_get_status(INF_USER(user)) == INF_USER_ACTIVE)
    {
      viewport_user = inf_text_gtk_viewport_find_user(viewport, user);
      g_assert(viewport_user != NULL);

      inf_text_gtk_viewport_remove_user(viewport_user);
    }
  }
}
static void
inf_text_gtk_viewport_user_notify_status_cb(GObject* object,
                                            GParamSpec* pspec,
                                            gpointer user_data)
{
  InfTextGtkViewport* viewport;
  InfTextGtkViewportPrivate* priv;
  InfTextUser* user;
  InfTextGtkViewportUser* viewport_user;

  viewport = INF_TEXT_GTK_VIEWPORT(user_data);
  priv = INF_TEXT_GTK_VIEWPORT_PRIVATE(viewport);
  user = INF_TEXT_USER(object);

  g_assert(user != priv->active_user);
  viewport_user = inf_text_gtk_viewport_find_user(viewport, user);

  if(inf_user_get_status(INF_USER(user)) == INF_USER_ACTIVE)
  {
    if(!viewport_user)
      inf_text_gtk_viewport_add_user(viewport, user);
  }
  else
  {
    if(viewport_user)
      inf_text_gtk_viewport_remove_user(viewport_user);
  }
}
Esempio n. 12
0
static void
inf_user_dispose(GObject* object)
{
  InfUser* user;
  user = INF_USER(object);

  G_OBJECT_CLASS(parent_class)->dispose(object);
}
Esempio n. 13
0
static void
inf_adopted_session_execute_request_cb(InfAdoptedAlgorithm* algorithm,
                                       InfAdoptedUser* user,
                                       InfAdoptedRequest* request,
                                       gboolean apply,
                                       gpointer user_data)
{
  InfAdoptedSession* session;
  InfAdoptedSessionPrivate* priv;
  GSList* item;
  InfAdoptedSessionLocalUser* local;
  guint id;

  session = INF_ADOPTED_SESSION(user_data);
  priv = INF_ADOPTED_SESSION_PRIVATE(session);

  if(inf_adopted_request_affects_buffer(request))
  {
    id = inf_adopted_request_get_user_id(request);

    /* A request has been executed, meaning we are no longer up to date. Send
     * a noop in some time, so that others know what we already processed. */
    for(item = priv->local_users; item != NULL; item = g_slist_next(item))
    {
      local = (InfAdoptedSessionLocalUser*)item->data;
      if(local->noop_time == 0)
        /* Except we issued the request ourselves, of course. */
        if(inf_user_get_id(INF_USER(local->user)) != id)
          inf_adopted_session_start_noop_timer(session, local);
    }
  }

  /* Mark inactive users active if they do something */
  /* Note: This behaviour is implicitly performed by both client and server,
   * and requires no further network traffic. However, users explictely have
   * to be set inactive. */
  if(inf_adopted_request_get_request_type(request) != INF_ADOPTED_REQUEST_DO ||
     !INF_ADOPTED_IS_NO_OPERATION(inf_adopted_request_get_operation(request)))
  {
    /* TODO: We should offer a virtual function to flush all requests for
     * local users, either here or even in InfSession via a vfunc, so that
     * we don't accidentally make local users active by a delayed request. */
    if(inf_user_get_status(INF_USER(user)) == INF_USER_INACTIVE)
      g_object_set(G_OBJECT(user), "status", INF_USER_ACTIVE, NULL);
  }
}
Esempio n. 14
0
static void
inf_user_table_foreach_user_func(gpointer key,
                                 gpointer value,
                                 gpointer user_data)
{
  InfUserTableForeachUserData* data;
  data = (InfUserTableForeachUserData*)user_data;

  data->func(INF_USER(value), data->user_data);
}
Esempio n. 15
0
static void
inf_user_set_property(GObject* object,
                      guint prop_id,
                      const GValue* value,
                      GParamSpec* pspec)
{
  InfUser* user;
  InfUserPrivate* priv;

  user = INF_USER(object);
  priv = INF_USER_PRIVATE(user);

  /* TODO: Check if properties are still valid.
   * There are several combinations possible:
   *
   * Status  | Flags | Connection | Desc
   * UNAVAIL |   0   |   unset    | User not available, was non-local last time
   *  AVAIL  |   0   |   unset    | INVALID
   * UNAVAIL | LOCAL |   unset    | User not available, was local last time
   *  AVAIL  | LOCAL |   unset    | User is available, and local
   * UNAVAIL |   0   |    set     | INVALID
   *  AVAIL  |   0   |    set     | User is available, non-local
   * UNAVAIL | LOCAL |    set     | INVALID
   *  AVAIL  | LOCAL |    set     | INVALID */

  switch(prop_id)
  {
  case PROP_ID:
    priv->id = g_value_get_uint(value);
    break;
  case PROP_NAME:
    g_free(priv->name);
    priv->name = g_value_dup_string(value);
    break;
  case PROP_STATUS:
    g_signal_emit(
      object,
      user_signals[SET_STATUS],
      0,
      g_value_get_enum(value)
    );

    break;
  case PROP_FLAGS:
    priv->flags = g_value_get_flags(value);
    break;
  case PROP_CONNECTION:
    if(priv->connection != NULL) g_object_unref(priv->connection);
    priv->connection = INF_XML_CONNECTION(g_value_dup_object(value));
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
    break;
  }
}
Esempio n. 16
0
static gboolean
inf_user_table_lookup_user_by_name_func(gpointer key,
                                        gpointer value,
                                        gpointer data)
{
  const gchar* user_name;
  user_name = inf_user_get_name(INF_USER(value));

  if(strcmp(user_name, (const gchar*)data) == 0) return TRUE;
  return FALSE;
}
Esempio n. 17
0
/**
 * inf_user_table_lookup_user_by_id:
 * @user_table: A #InfUserTable.
 * @id: User ID to lookup.
 *
 * Returns the #InfUser with the given User ID in @user_table.
 *
 * Return Value: A #InfUser, or %NULL.
 **/
InfUser*
inf_user_table_lookup_user_by_id(InfUserTable* user_table,
                                 guint id)
{
  InfUserTablePrivate* priv;

  g_return_val_if_fail(INF_IS_USER_TABLE(user_table), NULL);

  priv = INF_USER_TABLE_PRIVATE(user_table);

  return INF_USER(g_hash_table_lookup(priv->table, GUINT_TO_POINTER(id)));
}
Esempio n. 18
0
static void
inf_user_finalize(GObject* object)
{
  InfUser* user;
  InfUserPrivate* priv;

  user = INF_USER(object);
  priv = INF_USER_PRIVATE(user);

  g_free(priv->name);

  G_OBJECT_CLASS(parent_class)->finalize(object);
}
Esempio n. 19
0
/**
 * inf_user_table_foreach_local_user:
 * @user_table: A #InfUserTable.
 * @func: The function to call for each user.
 * @user_data: User data to pass to the function.
 *
 * Calls the given function for each local user in the user_table. A local
 * user is a user that has the %INF_USER_LOCAL flag set and that has not
 * status %INF_USER_UNAVAILABLE. You should not add or remove users while this
 * function is being executed.
 **/
void
inf_user_table_foreach_local_user(InfUserTable* user_table,
                                  InfUserTableForeachUserFunc func,
                                  gpointer user_data)
{
  InfUserTablePrivate* priv;
  GSList* item;

  g_return_if_fail(INF_IS_USER_TABLE(user_table));
  g_return_if_fail(func != NULL);

  priv = INF_USER_TABLE_PRIVATE(user_table);
  
  for(item = priv->locals; item != NULL; item = g_slist_next(item))
    func(INF_USER(item->data), user_data);
}
Esempio n. 20
0
static void
inf_user_init(GTypeInstance* instance,
              gpointer g_class)
{
  InfUser* user;
  InfUserPrivate* priv;

  user = INF_USER(instance);
  user->priv = INF_USER_GET_PRIVATE(user);
  priv = INF_USER_PRIVATE(user);

  priv->id = 0;
  priv->name = NULL;
  priv->status = INF_USER_UNAVAILABLE;
  priv->flags = 0;
  priv->connection = NULL;
}
static void
inf_text_default_insert_operation_apply(InfAdoptedOperation* operation,
                                        InfAdoptedUser* by,
                                        InfBuffer* buffer)
{
  InfTextDefaultInsertOperationPrivate* priv;

  g_assert(INF_TEXT_IS_DEFAULT_INSERT_OPERATION(operation));
  g_assert(INF_TEXT_IS_BUFFER(buffer));

  priv = INF_TEXT_DEFAULT_INSERT_OPERATION_PRIVATE(operation);

  inf_text_buffer_insert_chunk(
    INF_TEXT_BUFFER(buffer),
    priv->position,
    priv->chunk,
    INF_USER(by)
  );
}
static void
inf_text_remote_delete_operation_apply(InfAdoptedOperation* operation,
                                       InfAdoptedUser* by,
                                       InfBuffer* buffer)
{
  InfTextRemoteDeleteOperationPrivate* priv;

  g_assert(INF_TEXT_IS_REMOTE_DELETE_OPERATION(operation));
  g_assert(INF_TEXT_IS_BUFFER(buffer));

  priv = INF_TEXT_REMOTE_DELETE_OPERATION_PRIVATE(operation);

  inf_text_buffer_erase_text(
    INF_TEXT_BUFFER(buffer),
    priv->position,
    priv->length,
    INF_USER(by)
  );
}
static void
inf_text_gtk_viewport_user_added(InfTextGtkViewport* viewport,
                                 InfTextUser* user)
{
  InfTextGtkViewportPrivate* priv;
  priv = INF_TEXT_GTK_VIEWPORT_PRIVATE(viewport);

  /* Active user is guaranteed to be contained in user table, so if user was
   * just added then it can't be set as active user already. */
  g_assert(user != priv->active_user);

  g_signal_connect(
    user,
    "notify::status",
    G_CALLBACK(inf_text_gtk_viewport_user_notify_status_cb),
    viewport
  );

  if(inf_user_get_status(INF_USER(user)) == INF_USER_ACTIVE)
    inf_text_gtk_viewport_add_user(viewport, user);
}
static gboolean
inf_text_remote_delete_operation_apply(InfAdoptedOperation* operation,
                                       InfAdoptedUser* by,
                                       InfBuffer* buffer,
                                       GError** error)
{
  InfTextRemoteDeleteOperationPrivate* priv;

  g_assert(INF_TEXT_IS_REMOTE_DELETE_OPERATION(operation));
  g_assert(INF_TEXT_IS_BUFFER(buffer));

  priv = INF_TEXT_REMOTE_DELETE_OPERATION_PRIVATE(operation);

  if(priv->position + priv->length >
     inf_text_buffer_get_length(INF_TEXT_BUFFER(buffer)))
  {
    g_set_error(
      error,
      g_quark_from_static_string("INF_TEXT_OPERATION_ERROR"),
      INF_TEXT_OPERATION_ERROR_INVALID_DELETE,
      "%s",
      _("Attempt to remove text from after the end of the document")
    );

    return FALSE;
  }
  else
  {
    inf_text_buffer_erase_text(
      INF_TEXT_BUFFER(buffer),
      priv->position,
      priv->length,
      INF_USER(by)
    );
    
    return TRUE;
  }
}
Esempio n. 25
0
	void on_active_user_changed(InfUser* user)
	{
		if(m_active_user != NULL)
		{
			if(m_active)
				deactivate_user();

			g_signal_handler_disconnect(G_OBJECT(m_active_user),
			                            m_notify_status_handle);
		}

		m_active_user = user;

		if(user != NULL)
		{
			InfUserStatus user_status =
				inf_user_get_status(INF_USER(user));
			g_assert(user_status != INF_USER_UNAVAILABLE);

			m_notify_status_handle = g_signal_connect(
				G_OBJECT(user),
				"notify::status",
				G_CALLBACK(&on_user_notify_status_static),
				this
			);

			if( (user_status == INF_USER_ACTIVE && !m_active))
			{
				deactivate_user();
			}
			else if(user_status == INF_USER_INACTIVE && m_active)
			{
				activate_user();
			}
		}
	}
Esempio n. 26
0
static InfCommunicationScope
inf_adopted_session_process_xml_run(InfSession* session,
                                    InfXmlConnection* connection,
                                    const xmlNodePtr xml,
                                    GError** error)
{
  InfAdoptedSessionPrivate* priv;
  InfAdoptedSessionClass* session_class;
  InfAdoptedRequest* request;
  InfAdoptedUser* user;

  gboolean has_num;
  guint num;
  GError* local_error;
  InfAdoptedRequest* copy_req;
  guint i;

  priv = INF_ADOPTED_SESSION_PRIVATE(session);

  if(strcmp((const char*)xml->name, "request") == 0)
  {
    session_class = INF_ADOPTED_SESSION_GET_CLASS(session);
    g_assert(session_class->xml_to_request != NULL);

    user = inf_adopted_session_user_from_request_xml(
      INF_ADOPTED_SESSION(session),
      xml,
      error
    );

    if(user == NULL)
      return INF_COMMUNICATION_SCOPE_PTP;

    if(inf_user_get_status(INF_USER(user)) == INF_USER_UNAVAILABLE ||
       inf_user_get_connection(INF_USER(user)) != connection)
    {
      g_set_error(
        error,
        inf_user_error_quark(),
        INF_USER_ERROR_NOT_JOINED,
        "%s",
        _("User did not join from this connection")
      );

      return INF_COMMUNICATION_SCOPE_PTP;
    }

    local_error = NULL;
    has_num = inf_xml_util_get_attribute_uint(xml, "num", &num, &local_error);
    if(local_error != NULL)
    {
      g_propagate_error(error, local_error);
      return INF_COMMUNICATION_SCOPE_PTP;
    }
    
    request = session_class->xml_to_request(
      INF_ADOPTED_SESSION(session),
      xml,
      inf_adopted_user_get_vector(user),
      FALSE,
      error
    );

    if(request == NULL)
      return INF_COMMUNICATION_SCOPE_PTP;

    inf_adopted_algorithm_receive_request(priv->algorithm, request);

    /* Apply the request more than once if num is given. This is mostly used
     * for multiple undos and redos, but is in general allowed for any
     * request. */
    if(has_num)
    {
      for(i = 1; i < num; ++i)
      {
        /* TODO: This is a bit of a hack since requests are normally
         * immutable. It avoids an additional vector copy here though. */
        copy_req = inf_adopted_request_copy(request);

        inf_adopted_state_vector_add(
          inf_adopted_request_get_vector(copy_req),
          inf_user_get_id(INF_USER(user)),
          i
        );

        inf_adopted_algorithm_receive_request(priv->algorithm, copy_req);
        g_object_unref(copy_req);
      }
    }

    g_object_unref(request);

    /* Requests can always be forwarded since user is given. */
    return INF_COMMUNICATION_SCOPE_GROUP;
  }

  return INF_SESSION_CLASS(parent_class)->process_xml_run(
    session,
    connection,
    xml,
    error
  );
}
static gboolean
infinoted_plugin_transformation_protection_check_request_cb(InfAdoptedSession* session,
                                                            InfAdoptedRequest* request,
                                                            InfAdoptedUser* user,
                                                            gpointer user_data)
{
  InfinotedPluginTransformationProtectionSessionInfo* info;
  guint vdiff;
  InfXmlConnection* connection;
  gchar* request_str;
  gchar* current_str;
  gchar* remote_id;
  gchar* path;

  info = (InfinotedPluginTransformationProtectionSessionInfo*)user_data;

  vdiff = inf_adopted_state_vector_vdiff(
    inf_adopted_request_get_vector(request),
    inf_adopted_algorithm_get_current(
      inf_adopted_session_get_algorithm(session)
    )
  );

  if(vdiff > info->plugin->max_vdiff)
  {
    connection = inf_user_get_connection(INF_USER(user));

    /* Local requests do not need to be transformed, so always have a
     * zero vdiff. */
    g_assert(connection != NULL);

    /* Kill the connection */
    infd_session_proxy_unsubscribe(
      INFD_SESSION_PROXY(info->proxy),
      connection
    );

    /* Write a log message */
    path = inf_browser_get_path(
      INF_BROWSER(
        infinoted_plugin_manager_get_directory(info->plugin->manager)
      ),
      &info->iter
    );

    request_str = inf_adopted_state_vector_to_string(
      inf_adopted_request_get_vector(request)
    );

    current_str = inf_adopted_state_vector_to_string(
      inf_adopted_algorithm_get_current(
        inf_adopted_session_get_algorithm(session)
      )
    );

    g_object_get(G_OBJECT(connection), "remote-id", &remote_id, NULL);

    infinoted_log_warning(
      infinoted_plugin_manager_get_log(info->plugin->manager),
      _("In document \"%s\": Attempt to transform request \"%s\" to current state \"%s\" "
        "(vdiff=%u) by user \"%s\" (id=%u, conn=%s). Maximum allowed is %u; the "
        "connection has been unsubscribed."),
      path,
      request_str,
      current_str,
      vdiff,
      inf_user_get_name(INF_USER(user)),
      inf_user_get_id(INF_USER(user)),
      remote_id,
      info->plugin->max_vdiff
    );

    g_free(path);
    g_free(request_str);
    g_free(current_str);
    g_free(remote_id);

    /* Prevent the request from being transformed */
    return TRUE;
  }

  return FALSE;
}
Esempio n. 28
0
void Gobby::OperationOpen::read_finish()
{
	// If the last character is a newline character, remove it.
	GtkTextIter end_iter, test_iter;
	gtk_text_buffer_get_end_iter(m_content, &end_iter);
	test_iter = end_iter;
	if(gtk_text_iter_backward_char(&test_iter))
	{
		if(gtk_text_iter_get_char(&test_iter) == '\n')
		{
			gtk_text_buffer_delete(
				m_content, &test_iter, &end_iter);
		}
	}

	gtk_text_buffer_set_modified(m_content, FALSE);

	GtkTextIter insert_iter;
	GtkTextMark* insert = gtk_text_buffer_get_insert(m_content);
	gtk_text_buffer_get_iter_at_mark(m_content, &insert_iter, insert);

	InfUser* user = INF_USER(g_object_new(
		INF_TEXT_TYPE_USER,
		"id", 1,
		"flags", INF_USER_LOCAL,
		"name", m_preferences.user.name.get().c_str(),
		/* The user is made active when the user
		 * switches to the document. */
		"status", INF_USER_INACTIVE,
		"hue", m_preferences.user.hue.get(),
		"caret-position", gtk_text_iter_get_offset(&insert_iter),
		static_cast<void*>(NULL)));

	InfUserTable* user_table = inf_user_table_new();
	inf_user_table_add_user(user_table, user);
	g_object_unref(user);

	InfTextGtkBuffer* text_gtk_buffer =
		inf_text_gtk_buffer_new(m_content, user_table);
	g_object_unref(user_table);

	ConnectionManager& connection_manager =
		get_browser().get_connection_manager();
	InfCommunicationManager* communication_manager =
		connection_manager.get_communication_manager();

	InfBrowser* browser = m_parent.get_browser();

	InfIo* io;
	g_object_get(G_OBJECT(browser), "io", &io, NULL);

	InfTextSession* session = inf_text_session_new_with_user_table(
		communication_manager, INF_TEXT_BUFFER(text_gtk_buffer), io,
		user_table, INF_SESSION_RUNNING, NULL, NULL);

	g_object_unref(io);
	g_object_unref(text_gtk_buffer);

	InfRequest* request = inf_browser_add_note(
		m_parent.get_browser(), m_parent.get_browser_iter(),
		m_name.c_str(), "InfText", NULL,
		INF_SESSION(session), TRUE,
		on_request_finished_static, this);
	g_object_unref(session);

	if(request != NULL)
	{
		m_request = request;
		g_object_ref(m_request);

		// TODO: We can remove the node watch here, but need to have
		// the browser available in on_request_finished then. Maybe
		// just disconnect the signal, or bind it.
	}
}
static gboolean
infd_note_plugin_text_read_user(InfUserTable* user_table,
                                xmlNodePtr node,
                                GError** error)
{
  guint id;
  gdouble hue;
  xmlChar* name;
  gboolean result;
  InfUser* user;

  if(!inf_xml_util_get_attribute_uint_required(node, "id", &id, error))
    return FALSE;

  if(!inf_xml_util_get_attribute_double_required(node, "hue", &hue, error))
    return FALSE;

  name = inf_xml_util_get_attribute_required(node, "name", error);
  if(name == NULL)
    return FALSE;

  if(inf_user_table_lookup_user_by_id(user_table, id) != NULL)
  {
    g_set_error(
      error,
      g_quark_from_static_string("INF_NOTE_PLUGIN_TEXT_ERROR"),
      INFD_NOTE_PLUGIN_TEXT_ERROR_USER_EXISTS,
      "User with ID %u exists already",
      id
    );

    result = FALSE;
  }
  else
  {
    if(inf_user_table_lookup_user_by_name(user_table, (const gchar*)name))
    {
      g_set_error(
        error,
        g_quark_from_static_string("INF_NOTE_PLUGIN_TEXT_ERROR"),
        INFD_NOTE_PLUGIN_TEXT_ERROR_USER_EXISTS,
        "User with name `%s' exists already",
        (const gchar*)name
      );

      result = FALSE;
    }
    else
    {
      user = INF_USER(
        g_object_new(
          INF_TEXT_TYPE_USER,
          "id", id,
          "name", name,
          "hue", hue,
          NULL
        )
      );

      inf_user_table_add_user(user_table, user);
      g_object_unref(user);
      result = TRUE;
    }
  }

  xmlFree(name);
  return result;
}
static gboolean
perform_test(guint max_total_log_size,
             InfTextChunk* initial,
             GSList* users,
             GSList* requests,
             GError** error)
{
  InfTextBuffer* buffer;
  InfCommunicationManager* manager;
  InfIo* io;
  InfTextSession* session;
  InfAdoptedAlgorithm* algorithm;

  InfUserTable* user_table;
  InfTextUser* user;
  gchar* user_name;

  GSList* item;
  xmlNodePtr request;
  gboolean result;
  GError* local_error;
  
  guint verify_user_id;
  InfAdoptedUser* verify_user;
  guint verify_log_size;
  gint verify_can_undo;
  gint verify_can_redo;

  InfAdoptedRequestLog* log;
  guint log_size;

  buffer = INF_TEXT_BUFFER(inf_text_default_buffer_new("UTF-8"));
  inf_text_buffer_insert_chunk(buffer, 0, initial, NULL);

  manager = inf_communication_manager_new();
  io = INF_IO(inf_standalone_io_new());
  user_table = inf_user_table_new();
  local_error = NULL;

  for(item = users; item != NULL; item = g_slist_next(item))
  {
    user_name = g_strdup_printf("User_%u", GPOINTER_TO_UINT(item->data));

    user = INF_TEXT_USER(
      g_object_new(
        INF_TEXT_TYPE_USER,
        "id", GPOINTER_TO_UINT(item->data),
        "name", user_name,
        "status", INF_USER_ACTIVE,
        "flags", 0,
        NULL
      )
    );

    g_free(user_name);
    inf_user_table_add_user(user_table, INF_USER(user));
    g_object_unref(user);
  }

  session = INF_TEXT_SESSION(
    g_object_new(
      INF_TEXT_TYPE_SESSION,
      "communication-manager", manager,
      "buffer", buffer,
      "io", io,
      "user_table", user_table,
      "max-total-log-size", max_total_log_size,
      NULL
    )
  );
  
  algorithm = inf_adopted_session_get_algorithm(INF_ADOPTED_SESSION(session));

  g_object_unref(io);
  g_object_unref(manager);
  g_object_unref(user_table);
  g_object_unref(buffer);

  for(item = requests; item != NULL; item = item->next)
  {
    request = (xmlNodePtr)item->data;
    
    if(strcmp((const char*)request->name, "request") == 0)
    {
      /* Request */
      result = inf_communication_object_received(
        INF_COMMUNICATION_OBJECT(session),
        NULL,
        request,
        &local_error
      );
      
      if(local_error != NULL)
        goto fail;
    }
    else
    {
      /* TODO: Make an extra function out of this: */
      /* Verify */
      result = inf_xml_util_get_attribute_uint_required(
        request,
        "user",
        &verify_user_id,
        &local_error
      );
      
      if(result == FALSE)
        goto fail;

      verify_user = INF_ADOPTED_USER(
        inf_user_table_lookup_user_by_id(user_table, verify_user_id)
      );

      if(verify_user == NULL)
      {
        g_set_error(
          error,
          inf_test_text_cleanup_error_quark(),
          INF_TEST_TEXT_CLEANUP_USER_UNAVAILABLE,
          "User ID '%u' not available",
          verify_user_id
        );
        
        goto fail;
      }

      result = inf_xml_util_get_attribute_uint(
        request,
        "log-size",
        &verify_log_size,
        &local_error
      );

      if(local_error) goto fail;

      if(result)
      {
        log = inf_adopted_user_get_request_log(INF_ADOPTED_USER(verify_user));
        log_size = inf_adopted_request_log_get_end(log) -
          inf_adopted_request_log_get_begin(log);
        if(verify_log_size != log_size)
        {
          g_set_error(
            error,
            inf_test_text_cleanup_error_quark(),
            INF_TEST_TEXT_CLEANUP_VERIFY_FAILED,
            "Log size does not match; got %u, but expected %u",
            log_size,
            verify_log_size
          );

          goto fail;
        }
      }
      
      result = inf_xml_util_get_attribute_int(
        request,
        "can-undo",
        &verify_can_undo,
        &local_error
      );

      if(local_error) goto fail;

      if(result)
      {
        result = inf_adopted_algorithm_can_undo(algorithm, verify_user);
        if(result != verify_can_undo)
        {
          g_set_error(
            error,
            inf_test_text_cleanup_error_quark(),
            INF_TEST_TEXT_CLEANUP_VERIFY_FAILED,
            "can-undo does not match; got %d, but expected %d",
            (guint)result,
            verify_can_undo
          );

          goto fail;
        }
      }

      result = inf_xml_util_get_attribute_int(
        request,
        "can-redo",
        &verify_can_redo,
        &local_error
      );

      if(local_error) goto fail;

      if(result)
      {
        result = inf_adopted_algorithm_can_redo(algorithm, verify_user);
        if(result != verify_can_redo)
        {
          g_set_error(
            error,
            inf_test_text_cleanup_error_quark(),
            INF_TEST_TEXT_CLEANUP_VERIFY_FAILED,
            "can-redo does not match; got %d, but expected %d",
            (guint)result,
            verify_can_redo
          );

          goto fail;
        }
      }
    }
  }

  g_object_unref(session);
  return TRUE;

fail:
  g_object_unref(session);
  if(local_error) g_propagate_error(error, local_error);
  return FALSE;
}