← Back to team overview

widelands-dev team mailing list archive

[Merge] lp:~widelands-dev/widelands/savegame-menu into lp:widelands

 

GunChleoc has proposed merging lp:~widelands-dev/widelands/savegame-menu into lp:widelands.

Commit message:
Refactored load- and savegame screens

- New common class GameDetails to show information about savegames, analogous to MapDetails
- More informative savegame screen
- Load/Savegame screens now use 100% Box layout
- Fixed drawing of frame for scaled Minimap images

Requested reviews:
  Widelands Developers (widelands-dev)
Related bugs:
  Bug #1377660 in widelands: "Fullscreen Menu overhaul"
  https://bugs.launchpad.net/widelands/+bug/1377660

For more details, see:
https://code.launchpad.net/~widelands-dev/widelands/savegame-menu/+merge/322924

Complete overhaul of Load Game, Watch Replay and Save Game screens
-- 
Your team Widelands Developers is requested to review the proposed merge of lp:~widelands-dev/widelands/savegame-menu into lp:widelands.
=== modified file 'src/game_io/game_preload_packet.cc'
--- src/game_io/game_preload_packet.cc	2017-01-25 18:55:59 +0000
+++ src/game_io/game_preload_packet.cc	2017-04-21 07:38:25 +0000
@@ -46,9 +46,11 @@
 constexpr uint16_t kCurrentPacketVersion = 6;
 constexpr const char* kMinimapFilename = "minimap.png";
 
+// Win condition localization can come from the 'widelands' or 'win_conditions' textdomain.
 std::string GamePreloadPacket::get_localized_win_condition() const {
+	std::string result = _(win_condition_);
 	i18n::Textdomain td("win_conditions");
-	return _(win_condition_);
+	return _(result);
 }
 
 void GamePreloadPacket::read(FileSystem& fs, Game&, MapObjectLoader* const) {

=== modified file 'src/ui_basic/box.cc'
--- src/ui_basic/box.cc	2017-03-02 08:43:30 +0000
+++ src/ui_basic/box.cc	2017-04-21 07:38:25 +0000
@@ -88,6 +88,12 @@
 	inner_spacing_ = size;
 }
 
+void Box::set_max_size(int w, int h) {
+	max_x_ = w;
+	max_y_ = h;
+	set_desired_size(w, h);
+}
+
 /**
  * Compute the desired size based on our children. This assumes that the
  * infinite space is zero, and is later on also re-used to calculate the

=== modified file 'src/ui_basic/box.h'
--- src/ui_basic/box.h	2017-02-25 13:27:40 +0000
+++ src/ui_basic/box.h	2017-04-21 07:38:25 +0000
@@ -66,6 +66,8 @@
 
 	void set_min_desired_breadth(uint32_t min);
 	void set_inner_spacing(uint32_t size);
+	/// Sets the maximum dimensions and calls set_desired_size()
+	void set_max_size(int w, int h);
 
 protected:
 	void layout() override;

=== modified file 'src/ui_basic/icon.cc'
--- src/ui_basic/icon.cc	2017-01-25 18:55:59 +0000
+++ src/ui_basic/icon.cc	2017-04-21 07:38:25 +0000
@@ -54,16 +54,16 @@
 	if (pic_) {
 		const float scale = std::min(1.f, std::min(static_cast<float>(get_w()) / pic_->width(),
 		                                           static_cast<float>(get_h()) / pic_->height()));
-
-		const float width = scale * get_w();
-		const float height = scale * get_h();
-		const float x = (get_w() - width) / 2.f;
-		const float y = (get_h() - height) / 2.f;
+		// We need to be pixel perfect, so we use ints.
+		const int width = scale * get_w();
+		const int height = scale * get_h();
+		const int x = (get_w() - width) / 2;
+		const int y = (get_h() - height) / 2;
 		dst.blitrect_scale(Rectf(x, y, width, height), pic_,
 		                   Recti(0, 0, pic_->width(), pic_->height()), 1., BlendMode::UseAlpha);
-	}
-	if (draw_frame_) {
-		dst.draw_rect(Rectf(0.f, 0.f, get_w(), get_h()), framecolor_);
-	}
+		if (draw_frame_) {
+			dst.draw_rect(Rectf(x, y, width, height), framecolor_);
+		}
+	}	
 }
 }

=== modified file 'src/ui_basic/icon.h'
--- src/ui_basic/icon.h	2017-01-25 18:55:59 +0000
+++ src/ui_basic/icon.h	2017-04-21 07:38:25 +0000
@@ -33,6 +33,10 @@
 	Icon(Panel* parent, int32_t x, int32_t y, int32_t w, int32_t h, const Image* picture_id);
 
 	void set_icon(const Image* picture_id);
+	const Image* icon() const {
+		return pic_;
+	}
+
 	void set_frame(const RGBColor& color);
 	void set_no_frame();
 

=== modified file 'src/ui_basic/multilinetextarea.cc'
--- src/ui_basic/multilinetextarea.cc	2017-03-04 18:02:23 +0000
+++ src/ui_basic/multilinetextarea.cc	2017-04-21 07:38:25 +0000
@@ -51,6 +51,7 @@
      scrollmode_(scroll_mode),
      pic_background_(nullptr) {
 	assert(scrollmode_ == MultilineTextarea::ScrollMode::kNoScrolling || Scrollbar::kSize <= w);
+	set_scrollmode(scroll_mode);
 	set_thinks(false);
 
 	scrollbar_.moved.connect(boost::bind(&MultilineTextarea::scrollpos_changed, this, _1));
@@ -60,9 +61,6 @@
 	               as_uifont(UI::g_fh1->fontset()->representative_character(), UI_FONT_SIZE_SMALL))
 	      ->height());
 	scrollbar_.set_steps(1);
-	scrollbar_.set_force_draw(scrollmode_ == ScrollMode::kScrollNormalForced ||
-	                          scrollmode_ == ScrollMode::kScrollLogForced);
-
 	layout();
 }
 
@@ -192,6 +190,12 @@
 void MultilineTextarea::set_background(const Image* background) {
 	pic_background_ = background;
 }
+void MultilineTextarea::set_scrollmode(MultilineTextarea::ScrollMode scroll_mode) {
+	scrollmode_ = scroll_mode;
+	scrollbar_.set_force_draw(scrollmode_ == ScrollMode::kScrollNormalForced ||
+	                          scrollmode_ == ScrollMode::kScrollLogForced);
+	layout();
+}
 
 std::string MultilineTextarea::make_richtext() {
 	std::string temp = richtext_escape(text_);

=== modified file 'src/ui_basic/multilinetextarea.h'
--- src/ui_basic/multilinetextarea.h	2017-02-23 19:38:51 +0000
+++ src/ui_basic/multilinetextarea.h	2017-04-21 07:38:25 +0000
@@ -79,6 +79,7 @@
 	void scroll_to_top();
 
 	void set_background(const Image* background);
+	void set_scrollmode(MultilineTextarea::ScrollMode scroll_mode);
 
 protected:
 	void layout() override;

=== modified file 'src/ui_fsmenu/load_map_or_game.cc'
--- src/ui_fsmenu/load_map_or_game.cc	2017-01-25 18:55:59 +0000
+++ src/ui_fsmenu/load_map_or_game.cc	2017-04-21 07:38:25 +0000
@@ -38,7 +38,7 @@
      padding_(4),
      indent_(10),
      label_height_(20),
-     right_column_margin_(15),
+     right_column_margin_(16),
 
      // Main buttons
      back_(this, "back", 0, 0, 0, 0, g_gr->images().get("images/ui_basic/but0.png"), _("Back")),

=== modified file 'src/ui_fsmenu/loadgame.cc'
--- src/ui_fsmenu/loadgame.cc	2017-03-05 17:55:29 +0000
+++ src/ui_fsmenu/loadgame.cc	2017-04-21 07:38:25 +0000
@@ -19,667 +19,117 @@
 
 #include "ui_fsmenu/loadgame.h"
 
-#include <algorithm>
-#include <cstdio>
-#include <ctime>
-#include <memory>
-
-#include <boost/algorithm/string/predicate.hpp>
-#include <boost/format.hpp>
-
 #include "base/i18n.h"
-#include "base/log.h"
-#include "base/time_string.h"
-#include "game_io/game_loader.h"
-#include "game_io/game_preload_packet.h"
-#include "graphic/graphic.h"
-#include "graphic/image_io.h"
-#include "graphic/text_constants.h"
-#include "graphic/texture.h"
-#include "helper.h"
-#include "io/filesystem/layered_filesystem.h"
-#include "logic/game.h"
-#include "logic/game_controller.h"
-#include "logic/game_settings.h"
-#include "logic/replay.h"
-#include "ui_basic/icon.h"
-#include "ui_basic/messagebox.h"
-
-// TODO(GunChleoc): Arabic: line height broken for descriptions for Arabic.
-namespace {
-
-// This function concatenates the filename and localized map name for a savegame/replay.
-// If the filename starts with the map name, the map name is omitted.
-// It also prefixes autosave files with a numbered and localized "Autosave" prefix.
-std::string map_filename(const std::string& filename, const std::string& mapname) {
-	std::string result = FileSystem::filename_without_ext(filename.c_str());
-	std::string mapname_localized;
-	{
-		i18n::Textdomain td("maps");
-		mapname_localized = _(mapname);
-	}
-
-	if (boost::starts_with(result, "wl_autosave")) {
-		std::vector<std::string> autosave_name;
-		boost::split(autosave_name, result, boost::is_any_of("_"));
-		if (autosave_name.empty() || autosave_name.size() < 3) {
-			/** TRANSLATORS: %1% is a map's name. */
-			result = (boost::format(_("Autosave: %1%")) % mapname_localized).str();
-		} else {
-			/** TRANSLATORS: %1% is a number, %2% a map's name. */
-			result = (boost::format(_("Autosave %1%: %2%")) % autosave_name.back() % mapname_localized)
-			            .str();
-		}
-	} else if (!(boost::starts_with(result, mapname) ||
-	             boost::starts_with(result, mapname_localized))) {
-		/** TRANSLATORS: %1% is a filename, %2% a map's name. */
-		result = (boost::format(_("%1% (%2%)")) % result % mapname_localized).str();
-	}
-	return result;
-}
-
-}  // namespace
+#include "wui/gamedetails.h"
 
 FullscreenMenuLoadGame::FullscreenMenuLoadGame(Widelands::Game& g,
                                                GameSettingsProvider* gsp,
                                                GameController* gc,
                                                bool is_replay)
    : FullscreenMenuLoadMapOrGame(),
-     table_(this,
-            tablex_,
-            tabley_,
-            tablew_,
-            tableh_,
-            g_gr->images().get("images/ui_basic/but3.png"),
-            UI::TableRows::kMultiDescending),
-
-     is_replay_(is_replay),
+
+     main_box_(this, 0, 0, UI::Box::Vertical),
+     info_box_(&main_box_, 0, 0, UI::Box::Horizontal),
+
      // Main title
-     title_(this,
-            get_w() / 2,
-            tabley_ / 3,
+     title_(&main_box_,
+            0,
+            0,
             is_replay_ ? _("Choose a replay") : _("Choose a saved game"),
             UI::Align::kCenter),
 
-     // Savegame description
-     label_mapname_(this, right_column_x_, tabley_),
-     ta_mapname_(this,
-                 right_column_x_ + indent_,
-                 get_y_from_preceding(label_mapname_) + padding_,
-                 get_right_column_w(right_column_x_ + indent_),
-                 2 * label_height_ - padding_),
-
-     label_gametime_(this, right_column_x_, get_y_from_preceding(ta_mapname_) + 2 * padding_),
-     ta_gametime_(this,
-                  right_column_tab_,
-                  label_gametime_.get_y(),
-                  get_right_column_w(right_column_tab_),
-                  label_height_),
-
-     label_players_(this, right_column_x_, get_y_from_preceding(ta_gametime_)),
-     ta_players_(this,
-                 right_column_tab_,
-                 label_players_.get_y(),
-                 get_right_column_w(right_column_tab_),
-                 label_height_),
-
-     label_version_(this, right_column_x_, get_y_from_preceding(ta_players_)),
-     ta_version_(this, right_column_tab_, label_version_.get_y()),
-
-     label_win_condition_(this, right_column_x_, get_y_from_preceding(ta_version_) + 3 * padding_),
-     ta_win_condition_(this,
-                       right_column_x_ + indent_,
-                       get_y_from_preceding(label_win_condition_) + padding_,
-                       get_right_column_w(right_column_x_ + indent_),
-                       label_height_),
-
-     delete_(this,
-             "delete",
-             right_column_x_,
-             buty_ - buth_ - 2 * padding_,
-             butw_,
-             buth_,
-             g_gr->images().get("images/ui_basic/but0.png"),
-             _("Delete")),
-
-     ta_long_generic_message_(this,
-                              right_column_x_,
-                              get_y_from_preceding(ta_mapname_) + 2 * padding_,
-                              get_right_column_w(right_column_x_),
-                              delete_.get_y() - get_y_from_preceding(ta_mapname_) - 6 * padding_),
-
-     minimap_y_(get_y_from_preceding(ta_win_condition_) + 3 * padding_),
-     minimap_w_(get_right_column_w(right_column_x_)),
-     minimap_h_(delete_.get_y() - get_y_from_preceding(ta_win_condition_) - 6 * padding_),
-     minimap_icon_(this,
-                   right_column_x_,
-                   get_y_from_preceding(ta_win_condition_) + 3 * padding_,
-                   minimap_w_,
-                   minimap_h_,
-                   nullptr),
-
-     // "Data container" for the savegame information
+     load_or_save_(&info_box_,
+                   g,
+                   is_replay ? LoadOrSaveGame::FileType::kReplay : gsp->settings().multiplayer ?
+                               LoadOrSaveGame::FileType::kGameMultiPlayer :
+                               LoadOrSaveGame::FileType::kGameSinglePlayer,
+                   GameDetails::Style::kFsMenu,
+                   true),
+
+     is_replay_(is_replay),
      game_(g),
      settings_(gsp),
      ctrl_(gc) {
-	title_.set_fontsize(UI_FONT_SIZE_BIG);
-	ta_gametime_.set_tooltip(_("The time that elapsed inside this game"));
-	ta_players_.set_tooltip(_("The number of players"));
-	ta_version_.set_tooltip(_("The version of Widelands that this game was played under"));
-	ta_win_condition_.set_tooltip(_("The win condition that was set for this game"));
+
+	// Make sure that we have some space to work with.
+	main_box_.set_size(get_w(), get_w());
+
+	main_box_.add_space(padding_);
+	main_box_.add_inf_space();
+	main_box_.add(&title_, UI::Box::Resizing::kAlign, UI::Align::kCenter);
+	main_box_.add_inf_space();
+	main_box_.add_inf_space();
+	main_box_.add(&info_box_, UI::Box::Resizing::kExpandBoth);
+	main_box_.add_space(padding_);
+
+	info_box_.add(&load_or_save_.table(), UI::Box::Resizing::kFullSize);
+	info_box_.add_space(right_column_margin_);
+	info_box_.add(load_or_save_.game_details(), UI::Box::Resizing::kFullSize);
+
+	button_spacer_ = new UI::Panel(load_or_save_.game_details()->button_box(), 0, 0, 0, 0);
+	load_or_save_.game_details()->button_box()->add(button_spacer_);
+
+	layout();
+
+	ok_.set_enabled(false);
+	set_thinks(false);
 
 	if (is_replay_) {
 		back_.set_tooltip(_("Return to the main menu"));
 		ok_.set_tooltip(_("Load this replay"));
-		ta_mapname_.set_tooltip(_("The map that this replay is based on"));
-		delete_.set_tooltip(_("Delete this replay"));
 	} else {
-		back_.set_tooltip(_("Return to the single player menu"));
+		back_.set_tooltip(gsp->settings().multiplayer ? _("Return to the multiplayer game setup") :
+		                                                _("Return to the single player menu"));
 		ok_.set_tooltip(_("Load this game"));
-		ta_mapname_.set_tooltip(_("The map that this game is based on"));
-		delete_.set_tooltip(_("Delete this game"));
 	}
-	set_thinks(false);
-	minimap_icon_.set_visible(false);
 
 	back_.sigclicked.connect(boost::bind(&FullscreenMenuLoadGame::clicked_back, boost::ref(*this)));
 	ok_.sigclicked.connect(boost::bind(&FullscreenMenuLoadGame::clicked_ok, boost::ref(*this)));
-	delete_.sigclicked.connect(
-	   boost::bind(&FullscreenMenuLoadGame::clicked_delete, boost::ref(*this)));
-	table_.add_column(130, _("Save Date"), _("The date this game was saved"));
-	if (is_replay_ || settings_->settings().multiplayer) {
-		std::vector<std::string> modes;
-		if (is_replay_) {
-			/** TRANSLATORS: Tooltip for the "Mode" column when choosing a game/replay to load. */
-			/** TRANSLATORS: Make sure that you keep consistency in your translation. */
-			modes.push_back(_("SP = Single Player"));
-		}
-		/** TRANSLATORS: Tooltip for the "Mode" column when choosing a game/replay to load. */
-		/** TRANSLATORS: Make sure that you keep consistency in your translation. */
-		modes.push_back(_("MP = Multiplayer"));
-		/** TRANSLATORS: Tooltip for the "Mode" column when choosing a game/replay to load. */
-		/** TRANSLATORS: Make sure that you keep consistency in your translation. */
-		modes.push_back(_("H = Multiplayer (Host)"));
-		const std::string mode_tooltip_1 =
-		   /** TRANSLATORS: Tooltip for the "Mode" column when choosing a game/replay to load. */
-		   /** TRANSLATORS: %s is a list of game modes. */
-		   ((boost::format(_("Game Mode: %s.")) %
-		     i18n::localize_list(modes, i18n::ConcatenateWith::COMMA)))
-		      .str();
-		const std::string mode_tooltip_2 = _("Numbers are the number of players.");
-
-		table_.add_column(
-		   65,
-		   /** TRANSLATORS: Game Mode table column when choosing a game/replay to load. */
-		   /** TRANSLATORS: Keep this to 5 letters maximum. */
-		   /** TRANSLATORS: A tooltip will explain if you need to use an abbreviation. */
-		   _("Mode"), (boost::format("%s %s") % mode_tooltip_1 % mode_tooltip_2).str());
-	}
-	table_.add_column(0, _("Description"),
-	                  _("The filename that the game was saved under followed by the map’s name, "
-	                    "or the map’s name followed by the last objective achieved."),
-	                  UI::Align::kLeft, UI::TableColumnType::kFlexible);
-	table_.set_column_compare(
-	   0, boost::bind(&FullscreenMenuLoadGame::compare_date_descending, this, _1, _2));
-	table_.selected.connect(boost::bind(&FullscreenMenuLoadGame::entry_selected, this));
-	table_.double_clicked.connect(
+	load_or_save_.table().selected.connect(
+	   boost::bind(&FullscreenMenuLoadGame::entry_selected, this));
+	load_or_save_.table().double_clicked.connect(
 	   boost::bind(&FullscreenMenuLoadGame::clicked_ok, boost::ref(*this)));
-	table_.set_sort_column(0);
-	table_.focus();
+
 	fill_table();
+	if (!load_or_save_.table().empty()) {
+		load_or_save_.table().select(0);
+	}
 }
 
 void FullscreenMenuLoadGame::layout() {
-	// TODO(GunChleoc): Implement when we have box layout for the details.
-	table_.layout();
-}
-
-void FullscreenMenuLoadGame::think() {
-	if (ctrl_) {
-		ctrl_->think();
-	}
-}
-
-// Reverse default sort order for save date column
-bool FullscreenMenuLoadGame::compare_date_descending(uint32_t rowa, uint32_t rowb) {
-	const SavegameData& r1 = games_data_[table_[rowa]];
-	const SavegameData& r2 = games_data_[table_[rowb]];
-
-	return r1.savetimestamp < r2.savetimestamp;
+	FullscreenMenuLoadMapOrGame::layout();
+	main_box_.set_size(get_w() - 2 * tablex_, tabley_ + tableh_ + padding_);
+	main_box_.set_pos(Vector2i(tablex_, 0));
+	title_.set_fontsize(fs_big());
+	load_or_save_.delete_button()->set_desired_size(butw_, buth_);
+	button_spacer_->set_desired_size(butw_, buth_ + 2 * padding_);
+	load_or_save_.table().set_desired_size(tablew_, tableh_);
+	load_or_save_.game_details()->set_max_size(
+	   main_box_.get_w() - tablew_ - right_column_margin_, tableh_);
 }
 
 void FullscreenMenuLoadGame::clicked_ok() {
-	if (!table_.has_selection()) {
+	if (load_or_save_.table().selections().size() != 1) {
 		return;
 	}
-	const SavegameData& gamedata = games_data_[table_.get_selected()];
-	if (gamedata.errormessage.empty()) {
-		filename_ = gamedata.filename;
+
+	const SavegameData* gamedata = load_or_save_.entry_selected();
+	if (gamedata && gamedata->errormessage.empty()) {
+		filename_ = gamedata->filename;
 		end_modal<FullscreenMenuBase::MenuTarget>(FullscreenMenuBase::MenuTarget::kOk);
 	}
 }
 
-void FullscreenMenuLoadGame::clicked_delete() {
-	if (!table_.has_selection()) {
-		return;
-	}
-	std::set<uint32_t> selections = table_.selections();
-	size_t no_selections = selections.size();
-	std::string message;
-	if (no_selections > 1) {
-		if (is_replay_) {
-			message = (boost::format(ngettext("Do you really want to delete this %d replay?",
-			                                  "Do you really want to delete these %d replays?",
-			                                  no_selections)) %
-			           no_selections)
-			             .str();
-		} else {
-			message = (boost::format(ngettext("Do you really want to delete this %d game?",
-			                                  "Do you really want to delete these %d games?",
-			                                  no_selections)) %
-			           no_selections)
-			             .str();
-		}
-		message = (boost::format("%s\n%s") % message % filename_list_string()).str();
-
-	} else {
-		const SavegameData& gamedata = games_data_[table_.get_selected()];
-
-		message = (boost::format("%s %s\n") % label_mapname_.get_text() % gamedata.mapname).str();
-
-		message = (boost::format("%s %s %s\n") % message % label_win_condition_.get_text() %
-		           gamedata.wincondition)
-		             .str();
-
-		message =
-		   (boost::format("%s %s %s\n") % message % _("Save Date:") % gamedata.savedatestring).str();
-
-		message = (boost::format("%s %s %s\n") % message % label_gametime_.get_text() %
-		           gametimestring(gamedata.gametime))
-		             .str();
-
-		message =
-		   (boost::format("%s %s %s\n\n") % message % label_players_.get_text() % gamedata.nrplayers)
-		      .str();
-
-		message = (boost::format("%s %s %s\n") % message % _("Filename:") % gamedata.filename).str();
-
-		if (is_replay_) {
-			message =
-			   (boost::format("%s\n\n%s") % _("Do you really want to delete this replay?") % message)
-			      .str();
-		} else {
-			message =
-			   (boost::format("%s\n\n%s") % _("Do you really want to delete this game?") % message)
-			      .str();
-		}
-	}
-
-	UI::WLMessageBox confirmationBox(
-	   this, ngettext("Confirm deleting file", "Confirm deleting files", no_selections), message,
-	   UI::WLMessageBox::MBoxType::kOkCancel);
-
-	if (confirmationBox.run<UI::Panel::Returncodes>() == UI::Panel::Returncodes::kOk) {
-		for (const uint32_t index : selections) {
-			const std::string& deleteme = games_data_[table_.get(table_.get_record(index))].filename;
-			g_fs->fs_unlink(deleteme);
-			if (is_replay_) {
-				g_fs->fs_unlink(deleteme + WLGF_SUFFIX);
-			}
-		}
-		fill_table();
-	}
-}
-
-std::string FullscreenMenuLoadGame::filename_list_string() {
-	std::set<uint32_t> selections = table_.selections();
-	boost::format message;
-	int counter = 0;
-	for (const uint32_t index : selections) {
-		++counter;
-		// TODO(GunChleoc): We can exceed the texture size for the font renderer,
-		// so we have to restrict this for now.
-		if (counter > 50) {
-			message = boost::format("%s\n%s") % message % "...";
-			break;
-		}
-		const SavegameData& gamedata = games_data_[table_.get(table_.get_record(index))];
-
-		if (gamedata.errormessage.empty()) {
-			message =
-			   boost::format("%s\n%s") % message %
-			   /** TRANSLATORS %1% = map name, %2% = save date. */
-			   (boost::format(_("%1%, saved on %2%")) % gamedata.mapname % gamedata.savedatestring);
-		} else {
-			message = boost::format("%s\n%s") % message % gamedata.filename;
-		}
-	}
-	return message.str();
-}
-
-bool FullscreenMenuLoadGame::set_has_selection() {
-	bool has_selection = table_.selections().size() < 2;
-	ok_.set_enabled(has_selection);
-	delete_.set_enabled(table_.has_selection());
-
-	if (!has_selection) {
-		label_mapname_.set_text(std::string());
-		label_gametime_.set_text(std::string());
-		label_players_.set_text(std::string());
-		label_version_.set_text(std::string());
-		label_win_condition_.set_text(std::string());
-
-		ta_mapname_.set_text(std::string());
-		ta_gametime_.set_text(std::string());
-		ta_players_.set_text(std::string());
-		ta_version_.set_text(std::string());
-		ta_win_condition_.set_text(std::string());
-		minimap_icon_.set_icon(nullptr);
-		minimap_icon_.set_visible(false);
-		minimap_icon_.set_no_frame();
-		minimap_image_.reset();
-	} else {
-		label_mapname_.set_text(_("Map Name:"));
-		label_gametime_.set_text(_("Gametime:"));
-		label_players_.set_text(_("Players:"));
-		label_win_condition_.set_text(_("Win Condition:"));
-	}
-	return has_selection;
-}
-
 void FullscreenMenuLoadGame::entry_selected() {
-	size_t selections = table_.selections().size();
-	if (set_has_selection()) {
-
-		const SavegameData& gamedata = games_data_[table_.get_selected()];
-		ta_long_generic_message_.set_text(gamedata.errormessage);
-
-		if (gamedata.errormessage.empty()) {
-			ta_long_generic_message_.set_visible(false);
-			ta_mapname_.set_text(gamedata.mapname);
-			ta_gametime_.set_text(gametimestring(gamedata.gametime));
-
-			uint8_t number_of_players = gamedata.nrplayers;
-			if (number_of_players > 0) {
-				ta_players_.set_text(
-				   (boost::format("%u") % static_cast<unsigned int>(number_of_players)).str());
-			} else {
-				label_players_.set_text("");
-				ta_players_.set_text("");
-			}
-
-			if (gamedata.version.empty()) {
-				label_version_.set_text("");
-				ta_version_.set_text("");
-			} else {
-				label_version_.set_text(_("Widelands Version:"));
-				ta_version_.set_text(gamedata.version);
-			}
-
-			{
-				i18n::Textdomain td("win_conditions");
-				ta_win_condition_.set_text(_(gamedata.wincondition));
-			}
-
-			std::string minimap_path = gamedata.minimap_path;
-			// Delete former image
-			minimap_icon_.set_icon(nullptr);
-			minimap_icon_.set_visible(false);
-			minimap_icon_.set_no_frame();
-			minimap_image_.reset();
-			// Load the new one
-			if (!minimap_path.empty()) {
-				try {
-					// Load the image
-					minimap_image_ = load_image(
-					   minimap_path,
-					   std::unique_ptr<FileSystem>(g_fs->make_sub_file_system(gamedata.filename)).get());
-
-					// Scale it
-					double scale = double(minimap_w_) / minimap_image_->width();
-					double scaleY = double(minimap_h_) / minimap_image_->height();
-					if (scaleY < scale) {
-						scale = scaleY;
-					}
-					if (scale > 1.0)
-						scale = 1.0;  // Don't make the image too big; fuzziness will result
-					uint16_t w = scale * minimap_image_->width();
-					uint16_t h = scale * minimap_image_->height();
-
-					// Center the minimap in the available space
-					int32_t xpos =
-					   right_column_x_ + (get_w() - right_column_margin_ - w - right_column_x_) / 2;
-					int32_t ypos = minimap_y_;
-
-					// Set small minimaps higher up for a more harmonious look
-					if (h < minimap_h_ * 2 / 3) {
-						ypos += (minimap_h_ - h) / 3;
-					} else {
-						ypos += (minimap_h_ - h) / 2;
-					}
-
-					minimap_icon_.set_size(w, h);
-					minimap_icon_.set_pos(Vector2i(xpos, ypos));
-					minimap_icon_.set_frame(UI_FONT_CLR_FG);
-					minimap_icon_.set_visible(true);
-					minimap_icon_.set_icon(minimap_image_.get());
-				} catch (const std::exception& e) {
-					log("Failed to load the minimap image : %s\n", e.what());
-				}
-			}
-		} else {
-			label_mapname_.set_text(_("Filename:"));
-			ta_mapname_.set_text(gamedata.mapname);
-			label_gametime_.set_text("");
-			ta_gametime_.set_text("");
-			label_players_.set_text("");
-			ta_players_.set_text("");
-			label_version_.set_text("");
-			ta_version_.set_text("");
-			label_win_condition_.set_text("");
-			ta_win_condition_.set_text("");
-
-			minimap_icon_.set_icon(nullptr);
-			minimap_icon_.set_visible(false);
-			minimap_icon_.set_no_frame();
-			minimap_image_.reset();
-
-			ta_long_generic_message_.set_visible(true);
-			ok_.set_enabled(false);
-		}
-	} else if (selections > 1) {
-		label_mapname_.set_text(
-		   (boost::format(ngettext("Selected %d file:", "Selected %d files:", selections)) %
-		    selections)
-		      .str());
-		ta_long_generic_message_.set_visible(true);
-		ta_long_generic_message_.set_text(filename_list_string());
+	ok_.set_enabled(load_or_save_.table().selections().size() == 1);
+	load_or_save_.delete_button()->set_enabled(load_or_save_.has_selection());
+	if (load_or_save_.has_selection()) {
+		load_or_save_.entry_selected();
 	}
 }
 
-/**
- * Fill the file list
- */
 void FullscreenMenuLoadGame::fill_table() {
-
-	games_data_.clear();
-	table_.clear();
-
-	FilenameSet gamefiles;
-
-	if (is_replay_) {
-		gamefiles = filter(g_fs->list_directory(REPLAY_DIR),
-		                   [](const std::string& fn) { return boost::ends_with(fn, REPLAY_SUFFIX); });
-	} else {
-		gamefiles = g_fs->list_directory("save");
-	}
-
-	Widelands::GamePreloadPacket gpdp;
-
-	for (const std::string& gamefilename : gamefiles) {
-		if (gamefilename == "save/campvis" || gamefilename == "save\\campvis") {
-			continue;
-		}
-
-		SavegameData gamedata;
-
-		std::string savename = gamefilename;
-		if (is_replay_)
-			savename += WLGF_SUFFIX;
-
-		if (!g_fs->file_exists(savename.c_str())) {
-			continue;
-		}
-
-		gamedata.filename = gamefilename;
-
-		try {
-			Widelands::GameLoader gl(savename.c_str(), game_);
-			gl.preload_game(gpdp);
-
-			gamedata.gametype = gpdp.get_gametype();
-
-			if (!is_replay_) {
-				if (settings_->settings().multiplayer) {
-					if (gamedata.gametype == GameController::GameType::SINGLEPLAYER) {
-						continue;
-					}
-				} else if (gamedata.gametype > GameController::GameType::SINGLEPLAYER) {
-					continue;
-				}
-			}
-
-			gamedata.mapname = gpdp.get_mapname();
-			gamedata.gametime = gpdp.get_gametime();
-			gamedata.nrplayers = gpdp.get_number_of_players();
-			gamedata.version = gpdp.get_version();
-
-			gamedata.savetimestamp = gpdp.get_savetimestamp();
-			time_t t;
-			time(&t);
-			struct tm* currenttime = localtime(&t);
-			// We need to put these into variables because of a sideeffect of the localtime function.
-			int8_t current_year = currenttime->tm_year;
-			int8_t current_month = currenttime->tm_mon;
-			int8_t current_day = currenttime->tm_mday;
-
-			struct tm* savedate = localtime(&gamedata.savetimestamp);
-
-			if (gamedata.savetimestamp > 0) {
-				if (savedate->tm_year == current_year && savedate->tm_mon == current_month &&
-				    savedate->tm_mday == current_day) {  // Today
-
-					// Adding the 0 padding in a separate statement so translators won't have to deal
-					// with it
-					const std::string minute = (boost::format("%02u") % savedate->tm_min).str();
-
-					/** TRANSLATORS: Display date for choosing a savegame/replay */
-					/** TRANSLATORS: hour:minute */
-					gamedata.savedatestring =
-					   (boost::format(_("Today, %1%:%2%")) % savedate->tm_hour % minute).str();
-				} else if ((savedate->tm_year == current_year && savedate->tm_mon == current_month &&
-				            savedate->tm_mday == current_day - 1) ||
-				           (savedate->tm_year == current_year - 1 && savedate->tm_mon == 11 &&
-				            current_month == 0 && savedate->tm_mday == 31 &&
-				            current_day == 1)) {  // Yesterday
-					// Adding the 0 padding in a separate statement so translators won't have to deal
-					// with it
-					const std::string minute = (boost::format("%02u") % savedate->tm_min).str();
-
-					/** TRANSLATORS: Display date for choosing a savegame/replay */
-					/** TRANSLATORS: hour:minute */
-					gamedata.savedatestring =
-					   (boost::format(_("Yesterday, %1%:%2%")) % savedate->tm_hour % minute).str();
-				} else {  // Older
-
-					/** TRANSLATORS: Display date for choosing a savegame/replay */
-					/** TRANSLATORS: month day, year */
-					gamedata.savedatestring =
-					   (boost::format(_("%2% %1%, %3%")) % savedate->tm_mday %
-					    localize_month(savedate->tm_mon) % (1900 + savedate->tm_year))
-					      .str();
-				}
-			}
-
-			gamedata.wincondition = _(gpdp.get_localized_win_condition());
-			gamedata.minimap_path = gpdp.get_minimap_path();
-			games_data_.push_back(gamedata);
-
-			UI::Table<uintptr_t const>::EntryRecord& te = table_.add(games_data_.size() - 1);
-			te.set_string(0, gamedata.savedatestring);
-
-			if (is_replay_ || settings_->settings().multiplayer) {
-				std::string gametypestring;
-				switch (gamedata.gametype) {
-				case GameController::GameType::SINGLEPLAYER:
-					/** TRANSLATORS: "Single Player" entry in the Game Mode table column. */
-					/** TRANSLATORS: "Keep this to 6 letters maximum. */
-					/** TRANSLATORS: A tooltip will explain the abbreviation. */
-					/** TRANSLATORS: Make sure that this translation is consistent with the tooltip. */
-					gametypestring = _("SP");
-					break;
-				case GameController::GameType::NETHOST:
-					/** TRANSLATORS: "Multiplayer Host" entry in the Game Mode table column. */
-					/** TRANSLATORS: "Keep this to 2 letters maximum. */
-					/** TRANSLATORS: A tooltip will explain the abbreviation. */
-					/** TRANSLATORS: Make sure that this translation is consistent with the tooltip. */
-					/** TRANSLATORS: %1% is the number of players */
-					gametypestring =
-					   (boost::format(_("H (%1%)")) % static_cast<unsigned int>(gamedata.nrplayers))
-					      .str();
-					break;
-				case GameController::GameType::NETCLIENT:
-					/** TRANSLATORS: "Multiplayer" entry in the Game Mode table column. */
-					/** TRANSLATORS: "Keep this to 2 letters maximum. */
-					/** TRANSLATORS: A tooltip will explain the abbreviation. */
-					/** TRANSLATORS: Make sure that this translation is consistent with the tooltip. */
-					/** TRANSLATORS: %1% is the number of players */
-					gametypestring =
-					   (boost::format(_("MP (%1%)")) % static_cast<unsigned int>(gamedata.nrplayers))
-					      .str();
-					break;
-				case GameController::GameType::REPLAY:
-					gametypestring = "";
-					break;
-				}
-				te.set_string(1, gametypestring);
-				te.set_string(2, map_filename(gamedata.filename, gamedata.mapname));
-			} else {
-				te.set_string(1, map_filename(gamedata.filename, gamedata.mapname));
-			}
-		} catch (const WException& e) {
-			//  we simply skip illegal entries
-			gamedata.errormessage =
-			   ((boost::format("%s\n\n%s\n\n%s"))
-			    /** TRANSLATORS: Error message introduction for when an old savegame can't be loaded */
-			    % _("This file has the wrong format and can’t be loaded."
-			        " Maybe it was created with an older version of Widelands.")
-			    /** TRANSLATORS: This text is on a separate line with an error message below */
-			    % _("Error message:") % e.what())
-			      .str();
-
-			const std::string fs_filename =
-			   FileSystem::filename_without_ext(gamedata.filename.c_str());
-			gamedata.mapname = fs_filename;
-			games_data_.push_back(gamedata);
-
-			UI::Table<uintptr_t const>::EntryRecord& te = table_.add(games_data_.size() - 1);
-			te.set_string(0, "");
-			if (is_replay_ || settings_->settings().multiplayer) {
-				te.set_string(1, "");
-				/** TRANSLATORS: Prefix for incompatible files in load game screens */
-				te.set_string(2, (boost::format(_("Incompatible: %s")) % fs_filename).str());
-			} else {
-				te.set_string(1, (boost::format(_("Incompatible: %s")) % fs_filename).str());
-			}
-		}
-	}
-	table_.sort();
-
-	if (table_.size()) {
-		table_.select(0);
-	}
-	set_has_selection();
+	load_or_save_.fill_table();
 }
 
 bool FullscreenMenuLoadGame::handle_key(bool down, SDL_Keysym code) {
@@ -692,7 +142,7 @@
 			break;
 	/* no break */
 	case SDLK_DELETE:
-		clicked_delete();
+		load_or_save_.clicked_delete();
 		return true;
 	default:
 		break;

=== modified file 'src/ui_fsmenu/loadgame.h'
--- src/ui_fsmenu/loadgame.h	2017-01-26 09:28:40 +0000
+++ src/ui_fsmenu/loadgame.h	2017-04-21 07:38:25 +0000
@@ -22,52 +22,15 @@
 
 #include "ui_fsmenu/base.h"
 
-#include <memory>
-
-#include "graphic/image.h"
+#include "logic/game.h"
 #include "logic/game_controller.h"
+#include "logic/game_settings.h"
+#include "ui_basic/box.h"
 #include "ui_basic/button.h"
-#include "ui_basic/icon.h"
-#include "ui_basic/multilinetextarea.h"
-#include "ui_basic/table.h"
+#include "ui_basic/panel.h"
 #include "ui_basic/textarea.h"
 #include "ui_fsmenu/load_map_or_game.h"
-
-namespace Widelands {
-class EditorGameBase;
-class Game;
-class Map;
-class MapLoader;
-}
-class Image;
-class RenderTarget;
-class GameController;
-struct GameSettingsProvider;
-
-/**
- * Data about a savegame/replay that we're interested in.
- */
-struct SavegameData {
-	std::string filename;
-	std::string mapname;
-	std::string wincondition;
-	std::string minimap_path;
-	std::string savedatestring;
-	std::string errormessage;
-
-	uint32_t gametime;
-	uint32_t nrplayers;
-	std::string version;
-	time_t savetimestamp;
-	GameController::GameType gametype;
-
-	SavegameData()
-	   : gametime(0),
-	     nrplayers(0),
-	     savetimestamp(0),
-	     gametype(GameController::GameType::SINGLEPLAYER) {
-	}
-};
+#include "wui/load_or_save_game.h"
 
 /// Select a Saved Game in Fullscreen Mode. It's a modal fullscreen menu.
 class FullscreenMenuLoadGame : public FullscreenMenuLoadMapOrGame {
@@ -77,55 +40,41 @@
 	                       GameController* gc = nullptr,
 	                       bool is_replay = false);
 
+	/// Ths currently selected filename
 	const std::string& filename() {
 		return filename_;
 	}
 
-	void think() override;
-
 	bool handle_key(bool down, SDL_Keysym code) override;
 
 protected:
+	/// Sets the current selected filename and ends the modal screen with 'Ok' status.
 	void clicked_ok() override;
+
+	/// Update button status and game details
 	void entry_selected() override;
+
+	/// Fill load_or_save_'s table
 	void fill_table() override;
 
 private:
 	void layout() override;
 
 	/// Updates buttons and text labels and returns whether a table entry is selected.
-	bool set_has_selection();
 	bool compare_date_descending(uint32_t, uint32_t);
-	void clicked_delete();
-	std::string filename_list_string();
-
-	UI::Table<uintptr_t const> table_;
-
-	bool is_replay_;
-
+
+	UI::Box main_box_;
+	UI::Box info_box_;
 	UI::Textarea title_;
-	UI::Textarea label_mapname_;
-	UI::MultilineTextarea ta_mapname_;  // Multiline for long names
-	UI::Textarea label_gametime_;
-	UI::MultilineTextarea ta_gametime_;  // Multiline because we want tooltips
-	UI::Textarea label_players_;
-	UI::MultilineTextarea ta_players_;
-	UI::Textarea label_version_;
-	UI::Textarea ta_version_;
-	UI::Textarea label_win_condition_;
-	UI::MultilineTextarea ta_win_condition_;
-
-	UI::Button delete_;
-
-	UI::MultilineTextarea ta_long_generic_message_;
-
-	int32_t const minimap_y_, minimap_w_, minimap_h_;
-	UI::Icon minimap_icon_;
-	std::unique_ptr<const Image> minimap_image_;
-
-	std::vector<SavegameData> games_data_;
+
+	LoadOrSaveGame load_or_save_;
+
+	UI::Button* delete_;
+	// TODO(GunChleoc): Get rid of this hack once everything is 100% box layout
+	UI::Panel* button_spacer_;
 	std::string filename_;
 
+	bool is_replay_;
 	Widelands::Game& game_;
 	GameSettingsProvider* settings_;
 	GameController* ctrl_;

=== modified file 'src/wui/CMakeLists.txt'
--- src/wui/CMakeLists.txt	2017-03-04 06:55:30 +0000
+++ src/wui/CMakeLists.txt	2017-04-21 07:38:25 +0000
@@ -71,6 +71,10 @@
 
 wl_library(wui_common
   SRCS
+    gamedetails.cc
+    gamedetails.h
+    load_or_save_game.cc
+    load_or_save_game.h
     mapdetails.cc
     mapdetails.h
     mapdata.cc

=== modified file 'src/wui/game_main_menu_save_game.cc'
--- src/wui/game_main_menu_save_game.cc	2017-02-26 12:16:09 +0000
+++ src/wui/game_main_menu_save_game.cc	2017-04-21 07:38:25 +0000
@@ -22,7 +22,6 @@
 #include <boost/format.hpp>
 
 #include "base/i18n.h"
-#include "base/time_string.h"
 #include "game_io/game_loader.h"
 #include "game_io/game_preload_packet.h"
 #include "game_io/game_saver.h"
@@ -31,188 +30,132 @@
 #include "logic/game.h"
 #include "logic/game_controller.h"
 #include "logic/playersmanager.h"
+#include "ui_basic/messagebox.h"
 #include "wui/interactive_gamebase.h"
 
-namespace {
-
-#define WINDOW_WIDTH 440
-#define WINDOW_HEIGHT 440
-#define VMARGIN 5
-#define VSPACING 5
-#define HSPACING 5
-#define BUTTON_HEIGHT 20
-#define LIST_WIDTH 280
-#define LIST_HEIGHT (WINDOW_HEIGHT - 2 * VMARGIN - VSPACING)
-#define EDITBOX_Y (WINDOW_HEIGHT - 24 - VMARGIN)
-#define DESCRIPTION_X (VMARGIN + LIST_WIDTH + VSPACING)
-#define DESCRIPTION_WIDTH (WINDOW_WIDTH - DESCRIPTION_X - VMARGIN)
-#define CANCEL_Y (WINDOW_HEIGHT - BUTTON_HEIGHT - VMARGIN)
-#define DELETE_Y (CANCEL_Y - BUTTON_HEIGHT - VSPACING)
-#define OK_Y (DELETE_Y - BUTTON_HEIGHT - VSPACING)
-
-}  // namespace
-
 InteractiveGameBase& GameMainMenuSaveGame::igbase() {
 	return dynamic_cast<InteractiveGameBase&>(*get_parent());
 }
 
 GameMainMenuSaveGame::GameMainMenuSaveGame(InteractiveGameBase& parent,
                                            UI::UniqueWindow::Registry& registry)
-   : UI::UniqueWindow(&parent, "save_game", &registry, WINDOW_WIDTH, WINDOW_HEIGHT, _("Save Game")),
-     editbox_(this,
-              HSPACING,
-              EDITBOX_Y,
-              LIST_WIDTH,
-              0,
-              2,
-              g_gr->images().get("images/ui_basic/but1.png")),
-     ls_(this,
-         HSPACING,
-         VSPACING,
-         LIST_WIDTH,
-         LIST_HEIGHT - editbox_.get_h(),
-         g_gr->images().get("images/ui_basic/but1.png")),
-     name_label_(this, DESCRIPTION_X, 5, 0, 20, _("Map Name:")),
-     mapname_(this, DESCRIPTION_X, 20, 0, 20),
-     gametime_label_(this, DESCRIPTION_X, 45, 0, 20, _("Game Time:")),
-     gametime_(this, DESCRIPTION_X, 60, 0, 20),
-     players_label_(this, DESCRIPTION_X, 85, 0, 20),
-     win_condition_label_(this, DESCRIPTION_X, 110, 0, 20, _("Win condition:")),
-     win_condition_(this, DESCRIPTION_X, 125, 0, 20),
+   : UI::UniqueWindow(&parent,
+                      "save_game",
+                      &registry,
+                      parent.get_inner_w() - 40,
+                      parent.get_inner_h() - 40,
+                      _("Save Game")),
+     // Values for alignment and size
+     padding_(4),
+     butw_(150),
+
+     main_box_(this, 0, 0, UI::Box::Vertical),
+     info_box_(&main_box_, 0, 0, UI::Box::Horizontal),
+     filename_box_(&main_box_, 0, 0, UI::Box::Horizontal),
+     buttons_box_(&main_box_, 0, 0, UI::Box::Horizontal),
+
+     load_or_save_(&info_box_,
+                   igbase().game(),
+                   LoadOrSaveGame::FileType::kGame,
+                   GameDetails::Style::kWui,
+                   false),
+
+     editbox_label_(&filename_box_, 0, 0, 0, 0, _("Filename:"), UI::Align::kLeft),
+     editbox_(&filename_box_, 0, 0, 0, 0, 2, g_gr->images().get("images/ui_basic/but1.png")),
+
+     cancel_(&buttons_box_,
+             "cancel",
+             0,
+             0,
+             butw_,
+             0,
+             g_gr->images().get("images/ui_basic/but1.png"),
+             _("Cancel")),
+     ok_(&buttons_box_,
+         "ok",
+         0,
+         0,
+         butw_,
+         0,
+         g_gr->images().get("images/ui_basic/but5.png"),
+         _("OK")),
+
      curdir_(SaveHandler::get_base_dir()) {
+
+	layout();
+
+	main_box_.add_space(padding_);
+	main_box_.set_inner_spacing(padding_);
+	main_box_.add(&info_box_, UI::Box::Resizing::kExpandBoth);
+	main_box_.add_space(padding_);
+	main_box_.add(&filename_box_, UI::Box::Resizing::kFullSize);
+	main_box_.add_space(0);
+	main_box_.add(&buttons_box_, UI::Box::Resizing::kFullSize);
+
+	info_box_.set_inner_spacing(padding_);
+	info_box_.add_space(padding_);
+	info_box_.add(&load_or_save_.table(), UI::Box::Resizing::kFullSize);
+	info_box_.add(load_or_save_.game_details(), UI::Box::Resizing::kExpandBoth);
+
+	filename_box_.set_inner_spacing(padding_);
+	filename_box_.add_space(padding_);
+	filename_box_.add(&editbox_label_, UI::Box::Resizing::kAlign, UI::Align::kCenter);
+	filename_box_.add(&editbox_, UI::Box::Resizing::kFillSpace);
+
+	buttons_box_.set_inner_spacing(padding_);
+	buttons_box_.add_space(padding_);
+	buttons_box_.add_inf_space();
+	buttons_box_.add_inf_space();
+	buttons_box_.add(&cancel_, UI::Box::Resizing::kAlign, UI::Align::kCenter);
+	buttons_box_.add_inf_space();
+	buttons_box_.add(&ok_, UI::Box::Resizing::kAlign, UI::Align::kCenter);
+	buttons_box_.add_inf_space();
+	buttons_box_.add_inf_space();
+
+	ok_.set_enabled(false);
+
 	editbox_.changed.connect(boost::bind(&GameMainMenuSaveGame::edit_box_changed, this));
 	editbox_.ok.connect(boost::bind(&GameMainMenuSaveGame::ok, this));
 
-	button_ok_ = new UI::Button(this, "ok", DESCRIPTION_X, OK_Y, DESCRIPTION_WIDTH, BUTTON_HEIGHT,
-	                            g_gr->images().get("images/ui_basic/but4.png"), _("OK"));
-	button_ok_->sigclicked.connect(boost::bind(&GameMainMenuSaveGame::ok, this));
-
-	UI::Button* cancelbtn =
-	   new UI::Button(this, "cancel", DESCRIPTION_X, CANCEL_Y, DESCRIPTION_WIDTH, BUTTON_HEIGHT,
-	                  g_gr->images().get("images/ui_basic/but4.png"), _("Cancel"));
-	cancelbtn->sigclicked.connect(boost::bind(&GameMainMenuSaveGame::die, this));
-
-	UI::Button* deletebtn =
-	   new UI::Button(this, "delete", DESCRIPTION_X, DELETE_Y, DESCRIPTION_WIDTH, BUTTON_HEIGHT,
-	                  g_gr->images().get("images/ui_basic/but4.png"), _("Delete"));
-	deletebtn->sigclicked.connect(boost::bind(&GameMainMenuSaveGame::delete_clicked, this));
-
-	ls_.selected.connect(boost::bind(&GameMainMenuSaveGame::selected, this, _1));
-	ls_.double_clicked.connect(boost::bind(&GameMainMenuSaveGame::double_clicked, this, _1));
-
-	fill_list();
+	ok_.sigclicked.connect(boost::bind(&GameMainMenuSaveGame::ok, this));
+	cancel_.sigclicked.connect(boost::bind(&GameMainMenuSaveGame::die, this));
+
+	load_or_save_.table().selected.connect(boost::bind(&GameMainMenuSaveGame::entry_selected, this));
+	load_or_save_.table().double_clicked.connect(
+		boost::bind(&GameMainMenuSaveGame::ok, this));
+
+	load_or_save_.fill_table();
+	load_or_save_.select_by_name(parent.game().save_handler().get_cur_filename());
 
 	center_to_parent();
 	move_to_top();
 
-	std::string cur_filename = parent.game().save_handler().get_cur_filename();
-	if (!cur_filename.empty()) {
-		select_by_name(cur_filename);
-	} else {
-		// Display current game infos
-		{
-			// Try to translate the map name.
-			i18n::Textdomain td("maps");
-			mapname_.set_text(_(parent.game().get_map()->get_name()));
-		}
-		uint32_t gametime = parent.game().get_gametime();
-		gametime_.set_text(gametimestring(gametime));
-
-		int player_nr = parent.game().player_manager()->get_number_of_players();
-		players_label_.set_text(
-		   (boost::format(ngettext("%i player", "%i players", player_nr)) % player_nr).str());
-		{
-			i18n::Textdomain td("win_conditions");
-			win_condition_.set_text(_(parent.game().get_win_condition_displayname()));
-		}
-	}
-
-	editbox_.focus();
 	pause_game(true);
-}
-
-/**
- * called when a item is selected
- */
-void GameMainMenuSaveGame::selected(uint32_t) {
-	const std::string& name = ls_.get_selected();
-
-	Widelands::GameLoader gl(name, igbase().game());
-	Widelands::GamePreloadPacket gpdp;
-	gl.preload_game(gpdp);  //  This has worked before, no problem
-	{ editbox_.set_text(FileSystem::filename_without_ext(name.c_str())); }
-	edit_box_changed();
-
-	// Try to translate the map name.
-	{
-		i18n::Textdomain td("maps");
-		mapname_.set_text(_(gpdp.get_mapname()));
-	}
-
-	uint32_t gametime = gpdp.get_gametime();
-	gametime_.set_text(gametimestring(gametime));
-
-	if (gpdp.get_number_of_players() > 0) {
-		const std::string text =
-		   (boost::format(ngettext("%u Player", "%u Players", gpdp.get_number_of_players())) %
-		    static_cast<unsigned int>(gpdp.get_number_of_players()))
-		      .str();
-		players_label_.set_text(text);
-	} else {
-		// Keep label empty
-		players_label_.set_text("");
-	}
-	win_condition_.set_text(_(gpdp.get_localized_win_condition()));
-}
-
-/**
- * An Item has been doubleclicked
- */
-void GameMainMenuSaveGame::double_clicked(uint32_t) {
-	ok();
-}
-
-/*
- * fill the file list
- */
-void GameMainMenuSaveGame::fill_list() {
-	ls_.clear();
-	FilenameSet gamefiles;
-
-	//  Fill it with all files we find.
-	gamefiles = g_fs->list_directory(curdir_);
-
-	Widelands::GamePreloadPacket gpdp;
-
-	for (FilenameSet::iterator pname = gamefiles.begin(); pname != gamefiles.end(); ++pname) {
-		char const* const name = pname->c_str();
-
-		try {
-			Widelands::GameLoader gl(name, igbase().game());
-			gl.preload_game(gpdp);
-			ls_.add(FileSystem::filename_without_ext(name), name);
-		} catch (const WException&) {
-		}  //  we simply skip illegal entries
-	}
-	edit_box_changed();
-}
-
-void GameMainMenuSaveGame::select_by_name(std::string name) {
-	for (uint32_t idx = 0; idx < ls_.size(); idx++) {
-		const std::string val = ls_[idx];
-		if (name == val) {
-			ls_.select(idx);
-			return;
-		}
-	}
-}
-
-/*
- * The editbox was changed. Enable ok button
- */
+	set_thinks(false);
+}
+
+void GameMainMenuSaveGame::layout() {
+	main_box_.set_size(get_inner_w() - 2 * padding_, get_inner_h() - 2 * padding_);
+	load_or_save_.table().set_desired_size(get_inner_w() * 7 / 12, load_or_save_.table().get_h());
+}
+
+void GameMainMenuSaveGame::entry_selected() {
+	// TODO(GunChleoc): When editbox is focused, multiselect is not possible, because it steals the
+	// key presses.
+	ok_.set_enabled(load_or_save_.table().selections().size() == 1);
+	load_or_save_.delete_button()->set_enabled(load_or_save_.has_selection());
+	if (load_or_save_.has_selection()) {
+		const SavegameData& gamedata = *load_or_save_.entry_selected();
+		editbox_.set_text(FileSystem::filename_without_ext(gamedata.filename.c_str()));
+	}
+}
+
 void GameMainMenuSaveGame::edit_box_changed() {
 	// Prevent the user from creating nonsense directory names, like e.g. ".." or "...".
-	button_ok_->set_enabled(LayeredFileSystem::is_legal_filename(editbox_.text()));
+	const bool is_legal_filename = LayeredFileSystem::is_legal_filename(editbox_.text());
+	ok_.set_enabled(is_legal_filename);
+	load_or_save_.delete_button()->set_enabled(false);
+	load_or_save_.clear_selections();
 }
 
 static void dosave(InteractiveGameBase& igbase, const std::string& complete_filename) {
@@ -258,9 +201,6 @@
 	std::string const filename_;
 };
 
-/**
- * Called when the Ok button is clicked or the Return key pressed in the edit box.
- */
 void GameMainMenuSaveGame::ok() {
 	if (editbox_.text().empty())
 		return;
@@ -282,42 +222,6 @@
 	UI::UniqueWindow::die();
 }
 
-struct DeletionMessageBox : public UI::WLMessageBox {
-	DeletionMessageBox(GameMainMenuSaveGame& parent, const std::string& filename)
-	   : UI::WLMessageBox(&parent,
-	                      _("File deletion"),
-	                      str(boost::format(_("Do you really want to delete the file %s?")) %
-	                          FileSystem::fs_filename(filename.c_str())),
-	                      MBoxType::kOkCancel),
-	     filename_(filename) {
-	}
-
-	void clicked_ok() override {
-		g_fs->fs_unlink(filename_);
-		dynamic_cast<GameMainMenuSaveGame&>(*get_parent()).fill_list();
-		die();
-	}
-
-	void clicked_back() override {
-		die();
-	}
-
-private:
-	std::string const filename_;
-};
-
-/**
- * Called when the delete button has been clicked
- */
-void GameMainMenuSaveGame::delete_clicked() {
-	std::string const complete_filename =
-	   igbase().game().save_handler().create_file_name(curdir_, editbox_.text());
-
-	//  Check if file exists. If it does, let the user confirm the deletion.
-	if (g_fs->file_exists(complete_filename))
-		new DeletionMessageBox(*this, complete_filename);
-}
-
 void GameMainMenuSaveGame::pause_game(bool paused) {
 	if (igbase().is_multiplayer()) {
 		return;

=== modified file 'src/wui/game_main_menu_save_game.h'
--- src/wui/game_main_menu_save_game.h	2017-01-25 18:55:59 +0000
+++ src/wui/game_main_menu_save_game.h	2017-04-21 07:38:25 +0000
@@ -21,43 +21,61 @@
 #define WL_WUI_GAME_MAIN_MENU_SAVE_GAME_H
 
 #include "base/i18n.h"
+#include "ui_basic/box.h"
 #include "ui_basic/button.h"
 #include "ui_basic/editbox.h"
-#include "ui_basic/listselect.h"
-#include "ui_basic/messagebox.h"
 #include "ui_basic/textarea.h"
 #include "ui_basic/unique_window.h"
+#include "wui/load_or_save_game.h"
 
 class InteractiveGameBase;
 
+/// Displays a warning if the filename to be saved to already esists
 struct SaveWarnMessageBox;
+
+/// A window that lets the user save the current game and delete savegames.
 struct GameMainMenuSaveGame : public UI::UniqueWindow {
 	friend struct SaveWarnMessageBox;
 	GameMainMenuSaveGame(InteractiveGameBase&, UI::UniqueWindow::Registry& registry);
 
-	void fill_list();
-	void select_by_name(std::string name);
-
 protected:
 	void die() override;
 
 private:
+	void layout() override;
 	InteractiveGameBase& igbase();
-	void selected(uint32_t);
-	void double_clicked(uint32_t);
+
+	/// Update button status and game details and prefill the edibox.
+	void entry_selected();
+
+	/// Update buttons and table selection state
 	void edit_box_changed();
+
+	/// Called when the OK button is clicked or the Return key pressed in the edit box.
 	void ok();
-	void delete_clicked();
-
-	bool save_game(std::string);
+
+	/// Saves the current game to 'filename'
+	bool save_game(std::string filename);
+
+	/// Pause/unpause the game
 	void pause_game(bool paused);
 
+	// UI coordinates and spacers
+	int32_t const padding_;  // Common padding between panels
+	int32_t const butw_;     // Button dimensions
+
+	UI::Box main_box_;
+	UI::Box info_box_;
+	UI::Box filename_box_;
+	UI::Box buttons_box_;
+
+	LoadOrSaveGame load_or_save_;
+	UI::Button* delete_;
+
+	UI::Textarea editbox_label_;
 	UI::EditBox editbox_;
-	UI::Listselect<std::string> ls_;
 
-	UI::Textarea name_label_, mapname_, gametime_label_, gametime_, players_label_,
-	   win_condition_label_, win_condition_;
-	UI::Button* button_ok_;
+	UI::Button cancel_, ok_;
 	std::string curdir_;
 	std::string parentdir_;
 	std::string filename_;

=== added file 'src/wui/gamedetails.cc'
--- src/wui/gamedetails.cc	1970-01-01 00:00:00 +0000
+++ src/wui/gamedetails.cc	2017-04-21 07:38:25 +0000
@@ -0,0 +1,246 @@
+/*
+ * Copyright (C) 2016 by the Widelands Development Team
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ *
+ */
+
+#include "wui/gamedetails.h"
+
+#include <memory>
+
+#include <boost/algorithm/string/replace.hpp>
+#include <boost/lexical_cast.hpp>
+#include <boost/format.hpp>
+
+#include "base/i18n.h"
+#include "base/log.h"
+#include "base/time_string.h"
+#include "graphic/graphic.h"
+#include "graphic/image_io.h"
+#include "graphic/text_constants.h"
+#include "graphic/texture.h"
+#include "io/filesystem/layered_filesystem.h"
+
+// TODO(GunChleoc): Arabic: line height broken for descriptions for Arabic.
+namespace {
+// 'is_first' omits the vertical gap before the line.
+// 'noescape' is needed for error message formatting and does not call richtext_escape.
+std::string as_header_with_content(const std::string& header,
+                                   const std::string& content,
+                                   GameDetails::Style style,
+                                   bool is_first = false,
+                                   bool noescape = false) {
+	switch (style) {
+	case GameDetails::Style::kFsMenu:
+		return (boost::format(
+		           "<p><font size=%i bold=1 shadow=1>%s%s <font color=D1D1D1>%s</font></font></p>") %
+		        UI_FONT_SIZE_SMALL % (is_first ? "" : "<vspace gap=9>") %
+		        (noescape ? header : richtext_escape(header)) %
+		        (noescape ? content : richtext_escape(content)))
+		   .str();
+	case GameDetails::Style::kWui:
+		return (boost::format(
+		           "<p><font size=%i>%s<font bold=1 color=D1D1D1>%s</font> %s</font></p>") %
+		        UI_FONT_SIZE_SMALL % (is_first ? "" : "<vspace gap=6>") %
+		        (noescape ? header : richtext_escape(header)) %
+		        (noescape ? content : richtext_escape(content)))
+		   .str();
+	default:
+		NEVER_HERE();
+	}
+}
+
+}  // namespace
+
+SavegameData::SavegameData()
+   : gametime(""),
+     nrplayers("0"),
+     savetimestamp(0),
+     gametype(GameController::GameType::SINGLEPLAYER) {
+}
+
+void SavegameData::set_gametime(uint32_t input_gametime) {
+	gametime = gametimestring(input_gametime);
+}
+void SavegameData::set_nrplayers(Widelands::PlayerNumber input_nrplayers) {
+	nrplayers = boost::lexical_cast<std::string>(static_cast<unsigned int>(input_nrplayers));
+}
+void SavegameData::set_mapname(const std::string& input_mapname) {
+	i18n::Textdomain td("maps");
+	mapname = _(input_mapname);
+}
+
+GameDetails::GameDetails(Panel* parent, Style style)
+   : UI::Box(parent, 0, 0, UI::Box::Vertical),
+     style_(style),
+     padding_(4),
+     name_label_(
+        this,
+        0,
+        0,
+        0,
+        0,
+        "",
+        UI::Align::kLeft,
+        g_gr->images().get(style == GameDetails::Style::kFsMenu ? "images/ui_basic/but3.png" :
+                                                                  "images/ui_basic/but1.png"),
+        UI::MultilineTextarea::ScrollMode::kNoScrolling),
+     descr_(this,
+            0,
+            0,
+            0,
+            0,
+            "",
+            UI::Align::kLeft,
+            g_gr->images().get(style == GameDetails::Style::kFsMenu ? "images/ui_basic/but3.png" :
+                                                                      "images/ui_basic/but1.png"),
+            UI::MultilineTextarea::ScrollMode::kNoScrolling),
+     minimap_icon_(this, 0, 0, 0, 0, nullptr),
+     button_box_(new UI::Box(this, 0, 0, UI::Box::Vertical)) {
+	name_label_.force_new_renderer();
+	descr_.force_new_renderer();
+
+	add(&name_label_, UI::Box::Resizing::kFullSize);
+	add_space(padding_);
+	add(&descr_, UI::Box::Resizing::kExpandBoth);
+	add_space(padding_);
+	add(&minimap_icon_, UI::Box::Resizing::kAlign, UI::Align::kCenter);
+	add_space(padding_);
+	add(button_box_, UI::Box::Resizing::kFullSize);
+
+	minimap_icon_.set_visible(false);
+	minimap_icon_.set_frame(UI_FONT_CLR_FG);
+}
+
+void GameDetails::clear() {
+	name_label_.set_text("");
+	descr_.set_text("");
+	minimap_icon_.set_icon(nullptr);
+	minimap_icon_.set_visible(false);
+	minimap_icon_.set_size(0, 0);
+	minimap_image_.reset();
+}
+
+void GameDetails::update(const SavegameData& gamedata) {
+	clear();
+
+	if (gamedata.errormessage.empty()) {
+		if (gamedata.filename_list.empty()) {
+			name_label_.set_text(
+			   (boost::format("<rt>%s</rt>") %
+			    as_header_with_content(_("Map Name:"), gamedata.mapname, style_, true))
+			      .str());
+
+			name_label_.set_tooltip(gamedata.gametype == GameController::GameType::REPLAY ?
+			                           _("The map that this replay is based on") :
+			                           _("The map that this game is based on"));
+
+			// Show game information
+			std::string description =
+			   as_header_with_content(_("Game Time:"), gamedata.gametime, style_);
+
+			description = (boost::format("%s%s") % description %
+			               as_header_with_content(_("Players:"), gamedata.nrplayers, style_))
+			                 .str();
+
+			description = (boost::format("%s%s") % description %
+			               as_header_with_content(_("Widelands Version:"), gamedata.version, style_))
+			                 .str();
+
+			description = (boost::format("%s%s") % description %
+			               as_header_with_content(_("Win Condition:"), gamedata.wincondition, style_))
+			                 .str();
+
+			description = (boost::format("<rt>%s</rt>") % description).str();
+			descr_.set_text(description);
+
+			std::string minimap_path = gamedata.minimap_path;
+			if (!minimap_path.empty()) {
+				try {
+					// Load the image
+					minimap_image_ = load_image(
+					   minimap_path,
+					   std::unique_ptr<FileSystem>(g_fs->make_sub_file_system(gamedata.filename)).get());
+					minimap_icon_.set_visible(true);
+					minimap_icon_.set_icon(minimap_image_.get());
+				} catch (const std::exception& e) {
+					log("Failed to load the minimap image : %s\n", e.what());
+				}
+			}
+		} else {
+			std::string filename_list = richtext_escape(gamedata.filename_list);
+			boost::replace_all(filename_list, "\n", "<br> • ");
+			name_label_.set_text((boost::format("<rt>%s</rt>") %
+			                      as_header_with_content(gamedata.mapname, "", style_, true))
+			                        .str());
+
+			descr_.set_text((boost::format("<rt>%s</rt>") %
+			                 as_header_with_content("", filename_list, style_, true, true))
+			                   .str());
+			minimap_icon_.set_visible(false);
+		}
+	} else {
+		name_label_.set_text(
+		   (boost::format("<rt>%s</rt>") %
+		    as_header_with_content(_("Error:"), gamedata.errormessage, style_, true, true))
+		      .str());
+	}
+	layout();
+}
+
+void GameDetails::layout() {
+	if (get_w() == 0 && get_h() == 0) {
+		return;
+	}
+	UI::Box::layout();
+	if (minimap_icon_.icon() == nullptr) {
+		descr_.set_scrollmode(UI::MultilineTextarea::ScrollMode::kScrollNormal);
+		minimap_icon_.set_desired_size(0, 0);
+	} else {
+		descr_.set_scrollmode(UI::MultilineTextarea::ScrollMode::kNoScrolling);
+
+		// Scale the minimap image.
+		const float available_width = get_w() - 4 * padding_;
+		const float available_height =
+			get_h() - name_label_.get_h() - descr_.get_h() - button_box_->get_h() - 4 * padding_;
+
+		// Scale it
+		float scale = available_width / minimap_image_->width();
+		const float scale_y = available_height / minimap_image_->height();
+		if (scale_y < scale) {
+			scale = scale_y;
+		}
+		// Don't make the image too big; fuzziness will result
+		scale = std::min(1.f, scale);
+
+		const int w = scale * minimap_image_->width();
+		const int h = scale * minimap_image_->height();
+
+		// Center the minimap in the available space
+		const int xpos = (get_w() - w) / 2;
+		int ypos = name_label_.get_h() + descr_.get_h() + 2 * padding_;
+
+		// Set small minimaps higher up for a more harmonious look
+		if (h < available_height * 2 / 3) {
+			ypos += (available_height - h) / 3;
+		} else {
+			ypos += (available_height - h) / 2;
+		}
+
+		minimap_icon_.set_desired_size(w, h);
+		minimap_icon_.set_pos(Vector2i(xpos, ypos));
+	}
+}

=== added file 'src/wui/gamedetails.h'
--- src/wui/gamedetails.h	1970-01-01 00:00:00 +0000
+++ src/wui/gamedetails.h	2017-04-21 07:38:25 +0000
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2016 by the Widelands Development Team
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ *
+ */
+
+#ifndef WL_WUI_GAMEDETAILS_H
+#define WL_WUI_GAMEDETAILS_H
+
+#include <memory>
+
+#include "graphic/image.h"
+#include "logic/game_controller.h"
+#include "ui_basic/box.h"
+#include "ui_basic/icon.h"
+#include "ui_basic/multilinetextarea.h"
+
+/**
+ * Data about a savegame/replay that we're interested in.
+ */
+struct SavegameData {
+	/// The filename of the currenty selected file
+	std::string filename;
+	/// List of filenames when lumtiple files have been selected
+	std::string filename_list;
+	/// The name of the map that the game is bases on
+	std::string mapname;
+	/// The win condition that was played
+	std::string wincondition;
+	/// Filename of the minimap or empty if none available
+	std::string minimap_path;
+	/// "saved on ..."
+	std::string savedatestring;
+	/// Verbose date and time
+	std::string savedonstring;
+	/// An error message or empty if no error occurred
+	std::string errormessage;
+
+	/// Compact gametime information
+	std::string gametime;
+	/// Number of players on the map
+	std::string nrplayers;
+	/// The version of Widelands that the game as played with
+	std::string version;
+	/// Gametime as time stamp. For games, it's the time the game ended. For replays, it's the time the game started.
+	time_t savetimestamp;
+	/// Single payer, nethost, netclient or replay
+	GameController::GameType gametype;
+
+	SavegameData();
+
+	/// Converts timestamp to UI string and assigns it to gametime
+	void set_gametime(uint32_t input_gametime);
+	/// Sets the number of players on the map as a string
+	void set_nrplayers(Widelands::PlayerNumber input_nrplayers);
+	/// Sets the mapname as a localized string
+	void set_mapname(const std::string& input_mapname);
+};
+
+/**
+ * Show a Panel with information about a savegame/replay file
+ */
+class GameDetails : public UI::Box {
+public:
+	enum class Style { kFsMenu, kWui };
+
+	GameDetails(Panel* parent, Style style);
+
+	/// Reset the data
+	void clear();
+
+	/// Update the display from the 'gamedata'
+	void update(const SavegameData& gamedata);
+
+	/// Box on the bottom where extra buttons can be placed from the outside, e.g. a delete button.
+	UI::Box* button_box() {
+		return button_box_;
+	}
+
+private:
+	/// Layout the information on screen
+	void layout() override;
+
+	const Style style_;
+	const int padding_;
+
+	UI::MultilineTextarea name_label_;
+	UI::MultilineTextarea descr_;
+	UI::Icon minimap_icon_;
+	std::unique_ptr<const Image> minimap_image_;
+	UI::Box* button_box_;
+};
+
+#endif  // end of include guard: WL_WUI_GAMEDETAILS_H

=== added file 'src/wui/load_or_save_game.cc'
--- src/wui/load_or_save_game.cc	1970-01-01 00:00:00 +0000
+++ src/wui/load_or_save_game.cc	2017-04-21 07:38:25 +0000
@@ -0,0 +1,476 @@
+/*
+ * Copyright (C) 2002-2016 by the Widelands Development Team
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ *
+ */
+
+#include "wui/load_or_save_game.h"
+
+#include <ctime>
+
+#include <boost/algorithm/string.hpp>
+#include <boost/format.hpp>
+
+#include "base/i18n.h"
+#include "base/log.h"
+#include "base/time_string.h"
+#include "game_io/game_loader.h"
+#include "game_io/game_preload_packet.h"
+#include "helper.h"
+#include "io/filesystem/layered_filesystem.h"
+#include "logic/game.h"
+#include "logic/game_controller.h"
+#include "logic/game_settings.h"
+#include "logic/replay.h"
+#include "ui_basic/messagebox.h"
+
+namespace {
+// This function concatenates the filename and localized map name for a savegame/replay.
+// If the filename starts with the map name, the map name is omitted.
+// It also prefixes autosave files with a numbered and localized "Autosave" prefix.
+std::string
+map_filename(const std::string& filename, const std::string& mapname, bool localize_autosave) {
+	std::string result = FileSystem::filename_without_ext(filename.c_str());
+
+	if (localize_autosave && boost::starts_with(result, "wl_autosave")) {
+		std::vector<std::string> autosave_name;
+		boost::split(autosave_name, result, boost::is_any_of("_"));
+		if (autosave_name.empty() || autosave_name.size() < 3) {
+			/** TRANSLATORS: %1% is a map's name. */
+			result = (boost::format(_("Autosave: %1%")) % mapname).str();
+		} else {
+			/** TRANSLATORS: %1% is a number, %2% a map's name. */
+			result = (boost::format(_("Autosave %1%: %2%")) % autosave_name.back() % mapname).str();
+		}
+	} else if (!(boost::starts_with(result, mapname))) {
+		/** TRANSLATORS: %1% is a filename, %2% a map's name. */
+		result = (boost::format(_("%1% (%2%)")) % result % mapname).str();
+	}
+	return result;
+}
+}  // namespace
+
+LoadOrSaveGame::LoadOrSaveGame(UI::Panel* parent,
+                               Widelands::Game& g,
+                               FileType filetype,
+                               GameDetails::Style style,
+                               bool localize_autosave)
+   : parent_(parent),
+     table_(parent,
+            0,
+            0,
+            0,
+            0,
+            g_gr->images().get(style == GameDetails::Style::kFsMenu ? "images/ui_basic/but3.png" :
+                                                                      "images/ui_basic/but1.png"),
+            UI::TableRows::kMultiDescending),
+     filetype_(filetype),
+     localize_autosave_(localize_autosave),
+     // Savegame description
+     game_details_(parent, style),
+     delete_(new UI::Button(game_details()->button_box(),
+                            "delete",
+                            0,
+                            0,
+                            0,
+                            0,
+                            g_gr->images().get("images/ui_basic/but0.png"),
+                            _("Delete"))),
+     game_(g) {
+	table_.add_column(130, _("Save Date"), _("The date this game was saved"), UI::Align::kLeft);
+	if (filetype_ != FileType::kGameSinglePlayer) {
+		std::vector<std::string> modes;
+		if (filetype_ == FileType::kReplay) {
+			/** TRANSLATORS: Tooltip for the "Mode" column when choosing a game/replay to load. */
+			/** TRANSLATORS: Make sure that you keep consistency in your translation. */
+			modes.push_back(_("SP = Single Player"));
+		}
+		/** TRANSLATORS: Tooltip for the "Mode" column when choosing a game/replay to load. */
+		/** TRANSLATORS: Make sure that you keep consistency in your translation. */
+		modes.push_back(_("MP = Multiplayer"));
+		/** TRANSLATORS: Tooltip for the "Mode" column when choosing a game/replay to load. */
+		/** TRANSLATORS: Make sure that you keep consistency in your translation. */
+		modes.push_back(_("H = Multiplayer (Host)"));
+		const std::string mode_tooltip_1 =
+		   /** TRANSLATORS: Tooltip for the "Mode" column when choosing a game/replay to load. */
+		   /** TRANSLATORS: %s is a list of game modes. */
+		   ((boost::format(_("Game Mode: %s.")) %
+		     i18n::localize_list(modes, i18n::ConcatenateWith::COMMA)))
+		      .str();
+		const std::string mode_tooltip_2 = _("Numbers are the number of players.");
+
+		table_.add_column(
+		   65,
+		   /** TRANSLATORS: Game Mode table column when choosing a game/replay to load. */
+		   /** TRANSLATORS: Keep this to 5 letters maximum. */
+		   /** TRANSLATORS: A tooltip will explain if you need to use an abbreviation. */
+		   _("Mode"), (boost::format("%s %s") % mode_tooltip_1 % mode_tooltip_2).str());
+	}
+	table_.add_column(0, _("Description"),
+	                  _("The filename that the game was saved under followed by the map’s name, "
+	                    "or the map’s name followed by the last objective achieved."),
+	                  UI::Align::kLeft, UI::TableColumnType::kFlexible);
+	table_.set_column_compare(
+	   0, boost::bind(&LoadOrSaveGame::compare_date_descending, this, _1, _2));
+	table_.set_sort_column(0);
+	table_.focus();
+	fill_table();
+
+	game_details_.button_box()->add(delete_, style == GameDetails::Style::kFsMenu ?
+	                                            UI::Box::Resizing::kAlign :
+	                                            UI::Box::Resizing::kFullSize,
+	                                UI::Align::kLeft);
+	delete_->set_enabled(false);
+	delete_->sigclicked.connect(boost::bind(&LoadOrSaveGame::clicked_delete, boost::ref(*this)));
+}
+
+const std::string LoadOrSaveGame::filename_list_string() const {
+	std::set<uint32_t> selections = table_.selections();
+	boost::format message;
+	int counter = 0;
+	for (const uint32_t index : selections) {
+		++counter;
+		// TODO(GunChleoc): We can exceed the texture size for the font renderer,
+		// so we have to restrict this for now.
+		if (counter > 50) {
+			message = boost::format("%s\n%s") % message % "...";
+			break;
+		}
+		const SavegameData& gamedata = games_data_[table_.get(table_.get_record(index))];
+
+		if (gamedata.errormessage.empty()) {
+			std::vector<std::string> listme;
+			listme.push_back(richtext_escape(gamedata.mapname));
+			listme.push_back(gamedata.savedonstring);
+			message = (boost::format("%s\n%s") % message %
+			           i18n::localize_list(listme, i18n::ConcatenateWith::COMMA));
+		} else {
+			message = boost::format("%s\n%s") % message % richtext_escape(gamedata.filename);
+		}
+	}
+	return message.str();
+}
+
+bool LoadOrSaveGame::compare_date_descending(uint32_t rowa, uint32_t rowb) {
+	const SavegameData& r1 = games_data_[table_[rowa]];
+	const SavegameData& r2 = games_data_[table_[rowb]];
+
+	return r1.savetimestamp < r2.savetimestamp;
+}
+
+const SavegameData* LoadOrSaveGame::entry_selected() {
+	SavegameData* result = new SavegameData();
+	size_t selections = table_.selections().size();
+	if (selections == 1) {
+		delete_->set_tooltip(
+		   filetype_ == FileType::kReplay ?
+		      /** TRANSLATORS: Tooltip for the delete button. The user has selected 1 file */
+		      _("Delete this replay") :
+		      /** TRANSLATORS: Tooltip for the delete button. The user has selected 1 file */
+		      _("Delete this game"));
+		result = &games_data_[table_.get_selected()];
+	} else if (selections > 1) {
+		delete_->set_tooltip(
+		   filetype_ == FileType::kReplay ?
+		      /** TRANSLATORS: Tooltip for the delete button. The user has selected multiple files */
+		      _("Delete these replays") :
+		      /** TRANSLATORS: Tooltip for the delete button. The user has selected multiple files */
+		      _("Delete these games"));
+		result->mapname =
+		   (boost::format(ngettext("Selected %d file:", "Selected %d files:", selections)) %
+		    selections)
+		      .str();
+		result->filename_list = filename_list_string();
+	} else {
+		delete_->set_tooltip("");
+	}
+	game_details_.update(*result);
+	return result;
+}
+
+bool LoadOrSaveGame::has_selection() {
+	return table_.has_selection();
+}
+
+void LoadOrSaveGame::clear_selections() {
+	table_.clear_selections();
+	game_details_.clear();
+}
+
+void LoadOrSaveGame::select_by_name(const std::string& name) {
+	table_.clear_selections();
+	for (size_t idx = 0; idx < table_.size(); ++idx) {
+		const SavegameData& gamedata = games_data_[table_[idx]];
+		if (name == gamedata.filename) {
+			table_.select(idx);
+			return;
+		}
+	}
+}
+
+const std::string LoadOrSaveGame::get_filename(int index) const {
+	return games_data_[table_.get(table_.get_record(index))].filename;
+}
+
+void LoadOrSaveGame::clicked_delete() {
+	if (!has_selection()) {
+		return;
+	}
+	std::set<uint32_t> selections = table().selections();
+	const SavegameData& gamedata = *entry_selected();
+	size_t no_selections = selections.size();
+	std::string header = "";
+	if (filetype_ == FileType::kReplay) {
+		header = no_selections == 1 ?
+		            _("Do you really want to delete this replay?") :
+		            /** TRANSLATORS: Used with multiple replays, 1 replay has a separate string. */
+		            (boost::format(ngettext("Do you really want to delete this %d replay?",
+		                                    "Do you really want to delete these %d replays?",
+		                                    no_selections)) %
+		             no_selections)
+		               .str();
+	} else {
+		header = no_selections == 1 ?
+		            _("Do you really want to delete this game?") :
+		            /** TRANSLATORS: Used with multiple games, 1 game has a separate string. */
+		            (boost::format(ngettext("Do you really want to delete this %d game?",
+		                                    "Do you really want to delete these %d games?",
+		                                    no_selections)) %
+		             no_selections)
+		               .str();
+	}
+	std::string message = no_selections > 1 ? gamedata.filename_list : gamedata.filename;
+	message = (boost::format("%s\n%s") % header % message).str();
+
+	bool do_delete = SDL_GetModState() & KMOD_CTRL;
+	if (!do_delete) {
+		UI::WLMessageBox confirmationBox(
+			parent_, ngettext("Confirm deleting file", "Confirm deleting files", no_selections), message,
+			UI::WLMessageBox::MBoxType::kOkCancel);
+		do_delete = confirmationBox.run<UI::Panel::Returncodes>() == UI::Panel::Returncodes::kOk;
+	}
+	if (do_delete) {
+		for (const uint32_t index : selections) {
+			const std::string& deleteme = get_filename(index);
+			g_fs->fs_unlink(deleteme);
+			if (filetype_ == FileType::kReplay) {
+				g_fs->fs_unlink(deleteme + WLGF_SUFFIX);
+			}
+		}
+		fill_table();
+	}
+}
+
+void LoadOrSaveGame::fill_table() {
+
+	games_data_.clear();
+	table_.clear();
+
+	FilenameSet gamefiles;
+
+	if (filetype_ == FileType::kReplay) {
+		gamefiles = filter(g_fs->list_directory(REPLAY_DIR),
+		                   [](const std::string& fn) { return boost::ends_with(fn, REPLAY_SUFFIX); });
+	} else {
+		gamefiles = g_fs->list_directory("save");
+	}
+
+	Widelands::GamePreloadPacket gpdp;
+
+	for (const std::string& gamefilename : gamefiles) {
+		if (gamefilename == "save/campvis" || gamefilename == "save\\campvis") {
+			continue;
+		}
+
+		SavegameData gamedata;
+
+		std::string savename = gamefilename;
+		if (filetype_ == FileType::kReplay)
+			savename += WLGF_SUFFIX;
+
+		if (!g_fs->file_exists(savename.c_str())) {
+			continue;
+		}
+
+		gamedata.filename = gamefilename;
+
+		try {
+			Widelands::GameLoader gl(savename.c_str(), game_);
+			gl.preload_game(gpdp);
+
+			gamedata.gametype = gpdp.get_gametype();
+
+			if (filetype_ != FileType::kReplay) {
+				if (filetype_ == FileType::kGame) {
+					if (gamedata.gametype == GameController::GameType::REPLAY) {
+						continue;
+					}
+				} else if (filetype_ == FileType::kGameMultiPlayer) {
+					if (gamedata.gametype == GameController::GameType::SINGLEPLAYER) {
+						continue;
+					}
+				} else if (gamedata.gametype > GameController::GameType::SINGLEPLAYER) {
+					continue;
+				}
+			}
+
+			gamedata.set_mapname(gpdp.get_mapname());
+			gamedata.set_gametime(gpdp.get_gametime());
+			gamedata.set_nrplayers(gpdp.get_number_of_players());
+			gamedata.version = gpdp.get_version();
+
+			gamedata.savetimestamp = gpdp.get_savetimestamp();
+			time_t t;
+			time(&t);
+			struct tm* currenttime = localtime(&t);
+			// We need to put these into variables because of a sideeffect of the localtime function.
+			int8_t current_year = currenttime->tm_year;
+			int8_t current_month = currenttime->tm_mon;
+			int8_t current_day = currenttime->tm_mday;
+
+			struct tm* savedate = localtime(&gamedata.savetimestamp);
+
+			if (gamedata.savetimestamp > 0) {
+				if (savedate->tm_year == current_year && savedate->tm_mon == current_month &&
+				    savedate->tm_mday == current_day) {  // Today
+
+					// Adding the 0 padding in a separate statement so translators won't have to deal
+					// with it
+					const std::string minute = (boost::format("%02u") % savedate->tm_min).str();
+
+					gamedata.savedatestring =
+					   /** TRANSLATORS: Display date for choosing a savegame/replay. Placeholders are:
+					      hour:minute */
+					   (boost::format(_("Today, %1%:%2%")) % savedate->tm_hour % minute).str();
+					gamedata.savedonstring =
+					   /** TRANSLATORS: Display date for choosing a savegame/replay. Placeholders are:
+					      hour:minute. This is part of a list. */
+					   (boost::format(_("saved today at %1%:%2%")) % savedate->tm_hour % minute).str();
+				} else if ((savedate->tm_year == current_year && savedate->tm_mon == current_month &&
+				            savedate->tm_mday == current_day - 1) ||
+				           (savedate->tm_year == current_year - 1 && savedate->tm_mon == 11 &&
+				            current_month == 0 && savedate->tm_mday == 31 &&
+				            current_day == 1)) {  // Yesterday
+					// Adding the 0 padding in a separate statement so translators won't have to deal
+					// with it
+					const std::string minute = (boost::format("%02u") % savedate->tm_min).str();
+
+					gamedata.savedatestring =
+					   /** TRANSLATORS: Display date for choosing a savegame/replay. Placeholders are:
+					      hour:minute */
+					   (boost::format(_("Yesterday, %1%:%2%")) % savedate->tm_hour % minute).str();
+					gamedata.savedonstring =
+					   /** TRANSLATORS: Display date for choosing a savegame/replay. Placeholders are:
+					      hour:minute. This is part of a list. */
+					   (boost::format(_("saved yesterday at %1%:%2%")) % savedate->tm_hour % minute)
+					      .str();
+				} else {  // Older
+					gamedata.savedatestring =
+					   /** TRANSLATORS: Display date for choosing a savegame/replay. Placeholders are:
+					      month day, year */
+					   (boost::format(_("%2% %1%, %3%")) % savedate->tm_mday %
+					    localize_month(savedate->tm_mon) % (1900 + savedate->tm_year))
+					      .str();
+					gamedata.savedonstring =
+					   /** TRANSLATORS: Display date for choosing a savegame/replay. Placeholders are:
+					      month day, year. This is part of a list. */
+					   (boost::format(_("saved on %2% %1%, %3%")) % savedate->tm_mday %
+					    localize_month(savedate->tm_mon) % (1900 + savedate->tm_year))
+					      .str();
+				}
+			}
+
+			gamedata.wincondition = gpdp.get_localized_win_condition();
+			gamedata.minimap_path = gpdp.get_minimap_path();
+			games_data_.push_back(gamedata);
+
+			UI::Table<uintptr_t const>::EntryRecord& te = table_.add(games_data_.size() - 1);
+			te.set_string(0, gamedata.savedatestring);
+
+			if (filetype_ != FileType::kGameSinglePlayer) {
+				std::string gametypestring;
+				switch (gamedata.gametype) {
+				case GameController::GameType::SINGLEPLAYER:
+					/** TRANSLATORS: "Single Player" entry in the Game Mode table column. */
+					/** TRANSLATORS: "Keep this to 6 letters maximum. */
+					/** TRANSLATORS: A tooltip will explain the abbreviation. */
+					/** TRANSLATORS: Make sure that this translation is consistent with the tooltip. */
+					gametypestring = _("SP");
+					break;
+				case GameController::GameType::NETHOST:
+					/** TRANSLATORS: "Multiplayer Host" entry in the Game Mode table column. */
+					/** TRANSLATORS: "Keep this to 2 letters maximum. */
+					/** TRANSLATORS: A tooltip will explain the abbreviation. */
+					/** TRANSLATORS: Make sure that this translation is consistent with the tooltip. */
+					/** TRANSLATORS: %1% is the number of players */
+					gametypestring = (boost::format(_("H (%1%)")) % gamedata.nrplayers).str();
+					break;
+				case GameController::GameType::NETCLIENT:
+					/** TRANSLATORS: "Multiplayer" entry in the Game Mode table column. */
+					/** TRANSLATORS: "Keep this to 2 letters maximum. */
+					/** TRANSLATORS: A tooltip will explain the abbreviation. */
+					/** TRANSLATORS: Make sure that this translation is consistent with the tooltip. */
+					/** TRANSLATORS: %1% is the number of players */
+					gametypestring = (boost::format(_("MP (%1%)")) % gamedata.nrplayers).str();
+					break;
+				case GameController::GameType::REPLAY:
+					gametypestring = "";
+					break;
+				}
+				te.set_string(1, gametypestring);
+				if (filetype_ == FileType::kReplay) {
+					if (UI::g_fh1->fontset()->is_rtl()) {
+						te.set_string(
+						   2, (boost::format("%1% ← %2%") % gamedata.gametime % gamedata.mapname).str());
+					} else {
+						te.set_string(
+						   2, (boost::format("%1% → %2%") % gamedata.gametime % gamedata.mapname).str());
+					}
+				} else {
+					te.set_string(
+					   2, map_filename(gamedata.filename, gamedata.mapname, localize_autosave_));
+				}
+			} else {
+				te.set_string(1, map_filename(gamedata.filename, gamedata.mapname, localize_autosave_));
+			}
+		} catch (const WException& e) {
+			std::string errormessage = e.what();
+			boost::replace_all(errormessage, "\n", "<br>");
+			gamedata.errormessage =
+			   ((boost::format("<p>%s</p><p>%s</p><p>%s</p>"))
+			    /** TRANSLATORS: Error message introduction for when an old savegame can't be loaded */
+			    % _("This file has the wrong format and can’t be loaded."
+			        " Maybe it was created with an older version of Widelands.")
+			    /** TRANSLATORS: This text is on a separate line with an error message below */
+			    % _("Error message:") % errormessage)
+			      .str();
+
+			gamedata.mapname = FileSystem::filename_without_ext(gamedata.filename.c_str());
+			games_data_.push_back(gamedata);
+
+			UI::Table<uintptr_t const>::EntryRecord& te = table_.add(games_data_.size() - 1);
+			te.set_string(0, "");
+			if (filetype_ != FileType::kGameSinglePlayer) {
+				te.set_string(1, "");
+				/** TRANSLATORS: Prefix for incompatible files in load game screens */
+				te.set_string(2, (boost::format(_("Incompatible: %s")) % gamedata.mapname).str());
+			} else {
+				te.set_string(1, (boost::format(_("Incompatible: %s")) % gamedata.mapname).str());
+			}
+		}
+	}
+	table_.sort();
+}

=== added file 'src/wui/load_or_save_game.h'
--- src/wui/load_or_save_game.h	1970-01-01 00:00:00 +0000
+++ src/wui/load_or_save_game.h	2017-04-21 07:38:25 +0000
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2002-2016 by the Widelands Development Team
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ *
+ */
+
+#ifndef WL_WUI_LOAD_OR_SAVE_GAME_H
+#define WL_WUI_LOAD_OR_SAVE_GAME_H
+
+#include "logic/game.h"
+#include "ui_basic/panel.h"
+#include "ui_basic/table.h"
+#include "wui/gamedetails.h"
+
+/// Common functions for loading or saving a game or replay.
+class LoadOrSaveGame {
+	friend class GameMainMenuSaveGame;
+	friend class FullscreenMenuLoadGame;
+
+protected:
+	/// Choose which type of files to show
+	enum class FileType { kReplay, kGame, kGameMultiPlayer, kGameSinglePlayer };
+
+	/// A table of savegame/replay files and a game details panel.
+	LoadOrSaveGame(UI::Panel* parent,
+	               Widelands::Game& g,
+	               FileType filetype,
+	               GameDetails::Style style,
+	               bool localize_autosave);
+
+	//// Update gamedetails and tooltips and return information about the current selection
+	const SavegameData* entry_selected();
+
+	/// Whether the table has a selection
+	bool has_selection();
+
+	/// Clear table selections and game data
+	void clear_selections();
+
+	/// Finds the given filename on the table and selects it
+	void select_by_name(const std::string& name);
+
+	/// Read savegame/replay files and fill the table and games data.
+	void fill_table();
+
+	/// The table panel
+	UI::Table<uintptr_t const>& table() {
+		return table_;
+	}
+
+	/// The game details panel
+	GameDetails* game_details() {
+		return &game_details_;
+	}
+
+	/// Returns the filename for the table entry at 'index'
+	const std::string get_filename(int index) const;
+
+	/// The delete button shown on the bottom of the game details panel
+	UI::Button* delete_button() const {
+		return delete_;
+	}
+	/// Show confirmation window and delete the selected file(s)
+	void clicked_delete();
+
+private:
+	/// Formats the current table selection as a list of filenames with savedate information.
+	const std::string filename_list_string() const;
+
+	/// Reverse default sort order for save date column
+	bool compare_date_descending(uint32_t, uint32_t);
+
+	UI::Panel* parent_;
+	UI::Table<uintptr_t const> table_;
+	FileType filetype_;
+	bool localize_autosave_;
+	std::vector<SavegameData> games_data_;
+	GameDetails game_details_;
+	UI::Button* delete_;
+
+	Widelands::Game& game_;
+};
+
+#endif  // end of include guard: WL_WUI_LOAD_OR_SAVE_GAME_H


Follow ups