1 // Copyright 2008 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>
58 // GTK/GDK returned pointer
61 // Image to display and its dimensions
65 // Whether or not the key binding for quit is disabled
69 // Options structure - needed so that sm_save_yourself can know about the
70 // values of various program options which are usually only in main()'s scope
87 // Re-paint the window when necessary
88 gboolean
handle_expose(GtkWidget
* widget
, GdkEventExpose
* event
)
90 cairo_t
* cairo
= gdk_cairo_create(widget
->window
);
93 cairo_set_source_rgba (cairo
, 1.0f
, 1.0f
, 1.0f
, 0.0f
);
94 cairo_set_operator (cairo
, CAIRO_OPERATOR_SOURCE
);
97 // Set cairo's source pattern from the desired image
98 gdk_cairo_set_source_pixbuf(cairo
, pixbuf
, 0.0f
, 0.0f
);
101 cairo_rectangle(cairo
, 0.0f
, 0.0f
,
102 static_cast<double>(width
), static_cast<double>(height
));
105 cairo_destroy(cairo
);
110 // Handle mouse click events
111 gboolean
window_clicked(GtkWidget
* widget
, GdkEventButton
* event
)
113 if (event
->type
== GDK_BUTTON_PRESS
)
114 // Move the window with the mouse
115 gtk_window_begin_move_drag(GTK_WINDOW(window
),
116 event
->button
, static_cast<gint
>(event
->x_root
),
117 static_cast<gint
>(event
->y_root
), event
->time
);
121 // Set up an RGBA colourmap for the window
122 void handle_screen(GtkWidget
* window
, GdkScreen
* old
, gpointer data
)
124 GdkScreen
* s
= gtk_widget_get_screen(window
);
125 GdkColormap
* c
= gdk_screen_get_rgba_colormap(s
);
128 // Use standard RGB colourmap if we must. Window will have
129 // an ugly black border if this is the case.
130 c
= gdk_screen_get_rgb_colormap(s
);
132 gtk_widget_set_colormap(window
, c
);
135 // Handle key press events
136 void handle_key_press(GtkWidget
*Window
, GdkEventKey
*theEvent
)
138 switch( theEvent
->keyval
)
147 // Ignore all other keys
153 // Function for seeing whether or not a particular image file exists.
154 // If we find a file, copy the filename into image and return true, otherwise return false.
155 bool image_exists(std::string
& image
, const std::string
& find_image
)
158 // Set of image extensions to try appending when searching for an image
159 static const char* extensions
[16];
160 extensions
[0] = ".png"; extensions
[1] = ".gif"; extensions
[2] = ".jpg";
161 extensions
[3] = ".jpeg"; extensions
[4] = ".pnm"; extensions
[5] = ".xpm";
162 extensions
[6] = ".tif"; extensions
[7] = ".tiff";
163 extensions
[8] = ".PNG"; extensions
[9] = ".GIF"; extensions
[10] = ".JPG";
164 extensions
[11] = ".JPEG"; extensions
[12] = ".PNM"; extensions
[13] = ".XPM";
165 extensions
[14] = ".TIF"; extensions
[15] = ".TIFF";
168 // Try unadulterated filename, then extensions in lower & upper case
169 for (size_t i
= 0; i
< 16; ++i
)
171 std::string
ximage(find_image
);
172 if (i
> 0) ximage
.append(extensions
[i
- 1]);
173 if (stat(ximage
.c_str(), &s
) == 0)
183 // Session management "SaveYourself" callback
184 void sm_save_yourself(SmcConn conn
, SmPointer data
, int type
, Bool shutdown
, int istyle
, Bool fast
)
186 // Set the required properties for session management
187 smopts
* opts
= static_cast<smopts
*>(data
);
191 progname
.name
= SmProgram
;
192 progname
.type
= SmARRAY8
;
193 progname
.num_vals
= 1;
194 SmPropValue progname_value
;
195 progname_value
.length
= strlen(opts
->progname
);
196 progname_value
.value
= opts
->progname
;
197 progname
.vals
= &progname_value
;
204 SmPropValue uid_value
;
205 const char* c_uidstr
= g_get_user_name();
206 char* uidstr
= new char[strlen(c_uidstr
) + 1];
207 strcpy(uidstr
, c_uidstr
);
208 uid_value
.length
= strlen(c_uidstr
);
209 uid_value
.value
= uidstr
;
210 uid
.vals
= &uid_value
;
212 // Current working directory
214 cwd
.name
= SmCurrentDirectory
;
217 SmPropValue cwd_value
;
218 const char* c_cwdstr
= g_get_current_dir();
219 char* cwdstr
= new char[strlen(c_cwdstr
) + 1];
220 strcpy(cwdstr
, c_cwdstr
);
221 cwd_value
.length
= strlen(c_cwdstr
);
222 cwd_value
.value
= cwdstr
;
223 cwd
.vals
= &cwd_value
;
225 // Clone & restart commands
226 SmPropValue command
[6];
228 command
[numvals
++] = progname_value
;
229 SmProp restartcommand
, clonecommand
;
230 char sv
[] = "-stick";
231 char fv
[] = "-float";
232 char qv
[] = "-noquit";
236 SmPropValue stickprop_value
;
237 stickprop_value
.value
= sv
;
238 stickprop_value
.length
= 6;
239 command
[numvals
++] = stickprop_value
;
244 SmPropValue floatprop_value
;
245 floatprop_value
.value
= fv
;
246 floatprop_value
.length
= 6;
247 command
[numvals
++] = floatprop_value
;
252 SmPropValue noquitprop_value
;
253 noquitprop_value
.value
= qv
;
254 noquitprop_value
.length
= 7;
255 command
[numvals
++] = noquitprop_value
;
258 SmPropValue image_value
;
259 char* imagestr
= new char[strlen(opts
->image
) + 3];
260 imagestr
[0] = '-'; imagestr
[1] = 'F';
261 strcpy(imagestr
+ 2, opts
->image
);
262 image_value
.value
= imagestr
;
263 image_value
.length
= strlen(imagestr
);
264 command
[numvals
++] = image_value
;
266 SmPropValue smcid_value
;
267 std::string
c_smcidarg("-smcid");
268 c_smcidarg
.append(opts
->smcid
);
269 char* smcidarg
= new char[c_smcidarg
.length() + 1];
270 strcpy(smcidarg
, c_smcidarg
.c_str());
271 smcid_value
.value
= smcidarg
;
272 smcid_value
.length
= c_smcidarg
.length();
273 command
[numvals
++] = smcid_value
;
275 restartcommand
.num_vals
= numvals
;
276 clonecommand
.num_vals
= numvals
- 1;
277 restartcommand
.vals
= command
;
278 clonecommand
.vals
= command
;
280 restartcommand
.name
= SmRestartCommand
;
281 restartcommand
.type
= SmLISTofARRAY8
;
282 clonecommand
.name
= SmCloneCommand
;
283 clonecommand
.type
= SmLISTofARRAY8
;
286 props
[0] = &progname
;
289 props
[3] = &restartcommand
;
290 props
[4] = &clonecommand
;
291 SmcSetProperties(conn
, 5, props
);
293 // Delete any non-const things we had to explicitly allocate
299 // Send SaveYourselfDone.
300 SmcSaveYourselfDone(conn
, True
);
303 // Session management "Save complete" callback
304 void sm_save_complete(SmcConn conn
, SmPointer data
)
308 // Session management "Die" callback
309 void sm_die(SmcConn conn
, SmPointer data
)
311 // TODO - Have GTK quit event handler disconnect from SM if in use
312 SmcCloseConnection(conn
, 0, NULL
);
316 // Pump ICE messages when data comes in
317 gboolean
sm_pump_ice(GIOChannel
* chan
, GIOCondition cond
, gpointer data
)
319 IceProcessMessages(static_cast<IceConn
>(data
), NULL
, NULL
);
323 // Called whenever an ICE connection is opened or closed
324 void ice_connection_watch(IceConn conn
, IcePointer data
, Bool opening
, IcePointer
* wdata
)
328 // When we get a new ICE connection, install a IO channel which will pump
329 // ICE messages when data is waiting
330 int icefd
= IceConnectionNumber(conn
);
331 GIOChannel
* ioc
= g_io_channel_unix_new(icefd
);
332 g_io_add_watch(ioc
, G_IO_IN
, sm_pump_ice
, conn
);
335 // When an ICE connection closes, close the IO channel
336 g_io_channel_unref(static_cast<GIOChannel
*>(*wdata
));
342 int main (int argc
, char* argv
[])
344 gtk_init(&argc
, &argv
);
346 // Whether or not to set the window's keep-on-top hint
349 // Whether or not the window's sticky hint should be set
352 // Whether or not to fork into the background
353 bool background
= true;
356 // Whether or not to use session management
359 // Old session client ID, if being restored from a saved session
360 char* oldsmcid
= NULL
;
364 SmcCallbacks smcallbacks
;
365 smcallbacks
.save_yourself
.callback
= sm_save_yourself
;
366 smcallbacks
.save_yourself
.client_data
= &opts
;
367 smcallbacks
.die
.callback
= sm_die
;
368 smcallbacks
.die
.client_data
= NULL
;
369 smcallbacks
.save_complete
.callback
= sm_save_complete
;
370 smcallbacks
.save_complete
.client_data
= NULL
;
373 // Mimic the image loading behaviour of xteddy, to an extent.
375 // Start from the name of the executable itself
376 std::string
image(argv
[0]);
378 char* smimage
= argv
[0];
381 // Take just the last component of the path
382 std::string::size_type s
= image
.find_last_of('/');
383 if (s
!= std::string::npos
)
384 image
= image
.substr(s
+ 1);
387 // XXX This code is really quick and nasty
388 for (int i
= 1; i
< argc
; ++i
)
390 // Allow image specification with -F<file>
391 if (strncmp(argv
[i
], "-F", 2) == 0)
395 smimage
= argv
[i
] + 2;
398 // Allow disablement of "q" keybinding
399 else if (strncmp(argv
[i
], "-noquit", 7) == 0)
402 else if (strncmp(argv
[i
], "-float", 6) == 0)
404 // Allow sticking to all desktops
405 else if (strncmp(argv
[i
], "-stick", 6) == 0)
407 // Allow disablement of backgrounding
408 // Implies -nosm (see below)
409 else if (strncmp(argv
[i
], "-nobg", 5) == 0)
412 // Allow disablement of session management
413 else if (strncmp(argv
[i
], "-nosm", 5) == 0)
415 // Read in old session client ID
416 else if (strncmp(argv
[i
], "-smcid", 6) == 0)
417 oldsmcid
= argv
[i
] + 6;
420 std::cerr
<< "Unrecognised option: " << argv
[i
] << std::endl
;
424 // If we're being restarted by the session manager, don't bother
425 // backgrounding, as we aren't attached to a terminal anyway
426 if (sm
&& oldsmcid
!= NULL
)
429 // On the other hand, don't use session management if not forking
430 // into the background, if being started directly by the user
431 // rather than restarted as part of session.
432 // XXX This is how -nobg imples -nosm.
433 else if (!background
&& oldsmcid
== NULL
)
438 // Open & monitor a GIOChannel each time there is a new ICE connection
439 IceAddConnectionWatch(ice_connection_watch
, NULL
);
441 // Fill in SM opts so sm_save_yourself can set properties
442 opts
.sticky
= sticky
;
444 opts
.noquit
= noquit
;
445 opts
.progname
= argv
[0];
446 opts
.image
= smimage
;
448 // Try to connect to the session manager, displaying an error if we can't
450 memset(smerr
, '\0', 512);
451 if (SmcOpenConnection(NULL
, NULL
, SmProtoMajor
, SmProtoMinor
,
452 SmcSaveYourselfProcMask
| SmcDieProcMask
| SmcSaveCompleteProcMask
,
453 &smcallbacks
, oldsmcid
, &(opts
.smcid
), 512, smerr
) == NULL
)
455 if (strlen(smerr
) == 0)
456 std::cerr
<< "Cannot connect to session manager; unknown error." << std::endl
;
458 std::cerr
<< "Cannot connect to session manager: " << smerr
<< std::endl
;
460 // Continue anyway, just without SM (for convenience of anyone without a session manager)
465 // TODO - Use GTK dialogues to display errors, not stderr, as we may not be attached to a terminal
468 // If the given path contains slashes, assume it is absolute or
469 // relative to the current working directory and use it verbatim.
470 // Otherwise, search for it.
471 if (image
.find_last_of('/') == std::string::npos
)
473 // Search for the image
475 std::string
searchimage(PKGDATADIR
"/");
476 searchimage
.append(image
);
477 found
= image_exists(image
, searchimage
);
478 // Try converting a leading x to a c and vice versa
479 if (!found
&& image
.at(0) == 'x')
481 std::string
ximage(PKGDATADIR
"/c");
482 ximage
.append(image
.substr(1));
483 found
= image_exists(image
, ximage
);
485 if (!found
&& image
.at(0) == 'c')
487 std::string
ximage(PKGDATADIR
"/x");
488 ximage
.append(image
.substr(1));
489 found
= image_exists(image
, ximage
);
491 // Try prepending c or x
494 std::string
ximage(PKGDATADIR
"/c");
495 ximage
.append(image
);
496 found
= image_exists(image
, ximage
);
500 std::string
ximage(PKGDATADIR
"/x");
501 ximage
.append(image
);
502 found
= image_exists(image
, ximage
);
507 std::cerr
<< "Cannot find image!" << std::endl
;
512 // Load the image from the supplied file path
513 pixbuf
= gdk_pixbuf_new_from_file(image
.c_str(), &gerr
);
516 std::cerr
<< "Cannot load image \"" << image
<< "\": " << gerr
->message
<< std::endl
;
523 window
= gtk_window_new(GTK_WINDOW_TOPLEVEL
);
525 // Top-level windows don't normally respond to mouse button events directly.
526 // We want this one to, though.
527 gtk_widget_add_events(window
, GDK_BUTTON_PRESS_MASK
);
529 // Set up event callbacks
530 g_signal_connect(G_OBJECT(window
), "expose-event", G_CALLBACK(handle_expose
), NULL
);
531 g_signal_connect(G_OBJECT(window
), "destroy", G_CALLBACK(gtk_main_quit
), NULL
);
532 g_signal_connect(G_OBJECT(window
), "button-press-event", G_CALLBACK(window_clicked
), NULL
);
533 g_signal_connect(G_OBJECT(window
), "screen-changed", G_CALLBACK(handle_screen
), NULL
);
534 g_signal_connect(G_OBJECT(window
), "key-press-event", G_CALLBACK(handle_key_press
), NULL
);
537 // Grap pixmap and alpha-thresholded bitmap from original pixmap
540 gdk_pixbuf_render_pixmap_and_mask(pixbuf
, &pixmap
, &bitmap
, 128);
542 // Set window size to image size
543 gdk_drawable_get_size(pixmap
, &width
, &height
);
544 gtk_widget_set_size_request(window
, width
, height
);
546 // If we have an alpha channel on the image, set the window's input shape,
547 // clearing it completely first
550 gtk_widget_input_shape_combine_mask(window
, NULL
, 0, 0);
551 gtk_widget_input_shape_combine_mask(window
, bitmap
, 0, 0);
554 g_object_unref(G_OBJECT(pixmap
));
555 g_object_unref(G_OBJECT(bitmap
));
558 // Set up various other properties we're interested in
559 gtk_window_set_title(GTK_WINDOW(window
), "cteddy");
560 gtk_window_set_resizable(GTK_WINDOW(window
), false);
561 gtk_window_set_decorated(GTK_WINDOW(window
), false);
562 gtk_widget_set_app_paintable(window
, true);
564 // Trigger initial attempt to switch to an RGBA colourmap
565 handle_screen(window
, NULL
, NULL
);
567 // Have to do this before we can use window->window as a GdkWindow*
568 gtk_widget_realize(window
);
571 // Set the window's SM client ID
573 gdk_set_sm_client_id(opts
.smcid
);
577 // Load in the heart-shaped cursor image and use it for our window's cursor
578 GdkPixbuf
* heart
= gdk_pixbuf_new_from_file(PKGDATADIR
"/heart.png", &gerr
);
581 std::cerr
<< "Cannot load image: " << gerr
->message
<< std::endl
;
585 GdkCursor
* cursor
= gdk_cursor_new_from_pixbuf(gdk_display_get_default(),
587 gdk_window_set_cursor(window
->window
, cursor
);
590 // Clear the window's default background
591 gdk_window_set_back_pixmap(window
->window
, NULL
, false);
593 // Honour float & sticky options
595 gtk_window_set_keep_above(GTK_WINDOW(window
), true);
597 gtk_window_stick(GTK_WINDOW(window
));
599 gtk_widget_show_all(window
);
601 // Fork into background if desired
607 std::cerr
<< "Could not fork: " << strerror(errno
) << std::endl
;
612 // Macro for performing a particular system call,
613 // trying again if it returns EINTR, displaying an error otherwise
614 #define __IGNORE_EINTR(fun, msg) \
617 if (errno == EINTR) \
619 std::cerr << msg << strerror(errno) << std::endl; \
624 __IGNORE_EINTR(close(0) != 0, "Could not close stdin: ");
625 // Redirect stdout & stderr to /dev/null
627 __IGNORE_EINTR((nullfd
= open("/dev/null", O_WRONLY
)) < 0, "Cannot open /dev/null: ");
628 __IGNORE_EINTR(dup2(nullfd
, 1) < 0, "Cannot redirect stdout to /dev/null: ");
629 __IGNORE_EINTR(dup2(nullfd
, 2) < 0, "Cannot redirect stderr to /dev/null: ");
630 // Create new process session
633 std::cerr
<< "Cannot create new session: " << strerror(errno
) << std::endl
;