/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
/*
* Author :
* Peter Williams (peterw@helixcode.com)
*
* Copyright 2000, Helix Code, Inc. (http://www.helixcode.com)
*
* 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 Street #330, Boston, MA 02111-1307, USA.
*
*/
#include <config.h>
#include <string.h>
#include <errno.h>
#include <glib.h>
#include "folder-browser-factory.h"
#include "camel/camel-object.h"
#include "mail.h"
#include "mail-threads.h"
#define DEBUG(p) g_print p
/**
* A function and its userdata
**/
typedef struct closure_s
{
gpointer in_data;
gboolean free_in_data;
gpointer op_data;
const mail_operation_spec *spec;
CamelException *ex;
gchar *infinitive;
gchar *gerund;
}
closure_t;
/**
* A command issued through the compipe
**/
typedef struct com_msg_s
{
enum com_msg_type_e {
STARTING,
#if 0
PERCENTAGE,
HIDE_PBAR,
SHOW_PBAR,
#endif
MESSAGE,
PASSWORD,
ERROR,
FORWARD_EVENT,
FINISHED
} type;
gfloat percentage;
gchar *message;
closure_t *clur;
/* Password stuff */
gchar **reply;
gboolean secret;
gboolean *success;
/* Event stuff */
CamelObjectEventHookFunc event_hook;
CamelObject *event_obj;
gpointer event_event_data;
gpointer event_user_data;
} com_msg_t;
/**
* Stuff needed for blocking
**/
typedef struct block_info_s {
GMutex *mutex;
GCond *cond;
gboolean val;
} block_info_t;
#define BLOCK_INFO_INIT { NULL, NULL, FALSE }
/**
* @dispatch_thread_started: gboolean that tells us whether
* the dispatch thread has been launched.
**/
static gboolean dispatch_thread_started = FALSE;
/**
* @queue_len : the number of operations pending
* and being executed.
*
* Because camel is not thread-safe we work
* with the restriction that more than one mailbox
* cannot be accessed at once. Thus we cannot
* concurrently check mail and move messages, etc.
**/
static gint queue_len = 0;
/**
* @main_compipe: The pipe through which the dispatcher communicates
* with the main thread for GTK+ calls
*
* @chan_reader: the GIOChannel that reads our pipe
*
* @MAIN_READER: the fd in our main pipe that.... reads!
* @MAIN_WRITER: the fd in our main pipe that.... writes!
*/
#define MAIN_READER main_compipe[0]
#define MAIN_WRITER main_compipe[1]
#define DISPATCH_READER dispatch_compipe[0]
#define DISPATCH_WRITER dispatch_compipe[1]
static int main_compipe[2] = { -1, -1 };
static int dispatch_compipe[2] = { -1, -1 };
GIOChannel *chan_reader = NULL;
/**
* @modal_block: a condition maintained so that the
* calling thread (the dispatch thread) blocks correctly
* until the user has responded to some kind of modal
* dialog boxy thing.
*/
static block_info_t modal_block = BLOCK_INFO_INIT;
/**
* @finish_block: A condition so that the dispatch thread
* blocks until the main thread has finished the cleanup.
**/
static block_info_t finish_block = BLOCK_INFO_INIT;
/**
* @current_message: The current message for the status bar.
* @busy_status: Whether we are currently busy doing some async operation,
* for status bar purposes.
*/
static char *current_message = NULL;
static gboolean busy = FALSE;
/**
* Static prototypes
**/
static void ui_set_busy (void);
static void ui_unset_busy (void);
static void ui_set_message (const char *message);
static void ui_unset_message (void);
static void block_prepare (block_info_t *info);
static void block_wait (block_info_t *info);
static void block_hold (block_info_t *info);
static void block_release (block_info_t *info);
static void *dispatch (void * data);
static void check_dispatcher (void);
static void check_compipes (void);
static gboolean read_msg (GIOChannel * source, GIOCondition condition,
gpointer userdata);
static void show_error (com_msg_t * msg);
static void get_password (com_msg_t * msg);
static void get_password_cb (gchar * string, gpointer data);
static void cleanup_op (com_msg_t * msg);
static closure_t *new_closure (const mail_operation_spec * spec, gpointer input,
gboolean free_in_data);
static void free_closure (closure_t *clur);
/* Pthread code */
/* FIXME: support other thread types!!!! */
#ifdef G_THREADS_IMPL_POSIX
#include <pthread.h>
/**
* @dispatch_thread: the pthread_t (when using pthreads, of
* course) representing our dispatcher routine. Never used
* except to make pthread_create happy
**/
static pthread_t dispatch_thread;
/* FIXME: do we need to set any attributes for our thread?
* If so, we need to create a pthread_attr structure and
* fill it in somewhere. But the defaults should be good
* enough.
*/
#elif defined( G_THREADS_IMPL_SOLARIS )
#include <thread.h>
static thread_t dispatch_thread;
#else /* no supported thread impl */
void
f (void)
{
Error_No_supported_thread_implementation_recognized ();
choke on this;
}
#endif
static int
pipe_write (int fd, const void *buf, size_t count)
{
size_t res;
do {
res = write (fd, buf, count);
}
while (res == -1 && errno == EINTR);
return res;
}
static size_t
pipe_read (int fd, void *buf, size_t count)
{
size_t res;
do {
res = read (fd, buf, count);
} while (res == -1 && errno == EINTR);
return res;
}
/**
* mail_operation_queue:
* @spec: describes the operation to be performed
* @input: input data for the operation.
*
* Runs a mail operation asynchronously. If no other operation is running,
* we start another thread and call the callback in that thread. The function
* can then use the mail_op_ functions to perform limited UI returns, while
* the main UI is completely unlocked.
*
* If an async operation is going on when this function is called again,
* it waits for the currently executing operation to finish, then
* executes the callback function in another thread.
*
* Returns TRUE on success, FALSE on some sort of queueing error.
**/
gboolean
mail_operation_queue (const mail_operation_spec * spec, gpointer input,
gboolean free_in_data)
{
closure_t *clur;
g_assert (spec);
clur = new_closure (spec, input, free_in_data);
if (spec->setup)
(spec->setup) (clur->in_data, clur->op_data, clur->ex);
if (camel_exception_is_set (clur->ex)) {
if (clur->ex->id != CAMEL_EXCEPTION_USER_CANCEL) {
GtkWidget *err_dialog;
gchar *msg;
msg =
g_strdup_printf
(_("Error while preparing to %s:\n" "%s"),
clur->infinitive,
camel_exception_get_description (clur->ex));
err_dialog = gnome_error_dialog (msg);
g_free (msg);
gnome_dialog_set_close (GNOME_DIALOG (err_dialog),
TRUE);
gnome_dialog_run_and_close (GNOME_DIALOG (err_dialog));
g_warning ("Setup failed for `%s': %s",
clur->infinitive,
camel_exception_get_description (clur->
ex));
}
free_closure (clur);
return FALSE;
}
if (queue_len == 0) {
check_compipes ();
check_dispatcher ();
} /* else add self to queue */
pipe_write (DISPATCH_WRITER, clur, sizeof (closure_t));
/* dispatch allocates a separate buffer
* to hold the closure; it's in the pipe and
* can safely be freed
*/
g_free (clur);
queue_len++;
return TRUE;
}
#if 0
/**
* mail_op_set_percentage:
* @percentage: the percentage that will be displayed in the progress bar
*
* Set the percentage of the progress bar for the currently executing operation.
* Threadsafe for, nay, intended to be called by, the dispatching thread.
**/
void
mail_op_set_percentage (gfloat percentage)
{
com_msg_t msg;
msg.type = PERCENTAGE;
msg.percentage = percentage;
pipe_write (MAIN_WRITER, &msg, sizeof (msg));
}
/**
* mail_op_hide_progressbar:
*
* Hide the progress bar in the status box
* Threadsafe for, nay, intended to be called by, the dispatching thread.
**/
void
mail_op_hide_progressbar (void)
{
com_msg_t msg;
msg.type = HIDE_PBAR;
pipe_write (MAIN_WRITER, &msg, sizeof (msg));
}
/**
* mail_op_show_progressbar:
*
* Show the progress bar in the status box
* Threadsafe for, nay, intended to be called by, the dispatching thread.
**/
void
mail_op_show_progressbar (void)
{
com_msg_t msg;
msg.type = SHOW_PBAR;
pipe_write (MAIN_WRITER, &msg, sizeof (msg));
}
#endif
/**
* mail_op_set_message:
* @fmt: printf-style format string for the message
* @...: arguments to the format string
*
* Set the message displayed above the progress bar for the currently
* executing operation.
* Threadsafe for, nay, intended to be called by, the dispatching thread.
**/
void
mail_op_set_message (gchar * fmt, ...)
{
com_msg_t msg;
va_list val;
va_start (val, fmt);
msg.type = MESSAGE;
msg.message = g_strdup_vprintf (fmt, val);
va_end (val);
pipe_write (MAIN_WRITER, &msg, sizeof (msg));
}
/**
* mail_op_get_password:
* @prompt: the question put to the user
* @secret: whether the dialog box shold print stars when the user types
* @dest: where to store the reply
*
* Asks the user for a password (or string entry in general). Waits for
* the user's response. On success, returns TRUE and @dest contains the
* response. On failure, returns FALSE and @dest contains the error
* message.
**/
gboolean
mail_op_get_password (gchar * prompt, gboolean secret, gchar ** dest)
{
com_msg_t msg;
gboolean result;
msg.type = PASSWORD;
msg.secret = secret;
msg.message = prompt;
msg.reply = dest;
msg.success = &result;
(*dest) = NULL;
block_prepare (&modal_block);
pipe_write (MAIN_WRITER, &msg, sizeof (msg));
block_wait (&modal_block);
return result;
}
/**
* mail_op_error:
* @fmt: printf-style format string for the error
* @...: arguments to the format string
*
* Opens an error dialog for the currently executing operation.
* Threadsafe for, nay, intended to be called by, the dispatching thread.
**/
void
mail_op_error (gchar * fmt, ...)
{
com_msg_t msg;
va_list val;
va_start (val, fmt);
msg.type = ERROR;
msg.message = g_strdup_vprintf (fmt, val);
va_end (val);
block_prepare (&modal_block);
pipe_write (MAIN_WRITER, &msg, sizeof (msg));
block_wait (&modal_block);
}
/**
* mail_op_forward_event:
*
* Communicate a camel event over to the main thread.
**/
void
mail_op_forward_event (CamelObjectEventHookFunc func, CamelObject *o,
gpointer event_data, gpointer user_data)
{
com_msg_t msg;
msg.type = FORWARD_EVENT;
msg.event_hook = func;
msg.event_obj = o;
msg.event_event_data = event_data;
msg.event_user_data = user_data;
pipe_write (MAIN_WRITER, &msg, sizeof (msg));
}
/**
* mail_operation_wait_for_finish:
*
* Waits for the currently executing async operations
* to finish executing
*/
void
mail_operation_wait_for_finish (void)
{
while (queue_len)
gtk_main_iteration ();
/* Sigh. Otherwise we deadlock upon exit. */
GDK_THREADS_LEAVE ();
}
/**
* mail_operations_are_executing:
*
* Returns TRUE if operations are being executed asynchronously
* when called, FALSE if not.
**/
gboolean
mail_operations_are_executing (void)
{
return (queue_len > 0);
}
/**
* mail_operations_terminate:
*
* Let the operations finish then terminate the dispatch thread
**/
void
mail_operations_terminate (void)
{
closure_t clur;
mail_operation_wait_for_finish();
memset (&clur, 0, sizeof (closure_t));
clur.spec = NULL;
pipe_write (DISPATCH_WRITER, &clur, sizeof (closure_t));
close (DISPATCH_WRITER);
close (MAIN_READER);
}
void
mail_operations_get_status (int *busy_return,
const char **message_return)
{
*busy_return = busy;
*message_return = current_message;
}
/* ** Static functions **************************************************** */
static void check_dispatcher (void)
{
int res;
if (dispatch_thread_started)
return;
#if defined( G_THREADS_IMPL_POSIX )
res = pthread_create (&dispatch_thread, NULL,
(void *) &dispatch, NULL);
#elif defined( G_THREADS_IMPL_SOLARIS )
res = thr_create (NULL, 0, (void *) &dispatch, NULL, 0, &dispatch_thread);
#else /* no known impl */
Error_No_thread_create_implementation ();
choke on this;
#endif
if (res != 0) {
g_warning ("Error launching dispatch thread!");
/* FIXME: more error handling */
} else
dispatch_thread_started = TRUE;
}
/**
* check_compipes:
*
* Check and see if our pipe has been opened and open
* it if necessary.
**/
static void
check_compipes (void)
{
if (MAIN_READER < 0) {
if (pipe (main_compipe) < 0) {
g_warning ("Call to pipe(2) failed!");
/* FIXME: better error handling. How do we react? */
return;
}
chan_reader = g_io_channel_unix_new (MAIN_READER);
g_io_add_watch (chan_reader, G_IO_IN, read_msg, NULL);
}
if (DISPATCH_READER < 0) {
if (pipe (dispatch_compipe) < 0) {
g_warning ("Call to pipe(2) failed!");
/* FIXME: better error handling. How do we react? */
return;
}
}
}
/**
* dispatch:
* @clur: The operation to execute and its parameters
*
* Start a thread that executes the closure and exit
* it when done.
*/
static void *
dispatch (void *unused)
{
size_t len;
closure_t *clur;
com_msg_t msg;
/* Let the compipes be created */
sleep (1);
while (1) {
clur = g_new (closure_t, 1);
len = pipe_read (DISPATCH_READER, clur, sizeof (closure_t));
if (len <= 0)
break;
if (len != sizeof (closure_t)) {
g_warning ("dispatcher: Didn't read full message!");
continue;
}
if (clur->spec == NULL)
break;
msg.type = STARTING;
msg.message = g_strdup (clur->gerund);
pipe_write (MAIN_WRITER, &msg, sizeof (msg));
(clur->spec->callback) (clur->in_data, clur->op_data, clur->ex);
if (camel_exception_is_set (clur->ex)) {
if (clur->ex->id != CAMEL_EXCEPTION_USER_CANCEL) {
g_warning ("Callback failed for `%s': %s",
clur->infinitive,
camel_exception_get_description (clur->
ex));
mail_op_error (_("Error while `%s':\n%s"),
clur->gerund,
camel_exception_get_description (clur->
ex));
}
}
msg.type = FINISHED;
msg.clur = clur;
/* Wait for the cleanup to finish before starting our next op */
block_prepare (&finish_block);
pipe_write (MAIN_WRITER, &msg, sizeof (msg));
block_wait (&finish_block);
}
close (DISPATCH_READER);
close (MAIN_WRITER);
#ifdef G_THREADS_IMPL_POSIX
pthread_exit (0);
#elif defined( G_THREADS_IMPL_SOLARIS )
thr_exit (NULL);
#else /* no known impl */
Error_No_thread_exit_implemented ();
choke on this;
#endif
return NULL;
/*NOTREACHED*/
}
/**
* read_msg:
* @source: the channel that has data to read
* @condition: the reason we were called
* @userdata: unused
*
* A message has been recieved on our pipe; perform the appropriate
* action.
**/
static gboolean
read_msg (GIOChannel * source, GIOCondition condition, gpointer userdata)
{
com_msg_t *msg;
guint size;
msg = g_new0 (com_msg_t, 1);
g_io_channel_read (source, (gchar *) msg,
sizeof (com_msg_t) / sizeof (gchar), &size);
if (size != sizeof (com_msg_t)) {
g_warning (_("Incomplete message written on pipe!"));
msg->type = ERROR;
msg->message =
g_strdup (_
("Error reading commands from dispatching thread."));
}
/* This is very important, though I'm not quite sure why
* it is as we are in the main thread right now.
*/
/*g_message ("DLG: IN: read_msg");*/
switch (msg->type) {
case STARTING:
DEBUG (("*** Message -- STARTING %s\n", msg->message));
ui_set_message (msg->message);
ui_set_busy ();
g_free (msg->message);
break;
#if 0
case PERCENTAGE:
DEBUG (("*** Message -- PERCENTAGE\n"));
g_warning ("PERCENTAGE operation unsupported");
break;
case HIDE_PBAR:
DEBUG (("*** Message -- HIDE_PBAR\n"));
g_warning ("HIDE_PBAR operation unsupported");
break;
case SHOW_PBAR:
DEBUG (("*** Message -- SHOW_PBAR\n"));
g_warning ("HIDE_PBAR operation unsupported");
break;
#endif
case MESSAGE:
DEBUG (("*** Message -- MESSAGE\n"));
ui_set_message (msg->message);
g_free (msg->message);
break;
case PASSWORD:
DEBUG (("*** Message -- PASSWORD\n"));
g_assert (msg->reply);
g_assert (msg->success);
get_password (msg);
break;
case ERROR:
DEBUG (("*** Message -- ERROR\n"));
show_error (msg);
break;
/* Don't fall through; dispatch_func does the FINISHED
* call for us
*/
case FORWARD_EVENT:
DEBUG (("*** Message -- FORWARD_EVENT %p\n", msg->event_hook));
g_assert (msg->event_hook);
(msg->event_hook) (msg->event_obj, msg->event_event_data, msg->event_user_data);
break;
case FINISHED:
DEBUG (("*** Message -- FINISH %s\n", msg->clur->gerund));
cleanup_op (msg);
break;
default:
g_warning (_("Corrupted message from dispatching thread?"));
break;
}
/*g_message ("DLG: OUT: read_msg");*/
g_free (msg);
return TRUE;
}
/**
* cleanup_op:
*
* Cleanup after a finished operation
**/
static void
cleanup_op (com_msg_t * msg)
{
block_hold (&finish_block);
/* Run the cleanup */
if (msg->clur->spec->cleanup)
(msg->clur->spec->cleanup) (msg->clur->in_data,
msg->clur->op_data,
msg->clur->ex);
/* Tell the dispatch thread that it can start
* the next operation */
block_release (&finish_block);
/* Print an exception if the cleanup caused one */
if (camel_exception_is_set (msg->clur->ex) &&
msg->clur->ex->id != CAMEL_EXCEPTION_USER_CANCEL) {
g_warning ("Error on cleanup of `%s': %s",
msg->clur->infinitive,
camel_exception_get_description (msg->clur->ex));
}
free_closure (msg->clur);
queue_len--;
ui_unset_busy ();
ui_unset_message ();
}
/**
* show_error:
*
* Show the error dialog and wait for user OK
**/
static void
show_error (com_msg_t * msg)
{
GtkWidget *err_dialog;
/* Create the dialog */
err_dialog = gnome_error_dialog (msg->message);
g_free (msg->message);
/* Stop the other thread until the user reacts */
ui_unset_busy ();
block_hold (&modal_block);
/* Show the dialog. */
/* Do not GDK_THREADS_ENTER; we're inside the read_msg
* handler which takes care of this for us. Oh, if
* only GDK_THREADS_ENTER were recursive...
*/
gnome_dialog_run_and_close (GNOME_DIALOG (err_dialog));
/* Allow the other thread to proceed */
block_release (&modal_block);
ui_set_busy ();
}
/**
* get_password:
*
* Ask for a password and put the answer in *(msg->reply)
**/
static void
get_password (com_msg_t * msg)
{
GtkWidget *dialog;
int button;
/* Create the dialog */
dialog = gnome_request_dialog (msg->secret, msg->message, NULL,
0, get_password_cb, msg, NULL);
/* Stop the other thread */
ui_unset_busy ();
block_hold (&modal_block);
/* Show the dialog (or report an error) */
if (dialog == NULL) {
*(msg->success) = FALSE;
*(msg->reply) = g_strdup (_("Could not create dialog box."));
button = -1;
} else {
*(msg->reply) = NULL;
button = gnome_dialog_run_and_close (GNOME_DIALOG (dialog));
}
if (button == 1 || *(msg->reply) == NULL) {
*(msg->success) = FALSE;
*(msg->reply) = g_strdup (_("User cancelled query."));
} else if (button >= 0) {
*(msg->success) = TRUE;
}
/* Allow the other thread to proceed */
block_release (&modal_block);
ui_set_busy ();
}
static void
get_password_cb (gchar * string, gpointer data)
{
com_msg_t *msg = (com_msg_t *) data;
if (string)
*(msg->reply) = g_strdup (string);
else
*(msg->reply) = NULL;
}
static closure_t *
new_closure (const mail_operation_spec * spec, gpointer input,
gboolean free_in_data)
{
closure_t *clur;
clur = g_new0 (closure_t, 1);
clur->spec = spec;
clur->in_data = input;
clur->free_in_data = free_in_data;
clur->ex = camel_exception_new ();
clur->op_data = g_malloc (spec->datasize);
camel_exception_init (clur->ex);
clur->infinitive = (spec->describe) (input, FALSE);
clur->gerund = (spec->describe) (input, TRUE);
return clur;
}
static void
free_closure (closure_t *clur)
{
clur->spec = NULL;
if (clur->free_in_data)
g_free (clur->in_data);
clur->in_data = NULL;
g_free (clur->op_data);
clur->op_data = NULL;
camel_exception_free (clur->ex);
clur->ex = NULL;
g_free (clur->infinitive);
g_free (clur->gerund);
g_free (clur);
}
/* ******************** */
/**
*
* Thread A calls block_prepare
* Thread A causes thread B to do something
* Thread A calls block_wait
* Thread A continues when thread B calls block_release
*
* Thread B gets thread A's message
* Thread B calls block_hold
* Thread B does something
* Thread B calls block_release
*
**/
static void
block_prepare (block_info_t *info)
{
if (info->cond == NULL) {
info->cond = g_cond_new ();
info->mutex = g_mutex_new ();
}
g_mutex_lock (info->mutex);
info->val = FALSE;
}
static void
block_wait (block_info_t *info)
{
g_assert (info->cond);
while (info->val == FALSE)
g_cond_wait (info->cond, info->mutex);
g_mutex_unlock (info->mutex);
}
static void
block_hold (block_info_t *info)
{
g_assert (info->cond);
g_mutex_lock (info->mutex);
info->val = FALSE;
}
static void
block_release (block_info_t *info)
{
g_assert (info->cond);
info->val = TRUE;
g_cond_signal (info->cond);
g_mutex_unlock (info->mutex);
}
/* ******************** */
/* FIXME FIXME FIXME This is a totally evil hack. */
static Evolution_ShellView
retrieve_shell_view_interface_from_control (BonoboControl *control)
{
Bonobo_ControlFrame control_frame;
Evolution_ShellView shell_view_interface;
CORBA_Environment ev;
control_frame = bonobo_control_get_control_frame (control);
if (control_frame == NULL)
return CORBA_OBJECT_NIL;
CORBA_exception_init (&ev);
shell_view_interface = Bonobo_Unknown_query_interface (control_frame,
"IDL:Evolution/ShellView:1.0",
&ev);
CORBA_exception_free (&ev);
if (shell_view_interface != CORBA_OBJECT_NIL)
gtk_object_set_data (GTK_OBJECT (control),
"mail_threads_shell_view_interface",
shell_view_interface);
else
g_warning ("Control frame doesn't have Evolution/ShellView.");
return shell_view_interface;
}
static void
update_active_views (void)
{
GList *controls;
GList *p;
controls = folder_browser_factory_get_control_list ();
for (p = controls; p != NULL; p = p->next) {
BonoboControl *control;
Evolution_ShellView shell_view_interface;
CORBA_Environment ev;
control = BONOBO_CONTROL (p->data);
shell_view_interface = gtk_object_get_data (GTK_OBJECT (control), "mail_threads_shell_view_interface");
if (shell_view_interface == CORBA_OBJECT_NIL)
shell_view_interface = retrieve_shell_view_interface_from_control (control);
CORBA_exception_init (&ev);
if (shell_view_interface != CORBA_OBJECT_NIL) {
if (current_message == NULL && ! busy) {
Evolution_ShellView_unset_message (shell_view_interface, &ev);
} else {
if (current_message == NULL)
Evolution_ShellView_set_message (shell_view_interface,
"",
busy,
&ev);
else
Evolution_ShellView_set_message (shell_view_interface,
current_message,
busy,
&ev);
}
}
CORBA_exception_free (&ev);
}
}
static void
ui_set_busy (void)
{
busy = TRUE;
update_active_views ();
}
static void
ui_unset_busy (void)
{
busy = FALSE;
update_active_views ();
}
static void
ui_set_message (const char *message)
{
g_free (current_message);
current_message = g_strdup (message);
update_active_views ();
}
static void
ui_unset_message (void)
{
g_free (current_message);
current_message = NULL;
update_active_views ();
}