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

Source Code for Module horizons.gui.windows

  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 logging 
 23  import traceback 
 24  from typing import Any, Dict, Optional 
 25   
 26  from fife import fife 
 27  from fife.extensions.pychan.widgets import Icon 
 28   
 29  import horizons.globals 
 30  from horizons.gui.util import load_uh_widget 
 31  from horizons.gui.widgets.imagebutton import CancelButton, OkButton 
 32  from horizons.i18n import gettext as T 
 33  from horizons.util.python.callback import Callback 
34 35 36 -class Window:
37
38 - def __init__(self, windows=None):
39 # Reference to the window manager. Use it to open new windows or close 40 # this window. 41 self._windows = windows 42 43 self._modal_background = None
44
45 - def open(self, **kwargs):
46 """Open the window. 47 48 After this call, the window should be visible. If you decide to not show 49 the window here (e.g. an error occurred), you'll need to call 50 `self._windows.close()` to remove the window from the manager. 51 52 You may override this method in a subclass if you need to do stuff when 53 a window is first shown. 54 """ 55 return self.show(**kwargs)
56
57 - def show(self, **kwargs):
58 """Show the window. 59 60 After this call, the window should be visible. You should *never* call 61 this directly in your code. 62 """ 63 raise NotImplementedError
64
65 - def hide(self):
66 """Hide the window. 67 68 After this call, the window should not be visible anymore. However, it remains 69 in the stack of open windows and will be visible once it becomes the topmost 70 window again. 71 72 You should *never* call this directly in your code, other than in `close()` 73 when you overwrote it in your subclass. 74 """ 75 raise NotImplementedError
76
77 - def close(self):
78 """Closes the window. 79 80 You should *never* call this directly in your code. Use `self._windows.close()` 81 to ask the WindowManager to remove the window instead. 82 83 You may override this method in a subclass if you need to do stuff when 84 a window is closed. 85 """ 86 self.hide()
87
88 - def on_escape(self):
89 """Define what happens when ESC is pressed. 90 91 By default, the window will be closed. 92 """ 93 self._windows.close()
94
95 - def on_return(self):
96 """Define what happens when RETURN is pressed.""" 97 pass
98
99 - def _show_modal_background(self):
100 """ 101 Loads transparent background that de facto prohibits access to other 102 gui elements by eating all input events. 103 """ 104 height = horizons.globals.fife.engine_settings.getScreenHeight() 105 width = horizons.globals.fife.engine_settings.getScreenWidth() 106 image = horizons.globals.fife.imagemanager.loadBlank(width, height) 107 image = fife.GuiImage(image) 108 self._modal_background = Icon(image=image) 109 self._modal_background.position = (0, 0) 110 self._modal_background.show()
111
112 - def _hide_modal_background(self):
113 self._modal_background.hide()
114
115 116 -class Dialog(Window):
117 """ 118 A dialog is very similar to a window, the major difference between the two 119 is that when showing a `Window`, control flow will continue immediately. 120 However the call to show a `Dialog` will only return when the dialog is 121 closed. 122 """ 123 # Whether to block user interaction while displaying the dialog 124 modal = False 125 126 # Name of widget that should get the focus once the dialog is shown 127 focus = None # type: Optional[str] 128 129 # Maps Button names to return values that you can handle in `act` 130 return_events = {} # type: Dict[str, Any] 131
132 - def __init__(self, windows):
133 super().__init__(windows) 134 135 self._gui = None 136 self._hidden = False
137
138 - def prepare(self, **kwargs):
139 """Setup the dialog gui. 140 141 The widget has to be stored in `self._gui`. If you want to abort the dialog 142 here return False. 143 """ 144 raise NotImplementedError
145
146 - def act(self, retval):
147 """Do something after dialog is closed. 148 149 If you want to show the dialog again, you need to do that explicitly, e.g. with: 150 151 self._windows.open(self) 152 """ 153 return retval
154
155 - def show(self, **kwargs):
156 # if the dialog is already running but has been hidden, just show the widget 157 if self._hidden: 158 self._hidden = False 159 if self.modal: 160 self._show_modal_background() 161 self._gui.show() 162 self._gui.requestFocus() 163 return 164 165 # if `prepare` returned False, we stop the dialog 166 if self.prepare(**kwargs) is False: 167 self._windows.close() 168 return 169 170 self._gui.capture(self._on_keypress, event_name="keyPressed") 171 172 if self.modal: 173 self._show_modal_background() 174 175 retval = self._execute() 176 177 self._windows.close() 178 return self.act(retval)
179
180 - def hide(self):
181 if self.modal: 182 self._hide_modal_background() 183 self._gui.hide() 184 self._hidden = True
185
186 - def close(self):
187 self.hide() 188 # this dialog is gone (not just hidden), next time `show` is called, 189 # we want to execute the dialog again 190 self._hidden = False
191
192 - def on_escape(self):
193 # escape is handled in `_on_keypress` 194 pass
195
196 - def _on_keypress(self, event):
197 """Intercept ESC and ENTER keys and execute the appropriate actions.""" 198 199 # Convention says use cancel action 200 if event.getKey().getValue() == fife.Key.ESCAPE: 201 self.trigger_close(CancelButton.DEFAULT_NAME) 202 # Convention says use ok action 203 elif event.getKey().getValue() == fife.Key.ENTER: 204 self.trigger_close(OkButton.DEFAULT_NAME)
205
206 - def trigger_close(self, event_name):
207 """Close the dialog and execute the received event""" 208 event_to_call = self.return_events.get(event_name) 209 if event_to_call is not None: 210 self._abort(event_to_call)
211
212 - def _abort(self, retval=False):
213 """Break out of mainloop. 214 215 Program flow continues after the `self._execute` call in `show`. 216 """ 217 horizons.globals.fife.breakLoop(retval)
218
219 - def _execute(self):
220 """Execute the dialog synchronously. 221 222 This is done by entering a new mainloop in the engine until the dialog 223 is closed (see `abort`). 224 """ 225 for name, retval in self.return_events.items(): 226 cb = Callback(self._abort, retval) 227 self._gui.findChild(name=name).capture(cb) 228 229 self._gui.show() 230 231 if self.focus: 232 self._gui.findChild(name=self.focus).requestFocus() 233 else: 234 self._gui.is_focusable = True 235 self._gui.requestFocus() 236 237 return horizons.globals.fife.loop()
238 239 @classmethod
240 - def create_from_widget(cls, dlg, bind, event_map=None, modal=False, focus=None):
241 """Shows any pychan dialog. 242 243 @param dlg: dialog that is to be shown 244 @param bind: events that make the dialog return + return values {'ok': True, 'cancel': False} 245 @param event_map: dictionary with callbacks for buttons. See pychan docu: pychan.widget.mapEvents() 246 @param modal: Whether to block user interaction while displaying the dialog 247 @param focus: Which child widget should take focus 248 """ 249 def prepare(self, **kwargs): 250 self._gui = dlg 251 if event_map: 252 self._gui.mapEvents(event_map)
253 254 TempDialog = type('TempDialog', (Dialog, ), { 255 'modal': modal, 256 'return_events': bind, 257 'focus': focus, 258 'prepare': prepare, 259 }) 260 261 return TempDialog
262 299
300 301 -class WindowManager:
302
303 - def __init__(self):
304 self._windows = []
305
306 - def open(self, window, **kwargs):
307 """Open a new window on top. 308 309 Hide the current one and show the new one. 310 Keyword arguments will be passed through to the window's `open` method. 311 """ 312 if self._windows: 313 self._windows[-1].hide() 314 315 self._windows.append(window) 316 return window.open(**kwargs)
317
318 - def close(self):
319 """Close the top window. 320 321 If there is another window left, show it. 322 """ 323 window = self._windows.pop() 324 window.close() 325 if self._windows: 326 self._windows[-1].show()
327
328 - def toggle(self, window, **kwargs):
329 """Hide window if is currently visible (and on top), show it otherwise.""" 330 if self._windows and self._windows[-1] == window: 331 self.close() 332 else: 333 if window in self._windows: 334 self._windows.remove(window) 335 if self._windows: 336 self._windows[-1].hide() 337 self._windows.append(window) 338 window.show(**kwargs) 339 else: 340 self.open(window, **kwargs)
341
342 - def on_escape(self):
343 """Let the topmost window handle an escape key event.""" 344 if not self._windows: 345 return 346 347 self._windows[-1].on_escape()
348
349 - def on_return(self):
350 """Let the topmost window handle a return key event.""" 351 if not self._windows: 352 return 353 354 self._windows[-1].on_return()
355 356 @property
357 - def visible(self):
358 """Whether any windows are visible right now.""" 359 return bool(self._windows)
360
361 - def close_all(self):
362 while self._windows: 363 w = self._windows.pop() 364 w.close()
365
366 - def hide_all(self):
367 """Hide all windows. 368 369 Use `show_all` to restore the old state. 370 """ 371 if not self._windows: 372 return 373 374 # because we only show one window at a time, it is enough to hide the 375 # top-most window 376 self._windows[-1].hide()
377
378 - def show_all(self):
379 """Undo what `hide_all` did.""" 380 if not self._windows: 381 return 382 383 # because we only show one window at a time, it is enough to show the 384 # most recently added window 385 self._windows[-1].show()
386
387 - def open_popup(self, windowtitle, message, show_cancel_button=False, size=0):
388 """ 389 @param windowtitle: the title of the popup 390 @param message: the text displayed in the popup 391 @param show_cancel_button: boolean, show cancel button or not 392 @param size: 0, 1 or 2. Larger means bigger. 393 """ 394 window = Popup(self, windowtitle, message, show_cancel_button, size) 395 return self.open(window)
396
397 - def open_error_popup(self, windowtitle, description, advice=None, details=None, _first=True):
398 """Displays a popup containing an error message. 399 @param windowtitle: title of popup, will be auto-prefixed with "Error: " 400 @param description: string to tell the user what happened 401 @param advice: how the user might be able to fix the problem 402 @param details: technical details, relevant for debugging but not for the user 403 @param _first: Don't touch this. 404 405 Guide for writing good error messages: 406 http://www.useit.com/alertbox/20010624.html 407 """ 408 msg = "" 409 msg += description + "\n" 410 if advice: 411 msg += advice + "\n" 412 if details: 413 msg += T("Details: {error_details}").format(error_details=details) 414 try: 415 self.open_popup(T("Error: {error_message}").format(error_message=windowtitle), 416 msg) 417 except SystemExit: # user really wants us to die 418 raise 419 except: 420 # could be another game error, try to be persistent in showing the error message 421 # else the game would be gone without the user being able to read the message. 422 if _first: 423 traceback.print_exc() 424 log = logging.getLogger('gui.windows') 425 log.error('Exception while showing error, retrying once more.') 426 return self.open_error_popup(windowtitle, description, advice, details, _first=False) 427 else: 428 raise # it persists, we have to die.
429