/*
* e-emoticon-tool-button.c
*
* Copyright (C) 2008 Novell, Inc.
* Copyright (C) 2012 Dan Vrátil <dvratil@redhat.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of version 2 of the GNU Lesser General Public
* License as published by the Free Software Foundation.
*
* 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 Lesser 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.
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include "e-emoticon-tool-button.h"
/* XXX The "button" aspects of this widget are based heavily on the
* GtkComboBox tree-view implementation. Consider splitting it
* into a reusable "button-with-an-empty-window" widget. */
#include <string.h>
#include <glib/gi18n-lib.h>
#include <gdk/gdkkeysyms.h>
#include "e-emoticon-chooser.h"
#define E_EMOTICON_TOOL_BUTTON_GET_PRIVATE(obj) \
(G_TYPE_INSTANCE_GET_PRIVATE \
((obj), E_TYPE_EMOTICON_TOOL_BUTTON, EEmoticonToolButtonPrivate))
/* XXX Should calculate this dynamically. */
#define NUM_ROWS 7
#define NUM_COLS 3
enum {
PROP_0,
PROP_CURRENT_EMOTICON,
PROP_POPUP_SHOWN
};
enum {
POPUP,
POPDOWN,
LAST_SIGNAL
};
struct _EEmoticonToolButtonPrivate {
GtkWidget *active_button; /* not referenced */
GtkWidget *table;
GtkWidget *window;
guint popup_shown : 1;
guint popup_in_progress : 1;
GdkDevice *grab_keyboard;
GdkDevice *grab_mouse;
};
static guint signals[LAST_SIGNAL];
/* Forward Declarations */
static void e_emoticon_tool_button_interface_init
(EEmoticonChooserInterface *interface);
G_DEFINE_TYPE_WITH_CODE (
EEmoticonToolButton,
e_emoticon_tool_button,
GTK_TYPE_TOGGLE_TOOL_BUTTON,
G_IMPLEMENT_INTERFACE (
E_TYPE_EMOTICON_CHOOSER,
e_emoticon_tool_button_interface_init))
/* XXX Copied from _gtk_toolbar_elide_underscores() */
static gchar *
emoticon_tool_button_elide_underscores (const gchar *original)
{
gchar *q, *result;
const gchar *p, *end;
gsize len;
gboolean last_underscore;
if (!original)
return NULL;
len = strlen (original);
q = result = g_malloc (len + 1);
last_underscore = FALSE;
end = original + len;
for (p = original; p < end; p++) {
if (!last_underscore && *p == '_')
last_underscore = TRUE;
else {
last_underscore = FALSE;
if (original + 2 <= p && p + 1 <= end &&
p[-2] == '(' && p[-1] == '_' &&
p[0] != '_' && p[1] == ')') {
q--;
*q = '\0';
p++;
} else
*q++ = *p;
}
}
if (last_underscore)
*q++ = '_';
*q = '\0';
return result;
}
static void
emoticon_tool_button_reposition_window (EEmoticonToolButton *button)
{
GdkScreen *screen;
GdkWindow *window;
GdkRectangle monitor;
GtkAllocation allocation;
gint monitor_num;
gint x, y, width, height;
screen = gtk_widget_get_screen (GTK_WIDGET (button));
window = gtk_widget_get_window (GTK_WIDGET (button));
monitor_num = gdk_screen_get_monitor_at_window (screen, window);
gdk_screen_get_monitor_geometry (screen, monitor_num, &monitor);
gdk_window_get_origin (window, &x, &y);
if (!gtk_widget_get_has_window (GTK_WIDGET (button))) {
gtk_widget_get_allocation (GTK_WIDGET (button), &allocation);
x += allocation.x;
y += allocation.y;
}
gtk_widget_get_allocation (button->priv->window, &allocation);
width = allocation.width;
height = allocation.height;
x = CLAMP (x, monitor.x, monitor.x + monitor.width - width);
y = CLAMP (y, monitor.y, monitor.y + monitor.height - height);
gtk_window_move (GTK_WINDOW (button->priv->window), x, y);
}
static void
emoticon_tool_button_emoticon_clicked_cb (EEmoticonToolButton *button,
GtkWidget *emoticon_button)
{
button->priv->active_button = emoticon_button;
e_emoticon_tool_button_popdown (button);
}
static gboolean
emoticon_tool_button_emoticon_release_event_cb (EEmoticonToolButton *button,
GdkEventButton *event,
GtkButton *emoticon_button)
{
GtkStateType state;
state = gtk_widget_get_state (GTK_WIDGET (button));
if (state != GTK_STATE_NORMAL)
gtk_button_clicked (emoticon_button);
return FALSE;
}
static gboolean
emoticon_tool_button_button_release_event_cb (EEmoticonToolButton *button,
GdkEventButton *event)
{
GtkToggleToolButton *tool_button;
GtkWidget *event_widget;
gboolean popup_in_progress;
tool_button = GTK_TOGGLE_TOOL_BUTTON (button);
event_widget = gtk_get_event_widget ((GdkEvent *) event);
popup_in_progress = button->priv->popup_in_progress;
button->priv->popup_in_progress = FALSE;
if (event_widget != GTK_WIDGET (button))
goto popdown;
if (popup_in_progress)
return FALSE;
if (gtk_toggle_tool_button_get_active (tool_button))
goto popdown;
return FALSE;
popdown:
e_emoticon_tool_button_popdown (button);
return TRUE;
}
static void
emoticon_tool_button_child_show_cb (EEmoticonToolButton *button)
{
button->priv->popup_shown = TRUE;
g_object_notify (G_OBJECT (button), "popup-shown");
}
static void
emoticon_tool_button_child_hide_cb (EEmoticonToolButton *button)
{
button->priv->popup_shown = FALSE;
g_object_notify (G_OBJECT (button), "popup-shown");
}
static gboolean
emoticon_tool_button_child_key_press_event_cb (EEmoticonToolButton *button,
GdkEventKey *event)
{
GtkWidget *window = button->priv->window;
if (!gtk_bindings_activate_event (G_OBJECT (window), event))
gtk_bindings_activate_event (G_OBJECT (button), event);
return TRUE;
}
static void
emoticon_tool_button_set_property (GObject *object,
guint property_id,
const GValue *value,
GParamSpec *pspec)
{
switch (property_id) {
case PROP_CURRENT_EMOTICON:
e_emoticon_chooser_set_current_emoticon (
E_EMOTICON_CHOOSER (object),
g_value_get_boxed (value));
return;
case PROP_POPUP_SHOWN:
if (g_value_get_boolean (value))
e_emoticon_tool_button_popup (
E_EMOTICON_TOOL_BUTTON (object));
else
e_emoticon_tool_button_popdown (
E_EMOTICON_TOOL_BUTTON (object));
return;
}
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
}
static void
emoticon_tool_button_get_property (GObject *object,
guint property_id,
GValue *value,
GParamSpec *pspec)
{
EEmoticonToolButtonPrivate *priv;
priv = E_EMOTICON_TOOL_BUTTON_GET_PRIVATE (object);
switch (property_id) {
case PROP_CURRENT_EMOTICON:
g_value_set_boxed (
value,
e_emoticon_chooser_get_current_emoticon (
E_EMOTICON_CHOOSER (object)));
return;
case PROP_POPUP_SHOWN:
g_value_set_boolean (value, priv->popup_shown);
return;
}
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
}
static void
emoticon_tool_button_dispose (GObject *object)
{
EEmoticonToolButtonPrivate *priv;
priv = E_EMOTICON_TOOL_BUTTON_GET_PRIVATE (object);
if (priv->window != NULL) {
gtk_widget_destroy (priv->window);
priv->window = NULL;
}
/* Chain up to parent's dispose() method. */
G_OBJECT_CLASS (e_emoticon_tool_button_parent_class)->dispose (object);
}
static gboolean
emoticon_tool_button_press_event (GtkWidget *widget,
GdkEventButton *event)
{
EEmoticonToolButton *button;
GtkToggleToolButton *toggle_button;
GtkWidget *event_widget;
button = E_EMOTICON_TOOL_BUTTON (widget);
event_widget = gtk_get_event_widget ((GdkEvent *) event);
if (event_widget == button->priv->window)
return TRUE;
if (event_widget != widget)
return FALSE;
toggle_button = GTK_TOGGLE_TOOL_BUTTON (widget);
if (gtk_toggle_tool_button_get_active (toggle_button))
return FALSE;
e_emoticon_tool_button_popup (button);
button->priv->popup_in_progress = TRUE;
return TRUE;
}
static void
emoticon_tool_button_toggled (GtkToggleToolButton *button)
{
if (gtk_toggle_tool_button_get_active (button))
e_emoticon_tool_button_popup (
E_EMOTICON_TOOL_BUTTON (button));
else
e_emoticon_tool_button_popdown (
E_EMOTICON_TOOL_BUTTON (button));
}
static void
emoticon_tool_button_popup (EEmoticonToolButton *button)
{
GtkToggleToolButton *tool_button;
GdkWindow *window;
gboolean grab_status;
GdkDevice *device, *mouse, *keyboard;
guint32 activate_time;
device = gtk_get_current_event_device ();
g_return_if_fail (device != NULL);
if (!gtk_widget_get_realized (GTK_WIDGET (button)))
return;
if (button->priv->popup_shown)
return;
activate_time = gtk_get_current_event_time ();
if (gdk_device_get_source (device) == GDK_SOURCE_KEYBOARD) {
keyboard = device;
mouse = gdk_device_get_associated_device (device);
} else {
keyboard = gdk_device_get_associated_device (device);
mouse = device;
}
/* Position the window over the button. */
emoticon_tool_button_reposition_window (button);
/* Show the pop-up. */
gtk_widget_show (button->priv->window);
gtk_widget_grab_focus (button->priv->window);
/* Activate the tool button. */
tool_button = GTK_TOGGLE_TOOL_BUTTON (button);
gtk_toggle_tool_button_set_active (tool_button, TRUE);
/* Try to grab the pointer and keyboard. */
window = gtk_widget_get_window (button->priv->window);
grab_status = !keyboard ||
gdk_device_grab (
keyboard, window,
GDK_OWNERSHIP_WINDOW, TRUE,
GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK,
NULL, activate_time) == GDK_GRAB_SUCCESS;
if (grab_status) {
grab_status = !mouse ||
gdk_device_grab (mouse, window,
GDK_OWNERSHIP_WINDOW, TRUE,
GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK,
NULL, activate_time) == GDK_GRAB_SUCCESS;
if (!grab_status && keyboard)
gdk_device_ungrab (keyboard, activate_time);
}
if (grab_status) {
gtk_device_grab_add (button->priv->window, mouse, TRUE);
button->priv->grab_keyboard = keyboard;
button->priv->grab_mouse = mouse;
} else {
gtk_widget_hide (button->priv->window);
}
}
static void
emoticon_tool_button_popdown (EEmoticonToolButton *button)
{
GtkToggleToolButton *tool_button;
if (!gtk_widget_get_realized (GTK_WIDGET (button)))
return;
if (!button->priv->popup_shown)
return;
/* Hide the pop-up. */
gtk_device_grab_remove (button->priv->window, button->priv->grab_mouse);
gtk_widget_hide (button->priv->window);
/* Deactivate the tool button. */
tool_button = GTK_TOGGLE_TOOL_BUTTON (button);
gtk_toggle_tool_button_set_active (tool_button, FALSE);
if (button->priv->grab_keyboard)
gdk_device_ungrab (button->priv->grab_keyboard, GDK_CURRENT_TIME);
if (button->priv->grab_mouse)
gdk_device_ungrab (button->priv->grab_mouse, GDK_CURRENT_TIME);
button->priv->grab_keyboard = NULL;
button->priv->grab_mouse = NULL;
}
static EEmoticon *
emoticon_tool_button_get_current_emoticon (EEmoticonChooser *chooser)
{
EEmoticonToolButtonPrivate *priv;
priv = E_EMOTICON_TOOL_BUTTON_GET_PRIVATE (chooser);
if (priv->active_button == NULL)
return NULL;
return g_object_get_data (G_OBJECT (priv->active_button), "emoticon");
}
static void
emoticon_tool_button_set_current_emoticon (EEmoticonChooser *chooser,
EEmoticon *emoticon)
{
EEmoticonToolButtonPrivate *priv;
GList *list, *iter;
priv = E_EMOTICON_TOOL_BUTTON_GET_PRIVATE (chooser);
list = gtk_container_get_children (GTK_CONTAINER (priv->table));
for (iter = list; iter != NULL; iter = iter->next) {
GtkWidget *item = iter->data;
EEmoticon *candidate;
candidate = g_object_get_data (G_OBJECT (item), "emoticon");
if (candidate == NULL)
continue;
if (e_emoticon_equal (emoticon, candidate)) {
gtk_button_clicked (GTK_BUTTON (item));
break;
}
}
g_list_free (list);
}
static void
e_emoticon_tool_button_class_init (EEmoticonToolButtonClass *class)
{
GObjectClass *object_class;
GtkWidgetClass *widget_class;
GtkToggleToolButtonClass *toggle_tool_button_class;
g_type_class_add_private (class, sizeof (EEmoticonToolButtonPrivate));
object_class = G_OBJECT_CLASS (class);
object_class->set_property = emoticon_tool_button_set_property;
object_class->get_property = emoticon_tool_button_get_property;
object_class->dispose = emoticon_tool_button_dispose;
widget_class = GTK_WIDGET_CLASS (class);
widget_class->button_press_event = emoticon_tool_button_press_event;
toggle_tool_button_class = GTK_TOGGLE_TOOL_BUTTON_CLASS (class);
toggle_tool_button_class->toggled = emoticon_tool_button_toggled;
class->popup = emoticon_tool_button_popup;
class->popdown = emoticon_tool_button_popdown;
g_object_class_override_property (
object_class, PROP_CURRENT_EMOTICON, "current-emoticon");
g_object_class_install_property (
object_class,
PROP_POPUP_SHOWN,
g_param_spec_boolean (
"popup-shown",
"Popup Shown",
"Whether the button's dropdown is shown",
FALSE,
G_PARAM_READWRITE));
signals[POPUP] = g_signal_new (
"popup",
G_OBJECT_CLASS_TYPE (class),
G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
G_STRUCT_OFFSET (EEmoticonToolButtonClass, popup),
NULL, NULL,
g_cclosure_marshal_VOID__VOID,
G_TYPE_NONE, 0);
signals[POPDOWN] = g_signal_new (
"popdown",
G_OBJECT_CLASS_TYPE (class),
G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
G_STRUCT_OFFSET (EEmoticonToolButtonClass, popdown),
NULL, NULL,
g_cclosure_marshal_VOID__VOID,
G_TYPE_NONE, 0);
gtk_binding_entry_add_signal (
gtk_binding_set_by_class (class),
GDK_KEY_Down, GDK_MOD1_MASK, "popup", 0);
gtk_binding_entry_add_signal (
gtk_binding_set_by_class (class),
GDK_KEY_KP_Down, GDK_MOD1_MASK, "popup", 0);
gtk_binding_entry_add_signal (
gtk_binding_set_by_class (class),
GDK_KEY_Up, GDK_MOD1_MASK, "popdown", 0);
gtk_binding_entry_add_signal (
gtk_binding_set_by_class (class),
GDK_KEY_KP_Up, GDK_MOD1_MASK, "popdown", 0);
gtk_binding_entry_add_signal (
gtk_binding_set_by_class (class),
GDK_KEY_Escape, 0, "popdown", 0);
}
static void
e_emoticon_tool_button_interface_init (EEmoticonChooserInterface *interface)
{
interface->get_current_emoticon =
emoticon_tool_button_get_current_emoticon;
interface->set_current_emoticon =
emoticon_tool_button_set_current_emoticon;
}
static void
e_emoticon_tool_button_init (EEmoticonToolButton *button)
{
EEmoticonChooser *chooser;
GtkWidget *toplevel;
GtkWidget *container;
GtkWidget *widget;
GtkWidget *window;
GList *list, *iter;
gint ii;
button->priv = E_EMOTICON_TOOL_BUTTON_GET_PRIVATE (button);
/* Build the pop-up window. */
window = gtk_window_new (GTK_WINDOW_POPUP);
gtk_window_set_resizable (GTK_WINDOW (window), FALSE);
gtk_window_set_type_hint (
GTK_WINDOW (window), GDK_WINDOW_TYPE_HINT_COMBO);
button->priv->window = g_object_ref_sink (window);
toplevel = gtk_widget_get_toplevel (GTK_WIDGET (button));
if (GTK_IS_WINDOW (toplevel)) {
gtk_window_group_add_window (
gtk_window_get_group (GTK_WINDOW (toplevel)),
GTK_WINDOW (window));
gtk_window_set_transient_for (
GTK_WINDOW (window), GTK_WINDOW (toplevel));
}
g_signal_connect_swapped (
window, "show",
G_CALLBACK (emoticon_tool_button_child_show_cb), button);
g_signal_connect_swapped (
window, "hide",
G_CALLBACK (emoticon_tool_button_child_hide_cb), button);
g_signal_connect_swapped (
window, "button-release-event",
G_CALLBACK (emoticon_tool_button_button_release_event_cb),
button);
g_signal_connect_swapped (
window, "key-press-event",
G_CALLBACK (emoticon_tool_button_child_key_press_event_cb),
button);
/* Build the pop-up window contents. */
widget = gtk_frame_new (NULL);
gtk_frame_set_shadow_type (GTK_FRAME (widget), GTK_SHADOW_OUT);
gtk_container_add (GTK_CONTAINER (window), widget);
gtk_widget_show (widget);
container = widget;
widget = gtk_table_new (NUM_ROWS, NUM_COLS, TRUE);
gtk_table_set_row_spacings (GTK_TABLE (widget), 0);
gtk_table_set_col_spacings (GTK_TABLE (widget), 0);
gtk_container_add (GTK_CONTAINER (container), widget);
button->priv->table = g_object_ref (widget);
gtk_widget_show (widget);
container = widget;
chooser = E_EMOTICON_CHOOSER (button);
list = e_emoticon_chooser_get_items ();
g_assert (g_list_length (list) <= NUM_ROWS * NUM_COLS);
for (iter = list, ii = 0; iter != NULL; iter = iter->next, ii++) {
EEmoticon *emoticon = iter->data;
guint left = ii % NUM_COLS;
guint top = ii / NUM_COLS;
gchar *tooltip;
tooltip = emoticon_tool_button_elide_underscores (
gettext (emoticon->label));
widget = gtk_button_new ();
gtk_button_set_image (
GTK_BUTTON (widget),
gtk_image_new_from_icon_name (
emoticon->icon_name, GTK_ICON_SIZE_BUTTON));
gtk_button_set_relief (GTK_BUTTON (widget), GTK_RELIEF_NONE);
gtk_widget_set_tooltip_text (widget, tooltip);
gtk_widget_show (widget);
g_object_set_data_full (
G_OBJECT (widget), "emoticon",
e_emoticon_copy (emoticon),
(GDestroyNotify) e_emoticon_free);
g_signal_connect_swapped (
widget, "clicked",
G_CALLBACK (emoticon_tool_button_emoticon_clicked_cb),
button);
g_signal_connect_swapped (
widget, "clicked",
G_CALLBACK (e_emoticon_chooser_item_activated),
chooser);
g_signal_connect_swapped (
widget, "button-release-event",
G_CALLBACK (emoticon_tool_button_emoticon_release_event_cb),
button);
gtk_table_attach (
GTK_TABLE (container), widget,
left, left + 1, top, top + 1, 0, 0, 0, 0);
g_free (tooltip);
}
g_list_free (list);
}
GtkToolItem *
e_emoticon_tool_button_new (void)
{
return g_object_new (E_TYPE_EMOTICON_TOOL_BUTTON, NULL);
}
void
e_emoticon_tool_button_popup (EEmoticonToolButton *button)
{
g_return_if_fail (E_IS_EMOTICON_TOOL_BUTTON (button));
g_signal_emit (button, signals[POPUP], 0);
}
void
e_emoticon_tool_button_popdown (EEmoticonToolButton *button)
{
g_return_if_fail (E_IS_EMOTICON_TOOL_BUTTON (button));
g_signal_emit (button, signals[POPDOWN], 0);
}