1 #include "TechTreeWnd.h"
2
3 #include "ClientUI.h"
4 #include "CUIControls.h"
5 #include "CUIDrawUtil.h"
6 #include "CUIWnd.h"
7 #include "Sound.h"
8 #include "IconTextBrowseWnd.h"
9 #include "EncyclopediaDetailPanel.h"
10 #include "../client/human/HumanClientApp.h"
11 #include "../util/i18n.h"
12 #include "../util/Logger.h"
13 #include "../util/OptionsDB.h"
14 #include "../util/Directories.h"
15 #include "../util/ScopedTimer.h"
16 #include "../universe/Effects.h"
17 #include "../universe/Enums.h"
18 #include "../universe/Tech.h"
19 #include "../universe/UnlockableItem.h"
20 #include "../universe/ValueRef.h"
21 #include "../Empire/Empire.h"
22 #include "TechTreeLayout.h"
23 #include "TechTreeArcs.h"
24 #include "Hotkeys.h"
25
26 #include <GG/GLClientAndServerBuffer.h>
27 #include <GG/GUI.h>
28 #include <GG/Layout.h>
29 #include <GG/StaticGraphic.h>
30
31 #include <algorithm>
32
33 namespace {
34 const std::string RES_PEDIA_WND_NAME = "research.pedia";
35 const std::string RES_CONTROLS_WND_NAME = "research.control";
36
37 // command-line options
AddOptions(OptionsDB & db)38 void AddOptions(OptionsDB& db) {
39 db.Add("ui.research.tree.spacing.horizontal", UserStringNop("OPTIONS_DB_UI_TECH_LAYOUT_HORZ_SPACING"),
40 0.25, RangedStepValidator<double>(0.25, 0.25, 4.0));
41 db.Add("ui.research.tree.spacing.vertical", UserStringNop("OPTIONS_DB_UI_TECH_LAYOUT_VERT_SPACING"),
42 0.75, RangedStepValidator<double>(0.25, 0.25, 4.0));
43 db.Add("ui.research.tree.zoom.scale", UserStringNop("OPTIONS_DB_UI_TECH_LAYOUT_ZOOM_SCALE"),
44 1.0, RangedStepValidator<double>(1.0, -25.0, 10.0));
45 db.Add("ui.research.control.graphic.size", UserStringNop("OPTIONS_DB_UI_TECH_CTRL_ICON_SIZE"),
46 3.0, RangedStepValidator<double>(0.25, 0.5, 12.0));
47 db.Add("ui." + RES_PEDIA_WND_NAME + ".hidden.enabled", UserStringNop("OPTIONS_DB_RESEARCH_PEDIA_HIDDEN"), false);
48
49 // TechListBox::TechRow column widths
50 int default_pts = 16;
51 db.Add("ui.research.list.column.graphic.width", UserStringNop("OPTIONS_DB_UI_TECH_LISTBOX_COL_WIDTH_GRAPHIC"),
52 default_pts * 3, StepValidator<int>(1));
53 db.Add("ui.research.list.column.name.width", UserStringNop("OPTIONS_DB_UI_TECH_LISTBOX_COL_WIDTH_NAME"),
54 default_pts * 18, StepValidator<int>(1));
55 db.Add("ui.research.list.column.cost.width", UserStringNop("OPTIONS_DB_UI_TECH_LISTBOX_COL_WIDTH_COST"),
56 default_pts * 8, StepValidator<int>(1));
57 db.Add("ui.research.list.column.time.width", UserStringNop("OPTIONS_DB_UI_TECH_LISTBOX_COL_WIDTH_TIME"),
58 default_pts * 6, StepValidator<int>(1));
59 db.Add("ui.research.list.column.category.width", UserStringNop("OPTIONS_DB_UI_TECH_LISTBOX_COL_WIDTH_CATEGORY"),
60 default_pts * 12, StepValidator<int>(1));
61 db.Add("ui.research.list.column.description.width", UserStringNop("OPTIONS_DB_UI_TECH_LISTBOX_COL_WIDTH_DESCRIPTION"),
62 default_pts * 18, StepValidator<int>(1));
63
64 // Default status for TechTreeControl filter.
65 db.Add<bool>("ui.research.status.unresearchable.shown", UserStringNop("OPTIONS_DB_UI_TECH_TREE_STATUS_UNRESEARCHABLE"), false);
66 db.Add<bool>("ui.research.status.partial.shown", UserStringNop("OPTIONS_DB_UI_TECH_TREE_STATUS_HAS_RESEARCHED_PREREQ"), true);
67 db.Add<bool>("ui.research.status.researchable.shown", UserStringNop("OPTIONS_DB_UI_TECH_TREE_STATUS_RESEARCHABLE"), true);
68 db.Add<bool>("ui.research.status.completed.shown", UserStringNop("OPTIONS_DB_UI_TECH_TREE_STATUS_COMPLETED"), true);
69 }
70 bool temp_bool = RegisterOptions(&AddOptions);
71
TechPanelWidth()72 GG::X TechPanelWidth()
73 { return GG::X(ClientUI::Pts()*38); }
TechPanelHeight()74 GG::Y TechPanelHeight()
75 { return GG::Y(ClientUI::Pts()*6); }
76
77 const double ZOOM_STEP_SIZE = 1.12;
78 const double MIN_SCALE = std::pow(ZOOM_STEP_SIZE, -25.0);
79 const double MAX_SCALE = std::pow(ZOOM_STEP_SIZE, 10.0);
80 const double INITIAL_SCALE = std::pow(ZOOM_STEP_SIZE, -5.0);
81
82 const double PI = 3.1415926535897932384626433;
83
TechVisible(const std::string & tech_name,const std::set<std::string> & categories_shown,const std::set<TechStatus> & statuses_shown)84 bool TechVisible(const std::string& tech_name,
85 const std::set<std::string>& categories_shown,
86 const std::set<TechStatus>& statuses_shown)
87 {
88 const Tech* tech = GetTech(tech_name);
89 if (!tech)
90 return false;
91
92 // Unresearchable techs are never to be shown on tree
93 if (!tech->Researchable())
94 return false;
95
96 // check that category is visible
97 if (!categories_shown.count(tech->Category()))
98 return false;
99
100 // check tech status
101 const Empire* empire = GetEmpire(HumanClientApp::GetApp()->EmpireID());
102 if (!empire)
103 return true; // if no empire, techs have no status, so just return true
104 if (!statuses_shown.count(empire->GetTechStatus(tech_name)))
105 return false;
106
107 // all tests pass, so tech is visible
108 return true;
109 }
110 }
111
112 ///////////////////////////
113 // TechRowBrowseWnd //
114 ///////////////////////////
TechRowBrowseWnd(const std::string & tech_name,int empire_id)115 std::shared_ptr<GG::BrowseInfoWnd> TechRowBrowseWnd(const std::string& tech_name, int empire_id) {
116 const Empire* empire = GetEmpire(empire_id);
117 const Tech* tech = GetTech(tech_name);
118 if (!tech)
119 return nullptr;
120
121 std::string main_text;
122
123 main_text += UserString(tech->Category()) + " - ";
124 main_text += UserString(tech->ShortDescription()) + "\n";
125
126 if (empire) {
127 TechStatus tech_status = empire->GetTechStatus(tech_name);
128 if (!tech->Researchable()) {
129 main_text += UserString("TECH_WND_UNRESEARCHABLE") + "\n";
130
131 } else if (tech_status == TS_UNRESEARCHABLE) {
132 main_text += UserString("TECH_WND_STATUS_LOCKED") + "\n";
133
134 std::vector<std::string> unresearched_prereqs;
135 for (const std::string& prereq : tech->Prerequisites()) {
136 TechStatus prereq_status = empire->GetTechStatus(prereq);
137 if (prereq_status != TS_COMPLETE)
138 unresearched_prereqs.push_back(prereq);
139 }
140 if (!unresearched_prereqs.empty()) {
141 main_text += UserString("TECH_WND_UNRESEARCHED_PREREQUISITES");
142 for (const std::string& prereq : unresearched_prereqs)
143 { main_text += UserString(prereq) + " "; }
144 main_text += "\n";
145 }
146
147 } else if (tech_status == TS_RESEARCHABLE) {
148 main_text += UserString("TECH_WND_STATUS_RESEARCHABLE") + "\n";
149
150 } else if (tech_status == TS_COMPLETE) {
151 main_text += UserString("TECH_WND_STATUS_COMPLETED") + "\n";
152
153 } else if (tech_status == TS_HAS_RESEARCHED_PREREQ) {
154 main_text += UserString("TECH_WND_STATUS_PARTIAL_UNLOCK") + "\n";
155 }
156
157 const ResearchQueue& queue = empire->GetResearchQueue();
158 auto queue_it = queue.find(tech_name);
159 if (queue_it != queue.end()) {
160 main_text += UserString("TECH_WND_ENQUEUED") + "\n";
161
162 float progress = empire->ResearchProgress(tech_name);
163 float total_cost = tech->ResearchCost(empire_id);
164 float allocation = queue_it->allocated_rp;
165 float max_allocation = tech->PerTurnCost(empire_id);
166
167 // %1% / %2% + %3% / %4% RP/turn
168 main_text += boost::io::str(FlexibleFormat(UserString("TECH_WND_PROGRESS"))
169 % DoubleToString(progress, 3, false)
170 % DoubleToString(total_cost, 3, false)
171 % DoubleToString(allocation, 3, false)
172 % DoubleToString(max_allocation, 3, false)) + "\n";
173
174 int ETA = queue_it->turns_left;
175 if (ETA != -1)
176 main_text += boost::io::str(FlexibleFormat(UserString("TECH_WND_ETA"))
177 % ETA);
178
179 } else if (tech->Researchable()) {
180 int turns = tech->ResearchTime(empire_id);
181 float cost = tech->ResearchCost(empire_id);
182 const std::string& cost_units = UserString("ENC_RP");
183
184 main_text += boost::io::str(FlexibleFormat(UserString("ENC_COST_AND_TURNS_STR"))
185 % DoubleToString(cost, 3, false)
186 % cost_units
187 % turns);
188 }
189
190 } else if (tech->Researchable()) {
191 int turns = tech->ResearchTime(empire_id);
192 float cost = tech->ResearchCost(empire_id);
193 const std::string& cost_units = UserString("ENC_RP");
194
195 main_text += boost::io::str(FlexibleFormat(UserString("ENC_COST_AND_TURNS_STR"))
196 % DoubleToString(cost, 3, false)
197 % cost_units
198 % turns);
199 }
200
201 return GG::Wnd::Create<IconTextBrowseWnd>(
202 ClientUI::TechIcon(tech_name), UserString(tech_name), main_text);
203 }
204
205
206 //////////////////////////////////////////////////
207 // TechTreeWnd::TechTreeControls //
208 //////////////////////////////////////////////////
209 /** A panel of buttons that control how the tech tree is displayed: what
210 * categories, statuses and types of techs to show. */
211 class TechTreeWnd::TechTreeControls : public CUIWnd {
212 public:
213 //! \name Structors //@{
214 TechTreeControls(const std::string& config_name = "");
215 void CompleteConstruction() override;
216 //@}
217
218 //! \name Mutators //@{
219 void SizeMove(const GG::Pt& ul, const GG::Pt& lr) override;
220
221 void Render() override;
222
223 void LDrag(const GG::Pt& pt, const GG::Pt& move, GG::Flags<GG::ModKey> mod_keys) override;
224
225 /** Set checked value of control for TechStatus @p status to @p state */
226 void SetTechStatus(TechStatus status, bool state);
227 //@}
228
229 private:
230 void DoButtonLayout();
231
232 /** These values are determined when doing button layout, and stored.
233 * They are later used when rendering separator lines between the groups
234 * of buttons */
235 int m_buttons_per_row; // number of buttons that can fit into available horizontal space
236 GG::X m_col_offset; // horizontal distance between each column of buttons
237 GG::Y m_row_offset; // vertical distance between each row of buttons
238
239 /** These values are used for rendering separator lines between groups of buttons */
240 static const int BUTTON_SEPARATION; // vertical or horizontal sepration between adjacent buttons
241 static const int UPPER_LEFT_PAD; // offset of buttons' position from top left of controls box
242
243 // TODO: replace all the above stored information with a vector of pairs of GG::Pt (or perhaps GG::Rect)
244 // This will contain the start and end points of all separator lines that need to be drawn. This will be
245 // calculated by SizeMove, and stored, so that start and end positions don't need to be recalculated each
246 // time Render is called.
247
248 std::shared_ptr<GG::StateButton> m_view_type_button;
249 std::shared_ptr<GG::StateButton> m_all_cat_button;
250 std::map<std::string, std::shared_ptr<GG::StateButton>> m_cat_buttons;
251 std::map<TechStatus, std::shared_ptr<GG::StateButton>> m_status_buttons;
252
253 friend class TechTreeWnd; // so TechTreeWnd can access buttons
254 };
255
256 const int TechTreeWnd::TechTreeControls::BUTTON_SEPARATION = 2;
257 const int TechTreeWnd::TechTreeControls::UPPER_LEFT_PAD = 2;
258
TechTreeControls(const std::string & config_name)259 TechTreeWnd::TechTreeControls::TechTreeControls(const std::string& config_name) :
260 CUIWnd(UserString("TECH_DISPLAY"), GG::INTERACTIVE | GG::DRAGABLE | GG::RESIZABLE | GG::ONTOP, config_name)
261 {}
262
CompleteConstruction()263 void TechTreeWnd::TechTreeControls::CompleteConstruction() {
264 const int tooltip_delay = GetOptionsDB().Get<int>("ui.tooltip.delay");
265 const boost::filesystem::path icon_dir = ClientUI::ArtDir() / "icons" / "tech" / "controls";
266
267 // create a button for each tech category...
268 for (const std::string& category : GetTechManager().CategoryNames()) {
269 GG::Clr icon_clr = ClientUI::CategoryColor(category);
270 std::shared_ptr<GG::SubTexture> icon = std::make_shared<GG::SubTexture>(ClientUI::CategoryIcon(category));
271 m_cat_buttons[category] = GG::Wnd::Create<GG::StateButton>("", ClientUI::GetFont(), GG::FORMAT_NONE, GG::CLR_ZERO,
272 std::make_shared<CUIIconButtonRepresenter>(icon, icon_clr));
273 m_cat_buttons[category]->SetBrowseInfoWnd(GG::Wnd::Create<TextBrowseWnd>(UserString(category), ""));
274 m_cat_buttons[category]->SetBrowseModeTime(tooltip_delay);
275 AttachChild(m_cat_buttons[category]);
276 }
277
278 GG::Clr icon_color = GG::Clr(113, 150, 182, 255);
279 // and one for "ALL"
280 m_all_cat_button = GG::Wnd::Create<GG::StateButton>("", ClientUI::GetFont(), GG::FORMAT_NONE, GG::CLR_ZERO,
281 std::make_shared<CUIIconButtonRepresenter>(std::make_shared<GG::SubTexture>(ClientUI::GetTexture(icon_dir / "00_all_cats.png", true)), icon_color));
282 m_all_cat_button->SetBrowseInfoWnd(GG::Wnd::Create<TextBrowseWnd>(UserString("ALL"), ""));
283 m_all_cat_button->SetBrowseModeTime(tooltip_delay);
284 m_all_cat_button->SetCheck(true);
285 AttachChild(m_all_cat_button);
286
287 // create a button for each tech status
288 m_status_buttons[TS_UNRESEARCHABLE] = GG::Wnd::Create<GG::StateButton>("", ClientUI::GetFont(), GG::FORMAT_NONE, GG::CLR_ZERO,
289 std::make_shared<CUIIconButtonRepresenter>(std::make_shared<GG::SubTexture>(ClientUI::GetTexture(icon_dir / "01_locked.png", true)), icon_color));
290 m_status_buttons[TS_UNRESEARCHABLE]->SetBrowseInfoWnd(GG::Wnd::Create<TextBrowseWnd>(UserString("TECH_WND_STATUS_LOCKED"), ""));
291 m_status_buttons[TS_UNRESEARCHABLE]->SetBrowseModeTime(tooltip_delay);
292 m_status_buttons[TS_UNRESEARCHABLE]->SetCheck(
293 GetOptionsDB().Get<bool>("ui.research.status.unresearchable.shown"));
294 AttachChild(m_status_buttons[TS_UNRESEARCHABLE]);
295
296 m_status_buttons[TS_HAS_RESEARCHED_PREREQ] = GG::Wnd::Create<GG::StateButton>("", ClientUI::GetFont(), GG::FORMAT_NONE, GG::CLR_ZERO,
297 std::make_shared<CUIIconButtonRepresenter>(std::make_shared<GG::SubTexture>(ClientUI::GetTexture(icon_dir / "02_partial.png", true)), icon_color));
298 m_status_buttons[TS_HAS_RESEARCHED_PREREQ]->SetBrowseInfoWnd(GG::Wnd::Create<TextBrowseWnd>(UserString("TECH_WND_STATUS_PARTIAL_UNLOCK"), ""));
299 m_status_buttons[TS_HAS_RESEARCHED_PREREQ]->SetBrowseModeTime(tooltip_delay);
300 m_status_buttons[TS_HAS_RESEARCHED_PREREQ]->SetCheck(
301 GetOptionsDB().Get<bool>("ui.research.status.partial.shown"));
302 AttachChild(m_status_buttons[TS_HAS_RESEARCHED_PREREQ]);
303
304 m_status_buttons[TS_RESEARCHABLE] = GG::Wnd::Create<GG::StateButton>("", ClientUI::GetFont(), GG::FORMAT_NONE, GG::CLR_ZERO,
305 std::make_shared<CUIIconButtonRepresenter>(std::make_shared<GG::SubTexture>(ClientUI::GetTexture(icon_dir / "03_unlocked.png", true)), icon_color));
306 m_status_buttons[TS_RESEARCHABLE]->SetBrowseInfoWnd(GG::Wnd::Create<TextBrowseWnd>(UserString("TECH_WND_STATUS_RESEARCHABLE"), ""));
307 m_status_buttons[TS_RESEARCHABLE]->SetBrowseModeTime(tooltip_delay);
308 m_status_buttons[TS_RESEARCHABLE]->SetCheck(
309 GetOptionsDB().Get<bool>("ui.research.status.researchable.shown"));
310 AttachChild(m_status_buttons[TS_RESEARCHABLE]);
311
312 m_status_buttons[TS_COMPLETE] = GG::Wnd::Create<GG::StateButton>("", ClientUI::GetFont(), GG::FORMAT_NONE, GG::CLR_ZERO,
313 std::make_shared<CUIIconButtonRepresenter>(std::make_shared<GG::SubTexture>(ClientUI::GetTexture(icon_dir / "04_completed.png", true)), icon_color));
314 m_status_buttons[TS_COMPLETE]->SetBrowseInfoWnd(GG::Wnd::Create<TextBrowseWnd>(UserString("TECH_WND_STATUS_COMPLETED"), ""));
315 m_status_buttons[TS_COMPLETE]->SetBrowseModeTime(tooltip_delay);
316 m_status_buttons[TS_COMPLETE]->SetCheck(
317 GetOptionsDB().Get<bool>("ui.research.status.completed.shown"));
318 AttachChild(m_status_buttons[TS_COMPLETE]);
319
320 // create button to switch between tree and list views
321 m_view_type_button = GG::Wnd::Create<GG::StateButton>(
322 "", ClientUI::GetFont(), GG::FORMAT_NONE, GG::CLR_ZERO,
323 std::make_shared<CUIIconButtonRepresenter>(std::make_shared<GG::SubTexture>(ClientUI::GetTexture(icon_dir / "06_view_tree.png", true)), icon_color,
324 std::make_shared<GG::SubTexture>(ClientUI::GetTexture(icon_dir / "05_view_list.png", true)), GG::Clr(110, 172, 150, 255)));
325 m_view_type_button->SetBrowseInfoWnd(GG::Wnd::Create<TextBrowseWnd>(UserString("TECH_WND_VIEW_TYPE"), ""));
326 m_view_type_button->SetBrowseModeTime(tooltip_delay);
327 m_view_type_button->SetCheck(false);
328 AttachChild(m_view_type_button);
329
330 SetChildClippingMode(ClipToClient);
331
332 CUIWnd::CompleteConstruction();
333
334 DoButtonLayout();
335 SaveDefaultedOptions();
336 SaveOptions();
337 }
338
DoButtonLayout()339 void TechTreeWnd::TechTreeControls::DoButtonLayout() {
340 const int PTS = ClientUI::Pts();
341 const GG::X RIGHT_EDGE_PAD(PTS / 3);
342 const GG::X USABLE_WIDTH = std::max(ClientWidth() - RIGHT_EDGE_PAD, GG::X1); // space in which to do layout
343 const GG::X BUTTON_WIDTH = GG::X(PTS * std::max(GetOptionsDB().Get<double>("ui.research.control.graphic.size"), 0.5));
344 const GG::Y BUTTON_HEIGHT = GG::Y(Value(BUTTON_WIDTH));
345
346 m_col_offset = BUTTON_WIDTH + BUTTON_SEPARATION; // horizontal distance between each column of buttons
347 m_row_offset = BUTTON_HEIGHT + BUTTON_SEPARATION; // vertical distance between each row of buttons
348 m_buttons_per_row = std::max(Value(USABLE_WIDTH / (m_col_offset)), 1);
349
350 const int NUM_NON_CATEGORY_BUTTONS = 6; // ALL, Locked, Partial, Unlocked, Complete, ViewType
351
352 // place category buttons: fill each row completely before starting next row
353 int row = 0, col = -1;
354 for (auto& cat_button : m_cat_buttons) {
355 ++col;
356 if (col >= m_buttons_per_row) {
357 ++row;
358 col = 0;
359 }
360 GG::Pt ul(UPPER_LEFT_PAD + col*m_col_offset, UPPER_LEFT_PAD + row*m_row_offset);
361 GG::Pt lr = ul + GG::Pt(BUTTON_WIDTH, BUTTON_HEIGHT);
362 cat_button.second->SizeMove(ul, lr);
363 }
364
365 // add ALL button
366 ++col;
367 if (col >= m_buttons_per_row) {
368 ++row;
369 col =0;
370 }
371 GG::Pt all_cats_ul(UPPER_LEFT_PAD + col*m_col_offset, UPPER_LEFT_PAD + row*m_row_offset);
372 GG::Pt all_cats_lr = all_cats_ul + GG::Pt(BUTTON_WIDTH, BUTTON_HEIGHT);
373 m_all_cat_button->SizeMove(all_cats_ul, all_cats_lr);
374
375 // rowbreak after category buttons, before type and status buttons, unless all buttons fit on one row
376 if (m_buttons_per_row < (static_cast<int>(m_cat_buttons.size()) + NUM_NON_CATEGORY_BUTTONS)) {
377 col = -1;
378 ++row;
379 }
380
381 // place status buttons: fill each row completely before starting next row
382 for (auto& status_button : m_status_buttons) {
383 ++col;
384 if (col >= m_buttons_per_row) {
385 ++row;
386 col = 0;
387 }
388 GG::Pt ul(UPPER_LEFT_PAD + col*m_col_offset, UPPER_LEFT_PAD + row*m_row_offset);
389 GG::Pt lr = ul + GG::Pt(BUTTON_WIDTH, BUTTON_HEIGHT);
390 status_button.second->SizeMove(ul, lr);
391 }
392
393 // place view type button
394 ++col;
395 if (col + 1 > m_buttons_per_row) {
396 col = 0;
397 ++row;
398 }
399 GG::Pt view_type_ul(UPPER_LEFT_PAD + col*m_col_offset, UPPER_LEFT_PAD + row*m_row_offset);
400 GG::Pt view_type_lr = view_type_ul + GG::Pt(BUTTON_WIDTH, BUTTON_HEIGHT);
401 m_view_type_button->SizeMove(view_type_ul, view_type_lr);
402
403 // prevent window from being shrunk less than one button width, or current number of rows of height
404 SetMinSize(GG::Pt(UPPER_LEFT_PAD + BUTTON_WIDTH + 3*RIGHT_EDGE_PAD,
405 TopBorder() + BottomBorder() + UPPER_LEFT_PAD + (++row)*m_row_offset));
406 }
407
SizeMove(const GG::Pt & ul,const GG::Pt & lr)408 void TechTreeWnd::TechTreeControls::SizeMove(const GG::Pt& ul, const GG::Pt& lr) {
409 m_config_save = false;
410 // maybe later do something interesting with docking
411 CUIWnd::SizeMove(ul, lr); // set width and upper left as user-requested
412 DoButtonLayout(); // given set width, position buttons and set appropriate minimum height
413 m_config_save = true;
414 CUIWnd::SizeMove(ul, GG::Pt(lr.x, ul.y + MinSize().y)); // width and upper left unchanged. set height to minimum height
415 }
416
Render()417 void TechTreeWnd::TechTreeControls::Render() {
418 CUIWnd::Render();
419
420 //GG::Pt cl_ul = ClientUpperLeft();
421 //GG::Pt cl_lr = ClientLowerRight();
422
423 //glColor(ClientUI::WndOuterBorderColor());
424
425 //GG::Y category_bottom = cl_ul.y + m_category_button_rows*m_row_offset - BUTTON_SEPARATION/2 + UPPER_LEFT_PAD;
426 //GG::Line(cl_ul.x, category_bottom, cl_lr.x - 1, category_bottom);
427
428 //if (m_buttons_per_row >= 6) {
429 // // all six status and type buttons are on one row, and need a vertical separator between them
430 // GG::X middle = cl_ul.x + m_col_offset*3 - BUTTON_SEPARATION/2 + UPPER_LEFT_PAD;
431 // GG::Line(middle, category_bottom, middle, cl_lr.y - 1);
432 //} else {
433 // // the status and type buttons are split into separate vertical groups, and need a horiztonal separator between them
434 // GG::Y status_bottom = category_bottom + m_status_button_rows*m_row_offset;
435 // GG::Line(cl_ul.x, status_bottom, cl_lr.x - 1, status_bottom);
436 //}
437 }
438
LDrag(const GG::Pt & pt,const GG::Pt & move,GG::Flags<GG::ModKey> mod_keys)439 void TechTreeWnd::TechTreeControls::LDrag(const GG::Pt& pt, const GG::Pt& move, GG::Flags<GG::ModKey> mod_keys) {
440 if (m_drag_offset != GG::Pt(-GG::X1, -GG::Y1)) { // resize-dragging
441 GG::Pt new_lr = pt - m_drag_offset;
442
443 new_lr.y = Bottom(); // ignore y-resizes
444
445 // constrain to within parent
446 if (auto&& parent = Parent()) {
447 GG::Pt max_lr = parent->ClientLowerRight();
448 new_lr.x = std::min(new_lr.x, max_lr.x);
449 }
450
451 Resize(new_lr - UpperLeft());
452 } else { // normal-dragging
453 GG::Pt final_move = move;
454
455 if (auto&& parent = Parent()) {
456 GG::Pt ul = UpperLeft();
457 GG::Pt new_ul = ul + move;
458 //GG::Pt new_lr = lr + move;
459
460 GG::Pt min_ul = parent->ClientUpperLeft() + GG::Pt(GG::X1, GG::Y1);
461 GG::Pt max_lr = parent->ClientLowerRight();
462 GG::Pt max_ul = max_lr - Size();
463
464 new_ul.x = std::max(min_ul.x, std::min(max_ul.x, new_ul.x));
465 new_ul.y = std::max(min_ul.y, std::min(max_ul.y, new_ul.y));
466
467 final_move = new_ul - ul;
468 }
469
470 GG::Wnd::LDrag(pt, final_move, mod_keys);
471 }
472 }
473
SetTechStatus(TechStatus status,bool state)474 void TechTreeWnd::TechTreeControls::SetTechStatus(TechStatus status, bool state) {
475 auto control_it = m_status_buttons.find(status);
476 if (control_it == m_status_buttons.end()) {
477 WarnLogger() << "No control for status " << status << " found";
478 return;
479 }
480
481 control_it->second->SetCheck(state);
482 }
483
484
485 //////////////////////////////////////////////////
486 // TechTreeWnd::LayoutPanel //
487 //////////////////////////////////////////////////
488 /** The window that contains the actual tech panels and dependency arcs. */
489 class TechTreeWnd::LayoutPanel : public GG::Wnd {
490 public:
491 /** \name Structors */ //@{
492 LayoutPanel(GG::X w, GG::Y h);
493 //@}
494
495 void CompleteConstruction() override;
496
497 /** \name Accessors */ //@{
498 GG::Pt ClientLowerRight() const override;
499
500 double Scale() const;
501 std::set<std::string> GetCategoriesShown() const;
502 std::set<TechStatus> GetTechStatusesShown() const;
503
504 mutable TechTreeWnd::TechClickSignalType TechSelectedSignal;
505 mutable TechTreeWnd::TechClickSignalType TechDoubleClickedSignal;
506 mutable TechTreeWnd::TechSignalType TechPediaDisplaySignal;
507 //@}
508
509 //! \name Mutators //@{
510 void Render() override;
511 void SizeMove(const GG::Pt& ul, const GG::Pt& lr) override;
512
513 void Update(); ///< update indicated \a tech panel or all panels if \a tech_name is an empty string, without redoing layout
514 void Clear(); ///< remove all tech panels
515 void Reset(); ///< redo layout, recentre on a tech
516 void SetScale(double scale);
517 void ShowCategory(const std::string& category);
518 void ShowAllCategories();
519 void HideCategory(const std::string& category);
520 void HideAllCategories();
521 void ShowStatus(TechStatus status);
522 void HideStatus(TechStatus status);
523 void SelectTech(const std::string& tech_name);
524 void CenterOnTech(const std::string& tech_name);
525 void DoZoom(const GG::Pt &pt) const;
526 void UndoZoom() const;
527
528 // Converts between screen coordinates and virtual coordiantes
529 // doing the inverse or same transformation as DoZoom does with gl calls
530 GG::Pt ConvertPtScreenToZoomed(const GG::Pt& pt) const;
531 GG::Pt ConvertPtZoomedToScreen(const GG::Pt& pt) const;
532 //@}
533
534 private:
535 class TechPanel;
536
537 class LayoutSurface : public GG::Wnd {
538 public:
LayoutSurface()539 LayoutSurface() :
540 Wnd(GG::X0, GG::Y0, GG::X1, GG::Y1, GG::INTERACTIVE | GG::DRAGABLE)
541 {}
542
LDrag(const GG::Pt & pt,const GG::Pt & move,GG::Flags<GG::ModKey> mod_keys)543 void LDrag(const GG::Pt& pt, const GG::Pt& move, GG::Flags<GG::ModKey> mod_keys) override
544 { DraggedSignal(move); }
545
LButtonDown(const GG::Pt & pt,GG::Flags<GG::ModKey> mod_keys)546 void LButtonDown(const GG::Pt& pt, GG::Flags<GG::ModKey> mod_keys) override
547 { ButtonDownSignal(pt); }
548
LButtonUp(const GG::Pt & pt,GG::Flags<GG::ModKey> mod_keys)549 void LButtonUp(const GG::Pt& pt, GG::Flags<GG::ModKey> mod_keys) override
550 { ButtonUpSignal(pt); }
551
MouseWheel(const GG::Pt & pt,int move,GG::Flags<GG::ModKey> mod_keys)552 void MouseWheel(const GG::Pt& pt, int move, GG::Flags<GG::ModKey> mod_keys) override
553 { ZoomedSignal(move); }
554
555 mutable boost::signals2::signal<void (int)> ZoomedSignal;
556 mutable boost::signals2::signal<void (const GG::Pt&)> DraggedSignal;
557 mutable boost::signals2::signal<void (const GG::Pt&)> ButtonDownSignal;
558 mutable boost::signals2::signal<void (const GG::Pt&)> ButtonUpSignal;
559 };
560
561 void Layout(bool keep_position); // lays out tech panels
562
563 void DoLayout(); // arranges child controls (scrolls, buttons) to account for window size
564
565 void ScrolledSlot(int, int, int, int);
566
567 void TreeDraggedSlot(const GG::Pt& move);
568 void TreeDragBegin(const GG::Pt& move);
569 void TreeDragEnd(const GG::Pt& move);
570 void TreeZoomedSlot(int move);
571 bool TreeZoomInKeyboard();
572 bool TreeZoomOutKeyboard();
573 void ConnectKeyboardAcceleratorSignals();
574
575 double m_scale = INITIAL_SCALE;
576 std::set<std::string> m_categories_shown;
577 std::set<TechStatus> m_tech_statuses_shown;
578 std::string m_selected_tech_name;
579 std::string m_browsed_tech_name;
580 TechTreeLayout m_graph;
581
582 std::map<std::string, std::shared_ptr<TechPanel>> m_techs;
583 TechTreeArcs m_dependency_arcs;
584
585 std::shared_ptr<LayoutSurface> m_layout_surface;
586 std::shared_ptr<GG::Scroll> m_vscroll;
587 std::shared_ptr<GG::Scroll> m_hscroll;
588 double m_scroll_position_x = 0.0; //actual scroll position
589 double m_scroll_position_y = 0.0;
590 double m_drag_scroll_position_x = 0.0; //position when drag started
591 double m_drag_scroll_position_y = 0.0;
592 std::shared_ptr<GG::Button> m_zoom_in_button;
593 std::shared_ptr<GG::Button> m_zoom_out_button;
594 };
595
596
597 //////////////////////////////////////////////////
598 // TechTreeWnd::LayoutPanel::TechPanel //
599 //////////////////////////////////////////////////
600 /** Represents a single tech in the LayoutPanel. */
601 class TechTreeWnd::LayoutPanel::TechPanel : public GG::Wnd {
602 public:
603 TechPanel(const std::string& tech_name, const TechTreeWnd::LayoutPanel* panel);
604 virtual ~TechPanel();
605 void CompleteConstruction() override;
606
607 bool InWindow(const GG::Pt& pt) const override;
608
609 /** Update layout and format only if required.*/
610 void PreRender() override;
611
612 void Render() override;
613
LDrag(const GG::Pt & pt,const GG::Pt & move,GG::Flags<GG::ModKey> mod_keys)614 void LDrag(const GG::Pt& pt, const GG::Pt& move, GG::Flags<GG::ModKey> mod_keys) override
615 { ForwardEventToParent(); }
616
LButtonDown(const GG::Pt & pt,GG::Flags<GG::ModKey> mod_keys)617 void LButtonDown(const GG::Pt& pt, GG::Flags<GG::ModKey> mod_keys) override
618 { ForwardEventToParent(); }
619
LButtonUp(const GG::Pt & pt,GG::Flags<GG::ModKey> mod_keys)620 void LButtonUp(const GG::Pt& pt, GG::Flags<GG::ModKey> mod_keys) override
621 { ForwardEventToParent(); }
622
623 void LClick(const GG::Pt& pt, GG::Flags<GG::ModKey> mod_keys) override;
624
625 void RClick(const GG::Pt& pt, GG::Flags<GG::ModKey> mod_keys) override;
626
627 void LDoubleClick(const GG::Pt& pt, GG::Flags<GG::ModKey> mod_keys) override;
628
629 void MouseEnter(const GG::Pt& pt, GG::Flags<GG::ModKey> mod_keys) override;
630
631 void MouseLeave() override;
632
MouseWheel(const GG::Pt & pt,int move,GG::Flags<GG::ModKey> mod_keys)633 void MouseWheel(const GG::Pt& pt, int move, GG::Flags<GG::ModKey> mod_keys) override
634 { ForwardEventToParent(); }
635
636 void Update();
637 void Select(bool select);
638 int FontSize() const;
639
640 mutable TechTreeWnd::TechClickSignalType TechLeftClickedSignal;
641 mutable TechTreeWnd::TechClickSignalType TechDoubleClickedSignal;
642 mutable TechTreeWnd::TechSignalType TechPediaDisplaySignal;
643
644 private:
645 void InitBuffers();
646
647 GG::GL2DVertexBuffer m_border_buffer;
648 GG::GL2DVertexBuffer m_eta_border_buffer;
649 GG::GL2DVertexBuffer m_enqueued_indicator_buffer;
650
651 const std::string& m_tech_name;
652 std::string m_name_text;
653 std::string m_cost_and_duration_text;
654 std::string m_eta_text;
655 const TechTreeWnd::LayoutPanel* m_layout_panel;
656 std::shared_ptr<GG::StaticGraphic> m_icon;
657 std::vector<std::shared_ptr<GG::StaticGraphic>> m_unlock_icons;
658 std::shared_ptr<GG::TextControl> m_name_label;
659 std::shared_ptr<GG::TextControl> m_cost_and_duration_label;
660 std::shared_ptr<GG::TextControl> m_eta_label;
661 GG::Clr m_colour;
662 TechStatus m_status;
663 bool m_browse_highlight;
664 bool m_selected;
665 int m_eta;
666 bool m_enqueued;
667 };
668
TechPanel(const std::string & tech_name,const LayoutPanel * panel)669 TechTreeWnd::LayoutPanel::TechPanel::TechPanel(const std::string& tech_name, const LayoutPanel* panel) :
670 GG::Wnd(GG::X0, GG::Y0, TechPanelWidth(), TechPanelHeight(), GG::INTERACTIVE),
671 m_tech_name(tech_name),
672 m_layout_panel(panel),
673 m_colour(GG::CLR_GRAY),
674 m_status(TS_RESEARCHABLE),
675 m_browse_highlight(false),
676 m_selected(false),
677 m_eta(-1),
678 m_enqueued(false)
679 {
680 const int GRAPHIC_SIZE = Value(TechPanelHeight());
681 m_icon = GG::Wnd::Create<GG::StaticGraphic>(ClientUI::TechIcon(m_tech_name), GG::GRAPHIC_FITGRAPHIC | GG::GRAPHIC_PROPSCALE);
682 m_icon->Resize(GG::Pt(GG::X(GRAPHIC_SIZE), GG::Y(GRAPHIC_SIZE)));
683
684 m_name_label = GG::Wnd::Create<GG::TextControl>(GG::X0, GG::Y0, GG::X1, GG::Y1, "", ClientUI::GetFont(FontSize()), ClientUI::TextColor(), GG::FORMAT_WORDBREAK | GG::FORMAT_VCENTER | GG::FORMAT_LEFT);
685 m_cost_and_duration_label = GG::Wnd::Create<GG::TextControl>(GG::X0, GG::Y0, GG::X1, GG::Y1, "", ClientUI::GetFont(FontSize()), ClientUI::TextColor(), GG::FORMAT_VCENTER | GG::FORMAT_RIGHT);
686 m_eta_label = GG::Wnd::Create<GG::TextControl>(GG::X0, GG::Y0, GG::X1, GG::Y1, "", ClientUI::GetFont(FontSize()),ClientUI::TextColor());
687
688 // intentionally not attaching as child; TechPanel::Render the child Render() function instead.
689
690 SetBrowseModeTime(GetOptionsDB().Get<int>("ui.tooltip.delay"));
691 }
692
CompleteConstruction()693 void TechTreeWnd::LayoutPanel::TechPanel::CompleteConstruction() {
694 GG::Wnd::CompleteConstruction();
695 SetBrowseModeTime(GetOptionsDB().Get<int>("ui.tooltip.delay"));
696 Update();
697 }
698
~TechPanel()699 TechTreeWnd::LayoutPanel::TechPanel::~TechPanel()
700 {}
701
FontSize() const702 int TechTreeWnd::LayoutPanel::TechPanel::FontSize() const
703 { return ClientUI::Pts() * 3 / 2; }
704
InWindow(const GG::Pt & pt) const705 bool TechTreeWnd::LayoutPanel::TechPanel::InWindow(const GG::Pt& pt) const {
706 const GG::Pt p = m_layout_panel->ConvertPtScreenToZoomed(pt) - UpperLeft();
707 if (m_icon->InWindow(p))
708 return true;
709 return GG::Pt(GG::X0, GG::Y0) <= p && p < GG::Pt(TechPanelWidth(), TechPanelHeight());
710 }
711
PreRender()712 void TechTreeWnd::LayoutPanel::TechPanel::PreRender() {
713 GG::Wnd::PreRender();
714
715 const int PAD = 8;
716 GG::X text_left(GG::X(Value(TechPanelHeight())) + PAD);
717 GG::Y text_top(0);
718 GG::X text_width(TechPanelWidth() - text_left);
719 GG::Y text_height(TechPanelHeight()/2);
720
721 GG::Pt ul = GG::Pt(text_left, text_top);
722 GG::Pt lr = ul + GG::Pt(text_width + PAD, TechPanelHeight());
723
724 // size of tech name text
725 int font_pts = static_cast<int>(FontSize() * m_layout_panel->Scale() + 0.5);
726
727 if (font_pts > 6) {
728 if (font_pts < 10) {
729 m_name_label->SetText(m_name_text);
730 m_cost_and_duration_label->SetText(m_cost_and_duration_text);
731 } else {
732 m_name_label->SetText("<s>" + m_name_text + "</s>");
733 m_cost_and_duration_label->SetText("<s>" + m_cost_and_duration_text + "</s>");
734 }
735
736 GG::Pt text_ul(text_left + PAD/2, text_top);
737 GG::Pt text_size(text_width + PAD, m_unlock_icons.empty() ? text_height*2 : text_height - PAD/2);
738 m_name_label->SizeMove(text_ul, text_ul + text_size);
739
740 // show cost and duration for unresearched techs
741 const Empire* empire = GetEmpire(HumanClientApp::GetApp()->EmpireID());
742 if (empire) {
743 if (empire->TechResearched(m_tech_name))
744 m_cost_and_duration_label->Hide();
745 else {
746 GG::Pt text_ll(text_left + PAD / 2, TechPanelHeight() - font_pts - PAD);
747 text_size = GG::Pt(text_width + PAD / 2, GG::Y(font_pts));
748 m_cost_and_duration_label->SizeMove(text_ll, text_ll + text_size);
749 m_cost_and_duration_label->Show();
750 }
751 }
752
753 // ETA background and text
754 if (m_eta != -1 && font_pts > 10) {
755 GG::Pt panel_size = lr - ul;
756 GG::Pt eta_ul = ul + GG::Pt(panel_size.x * 3 / 4, panel_size.y * 3 / 4) - GG::Pt(GG::X(2), GG::Y(2));
757 GG::Pt eta_lr = eta_ul + GG::Pt(panel_size.x / 2, panel_size.y / 2) + GG::Pt(GG::X(2), GG::Y(2));
758
759 m_eta_label->SizeMove(eta_ul, eta_lr);
760 }
761 }
762 }
763
Render()764 void TechTreeWnd::LayoutPanel::TechPanel::Render() {
765 const int PAD = 8;
766 GG::X text_left(GG::X(Value(TechPanelHeight())) + PAD);
767 GG::Y text_top(0);
768 GG::X text_width(TechPanelWidth() - text_left);
769 GG::Y text_height(TechPanelHeight()/2);
770
771 GG::Pt ul = GG::Pt(text_left, text_top);
772 GG::Pt lr = ul + GG::Pt(text_width + PAD, TechPanelHeight());
773 GG::Clr border_colour = GG::CLR_WHITE;
774
775 m_layout_panel->DoZoom(UpperLeft());
776
777 glDisable(GL_TEXTURE_2D);
778 glEnable(GL_LINE_SMOOTH);
779 glLineWidth(2.0);
780
781 // size of tech name text
782 int font_pts = static_cast<int>(FontSize() * m_layout_panel->Scale() + 0.5);
783
784 // cancel out dependency line under tech icon
785 glColor(ClientUI::CtrlColor());
786 PartlyRoundedRect(m_icon->UpperLeft(), m_icon->LowerRight(), PAD, true, true, true, true, true);
787
788 // Render text part of tech panel, but only if zoomed in so the text is legible
789 if (font_pts > 6) {
790 // black out dependency lines under panel
791 glColor(GG::CLR_BLACK);
792 PartlyRoundedRect(ul, lr + GG::Pt(GG::X(4), GG::Y0), PAD, true, true, true, true, true);
793
794 // background of panel
795 glColor(m_colour);
796 PartlyRoundedRect(ul, lr + GG::Pt(GG::X(4), GG::Y0), PAD, true, true, true, true, true);
797
798 // panel border
799 if (m_browse_highlight) {
800 glColor(border_colour);
801 PartlyRoundedRect(ul, lr + GG::Pt(GG::X(4), GG::Y0), PAD, true, true, true, true, false);
802 }
803 else if (m_status == TS_COMPLETE || m_status == TS_RESEARCHABLE) {
804 border_colour = m_colour;
805 border_colour.a = 255;
806 glColor(border_colour);
807 PartlyRoundedRect(ul, lr + GG::Pt(GG::X(4), GG::Y0), PAD, true, true, true, true, false);
808 }
809 else {
810 border_colour = m_colour;
811 border_colour.a = 127;
812 // don't render border
813 }
814
815 // render tech panel text; for small font sizes, remove shadow
816 glEnable(GL_TEXTURE_2D);
817
818 GG::Pt text_ul(text_left + PAD/2, text_top);
819 GG::Pt text_size(text_width + PAD, m_unlock_icons.empty() ? text_height*2 : text_height - PAD/2);
820 m_name_label->SizeMove(text_ul, text_ul + text_size);
821
822 /// Need to render children too
823 GG::GUI::GetGUI()->RenderWindow(m_name_label);
824 GG::GUI::GetGUI()->RenderWindow(m_cost_and_duration_label);
825
826 // box around whole panel to indicate enqueue
827 if (m_enqueued) {
828 glColor(GG::CLR_WHITE);
829 GG::Pt gap = GG::Pt(GG::X(2 * PAD), GG::Y(2 * PAD));
830 GG::Pt enc_ul(-gap);
831 GG::Pt enc_lr(lr + gap);
832 PartlyRoundedRect(enc_ul, enc_lr, PAD + 6, true, true, true, true, false);
833 }
834
835 // ETA background and text
836 if (m_eta != -1 && font_pts > 10) {
837 GG::Pt panel_size = lr - ul;
838 GG::Pt eta_ul = ul + GG::Pt(panel_size.x * 3 / 4, panel_size.y * 3 / 4) - GG::Pt(GG::X(2), GG::Y(2));
839 GG::Pt eta_lr = eta_ul + GG::Pt(panel_size.x / 2, panel_size.y / 2) + GG::Pt(GG::X(2), GG::Y(2));
840
841 glColor(GG::CLR_BLACK);
842 CircleArc(eta_ul, eta_lr, 0, 2 * PI, true);
843 glColor(border_colour);
844 CircleArc(eta_ul, eta_lr, 0, 2 * PI, true);
845
846 glEnable(GL_TEXTURE_2D);
847
848 /// Need to render text too
849 GG::GUI::GetGUI()->RenderWindow(m_eta_label);
850 glDisable(GL_TEXTURE_2D);
851 }
852
853 } else {
854 // box only around icon to indicate enqueue (when zoomed far out)
855 if (m_enqueued) {
856 glColor(GG::CLR_WHITE);
857 GG::Pt gap = GG::Pt(GG::X(2 * PAD), GG::Y(2 * PAD));
858 GG::Pt enc_ul(-gap);
859 GG::Pt enc_lr(m_icon->LowerRight() + gap);
860 PartlyRoundedRect(enc_ul, enc_lr, PAD + 6, true, true, true, true, false);
861 }
862 }
863
864 // selection indicator
865 if (m_selected) {
866 // nothing!
867 }
868
869 // render tech icon
870 glDisable(GL_LINE_SMOOTH);
871 glEnable(GL_TEXTURE_2D);
872 m_icon->Render();
873
874 for (auto& icon : m_unlock_icons)
875 { icon->Render(); }
876
877 m_layout_panel->UndoZoom();
878 }
879
LClick(const GG::Pt & pt,GG::Flags<GG::ModKey> mod_keys)880 void TechTreeWnd::LayoutPanel::TechPanel::LClick(const GG::Pt& pt, GG::Flags<GG::ModKey> mod_keys) {
881 if (m_layout_panel->m_selected_tech_name != m_tech_name)
882 TechLeftClickedSignal(m_tech_name, mod_keys);
883 }
884
RClick(const GG::Pt & pt,GG::Flags<GG::ModKey> mod_keys)885 void TechTreeWnd::LayoutPanel::TechPanel::RClick(const GG::Pt& pt,
886 GG::Flags<GG::ModKey> mod_keys)
887 {
888 auto dclick_action = [this, pt]() { LDoubleClick(pt, GG::Flags<GG::ModKey>()); };
889 auto ctrl_dclick_action = [this, pt]() { LDoubleClick(pt, GG::MOD_KEY_CTRL); };
890 auto pedia_display_action = [this]() { TechPediaDisplaySignal(m_tech_name); };
891
892 auto popup = GG::Wnd::Create<CUIPopupMenu>(pt.x, pt.y);
893 if (!(m_enqueued) && !(m_status == TS_COMPLETE)) {
894 popup->AddMenuItem(GG::MenuItem(UserString("PRODUCTION_DETAIL_ADD_TO_QUEUE"), false, false, dclick_action));
895 popup->AddMenuItem(GG::MenuItem(UserString("PRODUCTION_DETAIL_ADD_TO_TOP_OF_QUEUE"), false, false,
896 ctrl_dclick_action));
897 }
898
899 std::string popup_label = boost::io::str(FlexibleFormat(UserString("ENC_LOOKUP")) % UserString(m_tech_name));
900 popup->AddMenuItem(GG::MenuItem(popup_label, false, false, pedia_display_action));
901 popup->Run();
902 }
903
LDoubleClick(const GG::Pt & pt,GG::Flags<GG::ModKey> mod_keys)904 void TechTreeWnd::LayoutPanel::TechPanel::LDoubleClick(const GG::Pt& pt, GG::Flags<GG::ModKey> mod_keys)
905 { TechDoubleClickedSignal(m_tech_name, mod_keys); }
906
MouseEnter(const GG::Pt & pt,GG::Flags<GG::ModKey> mod_keys)907 void TechTreeWnd::LayoutPanel::TechPanel::MouseEnter(const GG::Pt& pt, GG::Flags<GG::ModKey> mod_keys)
908 { m_browse_highlight = true; }
909
MouseLeave()910 void TechTreeWnd::LayoutPanel::TechPanel::MouseLeave()
911 { m_browse_highlight = false; }
912
Select(bool select)913 void TechTreeWnd::LayoutPanel::TechPanel::Select(bool select)
914 { m_selected = select; }
915
Update()916 void TechTreeWnd::LayoutPanel::TechPanel::Update() {
917 Select(m_layout_panel->m_selected_tech_name == m_tech_name);
918
919 int client_empire_id = HumanClientApp::GetApp()->EmpireID();
920
921 if (const Empire* empire = GetEmpire(client_empire_id)) {
922 m_status = empire->GetTechStatus(m_tech_name);
923 m_enqueued = empire->GetResearchQueue().InQueue(m_tech_name);
924
925 const ResearchQueue& queue = empire->GetResearchQueue();
926 auto queue_it = queue.find(m_tech_name);
927 if (queue_it != queue.end()) {
928 m_eta = queue_it->turns_left;
929 if (m_eta != -1)
930 m_eta_text = std::to_string(m_eta);
931 else
932 m_eta_text.clear();
933 }
934 }
935
936 GG::Clr icon_colour = GG::CLR_WHITE;
937 if (const Tech* tech = GetTech(m_tech_name)) {
938 m_colour = ClientUI::CategoryColor(tech->Category());
939 icon_colour = m_colour;
940
941 if (m_status == TS_UNRESEARCHABLE || m_status == TS_HAS_RESEARCHED_PREREQ) {
942 icon_colour = GG::CLR_GRAY;
943 m_colour.a = 64;
944 } else if (m_status == TS_RESEARCHABLE) {
945 icon_colour = GG::CLR_GRAY;
946 m_colour.a = 128;
947 } else {
948 m_colour.a = 255;
949 }
950
951 if (m_unlock_icons.empty()) {
952 const int PAD = 8;
953 GG::X icon_left(GG::X(Value(TechPanelHeight())) + PAD*3/2);
954 GG::Y icon_height = TechPanelHeight()/2;
955 GG::X icon_width = GG::X(Value(icon_height));
956 GG::Y icon_bottom = TechPanelHeight() - PAD/2;
957 GG::Y icon_top = icon_bottom - icon_height;
958
959 // add icons for unlocked items
960 for (const UnlockableItem& item : tech->UnlockedItems()) {
961 std::shared_ptr<GG::Texture> texture;
962 switch (item.type) {
963 case UIT_BUILDING: texture = ClientUI::BuildingIcon(item.name); break;
964 case UIT_SHIP_PART: texture = ClientUI::PartIcon(item.name); break;
965 case UIT_SHIP_HULL: texture = ClientUI::HullIcon(item.name); break;
966 default: break;
967 }
968
969 if (texture) {
970 auto graphic = GG::Wnd::Create<GG::StaticGraphic>(texture, GG::GRAPHIC_FITGRAPHIC | GG::GRAPHIC_PROPSCALE);
971 m_unlock_icons.push_back(graphic);
972 graphic->SizeMove(GG::Pt(icon_left, icon_top), GG::Pt(icon_left + icon_width, icon_top + icon_height));
973 icon_left += icon_width + PAD;
974 }
975 }
976 // add icons for modified part meters / specials
977 std::set<MeterType> meters_affected;
978 std::set<std::string> specials_affected;
979 std::set<std::string> parts_whose_meters_are_affected;
980 for (auto& effects_group : tech->Effects()) {
981 for (Effect::Effect* effect : effects_group->EffectsList()) {
982 if (const Effect::SetMeter* set_meter_effect = dynamic_cast<const Effect::SetMeter*>(effect)) {
983 meters_affected.insert(set_meter_effect->GetMeterType());
984
985 } else if (const Effect::SetShipPartMeter* set_ship_part_meter_effect = dynamic_cast<const Effect::SetShipPartMeter*>(effect)) {
986 const ValueRef::ValueRef<std::string>* part_name = set_ship_part_meter_effect->GetPartName();
987 if (part_name && part_name->ConstantExpr())
988 parts_whose_meters_are_affected.insert(part_name->Eval());
989
990 } else if (const Effect::AddSpecial* add_special_effect = dynamic_cast<const Effect::AddSpecial*>(effect)) {
991 const ValueRef::ValueRef<std::string>* special_name = add_special_effect->GetSpecialName();
992 if (special_name && special_name->ConstantExpr())
993 specials_affected.insert(special_name->Eval());
994 }
995 }
996 }
997
998 for (const std::string& part_name : parts_whose_meters_are_affected) {
999 std::shared_ptr<GG::Texture> texture = ClientUI::PartIcon(part_name);
1000 if (texture) {
1001 auto graphic = GG::Wnd::Create<GG::StaticGraphic>(texture, GG::GRAPHIC_FITGRAPHIC | GG::GRAPHIC_PROPSCALE);
1002 m_unlock_icons.push_back(graphic);
1003 graphic->SizeMove(GG::Pt(icon_left, icon_top), GG::Pt(icon_left + icon_width, icon_top + icon_height));
1004 icon_left += icon_width + PAD;
1005 }
1006 }
1007 for (MeterType meter_type : meters_affected) {
1008 std::shared_ptr<GG::Texture> texture = ClientUI::MeterIcon(meter_type);
1009 if (texture) {
1010 auto graphic = GG::Wnd::Create<GG::StaticGraphic>(texture, GG::GRAPHIC_FITGRAPHIC | GG::GRAPHIC_PROPSCALE);
1011 m_unlock_icons.push_back(graphic);
1012 graphic->SizeMove(GG::Pt(icon_left, icon_top), GG::Pt(icon_left + icon_width, icon_top + icon_height));
1013 icon_left += icon_width + PAD;
1014 }
1015 }
1016
1017 for (const std::string& special_name : specials_affected) {
1018 std::shared_ptr<GG::Texture> texture = ClientUI::SpecialIcon(special_name);
1019 if (texture) {
1020 auto graphic = GG::Wnd::Create<GG::StaticGraphic>(texture, GG::GRAPHIC_FITGRAPHIC | GG::GRAPHIC_PROPSCALE);
1021 m_unlock_icons.push_back(graphic);
1022 graphic->SizeMove(GG::Pt(icon_left, icon_top), GG::Pt(icon_left + icon_width, icon_top + icon_height));
1023 icon_left += icon_width + PAD;
1024 }
1025 }
1026 }
1027 }
1028 m_icon->SetColor(icon_colour);
1029
1030 m_name_text = UserString(m_tech_name);
1031 m_name_label->SetText("<s>" + m_name_text + "</s>");
1032
1033 if (const Tech* tech = GetTech(m_tech_name))
1034 m_cost_and_duration_text = boost::io::str(FlexibleFormat(UserString("TECH_TOTAL_COST_ALT_STR"))
1035 % DoubleToString(tech->ResearchCost(client_empire_id), 1, false)
1036 % boost::lexical_cast<int>(tech->ResearchTime(client_empire_id)));
1037 m_cost_and_duration_label->SetText("<s>" + m_cost_and_duration_text + "<s>");
1038
1039 m_eta_label->SetText("<s>" + m_eta_text + "</s>");
1040
1041 ClearBrowseInfoWnd();
1042 SetBrowseInfoWnd(TechRowBrowseWnd(m_tech_name, client_empire_id));
1043
1044 RequirePreRender();
1045 }
1046
1047
1048 //////////////////////////////////////////////////
1049 // TechTreeWnd::LayoutPanel //
1050 //////////////////////////////////////////////////
LayoutPanel(GG::X w,GG::Y h)1051 TechTreeWnd::LayoutPanel::LayoutPanel(GG::X w, GG::Y h) :
1052 GG::Wnd(GG::X0, GG::Y0, w, h, GG::INTERACTIVE)
1053 {}
1054
CompleteConstruction()1055 void TechTreeWnd::LayoutPanel::CompleteConstruction() {
1056 GG::Wnd::CompleteConstruction();
1057
1058 SetChildClippingMode(ClipToClient);
1059
1060 m_scale = std::pow(ZOOM_STEP_SIZE, GetOptionsDB().Get<double>("ui.research.tree.zoom.scale")); // (LATHANDA) Initialise Fullzoom and do real zooming using GL. TODO: Check best size
1061
1062 m_layout_surface = GG::Wnd::Create<LayoutSurface>();
1063
1064 m_vscroll = GG::Wnd::Create<CUIScroll>(GG::VERTICAL);
1065 m_hscroll = GG::Wnd::Create<CUIScroll>(GG::HORIZONTAL);
1066
1067 m_zoom_in_button = Wnd::Create<CUIButton>("+");
1068 m_zoom_in_button->SetColor(ClientUI::WndColor());
1069 m_zoom_out_button = Wnd::Create<CUIButton>("-");
1070 m_zoom_out_button->SetColor(ClientUI::WndColor());
1071
1072 DoLayout();
1073
1074 AttachChild(m_layout_surface);
1075 AttachChild(m_vscroll);
1076 AttachChild(m_hscroll);
1077 AttachChild(m_zoom_in_button);
1078 AttachChild(m_zoom_out_button);
1079
1080 #if BOOST_VERSION >= 106000
1081 using boost::placeholders::_1;
1082 using boost::placeholders::_2;
1083 using boost::placeholders::_3;
1084 using boost::placeholders::_4;
1085 #endif
1086
1087 m_layout_surface->DraggedSignal.connect(boost::bind(&TechTreeWnd::LayoutPanel::TreeDraggedSlot, this, _1));
1088 m_layout_surface->ButtonUpSignal.connect(boost::bind(&TechTreeWnd::LayoutPanel::TreeDragEnd, this, _1));
1089 m_layout_surface->ButtonDownSignal.connect(boost::bind(&TechTreeWnd::LayoutPanel::TreeDragBegin, this, _1));
1090 m_layout_surface->ZoomedSignal.connect(boost::bind(&TechTreeWnd::LayoutPanel::TreeZoomedSlot, this, _1));
1091 m_vscroll->ScrolledSignal.connect(boost::bind(&TechTreeWnd::LayoutPanel::ScrolledSlot, this, _1, _2, _3, _4));
1092 m_hscroll->ScrolledSignal.connect(boost::bind(&TechTreeWnd::LayoutPanel::ScrolledSlot, this, _1, _2, _3, _4));
1093 m_zoom_in_button->LeftClickedSignal.connect(boost::bind(&TechTreeWnd::LayoutPanel::TreeZoomedSlot, this, 1));
1094 m_zoom_out_button->LeftClickedSignal.connect(boost::bind(&TechTreeWnd::LayoutPanel::TreeZoomedSlot, this, -1));
1095
1096 ConnectKeyboardAcceleratorSignals();
1097
1098 // show all categories...
1099 m_categories_shown.clear();
1100 for (const std::string& category_name : GetTechManager().CategoryNames())
1101 m_categories_shown.insert(category_name);
1102
1103 // show statuses
1104 m_tech_statuses_shown.clear();
1105 //m_tech_statuses_shown.insert(TS_UNRESEARCHABLE);
1106 m_tech_statuses_shown.insert(TS_RESEARCHABLE);
1107 m_tech_statuses_shown.insert(TS_COMPLETE);
1108 }
1109
ConnectKeyboardAcceleratorSignals()1110 void TechTreeWnd::LayoutPanel::ConnectKeyboardAcceleratorSignals() {
1111 HotkeyManager* hkm = HotkeyManager::GetManager();
1112
1113 hkm->Connect(boost::bind(&TechTreeWnd::LayoutPanel::TreeZoomInKeyboard, this), "ui.zoom.in",
1114 AndCondition({VisibleWindowCondition(this), NoModalWndsOpenCondition}));
1115 hkm->Connect(boost::bind(&TechTreeWnd::LayoutPanel::TreeZoomInKeyboard, this), "ui.zoom.in.alt",
1116 AndCondition({VisibleWindowCondition(this), NoModalWndsOpenCondition}));
1117 hkm->Connect(boost::bind(&TechTreeWnd::LayoutPanel::TreeZoomOutKeyboard, this), "ui.zoom.out",
1118 AndCondition({VisibleWindowCondition(this), NoModalWndsOpenCondition}));
1119 hkm->Connect(boost::bind(&TechTreeWnd::LayoutPanel::TreeZoomOutKeyboard, this), "ui.zoom.out.alt",
1120 AndCondition({VisibleWindowCondition(this), NoModalWndsOpenCondition}));
1121
1122 hkm->RebuildShortcuts();
1123 }
1124
ClientLowerRight() const1125 GG::Pt TechTreeWnd::LayoutPanel::ClientLowerRight() const
1126 { return LowerRight() - GG::Pt(GG::X(ClientUI::ScrollWidth()), GG::Y(ClientUI::ScrollWidth())); }
1127
GetCategoriesShown() const1128 std::set<std::string> TechTreeWnd::LayoutPanel::GetCategoriesShown() const
1129 { return m_categories_shown; }
1130
Scale() const1131 double TechTreeWnd::LayoutPanel::Scale() const
1132 { return m_scale; }
1133
GetTechStatusesShown() const1134 std::set<TechStatus> TechTreeWnd::LayoutPanel::GetTechStatusesShown() const
1135 { return m_tech_statuses_shown; }
1136
Render()1137 void TechTreeWnd::LayoutPanel::Render() {
1138 GG::FlatRectangle(UpperLeft(), LowerRight(), ClientUI::CtrlColor(), GG::CLR_ZERO);
1139
1140 BeginClipping();
1141
1142 // render dependency arcs
1143 DoZoom(ClientUpperLeft());
1144
1145 m_dependency_arcs.Render(m_scale);
1146
1147 EndClipping();
1148
1149 UndoZoom();
1150 GG::GUI::RenderWindow(m_vscroll);
1151 GG::GUI::RenderWindow(m_hscroll);
1152 }
1153
SizeMove(const GG::Pt & ul,const GG::Pt & lr)1154 void TechTreeWnd::LayoutPanel::SizeMove(const GG::Pt& ul, const GG::Pt& lr) {
1155 const GG::Pt old_size = Size();
1156 GG::Wnd::SizeMove(ul, lr);
1157 if (old_size != Size())
1158 DoLayout();
1159 }
1160
DoLayout()1161 void TechTreeWnd::LayoutPanel::DoLayout() {
1162 const int SCRLWDTH = ClientUI::ScrollWidth();
1163
1164 GG::Pt vscroll_ul = GG::Pt(Width() - SCRLWDTH, GG::Y0);
1165 GG::Pt vscroll_lr = GG::Pt(Width(), Height() - SCRLWDTH);
1166 m_vscroll->SizeMove(vscroll_ul, vscroll_lr);
1167
1168 GG::Pt hscroll_ul = GG::Pt(GG::X0, Height() - SCRLWDTH);
1169 GG::Pt hscroll_lr = GG::Pt(Width() - SCRLWDTH, Height());
1170 m_hscroll->SizeMove(hscroll_ul, hscroll_lr);
1171
1172 const GG::X ZBSIZE(ClientUI::ScrollWidth() * 2);
1173 const int ZBOFFSET = ClientUI::ScrollWidth() / 2;
1174
1175 GG::Pt button_ul = GG::Pt(Width() - ZBSIZE - ZBOFFSET - SCRLWDTH, GG::Y(ZBOFFSET));
1176 m_zoom_in_button->MoveTo(button_ul);
1177 m_zoom_in_button->Resize(GG::Pt(ZBSIZE, m_zoom_in_button->MinUsableSize().y));
1178 button_ul += GG::Pt(GG::X0, m_zoom_in_button->Height() + ZBOFFSET);
1179 m_zoom_out_button->MoveTo(button_ul);
1180 m_zoom_out_button->Resize(GG::Pt(ZBSIZE, m_zoom_out_button->MinUsableSize().y));
1181 }
1182
Update()1183 void TechTreeWnd::LayoutPanel::Update()
1184 { Layout(true); }
1185
Clear()1186 void TechTreeWnd::LayoutPanel::Clear() {
1187 m_vscroll->ScrollTo(0);
1188 m_hscroll->ScrollTo(0);
1189 m_vscroll->SizeScroll(0, 1, 1, 1);
1190 m_hscroll->SizeScroll(0, 1, 1, 1);
1191 GG::SignalScroll(*m_vscroll, true);
1192 GG::SignalScroll(*m_hscroll, true);
1193
1194 // delete all panels
1195 for (const auto& tech_panel: m_techs)
1196 m_layout_surface->DetachChild(tech_panel.second);
1197 m_techs.clear();
1198 m_graph.Clear();
1199
1200 m_dependency_arcs.Reset();
1201
1202 m_selected_tech_name.clear();
1203 }
1204
Reset()1205 void TechTreeWnd::LayoutPanel::Reset() {
1206 // regenerate graph of panels and dependency lines
1207 Layout(false);
1208 }
1209
SetScale(double scale)1210 void TechTreeWnd::LayoutPanel::SetScale(double scale) {
1211 if (scale < MIN_SCALE)
1212 scale = MIN_SCALE;
1213 if (MAX_SCALE < scale)
1214 scale = MAX_SCALE;
1215 m_scale = scale;
1216 GetOptionsDB().Set<double>("ui.research.tree.zoom.scale", std::floor(0.1 + (std::log(m_scale) / std::log(ZOOM_STEP_SIZE))));
1217
1218 for (auto& entry : m_techs)
1219 entry.second->RequirePreRender();
1220 }
1221
ShowCategory(const std::string & category)1222 void TechTreeWnd::LayoutPanel::ShowCategory(const std::string& category) {
1223 if (!m_categories_shown.count(category)) {
1224 m_categories_shown.insert(category);
1225 Layout(true);
1226 }
1227 }
1228
ShowAllCategories()1229 void TechTreeWnd::LayoutPanel::ShowAllCategories() {
1230 const std::vector<std::string> all_cats = GetTechManager().CategoryNames();
1231 if (all_cats.size() == m_categories_shown.size())
1232 return;
1233 for (const std::string& category_name : all_cats)
1234 m_categories_shown.insert(category_name);
1235 Layout(true);
1236 }
1237
HideCategory(const std::string & category)1238 void TechTreeWnd::LayoutPanel::HideCategory(const std::string& category) {
1239 std::set<std::string>::iterator it = m_categories_shown.find(category);
1240 if (it != m_categories_shown.end()) {
1241 m_categories_shown.erase(it);
1242 Layout(true);
1243 }
1244 }
1245
HideAllCategories()1246 void TechTreeWnd::LayoutPanel::HideAllCategories() {
1247 if (m_categories_shown.empty())
1248 return;
1249 m_categories_shown.clear();
1250 Layout(true);
1251 }
1252
ShowStatus(TechStatus status)1253 void TechTreeWnd::LayoutPanel::ShowStatus(TechStatus status) {
1254 if (!m_tech_statuses_shown.count(status)) {
1255 m_tech_statuses_shown.insert(status);
1256 Layout(true);
1257 }
1258 }
1259
HideStatus(TechStatus status)1260 void TechTreeWnd::LayoutPanel::HideStatus(TechStatus status) {
1261 std::set<TechStatus>::iterator it = m_tech_statuses_shown.find(status);
1262 if (it != m_tech_statuses_shown.end()) {
1263 m_tech_statuses_shown.erase(it);
1264 Layout(true);
1265 }
1266 }
1267
CenterOnTech(const std::string & tech_name)1268 void TechTreeWnd::LayoutPanel::CenterOnTech(const std::string& tech_name) {
1269 const auto& it = m_techs.find(tech_name);
1270 if (it == m_techs.end()) {
1271 DebugLogger() << "TechTreeWnd::LayoutPanel::CenterOnTech couldn't centre on " << tech_name
1272 << " due to lack of such a tech panel";
1273 return;
1274 }
1275
1276 auto& tech_panel = it->second;
1277 GG::Pt center_point = tech_panel->UpperLeft();
1278 m_hscroll->ScrollTo(Value(center_point.x));
1279 GG::SignalScroll(*m_hscroll, true);
1280 m_vscroll->ScrollTo(Value(center_point.y));
1281 GG::SignalScroll(*m_vscroll, true);
1282 }
1283
DoZoom(const GG::Pt & pt) const1284 void TechTreeWnd::LayoutPanel::DoZoom(const GG::Pt& pt) const {
1285 glPushMatrix();
1286 //center to panel
1287 glTranslated(Value(Width()/2.0), Value(Height()/2.0), 0);
1288 //zoom
1289 glScaled(m_scale, m_scale, 1);
1290 //translate to actual scroll position
1291 glTranslated(-m_scroll_position_x, -m_scroll_position_y, 0);
1292 glTranslated(Value(pt.x), Value(pt.y), 0);
1293 }
1294
UndoZoom() const1295 void TechTreeWnd::LayoutPanel::UndoZoom() const
1296 { glPopMatrix(); }
1297
ConvertPtScreenToZoomed(const GG::Pt & pt) const1298 GG::Pt TechTreeWnd::LayoutPanel::ConvertPtScreenToZoomed(const GG::Pt& pt) const {
1299 double x = Value(pt.x);
1300 double y = Value(pt.y);
1301 x -= Value(Width()/2.0);
1302 y -= Value(Height()/2.0);
1303 x /= m_scale;
1304 y /= m_scale;
1305 x += m_scroll_position_x;
1306 y += m_scroll_position_y;
1307 return GG::Pt(GG::X(static_cast<int>(x)), GG::Y(static_cast<int>(y)));
1308 }
1309
ConvertPtZoomedToScreen(const GG::Pt & pt) const1310 GG::Pt TechTreeWnd::LayoutPanel::ConvertPtZoomedToScreen(const GG::Pt& pt) const {
1311 double x = Value(pt.x);
1312 double y = Value(pt.y);
1313 x -= m_scroll_position_x;
1314 y -= m_scroll_position_y;
1315 x *= m_scale;
1316 y *= m_scale;
1317 x += Value(Width()/2.0);
1318 y += Value(Height()/2.0);
1319 return GG::Pt(GG::X(static_cast<int>(x)), GG::Y(static_cast<int>(y)));
1320 }
1321
Layout(bool keep_position)1322 void TechTreeWnd::LayoutPanel::Layout(bool keep_position) {
1323 const GG::X TECH_PANEL_MARGIN_X(ClientUI::Pts()*16);
1324 const GG::Y TECH_PANEL_MARGIN_Y(ClientUI::Pts()*16 + 100);
1325 const double RANK_SEP = Value(TechPanelWidth()) * GetOptionsDB().Get<double>("ui.research.tree.spacing.horizontal");
1326 const double NODE_SEP = Value(TechPanelHeight()) * GetOptionsDB().Get<double>("ui.research.tree.spacing.vertical");
1327 const double WIDTH = Value(TechPanelWidth());
1328 const double HEIGHT = Value(TechPanelHeight());
1329 const double X_MARGIN(12);
1330
1331 // view state initial data
1332 int initial_hscroll_pos = m_hscroll->PosnRange().first;
1333 int initial_vscroll_pos = m_vscroll->PosnRange().first;
1334 double initial_hscroll_page_size = m_hscroll->PageSize();
1335 double initial_vscroll_page_size = m_vscroll->PageSize();
1336 const std::string selected_tech = m_selected_tech_name;
1337
1338 // cleanup old data for new layout
1339 Clear();
1340
1341 DebugLogger() << "Tech Tree Layout Preparing Tech Data";
1342
1343 // create a node for every tech
1344 TechManager& manager = GetTechManager();
1345 for (const auto& tech : manager) {
1346 if (!tech) continue;
1347 const std::string& tech_name = tech->Name();
1348 if (!TechVisible(tech_name, m_categories_shown, m_tech_statuses_shown)) continue;
1349 m_techs[tech_name] = GG::Wnd::Create<TechPanel>(tech_name, this);
1350 m_graph.AddNode(tech_name, m_techs[tech_name]->Width(), m_techs[tech_name]->Height());
1351 }
1352
1353 // create an edge for every prerequisite
1354 for (const auto& tech : manager) {
1355 if (!tech) continue;
1356 const std::string& tech_name = tech->Name();
1357 if (!TechVisible(tech_name, m_categories_shown, m_tech_statuses_shown)) continue;
1358 for (const std::string& prereq : tech->Prerequisites()) {
1359 if (!TechVisible(prereq, m_categories_shown, m_tech_statuses_shown)) continue;
1360 m_graph.AddEdge(prereq, tech_name);
1361 }
1362 }
1363
1364 DebugLogger() << "Tech Tree Layout Doing Graph Layout";
1365
1366 //calculate layout
1367 m_graph.DoLayout(static_cast<int>(WIDTH + RANK_SEP),
1368 static_cast<int>(HEIGHT + NODE_SEP),
1369 static_cast<int>(X_MARGIN));
1370
1371 DebugLogger() << "Tech Tree Layout Creating Panels";
1372
1373 std::set<std::string> visible_techs;
1374
1375 // create new tech panels and new dependency arcs
1376 for (const auto& tech : manager) {
1377 if (!tech) continue;
1378 const std::string& tech_name = tech->Name();
1379 if (!TechVisible(tech_name, m_categories_shown, m_tech_statuses_shown)) continue;
1380 //techpanel
1381 const TechTreeLayout::Node* node = m_graph.GetNode(tech_name);
1382 //move TechPanel
1383 auto& tech_panel = m_techs[tech_name];
1384 tech_panel->MoveTo(GG::Pt(node->GetX(), node->GetY()));
1385 m_layout_surface->AttachChild(tech_panel);
1386
1387 #if BOOST_VERSION >= 106000
1388 using boost::placeholders::_1;
1389 #endif
1390
1391 tech_panel->TechLeftClickedSignal.connect(
1392 boost::bind(&TechTreeWnd::LayoutPanel::SelectTech, this, _1));
1393 tech_panel->TechDoubleClickedSignal.connect(TechDoubleClickedSignal);
1394 tech_panel->TechPediaDisplaySignal.connect(TechPediaDisplaySignal);
1395
1396 visible_techs.insert(tech_name);
1397 }
1398
1399 m_dependency_arcs.Reset(m_graph, visible_techs);
1400
1401 // format window
1402 GG::Pt client_sz = ClientSize();
1403 GG::Pt layout_size(client_sz.x + m_graph.GetWidth(), client_sz.y + m_graph.GetHeight());
1404 m_layout_surface->Resize(layout_size);
1405 // format scrollbar
1406 m_vscroll->SizeScroll(0, Value(layout_size.y - 1), std::max(50, Value(std::min(layout_size.y / 10, client_sz.y))), Value(client_sz.y));
1407 m_hscroll->SizeScroll(0, Value(layout_size.x - 1), std::max(50, Value(std::min(layout_size.x / 10, client_sz.x))), Value(client_sz.x));
1408
1409 DebugLogger() << "Tech Tree Layout Done";
1410
1411 // restore save data
1412 if (keep_position) {
1413 m_selected_tech_name = selected_tech;
1414 // select clicked on tech
1415 if (m_techs.count(m_selected_tech_name))
1416 m_techs[m_selected_tech_name]->Select(true);
1417 double hscroll_page_size_ratio = m_hscroll->PageSize() / initial_hscroll_page_size;
1418 double vscroll_page_size_ratio = m_vscroll->PageSize() / initial_vscroll_page_size;
1419 m_hscroll->ScrollTo(static_cast<int>(initial_hscroll_pos * hscroll_page_size_ratio));
1420 m_vscroll->ScrollTo(static_cast<int>(initial_vscroll_pos * vscroll_page_size_ratio));
1421 GG::SignalScroll(*m_hscroll, true);
1422 GG::SignalScroll(*m_vscroll, true);
1423 } else {
1424 m_selected_tech_name.clear();
1425 // find a tech to centre view on
1426 for (const auto& tech : manager) {
1427 const std::string& tech_name = tech->Name();
1428 if (TechVisible(tech_name, m_categories_shown, m_tech_statuses_shown)) {
1429 CenterOnTech(tech_name);
1430 break;
1431 }
1432 }
1433 }
1434
1435 // ensure that the scrolls stay on top
1436 MoveChildUp(m_vscroll);
1437 MoveChildUp(m_hscroll);
1438 }
1439
ScrolledSlot(int,int,int,int)1440 void TechTreeWnd::LayoutPanel::ScrolledSlot(int, int, int, int) {
1441 m_scroll_position_x = m_hscroll->PosnRange().first;
1442 m_scroll_position_y = m_vscroll->PosnRange().first;
1443 }
1444
SelectTech(const std::string & tech_name)1445 void TechTreeWnd::LayoutPanel::SelectTech(const std::string& tech_name)
1446 {
1447 // deselect previously-selected tech panel
1448 if (m_techs.count(m_selected_tech_name))
1449 m_techs[m_selected_tech_name]->Select(false);
1450 // select clicked on tech
1451 if (m_techs.count(tech_name))
1452 m_techs[tech_name]->Select(true);
1453 m_selected_tech_name = tech_name;
1454 TechSelectedSignal(tech_name, GG::Flags<GG::ModKey>());
1455 }
1456
TreeDraggedSlot(const GG::Pt & move)1457 void TechTreeWnd::LayoutPanel::TreeDraggedSlot(const GG::Pt& move) {
1458 m_hscroll->ScrollTo(m_drag_scroll_position_x - Value(move.x / m_scale));
1459 m_vscroll->ScrollTo(m_drag_scroll_position_y - Value(move.y / m_scale));
1460 m_scroll_position_x = m_hscroll->PosnRange().first;
1461 m_scroll_position_y = m_vscroll->PosnRange().first;
1462 }
1463
TreeDragBegin(const GG::Pt & pt)1464 void TechTreeWnd::LayoutPanel::TreeDragBegin(const GG::Pt& pt) {
1465 m_drag_scroll_position_x = m_scroll_position_x;
1466 m_drag_scroll_position_y = m_scroll_position_y;
1467 }
1468
TreeDragEnd(const GG::Pt & pt)1469 void TechTreeWnd::LayoutPanel::TreeDragEnd(const GG::Pt& pt) {
1470 m_drag_scroll_position_x = m_scroll_position_x;
1471 m_drag_scroll_position_y = m_scroll_position_y;
1472 }
1473
TreeZoomedSlot(int move)1474 void TechTreeWnd::LayoutPanel::TreeZoomedSlot(int move) {
1475 if (0 < move)
1476 SetScale(m_scale * ZOOM_STEP_SIZE);
1477 else if (move < 0)
1478 SetScale(m_scale / ZOOM_STEP_SIZE);
1479 //std::cout << m_scale << std::endl;
1480 }
1481
1482 // The bool return value is to re-use the MapWnd::KeyboardZoomIn hotkey
TreeZoomInKeyboard()1483 bool TechTreeWnd::LayoutPanel::TreeZoomInKeyboard() {
1484 TreeZoomedSlot(1);
1485 return true;
1486 }
1487
TreeZoomOutKeyboard()1488 bool TechTreeWnd::LayoutPanel::TreeZoomOutKeyboard() {
1489 TreeZoomedSlot(-1);
1490 return true;
1491 }
1492
1493 //////////////////////////////////////////////////
1494 // TechTreeWnd::TechListBox //
1495 //////////////////////////////////////////////////
1496 class TechTreeWnd::TechListBox : public CUIListBox {
1497 public:
1498 /** \name Structors */ //@{
1499 TechListBox(GG::X w, GG::Y h);
1500 virtual ~TechListBox();
1501 //@}
1502
1503 void CompleteConstruction() override;
1504
1505 /** \name Accessors */ //@{
1506 bool TechRowCmp(const GG::ListBox::Row& lhs, const GG::ListBox::Row& rhs, std::size_t column);
1507 //@}
1508
1509 //! \name Mutators //@{
1510 void Reset();
1511 void Update(bool populate = true);
1512
1513 void ShowCategory(const std::string& category);
1514 void ShowAllCategories();
1515 void HideCategory(const std::string& category);
1516 void HideAllCategories();
1517 void ShowStatus(TechStatus status);
1518 void HideStatus(TechStatus status);
1519 //@}
1520
1521 mutable TechClickSignalType TechLeftClickedSignal; ///< emitted when a technology is single-left-clicked
1522 mutable TechClickSignalType TechDoubleClickedSignal;///< emitted when a technology is double-clicked
1523 mutable TechSignalType TechPediaDisplaySignal; ///< emitted when requesting a pedia lookup
1524
1525 private:
1526 class TechRow : public CUIListBox::Row {
1527 public:
1528 TechRow(GG::X w, const std::string& tech_name);
1529
1530 void CompleteConstruction() override;
1531
1532 void Render() override;
1533
GetTech()1534 const std::string& GetTech() { return m_tech; }
1535 static std::vector<GG::X> ColWidths(GG::X total_width);
1536 static std::vector<GG::Alignment> ColAlignments();
1537 void Update();
1538
1539 private:
1540 std::string m_tech;
1541 GG::Clr m_background_color;
1542 bool m_enqueued;
1543 };
1544
1545 void Populate(bool update = true);
1546 void TechDoubleClicked(GG::ListBox::iterator it, const GG::Pt& pt, const GG::Flags<GG::ModKey>& modkeys);
1547 void TechLeftClicked(GG::ListBox::iterator it, const GG::Pt& pt, const GG::Flags<GG::ModKey>& modkeys);
1548 void TechRightClicked(GG::ListBox::iterator it, const GG::Pt& pt, const GG::Flags<GG::ModKey>& modkeys);
1549 void ToggleSortCol(unsigned int col);
1550
1551 std::set<std::string> m_categories_shown;
1552 std::set<TechStatus> m_tech_statuses_shown;
1553 std::unordered_map<std::string, std::shared_ptr<TechRow>> m_tech_row_cache;
1554 std::shared_ptr<GG::ListBox::Row> m_header_row;
1555 size_t m_previous_sort_col;
1556 };
1557
Render()1558 void TechTreeWnd::TechListBox::TechRow::Render() {
1559 GG::Pt ul = UpperLeft();
1560 GG::Pt lr = LowerRight();
1561 GG::Pt offset{GG::X(3), GG::Y(3)};
1562 if (m_enqueued) {
1563 GG::FlatRectangle(ul, lr, ClientUI::WndColor(), GG::CLR_WHITE, 1);
1564 GG::FlatRoundedRectangle(ul + offset, lr - offset, m_background_color, GG::CLR_WHITE);
1565 } else {
1566 GG::FlatRectangle(ul, lr, m_background_color, GG::CLR_WHITE, 1);
1567 }
1568 }
1569
ColWidths(GG::X total_width)1570 std::vector<GG::X> TechTreeWnd::TechListBox::TechRow::ColWidths(GG::X total_width) {
1571 GG::X graphic_width(GetOptionsDB().Get<int>("ui.research.list.column.graphic.width"));
1572 GG::X name_width(GetOptionsDB().Get<int>("ui.research.list.column.name.width"));
1573 GG::X cost_width(GetOptionsDB().Get<int>("ui.research.list.column.cost.width"));
1574 GG::X time_width(GetOptionsDB().Get<int>("ui.research.list.column.time.width"));
1575 GG::X category_width(GetOptionsDB().Get<int>("ui.research.list.column.category.width"));
1576
1577 GG::X cols_width_sum = graphic_width + name_width + cost_width + time_width + category_width;
1578
1579 GG::X desc_width(std::max(GetOptionsDB().Get<int>("ui.research.list.column.description.width"),
1580 Value(total_width - cols_width_sum)));
1581
1582 return {graphic_width, name_width, cost_width, time_width, category_width, desc_width};
1583 }
1584
ColAlignments()1585 std::vector<GG::Alignment> TechTreeWnd::TechListBox::TechRow::ColAlignments() {
1586 // graphic, name, cost, time, category, description
1587 return {GG::ALIGN_CENTER, GG::ALIGN_LEFT, GG::ALIGN_RIGHT, GG::ALIGN_RIGHT, GG::ALIGN_LEFT, GG::ALIGN_LEFT};
1588 }
1589
TechRowCmp(const GG::ListBox::Row & lhs,const GG::ListBox::Row & rhs,std::size_t column)1590 bool TechTreeWnd::TechListBox::TechRowCmp(const GG::ListBox::Row& lhs, const GG::ListBox::Row& rhs, std::size_t column) {
1591 bool retval = false;
1592 const std::string lhs_key = boost::trim_copy(lhs.SortKey(column));
1593 const std::string rhs_key = boost::trim_copy(rhs.SortKey(column));
1594
1595 // When equal, sort by previous sorted column
1596 if ((lhs_key == rhs_key) && (m_previous_sort_col != column)) {
1597 retval = TechRowCmp(lhs, rhs, m_previous_sort_col);
1598 } else {
1599 try { // attempt compare by int
1600 retval = boost::lexical_cast<int>(lhs_key) < boost::lexical_cast<int>(rhs_key);
1601 } catch (const boost::bad_lexical_cast& e) {
1602 retval = GetLocale("en_US.UTF-8").operator()(lhs_key, rhs_key);
1603 }
1604 }
1605
1606 return retval;
1607 }
1608
TechRow(GG::X w,const std::string & tech_name)1609 TechTreeWnd::TechListBox::TechRow::TechRow(GG::X w, const std::string& tech_name) :
1610 CUIListBox::Row(w, GG::Y(ClientUI::Pts() * 2 + 5)),
1611 m_tech(tech_name),
1612 m_background_color(ClientUI::WndColor()),
1613 m_enqueued(false)
1614 { SetDragDropDataType("TechListBox::TechRow"); }
1615
CompleteConstruction()1616 void TechTreeWnd::TechListBox::TechRow::CompleteConstruction() {
1617
1618 CUIListBox::Row::CompleteConstruction();
1619
1620 const Tech* this_row_tech = ::GetTech(m_tech);
1621 if (!this_row_tech)
1622 return;
1623
1624 std::vector<GG::X> col_widths = ColWidths(Width());
1625 const GG::X GRAPHIC_WIDTH = col_widths[0];
1626 const GG::Y ICON_HEIGHT(std::min(Value(Height()) - 12, std::max(ClientUI::Pts(), Value(GRAPHIC_WIDTH) - 6)));
1627 // TODO replace string padding with new TextFormat flag
1628 std::string just_pad = " ";
1629
1630 auto graphic = GG::Wnd::Create<GG::StaticGraphic>(ClientUI::TechIcon(m_tech),
1631 GG::GRAPHIC_VCENTER | GG::GRAPHIC_CENTER | GG::GRAPHIC_PROPSCALE | GG::GRAPHIC_FITGRAPHIC);
1632 graphic->Resize(GG::Pt(GRAPHIC_WIDTH, ICON_HEIGHT));
1633 graphic->SetColor(ClientUI::CategoryColor(this_row_tech->Category()));
1634 push_back(graphic);
1635
1636 auto text = GG::Wnd::Create<CUILabel>(just_pad + UserString(m_tech), GG::FORMAT_LEFT);
1637 text->SetResetMinSize(false);
1638 text->ClipText(true);
1639 text->SetChildClippingMode(ClipToWindow);
1640 push_back(text);
1641
1642 std::string cost_str = std::to_string(std::lround(this_row_tech->ResearchCost(HumanClientApp::GetApp()->EmpireID())));
1643 text = GG::Wnd::Create<CUILabel>(cost_str + just_pad + just_pad, GG::FORMAT_RIGHT);
1644 text->SetResetMinSize(false);
1645 text->ClipText(true);
1646 text->SetChildClippingMode(ClipToWindow);
1647 push_back(text);
1648
1649 std::string time_str = std::to_string(this_row_tech->ResearchTime(HumanClientApp::GetApp()->EmpireID()));
1650 text = GG::Wnd::Create<CUILabel>(time_str + just_pad + just_pad, GG::FORMAT_RIGHT);
1651 text->SetResetMinSize(false);
1652 text->ClipText(true);
1653 text->SetChildClippingMode(ClipToWindow);
1654 push_back(text);
1655
1656 text = GG::Wnd::Create<CUILabel>(just_pad + UserString(this_row_tech->Category()), GG::FORMAT_LEFT);
1657 text->SetResetMinSize(false);
1658 text->ClipText(true);
1659 text->SetChildClippingMode(ClipToWindow);
1660 push_back(text);
1661
1662 text = GG::Wnd::Create<CUILabel>(just_pad + UserString(this_row_tech->ShortDescription()), GG::FORMAT_LEFT);
1663 text->ClipText(true);
1664 text->SetChildClippingMode(ClipToWindow);
1665 push_back(text);
1666 }
1667
Update()1668 void TechTreeWnd::TechListBox::TechRow::Update() {
1669 const Tech* this_row_tech = ::GetTech(m_tech);
1670 if (!this_row_tech || this->size() < 4)
1671 return;
1672 // TODO replace string padding with new TextFormat flag
1673 std::string just_pad = " ";
1674
1675 auto client_empire_id = HumanClientApp::GetApp()->EmpireID();
1676 auto empire = GetEmpire(client_empire_id);
1677
1678 std::string cost_str = std::to_string(std::lround(this_row_tech->ResearchCost(client_empire_id)));
1679 if (GG::Button* cost_btn = dynamic_cast<GG::Button*>((size() >= 3) ? at(2) : nullptr))
1680 cost_btn->SetText(cost_str + just_pad + just_pad);
1681
1682 std::string time_str = std::to_string(this_row_tech->ResearchTime(client_empire_id));
1683 if (GG::Button* time_btn = dynamic_cast<GG::Button*>((size() >= 4) ? at(3) : nullptr))
1684 time_btn->SetText(time_str + just_pad + just_pad);
1685
1686 // Adjust colors for tech status
1687 auto foreground_color = ClientUI::CategoryColor(this_row_tech->Category());
1688 auto this_row_status = empire ? empire->GetTechStatus(m_tech) : TS_RESEARCHABLE;
1689 if (this_row_status == TS_COMPLETE) {
1690 foreground_color.a = m_background_color.a; // preserve users 'wnd-color' trasparency
1691 AdjustBrightness(foreground_color, 0.3);
1692 m_background_color = foreground_color;
1693 foreground_color = ClientUI::TextColor();
1694 } else if (this_row_status == TS_UNRESEARCHABLE || this_row_status == TS_HAS_RESEARCHED_PREREQ) {
1695 foreground_color.a = 96;
1696 }
1697
1698 for (std::size_t i = 0; i < size(); ++i)
1699 at(i)->SetColor(foreground_color);
1700
1701 if (empire) {
1702 const ResearchQueue& rq = empire->GetResearchQueue();
1703 m_enqueued = rq.InQueue(m_tech);
1704 } else {
1705 m_enqueued = false;
1706 }
1707
1708 ClearBrowseInfoWnd();
1709 SetBrowseInfoWnd(TechRowBrowseWnd(m_tech, client_empire_id));
1710 }
1711
ToggleSortCol(unsigned int col)1712 void TechTreeWnd::TechListBox::ToggleSortCol(unsigned int col) {
1713 if (SortCol() != col) {
1714 m_previous_sort_col = SortCol();
1715 SetSortCol(col);
1716 } else { // toggle the sort direction
1717 SetStyle(Style() ^ GG::LIST_SORTDESCENDING);
1718 }
1719 }
1720
TechListBox(GG::X w,GG::Y h)1721 TechTreeWnd::TechListBox::TechListBox(GG::X w, GG::Y h) :
1722 CUIListBox()
1723 {
1724 Resize(GG::Pt(w, h));
1725 }
1726
CompleteConstruction()1727 void TechTreeWnd::TechListBox::CompleteConstruction() {
1728 CUIListBox::CompleteConstruction();
1729
1730 #if BOOST_VERSION >= 106000
1731 using boost::placeholders::_1;
1732 using boost::placeholders::_2;
1733 using boost::placeholders::_3;
1734 #endif
1735
1736 DoubleClickedRowSignal.connect(boost::bind(&TechListBox::TechDoubleClicked, this, _1, _2, _3));
1737 LeftClickedRowSignal.connect(boost::bind(&TechListBox::TechLeftClicked, this, _1, _2, _3));
1738 RightClickedRowSignal.connect(boost::bind(&TechListBox::TechRightClicked, this, _1, _2, _3));
1739
1740 SetStyle(GG::LIST_NOSEL);
1741
1742 // show all categories...
1743 m_categories_shown.clear();
1744 for (const std::string& category_name : GetTechManager().CategoryNames())
1745 m_categories_shown.insert(category_name);
1746
1747 // show all statuses except unreasearchable
1748 m_tech_statuses_shown.clear();
1749 //m_tech_statuses_shown.insert(TS_UNRESEARCHABLE);
1750 m_tech_statuses_shown.insert(TS_RESEARCHABLE);
1751 m_tech_statuses_shown.insert(TS_COMPLETE);
1752
1753 GG::X row_width = Width() - ClientUI::ScrollWidth() - ClientUI::Pts();
1754 std::vector<GG::X> col_widths = TechRow::ColWidths(row_width);
1755 const GG::Y HEIGHT(Value(col_widths[0]));
1756 m_header_row = GG::Wnd::Create<GG::ListBox::Row>(row_width, HEIGHT);
1757
1758 auto graphic_col = GG::Wnd::Create<CUILabel>(""); // graphic
1759 graphic_col->Resize(GG::Pt(col_widths[0], HEIGHT));
1760 graphic_col->ClipText(true);
1761 graphic_col->SetChildClippingMode(ClipToWindow);
1762 m_header_row->push_back(graphic_col);
1763
1764 auto name_col = Wnd::Create<CUIButton>(UserString("TECH_WND_LIST_COLUMN_NAME"));
1765 name_col->Resize(GG::Pt(col_widths[1], HEIGHT));
1766 name_col->SetChildClippingMode(ClipToWindow);
1767 name_col->LeftClickedSignal.connect([this]() { ToggleSortCol(1); });
1768 m_header_row->push_back(name_col);
1769
1770 auto cost_col = Wnd::Create<CUIButton>(UserString("TECH_WND_LIST_COLUMN_COST"));
1771 cost_col->Resize(GG::Pt(col_widths[2], HEIGHT));
1772 cost_col->SetChildClippingMode(ClipToWindow);
1773 cost_col->LeftClickedSignal.connect([this]() { ToggleSortCol(2); });
1774 m_header_row->push_back(cost_col);
1775
1776 auto time_col = Wnd::Create<CUIButton>(UserString("TECH_WND_LIST_COLUMN_TIME"));
1777 time_col->Resize(GG::Pt(col_widths[3], HEIGHT));
1778 time_col->SetChildClippingMode(ClipToWindow);
1779 time_col->LeftClickedSignal.connect([this]() { ToggleSortCol(3); });
1780 m_header_row->push_back(time_col);
1781
1782 auto category_col = Wnd::Create<CUIButton>( UserString("TECH_WND_LIST_COLUMN_CATEGORY"));
1783 category_col->Resize(GG::Pt(col_widths[4], HEIGHT));
1784 category_col->SetChildClippingMode(ClipToWindow);
1785 category_col->LeftClickedSignal.connect([this]() { ToggleSortCol(4); });
1786 m_header_row->push_back(category_col);
1787
1788 auto descr_col = Wnd::Create<CUIButton>(UserString("TECH_WND_LIST_COLUMN_DESCRIPTION"));
1789 descr_col->Resize(GG::Pt(col_widths[5], HEIGHT));
1790 descr_col->SetChildClippingMode(ClipToWindow);
1791 descr_col->LeftClickedSignal.connect([this]() { ToggleSortCol(5); });
1792 m_header_row->push_back(descr_col);
1793
1794 m_header_row->Resize(GG::Pt(row_width, HEIGHT));
1795
1796 // Initialize column widths before setting header
1797 int num_cols = m_header_row->size();
1798 std::vector<GG::Alignment> col_alignments = TechRow::ColAlignments();
1799 SetNumCols(num_cols);
1800 for (int i = 0; i < num_cols; ++i) {
1801 SetColWidth(i, m_header_row->at(i)->Width());
1802 SetColAlignment(i, col_alignments[i]);
1803 }
1804
1805 SetColHeaders(m_header_row);
1806 LockColWidths();
1807
1808 // Initialize sorting
1809 SetSortCol(2);
1810 m_previous_sort_col = 3;
1811 SetSortCmp([&](const GG::ListBox::Row& lhs, const GG::ListBox::Row& rhs, std::size_t col) { return TechRowCmp(lhs, rhs, col); });
1812 }
1813
~TechListBox()1814 TechTreeWnd::TechListBox::~TechListBox()
1815 {}
1816
Reset()1817 void TechTreeWnd::TechListBox::Reset()
1818 {
1819 m_tech_row_cache.clear();
1820 Populate();
1821 }
1822
Update(bool populate)1823 void TechTreeWnd::TechListBox::Update(bool populate /* = true */) {
1824 if (populate)
1825 Populate(false);
1826
1827 DebugLogger() << "Tech List Box Updating";
1828
1829 double insertion_elapsed = 0.0;
1830 ScopedTimer insertion_timer;
1831 GG::X row_width = Width() - ClientUI::ScrollWidth() - ClientUI::Pts();
1832
1833 // Try to preserve the first row, only works if a row for the tech is still visible
1834 std::string first_tech_shown;
1835 if (FirstRowShown() != end())
1836 if (auto first_row = FirstRowShown()->get())
1837 if (auto first_row_shown = dynamic_cast<TechRow*>(first_row))
1838 first_tech_shown = first_row_shown->GetTech();
1839
1840 // Skip setting first row during insertion
1841 bool first_tech_set = first_tech_shown.empty();
1842
1843 // remove techs in listbox, then reset the rest of its state
1844 for (iterator it = begin(); it != end(); ) {
1845 iterator temp_it = it++;
1846 Erase(temp_it);
1847 }
1848
1849 Clear();
1850
1851 // Add rows from cache
1852 for (auto& row : m_tech_row_cache) {
1853 auto& tech_row = row.second;
1854 if (TechVisible(tech_row->GetTech(), m_categories_shown, m_tech_statuses_shown)) {
1855 tech_row->Update();
1856 insertion_timer.restart();
1857 auto listbox_row_it = Insert(tech_row);
1858 insertion_elapsed += insertion_timer.duration();
1859 if (!first_tech_set && row.first == first_tech_shown) {
1860 first_tech_set = true;
1861 SetFirstRowShown(listbox_row_it);
1862 }
1863 }
1864 }
1865
1866 // set attributes after clear and insert, cached rows may have incorrect widths
1867 std::vector<GG::X> col_widths = TechRow::ColWidths(row_width);
1868 int num_cols = static_cast<int>(col_widths.size());
1869 for (int i = 0; i < num_cols; ++i) {
1870 SetColWidth(i, col_widths[i]);
1871 // only stretch the last column
1872 SetColStretch(i, (i < num_cols - 1) ? 0.0 : 1.0);
1873 }
1874 if (SortCol() < 1)
1875 SetSortCol(2);
1876
1877 if (!first_tech_set || first_tech_shown.empty())
1878 BringRowIntoView(begin());
1879
1880 DebugLogger() << "Tech List Box Updating Done, Insertion time = " << (insertion_elapsed * 1000) << " ms";
1881 }
1882
Populate(bool update)1883 void TechTreeWnd::TechListBox::Populate(bool update /* = true*/) {
1884 DebugLogger() << "Tech List Box Populating";
1885
1886 GG::X row_width = Width() - ClientUI::ScrollWidth() - ClientUI::Pts();
1887
1888 ScopedTimer creation_timer;
1889
1890 // Skip lookup check when starting with empty cache
1891 bool new_cache = m_tech_row_cache.empty();
1892 for (const auto& tech : GetTechManager()) {
1893 if (new_cache || !m_tech_row_cache.count(tech->Name()))
1894 m_tech_row_cache.emplace(tech->Name(), GG::Wnd::Create<TechRow>(row_width, tech->Name()));
1895 }
1896
1897 DebugLogger() << "Tech List Box Populating Done, Creation time = " << creation_timer.DurationString();
1898
1899 if (update)
1900 Update(false);
1901 }
1902
ShowCategory(const std::string & category)1903 void TechTreeWnd::TechListBox::ShowCategory(const std::string& category) {
1904 if (!m_categories_shown.count(category)) {
1905 m_categories_shown.insert(category);
1906 Populate();
1907 }
1908 }
1909
ShowAllCategories()1910 void TechTreeWnd::TechListBox::ShowAllCategories() {
1911 const std::vector<std::string> all_cats = GetTechManager().CategoryNames();
1912 if (all_cats.size() == m_categories_shown.size())
1913 return;
1914 for (const std::string& category_name : all_cats)
1915 m_categories_shown.insert(category_name);
1916 Populate();
1917 }
1918
HideCategory(const std::string & category)1919 void TechTreeWnd::TechListBox::HideCategory(const std::string& category) {
1920 std::set<std::string>::iterator it = m_categories_shown.find(category);
1921 if (it != m_categories_shown.end()) {
1922 m_categories_shown.erase(it);
1923 Populate();
1924 }
1925 }
1926
HideAllCategories()1927 void TechTreeWnd::TechListBox::HideAllCategories() {
1928 if (m_categories_shown.empty())
1929 return;
1930 m_categories_shown.clear();
1931 Populate();
1932 }
1933
ShowStatus(TechStatus status)1934 void TechTreeWnd::TechListBox::ShowStatus(TechStatus status) {
1935 if (!m_tech_statuses_shown.count(status)) {
1936 m_tech_statuses_shown.insert(status);
1937 Populate();
1938 }
1939 }
1940
HideStatus(TechStatus status)1941 void TechTreeWnd::TechListBox::HideStatus(TechStatus status) {
1942 std::set<TechStatus>::iterator it = m_tech_statuses_shown.find(status);
1943 if (it != m_tech_statuses_shown.end()) {
1944 m_tech_statuses_shown.erase(it);
1945 Populate();
1946 }
1947 }
1948
TechLeftClicked(GG::ListBox::iterator it,const GG::Pt & pt,const GG::Flags<GG::ModKey> & modkeys)1949 void TechTreeWnd::TechListBox::TechLeftClicked(GG::ListBox::iterator it, const GG::Pt& pt, const GG::Flags<GG::ModKey>& modkeys) {
1950 // determine type of row that was clicked, and emit appropriate signal
1951 if (TechRow* tech_row = dynamic_cast<TechRow*>(it->get()))
1952 TechLeftClickedSignal(tech_row->GetTech(), GG::Flags<GG::ModKey>());
1953 }
1954
TechRightClicked(GG::ListBox::iterator it,const GG::Pt & pt,const GG::Flags<GG::ModKey> & modkeys)1955 void TechTreeWnd::TechListBox::TechRightClicked(GG::ListBox::iterator it, const GG::Pt& pt, const GG::Flags<GG::ModKey>& modkeys) {
1956 if ((*it)->Disabled())
1957 return;
1958 const Empire* empire = GetEmpire(HumanClientApp::GetApp()->EmpireID());
1959 if (!empire)
1960 return;
1961
1962 TechRow* tech_row = dynamic_cast<TechRow*>(it->get());
1963 if (!tech_row)
1964 return;
1965 const std::string& tech_name = tech_row->GetTech();
1966
1967 auto popup = GG::Wnd::Create<CUIPopupMenu>(pt.x, pt.y);
1968 const ResearchQueue& rq = empire->GetResearchQueue();
1969 if (!rq.InQueue(tech_name)) {
1970 auto tech_dclick_action = [this, it, pt]() { TechDoubleClicked(it, pt, GG::Flags<GG::ModKey>()); };
1971 auto tech_ctrl_dclick_action = [this, it, pt]() { TechDoubleClicked(it, pt, GG::MOD_KEY_CTRL); };
1972
1973 if (!empire->TechResearched(tech_name)) {
1974 popup->AddMenuItem(GG::MenuItem(UserString("PRODUCTION_DETAIL_ADD_TO_QUEUE"), false, false,
1975 tech_dclick_action));
1976 popup->AddMenuItem(GG::MenuItem(UserString("PRODUCTION_DETAIL_ADD_TO_TOP_OF_QUEUE"), false, false,
1977 tech_ctrl_dclick_action));
1978 }
1979 }
1980
1981 auto pedia_display_action = [this, &tech_name]() { TechPediaDisplaySignal(tech_name); };
1982 std::string popup_label = boost::io::str(FlexibleFormat(UserString("ENC_LOOKUP")) % UserString(tech_name));
1983 popup->AddMenuItem(GG::MenuItem(popup_label, false, false, pedia_display_action));
1984
1985 popup->Run();
1986 }
1987
TechDoubleClicked(GG::ListBox::iterator it,const GG::Pt & pt,const GG::Flags<GG::ModKey> & modkeys)1988 void TechTreeWnd::TechListBox::TechDoubleClicked(GG::ListBox::iterator it, const GG::Pt& pt, const GG::Flags<GG::ModKey>& modkeys) {
1989 // determine type of row that was clicked, and emit appropriate signal
1990 TechRow* tech_row = dynamic_cast<TechRow*>(it->get());
1991 if (tech_row)
1992 TechDoubleClickedSignal(tech_row->GetTech(), modkeys);
1993 }
1994
1995
1996 //////////////////////////////////////////////////
1997 // TechTreeWnd //
1998 //////////////////////////////////////////////////
TechTreeWnd(GG::X w,GG::Y h,bool initially_hidden)1999 TechTreeWnd::TechTreeWnd(GG::X w, GG::Y h, bool initially_hidden /*= true*/) :
2000 GG::Wnd(GG::X0, GG::Y0, w, h, GG::INTERACTIVE),
2001 m_init_flag(initially_hidden)
2002 {}
2003
CompleteConstruction()2004 void TechTreeWnd::CompleteConstruction() {
2005 GG::Wnd::CompleteConstruction();
2006
2007 Sound::TempUISoundDisabler sound_disabler;
2008
2009 #if BOOST_VERSION >= 106000
2010 using boost::placeholders::_1;
2011 using boost::placeholders::_2;
2012 #endif
2013
2014 m_layout_panel = GG::Wnd::Create<LayoutPanel>(Width(), Height());
2015 m_layout_panel->TechSelectedSignal.connect(boost::bind(&TechTreeWnd::TechLeftClickedSlot, this, _1, _2));
2016 m_layout_panel->TechDoubleClickedSignal.connect(
2017 [this](const std::string& tech_name, GG::Flags<GG::ModKey> modkeys)
2018 { this->AddTechToResearchQueue(tech_name, modkeys & GG::MOD_KEY_CTRL); });
2019 m_layout_panel->TechPediaDisplaySignal.connect(boost::bind(&TechTreeWnd::TechPediaDisplaySlot, this, _1));
2020 AttachChild(m_layout_panel);
2021
2022 m_tech_list = GG::Wnd::Create<TechListBox>(Width(), Height());
2023 m_tech_list->TechLeftClickedSignal.connect(boost::bind(&TechTreeWnd::TechLeftClickedSlot, this, _1, _2));
2024 m_tech_list->TechDoubleClickedSignal.connect(
2025 [this](const std::string& tech_name, GG::Flags<GG::ModKey> modkeys)
2026 { this->AddTechToResearchQueue(tech_name, modkeys & GG::MOD_KEY_CTRL); });
2027 m_tech_list->TechPediaDisplaySignal.connect(
2028 boost::bind(&TechTreeWnd::TechPediaDisplaySlot, this, _1));
2029
2030 m_enc_detail_panel = GG::Wnd::Create<EncyclopediaDetailPanel>(GG::ONTOP | GG::INTERACTIVE | GG::DRAGABLE | GG::RESIZABLE | CLOSABLE | PINABLE, RES_PEDIA_WND_NAME);
2031 m_tech_tree_controls = GG::Wnd::Create<TechTreeControls>(RES_CONTROLS_WND_NAME);
2032
2033 m_enc_detail_panel->ClosingSignal.connect(boost::bind(&TechTreeWnd::HidePedia, this));
2034
2035 InitializeWindows();
2036 // Make sure the controls don't overlap the bottom scrollbar
2037 if (m_tech_tree_controls->Bottom() > m_layout_panel->Bottom() - ClientUI::ScrollWidth()) {
2038 m_tech_tree_controls->MoveTo(GG::Pt(m_tech_tree_controls->Left(),
2039 m_layout_panel->Bottom() - ClientUI::ScrollWidth() - m_tech_tree_controls->Height()));
2040 m_tech_tree_controls->SaveDefaultedOptions();
2041 }
2042
2043 HumanClientApp::GetApp()->RepositionWindowsSignal.connect(
2044 boost::bind(&TechTreeWnd::InitializeWindows, this));
2045
2046 AttachChild(m_enc_detail_panel);
2047 AttachChild(m_tech_tree_controls);
2048
2049 // connect category button clicks to update display
2050 for (auto& cat_button : m_tech_tree_controls->m_cat_buttons) {
2051 const std::string& category_name = cat_button.first;
2052 cat_button.second->CheckedSignal.connect(
2053 [this, category_name](bool checked) {
2054 if(checked)
2055 this->ShowCategory(category_name);
2056 else
2057 this->HideCategory(category_name);
2058 }
2059 );
2060 }
2061
2062 // connect button for all categories to update display
2063 m_tech_tree_controls->m_all_cat_button->CheckedSignal.connect(
2064 [this](bool checked) {
2065 if(checked)
2066 this->ShowAllCategories();
2067 else
2068 this->HideAllCategories();
2069 }
2070 );
2071
2072 // connect status and type button clicks to update display
2073 for (auto& status_button : m_tech_tree_controls->m_status_buttons) {
2074 TechStatus tech_status = status_button.first;
2075 status_button.second->CheckedSignal.connect(
2076 [this, tech_status](bool checked){ this->SetTechStatus(tech_status, checked); });
2077 }
2078
2079 // connect view type selector
2080 m_tech_tree_controls->m_view_type_button->CheckedSignal.connect(
2081 boost::bind(&TechTreeWnd::ToggleViewType, this, _1));
2082
2083 //TechTreeWnd in typically constructed before the UI client has
2084 //accesss to the technologies so showing these categories takes a
2085 //long time and generates errors, but is never seen by the user.
2086 if (!m_init_flag) {
2087 ShowAllCategories();
2088 SetTechStatus(TS_COMPLETE, GetOptionsDB().Get<bool>("ui.research.status.completed.shown"));
2089 SetTechStatus(TS_UNRESEARCHABLE, GetOptionsDB().Get<bool>("ui.research.status.unresearchable.shown"));
2090 SetTechStatus(TS_HAS_RESEARCHED_PREREQ, GetOptionsDB().Get<bool>("ui.research.status.partial.shown"));
2091 SetTechStatus(TS_RESEARCHABLE, GetOptionsDB().Get<bool>("ui.research.status.researchable.shown"));
2092 }
2093
2094 ShowTreeView();
2095 }
2096
~TechTreeWnd()2097 TechTreeWnd::~TechTreeWnd()
2098 {}
2099
SizeMove(const GG::Pt & ul,const GG::Pt & lr)2100 void TechTreeWnd::SizeMove(const GG::Pt& ul, const GG::Pt& lr) {
2101 const GG::Pt old_size = Size();
2102 GG::Wnd::SizeMove(ul, lr);
2103 if (old_size != Size()) {
2104 m_enc_detail_panel->ValidatePosition();
2105 m_tech_tree_controls->ValidatePosition();
2106 m_layout_panel->Resize(this->Size());
2107 m_tech_list->Resize(this->Size());
2108 }
2109 }
2110
Scale() const2111 double TechTreeWnd::Scale() const
2112 { return m_layout_panel->Scale(); }
2113
Update()2114 void TechTreeWnd::Update() {
2115 m_layout_panel->Update();
2116 m_tech_list->Update();
2117 }
2118
Clear()2119 void TechTreeWnd::Clear() {
2120 m_enc_detail_panel->OnIndex();
2121 m_layout_panel->Clear();
2122 }
2123
Reset()2124 void TechTreeWnd::Reset() {
2125 m_layout_panel->Reset();
2126 m_tech_list->Reset();
2127 }
2128
InitializeWindows()2129 void TechTreeWnd::InitializeWindows() {
2130 const GG::Pt pedia_ul(GG::X0, GG::Y0);
2131 const GG::Pt pedia_wh(GG::X(480), GG::Y(240));
2132
2133 // Don't know this wnd's height in advance so place it off the bottom edge,
2134 // it subclasses CUIWnd so it will reposition itself to be visible.
2135 const GG::Pt controls_ul(GG::X1, m_layout_panel->Height());
2136 const GG::Pt controls_wh((m_layout_panel->Width() * 0.6) - ClientUI::ScrollWidth(), GG::Y0);
2137
2138 m_enc_detail_panel->InitSizeMove(pedia_ul, pedia_ul + pedia_wh);
2139 m_tech_tree_controls->InitSizeMove(controls_ul, controls_ul + controls_wh);
2140 }
2141
Show()2142 void TechTreeWnd::Show() {
2143 GG::Wnd::Show();
2144
2145 // When Show() is called for TechTree the ClientUI should now have
2146 // access to the technologies so that parsing does not generate
2147 // errors.
2148 if (m_init_flag) {
2149 m_init_flag = false;
2150 ShowAllCategories();
2151 SetTechStatus(TS_COMPLETE, GetOptionsDB().Get<bool>("ui.research.status.completed.shown"));
2152 SetTechStatus(TS_UNRESEARCHABLE, GetOptionsDB().Get<bool>("ui.research.status.unresearchable.shown"));
2153 SetTechStatus(TS_HAS_RESEARCHED_PREREQ, GetOptionsDB().Get<bool>("ui.research.status.partial.shown"));
2154 SetTechStatus(TS_RESEARCHABLE, GetOptionsDB().Get<bool>("ui.research.status.researchable.shown"));
2155 }
2156 }
2157
ShowCategory(const std::string & category)2158 void TechTreeWnd::ShowCategory(const std::string& category) {
2159 m_layout_panel->ShowCategory(category);
2160 m_tech_list->ShowCategory(category);
2161
2162 const auto& maybe_button = m_tech_tree_controls->m_cat_buttons.find(category);
2163 if (maybe_button != m_tech_tree_controls->m_cat_buttons.end())
2164 maybe_button->second->SetCheck(true);
2165 }
2166
ShowAllCategories()2167 void TechTreeWnd::ShowAllCategories() {
2168 m_layout_panel->ShowAllCategories();
2169 m_tech_list->ShowAllCategories();
2170
2171 for (auto& cat_button : m_tech_tree_controls->m_cat_buttons)
2172 { cat_button.second->SetCheck(true); }
2173 }
2174
HideCategory(const std::string & category)2175 void TechTreeWnd::HideCategory(const std::string& category) {
2176 m_layout_panel->HideCategory(category);
2177 m_tech_list->HideCategory(category);
2178
2179 const auto& maybe_button = m_tech_tree_controls->m_cat_buttons.find(category);
2180 if (maybe_button != m_tech_tree_controls->m_cat_buttons.end())
2181 maybe_button->second->SetCheck(false);
2182 }
2183
HideAllCategories()2184 void TechTreeWnd::HideAllCategories() {
2185 m_layout_panel->HideAllCategories();
2186 m_tech_list->HideAllCategories();
2187
2188 for (auto& cat_button : m_tech_tree_controls->m_cat_buttons)
2189 { cat_button.second->SetCheck(false); }
2190 }
2191
ToggleAllCategories()2192 void TechTreeWnd::ToggleAllCategories() {
2193 std::set<std::string> shown_cats = m_layout_panel->GetCategoriesShown();
2194 const std::vector<std::string> all_cats = GetTechManager().CategoryNames();
2195
2196 if (shown_cats.size() == all_cats.size())
2197 HideAllCategories();
2198 else
2199 ShowAllCategories();
2200 }
2201
SetTechStatus(const TechStatus status,const bool state)2202 void TechTreeWnd::SetTechStatus(const TechStatus status, const bool state) {
2203 switch (status) {
2204 case TS_UNRESEARCHABLE:
2205 GetOptionsDB().Set<bool>("ui.research.status.unresearchable.shown", state);
2206 break;
2207 case TS_HAS_RESEARCHED_PREREQ:
2208 GetOptionsDB().Set<bool>("ui.research.status.partial.shown", state);
2209 break;
2210 case TS_RESEARCHABLE:
2211 GetOptionsDB().Set<bool>("ui.research.status.researchable.shown", state);
2212 break;
2213 case TS_COMPLETE:
2214 GetOptionsDB().Set<bool>("ui.research.status.completed.shown", state);
2215 break;
2216 default:
2217 ; // do nothing
2218 }
2219 GetOptionsDB().Commit();
2220
2221 if (state) {
2222 m_layout_panel->ShowStatus(status);
2223 m_tech_list->ShowStatus(status);
2224 } else {
2225 m_layout_panel->HideStatus(status);
2226 m_tech_list->HideStatus(status);
2227 }
2228
2229 m_tech_tree_controls->SetTechStatus(status, state);
2230 }
2231
ToggleViewType(bool show_list_view)2232 void TechTreeWnd::ToggleViewType(bool show_list_view)
2233 { show_list_view ? ShowListView() : ShowTreeView(); }
2234
ShowTreeView()2235 void TechTreeWnd::ShowTreeView() {
2236 AttachChild(m_layout_panel);
2237 MoveChildDown(m_layout_panel);
2238 DetachChild(m_tech_list);
2239 MoveChildUp(m_tech_tree_controls);
2240 }
2241
ShowListView()2242 void TechTreeWnd::ShowListView() {
2243 m_tech_list->Reset();
2244 AttachChild(m_tech_list);
2245 MoveChildDown(m_tech_list);
2246 DetachChild(m_layout_panel);
2247 MoveChildUp(m_tech_tree_controls);
2248 }
2249
CenterOnTech(const std::string & tech_name)2250 void TechTreeWnd::CenterOnTech(const std::string& tech_name) {
2251 // ensure tech exists and is visible
2252 const Tech* tech = ::GetTech(tech_name);
2253 if (!tech) return;
2254 const Empire* empire = GetEmpire(HumanClientApp::GetApp()->EmpireID());
2255 if (empire)
2256 SetTechStatus(empire->GetTechStatus(tech_name), true);
2257 ShowCategory(tech->Category());
2258
2259 // centre on it
2260 m_layout_panel->CenterOnTech(tech_name);
2261 }
2262
SetEncyclopediaTech(const std::string & tech_name)2263 void TechTreeWnd::SetEncyclopediaTech(const std::string& tech_name)
2264 { m_enc_detail_panel->SetTech(tech_name); }
2265
SelectTech(const std::string & tech_name)2266 void TechTreeWnd::SelectTech(const std::string& tech_name)
2267 { m_layout_panel->SelectTech(tech_name); }
2268
ShowPedia()2269 void TechTreeWnd::ShowPedia() {
2270 m_enc_detail_panel->Refresh();
2271 m_enc_detail_panel->Show();
2272
2273 OptionsDB& db = GetOptionsDB();
2274 db.Set("ui." + RES_PEDIA_WND_NAME + ".hidden.enabled", false);
2275 }
2276
HidePedia()2277 void TechTreeWnd::HidePedia() {
2278 m_enc_detail_panel->Hide();
2279
2280 OptionsDB& db = GetOptionsDB();
2281 db.Set("ui." + RES_PEDIA_WND_NAME + ".hidden.enabled", true);
2282 }
2283
TogglePedia()2284 void TechTreeWnd::TogglePedia() {
2285 if (!m_enc_detail_panel->Visible())
2286 ShowPedia();
2287 else
2288 HidePedia();
2289 }
2290
PediaVisible()2291 bool TechTreeWnd::PediaVisible()
2292 { return m_enc_detail_panel->Visible(); }
2293
TechIsVisible(const std::string & tech_name) const2294 bool TechTreeWnd::TechIsVisible(const std::string& tech_name) const
2295 { return TechVisible(tech_name, m_layout_panel->GetCategoriesShown(), m_layout_panel->GetTechStatusesShown()); }
2296
TechLeftClickedSlot(const std::string & tech_name,const GG::Flags<GG::ModKey> & modkeys)2297 void TechTreeWnd::TechLeftClickedSlot(const std::string& tech_name,
2298 const GG::Flags<GG::ModKey>& modkeys)
2299 {
2300 if (modkeys & GG::MOD_KEY_SHIFT) {
2301 AddTechToResearchQueue(tech_name, modkeys & GG::MOD_KEY_CTRL);
2302 } else {
2303 SetEncyclopediaTech(tech_name);
2304 TechSelectedSignal(tech_name);
2305 }
2306 }
2307
AddTechToResearchQueue(const std::string & tech_name,bool to_front)2308 void TechTreeWnd::AddTechToResearchQueue(const std::string& tech_name,
2309 bool to_front)
2310 {
2311 const Tech* tech = GetTech(tech_name);
2312 if (!tech) return;
2313 const Empire* empire = GetEmpire(HumanClientApp::GetApp()->EmpireID());
2314 TechStatus tech_status = TS_UNRESEARCHABLE;
2315 if (empire)
2316 tech_status = empire->GetTechStatus(tech_name);
2317
2318 int queue_pos = -1;
2319 if (to_front)
2320 queue_pos = 0;
2321
2322 // if tech can be researched already, just add it
2323 if (tech_status == TS_RESEARCHABLE) {
2324 std::vector<std::string> techs;
2325 techs.push_back(tech_name);
2326 AddTechsToQueueSignal(techs, queue_pos);
2327 return;
2328 }
2329
2330 if (tech_status != TS_UNRESEARCHABLE && tech_status != TS_HAS_RESEARCHED_PREREQ)
2331 return;
2332
2333 // if tech can't yet be researched, add any prerequisites it requires (recursively) and then add it
2334 TechManager& manager = GetTechManager();
2335 int empire_id = HumanClientApp::GetApp()->EmpireID();
2336 std::vector<std::string> tech_vec = manager.RecursivePrereqs(tech_name, empire_id);
2337 tech_vec.push_back(tech_name);
2338 AddTechsToQueueSignal(tech_vec, queue_pos);
2339 }
2340
TechPediaDisplaySlot(const std::string & tech_name)2341 void TechTreeWnd::TechPediaDisplaySlot(const std::string& tech_name) {
2342 SetEncyclopediaTech(tech_name);
2343 if (!PediaVisible())
2344 ShowPedia();
2345 }
2346