/* * Copyright © 2013 Red Hat, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * * Author: Joaquim Rocha * (based on previous work by Tias Guns and Soren Hauberg) */ #include "config.h" #include #include #include #include #include #include #include #include #include #include "calibrator.h" #include "calibrator-gui.h" #include "cc-clock-actor.h" #include "cc-target-actor.h" struct CalibArea { struct Calib calibrator; XYinfo axis; gboolean swap; gboolean success; int device_id; double X[4], Y[4]; int display_width, display_height; GtkWidget *window; ClutterActor *stage; ClutterActor *action_layer; ClutterActor *clock; ClutterActor *target; ClutterActor *success_image; ClutterActor *text_title_holder; ClutterActor *helper_text_title; ClutterActor *text_body_holder; ClutterActor *helper_text_body; ClutterActor *error_text; ClutterTransition *clock_timeline; GdkPixbuf *icon_success; FinishCallback callback; gpointer user_data; }; #define TARGET_SHOW_ANIMATION_DURATION 500 #define TARGET_HIDE_ANIMATION_DURATION 200 #define COLOR_GRAY 127 /* Window parameters */ #define WINDOW_OPACITY 0.9 /* Timeout parameters */ #define MAX_TIME 15000 /* 15000 = 15 sec */ #define END_TIME 750 /* 750 = 0.75 sec */ /* Text printed on screen */ #define HELP_TEXT_TITLE N_("Screen Calibration") #define HELP_TEXT_MAIN N_("Please tap the target markers as they " \ "appear on screen to calibrate the tablet.") #define HELP_TEXT_ANIMATION_DURATION 300 #define ERROR_MESSAGE N_("Mis-click detected, restarting...") #define ERROR_MESSAGE_ANIMATION_DURATION 500 #define ICON_SUCCESS "emblem-ok-symbolic" #define ICON_SIZE 300 static void set_display_size(CalibArea *calib_area, int width, int height) { int delta_x; int delta_y; calib_area->display_width = width; calib_area->display_height = height; /* Compute absolute circle centers */ delta_x = calib_area->display_width/NUM_BLOCKS; delta_y = calib_area->display_height/NUM_BLOCKS; calib_area->X[UL] = delta_x; calib_area->Y[UL] = delta_y; calib_area->X[UR] = calib_area->display_width - delta_x - 1; calib_area->Y[UR] = delta_y; calib_area->X[LL] = delta_x; calib_area->Y[LL] = calib_area->display_height - delta_y - 1; calib_area->X[LR] = calib_area->display_width - delta_x - 1; calib_area->Y[LR] = calib_area->display_height - delta_y - 1; /* reset calibration if already started */ reset(&calib_area->calibrator); } static void resize_display(CalibArea *calib_area) { gfloat width, height; clutter_actor_get_size (calib_area->stage, &width, &height); if (calib_area->display_width != width || calib_area->display_height != height) { gint i = calib_area->calibrator.num_clicks; set_display_size(calib_area, width, height); cc_target_actor_move_center (CC_TARGET_ACTOR (calib_area->target), calib_area->X[i], calib_area->Y[i]); } } static void on_allocation_changed (ClutterActor *actor, ClutterActorBox *box, ClutterAllocationFlags flags, CalibArea *area) { resize_display (area); } static gboolean on_delete_event (GtkWidget *widget, GdkEvent *event, CalibArea *area) { clutter_timeline_stop (CLUTTER_TIMELINE (area->clock_timeline)); gtk_widget_hide (area->window); (*area->callback) (area, area->user_data); return TRUE; } static gboolean draw_success_end_wait_callback (CalibArea *area) { on_delete_event (NULL, NULL, area); return FALSE; } static void set_success (CalibArea *area) { ClutterImage *image; GdkPixbuf *icon = area->icon_success; if (icon == NULL) return; image = CLUTTER_IMAGE (clutter_actor_get_content (area->success_image)); clutter_image_set_data (image, gdk_pixbuf_get_pixels (icon), gdk_pixbuf_get_has_alpha (icon) ? COGL_PIXEL_FORMAT_RGBA_8888 : COGL_PIXEL_FORMAT_RGB_888, gdk_pixbuf_get_width (icon), gdk_pixbuf_get_height (icon), gdk_pixbuf_get_rowstride (icon), NULL); clutter_actor_set_size (area->success_image, gdk_pixbuf_get_width (icon), gdk_pixbuf_get_height (icon)); clutter_actor_show (area->success_image); clutter_actor_hide (area->action_layer); } static void set_calibration_status (CalibArea *area) { GtkIconTheme *icon_theme; GtkIconInfo *icon_info; GdkRGBA white; icon_theme = gtk_icon_theme_get_default (); icon_info = gtk_icon_theme_lookup_icon (icon_theme, ICON_SUCCESS, ICON_SIZE, GTK_ICON_LOOKUP_USE_BUILTIN); if (icon_info == NULL) { g_warning ("Failed to find icon \"%s\"", ICON_SUCCESS); goto out; } gdk_rgba_parse (&white, "White"); area->icon_success = gtk_icon_info_load_symbolic (icon_info, &white, NULL, NULL, NULL, NULL, NULL); g_object_unref (icon_info); if (!area->icon_success) g_warning ("Failed to load icon \"%s\"", ICON_SUCCESS); out: area->success = finish (&area->calibrator, &area->axis, &area->swap); if (area->success && area->icon_success) { set_success (area); g_timeout_add (END_TIME, (GSourceFunc) draw_success_end_wait_callback, area); } else { on_delete_event (NULL, NULL, area); } } static ClutterTransition * get_error_message_transition (CalibArea *area) { ClutterTransition *transition; clutter_actor_show (area->error_text); transition = clutter_property_transition_new ("opacity"); clutter_timeline_set_progress_mode (CLUTTER_TIMELINE (transition), CLUTTER_EASE_OUT); clutter_timeline_set_duration (CLUTTER_TIMELINE (transition), ERROR_MESSAGE_ANIMATION_DURATION); clutter_transition_set_animatable (transition, CLUTTER_ANIMATABLE (area->error_text)); clutter_transition_set_from (transition, G_TYPE_UINT, 0); clutter_transition_set_to (transition, G_TYPE_UINT, 255); return transition; } static void show_error_message (CalibArea *area) { ClutterTransition *transition; clutter_actor_show (area->error_text); transition = get_error_message_transition (area); clutter_timeline_start (CLUTTER_TIMELINE (transition)); } static void on_error_message_transparent (ClutterTimeline *timeline, CalibArea *area) { clutter_actor_hide (area->error_text); } static void hide_error_message (CalibArea *area) { ClutterTransition *transition; transition = get_error_message_transition (area); clutter_transition_set_from (transition, G_TYPE_UINT, 255); clutter_transition_set_to (transition, G_TYPE_UINT, 0); g_signal_connect (CLUTTER_TIMELINE (transition), "completed", G_CALLBACK (on_error_message_transparent), area); clutter_timeline_start (CLUTTER_TIMELINE (transition)); } static gboolean on_button_press_event(ClutterActor *actor, ClutterButtonEvent *event, CalibArea *area) { gint num_clicks; gboolean success; if (area->success) return FALSE; /* Check matching device ID if a device ID was provided */ if (area->device_id > -1) { ClutterInputDevice *device; device = clutter_event_get_source_device ((ClutterEvent *) event); if (device != NULL && clutter_input_device_get_device_id (device) != area->device_id) { char *name; g_object_get (G_OBJECT (device), "name", &name, NULL); g_debug ("Ignoring input from device %s (%d)", name, clutter_input_device_get_device_id (device)); g_free (name); return FALSE; } } /* Handle click */ clutter_timeline_stop (CLUTTER_TIMELINE (area->clock_timeline)); clutter_timeline_start (CLUTTER_TIMELINE (area->clock_timeline)); success = add_click(&area->calibrator, (int) event->x, (int) event->y); num_clicks = area->calibrator.num_clicks; if (!success && num_clicks == 0) show_error_message (area); else { gboolean visible; g_object_get (area->error_text, "visible", &visible, NULL); if (visible) hide_error_message (area); } /* Are we done yet? */ if (num_clicks >= 4) { set_calibration_status (area); return FALSE; } cc_target_actor_move_center (CC_TARGET_ACTOR (area->target), area->X[num_clicks], area->Y[num_clicks]); return FALSE; } static gboolean on_key_release_event(ClutterActor *actor, ClutterKeyEvent *event, CalibArea *area) { if (area->success || event->type != CLUTTER_KEY_RELEASE || event->keyval != CLUTTER_KEY_Escape) { return FALSE; } on_delete_event (area->window, NULL, area); return FALSE; } static gboolean on_focus_out_event (GtkWidget *widget, GdkEvent *event, CalibArea *area) { if (area->success) return FALSE; /* If the calibrator window loses focus, simply bail out... */ on_delete_event (widget, NULL, area); return FALSE; } static void on_timeout (ClutterTimeline *timeline, CalibArea *area) { set_calibration_status (area); } static void show_helper_text_body (CalibArea *area) { ClutterTransition *transition; gfloat height; height = clutter_actor_get_height (area->helper_text_body); clutter_actor_show (area->helper_text_body); transition = clutter_property_transition_new ("y"); clutter_timeline_set_progress_mode (CLUTTER_TIMELINE (transition), CLUTTER_EASE_OUT); clutter_timeline_set_duration (CLUTTER_TIMELINE (transition), HELP_TEXT_ANIMATION_DURATION); clutter_transition_set_animatable (transition, CLUTTER_ANIMATABLE (area->helper_text_body)); clutter_transition_set_from (transition, G_TYPE_FLOAT, -height); clutter_transition_set_to (transition, G_TYPE_FLOAT, 0.0); clutter_timeline_start (CLUTTER_TIMELINE (transition)); } static void on_helper_text_title_shown (ClutterTimeline *timelines, CalibArea *area) { show_helper_text_body (area); } static void show_helper_text_title (CalibArea *area) { ClutterTransition *transition; gfloat height = clutter_actor_get_height (area->helper_text_title); clutter_actor_set_y (area->helper_text_title, - clutter_actor_get_height (area->helper_text_title)); clutter_actor_show (area->helper_text_title); transition = clutter_property_transition_new ("y"); clutter_timeline_set_progress_mode (CLUTTER_TIMELINE (transition), CLUTTER_EASE_OUT); clutter_timeline_set_duration (CLUTTER_TIMELINE (transition), HELP_TEXT_ANIMATION_DURATION); clutter_transition_set_animatable (transition, CLUTTER_ANIMATABLE (area->helper_text_title)); clutter_transition_set_from (transition, G_TYPE_FLOAT, -height); clutter_transition_set_to (transition, G_TYPE_FLOAT, 0.0); g_signal_connect (CLUTTER_TIMELINE (transition), "completed", G_CALLBACK (on_helper_text_title_shown), area); clutter_timeline_start (CLUTTER_TIMELINE (transition)); } static void on_fullscreen (GtkWindow *window, GdkEventWindowState *event, CalibArea *area) { ClutterRect rect; if ((event->changed_mask & GDK_WINDOW_STATE_FULLSCREEN) == 0) return; clutter_actor_show (area->action_layer); clutter_actor_show (area->clock); rect.origin.x = 0; rect.origin.y = 0; clutter_actor_get_size (area->helper_text_title, &rect.size.width, &rect.size.height); g_object_set (area->text_title_holder, "clip-rect", &rect, NULL); clutter_actor_get_size (area->helper_text_body, &rect.size.width, &rect.size.height); g_object_set (area->text_body_holder, "clip-rect", &rect, NULL); clutter_actor_set_y (area->helper_text_body, - clutter_actor_get_height (area->helper_text_body)); show_helper_text_title (area); } static void set_up_stage (CalibArea *calib_area, ClutterActor *stage) { ClutterPoint anchor; ClutterColor color; ClutterContent *success_content; gfloat height; gchar *markup; calib_area->stage = stage; calib_area->action_layer = clutter_actor_new (); calib_area->clock = cc_clock_actor_new (); calib_area->target = cc_target_actor_new (); calib_area->text_title_holder = clutter_actor_new (); calib_area->helper_text_title = clutter_text_new (); calib_area->text_body_holder = clutter_actor_new (); calib_area->helper_text_body = clutter_text_new (); calib_area->error_text = clutter_text_new (); calib_area->success_image = clutter_actor_new (); clutter_stage_set_use_alpha (CLUTTER_STAGE (stage), TRUE); /* bind the action layer's geometry to the stage's */ clutter_actor_add_constraint (calib_area->action_layer, clutter_bind_constraint_new (stage, CLUTTER_BIND_SIZE, 0)); clutter_actor_add_child (stage, calib_area->action_layer); g_signal_connect (stage, "allocation-changed", G_CALLBACK (on_allocation_changed), calib_area); clutter_color_from_string (&color, "#000"); color.alpha = WINDOW_OPACITY * 255; clutter_actor_set_background_color (stage, &color); clutter_actor_add_child (calib_area->action_layer, calib_area->clock); clutter_actor_add_constraint (calib_area->clock, clutter_align_constraint_new (stage, CLUTTER_ALIGN_BOTH, 0.5)); clutter_actor_add_child (calib_area->action_layer, calib_area->target); /* set the helper text */ anchor.x = 0; g_object_set (calib_area->text_title_holder, "pivot-point", &anchor, NULL); clutter_actor_add_child (calib_area->action_layer, calib_area->text_title_holder); clutter_actor_add_child (calib_area->text_title_holder, calib_area->helper_text_title); height = clutter_actor_get_height (calib_area->clock); clutter_actor_add_constraint (calib_area->text_title_holder, clutter_bind_constraint_new (calib_area->clock, CLUTTER_BIND_Y, height * 1.5)); clutter_actor_add_constraint (calib_area->text_title_holder, clutter_align_constraint_new (stage, CLUTTER_ALIGN_X_AXIS, .5)); clutter_text_set_line_alignment (CLUTTER_TEXT (calib_area->helper_text_title), PANGO_ALIGN_CENTER); color.red = COLOR_GRAY; color.green = COLOR_GRAY; color.blue = COLOR_GRAY; color.alpha = 255; markup = g_strdup_printf ("%s", _(HELP_TEXT_TITLE)); clutter_text_set_markup (CLUTTER_TEXT (calib_area->helper_text_title), markup); clutter_text_set_color (CLUTTER_TEXT (calib_area->helper_text_title), &color); g_free (markup); g_object_set (calib_area->text_body_holder, "pivot-point", &anchor, NULL); clutter_actor_add_child (calib_area->action_layer, calib_area->text_body_holder); clutter_actor_add_child (calib_area->text_body_holder, calib_area->helper_text_body); height = clutter_actor_get_height (calib_area->helper_text_title); clutter_actor_add_constraint (calib_area->text_body_holder, clutter_bind_constraint_new (calib_area->text_title_holder, CLUTTER_BIND_Y, height * 1.2)); clutter_actor_add_constraint (calib_area->text_body_holder, clutter_align_constraint_new (stage, CLUTTER_ALIGN_X_AXIS, .5)); clutter_text_set_line_alignment (CLUTTER_TEXT (calib_area->helper_text_body), PANGO_ALIGN_CENTER); markup = g_strdup_printf ("%s", _(HELP_TEXT_MAIN)); clutter_text_set_markup (CLUTTER_TEXT (calib_area->helper_text_body), markup); g_free (markup); /* set the error text */ g_object_set (calib_area->error_text, "pivot-point", &anchor, NULL); clutter_actor_add_child (calib_area->action_layer, calib_area->error_text); height = clutter_actor_get_height (calib_area->helper_text_body); clutter_actor_add_constraint (calib_area->error_text, clutter_bind_constraint_new (calib_area->text_title_holder, CLUTTER_BIND_Y, height * 3)); clutter_actor_add_constraint (calib_area->error_text, clutter_align_constraint_new (stage, CLUTTER_ALIGN_X_AXIS, .5)); clutter_text_set_line_alignment (CLUTTER_TEXT (calib_area->error_text), PANGO_ALIGN_CENTER); markup = g_strdup_printf ("" "%s", ERROR_MESSAGE); clutter_text_set_markup (CLUTTER_TEXT (calib_area->error_text), markup); g_free (markup); clutter_actor_hide (calib_area->error_text); /* configure success image */ success_content = clutter_image_new (); clutter_actor_set_content (calib_area->success_image, success_content); g_object_unref (success_content); clutter_actor_add_child (stage, calib_area->success_image); clutter_actor_add_constraint (calib_area->success_image, clutter_align_constraint_new (stage, CLUTTER_ALIGN_BOTH, .5)); /* animate clock */ calib_area->clock_timeline = clutter_property_transition_new ("angle"); clutter_timeline_set_progress_mode (CLUTTER_TIMELINE (calib_area->clock_timeline), CLUTTER_LINEAR); clutter_timeline_set_duration (CLUTTER_TIMELINE (calib_area->clock_timeline), MAX_TIME); clutter_transition_set_animatable (calib_area->clock_timeline, CLUTTER_ANIMATABLE (calib_area->clock)); clutter_transition_set_from (calib_area->clock_timeline, G_TYPE_FLOAT, .0); clutter_transition_set_to (calib_area->clock_timeline, G_TYPE_FLOAT, 360.0); clutter_timeline_set_repeat_count (CLUTTER_TIMELINE (calib_area->clock_timeline), -1); clutter_timeline_start (CLUTTER_TIMELINE (calib_area->clock_timeline)); g_signal_connect (CLUTTER_TIMELINE (calib_area->clock_timeline), "completed", G_CALLBACK (on_timeout), calib_area); g_signal_connect (stage, "button-press-event", G_CALLBACK (on_button_press_event), calib_area); g_signal_connect (stage, "key-release-event", G_CALLBACK (on_key_release_event), calib_area); } /** * Creates the windows and other objects required to do calibration * under GTK. When the window is closed (timed out, calibration finished * or user cancellation), callback will be called, where you should call * calib_area_finish(). */ CalibArea * calib_area_new (GdkScreen *screen, int monitor, int device_id, FinishCallback callback, gpointer user_data, XYinfo *old_axis, int threshold_doubleclick, int threshold_misclick) { CalibArea *calib_area; GdkRectangle rect; GdkVisual *visual; #ifndef FAKE_AREA GdkWindow *window; GdkCursor *cursor; #endif /* FAKE_AREA */ GtkWidget *clutter_embed; ClutterActor *stage; g_return_val_if_fail (old_axis, NULL); g_return_val_if_fail (callback, NULL); g_debug ("Current calibration: %d, %d, %d, %d\n", old_axis->x_min, old_axis->y_min, old_axis->x_max, old_axis->y_max); calib_area = g_new0 (CalibArea, 1); calib_area->callback = callback; calib_area->user_data = user_data; calib_area->device_id = device_id; calib_area->calibrator.old_axis.x_min = old_axis->x_min; calib_area->calibrator.old_axis.x_max = old_axis->x_max; calib_area->calibrator.old_axis.y_min = old_axis->y_min; calib_area->calibrator.old_axis.y_max = old_axis->y_max; calib_area->calibrator.threshold_doubleclick = threshold_doubleclick; calib_area->calibrator.threshold_misclick = threshold_misclick; calib_area->window = gtk_window_new (GTK_WINDOW_TOPLEVEL); #ifndef FAKE_AREA /* No cursor */ gtk_widget_realize (calib_area->window); window = gtk_widget_get_window (calib_area->window); cursor = gdk_cursor_new (GDK_BLANK_CURSOR); gdk_window_set_cursor (window, cursor); g_object_unref (cursor); gtk_widget_set_can_focus (calib_area->window, TRUE); gtk_window_set_keep_above (GTK_WINDOW (calib_area->window), TRUE); #endif /* FAKE_AREA */ /* Set up the embedded stage */ clutter_embed = gtk_clutter_embed_new (); gtk_container_add (GTK_CONTAINER (calib_area->window), clutter_embed); stage = gtk_clutter_embed_get_stage (GTK_CLUTTER_EMBED (clutter_embed)); /* Move to correct screen */ if (screen == NULL) screen = gdk_screen_get_default (); gdk_screen_get_monitor_geometry (screen, monitor, &rect); gtk_window_move (GTK_WINDOW (calib_area->window), rect.x, rect.y); gtk_window_set_default_size (GTK_WINDOW (calib_area->window), rect.width, rect.height); calib_area->calibrator.geometry = rect; set_up_stage (calib_area, stage); g_signal_connect (calib_area->window, "delete-event", G_CALLBACK (on_delete_event), calib_area); g_signal_connect (calib_area->window, "focus-out-event", G_CALLBACK(on_focus_out_event), calib_area); g_signal_connect (calib_area->window, "window-state-event", G_CALLBACK (on_fullscreen), calib_area); gtk_window_fullscreen (GTK_WINDOW (calib_area->window)); visual = gdk_screen_get_rgba_visual (screen); if (visual != NULL) gtk_widget_set_visual (GTK_WIDGET (calib_area->window), visual); gtk_widget_show_all (calib_area->window); clutter_actor_hide (calib_area->action_layer); return calib_area; } /* Finishes the calibration. Note that CalibArea * needs to be destroyed with calib_area_free() afterwards */ gboolean calib_area_finish (CalibArea *area, XYinfo *new_axis, gboolean *swap_xy) { g_return_val_if_fail (area != NULL, FALSE); *new_axis = area->axis; *swap_xy = area->swap; if (area->success) g_debug ("Final calibration: %d, %d, %d, %d\n", new_axis->x_min, new_axis->y_min, new_axis->x_max, new_axis->y_max); else g_debug ("Calibration was aborted or timed out"); return area->success; } void calib_area_free (CalibArea *area) { g_return_if_fail (area != NULL); g_clear_object (&area->icon_success); gtk_widget_destroy (area->window); g_free (area); } void calib_area_get_display_size (CalibArea *area, gint *width, gint *height) { g_return_if_fail (area != NULL); *width = area->display_width; *height = area->display_height; }