Fix minor compiler warning
[cteddy.git] / src / cteddy.cxx
blob13e52e1b224214efd5df7c404d6083f640dd3676
1 // Copyright 2008-2009 Philip Allison <sane@not.co.uk>
3 // This file is part of cteddy.
4 //
5 // cteddy is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // cteddy is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 // GNU General Public License for more details.
15 // You should have received a copy of the GNU General Public License
16 // along with cteddy. If not, see <http://www.gnu.org/licenses/>.
20 // Includes
23 // Project headers
24 #ifdef HAVE_CONFIG_H
25 # include "config.h"
26 #endif
28 // Language headers
29 #include <string>
30 #include <iostream>
31 #include <cstring>
32 #include <cerrno>
34 // Library headers
35 #include <gtk/gtk.h>
36 #include <gdk/gdkkeysyms.h>
37 #ifdef __USE_SM
38 # include <X11/SM/SMlib.h>
39 #endif
41 // System headers
42 #include <sys/types.h>
43 #include <sys/stat.h>
44 #include <fcntl.h>
45 #include <unistd.h>
46 #ifdef __USE_SM
47 # include <syslog.h>
48 #endif
52 // Globals
55 // Main window
56 GtkWidget* window = NULL;
58 // GTK/GDK returned pointer
59 GError* gerr = NULL;
61 // Image to display and its dimensions
62 GdkPixbuf* pixbuf = NULL;
63 gint width = 0, height = 0;
65 // Bitmap generated by thresholding image's alpha channel
66 // Used for window input (and sometimes shape) mask
67 GdkBitmap* bitmap = NULL;
69 // Whether or not the key binding for quit is disabled
70 bool noquit = false;
72 #ifdef __USE_SM
73 // Options structure - needed so that sm_save_yourself can know about the
74 // values of various program options which are usually only in main()'s scope
75 struct smopts
77 bool sticky;
78 bool noquit;
79 bool ontop;
80 char* image;
81 char* progname;
82 char* smcid;
84 #endif
88 // Implementation
91 // Re-paint the window when necessary
92 gboolean handle_expose(GtkWidget* widget, GdkEventExpose* event)
94 cairo_t* cairo = gdk_cairo_create(widget->window);
96 // Blank the surface
97 cairo_set_source_rgba (cairo, 1.0f, 1.0f, 1.0f, 0.0f);
98 cairo_set_operator (cairo, CAIRO_OPERATOR_SOURCE);
99 cairo_paint (cairo);
101 // Set cairo's source pattern from the desired image
102 gdk_cairo_set_source_pixbuf(cairo, pixbuf, 0.0f, 0.0f);
104 // Draw the image
105 cairo_rectangle(cairo, 0.0f, 0.0f,
106 static_cast<double>(width), static_cast<double>(height));
107 cairo_fill(cairo);
109 cairo_destroy(cairo);
111 return false;
114 // Handle mouse click events
115 gboolean window_clicked(GtkWidget* widget, GdkEventButton* event)
117 if (event->type == GDK_BUTTON_PRESS)
118 // Move the window with the mouse
119 gtk_window_begin_move_drag(GTK_WINDOW(window),
120 event->button, static_cast<gint>(event->x_root),
121 static_cast<gint>(event->y_root), event->time);
122 return true;
125 // Set up an RGBA colourmap for the window
126 void handle_screen(GtkWidget* window, GdkScreen* old, gpointer data)
128 GdkScreen* s = gtk_widget_get_screen(window);
129 GdkColormap* c = gdk_screen_get_rgba_colormap(s);
131 // Clear window's shape mask
132 gtk_widget_shape_combine_mask(window, NULL, 0, 0);
134 if (!c)
136 // Use standard RGB colourmap if we must.
137 c = gdk_screen_get_rgb_colormap(s);
138 // Also, set window's shape mask so that even without
139 // a true RGBA window, we don't get an ugly black border
140 if (bitmap)
141 gtk_widget_shape_combine_mask(window, bitmap, 0, 0);
144 gtk_widget_set_colormap(window, c);
147 // Handle key press events
148 void handle_key_press(GtkWidget *Window, GdkEventKey *theEvent)
150 switch( theEvent->keyval )
152 case GDK_q:
153 // Quit on "q"
154 if (!noquit)
155 gtk_main_quit();
156 break;
158 default:
159 // Ignore all other keys
160 break;
165 // Function for seeing whether or not a particular image file exists.
166 // If we find a file, copy the filename into image and return true, otherwise return false.
167 bool image_exists(std::string& image, const std::string& find_image)
170 // Set of image extensions to try appending when searching for an image
171 static const char* extensions[16];
172 extensions[0] = ".png"; extensions[1] = ".gif"; extensions[2] = ".jpg";
173 extensions[3] = ".jpeg"; extensions[4] = ".pnm"; extensions[5] = ".xpm";
174 extensions[6] = ".tif"; extensions[7] = ".tiff";
175 extensions[8] = ".PNG"; extensions[9] = ".GIF"; extensions[10] = ".JPG";
176 extensions[11] = ".JPEG"; extensions[12] = ".PNM"; extensions[13] = ".XPM";
177 extensions[14] = ".TIF"; extensions[15] = ".TIFF";
179 struct stat s;
180 // Try unadulterated filename, then extensions in lower & upper case
181 for (size_t i = 0; i < 16; ++i)
183 std::string ximage(find_image);
184 if (i > 0) ximage.append(extensions[i - 1]);
185 if (stat(ximage.c_str(), &s) == 0)
187 image = ximage;
188 return true;
191 return false;
194 #ifdef __USE_SM
195 // Session management "SaveYourself" callback
196 void sm_save_yourself(SmcConn conn, SmPointer data, int type, Bool shutdown, int istyle, Bool fast)
198 // Set the required properties for session management
199 smopts* opts = static_cast<smopts*>(data);
201 // Program name
202 SmProp progname;
203 progname.name = SmProgram;
204 progname.type = SmARRAY8;
205 progname.num_vals = 1;
206 SmPropValue progname_value;
207 progname_value.length = strlen(opts->progname);
208 progname_value.value = opts->progname;
209 progname.vals = &progname_value;
211 // User ID
212 SmProp uid;
213 uid.name = SmUserID;
214 uid.type = SmARRAY8;
215 uid.num_vals = 1;
216 SmPropValue uid_value;
217 const char* c_uidstr = g_get_user_name();
218 char* uidstr = new char[strlen(c_uidstr) + 1];
219 strcpy(uidstr, c_uidstr);
220 uid_value.length = strlen(c_uidstr);
221 uid_value.value = uidstr;
222 uid.vals = &uid_value;
224 // Current working directory
225 SmProp cwd;
226 cwd.name = SmCurrentDirectory;
227 cwd.type = SmARRAY8;
228 cwd.num_vals = 1;
229 SmPropValue cwd_value;
230 const char* c_cwdstr = g_get_current_dir();
231 char* cwdstr = new char[strlen(c_cwdstr) + 1];
232 strcpy(cwdstr, c_cwdstr);
233 cwd_value.length = strlen(c_cwdstr);
234 cwd_value.value = cwdstr;
235 cwd.vals = &cwd_value;
237 // Clone & restart commands
238 SmPropValue command[6];
239 int numvals = 0;
240 command[numvals++] = progname_value;
241 SmProp restartcommand, clonecommand;
242 char sv[] = "-stick";
243 char fv[] = "-float";
244 char qv[] = "-noquit";
246 if (opts->sticky)
248 SmPropValue stickprop_value;
249 stickprop_value.value = sv;
250 stickprop_value.length = 6;
251 command[numvals++] = stickprop_value;
254 if (opts->ontop)
256 SmPropValue floatprop_value;
257 floatprop_value.value = fv;
258 floatprop_value.length = 6;
259 command[numvals++] = floatprop_value;
262 if (opts->noquit)
264 SmPropValue noquitprop_value;
265 noquitprop_value.value = qv;
266 noquitprop_value.length = 7;
267 command[numvals++] = noquitprop_value;
270 SmPropValue image_value;
271 char* imagestr = new char[strlen(opts->image) + 3];
272 imagestr[0] = '-'; imagestr[1] = 'F';
273 strcpy(imagestr + 2, opts->image);
274 image_value.value = imagestr;
275 image_value.length = strlen(imagestr);
276 command[numvals++] = image_value;
278 SmPropValue smcid_value;
279 std::string c_smcidarg("-smcid");
280 c_smcidarg.append(opts->smcid);
281 char* smcidarg = new char[c_smcidarg.length() + 1];
282 strcpy(smcidarg, c_smcidarg.c_str());
283 smcid_value.value = smcidarg;
284 smcid_value.length = c_smcidarg.length();
285 command[numvals++] = smcid_value;
287 restartcommand.num_vals = numvals;
288 clonecommand.num_vals = numvals - 1;
289 restartcommand.vals = command;
290 clonecommand.vals = command;
292 restartcommand.name = SmRestartCommand;
293 restartcommand.type = SmLISTofARRAY8;
294 clonecommand.name = SmCloneCommand;
295 clonecommand.type = SmLISTofARRAY8;
297 SmProp* props[5];
298 props[0] = &progname;
299 props[1] = &uid;
300 props[2] = &cwd;
301 props[3] = &restartcommand;
302 props[4] = &clonecommand;
303 SmcSetProperties(conn, 5, props);
305 // Delete any non-const things we had to explicitly allocate
306 delete[] uidstr;
307 delete[] cwdstr;
308 delete[] smcidarg;
309 delete[] imagestr;
311 // Send SaveYourselfDone.
312 SmcSaveYourselfDone(conn, True);
315 // Session management "Save complete" callback
316 void sm_save_complete(SmcConn conn, SmPointer data)
320 // Session management "Die" callback
321 void sm_die(SmcConn conn, SmPointer data)
323 // TODO - Have GTK quit event handler disconnect from SM if in use
324 SmcCloseConnection(conn, 0, NULL);
325 gtk_main_quit();
328 // Pump ICE messages when data comes in
329 gboolean sm_pump_ice(GIOChannel* chan, GIOCondition cond, gpointer data)
331 IceProcessMessages(static_cast<IceConn>(data), NULL, NULL);
332 return true;
335 // Called whenever an ICE connection is opened or closed
336 void ice_connection_watch(IceConn conn, IcePointer data, Bool opening, IcePointer* wdata)
338 if (opening)
340 // When we get a new ICE connection, install a IO channel which will pump
341 // ICE messages when data is waiting
342 int icefd = IceConnectionNumber(conn);
343 GIOChannel* ioc = g_io_channel_unix_new(icefd);
344 g_io_add_watch(ioc, G_IO_IN, sm_pump_ice, conn);
345 *wdata = ioc;
346 } else {
347 // When an ICE connection closes, close the IO channel
348 g_io_channel_unref(static_cast<GIOChannel*>(*wdata));
351 #endif
353 // Entry point
354 int main (int argc, char* argv[])
356 gtk_init(&argc, &argv);
358 // Whether or not to set the window's keep-on-top hint
359 bool ontop = false;
361 // Whether or not the window's sticky hint should be set
362 bool sticky = false;
364 // Whether or not to fork into the background
365 bool background = true;
367 #ifdef __USE_SM
368 // Whether or not to use session management
369 bool sm = true;
371 // Old session client ID, if being restored from a saved session
372 char* oldsmcid = NULL;
374 // Set of callbacks
375 smopts opts;
376 SmcCallbacks smcallbacks;
377 smcallbacks.save_yourself.callback = sm_save_yourself;
378 smcallbacks.save_yourself.client_data = &opts;
379 smcallbacks.die.callback = sm_die;
380 smcallbacks.die.client_data = NULL;
381 smcallbacks.save_complete.callback = sm_save_complete;
382 smcallbacks.save_complete.client_data = NULL;
383 #endif
385 // Mimic the image loading behaviour of xteddy, to an extent.
387 // Start from the name of the executable itself
388 std::string image(argv[0]);
389 #ifdef __USE_SM
390 char* smimage = argv[0];
391 #endif
393 // Take just the last component of the path
394 std::string::size_type s = image.find_last_of('/');
395 if (s != std::string::npos)
396 image = image.substr(s + 1);
398 // Parse options
399 // XXX This code is really quick and nasty
400 for (int i = 1; i < argc; ++i)
402 // Allow image specification with -F<file>
403 if (strncmp(argv[i], "-F", 2) == 0)
405 image = argv[i] + 2;
406 #ifdef __USE_SM
407 smimage = argv[i] + 2;
408 #endif
410 // Allow disablement of "q" keybinding
411 else if (strncmp(argv[i], "-noquit", 7) == 0)
412 noquit = true;
413 // Allow keep-on-top
414 else if (strncmp(argv[i], "-float", 6) == 0)
415 ontop = true;
416 // Allow sticking to all desktops
417 else if (strncmp(argv[i], "-stick", 6) == 0)
418 sticky = true;
419 // Allow disablement of backgrounding
420 // Implies -nosm (see below)
421 else if (strncmp(argv[i], "-nobg", 5) == 0)
422 background = false;
423 #ifdef __USE_SM
424 // Allow disablement of session management
425 else if (strncmp(argv[i], "-nosm", 5) == 0)
426 sm = false;
427 // Read in old session client ID
428 else if (strncmp(argv[i], "-smcid", 6) == 0)
429 oldsmcid = argv[i] + 6;
430 #endif
431 else if (strncmp(argv[i], "-h", 2) == 0 || strncmp(argv[i], "--help", 6) == 0)
433 std::cout << "Usage: cteddy [options]" << std::endl << std::endl;
434 std::cout << "-F<image>" << std::endl;
435 std::cout << "\tDisplay an image other than xteddy" << std::endl;
436 std::cout << "-float" << std::endl;
437 std::cout << "\tFloat above other windows" << std::endl;
438 std::cout << "-stick" << std::endl;
439 std::cout << "\tStick to all desktops" << std::endl;
440 std::cout << "-noquit" << std::endl;
441 std::cout << "\tDisable the 'q' quit keybinding" << std::endl;
442 std::cout << "-nosm" << std::endl;
443 std::cout << "\tDisable session management support" << std::endl;
444 std::cout << "-nobg" << std::endl;
445 std::cout << "\tDo not detach from terminal (implies -nosm)" << std::endl;
446 std::cout << "--help, -h" << std::endl;
447 std::cout << "\tOutput this message" << std::endl;
448 std::cout << "--version, -v" << std::endl;
449 std::cout << "\tOutput version number and build information" << std::endl;
450 return 0;
452 else if (strncmp(argv[i], "-v", 2) == 0 || strncmp(argv[i], "--version", 9) == 0)
454 std::cout << PACKAGE_STRING << std::endl << std::endl;
455 std::cout << "Build options: " << CONFIGURE_OPTS << std::endl;
456 return 0;
458 else
459 std::cerr << "Unrecognised option: " << argv[i] << std::endl;
462 #ifdef __USE_SM
463 // If we're being restarted by the session manager, don't bother
464 // backgrounding, as we aren't attached to a terminal anyway
465 if (sm && oldsmcid != NULL)
466 background = false;
468 // On the other hand, don't use session management if not forking
469 // into the background, if being started directly by the user
470 // rather than restarted as part of session.
471 // XXX This is how -nobg imples -nosm.
472 else if (!background && oldsmcid == NULL)
473 sm = false;
475 if (sm)
477 // Open & monitor a GIOChannel each time there is a new ICE connection
478 IceAddConnectionWatch(ice_connection_watch, NULL);
480 // Fill in SM opts so sm_save_yourself can set properties
481 opts.sticky = sticky;
482 opts.ontop = ontop;
483 opts.noquit = noquit;
484 opts.progname = argv[0];
485 opts.image = smimage;
487 // Try to connect to the session manager, displaying an error if we can't
488 char smerr[512];
489 memset(smerr, '\0', 512);
490 if (SmcOpenConnection(NULL, NULL, SmProtoMajor, SmProtoMinor,
491 SmcSaveYourselfProcMask | SmcDieProcMask | SmcSaveCompleteProcMask,
492 &smcallbacks, oldsmcid, &(opts.smcid), 512, smerr) == NULL)
494 if (strlen(smerr) == 0)
495 std::cerr << "Cannot connect to session manager; unknown error." << std::endl;
496 else
497 std::cerr << "Cannot connect to session manager: " << smerr << std::endl;
499 // Continue anyway, just without SM (for convenience of anyone without a session manager)
500 sm = false;
504 // TODO - Use GTK dialogues to display errors, not stderr, as we may not be attached to a terminal
505 #endif
507 // If the given path contains slashes, assume it is absolute or
508 // relative to the current working directory and use it verbatim.
509 // Otherwise, search for it.
510 if (image.find_last_of('/') == std::string::npos)
512 // Search for the image
513 bool found = false;
514 std::string searchimage(PKGDATADIR "/");
515 searchimage.append(image);
516 found = image_exists(image, searchimage);
517 // Try converting a leading x to a c and vice versa
518 if (!found && image.at(0) == 'x')
520 std::string ximage(PKGDATADIR "/c");
521 ximage.append(image.substr(1));
522 found = image_exists(image, ximage);
524 if (!found && image.at(0) == 'c')
526 std::string ximage(PKGDATADIR "/x");
527 ximage.append(image.substr(1));
528 found = image_exists(image, ximage);
530 // Try prepending c or x
531 if (!found)
533 std::string ximage(PKGDATADIR "/c");
534 ximage.append(image);
535 found = image_exists(image, ximage);
537 if (!found)
539 std::string ximage(PKGDATADIR "/x");
540 ximage.append(image);
541 found = image_exists(image, ximage);
544 if (!found)
546 std::cerr << "Cannot find image!" << std::endl;
547 return 1;
551 // Load the image from the supplied file path
552 pixbuf = gdk_pixbuf_new_from_file(image.c_str(), &gerr);
553 if (gerr)
555 std::cerr << "Cannot load image \"" << image << "\": " << gerr->message << std::endl;
556 g_error_free(gerr);
557 return 1;
561 // Create the window
562 window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
564 // Top-level windows don't normally respond to mouse button events directly.
565 // We want this one to, though.
566 gtk_widget_add_events(window, GDK_BUTTON_PRESS_MASK);
568 // Set up event callbacks
569 g_signal_connect(G_OBJECT(window), "expose-event", G_CALLBACK(handle_expose), NULL);
570 g_signal_connect(G_OBJECT(window), "destroy", G_CALLBACK(gtk_main_quit), NULL);
571 g_signal_connect(G_OBJECT(window), "button-press-event", G_CALLBACK(window_clicked), NULL);
572 g_signal_connect(G_OBJECT(window), "screen-changed", G_CALLBACK(handle_screen), NULL);
573 g_signal_connect(G_OBJECT(window), "key-press-event", G_CALLBACK(handle_key_press), NULL);
575 // Grab pixmap and alpha-thresholded bitmap from original pixmap
576 GdkPixmap* pixmap;
577 gdk_pixbuf_render_pixmap_and_mask(pixbuf, &pixmap, &bitmap, 128);
579 // Set window size to image size
580 gdk_drawable_get_size(pixmap, &width, &height);
581 gtk_widget_set_size_request(window, width, height);
583 // If we have an alpha channel on the image, set the window's input shape,
584 // clearing it completely first
585 if (bitmap)
587 gtk_widget_input_shape_combine_mask(window, NULL, 0, 0);
588 gtk_widget_input_shape_combine_mask(window, bitmap, 0, 0);
591 // Deref the pixmap, but keep the bitmap around for now
592 // May be useful later for setting window's shape mask if we cannot get
593 // a true RGBA visual (i.e. compositing manager not running)
594 g_object_unref(G_OBJECT(pixmap));
596 // Set up various other properties we're interested in
597 gtk_window_set_title(GTK_WINDOW(window), "cteddy");
598 gtk_window_set_resizable(GTK_WINDOW(window), false);
599 gtk_window_set_decorated(GTK_WINDOW(window), false);
600 gtk_widget_set_app_paintable(window, true);
601 gtk_window_set_icon(GTK_WINDOW(window), pixbuf);
603 // Trigger initial attempt to switch to an RGBA colourmap
604 handle_screen(window, NULL, NULL);
606 // Have to do this before we can use window->window as a GdkWindow*
607 gtk_widget_realize(window);
609 #ifdef __USE_SM
610 // Set the window's SM client ID
611 if (sm)
612 gdk_set_sm_client_id(opts.smcid);
613 #endif
616 // Load in the heart-shaped cursor image and use it for our window's cursor
617 GdkPixbuf* heart = gdk_pixbuf_new_from_file(PKGDATADIR "/heart.png", &gerr);
618 if (gerr)
620 std::cerr << "Cannot load image: " << gerr->message << std::endl;
621 g_error_free(gerr);
622 return 1;
624 GdkCursor* cursor = gdk_cursor_new_from_pixbuf(gdk_display_get_default(),
625 heart, 12, 12);
626 gdk_window_set_cursor(window->window, cursor);
629 // Clear the window's default background
630 gdk_window_set_back_pixmap(window->window, NULL, false);
632 // Honour float & sticky options
633 if (ontop)
634 gtk_window_set_keep_above(GTK_WINDOW(window), true);
635 if (sticky)
636 gtk_window_stick(GTK_WINDOW(window));
638 gtk_widget_show_all(window);
640 // Fork into background if desired
641 if (background)
643 pid_t p = fork();
644 if (p < 0)
646 std::cerr << "Could not fork: " << strerror(errno) << std::endl;
647 return 1;
649 else if (p == 0)
651 // Macro for performing a particular system call,
652 // trying again if it returns EINTR, displaying an error otherwise
653 #define __IGNORE_EINTR(fun, msg) \
654 while(fun) \
656 if (errno == EINTR) \
657 continue; \
658 std::cerr << msg << strerror(errno) << std::endl; \
659 return 1; \
662 // Close stdin
663 __IGNORE_EINTR(close(0) != 0, "Could not close stdin: ");
664 // Redirect stdout & stderr to /dev/null
665 int nullfd;
666 __IGNORE_EINTR((nullfd = open("/dev/null", O_WRONLY)) < 0, "Cannot open /dev/null: ");
667 __IGNORE_EINTR(dup2(nullfd, 1) < 0, "Cannot redirect stdout to /dev/null: ");
668 __IGNORE_EINTR(dup2(nullfd, 2) < 0, "Cannot redirect stderr to /dev/null: ");
669 // Create new process session
670 if (setsid() < 0)
672 std::cerr << "Cannot create new session: " << strerror(errno) << std::endl;
673 return 1;
676 else
677 return 0;
680 gtk_main();
682 return 0;