summaryrefslogtreecommitdiff
path: root/modules/gtk
diff options
context:
space:
mode:
authorCharles Lehner <cel@celehner.com>2015-07-05 13:55:32 -0400
committerCharles Lehner <cel@celehner.com>2015-07-05 15:11:49 -0400
commit9ef06aa7fb2a236e6a41cd9c5cb34c73a4a7a378 (patch)
tree3ff77340f4ef6e58d18b52d81d8d8c18ce71a2ce /modules/gtk
parent5c6821f742c2b3751fff5574bba3666826d723f1 (diff)
Add gtk module
Diffstat (limited to 'modules/gtk')
-rw-r--r--modules/gtk/call_window.c489
-rw-r--r--modules/gtk/dial_dialog.c96
-rw-r--r--modules/gtk/gtk_mod.c853
-rw-r--r--modules/gtk/gtk_mod.h51
-rw-r--r--modules/gtk/module.mk14
-rw-r--r--modules/gtk/transfer_dialog.c131
-rw-r--r--modules/gtk/uri_entry.c44
7 files changed, 1678 insertions, 0 deletions
diff --git a/modules/gtk/call_window.c b/modules/gtk/call_window.c
new file mode 100644
index 0000000..11677ff
--- /dev/null
+++ b/modules/gtk/call_window.c
@@ -0,0 +1,489 @@
+/**
+ * @file gtk/call_window.c GTK+ call window
+ *
+ * Copyright (C) 2015 Charles E. Lehner
+ */
+
+#include <re.h>
+#include <baresip.h>
+#include <gtk/gtk.h>
+#include "gtk_mod.h"
+
+struct call_window {
+ struct gtk_mod *mod;
+ struct call *call;
+ struct mqueue *mq; /* for communicating from gtk thread to main thread */
+ struct {
+ struct vumeter_dec *dec;
+ struct vumeter_enc *enc;
+ } vu;
+ struct transfer_dialog *transfer_dialog;
+ GtkWidget *window;
+ GtkLabel *status;
+ GtkLabel *duration;
+ struct {
+ GtkWidget *hangup, *transfer, *hold, *mute;
+ } buttons;
+ struct {
+ GtkProgressBar *enc, *dec;
+ } progress;
+ guint duration_timer_tag;
+ guint vumeter_timer_tag;
+ bool closed;
+ int cur_key;
+};
+
+enum call_window_events {
+ MQ_HANGUP,
+ MQ_CLOSE,
+ MQ_HOLD,
+ MQ_MUTE,
+ MQ_TRANSFER,
+};
+
+static struct call_window *last_call_win = NULL;
+static struct vumeter_dec *last_dec = NULL;
+static struct vumeter_enc *last_enc = NULL;
+
+
+static void call_window_update_duration(struct call_window *win) {
+ gchar buf[32];
+
+ const uint32_t dur = call_duration(win->call);
+ const uint32_t sec = dur%60%60;
+ const uint32_t min = dur/60%60;
+ const uint32_t hrs = dur/60/60;
+
+ re_snprintf(buf, sizeof buf, "%u:%02u:%02u", hrs, min, sec);
+ gtk_label_set_text(win->duration, buf);
+}
+
+
+static void call_window_update_vumeters(struct call_window *win) {
+ double value;
+
+ if (win->vu.enc && win->vu.enc->started) {
+ value = min((double)win->vu.enc->avg_rec / 0x4000, 1);
+ gtk_progress_bar_set_fraction(win->progress.enc, value);
+ }
+ if (win->vu.dec && win->vu.dec->started) {
+ value = min((double)win->vu.dec->avg_play / 0x4000, 1);
+ gtk_progress_bar_set_fraction(win->progress.dec, value);
+ }
+}
+
+
+static gboolean call_timer(gpointer arg) {
+ struct call_window *win = arg;
+ call_window_update_duration(win);
+ return G_SOURCE_CONTINUE;
+}
+
+
+static gboolean vumeter_timer(gpointer arg) {
+ struct call_window *win = arg;
+ call_window_update_vumeters(win);
+ return G_SOURCE_CONTINUE;
+}
+
+static void vumeter_timer_start(struct call_window *win) {
+ if (!win->vumeter_timer_tag)
+ win->vumeter_timer_tag =
+ g_timeout_add(100, vumeter_timer, win);
+ if (win->vu.enc)
+ win->vu.enc->avg_rec = 0;
+ if (win->vu.dec)
+ win->vu.dec->avg_play = 0;
+}
+
+static void vumeter_timer_stop(struct call_window *win) {
+ if (win->vumeter_timer_tag) {
+ g_source_remove(win->vumeter_timer_tag);
+ win->vumeter_timer_tag = 0;
+ }
+ gtk_progress_bar_set_fraction(win->progress.enc, 0);
+ gtk_progress_bar_set_fraction(win->progress.dec, 0);
+}
+
+static void call_window_set_vu_dec(struct call_window *win,
+ struct vumeter_dec *dec)
+{
+ if (win->vu.dec)
+ mem_deref(win->vu.dec);
+ win->vu.dec = mem_ref(dec);
+ vumeter_timer_start(win);
+}
+
+static void call_window_set_vu_enc(struct call_window *win,
+ struct vumeter_enc *enc)
+{
+ if (win->vu.enc)
+ mem_deref(win->vu.enc);
+ win->vu.enc = mem_ref(enc);
+ vumeter_timer_start(win);
+}
+
+/* This is a hack to associate a call with its vumeters */
+
+void call_window_got_vu_dec(struct vumeter_dec *dec)
+{
+ if (last_call_win)
+ call_window_set_vu_dec(last_call_win, dec);
+ else
+ last_dec = dec;
+}
+
+void call_window_got_vu_enc(struct vumeter_enc *enc)
+{
+ if (last_call_win)
+ call_window_set_vu_enc(last_call_win, enc);
+ else
+ last_enc = enc;
+}
+
+static void got_call_window(struct call_window *win)
+{
+ if (last_enc)
+ call_window_set_vu_enc(win, last_enc);
+ if (last_dec)
+ call_window_set_vu_dec(win, last_dec);
+ if (!last_enc || !last_dec)
+ last_call_win = win;
+}
+
+
+static void call_on_hangup(GtkToggleButton *btn, struct call_window *win)
+{
+ (void)btn;
+ mqueue_push(win->mq, MQ_CLOSE, win);
+}
+
+static void call_on_hold_toggle(GtkToggleButton *btn, struct call_window *win)
+{
+ bool hold = gtk_toggle_button_get_active(btn);
+ if (hold)
+ vumeter_timer_stop(win);
+ else
+ vumeter_timer_start(win);
+ mqueue_push(win->mq, MQ_HOLD, (void *)(size_t)hold);
+}
+
+static void call_on_mute_toggle(GtkToggleButton *btn, struct call_window *win)
+{
+ bool mute = gtk_toggle_button_get_active(btn);
+ mqueue_push(win->mq, MQ_MUTE, (void *)(size_t)mute);
+}
+
+static void call_on_transfer(GtkToggleButton *btn, struct call_window *win)
+{
+ (void)btn;
+ if (!win->transfer_dialog)
+ win->transfer_dialog = transfer_dialog_alloc(win);
+ else
+ transfer_dialog_show(win->transfer_dialog);
+}
+
+static gboolean call_on_window_close(GtkWidget *widget, GdkEventAny *event,
+ struct call_window *win)
+{
+ (void)event;
+ (void)widget;
+ mqueue_push(win->mq, MQ_CLOSE, NULL);
+ return TRUE;
+}
+
+
+static gboolean call_on_key_press(GtkWidget *window, GdkEvent *ev,
+ struct call_window *win)
+{
+ gchar key = ev->key.string[0];
+ (void)window;
+
+ switch (key) {
+
+ case '1': case '2': case '3':
+ case '4': case '5': case '6':
+ case '7': case '8': case '9':
+ case '*': case '0': case '#':
+ win->cur_key = key;
+ call_send_digit(win->call, key);
+ return TRUE;
+
+ default:
+ return FALSE;
+ }
+}
+
+
+static gboolean call_on_key_release(GtkWidget *window, GdkEvent *ev,
+ struct call_window *win)
+{
+ (void)window;
+
+ if (win->cur_key && win->cur_key == ev->key.string[0]) {
+ win->cur_key = 0;
+ call_send_digit(win->call, 0);
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+
+static void call_window_set_status(struct call_window *win,
+ const char *status)
+{
+ gtk_label_set_text(win->status, status);
+}
+
+
+static void mqueue_handler(int id, void *data, void *arg)
+{
+ struct call_window *win = arg;
+
+ switch ((enum call_window_events)id) {
+
+ case MQ_HANGUP:
+ ua_hangup(uag_current(), win->call, 0, NULL);
+ break;
+
+ case MQ_CLOSE:
+ ua_hangup(uag_current(), win->call, 0, NULL);
+ win->closed = true;
+ mem_deref(win);
+ break;
+
+ case MQ_MUTE:
+ audio_mute(call_audio(win->call), (size_t)data);
+ break;
+
+ case MQ_HOLD:
+ call_hold(win->call, (size_t)data);
+ break;
+
+ case MQ_TRANSFER:
+ call_transfer(win->call, data);
+ break;
+ }
+}
+
+static void call_window_destructor(void *arg)
+{
+ struct call_window *window = arg;
+
+ gdk_threads_enter();
+ gtk_mod_call_window_closed(window->mod, window);
+ gtk_widget_destroy(window->window);
+ mem_deref(window->transfer_dialog);
+ gdk_threads_leave();
+
+ mem_deref(window->call);
+ mem_deref(window->mq);
+ mem_deref(window->vu.enc);
+ mem_deref(window->vu.dec);
+ if (window->duration_timer_tag)
+ g_source_remove(window->duration_timer_tag);
+ if (window->vumeter_timer_tag)
+ g_source_remove(window->vumeter_timer_tag);
+ /* TODO: avoid race conditions here */
+ last_call_win = NULL;
+}
+
+struct call_window *call_window_new(struct call *call, struct gtk_mod *mod)
+{
+ struct call_window *win;
+ GtkWidget *window, *label, *status, *button, *progress, *image;
+ GtkWidget *button_box, *vbox, *hbox;
+ GtkWidget *duration;
+ int err = 0;
+
+ win = mem_zalloc(sizeof(*win), call_window_destructor);
+ if (!win)
+ return NULL;
+
+ mqueue_alloc(&win->mq, mqueue_handler, win);
+ if (err)
+ goto out;
+
+ window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
+ gtk_window_set_title(GTK_WINDOW(window), call_peeruri(call));
+ gtk_window_set_type_hint(GTK_WINDOW(window),
+ GDK_WINDOW_TYPE_HINT_DIALOG);
+
+ vbox = gtk_vbox_new (FALSE, 0);
+ gtk_container_add(GTK_CONTAINER(window), vbox);
+
+ /* Peer name and URI */
+ label = gtk_label_new(call_peername(call));
+ gtk_box_pack_start(GTK_BOX(vbox), label, FALSE, FALSE, 0);
+
+ label = gtk_label_new(call_peeruri(call));
+ gtk_box_pack_start(GTK_BOX(vbox), label, FALSE, FALSE, 0);
+
+ /* Call duration */
+ duration = gtk_label_new(NULL);
+ gtk_box_pack_start(GTK_BOX(vbox), duration, FALSE, FALSE, 0);
+
+ /* Status */
+ status = gtk_label_new(NULL);
+ gtk_box_pack_start(GTK_BOX(vbox), status, FALSE, FALSE, 0);
+
+ /* Progress bars */
+ hbox = gtk_hbox_new(FALSE, 0);
+ gtk_box_set_spacing(GTK_BOX(hbox), 6);
+ gtk_container_set_border_width(GTK_CONTAINER(hbox), 5);
+ gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0);
+
+ /* Encoding vumeter */
+ image = gtk_image_new_from_icon_name("audio-input-microphone",
+ GTK_ICON_SIZE_BUTTON);
+ progress = gtk_progress_bar_new();
+ win->progress.enc = GTK_PROGRESS_BAR(progress);
+ gtk_box_pack_start(GTK_BOX(hbox), image, FALSE, FALSE, 0);
+ gtk_box_pack_start(GTK_BOX(hbox), progress, FALSE, FALSE, 0);
+
+ /* Decoding vumeter */
+ image = gtk_image_new_from_icon_name("audio-headphones",
+ GTK_ICON_SIZE_BUTTON);
+ progress = gtk_progress_bar_new();
+ win->progress.dec = GTK_PROGRESS_BAR(progress);
+ gtk_box_pack_end(GTK_BOX(hbox), progress, FALSE, FALSE, 0);
+ gtk_box_pack_end(GTK_BOX(hbox), image, FALSE, FALSE, 0);
+
+ /* Buttons */
+ button_box = gtk_hbutton_box_new();
+ gtk_button_box_set_layout(GTK_BUTTON_BOX(button_box),
+ GTK_BUTTONBOX_END);
+ gtk_box_set_spacing(GTK_BOX(button_box), 6);
+ gtk_container_set_border_width(GTK_CONTAINER(button_box), 5);
+ gtk_box_pack_end(GTK_BOX(vbox), button_box, FALSE, TRUE, 0);
+
+ /* Hang up */
+ button = gtk_button_new_with_label("Hangup");
+ win->buttons.hangup = button;
+ gtk_box_pack_end(GTK_BOX(button_box), button, FALSE, TRUE, 0);
+ g_signal_connect(button, "clicked",
+ G_CALLBACK(call_on_hangup), win);
+ image = gtk_image_new_from_icon_name("call-stop",
+ GTK_ICON_SIZE_BUTTON);
+ gtk_button_set_image(GTK_BUTTON(button), image);
+
+ /* Transfer */
+ button = gtk_button_new_with_label("Transfer");
+ win->buttons.transfer = button;
+ gtk_box_pack_end(GTK_BOX(button_box), button, FALSE, TRUE, 0);
+ g_signal_connect(button, "clicked", G_CALLBACK(call_on_transfer), win);
+ image = gtk_image_new_from_icon_name("forward", GTK_ICON_SIZE_BUTTON);
+ gtk_button_set_image(GTK_BUTTON(button), image);
+
+ /* Hold */
+ button = gtk_toggle_button_new_with_label("Hold");
+ win->buttons.hold = button;
+ gtk_box_pack_end(GTK_BOX(button_box), button, FALSE, TRUE, 0);
+ g_signal_connect(button, "toggled",
+ G_CALLBACK(call_on_hold_toggle), win);
+ image = gtk_image_new_from_icon_name("player_pause",
+ GTK_ICON_SIZE_BUTTON);
+ gtk_button_set_image(GTK_BUTTON(button), image);
+
+ /* Mute */
+ button = gtk_toggle_button_new_with_label("Mute");
+ win->buttons.mute = button;
+ gtk_box_pack_end(GTK_BOX(button_box), button, FALSE, TRUE, 0);
+ g_signal_connect(button, "toggled",
+ G_CALLBACK(call_on_mute_toggle), win);
+ image = gtk_image_new_from_icon_name("microphone-sensitivity-muted",
+ GTK_ICON_SIZE_BUTTON);
+ gtk_button_set_image(GTK_BUTTON(button), image);
+
+ gtk_widget_show_all(window);
+ gtk_window_present(GTK_WINDOW(window));
+
+ g_signal_connect(window, "delete_event",
+ G_CALLBACK(call_on_window_close), win);
+ g_signal_connect(window, "key-press-event",
+ G_CALLBACK(call_on_key_press), win);
+ g_signal_connect(window, "key-release-event",
+ G_CALLBACK(call_on_key_release), win);
+
+ win->call = mem_ref(call);
+ win->mod = mod;
+ win->window = window;
+ win->transfer_dialog = NULL;
+ win->status = GTK_LABEL(status);
+ win->duration = GTK_LABEL(duration);
+ win->closed = false;
+ win->duration_timer_tag = 0;
+ win->vumeter_timer_tag = 0;
+ win->vu.enc = NULL;
+ win->vu.dec = NULL;
+
+ got_call_window(win);
+
+out:
+ if (err)
+ mem_deref(win);
+
+ return win;
+}
+
+void call_window_transfer(struct call_window *win, const char *uri)
+{
+ mqueue_push(win->mq, MQ_TRANSFER, (char *)uri);
+}
+
+void call_window_closed(struct call_window *win, const char *reason)
+{
+ char buf[256];
+ const char *status;
+
+ vumeter_timer_stop(win);
+ if (win->duration_timer_tag) {
+ g_source_remove(win->duration_timer_tag);
+ win->duration_timer_tag = 0;
+ }
+ gtk_widget_set_sensitive(win->buttons.transfer, FALSE);
+ gtk_widget_set_sensitive(win->buttons.hold, FALSE);
+ gtk_widget_set_sensitive(win->buttons.mute, FALSE);
+
+ if (reason && reason[0]) {
+ re_snprintf(buf, sizeof buf, "closed: %s", reason);
+ status = buf;
+ } else {
+ status = "closed";
+ }
+ call_window_set_status(win, status);
+ win->transfer_dialog = mem_deref(win->transfer_dialog);
+}
+
+void call_window_ringing(struct call_window *win)
+{
+ call_window_set_status(win, "ringing");
+}
+
+void call_window_progress(struct call_window *win)
+{
+ win->duration_timer_tag = g_timeout_add_seconds(1, call_timer, win);
+ last_call_win = win;
+ call_window_set_status(win, "progress");
+}
+
+void call_window_established(struct call_window *win)
+{
+ call_window_update_duration(win);
+ win->duration_timer_tag = g_timeout_add_seconds(1, call_timer, win);
+ last_call_win = win;
+ call_window_set_status(win, "established");
+}
+
+void call_window_transfer_failed(struct call_window *win, const char *reason)
+{
+ if (win->transfer_dialog) {
+ transfer_dialog_fail(win->transfer_dialog, reason);
+ }
+}
+
+bool call_window_is_for_call(struct call_window *win, struct call *call)
+{
+ return win->call == call;
+}
diff --git a/modules/gtk/dial_dialog.c b/modules/gtk/dial_dialog.c
new file mode 100644
index 0000000..340362b
--- /dev/null
+++ b/modules/gtk/dial_dialog.c
@@ -0,0 +1,96 @@
+/**
+ * @file gtk/dial_dialog.c GTK+ dial dialog
+ *
+ * Copyright (C) 2015 Charles E. Lehner
+ */
+
+#include <re.h>
+#include <baresip.h>
+#include <stdlib.h>
+#include <pthread.h>
+#include <gtk/gtk.h>
+#include "gtk_mod.h"
+
+struct dial_dialog {
+ struct gtk_mod *mod;
+ GtkWidget *dialog;
+ GtkComboBox *uri_combobox;
+};
+
+static void dial_dialog_on_response(GtkDialog *dialog, gint response_id,
+ gpointer arg)
+{
+ struct dial_dialog *dd = arg;
+ char *uri;
+
+ if (response_id == GTK_RESPONSE_ACCEPT) {
+ uri = (char *)uri_combo_box_get_text(dd->uri_combobox);
+ gtk_mod_connect(dd->mod, uri);
+ }
+
+ gtk_widget_hide(GTK_WIDGET(dialog));
+}
+
+
+static void destructor(void *arg)
+{
+ struct dial_dialog *dd = arg;
+
+ gtk_widget_destroy(dd->dialog);
+}
+
+struct dial_dialog *dial_dialog_alloc(struct gtk_mod *mod)
+{
+ struct dial_dialog *dd;
+ GtkWidget *dial;
+ GtkWidget *content, *button, *image;
+ GtkWidget *uri_combobox;
+
+ dd = mem_zalloc(sizeof(*dd), destructor);
+ if (!dd)
+ return NULL;
+
+ dial = gtk_dialog_new_with_buttons("Dial", NULL, 0, NULL);
+
+ /* Cancel */
+ button = gtk_button_new_with_label("Cancel");
+ image = gtk_image_new_from_icon_name("call-stop",
+ GTK_ICON_SIZE_BUTTON);
+ gtk_button_set_image(GTK_BUTTON(button), image);
+ gtk_dialog_add_action_widget(GTK_DIALOG(dial), button,
+ GTK_RESPONSE_REJECT);
+
+ /* Call */
+ button = gtk_button_new_with_label("Call");
+ image = gtk_image_new_from_icon_name("call-start",
+ GTK_ICON_SIZE_BUTTON);
+ gtk_button_set_image(GTK_BUTTON(button), image);
+ gtk_dialog_add_action_widget(GTK_DIALOG(dial), button,
+ GTK_RESPONSE_ACCEPT);
+ gtk_widget_set_can_default (button, TRUE);
+
+ gtk_dialog_set_default_response(GTK_DIALOG(dial),
+ GTK_RESPONSE_ACCEPT);
+ uri_combobox = uri_combo_box_new();
+
+ content = gtk_dialog_get_content_area(GTK_DIALOG(dial));
+ gtk_box_pack_start(GTK_BOX(content), uri_combobox, FALSE, FALSE, 5);
+ gtk_widget_show_all(content);
+
+ g_signal_connect(G_OBJECT(dial), "response",
+ G_CALLBACK(dial_dialog_on_response), dd);
+ g_signal_connect(G_OBJECT(dial), "delete-event",
+ G_CALLBACK(gtk_widget_hide_on_delete), dd);
+
+ dd->dialog = dial;
+ dd->uri_combobox = GTK_COMBO_BOX(uri_combobox);
+ dd->mod = mod;
+
+ return dd;
+}
+
+void dial_dialog_show(struct dial_dialog *dd)
+{
+ gtk_window_present(GTK_WINDOW(dd->dialog));
+ gtk_widget_grab_focus(gtk_bin_get_child(GTK_BIN(dd->uri_combobox)));
+}
diff --git a/modules/gtk/gtk_mod.c b/modules/gtk/gtk_mod.c
new file mode 100644
index 0000000..813c0f1
--- /dev/null
+++ b/modules/gtk/gtk_mod.c
@@ -0,0 +1,853 @@
+/**
+ * @file gtk/gtk_mod.c GTK+ UI module
+ *
+ * Copyright (C) 2015 Charles E. Lehner
+ * Copyright (C) 2010 - 2015 Creytiv.com
+ */
+#include <re.h>
+#include <baresip.h>
+#include <stdlib.h>
+#include <pthread.h>
+#include <gtk/gtk.h>
+#include <gio/gio.h>
+#include "gtk_mod.h"
+
+/* About */
+#define COPYRIGHT " Copyright (C) 2010 - 2015 Alfred E. Heggestad et al."
+#define COMMENTS "A modular SIP User-Agent with audio and video support"
+#define WEBSITE "http://www.creytiv.com/baresip.html"
+#define LICENSE "BSD"
+
+/**
+ * @defgroup gtk_mod gtk_mod
+ *
+ * GTK+ Menu-based User-Interface module
+ *
+ * Creates a tray icon with a menu for making calls.
+ *
+ */
+
+struct gtk_mod {
+ pthread_t thread;
+ bool run;
+ bool contacts_inited;
+ bool accounts_inited;
+ struct mqueue *mq;
+ GApplication *app;
+ GtkStatusIcon *status_icon;
+ GtkWidget *app_menu;
+ GtkWidget *contacts_menu;
+ GtkWidget *accounts_menu;
+ GtkWidget *status_menu;
+ GSList *accounts_menu_group;
+ struct dial_dialog *dial_dialog;
+ GSList *call_windows;
+};
+
+struct gtk_mod mod_obj;
+
+enum gtk_mod_events {
+ MQ_CONNECT,
+ MQ_QUIT,
+ MQ_ANSWER,
+ MQ_HANGUP,
+ MQ_SELECT_UA,
+};
+
+static void answer_activated(GSimpleAction *, GVariant *, gpointer);
+static void reject_activated(GSimpleAction *, GVariant *, gpointer);
+
+static GActionEntry app_entries[] = {
+ {"answer", answer_activated, "x", NULL, NULL, {0} },
+ {"reject", reject_activated, "x", NULL, NULL, {0} },
+};
+
+static struct call *get_call_from_gvariant(GVariant *param)
+{
+ gint64 call_ptr;
+ struct call *call;
+ struct list *calls = ua_calls(uag_current());
+ struct le *le;
+
+ call_ptr = g_variant_get_int64(param);
+ call = GINT_TO_POINTER(call_ptr);
+
+ for (le = list_head(calls); le; le = le->next)
+ if (le->data == call)
+ return call;
+
+ return NULL;
+}
+
+
+static void menu_on_about(GtkMenuItem *menuItem, gpointer arg)
+{
+ (void)menuItem;
+ (void)arg;
+
+ gtk_show_about_dialog(NULL,
+ "program-name", "baresip",
+ "version", BARESIP_VERSION,
+ "logo-icon-name", "call-start",
+ "copyright", COPYRIGHT,
+ "comments", COMMENTS,
+ "website", WEBSITE,
+ "license", LICENSE,
+ NULL);
+}
+
+static void menu_on_quit(GtkMenuItem *menuItem, gpointer arg)
+{
+ struct gtk_mod *mod = arg;
+ (void)menuItem;
+
+ gtk_widget_destroy(GTK_WIDGET(mod->app_menu));
+ g_object_unref(G_OBJECT(mod->status_icon));
+
+ mqueue_push(mod->mq, MQ_QUIT, 0);
+ info("quit from gtk\n");
+}
+
+static void menu_on_dial(GtkMenuItem *menuItem, gpointer arg)
+{
+ struct gtk_mod *mod = arg;
+ (void)menuItem;
+ if (!mod->dial_dialog)
+ mod->dial_dialog = dial_dialog_alloc(mod);
+ dial_dialog_show(mod->dial_dialog);
+}
+
+static void menu_on_dial_contact(GtkMenuItem *menuItem, gpointer arg)
+{
+ struct gtk_mod *mod = arg;
+ const char *uri = gtk_menu_item_get_label(menuItem);
+ /* Queue dial from the main thread */
+ mqueue_push(mod->mq, MQ_CONNECT, (char *)uri);
+}
+
+static void init_contacts_menu(struct gtk_mod *mod)
+{
+ struct le *le;
+ GtkWidget *item;
+ GtkMenuShell *contacts_menu = GTK_MENU_SHELL(mod->contacts_menu);
+
+ /* Add contacts to submenu */
+ for (le = list_head(contact_list()); le; le = le->next) {
+ struct contact *c = le->data;
+ item = gtk_menu_item_new_with_label(contact_str(c));
+ gtk_menu_shell_append(contacts_menu, item);
+ g_signal_connect(G_OBJECT(item), "activate",
+ G_CALLBACK(menu_on_dial_contact), mod);
+ }
+}
+
+
+static void menu_on_account_toggled(GtkCheckMenuItem *menu_item,
+ struct gtk_mod *mod)
+{
+ struct ua *ua = g_object_get_data(G_OBJECT(menu_item), "ua");
+ if (menu_item->active)
+ mqueue_push(mod->mq, MQ_SELECT_UA, ua);
+}
+
+
+static void menu_on_presence_set(GtkMenuItem *item, struct gtk_mod *mod)
+{
+ struct le *le;
+ void *type = g_object_get_data(G_OBJECT(item), "presence");
+ enum presence_status status = GPOINTER_TO_UINT(type);
+ (void)mod;
+
+ for (le = list_head(uag_list()); le; le = le->next) {
+ struct ua *ua = le->data;
+ ua_presence_status_set(ua, status);
+ }
+}
+
+
+static GtkMenuItem *accounts_menu_add_item(struct gtk_mod *mod,
+ struct ua *ua)
+{
+ GtkMenuShell *accounts_menu = GTK_MENU_SHELL(mod->accounts_menu);
+ GtkWidget *item;
+ GSList *group = mod->accounts_menu_group;
+ struct ua *ua_current = uag_current();
+ char buf[256];
+
+ re_snprintf(buf, sizeof buf, "%s%s", ua_aor(ua),
+ ua_isregistered(ua) ? " (OK)" : "");
+ item = gtk_radio_menu_item_new_with_label(group, buf);
+ group = gtk_radio_menu_item_get_group(
+ GTK_RADIO_MENU_ITEM (item));
+ if (ua == ua_current)
+ gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(item),
+ TRUE);
+ g_object_set_data(G_OBJECT(item), "ua", ua);
+ g_signal_connect(item, "toggled",
+ G_CALLBACK(menu_on_account_toggled), mod);
+ gtk_menu_shell_append(accounts_menu, item);
+ mod->accounts_menu_group = group;
+
+ return GTK_MENU_ITEM(item);
+}
+
+static GtkMenuItem *accounts_menu_get_item(struct gtk_mod *mod,
+ struct ua *ua)
+{
+ GtkMenuItem *item;
+ GtkMenuShell *accounts_menu = GTK_MENU_SHELL(mod->accounts_menu);
+ GList *items = accounts_menu->children;
+
+ for (; items; items = items->next) {
+ item = items->data;
+ if (ua == g_object_get_data(G_OBJECT(item), "ua"))
+ return item;
+ }
+
+ /* Add new account not yet in menu */
+ return accounts_menu_add_item(mod, ua);
+}
+
+
+static void update_current_accounts_menu_item(struct gtk_mod *mod)
+{
+ GtkMenuItem *item = accounts_menu_get_item(mod,
+ uag_current());
+ gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(item), TRUE);
+}
+
+
+static void update_ua_presence(struct gtk_mod *mod)
+{
+ GtkCheckMenuItem *item;
+ enum presence_status cur_status;
+ void *status;
+ GtkMenuShell *status_menu = GTK_MENU_SHELL(mod->status_menu);
+ GList *items = status_menu->children;
+
+ cur_status = ua_presence_status(uag_current());
+
+ for (; items; items = items->next) {
+ item = items->data;
+ status = g_object_get_data(G_OBJECT(item), "presence");
+ if (cur_status == GPOINTER_TO_UINT(status))
+ break;
+ }
+ if (!item)
+ return;
+
+ gtk_check_menu_item_set_active(item, TRUE);
+}
+
+
+static const char *ua_event_reg_str(enum ua_event ev)
+{
+ switch (ev) {
+
+ case UA_EVENT_REGISTERING: return "registering";
+ case UA_EVENT_REGISTER_OK: return "OK";
+ case UA_EVENT_REGISTER_FAIL: return "ERR";
+ case UA_EVENT_UNREGISTERING: return "unregistering";
+ default: return "?";
+ }
+}
+
+
+static void accounts_menu_set_status(struct gtk_mod *mod,
+ struct ua *ua, enum ua_event ev)
+{
+ GtkMenuItem *item = accounts_menu_get_item(mod, ua);
+ char buf[256];
+ re_snprintf(buf, sizeof buf, "%s (%s)", ua_aor(ua),
+ ua_event_reg_str(ev));
+ gtk_menu_item_set_label(item, buf);
+}
+
+
+static void notify_incoming_call(struct gtk_mod *mod,
+ struct call *call)
+{
+ GNotification *notification;
+ GVariant *target;
+ char id[64];
+
+ re_snprintf(id, sizeof id, "incoming-call-%p", call);
+ id[sizeof id - 1] = '\0';
+
+ notification = g_notification_new("Incoming call");
+ g_notification_set_priority(notification,
+ G_NOTIFICATION_PRIORITY_URGENT);
+ target = g_variant_new_int64(GPOINTER_TO_INT(call));
+ g_notification_set_body(notification, call_peeruri(call));
+ g_notification_add_button_with_target_value(notification,
+ "Answer", "app.answer", target);
+ g_notification_add_button_with_target_value(notification,
+ "Reject", "app.reject", target);
+ g_application_send_notification(mod->app, id, notification);
+ g_object_unref(notification);
+}
+
+
+static void denotify_incoming_call(struct gtk_mod *mod,
+ struct call *call)
+{
+ char id[64];
+
+ re_snprintf(id, sizeof id, "incoming-call-%p", call);
+ id[sizeof id - 1] = '\0';
+ g_application_withdraw_notification(mod->app, id);
+}
+
+
+static void answer_activated(GSimpleAction *action, GVariant *parameter,
+ gpointer arg)
+{
+ struct gtk_mod *mod = arg;
+ struct call *call = get_call_from_gvariant(parameter);
+ (void)action;
+
+ if (call) {
+ denotify_incoming_call(mod, call);
+ mqueue_push(mod->mq, MQ_ANSWER, call);
+ }
+}
+
+static void reject_activated(GSimpleAction *action, GVariant *parameter,
+ gpointer arg)
+{
+ struct gtk_mod *mod = arg;
+ struct call *call = get_call_from_gvariant(parameter);
+ (void)action;
+
+ if (call) {
+ denotify_incoming_call(mod, call);
+ mqueue_push(mod->mq, MQ_HANGUP, call);
+ }
+}
+
+static struct call_window *new_call_window(struct gtk_mod *mod,
+ struct call *call)
+{
+ struct call_window *win = call_window_new(call, mod);
+ if (call) {
+ mod->call_windows = g_slist_append(mod->call_windows, win);
+ }
+ return win;
+}
+
+
+static struct call_window *get_call_window(struct gtk_mod *mod,
+ struct call *call)
+{
+ GSList *wins;
+
+ for (wins = mod->call_windows; wins; wins = wins->next) {
+ struct call_window *win = wins->data;
+ if (call_window_is_for_call(win, call))
+ return win;
+ }
+ return NULL;
+}
+
+
+static struct call_window *get_create_call_window(struct gtk_mod *mod,
+ struct call *call)
+{
+ struct call_window *win = get_call_window(mod, call);
+ if (!win)
+ win = new_call_window(mod, call);
+ return win;
+}
+
+
+void gtk_mod_call_window_closed(struct gtk_mod *mod, struct call_window *win)
+{
+ mod->call_windows = g_slist_remove(mod->call_windows, win);
+}
+
+static void ua_event_handler(struct ua *ua,
+ enum ua_event ev,
+ struct call *call,
+ const char *prm,
+ void *arg )
+{
+ struct gtk_mod *mod = arg;
+ struct call_window *win;
+
+ gdk_threads_enter();
+
+ switch (ev) {
+
+ case UA_EVENT_REGISTERING:
+ case UA_EVENT_UNREGISTERING:
+ case UA_EVENT_REGISTER_OK:
+ case UA_EVENT_REGISTER_FAIL:
+ accounts_menu_set_status(mod, ua, ev);
+ break;
+
+ case UA_EVENT_CALL_INCOMING:
+ notify_incoming_call(mod, call);
+ break;
+
+ case UA_EVENT_CALL_CLOSED:
+ win = get_call_window(mod, call);
+ if (win)
+ call_window_closed(win, prm);
+ else
+ denotify_incoming_call(mod, call);
+ break;
+
+ case UA_EVENT_CALL_RINGING:
+ win = get_create_call_window(mod, call);
+ if (win)
+ call_window_ringing(win);
+ break;
+
+ case UA_EVENT_CALL_PROGRESS:
+ win = get_create_call_window(mod, call);
+ if (win)
+ call_window_progress(win);
+ break;
+
+ case UA_EVENT_CALL_ESTABLISHED:
+ win = get_create_call_window(mod, call);
+ if (win)
+ call_window_established(win);
+ break;
+
+ case UA_EVENT_CALL_TRANSFER_FAILED:
+ win = get_create_call_window(mod, call);
+ if (win)
+ call_window_transfer_failed(win, prm);
+ break;
+
+ default:
+ break;
+ }
+
+ gdk_threads_leave();
+}
+
+static void message_handler(const struct pl *peer, const struct pl *ctype,
+ struct mbuf *body, void *arg)
+{
+ struct gtk_mod *mod = arg;
+ GNotification *notification;
+ char title[128];
+ char msg[512];
+ (void)ctype;
+
+ /* Display notification of chat */
+
+ re_snprintf(title, sizeof title, "Chat from %r", peer);
+ title[sizeof title - 1] = '\0';
+
+ re_snprintf(msg, sizeof msg, "%b",
+ mbuf_buf(body), mbuf_get_left(body));
+
+ notification = g_notification_new(title);
+ g_notification_set_body(notification, msg);
+ g_application_send_notification(mod->app, NULL, notification);
+ g_object_unref(notification);
+}
+
+
+static gboolean status_icon_on_button_press(GtkStatusIcon *status_icon,
+ GdkEventButton *event,
+ struct gtk_mod *mod)
+{
+ if (!mod->contacts_inited) {
+ init_contacts_menu(mod);
+ mod->contacts_inited = TRUE;
+ }
+
+ /* If the current UA was changed through another UI, update it here */
+ update_current_accounts_menu_item(mod);
+
+ update_ua_presence(mod);
+
+ gtk_widget_show_all(mod->app_menu);
+
+ gtk_menu_popup(GTK_MENU(mod->app_menu), NULL, NULL,
+ gtk_status_icon_position_menu, status_icon,
+ event->button, event->time);
+
+ return TRUE;
+}
+
+
+void gtk_mod_connect(struct gtk_mod *mod, const char *uri)
+{
+ mqueue_push(mod->mq, MQ_CONNECT, (char *)uri);
+}
+
+static void warning_dialog(const char *title, const char *fmt, ...)
+{
+ va_list ap;
+ char msg[512];
+ GtkWidget *dialog;
+
+ va_start(ap, fmt);
+ (void)re_vsnprintf(msg, sizeof msg, fmt, ap);
+ va_end(ap);
+
+ dialog = gtk_message_dialog_new(NULL, 0, GTK_MESSAGE_ERROR,
+ GTK_BUTTONS_CLOSE, "%s", title);
+ gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(dialog),
+ "%s", msg);
+ g_signal_connect_swapped(G_OBJECT(dialog), "response",
+ G_CALLBACK(gtk_widget_destroy), dialog);
+ gtk_window_set_title(GTK_WINDOW(dialog), title);
+ gtk_widget_show(dialog);
+}
+
+
+static void mqueue_handler(int id, void *data, void *arg)
+{
+ struct gtk_mod *mod = arg;
+ const char *uri;
+ struct call *call;
+ int err;
+ struct ua *ua = uag_current();
+ (void)mod;
+
+ switch ((enum gtk_mod_events)id) {
+
+ case MQ_CONNECT:
+ uri = data;
+ err = ua_connect(ua, &call, NULL, uri, NULL, VIDMODE_ON);
+ if (err) {
+ gdk_threads_enter();
+ warning_dialog("Call failed",
+ "Connecting to \"%s\" failed.\n"
+ "Error: %m", uri, err);
+ gdk_threads_leave();
+ break;
+ }
+ gdk_threads_enter();
+ err = new_call_window(mod, call) == NULL;
+ gdk_threads_leave();
+ if (err) {
+ ua_hangup(ua, call, 500, "Server Error");
+ }
+ break;
+
+ case MQ_HANGUP:
+ call = data;
+ ua_hangup(ua, call, 0, NULL);
+ break;
+
+ case MQ_QUIT:
+ ua_stop_all(false);
+ break;
+
+ case MQ_ANSWER:
+ call = data;
+ err = ua_answer(ua, call);
+ if (err) {
+ gdk_threads_enter();
+ warning_dialog("Call failed",
+ "Answering the call "
+ "from \"%s\" failed.\n"
+ "Error: %m",
+ call_peername(call), err);
+ gdk_threads_leave();
+ break;
+ }
+
+ gdk_threads_enter();
+ err = new_call_window(mod, call) == NULL;
+ gdk_threads_leave();
+ if (err) {
+ ua_hangup(ua, call, 500, "Server Error");
+ }
+ break;
+
+ case MQ_SELECT_UA:
+ ua = data;
+ uag_current_set(ua);
+ break;
+ }
+}
+
+static void *gtk_thread(void *arg)
+{
+ struct gtk_mod *mod = arg;
+ GtkMenuShell *app_menu;
+ GtkWidget *item;
+ GError *error = NULL;
+
+ gdk_threads_init();
+ gtk_init(0, NULL);
+
+ g_set_application_name("baresip");
+ mod->app = g_application_new ("com.creytiv.baresip",
+ G_APPLICATION_FLAGS_NONE);
+
+ g_application_register (G_APPLICATION (mod->app), NULL, &error);
+ if (error != NULL) {
+ warning ("Unable to register GApplication: %s",
+ error->message);
+ g_error_free (error);
+ error = NULL;
+ }
+
+ mod->status_icon = gtk_status_icon_new_from_icon_name("call-start");
+ gtk_status_icon_set_tooltip_text (mod->status_icon, "baresip");
+
+ g_signal_connect(G_OBJECT(mod->status_icon),
+ "button_press_event",
+ G_CALLBACK(status_icon_on_button_press), mod);
+ gtk_status_icon_set_visible(mod->status_icon, TRUE);
+
+ mod->contacts_inited = false;
+ mod->dial_dialog = NULL;
+ mod->call_windows = NULL;
+
+ /* App menu */
+ mod->app_menu = gtk_menu_new();
+ app_menu = GTK_MENU_SHELL(mod->app_menu);
+
+ /* Account submenu */
+ mod->accounts_menu = gtk_menu_new();
+ mod->accounts_menu_group = NULL;
+ item = gtk_menu_item_new_with_mnemonic("_Account");
+ gtk_menu_shell_append(app_menu, item);
+ gtk_menu_item_set_submenu(GTK_MENU_ITEM(item),
+ mod->accounts_menu);
+
+ /* Add accounts to submenu */
+ for (struct le *le = list_head(uag_list()); le; le = le->next) {
+ struct ua *ua = le->data;
+ accounts_menu_add_item(mod, ua);
+ }
+
+ /* Status submenu */
+ mod->status_menu = gtk_menu_new();
+ item = gtk_menu_item_new_with_mnemonic("_Status");
+ gtk_menu_shell_append(GTK_MENU_SHELL(app_menu), item);
+ gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), mod->status_menu);
+
+ /* Open */
+ item = gtk_radio_menu_item_new_with_label(NULL, "Open");
+ g_object_set_data(G_OBJECT(item), "presence",
+ GINT_TO_POINTER(PRESENCE_OPEN));
+ g_signal_connect(item, "activate",
+ G_CALLBACK(menu_on_presence_set), mod);
+ gtk_menu_shell_append(GTK_MENU_SHELL(mod->status_menu), item);
+ gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(item), TRUE);
+
+ /* Closed */
+ item = gtk_radio_menu_item_new_with_label_from_widget(
+ GTK_RADIO_MENU_ITEM(item), "Closed");
+ g_object_set_data(G_OBJECT(item), "presence",
+ GINT_TO_POINTER(PRESENCE_CLOSED));
+ g_signal_connect(item, "activate",
+ G_CALLBACK(menu_on_presence_set), mod);
+ gtk_menu_shell_append(GTK_MENU_SHELL(mod->status_menu), item);
+
+ gtk_menu_shell_append(app_menu, gtk_separator_menu_item_new());
+
+ /* Dial */
+ item = gtk_menu_item_new_with_mnemonic("_Dial...");
+ gtk_menu_shell_append(app_menu, item);
+ g_signal_connect(G_OBJECT(item), "activate",
+ G_CALLBACK(menu_on_dial), mod);
+
+ /* Dial contact */
+ mod->contacts_menu = gtk_menu_new();
+ item = gtk_menu_item_new_with_mnemonic("Dial _contact");
+ gtk_menu_shell_append(app_menu, item);
+ gtk_menu_item_set_submenu(GTK_MENU_ITEM(item),
+ mod->contacts_menu);
+
+ gtk_menu_shell_append(app_menu, gtk_separator_menu_item_new());
+
+ /* About */
+ item = gtk_menu_item_new_with_mnemonic("A_bout");
+ g_signal_connect(G_OBJECT(item), "activate",
+ G_CALLBACK(menu_on_about), mod);
+ gtk_menu_shell_append(app_menu, item);
+
+ gtk_menu_shell_append(app_menu, gtk_separator_menu_item_new());
+
+ /* Quit */
+ item = gtk_menu_item_new_with_mnemonic("_Quit");
+ g_signal_connect(G_OBJECT(item), "activate",
+ G_CALLBACK(menu_on_quit), mod);
+ gtk_menu_shell_append(app_menu, item);
+
+ g_action_map_add_action_entries(G_ACTION_MAP(mod->app),
+ app_entries, G_N_ELEMENTS(app_entries), mod);
+
+ info("gtk_menu starting\n");
+
+ uag_event_register( ua_event_handler, mod );
+ mod->run = true;
+ gtk_main();
+ mod->run = false;
+ uag_event_unregister(ua_event_handler);
+
+ if (mod->dial_dialog) {
+ mem_deref(mod->dial_dialog);
+ mod->dial_dialog = NULL;
+ }
+
+ return NULL;
+}
+
+
+static void vu_enc_destructor(void *arg)
+{
+ struct vumeter_enc *st = arg;
+
+ list_unlink(&st->af.le);
+}
+
+
+static void vu_dec_destructor(void *arg)
+{
+ struct vumeter_dec *st = arg;
+
+ list_unlink(&st->af.le);
+}
+
+
+static int16_t calc_avg_s16(const int16_t *sampv, size_t sampc)
+{
+ int32_t v = 0;
+ size_t i;
+
+ if (!sampv || !sampc)
+ return 0;
+
+ for (i=0; i<sampc; i++)
+ v += abs(sampv[i]);
+
+ return v/sampc;
+}
+
+
+static int vu_encode_update(struct aufilt_enc_st **stp, void **ctx,
+ const struct aufilt *af, struct aufilt_prm *prm)
+{
+ struct vumeter_enc *st;
+ (void)ctx;
+ (void)prm;
+
+ if (!stp || !af)
+ return EINVAL;
+
+ if (*stp)
+ return 0;
+
+ st = mem_zalloc(sizeof(*st), vu_enc_destructor);
+ if (!st)
+ return ENOMEM;
+
+ gdk_threads_enter();
+ call_window_got_vu_enc(st);
+ gdk_threads_leave();
+
+ *stp = (struct aufilt_enc_st *)st;
+
+ return 0;
+}
+
+
+static int vu_decode_update(struct aufilt_dec_st **stp, void **ctx,
+ const struct aufilt *af, struct aufilt_prm *prm)
+{
+ struct vumeter_dec *st;
+ (void)ctx;
+ (void)prm;
+
+ if (!stp || !af)
+ return EINVAL;
+
+ if (*stp)
+ return 0;
+
+ st = mem_zalloc(sizeof(*st), vu_dec_destructor);
+ if (!st)
+ return ENOMEM;
+
+ gdk_threads_enter();
+ call_window_got_vu_dec(st);
+ gdk_threads_leave();
+
+ *stp = (struct aufilt_dec_st *)st;
+
+ return 0;
+}
+
+
+static int vu_encode(struct aufilt_enc_st *st, int16_t *sampv, size_t *sampc)
+{
+ struct vumeter_enc *vu = (struct vumeter_enc *)st;
+
+ vu->avg_rec = calc_avg_s16(sampv, *sampc);
+ vu->started = true;
+
+ return 0;
+}
+
+
+static int vu_decode(struct aufilt_dec_st *st, int16_t *sampv, size_t *sampc)
+{
+ struct vumeter_dec *vu = (struct vumeter_dec *)st;
+
+ vu->avg_play = calc_avg_s16(sampv, *sampc);
+ vu->started = true;
+
+ return 0;
+}
+
+
+static struct aufilt vumeter = {
+ LE_INIT, "gtk_vumeter",
+ vu_encode_update, vu_encode,
+ vu_decode_update, vu_decode
+};
+
+
+static int module_init(void)
+{
+ int err = 0;
+
+ err = mqueue_alloc(&mod_obj.mq, mqueue_handler, &mod_obj);
+ if (err)
+ return err;
+ err = pthread_create(&mod_obj.thread, NULL, gtk_thread,
+ &mod_obj);
+ if (err)
+ return err;
+
+ aufilt_register(&vumeter);
+ err = message_init(message_handler, &mod_obj);
+
+ return err;
+}
+
+static int module_close(void)
+{
+ if (mod_obj.run) {
+ gdk_threads_enter();
+ gtk_main_quit();
+ gdk_threads_leave();
+ }
+ pthread_join(mod_obj.thread, NULL);
+ mem_deref(mod_obj.mq);
+ aufilt_unregister(&vumeter);
+ message_close();
+
+ return 0;
+}
+
+
+EXPORT_SYM const struct mod_export DECL_EXPORTS(gtk) = {
+ "gtk",
+ "application",
+ module_init,
+ module_close,
+};
diff --git a/modules/gtk/gtk_mod.h b/modules/gtk/gtk_mod.h
new file mode 100644
index 0000000..ab123ca
--- /dev/null
+++ b/modules/gtk/gtk_mod.h
@@ -0,0 +1,51 @@
+/**
+ * @file gtk/gtk_mod.h GTK+ UI module -- internal API
+ *
+ * Copyright (C) 2015 Charles E. Lehner
+ */
+
+struct gtk_mod;
+struct call_window;
+struct dial_dialog;
+struct transfer_dialog;
+
+struct vumeter_enc {
+ struct aufilt_enc_st af; /* inheritance */
+ int16_t avg_rec;
+ volatile bool started;
+};
+
+struct vumeter_dec {
+ struct aufilt_dec_st af; /* inheritance */
+ int16_t avg_play;
+ volatile bool started;
+};
+
+/* Main menu */
+void gtk_mod_connect(struct gtk_mod *, const char *uri);
+void gtk_mod_call_window_closed(struct gtk_mod *, struct call_window *);
+
+/* Call Window */
+struct call_window *call_window_new(struct call *call, struct gtk_mod *mod);
+void call_window_got_vu_dec(struct vumeter_dec *);
+void call_window_got_vu_enc(struct vumeter_enc *);
+void call_window_transfer(struct call_window *, const char *uri);
+void call_window_closed(struct call_window *, const char *reason);
+void call_window_ringing(struct call_window *);
+void call_window_progress(struct call_window *);
+void call_window_established(struct call_window *);
+void call_window_transfer_failed(struct call_window *, const char *reason);
+bool call_window_is_for_call(struct call_window *, struct call *);
+
+/* Dial Dialog */
+struct dial_dialog *dial_dialog_alloc(struct gtk_mod *);
+void dial_dialog_show(struct dial_dialog *);
+
+/* Call transfer dialog */
+struct transfer_dialog *transfer_dialog_alloc(struct call_window *);
+void transfer_dialog_show(struct transfer_dialog *);
+void transfer_dialog_fail(struct transfer_dialog *, const char *reason);
+
+/* URI entry combo box */
+GtkWidget *uri_combo_box_new(void);
+const char *uri_combo_box_get_text(GtkComboBox *box);
diff --git a/modules/gtk/module.mk b/modules/gtk/module.mk
new file mode 100644
index 0000000..2273d42
--- /dev/null
+++ b/modules/gtk/module.mk
@@ -0,0 +1,14 @@
+#
+# module.mk - GTK+ Menu-based UI
+#
+# Copyright (C) 2010 Creytiv.com
+# Copyright (C) 2015 Charles E. Lehner
+#
+
+MOD := gtk
+$(MOD)_SRCS += gtk_mod.c call_window.c dial_dialog.c transfer_dialog.c \
+ uri_entry.c
+$(MOD)_LFLAGS += `pkg-config --libs gtk+-2.0 `
+CFLAGS += `pkg-config --cflags gtk+-2.0 `
+
+include mk/mod.mk
diff --git a/modules/gtk/transfer_dialog.c b/modules/gtk/transfer_dialog.c
new file mode 100644
index 0000000..165c1c2
--- /dev/null
+++ b/modules/gtk/transfer_dialog.c
@@ -0,0 +1,131 @@
+/**
+ * @file transfer_dialog.c GTK+ call transfer dialog
+ *
+ * Copyright (C) 2015 Charles E. Lehner
+ */
+#include <re.h>
+#include <baresip.h>
+#include <gtk/gtk.h>
+#include "gtk_mod.h"
+
+struct transfer_dialog {
+ struct call_window *call_win;
+ GtkWidget *dialog;
+ GtkComboBox *uri_combobox;
+ GtkLabel *status_label;
+ GtkWidget *spinner;
+};
+
+static const char *status_progress = "progress";
+
+
+static void set_status(struct transfer_dialog *td, const char *status)
+{
+ if (status == status_progress) {
+ gtk_widget_show(td->spinner);
+ gtk_spinner_start(GTK_SPINNER(td->spinner));
+ gtk_label_set_text(td->status_label, NULL);
+ } else {
+ gtk_widget_hide(td->spinner);
+ gtk_spinner_stop(GTK_SPINNER(td->spinner));
+ gtk_label_set_text(td->status_label, status);
+ }
+}
+
+
+static void on_dialog_response(GtkDialog *dialog, gint response_id,
+ struct transfer_dialog *win)
+{
+ char *uri;
+
+ if (response_id == GTK_RESPONSE_ACCEPT) {
+ uri = (char *)uri_combo_box_get_text(win->uri_combobox);
+ set_status(win, status_progress);
+ call_window_transfer(win->call_win, uri);
+ } else {
+ set_status(win, NULL);
+ gtk_widget_hide(GTK_WIDGET(dialog));
+ }
+}
+
+
+static void destructor(void *arg)
+{
+ struct transfer_dialog *td = arg;
+
+ gtk_widget_destroy(td->dialog);
+}
+
+
+struct transfer_dialog *transfer_dialog_alloc(struct call_window *call_win)
+{
+ struct transfer_dialog *win;
+ GtkWidget *dialog, *content, *button, *image, *hbox, *spinner, *label;
+ GtkWidget *uri_combobox;
+
+ win = mem_zalloc(sizeof(*win), destructor);
+ if (!win)
+ return NULL;
+
+ dialog = gtk_dialog_new_with_buttons("Transfer", NULL, 0,
+ GTK_STOCK_CANCEL, GTK_RESPONSE_REJECT, NULL);
+
+ /* Transfer button */
+ button = gtk_button_new_with_label("Transfer");
+ image = gtk_image_new_from_icon_name("forward",
+ GTK_ICON_SIZE_BUTTON);
+ gtk_button_set_image(GTK_BUTTON(button), image);
+ gtk_dialog_add_action_widget(GTK_DIALOG(dialog), button,
+ GTK_RESPONSE_ACCEPT);
+ gtk_widget_set_can_default(button, TRUE);
+
+ gtk_dialog_set_default_response(GTK_DIALOG(dialog),
+ GTK_RESPONSE_ACCEPT);
+ /* Label */
+ content = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
+ label = gtk_label_new("Transfer call to:");
+ gtk_box_pack_start(GTK_BOX(content), label, FALSE, FALSE, 0);
+
+ /* URI entry */
+ uri_combobox = uri_combo_box_new();
+ gtk_box_pack_start(GTK_BOX(content), uri_combobox, FALSE, FALSE, 5);
+
+ g_signal_connect(dialog, "response", G_CALLBACK(on_dialog_response), win);
+ g_signal_connect(dialog, "delete-event",
+ G_CALLBACK(gtk_widget_hide_on_delete), win);
+
+ /* Spinner and status */
+ hbox = gtk_hbox_new(FALSE, 0);
+ gtk_box_pack_start(GTK_BOX(content), hbox, FALSE, FALSE, 0);
+
+ spinner = gtk_spinner_new();
+ gtk_box_pack_start(GTK_BOX(hbox), spinner, TRUE, TRUE, 0);
+
+ label = gtk_label_new(NULL);
+ gtk_box_pack_start(GTK_BOX(content), label, FALSE, FALSE, 0);
+ win->status_label = GTK_LABEL(label);
+
+ win->dialog = dialog;
+ win->uri_combobox = GTK_COMBO_BOX(uri_combobox);
+ win->call_win = call_win;
+ win->spinner = spinner;
+
+ gtk_widget_show_all(dialog);
+ gtk_widget_hide(spinner);
+
+ return win;
+}
+
+void transfer_dialog_show(struct transfer_dialog *td)
+{
+ gtk_window_present(GTK_WINDOW(td->dialog));
+ gtk_widget_grab_focus(gtk_bin_get_child(GTK_BIN(td->uri_combobox)));
+ set_status(td, NULL);
+}
+
+void transfer_dialog_fail(struct transfer_dialog *td, const char *reason)
+{
+ char buf[256];
+ re_snprintf(buf, sizeof buf, "Transfer failed: %s", reason);
+ set_status(td, buf);
+}
diff --git a/modules/gtk/uri_entry.c b/modules/gtk/uri_entry.c
new file mode 100644
index 0000000..601bf83
--- /dev/null
+++ b/modules/gtk/uri_entry.c
@@ -0,0 +1,44 @@
+/**
+ * @file uri_entry.c GTK+ URI entry combo box
+ *
+ * Copyright (C) 2015 Charles E. Lehner
+ */
+
+#include <re.h>
+#include <baresip.h>
+#include <gtk/gtk.h>
+#include "gtk_mod.h"
+
+/**
+ * Create a URI combox box.
+ *
+ * The combo box has a menu of contacts, and a text entry for a URI.
+ *
+ * @return the combo box
+ */
+GtkWidget *uri_combo_box_new(void)
+{
+ struct le *le;
+ GtkEntry *uri_entry;
+ GtkWidget *uri_combobox;
+
+ uri_combobox = gtk_combo_box_text_new_with_entry();
+ uri_entry = GTK_ENTRY(gtk_bin_get_child(GTK_BIN(uri_combobox)));
+ gtk_entry_set_activates_default(uri_entry, TRUE);
+
+ for (le = list_head(contact_list()); le; le = le->next) {
+ struct contact *c = le->data;
+ gtk_combo_box_text_append_text(
+ GTK_COMBO_BOX_TEXT(uri_combobox),
+ contact_str(c));
+ }
+
+ return uri_combobox;
+}
+
+const char *uri_combo_box_get_text(GtkComboBox *box)
+{
+ GtkEntry *entry = GTK_ENTRY(gtk_bin_get_child(GTK_BIN(box)));
+ GtkEntryBuffer *buf = gtk_entry_get_buffer(entry);
+ return gtk_entry_buffer_get_text(buf);
+}