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/>.
8 /** @file story_gui.cpp GUI for stories. */
11 #include "window_gui.h"
12 #include "strings_func.h"
13 #include "date_func.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"
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
{
45 struct LayoutCacheElement
{
46 const StoryPageElement
*pe
;
49 typedef std::vector
<LayoutCacheElement
> LayoutCache
;
51 enum class ElementFloat
{
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();
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
151 for (const StoryPage
*p
: this->story_pages
) {
152 if (p
->index
== this->selected_page_id
) {
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
);
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. */
240 if (iter
!= this->story_pages
.end()) {
241 this->SetSelectedPage((*iter
)->index
);
249 * Builds the page selector drop down list.
251 DropDownList
BuildDropDownList() const
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
);
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
);
268 list
.emplace_back(item
);
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;
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
);
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
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
;
319 return SPR_IMG_VIEW_LOCATION
;
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
335 SetDParamStr(0, pe
.text
);
336 return GetStringHeight(STR_BLACK_RAW_STRING
, max_width
);
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
;
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
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
;
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
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
;
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 */
421 int right_y
= main_y
;
422 /* Current width of left/right column, 0 indicates no content in column */
425 /* Indexes into element cache for yet unresolved floats */
426 std::vector
<size_t> left_floats
;
427 std::vector
<size_t> right_floats
;
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) {
452 case SPET_BUTTON_PUSH
:
453 case SPET_BUTTON_TILE
:
454 case SPET_BUTTON_VEHICLE
:
455 left_offset
= right_offset
= available_width
/ 5;
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
;
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
});
474 right_floats
.clear();
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
); });
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
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
)
553 ShowExtraViewportWindow((TileIndex
)pe
.referenced_id
);
555 ScrollMainWindowToTile((TileIndex
)pe
.referenced_id
);
560 ShowGoalsList((CompanyID
)this->window_number
);
563 case SPET_BUTTON_PUSH
:
564 if (this->active_button_id
!= INVALID_STORY_PAGE_ELEMENT
) ResetObjectToPlace();
565 this->active_button_id
= pe
.index
;
567 this->SetWidgetDirty(WID_SB_PAGE_PANEL
);
569 DoCommandP(0, pe
.index
, 0, CMD_STORY_PAGE_BUTTON
);
572 case SPET_BUTTON_TILE
:
573 if (this->active_button_id
== pe
.index
) {
574 ResetObjectToPlace();
575 this->active_button_id
= INVALID_STORY_PAGE_ELEMENT
;
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
);
584 case SPET_BUTTON_VEHICLE
:
585 if (this->active_button_id
== pe
.index
) {
586 ResetObjectToPlace();
587 this->active_button_id
= INVALID_STORY_PAGE_ELEMENT
;
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
);
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
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
);
662 if (this->window_number
== INVALID_COMPANY
) {
663 SetDParam(0, STR_STORY_BOOK_SPECTATOR_CAPTION
);
665 SetDParam(0, STR_STORY_BOOK_CAPTION
);
666 SetDParam(1, this->window_number
);
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
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
);
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
;
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
;
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
;
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
);
723 this->EnsureStoryPageElementLayout();
724 for (const LayoutCacheElement
&ce
: this->layout_cache
) {
725 y_offset
= ce
.bounds
.top
- scrollpos
;
726 switch (ce
.pe
->type
) {
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
);
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
);
741 SetDParamStr(0, ce
.pe
->text
);
742 DrawActionElement(y_offset
, ce
.bounds
.right
- ce
.bounds
.left
, line_height
, GetPageElementSprite(*ce
.pe
));
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
);
759 default: NOT_REACHED();
763 /* Restore clipping region. */
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;
772 d
.height
= FONT_HEIGHT_NORMAL
;
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
);
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
);
800 case WID_SB_PAGE_PANEL
: {
802 d
.height
+= padding
.height
+ WD_FRAMETEXT_TOP
+ WD_FRAMETEXT_BOTTOM
;
803 *size
= maxdim(*size
, d
);
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
820 case WID_SB_SEL_PAGE
: {
821 DropDownList list
= this->BuildDropDownList();
823 /* Get the index of selected page. */
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;
831 ShowDropDownList(this, std::move(list
), selected
, widget
);
836 case WID_SB_PREV_PAGE
:
837 this->SelectPrevPage();
840 case WID_SB_NEXT_PAGE
:
841 this->SelectNextPage();
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
);
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.
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
);
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
);
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();
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
[] = {
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
),
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
),
981 NWidget(NWID_VERTICAL
), SetFill(0, 1),
982 NWidget(NWID_VSCROLLBAR
, COLOUR_BROWN
, WID_SB_SCROLLBAR
),
983 NWidget(WWT_RESIZEBOX
, COLOUR_BROWN
),
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
)
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
);