1 #include "FleetWnd.h"
2 
3 #include "CUIControls.h"
4 #include "SidePanel.h"
5 #include "IconTextBrowseWnd.h"
6 #include "MeterBrowseWnd.h"
7 #include "ClientUI.h"
8 #include "Sound.h"
9 #include "../util/i18n.h"
10 #include "../util/Logger.h"
11 #include "../util/Order.h"
12 #include "../util/OptionsDB.h"
13 #include "../util/ScopedTimer.h"
14 #include "../client/human/HumanClientApp.h"
15 #include "../universe/Fleet.h"
16 #include "../universe/Planet.h"
17 #include "../universe/Predicates.h"
18 #include "../universe/Ship.h"
19 #include "../universe/ShipDesign.h"
20 #include "../universe/System.h"
21 #include "../universe/Enums.h"
22 #include "../universe/Pathfinder.h"
23 #include "../network/Message.h"
24 #include "../Empire/Empire.h"
25 
26 #include <GG/Enum.h>
27 #include <GG/GUI.h>
28 #include <GG/Layout.h>
29 #include <GG/StaticGraphic.h>
30 
31 #include <boost/cast.hpp>
32 
33 #include <tuple>
34 #include <unordered_set>
35 
36 
37 FO_COMMON_API extern const int INVALID_DESIGN_ID;
38 
39 namespace {
DataPanelIconSpace()40     const GG::Pt        DataPanelIconSpace()
41     { return GG::Pt(GG::X(ClientUI::Pts()*3), GG::Y(ClientUI::Pts()*2.5)); }
42     GG::X               FLEET_WND_WIDTH = GG::X(360);
43     GG::Y               FLEET_WND_HEIGHT = GG::Y(400);
44 
45     // how should ship and fleet icons be scaled and/or positioned in the reserved space
DataPanelIconStyle()46     const GG::Flags<GG::GraphicStyle>   DataPanelIconStyle()
47     { return GG::GRAPHIC_CENTER | GG::GRAPHIC_VCENTER | GG::GRAPHIC_FITGRAPHIC | GG::GRAPHIC_PROPSCALE; }
48 
49     const GG::X         DATA_PANEL_TEXT_PAD = GG::X(4); // padding on the left and right of fleet/ship description
50     const int           DATA_PANEL_BORDER = 1;          // how thick should the border around ship or fleet panel be
51     const int           PAD = 4;
52     const std::string   SHIP_DROP_TYPE_STRING = "FleetWnd ShipRow";
53     const std::string   FLEET_DROP_TYPE_STRING = "FleetWnd FleetRow";
54     const std::string   FLEET_WND_NAME = "map.fleet";
55 
LabelHeight()56     GG::Y LabelHeight()
57     { return GG::Y(ClientUI::Pts()*3/2); }
58 
59     /** How big fleet and ship statistics icons should be relative to the
60       * current font size.  Icons shouldn't scale below what they are for the
61       * default, 12 pt, font size. */
StatIconSize()62     GG::Pt StatIconSize() {
63         const int font_size = std::max(ClientUI::Pts(), 12);
64         return GG::Pt(GG::X(font_size*11/3), GG::Y(font_size*4/3));
65     }
66 
ListRowHeight()67     GG::Y ListRowHeight()
68     { return std::max(DataPanelIconSpace().y, LabelHeight() + StatIconSize().y) + 2*DATA_PANEL_BORDER + PAD; }
69 
DamageIcon()70     std::shared_ptr<GG::Texture> DamageIcon()
71     { return ClientUI::GetTexture(ClientUI::ArtDir() / "icons" / "meter" / "damage.png", true); }
72 
TroopIcon()73     std::shared_ptr<GG::Texture> TroopIcon()
74     { return ClientUI::GetTexture(ClientUI::ArtDir() / "icons" / "meter" / "troops.png", true); }
75 
ColonyIcon()76     std::shared_ptr<GG::Texture> ColonyIcon()
77     { return ClientUI::GetTexture(ClientUI::ArtDir() / "icons" / "meter" / "colony.png", true); }
78 
FightersIcon()79     std::shared_ptr<GG::Texture> FightersIcon()
80     { return ClientUI::GetTexture(ClientUI::ArtDir() / "icons" / "meter" / "fighters.png", true); }
81 
FleetCountIcon()82     std::shared_ptr<GG::Texture> FleetCountIcon()
83     { return ClientUI::GetTexture(ClientUI::ArtDir() / "icons" / "sitrep" / "fleet_arrived.png"); }
84 
IndustryIcon()85     std::shared_ptr<GG::Texture> IndustryIcon()
86     { return ClientUI::GetTexture(ClientUI::ArtDir() / "icons" / "meter" / "industry.png"); }
87 
ResearchIcon()88     std::shared_ptr<GG::Texture> ResearchIcon()
89     { return ClientUI::GetTexture(ClientUI::ArtDir() / "icons" / "meter" / "research.png"); }
90 
TradeIcon()91     std::shared_ptr<GG::Texture> TradeIcon()
92     { return ClientUI::GetTexture(ClientUI::ArtDir() / "icons" / "meter" / "trade.png"); }
93 
FleetDestinationText(int fleet_id)94     std::string FleetDestinationText(int fleet_id) {
95         std::string retval = "";
96         auto fleet = Objects().get<Fleet>(fleet_id);
97         if (!fleet)
98             return retval;
99 
100         int client_empire_id = HumanClientApp::GetApp()->EmpireID();
101 
102         const auto dest_sys = Objects().get<System>(fleet->FinalDestinationID());
103         const auto cur_sys = Objects().get<System>(fleet->SystemID());
104         bool returning_to_current_system = (dest_sys == cur_sys) && !fleet->TravelRoute().empty();
105         if (dest_sys && (dest_sys != cur_sys || returning_to_current_system)) {
106             std::pair<int, int> eta = fleet->ETA();       // .first is turns to final destination.  .second is turns to next system on route
107 
108             // name of final destination
109             std::string dest_name = dest_sys->ApparentName(client_empire_id);
110             if (GetOptionsDB().Get<bool>("ui.name.id.shown"))
111                 dest_name += " (" + std::to_string(dest_sys->ID()) + ")";
112 
113             // next system on path
114             std::string next_eta_text;
115             if (eta.second == Fleet::ETA_UNKNOWN)
116                 next_eta_text = UserString("FW_FLEET_ETA_UNKNOWN");
117             else if (eta.second == Fleet::ETA_NEVER)
118                 next_eta_text = UserString("FW_FLEET_ETA_NEVER");
119             else if (eta.second == Fleet::ETA_OUT_OF_RANGE)
120                 next_eta_text = UserString("FW_FLEET_ETA_OUT_OF_RANGE");
121             else
122                 next_eta_text = std::to_string(eta.second);
123 
124             // final destination
125             std::string final_eta_text;
126             if (eta.first == Fleet::ETA_UNKNOWN)
127                 final_eta_text = UserString("FW_FLEET_ETA_UNKNOWN");
128             else if (eta.first == Fleet::ETA_NEVER)
129                 final_eta_text = UserString("FW_FLEET_ETA_NEVER");
130             else if (eta.first == Fleet::ETA_OUT_OF_RANGE)
131                 final_eta_text = UserString("FW_FLEET_ETA_OUT_OF_RANGE");
132             else
133                 final_eta_text = std::to_string(eta.first);
134 
135             if (ClientUI::GetClientUI()->GetMapWnd()->IsFleetExploring(fleet->ID()))
136                 retval = boost::io::str(FlexibleFormat(UserString("FW_FLEET_EXPLORING_TO")) %
137                                         dest_name % final_eta_text % next_eta_text);
138             else {
139                 // "FW_FLEET_MOVING_TO" userstring is currently truncated to drop ETA info
140                 // so as to fit better in a small fleet window
141                 std::string moving_key = GetOptionsDB().Get<bool>("ui.map.fleet.eta.shown")
142                     ? UserString("FW_FLEET_MOVING_TO_ETA")
143                     : UserString("FW_FLEET_MOVING_TO");
144                 retval = boost::io::str(FlexibleFormat(moving_key) %
145                                         dest_name % final_eta_text % next_eta_text);
146             }
147 
148         } else if (cur_sys) {
149             std::string cur_system_name = cur_sys->ApparentName(client_empire_id);
150             if (GetOptionsDB().Get<bool>("ui.name.id.shown")) {
151                 cur_system_name = cur_system_name + " (" + std::to_string(cur_sys->ID()) + ")";
152             }
153 
154             if (ClientUI::GetClientUI()->GetMapWnd()->IsFleetExploring(fleet->ID())) {
155                 if (fleet->Fuel() == fleet->MaxFuel())
156                     retval = boost::io::str(FlexibleFormat(UserString("FW_FLEET_EXPLORING_WAITING")));
157                 else
158                     retval = boost::io::str(FlexibleFormat(UserString("FW_FLEET_EXPLORING_REFUEL")));
159             } else {
160                 retval = boost::io::str(FlexibleFormat(UserString("FW_FLEET_HOLDING_AT")) % cur_system_name);
161             }
162         }
163         return retval;
164     }
165 
ClientPlayerIsModerator()166     bool ClientPlayerIsModerator()
167     { return HumanClientApp::GetApp()->GetClientType() == Networking::CLIENT_TYPE_HUMAN_MODERATOR; }
168 
ContainsArmedShips(const std::vector<int> & ship_ids)169     bool ContainsArmedShips(const std::vector<int>& ship_ids) {
170         for (const auto& ship : Objects().find<Ship>(ship_ids)) {
171             if (!ship)
172                 continue;
173             if (ship->IsArmed() || ship->HasFighters())
174                 return true;
175         }
176         return false;
177     }
178 
AggressionForFleet(NewFleetAggression aggression_mode,const std::vector<int> & ship_ids)179     bool AggressionForFleet(NewFleetAggression aggression_mode, const std::vector<int>& ship_ids) {
180         if (aggression_mode == FLEET_AGGRESSIVE)
181             return true;
182         if (aggression_mode == FLEET_PASSIVE)
183             return false;
184         // auto aggression; examine ships to see if any are armed...
185         return ContainsArmedShips(ship_ids);
186     }
187 
CreateNewFleetFromShips(const std::vector<int> & ship_ids,NewFleetAggression aggression)188     void CreateNewFleetFromShips(const std::vector<int>& ship_ids,
189                                  NewFleetAggression aggression)
190     {
191         if (ClientPlayerIsModerator())
192             return; // todo: handle moderator actions for this...
193         int client_empire_id = HumanClientApp::GetApp()->EmpireID();
194         if (client_empire_id == ALL_EMPIRES)
195             return;
196 
197         // TODO: Should probably have the sound effect occur exactly once instead
198         //       of not at all.
199         Sound::TempUISoundDisabler sound_disabler;
200 
201         std::set<int> original_fleet_ids;           // ids of fleets from which ships were taken
202 
203         // validate ships in each group, and generate fleet names for those ships
204         std::vector<std::shared_ptr<const Ship>> ships = Objects().find<const Ship>(ship_ids);
205         if (ships.empty())
206             return;
207 
208         std::shared_ptr<const Ship> first_ship = *ships.begin();
209         auto system = Objects().get<System>(first_ship->SystemID());
210         if (!system)
211             return;
212 
213         // validate that ships are in the same system and all owned by this
214         // client's empire.
215         // also record the fleets from which ships are taken
216         for (auto& ship : ships) {
217              if (ship->SystemID() != system->ID()) {
218                  ErrorLogger() << "CreateNewFleetFromShips passed ships with inconsistent system ids";
219                  continue;
220              }
221              if (!ship->OwnedBy(client_empire_id)) {
222                  ErrorLogger() << "CreateNewFleetFromShips passed ships not owned by this client's empire";
223                  return;
224              }
225 
226              original_fleet_ids.insert(ship->FleetID());
227         }
228 
229         // create new fleet with ships
230         HumanClientApp::GetApp()->Orders().IssueOrder(
231             std::make_shared<NewFleetOrder>(client_empire_id, "", ship_ids, AggressionForFleet(aggression, ship_ids)));
232     }
233 
CreateNewFleetFromShipsWithDesign(const std::set<int> & ship_ids,int design_id,NewFleetAggression aggression)234     void CreateNewFleetFromShipsWithDesign(const std::set<int>& ship_ids,
235                                            int design_id,
236                                            NewFleetAggression aggression)
237     {
238         DebugLogger() << "CreateNewFleetFromShipsWithDesign with " << ship_ids.size()
239                                << " ship ids and design id: " << design_id;
240         if (ship_ids.empty() || design_id == INVALID_DESIGN_ID)
241             return;
242         int client_empire_id = HumanClientApp::GetApp()->EmpireID();
243         if (client_empire_id == ALL_EMPIRES && !ClientPlayerIsModerator())
244             return;
245 
246         // select ships with the requested design id
247         std::vector<int> ships_of_design_ids;
248         ships_of_design_ids.reserve(ship_ids.size());
249         for (auto& ship : Objects().find<Ship>(ship_ids)) {
250             if (ship->DesignID() == design_id)
251                 ships_of_design_ids.push_back(ship->ID());
252         }
253 
254         CreateNewFleetFromShips(ships_of_design_ids, aggression);
255     }
256 
CreateNewFleetsFromShipsForEachDesign(const std::set<int> & ship_ids,NewFleetAggression aggression)257     void CreateNewFleetsFromShipsForEachDesign(const std::set<int>& ship_ids,
258                                                NewFleetAggression aggression)
259     {
260         DebugLogger() << "CreateNewFleetsFromShipsForEachDesign with "
261                                << ship_ids.size() << " ship ids";
262         if (ship_ids.empty())
263             return;
264         int client_empire_id = HumanClientApp::GetApp()->EmpireID();
265         if (client_empire_id == ALL_EMPIRES && !ClientPlayerIsModerator())
266             return;
267 
268         // sort ships by ID into container, indexed by design id
269         std::map<int, std::vector<int>> designs_ship_ids;
270         for (auto& ship : Objects().find<Ship>(ship_ids))
271             designs_ship_ids[ship->DesignID()].push_back(ship->ID());
272 
273         // note that this will cause a UI update for each call to CreateNewFleetFromShips
274         // we can re-evaluate this code if it presents a noticable performance problem
275         for (const auto& entry : designs_ship_ids)
276         { CreateNewFleetFromShips(entry.second, aggression); }
277     }
278 
MergeFleetsIntoFleet(int fleet_id)279     void MergeFleetsIntoFleet(int fleet_id) {
280         if (ClientPlayerIsModerator())
281             return; // todo: handle moderator actions for this...
282         int client_empire_id = HumanClientApp::GetApp()->EmpireID();
283         if (client_empire_id == ALL_EMPIRES)
284             return;
285 
286         auto target_fleet = Objects().get<Fleet>(fleet_id);
287         if (!target_fleet) {
288             ErrorLogger() << "MergeFleetsIntoFleet couldn't get a fleet with id " << fleet_id;
289             return;
290         }
291 
292         auto system = Objects().get<System>(target_fleet->SystemID());
293         if (!system) {
294             ErrorLogger() << "MergeFleetsIntoFleet couldn't get system for the target fleet";
295             return;
296         }
297 
298         Sound::TempUISoundDisabler sound_disabler;
299 
300         // filter fleets in system to select just those owned by this client's
301         // empire, and collect their ship ids
302         std::vector<std::shared_ptr<Fleet>> all_system_fleets(Objects().find<Fleet>(system->FleetIDs()));
303         std::vector<int> empire_system_fleet_ids;
304         empire_system_fleet_ids.reserve(all_system_fleets.size());
305         std::vector<std::shared_ptr<Fleet>> empire_system_fleets;
306         empire_system_fleets.reserve(all_system_fleets.size());
307         std::vector<int> empire_system_ship_ids;
308 
309         for (auto& fleet : all_system_fleets) {
310             if (!fleet->OwnedBy(client_empire_id))
311                 continue;
312             if (fleet->ID() == target_fleet->ID() || fleet->ID() == INVALID_OBJECT_ID)
313                 continue;   // no need to do things to target fleet's contents
314 
315             empire_system_fleet_ids.push_back(fleet->ID());
316             empire_system_fleets.push_back(fleet);
317 
318             const std::set<int>& fleet_ships = fleet->ShipIDs();
319             std::copy(fleet_ships.begin(), fleet_ships.end(), std::back_inserter(empire_system_ship_ids));
320         }
321 
322 
323         // order ships moved into target fleet
324         HumanClientApp::GetApp()->Orders().IssueOrder(
325             std::make_shared<FleetTransferOrder>(client_empire_id, target_fleet->ID(), empire_system_ship_ids));
326     }
327 
328    /** Returns map from object ID to issued colonize orders affecting it. */
PendingScrapOrders()329     std::map<int, int> PendingScrapOrders() {
330         std::map<int, int> retval;
331         const ClientApp* app = ClientApp::GetApp();
332         if (!app)
333             return retval;
334         for (const auto& id_and_order : app->Orders()) {
335             if (std::shared_ptr<ScrapOrder> order = std::dynamic_pointer_cast<ScrapOrder>(id_and_order.second)) {
336                 retval[order->ObjectID()] = id_and_order.first;
337             }
338         }
339         return retval;
340     }
341 
AddOptions(OptionsDB & db)342     void AddOptions(OptionsDB& db) {
343         db.Add("ui.fleet.aggression", UserStringNop("OPTIONS_DB_FLEET_WND_AGGRESSION"), INVALID_FLEET_AGGRESSION);
344         db.Add("ui.fleet.scanline.color", UserStringNop("OPTIONS_DB_UI_FLEET_WND_SCANLINE_CLR"), GG::Clr(24, 24, 24, 192));
345     }
346     bool temp_bool = RegisterOptions(&AddOptions);
347 
NewFleetsAggressiveOptionSetting()348     NewFleetAggression NewFleetsAggressiveOptionSetting()
349     { return GetOptionsDB().Get<NewFleetAggression>("ui.fleet.aggression"); }
350 
SetNewFleetAggressiveOptionSetting(NewFleetAggression aggression)351     void SetNewFleetAggressiveOptionSetting(NewFleetAggression aggression)
352     { GetOptionsDB().Set<NewFleetAggression>("ui.fleet.aggression", aggression); }
353 
FleetAggressiveIcon()354     std::shared_ptr<GG::Texture> FleetAggressiveIcon()
355     { return ClientUI::GetTexture(ClientUI::ArtDir() / "icons" / "buttons" / "fleet_aggressive.png"); }
FleetAggressiveMouseoverIcon()356     std::shared_ptr<GG::Texture> FleetAggressiveMouseoverIcon()
357     { return ClientUI::GetTexture(ClientUI::ArtDir() / "icons" / "buttons" / "fleet_aggressive_mouseover.png"); }
FleetPassiveIcon()358     std::shared_ptr<GG::Texture> FleetPassiveIcon()
359     { return ClientUI::GetTexture(ClientUI::ArtDir() / "icons" / "buttons" / "fleet_passive.png"); }
FleetPassiveMouseoverIcon()360     std::shared_ptr<GG::Texture> FleetPassiveMouseoverIcon()
361     { return ClientUI::GetTexture(ClientUI::ArtDir() / "icons" / "buttons" / "fleet_passive_mouseover.png"); }
FleetAutoIcon()362     std::shared_ptr<GG::Texture> FleetAutoIcon()
363     { return ClientUI::GetTexture(ClientUI::ArtDir() / "icons" / "buttons" / "fleet_auto.png"); }
FleetAutoMouseoverIcon()364     std::shared_ptr<GG::Texture> FleetAutoMouseoverIcon()
365     { return ClientUI::GetTexture(ClientUI::ArtDir() / "icons" / "buttons" / "fleet_auto_mouseover.png"); }
366 }
367 
368 ////////////////////////////////////////////////
369 // FleetUIManager                             //
370 ////////////////////////////////////////////////
FleetUIManager()371 FleetUIManager::FleetUIManager() :
372     m_order_issuing_enabled(true),
373     m_active_fleet_wnd()
374 {}
375 
begin() const376 FleetUIManager::iterator FleetUIManager::begin() const
377 { return m_fleet_wnds.begin(); }
378 
empty() const379 bool FleetUIManager::empty() const
380 { return m_fleet_wnds.empty(); }
381 
end() const382 FleetUIManager::iterator FleetUIManager::end() const
383 { return m_fleet_wnds.end(); }
384 
ActiveFleetWnd() const385 FleetWnd* FleetUIManager::ActiveFleetWnd() const
386 { return GG::LockAndResetIfExpired(m_active_fleet_wnd).get(); }
387 
WndForFleetID(int fleet_id) const388 std::shared_ptr<FleetWnd> FleetUIManager::WndForFleetID(int fleet_id) const {
389     std::shared_ptr<FleetWnd> retval;
390     GG::ProcessThenRemoveExpiredPtrs(
391         m_fleet_wnds,
392         [&retval, fleet_id](std::shared_ptr<FleetWnd>& wnd)
393         {
394             if (!retval && wnd->ContainsFleet(fleet_id))
395                 retval = wnd;
396         });
397     return retval;
398 }
399 
WndForFleetIDs(const std::vector<int> & fleet_ids_) const400 std::shared_ptr<FleetWnd> FleetUIManager::WndForFleetIDs(const std::vector<int>& fleet_ids_) const {
401     std::unordered_set<int> fleet_ids;
402     for (const auto id : fleet_ids_)
403         fleet_ids.insert(id);
404     std::shared_ptr<FleetWnd> retval;
405     GG::ProcessThenRemoveExpiredPtrs(
406         m_fleet_wnds,
407         [&retval, fleet_ids](std::shared_ptr<FleetWnd>& wnd)
408         {
409             if (!retval && wnd->ContainsFleets(fleet_ids))
410                 retval = wnd;
411         });
412     return retval;
413 }
414 
SelectedShipID() const415 int FleetUIManager::SelectedShipID() const {
416     const auto&& active_wnd = GG::LockAndResetIfExpired(m_active_fleet_wnd);
417     if (!active_wnd)
418         return INVALID_OBJECT_ID;
419 
420     std::set<int> selected_ship_ids = active_wnd->SelectedShipIDs();
421     if (selected_ship_ids.size() != 1)
422         return INVALID_OBJECT_ID;
423 
424     return *selected_ship_ids.begin();
425 }
426 
SelectedShipIDs() const427 std::set<int> FleetUIManager::SelectedShipIDs() const {
428     const auto&& active_wnd = GG::LockAndResetIfExpired(m_active_fleet_wnd);
429     if (!active_wnd)
430         return std::set<int>();
431     return active_wnd->SelectedShipIDs();
432 }
433 
NewFleetWnd(const std::vector<int> & fleet_ids,double allowed_bounding_box_leeway,int selected_fleet_id,GG::Flags<GG::WndFlag> flags)434 std::shared_ptr<FleetWnd> FleetUIManager::NewFleetWnd(
435     const std::vector<int>& fleet_ids,
436     double allowed_bounding_box_leeway /*= 0*/,
437     int selected_fleet_id/* = INVALID_OBJECT_ID*/,
438     GG::Flags<GG::WndFlag> flags/* = GG::INTERACTIVE | GG::DRAGABLE | GG::ONTOP | CLOSABLE | GG::RESIZABLE*/)
439 {
440     std::string config_name = "";
441     if (!GetOptionsDB().Get<bool>("ui.fleet.multiple.enabled")) {
442         CloseAll();
443         // Only write to OptionsDB if in single fleet window mode.
444         config_name = FLEET_WND_NAME;
445     }
446     auto retval = GG::Wnd::Create<FleetWnd>(fleet_ids, m_order_issuing_enabled,
447                                             allowed_bounding_box_leeway,
448                                             selected_fleet_id, flags, config_name);
449 
450 #if BOOST_VERSION >= 106000
451     using boost::placeholders::_1;
452 #endif
453 
454     m_fleet_wnds.insert(std::weak_ptr<FleetWnd>(retval));
455     retval->ClosingSignal.connect(boost::bind(&FleetUIManager::FleetWndClosing, this, _1));
456     retval->ClickedSignal.connect(boost::bind(&FleetUIManager::FleetWndClicked, this, _1));
457     retval->FleetRightClickedSignal.connect(FleetRightClickedSignal);
458     retval->ShipRightClickedSignal.connect(ShipRightClickedSignal);
459 
460     GG::GUI::GetGUI()->Register(retval);
461 
462     return retval;
463 }
464 
CullEmptyWnds()465 void FleetUIManager::CullEmptyWnds() {
466     // scan through FleetWnds, deleting those that have no fleets
467     GG::ProcessThenRemoveExpiredPtrs(m_fleet_wnds,
468                                  [](std::shared_ptr<FleetWnd>& wnd)
469                                  {
470                                      if (wnd->FleetIDs().empty())
471                                          wnd->CloseClicked();
472                                  });
473 }
474 
SetActiveFleetWnd(std::shared_ptr<FleetWnd> fleet_wnd)475 void FleetUIManager::SetActiveFleetWnd(std::shared_ptr<FleetWnd> fleet_wnd) {
476     const auto&& active_wnd = GG::LockAndResetIfExpired(m_active_fleet_wnd);
477     if (fleet_wnd == active_wnd)
478         return;
479 
480     // disconnect old active FleetWnd signals
481     if (active_wnd) {
482         for (boost::signals2::connection& con : m_active_fleet_wnd_signals)
483             con.disconnect();
484         m_active_fleet_wnd_signals.clear();
485     }
486 
487     // set new active FleetWnd
488     m_active_fleet_wnd = fleet_wnd;
489 
490     // connect new active FleetWnd selection changed signal
491     m_active_fleet_wnd_signals.push_back(fleet_wnd->SelectedFleetsChangedSignal.connect(
492         ActiveFleetWndSelectedFleetsChangedSignal));
493     m_active_fleet_wnd_signals.push_back(fleet_wnd->SelectedShipsChangedSignal.connect(
494         ActiveFleetWndSelectedShipsChangedSignal));
495 
496     ActiveFleetWndChangedSignal();
497 }
498 
CloseAll()499 bool FleetUIManager::CloseAll() {
500     bool retval = false;
501 
502     // closing a fleet window removes it from m_fleet_wnds
503     GG::ProcessThenRemoveExpiredPtrs(m_fleet_wnds,
504                                  [&retval](std::shared_ptr<FleetWnd>& wnd) {
505                                      retval = true;
506                                      wnd->CloseClicked();
507                                  });
508 
509     m_active_fleet_wnd.reset();
510     ActiveFleetWndChangedSignal();
511 
512     // send order changes could be made on fleets
513     HumanClientApp::GetApp()->SendPartialOrders();
514 
515     return retval;
516 }
517 
RefreshAll()518 void FleetUIManager::RefreshAll() {
519     GG::ProcessThenRemoveExpiredPtrs(m_fleet_wnds,
520                                  [](std::shared_ptr<FleetWnd>& wnd)
521                                  { wnd->Refresh(); });
522 }
523 
GetFleetUIManager()524 FleetUIManager& FleetUIManager::GetFleetUIManager() {
525     static FleetUIManager retval;
526     return retval;
527 }
528 
FleetWndClosing(FleetWnd * fleet_wnd)529 void FleetUIManager::FleetWndClosing(FleetWnd* fleet_wnd) {
530     if (m_active_fleet_wnd.expired()) {
531         m_active_fleet_wnd.reset();
532         ActiveFleetWndChangedSignal();
533     }
534 
535     // send order changes could be made on this fleet
536     auto app = HumanClientApp::GetApp();
537     if (app)
538         app->SendPartialOrders();
539 }
540 
FleetWndClicked(std::shared_ptr<FleetWnd> fleet_wnd)541 void FleetUIManager::FleetWndClicked(std::shared_ptr<FleetWnd> fleet_wnd) {
542     if (fleet_wnd == GG::LockAndResetIfExpired(m_active_fleet_wnd))
543         return;
544     SetActiveFleetWnd(std::forward<std::shared_ptr<FleetWnd>>(fleet_wnd));
545 }
546 
EnableOrderIssuing(bool enable)547 void FleetUIManager::EnableOrderIssuing(bool enable/* = true*/) {
548     m_order_issuing_enabled = enable;
549     GG::ProcessThenRemoveExpiredPtrs(m_fleet_wnds,
550                                  [&enable](std::shared_ptr<FleetWnd>& wnd)
551                                  { wnd->EnableOrderIssuing(enable); });
552 }
553 
554 namespace {
ValidShipTransfer(std::shared_ptr<const Ship> ship,std::shared_ptr<const Fleet> new_fleet)555     bool ValidShipTransfer(std::shared_ptr<const Ship> ship, std::shared_ptr<const Fleet> new_fleet) {
556         if (!ship || !new_fleet)
557             return false;   // can't transfer no ship or to no fleet
558 
559         std::shared_ptr<const Fleet> current_fleet = Objects().get<Fleet>(ship->FleetID());
560         if (current_fleet && current_fleet->ID() == new_fleet->ID())
561             return false;   // can't transfer a fleet to a fleet it already is in
562 
563         if (ship->X() != new_fleet->X() || ship->Y() != new_fleet->Y())
564             return false;   // can't move fleets during a transfer.  can only transfer fleet at same location as ship
565 
566         if (new_fleet->SystemID() == INVALID_OBJECT_ID)
567             return false;   // not in a system
568 
569         if (ship->SystemID() != new_fleet->SystemID())
570             return false;   // fleets need to be in same system.  probably redundant with checking position
571 
572         if (ship->Unowned() || new_fleet->Unowned())
573             return false;   // need to own a ship to transfer it...
574 
575         if (ship->Owner() != new_fleet->Owner())
576             return false;   // need to have same owner.
577 
578         // all tests passed.  can transfer
579         return true;
580     }
581 
ValidFleetMerge(std::shared_ptr<const Fleet> fleet,std::shared_ptr<const Fleet> target_fleet)582     bool ValidFleetMerge(std::shared_ptr<const Fleet> fleet, std::shared_ptr<const Fleet> target_fleet) {
583         if (!fleet || !target_fleet)
584             return false;   // missing objects
585 
586         if (fleet->SystemID() != target_fleet->SystemID())
587             return false;   // at different systems
588 
589         if (fleet->SystemID() == INVALID_OBJECT_ID)
590             return false;   // not in a system
591 
592         if (fleet->X() != target_fleet->X() || fleet->Y() != target_fleet->Y())
593             return false;   // at different locations.
594 
595         if (fleet->Unowned() || target_fleet->Unowned())
596             return false;
597 
598         if (fleet->Owner() != target_fleet->Owner())
599             return false;   // different owners
600 
601         // all tests passed; can merge fleets
602         return true;
603     }
604 
605     ////////////////////////////////////////////////
606     // ShipDataPanel
607     ////////////////////////////////////////////////
608     /** Shows info about a single ship. */
609     class ShipDataPanel : public GG::Control {
610     public:
611         /** \name Structors */ //@{
612         ShipDataPanel(GG::X w, GG::Y h, int ship_id);
613         ~ShipDataPanel();
614         //@}
615 
616         //! \name Accessors //@{
617         /** Excludes border from the client area. */
618         GG::Pt ClientUpperLeft() const override;
619         /** Excludes border from the client area. */
620         GG::Pt ClientLowerRight() const override;
621         //@}
622 
623         //! \name Mutators //@{
624         /** Renders black panel background, border with color depending on the
625          *current state and a background for the ship's name text. */
626         void Render() override;
627         void PreRender() override;
628 
629         void SizeMove(const GG::Pt& ul, const GG::Pt& lr) override;
630 
631         void Select(bool b);
632 
633         /** Indicate ship data has changed and needs refresh. */
634         void RequireRefresh();
635         //@}
636 
637     private:
638         double StatValue(MeterType stat_name) const;
639 
640         void SetShipIcon();
641         void Refresh();
642         void DoLayout();
643         void Init();
644 
645         bool                                m_initialized = false;
646         bool                                m_needs_refresh = true;
647         int                                 m_ship_id = INVALID_OBJECT_ID;
648         std::shared_ptr<GG::StaticGraphic>  m_ship_icon;
649         std::vector<std::shared_ptr<GG::StaticGraphic>>
650                                             m_ship_icon_overlays;   /// An overlays for orders like scrap, colonize, invade, bombard destroy etc.
651         std::shared_ptr<ScanlineControl>    m_scanline_control;
652         std::shared_ptr<GG::Label>          m_ship_name_text;
653         std::shared_ptr<GG::Label>          m_design_name_text;
654         std::vector<std::pair<MeterType, std::shared_ptr<StatisticIcon>>>
655                                             m_stat_icons;           /// statistic icons and associated meter types
656         bool                                m_selected = false;
657         boost::signals2::connection         m_ship_connection;
658         boost::signals2::connection         m_fleet_connection;
659     };
660 
ShipDataPanel(GG::X w,GG::Y h,int ship_id)661     ShipDataPanel::ShipDataPanel(GG::X w, GG::Y h, int ship_id) :
662         Control(GG::X0, GG::Y0, w, h, GG::NO_WND_FLAGS),
663         m_ship_id(ship_id)
664     {
665         SetChildClippingMode(ClipToClient);
666         RequireRefresh();
667     }
668 
~ShipDataPanel()669     ShipDataPanel::~ShipDataPanel() {
670         m_ship_connection.disconnect();
671         m_fleet_connection.disconnect();
672     }
673 
ClientUpperLeft() const674     GG::Pt ShipDataPanel::ClientUpperLeft() const
675     { return UpperLeft() + GG::Pt(GG::X(DATA_PANEL_BORDER), GG::Y(DATA_PANEL_BORDER)); }
676 
ClientLowerRight() const677     GG::Pt ShipDataPanel::ClientLowerRight() const
678     { return LowerRight() - GG::Pt(GG::X(DATA_PANEL_BORDER), GG::Y(DATA_PANEL_BORDER));  }
679 
RequireRefresh()680     void ShipDataPanel::RequireRefresh() {
681         m_needs_refresh = true;
682         RequirePreRender();
683     }
684 
PreRender()685     void ShipDataPanel::PreRender() {
686         if (!m_initialized)
687             Init();
688 
689         GG::Control::PreRender();
690         if (m_needs_refresh)
691             Refresh();
692 
693         DoLayout();
694     }
695 
Render()696     void ShipDataPanel::Render() {
697         // main background position and colour
698         const GG::Clr& background_colour = ClientUI::WndColor();
699         const GG::Pt ul = UpperLeft(), lr = LowerRight(), cul = ClientUpperLeft();
700 
701         // title background colour and position
702         const GG::Clr& unselected_colour = ClientUI::WndOuterBorderColor();
703         const GG::Clr& selected_colour = ClientUI::WndInnerBorderColor();
704         GG::Clr border_colour = m_selected ? selected_colour : unselected_colour;
705         if (Disabled())
706             border_colour = DisabledColor(border_colour);
707         const GG::Pt text_ul = cul + GG::Pt(DataPanelIconSpace().x, GG::Y0);
708         const GG::Pt text_lr = cul + GG::Pt(ClientWidth(),           LabelHeight());
709 
710         // render
711         GG::FlatRectangle(ul,       lr,         background_colour,  border_colour, DATA_PANEL_BORDER);  // background and border
712         GG::FlatRectangle(text_ul,  text_lr,    border_colour,      GG::CLR_ZERO,  0);                  // title background box
713     }
714 
Select(bool b)715     void ShipDataPanel::Select(bool b) {
716         if (m_selected == b)
717             return;
718         m_selected = b;
719 
720         const GG::Clr& unselected_text_color = ClientUI::TextColor();
721         const GG::Clr& selected_text_color = GG::CLR_BLACK;
722 
723         GG::Clr text_color_to_use = m_selected ? selected_text_color : unselected_text_color;
724 
725         if (Disabled())
726             text_color_to_use = DisabledColor(text_color_to_use);
727 
728         if (m_ship_name_text)
729             m_ship_name_text->SetTextColor(text_color_to_use);
730         if (m_design_name_text)
731             m_design_name_text->SetTextColor(text_color_to_use);
732     }
733 
SizeMove(const GG::Pt & ul,const GG::Pt & lr)734     void ShipDataPanel::SizeMove(const GG::Pt& ul, const GG::Pt& lr) {
735         const GG::Pt old_size = Size();
736         GG::Control::SizeMove(ul, lr);
737         if (old_size != Size())
738             RequirePreRender();
739     }
740 
SetShipIcon()741     void ShipDataPanel::SetShipIcon() {
742         DetachChildAndReset(m_ship_icon);
743         for (auto& overlay : m_ship_icon_overlays)
744             DetachChildAndReset(overlay);
745         m_ship_icon_overlays.clear();
746         DetachChildAndReset(m_scanline_control);
747 
748         auto ship = Objects().get<Ship>(m_ship_id);
749         if (!ship)
750             return;
751 
752         std::shared_ptr<GG::Texture> icon;
753 
754         if (const ShipDesign* design = ship->Design())
755             icon = ClientUI::ShipDesignIcon(design->ID());
756         else
757             icon = ClientUI::ShipDesignIcon(INVALID_OBJECT_ID);  // default icon
758 
759         m_ship_icon = GG::Wnd::Create<GG::StaticGraphic>(icon, DataPanelIconStyle());
760         m_ship_icon->Resize(GG::Pt(DataPanelIconSpace().x, ClientHeight()));
761         AttachChild(m_ship_icon);
762 
763         // Add the overlay
764         auto add_overlay = [this](const std::string& file) {
765             if (auto overlay_texture = ClientUI::GetTexture(ClientUI::ArtDir() / "misc" / file, true)) {
766                 auto overlay = GG::Wnd::Create<GG::StaticGraphic>(overlay_texture, DataPanelIconStyle());
767                 overlay->Resize(GG::Pt(DataPanelIconSpace().x, ClientHeight()));
768                 AttachChild(overlay);
769                 m_ship_icon_overlays.push_back(overlay);
770             }
771         };
772 
773         if (ship->OrderedScrapped())
774             add_overlay("scrapped.png");
775         if (ship->OrderedColonizePlanet() != INVALID_OBJECT_ID)
776             add_overlay("colonizing.png");
777         if (ship->OrderedInvadePlanet() != INVALID_OBJECT_ID)
778             add_overlay("invading.png");
779         if (ship->OrderedBombardPlanet() != INVALID_OBJECT_ID)
780             add_overlay("bombarding.png");
781 
782         int client_empire_id = HumanClientApp::GetApp()->EmpireID();
783         if ((ship->GetVisibility(client_empire_id) < VIS_BASIC_VISIBILITY)
784             && GetOptionsDB().Get<bool>("ui.map.scanlines.shown"))
785         {
786             m_scanline_control = GG::Wnd::Create<ScanlineControl>(GG::X0, GG::Y0, m_ship_icon->Width(), m_ship_icon->Height(), true,
787                                                                   GetOptionsDB().Get<GG::Clr>("ui.fleet.scanline.color"));
788             AttachChild(m_scanline_control);
789         }
790     }
791 
Refresh()792     void ShipDataPanel::Refresh() {
793         m_needs_refresh = false;
794 
795         SetShipIcon();
796 
797         auto ship = Objects().get<Ship>(m_ship_id);
798         if (!ship) {
799             // blank text and delete icons
800             m_ship_name_text->SetText("");
801             DetachChildAndReset(m_design_name_text);
802             for (auto& type_and_icon: m_stat_icons)
803                 DetachChild(type_and_icon.second);
804             m_stat_icons.clear();
805             return;
806         }
807 
808 
809         int empire_id = HumanClientApp::GetApp()->EmpireID();
810 
811 
812         // name and design name update
813         const std::string& ship_name = ship->PublicName(empire_id);
814         std::string id_name_part;
815         if (GetOptionsDB().Get<bool>("ui.name.id.shown")) {
816             id_name_part = " (" + std::to_string(m_ship_id) + ")";
817         }
818         if (!ship->Unowned() && ship_name == UserString("FW_FOREIGN_SHIP")) {
819             const Empire* ship_owner_empire = GetEmpire(ship->Owner());
820             const std::string& owner_name = (ship_owner_empire ? ship_owner_empire->Name() : UserString("FW_FOREIGN"));
821             m_ship_name_text->SetText(boost::io::str(FlexibleFormat(UserString("FW_EMPIRE_SHIP")) % owner_name) + id_name_part);
822         } else {
823             m_ship_name_text->SetText(ship_name + id_name_part);
824         }
825 
826         if (m_design_name_text) {
827             std::string design_name = UserString("FW_UNKNOWN_DESIGN_NAME");
828             if (const ShipDesign* design = ship->Design())
829                 design_name = design->Name();
830             const std::string& species_name = ship->SpeciesName();
831             if (!species_name.empty()) {
832                 m_design_name_text->SetText(boost::io::str(FlexibleFormat(UserString("FW_SPECIES_SHIP_DESIGN_LABEL")) %
833                                                            design_name %
834                                                            UserString(species_name)));
835             } else {
836                 m_design_name_text->SetText(design_name);
837             }
838         }
839 
840         // update stat icon values and browse wnds
841         for (auto& entry : m_stat_icons) {
842             entry.second->SetValue(StatValue(entry.first));
843 
844             entry.second->ClearBrowseInfoWnd();
845             if (entry.first == METER_CAPACITY) {  // refers to damage
846                 entry.second->SetBrowseInfoWnd(GG::Wnd::Create<ShipDamageBrowseWnd>(
847                                                    m_ship_id, entry.first));
848 
849             } else if (entry.first == METER_TROOPS) {
850                 entry.second->SetBrowseInfoWnd(GG::Wnd::Create<IconTextBrowseWnd>(
851                                                    TroopIcon(), UserString("SHIP_TROOPS_TITLE"),
852                                                    UserString("SHIP_TROOPS_STAT")));
853 
854             } else if (entry.first == METER_SECONDARY_STAT) {
855                 entry.second->SetBrowseInfoWnd(GG::Wnd::Create<ShipFightersBrowseWnd>(
856                                                    m_ship_id, entry.first));
857                 entry.second->SetBrowseModeTime(GetOptionsDB().Get<int>("ui.tooltip.extended.delay"), 1);
858                 entry.second->SetBrowseInfoWnd(GG::Wnd::Create<ShipFightersBrowseWnd>(
859                                                    m_ship_id, entry.first, true), 1);
860 
861             } else if (entry.first == METER_POPULATION) {
862                 entry.second->SetBrowseInfoWnd(GG::Wnd::Create<IconTextBrowseWnd>(
863                                                    ColonyIcon(), UserString("SHIP_COLONY_TITLE"),
864                                                    UserString("SHIP_COLONY_STAT")));
865 
866             } else {
867                 entry.second->SetBrowseInfoWnd(GG::Wnd::Create<MeterBrowseWnd>(
868                                                    m_ship_id, entry.first, AssociatedMeterType(entry.first)));
869             }
870         }
871     }
872 
StatValue(MeterType stat_name) const873     double ShipDataPanel::StatValue(MeterType stat_name) const {
874         if (auto ship = Objects().get<Ship>(m_ship_id)) {
875             if (stat_name == METER_CAPACITY)
876                 return ship->TotalWeaponsDamage(0.0f, false);
877             else if (stat_name == METER_TROOPS)
878                 return ship->TroopCapacity();
879             else if (stat_name == METER_SECONDARY_STAT)
880                 return ship->FighterCount();
881             else if (stat_name == METER_POPULATION)
882                 return ship->ColonyCapacity();
883             else if (ship->UniverseObject::GetMeter(stat_name))
884                 return ship->GetMeter(stat_name)->Initial();
885 
886             ErrorLogger() << "ShipDataPanel::StatValue couldn't get stat of name: " << stat_name;
887         }
888         return 0.0;
889     }
890 
DoLayout()891     void ShipDataPanel::DoLayout() {
892         // resize ship and scrap indicator icons, they can fit and position themselves in the space provided
893         // client height should never be less than the height of the space resereved for the icon
894         if (m_ship_icon)
895             m_ship_icon->Resize(GG::Pt(DataPanelIconSpace().x, ClientHeight()));
896         for (auto& overlay :m_ship_icon_overlays)
897             overlay->Resize(GG::Pt(DataPanelIconSpace().x, ClientHeight()));
898 
899         // position ship name text at the top to the right of icons
900         const GG::Pt name_ul = GG::Pt(DataPanelIconSpace().x + DATA_PANEL_TEXT_PAD, GG::Y0);
901         const GG::Pt name_lr = GG::Pt(ClientWidth() - DATA_PANEL_TEXT_PAD,           LabelHeight());
902         if (m_ship_name_text)
903             m_ship_name_text->SizeMove(name_ul, name_lr);
904         if (m_design_name_text)
905             m_design_name_text->SizeMove(name_ul, name_lr);
906 
907         if (ClientWidth() < 250)
908             DetachChild(m_ship_name_text);
909         else
910             AttachChild(m_ship_name_text);
911 
912         // position ship statistic icons one after another horizontally and centered vertically
913         GG::Pt icon_ul = GG::Pt(name_ul.x, LabelHeight() + std::max(GG::Y0, (ClientHeight() - LabelHeight() - StatIconSize().y) / 2));
914         for (auto& entry : m_stat_icons) {
915             entry.second->SizeMove(icon_ul, icon_ul + StatIconSize());
916             icon_ul.x += StatIconSize().x;
917         }
918     }
919 
Init()920     void ShipDataPanel::Init() {
921         if (m_initialized)
922             return;
923         m_initialized = true;
924 
925         // ship name text.  blank if no ship.
926         auto ship = Objects().get<Ship>(m_ship_id);
927         std::string ship_name;
928         if (ship)
929             ship_name = ship->Name();
930 
931         if (GetOptionsDB().Get<bool>("ui.name.id.shown")) {
932             ship_name = ship_name + " (" + std::to_string(m_ship_id) + ")";
933         }
934 
935         m_ship_name_text = GG::Wnd::Create<CUILabel>(ship_name, GG::FORMAT_LEFT);
936         AttachChild(m_ship_name_text);
937 
938 
939         // design name and statistic icons
940         if (!ship)
941             return;
942 
943         if (const ShipDesign* design = ship->Design()) {
944             m_design_name_text = GG::Wnd::Create<CUILabel>(design->Name(), GG::FORMAT_RIGHT);
945             AttachChild(m_design_name_text);
946         }
947 
948 
949         //int tooltip_delay = GetOptionsDB().Get<int>("ui.tooltip.delay");
950 
951         std::vector<std::pair<MeterType, std::shared_ptr<GG::Texture>>> meters_icons;
952         meters_icons.reserve(13);
953         meters_icons.push_back({METER_STRUCTURE,          ClientUI::MeterIcon(METER_STRUCTURE)});
954         if (ship->IsArmed())
955             meters_icons.push_back({METER_CAPACITY,       DamageIcon()});
956         if (ship->HasFighters())
957             meters_icons.push_back({METER_SECONDARY_STAT, FightersIcon()});
958         if (ship->HasTroops())
959             meters_icons.push_back({METER_TROOPS,         TroopIcon()});
960         if (ship->CanColonize())
961             meters_icons.push_back({METER_POPULATION,     ColonyIcon()});
962         if (ship->GetMeter(METER_INDUSTRY)->Initial() > 0.0f)
963             meters_icons.push_back({METER_INDUSTRY,       IndustryIcon()});
964         if (ship->GetMeter(METER_RESEARCH)->Initial() > 0.0f)
965             meters_icons.push_back({METER_RESEARCH,       ResearchIcon()});
966         if (ship->GetMeter(METER_TRADE)->Initial() > 0.0f)
967             meters_icons.push_back({METER_TRADE,          TradeIcon()});
968 
969         for (auto& meter : {METER_SHIELD, METER_FUEL, METER_DETECTION,
970                             METER_STEALTH, METER_SPEED})
971         {
972             meters_icons.push_back({meter, ClientUI::MeterIcon(meter)});
973         }
974 
975         m_stat_icons.reserve(meters_icons.size());
976         for (auto& entry : meters_icons) {
977             auto icon = GG::Wnd::Create<StatisticIcon>(entry.second, 0, 0, false, StatIconSize().x, StatIconSize().y);
978             m_stat_icons.push_back({entry.first, icon});
979             AttachChild(icon);
980             std::string meter_string = boost::lexical_cast<std::string>(entry.first);
981 
982             icon->RightClickedSignal.connect([meter_string](const GG::Pt& pt) {
983                 auto zoom_article_action = [meter_string]() { ClientUI::GetClientUI()->ZoomToMeterTypeArticle(meter_string); };
984                 std::string popup_label = boost::io::str(FlexibleFormat(UserString("ENC_LOOKUP")) %
985                                                                         UserString(meter_string));
986 
987                 auto popup = GG::Wnd::Create<CUIPopupMenu>(pt.x, pt.y);
988                 popup->AddMenuItem(GG::MenuItem(popup_label, false, false, zoom_article_action));
989 
990                 popup->Run();
991             });
992         }
993 
994         // bookkeeping
995         m_ship_connection = ship->StateChangedSignal.connect(
996             boost::bind(&ShipDataPanel::RequireRefresh, this));
997 
998         if (auto fleet = Objects().get<Fleet>(ship->FleetID()))
999             m_fleet_connection = fleet->StateChangedSignal.connect(
1000                 boost::bind(&ShipDataPanel::RequireRefresh, this));
1001     }
1002 
1003     ////////////////////////////////////////////////
1004     // ShipRow
1005     ////////////////////////////////////////////////
1006     /** A ListBox::Row subclass used to represent ships in ShipListBoxes. */
1007     class ShipRow : public GG::ListBox::Row {
1008     public:
ShipRow(GG::X w,GG::Y h,int ship_id)1009         ShipRow(GG::X w, GG::Y h, int ship_id) :
1010             GG::ListBox::Row(w, h),
1011             m_ship_id(ship_id)
1012         {
1013             SetName("ShipRow");
1014             SetChildClippingMode(ClipToClient);
1015             if (Objects().get<Ship>(m_ship_id))
1016                 SetDragDropDataType(SHIP_DROP_TYPE_STRING);
1017         }
1018 
CompleteConstruction()1019         void CompleteConstruction() override {
1020             GG::ListBox::Row::CompleteConstruction();
1021             m_panel = GG::Wnd::Create<ShipDataPanel>(Width(), Height(), m_ship_id);
1022             push_back(m_panel);
1023         }
1024 
SizeMove(const GG::Pt & ul,const GG::Pt & lr)1025         void SizeMove(const GG::Pt& ul, const GG::Pt& lr) override {
1026             const GG::Pt old_size = Size();
1027             GG::ListBox::Row::SizeMove(ul, lr);
1028             if (!empty() && old_size != Size() && m_panel)
1029                 m_panel->Resize(Size());
1030         }
1031 
ShipID() const1032         int ShipID() const {return m_ship_id;}
1033 
1034     private:
1035         int                             m_ship_id = INVALID_OBJECT_ID;
1036         std::shared_ptr<ShipDataPanel>  m_panel;
1037     };
1038 }
1039 
1040 ////////////////////////////////////////////////
1041 // FleetDataPanel
1042 ////////////////////////////////////////////////
1043 /** Represents a single fleet.  This class is used as the drop-target in
1044   * FleetWnd or as the sole Control in each FleetRow. */
1045 class FleetDataPanel : public GG::Control {
1046 public:
1047     FleetDataPanel(GG::X w, GG::Y h, int fleet_id);
1048     FleetDataPanel(GG::X w, GG::Y h, int system_id, bool new_fleet_drop_target);
1049     ~FleetDataPanel();
1050 
1051     /** Upper left plus border insets. */
1052     GG::Pt ClientUpperLeft() const override;
1053 
1054     /** Lower right minus border insets. */
1055     GG::Pt ClientLowerRight() const override;
1056 
1057     void PreRender() override;
1058     void Render() override;
1059 
1060     void DragDropHere(const GG::Pt& pt, std::map<const Wnd*, bool>& drop_wnds_acceptable,
1061                       GG::Flags<GG::ModKey> mod_keys) override;
1062 
1063     void CheckDrops(const GG::Pt& pt, std::map<const Wnd*, bool>& drop_wnds_acceptable,
1064                     GG::Flags<GG::ModKey> mod_keys) override;
1065 
1066     void DragDropLeave() override;
1067     void AcceptDrops(const GG::Pt& pt, std::vector<std::shared_ptr<GG::Wnd>> wnds, GG::Flags<GG::ModKey> mod_keys) override;
1068     void SizeMove(const GG::Pt& ul, const GG::Pt& lr) override;
1069 
1070     bool                Selected() const;
1071     NewFleetAggression  GetNewFleetAggression() const;
1072     void                Select(bool b);
1073     void                SetSystemID(int id);
1074 
1075     /** Indicate fleet data has changed and needs refresh. */
1076     void RequireRefresh();
1077 
1078     mutable boost::signals2::signal<void (const std::vector<int>&)> NewFleetFromShipsSignal;
1079 
1080 protected:
1081     void DropsAcceptable(DropsAcceptableIter first, DropsAcceptableIter last,
1082                          const GG::Pt& pt, GG::Flags<GG::ModKey> mod_keys) const override;
1083 
1084 private:
1085     void ToggleAggression();
1086     void Refresh();
1087     void RefreshStateChangedSignals();
1088     void SetStatIconValues();
1089     void UpdateAggressionToggle();
1090     void DoLayout();
1091     void Init();
1092     void ColorTextForSelect();
1093 
1094     const int           m_fleet_id = INVALID_OBJECT_ID;
1095     int                 m_system_id = INVALID_OBJECT_ID;
1096     const bool          m_is_new_fleet_drop_target = false;
1097     NewFleetAggression  m_new_fleet_aggression = FLEET_PASSIVE;
1098     bool                m_needs_refresh = true;
1099 
1100     boost::signals2::connection                     m_fleet_connection;
1101     std::vector<boost::signals2::connection>        m_ship_connections;
1102 
1103     std::shared_ptr<GG::Control>                    m_fleet_icon;
1104     std::shared_ptr<GG::Label>                      m_fleet_name_text;
1105     std::shared_ptr<GG::Label>                      m_fleet_destination_text;
1106     std::shared_ptr<GG::Button>                     m_aggression_toggle;
1107     std::vector<std::shared_ptr<GG::StaticGraphic>> m_fleet_icon_overlays;
1108     std::shared_ptr<ScanlineControl>                m_scanline_control;
1109 
1110     std::vector<std::pair<MeterType, std::shared_ptr<StatisticIcon>>> m_stat_icons; // statistic icons and associated meter types
1111 
1112     bool m_selected = false;
1113     bool m_initialized = false;
1114 };
1115 
FleetDataPanel(GG::X w,GG::Y h,int fleet_id)1116 FleetDataPanel::FleetDataPanel(GG::X w, GG::Y h, int fleet_id) :
1117     Control(GG::X0, GG::Y0, w, h, GG::NO_WND_FLAGS),
1118     m_fleet_id(fleet_id),
1119     m_new_fleet_aggression(NewFleetsAggressiveOptionSetting())
1120 {
1121     RequireRefresh();
1122     SetChildClippingMode(ClipToClient);
1123 }
1124 
FleetDataPanel(GG::X w,GG::Y h,int system_id,bool new_fleet_drop_target)1125 FleetDataPanel::FleetDataPanel(GG::X w, GG::Y h, int system_id, bool new_fleet_drop_target) :
1126     Control(GG::X0, GG::Y0, w, h, GG::INTERACTIVE),
1127     m_system_id(system_id),
1128     m_is_new_fleet_drop_target(new_fleet_drop_target),
1129     m_new_fleet_aggression(NewFleetsAggressiveOptionSetting())
1130 {
1131     RequirePreRender();
1132     SetChildClippingMode(ClipToClient);
1133 }
1134 
~FleetDataPanel()1135 FleetDataPanel::~FleetDataPanel()
1136 {}
1137 
ClientUpperLeft() const1138 GG::Pt FleetDataPanel::ClientUpperLeft() const
1139 { return UpperLeft() + GG::Pt(GG::X(DATA_PANEL_BORDER), GG::Y(DATA_PANEL_BORDER)); }
1140 
ClientLowerRight() const1141 GG::Pt FleetDataPanel::ClientLowerRight() const
1142 { return LowerRight() - GG::Pt(GG::X(DATA_PANEL_BORDER), GG::Y(DATA_PANEL_BORDER));  }
1143 
Selected() const1144 bool FleetDataPanel::Selected() const
1145 { return m_selected; }
1146 
GetNewFleetAggression() const1147 NewFleetAggression FleetDataPanel::GetNewFleetAggression() const
1148 { return m_new_fleet_aggression; }
1149 
RequireRefresh()1150 void FleetDataPanel::RequireRefresh() {
1151     m_needs_refresh = true;
1152     RequirePreRender();
1153 }
1154 
PreRender()1155 void FleetDataPanel::PreRender() {
1156     if (!m_initialized)
1157         Init();
1158 
1159     GG::Wnd::PreRender();
1160     if (m_needs_refresh)
1161         Refresh();
1162     DoLayout();
1163 }
1164 
Render()1165 void FleetDataPanel::Render() {
1166     // main background position and colour
1167     const GG::Clr& background_colour = ClientUI::WndColor();
1168     const GG::Pt ul = UpperLeft(), lr = LowerRight(), cul = ClientUpperLeft();
1169 
1170     // title background colour and position
1171     const GG::Clr& unselected_colour = ClientUI::WndOuterBorderColor();
1172     const GG::Clr& selected_colour = ClientUI::WndInnerBorderColor();
1173     GG::Clr border_colour = m_selected ? selected_colour : unselected_colour;
1174     if (Disabled())
1175         border_colour = DisabledColor(border_colour);
1176     const GG::Pt text_ul = cul + GG::Pt(DataPanelIconSpace().x, GG::Y0);
1177     const GG::Pt text_lr = cul + GG::Pt(ClientWidth(),           LabelHeight());
1178 
1179     // render
1180     GG::FlatRectangle(ul,       lr,         background_colour,  border_colour, DATA_PANEL_BORDER);  // background and border
1181     GG::FlatRectangle(text_ul,  text_lr,    border_colour,      GG::CLR_ZERO,  0);                  // title background box
1182 }
1183 
DragDropHere(const GG::Pt & pt,std::map<const Wnd *,bool> & drop_wnds_acceptable,GG::Flags<GG::ModKey> mod_keys)1184 void FleetDataPanel::DragDropHere(const GG::Pt& pt, std::map<const Wnd*, bool>& drop_wnds_acceptable,
1185                                   GG::Flags<GG::ModKey> mod_keys)
1186 {
1187     if (!m_is_new_fleet_drop_target) {
1188         // normally the containing row (or the listbox that contains that) will
1189         // handle drag-drop related things
1190         ForwardEventToParent();
1191     }
1192 
1193     DropsAcceptable(drop_wnds_acceptable.begin(), drop_wnds_acceptable.end(), pt, mod_keys);
1194 
1195     if (Disabled()) {
1196         Select(false);
1197         return;
1198     }
1199 
1200     // select panel if all dragged Wnds can be dropped here...
1201 
1202     Select(true);   // default
1203 
1204     // get whether each Wnd is dropable
1205     DropsAcceptable(drop_wnds_acceptable.begin(), drop_wnds_acceptable.end(), pt, mod_keys);
1206 
1207     // scan through wnds, looking for one that isn't dropable
1208     for (const auto& drop_wnd_acceptable : drop_wnds_acceptable) {
1209         if (!drop_wnd_acceptable.second) {
1210             // wnd can't be dropped
1211             Select(false);
1212             break;
1213         }
1214     }
1215 }
1216 
CheckDrops(const GG::Pt & pt,std::map<const Wnd *,bool> & drop_wnds_acceptable,GG::Flags<GG::ModKey> mod_keys)1217 void FleetDataPanel::CheckDrops(const GG::Pt& pt, std::map<const Wnd*, bool>& drop_wnds_acceptable,
1218                                 GG::Flags<GG::ModKey> mod_keys)
1219 {
1220     if (!m_is_new_fleet_drop_target) {
1221         // normally the containing row (or the listbox that contains that) will
1222         // handle drag-drop related things
1223         ForwardEventToParent();
1224     }
1225     Control::CheckDrops(pt, drop_wnds_acceptable, mod_keys);
1226 }
1227 
DragDropLeave()1228 void FleetDataPanel::DragDropLeave()
1229 { Select(false); }
1230 
DropsAcceptable(DropsAcceptableIter first,DropsAcceptableIter last,const GG::Pt & pt,GG::Flags<GG::ModKey> mod_keys) const1231 void FleetDataPanel::DropsAcceptable(DropsAcceptableIter first, DropsAcceptableIter last,
1232                                      const GG::Pt& pt, GG::Flags<GG::ModKey> mod_keys) const
1233 {
1234     if (!m_is_new_fleet_drop_target) {
1235         // reject all
1236         Wnd::DropsAcceptable(first, last, pt, mod_keys);
1237         return;
1238     }
1239 
1240     // only used when FleetDataPanel sets independently in the FleetWnd, not
1241     // in a FleetListBox
1242 
1243     int this_client_empire_id = HumanClientApp::GetApp()->EmpireID();
1244     std::shared_ptr<const Fleet> this_panel_fleet = Objects().get<Fleet>(m_fleet_id);
1245 
1246     // for every Wnd being dropped...
1247     for (DropsAcceptableIter it = first; it != last; ++it) {
1248         it->second = false; // default
1249 
1250         // reject drops if not enabled
1251         if (this->Disabled())
1252             continue;
1253 
1254         // reject drops if a dropped Wnd isn't a valid ShipRow
1255         if (it->first->DragDropDataType() != SHIP_DROP_TYPE_STRING)
1256             continue;
1257 
1258         // reject drops if a ship being dropped doesn't exist
1259         const ShipRow* ship_row = boost::polymorphic_downcast<const ShipRow*>(it->first);
1260         if (!ship_row)
1261             continue;
1262         auto ship = Objects().get<Ship>(ship_row->ShipID());
1263         if (!ship)
1264             continue;
1265 
1266         // reject drops if the ship is not owned by this client's empire
1267         if (!ship->OwnedBy(this_client_empire_id))
1268             continue;
1269 
1270         if (m_is_new_fleet_drop_target) {
1271             // only allows dropping ships?
1272             // reject drops of ships not located in the same system as this drop target
1273             if (ship->SystemID() != m_system_id || m_system_id == INVALID_OBJECT_ID)
1274                 continue;
1275         } else {
1276             // reject drops if this panel represents a fleet, but this client's
1277             // empire does not own it.
1278             if (this_panel_fleet && !this_panel_fleet->OwnedBy(this_client_empire_id))
1279                 continue;
1280         }
1281 
1282         // all tests passed; can drop
1283         it->second = true;
1284     }
1285 }
1286 
AcceptDrops(const GG::Pt & pt,std::vector<std::shared_ptr<GG::Wnd>> wnds,GG::Flags<GG::ModKey> mod_keys)1287 void FleetDataPanel::AcceptDrops(const GG::Pt& pt, std::vector<std::shared_ptr<GG::Wnd>> wnds, GG::Flags<GG::ModKey> mod_keys) {
1288     if (!m_is_new_fleet_drop_target && Parent()) {
1289         // normally the containing row (or the listbox that contains that) will
1290         // handle drag-drops
1291         ForwardEventToParent();
1292     }
1293 
1294     // following only used when FleetDataPanel sets independently in the
1295     // FleetWnd, not in a FleetListBox
1296 
1297     DebugLogger() << "FleetWnd::AcceptDrops with " << wnds.size() << " wnds at pt: " << pt;
1298     std::vector<int> ship_ids;
1299     ship_ids.reserve(wnds.size());
1300     for (auto& wnd : wnds)
1301         if (const ShipRow* ship_row = boost::polymorphic_downcast<const ShipRow*>(wnd.get()))
1302             ship_ids.push_back(ship_row->ShipID());
1303     std::string id_list;
1304     for (int ship_id : ship_ids)
1305         id_list += std::to_string(ship_id) + " ";
1306     DebugLogger() << "FleetWnd::AcceptDrops found " << ship_ids.size() << " ship ids: " << id_list;
1307 
1308     if (ship_ids.empty())
1309         return;
1310 
1311     NewFleetFromShipsSignal(ship_ids);
1312 }
1313 
Select(bool b)1314 void FleetDataPanel::Select(bool b) {
1315     if (m_selected == b)
1316         return;
1317     m_selected = b;
1318 
1319     ColorTextForSelect();
1320 }
1321 
SetSystemID(int id)1322 void FleetDataPanel::SetSystemID(int id)
1323 { m_system_id = id; }
1324 
SizeMove(const GG::Pt & ul,const GG::Pt & lr)1325 void FleetDataPanel::SizeMove(const GG::Pt& ul, const GG::Pt& lr) {
1326     const GG::Pt old_size = Size();
1327     GG::Control::SizeMove(ul, lr);
1328     if (old_size != Size())
1329         DoLayout();
1330 }
1331 
ToggleAggression()1332 void FleetDataPanel::ToggleAggression() {
1333     if (!m_aggression_toggle)
1334         return;
1335     auto fleet = Objects().get<Fleet>(m_fleet_id);
1336     if (fleet) {
1337         if (ClientPlayerIsModerator())
1338             return; // todo: handle moderator actions for this...
1339         int client_empire_id = HumanClientApp::GetApp()->EmpireID();
1340         if (client_empire_id == ALL_EMPIRES)
1341             return;
1342 
1343         bool new_aggression_state = !fleet->Aggressive();
1344 
1345         // toggle fleet aggression status
1346         HumanClientApp::GetApp()->Orders().IssueOrder(
1347             std::make_shared<AggressiveOrder>(client_empire_id, m_fleet_id, new_aggression_state));
1348     } else if (m_is_new_fleet_drop_target) {
1349         // cycle new fleet aggression
1350         if (m_new_fleet_aggression == INVALID_FLEET_AGGRESSION)
1351             m_new_fleet_aggression = FLEET_AGGRESSIVE;
1352         else if (m_new_fleet_aggression == FLEET_AGGRESSIVE)
1353             m_new_fleet_aggression = FLEET_PASSIVE;
1354         else
1355             m_new_fleet_aggression = INVALID_FLEET_AGGRESSION;
1356         SetNewFleetAggressiveOptionSetting(m_new_fleet_aggression);
1357         UpdateAggressionToggle();
1358     }
1359 }
1360 
Refresh()1361 void FleetDataPanel::Refresh() {
1362     m_needs_refresh = false;
1363 
1364     DetachChildAndReset(m_fleet_icon);
1365     DetachChildAndReset(m_scanline_control);
1366     for (auto& overlay : m_fleet_icon_overlays)
1367         DetachChildAndReset(overlay);
1368     m_fleet_icon_overlays.clear();
1369 
1370     if (m_is_new_fleet_drop_target) {
1371         m_fleet_name_text->SetText(UserString("FW_NEW_FLEET_LABEL"));
1372         m_fleet_destination_text->Clear();
1373 
1374         std::shared_ptr<GG::Texture> new_fleet_texture = ClientUI::GetTexture(ClientUI::ArtDir() / "icons" / "buttons" / "new_fleet.png", true);
1375         m_fleet_icon = GG::Wnd::Create<GG::StaticGraphic>(new_fleet_texture, DataPanelIconStyle());
1376         AttachChild(m_fleet_icon);
1377 
1378     } else if (auto fleet = Objects().get<Fleet>(m_fleet_id)) {
1379         int client_empire_id = HumanClientApp::GetApp()->EmpireID();
1380         // set fleet name and destination text
1381         std::string public_fleet_name = fleet->PublicName(client_empire_id);
1382         if (!fleet->Unowned() && public_fleet_name == UserString("FW_FOREIGN_FLEET")) {
1383             const Empire* ship_owner_empire = GetEmpire(fleet->Owner());
1384             const std::string& owner_name = (ship_owner_empire ? ship_owner_empire->Name() : UserString("FW_FOREIGN"));
1385             std::string fleet_name = boost::io::str(FlexibleFormat(UserString("FW_EMPIRE_FLEET")) % owner_name);
1386             if (GetOptionsDB().Get<bool>("ui.name.id.shown")) {
1387                 fleet_name = fleet_name + " (" + std::to_string(m_fleet_id) + ")";
1388             }
1389             m_fleet_name_text->SetText(fleet_name);
1390         } else {
1391             if (GetOptionsDB().Get<bool>("ui.name.id.shown")) {
1392                 public_fleet_name = public_fleet_name + " (" + std::to_string(m_fleet_id) + ")";
1393             }
1394             m_fleet_name_text->SetText(public_fleet_name);
1395         }
1396         m_fleet_destination_text->SetText(FleetDestinationText(m_fleet_id));
1397 
1398         // set icons
1399         std::vector<std::shared_ptr<GG::Texture>> icons(FleetHeadIcons(fleet, FleetButton::SizeType::LARGE));
1400         icons.push_back(FleetSizeIcon(fleet, FleetButton::SizeType::LARGE));
1401         std::vector<GG::Flags<GG::GraphicStyle>> styles(icons.size(), DataPanelIconStyle());
1402 
1403         m_fleet_icon = GG::Wnd::Create<MultiTextureStaticGraphic>(icons, styles);
1404         AttachChild(m_fleet_icon);
1405 
1406         if (Empire* empire = GetEmpire(fleet->Owner()))
1407             m_fleet_icon->SetColor(empire->Color());
1408         else if (fleet->Unowned() && fleet->HasMonsters())
1409             m_fleet_icon->SetColor(GG::CLR_RED);
1410 
1411         auto all_ships = [fleet](const std::function<bool(const std::shared_ptr<const Ship>&)>& pred) {
1412             // Searching for each Ship one at a time is faster than
1413             // FindObjects(ship_ids), because an early exit avoids searching the
1414             // remaining ids.
1415             return std::all_of(
1416                 fleet->ShipIDs().begin(), fleet->ShipIDs().end(),
1417                 [&pred](const int ship_id) {
1418                     const auto& ship = Objects().get<const Ship>(ship_id);
1419                     if (!ship) {
1420                         WarnLogger() << "Object map is missing ship with expected id " << ship_id;
1421                         return false;
1422                     }
1423                     return pred(ship);
1424                 });
1425         };
1426 
1427         // Add the overlay
1428         auto add_overlay = [this](const std::string& file) {
1429             if (auto overlay_texture = ClientUI::GetTexture(ClientUI::ArtDir() / "misc" / file, true)) {
1430                 auto overlay = GG::Wnd::Create<GG::StaticGraphic>(overlay_texture, DataPanelIconStyle());
1431                 overlay->Resize(GG::Pt(DataPanelIconSpace().x, ClientHeight()));
1432                 AttachChild(overlay);
1433                 m_fleet_icon_overlays.push_back(overlay);
1434             }
1435         };
1436 
1437         // Add overlays for all ships colonizing, invading etc.
1438         std::shared_ptr<GG::Texture> overlay_texture;
1439         if (all_ships([](const std::shared_ptr<const Ship>& ship) { return ship->OrderedScrapped(); }))
1440             add_overlay("scrapped.png");
1441         if (all_ships(
1442                 [](const std::shared_ptr<const Ship>& ship)
1443                 {return ship->OrderedColonizePlanet() != INVALID_OBJECT_ID; })
1444            )
1445         {
1446             add_overlay("colonizing.png");
1447         }
1448         if (all_ships(
1449                 [](const std::shared_ptr<const Ship>& ship)
1450                 { return ship->OrderedInvadePlanet() != INVALID_OBJECT_ID; })
1451            )
1452         {
1453             add_overlay("invading.png");
1454         }
1455         if (all_ships(
1456                 [](const std::shared_ptr<const Ship>& ship)
1457                 { return ship->OrderedBombardPlanet() != INVALID_OBJECT_ID; })
1458            )
1459         {
1460             add_overlay("bombarding.png");
1461         }
1462 
1463         // Moving fleets can't be gifted.  The order will be automatically
1464         // cancelled on the server.  This make the UI appear to cancel the
1465         // order when the ship is moved without requiring the player to
1466         // re-order the gifting if the ship is stopped.
1467         if (fleet->OrderedGivenToEmpire() != ALL_EMPIRES && fleet->TravelRoute().empty())
1468             add_overlay("gifting.png");
1469 
1470         if ((fleet->GetVisibility(client_empire_id) < VIS_BASIC_VISIBILITY)
1471             && GetOptionsDB().Get<bool>("ui.map.scanlines.shown"))
1472         {
1473             m_scanline_control = GG::Wnd::Create<ScanlineControl>(GG::X0, GG::Y0, DataPanelIconSpace().x, ClientHeight(), true,
1474                                                                   GetOptionsDB().Get<GG::Clr>("ui.fleet.scanline.color"));
1475             AttachChild(m_scanline_control);
1476         }
1477 
1478         SetStatIconValues();
1479 
1480         RefreshStateChangedSignals();
1481     }
1482 
1483     UpdateAggressionToggle();
1484 }
1485 
RefreshStateChangedSignals()1486 void FleetDataPanel::RefreshStateChangedSignals() {
1487     m_fleet_connection.disconnect();
1488     for (auto& connection : m_ship_connections)
1489         connection.disconnect();
1490 
1491     auto fleet = Objects().get<Fleet>(m_fleet_id);
1492     if (!fleet)
1493         return;
1494 
1495     m_fleet_connection = fleet->StateChangedSignal.connect(
1496         boost::bind(&FleetDataPanel::RequireRefresh, this));
1497 
1498     m_ship_connections.reserve(fleet->NumShips());
1499     for (auto& ship : Objects().find<const Ship>(fleet->ShipIDs()))
1500         m_ship_connections.push_back(
1501             ship->StateChangedSignal.connect(
1502                 boost::bind(&FleetDataPanel::RequireRefresh, this)));
1503 }
1504 
SetStatIconValues()1505 void FleetDataPanel::SetStatIconValues() {
1506     int client_empire_id = HumanClientApp::GetApp()->EmpireID();
1507     const std::set<int>& this_client_known_destroyed_objects = GetUniverse().EmpireKnownDestroyedObjectIDs(client_empire_id);
1508     const std::set<int>& this_client_stale_object_info = GetUniverse().EmpireStaleKnowledgeObjectIDs(client_empire_id);
1509     int ship_count =        0;
1510     float damage_tally =    0.0f;
1511     float fighters_tally  = 0.0f;
1512     float structure_tally = 0.0f;
1513     float shield_tally =    0.0f;
1514     float min_fuel =        0.0f;
1515     float min_speed =       0.0f;
1516     float troops_tally =    0.0f;
1517     float colony_tally =    0.0f;
1518     std::vector<float> fuels;
1519     std::vector<float> speeds;
1520 
1521     auto fleet = Objects().get<Fleet>(m_fleet_id);
1522 
1523     fuels.reserve(fleet->NumShips());
1524     speeds.reserve(fleet->NumShips());
1525     for (auto& ship : Objects().find<const Ship>(fleet->ShipIDs())) {
1526         int ship_id = ship->ID();
1527         // skip known destroyed and stale info objects
1528         if (this_client_known_destroyed_objects.count(ship_id))
1529             continue;
1530         if (this_client_stale_object_info.count(ship_id))
1531             continue;
1532 
1533         if (ship->Design()) {
1534             ship_count++;
1535             damage_tally += ship->TotalWeaponsDamage(0.0f, false);
1536             fighters_tally += ship->FighterCount();
1537             troops_tally += ship->TroopCapacity();
1538             colony_tally += ship->ColonyCapacity();
1539             structure_tally += ship->GetMeter(METER_STRUCTURE)->Initial();
1540             shield_tally += ship->GetMeter(METER_SHIELD)->Initial();
1541             fuels.push_back(ship->GetMeter(METER_FUEL)->Initial());
1542             speeds.push_back(ship->GetMeter(METER_SPEED)->Initial());
1543         }
1544     }
1545     if (!fuels.empty())
1546         min_fuel = *std::min_element(fuels.begin(), fuels.end());
1547     if (!speeds.empty())
1548         min_speed = *std::min_element(speeds.begin(), speeds.end());
1549 
1550     for (const auto& entry : m_stat_icons) {
1551         MeterType stat_name = entry.first;
1552         const auto& icon = entry.second;
1553         DetachChild(icon);
1554         switch (stat_name) {
1555         case METER_SIZE:
1556             icon->SetValue(ship_count);
1557             AttachChild(icon);
1558             break;
1559         case METER_CAPACITY:
1560             icon->SetValue(damage_tally);
1561             if (fleet->HasArmedShips())
1562                 AttachChild(icon);
1563             break;
1564         case METER_SECONDARY_STAT:
1565             icon->SetValue(fighters_tally);
1566             if (fleet->HasFighterShips())
1567                 AttachChild(icon);
1568             break;
1569         case METER_TROOPS:
1570             icon->SetValue(troops_tally);
1571             if (fleet->HasTroopShips())
1572                 AttachChild(icon);
1573             break;
1574         case METER_POPULATION:
1575             icon->SetValue(colony_tally);
1576             if (fleet->HasColonyShips())
1577                 AttachChild(icon);
1578             break;
1579         case METER_INDUSTRY: {
1580             const auto resource_output = fleet->ResourceOutput(RE_INDUSTRY);
1581             icon->SetValue(resource_output);
1582             if (resource_output > 0.0f)
1583                 AttachChild(icon);
1584         }
1585             break;
1586         case METER_RESEARCH: {
1587             const auto resource_output = fleet->ResourceOutput(RE_RESEARCH);
1588             icon->SetValue(resource_output);
1589             if (resource_output > 0.0f)
1590                 AttachChild(icon);
1591         }
1592             break;
1593         case METER_TRADE: {
1594             const auto resource_output = fleet->ResourceOutput(RE_TRADE);
1595             icon->SetValue(resource_output);
1596             if (resource_output > 0.0f)
1597                 AttachChild(icon);
1598         }
1599             break;
1600         case METER_STRUCTURE:
1601             icon->SetValue(structure_tally);
1602             AttachChild(icon);
1603             break;
1604         case METER_SHIELD:
1605             icon->SetValue(shield_tally/ship_count);
1606             AttachChild(icon);
1607             break;
1608         case METER_FUEL:
1609             icon->SetValue(min_fuel);
1610             AttachChild(icon);
1611             break;
1612         case METER_SPEED:
1613             icon->SetValue(min_speed);
1614             AttachChild(icon);
1615             break;
1616         default:
1617             break;
1618         }
1619     }
1620 }
1621 
UpdateAggressionToggle()1622 void FleetDataPanel::UpdateAggressionToggle() {
1623     if (!m_aggression_toggle)
1624         return;
1625     int tooltip_delay = GetOptionsDB().Get<int>("ui.tooltip.delay");
1626     m_aggression_toggle->SetBrowseModeTime(tooltip_delay);
1627 
1628     NewFleetAggression aggression = FLEET_AGGRESSIVE;
1629 
1630     if (m_is_new_fleet_drop_target) {
1631         aggression = m_new_fleet_aggression;
1632     } else if (auto fleet = Objects().get<Fleet>(m_fleet_id)) {
1633         aggression = fleet->Aggressive() ? FLEET_AGGRESSIVE : FLEET_PASSIVE;
1634     } else {
1635         DetachChild(m_aggression_toggle);
1636         return;
1637     }
1638 
1639     if (aggression == FLEET_AGGRESSIVE) {
1640         m_aggression_toggle->SetUnpressedGraphic(GG::SubTexture(FleetAggressiveIcon()));
1641         m_aggression_toggle->SetPressedGraphic  (GG::SubTexture(FleetPassiveIcon()));
1642         m_aggression_toggle->SetRolloverGraphic (GG::SubTexture(FleetAggressiveMouseoverIcon()));
1643         m_aggression_toggle->SetBrowseInfoWnd(GG::Wnd::Create<IconTextBrowseWnd>(
1644             FleetAggressiveIcon(), UserString("FW_AGGRESSIVE"), UserString("FW_AGGRESSIVE_DESC")));
1645     } else if (aggression == FLEET_PASSIVE) {
1646         m_aggression_toggle->SetUnpressedGraphic(GG::SubTexture(FleetPassiveIcon()));
1647         if (m_is_new_fleet_drop_target)
1648             m_aggression_toggle->SetPressedGraphic  (GG::SubTexture(FleetAutoIcon()));
1649         else
1650             m_aggression_toggle->SetPressedGraphic  (GG::SubTexture(FleetAggressiveIcon()));
1651         m_aggression_toggle->SetRolloverGraphic (GG::SubTexture(FleetPassiveMouseoverIcon()));
1652         m_aggression_toggle->SetBrowseInfoWnd(GG::Wnd::Create<IconTextBrowseWnd>(
1653             FleetPassiveIcon(), UserString("FW_PASSIVE"), UserString("FW_PASSIVE_DESC")));
1654     } else {    // aggression == INVALID_FLEET_AGGRESSION
1655         m_aggression_toggle->SetUnpressedGraphic(GG::SubTexture(FleetAutoIcon()));
1656         m_aggression_toggle->SetPressedGraphic  (GG::SubTexture(FleetAggressiveIcon()));
1657         m_aggression_toggle->SetRolloverGraphic (GG::SubTexture(FleetAutoMouseoverIcon()));
1658         m_aggression_toggle->SetBrowseInfoWnd(GG::Wnd::Create<IconTextBrowseWnd>(
1659             FleetAutoIcon(), UserString("FW_AUTO"), UserString("FW_AUTO_DESC")));
1660     }
1661 }
1662 
DoLayout()1663 void FleetDataPanel::DoLayout() {
1664     if (m_fleet_icon) {
1665         // fleet icon will scale and position itself in the provided space
1666         m_fleet_icon->Resize(GG::Pt(DataPanelIconSpace().x, ClientHeight()));
1667     }
1668     if (m_scanline_control)
1669         m_scanline_control->Resize(GG::Pt(DataPanelIconSpace().x, ClientHeight()));
1670     for (auto& overlay : m_fleet_icon_overlays)
1671         overlay->Resize(GG::Pt(DataPanelIconSpace().x, ClientHeight()));
1672 
1673     // position fleet name and destination texts
1674     const GG::Pt name_ul = GG::Pt(DataPanelIconSpace().x + DATA_PANEL_TEXT_PAD, GG::Y0);
1675     const GG::Pt name_lr = GG::Pt(ClientWidth() - DATA_PANEL_TEXT_PAD - GG::X(Value(LabelHeight())),    LabelHeight());
1676     if (m_fleet_name_text)
1677         m_fleet_name_text->SizeMove(name_ul, name_lr);
1678     if (m_fleet_destination_text)
1679         m_fleet_destination_text->SizeMove(name_ul, name_lr);
1680 
1681     if (ClientWidth() < 250)
1682         DetachChild(m_fleet_name_text);
1683     else
1684         AttachChild(m_fleet_name_text);
1685 
1686     // position stat icons, centering them vertically if there's more space than required
1687     GG::Pt icon_ul = GG::Pt(name_ul.x, LabelHeight() + std::max(GG::Y0, (ClientHeight() - LabelHeight() - StatIconSize().y) / 2));
1688     for (auto& entry : m_stat_icons) {
1689         if (entry.second->Parent().get() != this)
1690             continue;
1691         entry.second->SizeMove(icon_ul, icon_ul + StatIconSize());
1692         icon_ul.x += StatIconSize().x;
1693     }
1694 
1695     // position aggression toggle / indicator
1696     if (m_aggression_toggle) {
1697         GG::Pt toggle_size(GG::X(Value(LabelHeight())), LabelHeight());
1698         GG::Pt toggle_ul = GG::Pt(ClientWidth() - toggle_size.x, GG::Y0);
1699         m_aggression_toggle->SizeMove(toggle_ul, toggle_ul + toggle_size);
1700     }
1701 }
1702 
Init()1703 void FleetDataPanel::Init() {
1704     m_initialized = true;
1705 
1706     m_fleet_name_text = GG::Wnd::Create<CUILabel>("", GG::FORMAT_LEFT);
1707     AttachChild(m_fleet_name_text);
1708     m_fleet_destination_text = GG::Wnd::Create<CUILabel>("", GG::FORMAT_RIGHT);
1709     AttachChild(m_fleet_destination_text);
1710 
1711     if (m_fleet_id == INVALID_OBJECT_ID) {
1712         m_aggression_toggle = Wnd::Create<CUIButton>(
1713             GG::SubTexture(FleetAggressiveIcon()),
1714             GG::SubTexture(FleetPassiveIcon()),
1715             GG::SubTexture(FleetAggressiveMouseoverIcon()));
1716         AttachChild(m_aggression_toggle);
1717         m_aggression_toggle->LeftClickedSignal.connect(
1718             boost::bind(&FleetDataPanel::ToggleAggression, this));
1719 
1720     } else if (auto fleet = Objects().get<Fleet>(m_fleet_id)) {
1721         int tooltip_delay = GetOptionsDB().Get<int>("ui.tooltip.delay");
1722         std::vector<std::tuple<MeterType, std::shared_ptr<GG::Texture>, std::string>> meters_icons_browsetext {
1723             std::make_tuple(MeterType::METER_SIZE,           FleetCountIcon(),                                "FW_FLEET_COUNT_SUMMARY"),
1724             std::make_tuple(MeterType::METER_CAPACITY,       DamageIcon(),                                    "FW_FLEET_DAMAGE_SUMMARY"),
1725             std::make_tuple(MeterType::METER_SECONDARY_STAT, FightersIcon(),                                  "FW_FLEET_FIGHTER_SUMMARY"),
1726             std::make_tuple(MeterType::METER_TROOPS,         TroopIcon(),                                     "FW_FLEET_TROOP_SUMMARY"),
1727             std::make_tuple(MeterType::METER_POPULATION,     ColonyIcon(),                                    "FW_FLEET_COLONY_SUMMARY"),
1728             std::make_tuple(MeterType::METER_INDUSTRY,       IndustryIcon(),                                  "FW_FLEET_INDUSTRY_SUMMARY"),
1729             std::make_tuple(MeterType::METER_RESEARCH,       ResearchIcon(),                                  "FW_FLEET_RESEARCH_SUMMARY"),
1730             std::make_tuple(MeterType::METER_STRUCTURE,      ClientUI::MeterIcon(MeterType::METER_STRUCTURE), "FW_FLEET_STRUCTURE_SUMMARY"),
1731             std::make_tuple(MeterType::METER_SHIELD,         ClientUI::MeterIcon(MeterType::METER_SHIELD),    "FW_FLEET_SHIELD_SUMMARY"),
1732             std::make_tuple(MeterType::METER_FUEL,           ClientUI::MeterIcon(MeterType::METER_FUEL),      "FW_FLEET_FUEL_SUMMARY"),
1733             std::make_tuple(MeterType::METER_SPEED,          ClientUI::MeterIcon(MeterType::METER_SPEED),     "FW_FLEET_SPEED_SUMMARY")};
1734 
1735         m_stat_icons.reserve(meters_icons_browsetext.size());
1736         for (const auto& entry : meters_icons_browsetext) {
1737             auto icon = GG::Wnd::Create<StatisticIcon>(std::get<1>(entry), 0, 0, false, StatIconSize().x, StatIconSize().y);
1738             auto meter_type = std::get<0>(entry);
1739             m_stat_icons.push_back({meter_type, icon});
1740             icon->SetBrowseModeTime(tooltip_delay);
1741             icon->SetBrowseText(UserString(std::get<2>(entry)));
1742             icon->RightClickedSignal.connect([meter_type](const GG::Pt& pt){
1743                 std::string meter_string = boost::lexical_cast<std::string>(meter_type);
1744 
1745                 auto zoom_article_action = [meter_string]() { ClientUI::GetClientUI()->ZoomToMeterTypeArticle(meter_string); };
1746 
1747                 auto popup = GG::Wnd::Create<CUIPopupMenu>(pt.x, pt.y);
1748 
1749                 std::string popup_label = boost::io::str(FlexibleFormat(UserString("ENC_LOOKUP")) %
1750                                                                         UserString(meter_string));
1751                 popup->AddMenuItem(GG::MenuItem(popup_label, false, false, zoom_article_action));
1752                 popup->Run();
1753             });
1754             AttachChild(icon);
1755             icon->SetBrowseModeTime(tooltip_delay);
1756         }
1757 
1758         int client_empire_id = HumanClientApp::GetApp()->EmpireID();
1759         if (fleet->OwnedBy(client_empire_id) || fleet->GetVisibility(client_empire_id) >= VIS_FULL_VISIBILITY) {
1760             m_aggression_toggle = Wnd::Create<CUIButton>(
1761                 GG::SubTexture(FleetAggressiveIcon()),
1762                 GG::SubTexture(FleetPassiveIcon()),
1763                 GG::SubTexture(FleetAggressiveMouseoverIcon()));
1764             AttachChild(m_aggression_toggle);
1765             m_aggression_toggle->LeftClickedSignal.connect(
1766                 boost::bind(&FleetDataPanel::ToggleAggression, this));
1767         }
1768 
1769         ColorTextForSelect();
1770     }
1771 }
1772 
ColorTextForSelect()1773 void FleetDataPanel::ColorTextForSelect() {
1774     const GG::Clr& unselected_text_color = ClientUI::TextColor();
1775     const GG::Clr& selected_text_color = GG::CLR_BLACK;
1776 
1777     GG::Clr text_color_to_use = m_selected ? selected_text_color : unselected_text_color;
1778 
1779     if (Disabled())
1780         text_color_to_use = DisabledColor(text_color_to_use);
1781     if (m_fleet_name_text)
1782         m_fleet_name_text->SetTextColor(text_color_to_use);
1783     if (m_fleet_destination_text)
1784         m_fleet_destination_text->SetTextColor(text_color_to_use);
1785 }
1786 
1787 namespace {
1788     ////////////////////////////////////////////////
1789     // FleetRow
1790     ////////////////////////////////////////////////
1791     /** A ListBox::Row subclass used to represent fleets in FleetsListBoxes. */
1792     class FleetRow : public GG::ListBox::Row {
1793     public:
FleetRow(int fleet_id,GG::X w,GG::Y h)1794         FleetRow(int fleet_id, GG::X w, GG::Y h) :
1795             GG::ListBox::Row(w, h),
1796             m_fleet_id(fleet_id)
1797         {
1798             if (Objects().get<Fleet>(fleet_id))
1799                 SetDragDropDataType(FLEET_DROP_TYPE_STRING);
1800             SetName("FleetRow");
1801             SetChildClippingMode(ClipToClient);
1802         }
1803 
CompleteConstruction()1804         void CompleteConstruction() override {
1805             GG::ListBox::Row::CompleteConstruction();
1806             m_panel = GG::Wnd::Create<FleetDataPanel>(Width(), Height(), m_fleet_id);
1807             push_back(m_panel);
1808         }
1809 
SizeMove(const GG::Pt & ul,const GG::Pt & lr)1810         void SizeMove(const GG::Pt& ul, const GG::Pt& lr) override {
1811             const GG::Pt old_size = Size();
1812             GG::ListBox::Row::SizeMove(ul, lr);
1813             if (!empty() && old_size != Size() && m_panel)
1814                 m_panel->Resize(Size());
1815         }
1816 
FleetID() const1817         int  FleetID() const { return m_fleet_id; }
1818 
1819     private:
1820         int                             m_fleet_id = INVALID_OBJECT_ID;
1821         std::shared_ptr<FleetDataPanel> m_panel;
1822     };
1823 }
1824 
1825 ////////////////////////////////////////////////
1826 // FleetsListBox
1827 ////////////////////////////////////////////////
1828 /** A CUIListBox subclass used to list all the fleets, and handle drag-and-drop
1829   * operations on them, in FleetWnd. */
1830 class FleetsListBox : public CUIListBox {
1831 public:
FleetsListBox(bool order_issuing_enabled)1832     FleetsListBox(bool order_issuing_enabled) :
1833         CUIListBox(),
1834         m_highlighted_row_it(end()),
1835         m_order_issuing_enabled(order_issuing_enabled)
1836     { InitRowSizes(); }
1837 
EnableOrderIssuing(bool enable)1838     void EnableOrderIssuing(bool enable) {
1839         m_order_issuing_enabled = enable;
1840     }
1841 
AcceptDrops(const GG::Pt & pt,std::vector<std::shared_ptr<GG::Wnd>> wnds,GG::Flags<GG::ModKey> mod_keys)1842     void AcceptDrops(const GG::Pt& pt, std::vector<std::shared_ptr<GG::Wnd>> wnds, GG::Flags<GG::ModKey> mod_keys) override {
1843         //DebugLogger() << "FleetsListBox::AcceptDrops";
1844         if (wnds.empty()) {
1845             ErrorLogger() << "FleetsListBox::AcceptDrops dropped wnds empty";
1846             return;
1847         }
1848         if (!m_order_issuing_enabled) {
1849             //DebugLogger() << "... order issuing disabled, aborting";
1850             return;
1851         }
1852 
1853         iterator drop_target_row = RowUnderPt(pt);
1854         //DebugLogger() << "... drop pt: " << pt;
1855         if (drop_target_row == end()) {
1856             //DebugLogger() << "... drop row is end, aborting";
1857             return;
1858         }
1859         //DebugLogger() << "... drop row is in position: " << std::distance(begin(), drop_target_row);
1860 
1861         // get drop target fleet
1862         const FleetRow* drop_target_fleet_row = boost::polymorphic_downcast<FleetRow*>(drop_target_row->get());
1863         if (!drop_target_fleet_row) {
1864             ErrorLogger() << "FleetsListBox::AcceptDrops  drop target not a fleet row. aborting";
1865             return;
1866         }
1867 
1868         int target_fleet_id = drop_target_fleet_row->FleetID();
1869         auto target_fleet = Objects().get<Fleet>(target_fleet_id);
1870         if (!target_fleet) {
1871             ErrorLogger() << "FleetsListBox::AcceptDrops  unable to get target fleet with id: " << target_fleet_id;
1872             return;
1873         }
1874 
1875 
1876         // sort dropped Wnds to extract fleets or ships dropped.  (should only be one or the other in a given drop)
1877         std::vector<std::shared_ptr<Fleet>> dropped_fleets;
1878         dropped_fleets.reserve(wnds.size());
1879         std::vector<std::shared_ptr<Ship>> dropped_ships;
1880         dropped_ships.reserve(wnds.size());
1881 
1882         //DebugLogger() << "... getting/sorting dropped fleets or ships...";
1883         for (const auto& wnd : wnds) {
1884             if (drop_target_fleet_row == wnd.get()) {
1885                 ErrorLogger() << "FleetsListBox::AcceptDrops  dropped wnd is same as drop target?! skipping";
1886                 continue;
1887             }
1888 
1889             if (wnd->DragDropDataType() == FLEET_DROP_TYPE_STRING) {
1890                 const FleetRow* fleet_row = boost::polymorphic_downcast<const FleetRow*>(wnd.get());
1891                 if (!fleet_row) {
1892                     ErrorLogger() << "FleetsListBox::AcceptDrops  unable to get fleet row from dropped wnd";
1893                     continue;
1894                 }
1895                 dropped_fleets.push_back(Objects().get<Fleet>(fleet_row->FleetID()));
1896 
1897             } else if (wnd->DragDropDataType() == SHIP_DROP_TYPE_STRING) {
1898                 const ShipRow* ship_row = boost::polymorphic_downcast<const ShipRow*>(wnd.get());
1899                 if (!ship_row) {
1900                     ErrorLogger() << "FleetsListBox::AcceptDrops  unable to get ship row from dropped wnd";
1901                     continue;
1902                 }
1903                 dropped_ships.push_back(Objects().get<Ship>(ship_row->ShipID()));
1904             }
1905         }
1906 
1907         if (dropped_ships.empty() && dropped_fleets.empty()) {
1908             ErrorLogger() << "FleetsListBox::AcceptDrops  no ships or fleets dropped... aborting";
1909             return;
1910         }
1911 
1912         if (dropped_ships.empty() == dropped_fleets.empty()) {
1913             ErrorLogger() << "FleetsListBox::AcceptDrops  dropped a mix of fleets and ships... aborting";
1914             return;
1915         }
1916         int empire_id = HumanClientApp::GetApp()->EmpireID();
1917 
1918         if (ClientPlayerIsModerator())
1919             return; // todo: handle moderator actions for this...
1920 
1921         // Collected ship ids for the transfer.
1922         std::vector<int> ship_ids;
1923         ship_ids.reserve(dropped_ships.size() + dropped_fleets.size());
1924 
1925         // Need ship ids in a vector for fleet transfer order
1926         for (const auto& dropped_fleet : dropped_fleets) {
1927             if (!dropped_fleet) {
1928                 ErrorLogger() << "FleetsListBox::AcceptDrops  unable to get dropped fleet?";
1929                 continue;
1930             }
1931 
1932             // get fleet's ships in a vector, as this is what FleetTransferOrder takes
1933             const auto& ship_ids_set = dropped_fleet->ShipIDs();
1934             std::copy(ship_ids_set.begin(), ship_ids_set.end(), std::back_inserter(ship_ids));
1935         }
1936 
1937         for (const auto& ship : dropped_ships) {
1938             if (!ship) {
1939                 ErrorLogger() << "FleetsListBox::AcceptDrops  couldn't get dropped ship?";
1940                 continue;
1941             }
1942             ship_ids.push_back(ship->ID());
1943         }
1944 
1945         // order the transfer
1946         if (!ship_ids.empty())
1947             HumanClientApp::GetApp()->Orders().IssueOrder(
1948                 std::make_shared<FleetTransferOrder>(empire_id, target_fleet_id, ship_ids));
1949     }
1950 
DragDropHere(const GG::Pt & pt,std::map<const Wnd *,bool> & drop_wnds_acceptable,GG::Flags<GG::ModKey> mod_keys)1951     void DragDropHere(const GG::Pt& pt, std::map<const Wnd*, bool>& drop_wnds_acceptable,
1952                       GG::Flags<GG::ModKey> mod_keys) override
1953     {
1954         CUIListBox::DragDropHere(pt, drop_wnds_acceptable, mod_keys);
1955 
1956         // default to removing highlighting of any row that has it.
1957         // used to check: if (m_highlighted_row_it != row_it) before doing this...
1958         //ClearHighlighting();
1959 
1960         // abort if this FleetsListBox can't be manipulated
1961         if (!m_order_issuing_enabled)
1962             return;
1963 
1964         // get FleetRow under drop point
1965         iterator row_it = RowUnderPt(pt);
1966         if (row_it == end())
1967             return; // not over a valid row
1968 
1969         // check if row under drop point is already selected.  if it is, don't
1970         // need to highlight it, since as of this writing, the two are the same
1971         // thing, visually and internally
1972         auto& drop_target_row = *row_it;
1973         assert(drop_target_row);
1974         assert(!drop_target_row->empty());
1975 
1976         GG::Control* control = !drop_target_row->empty() ? drop_target_row->at(0) : nullptr;
1977         assert(control);
1978 
1979         FleetDataPanel* drop_target_data_panel = boost::polymorphic_downcast<FleetDataPanel*>(control);
1980         assert(drop_target_data_panel);
1981 
1982         if (drop_target_data_panel->Selected())
1983             return;
1984 
1985         FleetRow* drop_target_fleet_row = boost::polymorphic_downcast<FleetRow*>(drop_target_row.get());
1986         assert(drop_target_fleet_row);
1987 
1988         auto drop_target_fleet = Objects().get<Fleet>(drop_target_fleet_row->FleetID());
1989         assert(drop_target_fleet);
1990 
1991 
1992         // get whether each Wnd is dropable
1993         DropsAcceptable(drop_wnds_acceptable.begin(), drop_wnds_acceptable.end(), pt, mod_keys);
1994 
1995 
1996         // scan through results in drops_acceptable_map and decide whether overall
1997         // drop is acceptable.  to be acceptable, all wnds must individually be
1998         // acceptable for dropping, and there must not be a mix of ships and fleets
1999         // being dropped.
2000         bool fleets_seen = false;
2001         bool ships_seen = false;
2002 
2003         for (auto& drop_wnd_acceptable : drop_wnds_acceptable) {
2004             if (!drop_wnd_acceptable.second)
2005                 return; // a row was an invalid drop. abort without highlighting drop target.
2006 
2007             const auto dropped_wnd = drop_wnd_acceptable.first;
2008             if (dropped_wnd->DragDropDataType() == FLEET_DROP_TYPE_STRING) {
2009                 fleets_seen = true;
2010                 if (ships_seen)
2011                     return; // can't drop both at once
2012 
2013                 const FleetRow* fleet_row = boost::polymorphic_downcast<const FleetRow*>(dropped_wnd);
2014                 assert(fleet_row);
2015                 auto fleet = Objects().get<Fleet>(fleet_row->FleetID());
2016 
2017                 if (!ValidFleetMerge(fleet, drop_target_fleet))
2018                     return; // not a valid drop
2019 
2020             } else if (dropped_wnd->DragDropDataType() == SHIP_DROP_TYPE_STRING) {
2021                 ships_seen = true;
2022                 if (fleets_seen)
2023                     return; // can't drop both at once
2024 
2025                 const ShipRow* ship_row = boost::polymorphic_downcast<const ShipRow*>(dropped_wnd);
2026                 assert(ship_row);
2027                 auto ship = Objects().get<Ship>(ship_row->ShipID());
2028 
2029                 if (!ValidShipTransfer(ship, drop_target_fleet))
2030                     return; // not a valid drop
2031             }
2032         }
2033 
2034         // passed all checks.  drop is valid!
2035         HighlightRow(row_it);
2036     }
2037 
DragDropLeave()2038     void DragDropLeave() override {
2039         CUIListBox::DragDropLeave();
2040         ClearHighlighting();
2041     }
2042 
SizeMove(const GG::Pt & ul,const GG::Pt & lr)2043     void SizeMove(const GG::Pt& ul, const GG::Pt& lr) override {
2044         const GG::Pt old_size = Size();
2045         CUIListBox::SizeMove(ul, lr);
2046         if (old_size != Size()) {
2047             const GG::Pt row_size = ListRowSize();
2048             for (auto& row : *this)
2049                 row->Resize(row_size);
2050         }
2051     }
2052 
ListRowSize() const2053     GG::Pt ListRowSize() const
2054     { return GG::Pt(Width() - RightMargin() - 5, ListRowHeight()); }
2055 
2056 protected:
DropsAcceptable(DropsAcceptableIter first,DropsAcceptableIter last,const GG::Pt & pt,GG::Flags<GG::ModKey> mod_keys) const2057     void DropsAcceptable(DropsAcceptableIter first, DropsAcceptableIter last,
2058                          const GG::Pt& pt, GG::Flags<GG::ModKey> mod_keys) const override
2059     {
2060         // default result, possibly to be updated later: reject all drops
2061         for (DropsAcceptableIter it = first; it != last; ++it)
2062             it->second = false;
2063 
2064         // early termination check: if this FleetsListBox does not presently allow order
2065         // issuing, all drops are unacceptable
2066         if (!m_order_issuing_enabled)
2067             return;
2068 
2069         // semi-early termination if pt is not over a FleetRow, all drops are unacceptable.
2070         // this is because ships can't be dropped into an empty spot of the FleetsListBox;
2071         // ships must be dropped into existing fleet panels / rows, or onto the new fleet
2072         // drop target.
2073         // as well, there is presently no way to drop a FleetRow into a FleetsListBox that
2074         // the row isn't already in, so if the drop isn't onto another FleetRow, there's
2075         // no sense in doing such a drop.
2076 
2077         // get FleetRow under drop point
2078         iterator row = RowUnderPt(pt);
2079 
2080         // if drop point isn't over a FleetRow, reject all drops (default case)
2081         if (row == end())
2082             return;
2083 
2084         // extract drop target fleet from row under drop point
2085         const FleetRow* target_fleet_row = boost::polymorphic_downcast<const FleetRow*>(row->get());
2086         std::shared_ptr<const Fleet> target_fleet;
2087         if (target_fleet_row)
2088             target_fleet = Objects().get<Fleet>(target_fleet_row->FleetID());
2089 
2090         // loop through dropped Wnds, checking if each is a valid ship or fleet.  this doesn't
2091         // consider whether there is a mixture of fleets and ships, as each row is considered
2092         // independently.  actual drops will probably only accept one or the other, not a mixture
2093         // of fleets and ships being dropped simultaneously.
2094         for (DropsAcceptableIter it = first; it != last; ++it) {
2095             if (it->first == target_fleet_row)
2096                 continue;   // can't drop onto self
2097 
2098             // for either of fleet or ship being dropped, check if merge or transfer is valid.
2099             // if any of the nested if's fail, the default rejection of the drop will remain set
2100             if (it->first->DragDropDataType() == FLEET_DROP_TYPE_STRING) {
2101                 if (const FleetRow* fleet_row = boost::polymorphic_downcast<const FleetRow*>(it->first))
2102                     if (auto fleet = Objects().get<Fleet>(fleet_row->FleetID()))
2103                         it->second = ValidFleetMerge(fleet, target_fleet);
2104 
2105             } else if (it->first->DragDropDataType() == SHIP_DROP_TYPE_STRING) {
2106                 if (const ShipRow* ship_row = boost::polymorphic_downcast<const ShipRow*>(it->first))
2107                     if (auto ship = Objects().get<Ship>(ship_row->ShipID()))
2108                         it->second = ValidShipTransfer(ship, target_fleet);
2109             } else {
2110                 // no valid drop type string
2111                 ErrorLogger() << "FleetsListBox unrecognized drop type: " << it->first->DragDropDataType();
2112             }
2113         }
2114     }
2115 
2116 private:
HighlightRow(iterator row_it)2117     void HighlightRow(iterator row_it) {
2118         if (row_it == end())
2119             return;
2120 
2121         if (row_it == m_highlighted_row_it)
2122             return;
2123 
2124         // get FleetDataPanel of row pointed to by row_it
2125         auto& selected_row = *row_it;
2126         assert(selected_row);
2127         assert(!selected_row->empty());
2128         GG::Control* control = !selected_row->empty() ? selected_row->at(0) : nullptr;
2129         FleetDataPanel* data_panel = boost::polymorphic_downcast<FleetDataPanel*>(control);
2130         assert(data_panel);
2131 
2132         // don't need to select and shouldn't store as highlighted if row is actually already selected in ListBox itself
2133         if (data_panel->Selected())
2134             return;
2135 
2136         // mark data panel selected, which indicates highlighting
2137         data_panel->Select(true);
2138         m_highlighted_row_it = row_it;
2139     }
2140 
ClearHighlighting()2141     void ClearHighlighting() {
2142         if (m_highlighted_row_it == end())
2143             return;
2144 
2145         // check that m_highlighted_row_it points to a valid row.
2146         // have been getting intermittant crashes when dragging rows after
2147         // drops onto other rows that occur after attempting to use an
2148         // invalid iterator
2149         bool valid_highlight_row = false;
2150         for (iterator test_it = this->begin(); test_it != this->end(); ++test_it) {
2151             if (test_it == m_highlighted_row_it) {
2152                 valid_highlight_row = true;
2153                 break;
2154             }
2155         }
2156         if (!valid_highlight_row) {
2157             ErrorLogger() << "FleetsListBox::ClearHighlighting : m_highlighted_row not valid! aborting";
2158             m_highlighted_row_it = end();
2159             return;
2160         }
2161 
2162         // Do not un-highlight a row from the selection set since rows in the selection set and the
2163         // drop target use the same graphical highlight effect.
2164         if (Selected(m_highlighted_row_it)) {
2165             m_highlighted_row_it = end();
2166             return;
2167         }
2168 
2169         auto& selected_row = *m_highlighted_row_it;
2170         if (!selected_row) {
2171             ErrorLogger() << "FleetsListBox::ClearHighlighting : no selected row!";
2172             return;
2173         }
2174         if (selected_row->empty()) {
2175             ErrorLogger() << "FleetsListBox::ClearHighlighting : selected row empty!";
2176             return;
2177         }
2178 
2179         GG::Control* control = !selected_row->empty() ? selected_row->at(0) : nullptr;
2180         if (!control) {
2181             ErrorLogger() << "FleetsListBox::ClearHighlighting : null control in selected row!";
2182             return;
2183         }
2184 
2185         FleetDataPanel* data_panel = boost::polymorphic_downcast<FleetDataPanel*>(control);
2186         if (!data_panel) {
2187             ErrorLogger() << "FleetsListBox::ClearHighlighting : no data panel!";
2188             return;
2189         }
2190 
2191         data_panel->Select(false);
2192         m_highlighted_row_it = end();
2193     }
2194 
InitRowSizes()2195     void InitRowSizes() {
2196         SetNumCols(1);
2197         ManuallyManageColProps();
2198     }
2199 
2200     iterator m_highlighted_row_it;
2201     bool     m_order_issuing_enabled = false;
2202 };
2203 
2204 ////////////////////////////////////////////////
2205 // ShipsListBox
2206 ////////////////////////////////////////////////
2207 /** A CUIListBox subclass used to list all the ships, and handle drag-and-drop
2208   * operations on them, in FleetDetailPanel. */
2209 class ShipsListBox : public CUIListBox {
2210 public:
ShipsListBox(int fleet_id,bool order_issuing_enabled)2211     ShipsListBox(int fleet_id, bool order_issuing_enabled) :
2212         CUIListBox(),
2213         m_fleet_id(fleet_id),
2214         m_order_issuing_enabled(order_issuing_enabled)
2215     {}
2216 
CompleteConstruction()2217     void CompleteConstruction() override {
2218         CUIListBox::CompleteConstruction();
2219         Refresh();
2220     }
2221 
Refresh()2222     void Refresh() {
2223         ScopedTimer timer("ShipsListBox::Refresh");
2224 
2225         auto fleet = Objects().get<Fleet>(m_fleet_id);
2226         if (!fleet) {
2227             Clear();
2228             return;
2229         }
2230 
2231         const GG::Pt row_size = ListRowSize();
2232         Clear();
2233 
2234         // repopulate list with ships in current fleet
2235 
2236         SetNumCols(1);
2237         ManuallyManageColProps();
2238 
2239         int this_client_empire_id = HumanClientApp::GetApp()->EmpireID();
2240         const std::set<int>& this_client_known_destroyed_objects =
2241             GetUniverse().EmpireKnownDestroyedObjectIDs(this_client_empire_id);
2242         const std::set<int>& this_client_stale_object_info =
2243             GetUniverse().EmpireStaleKnowledgeObjectIDs(this_client_empire_id);
2244 
2245         const std::set<int>& ship_ids = fleet->ShipIDs();
2246         std::vector<std::shared_ptr<GG::ListBox::Row>> rows;
2247         rows.reserve(ship_ids.size());
2248         for (int ship_id : ship_ids) {
2249             // skip known destroyed and stale info objects
2250             if (this_client_known_destroyed_objects.count(ship_id))
2251                 continue;
2252             if (this_client_stale_object_info.count(ship_id))
2253                 continue;
2254 
2255             auto row = GG::Wnd::Create<ShipRow>(GG::X1, row_size.y, ship_id);
2256             rows.push_back(row);
2257         }
2258         Insert(rows);
2259         for (auto& row : rows)
2260         { row->Resize(row_size); }
2261 
2262         SelRowsChangedSignal(this->Selections());
2263     }
2264 
SetFleet(int fleet_id)2265     void SetFleet(int fleet_id) {
2266         if (m_fleet_id == fleet_id)
2267             return;
2268 
2269         m_fleet_id = fleet_id;
2270         Refresh();
2271     }
2272 
EnableOrderIssuing(bool enable)2273     void EnableOrderIssuing(bool enable) {
2274         m_order_issuing_enabled = enable;
2275     }
2276 
DropsAcceptable(DropsAcceptableIter first,DropsAcceptableIter last,const GG::Pt & pt,GG::Flags<GG::ModKey> mod_keys) const2277     void DropsAcceptable(DropsAcceptableIter first, DropsAcceptableIter last,
2278                          const GG::Pt& pt, GG::Flags<GG::ModKey> mod_keys) const override
2279     {
2280         for (DropsAcceptableIter it = first; it != last; ++it) {
2281             it->second = false; // default
2282 
2283             if (!m_order_issuing_enabled)
2284                 continue;
2285 
2286             const auto& ship_row = dynamic_cast<const ShipRow*>(it->first);
2287             if (!ship_row)
2288                 continue;
2289 
2290             auto ship = Objects().get<Ship>(ship_row->ShipID());
2291             if (!ship) {
2292                 ErrorLogger() << "ShipsListBox::DropsAcceptable couldn't get ship for ship row";
2293                 continue;
2294             }
2295 
2296             auto fleet = Objects().get<Fleet>(ship->FleetID());
2297             if (!fleet) {
2298                 ErrorLogger() << "ShipsListBox::DropsAcceptable couldn't get fleet with id " << ship->FleetID();
2299                 continue;
2300             }
2301 
2302             if (ship && ValidShipTransfer(ship, fleet))
2303                 continue;   // leave false: ship transfer not valid
2304 
2305             // all tests passed; can drop
2306             it->second = true;
2307         }
2308     }
2309 
AcceptDrops(const GG::Pt & pt,std::vector<std::shared_ptr<GG::Wnd>> wnds,GG::Flags<GG::ModKey> mod_keys)2310     void AcceptDrops(const GG::Pt& pt, std::vector<std::shared_ptr<GG::Wnd>> wnds, GG::Flags<GG::ModKey> mod_keys) override {
2311         if (wnds.empty())
2312             return;
2313 
2314         std::shared_ptr<Ship> ship_from_dropped_wnd;
2315         std::vector<int> ship_ids;
2316         ship_ids.reserve(wnds.size());
2317         for (const auto& wnd : wnds) {
2318             if (wnd->DragDropDataType() == SHIP_DROP_TYPE_STRING) {
2319                 const ShipRow* ship_row = boost::polymorphic_downcast<const ShipRow*>(wnd.get());
2320                 assert(ship_row);
2321                 ship_ids.push_back(ship_row->ShipID());
2322                 ship_from_dropped_wnd = Objects().get<Ship>(ship_row->ShipID());
2323             }
2324         }
2325 
2326         if (!ship_from_dropped_wnd)
2327             return;
2328 
2329         int empire_id = HumanClientApp::GetApp()->EmpireID();
2330 
2331         if (ClientPlayerIsModerator())
2332             return; // todo: handle moderator actions for this...
2333 
2334         HumanClientApp::GetApp()->Orders().IssueOrder(
2335             std::make_shared<FleetTransferOrder>(empire_id, m_fleet_id, ship_ids));
2336     }
2337 
SizeMove(const GG::Pt & ul,const GG::Pt & lr)2338     void SizeMove(const GG::Pt& ul, const GG::Pt& lr) override {
2339         const GG::Pt old_size = Size();
2340         CUIListBox::SizeMove(ul, lr);
2341         if (old_size != Size()) {
2342             const GG::Pt row_size = ListRowSize();
2343             for (auto& row : *this)
2344                 row->Resize(row_size);
2345         }
2346     }
2347 
ListRowSize() const2348     GG::Pt ListRowSize() const
2349     { return GG::Pt(Width() - 5 - RightMargin(), ListRowHeight()); }
2350 
2351 private:
2352     int  m_fleet_id = INVALID_OBJECT_ID;
2353     bool m_order_issuing_enabled = false;
2354 };
2355 
2356 ////////////////////////////////////////////////
2357 // FleetDetailPanel
2358 ////////////////////////////////////////////////
2359 /** Used in lower half of FleetWnd to show the
2360   * ships in a fleet, and some basic info about the fleet. */
2361 class FleetDetailPanel : public GG::Wnd {
2362 public:
2363     FleetDetailPanel(GG::X w, GG::Y h, int fleet_id, bool order_issuing_enabled, GG::Flags<GG::WndFlag> flags = GG::NO_WND_FLAGS);
2364 
2365     void CompleteConstruction() override;
2366     void SizeMove(const GG::Pt& ul, const GG::Pt& lr) override;
2367 
2368     int             FleetID() const;
2369     std::set<int>   SelectedShipIDs() const;    ///< returns ids of ships selected in the detail panel's ShipsListBox
2370     void            SetFleet(int fleet_id);                         ///< sets the currently-displayed Fleet.  setting to INVALID_OBJECT_ID shows no fleet
2371     void            SelectShips(const std::set<int>& ship_ids);///< sets the currently-selected ships in the ships list
2372 
2373     void Refresh();
2374     void EnableOrderIssuing(bool enabled = true);
2375 
2376     /** emitted when the set of selected ships changes */
2377     mutable boost::signals2::signal<void (const ShipsListBox::SelectionSet&)> SelectedShipsChangedSignal;
2378     mutable boost::signals2::signal<void (int)>                               ShipRightClickedSignal;
2379 
2380 private:
2381     int  GetShipIDOfListRow(GG::ListBox::iterator it) const; ///< returns the ID number of the ship in row \a row_idx of the ships listbox
2382     void DoLayout();
2383     void UniverseObjectDeleted(std::shared_ptr<const UniverseObject> obj);
2384     void ShipSelectionChanged(const GG::ListBox::SelectionSet& rows);
2385     void ShipRightClicked(GG::ListBox::iterator it, const GG::Pt& pt, const GG::Flags<GG::ModKey>& modkeys);
2386     int  ShipInRow(GG::ListBox::iterator it) const;
2387 
2388     int                             m_fleet_id = INVALID_OBJECT_ID;
2389     bool                            m_order_issuing_enabled = false;
2390     boost::signals2::connection     m_fleet_connection;
2391     std::shared_ptr<ShipsListBox>   m_ships_lb;
2392 };
2393 
FleetDetailPanel(GG::X w,GG::Y h,int fleet_id,bool order_issuing_enabled,GG::Flags<GG::WndFlag> flags)2394 FleetDetailPanel::FleetDetailPanel(GG::X w, GG::Y h, int fleet_id, bool order_issuing_enabled,
2395                                    GG::Flags<GG::WndFlag> flags/* = GG::NO_WND_FLAGS*/) :
2396     GG::Wnd(GG::X0, GG::Y0, w, h, flags),
2397     m_order_issuing_enabled(order_issuing_enabled)
2398 {
2399     SetName("FleetDetailPanel");
2400     SetChildClippingMode(ClipToClient);
2401 
2402     m_ships_lb = GG::Wnd::Create<ShipsListBox>(m_fleet_id, order_issuing_enabled);
2403     m_ships_lb->SetHiliteColor(GG::CLR_ZERO);
2404 
2405     SetFleet(fleet_id);
2406 
2407     if (!m_order_issuing_enabled) {
2408         m_ships_lb->SetStyle(GG::LIST_NOSEL | GG::LIST_BROWSEUPDATES);
2409     } else {
2410         m_ships_lb->SetStyle(GG::LIST_QUICKSEL | GG::LIST_BROWSEUPDATES);
2411         m_ships_lb->AllowDropType(SHIP_DROP_TYPE_STRING);
2412     }
2413 
2414 #if BOOST_VERSION >= 106000
2415     using boost::placeholders::_1;
2416     using boost::placeholders::_2;
2417     using boost::placeholders::_3;
2418 #endif
2419 
2420     m_ships_lb->SelRowsChangedSignal.connect(
2421         boost::bind(&FleetDetailPanel::ShipSelectionChanged, this, _1));
2422     m_ships_lb->RightClickedRowSignal.connect(
2423         boost::bind(&FleetDetailPanel::ShipRightClicked, this, _1, _2, _3));
2424     GetUniverse().UniverseObjectDeleteSignal.connect(
2425         boost::bind(&FleetDetailPanel::UniverseObjectDeleted, this, _1));
2426 }
2427 
CompleteConstruction()2428 void FleetDetailPanel::CompleteConstruction() {
2429     GG::Wnd::CompleteConstruction();
2430 
2431     AttachChild(m_ships_lb);
2432     DoLayout();
2433 }
2434 
GetShipIDOfListRow(GG::ListBox::iterator it) const2435 int FleetDetailPanel::GetShipIDOfListRow(GG::ListBox::iterator it) const
2436 { return boost::polymorphic_downcast<ShipRow*>(it->get())->ShipID(); }
2437 
SetFleet(int fleet_id)2438 void FleetDetailPanel::SetFleet(int fleet_id) {
2439     // save old fleet id and set to new id
2440     int old_fleet_id = m_fleet_id;
2441     m_fleet_id = fleet_id;
2442 
2443     // if set fleet changed, or if current fleet is no fleet, disconnect any
2444     // signals related to that fleet
2445     if (m_fleet_id != old_fleet_id || m_fleet_id == INVALID_OBJECT_ID)
2446         m_fleet_connection.disconnect();
2447 
2448     // if set fleet unchanged, refresh ships list.  if set fleet changed, update ships list for new fleet
2449     if (m_fleet_id == old_fleet_id)
2450         m_ships_lb->Refresh();
2451     else
2452         m_ships_lb->SetFleet(m_fleet_id);
2453 
2454     // if set fleet changed, and new fleet exists, update state change signal connection
2455     if (m_fleet_id != old_fleet_id && m_fleet_id != INVALID_OBJECT_ID) {
2456         auto fleet = Objects().get<Fleet>(m_fleet_id);
2457         if (fleet && !fleet->Empty()) {
2458             m_fleet_connection = fleet->StateChangedSignal.connect(
2459                 boost::bind(&FleetDetailPanel::Refresh, this), boost::signals2::at_front);
2460         } else {
2461             ErrorLogger() << "FleetDetailPanel::SetFleet ignoring set to missing or empty fleet id (" << fleet_id << ")";
2462         }
2463     }
2464 }
2465 
SelectShips(const std::set<int> & ship_ids)2466 void FleetDetailPanel::SelectShips(const std::set<int>& ship_ids) {
2467     const GG::ListBox::SelectionSet initial_selections = m_ships_lb->Selections();
2468 
2469     m_ships_lb->DeselectAll();
2470 
2471     // loop through ships, selecting any indicated
2472     for (auto it = m_ships_lb->begin(); it != m_ships_lb->end(); ++it) {
2473         ShipRow* row = dynamic_cast<ShipRow*>(it->get());
2474         if (!row) {
2475             ErrorLogger() << "FleetDetailPanel::SelectShips couldn't cast a list row to ShipRow?";
2476             continue;
2477         }
2478 
2479         // if this row's ship should be selected, so so
2480         if (ship_ids.count(row->ShipID())) {
2481             m_ships_lb->SelectRow(it);
2482             m_ships_lb->BringRowIntoView(it);   // may cause earlier rows brought into view to be brought out of view... oh well
2483         }
2484     }
2485 
2486     if (initial_selections != m_ships_lb->Selections())
2487         ShipSelectionChanged(m_ships_lb->Selections());
2488 }
2489 
FleetID() const2490 int FleetDetailPanel::FleetID() const
2491 { return m_fleet_id; }
2492 
SelectedShipIDs() const2493 std::set<int> FleetDetailPanel::SelectedShipIDs() const {
2494     std::set<int> retval;
2495 
2496     for (const auto& selection : m_ships_lb->Selections()) {
2497         bool hasRow = false;
2498         for (auto& lb_row : *m_ships_lb) {
2499             if (lb_row == *selection) {
2500                 hasRow=true;
2501                 break;
2502             }
2503         }
2504         if (!hasRow) {
2505             ErrorLogger() << "FleetDetailPanel::SelectedShipIDs tried to get invalid ship row selection;";
2506             continue;
2507         }
2508         auto& row = *selection;
2509         ShipRow* ship_row = dynamic_cast<ShipRow*>(row.get());
2510         if (ship_row && (ship_row->ShipID() != INVALID_OBJECT_ID))
2511             retval.insert(ship_row->ShipID());
2512     }
2513     return retval;
2514 }
2515 
SizeMove(const GG::Pt & ul,const GG::Pt & lr)2516 void FleetDetailPanel::SizeMove(const GG::Pt& ul, const GG::Pt& lr) {
2517     const GG::Pt old_size = Size();
2518     GG::Wnd::SizeMove(ul, lr);
2519     if (old_size != Size())
2520         DoLayout();
2521 }
2522 
EnableOrderIssuing(bool enabled)2523 void FleetDetailPanel::EnableOrderIssuing(bool enabled/* = true*/) {
2524     m_order_issuing_enabled = enabled;
2525     m_ships_lb->EnableOrderIssuing(m_order_issuing_enabled);
2526 }
2527 
Refresh()2528 void FleetDetailPanel::Refresh()
2529 { SetFleet(m_fleet_id); }
2530 
DoLayout()2531 void FleetDetailPanel::DoLayout() {
2532     GG::X   LEFT = GG::X0;
2533     GG::X   RIGHT = ClientWidth();
2534     GG::Y   top = GG::Y0;
2535     GG::Y   bottom = ClientHeight();
2536 
2537     GG::Pt ul = GG::Pt(LEFT, top);
2538     GG::Pt lr = GG::Pt(RIGHT, top + LabelHeight());
2539 
2540     ul = GG::Pt(LEFT, top);
2541     lr = GG::Pt(RIGHT, bottom);
2542     m_ships_lb->SizeMove(ul, lr);
2543 }
2544 
UniverseObjectDeleted(std::shared_ptr<const UniverseObject> obj)2545 void FleetDetailPanel::UniverseObjectDeleted(std::shared_ptr<const UniverseObject> obj) {
2546     if (obj && obj->ID() == m_fleet_id)
2547         SetFleet(INVALID_OBJECT_ID);
2548 }
2549 
ShipSelectionChanged(const GG::ListBox::SelectionSet & rows)2550 void FleetDetailPanel::ShipSelectionChanged(const GG::ListBox::SelectionSet& rows) {
2551     for (auto it = m_ships_lb->begin(); it != m_ships_lb->end(); ++it) {
2552         try {
2553             ShipDataPanel* ship_panel = boost::polymorphic_downcast<ShipDataPanel*>(!(**it).empty() ? (**it).at(0) : nullptr);
2554             ship_panel->Select(rows.count(it));
2555         } catch (const std::exception& e) {
2556             ErrorLogger() << "FleetDetailPanel::ShipSelectionChanged caught exception: " << e.what();
2557             continue;
2558         }
2559     }
2560 
2561     SelectedShipsChangedSignal(rows);
2562 }
2563 
ShipRightClicked(GG::ListBox::iterator it,const GG::Pt & pt,const GG::Flags<GG::ModKey> & modkeys)2564 void FleetDetailPanel::ShipRightClicked(GG::ListBox::iterator it, const GG::Pt& pt, const GG::Flags<GG::ModKey>& modkeys) {
2565     // get ship that was clicked, aborting if problems arise doing so
2566     ShipRow* ship_row = dynamic_cast<ShipRow*>(it->get());
2567     if (!ship_row)
2568         return;
2569 
2570     auto ship = Objects().get<Ship>(ship_row->ShipID());
2571     if (!ship)
2572         return;
2573     auto fleet = Objects().get<Fleet>(m_fleet_id);
2574 
2575     const auto& map_wnd = ClientUI::GetClientUI()->GetMapWnd();
2576     if (ClientPlayerIsModerator() && map_wnd->GetModeratorActionSetting() != MAS_NoAction) {
2577         ShipRightClickedSignal(ship->ID());  // response handled in MapWnd
2578         return;
2579     }
2580 
2581     const ShipDesign* design = GetShipDesign(ship->DesignID()); // may be null
2582     int client_empire_id = HumanClientApp::GetApp()->EmpireID();
2583 
2584     auto popup = GG::Wnd::Create<CUIPopupMenu>(pt.x, pt.y);
2585 
2586     // Zoom to design
2587     if (design) {
2588         std::string popup_label = boost::io::str(FlexibleFormat(UserString("ENC_LOOKUP")) % design->Name(true));
2589         auto zoom_to_design_action = [design]() { ClientUI::GetClientUI()->ZoomToShipDesign(design->ID()); };
2590         popup->AddMenuItem(GG::MenuItem(popup_label, false, false, zoom_to_design_action));
2591     }
2592 
2593     // Rename ship context item
2594     if (ship->OwnedBy(client_empire_id) || ClientPlayerIsModerator()) {
2595         auto rename_action = [ship, client_empire_id]() {
2596             auto edit_wnd = GG::Wnd::Create<CUIEditWnd>(GG::X(350), UserString("ENTER_NEW_NAME"), ship->Name());
2597             edit_wnd->Run();
2598 
2599             if (ClientPlayerIsModerator())
2600                 // TODO: Moderator action for renaming ships
2601                 return;
2602 
2603             if (!RenameOrder::Check(client_empire_id, ship->ID(), edit_wnd->Result()))
2604                 return;
2605 
2606             HumanClientApp::GetApp()->Orders().IssueOrder(
2607                 std::make_shared<RenameOrder>(client_empire_id, ship->ID(), edit_wnd->Result()));
2608         };
2609         popup->AddMenuItem(GG::MenuItem(UserString("RENAME"), false, false, rename_action));
2610     }
2611 
2612     // Scrap or unscrap ships
2613     if (!ClientPlayerIsModerator()
2614         && !ship->OrderedScrapped()
2615         && ship->OwnedBy(client_empire_id))
2616     {
2617         // create popup menu with "Scrap" option
2618         auto scrap_action = [ship, client_empire_id]() {
2619             HumanClientApp::GetApp()->Orders().IssueOrder(
2620                 std::make_shared<ScrapOrder>(client_empire_id, ship->ID()));
2621         };
2622         popup->AddMenuItem(GG::MenuItem(UserString("ORDER_SHIP_SCRAP"), false, false, scrap_action));
2623     } else if (!ClientPlayerIsModerator()
2624                && ship->OwnedBy(client_empire_id))
2625     {
2626         auto unscrap_action = [ship]() {
2627             // find order to scrap this ship, and recind it
2628             auto pending_scrap_orders = PendingScrapOrders();
2629             auto pending_order_it = pending_scrap_orders.find(ship->ID());
2630             if (pending_order_it != pending_scrap_orders.end())
2631                 HumanClientApp::GetApp()->Orders().RescindOrder(pending_order_it->second);
2632         };
2633         // create popup menu with "Cancel Scrap" option
2634         popup->AddMenuItem(GG::MenuItem(UserString("ORDER_CANCEL_SHIP_SCRAP"), false, false, unscrap_action));
2635     }
2636 
2637     // Split fleets
2638     if (ship->OwnedBy(client_empire_id)
2639         && !ClientPlayerIsModerator()
2640         && fleet)
2641     {
2642         auto split_one_design_action = [this, design, fleet]() {
2643             // split ships with same design as clicked ship into separate fleet
2644             const auto&& parent = this->Parent();
2645             if (!parent)
2646                 return;
2647             const FleetWnd* parent_fleet_wnd = dynamic_cast<const FleetWnd*>(parent.get());
2648             if (!parent_fleet_wnd)
2649                 return;
2650             if (!design)
2651                 return;
2652             CreateNewFleetFromShipsWithDesign(fleet->ShipIDs(), design->ID(),
2653                                               parent_fleet_wnd->GetNewFleetAggression());
2654         };
2655 
2656         auto split_all_designs_action = [this, fleet]() {
2657             // split all ships into new fleets by ship design
2658             const auto&& parent = this->Parent();
2659             if (!parent)
2660                 return;
2661             const FleetWnd* parent_fleet_wnd = dynamic_cast<const FleetWnd*>(parent.get());
2662             if (!parent_fleet_wnd)
2663                 return;
2664             CreateNewFleetsFromShipsForEachDesign(fleet->ShipIDs(),
2665                                                   parent_fleet_wnd->GetNewFleetAggression());
2666         };
2667 
2668         if (design)
2669             popup->AddMenuItem( GG::MenuItem(UserString("FW_SPLIT_SHIPS_THIS_DESIGN"), false, false, split_one_design_action));
2670         popup->AddMenuItem(     GG::MenuItem(UserString("FW_SPLIT_SHIPS_ALL_DESIGNS"), false, false, split_all_designs_action));
2671     }
2672 
2673     // Allow dismissal of stale visibility information
2674     if (!ship->OwnedBy(client_empire_id) && fleet) {
2675         auto forget_ship_action = [ship]() {
2676             ClientUI::GetClientUI()->GetMapWnd()->ForgetObject(ship->ID());
2677         };
2678 
2679         auto visibility_turn_map =
2680             GetUniverse().GetObjectVisibilityTurnMapByEmpire(ship->ID(), client_empire_id);
2681 
2682         auto last_turn_visible_it = visibility_turn_map.find(VIS_BASIC_VISIBILITY);
2683         if (last_turn_visible_it != visibility_turn_map.end()
2684             && last_turn_visible_it->second < CurrentTurn())
2685         {
2686             popup->AddMenuItem(GG::MenuItem(UserString("FW_ORDER_DISMISS_SENSOR_GHOST"), false, false, forget_ship_action));
2687         }
2688     }
2689 
2690     popup->Run();
2691 }
2692 
ShipInRow(GG::ListBox::iterator it) const2693 int FleetDetailPanel::ShipInRow(GG::ListBox::iterator it) const {
2694     if (it == m_ships_lb->end())
2695         return INVALID_OBJECT_ID;
2696 
2697     if (ShipRow* ship_row = dynamic_cast<ShipRow*>(it->get()))
2698         return ship_row->ShipID();
2699 
2700     return INVALID_OBJECT_ID;
2701 }
2702 
2703 ////////////////////////////////////////////////
2704 // FleetWnd
2705 ////////////////////////////////////////////////
2706 namespace {
2707     /** \p create or grow a bounding \p box from \p pt. */
CreateOrGrowBox(bool create,const GG::Rect box,const GG::Pt pt)2708     GG::Rect CreateOrGrowBox(bool create, const GG::Rect box, const GG::Pt pt) {
2709         if (create)
2710             return GG::Rect(pt, pt);
2711         else
2712             return GG::Rect(
2713                 std::min(box.Left(),    pt.x),
2714                 std::min(box.Top(),     pt.y),
2715                 std::max(box.Right(),   pt.x),
2716                 std::max(box.Bottom(),  pt.y));
2717     }
2718 
2719     /** Is \p ll smaller or equal to the size of \p rr? */
SmallerOrEqual(GG::Rect ll,GG::Rect rr)2720     bool SmallerOrEqual(GG::Rect ll, GG::Rect rr) {
2721         return (ll.Width() <= rr.Width() && ll.Height() <= rr.Height());
2722     }
2723 }
2724 
FleetWnd(const std::vector<int> & fleet_ids,bool order_issuing_enabled,double allowed_bounding_box_leeway,int selected_fleet_id,GG::Flags<GG::WndFlag> flags,const std::string & config_name)2725 FleetWnd::FleetWnd(const std::vector<int>& fleet_ids, bool order_issuing_enabled,
2726                    double allowed_bounding_box_leeway /*= 0*/,
2727                    int selected_fleet_id/* = INVALID_OBJECT_ID*/,
2728                    GG::Flags<GG::WndFlag> flags/* = INTERACTIVE | DRAGABLE | ONTOP | CLOSABLE | RESIZABLE*/,
2729                    const std::string& config_name) :
2730     MapWndPopup("", flags | GG::RESIZABLE, config_name),
2731     m_order_issuing_enabled(order_issuing_enabled)
2732 {
2733     if (!fleet_ids.empty()) {
2734         if (auto fleet = Objects().get<Fleet>(*fleet_ids.begin()))
2735             m_empire_id = fleet->Owner();
2736     }
2737 
2738     for (int fleet_id : fleet_ids)
2739         m_fleet_ids.insert(fleet_id);
2740 
2741     // verify that the selected fleet id is valid.
2742     if (selected_fleet_id != INVALID_OBJECT_ID &&
2743         !m_fleet_ids.count(selected_fleet_id))
2744     {
2745         ErrorLogger() << "FleetWnd::FleetWnd couldn't find requested selected fleet with id " << selected_fleet_id;
2746         selected_fleet_id = INVALID_OBJECT_ID;
2747     }
2748 
2749     // Determine the size of the bounding box containing the fleets, plus the leeway
2750     bool is_first_fleet = true;
2751     for (const auto& fleet : Objects().find<Fleet>(m_fleet_ids)) {
2752         if (!fleet)
2753             continue;
2754 
2755         auto fleet_loc = GG::Pt(GG::X(fleet->X()), GG::Y(fleet->Y()));
2756         // Grow the fleets bounding box
2757         m_bounding_box = CreateOrGrowBox(is_first_fleet, m_bounding_box, fleet_loc);
2758         is_first_fleet = false;
2759     }
2760     m_bounding_box = GG::Rect(m_bounding_box.UpperLeft(),
2761                               m_bounding_box.LowerRight()
2762                               + GG::Pt(GG::X(allowed_bounding_box_leeway),
2763                                        GG::Y(allowed_bounding_box_leeway)));
2764 
2765     m_fleet_detail_panel = GG::Wnd::Create<FleetDetailPanel>(GG::X1, GG::Y1, selected_fleet_id, m_order_issuing_enabled);
2766 }
2767 
CompleteConstruction()2768 void FleetWnd::CompleteConstruction() {
2769     Sound::TempUISoundDisabler sound_disabler;
2770 
2771     // add fleet aggregate stat icons
2772     int tooltip_delay = GetOptionsDB().Get<int>("ui.tooltip.delay");
2773 
2774     m_stat_icons.reserve(7);
2775     for (auto entry : {
2776             std::make_tuple(METER_SIZE, FleetCountIcon(), UserStringNop("FW_FLEET_COUNT_SUMMARY")),
2777             std::make_tuple(METER_CAPACITY, DamageIcon(), UserStringNop("FW_FLEET_DAMAGE_SUMMARY")),
2778             std::make_tuple(METER_SECONDARY_STAT, FightersIcon(), UserStringNop("FW_FLEET_FIGHTER_SUMMARY")),
2779             std::make_tuple(METER_STRUCTURE, ClientUI::MeterIcon(METER_STRUCTURE), UserStringNop("FW_FLEET_STRUCTURE_SUMMARY")),
2780             std::make_tuple(METER_SHIELD, ClientUI::MeterIcon(METER_SHIELD), UserStringNop("FW_FLEET_SHIELD_SUMMARY")),
2781             std::make_tuple(METER_TROOPS, TroopIcon(), UserStringNop("FW_FLEET_TROOP_SUMMARY")),
2782             std::make_tuple(METER_POPULATION, ColonyIcon(), UserStringNop("FW_FLEET_COLONY_SUMMARY")),
2783         })
2784     {
2785         auto icon = GG::Wnd::Create<StatisticIcon>(std::get<1>(entry), 0, 0, false, StatIconSize().x, StatIconSize().y);
2786         m_stat_icons.push_back({std::get<0>(entry), icon});
2787         icon->SetBrowseModeTime(tooltip_delay);
2788         icon->SetBrowseText(UserString(std::get<2>(entry)));
2789         AttachChild(icon);
2790     }
2791 
2792 #if BOOST_VERSION >= 106000
2793     using boost::placeholders::_1;
2794     using boost::placeholders::_2;
2795     using boost::placeholders::_3;
2796 #endif
2797 
2798     // create fleet list box
2799     m_fleets_lb = GG::Wnd::Create<FleetsListBox>(m_order_issuing_enabled);
2800     m_fleets_lb->SetHiliteColor(GG::CLR_ZERO);
2801     m_fleets_lb->SelRowsChangedSignal.connect(
2802         boost::bind(&FleetWnd::FleetSelectionChanged, this, _1));
2803     m_fleets_lb->LeftClickedRowSignal.connect(
2804         boost::bind(&FleetWnd::FleetLeftClicked, this, _1, _2, _3));
2805     m_fleets_lb->RightClickedRowSignal.connect(
2806         boost::bind(&FleetWnd::FleetRightClicked, this, _1, _2, _3));
2807     m_fleets_lb->DoubleClickedRowSignal.connect(
2808         boost::bind(&FleetWnd::FleetDoubleClicked, this, _1, _2, _3));
2809     AttachChild(m_fleets_lb);
2810     m_fleets_lb->SetStyle(GG::LIST_NOSORT | GG::LIST_BROWSEUPDATES);
2811     m_fleets_lb->AllowDropType(SHIP_DROP_TYPE_STRING);
2812     m_fleets_lb->AllowDropType(FLEET_DROP_TYPE_STRING);
2813 
2814     // create fleet detail panel
2815     m_fleet_detail_panel->SelectedShipsChangedSignal.connect(
2816         boost::bind(&FleetWnd::ShipSelectionChanged, this, _1));
2817     m_fleet_detail_panel->ShipRightClickedSignal.connect(
2818         ShipRightClickedSignal);
2819     AttachChild(m_fleet_detail_panel);
2820 
2821     // determine fleets to show and populate list
2822     Refresh();
2823 
2824     // create drop target
2825     m_new_fleet_drop_target = GG::Wnd::Create<FleetDataPanel>(GG::X1, ListRowHeight(), m_system_id, true);
2826     AttachChild(m_new_fleet_drop_target);
2827     m_new_fleet_drop_target->NewFleetFromShipsSignal.connect(
2828         boost::bind(&FleetWnd::CreateNewFleetFromDrops, this, _1));
2829 
2830     GetUniverse().UniverseObjectDeleteSignal.connect(
2831         boost::bind(&FleetWnd::UniverseObjectDeleted, this, _1));
2832 
2833     RefreshStateChangedSignals();
2834 
2835     ResetDefaultPosition();
2836     MapWndPopup::CompleteConstruction();
2837 
2838     SetMinSize(GG::Pt(CUIWnd::MinimizedSize().x, TopBorder() + INNER_BORDER_ANGLE_OFFSET + BORDER_BOTTOM +
2839                                                  ListRowHeight() + 2*GG::Y(PAD)));
2840     DoLayout();
2841     SaveDefaultedOptions();
2842     SaveOptions();
2843 }
2844 
~FleetWnd()2845 FleetWnd::~FleetWnd() {
2846     // FleetWnd is registered as a top level window, the same as ClientUI and MapWnd.
2847     // Consequently, when the GUI shutsdown either could be destroyed before this Wnd
2848     if (auto client = ClientUI::GetClientUI())
2849         if (auto mapwnd = client->GetMapWnd())
2850             mapwnd->ClearProjectedFleetMovementLines();
2851     ClosingSignal(this);
2852 }
2853 
PreRender()2854 void FleetWnd::PreRender() {
2855     MapWndPopup::PreRender();
2856 
2857     if (m_needs_refresh)
2858         Refresh();
2859 }
2860 
CalculatePosition() const2861 GG::Rect FleetWnd::CalculatePosition() const {
2862     GG::Pt ul(GG::X(5), GG::GUI::GetGUI()->AppHeight() - FLEET_WND_HEIGHT - 5);
2863     GG::Pt wh(FLEET_WND_WIDTH, FLEET_WND_HEIGHT);
2864     return GG::Rect(ul, ul + wh);
2865 }
2866 
SetStatIconValues()2867 void FleetWnd::SetStatIconValues() {
2868     int client_empire_id = HumanClientApp::GetApp()->EmpireID();
2869     const std::set<int>& this_client_known_destroyed_objects = GetUniverse().EmpireKnownDestroyedObjectIDs(client_empire_id);
2870     const std::set<int>& this_client_stale_object_info = GetUniverse().EmpireStaleKnowledgeObjectIDs(client_empire_id);
2871     int ship_count =        0;
2872     float damage_tally =    0.0f;
2873     float fighters_tally  = 0.0f;
2874     float structure_tally = 0.0f;
2875     float shield_tally =    0.0f;
2876     float troop_tally =     0.0f;
2877     float colony_tally =    0.0f;
2878 
2879     for (auto& fleet : Objects().find<const Fleet>(m_fleet_ids)) {
2880         if ( !(((m_empire_id == ALL_EMPIRES) && (fleet->Unowned())) || fleet->OwnedBy(m_empire_id)) )
2881             continue;
2882 
2883         for (auto& ship : Objects().find<const Ship>(fleet->ShipIDs())) {
2884             int ship_id = ship->ID();
2885 
2886             // skip known destroyed and stale info objects
2887             if (this_client_known_destroyed_objects.count(ship_id))
2888                 continue;
2889             if (this_client_stale_object_info.count(ship_id))
2890                 continue;
2891 
2892             if (ship->Design()) {
2893                 ship_count++;
2894                 damage_tally += ship->TotalWeaponsDamage(0.0f, false);
2895                 fighters_tally += ship->FighterCount();
2896                 structure_tally += ship->GetMeter(METER_STRUCTURE)->Initial();
2897                 shield_tally += ship->GetMeter(METER_SHIELD)->Initial();
2898                 troop_tally += ship->TroopCapacity();
2899                 colony_tally += ship->ColonyCapacity();
2900             }
2901         }
2902     }
2903 
2904     for (auto& entry : m_stat_icons) {
2905         MeterType stat_name = entry.first;
2906         if (stat_name == METER_SHIELD)
2907             entry.second->SetValue(shield_tally/ship_count);
2908         else if (stat_name == METER_STRUCTURE)
2909             entry.second->SetValue(structure_tally);
2910         else if (stat_name == METER_CAPACITY)
2911             entry.second->SetValue(damage_tally);
2912         else if (stat_name == METER_SECONDARY_STAT)
2913             entry.second->SetValue(fighters_tally);
2914         else if (stat_name == METER_POPULATION)
2915             entry.second->SetValue(colony_tally);
2916         else if (stat_name == METER_SIZE)
2917             entry.second->SetValue(ship_count);
2918         else if (stat_name == METER_TROOPS)
2919             entry.second->SetValue(troop_tally);
2920     }
2921 }
2922 
RefreshStateChangedSignals()2923 void FleetWnd::RefreshStateChangedSignals() {
2924     m_system_connection.disconnect();
2925     if (auto system = Objects().get<System>(m_system_id))
2926         m_system_connection = system->StateChangedSignal.connect(
2927             boost::bind(&FleetWnd::RequireRefresh, this), boost::signals2::at_front);
2928 
2929     for (auto& fleet_connection : m_fleet_connections)
2930         fleet_connection.disconnect();
2931     m_fleet_connections.clear();
2932 
2933     for (const auto& fleet : Objects().find<Fleet>(m_fleet_ids)) {
2934         if (!fleet)
2935             continue;
2936         m_fleet_connections.push_back(
2937             fleet->StateChangedSignal.connect(
2938                 boost::bind(&FleetWnd::RequireRefresh, this)));
2939     }
2940 }
2941 
RequireRefresh()2942 void FleetWnd::RequireRefresh() {
2943     m_needs_refresh = true;
2944     RequirePreRender();
2945 }
2946 
Refresh()2947 void FleetWnd::Refresh() {
2948     m_needs_refresh = false;
2949 
2950     int this_client_empire_id = HumanClientApp::GetApp()->EmpireID();
2951     const auto& this_client_known_destroyed_objects = GetUniverse().EmpireKnownDestroyedObjectIDs(this_client_empire_id);
2952     const auto& this_client_stale_object_info = GetUniverse().EmpireStaleKnowledgeObjectIDs(this_client_empire_id);
2953 
2954     // save selected fleet(s) and ships(s)
2955     auto initially_selected_fleets = this->SelectedFleetIDs();
2956     auto initially_selected_ships = this->SelectedShipIDs();
2957 
2958     // remove existing fleet rows
2959     auto initial_fleet_ids = m_fleet_ids;
2960     m_fleet_ids.clear();
2961 
2962     std::multimap<std::pair<int, GG::Pt>, int> fleet_locations_ids;
2963     std::multimap<std::pair<int, GG::Pt>, int> selected_fleet_locations_ids;
2964 
2965     // Check all fleets in initial_fleet_ids and keep those that exist.
2966     std::unordered_set<int> fleets_that_exist;
2967     GG::Rect fleets_bounding_box;
2968     for (const auto& fleet : Objects().find<Fleet>(initial_fleet_ids)) {
2969         if (!fleet)
2970             continue;
2971 
2972         // skip known destroyed and stale info objects
2973         if (this_client_known_destroyed_objects.count(fleet->ID()))
2974             continue;
2975         if (this_client_stale_object_info.count(fleet->ID()))
2976             continue;
2977 
2978         auto fleet_loc = GG::Pt(GG::X(fleet->X()), GG::Y(fleet->Y()));
2979         // Grow the fleets bounding box
2980         fleets_bounding_box = CreateOrGrowBox(fleets_that_exist.empty(), fleets_bounding_box, fleet_loc);
2981 
2982         fleets_that_exist.insert(fleet->ID());
2983         fleet_locations_ids.insert({{fleet->SystemID(), fleet_loc}, fleet->ID()});
2984     }
2985 
2986     auto bounding_box_center = GG::Pt(fleets_bounding_box.MidX(), fleets_bounding_box.MidY());
2987 
2988 
2989     // Filter initially selected fleets according to existing fleets
2990     GG::Rect selected_fleets_bounding_box;
2991     for (const auto& fleet : Objects().find<Fleet>(initially_selected_fleets)) {
2992         if (!fleet)
2993             continue;
2994 
2995         if (!fleets_that_exist.count(fleet->ID()))
2996             continue;
2997 
2998         auto fleet_loc = GG::Pt(GG::X(fleet->X()), GG::Y(fleet->Y()));
2999 
3000         // Grow the selected fleets bounding box
3001         selected_fleets_bounding_box = CreateOrGrowBox(selected_fleet_locations_ids.empty(),
3002                                                        selected_fleets_bounding_box, fleet_loc);
3003         selected_fleet_locations_ids.insert({{fleet->SystemID(), fleet_loc}, fleet->ID()});
3004     }
3005     auto selected_bounding_box_center = GG::Pt(selected_fleets_bounding_box.MidX(),
3006                                                selected_fleets_bounding_box.MidY());
3007 
3008 
3009     // Determine FleetWnd location
3010 
3011     // Are all fleets in one system?  Use that location.
3012     // Otherwise, are all selected fleets in one system?  Use that location.
3013     // Otherwise, are all the moving fleets clustered within m_bounding_box of each other?
3014     // Otherwise, are all the selected fleets clustered within m_bounding_box of each other?
3015     // Otherwise, is the current location a system?  Use that location.
3016     // Otherwise remove all fleets as all fleets have gone in separate directions.
3017 
3018     std::pair<int, GG::Pt> location{INVALID_OBJECT_ID, GG::Pt(GG::X0, GG::Y0)};
3019     if (!fleet_locations_ids.empty()
3020         && fleet_locations_ids.begin()->first.first != INVALID_OBJECT_ID
3021         && (fleet_locations_ids.count(fleet_locations_ids.begin()->first) == fleet_locations_ids.size()))
3022     {
3023         location = fleet_locations_ids.begin()->first;
3024 
3025     } else if (!selected_fleet_locations_ids.empty()
3026                && selected_fleet_locations_ids.begin()->first.first != INVALID_OBJECT_ID
3027                && (selected_fleet_locations_ids.count(selected_fleet_locations_ids.begin()->first)
3028                    == selected_fleet_locations_ids.size()))
3029     {
3030         location = selected_fleet_locations_ids.begin()->first;
3031 
3032     } else if (!fleet_locations_ids.empty()
3033                && SmallerOrEqual(fleets_bounding_box, m_bounding_box))
3034     {
3035         location = {INVALID_OBJECT_ID, bounding_box_center};
3036         std::multimap<std::pair<int, GG::Pt>, int> fleets_near_enough;
3037         for (const auto& loc_and_id: fleet_locations_ids)
3038             fleets_near_enough.insert({location, loc_and_id.second});
3039         fleet_locations_ids.swap(fleets_near_enough);
3040 
3041     } else if (!selected_fleet_locations_ids.empty()
3042                && SmallerOrEqual(selected_fleets_bounding_box, m_bounding_box))
3043     {
3044         location = {INVALID_OBJECT_ID, selected_bounding_box_center};
3045         std::multimap<std::pair<int, GG::Pt>, int> fleets_near_enough;
3046         // Center bounding box on selected fleets.
3047         m_bounding_box = m_bounding_box +
3048             GG::Pt(selected_fleets_bounding_box.MidX() - m_bounding_box.MidX(),
3049                    selected_fleets_bounding_box.MidY() - m_bounding_box.MidY());
3050         for (const auto& loc_and_id: fleet_locations_ids) {
3051             const auto& pos = loc_and_id.first.second;
3052             if (m_bounding_box.Contains(pos))
3053                 fleets_near_enough.insert({location, loc_and_id.second});
3054         }
3055         fleet_locations_ids.swap(fleets_near_enough);
3056 
3057     } else if (auto system = Objects().get<System>(m_system_id)) {
3058         location = {m_system_id, GG::Pt(GG::X(system->X()), GG::Y(system->Y()))};
3059 
3060     } else {
3061         fleet_locations_ids.clear();
3062         selected_fleet_locations_ids.clear();
3063     }
3064 
3065 
3066     // Use fleets that are at the determined location
3067     auto flt_at_loc = fleet_locations_ids.equal_range(location);
3068     for (auto it = flt_at_loc.first; it != flt_at_loc.second; ++it)
3069     { m_fleet_ids.insert(it->second); }
3070 
3071     m_system_id = location.first;
3072 
3073     if (m_new_fleet_drop_target)
3074         m_new_fleet_drop_target->SetSystemID(m_system_id);
3075 
3076     // If the location is a system add in any ships from m_empire_id that are in the system.
3077     if (auto system = Objects().get<System>(m_system_id)) {
3078         m_fleet_ids.clear();
3079         // get fleets to show from system, based on required ownership
3080         for (auto& fleet : Objects().find<Fleet>(system->FleetIDs())) {
3081             int fleet_id = fleet->ID();
3082 
3083             // skip known destroyed and stale info objects
3084             if (this_client_known_destroyed_objects.count(fleet_id) ||
3085                     this_client_stale_object_info.count(fleet_id))
3086                 continue;
3087 
3088             if ( ((m_empire_id == ALL_EMPIRES) && (fleet->Unowned())) || fleet->OwnedBy(m_empire_id) )
3089                 m_fleet_ids.insert(fleet_id);
3090         }
3091     }
3092 
3093 
3094     // Add rows for the known good fleet_ids.
3095     m_fleets_lb->Clear();
3096     for (int fleet_id : m_fleet_ids)
3097         AddFleet(fleet_id);
3098 
3099 
3100     // Determine selected fleets that are at the determined location
3101     std::set<int> still_present_initially_selected_fleets;
3102     auto sel_flt_at_loc = selected_fleet_locations_ids.equal_range(location);
3103     for (auto it = sel_flt_at_loc.first; it != sel_flt_at_loc.second; ++it)
3104     { still_present_initially_selected_fleets.insert(it->second); }
3105 
3106 
3107     // Reselect previously-selected fleets, or default select first fleet in FleetWnd
3108     if (!still_present_initially_selected_fleets.empty()) {
3109         // reselect any previously-selected fleets
3110         SelectFleets(still_present_initially_selected_fleets);
3111         // reselect any previously-selected ships
3112         SelectShips(initially_selected_ships);
3113 
3114     } else if (!m_fleets_lb->Empty()) {
3115         // default select first fleet
3116         int first_fleet_id = FleetInRow(m_fleets_lb->begin());
3117         if (first_fleet_id != INVALID_OBJECT_ID) {
3118             std::set<int> fleet_id_set;
3119             fleet_id_set.insert(first_fleet_id);
3120             SelectFleets(fleet_id_set);
3121         }
3122     }
3123 
3124     SetName(TitleText());
3125     SetStatIconValues();
3126     RefreshStateChangedSignals();
3127 }
3128 
CloseClicked()3129 void FleetWnd::CloseClicked()
3130 { CUIWnd::CloseClicked(); }
3131 
LClick(const GG::Pt & pt,GG::Flags<GG::ModKey> mod_keys)3132 void FleetWnd::LClick(const GG::Pt& pt, GG::Flags<GG::ModKey> mod_keys) {
3133     MapWndPopup::LClick(pt, mod_keys);
3134     ClickedSignal(std::static_pointer_cast<FleetWnd>(shared_from_this()));
3135 }
3136 
DoLayout()3137 void FleetWnd::DoLayout() {
3138     const GG::X TOTAL_WIDTH(ClientWidth());
3139     const GG::X LEFT(GG::X0);
3140     const GG::X RIGHT(TOTAL_WIDTH);
3141 
3142     const GG::Y TOTAL_HEIGHT(ClientHeight());
3143     const GG::Y FLEET_STAT_HEIGHT(StatIconSize().y + PAD);
3144     const GG::Y AVAILABLE_HEIGHT(TOTAL_HEIGHT - GG::Y(INNER_BORDER_ANGLE_OFFSET+PAD) - FLEET_STAT_HEIGHT );
3145     GG::Y top( GG::Y0 + GG::Y(PAD) );
3146     const GG::Y ROW_HEIGHT(m_fleets_lb->ListRowSize().y);
3147 
3148     // position fleet aggregate stat icons
3149     GG::Pt icon_ul = GG::Pt(GG::X0 + DATA_PANEL_TEXT_PAD, top);
3150     for (auto& stat_icon : m_stat_icons) {
3151         stat_icon.second->SizeMove(icon_ul, icon_ul + StatIconSize());
3152         icon_ul.x += StatIconSize().x;
3153     }
3154     top += FLEET_STAT_HEIGHT;
3155 
3156     // are there any fleets owned by this client's empire int his FleetWnd?
3157     bool this_client_owns_fleets_in_this_wnd(false);
3158     int this_client_empire_id = HumanClientApp::GetApp()->EmpireID();
3159     for (const auto& fleet : Objects().find<Fleet>(m_fleet_ids)) {
3160         if (!fleet)
3161             continue;
3162         if (fleet->OwnedBy(this_client_empire_id)) {
3163             this_client_owns_fleets_in_this_wnd = true;
3164             break;
3165         }
3166     }
3167 
3168     // what parts of FleetWnd to show?
3169     bool show_new_fleet_drop_target(true);
3170     if (!m_new_fleet_drop_target || AVAILABLE_HEIGHT < 5*ROW_HEIGHT || !this_client_owns_fleets_in_this_wnd)
3171         show_new_fleet_drop_target = false;
3172 
3173     bool show_fleet_detail_panel(true);
3174     if (AVAILABLE_HEIGHT < 3*ROW_HEIGHT)
3175         show_fleet_detail_panel = false;
3176 
3177 
3178     // how tall to make fleets list?  subtract height for other panels from available height.
3179     GG::Y fleets_list_height(AVAILABLE_HEIGHT);
3180     if (show_fleet_detail_panel)
3181         fleets_list_height *= 0.5;
3182     if (show_new_fleet_drop_target)
3183         fleets_list_height -= (ROW_HEIGHT + GG::Y(PAD));
3184 
3185     // how tall to make ships list, if present?
3186     GG::Y ship_list_height(AVAILABLE_HEIGHT - fleets_list_height - GG::Y(PAD));
3187     if (show_new_fleet_drop_target)
3188         ship_list_height -= (ROW_HEIGHT + GG::Y(PAD));
3189 
3190 
3191     // position controls
3192     m_fleets_lb->SizeMove(                  GG::Pt(LEFT, top),              GG::Pt(RIGHT, top + fleets_list_height));
3193     top += fleets_list_height + GG::Y(PAD);
3194 
3195     if (show_new_fleet_drop_target) {
3196         AttachChild(m_new_fleet_drop_target);
3197         m_new_fleet_drop_target->SizeMove(  GG::Pt(LEFT + GG::X(PAD), top), GG::Pt(RIGHT - GG::X(PAD), top + ROW_HEIGHT));
3198         top += ROW_HEIGHT + GG::Y(PAD);
3199     } else {
3200         if (m_new_fleet_drop_target)
3201             DetachChild(m_new_fleet_drop_target);
3202     }
3203 
3204     if (show_fleet_detail_panel) {
3205         AttachChild(m_fleet_detail_panel);
3206         m_fleet_detail_panel->SizeMove(     GG::Pt(LEFT, top),              GG::Pt(RIGHT, top + ship_list_height));
3207         top += ship_list_height + GG::Y(PAD);
3208     } else {
3209         if (m_fleet_detail_panel)
3210             DetachChild(m_fleet_detail_panel);
3211     }
3212 }
3213 
AddFleet(int fleet_id)3214 void FleetWnd::AddFleet(int fleet_id) {
3215     auto fleet = Objects().get<Fleet>(fleet_id);
3216     if (!fleet /*|| fleet->Empty()*/)
3217         return;
3218 
3219     // verify that fleet is consistent
3220     const GG::Pt row_size = m_fleets_lb->ListRowSize();
3221     auto row = GG::Wnd::Create<FleetRow>(fleet_id, GG::X1, row_size.y);
3222     m_fleets_lb->Insert(row);
3223     row->Resize(row_size);
3224 }
3225 
DeselectAllFleets()3226 void FleetWnd::DeselectAllFleets() {
3227     m_fleets_lb->DeselectAll();
3228     FleetSelectionChanged(m_fleets_lb->Selections());
3229 }
3230 
SelectFleet(int fleet_id)3231 void FleetWnd::SelectFleet(int fleet_id) {
3232     if (fleet_id == INVALID_OBJECT_ID || !(Objects().get<Fleet>(fleet_id))) {
3233         ErrorLogger() << "FleetWnd::SelectFleet invalid id " << fleet_id;
3234         DeselectAllFleets();
3235         return;
3236     }
3237 
3238     for (auto it = m_fleets_lb->begin(); it != m_fleets_lb->end(); ++it) {
3239         FleetRow* row = dynamic_cast<FleetRow*>(it->get());
3240         if (row && row->FleetID() == fleet_id) {
3241             m_fleets_lb->DeselectAll();
3242             m_fleets_lb->SelectRow(it);
3243 
3244             FleetSelectionChanged(m_fleets_lb->Selections());
3245 
3246             m_fleets_lb->BringRowIntoView(it);
3247             break;
3248         }
3249     }
3250 }
3251 
SelectFleets(const std::set<int> & fleet_ids)3252 void FleetWnd::SelectFleets(const std::set<int>& fleet_ids) {
3253     const GG::ListBox::SelectionSet initial_selections = m_fleets_lb->Selections();
3254     m_fleets_lb->DeselectAll();
3255 
3256     // loop through fleets, selecting any indicated
3257     for (auto it = m_fleets_lb->begin(); it != m_fleets_lb->end(); ++it) {
3258         FleetRow* row = dynamic_cast<FleetRow*>(it->get());
3259         if (!row) {
3260             ErrorLogger() << "FleetWnd::SelectFleets couldn't cast a list row to FleetRow?";
3261             continue;
3262         }
3263 
3264         // if this row's fleet should be selected, so so
3265         if (fleet_ids.count(row->FleetID())) {
3266             m_fleets_lb->SelectRow(it);
3267             m_fleets_lb->BringRowIntoView(it);  // may cause earlier rows brought into view to be brought out of view... oh well
3268         }
3269     }
3270 
3271     const GG::ListBox::SelectionSet sels = m_fleets_lb->Selections();
3272 
3273     if (sels.empty() || initial_selections != sels)
3274         FleetSelectionChanged(sels);
3275 }
3276 
SelectShips(const std::set<int> & ship_ids)3277 void FleetWnd::SelectShips(const std::set<int>& ship_ids)
3278 { m_fleet_detail_panel->SelectShips(ship_ids); }
3279 
SizeMove(const GG::Pt & ul,const GG::Pt & lr)3280 void FleetWnd::SizeMove(const GG::Pt& ul, const GG::Pt& lr) {
3281     GG::Pt old_size = Size();
3282     MapWndPopup::SizeMove(ul, lr);
3283     if (Size() != old_size)
3284         DoLayout();
3285 }
3286 
SystemID() const3287 int FleetWnd::SystemID() const
3288 { return m_system_id; }
3289 
EmpireID() const3290 int FleetWnd::EmpireID() const
3291 { return m_empire_id; }
3292 
ContainsFleet(int fleet_id) const3293 bool FleetWnd::ContainsFleet(int fleet_id) const {
3294     for (auto it = m_fleets_lb->begin(); it != m_fleets_lb->end(); ++it) {
3295         auto fleet = Objects().get<Fleet>(FleetInRow(it));
3296         if (fleet && fleet->ID() == fleet_id)
3297             return true;
3298     }
3299     return false;
3300 }
3301 
3302 template <typename Set>
ContainsFleets(const Set & fleet_ids_) const3303 bool FleetWnd::ContainsFleets(const Set& fleet_ids_) const {
3304     if (fleet_ids_.empty())
3305         return false;
3306 
3307     auto fleet_ids = fleet_ids_;
3308 
3309     // Remove found ids from fleet_ids.  If fleet_ids is empty, all have been found.
3310     for (auto it = m_fleets_lb->begin(); it != m_fleets_lb->end(); ++it) {
3311         auto fleet = Objects().get<Fleet>(FleetInRow(it));
3312         if (fleet)
3313             fleet_ids.erase(fleet->ID());
3314 
3315         if (fleet_ids.empty())
3316             return true;
3317     }
3318 
3319     return fleet_ids.empty();
3320 }
3321 
FleetIDs() const3322 const std::set<int>& FleetWnd::FleetIDs() const
3323 { return m_fleet_ids; }
3324 
SelectedFleetIDs() const3325 std::set<int> FleetWnd::SelectedFleetIDs() const {
3326     std::set<int> retval;
3327     for (auto it = m_fleets_lb->begin(); it != m_fleets_lb->end(); ++it) {
3328         if (!m_fleets_lb->Selected(it))
3329             continue;
3330 
3331         int selected_fleet_id = FleetInRow(it);
3332         if (selected_fleet_id != INVALID_OBJECT_ID)
3333             retval.insert(selected_fleet_id);
3334     }
3335     return retval;
3336 }
3337 
SelectedShipIDs() const3338 std::set<int> FleetWnd::SelectedShipIDs() const
3339 { return m_fleet_detail_panel->SelectedShipIDs(); }
3340 
GetNewFleetAggression() const3341 NewFleetAggression FleetWnd::GetNewFleetAggression() const {
3342     if (m_new_fleet_drop_target)
3343         return m_new_fleet_drop_target->GetNewFleetAggression();
3344     return INVALID_FLEET_AGGRESSION;
3345 }
3346 
FleetSelectionChanged(const GG::ListBox::SelectionSet & rows)3347 void FleetWnd::FleetSelectionChanged(const GG::ListBox::SelectionSet& rows) {
3348     // show appropriate fleet in detail panel.  if one fleet is selected, show
3349     // its ships.  if more than one fleet is selected or no fleets are selected
3350     // then show no ships.
3351     if (rows.size() == 1) {
3352         // find selected row and fleet
3353         bool found_row = false;
3354         for (auto it = m_fleets_lb->begin(); it != m_fleets_lb->end(); ++it) {
3355             if (rows.count(it)) {
3356                 m_fleet_detail_panel->SetFleet(FleetInRow(it));
3357                 found_row = true;
3358                 break;
3359             }
3360         }
3361         if (!found_row)
3362             m_fleet_detail_panel->SetFleet(INVALID_OBJECT_ID);
3363     } else {
3364         m_fleet_detail_panel->SetFleet(INVALID_OBJECT_ID);
3365     }
3366 
3367 
3368     for (auto it = m_fleets_lb->begin(); it != m_fleets_lb->end(); ++it) {
3369         try {
3370             if (auto* fleet_panel = boost::polymorphic_downcast<FleetDataPanel*>(!(**it).empty() ? (**it).at(0) : nullptr))
3371                 fleet_panel->Select(rows.count(it));
3372         } catch (const std::exception& e) {
3373             ErrorLogger() << "FleetWnd::FleetSelectionChanged caught exception: " << e.what();
3374             continue;
3375         }
3376     }
3377 
3378     m_fleet_detail_panel->Refresh();
3379     SelectedFleetsChangedSignal();
3380 }
3381 
FleetRightClicked(GG::ListBox::iterator it,const GG::Pt & pt,const GG::Flags<GG::ModKey> & modkeys)3382 void FleetWnd::FleetRightClicked(GG::ListBox::iterator it, const GG::Pt& pt, const GG::Flags<GG::ModKey>& modkeys) {
3383     int client_empire_id = HumanClientApp::GetApp()->EmpireID();
3384 
3385     auto fleet = Objects().get<Fleet>(FleetInRow(it));
3386     if (!fleet)
3387         return;
3388 
3389     auto system = Objects().get<System>(fleet->SystemID());
3390     std::set<int> ship_ids_set = fleet->ShipIDs();
3391 
3392     std::vector<int> damaged_ship_ids;
3393     std::vector<int> unfueled_ship_ids;
3394     std::vector<int> not_full_fighters_ship_ids;
3395 
3396     damaged_ship_ids.reserve(ship_ids_set.size());
3397     unfueled_ship_ids.reserve(ship_ids_set.size());
3398     not_full_fighters_ship_ids.reserve(ship_ids_set.size());
3399 
3400     for (const auto& ship : Objects().find<Ship>(ship_ids_set)) {
3401         if (!ship)
3402             continue;
3403 
3404         // find damaged ships
3405         if (ship->GetMeter(METER_STRUCTURE)->Initial() < ship->GetMeter(METER_MAX_STRUCTURE)->Initial())
3406             damaged_ship_ids.push_back(ship->ID());
3407         // find ships with no remaining fuel
3408         if (ship->GetMeter(METER_FUEL)->Initial() < 1.0f)
3409             unfueled_ship_ids.push_back(ship->ID());
3410         // find ships that can carry fighters but dont have a full complement of them
3411         if (ship->HasFighters() && ship->FighterCount() < ship->FighterMax())
3412             not_full_fighters_ship_ids.push_back(ship->ID());
3413     }
3414 
3415     // determine which other empires are at peace with client empire and have
3416     // an owned object in this fleet's system
3417     std::set<int> peaceful_empires_in_system;
3418     if (system) {
3419         for (auto& obj : Objects().find<const UniverseObject>(system->ObjectIDs())) {
3420             if (obj->GetVisibility(client_empire_id) < VIS_PARTIAL_VISIBILITY)
3421                 continue;
3422             if (obj->Owner() == client_empire_id || obj->Unowned())
3423                 continue;
3424             if (peaceful_empires_in_system.count(obj->Owner()))
3425                 continue;
3426             if (Empires().GetDiplomaticStatus(client_empire_id, obj->Owner()) < DIPLO_PEACE)
3427             { continue; }
3428             peaceful_empires_in_system.insert(obj->Owner());
3429         }
3430     }
3431 
3432 
3433     auto popup = GG::Wnd::Create<CUIPopupMenu>(pt.x, pt.y);
3434 
3435     // add a fleet popup command to send the fleet exploring, and stop it from exploring
3436     if (system
3437         && !ClientUI::GetClientUI()->GetMapWnd()->IsFleetExploring(fleet->ID())
3438         && !ClientPlayerIsModerator()
3439         && fleet->OwnedBy(client_empire_id))
3440     {
3441         auto explore_action = [fleet]() { ClientUI::GetClientUI()->GetMapWnd()->SetFleetExploring(fleet->ID()); };
3442 
3443         popup->AddMenuItem(GG::MenuItem(UserString("ORDER_FLEET_EXPLORE"),       false, false, explore_action));
3444         popup->AddMenuItem(GG::MenuItem(true));
3445     }
3446     else if (system
3447              && !ClientPlayerIsModerator()
3448              && fleet->OwnedBy(client_empire_id))
3449     {
3450         auto stop_explore_action = [fleet]() { ClientUI::GetClientUI()->GetMapWnd()->StopFleetExploring(fleet->ID()); };
3451         popup->AddMenuItem(GG::MenuItem(UserString("ORDER_CANCEL_FLEET_EXPLORE"), false, false, stop_explore_action));
3452         popup->AddMenuItem(GG::MenuItem(true));
3453     }
3454 
3455     // Merge fleets
3456     if (system
3457         && fleet->OwnedBy(client_empire_id)
3458         && !ClientPlayerIsModerator()
3459        )
3460     {
3461         auto merge_action = [fleet]() { MergeFleetsIntoFleet(fleet->ID()); };
3462         popup->AddMenuItem(GG::MenuItem(UserString("FW_MERGE_SYSTEM_FLEETS"),   false, false, merge_action));
3463         popup->AddMenuItem(GG::MenuItem(true));
3464     }
3465 
3466     // Split damaged ships - need some, but not all, ships damaged, and need to be in a system
3467     if (system
3468         && ship_ids_set.size() > 1
3469         && !damaged_ship_ids.empty()
3470         && damaged_ship_ids.size() != ship_ids_set.size()
3471         && fleet->OwnedBy(client_empire_id)
3472         && !ClientPlayerIsModerator()
3473        )
3474     {
3475         NewFleetAggression nfa = fleet->Aggressive() ? FLEET_AGGRESSIVE : FLEET_PASSIVE;
3476         auto split_damage_action = [&damaged_ship_ids, nfa]() { CreateNewFleetFromShips(damaged_ship_ids, nfa); };
3477         popup->AddMenuItem(GG::MenuItem(UserString("FW_SPLIT_DAMAGED_FLEET"),     false, false, split_damage_action));
3478     }
3479 
3480     // Split unfueled ships - need some, but not all, ships unfueled, and need to be in a system
3481     if (system
3482         && ship_ids_set.size() > 1
3483         && unfueled_ship_ids.size() > 0
3484         && unfueled_ship_ids.size() < ship_ids_set.size()
3485         && fleet->OwnedBy(client_empire_id)
3486         && !ClientPlayerIsModerator()
3487        )
3488     {
3489         auto split_unfueled_action = [fleet, &unfueled_ship_ids]() {
3490             NewFleetAggression nfa = fleet->Aggressive() ? FLEET_AGGRESSIVE : FLEET_PASSIVE;
3491             CreateNewFleetFromShips(unfueled_ship_ids, nfa);
3492         };
3493 
3494         popup->AddMenuItem(GG::MenuItem(UserString("FW_SPLIT_UNFUELED_FLEET"),    false, false, split_unfueled_action));
3495     }
3496 
3497     // Split ships with not as many fighters as they could contain
3498     if (system
3499         && ship_ids_set.size() > 1
3500         && not_full_fighters_ship_ids.size() > 0
3501         && not_full_fighters_ship_ids.size() < ship_ids_set.size()
3502         && fleet->OwnedBy(client_empire_id)
3503         && !ClientPlayerIsModerator()
3504        )
3505     {
3506         auto split_not_full_fighters_action = [fleet, &not_full_fighters_ship_ids]() {
3507             NewFleetAggression nfa = fleet->Aggressive() ? FLEET_AGGRESSIVE : FLEET_PASSIVE;
3508             CreateNewFleetFromShips(not_full_fighters_ship_ids, nfa);
3509         };
3510         popup->AddMenuItem(GG::MenuItem(UserString("FW_SPLIT_NOT_FULL_FIGHTERS_FLEET"), false, false, split_not_full_fighters_action));
3511     }
3512 
3513     // Split fleet - can't split fleets without more than one ship, or which are not in a system
3514     if (system
3515         && ship_ids_set.size() > 1
3516         && (fleet->OwnedBy(client_empire_id))
3517         && !ClientPlayerIsModerator()
3518        )
3519     {
3520         auto split_action = [this, &ship_ids_set]() {
3521             ScopedTimer split_fleet_timer("FleetWnd::SplitFleet", true);
3522             // remove first ship from set, so it stays in its existing fleet
3523             auto ship_id_it = ship_ids_set.begin();
3524             ship_ids_set.erase(ship_id_it);
3525 
3526             NewFleetAggression new_aggression_setting = INVALID_FLEET_AGGRESSION;
3527             if (m_new_fleet_drop_target)
3528                 new_aggression_setting = m_new_fleet_drop_target->GetNewFleetAggression();
3529 
3530             // assemble container of containers of ids of fleets to create.
3531             // one ship id per vector
3532             for (int ship_id : ship_ids_set) {
3533                 CreateNewFleetFromShips(
3534                     std::vector<int>{ship_id},
3535                     new_aggression_setting);
3536             }
3537         };
3538 
3539         auto split_per_design_action = [this, fleet]() {
3540             NewFleetAggression new_aggression_setting = INVALID_FLEET_AGGRESSION;
3541             if (m_new_fleet_drop_target)
3542                 new_aggression_setting = m_new_fleet_drop_target->GetNewFleetAggression();
3543 
3544             CreateNewFleetsFromShipsForEachDesign(fleet->ShipIDs(), new_aggression_setting);
3545         };
3546 
3547         popup->AddMenuItem(GG::MenuItem(UserString("FW_SPLIT_FLEET"),             false, false, split_action));
3548         popup->AddMenuItem(GG::MenuItem(UserString("FW_SPLIT_SHIPS_ALL_DESIGNS"), false, false, split_per_design_action));
3549         popup->AddMenuItem(GG::MenuItem(true));
3550     }
3551 
3552     // Rename fleet
3553     if (fleet->OwnedBy(client_empire_id)
3554         || ClientPlayerIsModerator())
3555     {
3556         auto rename_action = [fleet, client_empire_id]() {
3557             auto edit_wnd = GG::Wnd::Create<CUIEditWnd>(GG::X(350), UserString("ENTER_NEW_NAME"), fleet->Name());
3558             edit_wnd->Run();
3559 
3560             if (ClientPlayerIsModerator())
3561                 // TODO: handle moderator actions for this...
3562                 return;
3563 
3564             if (!RenameOrder::Check(client_empire_id, fleet->ID(), edit_wnd->Result()))
3565                 return;
3566 
3567             HumanClientApp::GetApp()->Orders().IssueOrder(
3568                 std::make_shared<RenameOrder>(client_empire_id, fleet->ID(), edit_wnd->Result()));
3569         };
3570         popup->AddMenuItem(GG::MenuItem(UserString("RENAME"),                       false, false, rename_action));
3571         popup->AddMenuItem(GG::MenuItem(true));
3572     }
3573 
3574     bool post_scrap_bar = false;
3575 
3576     // add a fleet popup command to order all ships in the fleet scrapped
3577     if (system
3578         && fleet->HasShipsWithoutScrapOrders()
3579         && !ClientPlayerIsModerator()
3580         && fleet->OwnedBy(client_empire_id))
3581     {
3582         auto scrap_action = [fleet, client_empire_id]() {
3583             std::set<int> ship_ids = fleet->ShipIDs();
3584             for (int ship_id : ship_ids) {
3585                 HumanClientApp::GetApp()->Orders().IssueOrder(
3586                     std::make_shared<ScrapOrder>(client_empire_id, ship_id));
3587             }
3588         };
3589 
3590         popup->AddMenuItem(GG::MenuItem(UserString("ORDER_FLEET_SCRAP"),          false, false, scrap_action));
3591         post_scrap_bar = true;
3592     }
3593 
3594     // add a fleet popup command to cancel all scrap orders on ships in this fleet
3595     if (system
3596         && fleet->HasShipsOrderedScrapped()
3597         && !ClientPlayerIsModerator())
3598     {
3599         auto unscrap_action = [fleet]() {
3600             const OrderSet orders = HumanClientApp::GetApp()->Orders();
3601             for (int ship_id : fleet->ShipIDs()) {
3602                 for (const auto& id_and_order : orders) {
3603                     if (std::shared_ptr<ScrapOrder> order = std::dynamic_pointer_cast<ScrapOrder>(id_and_order.second)) {
3604                         if (order->ObjectID() == ship_id) {
3605                             HumanClientApp::GetApp()->Orders().RescindOrder(id_and_order.first);
3606                             // could break here, but won't to ensure there are no problems with doubled orders
3607                         }
3608                     }
3609                 }
3610             }
3611         };
3612 
3613         popup->AddMenuItem(GG::MenuItem(UserString("ORDER_CANCEL_FLEET_SCRAP"),   false, false, unscrap_action));
3614         post_scrap_bar = true;
3615     }
3616 
3617     if (fleet->OwnedBy(client_empire_id)
3618         && fleet->TravelRoute().empty()
3619         && !peaceful_empires_in_system.empty()
3620         && !ClientPlayerIsModerator())
3621     {
3622         if (post_scrap_bar)
3623             popup->AddMenuItem(GG::MenuItem(true));
3624 
3625         // submenus for each available recipient empire
3626         GG::MenuItem give_away_menu(UserString("ORDER_GIVE_FLEET_TO_EMPIRE"), false, false);
3627         for (auto& entry : Empires()) {
3628             int recipient_empire_id = entry.first;
3629             if (!peaceful_empires_in_system.count(recipient_empire_id))
3630                 continue;
3631             auto gift_action = [recipient_empire_id, fleet, client_empire_id]() {
3632                 HumanClientApp::GetApp()->Orders().IssueOrder(
3633                     std::make_shared<GiveObjectToEmpireOrder>(client_empire_id, fleet->ID(), recipient_empire_id));
3634             };
3635             give_away_menu.next_level.push_back(GG::MenuItem(entry.second->Name(), false, false, gift_action));
3636         }
3637         popup->AddMenuItem(std::move(give_away_menu));
3638 
3639         if (fleet->OrderedGivenToEmpire() != ALL_EMPIRES) {
3640             auto ungift_action = [fleet]() {
3641                 const OrderSet orders = HumanClientApp::GetApp()->Orders();
3642                 for (const auto& id_and_order : orders) {
3643                     if (auto order = std::dynamic_pointer_cast<
3644                         GiveObjectToEmpireOrder>(id_and_order.second))
3645                     {
3646                         if (order->ObjectID() == fleet->ID()) {
3647                             HumanClientApp::GetApp()->Orders().RescindOrder(id_and_order.first);
3648                             // could break here, but won't to ensure there are no problems with doubled orders
3649                         }
3650                     }
3651                 }
3652             };
3653             GG::MenuItem cancel_give_away_menu(UserString("ORDER_CANCEL_GIVE_FLEET"), false, false, ungift_action);
3654             popup->AddMenuItem(std::move(cancel_give_away_menu));
3655         }
3656     }
3657 
3658 
3659     // Allow dismissal of stale visibility information
3660     if (!fleet->OwnedBy(client_empire_id)) {
3661         auto forget_fleet_action = [fleet]() {
3662             ClientUI::GetClientUI()->GetMapWnd()->ForgetObject(fleet->ID());
3663         };
3664         auto visibility_turn_map =
3665             GetUniverse().GetObjectVisibilityTurnMapByEmpire(fleet->ID(), client_empire_id);
3666         auto last_turn_visible_it = visibility_turn_map.find(VIS_BASIC_VISIBILITY);
3667         if (last_turn_visible_it != visibility_turn_map.end()
3668             && last_turn_visible_it->second < CurrentTurn())
3669         {
3670             popup->AddMenuItem(GG::MenuItem(UserString("FW_ORDER_DISMISS_SENSOR_GHOST"), false, false, forget_fleet_action));
3671         }
3672     }
3673 
3674     popup->Run();
3675 }
3676 
FleetLeftClicked(GG::ListBox::iterator it,const GG::Pt & pt,const GG::Flags<GG::ModKey> & modkeys)3677 void FleetWnd::FleetLeftClicked(GG::ListBox::iterator it, const GG::Pt& pt, const GG::Flags<GG::ModKey>& modkeys)
3678 { ClickedSignal(std::static_pointer_cast<FleetWnd>(shared_from_this())); }
3679 
FleetDoubleClicked(GG::ListBox::iterator it,const GG::Pt & pt,const GG::Flags<GG::ModKey> & modkeys)3680 void FleetWnd::FleetDoubleClicked(GG::ListBox::iterator it, const GG::Pt& pt, const GG::Flags<GG::ModKey>& modkeys)
3681 { ClickedSignal(std::static_pointer_cast<FleetWnd>(shared_from_this())); }
3682 
FleetInRow(GG::ListBox::iterator it) const3683 int FleetWnd::FleetInRow(GG::ListBox::iterator it) const {
3684     if (it == m_fleets_lb->end())
3685         return INVALID_OBJECT_ID;
3686 
3687     try {
3688         //DebugLogger() << "FleetWnd::FleetInRow casting iterator to fleet row";
3689         if (FleetRow* fleet_row = dynamic_cast<FleetRow*>(it->get())) {
3690             return fleet_row->FleetID();
3691         }
3692     } catch (const std::exception& e) {
3693         ErrorLogger() << "FleetInRow caught exception: " << e.what();
3694     }
3695 
3696     return INVALID_OBJECT_ID;
3697 }
3698 
3699 namespace {
SystemNameNearestToFleet(int client_empire_id,int fleet_id)3700     std::string SystemNameNearestToFleet(int client_empire_id, int fleet_id) {
3701         auto fleet = Objects().get<Fleet>(fleet_id);
3702         if (!fleet)
3703             return "";
3704 
3705         int nearest_system_id(GetPathfinder()->NearestSystemTo(fleet->X(), fleet->Y()));
3706         if (auto system = Objects().get<System>(nearest_system_id)) {
3707             const std::string& sys_name = system->ApparentName(client_empire_id);
3708             return sys_name;
3709         }
3710         return "";
3711     }
3712 }
3713 
TitleText() const3714 std::string FleetWnd::TitleText() const {
3715     // if no fleets available, default to indicating no fleets
3716     if (m_fleet_ids.empty())
3717         return UserString("FW_NO_FLEET");
3718 
3719     int client_empire_id = HumanClientApp::GetApp()->EmpireID();
3720 
3721     // at least one fleet is available, so show appropriate title this
3722     // FleetWnd's empire and system
3723     const Empire* empire = GetEmpire(m_empire_id);
3724 
3725     if (auto system = Objects().get<System>(m_system_id)) {
3726         const std::string& sys_name = system->ApparentName(client_empire_id);
3727         return (empire
3728                 ? boost::io::str(FlexibleFormat(UserString("FW_EMPIRE_FLEETS_AT_SYSTEM")) %
3729                                  empire->Name() % sys_name)
3730                 : boost::io::str(FlexibleFormat(UserString("FW_GENERIC_FLEETS_AT_SYSTEM")) %
3731                                  sys_name));
3732     }
3733 
3734     const std::string sys_name = SystemNameNearestToFleet(client_empire_id, *m_fleet_ids.begin());
3735     if (!sys_name.empty()) {
3736         return (empire
3737                 ? boost::io::str(FlexibleFormat(UserString("FW_EMPIRE_FLEETS_NEAR_SYSTEM")) %
3738                                  empire->Name() % sys_name)
3739                 : boost::io::str(FlexibleFormat(UserString("FW_GENERIC_FLEETS_NEAR_SYSTEM")) %
3740                                  sys_name));
3741     }
3742 
3743     return (empire
3744             ? boost::io::str(FlexibleFormat(UserString("FW_EMPIRE_FLEETS")) %
3745                              empire->Name())
3746             : boost::io::str(FlexibleFormat(UserString("FW_GENERIC_FLEETS"))));
3747 }
3748 
CreateNewFleetFromDrops(const std::vector<int> & ship_ids)3749 void FleetWnd::CreateNewFleetFromDrops(const std::vector<int>& ship_ids) {
3750     DebugLogger() << "FleetWnd::CreateNewFleetFromDrops with " << ship_ids.size() << " ship ids";
3751 
3752     if (ship_ids.empty())
3753         return;
3754 
3755     NewFleetAggression aggression = INVALID_FLEET_AGGRESSION;
3756     if (m_new_fleet_drop_target)
3757         aggression = m_new_fleet_drop_target->GetNewFleetAggression();
3758 
3759     // deselect all ships so that response to fleet rearrangement doesn't attempt
3760     // to get the selected ships that are no longer in their old fleet.
3761     m_fleet_detail_panel->SelectShips(std::set<int>());
3762 
3763     CreateNewFleetFromShips(ship_ids, aggression);
3764 }
3765 
ShipSelectionChanged(const GG::ListBox::SelectionSet & rows)3766 void FleetWnd::ShipSelectionChanged(const GG::ListBox::SelectionSet& rows)
3767 { SelectedShipsChangedSignal(); }
3768 
UniverseObjectDeleted(std::shared_ptr<const UniverseObject> obj)3769 void FleetWnd::UniverseObjectDeleted(std::shared_ptr<const UniverseObject> obj) {
3770     // check if deleted object was a fleet.  The universe signals for all
3771     // object types, not just fleets.
3772     std::shared_ptr<const Fleet> deleted_fleet = std::dynamic_pointer_cast<const Fleet>(obj);
3773     if (!deleted_fleet)
3774         return;
3775 
3776     // if detail panel is showing the deleted fleet, reset to show nothing
3777     if (Objects().get<Fleet>(m_fleet_detail_panel->FleetID()) == deleted_fleet)
3778         m_fleet_detail_panel->SetFleet(INVALID_OBJECT_ID);
3779 
3780     const ObjectMap& objects = GetUniverse().Objects();
3781 
3782     // remove deleted fleet's row
3783     for (auto it = m_fleets_lb->begin(); it != m_fleets_lb->end(); ++it) {
3784         int row_fleet_id = FleetInRow(it);
3785         if (objects.get<Fleet>(row_fleet_id) == deleted_fleet) {
3786             m_fleets_lb->Erase(it);
3787             break;
3788         }
3789     }
3790 }
3791 
EnableOrderIssuing(bool enable)3792 void FleetWnd::EnableOrderIssuing(bool enable/* = true*/) {
3793     m_order_issuing_enabled = enable;
3794     if (m_new_fleet_drop_target)
3795         m_new_fleet_drop_target->Disable(!m_order_issuing_enabled);
3796     if (m_fleets_lb)
3797         m_fleets_lb->EnableOrderIssuing(m_order_issuing_enabled);
3798     if (m_fleet_detail_panel)
3799         m_fleet_detail_panel->EnableOrderIssuing(m_order_issuing_enabled);
3800 }
3801