1 /*
2 * Crossfire -- cooperative multi-player graphical RPG and adventure game
3 *
4 * Copyright (c) 1999-2013 Mark Wedel and the Crossfire Development Team
5 * Copyright (c) 1992 Frank Tore Johansen
6 *
7 * Crossfire is free software and comes with ABSOLUTELY NO WARRANTY. You are
8 * welcome to redistribute it under certain conditions. For details, see the
9 * 'LICENSE' and 'COPYING' files.
10 *
11 * The authors can be reached via e-mail to crossfire-devel@real-time.com
12 */
13
14 /**
15 * @file
16 * Handles spell related functionality.
17 */
18
19 #include "client.h"
20
21 #include <assert.h>
22 #include <gtk/gtk.h>
23
24 #include "image.h"
25 #include "metaserver.h"
26 #include "main.h"
27 #include "gtk2proto.h"
28
29 enum Styles {
30 Style_Attuned, Style_Repelled, Style_Denied, Style_Normal, Style_Last
31 };
32
33 static GtkListStore *spell_store;
34 static GtkTreeSelection *spell_selection;
35 static GtkWidget *spell_window, *spell_invoke,
36 *spell_cast, *spell_options, *spell_treeview,
37 *spell_label[Style_Last], *spell_eventbox[Style_Last];
38
39 enum {
40 LIST_IMAGE, LIST_NAME, LIST_LEVEL, LIST_TIME, LIST_COST,
41 LIST_DAMAGE, LIST_SKILL, LIST_PATH, LIST_DESCRIPTION, LIST_BACKGROUND,
42 LIST_MAX_SP, LIST_TAG, LIST_FOREGROUND, LIST_FONT
43 };
44
45 static const char *Style_Names[Style_Last] = {
46 "spell_attuned", "spell_repelled", "spell_denied", "spell_normal"
47 }; /**< The names of theme file styles that are used in the spell dialog. */
48
49 static gpointer description_renderer = NULL; /**< The cell renderer for the
50 * spell dialog descriptions.
51 */
52 static GtkStyle *spell_styles[Style_Last]; /**< The actual styles loaded, or
53 * NULL if no styles were found.
54 */
55 static int has_init = 0; /**< Whether or not the spell
56 * dialog initialized since
57 * the client started up.
58 */
59 /**
60 * Gets the style information for the inventory windows. This is a separate
61 * function because if the user changes styles, it can be nice to re-load the
62 * configuration. The style for the inventory/look is a bit special. That is
63 * because with gtk, styles are widget wide - all rows in the widget would use
64 * the same style. We want to adjust the styles based on other attributes.
65 */
spell_get_styles(void)66 void spell_get_styles(void)
67 {
68 int i;
69 GtkStyle *tmp_style;
70 static int style_has_init=0;
71
72 for (i=0; i < Style_Last; i++) {
73 if (style_has_init && spell_styles[i]) {
74 g_object_unref(spell_styles[i]);
75 }
76 tmp_style =
77 gtk_rc_get_style_by_paths(
78 gtk_settings_get_default(), NULL, Style_Names[i], G_TYPE_NONE);
79 if (tmp_style) {
80 spell_styles[i] = g_object_ref(tmp_style);
81 } else {
82 LOG(LOG_INFO, "spells.c::spell_get_styles",
83 "Unable to find style for %s", Style_Names[i]);
84 spell_styles[i] = NULL;
85 }
86 }
87 style_has_init = 1;
88 }
89
90 /**
91 * Used if a user just single clicks on an entry - at which point, we enable
92 * the cast & invoke buttons.
93 *
94 * @param selection
95 * @param model
96 * @param path
97 * @param path_currently_selected
98 * @param userdata
99 */
spell_selection_func(GtkTreeSelection * selection,GtkTreeModel * model,GtkTreePath * path,gboolean path_currently_selected,gpointer userdata)100 static gboolean spell_selection_func(GtkTreeSelection *selection,
101 GtkTreeModel *model,
102 GtkTreePath *path,
103 gboolean path_currently_selected,
104 gpointer userdata)
105 {
106 gtk_widget_set_sensitive(spell_invoke, TRUE);
107 gtk_widget_set_sensitive(spell_cast, TRUE);
108 return TRUE;
109 }
110
111 /**
112 * Adjust the line wrap width used by the spells dialog Description column
113 * text renderer and force redraw of the rows to cause row height adjustment.
114 * To compute the new wrap width, the widths of all other columns are
115 * subtracted from the width of the spells window to determine the available
116 * width for the description column. The remaining space is then configured
117 * as the new wrap width. Once the new wrap is computed, mark all the rows
118 * changed so that the renderer adjusts the row height to expand or contract
119 * to fit the reformatted description.
120 *
121 * @param widget
122 * @param user_data
123 */
on_spell_window_size_allocate(GtkWidget * widget,gpointer user_data)124 void on_spell_window_size_allocate(GtkWidget *widget, gpointer user_data)
125 {
126 guint i;
127 guint width;
128 gboolean valid;
129 GtkTreeIter iter;
130 guint column_count;
131 GList *column_list;
132 GtkTreeViewColumn *column;
133
134 /* If the spell window has not been set up yet, do nothing. */
135 if (!has_init) {
136 return;
137 }
138 /*
139 * How wide is the spell window?
140 */
141 width = spell_treeview->allocation.width;
142 /*
143 * How many columns are in the spell window tree view?
144 */
145 column_list = gtk_tree_view_get_columns(GTK_TREE_VIEW(spell_treeview));
146 column_count = g_list_length(column_list);
147 /*
148 * Subtract the width of all but the last (Description) column from the
149 * total window width to figure out how much may be used for the final
150 * description column.
151 */
152 for (i = 0; i < column_count - 1; i += 1) {
153 column = g_list_nth_data(column_list, i);
154 width -= gtk_tree_view_column_get_width(column);
155 }
156 /*
157 * The column list allocated by gtk_tree_view_get_columns must be freed
158 * when it is no longer needed.
159 */
160 g_list_free(column_list);
161 /*
162 * Update the global variable used to configure the wrap-width for the
163 * spell dialog description column, then apply it to the cell renderer.
164 */
165 g_object_set(G_OBJECT(description_renderer), "wrap-width", width, NULL);
166 /*
167 * Traverse the spell store, and mark each row as changed. Get the first
168 * row, mark it, and then process the rest of the rows (if there are any).
169 * This re-flows the spell descriptions to the new wrap-width, and adjusts
170 * the height of each row as needed to optimize the vertical space used.
171 */
172 valid = gtk_tree_model_get_iter_first(GTK_TREE_MODEL(spell_store), &iter);
173 while (valid) {
174 GtkTreePath *tree_path;
175
176 tree_path =
177 gtk_tree_model_get_path(GTK_TREE_MODEL(spell_store), &iter);
178 gtk_tree_model_row_changed(
179 GTK_TREE_MODEL(spell_store), tree_path, &iter);
180 gtk_tree_path_free(tree_path);
181 valid =
182 gtk_tree_model_iter_next(GTK_TREE_MODEL(spell_store), &iter);
183 }
184 }
185
186 /**
187 * When spell information updates, the treeview is cleared and re-populated.
188 * The clear/re-populate is easier than "editing" the contents.
189 */
update_spell_information(void)190 void update_spell_information(void)
191 {
192 int i;
193 Spell *spell;
194 GtkTreeIter iter;
195 char buf[MAX_BUF];
196 GtkStyle *row_style;
197 GdkColor *foreground=NULL;
198 GdkColor *background=NULL;
199 PangoFontDescription *font=NULL;
200
201 /* If the window/spellstore hasn't been created, return. */
202 if (!has_init) {
203 return;
204 }
205
206 cpl.spells_updated = 0;
207
208 /* We could try to do this in spell_get_styles, but if the window isn't
209 * active, it won't work. This is called whenever the window is made
210 * active, so we know it will work, and the time to set this info here,
211 * even though it may not change often, is pretty trivial.
212 */
213 for (i=0; i < Style_Last; i++) {
214 if (spell_styles[i]) {
215 gtk_widget_modify_fg(spell_label[i],
216 GTK_STATE_NORMAL, &spell_styles[i]->text[GTK_STATE_NORMAL]);
217 gtk_widget_modify_font(spell_label[i], spell_styles[i]->font_desc);
218 gtk_widget_modify_bg(spell_eventbox[i],
219 GTK_STATE_NORMAL, &spell_styles[i]->base[GTK_STATE_NORMAL]);
220 } else {
221 gtk_widget_modify_fg(spell_label[i],GTK_STATE_NORMAL, NULL);
222 gtk_widget_modify_font(spell_label[i], NULL);
223 gtk_widget_modify_bg(spell_eventbox[i],GTK_STATE_NORMAL, NULL);
224 }
225 }
226
227 gtk_list_store_clear(spell_store);
228 for (spell = cpl.spelldata; spell; spell=spell->next) {
229 gtk_list_store_append(spell_store, &iter);
230
231 buf[0] = 0;
232 if (spell->sp) {
233 snprintf(buf, sizeof(buf), "%d Mana ", spell->sp);
234 }
235 if (spell->grace)
236 snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf),
237 "%d Grace", spell->grace);
238
239 if (spell->path & cpl.stats.denied) {
240 row_style = spell_styles[Style_Denied];
241 } else if (spell->path & cpl.stats.repelled) {
242 row_style = spell_styles[Style_Repelled];
243 } else if (spell->path & cpl.stats.attuned) {
244 row_style = spell_styles[Style_Attuned];
245 } else {
246 row_style = spell_styles[Style_Normal];
247 }
248
249 if (row_style) {
250 foreground = &row_style->text[GTK_STATE_NORMAL];
251 background = &row_style->base[GTK_STATE_NORMAL];
252 font = row_style->font_desc;
253 } else {
254 foreground=NULL;
255 background=NULL;
256 font=NULL;
257 }
258
259 gtk_list_store_set(
260 spell_store, &iter,
261 LIST_NAME, spell->name,
262 LIST_LEVEL, spell->level,
263 LIST_COST, buf,
264 LIST_DAMAGE, spell->dam,
265 LIST_SKILL, spell->skill,
266 LIST_DESCRIPTION, spell->message,
267 LIST_BACKGROUND, background,
268 LIST_FOREGROUND, foreground,
269 LIST_FONT, font,
270 LIST_MAX_SP, (spell->sp > spell->grace) ? spell->sp : spell->grace,
271 LIST_TAG, spell->tag,
272 -1
273 );
274 }
275 }
276
277 /**
278 *
279 * @param menuitem
280 * @param user_data
281 */
on_spells_activate(GtkMenuItem * menuitem,gpointer user_data)282 void on_spells_activate(GtkMenuItem *menuitem, gpointer user_data) {
283 GtkWidget *widget;
284
285 if (!has_init) {
286 GtkCellRenderer *renderer;
287 GtkTreeViewColumn *column;
288
289 spell_window = GTK_WIDGET(gtk_builder_get_object(dialog_xml, "spell_window"));
290
291 spell_invoke = GTK_WIDGET(gtk_builder_get_object(dialog_xml, "spell_invoke"));
292 spell_cast = GTK_WIDGET(gtk_builder_get_object(dialog_xml, "spell_cast"));
293 spell_options = GTK_WIDGET(gtk_builder_get_object(dialog_xml, "spell_options"));
294 spell_treeview = GTK_WIDGET(gtk_builder_get_object(dialog_xml, "spell_treeview"));
295
296 g_signal_connect((gpointer) spell_window, "size-allocate",
297 G_CALLBACK(on_spell_window_size_allocate), NULL);
298 g_signal_connect((gpointer) spell_window, "delete-event",
299 G_CALLBACK(gtk_widget_hide_on_delete), NULL);
300 g_signal_connect((gpointer) spell_treeview, "row_activated",
301 G_CALLBACK(on_spell_treeview_row_activated), NULL);
302 g_signal_connect((gpointer) spell_cast, "clicked",
303 G_CALLBACK(on_spell_cast_clicked), NULL);
304 g_signal_connect((gpointer) spell_invoke, "clicked",
305 G_CALLBACK(on_spell_invoke_clicked), NULL);
306
307 widget = GTK_WIDGET(gtk_builder_get_object(dialog_xml, "spell_close"));
308 g_signal_connect((gpointer) widget, "clicked",
309 G_CALLBACK(on_spell_close_clicked), NULL);
310
311 spell_store =
312 gtk_list_store_new(
313 14,
314 G_TYPE_OBJECT, /* Image - not used */
315 G_TYPE_STRING, /* Name */
316 G_TYPE_INT, /* Level */
317 G_TYPE_INT, /* Time */
318 G_TYPE_STRING, /* SP/Grace */
319 G_TYPE_INT, /* Damage */
320 G_TYPE_STRING, /* Skill name */
321 G_TYPE_INT, /* Spell path */
322 G_TYPE_STRING, /* Description */
323 GDK_TYPE_COLOR, /* Background color of the entry */
324 G_TYPE_INT,
325 G_TYPE_INT,
326 GDK_TYPE_COLOR,
327 PANGO_TYPE_FONT_DESCRIPTION
328 );
329
330 gtk_tree_view_set_model(
331 GTK_TREE_VIEW(spell_treeview), GTK_TREE_MODEL(spell_store));
332 gtk_tree_view_set_rules_hint(GTK_TREE_VIEW(spell_treeview), TRUE);
333
334 /* Note: it is intentional we don't show (render) some fields:
335 * image - we don't have images right now it seems.
336 * time - not sure if it worth the space.
337 * spell path - done by color
338 *
339 * Note: Cell alignment is set to top right instead of the default,
340 * to improve readability when descriptions wrap to multiple lines.
341 */
342 renderer = gtk_cell_renderer_text_new();
343 renderer->xalign = 0;
344 renderer->yalign = 0;
345 column = gtk_tree_view_column_new_with_attributes(
346 "Spell", renderer, "text", LIST_NAME, NULL);
347 gtk_tree_view_append_column(GTK_TREE_VIEW(spell_treeview), column);
348 gtk_tree_view_column_set_sort_column_id(column, LIST_NAME);
349 gtk_tree_view_column_add_attribute(
350 column, renderer, "background-gdk", LIST_BACKGROUND);
351 gtk_tree_view_column_add_attribute(
352 column, renderer, "foreground-gdk", LIST_FOREGROUND);
353 gtk_tree_view_column_add_attribute(
354 column, renderer, "font-desc", LIST_FONT);
355
356 renderer = gtk_cell_renderer_text_new();
357 renderer->xalign = 0.4;
358 renderer->yalign = 0;
359 column = gtk_tree_view_column_new_with_attributes(
360 "Level", renderer, "text", LIST_LEVEL, NULL);
361 gtk_tree_view_append_column(GTK_TREE_VIEW(spell_treeview), column);
362 gtk_tree_view_column_set_sort_column_id(column, LIST_LEVEL);
363 gtk_tree_view_column_add_attribute(
364 column, renderer, "background-gdk", LIST_BACKGROUND);
365 gtk_tree_view_column_add_attribute(
366 column, renderer, "foreground-gdk", LIST_FOREGROUND);
367 gtk_tree_view_column_add_attribute(
368 column, renderer, "font-desc", LIST_FONT);
369
370 renderer = gtk_cell_renderer_text_new();
371 renderer->xalign = 0.4;
372 renderer->yalign = 0;
373 column = gtk_tree_view_column_new_with_attributes(
374 "Cost/Cast", renderer, "text", LIST_COST, NULL);
375 gtk_tree_view_append_column(GTK_TREE_VIEW(spell_treeview), column);
376
377 /* Since this is a string column, it would do a string sort. Instead,
378 * we set up a int column and tie this column to sort on that.
379 */
380 gtk_tree_view_column_set_sort_column_id(column, LIST_MAX_SP);
381 gtk_tree_view_column_add_attribute(
382 column, renderer, "background-gdk", LIST_BACKGROUND);
383 gtk_tree_view_column_add_attribute(
384 column, renderer, "foreground-gdk", LIST_FOREGROUND);
385 gtk_tree_view_column_add_attribute(
386 column, renderer, "font-desc", LIST_FONT);
387
388 renderer = gtk_cell_renderer_text_new();
389 renderer->xalign = 0.4;
390 renderer->yalign = 0;
391 column = gtk_tree_view_column_new_with_attributes(
392 "Damage", renderer, "text", LIST_DAMAGE, NULL);
393 gtk_tree_view_append_column(GTK_TREE_VIEW(spell_treeview), column);
394 gtk_tree_view_column_set_sort_column_id(column, LIST_DAMAGE);
395 gtk_tree_view_column_add_attribute(
396 column, renderer, "background-gdk", LIST_BACKGROUND);
397 gtk_tree_view_column_add_attribute(
398 column, renderer, "foreground-gdk", LIST_FOREGROUND);
399 gtk_tree_view_column_add_attribute(
400 column, renderer, "font-desc", LIST_FONT);
401
402 column = gtk_tree_view_column_new_with_attributes(
403 "Skill", renderer, "text", LIST_SKILL, NULL);
404 gtk_tree_view_append_column(GTK_TREE_VIEW(spell_treeview), column);
405 gtk_tree_view_column_set_sort_column_id(column, LIST_SKILL);
406 gtk_tree_view_column_add_attribute(
407 column, renderer, "background-gdk", LIST_BACKGROUND);
408 gtk_tree_view_column_add_attribute(
409 column, renderer, "foreground-gdk", LIST_FOREGROUND);
410 gtk_tree_view_column_add_attribute(
411 column, renderer, "font-desc", LIST_FONT);
412
413 renderer = gtk_cell_renderer_text_new();
414 renderer->xalign = 0;
415 renderer->yalign = 0;
416 column = gtk_tree_view_column_new_with_attributes(
417 "Description", renderer, "text", LIST_DESCRIPTION, NULL);
418 gtk_tree_view_append_column(GTK_TREE_VIEW(spell_treeview), column);
419 gtk_tree_view_column_add_attribute(
420 column, renderer, "background-gdk", LIST_BACKGROUND);
421 gtk_tree_view_column_add_attribute(
422 column, renderer, "foreground-gdk", LIST_FOREGROUND);
423 gtk_tree_view_column_add_attribute(
424 column, renderer, "font-desc", LIST_FONT);
425 /*
426 * Set up the description column so it wraps lengthy descriptions over
427 * multiple lines and at word boundaries. A default wrap-width is
428 * applied to constrain the column width to a reasonable value. The
429 * actual value used here is somewhat unimportant since a corrected
430 * width is computed and applied later, but, it does approximate the
431 * column size that is appropriate for the dialog's default width.
432 */
433 g_object_set(G_OBJECT(renderer),
434 "wrap-width", 300, "wrap-mode", PANGO_WRAP_WORD, NULL);
435 /*
436 * Preserve the description text cell renderer pointer to facilitate
437 * setting the wrap-width relative to the dialog's size and content.
438 */
439 description_renderer = renderer;
440
441 spell_selection =
442 gtk_tree_view_get_selection(GTK_TREE_VIEW(spell_treeview));
443 gtk_tree_selection_set_mode(spell_selection, GTK_SELECTION_BROWSE);
444 gtk_tree_selection_set_select_function(
445 spell_selection, spell_selection_func, NULL, NULL);
446
447 gtk_tree_sortable_set_sort_column_id(
448 GTK_TREE_SORTABLE(spell_store), LIST_NAME, GTK_SORT_ASCENDING);
449
450 /* The style code will set the colors for these */
451 spell_label[Style_Attuned] =
452 GTK_WIDGET(gtk_builder_get_object(dialog_xml, "spell_label_attuned"));
453 spell_label[Style_Repelled] =
454 GTK_WIDGET(gtk_builder_get_object(dialog_xml, "spell_label_repelled"));
455 spell_label[Style_Denied] =
456 GTK_WIDGET(gtk_builder_get_object(dialog_xml, "spell_label_denied"));
457 spell_label[Style_Normal] =
458 GTK_WIDGET(gtk_builder_get_object(dialog_xml, "spell_label_normal"));
459
460 /* We use eventboxes because the label widget is a transparent widget.
461 * We can't set the background in it and have it work. But we can set
462 * the background in the event box, and put the label widget in the
463 * eventbox.
464 */
465 spell_eventbox[Style_Attuned] =
466 GTK_WIDGET(gtk_builder_get_object(dialog_xml, "spell_eventbox_attuned"));
467 spell_eventbox[Style_Repelled] =
468 GTK_WIDGET(gtk_builder_get_object(dialog_xml, "spell_eventbox_repelled"));
469 spell_eventbox[Style_Denied] =
470 GTK_WIDGET(gtk_builder_get_object(dialog_xml, "spell_eventbox_denied"));
471 spell_eventbox[Style_Normal] =
472 GTK_WIDGET(gtk_builder_get_object(dialog_xml, "spell_eventbox_normal"));
473 }
474 gtk_widget_set_sensitive(spell_invoke, FALSE);
475 gtk_widget_set_sensitive(spell_cast, FALSE);
476 gtk_widget_show(spell_window);
477 spell_get_styles();
478
479 has_init = 1;
480
481 /* Must be called after has_init is set to 1 */
482 update_spell_information();
483 }
484
485 /**
486 *
487 * @param treeview
488 * @param path
489 * @param column
490 * @param user_data
491 */
on_spell_treeview_row_activated(GtkTreeView * treeview,GtkTreePath * path,GtkTreeViewColumn * column,gpointer user_data)492 void on_spell_treeview_row_activated(GtkTreeView *treeview,
493 GtkTreePath *path,
494 GtkTreeViewColumn *column,
495 gpointer user_data)
496 {
497 on_spell_cast_clicked(NULL, NULL);
498 }
499
500 /**
501 *
502 * @param button
503 * @param user_data
504 */
on_spell_cast_clicked(GtkButton * button,gpointer user_data)505 void on_spell_cast_clicked(GtkButton *button, gpointer user_data)
506 {
507 int tag;
508 char command[MAX_BUF];
509 const char *options = NULL;
510 GtkTreeIter iter;
511 GtkTreeModel *model;
512
513 options = gtk_entry_get_text(GTK_ENTRY(spell_options));
514
515 if (gtk_tree_selection_get_selected(spell_selection, &model, &iter)) {
516 gtk_tree_model_get(model, &iter, LIST_TAG, &tag, -1);
517
518 if (!tag) {
519 LOG(LOG_ERROR, "spells.c::on_spell_cast_clicked",
520 "Unable to get spell tag\n");
521 return;
522 }
523 snprintf(command, MAX_BUF-1, "cast %d %s", tag, options);
524 send_command(command, -1, 1);
525 }
526 }
527
528 /**
529 *
530 * @param button
531 * @param user_data
532 */
on_spell_invoke_clicked(GtkButton * button,gpointer user_data)533 void on_spell_invoke_clicked(GtkButton *button, gpointer user_data)
534 {
535 int tag;
536 char command[MAX_BUF];
537 const char *options=NULL;
538 GtkTreeIter iter;
539 GtkTreeModel *model;
540
541 options = gtk_entry_get_text(GTK_ENTRY(spell_options));
542
543 if (gtk_tree_selection_get_selected(spell_selection, &model, &iter)) {
544 gtk_tree_model_get(model, &iter, LIST_TAG, &tag, -1);
545
546 if (!tag) {
547 LOG(LOG_ERROR, "spells.c::on_spell_invoke_clicked",
548 "Unable to get spell tag\n");
549 return;
550 }
551 snprintf(command, MAX_BUF-1, "invoke %d %s", tag, options);
552 send_command(command, -1, 1);
553 }
554 }
555
556 /**
557 *
558 * @param button
559 * @param user_data
560 */
on_spell_close_clicked(GtkButton * button,gpointer user_data)561 void on_spell_close_clicked(GtkButton *button, gpointer user_data)
562 {
563 gtk_widget_hide(spell_window);
564 }
565
566