OpenTTD
story_gui.cpp
Go to the documentation of this file.
1 /* $Id: story_gui.cpp 27086 2014-12-18 18:22:23Z alberth $ */
2 
3 /*
4  * This file is part of OpenTTD.
5  * 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.
6  * 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.
7  * 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  */
9 
12 #include "stdafx.h"
13 #include "window_gui.h"
14 #include "strings_func.h"
15 #include "date_func.h"
16 #include "gui.h"
17 #include "story_base.h"
18 #include "core/geometry_func.hpp"
19 #include "company_func.h"
20 #include "command_func.h"
21 #include "widgets/dropdown_type.h"
22 #include "widgets/dropdown_func.h"
23 #include "sortlist_type.h"
24 #include "goal_base.h"
25 #include "viewport_func.h"
26 #include "window_func.h"
27 #include "company_base.h"
28 
29 #include "widgets/story_widget.h"
30 
31 #include "table/strings.h"
32 #include "table/sprites.h"
33 
34 #include "safeguards.h"
35 
38 
40 protected:
42 
47 
48  static GUIStoryPageList::SortFunction * const page_sorter_funcs[];
49  static GUIStoryPageElementList::SortFunction * const page_element_sorter_funcs[];
50 
53  {
54  if (this->story_pages.NeedRebuild()) {
55  this->story_pages.Clear();
56 
57  const StoryPage *p;
58  FOR_ALL_STORY_PAGES(p) {
59  if (this->IsPageAvailable(p)) {
60  *this->story_pages.Append() = p;
61  }
62  }
63 
64  this->story_pages.Compact();
65  this->story_pages.RebuildDone();
66  }
67 
68  this->story_pages.Sort();
69  }
70 
72  static int CDECL PageOrderSorter(const StoryPage * const *a, const StoryPage * const *b)
73  {
74  return (*a)->sort_value - (*b)->sort_value;
75  }
76 
79  {
80  if (this->story_page_elements.NeedRebuild()) {
81  this->story_page_elements.Clear();
82 
83  const StoryPage *p = GetSelPage();
84  if (p != NULL) {
85  const StoryPageElement *pe;
86  FOR_ALL_STORY_PAGE_ELEMENTS(pe) {
87  if (pe->page == p->index) {
88  *this->story_page_elements.Append() = pe;
89  }
90  }
91  }
92 
95  }
96 
97  this->story_page_elements.Sort();
98  }
99 
101  static int CDECL PageElementOrderSorter(const StoryPageElement * const *a, const StoryPageElement * const *b)
102  {
103  return (*a)->sort_value - (*b)->sort_value;
104  }
105 
106  /*
107  * Checks if a given page should be visible in the story book.
108  * @param page The page to check.
109  * @return True if the page should be visible, otherwise false.
110  */
111  bool IsPageAvailable(const StoryPage *page) const
112  {
113  return page->company == INVALID_COMPANY || page->company == this->window_number;
114  }
115 
121  {
122  if (!_story_page_pool.IsValidID(selected_page_id)) return NULL;
123  return _story_page_pool.Get(selected_page_id);
124  }
125 
130  int GetSelPageNum() const
131  {
132  int page_number = 0;
133  for (const StoryPage *const*iter = this->story_pages.Begin(); iter != this->story_pages.End(); iter++) {
134  const StoryPage *p = *iter;
135  if (p->index == this->selected_page_id) {
136  return page_number;
137  }
138  page_number++;
139  }
140  return -1;
141  }
142 
147  {
148  /* Verify that the selected page exist. */
149  if (!_story_page_pool.IsValidID(this->selected_page_id)) return false;
150 
151  return (*this->story_pages.Begin())->index == this->selected_page_id;
152  }
153 
158  {
159  /* Verify that the selected page exist. */
160  if (!_story_page_pool.IsValidID(this->selected_page_id)) return false;
161 
162  if (this->story_pages.Length() <= 1) return true;
163  const StoryPage *last = *(this->story_pages.End() - 1);
164  return last->index == this->selected_page_id;
165  }
166 
171  {
172  /* Generate generic title if selected page have no custom title. */
173  StoryPage *page = this->GetSelPage();
174  if (page != NULL && page->title == NULL) {
175  SetDParam(0, GetSelPageNum() + 1);
176  GetString(selected_generic_title, STR_STORY_BOOK_GENERIC_PAGE_ITEM, lastof(selected_generic_title));
177  }
178 
181 
182  this->vscroll->SetCount(this->GetContentHeight());
186  }
187 
192  {
193  if (!_story_page_pool.IsValidID(this->selected_page_id)) return;
194 
195  /* Find the last available page which is previous to the current selected page. */
196  const StoryPage *last_available;
197  last_available = NULL;
198  for (const StoryPage *const*iter = this->story_pages.Begin(); iter != this->story_pages.End(); iter++) {
199  const StoryPage *p = *iter;
200  if (p->index == this->selected_page_id) {
201  if (last_available == NULL) return; // No previous page available.
202  this->SetSelectedPage(last_available->index);
203  return;
204  }
205  last_available = p;
206  }
207  }
208 
213  {
214  if (!_story_page_pool.IsValidID(this->selected_page_id)) return;
215 
216  /* Find selected page. */
217  for (const StoryPage *const*iter = this->story_pages.Begin(); iter != this->story_pages.End(); iter++) {
218  const StoryPage *p = *iter;
219  if (p->index == this->selected_page_id) {
220  /* Select the page after selected page. */
221  iter++;
222  if (iter != this->story_pages.End()) {
223  this->SetSelectedPage((*iter)->index);
224  }
225  return;
226  }
227  }
228  }
229 
234  {
235  DropDownList *list = new DropDownList();
236  uint16 page_num = 1;
237  for (const StoryPage *const*iter = this->story_pages.Begin(); iter != this->story_pages.End(); iter++) {
238  const StoryPage *p = *iter;
239  bool current_page = p->index == this->selected_page_id;
240  DropDownListStringItem *item = NULL;
241  if (p->title != NULL) {
242  item = new DropDownListCharStringItem(p->title, p->index, current_page);
243  } else {
244  /* No custom title => use a generic page title with page number. */
245  DropDownListParamStringItem *str_item =
246  new DropDownListParamStringItem(STR_STORY_BOOK_GENERIC_PAGE_ITEM, p->index, current_page);
247  str_item->SetParam(0, page_num);
248  item = str_item;
249  }
250 
251  *list->Append() = item;
252  page_num++;
253  }
254 
255  /* Check if list is empty. */
256  if (list->Length() == 0) {
257  delete list;
258  list = NULL;
259  }
260 
261  return list;
262  }
263 
268  {
269  return this->GetWidget<NWidgetCore>(WID_SB_PAGE_PANEL)->current_x - WD_FRAMETEXT_LEFT - WD_FRAMERECT_RIGHT;
270  }
271 
279  uint GetHeadHeight(int max_width) const
280  {
281  StoryPage *page = this->GetSelPage();
282  if (page == NULL) return 0;
283  int height = 0;
284 
285  /* Title lines */
286  height += FONT_HEIGHT_NORMAL; // Date always use exactly one line.
287  SetDParamStr(0, page->title != NULL ? page->title : this->selected_generic_title);
288  height += GetStringHeight(STR_STORY_BOOK_TITLE, max_width);
289 
290  return height;
291  }
292 
300  {
301  switch (pe.type) {
302  case SPET_GOAL: {
303  Goal *g = Goal::Get((GoalID) pe.referenced_id);
304  if (g == NULL) return SPR_IMG_GOAL_BROKEN_REF;
305  return g->completed ? SPR_IMG_GOAL_COMPLETED : SPR_IMG_GOAL;
306  }
307  case SPET_LOCATION:
308  return SPR_IMG_VIEW_LOCATION;
309  default:
310  NOT_REACHED();
311  }
312  }
313 
320  uint GetPageElementHeight(const StoryPageElement &pe, int max_width)
321  {
322  switch (pe.type) {
323  case SPET_TEXT:
324  SetDParamStr(0, pe.text);
325  return GetStringHeight(STR_BLACK_RAW_STRING, max_width);
326  break;
327 
328  case SPET_GOAL:
329  case SPET_LOCATION: {
330  Dimension sprite_dim = GetSpriteSize(GetPageElementSprite(pe));
331  return sprite_dim.height;
332  break;
333  }
334  default:
335  NOT_REACHED();
336  }
337  return 0;
338  }
339 
346  {
347  StoryPage *page = this->GetSelPage();
348  if (page == NULL) return 0;
349  int max_width = GetAvailablePageContentWidth();
350  uint element_vertical_dist = FONT_HEIGHT_NORMAL;
351 
352  /* Head */
353  uint height = GetHeadHeight(max_width);
354 
355  /* Body */
356  for (const StoryPageElement **iter = this->story_page_elements.Begin(); iter != this->story_page_elements.End(); iter++) {
357  const StoryPageElement *pe = *iter;
358  height += element_vertical_dist;
359  height += GetPageElementHeight(*pe, max_width);
360  }
361 
362  return height;
363  }
364 
376  void DrawActionElement(int &y_offset, int width, int line_height, SpriteID action_sprite, StringID string_id = STR_JUST_RAW_STRING) const
377  {
378  Dimension sprite_dim = GetSpriteSize(action_sprite);
379  uint element_height = max(sprite_dim.height, (uint)line_height);
380 
381  uint sprite_top = y_offset + (element_height - sprite_dim.height) / 2;
382  uint text_top = y_offset + (element_height - line_height) / 2;
383 
384  DrawSprite(action_sprite, PAL_NONE, 0, sprite_top);
385  DrawString(sprite_dim.width + WD_FRAMETEXT_LEFT, width, text_top, string_id, TC_BLACK);
386 
387  y_offset += element_height;
388  }
389 
395  {
396  switch (pe.type) {
397  case SPET_TEXT:
398  /* Do nothing. */
399  break;
400 
401  case SPET_LOCATION:
402  if (_ctrl_pressed) {
404  } else {
406  }
407  break;
408 
409  case SPET_GOAL:
411  break;
412 
413  default:
414  NOT_REACHED();
415  }
416  }
417 
418 public:
420  {
421  this->CreateNestedTree();
422  this->vscroll = this->GetScrollbar(WID_SB_SCROLLBAR);
424 
425  /* Initalize page sort. */
426  this->story_pages.SetSortFuncs(StoryBookWindow::page_sorter_funcs);
427  this->story_pages.ForceRebuild();
428  this->BuildStoryPageList();
429  this->story_page_elements.SetSortFuncs(StoryBookWindow::page_element_sorter_funcs);
430  /* story_page_elements will get built by SetSelectedPage */
431 
432  this->FinishInitNested(window_number);
433  this->owner = (Owner)this->window_number;
434 
435  /* Initialize selected vars. */
436  this->selected_generic_title[0] = '\0';
438 
439  this->OnInvalidateData(-1);
440  }
441 
446  {
451  }
452 
457  void SetSelectedPage(uint16 page_index)
458  {
459  if (this->selected_page_id != page_index) {
460  this->selected_page_id = page_index;
461  this->RefreshSelectedPage();
463  }
464  }
465 
466  virtual void SetStringParameters(int widget) const
467  {
468  switch (widget) {
469  case WID_SB_SEL_PAGE: {
470  StoryPage *page = this->GetSelPage();
471  SetDParamStr(0, page != NULL && page->title != NULL ? page->title : this->selected_generic_title);
472  break;
473  }
474  case WID_SB_CAPTION:
475  if (this->window_number == INVALID_COMPANY) {
476  SetDParam(0, STR_STORY_BOOK_SPECTATOR_CAPTION);
477  } else {
478  SetDParam(0, STR_STORY_BOOK_CAPTION);
479  SetDParam(1, this->window_number);
480  }
481  break;
482  }
483  }
484 
485  virtual void OnPaint()
486  {
487  /* Detect if content has changed height. This can happen if a
488  * multi-line text contains eg. {COMPANY} and that company is
489  * renamed.
490  */
491  if (this->vscroll->GetCount() != this->GetContentHeight()) {
492  this->vscroll->SetCount(this->GetContentHeight());
495  }
496 
497  this->DrawWidgets();
498  }
499 
500  virtual void DrawWidget(const Rect &r, int widget) const
501  {
502  if (widget != WID_SB_PAGE_PANEL) return;
503 
504  StoryPage *page = this->GetSelPage();
505  if (page == NULL) return;
506 
507  const int x = r.left + WD_FRAMETEXT_LEFT;
508  const int y = r.top + WD_FRAMETEXT_TOP;
509  const int right = r.right - WD_FRAMETEXT_RIGHT;
510  const int bottom = r.bottom - WD_FRAMETEXT_BOTTOM;
511 
512  /* Set up a clipping region for the panel. */
513  DrawPixelInfo tmp_dpi;
514  if (!FillDrawPixelInfo(&tmp_dpi, x, y, right - x + 1, bottom - y + 1)) return;
515 
516  DrawPixelInfo *old_dpi = _cur_dpi;
517  _cur_dpi = &tmp_dpi;
518 
519  /* Draw content (now coordinates given to Draw** are local to the new clipping region). */
520  int line_height = FONT_HEIGHT_NORMAL;
521  int y_offset = - this->vscroll->GetPosition();
522 
523  /* Date */
524  if (page->date != INVALID_DATE) {
525  SetDParam(0, page->date);
526  DrawString(0, right - x, y_offset, STR_JUST_DATE_LONG, TC_BLACK);
527  }
528  y_offset += line_height;
529 
530  /* Title */
531  SetDParamStr(0, page->title != NULL ? page->title : this->selected_generic_title);
532  y_offset = DrawStringMultiLine(0, right - x, y_offset, bottom - y, STR_STORY_BOOK_TITLE, TC_BLACK, SA_TOP | SA_HOR_CENTER);
533 
534  /* Page elements */
535  for (const StoryPageElement *const*iter = this->story_page_elements.Begin(); iter != this->story_page_elements.End(); iter++) {
536  const StoryPageElement *const pe = *iter;
537  y_offset += line_height; // margin to previous element
538 
539  switch (pe->type) {
540  case SPET_TEXT:
541  SetDParamStr(0, pe->text);
542  y_offset = DrawStringMultiLine(0, right - x, y_offset, bottom - y, STR_JUST_RAW_STRING, TC_BLACK, SA_TOP | SA_LEFT);
543  break;
544 
545  case SPET_GOAL: {
546  Goal *g = Goal::Get((GoalID) pe->referenced_id);
547  StringID string_id = g == NULL ? STR_STORY_BOOK_INVALID_GOAL_REF : STR_JUST_RAW_STRING;
548  if (g != NULL) SetDParamStr(0, g->text);
549  DrawActionElement(y_offset, right - x, line_height, GetPageElementSprite(*pe), string_id);
550  break;
551  }
552 
553  case SPET_LOCATION:
554  SetDParamStr(0, pe->text);
555  DrawActionElement(y_offset, right - x, line_height, GetPageElementSprite(*pe));
556  break;
557 
558  default: NOT_REACHED();
559  }
560  }
561 
562  /* Restore clipping region. */
563  _cur_dpi = old_dpi;
564  }
565 
566  virtual void UpdateWidgetSize(int widget, Dimension *size, const Dimension &padding, Dimension *fill, Dimension *resize)
567  {
568  if (widget != WID_SB_SEL_PAGE && widget != WID_SB_PAGE_PANEL) return;
569 
570  Dimension d;
571  d.height = FONT_HEIGHT_NORMAL;
572  d.width = 0;
573 
574  switch (widget) {
575  case WID_SB_SEL_PAGE: {
576 
577  /* Get max title width. */
578  for (uint16 i = 0; i < this->story_pages.Length(); i++) {
579  const StoryPage *s = this->story_pages[i];
580 
581  if (s->title != NULL) {
582  SetDParamStr(0, s->title);
583  } else {
585  }
586  Dimension title_d = GetStringBoundingBox(STR_BLACK_RAW_STRING);
587 
588  if (title_d.width > d.width) {
589  d.width = title_d.width;
590  }
591  }
592 
593  d.width += padding.width;
594  d.height += padding.height;
595  *size = maxdim(*size, d);
596  break;
597  }
598 
599  case WID_SB_PAGE_PANEL: {
600  d.height *= 5;
601  d.height += padding.height + WD_FRAMETEXT_TOP + WD_FRAMETEXT_BOTTOM;
602  *size = maxdim(*size, d);
603  break;
604  }
605  }
606 
607  }
608 
609  virtual void OnResize()
610  {
612  this->vscroll->SetCount(this->GetContentHeight());
613  }
614 
615  virtual void OnClick(Point pt, int widget, int click_count)
616  {
617  switch (widget) {
618  case WID_SB_SEL_PAGE: {
619  DropDownList *list = this->BuildDropDownList();
620  if (list != NULL) {
621  /* Get the index of selected page. */
622  int selected = 0;
623  for (uint16 i = 0; i < this->story_pages.Length(); i++) {
624  const StoryPage *p = this->story_pages[i];
625  if (p->index == this->selected_page_id) break;
626  selected++;
627  }
628 
629  ShowDropDownList(this, list, selected, widget);
630  }
631  break;
632  }
633 
634  case WID_SB_PREV_PAGE:
635  this->SelectPrevPage();
636  break;
637 
638  case WID_SB_NEXT_PAGE:
639  this->SelectNextPage();
640  break;
641 
642  case WID_SB_PAGE_PANEL: {
643  uint clicked_y = this->vscroll->GetScrolledRowFromWidget(pt.y, this, WID_SB_PAGE_PANEL, WD_FRAMETEXT_TOP);
644  uint max_width = GetAvailablePageContentWidth();
645 
646  /* Skip head rows. */
647  uint head_height = this->GetHeadHeight(max_width);
648  if (clicked_y < head_height) return;
649 
650  /* Detect if a page element was clicked. */
651  uint y = head_height;
652  uint element_vertical_dist = FONT_HEIGHT_NORMAL;
653  for (const StoryPageElement *const*iter = this->story_page_elements.Begin(); iter != this->story_page_elements.End(); iter++) {
654  const StoryPageElement *const pe = *iter;
655 
656  y += element_vertical_dist; // margin row
657 
658  uint content_height = GetPageElementHeight(*pe, max_width);
659  if (clicked_y >= y && clicked_y < y + content_height) {
660  this->OnPageElementClick(*pe);
661  return;
662  }
663 
664  y += content_height;
665  }
666  }
667  }
668  }
669 
670  virtual void OnDropdownSelect(int widget, int index)
671  {
672  if (widget != WID_SB_SEL_PAGE) return;
673 
674  /* index (which is set in BuildDropDownList) is the page id. */
675  this->SetSelectedPage(index);
676  }
677 
685  virtual void OnInvalidateData(int data = 0, bool gui_scope = true)
686  {
687  if (!gui_scope) return;
688 
689  /* If added/removed page, force rebuild. Sort order never change so just a
690  * re-sort is never needed.
691  */
692  if (data == -1) {
693  this->story_pages.ForceRebuild();
694  this->BuildStoryPageList();
695 
696  /* Was the last page removed? */
697  if (this->story_pages.Length() == 0) {
698  this->selected_generic_title[0] = '\0';
699  }
700 
701  /* Verify page selection. */
702  if (!_story_page_pool.IsValidID(this->selected_page_id)) {
704  }
705  if (this->selected_page_id == INVALID_STORY_PAGE && this->story_pages.Length() > 0) {
706  /* No page is selected, but there exist at least one available.
707  * => Select first page.
708  */
709  this->SetSelectedPage(this->story_pages[0]->index);
710  }
711 
715  } else if (data >= 0 && this->selected_page_id == data) {
716  this->RefreshSelectedPage();
717  }
718  }
719 };
720 
721 GUIStoryPageList::SortFunction * const StoryBookWindow::page_sorter_funcs[] = {
722  &PageOrderSorter,
723 };
724 
725 GUIStoryPageElementList::SortFunction * const StoryBookWindow::page_element_sorter_funcs[] = {
726  &PageElementOrderSorter,
727 };
728 
729 static const NWidgetPart _nested_story_book_widgets[] = {
731  NWidget(WWT_CLOSEBOX, COLOUR_BROWN),
732  NWidget(WWT_CAPTION, COLOUR_BROWN, WID_SB_CAPTION), SetDataTip(STR_JUST_STRING, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS),
733  NWidget(WWT_SHADEBOX, COLOUR_BROWN),
734  NWidget(WWT_DEFSIZEBOX, COLOUR_BROWN),
735  NWidget(WWT_STICKYBOX, COLOUR_BROWN),
736  EndContainer(),
738  NWidget(NWID_VERTICAL), SetFill(1, 1),
741  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),
742  NWidget(NWID_BUTTON_DROPDOWN, COLOUR_BROWN, WID_SB_SEL_PAGE), SetMinimalSize(93, 12), SetFill(1, 0),
743  SetDataTip(STR_BLACK_RAW_STRING, STR_STORY_BOOK_SEL_PAGE_TOOLTIP), SetResize(1, 0),
744  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),
745  EndContainer(),
746  EndContainer(),
747  NWidget(NWID_VERTICAL), SetFill(0, 1),
748  NWidget(NWID_VSCROLLBAR, COLOUR_BROWN, WID_SB_SCROLLBAR),
749  NWidget(WWT_RESIZEBOX, COLOUR_BROWN),
750  EndContainer(),
751  EndContainer(),
752 };
753 
754 static WindowDesc _story_book_desc(
755  WDP_CENTER, "view_story", 400, 300,
757  0,
758  _nested_story_book_widgets, lengthof(_nested_story_book_widgets)
759 );
760 
766 void ShowStoryBook(CompanyID company, uint16 page_id)
767 {
768  if (!Company::IsValidID(company)) company = (CompanyID)INVALID_COMPANY;
769 
770  StoryBookWindow *w = AllocateWindowDescFront<StoryBookWindow>(&_story_book_desc, company, true);
771  if (page_id != INVALID_STORY_PAGE) w->SetSelectedPage(page_id);
772 }