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, ¬_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