1 #include "BuildingsPanel.h"
2
3 #include <GG/Button.h>
4 #include <GG/StaticGraphic.h>
5
6 #include "../util/i18n.h"
7 #include "../util/Logger.h"
8 #include "../util/OptionsDB.h"
9 #include "../util/Order.h"
10 #include "../universe/Building.h"
11 #include "../universe/BuildingType.h"
12 #include "../universe/Effect.h"
13 #include "../universe/Planet.h"
14 #include "../universe/Enums.h"
15 #include "../Empire/Empire.h"
16 #include "../client/human/HumanClientApp.h"
17 #include "CUIControls.h"
18 #include "IconTextBrowseWnd.h"
19 #include "MapWnd.h"
20 #include "MultiIconValueIndicator.h"
21 #include "MultiMeterStatusBar.h"
22
23 namespace {
24 /** How big we want meter icons with respect to the current UI font size.
25 * Meters should scale along font size, but not below the size for the
26 * default 12 points font. */
MeterIconSize()27 GG::Pt MeterIconSize() {
28 const int icon_size = std::max(ClientUI::Pts(), 12) * 4/3;
29 return GG::Pt(GG::X(icon_size), GG::Y(icon_size));
30 }
31
32 /** Returns map from object ID to issued colonize orders affecting it. */
PendingScrapOrders()33 std::map<int, int> PendingScrapOrders() {
34 std::map<int, int> retval;
35 const ClientApp* app = ClientApp::GetApp();
36 if (!app)
37 return retval;
38 for (const auto& id_and_order : app->Orders()) {
39 if (auto order = std::dynamic_pointer_cast<ScrapOrder>(id_and_order.second)) {
40 retval[order->ObjectID()] = id_and_order.first;
41 }
42 }
43 return retval;
44 }
45
ClientPlayerIsModerator()46 bool ClientPlayerIsModerator()
47 { return HumanClientApp::GetApp()->GetClientType() == Networking::CLIENT_TYPE_HUMAN_MODERATOR; }
48 }
49
BuildingsPanel(GG::X w,int columns,int planet_id)50 BuildingsPanel::BuildingsPanel(GG::X w, int columns, int planet_id) :
51 AccordionPanel(w, GG::Y(ClientUI::Pts()*2)),
52 m_planet_id(planet_id),
53 m_columns(columns),
54 m_building_indicators()
55 {}
56
CompleteConstruction()57 void BuildingsPanel::CompleteConstruction() {
58 AccordionPanel::CompleteConstruction();
59
60 SetName("BuildingsPanel");
61
62 if (m_columns < 1) {
63 ErrorLogger() << "Attempted to create a BuidingsPanel with less than 1 column";
64 m_columns = 1;
65 }
66
67 m_expand_button->LeftPressedSignal.connect(
68 boost::bind(&BuildingsPanel::ExpandCollapseButtonPressed, this));
69
70 // get owner, connect its production queue changed signal to update this panel
71 auto planet = Objects().get(m_planet_id);
72 if (planet) {
73 if (const Empire* empire = GetEmpire(planet->Owner())) {
74 const ProductionQueue& queue = empire->GetProductionQueue();
75 queue.ProductionQueueChangedSignal.connect(
76 boost::bind(&BuildingsPanel::RequirePreRender, this));
77 }
78 }
79
80 RequirePreRender();
81 }
82
~BuildingsPanel()83 BuildingsPanel::~BuildingsPanel()
84 {}
85
ExpandCollapse(bool expanded)86 void BuildingsPanel::ExpandCollapse(bool expanded) {
87 if (expanded == s_expanded_map[m_planet_id]) return; // nothing to do
88 s_expanded_map[m_planet_id] = expanded;
89
90 DoLayout();
91 }
92
Update()93 void BuildingsPanel::Update() {
94 // remove old indicators
95 for (auto& indicator : m_building_indicators)
96 DetachChild(indicator.get());
97 m_building_indicators.clear();
98
99 auto planet = Objects().get<Planet>(m_planet_id);
100 if (!planet) {
101 ErrorLogger() << "BuildingsPanel::Update couldn't get planet with id " << m_planet_id;
102 return;
103 }
104 int system_id = planet->SystemID();
105
106 const int indicator_size = static_cast<int>(Value(Width() * 1.0 / m_columns));
107
108 int this_client_empire_id = HumanClientApp::GetApp()->EmpireID();
109 const auto& this_client_known_destroyed_objects = GetUniverse().EmpireKnownDestroyedObjectIDs(this_client_empire_id);
110 const auto& this_client_stale_object_info = GetUniverse().EmpireStaleKnowledgeObjectIDs(this_client_empire_id);
111
112 // get existing / finished buildings and use them to create building indicators
113 for (int object_id : planet->BuildingIDs()) {
114 // skip known destroyed and stale info objects
115 if (this_client_known_destroyed_objects.count(object_id))
116 continue;
117 if (this_client_stale_object_info.count(object_id))
118 continue;
119
120 auto building = Objects().get<Building>(object_id);
121 if (!building) {
122 ErrorLogger() << "BuildingsPanel::Update couldn't get building with id: " << object_id << " on planet " << planet->Name();
123 continue;
124 }
125
126 if (building->SystemID() != system_id || building->PlanetID() != m_planet_id)
127 continue;
128
129 auto ind = GG::Wnd::Create<BuildingIndicator>(GG::X(indicator_size), object_id);
130 m_building_indicators.push_back(ind);
131
132 ind->RightClickedSignal.connect(BuildingRightClickedSignal);
133 }
134
135 // get in-progress buildings
136 const Empire* empire = GetEmpire(planet->Owner());
137 if (!empire)
138 return;
139
140 int queue_index = -1;
141 for (const auto& elem : empire->GetProductionQueue()) {
142 ++queue_index;
143 if (elem.item.build_type != BT_BUILDING) continue; // don't show in-progress ships in BuildingsPanel...
144 if (elem.location != m_planet_id) continue; // don't show buildings located elsewhere
145
146 double total_cost;
147 int total_turns;
148 double turn_spending = elem.allocated_pp;
149 std::tie(total_cost, total_turns) = empire->ProductionCostAndTime(elem);
150
151 double progress = std::max(0.0f, empire->ProductionStatus(queue_index));
152 double turns_completed = progress / std::max(total_cost, 1.0);
153 auto ind = GG::Wnd::Create<BuildingIndicator>(GG::X(indicator_size), elem.item.name,
154 turns_completed, total_turns, total_cost, turn_spending);
155
156 m_building_indicators.push_back(ind);
157 }
158 }
159
PreRender()160 void BuildingsPanel::PreRender() {
161 AccordionPanel::PreRender();
162 RefreshImpl();
163 }
164
Refresh()165 void BuildingsPanel::Refresh() {
166 RequirePreRender();
167 }
168
RefreshImpl()169 void BuildingsPanel::RefreshImpl() {
170 Update();
171 DoLayout();
172 }
173
EnableOrderIssuing(bool enable)174 void BuildingsPanel::EnableOrderIssuing(bool enable/* = true*/) {
175 for (auto& indicator : m_building_indicators)
176 { indicator->EnableOrderIssuing(enable); }
177 }
178
ExpandCollapseButtonPressed()179 void BuildingsPanel::ExpandCollapseButtonPressed()
180 { ExpandCollapse(!s_expanded_map[m_planet_id]); }
181
DoLayout()182 void BuildingsPanel::DoLayout() {
183 auto old_size = Size();
184 AccordionPanel::DoLayout();
185
186 int row = 0;
187 int column = 0;
188 const int padding = 5; // space around and between adjacent indicators
189 const GG::X effective_width = Width() - padding * (m_columns + 1); // padding on either side and between
190 const int indicator_size = static_cast<int>(Value(effective_width * 1.0 / m_columns));
191 GG::Y height;
192
193 // update size of panel and position and visibility of widgets
194 if (!s_expanded_map[m_planet_id]) {
195 int n = 0;
196 for (auto& ind : m_building_indicators) {
197 const GG::Pt ul = GG::Pt(MeterIconSize().x * n, GG::Y0);
198 const GG::Pt lr = ul + MeterIconSize();
199
200 if (lr.x < Width() - m_expand_button->Width()) {
201 ind->SizeMove(ul, lr);
202 AttachChild(ind);
203 } else {
204 DetachChild(ind.get());
205 }
206 ++n;
207 }
208
209 height = m_expand_button->Height();
210 } else {
211 for (auto& ind : m_building_indicators) {
212 const GG::Pt ul = GG::Pt(GG::X(padding * (column + 1) + indicator_size * column), GG::Y(padding * (row + 1) + indicator_size * row));
213 const GG::Pt lr = ul + GG::Pt(GG::X(indicator_size), GG::Y(indicator_size));
214
215 ind->SizeMove(ul, lr);
216 AttachChild(ind);
217 ind->Show();
218
219 ++column;
220 if (column >= m_columns) {
221 column = 0;
222 ++row;
223 }
224 }
225
226 if (column == 0)
227 height = GG::Y(padding * (row + 1) + row * indicator_size); // if column is 0, then there are no buildings in the next row
228 else
229 height = GG::Y(padding * (row + 2) + (row + 1) * indicator_size); // if column != 0, there are buildings in the next row, so need to make space
230 }
231
232 if (m_building_indicators.empty()) {
233 height = GG::Y(0); // hide if empty
234 DetachChild(m_expand_button.get());
235 } else {
236 AttachChild(m_expand_button);
237 m_expand_button->Show();
238 if (height < MeterIconSize().y)
239 height = MeterIconSize().y;
240 }
241
242 Resize(GG::Pt(Width(), height));
243
244 SetCollapsed(!s_expanded_map[m_planet_id]);
245
246 if (old_size != Size())
247 if (auto&& parent = Parent())
248 parent->RequirePreRender();
249 }
250
251 std::map<int, bool> BuildingsPanel::s_expanded_map;
252
253 /////////////////////////////////////
254 // BuildingIndicator //
255 /////////////////////////////////////
256 ScanlineRenderer BuildingIndicator::s_scanline_shader;
257
BuildingIndicator(GG::X w,int building_id)258 BuildingIndicator::BuildingIndicator(GG::X w, int building_id) :
259 GG::Wnd(GG::X0, GG::Y0, w, GG::Y(Value(w)), GG::INTERACTIVE),
260 m_building_id(building_id)
261 {
262 if (auto building = Objects().get<Building>(m_building_id))
263 building->StateChangedSignal.connect(
264 boost::bind(&BuildingIndicator::RequirePreRender, this));
265 }
266
BuildingIndicator(GG::X w,const std::string & building_type,double turns_completed,double total_turns,double total_cost,double turn_spending)267 BuildingIndicator::BuildingIndicator(GG::X w, const std::string& building_type,
268 double turns_completed, double total_turns, double total_cost, double turn_spending) :
269 GG::Wnd(GG::X0, GG::Y0, w, GG::Y(Value(w)), GG::INTERACTIVE),
270 m_building_id(INVALID_OBJECT_ID)
271 {
272 auto texture = ClientUI::BuildingIcon(building_type);
273
274 const BuildingType* type = GetBuildingType(building_type);
275 const std::string& desc = type ? type->Description() : "";
276
277 SetBrowseInfoWnd(GG::Wnd::Create<IconTextBrowseWnd>(
278 texture, UserString(building_type), UserString(desc)));
279
280 m_graphic = GG::Wnd::Create<GG::StaticGraphic>(texture, GG::GRAPHIC_FITGRAPHIC | GG::GRAPHIC_PROPSCALE);
281
282 float next_progress = turn_spending / std::max(1.0, total_cost);
283
284 m_progress_bar = GG::Wnd::Create<MultiTurnProgressBar>(total_turns,
285 turns_completed,
286 next_progress,
287 GG::LightenClr(ClientUI::TechWndProgressBarBackgroundColor()),
288 ClientUI::TechWndProgressBarColor(),
289 GG::LightenClr(ClientUI::ResearchableTechFillColor()));
290
291 }
292
CompleteConstruction()293 void BuildingIndicator::CompleteConstruction() {
294 GG::Wnd::CompleteConstruction();
295
296 if (m_building_id == INVALID_OBJECT_ID) {
297 AttachChild(m_graphic);
298 AttachChild(m_progress_bar);
299 RequirePreRender();
300 } else {
301 Refresh();
302 }
303 }
304
Render()305 void BuildingIndicator::Render() {
306 // copied from CUIWnd
307 GG::Pt ul = UpperLeft();
308 GG::Pt lr = LowerRight();
309
310 // Draw outline and background...
311 GG::FlatRectangle(ul, lr, ClientUI::WndColor(), ClientUI::WndOuterBorderColor(), 1);
312
313 // Scanlines for not currently-visible objects?
314 int empire_id = HumanClientApp::GetApp()->EmpireID();
315 if (empire_id == ALL_EMPIRES || !GetOptionsDB().Get<bool>("ui.map.scanlines.shown"))
316 return;
317 if (m_building_id == INVALID_OBJECT_ID)
318 return;
319 if (GetUniverse().GetObjectVisibilityByEmpire(m_building_id, empire_id) >= VIS_BASIC_VISIBILITY)
320 return;
321
322 s_scanline_shader.StartUsing();
323
324 GLfloat verts[8];
325 verts[0] = Value(ul.x); verts[1] = Value(ul.y);
326 verts[2] = Value(lr.x); verts[3] = Value(ul.y);
327 verts[4] = Value(lr.x); verts[5] = Value(lr.y);
328 verts[6] = Value(ul.x); verts[7] = Value(lr.y);
329
330 glDisable(GL_TEXTURE_2D);
331 glPushClientAttrib(GL_CLIENT_ALL_ATTRIB_BITS);
332 glEnableClientState(GL_VERTEX_ARRAY);
333 glDisableClientState(GL_COLOR_ARRAY);
334 glDisableClientState(GL_TEXTURE_COORD_ARRAY);
335
336 glVertexPointer(2, GL_FLOAT, 0, verts);
337 glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
338
339 glPopClientAttrib();
340 glEnable(GL_TEXTURE_2D);
341
342 s_scanline_shader.StopUsing();
343 }
344
PreRender()345 void BuildingIndicator::PreRender() {
346 GG::Wnd::PreRender();
347 Refresh();
348 }
349
Refresh()350 void BuildingIndicator::Refresh() {
351 SetBrowseModeTime(GetOptionsDB().Get<int>("ui.tooltip.delay"));
352
353 auto building = Objects().get<Building>(m_building_id);
354 if (!building)
355 return;
356
357 ClearBrowseInfoWnd();
358
359 DetachChildAndReset(m_graphic);
360 DetachChildAndReset(m_scrap_indicator);
361
362 if (const BuildingType* type = GetBuildingType(building->BuildingTypeName())) {
363 auto texture = ClientUI::BuildingIcon(type->Name());
364 m_graphic = GG::Wnd::Create<GG::StaticGraphic>(texture, GG::GRAPHIC_FITGRAPHIC | GG::GRAPHIC_PROPSCALE);
365 AttachChild(m_graphic);
366
367 std::string desc = UserString(type->Description());
368 if (building->GetMeter(METER_STEALTH))
369 desc = UserString("METER_STEALTH") + boost::io::str(boost::format(": %3.1f\n\n") % building->GetMeter(METER_STEALTH)->Current()) + desc;
370 if (GetOptionsDB().Get<bool>("resource.effects.description.shown") && !type->Effects().empty())
371 desc += "\n" + Dump(type->Effects());
372
373 SetBrowseInfoWnd(GG::Wnd::Create<IconTextBrowseWnd>(
374 texture, UserString(type->Name()), desc));
375 }
376
377 if (building && building->OrderedScrapped()) {
378 auto scrap_texture = ClientUI::GetTexture(ClientUI::ArtDir() / "misc" / "scrapped.png", true);
379 m_scrap_indicator = GG::Wnd::Create<GG::StaticGraphic>(scrap_texture, GG::GRAPHIC_FITGRAPHIC | GG::GRAPHIC_PROPSCALE);
380 AttachChild(m_scrap_indicator);
381 }
382
383 DoLayout();
384 }
385
SizeMove(const GG::Pt & ul,const GG::Pt & lr)386 void BuildingIndicator::SizeMove(const GG::Pt& ul, const GG::Pt& lr) {
387 GG::Pt old_size = Size();
388
389 GG::Wnd::SizeMove(ul, lr);
390
391 if (old_size != Size())
392 DoLayout();
393 }
394
MouseWheel(const GG::Pt & pt,int move,GG::Flags<GG::ModKey> mod_keys)395 void BuildingIndicator::MouseWheel(const GG::Pt& pt, int move, GG::Flags<GG::ModKey> mod_keys)
396 { ForwardEventToParent(); }
397
RClick(const GG::Pt & pt,GG::Flags<GG::ModKey> mod_keys)398 void BuildingIndicator::RClick(const GG::Pt& pt, GG::Flags<GG::ModKey> mod_keys) {
399 // verify that this indicator represents an existing building, and not a
400 // queued production item, and that the owner of the building is this
401 // client's player's empire
402 int empire_id = HumanClientApp::GetApp()->EmpireID();
403 auto building = Objects().get<Building>(m_building_id);
404 if (!building)
405 return;
406
407 const auto& map_wnd = ClientUI::GetClientUI()->GetMapWnd();
408 if (ClientPlayerIsModerator() && map_wnd->GetModeratorActionSetting() != MAS_NoAction) {
409 RightClickedSignal(m_building_id); // response handled in MapWnd
410 return;
411 }
412
413 auto scrap_building_action = [this, empire_id]()
414 { HumanClientApp::GetApp()->Orders().IssueOrder(std::make_shared<ScrapOrder>(empire_id, m_building_id)); };
415
416 auto un_scrap_building_action = [building]() {
417 // find order to scrap this building, and recind it
418 auto pending_scrap_orders = PendingScrapOrders();
419 auto it = pending_scrap_orders.find(building->ID());
420 if (it != pending_scrap_orders.end())
421 HumanClientApp::GetApp()->Orders().RescindOrder(it->second);
422 };
423
424 auto popup = GG::Wnd::Create<CUIPopupMenu>(pt.x, pt.y);
425
426 if (m_order_issuing_enabled) {
427 if (!building->OrderedScrapped()) {
428 // create popup menu with "Scrap" option
429 popup->AddMenuItem(GG::MenuItem(UserString("ORDER_BUIDLING_SCRAP"), false, false, scrap_building_action));
430 } else {
431 // create popup menu with "Cancel Scrap" option
432 popup->AddMenuItem(GG::MenuItem(UserString("ORDER_CANCEL_BUIDLING_SCRAP"), false, false, un_scrap_building_action));
433 }
434 }
435
436 const std::string& building_type = building->BuildingTypeName();
437 const BuildingType* bt = GetBuildingType(building_type);
438 if (bt) {
439 auto pedia_lookup_building_type_action = [building_type]() {
440 ClientUI::GetClientUI()->ZoomToBuildingType(building_type);
441 };
442 std::string popup_label = boost::io::str(FlexibleFormat(UserString("ENC_LOOKUP")) % UserString(building_type));
443 popup->AddMenuItem(GG::MenuItem(popup_label, false, false, pedia_lookup_building_type_action));
444 }
445
446 popup->Run();
447 }
448
EnableOrderIssuing(bool enable)449 void BuildingIndicator::EnableOrderIssuing(bool enable/* = true*/)
450 { m_order_issuing_enabled = enable; }
451
DoLayout()452 void BuildingIndicator::DoLayout() {
453 GG::Pt child_lr = Size() - GG::Pt(GG::X1, GG::Y1); // extra pixel prevents graphic from overflowing border box
454
455 if (m_graphic)
456 m_graphic->SizeMove(GG::Pt(GG::X0, GG::Y0), child_lr);
457
458 if (m_scrap_indicator)
459 m_scrap_indicator->SizeMove(GG::Pt(GG::X0, GG::Y0), child_lr);
460
461 GG::Y bar_top = Height() * 4 / 5;
462 if (m_progress_bar)
463 m_progress_bar->SizeMove(GG::Pt(GG::X0, bar_top), child_lr);
464 }
465