Update: Translations from eints
[openttd-github.git] / src / story_gui.cpp
blobfe9d928eb283d4b75cf9f5f2e02f9b27a328c04e
1 /*
2 * This file is part of OpenTTD.
3 * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
4 * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
5 * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <http://www.gnu.org/licenses/>.
6 */
8 /** @file story_gui.cpp GUI for stories. */
10 #include "stdafx.h"
11 #include "window_gui.h"
12 #include "strings_func.h"
13 #include "gui.h"
14 #include "story_base.h"
15 #include "core/geometry_func.hpp"
16 #include "company_func.h"
17 #include "command_func.h"
18 #include "dropdown_type.h"
19 #include "dropdown_func.h"
20 #include "sortlist_type.h"
21 #include "goal_base.h"
22 #include "viewport_func.h"
23 #include "window_func.h"
24 #include "company_base.h"
25 #include "tilehighlight_func.h"
26 #include "vehicle_base.h"
27 #include "story_cmd.h"
29 #include "widgets/story_widget.h"
31 #include "table/strings.h"
32 #include "table/sprites.h"
34 #include "safeguards.h"
36 static CursorID TranslateStoryPageButtonCursor(StoryPageButtonCursor cursor);
38 typedef GUIList<const StoryPage*> GUIStoryPageList;
39 typedef GUIList<const StoryPageElement*> GUIStoryPageElementList;
41 struct StoryBookWindow : Window {
42 protected:
43 struct LayoutCacheElement {
44 const StoryPageElement *pe;
45 Rect bounds;
47 typedef std::vector<LayoutCacheElement> LayoutCache;
49 enum class ElementFloat {
50 None,
51 Left,
52 Right,
55 Scrollbar *vscroll; ///< Scrollbar of the page text.
56 mutable LayoutCache layout_cache; ///< Cached element layout.
58 GUIStoryPageList story_pages; ///< Sorted list of pages.
59 GUIStoryPageElementList story_page_elements; ///< Sorted list of page elements that belong to the current page.
60 StoryPageID selected_page_id; ///< Pool index of selected page.
61 std::string selected_generic_title; ///< If the selected page doesn't have a custom title, this buffer is used to store a generic page title.
63 StoryPageElementID active_button_id; ///< Which button element the player is currently using
65 static const std::initializer_list<GUIStoryPageList::SortFunction * const> page_sorter_funcs;
66 static const std::initializer_list<GUIStoryPageElementList::SortFunction * const> page_element_sorter_funcs;
68 /** (Re)Build story page list. */
69 void BuildStoryPageList()
71 if (this->story_pages.NeedRebuild()) {
72 this->story_pages.clear();
74 for (const StoryPage *p : StoryPage::Iterate()) {
75 if (this->IsPageAvailable(p)) {
76 this->story_pages.push_back(p);
80 this->story_pages.RebuildDone();
83 this->story_pages.Sort();
86 /** Sort story pages by order value. */
87 static bool PageOrderSorter(const StoryPage * const &a, const StoryPage * const &b)
89 return a->sort_value < b->sort_value;
92 /** (Re)Build story page element list. */
93 void BuildStoryPageElementList()
95 if (this->story_page_elements.NeedRebuild()) {
96 this->story_page_elements.clear();
98 const StoryPage *p = GetSelPage();
99 if (p != nullptr) {
100 for (const StoryPageElement *pe : StoryPageElement::Iterate()) {
101 if (pe->page == p->index) {
102 this->story_page_elements.push_back(pe);
107 this->story_page_elements.RebuildDone();
110 this->story_page_elements.Sort();
111 this->InvalidateStoryPageElementLayout();
114 /** Sort story page elements by order value. */
115 static bool PageElementOrderSorter(const StoryPageElement * const &a, const StoryPageElement * const &b)
117 return a->sort_value < b->sort_value;
121 * Checks if a given page should be visible in the story book.
122 * @param page The page to check.
123 * @return True if the page should be visible, otherwise false.
125 bool IsPageAvailable(const StoryPage *page) const
127 return page->company == INVALID_COMPANY || page->company == this->window_number;
131 * Get instance of selected page.
132 * @return Instance of selected page or nullptr if no page is selected.
134 StoryPage *GetSelPage() const
136 if (!_story_page_pool.IsValidID(selected_page_id)) return nullptr;
137 return _story_page_pool.Get(selected_page_id);
141 * Get the page number of selected page.
142 * @return Number of available pages before to the selected one, or -1 if no page is selected.
144 int GetSelPageNum() const
146 int page_number = 0;
147 for (const StoryPage *p : this->story_pages) {
148 if (p->index == this->selected_page_id) {
149 return page_number;
151 page_number++;
153 return -1;
157 * Check if the selected page is also the first available page.
159 bool IsFirstPageSelected()
161 /* Verify that the selected page exist. */
162 if (!_story_page_pool.IsValidID(this->selected_page_id)) return false;
164 return this->story_pages.front()->index == this->selected_page_id;
168 * Check if the selected page is also the last available page.
170 bool IsLastPageSelected()
172 /* Verify that the selected page exist. */
173 if (!_story_page_pool.IsValidID(this->selected_page_id)) return false;
175 if (this->story_pages.size() <= 1) return true;
176 const StoryPage *last = this->story_pages.back();
177 return last->index == this->selected_page_id;
181 * Updates the content of selected page.
183 void RefreshSelectedPage()
185 /* Generate generic title if selected page have no custom title. */
186 StoryPage *page = this->GetSelPage();
187 if (page != nullptr && page->title.empty()) {
188 SetDParam(0, GetSelPageNum() + 1);
189 selected_generic_title = GetString(STR_STORY_BOOK_GENERIC_PAGE_ITEM);
192 this->story_page_elements.ForceRebuild();
193 this->BuildStoryPageElementList();
195 if (this->active_button_id != INVALID_STORY_PAGE_ELEMENT) ResetObjectToPlace();
197 this->vscroll->SetCount(this->GetContentHeight());
198 this->SetWidgetDirty(WID_SB_SCROLLBAR);
199 this->SetWidgetDirty(WID_SB_SEL_PAGE);
200 this->SetWidgetDirty(WID_SB_PAGE_PANEL);
204 * Selects the previous available page before the currently selected page.
206 void SelectPrevPage()
208 if (!_story_page_pool.IsValidID(this->selected_page_id)) return;
210 /* Find the last available page which is previous to the current selected page. */
211 const StoryPage *last_available;
212 last_available = nullptr;
213 for (const StoryPage *p : this->story_pages) {
214 if (p->index == this->selected_page_id) {
215 if (last_available == nullptr) return; // No previous page available.
216 this->SetSelectedPage(last_available->index);
217 return;
219 last_available = p;
224 * Selects the next available page after the currently selected page.
226 void SelectNextPage()
228 if (!_story_page_pool.IsValidID(this->selected_page_id)) return;
230 /* Find selected page. */
231 for (auto iter = this->story_pages.begin(); iter != this->story_pages.end(); iter++) {
232 const StoryPage *p = *iter;
233 if (p->index == this->selected_page_id) {
234 /* Select the page after selected page. */
235 iter++;
236 if (iter != this->story_pages.end()) {
237 this->SetSelectedPage((*iter)->index);
239 return;
245 * Builds the page selector drop down list.
247 DropDownList BuildDropDownList() const
249 DropDownList list;
250 uint16_t page_num = 1;
251 for (const StoryPage *p : this->story_pages) {
252 bool current_page = p->index == this->selected_page_id;
253 if (!p->title.empty()) {
254 list.push_back(MakeDropDownListStringItem(p->title, p->index, current_page));
255 } else {
256 /* No custom title => use a generic page title with page number. */
257 SetDParam(0, page_num);
258 list.push_back(MakeDropDownListStringItem(STR_STORY_BOOK_GENERIC_PAGE_ITEM, p->index, current_page));
260 page_num++;
263 return list;
267 * Get the width available for displaying content on the page panel.
269 uint GetAvailablePageContentWidth() const
271 return this->GetWidget<NWidgetCore>(WID_SB_PAGE_PANEL)->current_x - WidgetDimensions::scaled.frametext.Horizontal() - 1;
275 * Counts how many pixels of height that are used by Date and Title
276 * (excluding marginal after Title, as each body element has
277 * an empty row before the element).
278 * @param max_width Available width to display content.
279 * @return the height in pixels.
281 uint GetHeadHeight(int max_width) const
283 StoryPage *page = this->GetSelPage();
284 if (page == nullptr) return 0;
285 int height = 0;
287 /* Title lines */
288 height += GetCharacterHeight(FS_NORMAL); // Date always use exactly one line.
289 SetDParamStr(0, !page->title.empty() ? page->title : this->selected_generic_title);
290 height += GetStringHeight(STR_STORY_BOOK_TITLE, max_width);
292 return height;
296 * Decides which sprite to display for a given page element.
297 * @param pe The page element.
298 * @return The SpriteID of the sprite to display.
299 * @pre pe.type must be SPET_GOAL or SPET_LOCATION.
301 SpriteID GetPageElementSprite(const StoryPageElement &pe) const
303 switch (pe.type) {
304 case SPET_GOAL: {
305 Goal *g = Goal::Get((GoalID) pe.referenced_id);
306 if (g == nullptr) return SPR_IMG_GOAL_BROKEN_REF;
307 return g->completed ? SPR_IMG_GOAL_COMPLETED : SPR_IMG_GOAL;
309 case SPET_LOCATION:
310 return SPR_IMG_VIEW_LOCATION;
311 default:
312 NOT_REACHED();
317 * Get the height in pixels used by a page element.
318 * @param pe The story page element.
319 * @param max_width Available width to display content.
320 * @return the height in pixels.
322 uint GetPageElementHeight(const StoryPageElement &pe, int max_width) const
324 switch (pe.type) {
325 case SPET_TEXT:
326 SetDParamStr(0, pe.text);
327 return GetStringHeight(STR_JUST_RAW_STRING, max_width);
329 case SPET_GOAL:
330 case SPET_LOCATION: {
331 Dimension sprite_dim = GetSpriteSize(GetPageElementSprite(pe));
332 return sprite_dim.height;
335 case SPET_BUTTON_PUSH:
336 case SPET_BUTTON_TILE:
337 case SPET_BUTTON_VEHICLE: {
338 Dimension dim = GetStringBoundingBox(pe.text, FS_NORMAL);
339 return dim.height + WidgetDimensions::scaled.framerect.Vertical() + WidgetDimensions::scaled.frametext.Vertical();
342 default:
343 NOT_REACHED();
345 return 0;
349 * Get the float style of a page element.
350 * @param pe The story page element.
351 * @return The float style.
353 ElementFloat GetPageElementFloat(const StoryPageElement &pe) const
355 switch (pe.type) {
356 case SPET_BUTTON_PUSH:
357 case SPET_BUTTON_TILE:
358 case SPET_BUTTON_VEHICLE: {
359 StoryPageButtonFlags flags = StoryPageButtonData{ pe.referenced_id }.GetFlags();
360 if (flags & SPBF_FLOAT_LEFT) return ElementFloat::Left;
361 if (flags & SPBF_FLOAT_RIGHT) return ElementFloat::Right;
362 return ElementFloat::None;
365 default:
366 return ElementFloat::None;
371 * Get the width a page element would use if it was floating left or right.
372 * @param pe The story page element.
373 * @return The calculated width of the element.
375 int GetPageElementFloatWidth(const StoryPageElement &pe) const
377 switch (pe.type) {
378 case SPET_BUTTON_PUSH:
379 case SPET_BUTTON_TILE:
380 case SPET_BUTTON_VEHICLE: {
381 Dimension dim = GetStringBoundingBox(pe.text, FS_NORMAL);
382 return dim.width + WidgetDimensions::scaled.framerect.Vertical() + WidgetDimensions::scaled.frametext.Vertical();
385 default:
386 NOT_REACHED(); // only buttons can float
390 /** Invalidate the current page layout */
391 void InvalidateStoryPageElementLayout()
393 this->layout_cache.clear();
396 /** Create the page layout if it is missing */
397 void EnsureStoryPageElementLayout() const
399 /* Assume if the layout cache has contents it is valid */
400 if (!this->layout_cache.empty()) return;
402 StoryPage *page = this->GetSelPage();
403 if (page == nullptr) return;
404 int max_width = GetAvailablePageContentWidth();
405 int element_dist = GetCharacterHeight(FS_NORMAL);
407 /* Make space for the header */
408 int main_y = GetHeadHeight(max_width) + element_dist;
410 /* Current bottom of left/right column */
411 int left_y = main_y;
412 int right_y = main_y;
413 /* Current width of left/right column, 0 indicates no content in column */
414 int left_width = 0;
415 int right_width = 0;
416 /* Indexes into element cache for yet unresolved floats */
417 std::vector<size_t> left_floats;
418 std::vector<size_t> right_floats;
420 /* Build layout */
421 for (const StoryPageElement *pe : this->story_page_elements) {
422 ElementFloat fl = this->GetPageElementFloat(*pe);
424 if (fl == ElementFloat::None) {
425 /* Verify available width */
426 const int min_required_width = 10 * GetCharacterHeight(FS_NORMAL);
427 int left_offset = (left_width == 0) ? 0 : (left_width + element_dist);
428 int right_offset = (right_width == 0) ? 0 : (right_width + element_dist);
429 if (left_offset + right_offset + min_required_width >= max_width) {
430 /* Width of floats leave too little for main content, push down */
431 main_y = std::max(main_y, left_y);
432 main_y = std::max(main_y, right_y);
433 left_width = right_width = 0;
434 left_offset = right_offset = 0;
435 /* Do not add element_dist here, to keep together elements which were supposed to float besides each other. */
437 /* Determine height */
438 const int available_width = max_width - left_offset - right_offset;
439 const int height = GetPageElementHeight(*pe, available_width);
440 /* Check for button that needs extra margin */
441 if (left_offset == 0 && right_offset == 0) {
442 switch (pe->type) {
443 case SPET_BUTTON_PUSH:
444 case SPET_BUTTON_TILE:
445 case SPET_BUTTON_VEHICLE:
446 left_offset = right_offset = available_width / 5;
447 break;
448 default:
449 break;
452 /* Position element in main column */
453 LayoutCacheElement ce{ pe, {} };
454 ce.bounds.left = left_offset;
455 ce.bounds.right = max_width - right_offset;
456 ce.bounds.top = main_y;
457 main_y += height;
458 ce.bounds.bottom = main_y;
459 this->layout_cache.push_back(ce);
460 main_y += element_dist;
461 /* Clear all floats */
462 left_width = right_width = 0;
463 left_y = right_y = main_y = std::max({main_y, left_y, right_y});
464 left_floats.clear();
465 right_floats.clear();
466 } else {
467 /* Prepare references to correct column */
468 int &cur_width = (fl == ElementFloat::Left) ? left_width : right_width;
469 int &cur_y = (fl == ElementFloat::Left) ? left_y : right_y;
470 std::vector<size_t> &cur_floats = (fl == ElementFloat::Left) ? left_floats : right_floats;
471 /* Position element */
472 cur_width = std::max(cur_width, this->GetPageElementFloatWidth(*pe));
473 LayoutCacheElement ce{ pe, {} };
474 ce.bounds.left = (fl == ElementFloat::Left) ? 0 : (max_width - cur_width);
475 ce.bounds.right = (fl == ElementFloat::Left) ? cur_width : max_width;
476 ce.bounds.top = cur_y;
477 cur_y += GetPageElementHeight(*pe, cur_width);
478 ce.bounds.bottom = cur_y;
479 cur_floats.push_back(this->layout_cache.size());
480 this->layout_cache.push_back(ce);
481 cur_y += element_dist;
482 /* Update floats in column to all have the same width */
483 for (size_t index : cur_floats) {
484 LayoutCacheElement &ce = this->layout_cache[index];
485 ce.bounds.left = (fl == ElementFloat::Left) ? 0 : (max_width - cur_width);
486 ce.bounds.right = (fl == ElementFloat::Left) ? cur_width : max_width;
493 * Get the total height of the content displayed in this window.
494 * @return the height in pixels
496 int32_t GetContentHeight()
498 this->EnsureStoryPageElementLayout();
500 /* The largest bottom coordinate of any element is the height of the content */
501 int32_t max_y = std::accumulate(this->layout_cache.begin(), this->layout_cache.end(), 0, [](int32_t max_y, const LayoutCacheElement &ce) -> int32_t { return std::max<int32_t>(max_y, ce.bounds.bottom); });
503 return max_y;
507 * Draws a page element that is composed of a sprite to the left and a single line of
508 * text after that. These page elements are generally clickable and are thus called
509 * action elements.
510 * @param y_offset Current y_offset which will get updated when this method has completed its drawing.
511 * @param width Width of the region available for drawing.
512 * @param line_height Height of one line of text.
513 * @param action_sprite The sprite to draw.
514 * @param string_id The string id to draw.
515 * @return the number of lines.
517 void DrawActionElement(int &y_offset, int width, int line_height, SpriteID action_sprite, StringID string_id = STR_JUST_RAW_STRING) const
519 Dimension sprite_dim = GetSpriteSize(action_sprite);
520 uint element_height = std::max(sprite_dim.height, (uint)line_height);
522 uint sprite_top = y_offset + (element_height - sprite_dim.height) / 2;
523 uint text_top = y_offset + (element_height - line_height) / 2;
525 DrawSprite(action_sprite, PAL_NONE, 0, sprite_top);
526 DrawString(sprite_dim.width + WidgetDimensions::scaled.frametext.left, width, text_top, string_id, TC_BLACK);
528 y_offset += element_height;
532 * Internal event handler for when a page element is clicked.
533 * @param pe The clicked page element.
535 void OnPageElementClick(const StoryPageElement &pe)
537 switch (pe.type) {
538 case SPET_TEXT:
539 /* Do nothing. */
540 break;
542 case SPET_LOCATION:
543 if (_ctrl_pressed) {
544 ShowExtraViewportWindow((TileIndex)pe.referenced_id);
545 } else {
546 ScrollMainWindowToTile((TileIndex)pe.referenced_id);
548 break;
550 case SPET_GOAL:
551 ShowGoalsList((CompanyID)this->window_number);
552 break;
554 case SPET_BUTTON_PUSH:
555 if (this->active_button_id != INVALID_STORY_PAGE_ELEMENT) ResetObjectToPlace();
556 this->active_button_id = pe.index;
557 this->SetTimeout();
558 this->SetWidgetDirty(WID_SB_PAGE_PANEL);
560 Command<CMD_STORY_PAGE_BUTTON>::Post(0, pe.index, 0);
561 break;
563 case SPET_BUTTON_TILE:
564 if (this->active_button_id == pe.index) {
565 ResetObjectToPlace();
566 this->active_button_id = INVALID_STORY_PAGE_ELEMENT;
567 } else {
568 CursorID cursor = TranslateStoryPageButtonCursor(StoryPageButtonData{ pe.referenced_id }.GetCursor());
569 SetObjectToPlaceWnd(cursor, PAL_NONE, HT_RECT, this);
570 this->active_button_id = pe.index;
572 this->SetWidgetDirty(WID_SB_PAGE_PANEL);
573 break;
575 case SPET_BUTTON_VEHICLE:
576 if (this->active_button_id == pe.index) {
577 ResetObjectToPlace();
578 this->active_button_id = INVALID_STORY_PAGE_ELEMENT;
579 } else {
580 CursorID cursor = TranslateStoryPageButtonCursor(StoryPageButtonData{ pe.referenced_id }.GetCursor());
581 SetObjectToPlaceWnd(cursor, PAL_NONE, HT_VEHICLE, this);
582 this->active_button_id = pe.index;
584 this->SetWidgetDirty(WID_SB_PAGE_PANEL);
585 break;
587 default:
588 NOT_REACHED();
592 public:
593 StoryBookWindow(WindowDesc &desc, WindowNumber window_number) : Window(desc)
595 this->CreateNestedTree();
596 this->vscroll = this->GetScrollbar(WID_SB_SCROLLBAR);
597 this->vscroll->SetStepSize(GetCharacterHeight(FS_NORMAL));
599 /* Initialize page sort. */
600 this->story_pages.SetSortFuncs(StoryBookWindow::page_sorter_funcs);
601 this->story_pages.ForceRebuild();
602 this->BuildStoryPageList();
603 this->story_page_elements.SetSortFuncs(StoryBookWindow::page_element_sorter_funcs);
604 /* story_page_elements will get built by SetSelectedPage */
606 this->FinishInitNested(window_number);
607 this->owner = (Owner)this->window_number;
609 /* Initialize selected vars. */
610 this->selected_generic_title.clear();
611 this->selected_page_id = INVALID_STORY_PAGE;
613 this->active_button_id = INVALID_STORY_PAGE_ELEMENT;
615 this->OnInvalidateData(-1);
619 * Updates the disabled state of the prev/next buttons.
621 void UpdatePrevNextDisabledState()
623 this->SetWidgetDisabledState(WID_SB_PREV_PAGE, story_pages.empty() || this->IsFirstPageSelected());
624 this->SetWidgetDisabledState(WID_SB_NEXT_PAGE, story_pages.empty() || this->IsLastPageSelected());
625 this->SetWidgetDirty(WID_SB_PREV_PAGE);
626 this->SetWidgetDirty(WID_SB_NEXT_PAGE);
630 * Sets the selected page.
631 * @param page_index pool index of the page to select.
633 void SetSelectedPage(uint16_t page_index)
635 if (this->selected_page_id != page_index) {
636 if (this->active_button_id) ResetObjectToPlace();
637 this->active_button_id = INVALID_STORY_PAGE_ELEMENT;
638 this->selected_page_id = page_index;
639 this->RefreshSelectedPage();
640 this->UpdatePrevNextDisabledState();
644 void SetStringParameters(WidgetID widget) const override
646 switch (widget) {
647 case WID_SB_SEL_PAGE: {
648 StoryPage *page = this->GetSelPage();
649 SetDParamStr(0, page != nullptr && !page->title.empty() ? page->title : this->selected_generic_title);
650 break;
652 case WID_SB_CAPTION:
653 if (this->window_number == INVALID_COMPANY) {
654 SetDParam(0, STR_STORY_BOOK_SPECTATOR_CAPTION);
655 } else {
656 SetDParam(0, STR_STORY_BOOK_CAPTION);
657 SetDParam(1, this->window_number);
659 break;
663 void OnPaint() override
665 /* Detect if content has changed height. This can happen if a
666 * multi-line text contains eg. {COMPANY} and that company is
667 * renamed.
669 if (this->vscroll->GetCount() != this->GetContentHeight()) {
670 this->vscroll->SetCount(this->GetContentHeight());
671 this->SetWidgetDirty(WID_SB_SCROLLBAR);
672 this->SetWidgetDirty(WID_SB_PAGE_PANEL);
675 this->DrawWidgets();
678 void DrawWidget(const Rect &r, WidgetID widget) const override
680 if (widget != WID_SB_PAGE_PANEL) return;
682 StoryPage *page = this->GetSelPage();
683 if (page == nullptr) return;
685 Rect fr = r.Shrink(WidgetDimensions::scaled.frametext);
687 /* Set up a clipping region for the panel. */
688 DrawPixelInfo tmp_dpi;
689 if (!FillDrawPixelInfo(&tmp_dpi, fr)) return;
691 AutoRestoreBackup dpi_backup(_cur_dpi, &tmp_dpi);
693 /* Draw content (now coordinates given to Draw** are local to the new clipping region). */
694 fr = fr.Translate(-fr.left, -fr.top);
695 int line_height = GetCharacterHeight(FS_NORMAL);
696 const int scrollpos = this->vscroll->GetPosition();
697 int y_offset = -scrollpos;
699 /* Date */
700 if (page->date != CalendarTime::INVALID_DATE) {
701 SetDParam(0, page->date);
702 DrawString(0, fr.right, y_offset, STR_JUST_DATE_LONG, TC_BLACK);
704 y_offset += line_height;
706 /* Title */
707 SetDParamStr(0, !page->title.empty() ? page->title : this->selected_generic_title);
708 y_offset = DrawStringMultiLine(0, fr.right, y_offset, fr.bottom, STR_STORY_BOOK_TITLE, TC_BLACK, SA_TOP | SA_HOR_CENTER);
710 /* Page elements */
711 this->EnsureStoryPageElementLayout();
712 for (const LayoutCacheElement &ce : this->layout_cache) {
713 y_offset = ce.bounds.top - scrollpos;
714 switch (ce.pe->type) {
715 case SPET_TEXT:
716 SetDParamStr(0, ce.pe->text);
717 y_offset = DrawStringMultiLine(ce.bounds.left, ce.bounds.right, ce.bounds.top - scrollpos, ce.bounds.bottom - scrollpos, STR_JUST_RAW_STRING, TC_BLACK, SA_TOP | SA_LEFT);
718 break;
720 case SPET_GOAL: {
721 Goal *g = Goal::Get((GoalID) ce.pe->referenced_id);
722 StringID string_id = g == nullptr ? STR_STORY_BOOK_INVALID_GOAL_REF : STR_JUST_RAW_STRING;
723 if (g != nullptr) SetDParamStr(0, g->text);
724 DrawActionElement(y_offset, ce.bounds.right - ce.bounds.left, line_height, GetPageElementSprite(*ce.pe), string_id);
725 break;
728 case SPET_LOCATION:
729 SetDParamStr(0, ce.pe->text);
730 DrawActionElement(y_offset, ce.bounds.right - ce.bounds.left, line_height, GetPageElementSprite(*ce.pe));
731 break;
733 case SPET_BUTTON_PUSH:
734 case SPET_BUTTON_TILE:
735 case SPET_BUTTON_VEHICLE: {
736 const int tmargin = WidgetDimensions::scaled.bevel.top + WidgetDimensions::scaled.frametext.top;
737 const FrameFlags frame = this->active_button_id == ce.pe->index ? FR_LOWERED : FR_NONE;
738 const Colours bgcolour = StoryPageButtonData{ ce.pe->referenced_id }.GetColour();
740 DrawFrameRect(ce.bounds.left, ce.bounds.top - scrollpos, ce.bounds.right, ce.bounds.bottom - scrollpos - 1, bgcolour, frame);
742 SetDParamStr(0, ce.pe->text);
743 DrawString(ce.bounds.left + WidgetDimensions::scaled.bevel.left, ce.bounds.right - WidgetDimensions::scaled.bevel.right, ce.bounds.top + tmargin - scrollpos, STR_JUST_RAW_STRING, TC_WHITE, SA_CENTER);
744 break;
747 default: NOT_REACHED();
752 void UpdateWidgetSize(WidgetID widget, Dimension &size, [[maybe_unused]] const Dimension &padding, [[maybe_unused]] Dimension &fill, [[maybe_unused]] Dimension &resize) override
754 if (widget != WID_SB_SEL_PAGE && widget != WID_SB_PAGE_PANEL) return;
756 Dimension d;
757 d.height = GetCharacterHeight(FS_NORMAL);
758 d.width = 0;
760 switch (widget) {
761 case WID_SB_SEL_PAGE: {
763 /* Get max title width. */
764 for (size_t i = 0; i < this->story_pages.size(); i++) {
765 const StoryPage *s = this->story_pages[i];
767 if (!s->title.empty()) {
768 SetDParamStr(0, s->title);
769 } else {
770 SetDParamStr(0, this->selected_generic_title);
772 Dimension title_d = GetStringBoundingBox(STR_JUST_RAW_STRING);
774 if (title_d.width > d.width) {
775 d.width = title_d.width;
779 d.width += padding.width;
780 d.height += padding.height;
781 size = maxdim(size, d);
782 break;
785 case WID_SB_PAGE_PANEL: {
786 d.height *= 5;
787 d.height += padding.height + WidgetDimensions::scaled.frametext.Vertical();
788 size = maxdim(size, d);
789 break;
795 void OnResize() override
797 this->InvalidateStoryPageElementLayout();
798 this->vscroll->SetCapacityFromWidget(this, WID_SB_PAGE_PANEL, WidgetDimensions::scaled.frametext.Vertical());
799 this->vscroll->SetCount(this->GetContentHeight());
802 void OnClick([[maybe_unused]] Point pt, WidgetID widget, [[maybe_unused]] int click_count) override
804 switch (widget) {
805 case WID_SB_SEL_PAGE: {
806 DropDownList list = this->BuildDropDownList();
807 if (!list.empty()) {
808 /* Get the index of selected page. */
809 int selected = 0;
810 for (size_t i = 0; i < this->story_pages.size(); i++) {
811 const StoryPage *p = this->story_pages[i];
812 if (p->index == this->selected_page_id) break;
813 selected++;
816 ShowDropDownList(this, std::move(list), selected, widget);
818 break;
821 case WID_SB_PREV_PAGE:
822 this->SelectPrevPage();
823 break;
825 case WID_SB_NEXT_PAGE:
826 this->SelectNextPage();
827 break;
829 case WID_SB_PAGE_PANEL: {
830 int clicked_y = this->vscroll->GetScrolledRowFromWidget(pt.y, this, WID_SB_PAGE_PANEL, WidgetDimensions::scaled.frametext.top);
831 this->EnsureStoryPageElementLayout();
833 for (const LayoutCacheElement &ce : this->layout_cache) {
834 if (clicked_y >= ce.bounds.top && clicked_y < ce.bounds.bottom && pt.x >= ce.bounds.left && pt.x < ce.bounds.right) {
835 this->OnPageElementClick(*ce.pe);
836 return;
843 void OnDropdownSelect(WidgetID widget, int index) override
845 if (widget != WID_SB_SEL_PAGE) return;
847 /* index (which is set in BuildDropDownList) is the page id. */
848 this->SetSelectedPage(index);
852 * Some data on this window has become invalid.
853 * @param data Information about the changed data.
854 * -1 Rebuild page list and refresh current page;
855 * >= 0 Id of the page that needs to be refreshed. If it is not the current page, nothing happens.
856 * @param gui_scope Whether the call is done from GUI scope. You may not do everything when not in GUI scope. See #InvalidateWindowData() for details.
858 void OnInvalidateData([[maybe_unused]] int data = 0, [[maybe_unused]] bool gui_scope = true) override
860 if (!gui_scope) return;
862 /* If added/removed page, force rebuild. Sort order never change so just a
863 * re-sort is never needed.
865 if (data == -1) {
866 this->story_pages.ForceRebuild();
867 this->BuildStoryPageList();
869 /* Was the last page removed? */
870 if (this->story_pages.empty()) {
871 this->selected_generic_title.clear();
874 /* Verify page selection. */
875 if (!_story_page_pool.IsValidID(this->selected_page_id)) {
876 this->selected_page_id = INVALID_STORY_PAGE;
878 if (this->selected_page_id == INVALID_STORY_PAGE && !this->story_pages.empty()) {
879 /* No page is selected, but there exist at least one available.
880 * => Select first page.
882 this->SetSelectedPage(this->story_pages[0]->index);
885 this->SetWidgetDisabledState(WID_SB_SEL_PAGE, this->story_pages.empty());
886 this->SetWidgetDirty(WID_SB_SEL_PAGE);
887 this->UpdatePrevNextDisabledState();
888 } else if (data >= 0 && this->selected_page_id == data) {
889 this->RefreshSelectedPage();
893 void OnTimeout() override
895 this->active_button_id = INVALID_STORY_PAGE_ELEMENT;
896 this->SetWidgetDirty(WID_SB_PAGE_PANEL);
899 void OnPlaceObject([[maybe_unused]] Point pt, TileIndex tile) override
901 const StoryPageElement *const pe = StoryPageElement::GetIfValid(this->active_button_id);
902 if (pe == nullptr || pe->type != SPET_BUTTON_TILE) {
903 ResetObjectToPlace();
904 this->active_button_id = INVALID_STORY_PAGE_ELEMENT;
905 this->SetWidgetDirty(WID_SB_PAGE_PANEL);
906 return;
909 Command<CMD_STORY_PAGE_BUTTON>::Post(tile, pe->index, 0);
910 ResetObjectToPlace();
913 bool OnVehicleSelect(const Vehicle *v) override
915 const StoryPageElement *const pe = StoryPageElement::GetIfValid(this->active_button_id);
916 if (pe == nullptr || pe->type != SPET_BUTTON_VEHICLE) {
917 ResetObjectToPlace();
918 this->active_button_id = INVALID_STORY_PAGE_ELEMENT;
919 this->SetWidgetDirty(WID_SB_PAGE_PANEL);
920 return false;
923 /* Check that the vehicle matches the requested type */
924 StoryPageButtonData data{ pe->referenced_id };
925 VehicleType wanted_vehtype = data.GetVehicleType();
926 if (wanted_vehtype != VEH_INVALID && wanted_vehtype != v->type) return false;
928 Command<CMD_STORY_PAGE_BUTTON>::Post(0, pe->index, v->index);
929 ResetObjectToPlace();
930 return true;
933 void OnPlaceObjectAbort() override
935 this->active_button_id = INVALID_STORY_PAGE_ELEMENT;
936 this->SetWidgetDirty(WID_SB_PAGE_PANEL);
940 const std::initializer_list<GUIStoryPageList::SortFunction * const> StoryBookWindow::page_sorter_funcs = {
941 &PageOrderSorter,
944 const std::initializer_list<GUIStoryPageElementList::SortFunction * const> StoryBookWindow::page_element_sorter_funcs = {
945 &PageElementOrderSorter,
948 static constexpr NWidgetPart _nested_story_book_widgets[] = {
949 NWidget(NWID_HORIZONTAL),
950 NWidget(WWT_CLOSEBOX, COLOUR_BROWN),
951 NWidget(WWT_CAPTION, COLOUR_BROWN, WID_SB_CAPTION), SetDataTip(STR_JUST_STRING1, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS),
952 NWidget(WWT_SHADEBOX, COLOUR_BROWN),
953 NWidget(WWT_DEFSIZEBOX, COLOUR_BROWN),
954 NWidget(WWT_STICKYBOX, COLOUR_BROWN),
955 EndContainer(),
956 NWidget(NWID_HORIZONTAL),
957 NWidget(WWT_PANEL, COLOUR_BROWN, WID_SB_PAGE_PANEL), SetResize(1, 1), SetScrollbar(WID_SB_SCROLLBAR), EndContainer(),
958 NWidget(NWID_VSCROLLBAR, COLOUR_BROWN, WID_SB_SCROLLBAR),
959 EndContainer(),
960 NWidget(NWID_HORIZONTAL),
961 NWidget(WWT_TEXTBTN, COLOUR_BROWN, WID_SB_PREV_PAGE), SetMinimalSize(100, 0), SetFill(0, 0), SetDataTip(STR_STORY_BOOK_PREV_PAGE, STR_STORY_BOOK_PREV_PAGE_TOOLTIP),
962 NWidget(NWID_BUTTON_DROPDOWN, COLOUR_BROWN, WID_SB_SEL_PAGE), SetMinimalSize(93, 12), SetFill(1, 0),
963 SetDataTip(STR_JUST_RAW_STRING, STR_STORY_BOOK_SEL_PAGE_TOOLTIP), SetResize(1, 0),
964 NWidget(WWT_TEXTBTN, COLOUR_BROWN, WID_SB_NEXT_PAGE), SetMinimalSize(100, 0), SetFill(0, 0), SetDataTip(STR_STORY_BOOK_NEXT_PAGE, STR_STORY_BOOK_NEXT_PAGE_TOOLTIP),
965 NWidget(WWT_RESIZEBOX, COLOUR_BROWN),
966 EndContainer(),
969 static WindowDesc _story_book_desc(
970 WDP_AUTO, "view_story", 400, 300,
971 WC_STORY_BOOK, WC_NONE,
973 _nested_story_book_widgets
976 static WindowDesc _story_book_gs_desc(
977 WDP_CENTER, "view_story_gs", 400, 300,
978 WC_STORY_BOOK, WC_NONE,
980 _nested_story_book_widgets
983 static CursorID TranslateStoryPageButtonCursor(StoryPageButtonCursor cursor)
985 switch (cursor) {
986 case SPBC_MOUSE: return SPR_CURSOR_MOUSE;
987 case SPBC_ZZZ: return SPR_CURSOR_ZZZ;
988 case SPBC_BUOY: return SPR_CURSOR_BUOY;
989 case SPBC_QUERY: return SPR_CURSOR_QUERY;
990 case SPBC_HQ: return SPR_CURSOR_HQ;
991 case SPBC_SHIP_DEPOT: return SPR_CURSOR_SHIP_DEPOT;
992 case SPBC_SIGN: return SPR_CURSOR_SIGN;
993 case SPBC_TREE: return SPR_CURSOR_TREE;
994 case SPBC_BUY_LAND: return SPR_CURSOR_BUY_LAND;
995 case SPBC_LEVEL_LAND: return SPR_CURSOR_LEVEL_LAND;
996 case SPBC_TOWN: return SPR_CURSOR_TOWN;
997 case SPBC_INDUSTRY: return SPR_CURSOR_INDUSTRY;
998 case SPBC_ROCKY_AREA: return SPR_CURSOR_ROCKY_AREA;
999 case SPBC_DESERT: return SPR_CURSOR_DESERT;
1000 case SPBC_TRANSMITTER: return SPR_CURSOR_TRANSMITTER;
1001 case SPBC_AIRPORT: return SPR_CURSOR_AIRPORT;
1002 case SPBC_DOCK: return SPR_CURSOR_DOCK;
1003 case SPBC_CANAL: return SPR_CURSOR_CANAL;
1004 case SPBC_LOCK: return SPR_CURSOR_LOCK;
1005 case SPBC_RIVER: return SPR_CURSOR_RIVER;
1006 case SPBC_AQUEDUCT: return SPR_CURSOR_AQUEDUCT;
1007 case SPBC_BRIDGE: return SPR_CURSOR_BRIDGE;
1008 case SPBC_RAIL_STATION: return SPR_CURSOR_RAIL_STATION;
1009 case SPBC_TUNNEL_RAIL: return SPR_CURSOR_TUNNEL_RAIL;
1010 case SPBC_TUNNEL_ELRAIL: return SPR_CURSOR_TUNNEL_ELRAIL;
1011 case SPBC_TUNNEL_MONO: return SPR_CURSOR_TUNNEL_MONO;
1012 case SPBC_TUNNEL_MAGLEV: return SPR_CURSOR_TUNNEL_MAGLEV;
1013 case SPBC_AUTORAIL: return SPR_CURSOR_AUTORAIL;
1014 case SPBC_AUTOELRAIL: return SPR_CURSOR_AUTOELRAIL;
1015 case SPBC_AUTOMONO: return SPR_CURSOR_AUTOMONO;
1016 case SPBC_AUTOMAGLEV: return SPR_CURSOR_AUTOMAGLEV;
1017 case SPBC_WAYPOINT: return SPR_CURSOR_WAYPOINT;
1018 case SPBC_RAIL_DEPOT: return SPR_CURSOR_RAIL_DEPOT;
1019 case SPBC_ELRAIL_DEPOT: return SPR_CURSOR_ELRAIL_DEPOT;
1020 case SPBC_MONO_DEPOT: return SPR_CURSOR_MONO_DEPOT;
1021 case SPBC_MAGLEV_DEPOT: return SPR_CURSOR_MAGLEV_DEPOT;
1022 case SPBC_CONVERT_RAIL: return SPR_CURSOR_CONVERT_RAIL;
1023 case SPBC_CONVERT_ELRAIL: return SPR_CURSOR_CONVERT_ELRAIL;
1024 case SPBC_CONVERT_MONO: return SPR_CURSOR_CONVERT_MONO;
1025 case SPBC_CONVERT_MAGLEV: return SPR_CURSOR_CONVERT_MAGLEV;
1026 case SPBC_AUTOROAD: return SPR_CURSOR_AUTOROAD;
1027 case SPBC_AUTOTRAM: return SPR_CURSOR_AUTOTRAM;
1028 case SPBC_ROAD_DEPOT: return SPR_CURSOR_ROAD_DEPOT;
1029 case SPBC_BUS_STATION: return SPR_CURSOR_BUS_STATION;
1030 case SPBC_TRUCK_STATION: return SPR_CURSOR_TRUCK_STATION;
1031 case SPBC_ROAD_TUNNEL: return SPR_CURSOR_ROAD_TUNNEL;
1032 case SPBC_CLONE_TRAIN: return SPR_CURSOR_CLONE_TRAIN;
1033 case SPBC_CLONE_ROADVEH: return SPR_CURSOR_CLONE_ROADVEH;
1034 case SPBC_CLONE_SHIP: return SPR_CURSOR_CLONE_SHIP;
1035 case SPBC_CLONE_AIRPLANE: return SPR_CURSOR_CLONE_AIRPLANE;
1036 case SPBC_DEMOLISH: return ANIMCURSOR_DEMOLISH;
1037 case SPBC_LOWERLAND: return ANIMCURSOR_LOWERLAND;
1038 case SPBC_RAISELAND: return ANIMCURSOR_RAISELAND;
1039 case SPBC_PICKSTATION: return ANIMCURSOR_PICKSTATION;
1040 case SPBC_BUILDSIGNALS: return ANIMCURSOR_BUILDSIGNALS;
1041 default: return SPR_CURSOR_QUERY;
1046 * Raise or create the story book window for \a company, at page \a page_id.
1047 * @param company 'Owner' of the story book, may be #INVALID_COMPANY.
1048 * @param page_id Page to open, may be #INVALID_STORY_PAGE.
1049 * @param centered Whether to open the window centered.
1051 void ShowStoryBook(CompanyID company, uint16_t page_id, bool centered)
1053 if (!Company::IsValidID(company)) company = (CompanyID)INVALID_COMPANY;
1055 StoryBookWindow *w = AllocateWindowDescFront<StoryBookWindow>(centered ? _story_book_gs_desc : _story_book_desc, company, true);
1056 if (page_id != INVALID_STORY_PAGE) w->SetSelectedPage(page_id);