1# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX 2# All rights reserved. 3# 4# This software is provided without warranty under the terms of the BSD 5# license included in LICENSE.txt and may be redistributed only 6# under the conditions described in the aforementioned license. The license 7# is also available online at http://www.enthought.com/licenses/BSD.txt 8# Thanks for using Enthought open source! 9# Standard library imports. 10import logging 11import os.path 12 13# Enthought library imports. 14from envisage.api import Application, ExtensionPoint 15from traits.api import ( 16 Bool, Callable, Directory, Event, HasStrictTraits, 17 Instance, Int, List, Unicode, Vetoable) 18from traits.etsconfig.api import ETSConfig 19 20# Local imports 21from envisage._compat import pickle, STRING_BASE_CLASS 22 23 24# Logging. 25logger = logging.getLogger(__name__) 26 27#: Default filename for saving layout information 28DEFAULT_STATE_FILENAME = "application_memento" 29 30 31class TasksApplication(Application): 32 """The entry point for an Envisage Tasks application. 33 34 This class handles the common case for Tasks applications and is 35 intended to be subclassed to modify its start/stop behavior, etc. 36 37 """ 38 39 # Extension point IDs. 40 TASK_FACTORIES = 'envisage.ui.tasks.tasks' 41 TASK_EXTENSIONS = 'envisage.ui.tasks.task_extensions' 42 43 # Pickle protocol to use for persisting layout information. Subclasses may 44 # want to increase this, depending on their compatibility needs. Protocol 45 # version 2 is safe for Python >= 2.3. Protocol version 4 is safe for 46 # Python >= 3.4. 47 layout_save_protocol = Int(2) 48 49 #### 'TasksApplication' interface ######################################### 50 51 # The active task window (the last one to get focus). 52 active_window = Instance('envisage.ui.tasks.task_window.TaskWindow') 53 54 # The Pyface GUI for the application. 55 gui = Instance('pyface.gui.GUI') 56 57 # Icon for the whole application. Will be used to override all taskWindows 58 # icons to have the same. 59 icon = Instance('pyface.image_resource.ImageResource', allow_none=True) 60 61 # The name of the application (also used on window title bars). 62 name = Unicode 63 64 # The splash screen for the application. By default, there is no splash 65 # screen. 66 splash_screen = Instance('pyface.splash_screen.SplashScreen') 67 68 # The directory on the local file system used to persist window layout 69 # information. 70 state_location = Directory 71 72 # The filename that the application uses to persist window layout 73 # information. 74 state_filename = Unicode(DEFAULT_STATE_FILENAME) 75 76 # Contributed task factories. This attribute is primarily for run-time 77 # inspection; to instantiate a task, use the 'create_task' method. 78 task_factories = ExtensionPoint(id=TASK_FACTORIES) 79 80 # Contributed task extensions. 81 task_extensions = ExtensionPoint(id=TASK_EXTENSIONS) 82 83 # The list of task windows created by the application. 84 windows = List(Instance('envisage.ui.tasks.task_window.TaskWindow')) 85 86 # The factory for creating task windows. 87 window_factory = Callable 88 89 #### Application layout ################################################### 90 91 # The default layout for the application. If not specified, a single window 92 # will be created with the first available task factory. 93 default_layout = List( 94 Instance('pyface.tasks.task_window_layout.TaskWindowLayout')) 95 96 # Whether to always apply the default *application level* layout when the 97 # application is started. Even if this is True, the layout state of 98 # individual tasks will be restored. 99 always_use_default_layout = Bool(False) 100 101 #### Application lifecycle events ######################################### 102 103 # Fired after the initial windows have been created and the GUI event loop 104 # has been started. 105 application_initialized = Event 106 107 # Fired immediately before the extant windows are destroyed and the GUI 108 # event loop is terminated. 109 application_exiting = Event 110 111 # Fired when a task window has been created. 112 window_created = Event( 113 Instance('envisage.ui.tasks.task_window_event.TaskWindowEvent')) 114 115 # Fired when a task window is opening. 116 window_opening = Event( 117 Instance( 118 'envisage.ui.tasks.task_window_event.VetoableTaskWindowEvent')) 119 120 # Fired when a task window has been opened. 121 window_opened = Event( 122 Instance('envisage.ui.tasks.task_window_event.TaskWindowEvent')) 123 124 # Fired when a task window is closing. 125 window_closing = Event( 126 Instance( 127 'envisage.ui.tasks.task_window_event.VetoableTaskWindowEvent')) 128 129 # Fired when a task window has been closed. 130 window_closed = Event( 131 Instance('envisage.ui.tasks.task_window_event.TaskWindowEvent')) 132 133 #### Protected interface ################################################## 134 135 # An 'explicit' exit is when the the 'exit' method is called. 136 # An 'implicit' exit is when the user closes the last open window. 137 _explicit_exit = Bool(False) 138 139 # Application state. 140 _state = Instance( 141 'envisage.ui.tasks.tasks_application.TasksApplicationState') 142 143 ########################################################################### 144 # 'IApplication' interface. 145 ########################################################################### 146 147 def run(self): 148 """ Run the application. 149 150 Returns 151 ------- 152 bool 153 Whether the application started successfully (i.e., without a 154 veto). 155 """ 156 # Make sure the GUI has been created (so that, if required, the splash 157 # screen is shown). 158 gui = self.gui 159 160 started = self.start() 161 if started: 162 # Create windows from the default or saved application layout. 163 self._create_windows() 164 165 # Start the GUI event loop. 166 gui.set_trait_later(self, 'application_initialized', self) 167 gui.start_event_loop() 168 169 return started 170 171 ########################################################################### 172 # 'TasksApplication' interface. 173 ########################################################################### 174 175 def create_task(self, id): 176 """ Creates the Task with the specified ID. 177 178 Returns 179 ------- 180 pyface.tasks.task.Task 181 The new Task, or None if there is not a suitable TaskFactory. 182 """ 183 # Get the factory for the task. 184 factory = self._get_task_factory(id) 185 if factory is None: 186 return None 187 188 # Create the task using suitable task extensions. 189 extensions = [ext for ext in self.task_extensions 190 if ext.task_id == id or not ext.task_id] 191 task = factory.create_with_extensions(extensions) 192 task.id = factory.id 193 return task 194 195 def create_window(self, layout=None, restore=True, **traits): 196 """Creates a new TaskWindow, possibly with some Tasks. 197 198 Parameters 199 ---------- 200 layout : TaskWindowLayout, optional 201 The layout to use for the window. The tasks described in 202 the layout will be created and added to the window 203 automatically. If not specified, the window will contain 204 no tasks. 205 206 restore : bool, optional (default True) 207 If set, the application will restore old size and 208 positions for the window and its panes, if possible. If a 209 layout is not provided, this parameter has no effect. 210 211 **traits : dict, optional 212 Additional parameters to pass to ``window_factory()`` 213 when creating the TaskWindow. 214 215 Returns 216 ------- 217 envisage.ui.tasks.task_window.TaskWindow 218 The new TaskWindow. 219 220 """ 221 from .task_window_event import TaskWindowEvent 222 from pyface.tasks.task_window_layout import TaskWindowLayout 223 224 window = self.window_factory(application=self, **traits) 225 226 # Listen for the window events. 227 window.on_trait_change(self._on_window_activated, 'activated') 228 window.on_trait_change(self._on_window_opening, 'opening') 229 window.on_trait_change(self._on_window_opened, 'opened') 230 window.on_trait_change(self._on_window_closing, 'closing') 231 window.on_trait_change(self._on_window_closed, 'closed') 232 233 # Event notification. 234 self.window_created = TaskWindowEvent(window=window) 235 236 if layout: 237 # Create and add tasks. 238 for task_id in layout.get_tasks(): 239 task = self.create_task(task_id) 240 if task: 241 window.add_task(task) 242 else: 243 logger.error( 244 'Missing factory for task with ID %r', task_id) 245 246 # Apply a suitable layout. 247 if restore: 248 layout = self._restore_layout_from_state(layout) 249 else: 250 # Create an empty layout to set default size and position only 251 layout = TaskWindowLayout() 252 253 window.set_window_layout(layout) 254 255 return window 256 257 def exit(self, force=False): 258 """Exits the application, closing all open task windows. 259 260 Each window is sent a veto-able closing event. If any window vetoes the 261 close request, no window will be closed. Otherwise, all windows will be 262 closed and the GUI event loop will terminate. 263 264 This method is not called when the user clicks the close 265 button on a window or otherwise closes a window through his or 266 her window manager. It is only called via the File->Exit menu 267 item. It can also, of course, be called programatically. 268 269 Parameters 270 ---------- 271 force : bool, optional (default False) 272 If set, windows will receive no closing events and will be 273 destroyed unconditionally. This can be useful for reliably 274 tearing down regression tests, but should be used with 275 caution. 276 277 Returns 278 ------- 279 bool 280 A boolean indicating whether the application exited. 281 282 """ 283 self._explicit_exit = True 284 try: 285 if not force: 286 for window in reversed(self.windows): 287 window.closing = event = Vetoable() 288 if event.veto: 289 return False 290 291 self._prepare_exit() 292 for window in reversed(self.windows): 293 window.destroy() 294 window.closed = True 295 finally: 296 self._explicit_exit = False 297 return True 298 299 ########################################################################### 300 # Protected interface. 301 ########################################################################### 302 303 def _create_windows(self): 304 """ Called at startup to create TaskWindows from the default or saved 305 application layout. 306 """ 307 # Build a list of TaskWindowLayouts. 308 self._load_state() 309 if (self.always_use_default_layout or 310 not self._state.previous_window_layouts): 311 window_layouts = self.default_layout 312 else: 313 # Choose the stored TaskWindowLayouts, but only if all the task IDs 314 # are still valid. 315 window_layouts = self._state.previous_window_layouts 316 for layout in window_layouts: 317 for task_id in layout.get_tasks(): 318 if not self._get_task_factory(task_id): 319 logger.warning('Saved application layout references ' 320 'non-existent task %r. Falling back to ' 321 'default application layout.' % task_id) 322 window_layouts = self.default_layout 323 break 324 else: 325 continue 326 break 327 328 # Create a TaskWindow for each TaskWindowLayout. 329 for window_layout in window_layouts: 330 if self.always_use_default_layout: 331 window = self.create_window(window_layout, restore=False) 332 else: 333 window = self.create_window(window_layout, restore=True) 334 window.open() 335 336 def _get_task_factory(self, id): 337 """ Returns the TaskFactory with the specified ID, or None. 338 """ 339 for factory in self.task_factories: 340 if factory.id == id: 341 return factory 342 return None 343 344 def _prepare_exit(self): 345 """ Called immediately before the extant windows are destroyed and the 346 GUI event loop is terminated. 347 """ 348 self.application_exiting = self 349 self._save_state() 350 351 def _load_state(self): 352 """ Loads saved application state, if possible. 353 """ 354 state = TasksApplicationState() 355 filename = os.path.join(self.state_location, self.state_filename) 356 if os.path.exists(filename): 357 # Attempt to unpickle the saved application state. 358 logger.debug('Loading application state from %s', filename) 359 try: 360 with open(filename, 'rb') as f: 361 restored_state = pickle.load(f) 362 except Exception: 363 # If anything goes wrong, log the error and continue. 364 logger.exception('Error while restoring application state') 365 else: 366 if state.version == restored_state.version: 367 state = restored_state 368 logger.debug('Application state successfully restored') 369 else: 370 logger.warning( 371 'Discarding outdated application state: ' 372 'expected version %s, got version %s', 373 state.version, restored_state.version) 374 else: 375 logger.debug("No saved application state found at %s", filename) 376 377 self._state = state 378 379 def _restore_layout_from_state(self, layout): 380 """ Restores an equivalent layout from saved application state. 381 """ 382 # First, see if a window layout matches exactly. 383 match = self._state.get_equivalent_window_layout(layout) 384 if match: 385 # The active task is not part of the equivalency relation, so we 386 # ensure that it is correct. 387 match.active_task = layout.get_active_task() 388 layout = match 389 390 # If that fails, at least try to restore the layout of 391 # individual tasks. 392 else: 393 layout = layout.clone_traits() 394 for i, item in enumerate(layout.items): 395 id = item if isinstance(item, STRING_BASE_CLASS) else item.id 396 match = self._state.get_task_layout(id) 397 if match: 398 layout.items[i] = match 399 400 return layout 401 402 def _save_state(self): 403 """ Saves the application state. 404 """ 405 # Grab the current window layouts. 406 window_layouts = [w.get_window_layout() for w in self.windows] 407 self._state.previous_window_layouts = window_layouts 408 409 # Attempt to pickle the application state. 410 filename = os.path.join(self.state_location, self.state_filename) 411 logger.debug('Saving application state to %s', filename) 412 try: 413 with open(filename, 'wb') as f: 414 pickle.dump(self._state, f, protocol=self.layout_save_protocol) 415 except Exception: 416 # If anything goes wrong, log the error and continue. 417 logger.exception('Error while saving application state') 418 else: 419 logger.debug('Application state successfully saved') 420 421 #### Trait initializers ################################################### 422 423 def _window_factory_default(self): 424 from envisage.ui.tasks.task_window import TaskWindow 425 return TaskWindow 426 427 def _default_layout_default(self): 428 from pyface.tasks.task_window_layout import TaskWindowLayout 429 window_layout = TaskWindowLayout() 430 if self.task_factories: 431 window_layout.items = [self.task_factories[0].id] 432 return [window_layout] 433 434 def _gui_default(self): 435 from pyface.gui import GUI 436 return GUI(splash_screen=self.splash_screen) 437 438 def _state_location_default(self): 439 state_location = os.path.join(ETSConfig.application_home, 440 'tasks', ETSConfig.toolkit) 441 if not os.path.exists(state_location): 442 os.makedirs(state_location) 443 444 logger.debug('Tasks state location is %s', state_location) 445 446 return state_location 447 448 #### Trait change handlers ################################################ 449 450 def _on_window_activated(self, window, trait_name, event): 451 self.active_window = window 452 453 def _on_window_opening(self, window, trait_name, event): 454 from .task_window_event import VetoableTaskWindowEvent 455 # Event notification. 456 self.window_opening = window_event = VetoableTaskWindowEvent( 457 window=window) 458 459 if window_event.veto: 460 event.veto = True 461 462 def _on_window_opened(self, window, trait_name, event): 463 from .task_window_event import TaskWindowEvent 464 self.windows.append(window) 465 466 # Event notification. 467 self.window_opened = TaskWindowEvent(window=window) 468 469 def _on_window_closing(self, window, trait_name, event): 470 from .task_window_event import VetoableTaskWindowEvent 471 # Event notification. 472 self.window_closing = window_event = VetoableTaskWindowEvent( 473 window=window) 474 475 if window_event.veto: 476 event.veto = True 477 else: 478 # Store the layout of the window. 479 window_layout = window.get_window_layout() 480 self._state.push_window_layout(window_layout) 481 482 # If we're exiting implicitly and this is the last window, save 483 # state, because we won't get another chance. 484 if len(self.windows) == 1 and not self._explicit_exit: 485 self._prepare_exit() 486 487 def _on_window_closed(self, window, trait_name, event): 488 from .task_window_event import TaskWindowEvent 489 self.windows.remove(window) 490 491 # Event notification. 492 self.window_closed = TaskWindowEvent(window=window) 493 494 # Was this the last window? 495 if len(self.windows) == 0: 496 self.stop() 497 498 499class TasksApplicationState(HasStrictTraits): 500 """ A class used internally by TasksApplication for saving and restoring 501 application state. 502 """ 503 504 # TaskWindowLayouts for the windows extant at application 505 # exit. Only used if 'always_use_default_layout' is disabled. 506 previous_window_layouts = List( 507 Instance('pyface.tasks.task_window_layout.TaskWindowLayout')) 508 509 # A list of TaskWindowLayouts accumulated throughout the application's 510 # lifecycle. 511 window_layouts = List( 512 Instance('pyface.tasks.task_window_layout.TaskWindowLayout')) 513 514 # The "version" for the state data. This should be incremented whenever a 515 # backwards incompatible change is made to this class or any of the layout 516 # classes. This ensures that loading application state is always safe. 517 version = Int(1) 518 519 def get_equivalent_window_layout(self, window_layout): 520 """ Gets an equivalent TaskWindowLayout, if there is one. 521 """ 522 for layout in self.window_layouts: 523 if layout.is_equivalent_to(window_layout): 524 return layout 525 return None 526 527 def get_task_layout(self, task_id): 528 """ Gets a TaskLayout with the specified ID, there is one. 529 """ 530 for window_layout in self.window_layouts: 531 for layout in window_layout.items: 532 if layout.id == task_id: 533 return layout 534 return None 535 536 def push_window_layout(self, window_layout): 537 """ Merge a TaskWindowLayout into the accumulated list. 538 """ 539 self.window_layouts = [layout for layout in self.window_layouts 540 if not layout.is_equivalent_to(window_layout)] 541 self.window_layouts.insert(0, window_layout) 542