diff --git a/examples/application/python_editor/images/python_icon.png b/examples/application/python_editor/images/python_icon.png index 93e4a02c3..fe8d48381 100644 Binary files a/examples/application/python_editor/images/python_icon.png and b/examples/application/python_editor/images/python_icon.png differ diff --git a/examples/application/python_shell/images/python_icon.png b/examples/application/python_shell/images/python_icon.png index 93e4a02c3..fe8d48381 100644 Binary files a/examples/application/python_shell/images/python_icon.png and b/examples/application/python_shell/images/python_icon.png differ diff --git a/pyface/gui_application.py b/pyface/gui_application.py index 8662555f8..b81f6e18f 100644 --- a/pyface/gui_application.py +++ b/pyface/gui_application.py @@ -67,7 +67,8 @@ class GUIApplication(Application): #: The about dialog for the application. about_dialog = Instance(IDialog) - #: Icon for the application (used in window titlebars) + #: Icon for the application (used in window titlebars, docks, etc.) + #: This should be large (eg. 1024x1024 is standard on MacOS). icon = Image #: Logo of the application (used in splash screens and about dialogs) @@ -166,7 +167,7 @@ def start(self): if ok: # create the GUI so that the splash screen comes up first thing if self.gui is Undefined: - self.gui = GUI(splash_screen=self.splash_screen) + self.gui = GUI(self.splash_screen, self.name, self.icon) # create the initial windows to show self._create_windows() @@ -301,3 +302,14 @@ def _on_window_closed(self, event): window = event.object if window in self.windows: self.windows.remove(window) + + @observe('icon', post_init=True) + def _icon_updated(self, event): + print('--------------------------> here', event, self.gui) + if self.gui and event.new is not None: + self.gui.set_application_icon(event.new) + + @observe('name', post_init=True) + def _name_updated(self, event): + if self.gui and event.new: + self.gui.set_application_name(event.new) diff --git a/pyface/i_gui.py b/pyface/i_gui.py index 7649165d5..a6072264b 100644 --- a/pyface/i_gui.py +++ b/pyface/i_gui.py @@ -16,7 +16,7 @@ from traits.etsconfig.api import ETSConfig -from traits.api import Bool, HasTraits, Interface, Str +from traits.api import Any, Bool, HasTraits, Interface, Str # Logging. @@ -28,6 +28,9 @@ class IGUI(Interface): # 'GUI' interface -----------------------------------------------------# + #: The toolkit application object. This is typically read-only. + application = Any() + #: Is the GUI busy (i.e. should the busy cursor, often an hourglass, be #: displayed)? busy = Bool(False) @@ -158,6 +161,20 @@ def start_event_loop(self): def stop_event_loop(self): """ Stop the GUI event loop. """ + def set_application_icon(self, image): + """ Set the application icon in the OS. + + This controls the icon displayed in system docks and similar locations + within the operating system. + """ + + def set_application_name(self, image): + """ Set the application name in the OS. + + This controls the name displayed in system docks and similar locations + within the operating system. + """ + class MGUI(HasTraits): """ The mixin class that contains common code for toolkit specific diff --git a/pyface/tests/test_gui_application.py b/pyface/tests/test_gui_application.py index a19cc0155..a0c4688e7 100644 --- a/pyface/tests/test_gui_application.py +++ b/pyface/tests/test_gui_application.py @@ -13,6 +13,7 @@ from shutil import rmtree from tempfile import mkdtemp import unittest +from unittest import mock from traits.api import Bool, observe @@ -148,6 +149,8 @@ def test_defaults(self): self.assertEqual(app.home, ETSConfig.application_home) self.assertEqual(app.user_data, ETSConfig.user_data) self.assertEqual(app.company, ETSConfig.company) + self.assertIsNone(app.icon) + self.assertIsNone(app.logo) def test_initialize_application_home(self): dirpath = mkdtemp() @@ -293,3 +296,52 @@ def test_bad_stop(self): event_order = [event.event_type for event in self.application_events] self.assertEqual(event_order, EVENTS[:-1]) self.assertEqual(app.windows, []) + + def test_application_icon(self): + app = GUIApplication( + icon='core', + ) + app.gui + + with mock.patch('pyface.gui.GUI.set_application_icon') as mock_method: + self.gui.invoke_after(1000, app.exit) + app.run() + + mock_method.assert_called_once_with(app.icon) + + def test_application_icon_changed(self): + app = GUIApplication() + + with mock.patch('pyface.gui.GUI.set_application_icon') as mock_method: + self.gui.invoke_after(1000, app.exit) + app.run() + + mock_method.assert_not_called() + + with mock.patch('pyface.gui.GUI.set_application_icon') as mock_method: + app.icon = 'core' + + mock_method.assert_called_once_with(app.icon) + + def test_application_name(self): + app = GUIApplication() + + with mock.patch('pyface.gui.GUI.set_application_name') as mock_method: + self.gui.invoke_after(1000, app.exit) + app.run() + + mock_method.assert_called_once_with(app.name) + + def test_application_name_changed(self): + app = GUIApplication(name="") + + with mock.patch('pyface.gui.GUI.set_application_name') as mock_method: + self.gui.invoke_after(1000, app.exit) + app.run() + + mock_method.assert_not_called() + + with mock.patch('pyface.gui.GUI.set_application_name') as mock_method: + app.name = 'changed name' + + mock_method.assert_called_once_with(app.name) diff --git a/pyface/ui/qt4/gui.py b/pyface/ui/qt4/gui.py index a01a447ab..6b55b12b2 100644 --- a/pyface/ui/qt4/gui.py +++ b/pyface/ui/qt4/gui.py @@ -13,16 +13,13 @@ import logging +import sys +from traits.api import Bool, HasTraits, Property, observe, provides, Str from pyface.qt import QtCore, QtGui - - -from traits.api import Bool, HasTraits, observe, provides, Str -from pyface.util.guisupport import start_event_loop_qt4 - - from pyface.i_gui import IGUI, MGUI +from pyface.util.guisupport import get_app_qt4, start_event_loop_qt4 # Logging. @@ -37,6 +34,8 @@ class GUI(MGUI, HasTraits): # 'GUI' interface -----------------------------------------------------# + application = Property() + busy = Bool(False) started = Bool(False) @@ -47,7 +46,15 @@ class GUI(MGUI, HasTraits): # 'object' interface. # ------------------------------------------------------------------------ - def __init__(self, splash_screen=None): + def __init__(self, splash_screen=None, name="", icon=None): + # Change the application icon, if any + if icon is not None: + self.set_application_icon(icon) + + # Change the application name, if any + if name: + self.set_application_name(name) + # Display the (optional) splash screen. self._splash_screen = splash_screen @@ -109,7 +116,30 @@ def start_event_loop(self): def stop_event_loop(self): logger.debug("---------- stopping GUI event loop ----------") - QtGui.QApplication.quit() + self.application.quit() + + def set_application_icon(self, image): + """ Set the application icon in the OS. + + This controls the icon displayed in system docks and similar locations + within the operating system. + """ + # ensure application exists before doing anything else + app = self.application + app.setWindowIcon(image.create_icon()) + + def set_application_name(self, name): + """ Set the application name at the toolkit level. + + This sets the name displayed for the application in various places + in the OS. + + Note + ---- + This does not change the name of the application in the MacOS menu or + dock. + """ + self.application.setApplicationDisplayName(name) # ------------------------------------------------------------------------ # Trait handlers. @@ -129,6 +159,9 @@ def _update_busy_state(self, event): else: QtGui.QApplication.restoreOverrideCursor() + def _get_application(self): + return get_app_qt4(*sys.argv) + class _FutureCall(QtCore.QObject): """ This is a helper class that is similar to the wx FutureCall class. """ diff --git a/pyface/ui/wx/gui.py b/pyface/ui/wx/gui.py index 63419f0f3..c17ef09b5 100644 --- a/pyface/ui/wx/gui.py +++ b/pyface/ui/wx/gui.py @@ -16,14 +16,11 @@ import logging import sys - import wx +from traits.api import Bool, HasTraits, Property, provides, Str -from traits.api import Bool, HasTraits, provides, Str -from pyface.util.guisupport import start_event_loop_wx - - +from pyface.util.guisupport import get_app_wx, start_event_loop_wx from pyface.i_gui import IGUI, MGUI @@ -36,6 +33,8 @@ class GUI(MGUI, HasTraits): # 'GUI' interface -----------------------------------------------------# + application = Property() + busy = Bool(False) started = Bool(False) @@ -46,7 +45,15 @@ class GUI(MGUI, HasTraits): # 'object' interface. # ------------------------------------------------------------------------ - def __init__(self, splash_screen=None): + def __init__(self, splash_screen=None, name="", icon=None): + # Change the application icon, if any + if icon is not None: + self.set_application_icon(icon) + + # Change the application name, if any + if name: + self.set_application_name(name) + # Display the (optional) splash screen. self._splash_screen = splash_screen @@ -120,7 +127,38 @@ def stop_event_loop(self): """ Stop the GUI event loop. """ logger.debug("---------- stopping GUI event loop ----------") - wx.GetApp().ExitMainLoop() + self.application.ExitMainLoop() + + def set_application_icon(self, image): + """ Set the application icon in the OS. + + This controls the icon displayed in system docks and similar locations + within the operating system. + + Note + ---- + This does not change the dock icon in MacOS. + """ + # ensure app exists before doing anything else + self.application + icon = image.create_icon() + dock_icon = wx.adv.TaskBarIcon(wx.adv.TBI_DOCK) + dock_icon.SetIcon(icon) + + def set_application_name(self, name): + """ Set the application name at the toolkit level. + + This sets the name displayed for the application in various places + in the OS. + + Note + ---- + This does not change the name of the application in the MacOS menu or + dock. + """ + # ensure app exists before doing anything else + app = self.application + app.SetAppDisplayName(name) # ------------------------------------------------------------------------ # Trait handlers. @@ -140,3 +178,6 @@ def _busy_changed(self, new): del self._wx_cursor return + + def _get_application(self): + return get_app_wx()