Feature: Hide block signals in GUI by default (#8688)
[openttd-github.git] / src / story_gui.cpp
blobccb9ccf7b8ea34cdc0d79427529997c597a3ade3
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 "date_func.h"
14 #include "gui.h"
15 #include "story_base.h"
16 #include "core/geometry_func.hpp"
17 #include "company_func.h"
18 #include "command_func.h"
19 #include "widgets/dropdown_type.h"
20 #include "widgets/dropdown_func.h"
21 #include "sortlist_type.h"
22 #include "goal_base.h"
23 #include "viewport_func.h"
24 #include "window_func.h"
25 #include "company_base.h"
26 #include "tilehighlight_func.h"
27 #include "vehicle_base.h"
29 #include "widgets/story_widget.h"
31 #include "table/strings.h"
32 #include "table/sprites.h"
34 #include <numeric>
36 #include "safeguards.h"
38 static CursorID TranslateStoryPageButtonCursor(StoryPageButtonCursor cursor);
40 typedef GUIList<const StoryPage*> GUIStoryPageList;
41 typedef GUIList<const StoryPageElement*> GUIStoryPageElementList;
43 struct StoryBookWindow : Window {
44 protected:
45 struct LayoutCacheElement {
46 const StoryPageElement *pe;
47 Rect bounds;
49 typedef std::vector<LayoutCacheElement> LayoutCache;
51 enum class ElementFloat {
52 None,
53 Left,
54 Right,
57 Scrollbar *vscroll; ///< Scrollbar of the page text.
58 mutable LayoutCache layout_cache; ///< Cached element layout.
60 GUIStoryPageList story_pages; ///< Sorted list of pages.
61 GUIStoryPageElementList story_page_elements; ///< Sorted list of page elements that belong to the current page.
62 StoryPageID selected_page_id; ///< Pool index of selected page.
63 char selected_generic_title[255]; ///< If the selected page doesn't have a custom title, this buffer is used to store a generic page title.
65 StoryPageElementID active_button_id; ///< Which button element the player is currently using
67 static GUIStoryPageList::SortFunction * const page_sorter_funcs[];
68 static GUIStoryPageElementList::SortFunction * const page_element_sorter_funcs[];
70 /** (Re)Build story page list. */
71 void BuildStoryPageList()
73 if (this->story_pages.NeedRebuild()) {
74 this->story_pages.clear();
76 for (const StoryPage *p : StoryPage::Iterate()) {
77 if (this->IsPageAvailable(p)) {
78 this->story_pages.push_back(p);
82 this->story_pages.shrink_to_fit();
83 this->story_pages.RebuildDone();
86 this->story_pages.Sort();
89 /** Sort story pages by order value. */
90 static bool PageOrderSorter(const StoryPage * const &a, const StoryPage * const &b)
92 return a->sort_value < b->sort_value;
95 /** (Re)Build story page element list. */
96 void BuildStoryPageElementList()
98 if (this->story_page_elements.NeedRebuild()) {
99 this->story_page_elements.clear();
101 const StoryPage *p = GetSelPage();
102 if (p != nullptr) {
103 for (const StoryPageElement *pe : StoryPageElement::Iterate()) {
104 if (pe->page == p->index) {
105 this->story_page_elements.push_back(pe);
110 this->story_page_elements.shrink_to_fit();
111 this->story_page_elements.RebuildDone();
114 this->story_page_elements.Sort();
115 this->InvalidateStoryPageElementLayout();
118 /** Sort story page elements by order value. */
119 static bool PageElementOrderSorter(const StoryPageElement * const &a, const StoryPageElement * const &b)
121 return a->sort_value < b->sort_value;
125 * Checks if a given page should be visible in the story book.
126 * @param page The page to check.
127 * @return True if the page should be visible, otherwise false.
129 bool IsPageAvailable(const StoryPage *page) const
131 return page->company == INVALID_COMPANY || page->company == this->window_number;
135 * Get instance of selected page.
136 * @return Instance of selected page or nullptr if no page is selected.
138 StoryPage *GetSelPage() const
140 if (!_story_page_pool.IsValidID(selected_page_id)) return nullptr;
141 return _story_page_pool.Get(selected_page_id);
145 * Get the page number of selected page.
146 * @return Number of available pages before to the selected one, or -1 if no page is selected.
148 int GetSelPageNum() const
150 int page_number = 0;
151 for (const StoryPage *p : this->story_pages) {
152 if (p->index == this->selected_page_id) {
153 return page_number;
155 page_number++;
157 return -1;
161 * Check if the selected page is also the first available page.
163 bool IsFirstPageSelected()
165 /* Verify that the selected page exist. */
166 if (!_story_page_pool.IsValidID(this->selected_page_id)) return false;
168 return this->story_pages.front()->index == this->selected_page_id;
172 * Check if the selected page is also the last available page.
174 bool IsLastPageSelected()
176 /* Verify that the selected page exist. */
177 if (!_story_page_pool.IsValidID(this->selected_page_id)) return false;
179 if (this->story_pages.size() <= 1) return true;
180 const StoryPage *last = this->story_pages.back();
181 return last->index == this->selected_page_id;
185 * Updates the content of selected page.
187 void RefreshSelectedPage()
189 /* Generate generic title if selected page have no custom title. */
190 StoryPage *page = this->GetSelPage();
191 if (page != nullptr && page->title == nullptr) {
192 SetDParam(0, GetSelPageNum() + 1);
193 GetString(selected_generic_title, STR_STORY_BOOK_GENERIC_PAGE_ITEM, lastof(selected_generic_title));
196 this->story_page_elements.ForceRebuild();
197 this->BuildStoryPageElementList();
199 if (this->active_button_id != INVALID_STORY_PAGE_ELEMENT) ResetObjectToPlace();
201 this->vscroll->SetCount(this->GetContentHeight());
202 this->SetWidgetDirty(WID_SB_SCROLLBAR);
203 this->SetWidgetDirty(WID_SB_SEL_PAGE);
204 this->SetWidgetDirty(WID_SB_PAGE_PANEL);
208 * Selects the previous available page before the currently selected page.
210 void SelectPrevPage()
212 if (!_story_page_pool.IsValidID(this->selected_page_id)) return;
214 /* Find the last available page which is previous to the current selected page. */
215 const StoryPage *last_available;
216 last_available = nullptr;
217 for (const StoryPage *p : this->story_pages) {
218 if (p->index == this->selected_page_id) {
219 if (last_available == nullptr) return; // No previous page available.
220 this->SetSelectedPage(last_available->index);
221 return;
223 last_available = p;
228 * Selects the next available page after the currently selected page.
230 void SelectNextPage()
232 if (!_story_page_pool.IsValidID(this->selected_page_id)) return;
234 /* Find selected page. */
235 for (auto iter = this->story_pages.begin(); iter != this->story_pages.end(); iter++) {
236 const StoryPage *p = *iter;
237 if (p->index == this->selected_page_id) {
238 /* Select the page after selected page. */
239 iter++;
240 if (iter != this->story_pages.end()) {
241 this->SetSelectedPage((*iter)->index);
243 return;
249 * Builds the page selector drop down list.
251 DropDownList BuildDropDownList() const
253 DropDownList list;
254 uint16 page_num = 1;
255 for (const StoryPage *p : this->story_pages) {
256 bool current_page = p->index == this->selected_page_id;
257 DropDownListStringItem *item = nullptr;
258 if (p->title != nullptr) {
259 item = new DropDownListCharStringItem(p->title, p->index, current_page);
260 } else {
261 /* No custom title => use a generic page title with page number. */
262 DropDownListParamStringItem *str_item =
263 new DropDownListParamStringItem(STR_STORY_BOOK_GENERIC_PAGE_ITEM, p->index, current_page);
264 str_item->SetParam(0, page_num);
265 item = str_item;
268 list.emplace_back(item);
269 page_num++;
272 return list;
276 * Get the width available for displaying content on the page panel.
278 uint GetAvailablePageContentWidth() const
280 return this->GetWidget<NWidgetCore>(WID_SB_PAGE_PANEL)->current_x - WD_FRAMETEXT_LEFT - WD_FRAMETEXT_RIGHT - 1;
284 * Counts how many pixels of height that are used by Date and Title
285 * (excluding marginal after Title, as each body element has
286 * an empty row before the element).
287 * @param max_width Available width to display content.
288 * @return the height in pixels.
290 uint GetHeadHeight(int max_width) const
292 StoryPage *page = this->GetSelPage();
293 if (page == nullptr) return 0;
294 int height = 0;
296 /* Title lines */
297 height += FONT_HEIGHT_NORMAL; // Date always use exactly one line.
298 SetDParamStr(0, page->title != nullptr ? page->title : this->selected_generic_title);
299 height += GetStringHeight(STR_STORY_BOOK_TITLE, max_width);
301 return height;
305 * Decides which sprite to display for a given page element.
306 * @param pe The page element.
307 * @return The SpriteID of the sprite to display.
308 * @pre pe.type must be SPET_GOAL or SPET_LOCATION.
310 SpriteID GetPageElementSprite(const StoryPageElement &pe) const
312 switch (pe.type) {
313 case SPET_GOAL: {
314 Goal *g = Goal::Get((GoalID) pe.referenced_id);
315 if (g == nullptr) return SPR_IMG_GOAL_BROKEN_REF;
316 return g->completed ? SPR_IMG_GOAL_COMPLETED : SPR_IMG_GOAL;
318 case SPET_LOCATION:
319 return SPR_IMG_VIEW_LOCATION;
320 default:
321 NOT_REACHED();
326 * Get the height in pixels used by a page element.
327 * @param pe The story page element.
328 * @param max_width Available width to display content.
329 * @return the height in pixels.
331 uint GetPageElementHeight(const StoryPageElement &pe, int max_width) const
333 switch (pe.type) {
334 case SPET_TEXT:
335 SetDParamStr(0, pe.text);
336 return GetStringHeight(STR_BLACK_RAW_STRING, max_width);
338 case SPET_GOAL:
339 case SPET_LOCATION: {
340 Dimension sprite_dim = GetSpriteSize(GetPageElementSprite(pe));
341 return sprite_dim.height;
344 case SPET_BUTTON_PUSH:
345 case SPET_BUTTON_TILE:
346 case SPET_BUTTON_VEHICLE: {
347 Dimension dim = GetStringBoundingBox(pe.text, FS_NORMAL);
348 return dim.height + WD_BEVEL_TOP + WD_BEVEL_BOTTOM + WD_FRAMETEXT_TOP + WD_FRAMETEXT_BOTTOM;
351 default:
352 NOT_REACHED();
354 return 0;
358 * Get the float style of a page element.
359 * @param pe The story page element.
360 * @return The float style.
362 ElementFloat GetPageElementFloat(const StoryPageElement &pe) const
364 switch (pe.type) {
365 case SPET_BUTTON_PUSH:
366 case SPET_BUTTON_TILE:
367 case SPET_BUTTON_VEHICLE: {
368 StoryPageButtonFlags flags = StoryPageButtonData{ pe.referenced_id }.GetFlags();
369 if (flags & SPBF_FLOAT_LEFT) return ElementFloat::Left;
370 if (flags & SPBF_FLOAT_RIGHT) return ElementFloat::Right;
371 return ElementFloat::None;
374 default:
375 return ElementFloat::None;
380 * Get the width a page element would use if it was floating left or right.
381 * @param pe The story page element.
382 * @return The calculated width of the element.
384 int GetPageElementFloatWidth(const StoryPageElement &pe) const
386 switch (pe.type) {
387 case SPET_BUTTON_PUSH:
388 case SPET_BUTTON_TILE:
389 case SPET_BUTTON_VEHICLE: {
390 Dimension dim = GetStringBoundingBox(pe.text, FS_NORMAL);
391 return dim.width + WD_BEVEL_LEFT + WD_BEVEL_RIGHT + WD_FRAMETEXT_LEFT + WD_FRAMETEXT_RIGHT;
394 default:
395 NOT_REACHED(); // only buttons can float
399 /** Invalidate the current page layout */
400 void InvalidateStoryPageElementLayout()
402 this->layout_cache.clear();
405 /** Create the page layout if it is missing */
406 void EnsureStoryPageElementLayout() const
408 /* Assume if the layout cache has contents it is valid */
409 if (!this->layout_cache.empty()) return;
411 StoryPage *page = this->GetSelPage();
412 if (page == nullptr) return;
413 int max_width = GetAvailablePageContentWidth();
414 int element_dist = FONT_HEIGHT_NORMAL;
416 /* Make space for the header */
417 int main_y = GetHeadHeight(max_width) + element_dist;
419 /* Current bottom of left/right column */
420 int left_y = main_y;
421 int right_y = main_y;
422 /* Current width of left/right column, 0 indicates no content in column */
423 int left_width = 0;
424 int right_width = 0;
425 /* Indexes into element cache for yet unresolved floats */
426 std::vector<size_t> left_floats;
427 std::vector<size_t> right_floats;
429 /* Build layout */
430 for (const StoryPageElement *pe : this->story_page_elements) {
431 ElementFloat fl = this->GetPageElementFloat(*pe);
433 if (fl == ElementFloat::None) {
434 /* Verify available width */
435 const int min_required_width = 10 * FONT_HEIGHT_NORMAL;
436 int left_offset = (left_width == 0) ? 0 : (left_width + element_dist);
437 int right_offset = (right_width == 0) ? 0 : (right_width + element_dist);
438 if (left_offset + right_offset + min_required_width >= max_width) {
439 /* Width of floats leave too little for main content, push down */
440 main_y = std::max(main_y, left_y);
441 main_y = std::max(main_y, right_y);
442 left_width = right_width = 0;
443 left_offset = right_offset = 0;
444 /* Do not add element_dist here, to keep together elements which were supposed to float besides each other. */
446 /* Determine height */
447 const int available_width = max_width - left_offset - right_offset;
448 const int height = GetPageElementHeight(*pe, available_width);
449 /* Check for button that needs extra margin */
450 if (left_offset == 0 && right_offset == 0) {
451 switch (pe->type) {
452 case SPET_BUTTON_PUSH:
453 case SPET_BUTTON_TILE:
454 case SPET_BUTTON_VEHICLE:
455 left_offset = right_offset = available_width / 5;
456 break;
457 default:
458 break;
461 /* Position element in main column */
462 LayoutCacheElement ce{ pe, {} };
463 ce.bounds.left = left_offset;
464 ce.bounds.right = max_width - right_offset;
465 ce.bounds.top = main_y;
466 main_y += height;
467 ce.bounds.bottom = main_y;
468 this->layout_cache.push_back(ce);
469 main_y += element_dist;
470 /* Clear all floats */
471 left_width = right_width = 0;
472 left_y = right_y = main_y = std::max({main_y, left_y, right_y});
473 left_floats.clear();
474 right_floats.clear();
475 } else {
476 /* Prepare references to correct column */
477 int &cur_width = (fl == ElementFloat::Left) ? left_width : right_width;
478 int &cur_y = (fl == ElementFloat::Left) ? left_y : right_y;
479 std::vector<size_t> &cur_floats = (fl == ElementFloat::Left) ? left_floats : right_floats;
480 /* Position element */
481 cur_width = std::max(cur_width, this->GetPageElementFloatWidth(*pe));
482 LayoutCacheElement ce{ pe, {} };
483 ce.bounds.left = (fl == ElementFloat::Left) ? 0 : (max_width - cur_width);
484 ce.bounds.right = (fl == ElementFloat::Left) ? cur_width : max_width;
485 ce.bounds.top = cur_y;
486 cur_y += GetPageElementHeight(*pe, cur_width);
487 ce.bounds.bottom = cur_y;
488 cur_floats.push_back(this->layout_cache.size());
489 this->layout_cache.push_back(ce);
490 cur_y += element_dist;
491 /* Update floats in column to all have the same width */
492 for (size_t index : cur_floats) {
493 LayoutCacheElement &ce = this->layout_cache[index];
494 ce.bounds.left = (fl == ElementFloat::Left) ? 0 : (max_width - cur_width);
495 ce.bounds.right = (fl == ElementFloat::Left) ? cur_width : max_width;
502 * Get the total height of the content displayed in this window.
503 * @return the height in pixels
505 uint GetContentHeight()
507 this->EnsureStoryPageElementLayout();
509 /* The largest bottom coordinate of any element is the height of the content */
510 uint max_y = std::accumulate(this->layout_cache.begin(), this->layout_cache.end(), 0, [](uint max_y, const LayoutCacheElement &ce) -> uint { return std::max<uint>(max_y, ce.bounds.bottom); });
512 return max_y;
516 * Draws a page element that is composed of a sprite to the left and a single line of
517 * text after that. These page elements are generally clickable and are thus called
518 * action elements.
519 * @param y_offset Current y_offset which will get updated when this method has completed its drawing.
520 * @param width Width of the region available for drawing.
521 * @param line_height Height of one line of text.
522 * @param action_sprite The sprite to draw.
523 * @param string_id The string id to draw.
524 * @return the number of lines.
526 void DrawActionElement(int &y_offset, int width, int line_height, SpriteID action_sprite, StringID string_id = STR_JUST_RAW_STRING) const
528 Dimension sprite_dim = GetSpriteSize(action_sprite);
529 uint element_height = std::max(sprite_dim.height, (uint)line_height);
531 uint sprite_top = y_offset + (element_height - sprite_dim.height) / 2;
532 uint text_top = y_offset + (element_height - line_height) / 2;
534 DrawSprite(action_sprite, PAL_NONE, 0, sprite_top);
535 DrawString(sprite_dim.width + WD_FRAMETEXT_LEFT, width, text_top, string_id, TC_BLACK);
537 y_offset += element_height;
541 * Internal event handler for when a page element is clicked.
542 * @param pe The clicked page element.
544 void OnPageElementClick(const StoryPageElement& pe)
546 switch (pe.type) {
547 case SPET_TEXT:
548 /* Do nothing. */
549 break;
551 case SPET_LOCATION:
552 if (_ctrl_pressed) {
553 ShowExtraViewportWindow((TileIndex)pe.referenced_id);
554 } else {
555 ScrollMainWindowToTile((TileIndex)pe.referenced_id);
557 break;
559 case SPET_GOAL:
560 ShowGoalsList((CompanyID)this->window_number);
561 break;
563 case SPET_BUTTON_PUSH:
564 if (this->active_button_id != INVALID_STORY_PAGE_ELEMENT) ResetObjectToPlace();
565 this->active_button_id = pe.index;
566 this->SetTimeout();
567 this->SetWidgetDirty(WID_SB_PAGE_PANEL);
569 DoCommandP(0, pe.index, 0, CMD_STORY_PAGE_BUTTON);
570 break;
572 case SPET_BUTTON_TILE:
573 if (this->active_button_id == pe.index) {
574 ResetObjectToPlace();
575 this->active_button_id = INVALID_STORY_PAGE_ELEMENT;
576 } else {
577 CursorID cursor = TranslateStoryPageButtonCursor(StoryPageButtonData{ pe.referenced_id }.GetCursor());
578 SetObjectToPlaceWnd(cursor, PAL_NONE, HT_RECT, this);
579 this->active_button_id = pe.index;
581 this->SetWidgetDirty(WID_SB_PAGE_PANEL);
582 break;
584 case SPET_BUTTON_VEHICLE:
585 if (this->active_button_id == pe.index) {
586 ResetObjectToPlace();
587 this->active_button_id = INVALID_STORY_PAGE_ELEMENT;
588 } else {
589 CursorID cursor = TranslateStoryPageButtonCursor(StoryPageButtonData{ pe.referenced_id }.GetCursor());
590 SetObjectToPlaceWnd(cursor, PAL_NONE, HT_VEHICLE, this);
591 this->active_button_id = pe.index;
593 this->SetWidgetDirty(WID_SB_PAGE_PANEL);
594 break;
596 default:
597 NOT_REACHED();
601 public:
602 StoryBookWindow(WindowDesc *desc, WindowNumber window_number) : Window(desc)
604 this->CreateNestedTree();
605 this->vscroll = this->GetScrollbar(WID_SB_SCROLLBAR);
606 this->vscroll->SetStepSize(FONT_HEIGHT_NORMAL);
608 /* Initialize page sort. */
609 this->story_pages.SetSortFuncs(StoryBookWindow::page_sorter_funcs);
610 this->story_pages.ForceRebuild();
611 this->BuildStoryPageList();
612 this->story_page_elements.SetSortFuncs(StoryBookWindow::page_element_sorter_funcs);
613 /* story_page_elements will get built by SetSelectedPage */
615 this->FinishInitNested(window_number);
616 this->owner = (Owner)this->window_number;
618 /* Initialize selected vars. */
619 this->selected_generic_title[0] = '\0';
620 this->selected_page_id = INVALID_STORY_PAGE;
622 this->active_button_id = INVALID_STORY_PAGE_ELEMENT;
624 this->OnInvalidateData(-1);
628 * Updates the disabled state of the prev/next buttons.
630 void UpdatePrevNextDisabledState()
632 this->SetWidgetDisabledState(WID_SB_PREV_PAGE, story_pages.size() == 0 || this->IsFirstPageSelected());
633 this->SetWidgetDisabledState(WID_SB_NEXT_PAGE, story_pages.size() == 0 || this->IsLastPageSelected());
634 this->SetWidgetDirty(WID_SB_PREV_PAGE);
635 this->SetWidgetDirty(WID_SB_NEXT_PAGE);
639 * Sets the selected page.
640 * @param page_index pool index of the page to select.
642 void SetSelectedPage(uint16 page_index)
644 if (this->selected_page_id != page_index) {
645 if (this->active_button_id) ResetObjectToPlace();
646 this->active_button_id = INVALID_STORY_PAGE_ELEMENT;
647 this->selected_page_id = page_index;
648 this->RefreshSelectedPage();
649 this->UpdatePrevNextDisabledState();
653 void SetStringParameters(int widget) const override
655 switch (widget) {
656 case WID_SB_SEL_PAGE: {
657 StoryPage *page = this->GetSelPage();
658 SetDParamStr(0, page != nullptr && page->title != nullptr ? page->title : this->selected_generic_title);
659 break;
661 case WID_SB_CAPTION:
662 if (this->window_number == INVALID_COMPANY) {
663 SetDParam(0, STR_STORY_BOOK_SPECTATOR_CAPTION);
664 } else {
665 SetDParam(0, STR_STORY_BOOK_CAPTION);
666 SetDParam(1, this->window_number);
668 break;
672 void OnPaint() override
674 /* Detect if content has changed height. This can happen if a
675 * multi-line text contains eg. {COMPANY} and that company is
676 * renamed.
678 if (this->vscroll->GetCount() != this->GetContentHeight()) {
679 this->vscroll->SetCount(this->GetContentHeight());
680 this->SetWidgetDirty(WID_SB_SCROLLBAR);
681 this->SetWidgetDirty(WID_SB_PAGE_PANEL);
684 this->DrawWidgets();
687 void DrawWidget(const Rect &r, int widget) const override
689 if (widget != WID_SB_PAGE_PANEL) return;
691 StoryPage *page = this->GetSelPage();
692 if (page == nullptr) return;
694 const int x = r.left + WD_FRAMETEXT_LEFT;
695 const int y = r.top + WD_FRAMETEXT_TOP;
696 const int right = r.right - WD_FRAMETEXT_RIGHT;
697 const int bottom = r.bottom - WD_FRAMETEXT_BOTTOM;
699 /* Set up a clipping region for the panel. */
700 DrawPixelInfo tmp_dpi;
701 if (!FillDrawPixelInfo(&tmp_dpi, x, y, right - x + 1, bottom - y + 1)) return;
703 DrawPixelInfo *old_dpi = _cur_dpi;
704 _cur_dpi = &tmp_dpi;
706 /* Draw content (now coordinates given to Draw** are local to the new clipping region). */
707 int line_height = FONT_HEIGHT_NORMAL;
708 const int scrollpos = this->vscroll->GetPosition();
709 int y_offset = -scrollpos;
711 /* Date */
712 if (page->date != INVALID_DATE) {
713 SetDParam(0, page->date);
714 DrawString(0, right - x, y_offset, STR_JUST_DATE_LONG, TC_BLACK);
716 y_offset += line_height;
718 /* Title */
719 SetDParamStr(0, page->title != nullptr ? page->title : this->selected_generic_title);
720 y_offset = DrawStringMultiLine(0, right - x, y_offset, bottom - y, STR_STORY_BOOK_TITLE, TC_BLACK, SA_TOP | SA_HOR_CENTER);
722 /* Page elements */
723 this->EnsureStoryPageElementLayout();
724 for (const LayoutCacheElement &ce : this->layout_cache) {
725 y_offset = ce.bounds.top - scrollpos;
726 switch (ce.pe->type) {
727 case SPET_TEXT:
728 SetDParamStr(0, ce.pe->text);
729 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);
730 break;
732 case SPET_GOAL: {
733 Goal *g = Goal::Get((GoalID) ce.pe->referenced_id);
734 StringID string_id = g == nullptr ? STR_STORY_BOOK_INVALID_GOAL_REF : STR_JUST_RAW_STRING;
735 if (g != nullptr) SetDParamStr(0, g->text);
736 DrawActionElement(y_offset, ce.bounds.right - ce.bounds.left, line_height, GetPageElementSprite(*ce.pe), string_id);
737 break;
740 case SPET_LOCATION:
741 SetDParamStr(0, ce.pe->text);
742 DrawActionElement(y_offset, ce.bounds.right - ce.bounds.left, line_height, GetPageElementSprite(*ce.pe));
743 break;
745 case SPET_BUTTON_PUSH:
746 case SPET_BUTTON_TILE:
747 case SPET_BUTTON_VEHICLE: {
748 const int tmargin = WD_BEVEL_TOP + WD_FRAMETEXT_TOP;
749 const FrameFlags frame = this->active_button_id == ce.pe->index ? FR_LOWERED : FR_NONE;
750 const Colours bgcolour = StoryPageButtonData{ ce.pe->referenced_id }.GetColour();
752 DrawFrameRect(ce.bounds.left, ce.bounds.top - scrollpos, ce.bounds.right, ce.bounds.bottom - scrollpos - 1, bgcolour, frame);
754 SetDParamStr(0, ce.pe->text);
755 DrawString(ce.bounds.left + WD_BEVEL_LEFT, ce.bounds.right - WD_BEVEL_RIGHT, ce.bounds.top + tmargin - scrollpos, STR_JUST_RAW_STRING, TC_WHITE, SA_CENTER);
756 break;
759 default: NOT_REACHED();
763 /* Restore clipping region. */
764 _cur_dpi = old_dpi;
767 void UpdateWidgetSize(int widget, Dimension *size, const Dimension &padding, Dimension *fill, Dimension *resize) override
769 if (widget != WID_SB_SEL_PAGE && widget != WID_SB_PAGE_PANEL) return;
771 Dimension d;
772 d.height = FONT_HEIGHT_NORMAL;
773 d.width = 0;
775 switch (widget) {
776 case WID_SB_SEL_PAGE: {
778 /* Get max title width. */
779 for (size_t i = 0; i < this->story_pages.size(); i++) {
780 const StoryPage *s = this->story_pages[i];
782 if (s->title != nullptr) {
783 SetDParamStr(0, s->title);
784 } else {
785 SetDParamStr(0, this->selected_generic_title);
787 Dimension title_d = GetStringBoundingBox(STR_BLACK_RAW_STRING);
789 if (title_d.width > d.width) {
790 d.width = title_d.width;
794 d.width += padding.width;
795 d.height += padding.height;
796 *size = maxdim(*size, d);
797 break;
800 case WID_SB_PAGE_PANEL: {
801 d.height *= 5;
802 d.height += padding.height + WD_FRAMETEXT_TOP + WD_FRAMETEXT_BOTTOM;
803 *size = maxdim(*size, d);
804 break;
810 void OnResize() override
812 this->InvalidateStoryPageElementLayout();
813 this->vscroll->SetCapacityFromWidget(this, WID_SB_PAGE_PANEL, WD_FRAMETEXT_TOP + WD_FRAMETEXT_BOTTOM);
814 this->vscroll->SetCount(this->GetContentHeight());
817 void OnClick(Point pt, int widget, int click_count) override
819 switch (widget) {
820 case WID_SB_SEL_PAGE: {
821 DropDownList list = this->BuildDropDownList();
822 if (!list.empty()) {
823 /* Get the index of selected page. */
824 int selected = 0;
825 for (size_t i = 0; i < this->story_pages.size(); i++) {
826 const StoryPage *p = this->story_pages[i];
827 if (p->index == this->selected_page_id) break;
828 selected++;
831 ShowDropDownList(this, std::move(list), selected, widget);
833 break;
836 case WID_SB_PREV_PAGE:
837 this->SelectPrevPage();
838 break;
840 case WID_SB_NEXT_PAGE:
841 this->SelectNextPage();
842 break;
844 case WID_SB_PAGE_PANEL: {
845 int clicked_y = this->vscroll->GetScrolledRowFromWidget(pt.y, this, WID_SB_PAGE_PANEL, WD_FRAMETEXT_TOP);
846 this->EnsureStoryPageElementLayout();
848 for (const LayoutCacheElement &ce : this->layout_cache) {
849 if (clicked_y >= ce.bounds.top && clicked_y < ce.bounds.bottom && pt.x >= ce.bounds.left && pt.x < ce.bounds.right) {
850 this->OnPageElementClick(*ce.pe);
851 return;
858 void OnDropdownSelect(int widget, int index) override
860 if (widget != WID_SB_SEL_PAGE) return;
862 /* index (which is set in BuildDropDownList) is the page id. */
863 this->SetSelectedPage(index);
867 * Some data on this window has become invalid.
868 * @param data Information about the changed data.
869 * -1 Rebuild page list and refresh current page;
870 * >= 0 Id of the page that needs to be refreshed. If it is not the current page, nothing happens.
871 * @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.
873 void OnInvalidateData(int data = 0, bool gui_scope = true) override
875 if (!gui_scope) return;
877 /* If added/removed page, force rebuild. Sort order never change so just a
878 * re-sort is never needed.
880 if (data == -1) {
881 this->story_pages.ForceRebuild();
882 this->BuildStoryPageList();
884 /* Was the last page removed? */
885 if (this->story_pages.size() == 0) {
886 this->selected_generic_title[0] = '\0';
889 /* Verify page selection. */
890 if (!_story_page_pool.IsValidID(this->selected_page_id)) {
891 this->selected_page_id = INVALID_STORY_PAGE;
893 if (this->selected_page_id == INVALID_STORY_PAGE && this->story_pages.size() > 0) {
894 /* No page is selected, but there exist at least one available.
895 * => Select first page.
897 this->SetSelectedPage(this->story_pages[0]->index);
900 this->SetWidgetDisabledState(WID_SB_SEL_PAGE, this->story_pages.size() == 0);
901 this->SetWidgetDirty(WID_SB_SEL_PAGE);
902 this->UpdatePrevNextDisabledState();
903 } else if (data >= 0 && this->selected_page_id == data) {
904 this->RefreshSelectedPage();
908 void OnTimeout() override
910 this->active_button_id = INVALID_STORY_PAGE_ELEMENT;
911 this->SetWidgetDirty(WID_SB_PAGE_PANEL);
914 void OnPlaceObject(Point pt, TileIndex tile) override
916 const StoryPageElement *const pe = StoryPageElement::GetIfValid(this->active_button_id);
917 if (pe == nullptr || pe->type != SPET_BUTTON_TILE) {
918 ResetObjectToPlace();
919 this->active_button_id = INVALID_STORY_PAGE_ELEMENT;
920 this->SetWidgetDirty(WID_SB_PAGE_PANEL);
921 return;
924 DoCommandP(tile, pe->index, 0, CMD_STORY_PAGE_BUTTON);
925 ResetObjectToPlace();
928 bool OnVehicleSelect(const Vehicle *v) override
930 const StoryPageElement *const pe = StoryPageElement::GetIfValid(this->active_button_id);
931 if (pe == nullptr || pe->type != SPET_BUTTON_VEHICLE) {
932 ResetObjectToPlace();
933 this->active_button_id = INVALID_STORY_PAGE_ELEMENT;
934 this->SetWidgetDirty(WID_SB_PAGE_PANEL);
935 return false;
938 /* Check that the vehicle matches the requested type */
939 StoryPageButtonData data{ pe->referenced_id };
940 VehicleType wanted_vehtype = data.GetVehicleType();
941 if (wanted_vehtype != VEH_INVALID && wanted_vehtype != v->type) return false;
943 DoCommandP(0, pe->index, v->index, CMD_STORY_PAGE_BUTTON);
944 ResetObjectToPlace();
945 return true;
948 void OnPlaceObjectAbort() override
950 this->active_button_id = INVALID_STORY_PAGE_ELEMENT;
951 this->SetWidgetDirty(WID_SB_PAGE_PANEL);
955 GUIStoryPageList::SortFunction * const StoryBookWindow::page_sorter_funcs[] = {
956 &PageOrderSorter,
959 GUIStoryPageElementList::SortFunction * const StoryBookWindow::page_element_sorter_funcs[] = {
960 &PageElementOrderSorter,
963 static const NWidgetPart _nested_story_book_widgets[] = {
964 NWidget(NWID_HORIZONTAL),
965 NWidget(WWT_CLOSEBOX, COLOUR_BROWN),
966 NWidget(WWT_CAPTION, COLOUR_BROWN, WID_SB_CAPTION), SetDataTip(STR_JUST_STRING, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS),
967 NWidget(WWT_SHADEBOX, COLOUR_BROWN),
968 NWidget(WWT_DEFSIZEBOX, COLOUR_BROWN),
969 NWidget(WWT_STICKYBOX, COLOUR_BROWN),
970 EndContainer(),
971 NWidget(NWID_HORIZONTAL), SetFill(1, 1),
972 NWidget(NWID_VERTICAL), SetFill(1, 1),
973 NWidget(WWT_PANEL, COLOUR_BROWN, WID_SB_PAGE_PANEL), SetResize(1, 1), SetScrollbar(WID_SB_SCROLLBAR), EndContainer(),
974 NWidget(NWID_HORIZONTAL),
975 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),
976 NWidget(NWID_BUTTON_DROPDOWN, COLOUR_BROWN, WID_SB_SEL_PAGE), SetMinimalSize(93, 12), SetFill(1, 0),
977 SetDataTip(STR_BLACK_RAW_STRING, STR_STORY_BOOK_SEL_PAGE_TOOLTIP), SetResize(1, 0),
978 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),
979 EndContainer(),
980 EndContainer(),
981 NWidget(NWID_VERTICAL), SetFill(0, 1),
982 NWidget(NWID_VSCROLLBAR, COLOUR_BROWN, WID_SB_SCROLLBAR),
983 NWidget(WWT_RESIZEBOX, COLOUR_BROWN),
984 EndContainer(),
985 EndContainer(),
988 static WindowDesc _story_book_desc(
989 WDP_CENTER, "view_story", 400, 300,
990 WC_STORY_BOOK, WC_NONE,
992 _nested_story_book_widgets, lengthof(_nested_story_book_widgets)
995 static CursorID TranslateStoryPageButtonCursor(StoryPageButtonCursor cursor)
997 switch (cursor) {
998 case SPBC_MOUSE: return SPR_CURSOR_MOUSE;
999 case SPBC_ZZZ: return SPR_CURSOR_ZZZ;
1000 case SPBC_BUOY: return SPR_CURSOR_BUOY;
1001 case SPBC_QUERY: return SPR_CURSOR_QUERY;
1002 case SPBC_HQ: return SPR_CURSOR_HQ;
1003 case SPBC_SHIP_DEPOT: return SPR_CURSOR_SHIP_DEPOT;
1004 case SPBC_SIGN: return SPR_CURSOR_SIGN;
1005 case SPBC_TREE: return SPR_CURSOR_TREE;
1006 case SPBC_BUY_LAND: return SPR_CURSOR_BUY_LAND;
1007 case SPBC_LEVEL_LAND: return SPR_CURSOR_LEVEL_LAND;
1008 case SPBC_TOWN: return SPR_CURSOR_TOWN;
1009 case SPBC_INDUSTRY: return SPR_CURSOR_INDUSTRY;
1010 case SPBC_ROCKY_AREA: return SPR_CURSOR_ROCKY_AREA;
1011 case SPBC_DESERT: return SPR_CURSOR_DESERT;
1012 case SPBC_TRANSMITTER: return SPR_CURSOR_TRANSMITTER;
1013 case SPBC_AIRPORT: return SPR_CURSOR_AIRPORT;
1014 case SPBC_DOCK: return SPR_CURSOR_DOCK;
1015 case SPBC_CANAL: return SPR_CURSOR_CANAL;
1016 case SPBC_LOCK: return SPR_CURSOR_LOCK;
1017 case SPBC_RIVER: return SPR_CURSOR_RIVER;
1018 case SPBC_AQUEDUCT: return SPR_CURSOR_AQUEDUCT;
1019 case SPBC_BRIDGE: return SPR_CURSOR_BRIDGE;
1020 case SPBC_RAIL_STATION: return SPR_CURSOR_RAIL_STATION;
1021 case SPBC_TUNNEL_RAIL: return SPR_CURSOR_TUNNEL_RAIL;
1022 case SPBC_TUNNEL_ELRAIL: return SPR_CURSOR_TUNNEL_ELRAIL;
1023 case SPBC_TUNNEL_MONO: return SPR_CURSOR_TUNNEL_MONO;
1024 case SPBC_TUNNEL_MAGLEV: return SPR_CURSOR_TUNNEL_MAGLEV;
1025 case SPBC_AUTORAIL: return SPR_CURSOR_AUTORAIL;
1026 case SPBC_AUTOELRAIL: return SPR_CURSOR_AUTOELRAIL;
1027 case SPBC_AUTOMONO: return SPR_CURSOR_AUTOMONO;
1028 case SPBC_AUTOMAGLEV: return SPR_CURSOR_AUTOMAGLEV;
1029 case SPBC_WAYPOINT: return SPR_CURSOR_WAYPOINT;
1030 case SPBC_RAIL_DEPOT: return SPR_CURSOR_RAIL_DEPOT;
1031 case SPBC_ELRAIL_DEPOT: return SPR_CURSOR_ELRAIL_DEPOT;
1032 case SPBC_MONO_DEPOT: return SPR_CURSOR_MONO_DEPOT;
1033 case SPBC_MAGLEV_DEPOT: return SPR_CURSOR_MAGLEV_DEPOT;
1034 case SPBC_CONVERT_RAIL: return SPR_CURSOR_CONVERT_RAIL;
1035 case SPBC_CONVERT_ELRAIL: return SPR_CURSOR_CONVERT_ELRAIL;
1036 case SPBC_CONVERT_MONO: return SPR_CURSOR_CONVERT_MONO;
1037 case SPBC_CONVERT_MAGLEV: return SPR_CURSOR_CONVERT_MAGLEV;
1038 case SPBC_AUTOROAD: return SPR_CURSOR_AUTOROAD;
1039 case SPBC_AUTOTRAM: return SPR_CURSOR_AUTOTRAM;
1040 case SPBC_ROAD_DEPOT: return SPR_CURSOR_ROAD_DEPOT;
1041 case SPBC_BUS_STATION: return SPR_CURSOR_BUS_STATION;
1042 case SPBC_TRUCK_STATION: return SPR_CURSOR_TRUCK_STATION;
1043 case SPBC_ROAD_TUNNEL: return SPR_CURSOR_ROAD_TUNNEL;
1044 case SPBC_CLONE_TRAIN: return SPR_CURSOR_CLONE_TRAIN;
1045 case SPBC_CLONE_ROADVEH: return SPR_CURSOR_CLONE_ROADVEH;
1046 case SPBC_CLONE_SHIP: return SPR_CURSOR_CLONE_SHIP;
1047 case SPBC_CLONE_AIRPLANE: return SPR_CURSOR_CLONE_AIRPLANE;
1048 case SPBC_DEMOLISH: return ANIMCURSOR_DEMOLISH;
1049 case SPBC_LOWERLAND: return ANIMCURSOR_LOWERLAND;
1050 case SPBC_RAISELAND: return ANIMCURSOR_RAISELAND;
1051 case SPBC_PICKSTATION: return ANIMCURSOR_PICKSTATION;
1052 case SPBC_BUILDSIGNALS: return ANIMCURSOR_BUILDSIGNALS;
1053 default: return SPR_CURSOR_QUERY;
1058 * Raise or create the story book window for \a company, at page \a page_id.
1059 * @param company 'Owner' of the story book, may be #INVALID_COMPANY.
1060 * @param page_id Page to open, may be #INVALID_STORY_PAGE.
1062 void ShowStoryBook(CompanyID company, uint16 page_id)
1064 if (!Company::IsValidID(company)) company = (CompanyID)INVALID_COMPANY;
1066 StoryBookWindow *w = AllocateWindowDescFront<StoryBookWindow>(&_story_book_desc, company, true);
1067 if (page_id != INVALID_STORY_PAGE) w->SetSelectedPage(page_id);