chiark / gitweb /
desensitive queue context menu options when disconnected
[disorder] / disobedience / properties.c
CommitLineData
460b9539 1/*
2 * This file is part of DisOrder.
3 * Copyright (C) 2006 Richard Kettlewell
4 *
5 * This program 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 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful, but
11 * WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 * General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
18 * USA
19 */
20
21#include "disobedience.h"
22
23/* Track properties -------------------------------------------------------- */
24
25struct prefdata;
26
27static void kickoff_namepart(struct prefdata *f);
28static void completed_namepart(struct prefdata *f);
29static const char *get_edited_namepart(struct prefdata *f);
30static void set_namepart(struct prefdata *f, const char *value);
31static void set_namepart_completed(void *v);
32
33static void kickoff_string(struct prefdata *f);
34static void completed_string(struct prefdata *f);
35static const char *get_edited_string(struct prefdata *f);
36static void set_string(struct prefdata *f, const char *value);
37
38static void kickoff_boolean(struct prefdata *f);
39static void completed_boolean(struct prefdata *f);
40static const char *get_edited_boolean(struct prefdata *f);
41static void set_boolean(struct prefdata *f, const char *value);
42
43static void prefdata_completed(void *v, const char *value);
44static void prefdata_onerror(struct callbackdata *cbd,
45 int code,
46 const char *msg);
47static struct callbackdata *make_callbackdata(struct prefdata *f);
48static void prefdata_completed_common(struct prefdata *f,
49 const char *value);
50
51static void properties_ok(GtkButton *button, gpointer userdata);
52static void properties_apply(GtkButton *button, gpointer userdata);
53static void properties_cancel(GtkButton *button, gpointer userdata);
54
13aba192 55/** @brief Data for a single preference */
460b9539 56struct prefdata {
57 const char *track;
58 int row;
13aba192 59 const struct pref *p; /**< @brief kind of preference */
60 const char *value; /**< @brief value from server */
460b9539 61 GtkWidget *widget;
62};
63
64/* The type of a preference is the collection of callbacks needed to get,
65 * display and set it */
66struct preftype {
67 void (*kickoff)(struct prefdata *f);
68 /* Kick off the request to fetch the pref from the server. */
69
70 void (*completed)(struct prefdata *f);
71 /* Called when the value comes back in; creates the widget. */
72
73 const char *(*get_edited)(struct prefdata *f);
74 /* Get the edited value from the widget. */
75
76 void (*set)(struct prefdata *f, const char *value);
77 /* Set the new value and (if necessary) arrange for our display to update. */
78};
79
80/* A namepart pref */
81static const struct preftype preftype_namepart = {
82 kickoff_namepart,
83 completed_namepart,
84 get_edited_namepart,
85 set_namepart
86};
87
88/* A string pref */
89static const struct preftype preftype_string = {
90 kickoff_string,
91 completed_string,
92 get_edited_string,
93 set_string
94};
95
96/* A boolean pref */
97static const struct preftype preftype_boolean = {
98 kickoff_boolean,
99 completed_boolean,
100 get_edited_boolean,
101 set_boolean
102};
103
13aba192 104/* @brief The known prefs for each track */
460b9539 105static const struct pref {
13aba192 106 const char *label; /**< @brief user-level description */
107 const char *part; /**< @brief protocol-level tag */
108 const char *default_value; /**< @brief default value or NULL */
109 const struct preftype *type; /**< @brief underlying data type */
460b9539 110} prefs[] = {
111 { "Artist", "artist", 0, &preftype_namepart },
112 { "Album", "album", 0, &preftype_namepart },
113 { "Title", "title", 0, &preftype_namepart },
114 { "Tags", "tags", "", &preftype_string },
115 { "Random", "pick_at_random", "1", &preftype_boolean },
116};
117
118#define NPREFS (int)(sizeof prefs / sizeof *prefs)
119
120/* Buttons that appear at the bottom of the window */
121static const struct button {
122 const gchar *stock;
123 void (*clicked)(GtkButton *button, gpointer userdata);
124} buttons[] = {
125 { GTK_STOCK_OK, properties_ok },
126 { GTK_STOCK_APPLY, properties_apply },
127 { GTK_STOCK_CANCEL, properties_cancel },
128};
129
130#define NBUTTONS (int)(sizeof buttons / sizeof *buttons)
131
132static int prefs_unfilled; /* Prefs remaining to get */
133static int prefs_total; /* Total prefs */
134static struct prefdata *prefdatas; /* Current prefdatas */
135static GtkWidget *properties_window;
136static GtkWidget *properties_table;
137static GtkWidget *progress_window, *progress_bar;
138
139void properties(int ntracks, char **tracks) {
140 int n, m;
141 struct prefdata *f;
142 GtkWidget *hbox, *vbox, *button, *label, *entry;
143
144 /* If there is a properties window open then just bring it to the
145 * front. It might not have the right values in... */
146 if(properties_window) {
147 if(!prefs_unfilled)
148 gtk_window_present(GTK_WINDOW(properties_window));
149 return;
150 }
151 assert(properties_table == 0);
152 if(ntracks > INT_MAX / NPREFS) {
153 popup_error("Too many tracks selected");
154 return;
155 }
156 /* Create a new properties window */
157 properties_window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
158 g_signal_connect(properties_window, "destroy",
159 G_CALLBACK(gtk_widget_destroyed), &properties_window);
160 /* Most of the action is the table of preferences */
161 properties_table = gtk_table_new((NPREFS + 1) * ntracks, 2, FALSE);
162 g_signal_connect(properties_table, "destroy",
163 G_CALLBACK(gtk_widget_destroyed), &properties_table);
164 gtk_window_set_title(GTK_WINDOW(properties_window), "Track Properties");
165 /* Create labels for each pref of each track and kick off requests to the
166 * server to fill in the values */
167 prefs_total = NPREFS * ntracks;
168 prefdatas = xcalloc(prefs_total, sizeof *prefdatas);
169 for(n = 0; n < ntracks; ++n) {
170 label = gtk_label_new("Track");
171 gtk_misc_set_alignment(GTK_MISC(label), 1, 0);
172 gtk_table_attach(GTK_TABLE(properties_table),
173 label,
174 0, 1,
175 (NPREFS + 1) * n, (NPREFS + 1) * n + 1,
176 GTK_FILL, 0,
177 1, 1);
178 entry = gtk_entry_new();
179 gtk_entry_set_text(GTK_ENTRY(entry), tracks[n]);
180 gtk_editable_set_editable(GTK_EDITABLE(entry), FALSE);
181 gtk_table_attach(GTK_TABLE(properties_table),
182 entry,
183 1, 2,
184 (NPREFS + 1) * n, (NPREFS + 1) * n + 1,
185 GTK_EXPAND|GTK_FILL, 0,
186 1, 1);
187 for(m = 0; m < NPREFS; ++m) {
188 label = gtk_label_new(prefs[m].label);
189 gtk_misc_set_alignment(GTK_MISC(label), 1, 0);
190 gtk_table_attach(GTK_TABLE(properties_table),
191 label,
192 0, 1,
193 (NPREFS + 1) * n + 1 + m, (NPREFS + 1) * n + 2 + m,
194 GTK_FILL/*xoptions*/, 0/*yoptions*/,
195 1, 1);
196 f = &prefdatas[NPREFS * n + m];
197 f->track = tracks[n];
198 f->row = (NPREFS + 1) * n + 1 + m;
199 f->p = &prefs[m];
200 prefs[m].type->kickoff(f);
201 }
202 }
203 prefs_unfilled = prefs_total;
204 /* Buttons */
205 hbox = gtk_hbox_new(FALSE, 1);
206 for(n = 0; n < NBUTTONS; ++n) {
207 button = gtk_button_new_from_stock(buttons[n].stock);
208 g_signal_connect(G_OBJECT(button), "clicked",
209 G_CALLBACK(buttons[n].clicked), 0);
210 gtk_box_pack_start(GTK_BOX(hbox), button, FALSE, FALSE, 1);
211 }
212 /* Put it all together */
213 vbox = gtk_vbox_new(FALSE, 1);
214 gtk_box_pack_start(GTK_BOX(vbox),
215 scroll_widget(properties_table,
216 "properties"),
217 TRUE, TRUE, 1);
218 gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 1);
219 gtk_container_add(GTK_CONTAINER(properties_window), vbox);
220 /* The table only really wants to be vertically scrollable */
221 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(GTK_WIDGET(properties_table)->parent->parent),
222 GTK_POLICY_NEVER,
223 GTK_POLICY_AUTOMATIC);
224 /* Pop up a progress bar while we're waiting */
225 progress_window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
226 g_signal_connect(progress_window, "destroy",
227 G_CALLBACK(gtk_widget_destroyed), &progress_window);
228 gtk_window_set_default_size(GTK_WINDOW(progress_window), 360, -1);
229 gtk_window_set_title(GTK_WINDOW(progress_window),
230 "Fetching Track Properties");
231 progress_bar = gtk_progress_bar_new();
232 gtk_container_add(GTK_CONTAINER(progress_window), progress_bar);
233 gtk_widget_show_all(progress_window);
234}
235
236/* Everything is filled in now */
237static void prefdata_alldone(void) {
238 if(progress_window)
239 gtk_widget_destroy(progress_window);
240 /* Default size may be too small */
241 gtk_window_set_default_size(GTK_WINDOW(properties_window), 480, 512);
242 /* TODO: relate default size to required size more closely */
243 gtk_widget_show_all(properties_window);
244}
245
246/* Namepart preferences ---------------------------------------------------- */
247
248static void kickoff_namepart(struct prefdata *f) {
13aba192 249 /* We ask for the display name part. This is a bit bizarre if what we really
250 * wanted was the underlying preference, but in fact it should always match
251 * and will supply a sane default without having to know how to parse tracks
252 * names (which implies knowing collection roots). */
253 disorder_eclient_namepart(client, prefdata_completed, f->track, "display", f->p->part,
254 make_callbackdata(f));
460b9539 255}
256
257static void completed_namepart(struct prefdata *f) {
13aba192 258 if(!f->value) {
259 /* No setting */
260 f->value = "";
261 }
460b9539 262 f->widget = gtk_entry_new();
263 gtk_entry_set_text(GTK_ENTRY(f->widget), f->value);
264}
265
266static const char *get_edited_namepart(struct prefdata *f) {
267 return gtk_entry_get_text(GTK_ENTRY(f->widget));
268}
269
270static void set_namepart(struct prefdata *f, const char *value) {
271 char *s;
272 struct callbackdata *cbd = xmalloc(sizeof *cbd);
273
274 cbd->u.f = f;
275 byte_xasprintf(&s, "trackname_display_%s", f->p->part);
13aba192 276 /* We don't know what the default is so can never unset. This is a bug
277 * relative to the original design, which is supposed to only ever allow for
278 * non-trivial namepart preferences. I suppose the server could spot a
279 * default being set and translate it into an unset. */
280 disorder_eclient_set(client, set_namepart_completed, f->track, s, value,
281 cbd);
460b9539 282}
283
284/* Called when we've set a namepart */
285static void set_namepart_completed(void *v) {
286 struct callbackdata *cbd = v;
287 struct prefdata *f = cbd->u.f;
288
289 namepart_update(f->track, "display", f->p->part);
290}
291
292/* String preferences ------------------------------------------------------ */
293
294static void kickoff_string(struct prefdata *f) {
295 disorder_eclient_get(client, prefdata_completed, f->track, f->p->part,
296 make_callbackdata(f));
297}
298
299static void completed_string(struct prefdata *f) {
300 if(!f->value)
301 /* No setting, use the default value instead */
302 f->value = f->p->default_value;
303 f->widget = gtk_entry_new();
304 gtk_entry_set_text(GTK_ENTRY(f->widget), f->value);
305}
306
307static const char *get_edited_string(struct prefdata *f) {
308 return gtk_entry_get_text(GTK_ENTRY(f->widget));
309}
310
311static void set_string(struct prefdata *f, const char *value) {
312 if(strcmp(f->p->default_value, value))
313 /* Different from default, set it */
314 disorder_eclient_set(client, 0/*completed*/, f->track, f->p->part,
315 value, 0/*v*/);
316 else
317 /* Same as default, just unset */
318 disorder_eclient_unset(client, 0/*completed*/, f->track, f->p->part,
319 0/*v*/);
320}
321
322/* Boolean preferences ----------------------------------------------------- */
323
324static void kickoff_boolean(struct prefdata *f) {
325 disorder_eclient_get(client, prefdata_completed, f->track, f->p->part,
326 make_callbackdata(f));
327}
328
329static void completed_boolean(struct prefdata *f) {
330 f->widget = gtk_check_button_new();
331 if(!f->value)
332 /* Not set, use the default */
333 f->value = f->p->default_value;
334 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(f->widget),
335 strcmp(f->value, "0"));
336}
337
338static const char *get_edited_boolean(struct prefdata *f) {
339 return (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(f->widget))
340 ? "1" : "0");
341}
342
343static void set_boolean(struct prefdata *f, const char *value) {
344 char *s;
345
346 byte_xasprintf(&s, "trackname_display_%s", f->p->part);
347 if(strcmp(value, f->p->default_value))
348 disorder_eclient_set(client, 0/*completed*/, f->track, f->p->part, value,
349 0/*v*/);
350 else
351 /* If default value then delete the pref */
352 disorder_eclient_unset(client, 0/*completed*/, f->track, f->p->part,
353 0/*v*/);
354}
355
356/* Querying preferences ---------------------------------------------------- */
357
358/* Make a suitable callbackdata */
359static struct callbackdata *make_callbackdata(struct prefdata *f) {
360 struct callbackdata *cbd = xmalloc(sizeof *cbd);
361
362 cbd->onerror = prefdata_onerror;
363 cbd->u.f = f;
364 return cbd;
365}
366
367/* No pref was set */
368static void prefdata_onerror(struct callbackdata *cbd,
369 int attribute((unused)) code,
370 const char attribute((unused)) *msg) {
371 prefdata_completed_common(cbd->u.f, 0);
372}
373
374/* Got the value of a pref */
375static void prefdata_completed(void *v, const char *value) {
376 struct callbackdata *cbd = v;
377
378 prefdata_completed_common(cbd->u.f, value);
379}
380
381static void prefdata_completed_common(struct prefdata *f,
382 const char *value) {
383 f->value = value;
384 f->p->type->completed(f);
385 assert(f->value != 0); /* Had better set a default */
386 gtk_table_attach(GTK_TABLE(properties_table), f->widget,
387 1, 2,
388 f->row, f->row + 1,
389 GTK_EXPAND|GTK_FILL/*xoptions*/, 0/*yoptions*/,
390 1, 1);
391 --prefs_unfilled;
392 if(prefs_total && progress_window)
393 gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(progress_bar),
394 1.0 - (double)prefs_unfilled / prefs_total);
395 if(!prefs_unfilled)
396 prefdata_alldone();
397}
398
399/* Button callbacks -------------------------------------------------------- */
400
401static void properties_ok(GtkButton *button,
402 gpointer userdata) {
403 properties_apply(button, userdata);
404 properties_cancel(button, userdata);
405}
406
407static void properties_apply(GtkButton attribute((unused)) *button,
408 gpointer attribute((unused)) userdata) {
409 int n;
410 const char *edited;
411 struct prefdata *f;
412
413 /* For each possible property we see if we've changed it and if so tell the
414 * server */
415 for(n = 0; n < prefs_total; ++n) {
416 f = &prefdatas[n];
417 edited = f->p->type->get_edited(f);
418 if(strcmp(edited, f->value)) {
419 /* The value has changed */
420 f->p->type->set(f, edited);
421 f->value = xstrdup(edited);
422 }
423 }
424}
425
426static void properties_cancel(GtkButton attribute((unused)) *button,
427 gpointer attribute((unused)) userdata) {
428 gtk_widget_destroy(properties_window);
429}
430
431/*
432Local Variables:
433c-basic-offset:2
434comment-column:40
435fill-column:79
436indent-tabs-mode:nil
437End:
438*/