1 // Copyright 2008-2009 Philip Allison <sane@not.co.uk>
3 // This file is part of cteddy.
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.
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/>.
36 #include <gdk/gdkkeysyms.h>
38 # include <X11/SM/SMlib.h>
42 #include <sys/types.h>
56 GtkWidget
* window
= NULL
;
58 // GTK/GDK returned pointer
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
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
91 // Re-paint the window when necessary
92 gboolean
handle_expose(GtkWidget
* widget
, GdkEventExpose
* event
)
94 cairo_t
* cairo
= gdk_cairo_create(widget
->window
);
97 cairo_set_source_rgba (cairo
, 1.0f
, 1.0f
, 1.0f
, 0.0f
);
98 cairo_set_operator (cairo
, CAIRO_OPERATOR_SOURCE
);
101 // Set cairo's source pattern from the desired image
102 gdk_cairo_set_source_pixbuf(cairo
, pixbuf
, 0.0f
, 0.0f
);
105 cairo_rectangle(cairo
, 0.0f
, 0.0f
,
106 static_cast<double>(width
), static_cast<double>(height
));
109 cairo_destroy(cairo
);
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
);
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);
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
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
)
159 // Ignore all other keys
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";
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)
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
);
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
;
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
226 cwd
.name
= SmCurrentDirectory
;
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];
240 command
[numvals
++] = progname_value
;
241 SmProp restartcommand
, clonecommand
;
242 char sv
[] = "-stick";
243 char fv
[] = "-float";
244 char qv
[] = "-noquit";
248 SmPropValue stickprop_value
;
249 stickprop_value
.value
= sv
;
250 stickprop_value
.length
= 6;
251 command
[numvals
++] = stickprop_value
;
256 SmPropValue floatprop_value
;
257 floatprop_value
.value
= fv
;
258 floatprop_value
.length
= 6;
259 command
[numvals
++] = floatprop_value
;
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
;
298 props
[0] = &progname
;
301 props
[3] = &restartcommand
;
302 props
[4] = &clonecommand
;
303 SmcSetProperties(conn
, 5, props
);
305 // Delete any non-const things we had to explicitly allocate
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
);
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
);
335 // Called whenever an ICE connection is opened or closed
336 void ice_connection_watch(IceConn conn
, IcePointer data
, Bool opening
, IcePointer
* wdata
)
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
);
347 // When an ICE connection closes, close the IO channel
348 g_io_channel_unref(static_cast<GIOChannel
*>(*wdata
));
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
361 // Whether or not the window's sticky hint should be set
364 // Whether or not to fork into the background
365 bool background
= true;
368 // Whether or not to use session management
371 // Old session client ID, if being restored from a saved session
372 char* oldsmcid
= NULL
;
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
;
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]);
390 char* smimage
= argv
[0];
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);
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)
407 smimage
= argv
[i
] + 2;
410 // Allow disablement of "q" keybinding
411 else if (strncmp(argv
[i
], "-noquit", 7) == 0)
414 else if (strncmp(argv
[i
], "-float", 6) == 0)
416 // Allow sticking to all desktops
417 else if (strncmp(argv
[i
], "-stick", 6) == 0)
419 // Allow disablement of backgrounding
420 // Implies -nosm (see below)
421 else if (strncmp(argv
[i
], "-nobg", 5) == 0)
424 // Allow disablement of session management
425 else if (strncmp(argv
[i
], "-nosm", 5) == 0)
427 // Read in old session client ID
428 else if (strncmp(argv
[i
], "-smcid", 6) == 0)
429 oldsmcid
= argv
[i
] + 6;
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
;
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
;
459 std::cerr
<< "Unrecognised option: " << argv
[i
] << std::endl
;
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
)
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
)
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
;
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
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
;
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)
504 // TODO - Use GTK dialogues to display errors, not stderr, as we may not be attached to a terminal
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
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
533 std::string
ximage(PKGDATADIR
"/c");
534 ximage
.append(image
);
535 found
= image_exists(image
, ximage
);
539 std::string
ximage(PKGDATADIR
"/x");
540 ximage
.append(image
);
541 found
= image_exists(image
, ximage
);
546 std::cerr
<< "Cannot find image!" << std::endl
;
551 // Load the image from the supplied file path
552 pixbuf
= gdk_pixbuf_new_from_file(image
.c_str(), &gerr
);
555 std::cerr
<< "Cannot load image \"" << image
<< "\": " << gerr
->message
<< std::endl
;
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
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
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
);
610 // Set the window's SM client ID
612 gdk_set_sm_client_id(opts
.smcid
);
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
);
620 std::cerr
<< "Cannot load image: " << gerr
->message
<< std::endl
;
624 GdkCursor
* cursor
= gdk_cursor_new_from_pixbuf(gdk_display_get_default(),
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
634 gtk_window_set_keep_above(GTK_WINDOW(window
), true);
636 gtk_window_stick(GTK_WINDOW(window
));
638 gtk_widget_show_all(window
);
640 // Fork into background if desired
646 std::cerr
<< "Could not fork: " << strerror(errno
) << std::endl
;
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) \
656 if (errno == EINTR) \
658 std::cerr << msg << strerror(errno) << std::endl; \
663 __IGNORE_EINTR(close(0) != 0, "Could not close stdin: ");
664 // Redirect stdout & stderr to /dev/null
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
672 std::cerr
<< "Cannot create new session: " << strerror(errno
) << std::endl
;