4 * Pidgin is the legal property of its developers, whose names are too numerous
5 * to list here. Please refer to the COPYRIGHT file distributed with this
8 * This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
18 * You should have received a copy of the GNU General Public License
19 * along with this program; if not, write to the Free Software
20 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA
25 #include "buddylist.h"
28 #include "whiteboard.h"
30 #include "gtk3compat.h"
31 #include "gtkwhiteboard.h"
35 PIDGIN_WHITEBOARD_BRUSH_UP
,
36 PIDGIN_WHITEBOARD_BRUSH_DOWN
,
37 PIDGIN_WHITEBOARD_BRUSH_MOTION
38 } PidginWhiteboardBrushState
;
40 #define PIDGIN_TYPE_WHITEBOARD (pidgin_whiteboard_get_type())
41 G_DECLARE_FINAL_TYPE(PidginWhiteboard
, pidgin_whiteboard
, PIDGIN
, WHITEBOARD
,
46 * @cr: Cairo context for drawing
47 * @surface: Cairo surface for drawing
48 * @wb: Backend data for this whiteboard
49 * @drawing_area: Drawing area
50 * @color_button: A color chooser widget
51 * @width: Canvas width
52 * @height: Canvas height
53 * @brush_color: Foreground color
54 * @brush_size: Brush size
55 * @brush_state: The @PidginWhiteboardBrushState state of the brush
59 struct _PidginWhiteboard
64 cairo_surface_t
*surface
;
68 GtkWidget
*drawing_area
;
69 GtkWidget
*color_button
;
75 PidginWhiteboardBrushState brush_state
;
77 /* Tracks last position of the mouse when drawing */
80 /* Tracks how many brush motions made */
84 G_DEFINE_TYPE(PidginWhiteboard
, pidgin_whiteboard
, GTK_TYPE_WINDOW
)
86 /******************************************************************************
88 *****************************************************************************/
90 pidgin_whiteboard_rgb24_to_rgba(int color_rgb
, GdkRGBA
*color
)
92 color
->red
= ((color_rgb
>> 16) & 0xFF) / 255.0f
;
93 color
->green
= ((color_rgb
>> 8) & 0xFF) / 255.0f
;
94 color
->blue
= (color_rgb
& 0xFF) / 255.0f
;
99 whiteboard_close_cb(GtkWidget
*widget
, GdkEvent
*event
,
100 G_GNUC_UNUSED gpointer data
)
102 PidginWhiteboard
*gtkwb
= PIDGIN_WHITEBOARD(widget
);
104 g_clear_object(>kwb
->wb
);
109 static gboolean
pidgin_whiteboard_configure_event(GtkWidget
*widget
, GdkEventConfigure
*event
, gpointer data
)
111 PidginWhiteboard
*gtkwb
= (PidginWhiteboard
*)data
;
113 GtkAllocation allocation
;
114 GdkRGBA white
= {1.0, 1.0, 1.0, 1.0};
117 cairo_destroy(gtkwb
->cr
);
119 if (gtkwb
->surface
) {
120 cairo_surface_destroy(gtkwb
->surface
);
123 gtk_widget_get_allocation(widget
, &allocation
);
125 gtkwb
->surface
= cairo_image_surface_create(
126 CAIRO_FORMAT_RGB24
, allocation
.width
, allocation
.height
);
127 gtkwb
->cr
= cr
= cairo_create(gtkwb
->surface
);
128 gdk_cairo_set_source_rgba(cr
, &white
);
129 cairo_rectangle(cr
, 0, 0, allocation
.width
, allocation
.height
);
136 pidgin_whiteboard_draw_event(GtkWidget
*widget
, cairo_t
*cr
,
139 PidginWhiteboard
*gtkwb
= _gtkwb
;
141 cairo_set_source_surface(cr
, gtkwb
->surface
, 0, 0);
148 pidgin_whiteboard_set_canvas_as_icon(PidginWhiteboard
*gtkwb
)
152 /* Makes an icon from the whiteboard's canvas 'image' */
153 pixbuf
= gdk_pixbuf_get_from_surface(gtkwb
->surface
, 0, 0, gtkwb
->width
,
155 gtk_window_set_icon(GTK_WINDOW(gtkwb
), pixbuf
);
156 g_object_unref(pixbuf
);
160 pidgin_whiteboard_draw_brush_point(PurpleWhiteboard
*wb
, int x
, int y
,
163 PidginWhiteboard
*gtkwb
= purple_whiteboard_get_ui_data(wb
);
164 GtkWidget
*widget
= gtkwb
->drawing_area
;
165 cairo_t
*gfx_con
= gtkwb
->cr
;
168 /* Interpret and convert color */
169 pidgin_whiteboard_rgb24_to_rgba(color
, &rgba
);
170 gdk_cairo_set_source_rgba(gfx_con
, &rgba
);
173 cairo_arc(gfx_con
, x
, y
, size
/ 2.0, 0.0, 2.0 * M_PI
);
176 gtk_widget_queue_draw_area(widget
, x
- size
/ 2, y
- size
/ 2, size
,
179 /* Uses Bresenham's algorithm (as provided by Wikipedia) */
181 pidgin_whiteboard_draw_brush_line(PurpleWhiteboard
*wb
, int x0
, int y0
, int x1
,
182 int y1
, int color
, int size
)
198 gboolean steep
= abs(y1
- y0
) > abs(x1
- x0
);
231 pidgin_whiteboard_draw_brush_point(wb
, y
, x
, color
, size
);
233 pidgin_whiteboard_draw_brush_point(wb
, x
, y
, color
, size
);
240 if ((error
* 2) >= dx
) {
246 pidgin_whiteboard_draw_brush_point(wb
, y
, x
, color
,
249 pidgin_whiteboard_draw_brush_point(wb
, x
, y
, color
,
255 static gboolean
pidgin_whiteboard_brush_down(GtkWidget
*widget
, GdkEventButton
*event
, gpointer data
)
257 PidginWhiteboard
*gtkwb
= (PidginWhiteboard
*)data
;
259 PurpleWhiteboard
*wb
= gtkwb
->wb
;
260 GList
*draw_list
= purple_whiteboard_get_draw_list(wb
);
262 if (gtkwb
->brush_state
!= PIDGIN_WHITEBOARD_BRUSH_UP
) {
263 /* Potential double-click DOWN to DOWN? */
264 gtkwb
->brush_state
= PIDGIN_WHITEBOARD_BRUSH_DOWN
;
269 gtkwb
->brush_state
= PIDGIN_WHITEBOARD_BRUSH_DOWN
;
271 if (event
->button
== GDK_BUTTON_PRIMARY
&& gtkwb
->cr
!= NULL
) {
272 /* Check if draw_list has contents; if so, clear it */
275 purple_whiteboard_draw_list_destroy(draw_list
);
279 /* Set tracking variables */
280 gtkwb
->last_x
= event
->x
;
281 gtkwb
->last_y
= event
->y
;
283 gtkwb
->motion_count
= 0;
285 draw_list
= g_list_append(draw_list
,
286 GINT_TO_POINTER(gtkwb
->last_x
));
287 draw_list
= g_list_append(draw_list
,
288 GINT_TO_POINTER(gtkwb
->last_y
));
290 pidgin_whiteboard_draw_brush_point(gtkwb
->wb
,
292 gtkwb
->brush_color
, gtkwb
->brush_size
);
295 purple_whiteboard_set_draw_list(wb
, draw_list
);
300 static gboolean
pidgin_whiteboard_brush_motion(GtkWidget
*widget
, GdkEventMotion
*event
, gpointer data
)
307 GdkModifierType state
;
309 PidginWhiteboard
*gtkwb
= (PidginWhiteboard
*)data
;
311 PurpleWhiteboard
*wb
= gtkwb
->wb
;
312 GList
*draw_list
= purple_whiteboard_get_draw_list(wb
);
315 gdk_window_get_device_position(event
->window
, event
->device
, &x
, &y
,
321 state
= event
->state
;
324 if (state
& GDK_BUTTON1_MASK
&& gtkwb
->cr
!= NULL
) {
325 if ((gtkwb
->brush_state
!= PIDGIN_WHITEBOARD_BRUSH_DOWN
) &&
326 (gtkwb
->brush_state
!= PIDGIN_WHITEBOARD_BRUSH_MOTION
)) {
329 "***Bad brush state transition %d to MOTION\n",
332 gtkwb
->brush_state
= PIDGIN_WHITEBOARD_BRUSH_MOTION
;
336 gtkwb
->brush_state
= PIDGIN_WHITEBOARD_BRUSH_MOTION
;
338 dx
= x
- gtkwb
->last_x
;
339 dy
= y
- gtkwb
->last_y
;
341 gtkwb
->motion_count
++;
343 /* NOTE 100 is a temporary constant for how many deltas/motions in a
344 * stroke (needs UI Ops?)
346 if (gtkwb
->motion_count
== 100) {
347 draw_list
= g_list_append(draw_list
, GINT_TO_POINTER(dx
));
348 draw_list
= g_list_append(draw_list
, GINT_TO_POINTER(dy
));
350 /* Send draw list to the draw_list handler */
351 purple_whiteboard_send_draw_list(gtkwb
->wb
, draw_list
);
353 /* The brush stroke is finished, clear the list for another one */
356 purple_whiteboard_draw_list_destroy(draw_list
);
360 /* Reset motion tracking */
361 gtkwb
->motion_count
= 0;
363 draw_list
= g_list_append(
364 draw_list
, GINT_TO_POINTER(gtkwb
->last_x
));
365 draw_list
= g_list_append(
366 draw_list
, GINT_TO_POINTER(gtkwb
->last_y
));
368 dx
= x
- gtkwb
->last_x
;
369 dy
= y
- gtkwb
->last_y
;
372 draw_list
= g_list_append(draw_list
, GINT_TO_POINTER(dx
));
373 draw_list
= g_list_append(draw_list
, GINT_TO_POINTER(dy
));
375 pidgin_whiteboard_draw_brush_line(
376 gtkwb
->wb
, gtkwb
->last_x
, gtkwb
->last_y
, x
, y
,
377 gtkwb
->brush_color
, gtkwb
->brush_size
);
379 /* Set tracking variables */
384 purple_whiteboard_set_draw_list(wb
, draw_list
);
389 static gboolean
pidgin_whiteboard_brush_up(GtkWidget
*widget
, GdkEventButton
*event
, gpointer data
)
391 PidginWhiteboard
*gtkwb
= (PidginWhiteboard
*)data
;
393 PurpleWhiteboard
*wb
= gtkwb
->wb
;
394 GList
*draw_list
= purple_whiteboard_get_draw_list(wb
);
396 if ((gtkwb
->brush_state
!= PIDGIN_WHITEBOARD_BRUSH_DOWN
) &&
397 (gtkwb
->brush_state
!= PIDGIN_WHITEBOARD_BRUSH_MOTION
)) {
398 purple_debug_error("gtkwhiteboard",
399 "***Bad brush state transition %d to UP\n",
402 gtkwb
->brush_state
= PIDGIN_WHITEBOARD_BRUSH_UP
;
406 gtkwb
->brush_state
= PIDGIN_WHITEBOARD_BRUSH_UP
;
408 if (event
->button
== GDK_BUTTON_PRIMARY
&& gtkwb
->cr
!= NULL
) {
409 /* If the brush was never moved, express two sets of two deltas That's a
410 * 'point,' but not for Yahoo!
412 if (gtkwb
->motion_count
== 0) {
415 /* For Yahoo!, a (0 0) indicates the end of drawing */
416 /* FIXME: Yahoo Doodle specific! */
417 for (index
= 0; index
< 2; index
++) {
418 draw_list
= g_list_append(draw_list
, 0);
419 draw_list
= g_list_append(draw_list
, 0);
423 /* Send draw list to protocol draw_list handler */
424 purple_whiteboard_send_draw_list(gtkwb
->wb
, draw_list
);
426 pidgin_whiteboard_set_canvas_as_icon(gtkwb
);
428 /* The brush stroke is finished, clear the list for another one
431 purple_whiteboard_draw_list_destroy(draw_list
);
434 purple_whiteboard_set_draw_list(wb
, NULL
);
440 static void pidgin_whiteboard_set_dimensions(PurpleWhiteboard
*wb
, int width
, int height
)
442 PidginWhiteboard
*gtkwb
= purple_whiteboard_get_ui_data(wb
);
444 gtkwb
->width
= width
;
445 gtkwb
->height
= height
;
448 static void pidgin_whiteboard_set_brush(PurpleWhiteboard
*wb
, int size
, int color
)
450 PidginWhiteboard
*gtkwb
= purple_whiteboard_get_ui_data(wb
);
452 gtkwb
->brush_size
= size
;
453 gtkwb
->brush_color
= color
;
456 static void pidgin_whiteboard_clear(PurpleWhiteboard
*wb
)
458 PidginWhiteboard
*gtkwb
= purple_whiteboard_get_ui_data(wb
);
459 GtkWidget
*drawing_area
= gtkwb
->drawing_area
;
460 cairo_t
*cr
= gtkwb
->cr
;
461 GtkAllocation allocation
;
462 GdkRGBA white
= {1.0, 1.0, 1.0, 1.0};
464 gtk_widget_get_allocation(drawing_area
, &allocation
);
466 gdk_cairo_set_source_rgba(cr
, &white
);
467 cairo_rectangle(cr
, 0, 0, allocation
.width
, allocation
.height
);
470 gtk_widget_queue_draw_area(drawing_area
, 0, 0,
471 allocation
.width
, allocation
.height
);
474 static void pidgin_whiteboard_button_clear_press(GtkWidget
*widget
, gpointer data
)
476 PidginWhiteboard
*gtkwb
= (PidginWhiteboard
*)(data
);
478 /* Confirm whether the user really wants to clear */
479 GtkWidget
*dialog
= gtk_message_dialog_new(
480 GTK_WINDOW(gtkwb
), GTK_DIALOG_DESTROY_WITH_PARENT
,
481 GTK_MESSAGE_QUESTION
, GTK_BUTTONS_YES_NO
, "%s",
482 _("Do you really want to clear?"));
483 gint response
= gtk_dialog_run(GTK_DIALOG(dialog
));
484 gtk_widget_destroy(dialog
);
486 if (response
== GTK_RESPONSE_YES
)
488 pidgin_whiteboard_clear(gtkwb
->wb
);
490 pidgin_whiteboard_set_canvas_as_icon(gtkwb
);
492 /* Do protocol specific clearing procedures */
493 purple_whiteboard_send_clear(gtkwb
->wb
);
498 pidgin_whiteboard_button_save_press(GtkWidget
*widget
, gpointer _gtkwb
)
500 PidginWhiteboard
*gtkwb
= _gtkwb
;
502 GtkFileChooserNative
*chooser
;
505 chooser
= gtk_file_chooser_native_new(_("Save File"), GTK_WINDOW(gtkwb
),
506 GTK_FILE_CHOOSER_ACTION_SAVE
,
507 _("_Save"), _("_Cancel"));
509 gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(chooser
),
511 gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(chooser
),
514 result
= gtk_native_dialog_run(GTK_NATIVE_DIALOG(chooser
));
515 if (result
== GTK_RESPONSE_ACCEPT
) {
518 gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(chooser
));
520 pixbuf
= gdk_pixbuf_get_from_surface(
521 gtkwb
->surface
, 0, 0, gtkwb
->width
, gtkwb
->height
);
523 success
= gdk_pixbuf_save(pixbuf
, filename
, "png", NULL
,
524 "compression", "9", NULL
);
525 g_object_unref(pixbuf
);
527 purple_debug_info("gtkwhiteboard",
528 "whiteboard saved to \"%s\"", filename
);
530 purple_notify_error(NULL
, _("Whiteboard"),
531 _("Unable to save the file"), NULL
, NULL
);
532 purple_debug_error("gtkwhiteboard", "whiteboard "
533 "couldn't be saved to \"%s\"", filename
);
538 g_object_unref(chooser
);
542 color_selected(GtkColorButton
*button
, PidginWhiteboard
*gtkwb
)
545 PurpleWhiteboard
*wb
= gtkwb
->wb
;
546 int old_size
, old_color
;
549 gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(button
), &color
);
551 new_color
= (unsigned int)(color
.red
* 255) << 16;
552 new_color
|= (unsigned int)(color
.green
* 255) << 8;
553 new_color
|= (unsigned int)(color
.blue
* 255);
555 purple_whiteboard_get_brush(wb
, &old_size
, &old_color
);
556 purple_whiteboard_send_brush(wb
, old_size
, new_color
);
560 pidgin_whiteboard_create(PurpleWhiteboard
*wb
)
562 PidginWhiteboard
*gtkwb
;
566 gtkwb
= PIDGIN_WHITEBOARD(g_object_new(PIDGIN_TYPE_WHITEBOARD
, NULL
));
568 purple_whiteboard_set_ui_data(wb
, gtkwb
);
570 /* Get dimensions (default?) for the whiteboard canvas */
571 if (!purple_whiteboard_get_dimensions(wb
, >kwb
->width
,
573 /* Give some initial board-size */
578 if (!purple_whiteboard_get_brush(wb
, >kwb
->brush_size
,
579 >kwb
->brush_color
)) {
580 /* Give some initial brush-info */
581 gtkwb
->brush_size
= 2;
582 gtkwb
->brush_color
= 0xff0000;
585 /* Try and set window title as the name of the buddy, else just use
588 buddy
= purple_blist_find_buddy(purple_whiteboard_get_account(wb
),
589 purple_whiteboard_get_who(wb
));
591 gtk_window_set_title(GTK_WINDOW(gtkwb
),
593 ? purple_buddy_get_contact_alias(buddy
)
594 : purple_whiteboard_get_who(wb
));
595 gtk_widget_set_name(GTK_WIDGET(gtkwb
), purple_whiteboard_get_who(wb
));
597 gtk_widget_set_size_request(GTK_WIDGET(gtkwb
->drawing_area
),
598 gtkwb
->width
, gtkwb
->height
);
600 pidgin_whiteboard_rgb24_to_rgba(gtkwb
->brush_color
, &color
);
601 gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(gtkwb
->color_button
),
604 /* Make all this (window) visible */
605 gtk_widget_show(GTK_WIDGET(gtkwb
));
607 pidgin_whiteboard_set_canvas_as_icon(gtkwb
);
609 /* TODO Specific protocol/whiteboard assignment here? Needs a UI Op? */
610 /* Set default brush size and color */
612 ds->brush_size = DOODLE_BRUSH_MEDIUM;
618 pidgin_whiteboard_destroy(PurpleWhiteboard
*wb
)
620 PidginWhiteboard
*gtkwb
;
622 g_return_if_fail(wb
!= NULL
);
623 gtkwb
= purple_whiteboard_get_ui_data(wb
);
624 g_return_if_fail(gtkwb
!= NULL
);
626 /* TODO Ask if user wants to save picture before the session is closed
630 g_object_unref(gtkwb
);
631 purple_whiteboard_set_ui_data(wb
, NULL
);
634 /******************************************************************************
635 * GObject implementation
636 *****************************************************************************/
638 pidgin_whiteboard_init(PidginWhiteboard
*self
)
640 gtk_widget_init_template(GTK_WIDGET(self
));
642 self
->brush_state
= PIDGIN_WHITEBOARD_BRUSH_UP
;
646 pidgin_whiteboard_finalize(GObject
*obj
)
648 PidginWhiteboard
*gtkwb
= PIDGIN_WHITEBOARD(obj
);
650 /* Clear graphical memory */
651 g_clear_pointer(>kwb
->cr
, cairo_destroy
);
652 g_clear_pointer(>kwb
->surface
, cairo_surface_destroy
);
654 G_OBJECT_CLASS(pidgin_whiteboard_parent_class
)->finalize(obj
);
658 pidgin_whiteboard_class_init(PidginWhiteboardClass
*klass
)
660 GObjectClass
*obj_class
= G_OBJECT_CLASS(klass
);
661 GtkWidgetClass
*widget_class
= GTK_WIDGET_CLASS(klass
);
663 obj_class
->finalize
= pidgin_whiteboard_finalize
;
665 gtk_widget_class_set_template_from_resource(
666 widget_class
, "/im/pidgin/Pidgin/Whiteboard/whiteboard.ui");
668 gtk_widget_class_bind_template_child(widget_class
, PidginWhiteboard
,
670 gtk_widget_class_bind_template_child(widget_class
, PidginWhiteboard
,
673 gtk_widget_class_bind_template_callback(
674 widget_class
, whiteboard_close_cb
);
675 gtk_widget_class_bind_template_callback(
676 widget_class
, pidgin_whiteboard_draw_event
);
677 gtk_widget_class_bind_template_callback(
678 widget_class
, pidgin_whiteboard_configure_event
);
679 gtk_widget_class_bind_template_callback(
680 widget_class
, pidgin_whiteboard_brush_down
);
681 gtk_widget_class_bind_template_callback(
682 widget_class
, pidgin_whiteboard_brush_motion
);
683 gtk_widget_class_bind_template_callback(
684 widget_class
, pidgin_whiteboard_brush_up
);
685 gtk_widget_class_bind_template_callback(
686 widget_class
, pidgin_whiteboard_button_clear_press
);
687 gtk_widget_class_bind_template_callback(
688 widget_class
, pidgin_whiteboard_button_save_press
);
689 gtk_widget_class_bind_template_callback(
690 widget_class
, color_selected
);
693 /******************************************************************************
695 *****************************************************************************/
696 static PurpleWhiteboardUiOps ui_ops
=
698 pidgin_whiteboard_create
,
699 pidgin_whiteboard_destroy
,
700 pidgin_whiteboard_set_dimensions
,
701 pidgin_whiteboard_set_brush
,
702 pidgin_whiteboard_draw_brush_point
,
703 pidgin_whiteboard_draw_brush_line
,
704 pidgin_whiteboard_clear
,
711 PurpleWhiteboardUiOps
*
712 pidgin_whiteboard_get_ui_ops(void)