2013-05-02 14:23:42 +00:00
|
|
|
/* GTK - The GIMP Toolkit
|
|
|
|
* Copyright (C) 2013 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 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/>.
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include "config.h"
|
|
|
|
|
2013-05-02 09:47:06 +00:00
|
|
|
#include "gtkdebug.h"
|
2013-05-02 14:23:42 +00:00
|
|
|
#include "gtkpixelcacheprivate.h"
|
|
|
|
|
2013-05-07 12:27:17 +00:00
|
|
|
#define BLOW_CACHE_TIMEOUT_SEC 20
|
|
|
|
|
2013-05-02 14:23:42 +00:00
|
|
|
/* The extra size of the offscreen surface we allocate
|
|
|
|
to make scrolling more efficient */
|
2013-08-25 04:08:41 +00:00
|
|
|
#define DEFAULT_EXTRA_SIZE 64
|
2013-05-02 14:23:42 +00:00
|
|
|
|
2013-08-26 19:13:05 +00:00
|
|
|
/* When resizing viewport we allow this extra
|
2013-05-02 14:23:42 +00:00
|
|
|
size to avoid constantly reallocating when resizing */
|
2013-08-26 19:13:05 +00:00
|
|
|
#define ALLOW_SMALLER_SIZE 32
|
2013-05-02 14:23:42 +00:00
|
|
|
#define ALLOW_LARGER_SIZE 32
|
|
|
|
|
|
|
|
struct _GtkPixelCache {
|
|
|
|
cairo_surface_t *surface;
|
2013-08-25 04:08:41 +00:00
|
|
|
cairo_content_t content;
|
2013-05-02 14:23:42 +00:00
|
|
|
|
|
|
|
/* Valid if surface != NULL */
|
|
|
|
int surface_x;
|
|
|
|
int surface_y;
|
|
|
|
int surface_w;
|
|
|
|
int surface_h;
|
2013-06-03 15:04:03 +00:00
|
|
|
double surface_scale;
|
2013-05-02 14:23:42 +00:00
|
|
|
|
|
|
|
/* may be null if not dirty */
|
|
|
|
cairo_region_t *surface_dirty;
|
2013-05-07 12:27:17 +00:00
|
|
|
|
|
|
|
guint timeout_tag;
|
2013-08-25 04:08:41 +00:00
|
|
|
|
|
|
|
guint extra_width;
|
|
|
|
guint extra_height;
|
2013-05-02 14:23:42 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
GtkPixelCache *
|
|
|
|
_gtk_pixel_cache_new ()
|
|
|
|
{
|
|
|
|
GtkPixelCache *cache;
|
|
|
|
|
|
|
|
cache = g_new0 (GtkPixelCache, 1);
|
2013-08-25 04:08:41 +00:00
|
|
|
cache->extra_width = DEFAULT_EXTRA_SIZE;
|
|
|
|
cache->extra_height = DEFAULT_EXTRA_SIZE;
|
2013-05-02 14:23:42 +00:00
|
|
|
|
|
|
|
return cache;
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
_gtk_pixel_cache_free (GtkPixelCache *cache)
|
|
|
|
{
|
|
|
|
if (cache == NULL)
|
|
|
|
return;
|
|
|
|
|
2013-05-07 12:27:17 +00:00
|
|
|
if (cache->timeout_tag)
|
|
|
|
g_source_remove (cache->timeout_tag);
|
|
|
|
|
2013-05-02 14:23:42 +00:00
|
|
|
if (cache->surface != NULL)
|
|
|
|
cairo_surface_destroy (cache->surface);
|
|
|
|
|
|
|
|
if (cache->surface_dirty != NULL)
|
|
|
|
cairo_region_destroy (cache->surface_dirty);
|
|
|
|
|
|
|
|
g_free (cache);
|
|
|
|
}
|
|
|
|
|
2013-08-25 04:08:41 +00:00
|
|
|
void
|
|
|
|
_gtk_pixel_cache_set_extra_size (GtkPixelCache *cache,
|
|
|
|
guint extra_width,
|
|
|
|
guint extra_height)
|
|
|
|
{
|
|
|
|
cache->extra_width = extra_width ? extra_width : DEFAULT_EXTRA_SIZE;
|
|
|
|
cache->extra_height = extra_height ? extra_height : DEFAULT_EXTRA_SIZE;
|
|
|
|
}
|
|
|
|
|
2013-09-06 22:49:50 +00:00
|
|
|
void
|
|
|
|
_gtk_pixel_cache_get_extra_size (GtkPixelCache *cache,
|
|
|
|
guint *extra_width,
|
|
|
|
guint *extra_height)
|
|
|
|
{
|
|
|
|
if (extra_width)
|
|
|
|
*extra_width = cache->extra_width;
|
|
|
|
|
|
|
|
if (extra_height)
|
|
|
|
*extra_height = cache->extra_height;
|
|
|
|
}
|
|
|
|
|
2013-08-25 04:08:41 +00:00
|
|
|
void
|
|
|
|
_gtk_pixel_cache_set_content (GtkPixelCache *cache,
|
|
|
|
cairo_content_t content)
|
|
|
|
{
|
|
|
|
cache->content = content;
|
|
|
|
_gtk_pixel_cache_invalidate (cache, NULL);
|
|
|
|
}
|
|
|
|
|
2013-05-02 14:23:42 +00:00
|
|
|
/* Region is in canvas coordinates */
|
|
|
|
void
|
|
|
|
_gtk_pixel_cache_invalidate (GtkPixelCache *cache,
|
|
|
|
cairo_region_t *region)
|
|
|
|
{
|
|
|
|
cairo_rectangle_int_t r;
|
|
|
|
cairo_region_t *free_region = NULL;
|
|
|
|
|
|
|
|
if (cache->surface == NULL ||
|
|
|
|
(region != NULL && cairo_region_is_empty (region)))
|
|
|
|
return;
|
|
|
|
|
|
|
|
if (region == NULL)
|
|
|
|
{
|
|
|
|
r.x = cache->surface_x;
|
|
|
|
r.y = cache->surface_y;
|
|
|
|
r.width = cache->surface_w;
|
|
|
|
r.height = cache->surface_h;
|
|
|
|
|
|
|
|
free_region = region =
|
|
|
|
cairo_region_create_rectangle (&r);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (cache->surface_dirty == NULL)
|
|
|
|
{
|
|
|
|
cache->surface_dirty = cairo_region_copy (region);
|
|
|
|
cairo_region_translate (cache->surface_dirty,
|
|
|
|
-cache->surface_x,
|
|
|
|
-cache->surface_y);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
cairo_region_translate (region,
|
|
|
|
-cache->surface_x,
|
|
|
|
-cache->surface_y);
|
|
|
|
cairo_region_union (cache->surface_dirty, region);
|
|
|
|
cairo_region_translate (region,
|
|
|
|
cache->surface_x,
|
|
|
|
cache->surface_y);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (free_region)
|
|
|
|
cairo_region_destroy (free_region);
|
|
|
|
|
|
|
|
r.x = 0;
|
|
|
|
r.y = 0;
|
|
|
|
r.width = cache->surface_w;
|
|
|
|
r.height = cache->surface_h;
|
|
|
|
|
|
|
|
cairo_region_intersect_rectangle (cache->surface_dirty, &r);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
_gtk_pixel_cache_create_surface_if_needed (GtkPixelCache *cache,
|
|
|
|
GdkWindow *window,
|
|
|
|
cairo_rectangle_int_t *view_rect,
|
|
|
|
cairo_rectangle_int_t *canvas_rect)
|
|
|
|
{
|
|
|
|
cairo_rectangle_int_t rect;
|
|
|
|
int surface_w, surface_h;
|
|
|
|
cairo_content_t content;
|
|
|
|
cairo_pattern_t *bg;
|
|
|
|
double red, green, blue, alpha;
|
|
|
|
|
2013-09-09 09:07:50 +00:00
|
|
|
#ifdef G_ENABLE_DEBUG
|
|
|
|
if (gtk_get_debug_flags () & GTK_DEBUG_NO_PIXEL_CACHE)
|
|
|
|
return;
|
|
|
|
#endif
|
|
|
|
|
2013-08-25 04:08:41 +00:00
|
|
|
content = cache->content;
|
|
|
|
if (!content)
|
|
|
|
{
|
|
|
|
content = CAIRO_CONTENT_COLOR_ALPHA;
|
|
|
|
bg = gdk_window_get_background_pattern (window);
|
|
|
|
if (bg != NULL &&
|
|
|
|
cairo_pattern_get_type (bg) == CAIRO_PATTERN_TYPE_SOLID &&
|
|
|
|
cairo_pattern_get_rgba (bg, &red, &green, &blue, &alpha) == CAIRO_STATUS_SUCCESS &&
|
|
|
|
alpha == 1.0)
|
|
|
|
content = CAIRO_CONTENT_COLOR;
|
|
|
|
}
|
2013-05-02 14:23:42 +00:00
|
|
|
|
|
|
|
surface_w = view_rect->width;
|
|
|
|
if (canvas_rect->width > surface_w)
|
2013-08-25 04:08:41 +00:00
|
|
|
surface_w = MIN (surface_w + cache->extra_width, canvas_rect->width);
|
2013-05-02 14:23:42 +00:00
|
|
|
|
|
|
|
surface_h = view_rect->height;
|
|
|
|
if (canvas_rect->height > surface_h)
|
2013-08-25 04:08:41 +00:00
|
|
|
surface_h = MIN (surface_h + cache->extra_height, canvas_rect->height);
|
2013-05-02 14:23:42 +00:00
|
|
|
|
|
|
|
/* If current surface can't fit view_rect or is too large, kill it */
|
|
|
|
if (cache->surface != NULL &&
|
|
|
|
(cairo_surface_get_content (cache->surface) != content ||
|
2013-08-26 19:13:05 +00:00
|
|
|
cache->surface_w < MAX(view_rect->width, surface_w - ALLOW_SMALLER_SIZE) ||
|
2013-05-02 14:23:42 +00:00
|
|
|
cache->surface_w > surface_w + ALLOW_LARGER_SIZE ||
|
2013-08-26 19:13:05 +00:00
|
|
|
cache->surface_h < MAX(view_rect->height, surface_h - ALLOW_SMALLER_SIZE) ||
|
2013-06-03 15:04:03 +00:00
|
|
|
cache->surface_h > surface_h + ALLOW_LARGER_SIZE ||
|
|
|
|
cache->surface_scale != gdk_window_get_scale_factor (window)))
|
2013-05-02 14:23:42 +00:00
|
|
|
{
|
|
|
|
cairo_surface_destroy (cache->surface);
|
|
|
|
cache->surface = NULL;
|
|
|
|
if (cache->surface_dirty)
|
|
|
|
cairo_region_destroy (cache->surface_dirty);
|
|
|
|
cache->surface_dirty = NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Don't allocate a surface if view >= canvas, as we won't
|
|
|
|
be scrolling then anyway */
|
|
|
|
if (cache->surface == NULL &&
|
|
|
|
(view_rect->width < canvas_rect->width ||
|
|
|
|
view_rect->height < canvas_rect->height))
|
|
|
|
{
|
|
|
|
cache->surface_x = -canvas_rect->x;
|
|
|
|
cache->surface_y = -canvas_rect->y;
|
|
|
|
cache->surface_w = surface_w;
|
|
|
|
cache->surface_h = surface_h;
|
2013-06-03 15:04:03 +00:00
|
|
|
cache->surface_scale = gdk_window_get_scale_factor (window);
|
2013-05-02 14:23:42 +00:00
|
|
|
|
|
|
|
cache->surface =
|
|
|
|
gdk_window_create_similar_surface (window, content,
|
|
|
|
surface_w, surface_h);
|
|
|
|
rect.x = 0;
|
|
|
|
rect.y = 0;
|
|
|
|
rect.width = surface_w;
|
|
|
|
rect.height = surface_h;
|
|
|
|
cache->surface_dirty =
|
|
|
|
cairo_region_create_rectangle (&rect);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
_gtk_pixel_cache_set_position (GtkPixelCache *cache,
|
|
|
|
cairo_rectangle_int_t *view_rect,
|
|
|
|
cairo_rectangle_int_t *canvas_rect)
|
|
|
|
{
|
|
|
|
cairo_rectangle_int_t r, view_pos;
|
|
|
|
cairo_region_t *copy_region;
|
|
|
|
int new_surf_x, new_surf_y;
|
|
|
|
cairo_t *backing_cr;
|
|
|
|
|
|
|
|
if (cache->surface == NULL)
|
|
|
|
return;
|
|
|
|
|
|
|
|
/* Position of view inside canvas */
|
|
|
|
view_pos.x = -canvas_rect->x;
|
|
|
|
view_pos.y = -canvas_rect->y;
|
|
|
|
view_pos.width = view_rect->width;
|
|
|
|
view_pos.height = view_rect->height;
|
|
|
|
|
|
|
|
/* Reposition so all is visible */
|
|
|
|
if (view_pos.x < cache->surface_x ||
|
|
|
|
view_pos.x + view_pos.width >
|
|
|
|
cache->surface_x + cache->surface_w ||
|
|
|
|
view_pos.y < cache->surface_y ||
|
|
|
|
view_pos.y + view_pos.height >
|
|
|
|
cache->surface_y + cache->surface_h)
|
|
|
|
{
|
|
|
|
new_surf_x = cache->surface_x;
|
|
|
|
if (view_pos.x < cache->surface_x)
|
|
|
|
new_surf_x = MAX (view_pos.x + view_pos.width - cache->surface_w, 0);
|
|
|
|
else if (view_pos.x + view_pos.width >
|
|
|
|
cache->surface_x + cache->surface_w)
|
|
|
|
new_surf_x = MIN (view_pos.x, canvas_rect->width - cache->surface_w);
|
|
|
|
|
|
|
|
new_surf_y = cache->surface_y;
|
|
|
|
if (view_pos.y < cache->surface_y)
|
|
|
|
new_surf_y = MAX (view_pos.y + view_pos.height - cache->surface_h, 0);
|
|
|
|
else if (view_pos.y + view_pos.height >
|
|
|
|
cache->surface_y + cache->surface_h)
|
|
|
|
new_surf_y = MIN (view_pos.y, canvas_rect->height - cache->surface_h);
|
|
|
|
|
|
|
|
r.x = 0;
|
|
|
|
r.y = 0;
|
|
|
|
r.width = cache->surface_w;
|
|
|
|
r.height = cache->surface_h;
|
|
|
|
copy_region = cairo_region_create_rectangle (&r);
|
|
|
|
|
|
|
|
if (cache->surface_dirty)
|
|
|
|
{
|
|
|
|
cairo_region_subtract (copy_region, cache->surface_dirty);
|
|
|
|
cairo_region_destroy (cache->surface_dirty);
|
|
|
|
cache->surface_dirty = NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
cairo_region_translate (copy_region,
|
|
|
|
cache->surface_x - new_surf_x,
|
|
|
|
cache->surface_y - new_surf_y);
|
|
|
|
cairo_region_intersect_rectangle (copy_region, &r);
|
|
|
|
|
|
|
|
backing_cr = cairo_create (cache->surface);
|
|
|
|
gdk_cairo_region (backing_cr, copy_region);
|
|
|
|
cairo_set_operator (backing_cr, CAIRO_OPERATOR_SOURCE);
|
|
|
|
cairo_clip (backing_cr);
|
|
|
|
cairo_push_group (backing_cr);
|
|
|
|
cairo_set_source_surface (backing_cr, cache->surface,
|
|
|
|
cache->surface_x - new_surf_x,
|
|
|
|
cache->surface_y - new_surf_y);
|
|
|
|
cairo_paint (backing_cr);
|
|
|
|
cairo_pop_group_to_source (backing_cr);
|
|
|
|
cairo_paint (backing_cr);
|
|
|
|
cairo_destroy (backing_cr);
|
|
|
|
|
|
|
|
cache->surface_x = new_surf_x;
|
|
|
|
cache->surface_y = new_surf_y;
|
|
|
|
|
|
|
|
cairo_region_xor_rectangle (copy_region, &r);
|
|
|
|
cache->surface_dirty = copy_region;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
_gtk_pixel_cache_repaint (GtkPixelCache *cache,
|
|
|
|
GtkPixelCacheDrawFunc draw,
|
|
|
|
cairo_rectangle_int_t *view_rect,
|
|
|
|
cairo_rectangle_int_t *canvas_rect,
|
|
|
|
gpointer user_data)
|
|
|
|
{
|
|
|
|
cairo_t *backing_cr;
|
2013-11-05 18:21:28 +00:00
|
|
|
cairo_region_t *region_dirty = cache->surface_dirty;
|
|
|
|
cache->surface_dirty = NULL;
|
2013-05-02 14:23:42 +00:00
|
|
|
|
|
|
|
if (cache->surface &&
|
2013-11-05 18:21:28 +00:00
|
|
|
region_dirty &&
|
|
|
|
!cairo_region_is_empty (region_dirty))
|
2013-05-02 14:23:42 +00:00
|
|
|
{
|
|
|
|
backing_cr = cairo_create (cache->surface);
|
2013-11-05 18:21:28 +00:00
|
|
|
gdk_cairo_region (backing_cr, region_dirty);
|
2013-05-02 14:23:42 +00:00
|
|
|
cairo_clip (backing_cr);
|
|
|
|
cairo_translate (backing_cr,
|
|
|
|
-cache->surface_x - canvas_rect->x - view_rect->x,
|
|
|
|
-cache->surface_y - canvas_rect->y - view_rect->y);
|
2014-01-08 09:44:32 +00:00
|
|
|
|
|
|
|
cairo_save (backing_cr);
|
2013-05-02 14:23:42 +00:00
|
|
|
cairo_set_source_rgba (backing_cr,
|
|
|
|
0.0, 0, 0, 0.0);
|
|
|
|
cairo_set_operator (backing_cr, CAIRO_OPERATOR_SOURCE);
|
|
|
|
cairo_paint (backing_cr);
|
2014-01-08 09:44:32 +00:00
|
|
|
cairo_restore (backing_cr);
|
2013-05-02 14:23:42 +00:00
|
|
|
|
2013-05-02 09:47:06 +00:00
|
|
|
cairo_save (backing_cr);
|
2013-05-02 14:23:42 +00:00
|
|
|
draw (backing_cr, user_data);
|
2013-05-02 09:47:06 +00:00
|
|
|
cairo_restore (backing_cr);
|
|
|
|
|
|
|
|
#ifdef G_ENABLE_DEBUG
|
|
|
|
if (gtk_get_debug_flags () & GTK_DEBUG_PIXEL_CACHE)
|
|
|
|
{
|
|
|
|
GdkRGBA colors[] = {
|
|
|
|
{ 1, 0, 0, 0.08},
|
|
|
|
{ 0, 1, 0, 0.08},
|
|
|
|
{ 0, 0, 1, 0.08},
|
|
|
|
{ 1, 0, 1, 0.08},
|
|
|
|
{ 1, 1, 0, 0.08},
|
|
|
|
{ 0, 1, 1, 0.08},
|
|
|
|
};
|
|
|
|
static int current_color = 0;
|
|
|
|
|
|
|
|
gdk_cairo_set_source_rgba (backing_cr, &colors[(current_color++) % G_N_ELEMENTS (colors)]);
|
|
|
|
cairo_paint (backing_cr);
|
|
|
|
}
|
|
|
|
#endif
|
2013-05-02 14:23:42 +00:00
|
|
|
|
|
|
|
cairo_destroy (backing_cr);
|
|
|
|
}
|
|
|
|
|
2013-11-05 18:21:28 +00:00
|
|
|
if (region_dirty)
|
|
|
|
cairo_region_destroy (region_dirty);
|
2013-05-02 14:23:42 +00:00
|
|
|
}
|
|
|
|
|
2013-05-07 12:27:17 +00:00
|
|
|
static gboolean
|
|
|
|
blow_cache_cb (gpointer user_data)
|
|
|
|
{
|
|
|
|
GtkPixelCache *cache = user_data;
|
|
|
|
|
|
|
|
cache->timeout_tag = 0;
|
|
|
|
|
|
|
|
if (cache->surface)
|
|
|
|
{
|
|
|
|
cairo_surface_destroy (cache->surface);
|
|
|
|
cache->surface = NULL;
|
|
|
|
if (cache->surface_dirty)
|
|
|
|
cairo_region_destroy (cache->surface_dirty);
|
|
|
|
cache->surface_dirty = NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
return G_SOURCE_REMOVE;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2013-05-02 14:23:42 +00:00
|
|
|
void
|
|
|
|
_gtk_pixel_cache_draw (GtkPixelCache *cache,
|
|
|
|
cairo_t *cr,
|
|
|
|
GdkWindow *window,
|
|
|
|
/* View position in widget coords */
|
|
|
|
cairo_rectangle_int_t *view_rect,
|
|
|
|
/* Size and position of canvas in view coords */
|
|
|
|
cairo_rectangle_int_t *canvas_rect,
|
|
|
|
GtkPixelCacheDrawFunc draw,
|
|
|
|
gpointer user_data)
|
|
|
|
{
|
2013-05-07 12:27:17 +00:00
|
|
|
if (cache->timeout_tag)
|
|
|
|
g_source_remove (cache->timeout_tag);
|
|
|
|
|
|
|
|
cache->timeout_tag = g_timeout_add_seconds (BLOW_CACHE_TIMEOUT_SEC,
|
|
|
|
blow_cache_cb, cache);
|
2013-10-22 13:43:43 +00:00
|
|
|
g_source_set_name_by_id (cache->timeout_tag, "[gtk+] blow_cache_cb");
|
2013-05-07 12:27:17 +00:00
|
|
|
|
2013-05-02 14:23:42 +00:00
|
|
|
_gtk_pixel_cache_create_surface_if_needed (cache, window,
|
|
|
|
view_rect, canvas_rect);
|
|
|
|
_gtk_pixel_cache_set_position (cache, view_rect, canvas_rect);
|
|
|
|
_gtk_pixel_cache_repaint (cache, draw, view_rect, canvas_rect, user_data);
|
|
|
|
|
|
|
|
if (cache->surface &&
|
|
|
|
/* Don't use backing surface if rendering elsewhere */
|
|
|
|
cairo_surface_get_type (cache->surface) == cairo_surface_get_type (cairo_get_target (cr)))
|
|
|
|
{
|
|
|
|
cairo_save (cr);
|
|
|
|
cairo_set_source_surface (cr, cache->surface,
|
|
|
|
cache->surface_x + view_rect->x + canvas_rect->x,
|
|
|
|
cache->surface_y + view_rect->y + canvas_rect->y);
|
|
|
|
cairo_rectangle (cr, view_rect->x, view_rect->y,
|
|
|
|
view_rect->width, view_rect->height);
|
|
|
|
cairo_fill (cr);
|
|
|
|
cairo_restore (cr);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
cairo_rectangle (cr,
|
|
|
|
view_rect->x, view_rect->y,
|
|
|
|
view_rect->width, view_rect->height);
|
|
|
|
cairo_clip (cr);
|
|
|
|
draw (cr, user_data);
|
|
|
|
}
|
|
|
|
}
|