From a9641d276300581a0c5acb6b79e0e3d2c33ec6c0 Mon Sep 17 00:00:00 2001
From: Marko Lindqvist <cazfi74@gmail.com>
Date: Sat, 4 May 2024 05:48:07 +0300
Subject: [PATCH 64/64] Lua: Add server.popup_image()

Server lua can request client to popup an image.

See RM #546

Signed-off-by: Marko Lindqvist <cazfi74@gmail.com>
---
 client/gui-gtk-3.22/dialogs.c      | 22 ++++++++
 client/gui-gtk-4.0/dialogs.c       | 22 ++++++++
 client/gui-gtk-5.0/dialogs.c       | 22 ++++++++
 client/gui-qt/dialogs.cpp          | 27 +++++++++
 client/gui-qt/pages.cpp            |  2 +-
 client/gui-qt/qtg_cxxside.cpp      |  2 +
 client/gui-qt/qtg_cxxside.h        |  2 +
 client/gui-sdl2/dialogs.c          | 91 ++++++++++++++++++++++++++++++
 client/gui-sdl3/dialogs.c          | 91 ++++++++++++++++++++++++++++++
 client/gui-stub/dialogs.c          |  7 +++
 client/gui_cbsetter.c              |  2 +
 client/gui_interface.c             |  8 +++
 client/gui_interface.h             |  2 +
 client/include/dialogs_g.h         |  2 +
 client/packhand.c                  |  8 +++
 client/tilespec.c                  | 18 +++++-
 client/tilespec.h                  |  3 +
 common/networking/packets.def      |  8 ++-
 server/scripting/api_server_base.c | 21 +++++++
 server/scripting/api_server_base.h |  2 +
 server/scripting/tolua_server.pkg  |  3 +
 21 files changed, 362 insertions(+), 3 deletions(-)

diff --git a/client/gui-gtk-3.22/dialogs.c b/client/gui-gtk-3.22/dialogs.c
index 1c3ddde177..938712203b 100644
--- a/client/gui-gtk-3.22/dialogs.c
+++ b/client/gui-gtk-3.22/dialogs.c
@@ -1629,3 +1629,25 @@ void request_action_confirmation(const char *expl,
 
   gtk_window_present(GTK_WINDOW(dialog));
 }
+
+/**********************************************************************//**
+  Popup image window
+**************************************************************************/
+void popup_image(const char *tag, int width, int height)
+{
+  struct sprite *spr = load_popup_sprite(tag);
+
+  if (spr != NULL) {
+    GdkPixbuf *pix = sprite_get_pixbuf(spr);
+    GtkWidget *win = gtk_window_new(GTK_WINDOW_TOPLEVEL);
+    GtkWidget *img = gtk_image_new_from_pixbuf(pix);
+
+    gtk_window_set_default_size(GTK_WINDOW(win), width, height);
+    gtk_container_add(GTK_CONTAINER(win), img);
+    gtk_widget_show(win);
+
+    unload_popup_sprite(tag);
+  } else {
+    log_error(_("No image for tag \"%s\", requested by the server."), tag);
+  }
+}
diff --git a/client/gui-gtk-4.0/dialogs.c b/client/gui-gtk-4.0/dialogs.c
index b78df57ba0..85a17ccffa 100644
--- a/client/gui-gtk-4.0/dialogs.c
+++ b/client/gui-gtk-4.0/dialogs.c
@@ -1670,3 +1670,25 @@ void request_action_confirmation(const char *expl,
 
   gtk_window_present(GTK_WINDOW(dialog));
 }
+
+/**********************************************************************//**
+  Popup image window
+**************************************************************************/
+void popup_image(const char *tag, int width, int height)
+{
+  struct sprite *spr = load_popup_sprite(tag);
+
+  if (spr != NULL) {
+    GdkPixbuf *pix = sprite_get_pixbuf(spr);
+    GtkWidget *win = gtk_window_new();
+    GtkWidget *img = gtk_image_new_from_pixbuf(pix);
+
+    gtk_window_set_default_size(GTK_WINDOW(win), width, height);
+    gtk_window_set_child(GTK_WINDOW(win), img);
+    gtk_widget_show(win);
+
+    unload_popup_sprite(tag);
+  } else {
+    log_error(_("No image for tag \"%s\", requested by the server."), tag);
+  }
+}
diff --git a/client/gui-gtk-5.0/dialogs.c b/client/gui-gtk-5.0/dialogs.c
index a338e8660c..19439c19da 100644
--- a/client/gui-gtk-5.0/dialogs.c
+++ b/client/gui-gtk-5.0/dialogs.c
@@ -1670,3 +1670,25 @@ void request_action_confirmation(const char *expl,
 
   gtk_window_present(GTK_WINDOW(dialog));
 }
+
+/**********************************************************************//**
+  Popup image window
+**************************************************************************/
+void popup_image(const char *tag, int width, int height)
+{
+  struct sprite *spr = load_popup_sprite(tag);
+
+  if (spr != NULL) {
+    GdkPixbuf *pix = sprite_get_pixbuf(spr);
+    GtkWidget *win = gtk_window_new();
+    GtkWidget *img = gtk_image_new_from_pixbuf(pix);
+
+    gtk_window_set_default_size(GTK_WINDOW(win), width, height);
+    gtk_window_set_child(GTK_WINDOW(win), img);
+    gtk_widget_show(win);
+
+    unload_popup_sprite(tag);
+  } else {
+    log_error(_("No image for tag \"%s\", requested by the server."), tag);
+  }
+}
diff --git a/client/gui-qt/dialogs.cpp b/client/gui-qt/dialogs.cpp
index 4491596f29..41cff1b92d 100644
--- a/client/gui-qt/dialogs.cpp
+++ b/client/gui-qt/dialogs.cpp
@@ -31,6 +31,7 @@
 #include <QTableWidgetItem>
 #include <QTextEdit>
 #include <QVBoxLayout>
+#include <QWindow>
 #include <QtMath>
 
 // utility
@@ -4966,3 +4967,29 @@ void qtg_request_action_confirmation(const char *expl,
 
   popup_act_confirmation_dialog(hdr, body_text, data);
 }
+
+/**********************************************************************//**
+  Popup image window
+**************************************************************************/
+void qtg_popup_image(const char *tag, int width, int height)
+{
+  struct sprite *spr = load_popup_sprite(tag);
+
+  if (spr != nullptr) {
+    QDialog *win = new QDialog(gui());
+    QVBoxLayout *layout = new QVBoxLayout(win);
+    QPixmap *pm = new QPixmap(*spr->pm);
+    QLabel *lbl = new QLabel;
+
+    win->setFixedSize(width, height);
+    lbl->setPixmap(*pm);
+    layout->addWidget(lbl);
+    win->setLayout(layout);
+
+    win->show();
+
+    unload_popup_sprite(tag);
+  } else {
+    log_error(_("No image for tag \"%s\", requested by the server."), tag);
+  }
+}
diff --git a/client/gui-qt/pages.cpp b/client/gui-qt/pages.cpp
index 2050075d84..3facaaffea 100644
--- a/client/gui-qt/pages.cpp
+++ b/client/gui-qt/pages.cpp
@@ -137,7 +137,7 @@ void update_start_page(void)
 void fc_client::create_main_page(void)
 {
   QPixmap main_graphics(tileset_main_intro_filename(tileset));
-  QLabel* free_main_pic = new QLabel;
+  QLabel *free_main_pic = new QLabel;
   QPainter painter(&main_graphics);
   QStringList buttons_names;
   int buttons_nr;
diff --git a/client/gui-qt/qtg_cxxside.cpp b/client/gui-qt/qtg_cxxside.cpp
index cb0fd2b68f..058b879da7 100644
--- a/client/gui-qt/qtg_cxxside.cpp
+++ b/client/gui-qt/qtg_cxxside.cpp
@@ -136,4 +136,6 @@ void setup_gui_funcs()
     = qtg_real_units_report_dialog_update;
   funcs->endgame_report_dialog_start = qtg_endgame_report_dialog_start;
   funcs->endgame_report_dialog_player = qtg_endgame_report_dialog_player;
+
+  funcs->popup_image = qtg_popup_image;
 }
diff --git a/client/gui-qt/qtg_cxxside.h b/client/gui-qt/qtg_cxxside.h
index fec25ac318..7446c47124 100644
--- a/client/gui-qt/qtg_cxxside.h
+++ b/client/gui-qt/qtg_cxxside.h
@@ -166,4 +166,6 @@ void qtg_real_units_report_dialog_update(void *unused);
 void qtg_endgame_report_dialog_start(const struct packet_endgame_report *packet);
 void qtg_endgame_report_dialog_player(const struct packet_endgame_player *packet);
 
+void qtg_popup_image(const char *tag, int width, int height);
+
 #endif // FC__QTG_CXXSIDE_H
diff --git a/client/gui-sdl2/dialogs.c b/client/gui-sdl2/dialogs.c
index 14ed9b8c61..6ac1a8c3c8 100644
--- a/client/gui-sdl2/dialogs.c
+++ b/client/gui-sdl2/dialogs.c
@@ -3716,3 +3716,94 @@ void request_action_confirmation(const char *expl,
   /* TODO: Implement. Currently just pass everything as confirmed */
   action_confirmation(data, TRUE);
 }
+
+struct advanced_dialog *advanced_image_popup = NULL;
+
+/**********************************************************************//**
+  User interacted with image popup dialog.
+**************************************************************************/
+static int image_popup_window_callback(struct widget *pwindow)
+{
+  if (PRESSED_EVENT(main_data.event)) {
+    move_window_group(advanced_image_popup->begin_widget_list, pwindow);
+  }
+
+  return -1;
+}
+
+/**********************************************************************//**
+  User interacted with image popup dialog close button.
+**************************************************************************/
+static int exit_image_popup_callback(struct widget *pwidget)
+{
+  if (PRESSED_EVENT(main_data.event)) {
+    if (advanced_image_popup != NULL) {
+      popdown_window_group_dialog(advanced_image_popup->begin_widget_list,
+                                  advanced_image_popup->end_widget_list);
+      FC_FREE(advanced_image_popup->scroll);
+      FC_FREE(advanced_image_popup);
+      flush_dirty();
+    }
+  }
+
+  return -1;
+}
+
+/**********************************************************************//**
+  Popup image window
+**************************************************************************/
+void popup_image(const char *tag, int width, int height)
+{
+  if (advanced_image_popup == NULL) {
+    struct sprite *spr = load_popup_sprite(tag);
+
+    if (spr != NULL) {
+      struct widget *win = create_window_skeleton(NULL, NULL, 0);
+      struct widget *buf;
+      SDL_Surface *surf = copy_surface(GET_SURF(spr));
+      SDL_Rect dst;
+      SDL_Rect area;
+
+      advanced_image_popup = fc_calloc(1, sizeof(struct advanced_dialog));
+
+      win->action = image_popup_window_callback;
+      set_wstate(win, FC_WS_NORMAL);
+
+      add_to_gui_list(ID_WINDOW, win);
+      advanced_image_popup->end_widget_list = win;
+
+      /* Create exit button */
+      buf = create_themeicon(current_theme->small_cancel_icon, win->dst,
+                             WF_WIDGET_HAS_INFO_LABEL | WF_RESTORE_BACKGROUND);
+      buf->info_label = create_utf8_from_char_fonto(_("Close Dialog (Esc)"),
+                                                    FONTO_ATTENTION);
+      buf->action = exit_image_popup_callback;
+      set_wstate(buf, FC_WS_NORMAL);
+      buf->key = SDLK_ESCAPE;
+
+      add_to_gui_list(ID_BUTTON, buf);
+      advanced_image_popup->begin_widget_list = buf;
+
+      resize_window(win, NULL, get_theme_color(COLOR_THEME_BACKGROUND),
+                    win->size.w - win->area.w + width,
+                    win->size.h - win->area.h + buf->area.h + height);
+
+      widget_set_position(win,
+                          (main_window_width() - win->size.w) / 2,
+                          (main_window_height() - win->size.h) / 2);
+
+      area = win->area;
+      dst.x = area.x;
+      dst.y = area.y + buf->size.y;
+      alphablit(surf, NULL, win->theme, &dst, 255);
+
+      /* Redraw */
+      redraw_group(advanced_image_popup->begin_widget_list, win, 0);
+      widget_flush(win);
+
+      unload_popup_sprite(tag);
+    } else {
+      log_error(_("No image for tag \"%s\", requested by the server."), tag);
+    }
+  }
+}
diff --git a/client/gui-sdl3/dialogs.c b/client/gui-sdl3/dialogs.c
index 36bd716154..0b25effd45 100644
--- a/client/gui-sdl3/dialogs.c
+++ b/client/gui-sdl3/dialogs.c
@@ -3714,3 +3714,94 @@ void request_action_confirmation(const char *expl,
   /* TODO: Implement. Currently just pass everything as confirmed */
   action_confirmation(data, TRUE);
 }
+
+struct advanced_dialog *advanced_image_popup = NULL;
+
+/**********************************************************************//**
+  User interacted with image popup dialog.
+**************************************************************************/
+static int image_popup_window_callback(struct widget *pwindow)
+{
+  if (PRESSED_EVENT(main_data.event)) {
+    move_window_group(advanced_image_popup->begin_widget_list, pwindow);
+  }
+
+  return -1;
+}
+
+/**********************************************************************//**
+  User interacted with image popup dialog close button.
+**************************************************************************/
+static int exit_image_popup_callback(struct widget *pwidget)
+{
+  if (PRESSED_EVENT(main_data.event)) {
+    if (advanced_image_popup != NULL) {
+      popdown_window_group_dialog(advanced_image_popup->begin_widget_list,
+                                  advanced_image_popup->end_widget_list);
+      FC_FREE(advanced_image_popup->scroll);
+      FC_FREE(advanced_image_popup);
+      flush_dirty();
+    }
+  }
+
+  return -1;
+}
+
+/**********************************************************************//**
+  Popup image window
+**************************************************************************/
+void popup_image(const char *tag, int width, int height)
+{
+  if (advanced_image_popup == NULL) {
+    struct sprite *spr = load_popup_sprite(tag);
+
+    if (spr != NULL) {
+      struct widget *win = create_window_skeleton(NULL, NULL, 0);
+      struct widget *buf;
+      SDL_Surface *surf = copy_surface(GET_SURF(spr));
+      SDL_Rect dst;
+      SDL_Rect area;
+
+      advanced_image_popup = fc_calloc(1, sizeof(struct advanced_dialog));
+
+      win->action = image_popup_window_callback;
+      set_wstate(win, FC_WS_NORMAL);
+
+      add_to_gui_list(ID_WINDOW, win);
+      advanced_image_popup->end_widget_list = win;
+
+      /* Create exit button */
+      buf = create_themeicon(current_theme->small_cancel_icon, win->dst,
+                             WF_WIDGET_HAS_INFO_LABEL | WF_RESTORE_BACKGROUND);
+      buf->info_label = create_utf8_from_char_fonto(_("Close Dialog (Esc)"),
+                                                    FONTO_ATTENTION);
+      buf->action = exit_image_popup_callback;
+      set_wstate(buf, FC_WS_NORMAL);
+      buf->key = SDLK_ESCAPE;
+
+      add_to_gui_list(ID_BUTTON, buf);
+      advanced_image_popup->begin_widget_list = buf;
+
+      resize_window(win, NULL, get_theme_color(COLOR_THEME_BACKGROUND),
+                    win->size.w - win->area.w + width,
+                    win->size.h - win->area.h + buf->area.h + height);
+
+      widget_set_position(win,
+                          (main_window_width() - win->size.w) / 2,
+                          (main_window_height() - win->size.h) / 2);
+
+      area = win->area;
+      dst.x = area.x;
+      dst.y = area.y + buf->size.y;
+      alphablit(surf, NULL, win->theme, &dst, 255);
+
+      /* Redraw */
+      redraw_group(advanced_image_popup->begin_widget_list, win, 0);
+      widget_flush(win);
+
+      unload_popup_sprite(tag);
+    } else {
+      log_error(_("No image for tag \"%s\", requested by the server."), tag);
+    }
+  }
+}
diff --git a/client/gui-stub/dialogs.c b/client/gui-stub/dialogs.c
index 93ae7e3704..343747878b 100644
--- a/client/gui-stub/dialogs.c
+++ b/client/gui-stub/dialogs.c
@@ -373,3 +373,10 @@ void gui_request_action_confirmation(const char *expl,
   /* Just confirm */
   action_confirmation(data, TRUE);
 }
+
+/**********************************************************************//**
+  Popup image window
+**************************************************************************/
+void gui_popup_image(const char *tag, int width, int height)
+{
+}
diff --git a/client/gui_cbsetter.c b/client/gui_cbsetter.c
index 8e4b905d05..201cbbe95f 100644
--- a/client/gui_cbsetter.c
+++ b/client/gui_cbsetter.c
@@ -124,4 +124,6 @@ void setup_gui_funcs(void)
   funcs->gui_clear_theme = gui_gui_clear_theme;
   funcs->get_gui_specific_themes_directories = gui_get_gui_specific_themes_directories;
   funcs->get_usable_themes_in_directory = gui_get_usable_themes_in_directory;
+
+  funcs->popup_image = gui_popup_image;
 }
diff --git a/client/gui_interface.c b/client/gui_interface.c
index adc29bcd39..7ed70b6872 100644
--- a/client/gui_interface.c
+++ b/client/gui_interface.c
@@ -766,3 +766,11 @@ void endgame_report_dialog_player(const struct packet_endgame_player *packet)
 {
   funcs.endgame_report_dialog_player(packet);
 }
+
+/**********************************************************************//**
+  Call popup_image callback
+**************************************************************************/
+void popup_image(const char *tag, int width, int height)
+{
+  funcs.popup_image(tag, width, height);
+}
diff --git a/client/gui_interface.h b/client/gui_interface.h
index 3217b76639..b243c83469 100644
--- a/client/gui_interface.h
+++ b/client/gui_interface.h
@@ -173,6 +173,8 @@ struct gui_funcs {
   void (*real_units_report_dialog_update)(void *unused);
   void (*endgame_report_dialog_start)(const struct packet_endgame_report *packet);
   void (*endgame_report_dialog_player)(const struct packet_endgame_player *packet);
+
+  void (*popup_image)(const char *tag, int width, int height);
 };
 
 struct gui_funcs *get_gui_funcs(void);
diff --git a/client/include/dialogs_g.h b/client/include/dialogs_g.h
index 67be2bc34f..33f3512e9e 100644
--- a/client/include/dialogs_g.h
+++ b/client/include/dialogs_g.h
@@ -94,4 +94,6 @@ GUI_FUNC_PROTO(void, request_action_confirmation, const char *expl,
 
 GUI_FUNC_PROTO(void, update_infra_dialog, void)
 
+GUI_FUNC_PROTO(void, popup_image, const char *tag, int width, int height)
+
 #endif /* FC__DIALOGS_G_H */
diff --git a/client/packhand.c b/client/packhand.c
index 5fcd0137a0..d4cdc6de77 100644
--- a/client/packhand.c
+++ b/client/packhand.c
@@ -5556,6 +5556,14 @@ void handle_play_music(const char *tag)
   play_single_track(tag);
 }
 
+/************************************************************************//**
+  Popup image
+****************************************************************************/
+void handle_popup_image(const char *tag, int width, int height)
+{
+  popup_image(tag, width, height);
+}
+
 /************************************************************************//**
   Open meeting
 ****************************************************************************/
diff --git a/client/tilespec.c b/client/tilespec.c
index f2ab74fcf3..352bdfce89 100644
--- a/client/tilespec.c
+++ b/client/tilespec.c
@@ -429,7 +429,7 @@ struct specfile {
 /*
  * Information about an individual sprite. All fields except 'sprite' are
  * filled at the time of the scan of the specfile. 'Sprite' is
- * set/cleared on demand in load_sprite/unload_sprite.
+ * set/cleared on demand in load_sprite()/unload_sprite().
  */
 struct small_sprite {
   int ref_count;
@@ -7614,3 +7614,19 @@ int tileset_svg_flag_height(struct tileset *t)
 {
   return t->svg_height;
 }
+
+/************************************************************************//**
+  Load sprite from popup image tag
+****************************************************************************/
+struct sprite *load_popup_sprite(const char *tag)
+{
+  return load_sprite(tileset, tag, TRUE, TRUE, FALSE);
+}
+
+/************************************************************************//**
+  Unload sprite from popup image tag
+****************************************************************************/
+void unload_popup_sprite(const char *tag)
+{
+  unload_sprite(tileset, tag);
+}
diff --git a/client/tilespec.h b/client/tilespec.h
index 8eae712fe2..f39e9a4d23 100644
--- a/client/tilespec.h
+++ b/client/tilespec.h
@@ -466,6 +466,9 @@ int tileset_svg_flag_height(struct tileset *t);
 
 int index_ts_topology(int idx);
 
+struct sprite *load_popup_sprite(const char *tag);
+void unload_popup_sprite(const char *tag);
+
 #ifdef __cplusplus
 }
 #endif /* __cplusplus */
diff --git a/common/networking/packets.def b/common/networking/packets.def
index 77ee0eeb10..190a6c974a 100644
--- a/common/networking/packets.def
+++ b/common/networking/packets.def
@@ -3,7 +3,7 @@
 Max used id:
 ============
 
-Max id: 514
+Max id: 515
 
 Packets are not ordered by their id, but by their category. New packet
 with higher id may get added to existing category, and not to the end of file.
@@ -2416,6 +2416,12 @@ PACKET_PLAY_MUSIC = 245; sc, lsend, handle-via-fields
   STRING tag[MAX_LEN_NAME];
 end
 
+PACKET_POPUP_IMAGE = 515; sc, lsend, handle-via-fields
+  STRING tag[MAX_LEN_NAME];
+  UINT16 width;
+  UINT16 height;
+end
+
 /*************** Webclient specific packets ****************/
 /* Use range 256:511 for these                             */
 
diff --git a/server/scripting/api_server_base.c b/server/scripting/api_server_base.c
index 290cc32dd8..740ffda6ae 100644
--- a/server/scripting/api_server_base.c
+++ b/server/scripting/api_server_base.c
@@ -87,6 +87,27 @@ bool api_play_music(lua_State *L, Player *pplayer, const char *tag)
   return TRUE;
 }
 
+/**********************************************************************//**
+  Popup image to player
+**************************************************************************/
+bool api_popup_image(lua_State *L, Player *pplayer, const char *tag,
+                     int width, int height)
+{
+  struct packet_popup_image p;
+
+  LUASCRIPT_CHECK_STATE(L, FALSE);
+  LUASCRIPT_CHECK_SELF(L, pplayer, FALSE);
+  LUASCRIPT_CHECK_ARG_NIL(L, tag, 3, API_TYPE_STRING, FALSE);
+
+  sz_strlcpy(p.tag, tag);
+  p.width = width;
+  p.height = height;
+
+  lsend_packet_popup_image(pplayer->connections, &p);
+
+  return TRUE;
+}
+
 /**********************************************************************//**
   Return the formatted value of the setting or NULL if no such setting
   exists.
diff --git a/server/scripting/api_server_base.h b/server/scripting/api_server_base.h
index 50f1818804..7437d8cb33 100644
--- a/server/scripting/api_server_base.h
+++ b/server/scripting/api_server_base.h
@@ -29,5 +29,7 @@ bool api_server_save(lua_State *L, const char *filename);
 const char *api_server_setting_get(lua_State *L, const char *sett_name);
 
 bool api_play_music(lua_State *L, Player *pplayer, const char *tag);
+bool api_popup_image(lua_State *L, Player *pplayer, const char *tag,
+                     int width, int height);
 
 #endif /* FC__API_SERVER_BASE_H */
diff --git a/server/scripting/tolua_server.pkg b/server/scripting/tolua_server.pkg
index bc0d69fc16..93f2d052d0 100644
--- a/server/scripting/tolua_server.pkg
+++ b/server/scripting/tolua_server.pkg
@@ -50,6 +50,9 @@ module server {
     @ civilization_score (lua_State *L, Player *pplayer);
   bool api_play_music
     @ play_music (lua_State *L, Player *pplayer, const char *tag);
+  bool api_popup_image
+    @ popup_image (lua_State *L, Player *pplayer, const char *tag,
+                   int width, int height);
 
   module setting {
     const char *api_server_setting_get
-- 
2.43.0

