1 /***********************************************************************
2  Freeciv - Copyright (C) 1996 - A Kjeldberg, L Gregersen, P Unold
3    This program is free software; you can redistribute it and/or modify
4    it under the terms of the GNU General Public License as published by
5    the Free Software Foundation; either version 2, or (at your option)
6    any later version.
7 
8    This program is distributed in the hope that it will be useful,
9    but WITHOUT ANY WARRANTY; without even the implied warranty of
10    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11    GNU General Public License for more details.
12 ***********************************************************************/
13 
14 #ifdef HAVE_CONFIG_H
15 #include <fc_config.h>
16 #endif
17 
18 #include <stdio.h>
19 #include <stdlib.h>
20 #include <string.h>
21 
22 #include <gtk/gtk.h>
23 #include <gdk/gdkkeysyms.h>
24 
25 /* utility */
26 #include "astring.h"
27 #include "fcintl.h"
28 #include "log.h"
29 #include "support.h"
30 
31 /* common */
32 #include "game.h"
33 #include "map.h"
34 #include "packets.h"
35 #include "player.h"
36 #include "unit.h"
37 #include "unitlist.h"
38 
39 /* client */
40 #include "client_main.h"
41 #include "control.h"
42 #include "goto.h"
43 #include "options.h"
44 #include "text.h"
45 
46 /* clien/gui-gtk-3.22 */
47 #include "plrdlg.h"
48 #include "dialogs.h"
49 #include "gui_main.h"
50 #include "gui_stuff.h"
51 #include "mapview.h"
52 
53 #include "gotodlg.h"
54 
55 
56 static GtkWidget *dshell = NULL;
57 static GtkWidget *view;
58 static GtkWidget *source;
59 static GtkWidget *all_toggle;
60 static GtkListStore *goto_list_store;
61 static GtkTreeSelection *goto_list_selection;
62 struct tile *original_tile;
63 static bool gotodlg_updating = FALSE;
64 
65 static void update_goto_dialog(GtkToggleButton *button);
66 static void update_source_label(void);
67 static void refresh_airlift_column(void);
68 static void refresh_airlift_button(void);
69 static void goto_selection_callback(GtkTreeSelection *selection, gpointer data);
70 
71 static struct city *get_selected_city(void);
72 
73 enum {
74   CMD_AIRLIFT = 1, CMD_GOTO
75 };
76 
77 enum {
78   GD_COL_CITY_ID = 0,   /* Not shown if not compiled with --enable-debug. */
79   GD_COL_CITY_NAME,
80   GD_COL_FLAG,
81   GD_COL_NATION,
82   GD_COL_AIRLIFT,
83 
84   GD_COL_NUM
85 };
86 
87 /**************************************************************************
88   User has responded to goto dialog
89 **************************************************************************/
goto_cmd_callback(GtkWidget * dlg,gint arg)90 static void goto_cmd_callback(GtkWidget *dlg, gint arg)
91 {
92   switch (arg) {
93   case GTK_RESPONSE_CANCEL:
94     center_tile_mapcanvas(original_tile);
95     break;
96 
97   case CMD_AIRLIFT:
98     {
99       struct city *pdestcity = get_selected_city();
100 
101       if (pdestcity) {
102         unit_list_iterate(get_units_in_focus(), punit) {
103           if (unit_can_airlift_to(punit, pdestcity)) {
104             request_unit_airlift(punit, pdestcity);
105           }
106         } unit_list_iterate_end;
107       }
108     }
109     break;
110 
111   case CMD_GOTO:
112     {
113       struct city *pdestcity = get_selected_city();
114 
115       if (pdestcity) {
116 	unit_list_iterate(get_units_in_focus(), punit) {
117           send_goto_tile(punit, pdestcity->tile);
118         } unit_list_iterate_end;
119       }
120     }
121     break;
122 
123   default:
124     break;
125   }
126 
127   gtk_widget_destroy(dlg);
128   dshell = NULL;
129 }
130 
131 
132 /**************************************************************************
133   Create goto -dialog for gotoing or airlifting unit
134 **************************************************************************/
create_goto_dialog(void)135 static void create_goto_dialog(void)
136 {
137   GtkWidget *sw, *label, *frame, *vbox;
138   GtkCellRenderer *rend;
139   GtkTreeViewColumn *col;
140 
141   dshell = gtk_dialog_new_with_buttons(_("Goto/Airlift Unit"),
142                                        NULL,
143                                        0,
144                                        _("_Cancel"),
145                                        GTK_RESPONSE_CANCEL,
146                                        _("Air_lift"),
147                                        CMD_AIRLIFT,
148                                        _("_Goto"),
149                                        CMD_GOTO,
150                                        NULL);
151   setup_dialog(dshell, toplevel);
152   gtk_window_set_position(GTK_WINDOW(dshell), GTK_WIN_POS_MOUSE);
153   gtk_dialog_set_default_response(GTK_DIALOG(dshell), CMD_GOTO);
154   g_signal_connect(dshell, "destroy",
155 		   G_CALLBACK(gtk_widget_destroyed), &dshell);
156   g_signal_connect(dshell, "response",
157                    G_CALLBACK(goto_cmd_callback), NULL);
158 
159   source = gtk_label_new("" /* filled in later */);
160   gtk_label_set_line_wrap(GTK_LABEL(source), TRUE);
161   gtk_label_set_justify(GTK_LABEL(source), GTK_JUSTIFY_CENTER);
162   gtk_box_pack_start(GTK_BOX(gtk_dialog_get_content_area(GTK_DIALOG(dshell))),
163         source, FALSE, FALSE, 0);
164 
165   label = g_object_new(GTK_TYPE_LABEL,
166     "use-underline", TRUE,
167     "label", _("Select destination ci_ty"),
168     "xalign", 0.0,
169     "yalign", 0.5,
170     NULL);
171   frame = gtk_frame_new("");
172   gtk_frame_set_label_widget(GTK_FRAME(frame), label);
173   gtk_box_pack_start(GTK_BOX(gtk_dialog_get_content_area(GTK_DIALOG(dshell))),
174         frame, TRUE, TRUE, 0);
175 
176   vbox = gtk_grid_new();
177   gtk_orientable_set_orientation(GTK_ORIENTABLE(vbox),
178                                  GTK_ORIENTATION_VERTICAL);
179   gtk_grid_set_row_spacing(GTK_GRID(vbox), 6);
180   gtk_container_add(GTK_CONTAINER(frame), vbox);
181 
182   goto_list_store = gtk_list_store_new(GD_COL_NUM, G_TYPE_INT, G_TYPE_STRING,
183                                        GDK_TYPE_PIXBUF, G_TYPE_STRING, G_TYPE_STRING);
184   gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(goto_list_store),
185                                        GD_COL_CITY_NAME, GTK_SORT_ASCENDING);
186 
187   view = gtk_tree_view_new_with_model(GTK_TREE_MODEL(goto_list_store));
188   gtk_widget_set_hexpand(view, TRUE);
189   gtk_widget_set_vexpand(view, TRUE);
190   g_object_unref(goto_list_store);
191   goto_list_selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(view));
192   gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(view), TRUE);
193   gtk_tree_view_set_search_column(GTK_TREE_VIEW(view), GD_COL_CITY_NAME);
194   gtk_tree_view_set_enable_search(GTK_TREE_VIEW(view), TRUE);
195 
196   /* Set the mnemonic in the frame label to focus the city list */
197   gtk_label_set_mnemonic_widget(GTK_LABEL(label), view);
198 
199 #ifdef DEBUG
200   rend = gtk_cell_renderer_text_new();
201   col = gtk_tree_view_column_new_with_attributes(_("Id"), rend,
202     "text", GD_COL_CITY_ID, NULL);
203   gtk_tree_view_append_column(GTK_TREE_VIEW(view), col);
204   gtk_tree_view_column_set_sort_column_id(col, GD_COL_CITY_ID);
205 #endif /* DEBUG */
206 
207   rend = gtk_cell_renderer_text_new();
208   col = gtk_tree_view_column_new_with_attributes(_("City"), rend,
209     "text", GD_COL_CITY_NAME, NULL);
210   gtk_tree_view_append_column(GTK_TREE_VIEW(view), col);
211   gtk_tree_view_column_set_sort_column_id(col, GD_COL_CITY_NAME);
212 
213   rend = gtk_cell_renderer_pixbuf_new();
214   col = gtk_tree_view_column_new_with_attributes(NULL, rend,
215     "pixbuf", GD_COL_FLAG, NULL);
216   gtk_tree_view_append_column(GTK_TREE_VIEW(view), col);
217 
218   rend = gtk_cell_renderer_text_new();
219   col = gtk_tree_view_column_new_with_attributes(_("Nation"), rend,
220     "text", GD_COL_NATION, NULL);
221   gtk_tree_view_append_column(GTK_TREE_VIEW(view), col);
222   gtk_tree_view_column_set_sort_column_id(col, GD_COL_NATION);
223 
224   rend = gtk_cell_renderer_text_new();
225   col = gtk_tree_view_column_new_with_attributes(_("Airlift"), rend,
226     "text", GD_COL_AIRLIFT, NULL);
227   gtk_tree_view_append_column(GTK_TREE_VIEW(view), col);
228   gtk_tree_view_column_set_sort_column_id(col, GD_COL_AIRLIFT);
229 
230   sw = gtk_scrolled_window_new(NULL, NULL);
231   gtk_container_add(GTK_CONTAINER(sw), view);
232   gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(sw),
233     GTK_POLICY_NEVER, GTK_POLICY_ALWAYS);
234   gtk_scrolled_window_set_min_content_height(GTK_SCROLLED_WINDOW(sw), 200);
235 
236   gtk_container_add(GTK_CONTAINER(vbox), sw);
237 
238   all_toggle = gtk_check_button_new_with_mnemonic(_("Show _All Cities"));
239   gtk_container_add(GTK_CONTAINER(vbox), all_toggle);
240 
241   g_signal_connect(all_toggle, "toggled", G_CALLBACK(update_goto_dialog), NULL);
242 
243   g_signal_connect(goto_list_selection, "changed",
244     G_CALLBACK(goto_selection_callback), NULL);
245 
246   gtk_widget_show_all(dshell);
247 
248   original_tile = get_center_tile_mapcanvas();
249 
250   update_source_label();
251   update_goto_dialog(GTK_TOGGLE_BUTTON(all_toggle));
252   gtk_tree_view_focus(GTK_TREE_VIEW(view));
253 }
254 
255 /****************************************************************
256 popup the dialog
257 *****************************************************************/
popup_goto_dialog(void)258 void popup_goto_dialog(void)
259 {
260   if (!can_client_issue_orders() || get_num_units_in_focus() == 0) {
261     return;
262   }
263 
264   if (!dshell) {
265     create_goto_dialog();
266   }
267 
268   gtk_window_present(GTK_WINDOW(dshell));
269 }
270 
271 /**************************************************************************
272   Return currently selected city
273 **************************************************************************/
get_selected_city(void)274 static struct city *get_selected_city(void)
275 {
276   GtkTreeModel *model;
277   GtkTreeIter it;
278   int city_id;
279 
280   if (!gtk_tree_selection_get_selected(goto_list_selection, NULL, &it)) {
281     return NULL;
282   }
283 
284   model = gtk_tree_view_get_model(GTK_TREE_VIEW(view));
285 
286   gtk_tree_model_get(model, &it, GD_COL_CITY_ID, &city_id, -1);
287 
288   return game_city_by_number(city_id);
289 }
290 
291 /**************************************************************************
292   Appends the list of the city owned by the player in the goto dialog.
293 **************************************************************************/
list_store_append_player_cities(GtkListStore * store,const struct player * pplayer)294 static bool list_store_append_player_cities(GtkListStore *store,
295                                             const struct player *pplayer)
296 {
297   GtkTreeIter it;
298   struct nation_type *pnation = nation_of_player(pplayer);
299   const char *nation = nation_adjective_translation(pnation);
300   GdkPixbuf *pixbuf;
301 
302   if (city_list_size(pplayer->cities) == 0) {
303     return FALSE;
304   }
305 
306   pixbuf = get_flag(pnation);
307 
308   city_list_iterate(pplayer->cities, pcity) {
309     gtk_list_store_append(store, &it);
310     gtk_list_store_set(store, &it,
311                        GD_COL_CITY_ID, pcity->id,
312                        GD_COL_CITY_NAME, city_name_get(pcity),
313                        GD_COL_FLAG, pixbuf,
314                        GD_COL_NATION, nation,
315                        /* GD_COL_AIRLIFT is populated later */
316                        -1);
317   } city_list_iterate_end;
318   g_object_unref(pixbuf);
319 
320   return TRUE;
321 }
322 
323 /**************************************************************************
324   Refresh the label that shows where the selected unit(s) currently are
325   (and the relevant cities' airlift capacities, if relevant).
326 **************************************************************************/
update_source_label(void)327 static void update_source_label(void)
328 {
329   /* Arbitrary limit to stop the label getting ridiculously long */
330   static const int max_cities = 10;
331   struct {
332     const struct city *city;
333     struct unit_list *units;
334   } cities[max_cities];
335   int ncities = 0;
336   bool too_many = FALSE;
337   bool no_city = FALSE; /* any units not in a city? */
338   struct astring strs[max_cities];
339   int nstrs;
340   char *last_str;
341   const char *descriptions[max_cities+1];
342   int i;
343 
344   /* Sanity check: if no units selected, give up */
345   if (unit_list_size(get_units_in_focus()) == 0) {
346     gtk_label_set_text(GTK_LABEL(source), _("No units selected."));
347     return;
348   }
349 
350   /* Divide selected units up into a list of unique cities */
351   unit_list_iterate(get_units_in_focus(), punit) {
352     const struct city *pcity = tile_city(unit_tile(punit));
353     if (pcity) {
354       /* Inefficient, but it's not a long list */
355       for (i = 0; i < ncities; i++) {
356         if (cities[i].city == pcity) {
357           unit_list_append(cities[i].units, punit);
358           break;
359         }
360       }
361       if (i == ncities) {
362         if (ncities < max_cities) {
363           cities[ncities].city = pcity;
364           cities[ncities].units = unit_list_new();
365           unit_list_append(cities[ncities].units, punit);
366           ncities++;
367         } else {
368           too_many = TRUE;
369           break;
370         }
371       }
372     } else {
373       no_city = TRUE;
374     }
375   } unit_list_iterate_end;
376 
377   /* Describe the individual cities. */
378   for (i = 0; i < ncities; i++) {
379     const char *air_text = get_airlift_text(cities[i].units, NULL);
380 
381     astr_init(&strs[i]);
382     if (air_text != NULL) {
383       astr_add(&strs[i],
384                /* TRANS: goto/airlift dialog. "Paris (airlift: 2/4)".
385                 * A set of these appear in an "and"-separated list. */
386                _("%s (airlift: %s)"),
387                city_name_get(cities[i].city), air_text);
388     } else {
389       astr_add(&strs[i], "%s", city_name_get(cities[i].city));
390     }
391     descriptions[i] = astr_str(&strs[i]);
392     unit_list_destroy(cities[i].units);
393   }
394   if (too_many) {
395     /* TRANS: goto/airlift dialog. Too many cities to list, some omitted.
396      * Appears at the end of an "and"-separated list. */
397     descriptions[ncities] = last_str = fc_strdup(Q_("?gotodlg:more"));
398     nstrs = ncities+1;
399   } else if (no_city) {
400     /* TRANS: goto/airlift dialog. For units not currently in a city.
401      * Appears at the end of an "and"-separated list. */
402     descriptions[ncities] = last_str = fc_strdup(Q_("?gotodlg:no city"));
403     nstrs = ncities+1;
404   } else {
405     last_str = NULL;
406     nstrs = ncities;
407   }
408 
409   /* Finally, update the label. */
410   {
411     struct astring label = ASTRING_INIT, list = ASTRING_INIT;
412     astr_set(&label,
413              /* TRANS: goto/airlift dialog. Current location of units; %s is an
414               * "and"-separated list of cities and associated info */
415              _("Currently in: %s"),
416              astr_build_and_list(&list, descriptions, nstrs));
417     astr_free(&list);
418     gtk_label_set_text(GTK_LABEL(source), astr_str(&label));
419     astr_free(&label);
420   }
421 
422   /* Clear up. */
423   for (i = 0; i < ncities; i++) {
424     astr_free(&strs[i]);
425   }
426   free(last_str); /* might have been NULL */
427 }
428 
429 /**************************************************************************
430   Refresh city list (in response to "all cities" checkbox changing).
431 **************************************************************************/
update_goto_dialog(GtkToggleButton * button)432 static void update_goto_dialog(GtkToggleButton *button)
433 {
434   bool nonempty = FALSE;
435 
436   if (!client_has_player()) {
437     /* Case global observer. */
438     return;
439   }
440 
441   gotodlg_updating = TRUE;
442 
443   gtk_list_store_clear(goto_list_store);
444 
445   if (gtk_toggle_button_get_active(button)) {
446     players_iterate(pplayer) {
447       nonempty |= list_store_append_player_cities(goto_list_store, pplayer);
448     } players_iterate_end;
449   } else {
450     nonempty |= list_store_append_player_cities(goto_list_store, client_player());
451   }
452 
453   gotodlg_updating = FALSE;
454 
455   refresh_airlift_column();
456 
457   if (!nonempty) {
458     /* No selection causes callbacks to fire, causing also Airlift button
459      * to update. Do it here. */
460     refresh_airlift_button();
461   }
462 }
463 
464 /**************************************************************************
465   Refresh airlift column in city list (without tearing everything down).
466 **************************************************************************/
refresh_airlift_column(void)467 static void refresh_airlift_column(void)
468 {
469   GtkTreeIter iter;
470   bool valid;
471 
472   valid = gtk_tree_model_get_iter_first(GTK_TREE_MODEL(goto_list_store), &iter);
473   while (valid) {
474     int city_id;
475     const struct city *pcity;
476     const char *air_text;
477 
478     gtk_tree_model_get(GTK_TREE_MODEL(goto_list_store), &iter,
479                        GD_COL_CITY_ID, &city_id, -1);
480     pcity = game_city_by_number(city_id);
481     fc_assert_ret(pcity != NULL);
482     air_text = get_airlift_text(get_units_in_focus(), pcity);
483     gtk_list_store_set(GTK_LIST_STORE(goto_list_store), &iter,
484                        GD_COL_AIRLIFT, air_text ? air_text : "-", -1);
485     valid = gtk_tree_model_iter_next(GTK_TREE_MODEL(goto_list_store), &iter);
486   }
487 }
488 
489 /**************************************************************************
490   Refresh the state of the "Airlift" button for the currently selected
491   unit(s) and city.
492 **************************************************************************/
refresh_airlift_button(void)493 static void refresh_airlift_button(void)
494 {
495   struct city *pdestcity = get_selected_city();
496 
497   if (NULL != pdestcity) {
498     bool can_airlift = FALSE;
499 
500     /* Allow action if any of the selected units can airlift. */
501     unit_list_iterate(get_units_in_focus(), punit) {
502       if (unit_can_airlift_to(punit, pdestcity)) {
503         can_airlift = TRUE;
504         break;
505       }
506     } unit_list_iterate_end;
507 
508     if (can_airlift) {
509       gtk_dialog_set_response_sensitive(GTK_DIALOG(dshell),
510                                         CMD_AIRLIFT, TRUE);
511       return;
512     }
513   }
514   gtk_dialog_set_response_sensitive(GTK_DIALOG(dshell), CMD_AIRLIFT, FALSE);
515 }
516 
517 /**************************************************************************
518   Update goto dialog. button tells if cities of all players or just
519   client's player should be listed.
520 **************************************************************************/
goto_selection_callback(GtkTreeSelection * selection,gpointer data)521 static void goto_selection_callback(GtkTreeSelection *selection,
522                                     gpointer data)
523 {
524   struct city *pdestcity;
525 
526   if (gotodlg_updating) {
527     return;
528   }
529 
530   pdestcity = get_selected_city();
531 
532   if (NULL != pdestcity) {
533     center_tile_mapcanvas(city_tile(pdestcity));
534   }
535   refresh_airlift_button();
536 }
537 
538 /**************************************************************************
539   Called when the set of units in focus has changed; updates airlift info
540 **************************************************************************/
goto_dialog_focus_units_changed(void)541 void goto_dialog_focus_units_changed(void)
542 {
543   /* Is the dialog currently being displayed? */
544   if (dshell) {
545     /* Location of current units and ability to airlift may have changed */
546     update_source_label();
547     refresh_airlift_column();
548     refresh_airlift_button();
549   }
550 }
551