← Back to team overview

widelands-dev team mailing list archive

[Merge] lp:~widelands-dev/widelands/fix-dropdowns into lp:widelands

 

GunChleoc has proposed merging lp:~widelands-dev/widelands/fix-dropdowns into lp:widelands.

Commit message:
Dropdown fixes and improvements
- Fix positioning of dropdown lists on fullscreen switches
- Define dropdpwn list height by number of items
- Add dropdowns to Lua interface
- Panel::free_children() now frees all its children
- Create styles for hotkeys and reduce number of render calls in listselect
- Get rid of map width/height code duplication in the editor
- Add support for using dropdowns as toolbar menus with hotkeys
- Fix toggling of minimized UniqueWIndows


Requested reviews:
  Widelands Developers (widelands-dev)
Related bugs:
  Bug #1643563 in widelands: "Toolbar redesign"
  https://bugs.launchpad.net/widelands/+bug/1643563

For more details, see:
https://code.launchpad.net/~widelands-dev/widelands/fix-dropdowns/+merge/368223

I pulled out some code from

https://code.launchpad.net/~widelands-dev/widelands/toolbar-dropdown-menus

to make it more reviewable, because that branch has become too big.

The new dropdown functionality in the Lua interface and for toolbar menus is not being used in this branch, but it can be tested with the other branch.
-- 
Your team Widelands Developers is requested to review the proposed merge of lp:~widelands-dev/widelands/fix-dropdowns into lp:widelands.
=== modified file 'data/templates/default/init.lua'
--- data/templates/default/init.lua	2019-05-25 09:40:09 +0000
+++ data/templates/default/init.lua	2019-06-01 10:27:23 +0000
@@ -422,6 +422,13 @@
             size = fs_font_size,
             bold = true,
             shadow = true
+         },
+         hotkey = {
+            color = {180, 180, 180},
+            face = fs_font_face,
+            size = fs_font_size,
+            bold = true,
+            shadow = true
          }
       },
       wui = {
@@ -438,6 +445,13 @@
             size = fs_font_size,
             bold = true,
             shadow = true
+         },
+         hotkey = {
+            color = {180, 180, 180},
+            face = fs_font_face,
+            size = fs_font_size,
+            bold = true,
+            shadow = true
          }
       },
    },
@@ -615,6 +629,12 @@
          size = 14,
          bold = false,
       },
+      tooltip_hotkey = {
+         color = {180, 180, 180},
+         face = fs_font_face,
+         size = 14,
+         bold = false,
+      },
       tooltip_header = {
          color = fs_font_color,
          face = fs_font_face,

=== modified file 'src/editor/CMakeLists.txt'
--- src/editor/CMakeLists.txt	2019-05-12 07:45:59 +0000
+++ src/editor/CMakeLists.txt	2019-06-01 10:27:23 +0000
@@ -65,6 +65,8 @@
     ui_menus/main_menu_save_map.h
     ui_menus/main_menu_save_map_make_directory.cc
     ui_menus/main_menu_save_map_make_directory.h
+    ui_menus/map_size_box.cc
+    ui_menus/map_size_box.h
     ui_menus/player_menu.cc
     ui_menus/player_menu.h
     ui_menus/tool_change_height_options_menu.cc

=== modified file 'src/editor/ui_menus/main_menu_new_map.cc'
--- src/editor/ui_menus/main_menu_new_map.cc	2019-04-26 05:52:49 +0000
+++ src/editor/ui_menus/main_menu_new_map.cc	2019-06-01 10:27:23 +0000
@@ -46,24 +46,7 @@
      margin_(4),
      box_width_(get_inner_w() - 2 * margin_),
      box_(this, margin_, margin_, UI::Box::Vertical, 0, 0, margin_),
-     width_(&box_,
-            0,
-            0,
-            box_width_,
-            box_width_ / 3,
-            24,
-            _("Width"),
-            UI::DropdownType::kTextual,
-            UI::PanelStyle::kWui),
-     height_(&box_,
-             0,
-             0,
-             box_width_,
-             box_width_ / 3,
-             24,
-             _("Height"),
-             UI::DropdownType::kTextual,
-             UI::PanelStyle::kWui),
+	 map_size_box_(box_, "new_map_menu", 4, parent.egbase().map().get_width(), parent.egbase().map().get_height()),
      list_(&box_, 0, 0, box_width_, 330, UI::PanelStyle::kWui),
      // Buttons
      button_box_(&box_, 0, 0, UI::Box::Horizontal, 0, 0, margin_),
@@ -84,18 +67,8 @@
                     UI::ButtonStyle::kWuiSecondary,
                     _("Cancel")) {
 
-	for (const int32_t& i : Widelands::kMapDimensions) {
-		width_.add(std::to_string(i), i);
-		height_.add(std::to_string(i), i);
-	}
-	width_.select(parent.egbase().map().get_width());
-	height_.select(parent.egbase().map().get_height());
-	width_.set_max_items(12);
-	height_.set_max_items(12);
-
 	box_.set_size(100, 20);  // Prevent assert failures
-	box_.add(&width_);
-	box_.add(&height_);
+	box_.add(&map_size_box_, UI::Box::Resizing::kExpandBoth);
 	box_.add_space(margin_);
 	UI::Textarea* terrain_label = new UI::Textarea(&box_, _("Terrain:"));
 	box_.add(terrain_label);
@@ -113,9 +86,7 @@
 	}
 	box_.add(&button_box_);
 
-	box_.set_size(box_width_, width_.get_h() + height_.get_h() + terrain_label->get_h() +
-	                             list_.get_h() + button_box_.get_h() + 9 * margin_);
-	set_size(get_w(), box_.get_h() + 2 * margin_ + get_h() - get_inner_h());
+	set_center_panel(&box_);
 	fill_list();
 	center_to_parent();
 }
@@ -132,8 +103,8 @@
 
 	map->create_empty_map(
 	   egbase.world(),
-	   width_.get_selected() > 0 ? width_.get_selected() : Widelands::kMapDimensions[0],
-	   height_.get_selected() > 0 ? height_.get_selected() : Widelands::kMapDimensions[0],
+	   map_size_box_.selected_width(),
+	   map_size_box_.selected_height(),
 	   list_.get_selected(), _("No Name"),
 	   g_options.pull_section("global").get_string("realname", pgettext("author_name", "Unknown")));
 

=== modified file 'src/editor/ui_menus/main_menu_new_map.h'
--- src/editor/ui_menus/main_menu_new_map.h	2019-04-26 05:52:49 +0000
+++ src/editor/ui_menus/main_menu_new_map.h	2019-06-01 10:27:23 +0000
@@ -20,10 +20,10 @@
 #ifndef WL_EDITOR_UI_MENUS_MAIN_MENU_NEW_MAP_H
 #define WL_EDITOR_UI_MENUS_MAIN_MENU_NEW_MAP_H
 
-#include "logic/widelands.h"
+#include "editor/ui_menus/map_size_box.h"
+#include "logic/map_objects/description_maintainer.h"
 #include "ui_basic/box.h"
 #include "ui_basic/button.h"
-#include "ui_basic/dropdown.h"
 #include "ui_basic/listselect.h"
 #include "ui_basic/window.h"
 
@@ -46,8 +46,7 @@
 	int32_t margin_;
 	int32_t box_width_;
 	UI::Box box_;
-	UI::Dropdown<int32_t> width_;
-	UI::Dropdown<int32_t> height_;
+	MapSizeBox map_size_box_;
 
 	// Terrains list
 	UI::Listselect<Widelands::DescriptionIndex> list_;

=== modified file 'src/editor/ui_menus/main_menu_random_map.cc'
--- src/editor/ui_menus/main_menu_random_map.cc	2019-05-26 17:21:15 +0000
+++ src/editor/ui_menus/main_menu_random_map.cc	2019-06-01 10:27:23 +0000
@@ -55,24 +55,7 @@
      label_height_(text_height(UI::FontStyle::kLabel) + 2),
      box_(this, margin_, margin_, UI::Box::Vertical, 0, 0, margin_),
      // Size
-     width_(&box_,
-            0,
-            0,
-            box_width_,
-            box_width_ / 3,
-            24,
-            _("Width"),
-            UI::DropdownType::kTextual,
-            UI::PanelStyle::kWui),
-     height_(&box_,
-             0,
-             0,
-             box_width_,
-             box_width_ / 3,
-             24,
-             _("Height"),
-             UI::DropdownType::kTextual,
-             UI::PanelStyle::kWui),
+	 map_size_box_(box_, "random_map_menu", 4, parent.egbase().map().get_width(), parent.egbase().map().get_height()),
      max_players_(2),
      players_(&box_,
               0,
@@ -212,28 +195,13 @@
                     UI::ButtonStyle::kWuiSecondary,
                     _("Cancel")) {
 	int32_t box_height = 0;
+	box_.set_size(100, 20);  // Prevent assert failures
 
 	// ---------- Width + Height ----------
 
-	for (const int32_t& i : Widelands::kMapDimensions) {
-		width_.add(std::to_string(i), i);
-		height_.add(std::to_string(i), i);
-	}
-	width_.select(parent.egbase().map().get_width());
-	height_.select(parent.egbase().map().get_height());
-	width_.set_max_items(12);
-	height_.set_max_items(12);
-
-	width_.selected.connect(
-	   boost::bind(&MainMenuNewRandomMap::button_clicked, this, ButtonId::kMapSize));
-	height_.selected.connect(
-	   boost::bind(&MainMenuNewRandomMap::button_clicked, this, ButtonId::kMapSize));
-
-	box_.set_size(100, 20);  // Prevent assert failures
-	box_.add(&width_);
-	box_.add(&height_);
-	box_height += margin_ + width_.get_h();
-	box_height += margin_ + height_.get_h();
+	map_size_box_.set_selection_function([this] { button_clicked(ButtonId::kMapSize); });
+	box_.add(&map_size_box_, UI::Box::Resizing::kExpandBoth);
+	box_height += margin_ + map_size_box_.get_h();
 
 	// ---------- Players -----------
 
@@ -396,19 +364,6 @@
  */
 void MainMenuNewRandomMap::button_clicked(MainMenuNewRandomMap::ButtonId n) {
 	switch (n) {
-	case ButtonId::kPlayers:  // intended fall-through
-	case ButtonId::kMapSize:
-		// Restrict maximum players according to map size, but allow at least 2 players.
-		max_players_ = std::min(
-		   static_cast<size_t>(kMaxMapgenPlayers), (find_dimension_index(width_.get_selected()) +
-		                                            find_dimension_index(height_.get_selected())) /
-		                                                 2 +
-		                                              2);
-		players_.set_interval(1, max_players_);
-		if (players_.get_value() > max_players_) {
-			players_.set_value(max_players_);
-		}
-		break;
 	case ButtonId::kWater:
 		waterval_ = water_.get_value();
 		normalize_landmass(n);
@@ -433,17 +388,20 @@
 		break;
 	case ButtonId::kIslandMode:
 		break;
+	case ButtonId::kPlayers:  // intended fall-through
+	case ButtonId::kMapSize:
 	case ButtonId::kNone:
-		// Make sure that all conditions are met
+		// Restrict maximum players according to map size, but allow at least 2 players.
 		max_players_ = std::min(
-		   static_cast<size_t>(kMaxMapgenPlayers), (find_dimension_index(width_.get_selected()) +
-		                                            find_dimension_index(height_.get_selected())) /
+		   static_cast<size_t>(kMaxMapgenPlayers), (find_dimension_index(map_size_box_.selected_width()) +
+		                                            find_dimension_index(map_size_box_.selected_height())) /
 		                                                 2 +
 		                                              2);
 		players_.set_interval(1, max_players_);
 		if (players_.get_value() > max_players_) {
 			players_.set_value(max_players_);
 		}
+		// Make sure that landmass is consistent
 		normalize_landmass(n);
 	}
 	nr_edit_box_changed();  // Update ID String
@@ -598,8 +556,8 @@
 		sstrm << map_info.mapNumber;
 		map_number_edit_.set_text(sstrm.str());
 
-		width_.select(map_info.w);
-		height_.select(map_info.h);
+		map_size_box_.select_width(map_info.w);
+		map_size_box_.select_height(map_info.h);
 
 		players_.set_interval(1, map_info.numPlayers);  // hack to make sure we can set the value
 		players_.set_value(map_info.numPlayers);
@@ -652,8 +610,8 @@
 }
 
 void MainMenuNewRandomMap::set_map_info(Widelands::UniqueRandomMapInfo& map_info) const {
-	map_info.w = width_.get_selected() > 0 ? width_.get_selected() : Widelands::kMapDimensions[0];
-	map_info.h = height_.get_selected() > 0 ? height_.get_selected() : Widelands::kMapDimensions[0];
+	map_info.w = map_size_box_.selected_width();
+	map_info.h = map_size_box_.selected_height();
 	map_info.waterRatio = static_cast<double>(waterval_) / 100.0;
 	map_info.landRatio = static_cast<double>(landval_) / 100.0;
 	map_info.wastelandRatio = static_cast<double>(wastelandval_) / 100.0;

=== modified file 'src/editor/ui_menus/main_menu_random_map.h'
--- src/editor/ui_menus/main_menu_random_map.h	2019-04-10 10:42:22 +0000
+++ src/editor/ui_menus/main_menu_random_map.h	2019-06-01 10:27:23 +0000
@@ -23,6 +23,7 @@
 #include <vector>
 
 #include "base/macros.h"
+#include "editor/ui_menus/map_size_box.h"
 #include "ui_basic/box.h"
 #include "ui_basic/checkbox.h"
 #include "ui_basic/dropdown.h"
@@ -89,8 +90,7 @@
 	UI::Box box_;
 
 	// Size
-	UI::Dropdown<int32_t> width_;
-	UI::Dropdown<int32_t> height_;
+	MapSizeBox map_size_box_;
 
 	uint8_t max_players_;
 	UI::SpinBox players_;

=== added file 'src/editor/ui_menus/map_size_box.cc'
--- src/editor/ui_menus/map_size_box.cc	1970-01-01 00:00:00 +0000
+++ src/editor/ui_menus/map_size_box.cc	2019-06-01 10:27:23 +0000
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2019 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 "editor/ui_menus/map_size_box.h"
+
+#include "base/i18n.h"
+#include "logic/map.h"
+
+MapSizeBox::MapSizeBox(UI::Box& parent, const std::string& name, int spacing, int map_width, int map_height)
+   : UI::Box(&parent, 0, 0, UI::Box::Horizontal, 0, 0, spacing),
+	 width_(this,
+            name + "_map_width",
+            0,
+            0,
+            160,
+            12,
+            24,
+            _("Width"),
+            UI::DropdownType::kTextual,
+            UI::PanelStyle::kWui,
+            UI::ButtonStyle::kWuiSecondary),
+     height_(this,
+             name + "_map_height",
+             0,
+             0,
+             160,
+             12,
+             24,
+             _("Height"),
+             UI::DropdownType::kTextual,
+             UI::PanelStyle::kWui,
+             UI::ButtonStyle::kWuiSecondary) {
+	for (const int32_t& i : Widelands::kMapDimensions) {
+		width_.add(std::to_string(i), i);
+		height_.add(std::to_string(i), i);
+	}
+	width_.select(map_width);
+	height_.select(map_height);
+	add(&width_, UI::Box::Resizing::kFillSpace);
+	add(&height_, UI::Box::Resizing::kFillSpace);
+}
+
+void MapSizeBox::set_selection_function(const std::function<void()> func) {
+	width_.selected.connect(func);
+	height_.selected.connect(func);
+}
+
+uint32_t MapSizeBox::selected_width() const {
+	return width_.get_selected();
+}
+uint32_t MapSizeBox::selected_height() const {
+	return height_.get_selected();
+}
+
+void MapSizeBox::select_width(int new_width) {
+	width_.select(new_width);
+}
+void MapSizeBox::select_height(int new_height) {
+	height_.select(new_height);
+}

=== added file 'src/editor/ui_menus/map_size_box.h'
--- src/editor/ui_menus/map_size_box.h	1970-01-01 00:00:00 +0000
+++ src/editor/ui_menus/map_size_box.h	2019-06-01 10:27:23 +0000
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2019 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_EDITOR_UI_MENUS_MAP_SIZE_BOX_H
+#define WL_EDITOR_UI_MENUS_MAP_SIZE_BOX_H
+
+#include "ui_basic/box.h"
+#include "ui_basic/dropdown.h"
+
+/**
+ * A box containing 2 dropdowns to select map width and height with horizontal layout.
+ * Selections are taken from Widelands::kMapDimensions.
+ */
+struct MapSizeBox : public UI::Box {
+
+	/**
+	 * @param parent The parent panel
+	 * @param name A string to prefix for the dropdown names, so that they can be idenfitied uniquely
+	 * @param spacing The horizontal space between the 2 dropdowns
+	 * @param map_width Width to preselect
+	 * @param map_height Height to preselect
+	 */
+	MapSizeBox(UI::Box& parent, const std::string& name, int spacing, int map_width, int map_height);
+
+	/// This function will be triggered when a new width or height is selected from the dropdowns
+	void set_selection_function(const std::function<void()> func);
+	/// The currently selected width
+	uint32_t selected_width() const;
+	/// The currently selected height
+	uint32_t selected_height() const;
+	/// Set the selected width
+	void select_width(int new_width);
+	/// Set the selected height
+	void select_height(int new_height);
+
+private:
+	UI::Dropdown<uint32_t> width_;
+	UI::Dropdown<uint32_t> height_;
+};
+
+#endif  // end of include guard: WL_EDITOR_UI_MENUS_MAP_SIZE_BOX_H

=== modified file 'src/editor/ui_menus/player_menu.cc'
--- src/editor/ui_menus/player_menu.cc	2019-05-12 07:45:59 +0000
+++ src/editor/ui_menus/player_menu.cc	2019-06-01 10:27:23 +0000
@@ -38,8 +38,9 @@
 
 namespace {
 constexpr int kMargin = 4;
+// Make room for 8 players
 // If this ever gets changed, don't forget to change the strings in the warning box as well.
-constexpr Widelands::PlayerNumber max_recommended_players = 8;
+constexpr Widelands::PlayerNumber kMaxRecommendedPlayers = 8;
 }  // namespace
 
 class EditorPlayerMenuWarningBox : public UI::Window {
@@ -122,14 +123,15 @@
    : UI::UniqueWindow(&parent, "players_menu", &registry, 100, 100, _("Player Options")),
      box_(this, kMargin, kMargin, UI::Box::Vertical),
      no_of_players_(&box_,
+					"dropdown_map_players",
                     0,
                     0,
                     50,
-                    100,
+                    kMaxRecommendedPlayers,
                     24,
                     _("Number of players"),
                     UI::DropdownType::kTextual,
-                    UI::PanelStyle::kWui) {
+                    UI::PanelStyle::kWui, UI::ButtonStyle::kWuiSecondary) {
 	box_.set_size(100, 100);  // Prevent assert failures
 	box_.add(&no_of_players_, UI::Box::Resizing::kFullSize);
 	box_.add_space(2 * kMargin);
@@ -153,7 +155,7 @@
 	iterate_player_numbers(p, kMaxPlayers) {
 		const bool map_has_player = p <= nr_players;
 
-		no_of_players_.add(boost::lexical_cast<std::string>(static_cast<unsigned int>(p)), p);
+		no_of_players_.add(boost::lexical_cast<std::string>(static_cast<unsigned int>(p)), p, nullptr, p == nr_players);
 		no_of_players_.selected.connect(
 		   boost::bind(&EditorPlayerMenu::no_of_players_clicked, boost::ref(*this)));
 
@@ -168,8 +170,8 @@
 
 		// Tribe
 		UI::Dropdown<std::string>* plr_tribe =
-		   new UI::Dropdown<std::string>(row, 0, 0, 50, 400, plr_name->get_h(), _("Tribe"),
-		                                 UI::DropdownType::kPictorial, UI::PanelStyle::kWui);
+		   new UI::Dropdown<std::string>(row, (boost::format("dropdown_tribe%d") % static_cast<unsigned int>(p)).str(), 0, 0, 50, 16, plr_name->get_h(), _("Tribe"),
+		                                 UI::DropdownType::kPictorial, UI::PanelStyle::kWui, UI::ButtonStyle::kWuiSecondary);
 		{
 			i18n::Textdomain td("tribes");
 			for (const Widelands::TribeBasicInfo& tribeinfo : Widelands::get_all_tribeinfos()) {
@@ -219,8 +221,6 @@
 		   std::unique_ptr<PlayerEditRow>(new PlayerEditRow(row, plr_name, plr_position, plr_tribe)));
 	}
 
-	// Make room for 8 players
-	no_of_players_.set_max_items(max_recommended_players);
 	no_of_players_.select(nr_players);
 
 	// Init button states
@@ -249,13 +249,13 @@
 	}
 
 	// Display a warning if there are too many players
-	if (nr_players > max_recommended_players) {
+	if (nr_players > kMaxRecommendedPlayers) {
 		if (g_options.pull_section("global").get_bool(
 		       "editor_player_menu_warn_too_many_players", true)) {
 			EditorPlayerMenuWarningBox warning(get_parent());
 			if (warning.run<UI::Panel::Returncodes>() == UI::Panel::Returncodes::kBack) {
 				// Abort setting of players
-				no_of_players_.select(std::min(old_nr_players, max_recommended_players));
+				no_of_players_.select(std::min(old_nr_players, kMaxRecommendedPlayers));
 			}
 		}
 	}

=== modified file 'src/editor/ui_menus/tool_resize_options_menu.cc'
--- src/editor/ui_menus/tool_resize_options_menu.cc	2019-04-24 07:09:29 +0000
+++ src/editor/ui_menus/tool_resize_options_menu.cc	2019-06-01 10:27:23 +0000
@@ -39,69 +39,32 @@
    : EditorToolOptionsMenu(parent, registry, 260, 200, _("Resize")),
      resize_tool_(resize_tool),
      box_(this, hmargin(), vmargin(), UI::Box::Vertical, 0, 0, vspacing()),
-     new_width_(&box_,
-                0,
-                0,
-                get_inner_w() - 2 * hmargin(),
-                200,
-                24,
-                _("New width"),
-                UI::DropdownType::kTextual,
-                UI::PanelStyle::kWui),
-     new_height_(&box_,
-                 0,
-                 0,
-                 get_inner_w() - 2 * hmargin(),
-                 200,
-                 24,
-                 _("New height"),
-                 UI::DropdownType::kTextual,
-                 UI::PanelStyle::kWui),
+     map_size_box_(box_, "tool_resize_map", 4, parent.egbase().map().get_width(), parent.egbase().map().get_height()),
      text_area_(
-        &box_,
-        0,
-        0,
-        get_inner_w() - 2 * hmargin(),
-        48,
-        UI::PanelStyle::kWui,
-        _("Select the new map size, then click the map to split it at the desired location."),
-        UI::Align::kCenter,
-        UI::MultilineTextarea::ScrollMode::kNoScrolling) {
-
-	for (const int32_t& i : Widelands::kMapDimensions) {
-		new_width_.add(std::to_string(i), i);
-		new_height_.add(std::to_string(i), i);
-	}
-	new_width_.select(parent.egbase().map().get_width());
-	new_height_.select(parent.egbase().map().get_height());
-	new_width_.set_max_items(8);
-	new_height_.set_max_items(8);
-
-	new_width_.selected.connect(
-	   boost::bind(&EditorToolResizeOptionsMenu::update_width, boost::ref(*this)));
-	new_height_.selected.connect(
-	   boost::bind(&EditorToolResizeOptionsMenu::update_height, boost::ref(*this)));
-
-	box_.add(&text_area_);
-	box_.set_size(100, 20);  // Prevent assert failures
-	box_.add(&new_width_, UI::Box::Resizing::kFullSize);
-	box_.add(&new_height_, UI::Box::Resizing::kFullSize);
-
-	box_.set_size(get_inner_w() - 2 * hmargin(),
-	              new_width_.get_h() + new_height_.get_h() + text_area_.get_h() + 2 * vspacing());
-	set_inner_size(get_inner_w(), box_.get_h() + 1 * vmargin());
+			   &box_,
+			   0,
+			   0,
+			   get_inner_w() - 2 * hmargin(),
+			   48,
+			   UI::PanelStyle::kWui,
+			   _("Select the new map size, then click the map to split it at the desired location."),
+			   UI::Align::kCenter,
+			   UI::MultilineTextarea::ScrollMode::kNoScrolling) {
+
+	map_size_box_.set_selection_function([this] { update_dimensions(); });
+
+	box_.add(&map_size_box_, UI::Box::Resizing::kExpandBoth);
+	box_.add(&text_area_, UI::Box::Resizing::kFullSize);
+
+	set_center_panel(&box_);
 }
 
-void EditorToolResizeOptionsMenu::update_width() {
-	int32_t w = new_width_.get_selected();
+void EditorToolResizeOptionsMenu::update_dimensions() {
+	const int32_t w = map_size_box_.selected_width();
+	const int32_t h = map_size_box_.selected_height();
 	assert(w > 0);
+	assert(h > 0);
 	resize_tool_.set_width(w);
-	select_correct_tool();
-}
-
-void EditorToolResizeOptionsMenu::update_height() {
-	int32_t h = new_height_.get_selected();
-	assert(h > 0);
 	resize_tool_.set_height(h);
 	select_correct_tool();
 }

=== modified file 'src/editor/ui_menus/tool_resize_options_menu.h'
--- src/editor/ui_menus/tool_resize_options_menu.h	2019-04-24 07:09:29 +0000
+++ src/editor/ui_menus/tool_resize_options_menu.h	2019-06-01 10:27:23 +0000
@@ -20,6 +20,7 @@
 #ifndef WL_EDITOR_UI_MENUS_TOOL_RESIZE_OPTIONS_MENU_H
 #define WL_EDITOR_UI_MENUS_TOOL_RESIZE_OPTIONS_MENU_H
 
+#include "editor/ui_menus/map_size_box.h"
 #include "editor/ui_menus/tool_options_menu.h"
 #include "ui_basic/box.h"
 #include "ui_basic/dropdown.h"
@@ -33,13 +34,11 @@
 
 private:
 	EditorInteractive& eia();
-	void update_width();
-	void update_height();
+	void update_dimensions();
 
 	EditorResizeTool& resize_tool_;
 	UI::Box box_;
-	UI::Dropdown<int32_t> new_width_;
-	UI::Dropdown<int32_t> new_height_;
+	MapSizeBox map_size_box_;
 	UI::MultilineTextarea text_area_;
 };
 

=== modified file 'src/graphic/style_manager.cc'
--- src/graphic/style_manager.cc	2019-05-26 17:21:15 +0000
+++ src/graphic/style_manager.cc	2019-06-01 10:27:23 +0000
@@ -214,6 +214,7 @@
 	add_font_style(UI::FontStyle::kDisabled, *element_table, "disabled");
 	add_font_style(UI::FontStyle::kLabel, *element_table, "label");
 	add_font_style(UI::FontStyle::kTooltipHeader, *element_table, "tooltip_header");
+	add_font_style(UI::FontStyle::kTooltipHotkey, *element_table, "tooltip_hotkey");
 	add_font_style(UI::FontStyle::kTooltip, *element_table, "tooltip");
 	add_font_style(UI::FontStyle::kWarning, *element_table, "warning");
 	add_font_style(
@@ -339,7 +340,7 @@
 void StyleManager::add_table_style(UI::PanelStyle style, const LuaTable& table) {
 	table_styles_.insert(std::make_pair(
 	   style, std::unique_ptr<const UI::TableStyleInfo>(new UI::TableStyleInfo(
-	             read_font_style(table, "enabled"), read_font_style(table, "disabled")))));
+	             read_font_style(table, "enabled"), read_font_style(table, "disabled"), read_font_style(table, "hotkey")))));
 }
 
 void StyleManager::set_statistics_plot_style(const LuaTable& table) {

=== modified file 'src/graphic/styles/font_style.h'
--- src/graphic/styles/font_style.h	2019-05-26 17:21:15 +0000
+++ src/graphic/styles/font_style.h	2019-06-01 10:27:23 +0000
@@ -44,6 +44,7 @@
 	kDisabled,
 	kLabel,
 	kTooltipHeader,
+	kTooltipHotkey,
 	kTooltip,
 	kWarning,
 	kWuiAttackBoxSliderLabel,

=== modified file 'src/graphic/styles/table_style.h'
--- src/graphic/styles/table_style.h	2019-05-26 17:21:15 +0000
+++ src/graphic/styles/table_style.h	2019-06-01 10:27:23 +0000
@@ -27,8 +27,8 @@
 namespace UI {
 
 struct TableStyleInfo {
-	explicit TableStyleInfo(UI::FontStyleInfo* init_enabled, UI::FontStyleInfo* init_disabled)
-	   : enabled_(init_enabled), disabled_(init_disabled) {
+	explicit TableStyleInfo(UI::FontStyleInfo* init_enabled, UI::FontStyleInfo* init_disabled, UI::FontStyleInfo* init_hotkey)
+	   : enabled_(init_enabled), disabled_(init_disabled), hotkey_(init_hotkey) {
 	}
 
 	const UI::FontStyleInfo& enabled() const {
@@ -37,10 +37,14 @@
 	const UI::FontStyleInfo& disabled() const {
 		return *disabled_.get();
 	}
+	const UI::FontStyleInfo& hotkey() const {
+		return *hotkey_.get();
+	}
 
 private:
 	std::unique_ptr<const UI::FontStyleInfo> enabled_;
 	std::unique_ptr<const UI::FontStyleInfo> disabled_;
+	std::unique_ptr<const UI::FontStyleInfo> hotkey_;
 };
 
 }  // namespace UI

=== modified file 'src/graphic/text/rt_parse.cc'
--- src/graphic/text/rt_parse.cc	2019-02-28 11:46:22 +0000
+++ src/graphic/text/rt_parse.cc	2019-06-01 10:27:23 +0000
@@ -119,8 +119,10 @@
 
 void Tag::parse_attribute(TextStream& ts, std::unordered_set<std::string>& allowed_attrs) {
 	std::string aname = ts.till_any("=");
-	if (!allowed_attrs.count(aname))
-		throw SyntaxErrorImpl(ts.line(), ts.col(), "an allowed attribute", aname, ts.peek(100));
+	if (!allowed_attrs.count(aname)) {
+		const std::string error_info = (boost::format("an allowed attribute for '%s' tag") % name_).str();
+		throw SyntaxErrorImpl(ts.line(), ts.col(), error_info, aname, ts.peek(100));
+	}
 
 	ts.skip(1);
 

=== modified file 'src/graphic/text_layout.cc'
--- src/graphic/text_layout.cc	2019-05-26 17:21:15 +0000
+++ src/graphic/text_layout.cc	2019-06-01 10:27:23 +0000
@@ -243,3 +243,10 @@
 	}
 	NEVER_HERE();
 }
+
+std::string as_tooltip_text_with_hotkey(const std::string& text, const std::string& hotkey) {
+	static boost::format f("<rt><p>%s %s</p></rt>");
+	f % g_gr->styles().font_style(UI::FontStyle::kTooltip).as_font_tag(text);
+	f % g_gr->styles().font_style(UI::FontStyle::kTooltipHotkey).as_font_tag("(" + hotkey + ")");
+	return f.str();
+}

=== modified file 'src/graphic/text_layout.h'
--- src/graphic/text_layout.h	2019-05-26 17:21:15 +0000
+++ src/graphic/text_layout.h	2019-06-01 10:27:23 +0000
@@ -107,4 +107,6 @@
 /// Paragraph in menu info texts
 std::string as_content(const std::string& txt, UI::PanelStyle style);
 
+std::string as_tooltip_text_with_hotkey(const std::string& text, const std::string& hotkey);
+
 #endif  // end of include guard: WL_GRAPHIC_TEXT_LAYOUT_H

=== modified file 'src/scripting/lua_ui.cc'
--- src/scripting/lua_ui.cc	2019-02-23 11:00:49 +0000
+++ src/scripting/lua_ui.cc	2019-06-01 10:27:23 +0000
@@ -73,7 +73,7 @@
 */
 const char LuaPanel::className[] = "Panel";
 const PropertyType<LuaPanel> LuaPanel::Properties[] = {
-   PROP_RO(LuaPanel, buttons),    PROP_RO(LuaPanel, tabs),       PROP_RO(LuaPanel, windows),
+   PROP_RO(LuaPanel, buttons), PROP_RO(LuaPanel, dropdowns),   PROP_RO(LuaPanel, tabs),       PROP_RO(LuaPanel, windows),
    PROP_RW(LuaPanel, position_x), PROP_RW(LuaPanel, position_y), PROP_RW(LuaPanel, width),
    PROP_RW(LuaPanel, height),     {nullptr, nullptr, nullptr},
 };
@@ -82,6 +82,26 @@
    {nullptr, nullptr},
 };
 
+// Look for all descendant panels of class P and add the corresponding Lua version to the currently active Lua table. Class P needs to be a NamedPanel.
+template <class P, class LuaP>
+static void put_all_visible_panels_into_table(lua_State* L, UI::Panel* g) {
+	if (g == nullptr) {
+		return;
+	}
+
+	for (UI::Panel* child = g->get_first_child(); child; child = child->get_next_sibling()) {
+		put_all_visible_panels_into_table<P, LuaP>(L, child);
+
+		if (upcast(P, specific_panel, child)) {
+			if (specific_panel->is_visible()) {
+				lua_pushstring(L, specific_panel->get_name());
+				to_lua<LuaP>(L, new LuaP(specific_panel));
+				lua_rawset(L, -3);
+			}
+		}
+	}
+}
+
 /*
  * Properties
  */
@@ -97,26 +117,26 @@
 
       (RO) An :class:`array` of all visible buttons inside this Panel.
 */
-static void put_all_visible_buttons_into_table(lua_State* L, UI::Panel* g) {
-	if (!g)
-		return;
-
-	for (UI::Panel* f = g->get_first_child(); f; f = f->get_next_sibling()) {
-		put_all_visible_buttons_into_table(L, f);
-
-		if (upcast(UI::Button, b, f))
-			if (b->is_visible()) {
-				lua_pushstring(L, b->get_name());
-				to_lua<LuaButton>(L, new LuaButton(b));
-				lua_rawset(L, -3);
-			}
-	}
-}
 int LuaPanel::get_buttons(lua_State* L) {
 	assert(panel_);
 
 	lua_newtable(L);
-	put_all_visible_buttons_into_table(L, panel_);
+	put_all_visible_panels_into_table<UI::Button, LuaButton>(L, panel_);
+
+	return 1;
+}
+
+
+/* RST
+   .. attribute:: dropdowns
+
+      (RO) An :class:`array` of all visible dropdowns inside this Panel.
+*/
+int LuaPanel::get_dropdowns(lua_State* L) {
+	assert(panel_);
+
+	lua_newtable(L);
+	put_all_visible_panels_into_table<UI::BaseDropdown, LuaDropdown>(L, panel_);
 
 	return 1;
 }
@@ -156,25 +176,11 @@
       (RO) A :class:`array` of all currently open windows that are
          children of this Panel.
 */
-static void put_all_visible_windows_into_table(lua_State* L, UI::Panel* g) {
-	if (!g)
-		return;
-
-	for (UI::Panel* f = g->get_first_child(); f; f = f->get_next_sibling()) {
-		put_all_visible_windows_into_table(L, f);
-
-		if (upcast(UI::Window, win, f)) {
-			lua_pushstring(L, win->get_name());
-			to_lua<LuaWindow>(L, new LuaWindow(win));
-			lua_rawset(L, -3);
-		}
-	}
-}
 int LuaPanel::get_windows(lua_State* L) {
 	assert(panel_);
 
 	lua_newtable(L);
-	put_all_visible_windows_into_table(L, panel_);
+	put_all_visible_panels_into_table<UI::Window, LuaWindow>(L, panel_);
 
 	return 1;
 }
@@ -317,6 +323,7 @@
       event in tutorials
 */
 int LuaButton::press(lua_State* /* L */) {
+	log("Pressing button '%s'\n", get()->get_name().c_str());
 	get()->handle_mousein(true);
 	get()->handle_mousepress(SDL_BUTTON_LEFT, 1, 1);
 	return 0;
@@ -328,12 +335,102 @@
       it.
 */
 int LuaButton::click(lua_State* /* L */) {
+	log("Clicking button '%s'\n", get()->get_name().c_str());
 	get()->handle_mousein(true);
 	get()->handle_mousepress(SDL_BUTTON_LEFT, 1, 1);
 	get()->handle_mouserelease(SDL_BUTTON_LEFT, 1, 1);
 	return 0;
 }
 
+
+/*
+ * C Functions
+ */
+
+/* RST
+Dropdown
+--------
+
+.. class:: Dropdown
+
+   Child of: :class:`Panel`
+
+   This represents a dropdown menu.
+*/
+const char LuaDropdown::className[] = "Dropdown";
+const MethodType<LuaDropdown> LuaDropdown::Methods[] = {
+   METHOD(LuaDropdown, open),
+   METHOD(LuaDropdown, highlight_item),
+   METHOD(LuaDropdown, select),
+   {nullptr, nullptr},
+};
+const PropertyType<LuaDropdown> LuaDropdown::Properties[] = {
+   PROP_RO(LuaDropdown, name),
+   {nullptr, nullptr, nullptr},
+};
+
+/*
+ * Properties
+ */
+
+// Documented in parent Class
+int LuaDropdown::get_name(lua_State* L) {
+	lua_pushstring(L, get()->get_name());
+	return 1;
+}
+
+/*
+ * Lua Functions
+ */
+/* RST
+   .. method:: open
+
+      Open this dropdown menu.
+*/
+int LuaDropdown::open(lua_State* /* L */) {
+	log("Opening dropdown '%s'\n", get()->get_name().c_str());
+	get()->set_list_visibility(true);
+	return 0;
+}
+
+/* RST
+   .. method:: highlight_item(index)
+
+      :arg index: the index of the item to highlight, starting from ``1``
+      :type index: :class:`integer`
+
+      Highlights an item in this dropdown without triggering a selection.
+*/
+int LuaDropdown::highlight_item(lua_State* L) {
+	unsigned int desired_item = luaL_checkuint32(L, -1);
+	if (desired_item < 1 || desired_item > get()->size()) {
+		report_error(L, "Attempted to highlight item %d on dropdown '%s'. Avaliable range for this dropdown is 1-%d.", desired_item, get()->get_name().c_str(), get()->size());
+	}
+	log("Highlighting item %d in dropdown '%s'\n", desired_item, get()->get_name().c_str());
+	// Open the dropdown
+	get()->set_list_visibility(true);
+	// Press arrow down until the desired item is highlighted
+	SDL_Keysym code;
+	code.sym = SDLK_DOWN;
+	for (size_t i = 1; i < desired_item; ++i) {
+		get()->handle_key(true, code);
+	}
+	return 0;
+}
+
+/* RST
+   .. method:: select()
+
+      Selects the currently highlighted item in this dropdown.
+*/
+int LuaDropdown::select(lua_State* /* L */) {
+	log("Selecting current item in dropdown '%s'\n", get()->get_name().c_str());
+	SDL_Keysym code;
+	code.sym = SDLK_RETURN;
+	get()->handle_key(true, code);
+	return 0;
+}
+
 /*
  * C Functions
  */
@@ -388,6 +485,7 @@
       Click this tab making it the active one.
 */
 int LuaTab::click(lua_State* /* L */) {
+	log("Clicking tab '%s'\n", get()->get_name().c_str());
 	get()->activate();
 	return 0;
 }
@@ -437,6 +535,7 @@
       not use it any longer.
 */
 int LuaWindow::close(lua_State* /* L */) {
+	log("Closing window '%s'\n", get()->get_name().c_str());
 	delete panel_;
 	panel_ = nullptr;
 	return 0;
@@ -789,6 +888,10 @@
 	add_parent<LuaButton, LuaPanel>(L);
 	lua_pop(L, 1);  // Pop the meta table
 
+	register_class<LuaDropdown>(L, "ui", true);
+	add_parent<LuaDropdown, LuaPanel>(L);
+	lua_pop(L, 1);  // Pop the meta table
+
 	register_class<LuaTab>(L, "ui", true);
 	add_parent<LuaTab, LuaPanel>(L);
 	lua_pop(L, 1);  // Pop the meta table

=== modified file 'src/scripting/lua_ui.h'
--- src/scripting/lua_ui.h	2019-02-23 11:00:49 +0000
+++ src/scripting/lua_ui.h	2019-06-01 10:27:23 +0000
@@ -23,6 +23,7 @@
 #include "scripting/lua.h"
 #include "scripting/luna.h"
 #include "ui_basic/button.h"
+#include "ui_basic/dropdown.h"
 #include "ui_basic/tabpanel.h"
 #include "ui_basic/window.h"
 #include "wui/interactive_base.h"
@@ -57,7 +58,7 @@
 	}
 
 	void __persist(lua_State* L) override {
-		report_error(L, "Trying to persist a User Interface Panel which is no supported!");
+		report_error(L, "Trying to persist a User Interface Panel which is not supported!");
 	}
 	void __unpersist(lua_State* L) override {
 		report_error(L, "Trying to unpersist a User Interface Panel which is "
@@ -68,6 +69,7 @@
 	 * Properties
 	 */
 	int get_buttons(lua_State* L);
+	int get_dropdowns(lua_State* L);
 	int get_tabs(lua_State* L);
 	int get_windows(lua_State* L);
 	int get_width(lua_State* L);
@@ -121,6 +123,41 @@
 	}
 };
 
+
+class LuaDropdown : public LuaPanel {
+public:
+	LUNA_CLASS_HEAD(LuaDropdown);
+
+	LuaDropdown() : LuaPanel() {
+	}
+	explicit LuaDropdown(UI::Panel* p) : LuaPanel(p) {
+	}
+	explicit LuaDropdown(lua_State* L) : LuaPanel(L) {
+	}
+	~LuaDropdown() override {
+	}
+
+	/*
+	 * Properties
+	 */
+	int get_name(lua_State* L);
+
+	/*
+	 * Lua Methods
+	 */
+	int open(lua_State* L);
+	int highlight_item(lua_State* L);
+	int select(lua_State* L);
+
+	/*
+	 * C Methods
+	 */
+	UI::BaseDropdown* get() {
+		return static_cast<UI::BaseDropdown*>(panel_);
+	}
+};
+
+
 class LuaTab : public LuaPanel {
 public:
 	LUNA_CLASS_HEAD(LuaTab);

=== modified file 'src/ui_basic/button.cc'
--- src/ui_basic/button.cc	2019-05-26 17:21:15 +0000
+++ src/ui_basic/button.cc	2019-06-01 10:27:23 +0000
@@ -216,10 +216,10 @@
 			}
 		}
 
-	} else if (title_.length()) {
+	} else if (!title_.empty()) {
 		//  Otherwise draw title string centered
 		std::shared_ptr<const UI::RenderedText> rendered_text = autofit_text(
-		   richtext_escape(title_), style_to_use.font(), get_inner_w() - 2 * kButtonImageMargin);
+		   title_, style_to_use.font(), get_inner_w() - 2 * kButtonImageMargin);
 
 		// Blit on pixel boundary (not float), so that the text is blitted pixel perfect.
 		rendered_text->draw(dst, Vector2i((get_w() - rendered_text->width()) / 2,
@@ -285,7 +285,6 @@
 				time_nextact_ = time;
 			play_click();
 			sigclicked();
-			clicked();
 			//  The button may not exist at this point (for example if the button
 			//  closed the dialog that it is part of). So member variables may no
 			//  longer be accessed.
@@ -339,7 +338,6 @@
 		if (highlighted_ && enabled_) {
 			play_click();
 			sigclicked();
-			clicked();
 			//  The button may not exist at this point (for example if the button
 			//  closed the dialog that it is part of). So member variables may no
 			//  longer be accessed.

=== modified file 'src/ui_basic/button.h'
--- src/ui_basic/button.h	2019-04-17 16:52:55 +0000
+++ src/ui_basic/button.h	2019-06-01 10:27:23 +0000
@@ -156,9 +156,6 @@
 	boost::signals2::signal<void()> sigmouseout;
 
 protected:
-	virtual void clicked() {
-	}  /// Override this to react on the click.
-
 	bool highlighted_;  //  mouse is over the button
 	bool pressed_;      //  mouse is clicked over the button
 	bool enabled_;

=== modified file 'src/ui_basic/checkbox.cc'
--- src/ui_basic/checkbox.cc	2019-05-26 17:21:15 +0000
+++ src/ui_basic/checkbox.cc	2019-06-01 10:27:23 +0000
@@ -177,7 +177,7 @@
  */
 bool Statebox::handle_mousepress(const uint8_t btn, int32_t, int32_t) {
 	if (btn == SDL_BUTTON_LEFT && (flags_ & Is_Enabled)) {
-		clicked();
+		button_clicked();
 		return true;
 	}
 	return false;
@@ -190,7 +190,7 @@
 /**
  * Toggle the checkbox state
  */
-void Checkbox::clicked() {
+void Checkbox::button_clicked() {
 	clickedto(!get_state());
 	set_state(!get_state());
 	play_click();

=== modified file 'src/ui_basic/checkbox.h'
--- src/ui_basic/checkbox.h	2019-02-23 11:00:49 +0000
+++ src/ui_basic/checkbox.h	2019-06-01 10:27:23 +0000
@@ -74,7 +74,7 @@
 
 private:
 	void layout() override;
-	virtual void clicked() = 0;
+	virtual void button_clicked() = 0;
 
 	enum Flags {
 		Is_Highlighted = 0x01,
@@ -131,7 +131,7 @@
 	}
 
 private:
-	void clicked() override;
+	void button_clicked() override;
 };
 }  // namespace UI
 

=== modified file 'src/ui_basic/dropdown.cc'
--- src/ui_basic/dropdown.cc	2019-05-26 17:21:15 +0000
+++ src/ui_basic/dropdown.cc	2019-06-01 10:27:23 +0000
@@ -27,7 +27,6 @@
 #include "base/macros.h"
 #include "graphic/align.h"
 #include "graphic/font_handler.h"
-#include "graphic/graphic.h"
 #include "graphic/rendertarget.h"
 #include "graphic/text_layout.h"
 #include "ui_basic/mouse_constants.h"
@@ -53,30 +52,31 @@
 
 int BaseDropdown::next_id_ = 0;
 
-BaseDropdown::BaseDropdown(UI::Panel* parent,
+BaseDropdown::BaseDropdown(UI::Panel* parent, const std::string& name,
                            int32_t x,
                            int32_t y,
                            uint32_t w,
-                           uint32_t h,
+                           uint32_t max_list_items,
                            int button_dimension,
                            const std::string& label,
                            const DropdownType type,
-                           UI::PanelStyle style)
-   : UI::Panel(parent,
+                           UI::PanelStyle style, ButtonStyle button_style)
+   : UI::NamedPanel(parent,
+					name,
                x,
                y,
-               type == DropdownType::kPictorial ? button_dimension : w,
+               (type == DropdownType::kPictorial || type_ == DropdownType::kPictorialMenu) ? button_dimension : w,
                // Height only to fit the button, so we can use this in Box layout.
                base_height(button_dimension, style)),
      id_(next_id_++),
-     max_list_height_(h - 2 * get_h()),
-     list_width_(w),
+     max_list_items_(max_list_items),
+	 max_list_height_(std::numeric_limits<uint32_t>::max()),
      list_offset_x_(0),
      list_offset_y_(0),
      button_dimension_(button_dimension),
      base_height_(base_height(button_dimension, style)),
      mouse_tolerance_(50),
-     button_box_(this, 0, 0, UI::Box::Horizontal, w, h),
+     button_box_(this, 0, 0, UI::Box::Horizontal, w, get_h()),
      push_button_(type == DropdownType::kTextual ?
                      new UI::Button(&button_box_,
                                     "dropdown_select",
@@ -84,9 +84,7 @@
                                     0,
                                     button_dimension,
                                     get_h(),
-                                    style == UI::PanelStyle::kFsMenu ?
-                                       UI::ButtonStyle::kFsMenuMenu :
-                                       UI::ButtonStyle::kWuiSecondary,
+                                    button_style,
                                     g_gr->images().get("images/ui_basic/scrollbar_down.png")) :
                      nullptr),
      display_button_(&button_box_,
@@ -97,8 +95,10 @@
                         w - button_dimension :
                         type == DropdownType::kTextualNarrow ? w : button_dimension,
                      get_h(),
-                     style == UI::PanelStyle::kFsMenu ? UI::ButtonStyle::kFsMenuSecondary :
-                                                        UI::ButtonStyle::kWuiSecondary,
+					 type == DropdownType::kTextual ?
+						 (style == UI::PanelStyle::kFsMenu ? UI::ButtonStyle::kFsMenuSecondary :
+															 UI::ButtonStyle::kWuiSecondary) :
+						 button_style,
                      label),
      label_(label),
      type_(type),
@@ -110,26 +110,30 @@
 	}
 
 	// Close whenever another dropdown is opened
-	subscriber_ = Notifications::subscribe<NoteDropdown>([this](const NoteDropdown& note) {
+	dropdown_subscriber_ = Notifications::subscribe<NoteDropdown>([this](const NoteDropdown& note) {
 		if (id_ != note.id) {
 			close();
 		}
 	});
+	graphic_resolution_changed_subscriber_ = Notifications::subscribe<GraphicResolutionChanged>(
+	   [this](const GraphicResolutionChanged&) {
+		layout();
+	});
 
-	assert(max_list_height_ > 0);
+	assert(max_list_items_ > 0);
 	// Hook into highest parent that we can get so that we can drop down outside the panel.
-	// Positioning breaks down with TabPanels, so we exclude them.
-	while (parent->get_parent() && !is_a(UI::TabPanel, parent->get_parent())) {
-		parent = parent->get_parent();
+	UI::Panel* list_parent = &display_button_;
+	while (list_parent->get_parent()) {
+		list_parent = list_parent->get_parent();
 	}
-	list_ = new UI::Listselect<uintptr_t>(parent, 0, 0, w, 0, style, ListselectLayout::kDropdown);
+	list_ = new UI::Listselect<uintptr_t>(list_parent, 0, 0, w, 0, style, ListselectLayout::kDropdown);
 
 	list_->set_visible(false);
-	button_box_.add(&display_button_);
+	button_box_.add(&display_button_, UI::Box::Resizing::kExpandBoth);
 	display_button_.sigclicked.connect(boost::bind(&BaseDropdown::toggle_list, this));
 	if (push_button_ != nullptr) {
 		display_button_.set_perm_pressed(true);
-		button_box_.add(push_button_);
+		button_box_.add(push_button_, UI::Box::Resizing::kFullSize);
 		push_button_->sigclicked.connect(boost::bind(&BaseDropdown::toggle_list, this));
 	}
 	button_box_.set_size(w, get_h());
@@ -138,26 +142,19 @@
 	set_can_focus(true);
 	set_value();
 
-	// Find parent windows so that we can move the list along with them
-	UI::Panel* parent_window_candidate = get_parent();
-	while (parent_window_candidate) {
-		if (upcast(UI::Window, window, parent_window_candidate)) {
-			window->position_changed.connect(boost::bind(&BaseDropdown::layout, this));
-		}
-		parent_window_candidate = parent_window_candidate->get_parent();
+	// Find parent windows, boxes etc. so that we can move the list along with them
+	UI::Panel* ancestor = this;
+	while ((ancestor = ancestor->get_parent()) != nullptr) {
+		ancestor->position_changed.connect([this] {layout(); });
 	}
-
 	layout();
 }
 
 BaseDropdown::~BaseDropdown() {
 	// The list needs to be able to drop outside of windows, so it won't close with the window.
-	// Deleting here leads to conflict with who gets to delete it, so we hide it instead.
+	// Deleting here leads to a conflict as to who gets to delete it, so we just leave it.
+	// It will be hidden as soon as the mouse moves away anyway.
 	// TODO(GunChleoc): Investigate whether we can find a better solution for this
-	if (list_) {
-		list_->clear();
-		list_->set_visible(false);
-	}
 }
 
 void BaseDropdown::set_height(int height) {
@@ -165,27 +162,19 @@
 	layout();
 }
 
-void BaseDropdown::set_max_items(int items) {
-	set_height(list_->get_lineheight() * items + base_height_);
-}
-
 void BaseDropdown::layout() {
-	const int base_h = base_height_;
-	const int w = type_ == DropdownType::kPictorial ? button_dimension_ : get_w();
-	button_box_.set_size(w, base_h);
-	display_button_.set_desired_size(
-	   type_ == DropdownType::kTextual ? w - button_dimension_ : w, base_h);
-	int new_list_height =
-	   std::min(static_cast<int>(list_->size()) * list_->get_lineheight(), max_list_height_);
-	list_->set_size(type_ != DropdownType::kPictorial ? w : list_width_, new_list_height);
-	set_desired_size(w, base_h);
+	int list_width = list_->calculate_desired_width();
+
+	const int new_list_height =
+	   std::min(max_list_height_ / list_->get_lineheight(), std::min(list_->size(), max_list_items_)) * list_->get_lineheight();
+	list_->set_size(std::max(list_width, button_box_.get_w()), new_list_height);
 
 	// Update list position. The list is hooked into the highest parent that we can get so that we
-	// can drop down outside the panel. Positioning breaks down with TabPanels, so we exclude them.
-	UI::Panel* parent = get_parent();
-	int new_list_x = get_x() + parent->get_x() + parent->get_lborder();
-	int new_list_y = get_y() + parent->get_y() + parent->get_tborder();
-	while (parent->get_parent() && !is_a(UI::TabPanel, parent->get_parent())) {
+	// can drop down outside the panel.
+	UI::Panel* parent = &display_button_;
+	int new_list_x = display_button_.get_x();
+	int new_list_y = display_button_.get_y();
+	while (parent->get_parent()) {
 		parent = parent->get_parent();
 		new_list_x += parent->get_x() + parent->get_lborder();
 		new_list_y += parent->get_y() + parent->get_tborder();
@@ -217,13 +206,24 @@
 	}
 }
 
+void BaseDropdown::set_size(int nw, int nh) {
+	button_box_.set_size(nw, nh);
+	Panel::set_size(nw, nh);
+	layout();
+}
+void BaseDropdown::set_desired_size(int nw, int nh) {
+	button_box_.set_desired_size(nw, nh);
+	Panel::set_desired_size(nw, nh);
+	layout();
+}
+
 void BaseDropdown::add(const std::string& name,
                        const uint32_t value,
                        const Image* pic,
                        const bool select_this,
-                       const std::string& tooltip_text) {
+                       const std::string& tooltip_text, const std::string& hotkey = std::string()) {
 	assert(pic != nullptr || type_ != DropdownType::kPictorial);
-	list_->add(name, value, pic, select_this, tooltip_text);
+	list_->add(name, value, pic, select_this, tooltip_text, hotkey);
 	if (select_this) {
 		set_value();
 	}
@@ -248,7 +248,7 @@
 
 void BaseDropdown::set_label(const std::string& text) {
 	label_ = text;
-	if (type_ != DropdownType::kPictorial) {
+	if (type_ != DropdownType::kPictorial && type_ != DropdownType::kPictorialMenu) {
 		display_button_.set_title(label_);
 	}
 }
@@ -267,7 +267,7 @@
 
 void BaseDropdown::set_errored(const std::string& error_message) {
 	set_tooltip((boost::format(_("%1%: %2%")) % _("Error") % error_message).str());
-	if (type_ != DropdownType::kPictorial) {
+	if (type_ != DropdownType::kPictorial && type_ != DropdownType::kPictorialMenu) {
 		set_label(_("Error"));
 	} else {
 		set_image(g_gr->images().get("images/ui_basic/different.png"));
@@ -295,7 +295,7 @@
 
 void BaseDropdown::set_pos(Vector2i point) {
 	UI::Panel::set_pos(point);
-	list_->set_pos(Vector2i(point.x, point.y + get_h()));
+	layout();
 }
 
 void BaseDropdown::clear() {
@@ -321,6 +321,11 @@
 }
 
 void BaseDropdown::update() {
+	if (type_ == DropdownType::kPictorialMenu) {
+		// Menus never change their main image and text
+		return;
+	}
+
 	const std::string name = list_->has_selection() ?
 	                            list_->get_selected_name() :
 	                            /** TRANSLATORS: Selection in Dropdown menus. */
@@ -349,6 +354,34 @@
 	current_selection_ = list_->selection_index();
 }
 
+void BaseDropdown::toggle() {
+	set_list_visibility(!list_->is_visible());
+}
+
+void BaseDropdown::set_list_visibility(bool open) {
+	if (!is_enabled_) {
+		list_->set_visible(false);
+		return;
+	}
+	list_->set_visible(open);
+	if (list_->is_visible()) {
+		list_->move_to_top();
+		focus();
+		set_mouse_pos(
+					Vector2i(
+						display_button_.get_x() + (display_button_.get_w() * 3 / 5),
+						display_button_.get_y() + (display_button_.get_h() * 2 / 5)));
+		if (type_ == DropdownType::kPictorialMenu && !has_selection() && !list_->empty()) {
+			select(0);
+		}
+	}
+	if (type_ != DropdownType::kTextual) {
+		display_button_.set_perm_pressed(list_->is_visible());
+	}
+	// Make sure that the list covers and deactivates the elements below it
+	set_layout_toplevel(list_->is_visible());
+}
+
 void BaseDropdown::toggle_list() {
 	if (!is_enabled_) {
 		list_->set_visible(false);
@@ -387,6 +420,7 @@
 		case SDLK_RETURN:
 			if (list_->is_visible()) {
 				set_value();
+				return true;
 			}
 			break;
 		case SDLK_ESCAPE:
@@ -397,6 +431,7 @@
 			}
 			break;
 		case SDLK_DOWN:
+		case SDLK_UP:
 			if (!list_->is_visible() && !is_mouse_away()) {
 				toggle_list();
 				return true;

=== modified file 'src/ui_basic/dropdown.h'
--- src/ui_basic/dropdown.h	2019-04-17 16:52:55 +0000
+++ src/ui_basic/dropdown.h	2019-06-01 10:27:23 +0000
@@ -25,6 +25,7 @@
 
 #include <boost/signals2.hpp>
 
+#include "graphic/graphic.h"
 #include "graphic/image.h"
 #include "notifications/note_ids.h"
 #include "notifications/notifications.h"
@@ -44,31 +45,35 @@
 	}
 };
 
-/// The narrow textual dropdown omits the extra push button
-enum class DropdownType { kTextual, kTextualNarrow, kPictorial };
+/// The narrow textual dropdown omits the extra push button.
+/// Use kPictorialMenu if you want to trigger an action without changing the menu button.
+enum class DropdownType { kTextual, kTextualNarrow, kPictorial, kPictorialMenu };
 
 /// Implementation for a dropdown menu that lets the user select a value.
-class BaseDropdown : public Panel {
+class BaseDropdown : public NamedPanel {
 protected:
 	/// \param parent             the parent panel
+	/// \param name               a name so that we can reference the dropdown via Lua
 	/// \param x                  the x-position within 'parent'
 	/// \param y                  the y-position within 'parent'
 	/// \param list_w             the dropdown's width
-	/// \param list_h             the maximum height for the dropdown list
+	/// \param max_list_items     the maximum number of items shown in the list before it starts using a scrollbar
 	/// \param button_dimension   the width of the push button in textual dropdowns. For pictorial
 	/// dropdowns, this is both the width and the height of the button.
 	/// \param label              a label to prefix to the selected entry on the display button.
 	/// \param type               whether this is a textual or pictorial dropdown
 	/// \param style              the style used for buttons and background
 	BaseDropdown(Panel* parent,
+				 const std::string& name,
 	             int32_t x,
 	             int32_t y,
 	             uint32_t list_w,
-	             uint32_t list_h,
+	             uint32_t max_list_items,
 	             int button_dimension,
 	             const std::string& label,
 	             const DropdownType type,
-	             PanelStyle style);
+	             PanelStyle style,
+				 ButtonStyle button_style);
 	~BaseDropdown() override;
 
 public:
@@ -120,10 +125,20 @@
 	/// Handle keypresses
 	bool handle_key(bool down, SDL_Keysym code) override;
 
+	/// Set maximum available height in the UI
 	void set_height(int height);
 
-	/// Set the number of items to fit in the list
-	void set_max_items(int items);
+	/// Toggle the list on and off and position the mouse on the button so that the dropdown won't close on us.
+	/// If this is a menu and nothing was selected yet, select the first item for easier keyboard navigation.
+	void toggle();
+
+	/// If 'open', show the list and position the mouse on the button so that the dropdown won't close on us.
+	/// If this is a menu and nothing was selected yet, select the first item for easier keyboard navigation.
+	/// If not 'open', close the list.
+	void set_list_visibility(bool open);
+
+	void set_size(int nw, int nh) override;
+	void set_desired_size(int w, int h) override;
 
 protected:
 	/// Add an element to the list
@@ -132,13 +147,14 @@
 	/// \param pic          an image to illustrate the entry. Can be nullptr for textual dropdowns.
 	/// \param select_this  whether this element should be selected
 	/// \param tooltip_text a tooltip for this entry
+	/// \param hotkey       a hotkey tip if any
 	///
 	/// Text conventions: Title Case for the 'name', Sentence case for the 'tooltip_text'
 	void add(const std::string& name,
 	         uint32_t value,
-	         const Image* pic = nullptr,
-	         const bool select_this = false,
-	         const std::string& tooltip_text = std::string());
+	         const Image* pic,
+	         const bool select_this,
+	         const std::string& tooltip_text, const std::string& hotkey);
 
 	/// \return the index of the selected element
 	uint32_t get_selected() const;
@@ -161,7 +177,7 @@
 
 	/// Updates the title and tooltip of the display button and triggers a 'selected' signal.
 	void set_value();
-	/// Toggles the dropdown list on and off.
+	/// Toggles the dropdown list on and off and sends a notification if the list is visible afterwards.
 	void toggle_list();
 	/// Toggle the list closed if the dropdown is currently expanded.
 	void close();
@@ -172,11 +188,12 @@
 	/// Give each dropdown a unique ID
 	static int next_id_;
 	const int id_;
-	std::unique_ptr<Notifications::Subscriber<NoteDropdown>> subscriber_;
+	std::unique_ptr<Notifications::Subscriber<NoteDropdown>> dropdown_subscriber_;
+	std::unique_ptr<Notifications::Subscriber<GraphicResolutionChanged>> graphic_resolution_changed_subscriber_;
 
 	// Dimensions
-	int max_list_height_;
-	int list_width_;
+	unsigned int max_list_items_;
+	unsigned int max_list_height_;
 	int list_offset_x_;
 	int list_offset_y_;
 	const int button_dimension_;
@@ -199,10 +216,11 @@
 template <typename Entry> class Dropdown : public BaseDropdown {
 public:
 	/// \param parent             the parent panel
+	/// \param name               a name so that we can reference the dropdown via Lua
 	/// \param x                  the x-position within 'parent'
 	/// \param y                  the y-position within 'parent'
 	/// \param list_w             the dropdown's width
-	/// \param list_h             the maximum height for the dropdown list
+	/// \param max_list_items     the maximum number of items shown in the list before it starts using a scrollbar
 	/// \param button_dimension   the width of the push button in textual dropdowns. For pictorial
 	/// dropdowns, this is both the width and the height of the button.
 	/// \param label              a label to prefix to the selected entry on the display button.
@@ -210,15 +228,17 @@
 	/// \param style              the style used for buttons and background
 	/// Text conventions: Title Case for all elements
 	Dropdown(Panel* parent,
+			 const std::string& name,
 	         int32_t x,
 	         int32_t y,
 	         uint32_t list_w,
-	         uint32_t list_h,
+	         uint32_t max_list_items,
 	         int button_dimension,
 	         const std::string& label,
 	         const DropdownType type,
-	         PanelStyle style)
-	   : BaseDropdown(parent, x, y, list_w, list_h, button_dimension, label, type, style) {
+	         PanelStyle style,
+			 ButtonStyle button_style)
+	   : BaseDropdown(parent, name, x, y, list_w, max_list_items, button_dimension, label, type, style, button_style) {
 	}
 	~Dropdown() {
 		entry_cache_.clear();
@@ -231,13 +251,14 @@
 	/// only.
 	/// \param select_this  whether this element should be selected
 	/// \param tooltip_text a tooltip for this entry
+	/// \param hotkey       a hotkey tip if any
 	void add(const std::string& name,
 	         Entry value,
 	         const Image* pic = nullptr,
 	         const bool select_this = false,
-	         const std::string& tooltip_text = std::string()) {
+	         const std::string& tooltip_text = std::string(), const std::string& hotkey = std::string()) {
 		entry_cache_.push_back(std::unique_ptr<Entry>(new Entry(value)));
-		BaseDropdown::add(name, size(), pic, select_this, tooltip_text);
+		BaseDropdown::add(name, size(), pic, select_this, tooltip_text, hotkey);
 	}
 
 	/// \return the selected element

=== modified file 'src/ui_basic/listselect.cc'
--- src/ui_basic/listselect.cc	2019-05-26 17:21:15 +0000
+++ src/ui_basic/listselect.cc	2019-06-01 10:27:23 +0000
@@ -34,8 +34,25 @@
 #include "ui_basic/mouse_constants.h"
 
 constexpr int kMargin = 2;
+constexpr int kHotkeyGap = 16;
 
 namespace UI {
+
+
+BaseListselect::EntryRecord::EntryRecord(const std::string& init_name,
+					 uint32_t init_entry,
+					 const Image* init_pic,
+					 const std::string& tooltip_text, const std::string& hotkey_text, const TableStyleInfo& style) :
+	name(init_name),
+	entry_(init_entry),
+	pic(init_pic),
+	tooltip(tooltip_text),
+	name_alignment(i18n::has_rtl_character(init_name.c_str(), 20) ? Align::kRight : Align::kLeft),
+	hotkey_alignment(i18n::has_rtl_character(hotkey_text.c_str(), 20) ? Align::kRight : Align::kLeft) {
+	rendered_name = UI::g_fh->render(as_richtext_paragraph(name, style.enabled()));
+	rendered_hotkey = UI::g_fh->render(as_richtext_paragraph(hotkey_text, style.hotkey()));
+}
+
 /**
  * Initialize a list select panel
  *
@@ -53,17 +70,19 @@
                                UI::PanelStyle style,
                                const ListselectLayout selection_mode)
    : Panel(parent, x, y, w, h),
+	 widest_text_(0),
+	 widest_hotkey_(0),
      scrollbar_(this, get_w() - Scrollbar::kSize, 0, Scrollbar::kSize, h, style),
      scrollpos_(0),
      selection_(no_selection_index()),
      last_click_time_(-10000),
      last_selection_(no_selection_index()),
      selection_mode_(selection_mode),
-     font_style_(&g_gr->styles().table_style(style).enabled()),
+	 table_style_(g_gr->styles().table_style(style)),
      background_style_(selection_mode == ListselectLayout::kDropdown ?
                           g_gr->styles().dropdown_style(style) :
                           nullptr),
-     lineheight_(text_height(*font_style_) + kMargin) {
+	 lineheight_(text_height(table_style_.enabled()) + kMargin) {
 	set_thinks(false);
 
 	scrollbar_.moved.connect(boost::bind(&BaseListselect::set_scrollpos, this, _1));
@@ -115,13 +134,12 @@
                          uint32_t entry,
                          const Image* pic,
                          bool const sel,
-                         const std::string& tooltip_text) {
-	EntryRecord* er = new EntryRecord();
+                         const std::string& tooltip_text, const std::string& hotkey) {
+	EntryRecord* er = new EntryRecord(
+						  name,
+						  entry, pic, tooltip_text,
+						  hotkey, table_style_);
 
-	er->entry_ = entry;
-	er->pic = pic;
-	er->name = name;
-	er->tooltip = tooltip_text;
 	int entry_height = lineheight_;
 	if (pic) {
 		int w = pic->width();
@@ -142,41 +160,6 @@
 		select(entry_records_.size() - 1);
 }
 
-void BaseListselect::add_front(const std::string& name,
-                               const Image* pic,
-                               bool const sel,
-                               const std::string& tooltip_text) {
-	EntryRecord* er = new EntryRecord();
-
-	er->entry_ = 0;
-	for (EntryRecord* temp_entry : entry_records_) {
-		++(temp_entry)->entry_;
-	}
-
-	er->pic = pic;
-	er->name = name;
-	er->tooltip = tooltip_text;
-
-	int entry_height = lineheight_;
-	if (pic) {
-		int w = pic->width();
-		int h = pic->height();
-		entry_height = (h >= entry_height) ? h : entry_height;
-		if (max_pic_width_ < w)
-			max_pic_width_ = w;
-	}
-
-	if (entry_height > lineheight_)
-		lineheight_ = entry_height;
-
-	entry_records_.push_front(er);
-
-	layout();
-
-	if (sel)
-		select(0);
-}
-
 /**
  * Sort the listbox alphabetically. make sure that the current selection stays
  * valid (though it might scroll out of visibility).
@@ -275,13 +258,50 @@
 }
 
 int BaseListselect::get_lineheight() const {
-	return lineheight_ + kMargin;
+	return lineheight_ + (selection_mode_ == ListselectLayout::kDropdown ? 2 * kMargin : kMargin);
 }
 
 uint32_t BaseListselect::get_eff_w() const {
 	return scrollbar_.is_enabled() ? get_w() - scrollbar_.get_w() : get_w();
 }
 
+int BaseListselect::calculate_desired_width() {
+	if (entry_records_.empty()) {
+		return 0;
+	}
+	// Make enough room for all texts + hotkeys in tabular format
+	widest_text_ = 0;
+	widest_hotkey_ = 0;
+	size_t entry_with_widest_text = 0;
+	size_t entry_with_widest_hotkey = 0;
+
+	// Find the widest entries
+	for (size_t i = 0; i < entry_records_.size(); ++i) {
+		const EntryRecord& er = *entry_records_[i];
+		const int current_text_width = er.rendered_name->width();
+		if (current_text_width > widest_text_) {
+			widest_text_ = current_text_width;
+			entry_with_widest_text = i;
+		}
+		const int current_hotkey_width = er.rendered_hotkey->width();
+		if (current_hotkey_width > widest_hotkey_) {
+			widest_hotkey_ = current_hotkey_width;
+			entry_with_widest_hotkey = i;
+		}
+	}
+
+	// Add up the width
+	int text_width = entry_records_[entry_with_widest_text]->rendered_name->width();
+	if (widest_hotkey_ > 0) {
+		text_width += kHotkeyGap;
+		text_width += entry_records_[entry_with_widest_hotkey]->rendered_hotkey->width();
+	}
+
+	const int picw = max_pic_width_ ? max_pic_width_ + 10 : 0;
+	const int old_width = get_w();
+	return text_width + picw + 8 + old_width - get_eff_w();
+}
+
 void BaseListselect::layout() {
 	scrollbar_.set_size(scrollbar_.get_w(), get_h());
 	scrollbar_.set_pos(Vector2i(get_w() - Scrollbar::kSize, 0));
@@ -294,15 +314,9 @@
 	}
 	// For dropdowns, autoincrease width
 	if (selection_mode_ == ListselectLayout::kDropdown) {
-		for (size_t i = 0; i < entry_records_.size(); ++i) {
-			const EntryRecord& er = *entry_records_[i];
-			std::shared_ptr<const UI::RenderedText> rendered_text =
-			   UI::g_fh->render(as_richtext_paragraph(richtext_escape(er.name), *font_style_));
-			int picw = max_pic_width_ ? max_pic_width_ + 10 : 0;
-			int difference = rendered_text->width() + picw + 8 - get_eff_w();
-			if (difference > 0) {
-				set_size(get_w() + difference, get_h());
-			}
+		const int new_width = calculate_desired_width();
+		if (new_width > get_w()) {
+			set_size(new_width, get_h());
 		}
 	}
 }
@@ -341,10 +355,9 @@
 		assert(eff_h < std::numeric_limits<int32_t>::max());
 
 		const EntryRecord& er = *entry_records_[idx];
-		std::shared_ptr<const UI::RenderedText> rendered_text =
-		   UI::g_fh->render(as_richtext_paragraph(richtext_escape(er.name), *font_style_));
+		const int text_height = std::max(er.rendered_name->height(), er.rendered_hotkey->height());
 
-		int lineheight = std::max(get_lineheight(), rendered_text->height());
+		int lineheight = std::max(get_lineheight(), text_height);
 
 		// Don't draw over the bottom edge
 		lineheight = std::min(eff_h - y, lineheight);
@@ -380,26 +393,15 @@
 		// Now draw pictures
 		if (er.pic) {
 			dst.blit(Vector2i(UI::g_fh->fontset()->is_rtl() ? get_eff_w() - er.pic->width() - 1 : 1,
-			                  y + (get_lineheight() - er.pic->height()) / 2),
+			                  y + (lineheight_ - er.pic->height()) / 2),
 			         er.pic);
 		}
 
-		// Position the text according to alignment
-		Align alignment = i18n::has_rtl_character(er.name.c_str(), 20) ? Align::kRight : Align::kLeft;
-		if (alignment == UI::Align::kRight) {
-			point.x += maxw - picw;
-		}
-
-		// Shift for image width
-		if (!UI::g_fh->fontset()->is_rtl()) {
-			point.x += picw;
-		}
-
 		// Fix vertical position for mixed font heights
-		if (get_lineheight() > rendered_text->height()) {
-			point.y += (lineheight_ - rendered_text->height()) / 2;
+		if (get_lineheight() > text_height) {
+			point.y += (lineheight_ - text_height) / 2;
 		} else {
-			point.y -= (rendered_text->height() - lineheight_) / 2;
+			point.y -= (text_height - lineheight_) / 2;
 		}
 
 		// Don't draw over the bottom edge
@@ -407,8 +409,35 @@
 		if (lineheight < 0) {
 			break;
 		}
-		rendered_text->draw(
-		   dst, point, Recti(0, 0, maxw, lineheight), alignment, RenderedText::CropMode::kSelf);
+
+		// Tabular layout for hotkeys + shift for image width
+		Vector2i text_point(point);
+		Vector2i hotkey_point(point);
+		if (UI::g_fh->fontset()->is_rtl()) {
+			if (er.name_alignment == UI::Align::kRight) {
+				text_point.x = maxw - widest_text_ - picw;
+			} else if (widest_hotkey_ > 0) {
+				text_point.x += widest_hotkey_ + kHotkeyGap;
+			}
+		} else {
+			hotkey_point.x = maxw - widest_hotkey_;
+			text_point.x += picw;
+		}
+
+		// Position the text and hotkey according to their alignment
+		if (er.name_alignment == UI::Align::kRight) {
+			text_point.x += widest_text_ - er.rendered_name->width();
+		}
+		if (er.hotkey_alignment == UI::Align::kRight) {
+			hotkey_point.x += widest_hotkey_ - er.rendered_hotkey->width();
+		}
+
+		er.rendered_name->draw(
+		   dst, text_point, Recti(0, 0, maxw - widest_hotkey_, lineheight), UI::Align::kLeft, RenderedText::CropMode::kSelf);
+		if (er.rendered_hotkey->width() > 0) {
+			er.rendered_hotkey->draw(
+			   dst, hotkey_point, Recti(0, 0, maxw - widest_text_, lineheight), UI::Align::kLeft, RenderedText::CropMode::kSelf);
+		}
 		y += get_lineheight();
 		++idx;
 	}

=== modified file 'src/ui_basic/listselect.h'
--- src/ui_basic/listselect.h	2019-04-19 05:59:14 +0000
+++ src/ui_basic/listselect.h	2019-06-01 10:27:23 +0000
@@ -26,7 +26,7 @@
 #include <boost/signals2.hpp>
 
 #include "graphic/color.h"
-#include "graphic/styles/font_style.h"
+#include "graphic/styles/table_style.h"
 #include "ui_basic/panel.h"
 #include "ui_basic/scrollbar.h"
 
@@ -66,16 +66,10 @@
 	 */
 	void add(const std::string& name,
 	         uint32_t value,
-	         const Image* pic = nullptr,
-	         const bool select_this = false,
-	         const std::string& tooltip_text = std::string());
-	/**
-	 * Text conventions: Title Case for the 'name', Sentence case for the 'tooltip_text'
-	 */
-	void add_front(const std::string& name,
-	               const Image* pic = nullptr,
-	               const bool select_this = false,
-	               const std::string& tooltip_text = std::string());
+	         const Image* pic,
+	         const bool select_this,
+	         const std::string& tooltip_text, const std::string& hotkey);
+
 	void remove(uint32_t);
 	void remove(const char* name);
 
@@ -113,6 +107,8 @@
 
 	uint32_t get_eff_w() const;
 
+	int calculate_desired_width();
+
 	void layout() override;
 
 	// Drawing and event handling
@@ -125,22 +121,32 @@
 
 private:
 	static const int32_t DOUBLE_CLICK_INTERVAL = 500;  // half a second
+	static const int32_t ms_darken_value = -20;
 
 	void set_scrollpos(int32_t);
 
-private:
-	static const int32_t ms_darken_value = -20;
-
 	struct EntryRecord {
-		uint32_t entry_;
+		explicit EntryRecord(const std::string& init_name,
+							 uint32_t init_entry,
+							 const Image* init_pic,
+							 const std::string& tooltip_text, const std::string& hotkey_text,
+							 const UI::TableStyleInfo& style);
+
+		const std::string name;
+		const uint32_t entry_;
 		const Image* pic;
-		std::string name;
-		std::string tooltip;
+		const std::string tooltip;
+		const Align name_alignment;
+		const Align hotkey_alignment;
+		std::shared_ptr<const UI::RenderedText> rendered_name;
+		std::shared_ptr<const UI::RenderedText> rendered_hotkey;
 	};
-	using EntryRecordDeque = std::deque<EntryRecord*>;
 
 	int max_pic_width_;
-	EntryRecordDeque entry_records_;
+	int widest_text_;
+	int widest_hotkey_;
+
+	std::deque<EntryRecord*> entry_records_;
 	Scrollbar scrollbar_;
 	uint32_t scrollpos_;  //  in pixels
 	uint32_t selection_;
@@ -148,7 +154,7 @@
 	uint32_t last_selection_;  // for double clicks
 	ListselectLayout selection_mode_;
 	const Image* check_pic_;
-	const FontStyleInfo* font_style_;
+	const UI::TableStyleInfo& table_style_;
 	const UI::PanelStyleInfo* background_style_;  // Background color and texture. Not owned.
 	int lineheight_;
 	std::string current_tooltip_;
@@ -169,17 +175,9 @@
 	         Entry value,
 	         const Image* pic = nullptr,
 	         const bool select_this = false,
-	         const std::string& tooltip_text = std::string()) {
+	         const std::string& tooltip_text = std::string(), const std::string& hotkey = std::string()) {
 		entry_cache_.push_back(value);
-		BaseListselect::add(name, entry_cache_.size() - 1, pic, select_this, tooltip_text);
-	}
-	void add_front(const std::string& name,
-	               Entry value,
-	               const Image* pic = nullptr,
-	               const bool select_this = false,
-	               const std::string& tooltip_text = std::string()) {
-		entry_cache_.push_front(value);
-		BaseListselect::add_front(name, pic, select_this, tooltip_text);
+		BaseListselect::add(name, entry_cache_.size() - 1, pic, select_this, tooltip_text, hotkey);
 	}
 
 	const Entry& operator[](uint32_t const i) const {
@@ -218,15 +216,8 @@
 	         Entry& value,
 	         const Image* pic = nullptr,
 	         const bool select_this = false,
-	         const std::string& tooltip_text = std::string()) {
-		Base::add(name, &value, pic, select_this, tooltip_text);
-	}
-	void add_front(const std::string& name,
-	               Entry& value,
-	               const Image* pic = nullptr,
-	               const bool select_this = false,
-	               const std::string& tooltip_text = std::string()) {
-		Base::add_front(name, &value, pic, select_this, tooltip_text);
+	         const std::string& tooltip_text = std::string(), const std::string& hotkey = std::string()) {
+		Base::add(name, &value, pic, select_this, tooltip_text, hotkey);
 	}
 
 	Entry& operator[](uint32_t const i) const {

=== modified file 'src/ui_basic/panel.cc'
--- src/ui_basic/panel.cc	2019-05-03 19:24:19 +0000
+++ src/ui_basic/panel.cc	2019-06-01 10:27:23 +0000
@@ -123,8 +123,12 @@
 	// Scan-build claims this results in double free.
 	// This is a false positive.
 	// See https://bugs.launchpad.net/widelands/+bug/1198928
-	while (first_child_)
+	while (first_child_) {
+		Panel* next_child = first_child_->next_;
 		delete first_child_;
+		first_child_ = next_child;
+	}
+	first_child_ = nullptr;
 }
 
 /**
@@ -530,6 +534,7 @@
 bool Panel::handle_mousepress(const uint8_t btn, int32_t, int32_t) {
 	if (btn == SDL_BUTTON_LEFT && get_can_focus()) {
 		focus();
+		clicked();
 	}
 	return false;
 }
@@ -735,10 +740,12 @@
 		Panel* p = next;
 		next = p->next_;
 
-		if (p->flags_ & pf_die)
+		if (p->flags_ & pf_die) {
 			delete p;
-		else if (p->flags_ & pf_child_die)
+			p = nullptr;
+		} else if (p->flags_ & pf_child_die) {
 			p->check_child_death();
+		}
 	}
 
 	flags_ &= ~pf_child_die;

=== modified file 'src/ui_basic/panel.h'
--- src/ui_basic/panel.h	2019-05-12 07:45:59 +0000
+++ src/ui_basic/panel.h	2019-06-01 10:27:23 +0000
@@ -89,6 +89,7 @@
 	      const std::string& tooltip_text = std::string());
 	virtual ~Panel();
 
+	boost::signals2::signal<void()> clicked;
 	boost::signals2::signal<void()> position_changed;
 
 	Panel* get_parent() const {
@@ -119,8 +120,8 @@
 	virtual void end();
 
 	// Geometry
-	void set_size(int nw, int nh);
-	void set_desired_size(int w, int h);
+	virtual void set_size(int nw, int nh);
+	virtual void set_desired_size(int w, int h);
 	virtual void set_pos(Vector2i);
 	virtual void move_inside_parent();
 	virtual void layout();

=== modified file 'src/ui_basic/radiobutton.cc'
--- src/ui_basic/radiobutton.cc	2019-02-23 11:00:49 +0000
+++ src/ui_basic/radiobutton.cc	2019-06-01 10:27:23 +0000
@@ -48,7 +48,7 @@
  * Inform the radiogroup about the click; the group is responsible of setting
  * button states.
  */
-void Radiobutton::clicked() {
+void Radiobutton::button_clicked() {
 	group_.set_state(id_);
 	play_click();
 }

=== modified file 'src/ui_basic/radiobutton.h'
--- src/ui_basic/radiobutton.h	2019-02-23 11:00:49 +0000
+++ src/ui_basic/radiobutton.h	2019-06-01 10:27:23 +0000
@@ -42,7 +42,7 @@
 	}
 
 private:
-	void clicked() override;
+	void button_clicked() override;
 
 	Radiobutton* nextbtn_;
 	Radiogroup& group_;

=== modified file 'src/ui_basic/unique_window.cc'
--- src/ui_basic/unique_window.cc	2019-02-23 11:00:49 +0000
+++ src/ui_basic/unique_window.cc	2019-06-01 10:27:23 +0000
@@ -58,7 +58,13 @@
  */
 void UniqueWindow::Registry::toggle() {
 	if (window) {
-		window->die();
+		// There is already a window. If it is minimal, restore it.
+		if (window->is_minimal()) {
+			window->restore();
+			opened();
+		} else {
+			window->die();
+		}
 	} else {
 		open_window();
 	}

=== modified file 'src/ui_fsmenu/launch_game.cc'
--- src/ui_fsmenu/launch_game.cc	2019-05-26 17:21:15 +0000
+++ src/ui_fsmenu/launch_game.cc	2019-06-01 10:27:23 +0000
@@ -46,15 +46,15 @@
      buth_(get_h() * 9 / 200),
 
      win_condition_dropdown_(this,
+							 "dropdown_wincondition",
                              get_w() * 7 / 10,
                              get_h() * 4 / 10 + buth_,
                              butw_,
-                             get_h() - get_h() * 4 / 10 - buth_,
+                             10, // max number of items
                              buth_,
                              "",
                              UI::DropdownType::kTextual,
-                             UI::PanelStyle::kFsMenu),
-
+                             UI::PanelStyle::kFsMenu, UI::ButtonStyle::kFsMenuMenu),
      peaceful_(this, Vector2i(get_w() * 7 / 10, get_h() * 19 / 40 + buth_), _("Peaceful mode")),
      ok_(this, "ok", 0, 0, butw_, buth_, UI::ButtonStyle::kFsMenuPrimary, _("Start game")),
      back_(this, "back", 0, 0, butw_, buth_, UI::ButtonStyle::kFsMenuSecondary, _("Back")),

=== modified file 'src/ui_fsmenu/launch_spg.cc'
--- src/ui_fsmenu/launch_spg.cc	2019-05-26 17:21:15 +0000
+++ src/ui_fsmenu/launch_spg.cc	2019-06-01 10:27:23 +0000
@@ -114,6 +114,8 @@
 	ok_.set_pos(Vector2i(get_w() * 7 / 10, get_h() * 9 / 10));
 	back_.set_pos(Vector2i(get_w() * 7 / 10, get_h() * 17 / 20));
 	win_condition_dropdown_.set_pos(Vector2i(get_w() * 7 / 10, get_h() * 4 / 10 + buth_));
+	win_condition_dropdown_.set_size(select_map_.get_w(), win_condition_dropdown_.get_h());
+
 	title_.set_text(_("Launch Game"));
 	select_map_.sigclicked.connect(
 	   boost::bind(&FullscreenMenuLaunchSPG::select_map, boost::ref(*this)));

=== modified file 'src/ui_fsmenu/options.cc'
--- src/ui_fsmenu/options.cc	2019-05-26 17:21:15 +0000
+++ src/ui_fsmenu/options.cc	2019-06-01 10:27:23 +0000
@@ -105,23 +105,25 @@
 
      // Interface options
      language_dropdown_(&box_interface_left_,
-                        0,
-                        0,
-                        100,  // 100 is arbitrary, will be resized in layout().
-                        100,  // 100 is arbitrary, will be resized in layout().
+						"dropdown_language",
+                        0,
+                        0,
+                        100,  // 100 is arbitrary, will be resized in layout().
+                        50,
                         24,
                         _("Language"),
                         UI::DropdownType::kTextual,
-                        UI::PanelStyle::kFsMenu),
+                        UI::PanelStyle::kFsMenu, UI::ButtonStyle::kFsMenuMenu),
      resolution_dropdown_(&box_interface_left_,
-                          0,
-                          0,
-                          100,  // 100 is arbitrary, will be resized in layout().
-                          100,  // 100 is arbitrary, will be resized in layout().
+						  "dropdown_resolution",
+                          0,
+                          0,
+                          100,  // 100 is arbitrary, will be resized in layout().
+                          50,
                           24,
                           _("Window Size"),
                           UI::DropdownType::kTextual,
-                          UI::PanelStyle::kFsMenu),
+                          UI::PanelStyle::kFsMenu, UI::ButtonStyle::kFsMenuMenu),
 
      fullscreen_(&box_interface_left_, Vector2i::zero(), _("Fullscreen"), "", 0),
      inputgrab_(&box_interface_left_, Vector2i::zero(), _("Grab Input"), "", 0),

=== modified file 'src/wui/economy_options_window.cc'
--- src/wui/economy_options_window.cc	2019-05-29 16:59:16 +0000
+++ src/wui/economy_options_window.cc	2019-06-01 10:27:23 +0000
@@ -52,7 +52,7 @@
         &tabpanel_, this, serial_, player_, can_act, Widelands::wwWORKER, kDesiredWidth)),
      dropdown_box_(this, 0, 0, UI::Box::Horizontal),
      dropdown_(
-        &dropdown_box_, 0, 0, 174, 200, 34, "", UI::DropdownType::kTextual, UI::PanelStyle::kWui),
+        &dropdown_box_, "economy_profiles", 0, 0, 174, 10, 34, "", UI::DropdownType::kTextual, UI::PanelStyle::kWui, UI::ButtonStyle::kWuiSecondary), // NOCOM test if this is the correct button style. heap-use-after-free somewhere too.
      time_last_thought_(0),
      save_profile_dialog_(nullptr) {
 	set_center_panel(&main_box_);

=== modified file 'src/wui/game_client_disconnected.cc'
--- src/wui/game_client_disconnected.cc	2019-02-23 11:00:49 +0000
+++ src/wui/game_client_disconnected.cc	2019-06-01 10:27:23 +0000
@@ -74,16 +74,17 @@
                /** TRANSLATORS: Button tooltip */
                _("Replace the disconnected player with the selected AI and continue playing")),
      type_dropdown_(&box_h_,
+					"dropdown_ai",
                     width - 50,  // x
                     0,           // y
                     60,          // width of selection box
-                    800,         // height of selection box, shrinks automatically
+                    16,          // maximum number of items in the selection box, shrinks automatically
                     35,          // width/height of button
                     /** TRANSLATORS: Dropdown tooltip to select the AI difficulty when a player has
                        disconnected from a game */
                     _("AI for the disconnected player"),
                     UI::DropdownType::kPictorial,
-                    UI::PanelStyle::kWui),
+                    UI::PanelStyle::kWui, UI::ButtonStyle::kWuiMenu),
      exit_game_(&box_,
                 "exit_game",
                 0,

=== modified file 'src/wui/game_message_menu.cc'
--- src/wui/game_message_menu.cc	2019-05-26 17:21:15 +0000
+++ src/wui/game_message_menu.cc	2019-06-01 10:27:23 +0000
@@ -130,11 +130,9 @@
 	   new UI::Button(this, "center_main_mapview_on_location", kWindowWidth - kPadding - kButtonSize,
 	                  archivebtn_->get_y(), kButtonSize, kButtonSize, UI::ButtonStyle::kWuiPrimary,
 	                  g_gr->images().get("images/wui/menus/menu_goto.png"),
-	                  /** TRANSLATORS: %s is a tooltip, G is the corresponding hotkey */
-	                  (boost::format(_("G: %s"))
+	                  as_tooltip_text_with_hotkey(
 	                   /** TRANSLATORS: Tooltip in the messages window */
-	                   % _("Center main mapview on location"))
-	                     .str());
+	                   _("Center main mapview on location"), "g"));
 	centerviewbtn_->sigclicked.connect(boost::bind(&GameMessageMenu::center_view, this));
 	centerviewbtn_->set_enabled(false);
 
@@ -524,10 +522,9 @@
 		message_filter_ = msgtype;
 
 		/** TRANSLATORS: %1% is a tooltip, %2% is the corresponding hotkey */
-		button.set_tooltip((boost::format(_("%1% (Hotkey: %2%)"))
+		button.set_tooltip(as_tooltip_text_with_hotkey(
 		                    /** TRANSLATORS: Tooltip in the messages window */
-		                    % _("Show all messages") % pgettext("hotkey", "Alt + 0"))
-		                      .str());
+		                    _("Show all messages"), pgettext("hotkey", "Alt+0")));
 	}
 }
 
@@ -535,27 +532,22 @@
  * Helper for filter_messages
  */
 void GameMessageMenu::set_filter_messages_tooltips() {
-	geologistsbtn_->set_tooltip((boost::format(_("%1% (Hotkey: %2%)"))
+	geologistsbtn_->set_tooltip(as_tooltip_text_with_hotkey(
 	                             /** TRANSLATORS: Tooltip in the messages window */
-	                             % _("Show geologists' messages only") %
-	                             pgettext("hotkey", "Alt + 1"))
-	                               .str());
-	economybtn_->set_tooltip((boost::format(_("%1% (Hotkey: %2%)"))
+	                             _("Show geologists' messages only"),
+	                             pgettext("hotkey", "Alt+1")));
+	economybtn_->set_tooltip(as_tooltip_text_with_hotkey(
 	                          /** TRANSLATORS: Tooltip in the messages window */
-	                          % _("Show economy messages only") % pgettext("hotkey", "Alt + 2"))
-	                            .str());
-	seafaringbtn_->set_tooltip((boost::format(_("%1% (Hotkey: %2%)"))
+	                          _("Show economy messages only"), pgettext("hotkey", "Alt+2")));
+	seafaringbtn_->set_tooltip(as_tooltip_text_with_hotkey(
 	                            /** TRANSLATORS: Tooltip in the messages window */
-	                            % _("Show seafaring messages only") % pgettext("hotkey", "Alt + 3"))
-	                              .str());
-	warfarebtn_->set_tooltip((boost::format(_("%1% (Hotkey: %2%)"))
+	                            _("Show seafaring messages only"), pgettext("hotkey", "Alt+3")));
+	warfarebtn_->set_tooltip(as_tooltip_text_with_hotkey(
 	                          /** TRANSLATORS: Tooltip in the messages window */
-	                          % _("Show warfare messages only") % pgettext("hotkey", "Alt + 4"))
-	                            .str());
-	scenariobtn_->set_tooltip((boost::format(_("%1% (Hotkey: %2%)"))
+	                          _("Show warfare messages only"), pgettext("hotkey", "Alt+4")));
+	scenariobtn_->set_tooltip(as_tooltip_text_with_hotkey(
 	                           /** TRANSLATORS: Tooltip in the messages window */
-	                           % _("Show scenario messages only") % pgettext("hotkey", "Alt + 5"))
-	                             .str());
+	                           _("Show scenario messages only"), pgettext("hotkey", "Alt+5")));
 }
 
 /**
@@ -651,6 +643,6 @@
 		}
 		break;
 	}
-	/** TRANSLATORS: %s is a tooltip, Del is the corresponding hotkey */
-	archivebtn_->set_tooltip((boost::format(_("Del: %s")) % button_tooltip).str());
+	/** TRANSLATORS: Del is the "Delete" key on the keyboard */
+	archivebtn_->set_tooltip(as_tooltip_text_with_hotkey(button_tooltip, pgettext("hotkey", "Del")));
 }

=== modified file 'src/wui/multiplayersetupgroup.cc'
--- src/wui/multiplayersetupgroup.cc	2019-05-12 07:45:59 +0000
+++ src/wui/multiplayersetupgroup.cc	2019-06-01 10:27:23 +0000
@@ -55,7 +55,8 @@
 	                       GameSettingsProvider* const settings)
 	   : UI::Box(parent, 0, 0, UI::Box::Horizontal, w, h, kPadding),
 	     slot_dropdown_(
-	        this, 0, 0, h, 200, h, _("Role"), UI::DropdownType::kPictorial, UI::PanelStyle::kFsMenu),
+	        this, (boost::format("dropdown_slot%d") % static_cast<unsigned int>(id)).str(),
+			 0, 0, h, 16, h, _("Role"), UI::DropdownType::kPictorial, UI::PanelStyle::kFsMenu, UI::ButtonStyle::kFsMenuSecondary),
 	     // Name needs to be initialized after the dropdown, otherwise the layout function will
 	     // crash.
 	     name(this, 0, 0, w - h - UI::Scrollbar::kSize * 11 / 5, h),
@@ -187,34 +188,37 @@
 	            (boost::format(_("Player %u")) % static_cast<unsigned int>(id_ + 1)).str(),
 	            UI::Button::VisualState::kFlat),
 	     type_dropdown_(this,
+						(boost::format("dropdown_type%d") % static_cast<unsigned int>(id)).str(),
 	                    0,
 	                    0,
 	                    50,
-	                    200,
+	                    16,
 	                    h,
 	                    _("Type"),
 	                    UI::DropdownType::kPictorial,
-	                    UI::PanelStyle::kFsMenu),
+	                    UI::PanelStyle::kFsMenu, UI::ButtonStyle::kFsMenuSecondary),
 	     tribes_dropdown_(this,
+						  (boost::format("dropdown_tribes%d") % static_cast<unsigned int>(id)).str(),
 	                      0,
 	                      0,
 	                      50,
-	                      200,
+	                      16,
 	                      h,
 	                      _("Tribe"),
 	                      UI::DropdownType::kPictorial,
-	                      UI::PanelStyle::kFsMenu),
+	                      UI::PanelStyle::kFsMenu, UI::ButtonStyle::kFsMenuSecondary),
 	     init_dropdown_(this,
+						(boost::format("dropdown_init%d") % static_cast<unsigned int>(id)).str(),
 	                    0,
 	                    0,
 	                    w - 4 * h - 3 * kPadding,
-	                    200,
+	                    16,
 	                    h,
 	                    "",
 	                    UI::DropdownType::kTextualNarrow,
-	                    UI::PanelStyle::kFsMenu),
+	                    UI::PanelStyle::kFsMenu, UI::ButtonStyle::kFsMenuSecondary),
 	     team_dropdown_(
-	        this, 0, 0, h, 200, h, _("Team"), UI::DropdownType::kPictorial, UI::PanelStyle::kFsMenu),
+	        this, (boost::format("dropdown_team%d") % static_cast<unsigned int>(id)).str(), 0, 0, h, 16, h, _("Team"), UI::DropdownType::kPictorial, UI::PanelStyle::kFsMenu, UI::ButtonStyle::kFsMenuSecondary),
 	     last_state_(PlayerSettings::State::kClosed),
 	     type_selection_locked_(false),
 	     tribe_selection_locked_(false),

=== modified file 'src/wui/seafaring_statistics_menu.cc'
--- src/wui/seafaring_statistics_menu.cc	2019-02-23 11:00:49 +0000
+++ src/wui/seafaring_statistics_menu.cc	2019-06-01 10:27:23 +0000
@@ -26,6 +26,7 @@
 
 #include "economy/fleet.h"
 #include "graphic/graphic.h"
+#include "graphic/text_layout.h"
 #include "logic/game.h"
 #include "logic/player.h"
 #include "logic/playercommand.h"
@@ -98,10 +99,8 @@
                kButtonSize,
                UI::ButtonStyle::kWuiPrimary,
                g_gr->images().get("images/wui/menus/menu_watch_follow.png"),
-               (boost::format(_("%1% (Hotkey: %2%)")) %
                 /** TRANSLATORS: Tooltip in the seafaring statistics window */
-                _("Watch the selected ship") % pgettext("hotkey", "W"))
-                  .str()),
+                as_tooltip_text_with_hotkey(_("Watch the selected ship"), "w")),
      openwindowbtn_(
         &navigation_box_,
         "seafaring_stats_watch_button",
@@ -112,13 +111,12 @@
         UI::ButtonStyle::kWuiPrimary,
         g_gr->images().get("images/ui_basic/fsel.png"),
         (boost::format("%s<br>%s") %
-         (boost::format(pgettext("hotkey_description", "%1%: %2%")) % pgettext("hotkey", "O") %
-          /** TRANSLATORS: Tooltip in the seafaring statistics window */
-          _("Open the selected ship’s window")) %
-         (boost::format(pgettext("hotkey_description", "%1%: %2%")) %
-          pgettext("hotkey", "CTRL + O") %
-          /** TRANSLATORS: Tooltip in the seafaring statistics window */
-          _("Go to the selected ship and open its window")))
+		  as_tooltip_text_with_hotkey(
+          /** TRANSLATORS: Tooltip in the seafaring statistics window */
+          _("Open the selected ship’s window"), "o") %
+		  as_tooltip_text_with_hotkey(
+          /** TRANSLATORS: Tooltip in the seafaring statistics window */
+          _("Go to the selected ship and open its window"), pgettext("hotkey", "CTRL+o")))
            .str()),
      centerviewbtn_(&navigation_box_,
                     "seafaring_stats_center_main_mapview_button",
@@ -128,10 +126,9 @@
                     kButtonSize,
                     UI::ButtonStyle::kWuiPrimary,
                     g_gr->images().get("images/wui/ship/menu_ship_goto.png"),
-                    (boost::format(_("%1% (Hotkey: %2%)")) %
+                    as_tooltip_text_with_hotkey(
                      /** TRANSLATORS: Tooltip in the seafaring statistics window */
-                     _("Center the map on the selected ship") % pgettext("hotkey", "G"))
-                       .str()),
+                     _("Center the map on the selected ship"), "g")),
      table_(&main_box_, 0, 0, get_inner_w() - 2 * kPadding, 100, UI::PanelStyle::kWui) {
 
 	const Widelands::TribeDescr& tribe = iplayer().player().tribe();
@@ -495,39 +492,32 @@
 		button.set_perm_pressed(true);
 		ship_filter_ = status;
 
-		/** TRANSLATORS: %1% is a tooltip, %2% is the corresponding hotkey */
-		button.set_tooltip((boost::format(_("%1% (Hotkey: %2%)"))
-		                    /** TRANSLATORS: Tooltip in the messages window */
-		                    % _("Show all ships") % pgettext("hotkey", "Alt + 0"))
-		                      .str());
+		button.set_tooltip(as_tooltip_text_with_hotkey(
+		                    /** TRANSLATORS: Tooltip in the ship statistics window */
+		                    _("Show all ships"), pgettext("hotkey", "Alt+0")));
 	}
 }
 
 void SeafaringStatisticsMenu::set_filter_ships_tooltips() {
 
-	idle_btn_.set_tooltip((boost::format(_("%1% (Hotkey: %2%)"))
-	                       /** TRANSLATORS: Tooltip in the messages window */
-	                       % _("Show idle ships") % pgettext("hotkey", "Alt + 1"))
-	                         .str());
-	shipping_btn_.set_tooltip((boost::format(_("%1% (Hotkey: %2%)"))
-	                           /** TRANSLATORS: Tooltip in the messages window */
-	                           % _("Show ships shipping wares and workers") %
-	                           pgettext("hotkey", "Alt + 2"))
-	                             .str());
-	waiting_btn_.set_tooltip((boost::format(_("%1% (Hotkey: %2%)"))
-	                          /** TRANSLATORS: Tooltip in the messages window */
-	                          % _("Show waiting expeditions") % pgettext("hotkey", "Alt + 3"))
-	                            .str());
-	scouting_btn_.set_tooltip((boost::format(_("%1% (Hotkey: %2%)"))
-	                           /** TRANSLATORS: Tooltip in the messages window */
-	                           % _("Show scouting expeditions") % pgettext("hotkey", "Alt + 4"))
-	                             .str());
+	idle_btn_.set_tooltip(as_tooltip_text_with_hotkey(
+	                       /** TRANSLATORS: Tooltip in the ship statistics window */
+	                       _("Show idle ships"), pgettext("hotkey", "Alt+1")));
+	shipping_btn_.set_tooltip(as_tooltip_text_with_hotkey(
+	                           /** TRANSLATORS: Tooltip in the ship statistics window */
+	                           _("Show ships shipping wares and workers"),
+	                           pgettext("hotkey", "Alt+2")));
+	waiting_btn_.set_tooltip(as_tooltip_text_with_hotkey(
+	                          /** TRANSLATORS: Tooltip in the ship statistics window */
+	                          _("Show waiting expeditions"), pgettext("hotkey", "Alt+3")));
+	scouting_btn_.set_tooltip(as_tooltip_text_with_hotkey(
+	                           /** TRANSLATORS: Tooltip in the ship statistics window */
+	                           _("Show scouting expeditions"), pgettext("hotkey", "Alt+4")));
 	portspace_btn_.set_tooltip(
-	   (boost::format(_("%1% (Hotkey: %2%)"))
-	    /** TRANSLATORS: Tooltip in the messages window */
-	    % _("Show expeditions that have found a port space or are founding a colony") %
-	    pgettext("hotkey", "Alt + 5"))
-	      .str());
+	   as_tooltip_text_with_hotkey(
+	    /** TRANSLATORS: Tooltip in the ship statistics window */
+	    _("Show expeditions that have found a port space or are founding a colony"),
+	    pgettext("hotkey", "Alt+5")));
 }
 
 bool SeafaringStatisticsMenu::satisfies_filter(const ShipInfo& info, ShipFilterStatus filter) {


Follow ups