Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Experiment: Add a GUI-providing plugin #562

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
28 changes: 20 additions & 8 deletions envisage/examples/demo/GUI_Application/traitsui_gui_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@

""" A simple example of a GUIApplication which wraps a TraitsUI """

from pyface.api import SplashScreen
from traits.api import Enum, HasTraits, Instance, Int, on_trait_change, Str
from traitsui.api import Item, OKCancelButtons, View
from traitsui.api import Item, View

from envisage.api import Plugin
from envisage.ui.gui_application import GUIApplication
from envisage.api import CorePlugin, Plugin
from envisage.ui.api import GUIApplication, GUIPlugin


class Person(HasTraits):
Expand All @@ -30,7 +31,6 @@ class Person(HasTraits):
Item("name"),
Item("age"),
Item("gender"),
buttons=OKCancelButtons,
)


Expand All @@ -51,15 +51,27 @@ def on_application_start(self):
person = Person()

# keep a reference to the ui object to avoid garbage collection
self.ui = person.edit_traits()
self.ui = person.edit_traits(kind="live")


# Application entry point.
if __name__ == "__main__":
def main():
"""Main method of the traitsui gui app example"""

# Create the application.
application = GUIApplication(
id="person_view", plugins=[PersonViewPlugin()]
id="person_view",
plugins=[
CorePlugin(),
GUIPlugin(),
PersonViewPlugin(),
],
splash_screen=SplashScreen(image="splash"),
)

# Run it!
application.run()


# Application entry point.
if __name__ == "__main__":
main()
3 changes: 2 additions & 1 deletion envisage/examples/demo/plugins/tasks/run_attractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

# Plugin imports.
from envisage.api import CorePlugin
from envisage.ui.api import GUIPlugin
from envisage.ui.tasks.api import TasksPlugin


Expand All @@ -20,7 +21,7 @@ def main(argv):
from attractors.attractors_application import AttractorsApplication
from attractors.attractors_plugin import AttractorsPlugin

plugins = [CorePlugin(), TasksPlugin(), AttractorsPlugin()]
plugins = [CorePlugin(), GUIPlugin(), TasksPlugin(), AttractorsPlugin()]
app = AttractorsApplication(plugins=plugins)
app.run()

Expand Down
17 changes: 17 additions & 0 deletions envisage/ui/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# (C) Copyright 2007-2023 Enthought, Inc., Austin, TX
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE.txt and may be redistributed only under
# the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!
"""
- :class:`~.IGUI_PROTOCOL`
- :class:`~.GUIApplication`
- :class:`~.GUIPlugin`
"""
from .gui_application import GUIApplication
from .gui_plugin import GUIPlugin
from .ids import IGUI_PROTOCOL
45 changes: 31 additions & 14 deletions envisage/ui/gui_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@

"""

from traits.api import Event, Supports
from traits.api import Event, Instance, observe

from envisage.api import Application
from envisage.ui.ids import IGUI_PROTOCOL


class GUIApplication(Application):
Expand All @@ -42,11 +43,11 @@ class GUIApplication(Application):
#### 'GUIApplication' interface #########################################

#: The Pyface GUI for the application.
gui = Supports("pyface.i_gui.IGUI")
gui = Instance(IGUI_PROTOCOL)

#: The splash screen for the application. By default, there is no splash
#: screen.
splash_screen = Supports("pyface.i_splash_screen.ISplashScreen")
splash_screen = Instance("pyface.i_splash_screen.ISplashScreen")

#### Application lifecycle events #########################################

Expand All @@ -67,25 +68,41 @@ def run(self):
veto).

"""

# Make sure the GUI has been created (so that, if required, the splash
# screen is shown).
gui = self.gui
# show the splash screen if provided
if self.splash_screen is not None:
self.splash_screen.open()

started = self.start()
if started:
gui.set_trait_later(self, "application_initialized", self)
if self.gui is None:
gui_services = self.get_services(IGUI_PROTOCOL)
if gui_services:
self.gui = gui_services[0]
else:
# fall-back if not provided by plugin
from pyface.api import GUI

self.gui = GUI()
self.gui.set_trait_later(self, "application_initialized", self)

# Start the GUI event loop. The application will block here.
gui.start_event_loop()
self.gui.start_event_loop()

self.gui = None

# clean up plugins once event loop stops
self.stop()

return started

#### Trait initializers ###################################################
#### Trait observers ######################################################

def _gui_default(self):
from pyface.api import GUI

return GUI(splash_screen=self.splash_screen)
@observe("application_initialized")
def _close_splash_screen(self, event):
"""
Once the app has started we don't need the splash screen any more.
"""
if self.splash_screen is not None:
self.splash_screen.close()
self.splash_screen.destroy()
self.splash_screen = None
88 changes: 88 additions & 0 deletions envisage/ui/gui_plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# (C) Copyright 2007-2023 Enthought, Inc., Austin, TX
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE.txt and may be redistributed only under
# the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!
"""
Envisage GUI Plugin
-------------------

This is a plugin which contributes a Pyface GUI instance to be used in
applications with a desktop UI. This does not support the splash screen
features of IGUI - those are better controlled by the application or main
script as they generally should be shown as soon as possible after startup,
often before expensive imports or operations.
"""

from traits.api import Instance, List

from envisage.ids import SERVICE_OFFERS
from envisage.plugin import Plugin
from envisage.service_offer import ServiceOffer
from envisage.ui.ids import IGUI_PROTOCOL


class GUIPlugin(Plugin):
"""A simple plugin that provides a Pyface GUI.

This class handles the life-cycle of a Pyface GUI. Applications that need
the GUI object should request the `pyface.i_gui.IGUI` interface, allowing
things like alternative event loops (eg. intergation with asyncio).
"""

#### 'GUIPlugin' interface #########################################

service_offers = List(contributes_to=SERVICE_OFFERS)

#: The Pyface GUI for the application.
_gui = Instance("pyface.i_gui.IGUI")

#### 'IPlugin' interface ##################################################

#: The plugin's unique identifier.
id = "envisage.ui.gui"

#: The plugin's name (suitable for displaying to the user).
name = "GUI Plugin"

###########################################################################
# 'IPlugin' interface.
###########################################################################

def stop(self):
self._destroy_gui()
super().stop()

###########################################################################
# Private interface.
###########################################################################

def _create_gui(self):
if self._gui is None:
from pyface.gui import GUI

self._gui = GUI()

return self._gui

def _destroy_gui(self):
if self._gui is not None:
self._gui = None

#### Trait initializers ###################################################

def _service_offers_default(self):
i_gui_service_offer = ServiceOffer(
protocol=IGUI_PROTOCOL,
factory=self._create_gui,
)
return [i_gui_service_offer]

def __gui_default(self):
from pyface.gui import GUI

return GUI()
12 changes: 12 additions & 0 deletions envisage/ui/ids.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# (C) Copyright 2007-2023 Enthought, Inc., Austin, TX
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE.txt and may be redistributed only under
# the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!

#: IGUI service protocol
IGUI_PROTOCOL = "pyface.i_gui.IGUI"
51 changes: 8 additions & 43 deletions envisage/ui/tasks/tasks_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@
Instance,
Int,
List,
observe,
Str,
Vetoable,
)
from traits.etsconfig.api import ETSConfig

# Enthought library imports.
from envisage.api import Application, ExtensionPoint
from envisage.api import ExtensionPoint
from envisage.ui.api import GUIApplication

# Logging.
logger = logging.getLogger(__name__)
Expand All @@ -36,7 +38,7 @@
DEFAULT_STATE_FILENAME = "application_memento"


class TasksApplication(Application):
class TasksApplication(GUIApplication):
"""The entry point for an Envisage Tasks application.

This class handles the common case for Tasks applications and is
Expand All @@ -58,20 +60,13 @@ class TasksApplication(Application):
#: The active task window (the last one to get focus).
active_window = Instance("envisage.ui.tasks.task_window.TaskWindow")

#: The Pyface GUI for the application.
gui = Instance("pyface.i_gui.IGUI")

#: Icon for the whole application. Will be used to override all taskWindows
#: icons to have the same.
icon = Instance("pyface.image_resource.ImageResource", allow_none=True)

#: The name of the application (also used on window title bars).
name = Str

#: The splash screen for the application. By default, there is no splash
#: screen.
splash_screen = Instance("pyface.splash_screen.SplashScreen")

#: The directory on the local file system used to persist window layout
#: information.
state_location = Directory
Expand Down Expand Up @@ -152,35 +147,6 @@ class TasksApplication(Application):
"envisage.ui.tasks.tasks_application.TasksApplicationState"
)

###########################################################################
# 'IApplication' interface.
###########################################################################

def run(self):
"""Run the application.

Returns
-------
bool
Whether the application started successfully (i.e., without a
veto).
"""
# Make sure the GUI has been created (so that, if required, the splash
# screen is shown).
gui = self.gui

started = self.start()
if started:
# Create windows from the default or saved application layout.
self._create_windows()

# Start the GUI event loop.
gui.set_trait_later(self, "application_initialized", self)
gui.start_event_loop()
self.stop()

return started

###########################################################################
# 'TasksApplication' interface.
###########################################################################
Expand Down Expand Up @@ -462,11 +428,6 @@ def _default_layout_default(self):
window_layout.items = [self.task_factories[0].id]
return [window_layout]

def _gui_default(self):
from pyface.gui import GUI

return GUI(splash_screen=self.splash_screen)

def _state_location_default(self):
state_location = os.path.join(self.home, "tasks", ETSConfig.toolkit)
logger.debug("Tasks state location is %s", state_location)
Expand All @@ -475,6 +436,10 @@ def _state_location_default(self):

#### Trait change handlers ################################################

@observe("application_initialized")
def _show_initial_windows(self, event):
self._create_windows()

def _on_window_activated(self, window, trait_name, event):
self.active_window = window

Expand Down
1 change: 1 addition & 0 deletions envisage/ui/tasks/tasks_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!

# Enthought library imports.
from traits.api import Callable, Instance, List

Expand Down