1 /* LogbookPanel.cpp
2 Copyright (c) 2017 by Michael Zahniser
3
4 Endless Sky is free software: you can redistribute it and/or modify it under the
5 terms of the GNU General Public License as published by the Free Software
6 Foundation, either version 3 of the License, or (at your option) any later version.
7
8 Endless Sky is distributed in the hope that it will be useful, but WITHOUT ANY
9 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
10 PARTICULAR PURPOSE. See the GNU General Public License for more details.
11 */
12
13 #include "LogbookPanel.h"
14
15 #include "text/alignment.hpp"
16 #include "Color.h"
17 #include "text/DisplayText.h"
18 #include "FillShader.h"
19 #include "text/Font.h"
20 #include "text/FontSet.h"
21 #include "GameData.h"
22 #include "text/layout.hpp"
23 #include "PlayerInfo.h"
24 #include "Preferences.h"
25 #include "Screen.h"
26 #include "Sprite.h"
27 #include "SpriteSet.h"
28 #include "SpriteShader.h"
29 #include "UI.h"
30 #include "text/WrappedText.h"
31
32 #include <algorithm>
33 #include <set>
34
35 using namespace std;
36
37 namespace {
38 const double SIDEBAR_WIDTH = 100.;
39 const double TEXT_WIDTH = 400.;
40 const double PAD = 10.;
41 const double WIDTH = SIDEBAR_WIDTH + TEXT_WIDTH;
42 const double LINE_HEIGHT = 25.;
43
44 // The minimum distance in pixels between the selected month and the edge of the screen before the month gets centered
45 const double MINIMUM_SELECTION_DISTANCE = LINE_HEIGHT * 3;
46
47 const double GAP = 30.;
48 const string MONTH[] = {
49 " January", " February", " March", " April", " May", " June",
50 " July", " August", " September", " October", " November", " December"};
51 }
52
53
54
LogbookPanel(PlayerInfo & player)55 LogbookPanel::LogbookPanel(PlayerInfo &player)
56 : player(player)
57 {
58 SetInterruptible(false);
59 if(!player.Logbook().empty())
60 {
61 selectedDate = (--player.Logbook().end())->first;
62 selectedName = MONTH[selectedDate.Month() - 1];
63 }
64 Update();
65 }
66
67
68
69 // Draw this panel.
Draw()70 void LogbookPanel::Draw()
71 {
72 // Dim out everything outside this panel.
73 DrawBackdrop();
74
75 // Draw the panel. The sidebar should be slightly darker than the rest.
76 const Color &sideColor = *GameData::Colors().Get("logbook sidebar");
77 FillShader::Fill(
78 Point(Screen::Left() + .5 * SIDEBAR_WIDTH, 0.),
79 Point(SIDEBAR_WIDTH, Screen::Height()),
80 sideColor);
81 const Color &backColor = *GameData::Colors().Get("logbook background");
82 FillShader::Fill(
83 Point(Screen::Left() + SIDEBAR_WIDTH + .5 * TEXT_WIDTH, 0.),
84 Point(TEXT_WIDTH, Screen::Height()),
85 backColor);
86 const Color &lineColor = *GameData::Colors().Get("logbook line");
87 FillShader::Fill(
88 Point(Screen::Left() + SIDEBAR_WIDTH - .5, 0.),
89 Point(1., Screen::Height()),
90 lineColor);
91
92 const Sprite *edgeSprite = SpriteSet::Get("ui/right edge");
93 if(edgeSprite->Height())
94 {
95 // If the screen is high enough, the edge sprite should repeat.
96 double spriteHeight = edgeSprite->Height();
97 Point pos(
98 Screen::Left() + WIDTH + .5 * edgeSprite->Width(),
99 Screen::Top() + .5 * spriteHeight);
100 for( ; pos.Y() - .5 * spriteHeight < Screen::Bottom(); pos.Y() += spriteHeight)
101 SpriteShader::Draw(edgeSprite, pos);
102 }
103
104 // Colors to be used for drawing the log.
105 const Font &font = FontSet::Get(14);
106 const Color &dim = *GameData::Colors().Get("dim");
107 const Color &medium = *GameData::Colors().Get("medium");
108 const Color &bright = *GameData::Colors().Get("bright");
109
110 // Draw the sidebar.
111 // The currently selected sidebar item should be highlighted. This is how
112 // big the highlight rectangle is.
113 Point highlightSize(SIDEBAR_WIDTH - 4., LINE_HEIGHT);
114 Point highlightOffset = Point(4. - PAD, 0.) + .5 * highlightSize;
115 Point textOffset(0., .5 * (LINE_HEIGHT - font.Height()));
116 // Start at this point on the screen:
117 Point pos = Screen::TopLeft() + Point(PAD, PAD - categoryScroll);
118 for(size_t i = 0; i < contents.size(); ++i)
119 {
120 if(selectedDate ? dates[i].Month() == selectedDate.Month() : selectedName == contents[i])
121 {
122 FillShader::Fill(pos + highlightOffset - Point(1., 0.), highlightSize + Point(0., 2.), lineColor);
123 FillShader::Fill(pos + highlightOffset, highlightSize, backColor);
124 }
125 font.Draw(contents[i], pos + textOffset, dates[i].Month() ? medium : bright);
126 pos.Y() += LINE_HEIGHT;
127 }
128
129 maxCategoryScroll = max(0., maxCategoryScroll + pos.Y() - Screen::Bottom());
130
131 // Parameters for drawing the main text:
132 WrappedText wrap(font);
133 wrap.SetAlignment(Alignment::JUSTIFIED);
134 wrap.SetWrapWidth(TEXT_WIDTH - 2. * PAD);
135
136 // Draw the main text.
137 pos = Screen::TopLeft() + Point(SIDEBAR_WIDTH + PAD, PAD + .5 * (LINE_HEIGHT - font.Height()) - scroll);
138
139 // Branch based on whether this is an ordinary log month or a special page.
140 auto pit = player.SpecialLogs().find(selectedName);
141 if(selectedDate && begin != end)
142 {
143 const auto layout = Layout(static_cast<int>(TEXT_WIDTH - 2. * PAD), Alignment::RIGHT);
144 for(auto it = begin; it != end; ++it)
145 {
146 string date = it->first.ToString();
147 font.Draw({date, layout}, pos + Point(0., textOffset.Y()), dim);
148 pos.Y() += LINE_HEIGHT;
149
150 wrap.Wrap(it->second);
151 wrap.Draw(pos, medium);
152 pos.Y() += wrap.Height() + GAP;
153 }
154 }
155 else if(!selectedDate && pit != player.SpecialLogs().end())
156 {
157 for(const auto &it : pit->second)
158 {
159 font.Draw(it.first, pos + textOffset, bright);
160 pos.Y() += LINE_HEIGHT;
161
162 wrap.Wrap(it.second);
163 wrap.Draw(pos, medium);
164 pos.Y() += wrap.Height() + GAP;
165 }
166 }
167
168 maxScroll = max(0., scroll + pos.Y() - Screen::Bottom());
169 }
170
171
172
KeyDown(SDL_Keycode key,Uint16 mod,const Command & command,bool isNewPress)173 bool LogbookPanel::KeyDown(SDL_Keycode key, Uint16 mod, const Command &command, bool isNewPress)
174 {
175 if(key == 'd' || key == SDLK_ESCAPE || (key == 'w' && (mod & (KMOD_CTRL | KMOD_GUI))))
176 GetUI()->Pop(this);
177 else if(key == SDLK_PAGEUP || key == SDLK_PAGEDOWN)
178 {
179 double direction = (key == SDLK_PAGEUP) - (key == SDLK_PAGEDOWN);
180 Drag(0., (Screen::Height() - 100.) * direction);
181 }
182 else if(key == SDLK_UP || key == SDLK_DOWN)
183 {
184 // Find the index of the currently selected line.
185 size_t i = 0;
186 for( ; i < contents.size(); ++i)
187 if(contents[i] == selectedName)
188 break;
189 if(i == contents.size())
190 return true;
191
192 if(key == SDLK_DOWN)
193 {
194 ++i;
195 if(i >= contents.size())
196 i = 0;
197 }
198 else if(i)
199 {
200 --i;
201 // Skip the entry that is just the currently selected year.
202 if(dates[i] && !dates[i].Month())
203 {
204 // If this is the very top of the list, don't move the selection
205 // up. (That is, you can't select the year heading line.)
206 if(i)
207 --i;
208 else
209 ++i;
210 }
211 }
212 else
213 i = contents.size() - 1;
214 if(contents[i] != selectedName)
215 {
216 selectedDate = dates[i];
217 selectedName = contents[i];
218 scroll = 0.;
219 Update(key == SDLK_UP);
220
221 // Find our currently selected item again
222 for(i = 0 ; i < contents.size(); ++i)
223 if(contents[i] == selectedName)
224 break;
225
226 if(i == contents.size())
227 return true;
228
229 // Check if it's too far down or up
230 int position = i * LINE_HEIGHT - categoryScroll;
231
232 // If it's out of bounds, recenter it
233 if(position < MINIMUM_SELECTION_DISTANCE || position > (Screen::Height() - MINIMUM_SELECTION_DISTANCE))
234 categoryScroll = position - (Screen::Height() / 2);
235
236 categoryScroll = max(categoryScroll, 0.);
237 }
238 }
239
240 return true;
241 }
242
243
244
Click(int x,int y,int clicks)245 bool LogbookPanel::Click(int x, int y, int clicks)
246 {
247 x -= Screen::Left();
248 y -= Screen::Top();
249 if(x < SIDEBAR_WIDTH)
250 {
251 size_t index = (y - PAD + categoryScroll) / LINE_HEIGHT;
252 if(index < contents.size())
253 {
254 selectedDate = dates[index];
255 selectedName = contents[index];
256 scroll = 0.;
257 // If selecting a different year, select the first month in that
258 // year.
259 Update(false);
260 }
261 }
262 else if(x > WIDTH)
263 GetUI()->Pop(this);
264
265 return true;
266 }
267
268
269
Drag(double dx,double dy)270 bool LogbookPanel::Drag(double dx, double dy)
271 {
272 if((hoverPoint.X() - Screen::Left()) > SIDEBAR_WIDTH)
273 scroll = max(0., min(maxScroll, scroll - dy));
274 else
275 categoryScroll = max(0., min(maxCategoryScroll, categoryScroll - dy));
276
277 return true;
278 }
279
280
281
Scroll(double dx,double dy)282 bool LogbookPanel::Scroll(double dx, double dy)
283 {
284 return Drag(0., dy * Preferences::ScrollSpeed());
285 }
286
287
288
Hover(int x,int y)289 bool LogbookPanel::Hover(int x, int y)
290 {
291 hoverPoint = Point(x, y);
292 return true;
293 }
294
295
296
Update(bool selectLast)297 void LogbookPanel::Update(bool selectLast)
298 {
299 contents.clear();
300 dates.clear();
301 for(const auto &it : player.SpecialLogs())
302 {
303 contents.emplace_back(it.first);
304 dates.emplace_back();
305 }
306 // The logbook should never be opened if it has no entries, but just in case:
307 if(player.Logbook().empty())
308 {
309 begin = end = player.Logbook().end();
310 return;
311 }
312
313 // Check what years and months have entries for them.
314 set<int> years;
315 set<int> months;
316 for(const auto &it : player.Logbook())
317 {
318 years.insert(it.first.Year());
319 if(it.first.Year() == selectedDate.Year() && it.first.Month() >= 1 && it.first.Month() <= 12)
320 months.insert(it.first.Month());
321 }
322
323 // Generate the table of contents.
324 for(int year : years)
325 {
326 contents.emplace_back(to_string(year));
327 dates.emplace_back(0, 0, year);
328 if(selectedDate && year == selectedDate.Year())
329 for(int month : months)
330 {
331 contents.emplace_back(MONTH[month - 1]);
332 dates.emplace_back(0, month, year);
333 }
334 }
335 // If a special category is selected, bail out here.
336 if(!selectedDate)
337 {
338 begin = end = player.Logbook().end();
339 return;
340 }
341
342 // Make sure a month is selected, within the current year.
343 if(!selectedDate.Month())
344 {
345 selectedDate = Date(0, selectLast ? *--months.end() : *months.begin(), selectedDate.Year());
346 selectedName = MONTH[selectedDate.Month() - 1];
347 }
348 // Get the range of entries that include the selected month.
349 begin = player.Logbook().lower_bound(Date(0, selectedDate.Month(), selectedDate.Year()));
350 end = player.Logbook().lower_bound(Date(32, selectedDate.Month(), selectedDate.Year()));
351 }
352