/*
 * Metacity command-hotkey editor
 * Copyright 2003 Patrick Reynolds (reynolds .at. cs duke edu)
 * Distributed under the GNU General Public License (GPL) version 2 or later
 *
 * Build me: gcc -Wall metacity-hotkeys.c -o metacity-hotkeys \
               `pkg-config gtk+-2.0 gconf-2.0 --cflags --libs`
 * Run me:   ./metacity-hotkeys
 */

#define GTK_DISABLE_DEPRECATED
#define G_DISABLE_DEPRECATED

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <gconf/gconf-client.h>
#include <gtk/gtk.h>

#define MAX_HOTKEYS 32
#define BINDINGS "/apps/metacity/global_keybindings"
#define COMMANDS "/apps/metacity/keybinding_commands"
typedef struct _Hotkey Hotkey;
struct _Hotkey {
	char *key, *cmd;
};

static GConfClient *conf;
static Hotkey hotkey[MAX_HOTKEYS];
static GtkListStore *list_model;

static GtkWidget* create_main_window(void);
static void read_hotkeys(void);
static void add_hotkeys_to_model(void);
static void row_changed(GtkTreeSelection *sel);
static void row_delete(GtkWidget *button, gpointer user_data);
static void entry_changed(GtkWidget *entry, gpointer user_data);
static void grab_clicked(GtkWidget *w);
static void grab_key_pressed(GtkWidget *w, GdkEventKey *key);
static void add_update_clicked(GtkWidget *w, gpointer user_data);
static void key_updated(GConfClient *conf, guint cnxn_id, GConfEntry *entry,
		gpointer user_data);
static gboolean key_into_hotkeys(GConfEntry *ent);
int get_selected_keyno(GtkTreeSelection *sel);
static void set_confkey_pair(int keyno);

static GtkWidget *main_window;

int main(int argc, char **argv) {
  gtk_init(&argc, &argv);

  conf = gconf_client_get_default();
  gconf_client_add_dir(conf, BINDINGS, GCONF_CLIENT_PRELOAD_ONELEVEL, NULL);
  gconf_client_add_dir(conf, COMMANDS, GCONF_CLIENT_PRELOAD_ONELEVEL, NULL);
	
  main_window = create_main_window();
	read_hotkeys();
	add_hotkeys_to_model();
  gtk_widget_show_all(main_window);
  
  gtk_main();

  g_object_unref(G_OBJECT(conf));
  
  return 0;
}

enum {
	COLUMN_CMD,
	COLUMN_KEY,
	COLUMN_KEYNUM,   /* hidden */
	NCOLUMNS
};

static GtkWidget *add_update_button, *delete_button, *key_entry, *cmd_entry;

static GtkWidget* create_main_window() {
  GtkWidget *window, *vbox, *tree, *table, *hbox, *button, *alignment, *label;
	GtkWidget *frame;
	GtkCellRenderer *renderer;
	GtkTreeViewColumn *column;
	GtkTreeSelection *sel;
  
  window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
  gtk_window_set_title(GTK_WINDOW(window), "Metacity Command Keys");
  
  vbox = gtk_vbox_new(FALSE, 5);
  gtk_container_add(GTK_CONTAINER(window), vbox);
  gtk_container_set_border_width(GTK_CONTAINER (vbox), 5);

	frame = gtk_frame_new(NULL);
	gtk_frame_set_shadow_type(GTK_FRAME(frame), GTK_SHADOW_ETCHED_IN);
	gtk_box_pack_start(GTK_BOX(vbox), frame, TRUE, TRUE, 0);

	list_model = gtk_list_store_new(NCOLUMNS, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_INT);
	tree = gtk_tree_view_new_with_model(GTK_TREE_MODEL(list_model));
	renderer = gtk_cell_renderer_text_new();
	column = gtk_tree_view_column_new_with_attributes(
		"Command", renderer, "text", COLUMN_CMD, NULL);
	gtk_tree_view_append_column(GTK_TREE_VIEW(tree), column);
	column = gtk_tree_view_column_new_with_attributes(
		"Key/Combination", renderer, "text", COLUMN_KEY, NULL);
	gtk_tree_view_append_column(GTK_TREE_VIEW(tree), column);
#if 0
	column = gtk_tree_view_column_new_with_attributes(
		"Key#", renderer, "text", COLUMN_KEYNUM, NULL);
	gtk_tree_view_append_column(GTK_TREE_VIEW(tree), column);
#endif
	gtk_container_add(GTK_CONTAINER(frame), tree);
	sel = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree));
	g_signal_connect(G_OBJECT(sel), "changed", G_CALLBACK(row_changed), NULL);

	table = gtk_table_new(2, 2, FALSE);
	gtk_table_set_row_spacings(GTK_TABLE(table), 5);
	gtk_table_set_col_spacings(GTK_TABLE(table), 5);
	gtk_box_pack_start(GTK_BOX(vbox), table, FALSE, FALSE, 0);
	label = gtk_label_new("Key:");
	gtk_misc_set_alignment(GTK_MISC(label), 1, 0.5);
	gtk_table_attach(GTK_TABLE(table), label, 0, 1, 0, 1, GTK_FILL, 0, 0, 0);
	label = gtk_label_new("Command:");
	gtk_misc_set_alignment(GTK_MISC(label), 1, 0.5);
	gtk_table_attach(GTK_TABLE(table), label, 0, 1, 1, 2, GTK_FILL, 0, 0, 0);

	hbox = gtk_hbox_new(FALSE, 5);
	gtk_table_attach_defaults(GTK_TABLE(table), hbox, 1, 2, 0, 1);
	key_entry = gtk_entry_new();
	g_signal_connect(G_OBJECT(key_entry), "changed",
		G_CALLBACK(entry_changed), sel);
	gtk_box_pack_start(GTK_BOX(hbox), key_entry, TRUE, TRUE, 0);
	button = gtk_button_new_with_label("Grab");
	g_signal_connect(G_OBJECT(button), "clicked",
		G_CALLBACK(grab_clicked), NULL);
	gtk_box_pack_start(GTK_BOX(hbox), button, FALSE, FALSE, 0);

	cmd_entry = gtk_entry_new();
	gtk_table_attach_defaults(GTK_TABLE(table), cmd_entry, 1, 2, 1, 2);
	g_signal_connect(G_OBJECT(cmd_entry), "changed",
		G_CALLBACK(entry_changed), sel);

	alignment = gtk_alignment_new(1, 0.5, 0, 1);
	gtk_box_pack_start(GTK_BOX(vbox), alignment, FALSE, FALSE, 0);
	hbox = gtk_hbox_new(FALSE, 5);
	gtk_container_add(GTK_CONTAINER(alignment), hbox);
	add_update_button = gtk_button_new_with_label("Add");
	g_signal_connect(G_OBJECT(add_update_button), "clicked",
		G_CALLBACK(add_update_clicked), sel);
	gtk_box_pack_start(GTK_BOX(hbox), add_update_button, FALSE, FALSE, 0);
	delete_button = gtk_button_new_with_label("Delete");
	gtk_box_pack_start(GTK_BOX(hbox), delete_button, FALSE, FALSE, 0);
	g_signal_connect(G_OBJECT(delete_button), "clicked",
		G_CALLBACK(row_delete), sel);
	button = gtk_button_new_with_label("Close");
	gtk_box_pack_start(GTK_BOX(hbox), button, FALSE, FALSE, 0);
	g_signal_connect(G_OBJECT(button), "clicked",
		gtk_main_quit, NULL);

  g_signal_connect(G_OBJECT(window), "destroy",
		G_CALLBACK(gtk_main_quit), NULL);
  
  return window;
}

static void read_hotkeys() {
	GSList *entries, *p;
	int i;

	for (i=0; i<MAX_HOTKEYS; i++) {
		if (hotkey[i].key) g_free(hotkey[i].key);
		if (hotkey[i].cmd) g_free(hotkey[i].cmd);
		hotkey[i].key = hotkey[i].cmd = NULL;
	}

	entries = gconf_client_all_entries(conf, BINDINGS, NULL);
	for (p=entries; p; p=p->next) {
		GConfEntry *ent = (GConfEntry*)p->data;
		if (key_into_hotkeys(ent))
			gconf_client_notify_add(conf, ent->key, key_updated, NULL, NULL, NULL);
		gconf_value_free(ent->value);
	}
	g_slist_free(entries);

	entries = gconf_client_all_entries(conf, COMMANDS, NULL);
	for (p=entries; p; p=p->next) {
		GConfEntry *ent = (GConfEntry*)p->data;
		if (key_into_hotkeys(ent))
			gconf_client_notify_add(conf, ent->key, key_updated, NULL, NULL, NULL);
		gconf_value_free(ent->value);
	}
	g_slist_free(entries);
}

static void add_hotkeys_to_model() {
	int i;
	GtkTreeIter iter;

	gtk_list_store_clear(list_model);

	gtk_list_store_append(list_model, &iter);
	gtk_list_store_set(list_model, &iter,
		COLUMN_CMD, "< add new command >", -1);
	for (i=0; i<MAX_HOTKEYS; i++) {
		if (hotkey[i].key && hotkey[i].cmd) {
			gtk_list_store_append(list_model, &iter);
			gtk_list_store_set(list_model, &iter,
				COLUMN_CMD, hotkey[i].cmd,
				COLUMN_KEY, hotkey[i].key,
				COLUMN_KEYNUM, i+1,
				-1);
		}
	}
}

static void row_changed(GtkTreeSelection *sel) {
	GtkTreeIter iter;
	GValue val = { 0 };
	int keyno;
	GtkTreeModel *model;

	if (!gtk_tree_selection_get_selected(sel, &model, &iter)) {
		gtk_entry_set_text(GTK_ENTRY(cmd_entry), "");
		gtk_entry_set_text(GTK_ENTRY(key_entry), "");
		gtk_widget_set_sensitive(delete_button, FALSE);
		gtk_button_set_label(GTK_BUTTON(add_update_button), "Add");
		gtk_widget_set_sensitive(add_update_button, FALSE);
		return;
	}
	gtk_tree_model_get_value(model, &iter, COLUMN_KEYNUM, &val);
	keyno = g_value_get_int(&val);

	if (keyno == 0) {
		gtk_entry_set_text(GTK_ENTRY(cmd_entry), "");
		gtk_entry_set_text(GTK_ENTRY(key_entry), "");
		gtk_widget_set_sensitive(delete_button, FALSE);
		gtk_widget_set_sensitive(add_update_button, FALSE);
		gtk_button_set_label(GTK_BUTTON(add_update_button), "Add");
	}
	else {
		gtk_entry_set_text(GTK_ENTRY(cmd_entry), hotkey[keyno-1].cmd);
		gtk_entry_set_text(GTK_ENTRY(key_entry), hotkey[keyno-1].key);
		gtk_widget_set_sensitive(delete_button, TRUE);
		gtk_widget_set_sensitive(add_update_button, FALSE);
		gtk_button_set_label(GTK_BUTTON(add_update_button), "Update");
	}
}

void row_delete(GtkWidget *button, gpointer user_data) {
	int keyno = get_selected_keyno(GTK_TREE_SELECTION(user_data));
	gchar *confkey;

	g_return_if_fail(keyno > 0);

	confkey = g_strdup_printf(BINDINGS"/run_command_%d", keyno);
	gconf_client_set_string(conf, confkey, "disabled", NULL);
	g_free(confkey);
	confkey = g_strdup_printf(COMMANDS"/command_%d", keyno);
	gconf_client_set_string(conf, confkey, " ", NULL);
	g_free(confkey);
}

void entry_changed(GtkWidget *widget, gpointer user_data) {
	const char *key, *cmd;
	gboolean state;
	int keyno = get_selected_keyno(GTK_TREE_SELECTION(user_data));

	key = gtk_entry_get_text(GTK_ENTRY(key_entry));
	cmd = gtk_entry_get_text(GTK_ENTRY(cmd_entry));
	state = (key != NULL && key[0] != '\0' && cmd != NULL && cmd[0] != '\0');
	if (keyno > 0) {
		state &= (strcmp(key, hotkey[keyno-1].key) || strcmp(cmd, hotkey[keyno-1].cmd));
	}
	gtk_widget_set_sensitive(add_update_button, state);
}

int get_selected_keyno(GtkTreeSelection *sel) {
	GtkTreeIter iter;
	GtkTreeModel *model;
	GValue val = { 0 };

	if (gtk_tree_selection_get_selected(sel, &model, &iter)) {
		gtk_tree_model_get_value(model, &iter, COLUMN_KEYNUM, &val);
		return g_value_get_int(&val);
	}
	return -1;
}

static void grab_clicked(GtkWidget *button) {
	GtkWidget *w, *label;

	w = gtk_window_new(GTK_WINDOW_TOPLEVEL);
	gtk_window_set_modal(GTK_WINDOW(w), TRUE);
	gtk_window_set_decorated(GTK_WINDOW(w), FALSE);
	gtk_window_set_transient_for(GTK_WINDOW(w), GTK_WINDOW(main_window));
	gtk_window_set_position(GTK_WINDOW(w), GTK_WIN_POS_CENTER_ON_PARENT);
	gtk_container_set_border_width(GTK_CONTAINER(w), 15);
	label = gtk_label_new("Press the key or key combination now.\nPress Escape to cancel.");
	gtk_label_set_justify(GTK_LABEL(label), GTK_JUSTIFY_CENTER);
	gtk_container_add(GTK_CONTAINER(w), label);
	gtk_widget_show_all(w);

	gdk_keyboard_grab(w->window, FALSE, GDK_CURRENT_TIME);
	g_signal_connect(G_OBJECT(w), "key_press_event",
		G_CALLBACK(grab_key_pressed), NULL);
}

static void grab_key_pressed(GtkWidget *w, GdkEventKey *ev) {
	char *accel;
	if (!gtk_accelerator_valid(ev->keyval, ev->state)) return;
	gdk_keyboard_ungrab(GDK_CURRENT_TIME);
	gtk_widget_destroy(w);

	accel = gtk_accelerator_name(ev->keyval, ev->state);
	/* Escape means cancel, anything else is a valid accelerator. */
	if (strcasecmp(accel, "Escape") != 0)
		gtk_entry_set_text(GTK_ENTRY(key_entry), accel);
	g_free(accel);
}

static void add_update_clicked(GtkWidget *w, gpointer user_data) {
	int keyno = get_selected_keyno(GTK_TREE_SELECTION(user_data));
	if (keyno > 0) {
		set_confkey_pair(keyno);
		gtk_widget_set_sensitive(add_update_button, FALSE);
	}
	else {
		int i, keyno=-1;

		for (i=0; i<MAX_HOTKEYS; i++)
			if (hotkey[i].key == NULL || hotkey[i].cmd == NULL) {
				keyno = i+1;
				break;
			}
		g_return_if_fail(keyno != -1);

		set_confkey_pair(keyno);
		gtk_entry_set_text(GTK_ENTRY(key_entry), "");
		gtk_entry_set_text(GTK_ENTRY(cmd_entry), "");
		gtk_widget_set_sensitive(add_update_button, FALSE);
	}
}

static void key_updated(GConfClient *conf, guint cnxn_id, GConfEntry *entry,
		gpointer user_data) {
	key_into_hotkeys(entry);
	add_hotkeys_to_model();
}

static gboolean key_into_hotkeys(GConfEntry *ent) {
	g_return_val_if_fail(ent->value->type == GCONF_VALUE_STRING, FALSE);

	if (!strncmp(ent->key, BINDINGS"/run_command_", sizeof(BINDINGS)-1+13)) {
		int keyno = atoi(ent->key + sizeof(BINDINGS) - 1 + 13);
		g_return_val_if_fail(keyno >= 1 && keyno <= MAX_HOTKEYS, FALSE);

		if (!strcmp(gconf_value_get_string(ent->value), "disabled")) {
			if (hotkey[keyno-1].key) {
				g_free(hotkey[keyno-1].key);
				hotkey[keyno-1].key = NULL;
			}
			return TRUE;  /* worth watching, but nothing to grab */
		}

		hotkey[keyno-1].key = g_strdup(gconf_value_get_string(ent->value));

		return TRUE;
	}
	else if (!strncmp(ent->key, COMMANDS"/command_", sizeof(COMMANDS)-1+9)) {
		int keyno = atoi(ent->key + sizeof(COMMANDS) - 1 + 9);
		g_return_val_if_fail(keyno >= 1 && keyno <= MAX_HOTKEYS, FALSE);
		hotkey[keyno-1].cmd = g_strdup(gconf_value_get_string(ent->value));

		return TRUE;
	}

	return FALSE;
}

static void set_confkey_pair(int keyno) {
	const char *key = gtk_entry_get_text(GTK_ENTRY(key_entry));
	const char *cmd = gtk_entry_get_text(GTK_ENTRY(cmd_entry));
	char *confkey;

	g_assert(key && key[0] && cmd && cmd[0]);

	confkey = g_strdup_printf(BINDINGS"/run_command_%d", keyno);
	gconf_client_set_string(conf, confkey, key, NULL);
	g_free(confkey);
	confkey = g_strdup_printf(COMMANDS"/command_%d", keyno);
	gconf_client_set_string(conf, confkey, cmd, NULL);
	g_free(confkey);
}
