← Back to team overview

widelands-dev team mailing list archive

[Merge] lp:~widelands-dev/widelands/table_multiselect into lp:widelands

 

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

Commit message:
Let the user select multiple entries in a table using Ctrl/Shift + Click and Ctrl+A. The new table mode is used in the messages window and in game/replay deleting.

Requested reviews:
  Widelands Developers (widelands-dev)

For more details, see:
https://code.launchpad.net/~widelands-dev/widelands/table_multiselect/+merge/312747
-- 
Your team Widelands Developers is requested to review the proposed merge of lp:~widelands-dev/widelands/table_multiselect into lp:widelands.
=== modified file 'data/campaigns/tutorial01_basic_control.wmf/scripting/texts.lua'
--- data/campaigns/tutorial01_basic_control.wmf/scripting/texts.lua	2016-09-20 17:01:35 +0000
+++ data/campaigns/tutorial01_basic_control.wmf/scripting/texts.lua	2016-12-07 21:22:33 +0000
@@ -405,6 +405,7 @@
    rt(
       p(_[[Once you have archived a message, another message will be selected automatically from the list.]]) ..
       paragraphdivider() ..
+      listitem_arrow(_[[You can also hold down the Ctrl or Shift key to select multiple messages, or press Ctrl + A to select them all.]]) ..
       listitem_bullet(_[[Archive all messages that you currently have in your inbox, including this one.]])
    ),
    obj_name = "archive_all_messages",
@@ -414,7 +415,8 @@
       p(_[[The message window is central to fully controlling your tribe’s fortune. However, you will get a lot of messages in a real game. To keep your head straight, you should try to keep the inbox empty.]]) ..
       paragraphdivider() ..
       listitem_bullet(_[[Archive all your messages in your inbox now.]]) ..
-      listitem_arrow(_[[To do so, open the message window by pressing ‘n’ or clicking the second button from the right at the very bottom of the screen. The newest message will be marked for you automatically. Keep clicking the ‘Archive selected message’ button until all messages have been archived and the list is empty.]])
+      listitem_arrow(_[[To do so, open the message window by pressing ‘n’ or clicking the second button from the right at the very bottom of the screen. The newest message will be marked for you automatically. Keep clicking the ‘Archive selected message’ button until all messages have been archived and the list is empty.]]) ..
+      listitem_arrow(_[[You can also hold down the Ctrl or Shift key to select multiple messages, or press Ctrl + A to select them all.]])
    )
 }
 

=== modified file 'data/tribes/scripting/help/controls.lua'
--- data/tribes/scripting/help/controls.lua	2016-10-25 19:18:22 +0000
+++ data/tribes/scripting/help/controls.lua	2016-12-07 21:22:33 +0000
@@ -76,6 +76,17 @@
                dl(help_format_hotkey(pgettext("hotkey", "F6")), _"Show the debug console (only in debug-builds)")
          ) ..
 
+         h2(_"Table Control") ..
+         h3(_"In tables that allow the selection of multiple entries, the following key combinations are available:") ..
+         p(
+               -- TRANSLATORS: This is an access key combination. Localize, but do not change the key.
+               dl(help_format_hotkey(pgettext("hotkey", "Ctrl + Click")), pgettext("table_control", "Select multiple entries")) ..
+               -- TRANSLATORS: This is an access key combination. Localize, but do not change the key.
+               dl(help_format_hotkey(pgettext("hotkey", "Shift + Click")), pgettext("table_control", "Select a range of entries")) ..
+               -- TRANSLATORS: This is an access key combination. Localize, but do not change the key.
+               dl(help_format_hotkey(pgettext("hotkey", "Ctrl + A")), pgettext("table_control", "Select all entries"))) ..
+
+         h2(_"Message Window") ..
          h3(_"In the message window, the following additional shortcuts are available:") ..
          p(
                -- TRANSLATORS: This is the helptext for an access key combination.
@@ -93,7 +104,7 @@
                -- TRANSLATORS: This is the helptext for an access key combination.
                dl(help_format_hotkey("G"), _"Jump to the location corresponding to the current message") ..
                -- TRANSLATORS: This is an access key combination. Localize, but do not change the key.
-               dl(help_format_hotkey(pgettext("hotkey", "Delete")), _"Archive/Restore the current message")
+               dl(help_format_hotkey(pgettext("hotkey", "Del")), _"Archive/Restore the current message")
           )
       )
 }

=== modified file 'src/editor/ui_menus/main_menu_load_or_save_map.cc'
--- src/editor/ui_menus/main_menu_load_or_save_map.cc	2016-11-03 07:20:57 +0000
+++ src/editor/ui_menus/main_menu_load_or_save_map.cc	2016-12-07 21:22:33 +0000
@@ -49,7 +49,7 @@
      right_column_x_(tablew_ + 2 * padding_),
      butw_((get_inner_w() - right_column_x_ - 2 * padding_) / 2),
 
-     table_(this, tablex_, tabley_, tablew_, tableh_, false),
+     table_(this, tablex_, tabley_, tablew_, tableh_),
      map_details_(this,
                   right_column_x_,
                   tabley_,

=== modified file 'src/ui_basic/table.cc'
--- src/ui_basic/table.cc	2016-11-15 07:13:34 +0000
+++ src/ui_basic/table.cc	2016-12-07 21:22:33 +0000
@@ -44,13 +44,8 @@
  *       w       dimensions, in pixels, of the Table
  *       h
 */
-Table<void*>::Table(Panel* const parent,
-                    int32_t x,
-                    int32_t y,
-                    uint32_t w,
-                    uint32_t h,
-                    const Image* button_background,
-                    const bool descending)
+Table<void*>::Table(
+	Panel* const parent, int32_t x, int32_t y, uint32_t w, uint32_t h, const Image* button_background, TableRows rowtype)
    : Panel(parent, x, y, w, h),
      total_width_(0),
      headerheight_(
@@ -67,8 +62,12 @@
      last_click_time_(-10000),
      last_selection_(no_selection_index()),
      sort_column_(0),
-     sort_descending_(descending),
-     flexible_column_(std::numeric_limits<size_t>::max()) {
+     sort_descending_(rowtype == TableRows::kSingleDescending ||
+                      rowtype == TableRows::kMultiDescending),
+	  flexible_column_(std::numeric_limits<size_t>::max()),
+     is_multiselect_(rowtype == TableRows::kMulti || rowtype == TableRows::kMultiDescending),
+     ctrl_down_(false),
+     shift_down_(false) {
 	set_thinks(false);
 	set_can_focus(true);
 	scrollbar_filler_button_->set_visible(false);
@@ -91,6 +90,7 @@
 	for (Column& column : columns_) {
 		delete column.btn;
 	}
+	multiselect_.clear();
 }
 
 /// Add a new column to this table.
@@ -98,8 +98,7 @@
                               const std::string& title,
                               const std::string& tooltip_string,
                               Align const alignment,
-                              TableColumnType column_type,
-                              bool const is_checkbox_column) {
+                              TableColumnType column_type) {
 	//  If there would be existing entries, they would not get the new column.
 	assert(size() == 0);
 
@@ -121,16 +120,7 @@
 		   boost::bind(&Table::header_button_clicked, boost::ref(*this), columns_.size()));
 		c.width = width;
 		c.alignment = alignment;
-		c.is_checkbox_column = is_checkbox_column;
-
-		if (is_checkbox_column) {
-			c.compare =
-			   boost::bind(&Table<void*>::default_compare_checkbox, this, columns_.size(), _1, _2);
-		} else {
-			c.compare =
-			   boost::bind(&Table<void*>::default_compare_string, this, columns_.size(), _1, _2);
-		}
-
+		c.compare = boost::bind(&Table<void*>::default_compare_string, this, columns_.size(), _1, _2);
 		columns_.push_back(c);
 		if (column_type == TableColumnType::kFlexible) {
 			assert(flexible_column_ == std::numeric_limits<size_t>::max());
@@ -156,24 +146,6 @@
 	column.compare = fn;
 }
 
-void Table<void*>::EntryRecord::set_checked(uint8_t const col, bool const checked) {
-	Data& cell = data_.at(col);
-
-	cell.d_checked = checked;
-	cell.d_picture = g_gr->images().get(checked ? "images/ui_basic/checkbox_checked.png" :
-	                                              "images/ui_basic/checkbox_empty.png");
-}
-
-void Table<void*>::EntryRecord::toggle(uint8_t const col) {
-	set_checked(col, !is_checked(col));
-}
-
-bool Table<void*>::EntryRecord::is_checked(uint8_t const col) const {
-	const Data& cell = data_.at(col);
-
-	return cell.d_checked;
-}
-
 Table<void*>::EntryRecord* Table<void*>::find(const void* const entry) const
 
 {
@@ -213,8 +185,13 @@
 	if (scrollbar_)
 		scrollbar_->set_steps(1);
 	scrollpos_ = 0;
+	last_click_time_ = -10000;
+	clear_selections();
+}
+
+void Table<void*>::clear_selections() {
+	multiselect_.clear();
 	selection_ = no_selection_index();
-	last_click_time_ = -10000;
 	last_selection_ = no_selection_index();
 }
 
@@ -250,7 +227,7 @@
 
 		const EntryRecord& er = *entry_records_[idx];
 
-		if (idx == selection_) {
+		if (idx == selection_ || multiselect_.count(idx)) {
 			assert(2 <= get_eff_w());
 			dst.brighten_rect(Rectf(1.f, y, get_eff_w() - 2, lineheight_), -ms_darken_value);
 		}
@@ -364,8 +341,33 @@
  * handle key presses
  */
 bool Table<void*>::handle_key(bool down, SDL_Keysym code) {
+	if (is_multiselect_) {
+		switch (code.sym) {
+		case SDLK_LSHIFT:
+		case SDLK_RSHIFT:
+			shift_down_ = down;
+			break;
+		case SDLK_LCTRL:
+		case SDLK_RCTRL:
+			ctrl_down_ = down;
+			break;
+		default:
+			break;
+		}
+	}
 	if (down) {
 		switch (code.sym) {
+		case SDLK_a:
+			if (is_multiselect_ && ctrl_down_ && !empty()) {
+				multiselect_.clear();
+				for (uint32_t i = 0; i < size(); ++i) {
+					toggle_entry(i);
+				}
+				selection_ = 0;
+				selected(0);
+				return true;
+			}
+			break;
 		case SDLK_UP:
 		case SDLK_KP_8:
 			move_selection(-1);
@@ -391,7 +393,7 @@
 /**
  * Handle mouse presses: select the appropriate entry
  */
-bool Table<void*>::handle_mousepress(uint8_t const btn, int32_t x, int32_t const y) {
+bool Table<void*>::handle_mousepress(uint8_t const btn, int32_t, int32_t const y) {
 	if (get_can_focus())
 		focus();
 
@@ -408,26 +410,39 @@
 
 		uint32_t const row = (y + scrollpos_ - headerheight_) / get_lineheight();
 		if (row < entry_records_.size()) {
-			select(row);
-			Columns::size_type const nr_cols = columns_.size();
-			for (uint8_t col = 0; col < nr_cols; ++col) {
-				const Column& column = columns_.at(col);
-				x -= column.width;
-				if (x <= 0) {
-					if (column.is_checkbox_column) {
-						play_click();
-						entry_records_.at(row)->toggle(col);
-					}
-					break;
+			play_click();
+			if (is_multiselect_) {
+				// Ranged selection with Shift
+				if (shift_down_) {
+					multiselect_.clear();
+					if (has_selection()) {
+						const uint32_t last_selected = selection_index();
+						const uint32_t lower_bound = std::min(row, selection_);
+						const uint32_t upper_bound = std::max(row, selection_);
+						for (uint32_t i = lower_bound; i <= upper_bound; ++i) {
+							toggle_entry(i);
+						}
+						select(last_selected);
+					} else {
+						select(toggle_entry(row));
+					}
+				} else {
+					// Single selection without Ctrl
+					if (!ctrl_down_) {
+						multiselect_.clear();
+					}
+					select(toggle_entry(row));
 				}
+			} else {
+				select(row);
 			}
 		}
 
-		if  //  check if doubleclicked
-		   (time - real_last_click_time < DOUBLE_CLICK_INTERVAL && last_selection_ == selection_ &&
-		    selection_ != no_selection_index())
+		// Check if doubleclicked
+		if (!ctrl_down_ && !shift_down_ && time - real_last_click_time < DOUBLE_CLICK_INTERVAL &&
+		    last_selection_ == selection_ && selection_ != no_selection_index()) {
 			double_clicked(selection_);
-
+		}
 		return true;
 	}
 	default:
@@ -481,26 +496,45 @@
  * Args: i  the entry to select
  */
 void Table<void*>::select(const uint32_t i) {
-	if (empty() || selection_ == i)
+	if (empty() || selection_ == i || i == no_selection_index())
 		return;
 
 	selection_ = i;
+	if (is_multiselect_) {
+		multiselect_.insert(selection_);
+	}
 
 	selected(selection_);
 }
 
 /**
+ * Adds/removes the row from multiselect.
+ * Returns the row that should be selected afterwards, or no_selection_index() if
+ * the multiselect is empty.
+ */
+uint32_t Table<void*>::toggle_entry(uint32_t row) {
+	assert(is_multiselect_);
+	if (multiselect_.count(row)) {
+		multiselect_.erase(row);
+		// Find last selection
+		if (multiselect_.empty()) {
+			return no_selection_index();
+		} else {
+			return *multiselect_.lower_bound(0);
+		}
+	} else {
+		multiselect_.insert(row);
+		return row;
+	}
+}
+
+/**
  * Add a new entry to the table.
 */
 Table<void*>::EntryRecord& Table<void*>::add(void* const entry, const bool do_select) {
 	EntryRecord& result = *new EntryRecord(entry);
 	entry_records_.push_back(&result);
 	result.data_.resize(columns_.size());
-	for (size_t i = 0; i < columns_.size(); ++i) {
-		if (columns_.at(i).is_checkbox_column) {
-			result.data_.at(i).d_picture = g_gr->images().get("images/ui_basic/checkbox_empty.png");
-		}
-	}
 
 	if (do_select) {
 		select(entry_records_.size() - 1);
@@ -522,6 +556,7 @@
  */
 void Table<void*>::remove(const uint32_t i) {
 	assert(i < entry_records_.size());
+	multiselect_.clear();
 
 	const EntryRecordVector::iterator it = entry_records_.begin() + i;
 	delete *it;
@@ -531,14 +566,18 @@
 	} else if (selection_ > i && selection_ != no_selection_index()) {
 		selection_--;
 	}
+	if (is_multiselect_ && selection_ != no_selection_index()) {
+		multiselect_.insert(selection_);
+	}
 	layout();
 }
 
 bool Table<void*>::sort_helper(uint32_t a, uint32_t b) {
-	if (sort_descending_)
+	if (sort_descending_) {
 		return columns_[sort_column_].compare(b, a);
-	else
+	} else {
 		return columns_[sort_column_].compare(a, b);
+	}
 }
 
 void Table<void*>::layout() {
@@ -633,16 +672,10 @@
 			newselection = i;
 	}
 	selection_ = newselection;
-}
-
-/**
- * Default comparison for checkbox columns:
- * checked items come before unchecked ones.
- */
-bool Table<void*>::default_compare_checkbox(uint32_t column, uint32_t a, uint32_t b) {
-	EntryRecord& ea = get_record(a);
-	EntryRecord& eb = get_record(b);
-	return ea.is_checked(column) && !eb.is_checked(column);
+	multiselect_.clear();
+	if (is_multiselect_ && selection_ != no_selection_index()) {
+		multiselect_.insert(selection_);
+	}
 }
 
 bool Table<void*>::default_compare_string(uint32_t column, uint32_t a, uint32_t b) {
@@ -651,7 +684,7 @@
 	return ea.get_string(column) < eb.get_string(column);
 }
 
-Table<void*>::EntryRecord::EntryRecord(void* const e) : entry_(e), use_clr(false) {
+Table<void*>::EntryRecord::EntryRecord(void* const e) : entry_(e) {
 }
 
 void Table<void*>::EntryRecord::set_picture(uint8_t const col,

=== modified file 'src/ui_basic/table.h'
--- src/ui_basic/table.h	2016-11-15 06:23:03 +0000
+++ src/ui_basic/table.h	2016-12-07 21:22:33 +0000
@@ -21,6 +21,7 @@
 #define WL_UI_BASIC_TABLE_H
 
 #include <limits>
+#include <set>
 #include <vector>
 
 #include <boost/function.hpp>
@@ -35,6 +36,7 @@
 struct Scrollbar;
 struct Button;
 
+enum class TableRows { kSingle, kMulti, kSingleDescending, kMultiDescending };
 enum class TableColumnType { kFixed, kFlexible };
 
 /** A table with columns and lines.
@@ -57,7 +59,7 @@
 	      uint32_t w,
 	      uint32_t h,
 	      const Image* button_background = g_gr->images().get("images/ui_basic/but3.png"),
-	      bool descending = false);
+	      TableRows rowtype = TableRows::kSingle);
 	~Table();
 
 	boost::signals2::signal<void(uint32_t)> selected;
@@ -68,8 +70,7 @@
 	                const std::string& title = std::string(),
 	                const std::string& tooltip = std::string(),
 	                Align = UI::Align::kLeft,
-	                TableColumnType column_type = TableColumnType::kFixed,
-	                bool is_checkbox_column = false);
+	                TableColumnType column_type = TableColumnType::kFixed);
 
 	void set_column_title(uint8_t col, const std::string& title);
 
@@ -89,11 +90,14 @@
 	static uint32_t no_selection_index();
 	bool has_selection() const;
 	uint32_t selection_index() const;
+	std::set<uint32_t> selections() const;
+	void clear_selections();
 	EntryRecord& get_record(uint32_t) const;
 	static Entry get(const EntryRecord&);
 	EntryRecord* find(Entry) const;
 
 	void select(uint32_t);
+	uint32_t toggle_entry(uint32_t row);
 	void move_selection(int32_t offset);
 	struct NoSelection : public std::exception {
 		char const* what() const noexcept override {
@@ -140,10 +144,6 @@
 			return clr;
 		}
 
-		void set_checked(uint8_t col, bool checked);
-		void toggle(uint8_t col);
-		bool is_checked(uint8_t col) const;
-
 	private:
 		friend class Table<void*>;
 		void* entry_;
@@ -152,10 +152,6 @@
 		struct Data {
 			const Image* d_picture;
 			std::string d_string;
-			bool d_checked;
-
-			Data() : d_checked(false) {
-			}
 		};
 		std::vector<Data> data_;
 	};
@@ -173,7 +169,7 @@
 	      uint32_t w,
 	      uint32_t h,
 	      const Image* button_background = g_gr->images().get("images/ui_basic/but3.png"),
-	      bool descending = false);
+	      TableRows rowtype = TableRows::kSingle);
 	~Table();
 
 	boost::signals2::signal<void(uint32_t)> selected;
@@ -183,8 +179,7 @@
 	                const std::string& title = std::string(),
 	                const std::string& tooltip = std::string(),
 	                Align = UI::Align::kLeft,
-	                TableColumnType column_type = TableColumnType::kFixed,
-	                bool is_checkbox_column = false);
+	                TableColumnType column_type = TableColumnType::kFixed);
 
 	void set_column_title(uint8_t col, const std::string& title);
 	void set_column_compare(uint8_t col, const CompareFn& fn);
@@ -225,6 +220,12 @@
 	bool has_selection() const {
 		return selection_ != no_selection_index();
 	}
+	/// The set of highlighted entries in multiselect mode
+	std::set<uint32_t> selections() const {
+		return multiselect_;
+	}
+	void clear_selections();
+
 	uint32_t selection_index() const {
 		return selection_;
 	}
@@ -238,6 +239,7 @@
 	EntryRecord* find(const void* entry) const;
 
 	void select(uint32_t);
+	uint32_t toggle_entry(uint32_t row);
 	void move_selection(int32_t offset);
 	struct NoSelection : public std::exception {
 		char const* what() const noexcept override {
@@ -276,7 +278,6 @@
 	bool handle_key(bool down, SDL_Keysym code) override;
 
 private:
-	bool default_compare_checkbox(uint32_t column, uint32_t a, uint32_t b);
 	bool default_compare_string(uint32_t column, uint32_t a, uint32_t b);
 	bool sort_helper(uint32_t a, uint32_t b);
 	void layout() override;
@@ -285,7 +286,6 @@
 		Button* btn;
 		uint32_t width;
 		Align alignment;
-		bool is_checkbox_column;
 		CompareFn compare;
 	};
 	using Columns = std::vector<Column>;
@@ -302,12 +302,16 @@
 	UI::Button* scrollbar_filler_button_;
 	int32_t scrollpos_;  //  in pixels
 	uint32_t selection_;
+	std::set<uint32_t> multiselect_;
 	uint32_t last_click_time_;
 	uint32_t last_selection_;  // for double clicks
 	Columns::size_type sort_column_;
 	bool sort_descending_;
 	// This column will grow/shrink depending on the scrollbar being present
 	size_t flexible_column_;
+	bool is_multiselect_;
+	bool ctrl_down_;   // Whether the ctrl key is being pressed
+	bool shift_down_;  // Whether the shift key is being pressed
 
 	void header_button_clicked(Columns::size_type);
 	using EntryRecordVector = std::vector<EntryRecord*>;
@@ -324,8 +328,8 @@
 	      uint32_t w,
 	      uint32_t h,
 	      const Image* button_background = g_gr->images().get("images/ui_basic/but3.png"),
-	      const bool descending = false)
-	   : Base(parent, x, y, w, h, button_background, descending) {
+	      TableRows rowtype = TableRows::kSingle)
+	   : Base(parent, x, y, w, h, button_background, rowtype) {
 	}
 
 	EntryRecord& add(Entry const* const entry = 0, bool const select_this = false) {
@@ -354,8 +358,8 @@
 	      uint32_t w,
 	      uint32_t h,
 	      const Image* button_background = g_gr->images().get("images/ui_basic/but3.png"),
-	      const bool descending = false)
-	   : Base(parent, x, y, w, h, button_background, descending) {
+	      TableRows rowtype = TableRows::kSingle)
+	   : Base(parent, x, y, w, h, button_background, rowtype) {
 	}
 
 	EntryRecord& add(Entry* const entry = 0, bool const select_this = false) {
@@ -384,8 +388,8 @@
 	      uint32_t w,
 	      uint32_t h,
 	      const Image* button_background = g_gr->images().get("images/ui_basic/but3.png"),
-	      const bool descending = false)
-	   : Base(parent, x, y, w, h, button_background, descending) {
+	      TableRows rowtype = TableRows::kSingle)
+	   : Base(parent, x, y, w, h, button_background, rowtype) {
 	}
 
 	EntryRecord& add(const Entry& entry, bool const select_this = false) {
@@ -418,8 +422,8 @@
 	      uint32_t w,
 	      uint32_t h,
 	      const Image* button_background = g_gr->images().get("images/ui_basic/but3.png"),
-	      const bool descending = false)
-	   : Base(parent, x, y, w, h, button_background, descending) {
+	      TableRows rowtype = TableRows::kSingle)
+	   : Base(parent, x, y, w, h, button_background, rowtype) {
 	}
 
 	EntryRecord& add(Entry& entry, bool const select_this = false) {
@@ -454,8 +458,8 @@
 	      uint32_t w,
 	      uint32_t h,
 	      const Image* button_background = g_gr->images().get("images/ui_basic/but3.png"),
-	      const bool descending = false)
-	   : Base(parent, x, y, w, h, button_background, descending) {
+	      TableRows rowtype = TableRows::kSingle)
+	   : Base(parent, x, y, w, h, button_background, rowtype) {
 	}
 
 	EntryRecord& add(uintptr_t const entry, bool const select_this = false) {
@@ -486,8 +490,8 @@
 	      uint32_t w,
 	      uint32_t h,
 	      const Image* button_background = g_gr->images().get("images/ui_basic/but3.png"),
-	      const bool descending = false)
-	   : Base(parent, x, y, w, h, button_background, descending) {
+	      TableRows rowtype = TableRows::kSingle)
+	   : Base(parent, x, y, w, h, button_background, rowtype) {
 	}
 };
 }

=== modified file 'src/ui_fsmenu/loadgame.cc'
--- src/ui_fsmenu/loadgame.cc	2016-12-06 09:22:28 +0000
+++ src/ui_fsmenu/loadgame.cc	2016-12-07 21:22:33 +0000
@@ -91,7 +91,7 @@
             tablew_,
             tableh_,
             g_gr->images().get("images/ui_basic/but3.png"),
-            true),
+            UI::TableRows::kMultiDescending),
 
      is_replay_(is_replay),
      // Main title
@@ -151,11 +151,11 @@
              g_gr->images().get("images/ui_basic/but0.png"),
              _("Delete")),
 
-     ta_errormessage_(this,
-                      right_column_x_,
-                      get_y_from_preceding(ta_mapname_) + 2 * padding_,
-                      get_right_column_w(right_column_x_),
-                      delete_.get_y() - get_y_from_preceding(ta_mapname_) - 6 * padding_),
+     ta_long_generic_message_(this,
+                              right_column_x_,
+                              get_y_from_preceding(ta_mapname_) + 2 * padding_,
+                              get_right_column_w(right_column_x_),
+                              delete_.get_y() - get_y_from_preceding(ta_mapname_) - 6 * padding_),
 
      minimap_y_(get_y_from_preceding(ta_win_condition_) + 3 * padding_),
      minimap_w_(get_right_column_w(right_column_x_)),
@@ -271,52 +271,104 @@
 	if (!table_.has_selection()) {
 		return;
 	}
-	const SavegameData& gamedata = games_data_[table_.get_selected()];
-
-	std::string message =
-	   (boost::format("%s %s\n") % label_mapname_.get_text() % gamedata.mapname).str();
-
-	message = (boost::format("%s %s %s\n") % message % label_win_condition_.get_text() %
-	           gamedata.wincondition)
-	             .str();
-
-	message =
-	   (boost::format("%s %s %s\n") % message % _("Save Date:") % gamedata.savedatestring).str();
-
-	message = (boost::format("%s %s %s\n") % message % label_gametime_.get_text() %
-	           gametimestring(gamedata.gametime))
-	             .str();
-
-	message =
-	   (boost::format("%s %s %s\n\n") % message % label_players_.get_text() % gamedata.nrplayers)
-	      .str();
-
-	message = (boost::format("%s %s %s\n") % message % _("Filename:") % gamedata.filename).str();
-
-	if (is_replay_) {
-		message =
-		   (boost::format("%s\n\n%s") % _("Do you really want to delete this replay?") % message)
+	std::set<uint32_t> selections = table_.selections();
+	size_t no_selections = selections.size();
+	std::string message;
+	if (no_selections > 1) {
+		if (is_replay_) {
+			message = (boost::format(ngettext("Do you really want to delete this %d replay?",
+			                                  "Do you really want to delete these %d replays?",
+			                                  no_selections)) %
+			           no_selections)
+			             .str();
+		} else {
+			message = (boost::format(ngettext("Do you really want to delete this %d game?",
+			                                  "Do you really want to delete these %d games?",
+			                                  no_selections)) %
+			           no_selections)
+			             .str();
+		}
+		message = (boost::format("%s\n%s") % message % filename_list_string()).str();
+
+	} else {
+		const SavegameData& gamedata = games_data_[table_.get_selected()];
+
+		message = (boost::format("%s %s\n") % label_mapname_.get_text() % gamedata.mapname).str();
+
+		message = (boost::format("%s %s %s\n") % message % label_win_condition_.get_text() %
+		           gamedata.wincondition)
+		             .str();
+
+		message =
+		   (boost::format("%s %s %s\n") % message % _("Save Date:") % gamedata.savedatestring).str();
+
+		message = (boost::format("%s %s %s\n") % message % label_gametime_.get_text() %
+		           gametimestring(gamedata.gametime))
+		             .str();
+
+		message =
+		   (boost::format("%s %s %s\n\n") % message % label_players_.get_text() % gamedata.nrplayers)
 		      .str();
-	} else {
-		message =
-		   (boost::format("%s\n\n%s") % _("Do you really want to delete this game?") % message).str();
+
+		message = (boost::format("%s %s %s\n") % message % _("Filename:") % gamedata.filename).str();
+
+		if (is_replay_) {
+			message =
+			   (boost::format("%s\n\n%s") % _("Do you really want to delete this replay?") % message)
+			      .str();
+		} else {
+			message =
+			   (boost::format("%s\n\n%s") % _("Do you really want to delete this game?") % message)
+			      .str();
+		}
 	}
 
 	UI::WLMessageBox confirmationBox(
-	   this, _("Confirm deleting file"), message, UI::WLMessageBox::MBoxType::kOkCancel);
+	   this, ngettext("Confirm deleting file", "Confirm deleting files", no_selections), message,
+	   UI::WLMessageBox::MBoxType::kOkCancel);
+
 	if (confirmationBox.run<UI::Panel::Returncodes>() == UI::Panel::Returncodes::kOk) {
-		g_fs->fs_unlink(gamedata.filename);
-		if (is_replay_) {
-			g_fs->fs_unlink(gamedata.filename + WLGF_SUFFIX);
+		for (const uint32_t index : selections) {
+			const std::string& filename = games_data_[table_.get(table_.get_record(index))].filename;
+			g_fs->fs_unlink(filename);
+			if (is_replay_) {
+				g_fs->fs_unlink(filename + WLGF_SUFFIX);
+			}
 		}
 		fill_table();
 	}
 }
 
+std::string FullscreenMenuLoadGame::filename_list_string() {
+	std::set<uint32_t> selections = table_.selections();
+	boost::format message;
+	int counter = 0;
+	for (const uint32_t index : selections) {
+		++counter;
+		// TODO(GunChleoc): We can exceed the texture size for the font renderer,
+		// so we have to restrict this for now.
+		if (counter > 50) {
+			message = boost::format("%s\n%s") % message % "...";
+			break;
+		}
+		const SavegameData& gamedata = games_data_[table_.get(table_.get_record(index))];
+
+		if (gamedata.errormessage.empty()) {
+			message =
+			   boost::format("%s\n%s") % message %
+			   /** TRANSLATORS %1% = map name, %2% = save date. */
+			   (boost::format(_("%1%, saved on %2%")) % gamedata.mapname % gamedata.savedatestring);
+		} else {
+			message = boost::format("%s\n%s") % message % gamedata.filename;
+		}
+	}
+	return message.str();
+}
+
 bool FullscreenMenuLoadGame::set_has_selection() {
-	bool has_selection = table_.has_selection();
+	bool has_selection = table_.selections().size() < 2;
 	ok_.set_enabled(has_selection);
-	delete_.set_enabled(has_selection);
+	delete_.set_enabled(table_.has_selection());
 
 	if (!has_selection) {
 		label_mapname_.set_text(std::string());
@@ -344,13 +396,14 @@
 }
 
 void FullscreenMenuLoadGame::entry_selected() {
+	size_t selections = table_.selections().size();
 	if (set_has_selection()) {
 
 		const SavegameData& gamedata = games_data_[table_.get_selected()];
-		ta_errormessage_.set_text(gamedata.errormessage);
+		ta_long_generic_message_.set_text(gamedata.errormessage);
 
 		if (gamedata.errormessage.empty()) {
-			ta_errormessage_.set_visible(false);
+			ta_long_generic_message_.set_visible(false);
 			ta_mapname_.set_text(gamedata.mapname);
 			ta_gametime_.set_text(gametimestring(gamedata.gametime));
 
@@ -439,9 +492,16 @@
 			minimap_icon_.set_no_frame();
 			minimap_image_.reset();
 
-			ta_errormessage_.set_visible(true);
+			ta_long_generic_message_.set_visible(true);
 			ok_.set_enabled(false);
 		}
+	} else if (selections > 1) {
+		label_mapname_.set_text(
+		   (boost::format(ngettext("Selected %d file:", "Selected %d files:", selections)) %
+		    selections)
+		      .str());
+		ta_long_generic_message_.set_visible(true);
+		ta_long_generic_message_.set_text(filename_list_string());
 	}
 }
 

=== modified file 'src/ui_fsmenu/loadgame.h'
--- src/ui_fsmenu/loadgame.h	2016-10-12 05:07:59 +0000
+++ src/ui_fsmenu/loadgame.h	2016-12-07 21:22:33 +0000
@@ -97,6 +97,7 @@
 	bool set_has_selection();
 	bool compare_date_descending(uint32_t, uint32_t);
 	void clicked_delete();
+	std::string filename_list_string();
 
 	UI::Table<uintptr_t const> table_;
 
@@ -116,7 +117,7 @@
 
 	UI::Button delete_;
 
-	UI::MultilineTextarea ta_errormessage_;
+	UI::MultilineTextarea ta_long_generic_message_;
 
 	int32_t const minimap_y_, minimap_w_, minimap_h_;
 	UI::Icon minimap_icon_;

=== modified file 'src/ui_fsmenu/mapselect.cc'
--- src/ui_fsmenu/mapselect.cc	2016-12-06 09:22:28 +0000
+++ src/ui_fsmenu/mapselect.cc	2016-12-07 21:22:33 +0000
@@ -48,7 +48,7 @@
      // Main title
      title_(this, 0, 0, _("Choose a map"), UI::Align::kHCenter),
      checkboxes_(this, 0, 0, UI::Box::Vertical, 0, 0, 2 * padding_),
-     table_(this, tablex_, tabley_, tablew_, tableh_, false),
+	  table_(this, tablex_, tabley_, tablew_, tableh_),
      map_details_(this,
                   right_column_x_,
                   tabley_,

=== modified file 'src/wlapplication.cc'
--- src/wlapplication.cc	2016-12-06 09:22:28 +0000
+++ src/wlapplication.cc	2016-12-07 21:22:33 +0000
@@ -1396,7 +1396,7 @@
 		if (is_autogenerated_and_expired(filename)) {
 			log("Deleting replay %s\n", filename.c_str());
 			g_fs->fs_unlink(filename);
-			g_fs->fs_unlink(filename + ".wgf");
+			g_fs->fs_unlink(filename + WLGF_SUFFIX);
 		}
 	}
 }

=== modified file 'src/wui/game_message_menu.cc'
--- src/wui/game_message_menu.cc	2016-12-03 13:32:28 +0000
+++ src/wui/game_message_menu.cc	2016-12-07 21:22:33 +0000
@@ -62,7 +62,7 @@
 
 	list = new UI::Table<uintptr_t>(this, kPadding, kButtonSize + 2 * kPadding,
 	                                kWindowWidth - 2 * kPadding, kTableHeight,
-	                                g_gr->images().get("images/ui_basic/but1.png"));
+											  g_gr->images().get("images/ui_basic/but1.png"), UI::TableRows::kMulti);
 	list->selected.connect(boost::bind(&GameMessageMenu::selected, this, _1));
 	list->double_clicked.connect(boost::bind(&GameMessageMenu::double_clicked, this, _1));
 	list->add_column(kWindowWidth - 2 * kPadding - 60 - 60 - 75, _("Title"));
@@ -118,12 +118,8 @@
 	archivebtn_ = new UI::Button(this, "archive_or_restore_selected_messages", kPadding,
 	                             kWindowHeight - kPadding - kButtonSize, kButtonSize, kButtonSize,
 	                             g_gr->images().get("images/ui_basic/but2.png"),
-	                             g_gr->images().get("images/wui/messages/message_archive.png"),
-	                             /** TRANSLATORS: %s is a tooltip, Del is the corresponding hotkey */
-	                             (boost::format(_("Del: %s"))
-	                              /** TRANSLATORS: Tooltip in the messages window */
-	                              % _("Archive selected message"))
-	                                .str());
+	                             g_gr->images().get("images/wui/messages/message_archive.png"));
+	update_archive_button_tooltip();
 	archivebtn_->sigclicked.connect(boost::bind(&GameMessageMenu::archive_or_restore, this));
 
 	togglemodebtn_ = new UI::Button(
@@ -231,6 +227,14 @@
 	return false;  // shouldn't happen
 }
 
+bool GameMessageMenu::should_be_hidden(const Widelands::Message& message) {
+	// Wrong box
+	return ((mode == Archive) != (message.status() == Message::Status::kArchived)) ||
+	       // Filtered out
+	       (message_filter_ != Message::Type::kAllMessages &&
+	        message.message_type_category() != message_filter_);
+}
+
 static char const* const status_picture_filename[] = {"images/wui/messages/message_new.png",
                                                       "images/wui/messages/message_read.png",
                                                       "images/wui/messages/message_archived.png"};
@@ -248,13 +252,15 @@
 
 void GameMessageMenu::think() {
 	MessageQueue& mq = iplayer().player().messages();
+	size_t no_selections = list->selections().size();
+	size_t list_size = list->size();
 
 	// Update messages in the list and remove messages
 	// that should no longer be shown
 	for (uint32_t j = list->size(); j; --j) {
 		MessageId id_((*list)[j - 1]);
 		if (Message const* const message = mq[id_]) {
-			if ((mode == Archive) != (message->status() == Message::Status::kArchived)) {
+			if (should_be_hidden(*message)) {
 				list->remove(j - 1);
 			} else {
 				update_record(list->get_record(j - 1), *message);
@@ -268,28 +274,13 @@
 	for (const auto& temp_message : mq) {
 		MessageId const id = temp_message.first;
 		const Message& message = *temp_message.second;
-		Message::Status const status = message.status();
-		if ((mode == Archive) != (status == Message::Status::kArchived))
-			continue;
-		if (!list->find(id.value())) {
+		if (!should_be_hidden(message) && !list->find(id.value())) {
 			UI::Table<uintptr_t>::EntryRecord& er = list->add(id.value());
 			update_record(er, message);
 			list->sort();
 		}
 	}
 
-	// Filter message type
-	if (message_filter_ != Message::Type::kAllMessages) {
-		for (uint32_t j = list->size(); j; --j) {
-			MessageId id_((*list)[j - 1]);
-			if (Message const* const message = mq[id_]) {
-				if (message->message_type_category() != message_filter_) {
-					list->remove(j - 1);
-				}
-			}
-		}
-	}
-
 	if (list->size()) {
 		if (!list->has_selection())
 			list->select(0);
@@ -297,6 +288,10 @@
 		centerviewbtn_->set_enabled(false);
 		message_body.set_text(std::string());
 	}
+
+	if (list_size != list->size() || no_selections != list->selections().size()) {
+		update_archive_button_tooltip();
+	}
 }
 
 void GameMessageMenu::update_record(UI::Table<uintptr_t>::EntryRecord& er,
@@ -332,6 +327,7 @@
 			                  "<p font-size=8> <br></p></rt>%s") %
 			    message->heading() % message->body())
 			      .str());
+			update_archive_button_tooltip();
 			return;
 		}
 	}
@@ -411,33 +407,27 @@
 }
 
 void GameMessageMenu::archive_or_restore() {
+	if (!list->has_selection()) {
+		return;
+	}
 	Widelands::Game& game = iplayer().game();
-	uint32_t const gametime = game.get_gametime();
-	Widelands::Player& player = iplayer().player();
-	Widelands::PlayerNumber const plnum = player.player_number();
-	bool work_done = false;
-
-	switch (mode) {
-	case Inbox:
-		// Archive highlighted message
-		if (!work_done) {
-			if (!list->has_selection())
-				return;
-
+	const Widelands::PlayerNumber plnum = iplayer().player().player_number();
+
+	std::set<uint32_t> selections = list->selections();
+	for (const uint32_t index : selections) {
+		const uintptr_t selected = list->get(list->get_record(index));
+		switch (mode) {
+		case Inbox:
+			// Archive highlighted message
 			game.send_player_command(*new Widelands::CmdMessageSetStatusArchived(
-			   gametime, plnum, MessageId(list->get_selected())));
-		}
-		break;
-	case Archive:
-		// Restore highlighted message
-		if (!work_done) {
-			if (!list->has_selection())
-				return;
-
+			   game.get_gametime(), plnum, MessageId(selected)));
+			break;
+		case Archive:
+			// Restore highlighted message
 			game.send_player_command(*new Widelands::CmdMessageSetStatusRead(
-			   gametime, plnum, MessageId(list->get_selected())));
+			   game.get_gametime(), plnum, MessageId(selected)));
+			break;
 		}
-		break;
 	}
 }
 
@@ -456,6 +446,7 @@
  * @param msgtype the types of messages to show
  */
 void GameMessageMenu::filter_messages(Widelands::Message::Type const msgtype) {
+	list->clear_selections();
 	switch (msgtype) {
 	case Widelands::Message::Type::kGeologists:
 		toggle_filter_messages_button(*geologistsbtn_, msgtype);
@@ -590,11 +581,6 @@
 		mode = Archive;
 		set_title(_("Messages: Archive"));
 		archivebtn_->set_pic(g_gr->images().get("images/wui/messages/message_restore.png"));
-		/** TRANSLATORS: %s is a tooltip, Del is the corresponding hotkey */
-		archivebtn_->set_tooltip((boost::format(_("Del: %s"))
-		                          /** TRANSLATORS: Tooltip in the messages window */
-		                          % _("Restore selected message"))
-		                            .str());
 		togglemodebtn_->set_pic(g_gr->images().get("images/wui/messages/message_new.png"));
 		togglemodebtn_->set_tooltip(_("Show Inbox"));
 		break;
@@ -602,13 +588,50 @@
 		mode = Inbox;
 		set_title(_("Messages: Inbox"));
 		archivebtn_->set_pic(g_gr->images().get("images/wui/messages/message_archive.png"));
-		/** TRANSLATORS: %s is a tooltip, Del is the corresponding hotkey */
-		archivebtn_->set_tooltip((boost::format(_("Del: %s"))
-		                          /** TRANSLATORS: Tooltip in the messages window */
-		                          % _("Archive selected message"))
-		                            .str());
 		togglemodebtn_->set_pic(g_gr->images().get("images/wui/messages/message_archived.png"));
 		togglemodebtn_->set_tooltip(_("Show Archive"));
 		break;
 	}
+	update_archive_button_tooltip();
+}
+
+void GameMessageMenu::update_archive_button_tooltip() {
+	if (list->empty() || !list->has_selection()) {
+		archivebtn_->set_tooltip("");
+		archivebtn_->set_enabled(false);
+		return;
+	}
+	archivebtn_->set_enabled(true);
+	std::string tooltip = "";
+	size_t no_selections = list->selections().size();
+	switch (mode) {
+	case Archive:
+		if (no_selections > 1) {
+			/** TRANSLATORS: Tooltip in the messages window. There is a separate string for 1 message.
+			 */
+			tooltip = (boost::format(ngettext("Restore the selected %d message",
+			                                  "Restore the selected %d messages", no_selections)) %
+			           no_selections)
+			             .str();
+		} else {
+			/** TRANSLATORS: Tooltip in the messages window */
+			tooltip = _("Restore selected message");
+		}
+		break;
+	case Inbox:
+		if (no_selections > 1) {
+			/** TRANSLATORS: Tooltip in the messages window. There is a separate string for 1 message.
+			 */
+			tooltip = (boost::format(ngettext("Archive the selected %d message",
+			                                  "Archive the selected %d messages", no_selections)) %
+			           no_selections)
+			             .str();
+		} else {
+			/** TRANSLATORS: Tooltip in the messages window */
+			tooltip = _("Archive selected message");
+		}
+		break;
+	}
+	/** TRANSLATORS: %s is a tooltip, Del is the corresponding hotkey */
+	archivebtn_->set_tooltip((boost::format(_("Del: %s")) % tooltip).str());
 }

=== modified file 'src/wui/game_message_menu.h'
--- src/wui/game_message_menu.h	2016-09-16 10:32:40 +0000
+++ src/wui/game_message_menu.h	2016-12-07 21:22:33 +0000
@@ -59,6 +59,8 @@
 	bool compare_status(uint32_t a, uint32_t b);
 	bool compare_type(uint32_t a, uint32_t b);
 	bool compare_time_sent(uint32_t a, uint32_t b);
+	bool should_be_hidden(const Widelands::Message& message);
+
 	void archive_or_restore();
 	void toggle_mode();
 	void center_view();
@@ -67,6 +69,7 @@
 	void set_filter_messages_tooltips();
 	std::string display_message_type_icon(Widelands::Message);
 	void update_record(UI::Table<uintptr_t>::EntryRecord& er, const Widelands::Message&);
+	void update_archive_button_tooltip();
 
 	UI::Table<uintptr_t>* list;
 	UI::MultilineTextarea message_body;

=== modified file 'src/wui/maptable.cc'
--- src/wui/maptable.cc	2016-10-07 08:19:52 +0000
+++ src/wui/maptable.cc	2016-12-07 21:22:33 +0000
@@ -26,10 +26,8 @@
 #include "graphic/graphic.h"
 #include "io/filesystem/filesystem.h"
 
-MapTable::MapTable(
-   UI::Panel* parent, int32_t x, int32_t y, uint32_t w, uint32_t h, const bool descending)
-   : UI::Table<uintptr_t>(
-        parent, x, y, w, h, g_gr->images().get("images/ui_basic/but3.png"), descending) {
+MapTable::MapTable(UI::Panel* parent, int32_t x, int32_t y, uint32_t w, uint32_t h)
+   : UI::Table<uintptr_t>(parent, x, y, w, h, g_gr->images().get("images/ui_basic/but3.png")) {
 
 	/** TRANSLATORS: Column title for number of players in map list */
 	add_column(35, _("Pl."), _("Number of players"), UI::Align::kHCenter);

=== modified file 'src/wui/maptable.h'
--- src/wui/maptable.h	2016-08-04 15:49:05 +0000
+++ src/wui/maptable.h	2016-12-07 21:22:33 +0000
@@ -32,7 +32,7 @@
  */
 class MapTable : public UI::Table<uintptr_t> {
 public:
-	MapTable(UI::Panel* parent, int32_t x, int32_t y, uint32_t w, uint32_t h, const bool descending);
+	MapTable(UI::Panel* parent, int32_t x, int32_t y, uint32_t w, uint32_t h);
 
 	/// Fill the table with maps and directories.
 	void fill(const std::vector<MapData>& entries, MapData::DisplayType type);


Follow ups