1 /* logjam - a GTK client for LiveJournal.
2 * Copyright (C) 2000-2003 Evan Martin <evan@livejournal.com>
4 * vim: tabstop=4 shiftwidth=4 noexpandtab :
8 #include "liblj/livejournal.h" /* lj_md5_hash */
13 gchar
*gettext_translate_func (const gchar
*path
, gpointer data
) {
14 /* gettext returns a const, but gtk wants nonconst. *shrug*. */
15 return (gchar
*)gettext(path
);
19 GtkWidget
*jam_table_new (int rows
, int cols
) {
21 table
= gtk_table_new(rows
, cols
, FALSE
);
26 GtkWidget
*jam_table_label (GtkTable
*table
, int row
, const char *text
) {
27 GtkWidget
*label
= gtk_label_new_with_mnemonic(text
);
28 gtk_misc_set_alignment(GTK_MISC(label
), 0.0f
, 0.5f
);
29 gtk_table_attach(table
, GTK_WIDGET(label
), 0, 1, row
, row
+1, GTK_FILL
, GTK_FILL
, 6, 6);
34 void jam_table_content (GtkTable
*table
, int row
, GtkWidget
*content
) {
35 gtk_table_attach(table
, GTK_WIDGET(content
), 1, 2, row
, row
+1, GTK_EXPAND
|GTK_FILL
, 0, 0, 0);
39 void jam_table_label_content_mne (GtkTable
*table
, int row
, const char *text
, GtkWidget
*content
, GtkWidget
*mne
) {
41 label
= jam_table_label(table
, row
, text
);
42 jam_table_content(table
, row
, content
);
43 gtk_label_set_mnemonic_widget(GTK_LABEL(label
), mne
);
47 void jam_table_label_content (GtkTable
*table
, int row
, const char *text
, GtkWidget
*content
) {
48 jam_table_label_content_mne(table
, row
, text
, content
, content
);
52 void jam_table_fillrow (GtkTable
*table
, int row
, GtkWidget
*content
) {
53 gtk_table_attach(table
, GTK_WIDGET(content
), 0, 2, row
, row
+1, GTK_EXPAND
|GTK_FILL
, 0, 2, 2);
57 void jam_spin_button_set (GtkSpinButton
*w
, gboolean numeric
, gdouble min
, gdouble max
, gdouble step
, gdouble page
, gint digits
) {
58 g_assert(GTK_IS_SPIN_BUTTON(w
));
59 gtk_spin_button_set_numeric(w
, numeric
);
60 gtk_spin_button_set_range(w
, min
, max
);
61 gtk_spin_button_set_increments(w
, step
, page
);
62 gtk_spin_button_set_digits(w
, digits
);
66 void jam_win_set_size (GtkWindow
*win
, int width
, int height
) {
69 gtk_window_set_default_size(win
, width
, height
);
71 gtk_window_set_default_size(win
, width
, (int)(0.618 *width
));
73 } else if (height
> 0) {
74 gtk_window_set_default_size(win
, (int)(1.618 *height
), height
);
79 void jam_window_init (GtkWindow
*win
, GtkWindow
*parent
, const gchar
*title
, int width
, int height
) {
80 if (parent
) gtk_window_set_transient_for(GTK_WINDOW(win
), parent
);
81 if (title
) gtk_window_set_title(GTK_WINDOW(win
), title
);
82 jam_win_set_size(GTK_WINDOW(win
), width
, height
);
86 static void jam_dialog_set_contents_container (GtkDialog
*dlg
, GtkWidget
*container
) {
87 gtk_container_set_border_width(GTK_CONTAINER(container
), 12);
88 gtk_box_pack_start(GTK_BOX(dlg
->vbox
), container
, TRUE
, TRUE
, 0);
89 gtk_widget_show_all(dlg
->vbox
);
90 if (GTK_IS_NOTEBOOK(container
)) gtk_dialog_set_has_separator(dlg
, FALSE
);
94 GtkWidget
*jam_dialog_set_contents (GtkDialog
*dlg
, GtkWidget
*contents
) {
96 if (GTK_IS_CONTAINER(contents
)) {
97 jam_dialog_set_contents_container(dlg
, contents
);
100 vbox
= gtk_vbox_new(FALSE
, 5);
101 if (contents
) gtk_box_pack_start(GTK_BOX(vbox
), contents
, TRUE
, TRUE
, 0);
102 jam_dialog_set_contents_container(dlg
, vbox
);
107 GtkWidget
*jam_dialog_buttonbox_new (void) {
108 return gtk_hbox_new(FALSE
, 5);
112 void jam_dialog_buttonbox_add (GtkWidget
*box
, GtkWidget
*button
) {
113 gtk_box_pack_start(GTK_BOX(box
), button
, FALSE
, FALSE
, 0);
117 GtkWidget
*jam_dialog_buttonbox_button_with_label (GtkWidget
*box
, const char *label
) {
120 g_snprintf(buf
, sizeof(buf
), " %s ", label
);
121 button
= gtk_button_new_with_mnemonic(buf
);
122 jam_dialog_buttonbox_add(box
, button
);
127 GtkWidget
*jam_dialog_buttonbox_button_from_stock (GtkWidget
*box
, const char *id
) {
128 GtkWidget
*button
= gtk_button_new_from_stock(id
);
129 jam_dialog_buttonbox_add(box
, button
);
134 void jam_dialog_set_contents_buttonbox (GtkWidget
*dlg
, GtkWidget
*contents
, GtkWidget
*buttonbox
) {
135 GtkWidget
*vbox
, *hbox
;;
136 vbox
= jam_dialog_set_contents(GTK_DIALOG(dlg
), contents
);
137 hbox
= gtk_hbox_new(FALSE
, 0);
138 gtk_box_pack_end(GTK_BOX(hbox
), buttonbox
, FALSE
, FALSE
, 0);
139 gtk_box_pack_end(GTK_BOX(vbox
), hbox
, FALSE
, FALSE
, 0);
140 gtk_widget_show_all(vbox
);
144 int jam_confirm (GtkWindow
*parent
, const char *title
, const char *msg
) {
147 dlg
= gtk_message_dialog_new(GTK_WINDOW(parent
), 0, GTK_MESSAGE_QUESTION
, GTK_BUTTONS_YES_NO
, "%s", msg
);
148 jam_window_init(GTK_WINDOW(dlg
), parent
, title
, -1, -1);
149 res
= (gtk_dialog_run(GTK_DIALOG(dlg
)) == GTK_RESPONSE_YES
);
150 gtk_widget_destroy(dlg
);
155 void gdkcolor_to_hex (GdkColor
*color
, char *buf
) {
156 g_snprintf(buf
, 8, "#%02X%02X%02X", color
->red
>>8, color
->green
>>8, color
->blue
>>8);
160 GtkWidget
*scroll_wrap (GtkWidget
*w
) {
161 GtkWidget
*scroll
= gtk_scrolled_window_new(NULL
, NULL
);
162 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll
), GTK_POLICY_AUTOMATIC
, GTK_POLICY_AUTOMATIC
);
163 gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scroll
), GTK_SHADOW_IN
);
164 gtk_container_add(GTK_CONTAINER(scroll
), w
);
169 static void geometry_save (Geometry
*geom
, GtkWindow
*win
, GtkPaned
*paned
) {
170 if (win
&& GTK_WIDGET(win
)->window
) {
171 gtk_window_get_position(win
, &geom
->x
, &geom
->y
);
172 gtk_window_get_size(win
, &geom
->width
, &geom
->height
);
174 if (paned
) geom
->panedpos
= gtk_paned_get_position(paned
);
178 static void geometry_load (Geometry
*geom
, GtkWindow
*win
, GtkPaned
*paned
) {
179 if (win
&& geom
->width
> 0) {
180 gtk_window_move(win
, geom
->x
, geom
->y
);
181 gtk_window_set_default_size(win
, geom
->width
, geom
->height
);
183 if (paned
&& geom
->panedpos
> 0) gtk_paned_set_position(paned
, geom
->panedpos
);
187 static gboolean
geometry_win_event (GtkWindow
*win
, GdkEvent
*e
, Geometry
*geom
) {
188 geometry_save(geom
, win
, NULL
);
193 static void geometry_show_cb (GtkWindow
*win
, Geometry
*geom
) {
194 geometry_load(geom
, win
, NULL
);
198 static gboolean
geometry_paned_event (GtkPaned
*paned
, gpointer d
, Geometry
*geom
) {
199 geometry_save(geom
, NULL
, paned
);
204 void geometry_tie (GtkWidget
*win
, GeometryType g
) {
205 geometry_tie_full(g
, GTK_WINDOW(win
), NULL
);
209 void geometry_tie_full (GeometryType g
, GtkWindow
*win
, GtkPaned
*paned
) {
210 Geometry
*geom
= &conf
.geometries
[g
];
211 /* the reference point is at the top left corner of the window itself,
212 * ignoring window manager decorations. */
213 gtk_window_set_gravity(win
, GDK_GRAVITY_STATIC
);
214 /* load the existing geometry for this window */
215 geometry_load(geom
, win
, paned
);
216 /* and track the new geometry */
217 g_signal_connect(G_OBJECT(win
), "configure-event", G_CALLBACK(geometry_win_event
), geom
);
218 g_signal_connect(G_OBJECT(win
), "show", G_CALLBACK(geometry_show_cb
), geom
);
219 if (paned
) g_signal_connect(G_OBJECT(paned
), "notify::position", G_CALLBACK(geometry_paned_event
), geom
);
223 static void forget_cb (GObject
*obj
, gboolean
*state
) {
228 /* GLib unfortunately doesn't make up its mind over whether TRUE or
229 * FALSE should denote comparison success, so its provided g_str_equal
230 * is useless as a custom plugin to g_list_find_custom. */
231 static gboolean
_str_equal (gconstpointer v1
, gconstpointer v2
) {
232 return !g_str_equal(v1
, v2
);
236 void jam_message_va (GtkWindow
*parent
, MessageType type
, gboolean forgettable
, const char *title
, const char *message
, va_list ap
) {
238 GtkWidget
*forget_check
;
239 gint msgtype
= 0, buttontype
= 0;
240 const gchar
*mtitle
= NULL
;
241 gboolean forget_state
;
246 g_vsnprintf(fullmsg
, 1024, message
, ap
);
247 { /* compute hash of this message */
249 id
= g_string_new(title
);
250 g_string_append(id
, message
);
251 lj_md5_hash(id
->str
, ourhash
);
252 g_string_free(id
, TRUE
);
254 /* do nothing if the user has asked not to view this message again */
255 if (g_slist_find_custom(app
.quiet_dlgs
, ourhash
, _str_equal
)) return;
256 mtitle
= (const gchar
*)title
;
259 msgtype
= GTK_MESSAGE_INFO
;
260 buttontype
= GTK_BUTTONS_OK
;
262 case JAM_MSG_WARNING
:
263 msgtype
= GTK_MESSAGE_WARNING
;
264 buttontype
= GTK_BUTTONS_CLOSE
;
265 if (!title
) mtitle
= _("Warning");
268 msgtype
= GTK_MESSAGE_ERROR
;
269 buttontype
= GTK_BUTTONS_CLOSE
;
270 if (!title
) mtitle
= _("Error");
273 if (mtitle
== NULL
) mtitle
= _("WTF?!");
274 /* TODO: switch to jam_dialogs, which are prettier */
275 dlg
= gtk_message_dialog_new(parent
, 0, msgtype
, buttontype
, "%s", fullmsg
);
276 gtk_window_set_title(GTK_WINDOW(dlg
), mtitle
);
277 gtk_window_set_transient_for(GTK_WINDOW(dlg
), GTK_WINDOW(parent
));
279 forget_state
= FALSE
;
280 forget_check
= gtk_check_button_new_with_label(_("Do not show again"));
281 g_signal_connect(G_OBJECT(forget_check
), "toggled", G_CALLBACK(forget_cb
), &forget_state
);
282 gtk_box_set_homogeneous(GTK_BOX(GTK_DIALOG(dlg
)->action_area
), FALSE
); /* XXX: this doesn't work :( */
283 /* aggressively make this dialog less ugly */
284 gtk_button_box_set_layout(GTK_BUTTON_BOX(GTK_DIALOG(dlg
)->action_area
), GTK_BUTTONBOX_EDGE
);
285 gtk_box_set_child_packing(GTK_BOX(GTK_DIALOG(dlg
)->action_area
), ((GtkBoxChild
*)GTK_BOX(GTK_DIALOG(dlg
)->action_area
)->children
->data
)->widget
, FALSE
, FALSE
, 0, GTK_PACK_END
);
286 /* force our checkbox to *really* be first */
287 gtk_container_add(GTK_CONTAINER((GTK_DIALOG(dlg
)->action_area
)), forget_check
);
288 gtk_box_reorder_child(GTK_BOX((GTK_DIALOG(dlg
)->action_area
)), forget_check
, 0);
289 gtk_widget_show_all(GTK_DIALOG(dlg
)->action_area
);
291 gtk_dialog_run(GTK_DIALOG(dlg
));
292 /* flag this dialog for oblivion if the user didn't like it */
293 if (forgettable
&& forget_state
) app
.quiet_dlgs
= g_slist_append(app
.quiet_dlgs
, g_strdup(ourhash
));
294 gtk_widget_destroy(dlg
);
298 __attribute__((format(printf
,5,6))) void jam_message (GtkWindow
*parent
, MessageType type
, gboolean forgettable
, const char *title
, const char *message
, ...) {
300 va_start(ap
, message
);
301 jam_message_va(parent
, type
, forgettable
, title
, message
, ap
);
306 __attribute__((format(printf
,2,3))) void jam_warning (GtkWindow
*parent
, const char *message
, ...) {
308 va_start(ap
, message
);
309 jam_message_va(parent
, JAM_MSG_WARNING
, FALSE
, NULL
, message
, ap
);
314 /* utility thunk function */
315 void jam_messagebox (GtkWindow
*parent
, const char *title
, const char *message
) {
316 jam_message(parent
, JAM_MSG_INFO
, FALSE
, title
, "%s", message
);
320 /* text sort utility function for GtkTreeModels */
321 gint
text_sort_func (GtkTreeModel
*model
, GtkTreeIter
*a
, GtkTreeIter
*b
, gpointer data
) {
324 gtk_tree_model_get(model
, a
, 0, &ta
, -1);
325 gtk_tree_model_get(model
, b
, 0, &tb
, -1);
326 ret
= g_ascii_strcasecmp(ta
, tb
);
333 void jam_widget_set_visible (GtkWidget
*w
, gboolean visible
) {
334 if (visible
) gtk_widget_show(w
); else gtk_widget_hide(w
);
338 void jam_widget_set_font (GtkWidget
*w
, const gchar
*font_name
) {
339 PangoFontDescription
*font_desc
;
340 font_desc
= pango_font_description_from_string(font_name
);
341 gtk_widget_modify_font(w
, font_desc
);
342 pango_font_description_free(font_desc
);
346 GtkWidget
*labelled_box_new_all (const char *caption
, GtkWidget
*w
, gboolean expand
, GtkSizeGroup
*sg
, GtkWidget
*mne
) {
348 l
= gtk_label_new_with_mnemonic(caption
);
349 gtk_misc_set_alignment(GTK_MISC(l
), 0, 0.5);
350 if (sg
) gtk_size_group_add_widget(sg
, l
);
352 if (mne
) gtk_label_set_mnemonic_widget(GTK_LABEL(l
), mne
);
353 hbox
= gtk_hbox_new(FALSE
, 12);
354 gtk_box_pack_start(GTK_BOX(hbox
), l
, FALSE
, FALSE
, 0);
355 if (w
) gtk_box_pack_start(GTK_BOX(hbox
), w
, expand
, expand
, 0);
360 GtkWidget
*jam_form_label_new (const char *text
) {
362 label
= gtk_label_new(text
);
363 gtk_misc_set_alignment(GTK_MISC(label
), 0, 0.5);
364 gtk_label_set_selectable(GTK_LABEL(label
), TRUE
);
365 gtk_label_set_line_wrap(GTK_LABEL(label
), TRUE
);
370 static void selection_enable_cb (GtkTreeSelection
*sel
, GtkWidget
*w
) {
371 gtk_widget_set_sensitive(w
, gtk_tree_selection_get_selected(sel
, NULL
, NULL
));
375 static void jr_up_cb (JamReorderable
*jr
) {
376 GtkTreeSelection
*sel
;
378 GtkTreeIter iter
, i2
;
380 sel
= gtk_tree_view_get_selection(GTK_TREE_VIEW(jr
->view
));
381 if (!gtk_tree_selection_get_selected(sel
, &model
, &iter
)) return;
382 path
= gtk_tree_model_get_path(model
, &iter
);
383 if (gtk_tree_path_prev(path
)) {
384 gtk_tree_model_get_iter(model
, &i2
, path
);
385 gtk_list_store_swap(jr
->store
, &iter
, &i2
);
387 gtk_tree_path_free(path
);
391 static void jr_down_cb (JamReorderable
*jr
) {
392 GtkTreeSelection
*sel
;
393 GtkTreeIter iter
, i2
;
394 sel
= gtk_tree_view_get_selection(GTK_TREE_VIEW(jr
->view
));
395 if (!gtk_tree_selection_get_selected(sel
, NULL
, &iter
)) return;
397 if (gtk_tree_model_iter_next(GTK_TREE_MODEL(jr
->store
), &i2
)) gtk_list_store_swap(jr
->store
, &iter
, &i2
);
401 void jam_reorderable_make (JamReorderable
*jr
) {
402 GtkWidget
*hbox
, *bbox
;
404 GtkTreeSelection
*sel
;
406 jr
->view
= gtk_tree_view_new_with_model(GTK_TREE_MODEL(jr
->store
));
408 jr
->box
= hbox
= gtk_hbox_new(FALSE
, 6);
409 sel
= gtk_tree_view_get_selection(GTK_TREE_VIEW(jr
->view
));
410 gtk_box_pack_start(GTK_BOX(hbox
), scroll_wrap(jr
->view
), TRUE
, TRUE
, 0);
412 bbox
= gtk_vbutton_box_new();
413 gtk_button_box_set_layout(GTK_BUTTON_BOX(bbox
), GTK_BUTTONBOX_START
);
414 gtk_box_set_spacing(GTK_BOX(bbox
), 6);
416 jr
->add
= button
= gtk_button_new_from_stock(GTK_STOCK_ADD
);
417 gtk_box_pack_start(GTK_BOX(bbox
), button
, FALSE
, FALSE
, 0);
419 jr
->edit
= button
= gtk_button_new_from_stock(GTK_STOCK_PROPERTIES
);
420 g_signal_connect(G_OBJECT(sel
), "changed", G_CALLBACK(selection_enable_cb
), button
);
421 gtk_box_pack_start(GTK_BOX(bbox
), button
, FALSE
, FALSE
, 0);
423 jr
->remove
= button
= gtk_button_new_from_stock(GTK_STOCK_REMOVE
);
424 g_signal_connect(G_OBJECT(sel
), "changed", G_CALLBACK(selection_enable_cb
), button
);
425 gtk_box_pack_start(GTK_BOX(bbox
), button
, FALSE
, FALSE
, 0);
427 button
= gtk_button_new_from_stock(GTK_STOCK_GO_UP
);
428 g_signal_connect(G_OBJECT(sel
), "changed", G_CALLBACK(selection_enable_cb
), button
);
429 g_signal_connect_swapped(G_OBJECT(button
), "clicked", G_CALLBACK(jr_up_cb
), jr
);
430 gtk_box_pack_start(GTK_BOX(bbox
), button
, FALSE
, FALSE
, 0);
431 gtk_button_box_set_child_secondary(GTK_BUTTON_BOX(bbox
), button
, TRUE
);
433 button
= gtk_button_new_from_stock(GTK_STOCK_GO_DOWN
);
434 g_signal_connect(G_OBJECT(sel
), "changed", G_CALLBACK(selection_enable_cb
), button
);
435 g_signal_connect_swapped(G_OBJECT(button
), "clicked", G_CALLBACK(jr_down_cb
), jr
);
436 gtk_box_pack_start(GTK_BOX(bbox
), button
, FALSE
, FALSE
, 0);
437 gtk_button_box_set_child_secondary(GTK_BUTTON_BOX(bbox
), button
, TRUE
);
439 gtk_box_pack_start(GTK_BOX(hbox
), bbox
, FALSE
, FALSE
, 0);
441 g_signal_emit_by_name(G_OBJECT(sel
), "changed");
445 /* picked up from gtk/gtkclipboard.c, to allow blocking clipboard requests that time out */
452 static void clipboard_text_received_func (GtkClipboard
*clipboard
, const gchar
*text
, gpointer data
) {
453 WaitResults
*results
= data
;
454 results
->data
= g_strdup(text
);
455 g_main_loop_quit(results
->loop
);
459 /* adapted from gtk_clipboard_wait_for_text */
460 gchar
*jam_clipboard_wait_for_text_timeout (GtkClipboard
*clipboard
, gint timeout
) {
463 g_return_val_if_fail(clipboard
!= NULL
, NULL
);
464 g_return_val_if_fail(clipboard
!= NULL
, NULL
);
466 results
.loop
= g_main_loop_new(NULL
, TRUE
);
467 tag
= g_timeout_add(timeout
, (GSourceFunc
) g_main_loop_quit
, results
.loop
);
468 gtk_clipboard_request_text(clipboard
, clipboard_text_received_func
, &results
);
469 if (g_main_loop_is_running(results
.loop
)) {
471 g_main_loop_run(results
.loop
);
474 g_source_remove(tag
);
475 g_main_loop_unref(results
.loop
);