Package horizons :: Package gui :: Package widgets :: Module logbook
[hide private]
[frames] | no frames]

Source Code for Module horizons.gui.widgets.logbook

  1  # ################################################### 
  2  # Copyright (C) 2008-2017 The Unknown Horizons Team 
  3  # team@unknown-horizons.org 
  4  # This file is part of Unknown Horizons. 
  5  # 
  6  # Unknown Horizons is free software; you can redistribute it and/or modify 
  7  # it under the terms of the GNU General Public License as published by 
  8  # the Free Software Foundation; either version 2 of the License, or 
  9  # (at your option) any later version. 
 10  # 
 11  # This program is distributed in the hope that it will be useful, 
 12  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
 13  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 14  # GNU General Public License for more details. 
 15  # 
 16  # You should have received a copy of the GNU General Public License 
 17  # along with this program; if not, write to the 
 18  # Free Software Foundation, Inc., 
 19  # 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA 
 20  # ################################################### 
 21   
 22  import json 
 23  import logging 
 24  from itertools import groupby 
 25   
 26  from fife import fife 
 27  from fife.extensions.pychan.exceptions import InitializationError 
 28  from fife.extensions.pychan.widgets import HBox, Icon, Label 
 29   
 30  from horizons.command.game import UnPauseCommand 
 31  from horizons.command.misc import Chat 
 32  from horizons.component.ambientsoundcomponent import AmbientSoundComponent 
 33  from horizons.gui.widgets.imagebutton import OkButton 
 34  from horizons.gui.widgets.pickbeltwidget import PickBeltWidget 
 35  from horizons.gui.windows import Window 
 36  from horizons.i18n import gettext as T, gettext_lazy as LazyT 
 37  from horizons.scenario.actions import show_message 
 38  from horizons.util.python.callback import Callback 
 39   
 40   
41 -class LogBook(PickBeltWidget, Window):
42 """Implementation of the logbook as described here: 43 https://github.com/unknown-horizons/unknown-horizons/wiki/Message-System 44 45 It displays longer messages, which are essential for scenarios. 46 Headings can be specified for each entry. 47 """ 48 log = logging.getLogger('gui.widgets.logbook') 49 50 widget_xml = 'captains_log.xml' 51 page_pos = (170, 38) 52 sections = (('logbook', LazyT('Logbook')), 53 ('statistics', LazyT('Statistics')), 54 ('chat_overview', LazyT('Chat'))) 55
56 - def __init__(self, session, windows):
57 self.statistics_index = [i for i, sec in self.sections].index('statistics') 58 self.logbook_index = [i for i, sec in self.sections].index('logbook') 59 self._page_ids = {} # dict mapping self._cur_entry to message.msgcount 60 super().__init__() 61 self.session = session 62 self._windows = windows 63 self._parameters = [] # list of lists of all parameters added to a logbook page 64 self._message_log = [] # list of all messages that have been displayed 65 self._messages_to_display = [] # list messages to display on page close 66 self._displayed_messages = [] # list of messages that were already displayed 67 self._cur_entry = 0 # remember current location; 0 to len(messages)-1 68 self._hiding_widget = False # True if and only if the widget is currently in the process of being hidden 69 self.stats_visible = None 70 self.last_stats_widget = 'players' 71 self.current_mode = 0 # used to determine if the logbook is on the statistics page 72 self._init_gui()
73 74 # self.add_captainslog_entry([ 75 # ['Headline', "Heading"], 76 # ['Image', "content/gui/images/background/hr.png"], 77 # ['Label', "Welcome to the Captain's log"], 78 # ['Label', "\n\n"], 79 # ]) # test code 80
81 - def _init_gui(self):
82 """Initial gui setup for all subpages accessible through pickbelts.""" 83 self._gui = self.get_widget() 84 self._gui.mapEvents({ 85 OkButton.DEFAULT_NAME: self._windows.close, 86 'backwardButton': Callback(self._scroll, -2), 87 'forwardButton': Callback(self._scroll, 2), 88 'stats_players': Callback(self.show_statswidget, widget='players'), 89 'stats_settlements': Callback(self.show_statswidget, widget='settlements'), 90 'stats_ships': Callback(self.show_statswidget, widget='ships'), 91 'chatTextField': self._send_chat_message, 92 }) 93 94 # stuff in the game message / chat history subwidget 95 self.textfield = self._gui.findChild(name="chatTextField") 96 self.textfield.capture(self._chatfield_onfocus, 'mouseReleased', 'default') 97 self.chatbox = self._gui.findChild(name="chatbox") 98 self.messagebox = self._gui.findChild(name="game_messagebox") 99 #self._display_chat_history() # initially print all loaded messages 100 #self._display_message_history() 101 102 # these buttons flip pages in the captain's log if there are more than two 103 self.backward_button = self._gui.findChild(name="backwardButton") 104 self.forward_button = self._gui.findChild(name="forwardButton") 105 self._redraw_captainslog()
106
107 - def update_view(self, number=0):
108 """ update_view from PickBeltWidget, cleaning up the logbook subwidgets 109 """ 110 self.current_mode = number 111 112 # self.session might not exist yet during callback setup for pickbelts 113 if hasattr(self, 'session'): 114 self._hide_statswidgets() 115 if self.statistics_index == number: 116 self.show_statswidget(self.last_stats_widget) 117 super().update_view(number)
118
119 - def save(self, db):
120 db("INSERT INTO logbook(widgets) VALUES(?)", json.dumps(self._parameters)) 121 for message in self._message_log: 122 db("INSERT INTO logbook_messages(message) VALUES(?)", message) 123 db("INSERT INTO metadata(name, value) VALUES(?, ?)", 124 "logbook_cur_entry", self._cur_entry)
125
126 - def load(self, db):
127 db_data = db("SELECT widgets FROM logbook") 128 widget_list = json.loads(db_data[0][0] if db_data else "[]") 129 for widgets in widget_list: 130 self.add_captainslog_entry(widgets, show_logbook=False) 131 132 for msg in db("SELECT message FROM logbook_messages"): 133 self._message_log.append(msg[0]) # each line of the table is one tuple 134 # wipe self._messages_to_display on load, otherwise all previous messages get displayed 135 self._messages_to_display = [] 136 self._displayed_messages = [] 137 138 value = db('SELECT value FROM metadata WHERE name = "logbook_cur_entry"') 139 if (value and value[0] and value[0][0]): 140 self.set_cur_entry(int(value[0][0])) # this also redraws 141 142 self.display_messages()
143
144 - def show(self, msg_id=None):
145 if not hasattr(self, '_gui'): 146 self._init_gui() 147 if msg_id: 148 self._cur_entry = self._page_ids[msg_id] 149 if not self.is_visible(): 150 self._gui.show() 151 self._redraw_captainslog() 152 if self.current_mode == self.statistics_index: 153 self.show_statswidget(self.last_stats_widget)
154
155 - def display_messages(self):
156 """Display all messages in self._messages_to_display and map the to the current logbook page""" 157 for message in self._messages_to_display: 158 if message in self._displayed_messages: 159 continue 160 for msg_id in show_message(self.session, "logbook", message): 161 self._page_ids[msg_id] = self._cur_entry 162 self._displayed_messages.append(message)
163
164 - def hide(self):
165 if not self._hiding_widget: 166 self._hiding_widget = True 167 self._hide_statswidgets() 168 self._gui.hide() 169 self._hiding_widget = False 170 171 self.display_messages() 172 self._message_log.extend(self._messages_to_display) 173 self._messages_to_display = [] 174 # Make sure the game is unpaused always and in any case 175 UnPauseCommand(suggestion=False).execute(self.session)
176
177 - def is_visible(self):
178 return hasattr(self, '_gui') and self._gui.isVisible()
179
180 - def _redraw_captainslog(self):
181 """Redraws gui. Necessary when current message has changed.""" 182 if self._parameters: # there is something to display if this has items 183 self._display_parameters_on_page(self._parameters[self._cur_entry], 'left') 184 if self._cur_entry + 1 < len(self._parameters): # check for content on right page 185 self._display_parameters_on_page(self._parameters[self._cur_entry + 1], 'right') 186 else: 187 self._display_parameters_on_page([], 'right') # display empty page 188 else: 189 self._display_parameters_on_page([ 190 ['Headline', T("Emptiness")], 191 ['Image', "content/gui/images/background/hr.png"], 192 ['Label', "\n\n"], 193 ['Label', T('There is nothing written in your logbook yet!')], 194 ], 'left') 195 self.backward_button.set_active() 196 self.forward_button.set_active() 197 if not self._parameters or self._cur_entry == 0: 198 self.backward_button.set_inactive() 199 if not self._parameters or self._cur_entry >= len(self._parameters) - 2: 200 self.forward_button.set_inactive() 201 self._gui.adaptLayout()
202 203 204 ######## 205 # LOGBOOK SUBWIDGET 206 ######## 207
208 - def parse_logbook_item(self, parameter):
209 # Some error checking for widgets that are to be loaded. 210 # This happens, for example, with outdated YAML stored in old 211 # scenario savegames. Instead of crashing, display nothing. 212 def _icon(image): 213 try: 214 # Pychan can only use str objects as file path. 215 # json.loads() however returns unicode. 216 return Icon(image=str(image)) 217 except fife.NotFound: 218 return None
219 220 def _label(text, font='default'): 221 try: 222 return Label(text=str(text), wrap_text=True, 223 min_size=(325, 0), max_size=(325, 1024), 224 font=font) 225 except InitializationError: 226 return None
227 228 if parameter and parameter[0]: # allow empty Labels 229 parameter_type = parameter[0] 230 if isinstance(parameter, str): 231 add = _label(parameter) 232 elif parameter_type == 'Label': 233 add = _label(parameter[1]) 234 elif parameter_type == 'Image': 235 add = _icon(parameter[1]) 236 elif parameter_type == 'Gallery': 237 add = HBox() 238 for image in parameter[1]: 239 new_icon = _icon(image) 240 if new_icon is not None: 241 add.addChild(new_icon) 242 elif parameter_type == 'Headline': 243 add = HBox() 244 is_not_last_headline = self._parameters and self._cur_entry is not None and self._cur_entry < (len(self._parameters) - 2) 245 if is_not_last_headline: 246 add.addChild(_icon("content/gui/images/tabwidget/done.png")) 247 add.addChild(_label(parameter[1], font='headline')) 248 elif parameter_type == 'BoldLabel': 249 add = _label(parameter[1], font='default_bold') 250 elif parameter_type == 'Message': 251 add = None 252 # parameters are re-read on page reload. 253 # duplicate_message stops messages from 254 # being duplicated on page reload. 255 message = parameter[1] 256 # message is already going to be displayed or has been displayed 257 # before (e.g. re-opening older logbook pages) 258 duplicate_message = (message in self._messages_to_display or 259 message in self._message_log) 260 261 if not duplicate_message: 262 self._messages_to_display.append(message) # the new message has not been displayed 263 else: 264 self.log.warning('Unknown parameter type %s in parameter %s', 265 parameter[0], parameter) 266 add = None 267 return add 268
269 - def _display_parameters_on_page(self, parameters, page):
270 """ 271 @param parameters: parameter list, cf. docstring of add_captainslog_entry 272 @param page: 'left' or 'right' 273 """ 274 parameterbox = self._gui.findChild(name="custom_widgets_{page}".format(page=page)) 275 parameterbox.removeAllChildren() 276 for parameter_definition in parameters: 277 add = self.parse_logbook_item(parameter_definition) 278 if add is not None: 279 parameterbox.addChild(add)
280
281 - def add_captainslog_entry(self, parameters, show_logbook=True):
282 """Adds an entry to the logbook VBoxes consisting of a parameter list. 283 Check e.g. content/scenarios/tutorial_en.yaml for real-life usage. 284 285 @param parameters: Each item in here is a list like the following: 286 [Label, "Awesome text to be displayed as a label"] 287 "Shortcut notation for a Label" 288 [Headline, "Label to be styled as headline (in small caps)"] 289 [BoldLabel, "Like Label but with bold font, use to highlight lines"] 290 [Image, "content/gui/images/path/to/the/file.png"] 291 [Gallery, ["/path/1.png", "/path/file.png", "/file/3.png"]] 292 [Message, "Text to display as a notification on logbook close"] 293 [Pagebreak] 294 """ 295 #TODO last line of message text sometimes get eaten. Ticket #535 296 def _split_on_pagebreaks(parameters): 297 """This black magic splits the parameter list on each ['Pagebreak'] 298 >> [['a','a'], ['b','b'], ['Pagebreak'], ['c','c'], ['d','d']] 299 >>>> into [[['a', 'a'], ['b', 'b']], [['c', 'c'], ['d', 'd']]] 300 #TODO n successive pagebreaks should insert (n-1) blank pages (currently 0 are inserted) 301 """ 302 return [list(l[1]) for l in groupby(parameters, lambda x: x != ['Pagebreak']) if l[0]]
303 304 # If a scenario goal has been completed, remove the corresponding message 305 for message in self._displayed_messages: 306 self.session.ingame_gui.message_widget.remove(message) 307 308 self._displayed_messages = [] # Reset displayed messages 309 for parameter_list in _split_on_pagebreaks(parameters): 310 self._parameters.append(parameter_list) 311 for parameter_definition in parameter_list: 312 self.parse_logbook_item(parameter_definition) 313 # if a new entry contains more than one page, we want to display the first 314 # unread message. note that len(parameters) starts at 1 and _cur_entry at 0. 315 # position always refers to the left page, so only multiples of 2 are valid 316 len_old = len(self._parameters) - len(_split_on_pagebreaks(parameters)) 317 if len_old % 2 == 1: # uneven amount => empty last page, space for 1 new 318 self._cur_entry = len_old - 1 319 else: # even amount => all pages filled. we could display two new messages 320 self._cur_entry = len_old 321 if show_logbook and hasattr(self, "_gui"): 322 self._redraw_captainslog() 323 self._windows.open(self) 324 self.show_logbookwidget() 325
326 - def clear(self):
327 """Remove all entries""" 328 self._parameters = [] 329 self._cur_entry = 0
330
331 - def get_cur_entry(self):
332 return self._cur_entry
333
334 - def set_cur_entry(self, cur_entry):
335 if cur_entry < 0 or (cur_entry >= len(self._parameters) and len(self._parameters) != 0): 336 raise ValueError("ERROR: Logbook entry out of Logbook bounds. This should never happen.") 337 self._cur_entry = cur_entry 338 self._redraw_captainslog()
339
340 - def _scroll(self, direction):
341 """Scroll back or forth one message. 342 @param direction: -1 or 1""" 343 if not self._parameters: 344 return 345 new_cur = self._cur_entry + direction 346 if new_cur < 0 or new_cur >= len(self._parameters): 347 return # invalid scroll 348 self._cur_entry = new_cur 349 AmbientSoundComponent.play_special('flippage') 350 self._redraw_captainslog()
351
352 - def show_logbookwidget(self):
353 """Shows logbook with Logbook page selected""" 354 if self.current_mode != self.logbook_index: 355 self.update_view(self.logbook_index)
356 357 ######## 358 # STATISTICS SUBWIDGET 359 ######## 360 # 361 #TODO list: 362 # [ ] Extract this stuff to extra widget class that properly handles all the 363 # hide and save calls 364 # [ ] fix stats show/hide mess: how is update_view called before self.__init__ 365 # [ ] save last shown stats widget and re-show it when clicking on Statistics 366 # [ ] semantic distinction between general widget and subwidgets (log, stats) 367 # 368 ######## 369
370 - def show_statswidget(self, widget='players'):
371 """Shows logbook with Statistics page selected""" 372 if self.current_mode != self.statistics_index: 373 self.update_view(self.statistics_index) 374 self._hide_statswidgets() 375 if widget: 376 getattr(self, '_show_{widget}'.format(widget=widget))() 377 self.stats_visible = widget 378 self.last_stats_widget = widget
379
380 - def toggle_stats_visibility(self, widget='players'):
381 """ 382 Only hides logbook if hotkey of current stats selection pressed. 383 Otherwise, switch to displaying the new widget instead of hiding. 384 @param widget: 'players' or 'settlements' or 'ships' 385 """ 386 # we're treating every statswidget as a separate window, so if the stats change, 387 # close the logbook and reopen it with a different active widget 388 if self.stats_visible != widget: 389 if self.stats_visible: 390 self._windows.close() 391 392 self._windows.open(self) 393 self.show_statswidget(widget=widget) 394 else: 395 self._windows.close()
396
397 - def _show_ships(self):
398 self.session.ingame_gui.players_ships.show()
399
400 - def _show_settlements(self):
401 self.session.ingame_gui.players_settlements.show()
402
403 - def _show_players(self):
404 self.session.ingame_gui.players_overview.show()
405
406 - def _hide_statswidgets(self):
407 statswidgets = [ 408 self.session.ingame_gui.players_overview, 409 self.session.ingame_gui.players_ships, 410 self.session.ingame_gui.players_settlements, 411 ] 412 for statswidget in statswidgets: 413 # we don't care which one is shown currently (if any), just hide all of them 414 statswidget.hide() 415 self.stats_visible = None
416 417 418 ######## 419 # MESSAGE AND CHAT HISTORY SUBWIDGET 420 ######## 421 # 422 #TODO list: 423 # [ ] use message bus to check for new updates 424 # [ ] only display new message on update, not reload whole history 425 # [x] update message history on new game messages. not on sending a chat line 426 # [ ] implement word wrapping for message history display 427 # 428 ######## 429
430 - def _send_chat_message(self):
431 """Sends a chat message. Called when user presses enter in the input field""" 432 msg = self.textfield.text 433 if msg: 434 Chat(msg).execute(self.session) 435 self.textfield.text = '' 436 self._display_chat_history()
437
438 - def display_message_history(self):
439 self.messagebox.items = [] 440 messages = self.session.ingame_gui.message_widget.active_messages + \ 441 self.session.ingame_gui.message_widget.archive 442 for msg in sorted(messages, key=lambda m: m.created): 443 if msg.id != 'CHAT': # those get displayed in the chat window instead 444 self.messagebox.items.append(msg.message) 445 self.messagebox.selected = len(self.messagebox.items) - 1 # scroll to bottom
446
447 - def _display_chat_history(self):
448 self.chatbox.items = [] 449 messages = self.session.ingame_gui.message_widget.chat 450 for msg in sorted(messages, key=lambda m: m.created): 451 self.chatbox.items.append(msg.message) 452 self.chatbox.selected = len(self.chatbox.items) - 1 # scroll to bottom
453
454 - def _chatfield_onfocus(self):
455 """Removes text in chat input field when it gets focused.""" 456 self.textfield.text = '' 457 self.textfield.capture(None, 'mouseReleased', 'default')
458