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