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