/* Copyright (C) 1999-2002 Paul Barton-Davis 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., 675 Mass Ave, Cambridge, MA 02139, USA. $Id: gtk_ui.cc,v 1.28 2006/02/15 15:37:31 essej Exp $ */ #include <cmath> #include <fcntl.h> #include <signal.h> #include <unistd.h> #include <cerrno> #include <climits> #include <cctype> #include <gtk--.h> #include <pbd/error.h> #include <pbd/touchable.h> #include <pbd/failed_constructor.h> #include <pbd/pthread_utils.h> #include <gtkmmext/gtk_ui.h> #include <gtkmmext/textviewer.h> #include <gtkmmext/popup.h> #include <gtkmmext/utils.h> #include "i18n.h" using namespace Gtkmmext; using std::map; pthread_t UI::gui_thread; UI *UI::theGtkUI = 0; UI::UI (string name, int *argc, char ***argv, string rcfile) : _ui_name (name) { theMain = new Gtk::Main (argc, argv); tips = new Gtk::Tooltips; if (pthread_key_create (&thread_request_buffer_key, 0)) { cerr << _("cannot create thread request buffer key") << endl; throw failed_constructor(); } PBD::ThreadCreated.connect (slot (*this, &UI::register_thread)); _ok = false; _active = false; if (!theGtkUI) { theGtkUI = this; gui_thread = pthread_self (); } else { fatal << "duplicate UI requested" << endmsg; /* NOTREACHED */ } if (setup_signal_pipe ()) { return; } load_rcfile (rcfile); errors = new TextViewer (850,100); errors->text().set_editable (false); errors->text().set_name ("ErrorText"); string title; title = _ui_name; title += ": Log"; errors->set_title (title); errors->dismiss_button().set_name ("ErrorLogCloseButton"); errors->realize(); errors->get_window().set_decorations (GdkWMDecoration (GDK_DECOR_BORDER|GDK_DECOR_RESIZEH)); errors->delete_event.connect (bind (slot (just_hide_it), (Gtk::Window *) errors)); register_thread (pthread_self(), X_("GUI")); _ok = true; } UI::~UI () { close (signal_pipe[0]); close (signal_pipe[1]); } int UI::load_rcfile (string path) { if (path.length() == 0) { return -1; } if (access (path.c_str(), R_OK)) { error << "UI: couldn't find rc file \"" << path << '"' << endmsg; return -1; } gtk_rc_parse (path.c_str()); Gtk::Label *a_widget1; Gtk::Label *a_widget2; Gtk::Label *a_widget3; Gtk::Label *a_widget4; Gtk::Style *style; a_widget1 = new Gtk::Label; a_widget2 = new Gtk::Label; a_widget3 = new Gtk::Label; a_widget4 = new Gtk::Label; a_widget1->set_name ("FatalMessage"); a_widget1->ensure_style (); style = a_widget1->get_style (); fatal_message_style = style->gtkobj(); a_widget2->set_name ("ErrorMessage"); a_widget2->ensure_style (); style = a_widget2->get_style (); error_message_style = style->gtkobj(); a_widget3->set_name ("WarningMessage"); a_widget3->ensure_style (); style = a_widget3->get_style (); warning_message_style = style->gtkobj(); a_widget4->set_name ("InfoMessage"); a_widget4->ensure_style (); style = a_widget4->get_style (); info_message_style = style->gtkobj(); delete a_widget1; delete a_widget2; delete a_widget3; delete a_widget4; return 0; } void UI::run (Receiver &old_receiver) { listen_to (error); listen_to (info); listen_to (warning); listen_to (fatal); old_receiver.hangup (); starting (); _active = true; theMain->run (); _active = false; stopping (); hangup (); return; } bool UI::running () { return _active; } void UI::kill () { if (_active) { pthread_kill (gui_thread, SIGKILL); } } void UI::quit () { request (Quit); } void UI::do_quit () { Gtk::Main::quit(); } void UI::touch_display (Touchable *display) { Request *req = get_request (TouchDisplay); if (req == 0) { return; } req->display = display; send_request (req); } void UI::call_slot (SigC::Slot0<void> slot) { Request *req = get_request (CallSlot); if (req == 0) { return; } req->slot = slot; send_request (req); } void UI::call_slot_locked (SigC::Slot0<void> slot) { if (caller_is_gui_thread()) { call_slot (slot); return; } Request *req = get_request (CallSlotLocked); if (req == 0) { return; } req->slot = slot; pthread_mutex_init (&req->slot_lock, NULL); pthread_cond_init (&req->slot_cond, NULL); pthread_mutex_lock (&req->slot_lock); send_request (req); pthread_cond_wait (&req->slot_cond, &req->slot_lock); pthread_mutex_unlock (&req->slot_lock); delete req; } void UI::set_tip (Gtk::Widget *w, const gchar *tip, const gchar *hlp) { Request *req = get_request (SetTip); if (req == 0) { return; } req->widget = w; req->msg = tip; req->msg2 = hlp; send_request (req); } void UI::set_state (Gtk::Widget *w, GtkStateType state) { Request *req = get_request (StateChange); if (req == 0) { return; } req->new_state = state; req->widget = w; send_request (req); } void UI::idle_add (int (*func)(void *), void *arg) { Request *req = get_request (AddIdle); if (req == 0) { return; } req->function = func; req->arg = arg; send_request (req); } void UI::timeout_add (unsigned int timeout, int (*func)(void *), void *arg) { Request *req = get_request (AddTimeout); if (req == 0) { return; } req->function = func; req->arg = arg; req->timeout = timeout; send_request (req); } /* END abstract_ui interfaces */ /* Handling requests */ void UI::register_thread (pthread_t thread_id, string name) { RingBufferNPT<Request>* b = new RingBufferNPT<Request> (2048); { PBD::LockMonitor lm (request_buffer_map_lock, __LINE__, __FILE__); request_buffers[thread_id] = b; } pthread_setspecific (thread_request_buffer_key, b); } UI::Request::Request() { } UI::Request* UI::get_request (RequestType rt) { RingBufferNPT<Request>* rbuf = static_cast<RingBufferNPT<Request>* >(pthread_getspecific (thread_request_buffer_key)); if (rbuf == 0) { /* Cannot happen, but if it does we can't use the error reporting mechanism */ cerr << _("programming error: ") << compose (X_("no GUI request buffer found for thread %1"), pthread_self()) << endl; abort (); } RingBufferNPT<Request>::rw_vector vec; rbuf->get_write_vector (&vec); if (vec.len[0] == 0) { if (vec.len[1] == 0) { cerr << compose (X_("no space in GUI request buffer for thread %1"), pthread_self()) << endl; return 0; } else { vec.buf[1]->type = rt; return vec.buf[1]; } } else { vec.buf[0]->type = rt; return vec.buf[0]; } } int UI::setup_signal_pipe () { /* setup the pipe that other threads send us notifications/requests through. */ if (pipe (signal_pipe)) { error << "UI: cannot create error signal pipe (" << strerror (errno) << ")" << endmsg; return -1; } if (fcntl (signal_pipe[0], F_SETFL, O_NONBLOCK)) { error << "UI: cannot set O_NONBLOCK on " "signal read pipe (" << strerror (errno) << ")" << endmsg; return -1; } if (fcntl (signal_pipe[1], F_SETFL, O_NONBLOCK)) { error << "UI: cannot set O_NONBLOCK on " "signal write pipe (" << strerror (errno) << ")" << endmsg; return -1; } /* add the pipe to the select/poll loop that GDK does */ gdk_input_add (signal_pipe[0], GDK_INPUT_READ, UI::signal_pipe_callback, this); return 0; } void UI::signal_pipe_callback (void *arg, int fd, GdkInputCondition cond) { char buf[256]; /* flush (nonblocking) pipe */ while (read (fd, buf, 256) > 0); ((UI *) arg)->handle_ui_requests (); } void UI::handle_ui_requests () { RequestBufferMap::iterator i; request_buffer_map_lock.lock (); for (i = request_buffers.begin(); i != request_buffers.end(); ++i) { RingBufferNPT<Request>::rw_vector vec; while (true) { /* we must process requests 1 by 1 because the request may run a recursive main event loop that will itself call handle_ui_requests. when we return from the request handler, we cannot expect that the state of queued requests is even remotely consistent with the condition before we called it. */ i->second->get_read_vector (&vec); if (vec.len[0] == 0) { break; } else { /* copy constructor does a deep copy of the Request object, unlike Ringbuffer::read() */ Request req (*vec.buf[0]); i->second->increment_read_ptr (1); request_buffer_map_lock.unlock (); do_request (&req); request_buffer_map_lock.lock (); } } } request_buffer_map_lock.unlock (); } void UI::do_request (Request* req) { switch (req->type) { case ErrorMessage: // cerr << "error message\n"; process_error_message (req->chn, req->msg); free (const_cast<char*>(req->msg)); /* it was strdup'ed */ req->msg = 0; break; case Quit: // cerr << "quit\n"; do_quit (); break; case CallSlot: // cerr << "call slot\n"; req->slot.call (); break; case CallSlotLocked: // cerr << "call slot locked\n"; pthread_mutex_lock (&req->slot_lock); req->slot.call (); pthread_cond_signal (&req->slot_cond); pthread_mutex_unlock (&req->slot_lock); break; case TouchDisplay: // cerr << "touch display\n"; req->display->touch (); if (req->display->delete_after_touch()) { delete req->display; } break; case StateChange: // cerr << "state change\n"; req->widget->set_state (req->new_state); break; case SetTip: // cerr << "set tip\n"; /* XXX need to figure out how this works */ break; case AddIdle: // cerr << "add idle\n"; gtk_idle_add (req->function, req->arg); break; case AddTimeout: // cerr << "add timeout\n"; gtk_timeout_add (req->timeout, req->function, req->arg); break; default: error << "UI: unknown request type " << (int) req->type << endmsg; } } void UI::send_request (Request *req) { if (instance() == 0) { return; /* XXX is this the right thing to do ? */ } if (caller_is_gui_thread()) { // cerr << "GUI thread sent request " << req << " type = " << req->type << endl; do_request (req); } else { const char c = 0; RingBufferNPT<Request*>* rbuf = static_cast<RingBufferNPT<Request*> *> (pthread_getspecific (thread_request_buffer_key)); if (rbuf == 0) { /* can't use the error system to report this, because this thread isn't registered! */ cerr << _("programming error: ") << compose (X_("UI::send_request() called from %1, but no request buffer exists for that thread"), pthread_self()) << endl; abort (); } // cerr << "thread " << pthread_self() << " sent request " << req << " type = " << req->type << endl; rbuf->increment_write_ptr (1); write (signal_pipe[1], &c, 1); } } void UI::request (RequestType rt) { Request *req = get_request (rt); if (req == 0) { return; } send_request (req); } /*====================================================================== Error Display ======================================================================*/ void UI::receive (Transmitter::Channel chn, const char *str) { if (caller_is_gui_thread()) { process_error_message (chn, str); } else { Request* req = get_request (ErrorMessage); if (req == 0) { return; } req->chn = chn; req->msg = strdup (str); send_request (req); } } #define OLD_STYLE_ERRORS 1 void UI::process_error_message (Transmitter::Channel chn, const char *str) { GtkStyle *style; char *prefix; size_t prefix_len; bool fatal_received = false; #ifndef OLD_STYLE_ERRORS PopUp* popup = new PopUp (GTK_WIN_POS_CENTER, 0, true); #endif switch (chn) { case Transmitter::Fatal: prefix = "[FATAL]: "; style = fatal_message_style; prefix_len = 9; fatal_received = true; break; case Transmitter::Error: #if OLD_STYLE_ERRORS prefix = "[ERROR]: "; style = error_message_style; prefix_len = 9; #else popup->set_name ("ErrorMessage"); popup->set_text (str); popup->touch (); return; #endif break; case Transmitter::Info: #if OLD_STYLE_ERRORS prefix = "[INFO]: "; style = info_message_style; prefix_len = 8; #else popup->set_name ("InfoMessage"); popup->set_text (str); popup->touch (); return; #endif break; case Transmitter::Warning: #if OLD_STYLE_ERRORS prefix = "[WARNING]: "; style = warning_message_style; prefix_len = 11; #else popup->set_name ("WarningMessage"); popup->set_text (str); popup->touch (); return; #endif break; default: /* no choice but to use text/console output here */ cerr << "programmer error in UI::check_error_messages (channel = " << chn << ")\n"; ::exit (1); } errors->text().freeze(); if (fatal_received) { handle_fatal (str); } else { display_message (prefix, prefix_len, style, str); if (!errors->is_visible()) { toggle_errors(); } } errors->text().thaw(); } void UI::toggle_errors () { if (!errors->is_visible()) { errors->set_position (GTK_WIN_POS_MOUSE); errors->show (); } else { errors->hide (); } } void UI::display_message (const char *prefix, gint prefix_len, GtkStyle *style, const char *msg) { /* stupid, stupid, stupid Gtk-- */ Gdk_Font pfont(style->font); Gdk_Font mfont (style->font); Gdk_Color pfg (&style->fg[GTK_STATE_ACTIVE]); Gdk_Color pbg (&style->bg[GTK_STATE_ACTIVE]); Gdk_Color mfg (&style->fg[GTK_STATE_NORMAL]); Gdk_Color mbg (&style->bg[GTK_STATE_NORMAL]); errors->text().insert (pfont, pfg, pbg, prefix, prefix_len); errors->text().insert (mfont, mfg, mbg, msg, strlen (msg)); errors->text().insert (mfont, mfg, mbg, "\n", 1); errors->scroll_to_bottom (); } void UI::handle_fatal (const char *message) { Gtk::Window win (GTK_WINDOW_DIALOG); Gtk::VBox packer; Gtk::Label label (message); Gtk::Button quit (_("Press To Exit")); win.set_default_size (400, 100); string title; title = _ui_name; title += ": Fatal Error"; win.set_title (title); win.set_position (GTK_WIN_POS_MOUSE); win.add (packer); packer.pack_start (label, true, true); packer.pack_start (quit, false, false); quit.clicked.connect(SigC::slot(this,&UI::quit)); win.show_all (); win.set_modal (true); theMain->run (); exit (1); } void UI::popup_error (const char *text) { PopUp *pup; if (!caller_is_gui_thread()) { error << "non-UI threads can't use UI::popup_error" << endmsg; return; } pup = new PopUp (GTK_WIN_POS_MOUSE, 0, true); pup->set_text (text); pup->touch (); } void UI::flush_pending () { if (!caller_is_gui_thread()) { error << "non-UI threads cannot call UI::flush_pending()" << endmsg; return; } gtk_main_iteration(); while (gtk_events_pending()) { gtk_main_iteration(); } } gint UI::just_hide_it (GdkEventAny *ev, Gtk::Window *win) { win->hide_all (); return TRUE; } GdkColor UI::get_color (const string& prompt, bool& picked, gdouble *initial) { GdkColor color; Gtk::ColorSelectionDialog color_dialog (prompt); color_dialog.set_modal (true); color_dialog.get_cancel_button()->clicked.connect (bind (slot (this, &UI::color_selection_done), false)); color_dialog.get_ok_button()->clicked.connect (bind (slot (this, &UI::color_selection_done), true)); color_dialog.delete_event.connect (slot (*this, &UI::color_selection_deleted)); if (initial) { color_dialog.get_colorsel()->set_color (initial); } color_dialog.show_all (); color_picked = false; picked = false; Gtk::Main::run(); color_dialog.hide_all (); if (color_picked) { gdouble f_rgba[4]; color_dialog.get_colorsel()->get_color (f_rgba); color.red = (gushort) rint (f_rgba[0] * 65535); color.green = (gushort) rint (f_rgba[1] * 65535); color.blue = (gushort) rint (f_rgba[2] * 65535); picked = true; } return color; } void UI::color_selection_done (bool status) { color_picked = status; Gtk::Main::quit (); } gint UI::color_selection_deleted (GdkEventAny *ev) { Gtk::Main::quit (); return TRUE; }