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