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"
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
{
43 struct LayoutCacheElement
{
44 const StoryPageElement
*pe
;
47 typedef std::vector
<LayoutCacheElement
> LayoutCache
;
49 enum class ElementFloat
{
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();
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
147 for (const StoryPage
*p
: this->story_pages
) {
148 if (p
->index
== this->selected_page_id
) {
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
);
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. */
236 if (iter
!= this->story_pages
.end()) {
237 this->SetSelectedPage((*iter
)->index
);
245 * Builds the page selector drop down list.
247 DropDownList
BuildDropDownList() const
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
));
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
));
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;
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
);
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
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
;
310 return SPR_IMG_VIEW_LOCATION
;
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
326 SetDParamStr(0, pe
.text
);
327 return GetStringHeight(STR_JUST_RAW_STRING
, max_width
);
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();
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
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
;
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
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();
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 */
412 int right_y
= main_y
;
413 /* Current width of left/right column, 0 indicates no content in column */
416 /* Indexes into element cache for yet unresolved floats */
417 std::vector
<size_t> left_floats
;
418 std::vector
<size_t> right_floats
;
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) {
443 case SPET_BUTTON_PUSH
:
444 case SPET_BUTTON_TILE
:
445 case SPET_BUTTON_VEHICLE
:
446 left_offset
= right_offset
= available_width
/ 5;
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
;
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
});
465 right_floats
.clear();
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
); });
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
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
)
544 ShowExtraViewportWindow((TileIndex
)pe
.referenced_id
);
546 ScrollMainWindowToTile((TileIndex
)pe
.referenced_id
);
551 ShowGoalsList((CompanyID
)this->window_number
);
554 case SPET_BUTTON_PUSH
:
555 if (this->active_button_id
!= INVALID_STORY_PAGE_ELEMENT
) ResetObjectToPlace();
556 this->active_button_id
= pe
.index
;
558 this->SetWidgetDirty(WID_SB_PAGE_PANEL
);
560 Command
<CMD_STORY_PAGE_BUTTON
>::Post(0, pe
.index
, 0);
563 case SPET_BUTTON_TILE
:
564 if (this->active_button_id
== pe
.index
) {
565 ResetObjectToPlace();
566 this->active_button_id
= INVALID_STORY_PAGE_ELEMENT
;
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
);
575 case SPET_BUTTON_VEHICLE
:
576 if (this->active_button_id
== pe
.index
) {
577 ResetObjectToPlace();
578 this->active_button_id
= INVALID_STORY_PAGE_ELEMENT
;
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
);
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
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
);
653 if (this->window_number
== INVALID_COMPANY
) {
654 SetDParam(0, STR_STORY_BOOK_SPECTATOR_CAPTION
);
656 SetDParam(0, STR_STORY_BOOK_CAPTION
);
657 SetDParam(1, this->window_number
);
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
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
);
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
;
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
;
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
);
711 this->EnsureStoryPageElementLayout();
712 for (const LayoutCacheElement
&ce
: this->layout_cache
) {
713 y_offset
= ce
.bounds
.top
- scrollpos
;
714 switch (ce
.pe
->type
) {
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
);
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
);
729 SetDParamStr(0, ce
.pe
->text
);
730 DrawActionElement(y_offset
, ce
.bounds
.right
- ce
.bounds
.left
, line_height
, GetPageElementSprite(*ce
.pe
));
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
);
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;
757 d
.height
= GetCharacterHeight(FS_NORMAL
);
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
);
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
);
785 case WID_SB_PAGE_PANEL
: {
787 d
.height
+= padding
.height
+ WidgetDimensions::scaled
.frametext
.Vertical();
788 size
= maxdim(size
, d
);
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
805 case WID_SB_SEL_PAGE
: {
806 DropDownList list
= this->BuildDropDownList();
808 /* Get the index of selected page. */
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;
816 ShowDropDownList(this, std::move(list
), selected
, widget
);
821 case WID_SB_PREV_PAGE
:
822 this->SelectPrevPage();
825 case WID_SB_NEXT_PAGE
:
826 this->SelectNextPage();
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
);
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.
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
);
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
);
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();
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
= {
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
),
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
),
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
),
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
)
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
);