Review: Needs Fixing

Great work, I really like the new list!

Most of the code looks good, three small bugs and some other comments, though.

The bugs:
- Hotkey 'e' even works on non-seafaring maps
- Missing "ship name" column in the list when there currently are no ships
- Memory leak with create_shipinfo

Two non code related comments (feel free to ignore) :
- I would like a button for opening the ship window without actually moving 
there. This could, e.g., be useful to start constructing a port while busy 
elsewhere. Currently both buttons move the view to the ship, maybe have one 
button for the window and one button to move to the ship?
- I am not really happy with calling the window "ship statistics" (in code and 
UI). For me, that sounds like a graph showing the number of ships (or so), and 
not like a list of all ships.

For the other comments, see the diff.

Diff comments:

> === modified file 'data/tribes/scripting/help/controls.lua'
> --- data/tribes/scripting/help/controls.lua   2017-12-06 08:16:46 +0000
> +++ data/tribes/scripting/help/controls.lua   2018-04-16 15:39:30 +0000
> @@ -52,6 +62,8 @@
>                 dl(help_format_hotkey("I"), _"Toggle stock inventory") ..
>                 -- TRANSLATORS: This is an access key combination. The hotkey 
> is 'b'
>                 dl(help_format_hotkey("B"), _"Toggle building statistics") ..
> +               -- TRANSLATORS: This is an access key combination. The hotkey 
> is 'p'

I guess you mean 'e' here?

> +               dl(help_format_hotkey("E"), _"Toggle seafaring statistics") ..
>                 -- TRANSLATORS: This is an access key combination. Localize, 
> but do not change the key.
>                 dl(help_format_hotkey(pgettext("hotkey", "Home")), _"Center 
> main mapview on starting location") ..
>                 -- TRANSLATORS: This is an access key combination. Localize, 
> but do not change the key.
> @@ -101,5 +103,28 @@
>                 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")
> -          )
> +          ) ..
> +
> +         -- TRANSLATORS: Heading in "Controls" help
> +         h2(_"Ship Statistics") ..
> +         p(
> +               -- TRANSLATORS: This is the helptext for an access key 
> combination.
> +               dl(help_format_hotkey(pgettext("hotkey", "Alt + 0")), _"Show 
> all ships") ..
> +               -- TRANSLATORS: This is the helptext for an access key 
> combination.
> +               dl(help_format_hotkey(pgettext("hotkey", "Alt + 1")), _"Show 
> idle ships") ..
> +               -- TRANSLATORS: This is the helptext for an access key 
> combination.
> +               dl(help_format_hotkey(pgettext("hotkey", "Alt + 2")), _"Show 
> ships shipping wares and workers") ..
> +               -- TRANSLATORS: This is the helptext for an access key 
> combination.
> +               dl(help_format_hotkey(pgettext("hotkey", "Alt + 3")), _"Show 
> waiting expeditions") ..
> +               -- TRANSLATORS: This is the helptext for an access key 
> combination.
> +               dl(help_format_hotkey(pgettext("hotkey", "Alt + 4")), _"Show 
> scouting expeditions") ..
> +               -- TRANSLATORS: This is the helptext for an access key 
> combination.
> +               dl(help_format_hotkey(pgettext("hotkey", "Alt + 5")), _"Show 
> colonizing expeditions and expeditions with port space found") ..

I am not quite sure what "colonizing expeditions" are. Ships currently building 
ports?

> +               -- TRANSLATORS: This is the helptext for an access key 
> combination.
> +               dl(help_format_hotkey("G"), _"Center the map on the selected 
> ship") ..
> +               -- TRANSLATORS: This is the helptext for an access key 
> combination.
> +               dl(help_format_hotkey("O"), _"Go to the selected ship and 
> open its window") ..
> +               -- TRANSLATORS: This is the helptext for an access key 
> combination.
> +               dl(help_format_hotkey("W"), _"Watch the selected ship")
> +         )
>  }
> 
> === modified file 'src/logic/map_objects/tribes/ship.cc'
> --- src/logic/map_objects/tribes/ship.cc      2018-04-07 16:59:00 +0000
> +++ src/logic/map_objects/tribes/ship.cc      2018-04-16 15:39:30 +0000
> @@ -847,14 +849,14 @@
>                    pgettext("ship", "Expedition"), _("Expedition Ready"),
>                    _("An expedition ship is waiting for your commands."),
>                    "images/wui/buildings/start_expedition.png");
> -     Notifications::publish(NoteShipMessage(this, 
> NoteShipMessage::Message::kWaitingForCommand));
> +     Notifications::publish(NoteShip(this, 
> NoteShip::Action::kWaitingForCommand));

You could use set_ship_state_and_notify() here.

>  }
>  
>  /// Initializes / changes the direction of scouting to @arg direction
>  /// @note only called via player command
>  void Ship::exp_scouting_direction(Game&, WalkingDir scouting_direction) {
>       assert(expedition_);
> -     ship_state_ = ShipStates::kExpeditionScouting;
> +     set_ship_state_and_notify(ShipStates::kExpeditionScouting, 
> NoteShip::Action::kDestinationChanged);
>       expedition_->scouting_direction = scouting_direction;
>       expedition_->island_exploration = false;
>  }
> 
> === modified file 'src/logic/player.cc'
> --- src/logic/player.cc       2018-04-07 16:59:00 +0000
> +++ src/logic/player.cc       2018-04-16 15:39:30 +0000
> @@ -372,6 +373,18 @@
>       game->cmdqueue().enqueue(new CmdDeleteMessage(game->get_gametime(), 
> player_number_, message_id));
>  }
>  
> +const std::set<Serial>& Player::ships() const {
> +     return ships_;
> +}
> +void Player::add_ship(Serial ship) {
> +     ships_.insert(ship);
> +}
> +void Player::remove_ship(Serial ship) {
> +     if (ships_.count(ship) == 1) {
> +             ships_.erase(ship);
> +     }

Using ships.find() + erase(iter) could be a bit faster. But I don't know for 
sure and it doesn't matters anyway.

> +}
> +
>  /*
>  ===============
>  Return filtered buildcaps that take the player's territory into account.
> @@ -1303,7 +1316,7 @@
>               remaining_shipnames_.erase(it);
>               return new_name;
>       }
> -     return "Ship";
> +     return (boost::format(pgettext("shipname", "Ship %d")) % 
> ships_.size()).str();

I think this will return strange results after sinking a ship, i.e. having two 
(or more) with the same number.
Maybe add an explicit counter for the additional ships?

>  }
>  
>  /**
> 
> === modified file 'src/scripting/lua_game.cc'
> --- src/scripting/lua_game.cc 2018-04-07 16:59:00 +0000
> +++ src/scripting/lua_game.cc 2018-04-16 15:39:30 +0000
> @@ -672,30 +672,16 @@
>  */
>  int LuaPlayer::get_ships(lua_State* L) {
>       EditorGameBase& egbase = get_egbase(L);
> -     const Map& map = egbase.map();
>       PlayerNumber p = (get(L, egbase)).player_number();
>       lua_newtable(L);
>       uint32_t cidx = 1;
> -
> -     std::set<OPtr<Ship>> found_ships;
> -     for (int16_t y = 0; y < map.get_height(); ++y) {
> -             for (int16_t x = 0; x < map.get_width(); ++x) {

Oh dear... Glad I didn't knew about this code. ^^

> -                     FCoords f = map.get_fcoords(Coords(x, y));
> -                     // there are too many bobs on the map so we investigate
> -                     // only bobs on water
> -                     if (f.field->nodecaps() & MOVECAPS_SWIM) {
> -                             for (Bob* bob = f.field->get_first_bob(); bob; 
> bob = bob->get_next_on_field()) {
> -                                     if (upcast(Ship, ship, bob)) {
> -                                             if 
> (ship->get_owner()->player_number() == p && !found_ships.count(ship)) {
> -                                                     
> found_ships.insert(ship);
> -                                                     lua_pushuint32(L, 
> cidx++);
> -                                                     
> LuaMaps::upcasted_map_object_to_lua(L, ship);
> -                                                     lua_rawset(L, -3);
> -                                             }
> -                                     }
> -                             }
> -                     }
> -             }
> +     for (const auto& serial : egbase.player(p).ships()) {
> +             Widelands::MapObject* obj = egbase.objects().get_object(serial);
> +             assert(obj->descr().type() == Widelands::MapObjectType::SHIP);
> +             upcast(Widelands::Ship, ship, obj);
> +             lua_pushuint32(L, cidx++);
> +             LuaMaps::upcasted_map_object_to_lua(L, ship);
> +             lua_rawset(L, -3);
>       }
>       return 1;
>  }
> 
> === modified file 'src/ui_basic/table.cc'
> --- src/ui_basic/table.cc     2018-04-07 16:59:00 +0000
> +++ src/ui_basic/table.cc     2018-04-16 15:39:30 +0000
> @@ -551,6 +551,21 @@
>       layout();
>  }
>  
> +/**
> + * Remove the given table entry if it exists.
> + */
> +void Table<void*>::remove_entry(const void* const entry) {
> +     EntryRecord* er = find(entry);
> +     if (er != nullptr) {
> +             for (uint32_t i = 0; i < entry_records_.size(); ++i) {

find() is iterating over the table entries as well. Skip the find() call and 
iterate immediately?
Not sure whether that is really more efficient on average since I don't know 
about possible speed differences between for-each and for(int).

> +                     if (entry_records_[i] == er) {
> +                             remove(i);
> +                             return;
> +                     }
> +             }
> +     }
> +}
> +
>  bool Table<void*>::sort_helper(uint32_t a, uint32_t b) {
>       if (sort_descending_) {
>               return columns_[sort_column_].compare(b, a);
> 
> === modified file 'src/wui/interactive_player.cc'
> --- src/wui/interactive_player.cc     2018-04-07 16:59:00 +0000
> +++ src/wui/interactive_player.cc     2018-04-16 15:39:30 +0000
> @@ -459,6 +460,14 @@
>                       }
>                       return true;
>  
> +             case SDLK_e:

This should only do something on seafaring maps but works on all maps.

> +                     if (main_windows_.seafaring_stats.window == nullptr) {
> +                             new SeafaringStatisticsMenu(*this, 
> main_windows_.seafaring_stats);
> +                     } else {
> +                             main_windows_.seafaring_stats.toggle();
> +                     }
> +                     return true;
> +
>               case SDLK_s:
>                       if (code.mod & (KMOD_LCTRL | KMOD_RCTRL))
>                               new GameMainMenuSaveGame(*this, 
> main_windows_.savegame);
> 
> === added file 'src/wui/seafaring_statistics_menu.cc'
> --- src/wui/seafaring_statistics_menu.cc      1970-01-01 00:00:00 +0000
> +++ src/wui/seafaring_statistics_menu.cc      2018-04-16 15:39:30 +0000
> @@ -0,0 +1,568 @@
> +/*
> + * Copyright (C) 2017-2018 by the Widelands Development Team
> + *
> + * This program is free software; you can redistribute it and/or
> + * modify it under the terms of the GNU General Public License
> + * as published by the Free Software Foundation; either version 2
> + * of the License, or (at your option) any later version.
> + *
> + * This program is distributed in the hope that it will be useful,
> + * but WITHOUT ANY WARRANTY; without even the implied warranty of
> + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
> + * GNU General Public License for more details.
> + *
> + * You should have received a copy of the GNU General Public License
> + * along with this program; if not, write to the Free Software
> + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  
> 02110-1301, USA.
> + *
> + */
> +
> +#include "wui/seafaring_statistics_menu.h"
> +
> +#include <memory>
> +
> +#include <boost/bind.hpp>
> +#include <boost/format.hpp>
> +
> +#include "economy/fleet.h"
> +#include "graphic/graphic.h"
> +#include "logic/game.h"
> +#include "logic/player.h"
> +#include "logic/playercommand.h"
> +#include "ui_basic/box.h"
> +#include "wui/interactive_player.h"
> +#include "wui/shipwindow.h"
> +#include "wui/watchwindow.h"
> +
> +inline InteractivePlayer& SeafaringStatisticsMenu::iplayer() const {
> +     return dynamic_cast<InteractivePlayer&>(*get_parent());
> +}
> +
> +constexpr int kPadding = 5;
> +constexpr int kButtonSize = 34;
> +
> +SeafaringStatisticsMenu::SeafaringStatisticsMenu(InteractivePlayer& plr,
> +                                                 UI::UniqueWindow::Registry& 
> registry)
> +   : UI::UniqueWindow(&plr, "seafaring_statistics", &registry, 355, 375, 
> _("Seafaring Statistics")),
> +     main_box_(this, kPadding, kPadding, UI::Box::Vertical, get_inner_w(), 
> get_inner_h(), kPadding),
> +     filter_box_(
> +        &main_box_, 0, 0, UI::Box::Horizontal, get_inner_w() - 2 * kPadding, 
> kButtonSize, kPadding),
> +     idle_btn_(&filter_box_,
> +               "filter_ship_idle",
> +               0,
> +               0,
> +               kButtonSize,
> +               kButtonSize,
> +               g_gr->images().get("images/ui_basic/but0.png"),
> +               status_to_image(ShipFilterStatus::kIdle)),
> +     waiting_btn_(&filter_box_,
> +                  "filter_ship_waiting",
> +                  0,
> +                  0,
> +                  kButtonSize,
> +                  kButtonSize,
> +                  g_gr->images().get("images/ui_basic/but0.png"),
> +                  status_to_image(ShipFilterStatus::kExpeditionWaiting)),
> +     scouting_btn_(&filter_box_,
> +                   "filter_ship_scouting",
> +                   0,
> +                   0,
> +                   kButtonSize,
> +                   kButtonSize,
> +                   g_gr->images().get("images/ui_basic/but0.png"),
> +                   status_to_image(ShipFilterStatus::kExpeditionScouting)),
> +     portspace_btn_(&filter_box_,
> +                    "filter_ship_portspace",
> +                    0,
> +                    0,
> +                    kButtonSize,
> +                    kButtonSize,
> +                    g_gr->images().get("images/ui_basic/but0.png"),
> +                    
> status_to_image(ShipFilterStatus::kExpeditionPortspaceFound)),
> +     shipping_btn_(&filter_box_,
> +                   "filter_ship_transporting",
> +                   0,
> +                   0,
> +                   kButtonSize,
> +                   kButtonSize,
> +                   g_gr->images().get("images/ui_basic/but0.png"),
> +                   status_to_image(ShipFilterStatus::kShipping)),
> +     ship_filter_(ShipFilterStatus::kAll),
> +     navigation_box_(
> +        &main_box_, 0, 0, UI::Box::Horizontal, get_inner_w() - 2 * kPadding, 
> kButtonSize, kPadding),
> +     watchbtn_(&navigation_box_,
> +               "seafaring_stats_watch_button",
> +               0,
> +               0,
> +               kButtonSize,
> +               kButtonSize,
> +               g_gr->images().get("images/ui_basic/but2.png"),
> +               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()),
> +     openwindowbtn_(&navigation_box_,
> +                    "seafaring_stats_watch_button",
> +                    0,
> +                    0,
> +                    kButtonSize,
> +                    kButtonSize,
> +                    g_gr->images().get("images/ui_basic/but2.png"),
> +                    g_gr->images().get("images/ui_basic/fsel.png"),
> +                    (boost::format(_("%1% (Hotkey: %2%)")) %
> +                     /** TRANSLATORS: Tooltip in the seafaring statistics 
> window */
> +                     _("Go to the selected ship and open its window") %
> +                     pgettext("hotkey", "O"))
> +                       .str()),
> +     centerviewbtn_(&navigation_box_,
> +                    "seafaring_stats_center_main_mapview_button",
> +                    0,
> +                    0,
> +                    kButtonSize,
> +                    kButtonSize,
> +                    g_gr->images().get("images/ui_basic/but2.png"),
> +                    g_gr->images().get("images/wui/ship/menu_ship_goto.png"),
> +                    (boost::format(_("%1% (Hotkey: %2%)")) %
> +                     /** TRANSLATORS: Tooltip in the seafaring statistics 
> window */
> +                     _("Center the map on the selected ship") %
> +                     pgettext("hotkey", "G"))
> +                       .str()),
> +     table_(&main_box_,
> +            0,
> +            0,
> +            get_inner_w() - 2 * kPadding,
> +            100,
> +            g_gr->images().get("images/ui_basic/but1.png")) {
> +
> +     const Widelands::TribeDescr& tribe = iplayer().player().tribe();
> +     colony_icon_ = tribe.get_worker_descr(tribe.builder())->icon();
> +
> +     // Buttons for ship states
> +     main_box_.add(&filter_box_, UI::Box::Resizing::kFullSize);
> +     filter_box_.add(&idle_btn_);
> +     filter_box_.add(&shipping_btn_);
> +     filter_box_.add(&waiting_btn_);
> +     filter_box_.add(&scouting_btn_);
> +     filter_box_.add(&portspace_btn_);
> +
> +     main_box_.add(&table_, UI::Box::Resizing::kExpandBoth);
> +
> +     // Navigation buttons
> +     main_box_.add(&navigation_box_, UI::Box::Resizing::kFullSize);
> +     navigation_box_.add(&watchbtn_);
> +     navigation_box_.add_inf_space();
> +     navigation_box_.add(&openwindowbtn_);
> +     navigation_box_.add(&centerviewbtn_);
> +     main_box_.set_size(get_inner_w() - 2 * kPadding, get_inner_h() - 2 * 
> kPadding);
> +
> +     // Configure actions
> +     idle_btn_.sigclicked.connect(
> +        boost::bind(&SeafaringStatisticsMenu::filter_ships, this, 
> ShipFilterStatus::kIdle));
> +     shipping_btn_.sigclicked.connect(
> +        boost::bind(&SeafaringStatisticsMenu::filter_ships, this, 
> ShipFilterStatus::kShipping));
> +     waiting_btn_.sigclicked.connect(boost::bind(
> +        &SeafaringStatisticsMenu::filter_ships, this, 
> ShipFilterStatus::kExpeditionWaiting));
> +     scouting_btn_.sigclicked.connect(boost::bind(
> +        &SeafaringStatisticsMenu::filter_ships, this, 
> ShipFilterStatus::kExpeditionScouting));
> +     portspace_btn_.sigclicked.connect(boost::bind(
> +        &SeafaringStatisticsMenu::filter_ships, this, 
> ShipFilterStatus::kExpeditionPortspaceFound));
> +     ship_filter_ = ShipFilterStatus::kAll;
> +     set_filter_ships_tooltips();
> +
> +     
> watchbtn_.sigclicked.connect(boost::bind(&SeafaringStatisticsMenu::watch_ship,
>  this));
> +     
> openwindowbtn_.sigclicked.connect(boost::bind(&SeafaringStatisticsMenu::open_ship_window,
>  this));
> +     
> centerviewbtn_.sigclicked.connect(boost::bind(&SeafaringStatisticsMenu::center_view,
>  this));
> +
> +     // Configure table
> +     table_.selected.connect(boost::bind(&SeafaringStatisticsMenu::selected, 
> this));
> +     
> table_.double_clicked.connect(boost::bind(&SeafaringStatisticsMenu::double_clicked,
>  this));
> +     table_.add_column(
> +        0, pgettext("ship", "Name"), "", UI::Align::kLeft, 
> UI::TableColumnType::kFlexible);
> +     table_.add_column(200, pgettext("ship", "Status"));
> +     table_.set_sort_column(ColName);
> +     fill_table();
> +
> +     set_can_focus(true);
> +     set_thinks(false);
> +     table_.focus();
> +
> +     shipnotes_subscriber_ =
> +        Notifications::subscribe<Widelands::NoteShip>([this](const 
> Widelands::NoteShip& note) {
> +                if (iplayer().get_player() == note.ship->get_owner()) {
> +                        switch (note.action) {
> +                        case 
> Widelands::NoteShip::Action::kDestinationChanged:
> +                        case Widelands::NoteShip::Action::kWaitingForCommand:
> +                        case Widelands::NoteShip::Action::kGained:
> +                                update_ship(*note.ship);
> +                                break;
> +                        case Widelands::NoteShip::Action::kLost:
> +                                remove_ship(note.ship->serial());
> +                                break;
> +                        default:
> +                                NEVER_HERE();
> +                        }
> +                }
> +             });
> +}
> +
> +const std::string
> +SeafaringStatisticsMenu::status_to_string(SeafaringStatisticsMenu::ShipFilterStatus
>  status) const {
> +     switch (status) {
> +     case SeafaringStatisticsMenu::ShipFilterStatus::kIdle:
> +             return pgettext("ship_state", "Idle");
> +     case SeafaringStatisticsMenu::ShipFilterStatus::kShipping:
> +             return pgettext("ship_state", "Shipping");
> +     case SeafaringStatisticsMenu::ShipFilterStatus::kExpeditionWaiting:
> +             return pgettext("ship_state", "Waiting");
> +     case SeafaringStatisticsMenu::ShipFilterStatus::kExpeditionScouting:
> +             return pgettext("ship_state", "Scouting");
> +     case 
> SeafaringStatisticsMenu::ShipFilterStatus::kExpeditionPortspaceFound:
> +             return pgettext("ship_state", "Port Space Found");
> +     case SeafaringStatisticsMenu::ShipFilterStatus::kExpeditionColonizing:
> +             return pgettext("ship_state", "Founding a Colony");
> +     case SeafaringStatisticsMenu::ShipFilterStatus::kAll:
> +             return "All";  // The user shouldn't see this, so we don't 
> localize
> +     default:
> +             NEVER_HERE();
> +     }
> +}
> +
> +const Image*
> +SeafaringStatisticsMenu::status_to_image(SeafaringStatisticsMenu::ShipFilterStatus
>  status) const {
> +     std::string filename = "";
> +     switch (status) {
> +     case SeafaringStatisticsMenu::ShipFilterStatus::kIdle:
> +             filename = "images/wui/stats/ship_stats_idle.png";
> +             break;
> +     case SeafaringStatisticsMenu::ShipFilterStatus::kShipping:
> +             filename = "images/wui/stats/ship_stats_shipping.png";
> +             break;
> +     case SeafaringStatisticsMenu::ShipFilterStatus::kExpeditionWaiting:
> +             filename = "images/wui/buildings/start_expedition.png";
> +             break;
> +     case SeafaringStatisticsMenu::ShipFilterStatus::kExpeditionScouting:
> +             filename = "images/wui/ship/ship_explore_island_cw.png";
> +             break;
> +     case 
> SeafaringStatisticsMenu::ShipFilterStatus::kExpeditionPortspaceFound:
> +             filename = "images/wui/ship/ship_construct_port_space.png";
> +             break;
> +     case SeafaringStatisticsMenu::ShipFilterStatus::kExpeditionColonizing:
> +             return colony_icon_;
> +     case SeafaringStatisticsMenu::ShipFilterStatus::kAll:
> +             filename = "images/wui/ship/ship_scout_ne.png";
> +             break;
> +     default:
> +             NEVER_HERE();
> +     }
> +     return g_gr->images().get(filename);
> +}
> +
> +const SeafaringStatisticsMenu::ShipInfo*
> +SeafaringStatisticsMenu::create_shipinfo(const Widelands::Ship& ship) const {
> +     if (&ship == nullptr) {
> +             return new ShipInfo();
> +     }
> +     const Widelands::Ship::ShipStates state = ship.get_ship_state();
> +     ShipFilterStatus status = ShipFilterStatus::kAll;
> +     switch (state) {
> +     case Widelands::Ship::ShipStates::kTransport:
> +             if (ship.get_destination(iplayer().game()) != nullptr) {
> +                     status = ShipFilterStatus::kShipping;
> +             } else {
> +                     status = ShipFilterStatus::kIdle;
> +             }
> +             break;
> +     case Widelands::Ship::ShipStates::kExpeditionWaiting:
> +             status = ShipFilterStatus::kExpeditionWaiting;
> +             break;
> +     case Widelands::Ship::ShipStates::kExpeditionScouting:
> +             status = ShipFilterStatus::kExpeditionScouting;
> +             break;
> +     case Widelands::Ship::ShipStates::kExpeditionPortspaceFound:
> +             status = ShipFilterStatus::kExpeditionPortspaceFound;
> +             break;
> +     case Widelands::Ship::ShipStates::kExpeditionColonizing:
> +             status = ShipFilterStatus::kExpeditionColonizing;
> +             break;
> +     case Widelands::Ship::ShipStates::kSinkRequest:
> +     case Widelands::Ship::ShipStates::kSinkAnimation:
> +             status = ShipFilterStatus::kAll;
> +     }
> +     return new ShipInfo(ship.get_shipname(), status, ship.serial());
> +}
> +
> +void 
> SeafaringStatisticsMenu::set_entry_record(UI::Table<uintptr_t>::EntryRecord* 
> er,
> +                                               const ShipInfo& info) {
> +     if (info.status != ShipFilterStatus::kAll) {
> +             er->set_string(ColName, info.name);
> +             er->set_picture(ColStatus, status_to_image(info.status), 
> status_to_string(info.status));
> +     }
> +}
> +
> +Widelands::Ship* SeafaringStatisticsMenu::serial_to_ship(Widelands::Serial 
> serial) const {
> +     Widelands::MapObject* obj = 
> iplayer().game().objects().get_object(serial);
> +     assert(obj->descr().type() == Widelands::MapObjectType::SHIP);
> +     upcast(Widelands::Ship, ship, obj);
> +     return ship;
> +}
> +
> +void SeafaringStatisticsMenu::update_ship(const Widelands::Ship& ship) {
> +     assert(iplayer().get_player() == ship.get_owner());
> +     const ShipInfo* info = create_shipinfo(ship);
> +     // Remove ships that don't satisfy the filter
> +     if (ship_filter_ != ShipFilterStatus::kAll && !satisfies_filter(*info, 
> ship_filter_)) {
> +             remove_ship(info->serial);
> +             return;
> +     }
> +     // Try to find the ship in the table
> +     if (data_.count(info->serial) == 1) {
> +             const ShipInfo* old_info = data_[info->serial].get();
> +             if (info->status != old_info->status) {
> +                     // The status has changed - we need an update
> +                     data_[info->serial] = std::unique_ptr<const 
> ShipInfo>(info);
> +                     UI::Table<uintptr_t>::EntryRecord* er = 
> table_.find(info->serial);
> +                     set_entry_record(er, *info);
> +             }
> +     } else {
> +             // This is a new ship or it was filtered away before
> +             data_.insert(std::make_pair(info->serial, std::unique_ptr<const 
> ShipInfo>(info)));
> +             UI::Table<uintptr_t>::EntryRecord& er = 
> table_.add(info->serial);
> +             set_entry_record(&er, *info);
> +     }
> +     table_.sort();
> +     set_buttons_enabled();
> +}
> +
> +void SeafaringStatisticsMenu::remove_ship(Widelands::Serial serial) {
> +     if (data_.count(serial) == 1) {
> +             table_.remove_entry(serial);
> +             data_.erase(data_.find(serial));
> +             if (!table_.empty() && !table_.has_selection()) {
> +                     table_.select(0);
> +             }
> +             set_buttons_enabled();
> +     }
> +}
> +
> +void 
> SeafaringStatisticsMenu::update_entry_record(UI::Table<uintptr_t>::EntryRecord&
>  er,
> +                                                  const ShipInfo& info) {
> +     er.set_picture(ColStatus, status_to_image(info.status), 
> status_to_string(info.status));
> +}
> +
> +void SeafaringStatisticsMenu::selected() {
> +     set_buttons_enabled();
> +}
> +
> +void SeafaringStatisticsMenu::double_clicked() {
> +     if (table_.has_selection()) {
> +             center_view();
> +     }
> +}
> +
> +void SeafaringStatisticsMenu::set_buttons_enabled() {

Maybe call the method update_buttons_state() ? The current name sounds like 
they will always be set to enabled.

> +     centerviewbtn_.set_enabled(table_.has_selection());
> +     openwindowbtn_.set_enabled(table_.has_selection());
> +     watchbtn_.set_enabled(table_.has_selection());
> +}
> +
> +bool SeafaringStatisticsMenu::handle_key(bool down, SDL_Keysym code) {
> +     if (down) {
> +             switch (code.sym) {
> +             // Don't forget to change the tooltips if any of these get 
> reassigned
> +             case SDLK_g:
> +                     center_view();
> +                     return true;
> +             case SDLK_o:
> +                     open_ship_window();
> +                     return true;
> +             case SDLK_w:
> +                     watch_ship();
> +                     return true;
> +             case SDLK_0:
> +                     if (code.mod & KMOD_ALT) {
> +                             filter_ships(ShipFilterStatus::kAll);
> +                             return true;
> +                     }
> +                     return false;
> +             case SDLK_1:
> +                     if (code.mod & KMOD_ALT) {
> +                             filter_ships(ShipFilterStatus::kIdle);
> +                             return true;
> +                     }
> +                     return false;
> +             case SDLK_2:
> +                     if (code.mod & KMOD_ALT) {
> +                             filter_ships(ShipFilterStatus::kShipping);
> +                             return true;
> +                     }
> +                     return false;
> +             case SDLK_3:
> +                     if (code.mod & KMOD_ALT) {
> +                             
> filter_ships(ShipFilterStatus::kExpeditionWaiting);
> +                             return true;
> +                     }
> +                     return false;
> +             case SDLK_4:
> +                     if (code.mod & KMOD_ALT) {
> +                             
> filter_ships(ShipFilterStatus::kExpeditionScouting);
> +                             return true;
> +                     }
> +                     return false;
> +             case SDLK_5:
> +                     if (code.mod & KMOD_ALT) {
> +                             
> filter_ships(ShipFilterStatus::kExpeditionPortspaceFound);
> +                             return true;
> +                     }
> +                     return false;
> +             case SDL_SCANCODE_KP_PERIOD:
> +             case SDLK_KP_PERIOD:
> +                     if (code.mod & KMOD_NUM)
> +                             break;
> +             /* no break */
> +             default:
> +                     break;  // not handled
> +             }
> +     }
> +
> +     return table_.handle_key(down, code);
> +}
> +
> +void SeafaringStatisticsMenu::center_view() {
> +     if (table_.has_selection()) {
> +             Widelands::Ship* ship = serial_to_ship(table_.get_selected());
> +             iplayer().map_view()->scroll_to_field(ship->get_position(), 
> MapView::Transition::Smooth);
> +     }
> +}
> +
> +void SeafaringStatisticsMenu::watch_ship() {
> +     if (table_.has_selection()) {
> +             Widelands::Ship* ship = serial_to_ship(table_.get_selected());
> +             WatchWindow* window = show_watch_window(iplayer(), 
> ship->get_position());
> +             window->follow(ship);
> +     }
> +}
> +
> +void SeafaringStatisticsMenu::open_ship_window() {
> +     if (table_.has_selection()) {
> +             center_view();
> +             Widelands::Ship* ship = serial_to_ship(table_.get_selected());
> +             iplayer().show_ship_window(ship);
> +     }
> +}
> +
> +void SeafaringStatisticsMenu::filter_ships(ShipFilterStatus status) {
> +     switch (status) {
> +     case ShipFilterStatus::kExpeditionWaiting:
> +             toggle_filter_ships_button(waiting_btn_, status);
> +             break;
> +     case ShipFilterStatus::kExpeditionScouting:
> +             toggle_filter_ships_button(scouting_btn_, status);
> +             break;
> +     // We're grouping the "colonizing" status with the port space.
> +     case ShipFilterStatus::kExpeditionColonizing:
> +     case ShipFilterStatus::kExpeditionPortspaceFound:
> +             toggle_filter_ships_button(portspace_btn_, status);
> +             break;
> +     case ShipFilterStatus::kShipping:
> +             toggle_filter_ships_button(shipping_btn_, status);
> +             break;
> +     case ShipFilterStatus::kIdle:
> +             toggle_filter_ships_button(idle_btn_, status);
> +             break;
> +     case ShipFilterStatus::kAll:
> +             set_filter_ships_tooltips();
> +             ship_filter_ = ShipFilterStatus::kAll;
> +             waiting_btn_.set_perm_pressed(false);
> +             scouting_btn_.set_perm_pressed(false);
> +             portspace_btn_.set_perm_pressed(false);
> +             shipping_btn_.set_perm_pressed(false);
> +             idle_btn_.set_perm_pressed(false);
> +             break;
> +     }
> +     fill_table();
> +}
> +
> +void SeafaringStatisticsMenu::toggle_filter_ships_button(UI::Button& button,
> +                                                         ShipFilterStatus 
> status) {
> +     set_filter_ships_tooltips();
> +     if (button.style() == UI::Button::Style::kPermpressed) {
> +             button.set_perm_pressed(false);
> +             ship_filter_ = ShipFilterStatus::kAll;
> +     } else {
> +             waiting_btn_.set_perm_pressed(false);
> +             scouting_btn_.set_perm_pressed(false);
> +             portspace_btn_.set_perm_pressed(false);
> +             shipping_btn_.set_perm_pressed(false);
> +             idle_btn_.set_perm_pressed(false);
> +             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());
> +     }
> +}
> +
> +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());
> +     portspace_btn_.set_tooltip(
> +        (boost::format(_("%1% (Hotkey: %2%)"))
> +         /** TRANSLATORS: Tooltip in the messages window */
> +         % _("Show colonizing expeditions and expeditions with port space 
> found") %
> +         pgettext("hotkey", "Alt + 5"))
> +           .str());
> +}
> +
> +bool SeafaringStatisticsMenu::satisfies_filter(const ShipInfo& info, 
> ShipFilterStatus filter) {
> +     return filter == info.status || (filter == 
> ShipFilterStatus::kExpeditionPortspaceFound &&
> +                                      info.status == 
> ShipFilterStatus::kExpeditionColonizing);
> +}
> +
> +void SeafaringStatisticsMenu::fill_table() {

I don't really understand this method. I think set_buttons_enabled() should 
disable all buttons. I intended to complain that they aren't re-enabled when 
something is selected later on but that seems to be done somehow. Well, 
everything fine then, I guess.
Ah, some "hidden" callback by using table_.select(0) maybe? Possibly make it 
explicit by moving set_buttons_enabled() to the end of the method.

> +     const Widelands::Serial last_selection =
> +        table_.has_selection() ? table_.get_selected() : 
> Widelands::INVALID_INDEX;
> +     table_.clear();
> +     data_.clear();
> +     set_buttons_enabled();
> +     for (const auto& serial : iplayer().player().ships()) {
> +             Widelands::Ship* ship = serial_to_ship(serial);
> +             assert(iplayer().get_player() == ship->get_owner());
> +             const ShipInfo* info = create_shipinfo(*ship);
> +             if (info->status != ShipFilterStatus::kAll) {
> +                     if (ship_filter_ == ShipFilterStatus::kAll || 
> satisfies_filter(*info, ship_filter_)) {
> +                             data_.insert(std::make_pair(serial, 
> std::unique_ptr<const ShipInfo>(info)));
> +                             UI::Table<uintptr_t const>::EntryRecord& er =
> +                                table_.add(serial, serial == last_selection);
> +                             set_entry_record(&er, *info);
> +                     }
> +             }
> +     }
> +
> +     if (!table_.empty()) {
> +             table_.sort();
> +             if (!table_.has_selection()) {
> +                     table_.select(0);
> +             }
> +     }
> +}
> 
> === added file 'src/wui/seafaring_statistics_menu.h'
> --- src/wui/seafaring_statistics_menu.h       1970-01-01 00:00:00 +0000
> +++ src/wui/seafaring_statistics_menu.h       2018-04-16 15:39:30 +0000
> @@ -0,0 +1,159 @@
> +/*
> + * Copyright (C) 2017-2018 by the Widelands Development Team
> + *
> + * This program is free software; you can redistribute it and/or
> + * modify it under the terms of the GNU General Public License
> + * as published by the Free Software Foundation; either version 2
> + * of the License, or (at your option) any later version.
> + *
> + * This program is distributed in the hope that it will be useful,
> + * but WITHOUT ANY WARRANTY; without even the implied warranty of
> + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
> + * GNU General Public License for more details.
> + *
> + * You should have received a copy of the GNU General Public License
> + * along with this program; if not, write to the Free Software
> + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  
> 02110-1301, USA.
> + *
> + */
> +
> +#ifndef WL_WUI_SEAFARING_STATISTICS_MENU_H
> +#define WL_WUI_SEAFARING_STATISTICS_MENU_H
> +
> +#include <memory>
> +#include <unordered_map>
> +
> +#include "base/i18n.h"
> +#include "logic/map_objects/tribes/ship.h"
> +#include "notifications/notifications.h"
> +#include "ui_basic/box.h"
> +#include "ui_basic/button.h"
> +#include "ui_basic/table.h"
> +#include "ui_basic/unique_window.h"
> +
> +class InteractivePlayer;
> +
> +/// Shows a list of the ships owned by the interactive player with filtering 
> and navigation options.
> +struct SeafaringStatisticsMenu : public UI::UniqueWindow {
> +     SeafaringStatisticsMenu(InteractivePlayer&, 
> UI::UniqueWindow::Registry&);
> +
> +private:
> +     /// For identifying the columns in the table.
> +     enum Cols { ColName, ColStatus };
> +     /**
> +      * A list of ship status that we can filter for. This differs a bit 
> from that the Widelands::Ship

"from that" or "from what" ? Okay, probably you will be right... ;)

> +      * class has, so we define our own.
> +      * */
> +     enum class ShipFilterStatus {
> +             kIdle,
> +             kShipping,
> +             kExpeditionWaiting,
> +             kExpeditionScouting,
> +             kExpeditionPortspaceFound,
> +             kExpeditionColonizing,
> +             kAll
> +     };
> +
> +     /// Returns the localized strings that we use to display the 'status' 
> in the table.
> +     const std::string status_to_string(ShipFilterStatus status) const;
> +     /// Returns the icon that we use to represent the 'status' in the table 
> and on the filter
> +     /// buttons.
> +     const Image* status_to_image(ShipFilterStatus status) const;
> +
> +     /// The dataset that we need to display ships in the table.
> +     struct ShipInfo {
> +             ShipInfo(const std::string& init_name,
> +                      const ShipFilterStatus init_status,
> +                      const Widelands::Serial init_serial)
> +                : name(init_name), status(init_status), serial(init_serial) {
> +             }
> +             ShipInfo() : ShipInfo("", ShipFilterStatus::kAll, 0) {
> +             }
> +             ShipInfo(const ShipInfo& other) : ShipInfo(other.name, 
> other.status, other.serial) {
> +             }
> +             bool operator==(const ShipInfo& other) const {
> +                     return serial == other.serial;
> +             }
> +             bool operator<(const ShipInfo& other) const {
> +                     return serial < other.serial;
> +             }
> +
> +             const std::string name;
> +             ShipFilterStatus status;
> +             const Widelands::Serial serial;
> +     };
> +
> +     /// Creates our dataset from a 'ship'. Make sure to take ownership of 
> the result.

I might miss something, but it seems you are leaking the memory on both calls 
of this method. Return an object (its small after all) or a smart pointer?
Edit: Okay, ASAN is with me on this.

> +     const ShipInfo* create_shipinfo(const Widelands::Ship& ship) const;
> +     /// Uses the 'serial' to identify and get a ship from the game.
> +     Widelands::Ship* serial_to_ship(Widelands::Serial serial) const;
> +
> +     /// Convenience function to upcast the panel's parent.
> +     InteractivePlayer& iplayer() const;
> +
> +     /// A ship was selected, so enable the navigation buttons.
> +     void selected();
> +     /// A ship was double clicked. Centers main view on ship.
> +     void double_clicked();
> +     /// Handle filter and navigation hotkeys
> +     bool handle_key(bool down, SDL_Keysym code) override;
> +
> +     /// Enables the navigation buttons if a ship is selected, disables them 
> otherwise.
> +     void set_buttons_enabled();
> +     /// Center the mapview on the currently selected ship.
> +     void center_view();
> +     /// Follow the selected ship in a watch window.
> +     void watch_ship();
> +     /// Center the mapview on the currently selected ship and open its 
> window.
> +     void open_ship_window();
> +
> +     /**
> +      * Updates the status for the ship. If the ship is new and satisfies 
> the 'ship_filter_',
> +      * adds it to the table and data. If it doesn't satisfy the 
> 'ship_filter_', removes the ship
> +      * instead.
> +      * */
> +     void update_ship(const Widelands::Ship&);
> +     /// If we listed the ship, remove it from table and data.
> +     void remove_ship(Widelands::Serial serial);
> +     /// Sets the contents for the entry record in the table.
> +     void set_entry_record(UI::Table<uintptr_t>::EntryRecord*, const 
> ShipInfo& info);
> +     /// Updates the ship status display in the table.
> +     void update_entry_record(UI::Table<uintptr_t>::EntryRecord& er, const 
> ShipInfo&);
> +     /// Rebuilds data and table with all ships that satisfy the current 
> 'ship_filter_'.
> +     void fill_table();
> +
> +     /// Show only the ships that have the given status. Toggle the 
> appropriate buttons.
> +     void filter_ships(ShipFilterStatus status);
> +     /// Helper for filter_ships
> +     void toggle_filter_ships_button(UI::Button&, ShipFilterStatus);
> +     /// Helper for filter_ships
> +     void set_filter_ships_tooltips();
> +
> +     /// We group colonizing status with port space found. Anything else 
> needs to have an identical
> +     /// status.
> +     bool satisfies_filter(const ShipInfo& info, ShipFilterStatus filter);
> +
> +     const Image* colony_icon_;
> +     UI::Box main_box_;
> +     // Buttons for ship states
> +     UI::Box filter_box_;
> +     UI::Button idle_btn_;
> +     UI::Button waiting_btn_;
> +     UI::Button scouting_btn_;
> +     UI::Button portspace_btn_;
> +     UI::Button shipping_btn_;
> +     ShipFilterStatus ship_filter_;
> +     // Navigation buttons
> +     UI::Box navigation_box_;
> +     UI::Button watchbtn_;
> +     UI::Button openwindowbtn_;
> +     UI::Button centerviewbtn_;
> +
> +     // Data
> +     UI::Table<uintptr_t> table_;
> +     std::unordered_map<Widelands::Serial, std::unique_ptr<const ShipInfo>> 
> data_;
> +
> +     std::unique_ptr<Notifications::Subscriber<Widelands::NoteShip>> 
> shipnotes_subscriber_;
> +};
> +
> +#endif  // end of include guard: WL_WUI_SEAFARING_STATISTICS_MENU_H
> 
> === modified file 'src/wui/shipwindow.cc'
> --- src/wui/shipwindow.cc     2018-04-07 16:59:00 +0000
> +++ src/wui/shipwindow.cc     2018-04-16 15:39:30 +0000
> @@ -279,12 +301,20 @@
>  
>  /// Move the main view towards the current ship location
>  void ShipWindow::act_goto() {
> -     igbase_.map_view()->scroll_to_field(ship_.get_position(), 
> MapView::Transition::Smooth);

Hm, I have seen this code before... Guess the "branch dependency" works just 
fine. -_-

> +     Widelands::Ship* ship = ship_.get(igbase_.egbase());
> +     if (ship == nullptr) {
> +             return;
> +     }
> +     igbase_.map_view()->scroll_to_field(ship->get_position(), 
> MapView::Transition::Smooth);
>  }
>  
>  /// Move the main view towards the current destination of the ship
>  void ShipWindow::act_destination() {
> -     if (PortDock* destination = ship_.get_destination(igbase_.egbase())) {
> +     Widelands::Ship* ship = ship_.get(igbase_.egbase());
> +     if (ship == nullptr) {
> +             return;
> +     }
> +     if (PortDock* destination = ship->get_destination(igbase_.egbase())) {
>               igbase_.map_view()->scroll_to_field(
>                  destination->get_warehouse()->get_position(), 
> MapView::Transition::Smooth);
>       }


-- 
https://code.launchpad.net/~widelands-dev/widelands/bug-1358880-ship-statistics/+merge/343293
Your team Widelands Developers is subscribed to branch 
lp:~widelands-dev/widelands/ships_optr.

_______________________________________________
Mailing list: https://launchpad.net/~widelands-dev
Post to     : widelands-dev@lists.launchpad.net
Unsubscribe : https://launchpad.net/~widelands-dev
More help   : https://help.launchpad.net/ListHelp

Reply via email to