diff --git a/.github/actions/install-qt-support/action.yml b/.github/actions/install-qt-support/action.yml index 15e7a22a6..66b98ab7e 100644 --- a/.github/actions/install-qt-support/action.yml +++ b/.github/actions/install-qt-support/action.yml @@ -20,6 +20,7 @@ runs: sudo apt-get install libxcb-render-util0 sudo apt-get install libxcb-xinerama0 sudo apt-get install libxcb-shape0 + sudo apt-get install libxcb-cursor0 sudo apt-get install pulseaudio sudo apt-get install libpulse-mainloop-glib0 # Needed to work around https://bugreports.qt.io/browse/PYSIDE-1547 diff --git a/.github/workflows/test-docs-with-edm.yml b/.github/workflows/test-docs-with-edm.yml new file mode 100644 index 000000000..f9733e920 --- /dev/null +++ b/.github/workflows/test-docs-with-edm.yml @@ -0,0 +1,56 @@ +# This workflow targets stable released dependencies from EDM. +# Note that some packages may not actually be installed from EDM but from +# PyPI, see etstool.py implementations. + +name: Build docs with EDM + +on: [pull_request, workflow_dispatch] + +env: + INSTALL_EDM_VERSION: 3.5.0 + QT_MAC_WANTS_LAYER: 1 + +jobs: + + # Build docs using EDM packages + docs-with-edm: + strategy: + matrix: + os: [ubuntu-latest] + toolkit: ['pyside6'] + fail-fast: false + timeout-minutes: 20 # should be plenty, it's usually < 5 + runs-on: ${{ matrix.os }} + env: + # Set root directory, mainly for Windows, so that the EDM Python + # environment lives in the same drive as the cloned source. Otherwise + # 'pip install' raises an error while trying to compute + # relative path between the site-packages and the source directory. + EDM_ROOT_DIRECTORY: ${{ github.workspace }}/.edm + steps: + - uses: actions/checkout@v3 + - name: Install Qt dependencies + uses: ./.github/actions/install-qt-support + if: matrix.toolkit != 'wx' + - name: Cache EDM packages + uses: actions/cache@v3 + with: + path: ~/.cache + key: ${{ runner.os }}-${{ matrix.toolkit }}-${{ hashFiles('etstool.py') }} + - name: Setup EDM + uses: enthought/setup-edm-action@v2 + with: + edm-version: ${{ env.INSTALL_EDM_VERSION }} + - name: Install click to the default EDM environment + run: edm install -y wheel click coverage + - name: Install test environment + run: edm run -- python etstool.py install --toolkit=${{ matrix.toolkit }} + - name: Build docs + run: xvfb-run -a edm run -- python etstool.py docs --toolkit=${{ matrix.toolkit }} + - name: Archive HTML docs + uses: actions/upload-artifact@v3 + with: + name: html-doc-bundle + path: docs/build/html + # don't need these kept for long + retention-days: 7 diff --git a/.github/workflows/test-with-edm.yml b/.github/workflows/test-with-edm.yml index 8f72f1a08..917c71e3d 100644 --- a/.github/workflows/test-with-edm.yml +++ b/.github/workflows/test-with-edm.yml @@ -16,6 +16,8 @@ jobs: test-with-edm: strategy: matrix: + # Note: MacOS support is contingent on not hitting any issues with + # unsupported CPU features such as AVX2 and so may be unstable. os: [ubuntu-latest, macos-latest, windows-latest] toolkit: ['pyside6'] fail-fast: false diff --git a/.gitignore b/.gitignore index 853bfd569..0a46cb31c 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ build/ dist/ docs/build/ +docs/source/api # Auto-generated by setup.py pyface/_version.py diff --git a/CHANGES.txt b/CHANGES.txt index b49dbfa1d..3e6c80236 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -2,6 +2,154 @@ Pyface Changelog ================ +Release 8.0.0 +============= + +Highlights of this release +-------------------------- + +This is a major release which removes a number of things which have been +deprecated for a number of years. The most significant change in this release +is that the Qt toolkit backend has been moved from the ``pyface.ui.qt4`` +package to the ``pyface.ui.qt`` package. The "qt4" namespace had been an +ongoing source of confusion as Qt5 and Qt6 became the most popular versions of +Qt in use. Because this change has the potential to cause significant backwards +compatibility issues, this release includes import hooks to continue to support +imports from ``pyface.ui.qt4...`` which can be enabled by: + +- using "qt4" for the `ETS_TOOLKIT` or `ETSConfig.toolkit` values. +- setting the `ETS_QT4_IMPORT` environment variable to a non-empty value +- explicitly adding the import hooks to `sys.meta_path` + +In particular the environment variables allow users of ETS-based applications +such as Mayavi to continue to use those applications with newer versions of +Pyface until there is time to update their code to the new import locations. + +Additionally this release uses the module-level ``__getattr__`` system +introduced in Python 3.7 to delay imports from the ``api`` modules which have +side-effects, particularly toolkit selection. This means that you can, for +example, import ``pyface.api`` and toolkit selection will be deferred until +you actually request a class or object which is toolkit-dependent. Part of +this included adding formal interfaces for ``ActionManager`` and its subclasses + +In addition this release: + +- adds support for Python 3.11 and drops support for Python 3.6 +- adds support for PySide 6.4+ and the new enum system +- removes code supporting PyQt4, and supports more modern imports from + `pyface.qt`, such as `pyface.qt.QtWidgets`. +- removes many things flagged as deprecated in Pyface 7 +- consistently add interface classes to ``api`` modules. +- adds new Field subclasses, including an ImageField and LabelField +- moves to a pyproject.toml-based packaging solution, removing setup.py + +Detailed changes +---------------- + +Thanks to: + +* Mark Dickinson +* Dominik Gresch +* JaRoSchm +* Rahul Poruri +* Corran Webster + +Features + +* Refactor IField and add IImageField and ILabelField Field classes (#1234) +* Move ``pyface.ui.qt4`` to ``pyface.ui.qt`` (#1223, #1228) +* Delayed imports with side-effects in ``api`` modules; new interfaces for + ``ActionManager`` and its subclasses (#1229) + +Enhancements + +* Better handling of sizes for multiple (or no) screens (#1237) +* More arguments for the ``confirm`` function (#1221) +* Expose interfaces in ``api`` modules and other improvemnts to ``api`` modules + (#1220, #1222, #1229) +* remove deprecated modules and code (#1209, #1215) +* PySide 6.5 support (#1238) +* Python 3.11 and PySide 6.4 support (#1210) + +Fixes + +* Always use a no-delay timer for ``GUI`` ``invoke_later`` and + ``set_trait_later`` on Qt (#1226) +* Work arounds for some end-of-process segfaults on PySide < 6.4.3 (#1214) +* Use ``exec`` instead of ``exec_`` where possible (#1208) +* Emit a warning rather than asserting in SplitTabWidget (#1192) +* Remove required parent argument in Wx StatusBarManager (#1192) +* Use integer division in DockSplitter.draw (#1190) + +Documentation + +* general updates and enhancements (#1238) +* update copyright dates (#1191) + +CI + +* remove ``setup.py`` and use ``pyproject.toml`` (#1203, #1214) + +Release 7.4.4 +============= + +Highlights of this release +-------------------------- + +This is a quick bugfix release that resolves some issues with the 7.4.3 release +on CI for downstream projects. The issues were on testing code, and so +shouldn't impact application code or behaviour. + +Detailed changes +---------------- + +Thanks to: + +* Corran Webster + +Fixes + +* Don't raise ConditionTimeoutError if test doesn't time out (#1182) + +CI + +* get CI working again with ubuntu-current on GitHub (#1186) + +Release 7.4.3 +============= + +Highlights of this release +-------------------------- + +This is a bugfix release that collects a number of additional issues discovered +and fixed since the 7.4.2 release. Among the fixes, this pins PySide6 to less than +6.4.0, as 6.4 has breaking changes in it. + +Detailed changes +---------------- + +Thanks to: + +* Alex Chabot-Leclerc +* Mark Dickinson +* Eric Larson +* Steven Kern +* Corran Webster + +Fixes + +* Fix code editor gutter widget on recent Python versions (#1176) +* fix issues with FileDialog and DirectoryDialog close method on Linux (#1175) +* update setup.py metadata (#1173) +* restrict to PySide versions before 6.4.0 (#1169) +* don't do unneccessary evaluations of conditions in EventLoopHelper (#1168) +* fix a deleted object error in PyQt5 (#1161) +* better reporting of toolkit errors (#1157) + +Documentation + +* fix some Python 2 style print statements in documentation (#1157) + Release 7.4.2 ============= diff --git a/README.rst b/README.rst index 718e977bf..58a70a812 100644 --- a/README.rst +++ b/README.rst @@ -3,10 +3,12 @@ Pyface: Traits-capable Windowing Framework ========================================== The Pyface project contains a toolkit-independent GUI abstraction layer, -which is used to support the "visualization" features of the Traits package. -Thus, you can write code in terms of the Traits API (views, items, editors, -etc.), and let Pyface and your selected toolkit and back-end take care of -the details of displaying them. +which is used to support the "visualization" features of the Enthought Tool +Suite libraries. Pyface contains Traits-aware wrappers of standard GUI +elements such as Windows, Dialogs and Fields, together with the "Tasks" +application framework which provides a rich GUI experience with dock panes, +tabbed editors, and so forth. This permits you to write cross-platform +interactive GUI code without needing to use the underlying GUI backend. The following GUI backends are supported: @@ -14,33 +16,79 @@ The following GUI backends are supported: - PyQt5 (stable) and PyQt6 (in development) - wxPython 4 (experimental) +Example +------- + +The following code creates a window with a simple Python shell: + +.. code-block:: python + + from pyface.api import ApplicationWindow, GUI, IPythonShell + + class MainWindow(ApplicationWindow): + """ The main application window. """ + + #: The PythonShell that forms the contents of the window + shell = Instance(IPythonShell, allow_none=False) + + def _create_contents(self, parent): + """ Create the editor. """ + self.shell.create(parent) + return self.shell.control + + def destroy(self): + self.shell.destroy() + super().destroy() + + def _shell_default(self): + from pyface.api import PythonShell + return PythonShell() + + # Application entry point. + if __name__ == "__main__": + # Create the GUI. + gui = GUI() + + # Create and open the main window. + window = MainWindow(title="Python Shell", size=(640, 480)) + window.open() + + # Start the GUI event loop! + gui.start_event_loop() + +.. image:: https://raw.github.com/enthought/pyface/main/shell_window.png + :alt: A Pyface GUI window containing a Python shell. + Installation ------------ -GUI backends are marked as optional dependencies of Pyface. Some features -or infrastructures may also require additional dependencies. +Pyface is a pure Python package. In most cases Pyface will be installable +using a simple ``pip install`` command. -To install with PySide2 dependencies:: +To install with a backend, choose one of the following, as appropriate: - $ pip install pyface[pyside2] +.. code-block:: console -To install with PySide6 dependencies (experimental):: + $ pip install pyface[pyside2] $ pip install pyface[pyside6] -To install with PyQt5 dependencies:: - $ pip install pyface[pyqt5] -To install with wxPython4 dependencies (experimental):: - $ pip install pyface[wx] -``pillow`` is an optional dependency for the PILImage class:: +Some optional functionality uses ``pillow`` and ``numpy`` and these can be +installed using optional dependencies: + +.. code-block:: console $ pip install pyface[pillow] -To install with additional test dependencies:: + $ pip install pyface[numpy] + +For running tests a few more packages are required: + +.. code-block:: console $ pip install pyface[test] @@ -51,20 +99,6 @@ Documentation * `API Documentation `_. -Prerequisites -------------- - -Pyface depends on: - -* `Traits `_ - -* a GUI toolkit as described above - -* Pygments for syntax highlighting in the Qt code editor widget. - -* some widgets may have additional optional dependencies such as NumPy or - Pillow. - .. end_of_long_description Developing Pyface diff --git a/docs/source/conf.py b/docs/source/conf.py index 8b58d78e6..3cddc233b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -11,9 +11,10 @@ # All configuration values have a default value; values that are commented out # serve to show the default value. -import os -import runpy -import sys +try: + from importlib.metadata import version as metadata_version +except: + from importlib_metadata import version as metadata_version # General configuration # --------------------- @@ -45,10 +46,7 @@ # The default replacements for |version| and |release|, also used in various # other places throughout the built documents. -version_py = os.path.join('..', '..', 'pyface', '_version.py') -version_content = runpy.run_path(version_py) -version = ".".join(version_content["version"].split(".", 2)[:2]) -release = version +version = release = metadata_version("pyface") # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: diff --git a/docs/source/fields.rst b/docs/source/fields.rst index ac6d12639..2bb7c4049 100644 --- a/docs/source/fields.rst +++ b/docs/source/fields.rst @@ -9,16 +9,81 @@ to a Traits interface, so that you can use them in a cross-toolkit manner. Where possible, these classes share a common API for the same functionality. In particular, all classes have a -:py:attr:`~pyface.fields.i_field.IField.value` trait which holds the (usually -user-editable) value displayed in the field. Code using the field can listen -to changes in this trait to react to the user entering a new value for the -field without needing to know anything about the underlying toolkit event -signalling mechanisms. +:py:attr:`~pyface.fields.i_field.IField.value` trait which holds the value +displayed in the field. -All fields also provide a trait for setting the -:py:attr:`~pyface.fields.i_field.IField.context_menu` of the field. Context -menus should be :py:class:`~pyface.action.menu_manager.MenuManager` -instances. +Fields where the value is user-editable rather than simply displayed implement +the :py:attr:`~pyface.fields.i_editable_field.IEditableField` interface. Code +using the field can listen to changes in the value trait to react to the user +entering a new value for the field without needing to know anything about the +underlying toolkit event signalling mechanisms. + +Fields inherit from :py:class:`~pyface.i_widget.IWidget` and +:py:class:`~pyface.i_layout_item.ILayoutItem` which have a number of +additional traits with useful features: + +:py:attr:`~pyface.i_widget.IWidget.tooltip` + A tooltip for the widget, which should hold string. + +:py:attr:`~pyface.i_widget.IWidget.context_menu` + A context menu for the widget, which should hold an + :py:class:`~pyface.action.i_menu_manager.IMenuManager` instance. + +:py:attr:`~pyface.i_layout_item.ILayoutItem.minimum_size` + A tuple holding the minimum size of a layout widget. + +:py:attr:`~pyface.i_layout_item.ILayoutItem.maximum_size` + A tuple holding the minimum size of a layout widget. + +:py:attr:`~pyface.i_layout_item.ILayoutItem.stretch` + A tuple holding information about the distribution of addtional space into + the widget when growing in a layout. Higher numbers mean proportinally + more space. + +:py:attr:`~pyface.i_layout_item.ILayoutItem.size_policy` + A tuple holding information about how the widget can grow and shrink. + +:py:attr:`~pyface.fields.i_field.IField.alignment` + A value holding the horizontal alignment of the contents of the field. + +ComboField +========== + +The :py:class:`~pyface.fields.i_combo_field.IComboField` interface has an arbitrary +:py:attr:`~pyface.fields.i_combo_field.IComboField.value` that must come from a list +of valid :py:attr:`~pyface.fields.i_combo_field.IComboField.values`. For non-text +values, a :py:attr:`~pyface.fields.i_combo_field.IComboField.formatter` function +should be provided, defaulting to :py:func:`str`. + +LabelField +========== + +The :py:class:`~pyface.fields.i_label_field.ILabelField` interface has a string +for the :py:attr:`~pyface.fields.i_label_field.ILabelField.value` which is not +user-editable. + +In the Qt backend they can have an image for an +:py:attr:`~pyface.fields.i_label_field.ILabelField.icon`. + +ImageField +========== + +The :py:class:`~pyface.fields.i_image_field.IImageField` interface has an +:py:class:`~pyface.i_image.IImage` for its +:py:attr:`~pyface.fields.i_image_field.IImageField.value` which is not +user-editable. + +SpinField +========= + +The :py:class:`~pyface.fields.i_spin_field.ISpinField` interface has an integer +for the :py:attr:`~pyface.fields.i_spin_field.ISpinField.value`, and also +requires a range to be set, either via setting the min/max values as a tuple to +the :py:attr:`~pyface.fields.i_spin_field.ISpinField.bounds` trait, or by setting +values individually to :py:attr:`~pyface.fields.i_spin_field.ISpinField.minimum` +and :py:attr:`~pyface.fields.i_spin_field.ISpinField.maximum`. The +:py:attr:`~pyface.fields.i_spin_field.ISpinField.wrap` trait determines whether +the spinner wraps around at the extreme values. TextField ========= @@ -41,26 +106,6 @@ the Qt backend has a number of other options as well). The text field can be set to read-only mode via the :py:attr:`~pyface.fields.i_text_field.ITextField.read_only` trait. -SpinField -========= - -The :py:class:`~pyface.fields.i_spin_field.ISpinField` interface has an integer -for the :py:attr:`~pyface.fields.i_spin_field.ISpinField.value`, and also -requires a range to be set, either via setting the min/max values as a tuple to -the :py:attr:`~pyface.fields.i_spin_field.ISpinField.range` trait, or by setting -values individually to :py:attr:`~pyface.fields.i_spin_field.ISpinField.minimum` -and :py:attr:`~pyface.fields.i_spin_field.ISpinField.maximum`. - -ComboField -========== - -The :py:class:`~pyface.fields.i_combo_field.IComboField` interface has an arbitrary -:py:attr:`~pyface.fields.i_combo_field.IComboField.value` that must come from a list -of valid :py:attr:`~pyface.fields.i_combo_field.IComboField.values`. For non-text -values, a :py:attr:`~pyface.fields.i_combo_field.IComboField.formatter` function -should be provided - this defaults to either :py:func:`str` (Python 3+) or -:py:func:`unicode` (Python 2). - TimeField ========== diff --git a/docs/source/index.rst b/docs/source/index.rst index 21281763e..d2c16fd55 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -7,26 +7,25 @@ the TraitsUI package. Thus, you can write code in the abstraction of the Pyface API and the selected toolkit and backend take care of the details of displaying them. -Pyface -====== - Pyface enables programmers to interact with generic GUI objects, such as an -"MDI Application Window", rather than with raw GUI widgets. (Pyface is named by -analogy to JFace in Java.) TraitsUI uses Pyface to implement views and editors -for displaying and editing Traits-based objects. +"Application Window", rather than with raw GUI widgets. TraitsUI uses Pyface +to implement views and editors for displaying and editing Traits-based objects. +In addition to wrappers around fields and dialogs, Pyface includes an +application framework called "Tasks" for building full-featured +applications featuring dock panes, tabbed editor areas and so forth. Toolkit Backends ================ -TraitsUI and Pyface define APIs that are independent of any GUI toolkit. -However, in order to actually produce user interfaces with them, you must +Pyface defines APIs that are independent of any GUI toolkit. However, in +order to actually produce user interfaces with them, you must install a supported Python-based GUI toolkit and the appropriate toolkit-specific backend project. Conversely, a "null" backend is automatically used in the absence of a real backend. Currently, the GUI toolkits are -* PySide2 (stable) and PySide6 (experimental) +* PySide2 and PySide6 (stable) * PyQt5 (stable) and PyQt6 (in development) * wxPython 4 (experimental) diff --git a/docs/source/toolkits.rst b/docs/source/toolkits.rst index 5bc91eaa2..9960000c8 100644 --- a/docs/source/toolkits.rst +++ b/docs/source/toolkits.rst @@ -23,9 +23,10 @@ ways: from tratis.etsconfig.api import ETSConfig ETSConfig.toolkit = 'qt' - This must be done _before_ any widget imports in your application, including - importing :py:mod:`pyface.api`. Precisely, this must be set before the - first import of :py:mod:`pyface.toolkit`. + This must be done _before_ any widget imports in your application. + Precisely, this must be set before the first call to + :py:func:`pyface.base_toolkit.find_toolkit` (which usually happens + as a side-effect of importing :py:mod:`pyface.toolkit`). If for some reason Pyface can't load a deliberately specified toolkit, then it will raise an exception. @@ -35,6 +36,14 @@ toolkits, in that order, and then any other toolkits that it knows about other than ``null``. If all of those fail, then it will try to load the ``null`` toolkit. +Pyface tries to defer toolkit selection as long as possible until it is +actually needed because importing a toolkit tends to be slow and have +significant side-effects. Very occasionally an application or test suite may +need to ensure that the toolkit has been selected (for example, to enable +"ui" dispatch from background threads in Traits). This can be achieved by +either importing :py:mod:`pyface.toolkit` or, more directly, by calling +:py:func:`pyface.base_toolkit.find_toolkit`. + Once selected, the toolkit infrastructure is largely transparent to the application. diff --git a/docs/source/traits.rst b/docs/source/traits.rst index 2be140880..b3391c580 100644 --- a/docs/source/traits.rst +++ b/docs/source/traits.rst @@ -32,13 +32,20 @@ color spaces, such as HSV or HLS:: which allow specification of colors via CSS-style color strings, such as:: Color.from_str("aquamarine") - Color.from_str("#662244") + Color.from_str("#662244cc") -All standard web colors are understood, as well as hexadecimal RGB(A) with -1, 2 or 4 hex digits per channel. +All `standard web colors `_ +are understood, as well as hexadecimal RGB(A) with 1, 2 or 4 hex digits per +channel. |Color| instances are mutable, as their intended use is as values stored -in |PyfaceColor| trait classes which can be modified and listened to. The +in |PyfaceColor| trait classes which can be modified and listened to. This +means that they are comparatively heavyweight objects and should be shared +where possible and aren't particularly suited for situations where large +numbers of distinct and independent colors are needed: NumPy arrays are likely +better suited for this sort of application. + +The |PyfaceColor| validator understands string descriptions of colors, and will accept them as values when initializing or modifying the trait:: @@ -64,8 +71,96 @@ building applications on top of Pyface. It is intended that this trait will eventually replace the ``Color`` trait from TraitsUI. +Fonts +===== + +Just as with colors, it is common to want to be able to specify the font to use +for text in the UI. Each toolkit usually has its own way of representing +fonts, and so the ability to specify a font in a toolkit-independent way that +can be converted to a toolkit-specific representation is important. This is +particularly so when you want to allow the user to specify a font. + +Pyface provides a |Font| class and a corresponding |PyfaceFont| trait-type +that allows this sort of representation. Internally, the |Font| class +stores font attributes such as |Font.weight| or |Font.size| as traits:: + + font = Font(family=["Comic Sans"], size=24, weight="bold") + font.weight + +Some of these attributes are mapped traits, or otherwise convert string +values to numeric values. For example |Font.size| will accept strings like +"12pt", while |Font.weight| holds a numeric weight value in a mapped +attribute:: + +.. code-block:: pycon + + >>> font.weight + "bold" + >>> font.weight_ + 700 + +|Font| instances are mutable, as their intended use is as values stored +in |PyfaceFont| trait classes which can be modified and listened to. + +As a convenience, the |PyfaceFont| validator understands string descriptions +of fonts, and will accept them as values when initializing or modifying the +trait:: + + class Style(HasStrictTraits): + + font = PyfaceFont("24 pt Bold Comic Sans") + + @observe('font.*') + def font_changed(self, event): + print('The font has changed to {}'.format(self.font)) + + style = Style(font='12 italc Helvetica') + style.font.weight = 'light' + style.font = Font( + family=["Helvetica", "Arial", "sans-serif"], + variants={"small-caps"}, + ) + +The parsing of strings to fonts is currently handled by a |simple_parser| that +is modelled on the ``Font`` trait from TraitsUI, but it can be substituted +for a more sophisticated one, if needed. + +For interactions with the toolkit, the |Font.from_toolkit| and +|Font.to_toolkit| methods allow conversion to and from the appropriate +toolkit font objects, such as Qt's :py:class:`QFont` or +:py:class:`wx.Font`. These are most likely to be needed by internal +Pyface functionality, and should not be needed by developers who are +building applications on top of Pyface. + +It is intended that this trait will eventually replace the ``Font`` +trait from TraitsUI. It is also likely that the simple parser will be replaced +with a parser that understands CSS-like font strings. + +Layout Traits +============= + +Pyface also provides a number of classes and traits to assist with +layout-related functionality. These include the convenience Enums |Alignment|, +|Orientation| and |Position| as well as the classes |Margin| and |Border| and +their corresponding traits |HasMargin| and |HasBorder|. + + +.. |Alignment| replace:: :py:attr:`~pyface.ui_traits.Alignment` +.. |Border| replace:: :py:class:`~pyface.ui_traits.Border` .. |Color| replace:: :py:class:`~pyface.color.Color` .. |Color.from_str| replace:: :py:meth:`~pyface.color.Color.from_str` .. |Color.from_toolkit| replace:: :py:meth:`~pyface.color.Color.from_toolkit` .. |Color.to_toolkit| replace:: :py:meth:`~pyface.color.Color.to_toolkit` +.. |Font| replace:: :py:class:`~pyface.font.Font` +.. |Font.size| replace:: :py:attr:`~pyface.font.Font.size` +.. |Font.from_toolkit| replace:: :py:meth:`~pyface.font.Font.from_toolkit` +.. |Font.to_toolkit| replace:: :py:meth:`~pyface.font.Font.to_toolkit` +.. |Font.weight| replace:: :py:attr:`~pyface.font.Font.weight` +.. |HasMargin| replace:: :py:class:`~pyface.ui_traits.HasMargin` +.. |HasBorder| replace:: :py:class:`~pyface.ui_traits.HasBorder` +.. |Margin| replace:: :py:class:`~pyface.ui_traits.Margin` +.. |Orientation| replace:: :py:attr:`~pyface.ui_traits.Orientation` +.. |Position| replace:: :py:attr:`~pyface.ui_traits.Position` .. |PyfaceColor| replace:: :py:class:`~pyface.ui_traits.PyfaceColor` +.. |PyfaceFont| replace:: :py:class:`~pyface.ui_traits.PyfaceFont` +.. |simple_parser| replace:: :py:func:`~pyface.util.font_parser.simple_parser` diff --git a/etstool.py b/etstool.py index 6d661394f..4272abdf6 100644 --- a/etstool.py +++ b/etstool.py @@ -95,7 +95,6 @@ "importlib_resources>=1.1.0", "traits" + TRAITS_VERSION_REQUIRES, "traitsui", - "numpy", "pygments", "coverage", "flake8", @@ -112,8 +111,10 @@ ) if b'AVX2' in result.stdout.split(): dependencies.add('pillow_simd') + dependencies.add('numpy') else: dependencies.add('pillow_simd') + dependencies.add('numpy') source_dependencies = { diff --git a/examples/application/hello_world/hello_application.py b/examples/application/hello_world/hello_application.py index ebe5d36a1..e1cab73c6 100644 --- a/examples/application/hello_world/hello_application.py +++ b/examples/application/hello_world/hello_application.py @@ -17,9 +17,9 @@ computation. """ - import argparse -from pyface.application import Application + +from pyface.api import Application from traits.api import Str diff --git a/pyface/action/action.py b/pyface/action/action.py index 2542ac982..526c07f32 100644 --- a/pyface/action/action.py +++ b/pyface/action/action.py @@ -124,7 +124,7 @@ def perform(self, event): Parameters ---------- - event : ActionEvent instance + event : ActionEvent The event which triggered the action. """ if self.on_perform is not None: diff --git a/pyface/action/action_controller.py b/pyface/action/action_controller.py index d8ddf9379..b2c926df9 100644 --- a/pyface/action/action_controller.py +++ b/pyface/action/action_controller.py @@ -25,14 +25,14 @@ def perform(self, action, event): Parameters ---------- - action : Action instance + action : Action The action to perform. - event : ActionEvent instance + event : ActionEvent The event that triggered the action. Returns ------- - result : any + result : Any The result of the action's perform method (usually None). """ return action.perform(event) @@ -42,7 +42,7 @@ def can_add_to_menu(self, action): Parameters ---------- - action : Action instance + action : Action The action to consider. Returns @@ -57,7 +57,7 @@ def add_to_menu(self, action): Parameters ---------- - action : Action instance + action : Action The action added to the menu. """ pass @@ -67,7 +67,7 @@ def can_add_to_toolbar(self, action): Parameters ---------- - action : Action instance + action : Action The action to consider. Returns @@ -82,7 +82,7 @@ def add_to_toolbar(self, action): Parameters ---------- - action : Action instance + action : Action The action added to the toolbar. """ pass diff --git a/pyface/action/action_item.py b/pyface/action/action_item.py index 987ab1b02..d9744383c 100644 --- a/pyface/action/action_item.py +++ b/pyface/action/action_item.py @@ -93,7 +93,7 @@ def add_to_menu(self, parent, menu, controller): The parent of the new menu item control. menu : toolkit menu The menu to add the action item to. - controller : ActionController instance or None + controller : pyface.action.action_controller.ActionController or None The controller to use. """ if (controller is None) or controller.can_add_to_menu(self.action): @@ -117,9 +117,9 @@ def add_to_toolbar( The parent of the new menu item control. tool_bar : toolkit toolbar The toolbar to add the action item to. - image_cache : ImageCache instance + image_cache : ImageCache The image cache for resized images. - controller : ActionController instance or None + controller : pyface.action.action_controller.ActionController or None The controller to use. show_labels : bool Should the toolbar item show a label. @@ -145,7 +145,7 @@ def add_to_palette(self, tool_palette, image_cache, show_labels=True): The parent of the new menu item control. tool_palette : toolkit tool palette The tool palette to add the action item to. - image_cache : ImageCache instance + image_cache : ImageCache The image cache for resized images. show_labels : bool Should the toolbar item show a label. diff --git a/pyface/action/action_manager.py b/pyface/action/action_manager.py index 29dda760c..4f2ef2703 100644 --- a/pyface/action/action_manager.py +++ b/pyface/action/action_manager.py @@ -45,7 +45,7 @@ class ActionManager(HasTraits): enabled = Bool(True) #: All of the contribution groups in the manager. - groups = Property(List(Group)) + groups = Property(List(Instance(Group)), observe='_groups.items') #: The manager's unique identifier (if it has one). id = Str() @@ -61,7 +61,7 @@ class ActionManager(HasTraits): # Private interface ---------------------------------------------------- #: All of the contribution groups in the manager. - _groups = List(Group) + _groups = List(Instance(Group)) # ------------------------------------------------------------------------ # 'object' interface. @@ -72,7 +72,7 @@ def __init__(self, *args, **traits): Parameters ---------- - args : collection of strings, Group instances, or ActionManagerItem instances + args : collection of strings, Group instances, or ActionManagerItem s Positional arguments are interpreted as Items or Groups managed by the action manager. @@ -142,7 +142,7 @@ def append(self, item): Parameters ---------- - item : string, Group instance or ActionManagerItem instance + item : string, Group instance or ActionManagerItem The item to append. Notes @@ -180,7 +180,7 @@ def insert(self, index, item): ---------- index : int The position at which to insert the object - item : string, Group instance or ActionManagerItem instance + item : string, Group instance or ActionManagerItem The item to insert. Notes @@ -213,7 +213,7 @@ def find_group(self, id): Returns ------- - group : Group instance + group : Group The group which matches the id, or None if no such group exists. """ for group in self._groups: @@ -256,7 +256,7 @@ def walk(self, fn): Parameters ---------- - fn : callable + fn : Callable A callable to apply to the tree of groups and items, starting with the manager. """ @@ -272,7 +272,9 @@ def walk_group(self, group, fn): Parameters ---------- - fn : callable + group : Group + The group to walk. + fn : Callable A callable to apply to the tree of groups and items. """ fn(group) @@ -290,7 +292,10 @@ def walk_item(self, item, fn): Parameters ---------- - fn : callable + item : item + The item to walk. This may be a submenu or similar in addition to + simple Action items. + fn : Callable A callable to apply to the tree of items and subgroups. """ if hasattr(item, "groups"): @@ -309,7 +314,7 @@ def _get_default_group(self): Returns ------- - group : Group instance + group : Group The manager's default group. """ group = self.find_group(self.DEFAULT_GROUP) @@ -324,7 +329,7 @@ def _prepare_item(self, item): Parameters ---------- - item : string, Group instance or ActionManagerItem instance + item : string, Group instance or ActionManagerItem The item to be added to this ActionManager Returns diff --git a/pyface/action/action_manager_item.py b/pyface/action/action_manager_item.py index b4cf6b675..a6297ed17 100644 --- a/pyface/action/action_manager_item.py +++ b/pyface/action/action_manager_item.py @@ -32,7 +32,7 @@ class ActionManagerItem(HasTraits): id = Str() #: The group the item belongs to. - parent = Instance("pyface.action.api.Group") + parent = Instance("pyface.action.group.Group") #: Is the item enabled? enabled = Bool(True) @@ -53,7 +53,7 @@ def add_to_menu(self, parent, menu, controller): The parent of the new menu item control. menu : toolkit menu The menu to add the action item to. - controller : ActionController instance or None + controller : pyface.action.action_controller.ActionController or None The controller to use. """ raise NotImplementedError() @@ -67,9 +67,9 @@ def add_to_toolbar(self, parent, tool_bar, image_cache, controller): The parent of the new menu item control. tool_bar : toolkit toolbar The toolbar to add the action item to. - image_cache : ImageCache instance + image_cache : ImageCache The image cache for resized images. - controller : ActionController instance or None + controller : pyface.action.action_controller.ActionController or None The controller to use. show_labels : bool Should the toolbar item show a label. diff --git a/pyface/action/api.py b/pyface/action/api.py index ad10c0ef5..82e830e87 100644 --- a/pyface/action/api.py +++ b/pyface/action/api.py @@ -65,10 +65,11 @@ """ +# Imports which don't select the toolkit as a side-effect. + from .action import Action from .action_controller import ActionController from .action_event import ActionEvent -from .action_item import ActionItem from .action_manager import ActionManager from .action_manager_item import ActionManagerItem from .field_action import FieldAction @@ -80,16 +81,71 @@ ExitAction, GUIApplicationAction, ) +from .i_action_manager import IActionManager +from .i_menu_bar_manager import IMenuBarManager +from .i_menu_manager import IMenuManager +from .i_status_bar_manager import IStatusBarManager +from .i_tool_bar_manager import IToolBarManager from .listening_action import ListeningAction -from .menu_manager import MenuManager -from .menu_bar_manager import MenuBarManager -from .status_bar_manager import StatusBarManager -from .tool_bar_manager import ToolBarManager from .traitsui_widget_action import TraitsUIWidgetAction from .window_action import CloseWindowAction, WindowAction -# This part of the module handles widgets that are still wx specific. This -# will all be removed when everything has been ported to PyQt and pyface -# becomes toolkit agnostic. -from traits.etsconfig.api import ETSConfig +# ---------------------------------------------------------------------------- +# Deferred imports +# ---------------------------------------------------------------------------- + +# These imports have the side-effect of performing toolkit selection + +_toolkit_imports = { + 'MenuManager': "menu_manager", + 'MenuBarManager': "menu_bar_manager", + 'StatusBarManager': "status_bar_manager", + 'ToolBarManager': "tool_bar_manager", +} + +# These are pyface.* imports that have selection as a side-effect +# TODO: refactor to delay imports where possible +_relative_imports = { + 'ActionItem': "action_item", +} + + +def __getattr__(name): + """Lazily load attributes with side-effects + + In particular, lazily load toolkit backend names. For efficiency, lazily + loaded objects are injected into the module namespace + """ + # sentinel object for no result + not_found = object() + result = not_found + + if name in _relative_imports: + from importlib import import_module + source = _relative_imports[name] + module = import_module(f"pyface.action.{source}") + result = getattr(module, name) + + elif name in _toolkit_imports: + from pyface.toolkit import toolkit_object + source = _toolkit_imports[name] + result = toolkit_object(f"action.{source}:{name}") + + if result is not_found: + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + globals()[name] = result + return result + + +# ---------------------------------------------------------------------------- +# Introspection support +# ---------------------------------------------------------------------------- + +# the list of available names we report for introspection purposes +_extra_names = set(_toolkit_imports) | set(_relative_imports) + + +def __dir__(): + return sorted(set(globals()) | _extra_names) diff --git a/pyface/action/field_action.py b/pyface/action/field_action.py index 830223f72..f9848ac1e 100644 --- a/pyface/action/field_action.py +++ b/pyface/action/field_action.py @@ -70,7 +70,7 @@ def perform(self, event): Parameters ---------- - event : ActionEvent instance + event : ActionEvent The event which triggered the action. """ if self.on_perform is not None: diff --git a/pyface/action/group.py b/pyface/action/group.py index bd5cb1da6..3830d8f86 100644 --- a/pyface/action/group.py +++ b/pyface/action/group.py @@ -18,7 +18,6 @@ from pyface.action.action import Action -from pyface.action.action_item import ActionItem class Group(HasTraits): @@ -151,6 +150,8 @@ def insert(self, index, item): action, and that is inserted. If the item is a callable, then an Action is created for the callable, and then that is handled as above. """ + from pyface.action.action_item import ActionItem + if isinstance(item, Action): item = ActionItem(action=item) elif callable(item): diff --git a/pyface/action/i_action_manager.py b/pyface/action/i_action_manager.py new file mode 100644 index 000000000..3a4f98de7 --- /dev/null +++ b/pyface/action/i_action_manager.py @@ -0,0 +1,199 @@ +# (C) Copyright 2005-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! + +"""Interface for all action managers.""" + + +from traits.api import Bool, Constant, Event, Instance, Interface, List, Str + +from pyface.action.action_controller import ActionController +from pyface.action.group import Group + + +class IActionManager(Interface): + """ Abstract base class for all action managers. + + An action manager contains a list of groups, with each group containing a + list of items. + + There are currently three concrete sub-classes: + + 1) 'MenuBarManager' + 2) 'MenuManager' + 3) 'ToolBarManager' + + """ + + # 'ActionManager' interface -------------------------------------------- + + #: The Id of the default group. + DEFAULT_GROUP = Constant("additions") + + #: The action controller (if any) used to control how actions are performed. + controller = Instance(ActionController) + + #: Is the action manager enabled? + enabled = Bool(True) + + #: All of the contribution groups in the manager. + groups = List(Instance(Group)) + + #: The manager's unique identifier (if it has one). + id = Str() + + #: Is the action manager visible? + visible = Bool(True) + + # Events ---- + + #: fixme: We probably need more granular events than this! + changed = Event() + + # ------------------------------------------------------------------------ + # 'object' interface. + # ------------------------------------------------------------------------ + + def __init__(self, *items, **traits): + """ Creates a new action manager. + + Parameters + ---------- + items : collection of strings, Group, or ActionManagerItem s + Positional arguments are interpreted as Items or Groups + managed by the action manager. + traits : additional traits + Traits to be passed to the usual Traits ``__init__``. + + Notes + ----- + + If a Group is passed as a positional agrument then it is added to the + manager and any subsequent Items arguments are appended to the Group + until another Group is encountered. + + If a string is passed, a Group is created with id set to the string. + """ + + # ------------------------------------------------------------------------ + # 'IActionManager' interface. + # ------------------------------------------------------------------------ + + def append(self, item): + """ Append an item to the manager. + + Parameters + ---------- + item : string, Group instance or ActionManagerItem + The item to append. + + Notes + ----- + + If the item is a group, the Group is appended to the manager's list + of groups. It the item is a string, then a group is created with + the string as the ``id`` and the new group is appended to the list + of groups. If the item is an ActionManagerItem then the item is + appended to the manager's default group. + """ + + def destroy(self): + """ Called when the manager is no longer required. + + By default this method simply calls 'destroy' on all of the manager's + groups. + """ + + def insert(self, index, item): + """ Insert an item into the manager at the specified index. + + Parameters + ---------- + index : int + The position at which to insert the object + item : string, Group instance or ActionManagerItem + The item to insert. + + Notes + ----- + + If the item is a group, the Group is inserted into the manager's list + of groups. It the item is a string, then a group is created with + the string as the ``id`` and the new group is inserted into the list + of groups. If the item is an ActionManagerItem then the item is + inserted into the manager's defualt group. + """ + + def find_group(self, id): + """ Find a group with a specified Id. + + Parameters + ---------- + id : str + The id of the group to find. + + Returns + ------- + group : Group + The group which matches the id, or None if no such group exists. + """ + + def find_item(self, path): + """ Find an item using a path. + + Parameters + ---------- + path : str + A '/' separated list of contribution Ids. + + Returns + ------- + item : ActionManagerItem or None + Returns the matching ActionManagerItem, or None if any component + of the path is not found. + """ + + def walk(self, fn): + """ Walk the manager applying a function at every item. + + The components are walked in pre-order. + + Parameters + ---------- + fn : Callable + A callable to apply to the tree of groups and items, starting with + the manager. + """ + + def walk_group(self, group, fn): + """ Walk a group applying a function at every item. + + The components are walked in pre-order. + + Parameters + ---------- + group : Group + The group to walk. + fn : Callable + A callable to apply to the tree of groups and items. + """ + + def walk_item(self, item, fn): + """ Walk an item (may be a sub-menu manager remember!). + + The components are walked in pre-order. + + Parameters + ---------- + item : item + The item to walk. This may be a submenu or similar in addition to + simple Action items. + fn : Callable + A callable to apply to the tree of items and subgroups. + """ diff --git a/pyface/action/i_menu_bar_manager.py b/pyface/action/i_menu_bar_manager.py new file mode 100644 index 000000000..7a23a7fb8 --- /dev/null +++ b/pyface/action/i_menu_bar_manager.py @@ -0,0 +1,33 @@ +# (C) Copyright 2005-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! + + +""" The interface for a menu bar manager. """ + +from pyface.action.i_action_manager import IActionManager + + +class IMenuBarManager(IActionManager): + """ The interface for a menu bar manager. """ + + # ------------------------------------------------------------------------ + # 'MenuBarManager' interface. + # ------------------------------------------------------------------------ + + def create_menu_bar(self, parent, controller=None): + """ Creates a menu bar representation of the manager. + + Parameters + ---------- + parent : toolkit control + The toolkit control that owns the menubar. + controller : pyface.action.action_controller.ActionController + An optional ActionController for all items in the menubar. + """ diff --git a/pyface/action/i_menu_manager.py b/pyface/action/i_menu_manager.py new file mode 100644 index 000000000..cd1132562 --- /dev/null +++ b/pyface/action/i_menu_manager.py @@ -0,0 +1,45 @@ +# (C) Copyright 2005-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! + +from traits.api import Instance, Str + +from pyface.action.action import Action +from pyface.action.i_action_manager import IActionManager + + +class IMenuManager(IActionManager): + """ A menu manager realizes itself in a menu control. + + This could be a sub-menu or a context (popup) menu. + """ + + # 'IMenuManager' interface ---------------------------------------------# + + # The menu manager's name (if the manager is a sub-menu, this is what its + # label will be). + name = Str() + + # The default action for tool button when shown in a toolbar (Qt only) + action = Instance(Action) + + # ------------------------------------------------------------------------ + # 'IMenuManager' interface. + # ------------------------------------------------------------------------ + + def create_menu(self, parent, controller=None): + """ Creates a menu representation of the manager. + + Parameters + ---------- + parent : toolkit control + The toolkit control that owns the menu. + controller : pyface.action.action_controller.ActionController + An optional ActionController for all items in the menu. + """ diff --git a/pyface/action/i_status_bar_manager.py b/pyface/action/i_status_bar_manager.py new file mode 100644 index 000000000..c2d43def0 --- /dev/null +++ b/pyface/action/i_status_bar_manager.py @@ -0,0 +1,43 @@ +# (C) Copyright 2005-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! + + +from traits.api import Any, Bool, Interface, List, Str + + +class IStatusBarManager(Interface): + """ The interface for a status bar manager. """ + + # The message displayed in the first field of the status bar. + message = Str() + + # The messages to be displayed in the status bar fields. + messages = List(Str) + + # The toolkit-specific control that represents the status bar. + status_bar = Any() + + # Whether to show a size grip on the status bar. + size_grip = Bool(False) + + # Whether the status bar is visible. + visible = Bool(True) + + def create_status_bar(self, parent): + """ Creates a status bar. + + Parameters + ---------- + parent : toolkit control + The toolkit control that owns the status bar. + """ + + def destroy(self): + """ Destroys the status bar. """ diff --git a/pyface/action/i_tool_bar_manager.py b/pyface/action/i_tool_bar_manager.py new file mode 100644 index 000000000..08e536036 --- /dev/null +++ b/pyface/action/i_tool_bar_manager.py @@ -0,0 +1,50 @@ +# (C) Copyright 2005-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! + +from traits.api import Bool, Str, Tuple + +from pyface.action.i_action_manager import IActionManager +from pyface.ui_traits import Orientation + + +class IToolBarManager(IActionManager): + """ The interface for a tool bar manager. """ + + # 'IToolBarManager' interface ------------------------------------------- + + # The size of tool images (width, height). + image_size = Tuple((16, 16)) + + # The toolbar name (used to distinguish multiple toolbars). + name = Str("ToolBar") + + # The orientation of the toolbar. + orientation = Orientation("horizontal") + + # Should we display the name of each tool bar tool under its image? + show_tool_names = Bool(True) + + # Should we display the horizontal divider? + show_divider = Bool(True) + + # ------------------------------------------------------------------------ + # 'ToolBarManager' interface. + # ------------------------------------------------------------------------ + + def create_tool_bar(self, parent, controller=None): + """ Creates a tool bar. + + Parameters + ---------- + parent : toolkit control + The toolkit control that owns the toolbar. + controller : pyface.action.action_controller.ActionController + An optional ActionController for all items in the toolbar. + """ diff --git a/pyface/action/tests/test_api.py b/pyface/action/tests/test_api.py new file mode 100644 index 000000000..27523b7e2 --- /dev/null +++ b/pyface/action/tests/test_api.py @@ -0,0 +1,35 @@ +# (C) Copyright 2005-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! + +""" Test for pyface.api """ + +import unittest + + +class TestApi(unittest.TestCase): + """ Test importable items in any environment.""" + + def test_api_importable(self): + # make sure api is importable with the most minimal + # required dependencies, including in the absence of toolkit backends. + from pyface.action import api # noqa: F401 + + def test_public_attrs(self): + # make sure everything advertised by dir() is available except optional + from pyface.action import api + + attrs = [ + name + for name in dir(api) + if not name.startswith('_') + ] + for attr in attrs: + with self.subTest(attr=attr): + self.assertIsNotNone(getattr(api, attr, None)) diff --git a/pyface/action/window_action.py b/pyface/action/window_action.py index 7bfd4a1bd..73979bcdf 100644 --- a/pyface/action/window_action.py +++ b/pyface/action/window_action.py @@ -13,11 +13,10 @@ """ Abstract base class for all window actions. """ -from pyface.window import Window from traits.api import Instance, Property - from pyface.action.listening_action import ListeningAction +from pyface.i_window import IWindow class WindowAction(ListeningAction): @@ -30,7 +29,7 @@ class WindowAction(ListeningAction): # 'WindowAction' interface ----------------------------------------------- #: The window that the action is associated with. - window = Instance(Window) + window = Instance(IWindow) # ------------------------------------------------------------------------ # Protected interface. diff --git a/pyface/api.py b/pyface/api.py index 0ea18758b..2fbfa2e1c 100644 --- a/pyface/api.py +++ b/pyface/api.py @@ -16,6 +16,7 @@ - :class:`~.ApplicationWindow` - :attr:`~.clipboard` - :class:`~.Clipboard` +- :func:`~.find_toolkit` - :class:`~.GUI` - :class:`~.GUIApplication` - :class:`~.ImageResource` @@ -24,6 +25,7 @@ - :class:`~.SplitApplicationWindow` - :class:`~.SplitPanel` - :class:`~.SystemMetrics` +- :class:`~.Toolkit` - :class:`~.Window` - :class:`~.Widget` @@ -134,87 +136,15 @@ """ -import logging as _logging +# Imports which don't select the toolkit as a side-effect. -from .about_dialog import AboutDialog from .application import Application -from .application_window import ApplicationWindow -from .beep import beep -from .clipboard import clipboard, Clipboard -from .confirmation_dialog import confirm, ConfirmationDialog +from .base_toolkit import Toolkit, find_toolkit from .color import Color from .constant import OK, CANCEL, YES, NO -from .dialog import Dialog -from .directory_dialog import DirectoryDialog -from .drop_handler import BaseDropHandler, FileDropHandler -from .file_dialog import FileDialog from .filter import Filter from .font import Font -from .gui import GUI from .gui_application import GUIApplication -from .heading_text import HeadingText -from .image_cache import ImageCache -from .image_resource import ImageResource -from .key_pressed_event import KeyPressedEvent -from .layered_panel import LayeredPanel -from .message_dialog import error, information, warning, MessageDialog -from .progress_dialog import ProgressDialog - -from .util._optional_dependencies import optional_import as _optional_import - -# Excuse numpy dependency, otherwise re-raise -with _optional_import( - "numpy", - msg="ArrayImage not available due to missing numpy.", - logger=_logging.getLogger(__name__)): - - # We need to manually try importing numpy because the ``ArrayImage`` - # import will end up raising a ``TraitError`` exception instead of an - # ``ImportError``, which isnt caught by ``_optional_import``. - import numpy - - from .array_image import ArrayImage - - del numpy - -# Excuse pillow dependency, otherwise re-raise -with _optional_import( - "pillow", - msg="PILImage not available due to missing pillow.", - logger=_logging.getLogger(__name__)): - from .pil_image import PILImage - -# Excuse pygments dependency (for Qt), otherwise re-raise -with _optional_import( - "pygments", - msg="PythonEditor not available due to missing pygments.", - logger=_logging.getLogger(__name__)): - from .python_editor import PythonEditor - -with _optional_import( - "pygments", - msg="PythonShell not available due to missing pygments.", - logger=_logging.getLogger(__name__)): - from .python_shell import PythonShell - -from .sorter import Sorter -from .single_choice_dialog import choose_one, SingleChoiceDialog -from .splash_screen import SplashScreen -from .split_application_window import SplitApplicationWindow -from .split_dialog import SplitDialog -from .split_panel import SplitPanel -from .system_metrics import SystemMetrics -from .ui_traits import ( - Alignment, Border, HasBorder, HasMargin, Image, Margin, Orientation, - Position, PyfaceColor, PyfaceFont -) -from .window import Window -from .widget import Widget - -# ---------------------------------------------------------------------------- -# Public Interfaces -# ---------------------------------------------------------------------------- - from .i_about_dialog import IAboutDialog from .i_application_window import IApplicationWindow from .i_clipboard import IClipboard @@ -241,27 +171,122 @@ from .i_system_metrics import ISystemMetrics from .i_widget import IWidget from .i_window import IWindow +from .sorter import Sorter +from .ui_traits import ( + Alignment, Border, HasBorder, HasMargin, Image, Margin, Orientation, + Position, PyfaceColor, PyfaceFont +) # ---------------------------------------------------------------------------- -# Legacy and Wx-specific imports. +# Deferred imports # ---------------------------------------------------------------------------- -# These widgets currently only have Wx implementations -# will return Unimplemented for Qt. +# These imports have the side-effect of performing toolkit selection + +_toolkit_imports = { + 'AboutDialog': 'about_dialog', + 'ApplicationWindow': "application_window", + 'BaseDropHandler': "drop_handler", + 'beep': "beep", + 'Clipboard': "clipboard", + 'ConfirmationDialog': "confirmation_dialog", + 'Dialog': "dialog", + 'DirectoryDialog': "directory_dialog", + 'FileDropHandler': "drop_handler", + 'FileDialog': "file_dialog", + 'GUI': 'gui', + 'HeadingText': "heading_text", + 'ImageCache': "image_cache", + 'ImageResource': "image_resource", + 'KeyPressedEvent': "key_pressed_event", + 'LayeredPanel': "layered_panel", + 'MessageDialog': "message_dialog", + 'ProgressDialog': "progress_dialog", + 'SingleChoiceDialog': "single_choice_dialog", + 'SplashScreen': "splash_screen", + 'SystemMetrics': "system_metrics", + 'Window': "window", + 'Widget': "widget", + + # Wx-only (or legacy) imports + 'ExpandablePanel': "expandable_panel", + 'ImageWidget': "image_widget", + 'MDIApplicationWindow': "mdi_application_window", + 'MDIWindowMenu': "mdi_window_menu", + 'MultiToolbarWindow': "multi_toolbar_window", +} + +# These are pyface.* imports that have selection as a side-effect +# TODO: refactor to delay imports where possible +_relative_imports = { + 'choose_one': "single_choice_dialog", + 'clipboard': "clipboard", + 'confirm': "confirmation_dialog", + 'error': "message_dialog", + 'information': "message_dialog", + 'SplitApplicationWindow': "split_application_window", + 'SplitDialog': "split_dialog", + 'SplitPanel': "split_panel", + 'warning': "message_dialog", +} +_optional_imports = { + 'ArrayImage': ("numpy", "array_image"), + 'PILImage': ("pillow", "pil_image"), + 'PythonEditor': ("pygments", "python_editor"), + 'PythonShell': ("pygments", "python_shell"), +} + + +def __getattr__(name): + """Lazily load attributes with side-effects + + In particular, lazily load toolkit backend names. For efficiency, lazily + loaded objects are injected into the module namespace + """ + # sentinel object for no result + not_found = object() + result = not_found + + if name in _relative_imports: + from importlib import import_module + source = _relative_imports[name] + module = import_module(f"pyface.{source}") + result = getattr(module, name) + + elif name in _toolkit_imports: + from pyface.toolkit import toolkit_object + source = _toolkit_imports[name] + result = toolkit_object(f"{source}:{name}") + + elif name in _optional_imports: + from importlib import import_module + import logging + from pyface.util._optional_dependencies import optional_import + dependency, source = _optional_imports[name] + with optional_import( + dependency, + msg=f"{name} is not available due to missing {dependency}.", + logger=logging.getLogger(__name__), + ): + module = import_module(f"pyface.{source}") + result = getattr(module, name) + + if result is not_found: + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + globals()[name] = result + return result -from .expandable_panel import ExpandablePanel -from .image_widget import ImageWidget -from .mdi_application_window import MDIApplicationWindow -from .mdi_window_menu import MDIWindowMenu -from .multi_toolbar_window import MultiToolbarWindow -# This code isn't toolkit widget code, but is wx-specific -from traits.etsconfig.api import ETSConfig +# ---------------------------------------------------------------------------- +# Introspection support +# ---------------------------------------------------------------------------- -if ETSConfig.toolkit == "wx": +# the list of available names we report for introspection purposes +_extra_names = ( + set(_toolkit_imports) | set(_relative_imports) | set(_optional_imports) +) - # Fix for broken Pycrust introspect module. - # XXX move this somewhere better? - CJW 2017 - from .util import fix_introspect_bug -del ETSConfig +def __dir__(): + return sorted(set(globals()) | _extra_names) diff --git a/pyface/base_toolkit.py b/pyface/base_toolkit.py index ad53b1591..0bc1fff9b 100644 --- a/pyface/base_toolkit.py +++ b/pyface/base_toolkit.py @@ -179,7 +179,7 @@ def import_toolkit(toolkit_name, entry_point="pyface.toolkits"): Returns ------- - toolkit_object : callable + toolkit_object : Callable A callable object that implements the Toolkit interface. Raises @@ -231,7 +231,11 @@ def import_toolkit(toolkit_name, entry_point="pyface.toolkits"): raise RuntimeError(msg) from toolkit_exception -def find_toolkit(entry_point, toolkits=None, priorities=default_priorities): +def find_toolkit( + entry_point="pyface.toolkits", + toolkits=None, + priorities=default_priorities, +): """ Find a toolkit that works. If ETSConfig is set, then attempt to find a matching toolkit. Otherwise @@ -247,12 +251,12 @@ def find_toolkit(entry_point, toolkits=None, priorities=default_priorities): toolkits : collection of strings Only consider toolkits which match the given strings, ignore other ones. - priorities : callable + priorities : Callable A callable function that returns an priority for each plugin. Returns ------- - toolkit : Toolkit instance + toolkit : Toolkit A callable object that implements the Toolkit interface. Raises diff --git a/pyface/data_view/abstract_data_exporter.py b/pyface/data_view/abstract_data_exporter.py index 8602cfad6..6d1e31d44 100644 --- a/pyface/data_view/abstract_data_exporter.py +++ b/pyface/data_view/abstract_data_exporter.py @@ -36,9 +36,9 @@ def add_data(self, data_wrapper, model, indices): Parameters ---------- - data_wrapper : DataWrapper instance + data_wrapper : DataWrapper The data wrapper that will be used to export data. - model : AbstractDataModel instance + model : AbstractDataModel The data model holding the data. indices : list of (row, column) index pairs The indices where the data is to be stored. @@ -55,14 +55,14 @@ def get_data(self, model, indices): Parameters ---------- - model : AbstractDataModel instance + model : AbstractDataModel The data model holding the data. indices : list of (row, column) index pairs The indices where the data is to be stored. Returns ------- - data : any + data : Any The data, of a type that can be serialized by the format. """ raise NotImplementedError() diff --git a/pyface/data_view/abstract_data_model.py b/pyface/data_view/abstract_data_model.py index 6a6d10aac..2690ad861 100644 --- a/pyface/data_view/abstract_data_model.py +++ b/pyface/data_view/abstract_data_model.py @@ -150,7 +150,7 @@ def get_value(self, row, column): Returns ------- - value : any + value : Any The value represented by the given row and column. Raises @@ -202,7 +202,7 @@ def set_value(self, row, column, value): The indices of the row as a sequence from root to leaf. column : sequence of int The indices of the column as a sequence of length 0 or 1. - value : any + value : Any The new value for the given row and column. Raises diff --git a/pyface/data_view/abstract_value_type.py b/pyface/data_view/abstract_value_type.py index 47cd23640..697007f8f 100644 --- a/pyface/data_view/abstract_value_type.py +++ b/pyface/data_view/abstract_value_type.py @@ -94,7 +94,7 @@ def get_editor_value(self, model, row, column): Returns ------- - value : any + value : Any The value to edit. """ return model.get_value(row, column) @@ -113,7 +113,7 @@ def set_editor_value(self, model, row, column, value): The row in the data model being queried. column : sequence of int The column in the data model being queried. - value : any + value : Any The value to set. Raises @@ -228,7 +228,7 @@ def get_color(self, model, row, column): Returns ------- - color : Color instance + color : Color The color associated with the cell. """ return Color(rgba=(1.0, 1.0, 1.0, 1.0)) diff --git a/pyface/data_view/api.py b/pyface/data_view/api.py index 7d19218cc..e4ef206e3 100644 --- a/pyface/data_view/api.py +++ b/pyface/data_view/api.py @@ -65,24 +65,64 @@ """ -from pyface.data_view.abstract_data_exporter import AbstractDataExporter # noqa: 401 -from pyface.data_view.abstract_data_model import AbstractDataModel # noqa: 401 -from pyface.data_view.abstract_value_type import AbstractValueType # noqa: 401 -from pyface.data_view.data_formats import ( # noqa: 401 +from pyface.data_view.abstract_data_exporter import AbstractDataExporter +from pyface.data_view.abstract_data_model import AbstractDataModel +from pyface.data_view.abstract_value_type import AbstractValueType +from pyface.data_view.data_formats import ( csv_column_format, csv_format, csv_row_format, from_csv, from_csv_column, from_csv_row, from_json, from_npy, html_format, npy_format, standard_text_format, to_csv, to_csv_column, to_csv_row, to_json, to_npy, table_format, text_column_format, text_row_format ) -from pyface.data_view.data_view_errors import ( # noqa: 401 +from pyface.data_view.data_view_errors import ( DataViewError, DataViewGetError, DataViewSetError ) -from pyface.data_view.data_view_widget import DataViewWidget # noqa: 401 -from pyface.data_view.data_wrapper import DataWrapper # noqa: 401 -from pyface.data_view.i_data_view_widget import IDataViewWidget # noqa: 401 -from pyface.data_view.i_data_wrapper import ( # noqa: 401 +from pyface.data_view.i_data_view_widget import IDataViewWidget +from pyface.data_view.i_data_wrapper import ( DataFormat, IDataWrapper, text_format ) -from pyface.data_view.index_manager import ( # noqa: 401 +from pyface.data_view.index_manager import ( AbstractIndexManager, IntIndexManager, TupleIndexManager ) + + +# ---------------------------------------------------------------------------- +# Deferred imports +# ---------------------------------------------------------------------------- + +# These imports have the side-effect of performing toolkit selection + +_toolkit_imports = { + 'DataViewWidget': 'data_view_widget', + 'DataWrapper': 'data_wrapper', +} + + +def __getattr__(name): + """Lazily load attributes with side-effects + + In particular, lazily load toolkit backend names. For efficiency, lazily + loaded objects are injected into the module namespace + """ + # sentinel object for no result + not_found = object() + result = not_found + + if name in _toolkit_imports: + from pyface.toolkit import toolkit_object + source = _toolkit_imports[name] + result = toolkit_object(f"data_view.{source}:{name}") + + if result is not_found: + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + globals()[name] = result + return result + + +# ---------------------------------------------------------------------------- +# Introspection support +# ---------------------------------------------------------------------------- + +def __dir__(): + return sorted(set(globals()) | set(_toolkit_imports)) diff --git a/pyface/data_view/data_formats.py b/pyface/data_view/data_formats.py index e2f1f0d69..a5f315404 100644 --- a/pyface/data_view/data_formats.py +++ b/pyface/data_view/data_formats.py @@ -23,9 +23,9 @@ def to_json(data, default=None): Parameters ---------- - data : any + data : Any The data to be serialized. - default : callable or None + default : Callable or None Callable that takes a Python object and returns a JSON-serializable data structure. @@ -45,13 +45,13 @@ def from_json(raw_data, object_hook=None): ---------- raw_data : bytes The serialized JSON data as a byte string. - object_hook : callable + object_hook : Callable Callable that takes a dictionary and returns an corresponding Python object. Returns ------- - data : any + data : Any The data extracted. """ return json.loads(raw_data.decode('utf-8'), object_hook=object_hook) diff --git a/pyface/data_view/data_models/array_data_model.py b/pyface/data_view/data_models/array_data_model.py index dce74f47a..3b48afd72 100644 --- a/pyface/data_view/data_models/array_data_model.py +++ b/pyface/data_view/data_models/array_data_model.py @@ -205,7 +205,7 @@ def set_value(self, row, column, value): Returns ------- - value : any + value : Any The value represented by the given row and column. """ if self.can_set_value(row, column): diff --git a/pyface/data_view/data_models/data_accessors.py b/pyface/data_view/data_models/data_accessors.py index cb23ee0da..4b375c40e 100644 --- a/pyface/data_view/data_models/data_accessors.py +++ b/pyface/data_view/data_models/data_accessors.py @@ -52,12 +52,12 @@ def get_value(self, obj): Parameters ---------- - obj : any + obj : Any The object that contains the data. Returns ------- - value : any + value : Any The data value contained in the object. """ raise NotImplementedError() @@ -67,7 +67,7 @@ def can_set_value(self, obj): Parameters ---------- - obj : any + obj : Any The object that contains the data. Returns @@ -82,9 +82,9 @@ def set_value(self, obj, value): Parameters ---------- - obj : any + obj : Any The object that contains the data. - value : any + value : Any The data value to set. Raises @@ -119,12 +119,12 @@ def get_value(self, obj): Parameters ---------- - obj : any + obj : Any An object. Returns ------- - value : any + value : Any The data value contained in this class' value trait. """ return self.value @@ -149,12 +149,12 @@ def get_value(self, obj): Parameters ---------- - obj : any + obj : Any The object that contains the data. Returns ------- - value : any + value : Any The data value contained in the object's attribute. """ return xgetattr(obj, self.attr) @@ -164,7 +164,7 @@ def can_set_value(self, obj): Parameters ---------- - obj : any + obj : Any The object that contains the data. Returns @@ -211,7 +211,7 @@ def get_value(self, obj): Returns ------- - value : any + value : Any The data value contained in the object at the index. """ return obj[self.index] @@ -221,7 +221,7 @@ def can_set_value(self, obj): Parameters ---------- - obj : any + obj : Any The object that contains the data. Returns @@ -236,9 +236,9 @@ def set_value(self, obj, value): Parameters ---------- - obj : any + obj : Any The object that contains the data. - value : any + value : Any The data value to set. Raises @@ -280,7 +280,7 @@ def get_value(self, obj): Returns ------- - value : any + value : Any The data value contained in the given key of the object. """ return obj[self.key] @@ -292,7 +292,7 @@ def can_set_value(self, obj): ---------- obj : mapping The object that contains the data. - value : any + value : Any The data value to set. Raises @@ -307,9 +307,9 @@ def set_value(self, obj, value): Parameters ---------- - obj : any + obj : Any The object that contains the data. - value : any + value : Any The data value to set. Raises diff --git a/pyface/data_view/data_models/row_table_data_model.py b/pyface/data_view/data_models/row_table_data_model.py index e9abf39eb..f9899055e 100644 --- a/pyface/data_view/data_models/row_table_data_model.py +++ b/pyface/data_view/data_models/row_table_data_model.py @@ -111,7 +111,7 @@ def get_value(self, row, column): Returns ------- - value : any + value : Any The value represented by the given row and column. """ if len(column) == 0: @@ -162,7 +162,7 @@ def set_value(self, row, column, value): The indices of the row as a sequence from root to leaf. column : sequence of int The indices of the column as a sequence of length 0 or 1. - value : any + value : Any The new value for the given row and column. Raises diff --git a/pyface/data_view/exporters/item_exporter.py b/pyface/data_view/exporters/item_exporter.py index 12b5bf2ec..d648afdd7 100644 --- a/pyface/data_view/exporters/item_exporter.py +++ b/pyface/data_view/exporters/item_exporter.py @@ -26,9 +26,9 @@ def add_data(self, data_wrapper, model, indices): Parameters ---------- - data_wrapper : DataWrapper instance + data_wrapper : DataWrapper The data wrapper that will be used to export data. - model : AbstractDataModel instance + model : AbstractDataModel The data model holding the data. indices : list of (row, column) index pairs The indices where the data is to be stored. @@ -42,14 +42,14 @@ def get_data(self, model, indices): Parameters ---------- - model : AbstractDataModel instance + model : AbstractDataModel The data model holding the data. indices : list of (row, column) index pairs The indices where the data is to be stored. Returns ------- - data : any + data : Any The data, of a type that can be serialized by the format. """ if len(indices) != 1: diff --git a/pyface/data_view/exporters/row_exporter.py b/pyface/data_view/exporters/row_exporter.py index f5cd9accb..e1bcd8e95 100644 --- a/pyface/data_view/exporters/row_exporter.py +++ b/pyface/data_view/exporters/row_exporter.py @@ -44,14 +44,14 @@ def get_data(self, model, indices): Parameters ---------- - model : AbstractDataModel instance + model : AbstractDataModel The data model holding the data. indices : list of (row, column) index pairs The indices where the data is to be stored. Returns ------- - data : any + data : Any The data, of a type that can be serialized by the format. """ rows = sorted({row for row, column in indices}) diff --git a/pyface/data_view/i_data_wrapper.py b/pyface/data_view/i_data_wrapper.py index 5152a95ed..ed5bba885 100644 --- a/pyface/data_view/i_data_wrapper.py +++ b/pyface/data_view/i_data_wrapper.py @@ -93,7 +93,7 @@ def get_format(self, format): Returns ------- - data : any + data : Any The data decoded for the given format. """ raise NotImplementedError() @@ -105,7 +105,7 @@ def set_format(self, format, data): ---------- format : DataFormat A data format object. - data : any + data : Any The data to be encoded and stored. """ raise NotImplementedError() @@ -171,7 +171,7 @@ def get_format(self, format): Returns ------- - data : any + data : Any The data decoded for the given format. """ return format.deserialize(self.get_mimedata(format.mimetype)) @@ -183,7 +183,7 @@ def set_format(self, format, data): ---------- format : DataFormat A data format object. - data : any + data : Any The data to be encoded and stored. """ self.set_mimedata(format.mimetype, format.serialize(data)) diff --git a/pyface/data_view/value_types/color_value.py b/pyface/data_view/value_types/color_value.py index e8f4dae12..900500c44 100644 --- a/pyface/data_view/value_types/color_value.py +++ b/pyface/data_view/value_types/color_value.py @@ -34,7 +34,7 @@ def is_valid(self, model, row, column, value): The row in the data model being queried. column : sequence of int The column in the data model being queried. - value : any + value : Any The value to test. Returns @@ -168,7 +168,7 @@ def get_color(self, model, row, column): Returns ------- - color : Color instance + color : Color The color associated with the cell. """ return model.get_value(row, column) diff --git a/pyface/data_view/value_types/constant_value.py b/pyface/data_view/value_types/constant_value.py index 855a24e95..1d6bc4133 100644 --- a/pyface/data_view/value_types/constant_value.py +++ b/pyface/data_view/value_types/constant_value.py @@ -79,7 +79,7 @@ def get_color(self, model, row, column): Returns ------- - color : Color instance + color : Color The color associated with the cell. """ return self.color diff --git a/pyface/data_view/value_types/editable_value.py b/pyface/data_view/value_types/editable_value.py index 918cdf961..f03b29425 100644 --- a/pyface/data_view/value_types/editable_value.py +++ b/pyface/data_view/value_types/editable_value.py @@ -40,7 +40,7 @@ def is_valid(self, model, row, column, value): The row in the data model being queried. column : sequence of int The column in the data model being queried. - value : any + value : Any The value to validate. Returns @@ -85,7 +85,7 @@ def set_editor_value(self, model, row, column, value): The row in the data model being set. column : sequence of int The column in the data model being set. - value : any + value : Any The value being set. Raises diff --git a/pyface/data_view/value_types/enum_value.py b/pyface/data_view/value_types/enum_value.py index 45a634347..3ff260ab2 100644 --- a/pyface/data_view/value_types/enum_value.py +++ b/pyface/data_view/value_types/enum_value.py @@ -43,7 +43,7 @@ def is_valid(self, model, row, column, value): The row in the data model being queried. column : sequence of int The column in the data model being queried. - value : any + value : Any The value to validate. Returns @@ -127,7 +127,7 @@ def get_color(self, model, row, column): Returns ------- - color : Color instance + color : Color The color associated with the cell. """ return self.colors(model.get_value(row, column)) diff --git a/pyface/data_view/value_types/numeric_value.py b/pyface/data_view/value_types/numeric_value.py index 447f0e989..b13d46317 100644 --- a/pyface/data_view/value_types/numeric_value.py +++ b/pyface/data_view/value_types/numeric_value.py @@ -51,7 +51,7 @@ def is_valid(self, model, row, column, value): The row in the data model being queried. column : sequence of int The column in the data model being queried. - value : any + value : Any The value to validate. Returns diff --git a/pyface/dock/dock_window_feature.py b/pyface/dock/dock_window_feature.py index 82f5a6528..34fff4500 100644 --- a/pyface/dock/dock_window_feature.py +++ b/pyface/dock/dock_window_feature.py @@ -302,7 +302,7 @@ def drop(self, object): Parameters ---------- - object : any object + object : Any object The object being dropped onto the feature image Returns @@ -329,7 +329,7 @@ def can_drop(self, object): Parameters ---------- - object : any object + object : Any object The object being dragged onto the feature image Returns diff --git a/pyface/fields/api.py b/pyface/fields/api.py index eebe2c632..4287c9038 100644 --- a/pyface/fields/api.py +++ b/pyface/fields/api.py @@ -14,6 +14,10 @@ - :class:`~.CheckBoxField` - :class:`~.ComboField` +- :class:`~.EditableField` +- :class:`~.Field` +- :class:`~.ImageField` +- :class:`~.LabelField` - :class:`~.RadioButtonField` - :class:`~.SpinField` - :class:`~.TextField` @@ -23,7 +27,10 @@ Interfaces ---------- - :class:`~.IComboField` +- :class:`~.IEditableField` - :class:`~.IField` +- :class:`~.IImageField` +- :class:`~.ILabelField` - :class:`~.ISpinField` - :class:`~.ITextField` - :class:`~.ITimeField` @@ -32,16 +39,62 @@ """ from .i_combo_field import IComboField +from .i_editable_field import IEditableField from .i_field import IField +from .i_image_field import IImageField +from .i_label_field import ILabelField from .i_spin_field import ISpinField from .i_text_field import ITextField from .i_time_field import ITimeField from .i_toggle_field import IToggleField -from .combo_field import ComboField -from .spin_field import SpinField -from .text_field import TextField -from .time_field import TimeField -from .toggle_field import ( - CheckBoxField, RadioButtonField, ToggleButtonField, -) + +# ---------------------------------------------------------------------------- +# Deferred imports +# ---------------------------------------------------------------------------- + +# These imports have the side-effect of performing toolkit selection + +_toolkit_imports = { + 'CheckBoxField': 'toggle_field', + 'ComboField': 'combo_field', + 'EditableField': 'editable_field', + 'Field': 'field', + 'ImageField': 'image_field', + 'LabelField': 'label_field', + 'RadioButtonField': 'toggle_field', + 'SpinField': 'spin_field', + 'TextField': 'text_field', + 'TimeField': 'time_field', + 'ToggleButtonField': 'toggle_field', +} + + +def __getattr__(name): + """Lazily load attributes with side-effects + + In particular, lazily load toolkit backend names. For efficiency, lazily + loaded objects are injected into the module namespace + """ + # sentinel object for no result + not_found = object() + result = not_found + + if name in _toolkit_imports: + from pyface.toolkit import toolkit_object + source = _toolkit_imports[name] + result = toolkit_object(f"fields.{source}:{name}") + + if result is not_found: + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + globals()[name] = result + return result + + +# ---------------------------------------------------------------------------- +# Introspection support +# ---------------------------------------------------------------------------- + +def __dir__(): + return sorted(set(globals()) | set(_toolkit_imports)) diff --git a/pyface/fields/i_combo_field.py b/pyface/fields/i_combo_field.py index 2220b85c7..18839fafd 100644 --- a/pyface/fields/i_combo_field.py +++ b/pyface/fields/i_combo_field.py @@ -13,10 +13,10 @@ from traits.api import Callable, HasTraits, Enum, List -from pyface.fields.i_field import IField +from pyface.fields.i_editable_field import IEditableField -class IComboField(IField): +class IComboField(IEditableField): """ The combo field interface. This is for comboboxes holding a list of values. @@ -64,7 +64,6 @@ def __init__(self, values, **traits): def _initialize_control(self): super()._initialize_control() self._set_control_values(self.values) - self._set_control_value(self.value) def _add_event_listeners(self): """ Set up toolkit-specific bindings for events """ @@ -72,13 +71,9 @@ def _add_event_listeners(self): self.observe( self._values_updated, "values.items,formatter", dispatch="ui" ) - if self.control is not None: - self._observe_control_value() def _remove_event_listeners(self): """ Remove toolkit-specific bindings for events """ - if self.control is not None: - self._observe_control_value(remove=True) self.observe( self._values_updated, "values.items,formatter", diff --git a/pyface/fields/i_editable_field.py b/pyface/fields/i_editable_field.py new file mode 100644 index 000000000..6e28bb14e --- /dev/null +++ b/pyface/fields/i_editable_field.py @@ -0,0 +1,62 @@ +# (C) Copyright 2005-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! + +""" The editable field interface. """ + + +from traits.api import HasTraits + +from .i_field import IField + + +class IEditableField(IField): + """ The editable field interface. + + A editable field is a widget that displays a user-editable value. + """ + + +class MEditableField(HasTraits): + """The editable field mix-in. + + Classes which use this mixin should implement _observe_control_value to + connect a toolkit handler that calls _update_value. + """ + + # ------------------------------------------------------------------------ + # IWidget interface + # ------------------------------------------------------------------------ + + def _add_event_listeners(self): + """ Set up toolkit-specific bindings for events """ + super()._add_event_listeners() + self._observe_control_value() + + def _remove_event_listeners(self): + """ Remove toolkit-specific bindings for events """ + self._observe_control_value(remove=True) + super()._remove_event_listeners() + + # ------------------------------------------------------------------------ + # Private interface + # ------------------------------------------------------------------------ + + def _update_value(self, value): + """ Handle a change to the value from user interaction + + This is a method suitable for calling from a toolkit event handler. + """ + self.value = self._get_control_value() + + # Toolkit control interface --------------------------------------------- + + def _observe_control_value(self, remove=False): + """ Toolkit specific method to change the control value observer. """ + raise NotImplementedError() diff --git a/pyface/fields/i_field.py b/pyface/fields/i_field.py index 4380bf1d4..06e029110 100644 --- a/pyface/fields/i_field.py +++ b/pyface/fields/i_field.py @@ -14,6 +14,7 @@ from traits.api import Any, HasTraits from pyface.i_layout_widget import ILayoutWidget +from pyface.ui_traits import Alignment class IField(ILayoutWidget): @@ -26,6 +27,9 @@ class IField(ILayoutWidget): #: The value held by the field. value = Any() + #: The alignment of the field's content. + alignment = Alignment() + class MField(HasTraits): """ The field mix-in. """ @@ -33,44 +37,52 @@ class MField(HasTraits): #: The value held by the field. value = Any() + #: The alignment of the text in the field. + alignment = Alignment() + # ------------------------------------------------------------------------ # IWidget interface # ------------------------------------------------------------------------ + def create(self, parent=None): + """ Creates the toolkit specific control. + + This method should create the control and assign it to the + :py:attr:``control`` trait. + """ + super().create(parent=parent) + + self.show(self.visible) + self.enable(self.enabled) + + def _initialize_control(self): + """ Perform any post-creation initialization for the control. + """ + super()._initialize_control() + self._set_control_value(self.value) + if self.alignment != 'default': + self._set_control_alignment(self.alignment) + def _add_event_listeners(self): """ Set up toolkit-specific bindings for events """ super()._add_event_listeners() self.observe(self._value_updated, "value", dispatch="ui") + self.observe(self._alignment_updated, "alignment", dispatch="ui") def _remove_event_listeners(self): """ Remove toolkit-specific bindings for events """ self.observe( self._value_updated, "value", dispatch="ui", remove=True ) + self.observe( + self._alignment_updated, "alignment", dispatch="ui", remove=True + ) super()._remove_event_listeners() # ------------------------------------------------------------------------ # Private interface # ------------------------------------------------------------------------ - def create(self, parent=None): - """ Creates the toolkit specific control. - - This method should create the control and assign it to the - :py:attr:``control`` trait. - """ - super().create(parent=parent) - - self.show(self.visible) - self.enable(self.enabled) - - def _update_value(self, value): - """ Handle a change to the value from user interaction - - This is a method suitable for calling from a toolkit event handler. - """ - self.value = self._get_control_value() - def _get_control(self): """ If control is not passed directly, get it from the trait. """ control = self.control @@ -88,8 +100,12 @@ def _set_control_value(self, value): """ Toolkit specific method to set the control's value. """ raise NotImplementedError() - def _observe_control_value(self, remove=False): - """ Toolkit specific method to change the control value observer. """ + def _get_control_alignment(self): + """ Toolkit specific method to get the control's read_only state. """ + raise NotImplementedError() + + def _set_control_alignment(self, alignment): + """ Toolkit specific method to set the control's alignment. """ raise NotImplementedError() # Trait change handlers ------------------------------------------------- @@ -98,3 +114,8 @@ def _value_updated(self, event): value = event.new if self.control is not None: self._set_control_value(value) + + def _alignment_updated(self, event): + alignment = event.new + if self.control is not None: + self._set_control_alignment(alignment) diff --git a/pyface/fields/i_image_field.py b/pyface/fields/i_image_field.py new file mode 100644 index 000000000..3e45509fb --- /dev/null +++ b/pyface/fields/i_image_field.py @@ -0,0 +1,36 @@ +# (C) Copyright 2005-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! + +""" The image field interface. """ + +from traits.api import Any, HasTraits + +from pyface.fields.i_field import IField +from pyface.ui_traits import Image + + +class IImageField(IField): + """ The image field interface. + + This is for a field that edits a IImage value. + """ + + #: The current value of the image field + value = Image() + + +class MImageField(HasTraits): + """ Mixin class for ImageField implementations """ + + #: The current value of the image field + value = Image() + + #: The toolkit image to display + _toolkit_value = Any() diff --git a/pyface/fields/i_label_field.py b/pyface/fields/i_label_field.py new file mode 100644 index 000000000..437145917 --- /dev/null +++ b/pyface/fields/i_label_field.py @@ -0,0 +1,67 @@ +# (C) Copyright 2005-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! + +""" The label field interface. """ + + +from traits.api import HasTraits, Str + +from pyface.fields.i_field import IField +from pyface.ui_traits import Image + + +class ILabelField(IField): + """ The label field interface. """ + + #: The value held by the field. + value = Str() + + #: The icon to display with the toggle. + icon = Image() + + +class MLabelField(HasTraits): + """ The text field mix-in. """ + + #: The value held by the field. + value = Str() + + #: The icon to display with the toggle. + icon = Image() + + # ------------------------------------------------------------------------ + # Private interface + # ------------------------------------------------------------------------ + + def _initialize_control(self): + super()._initialize_control() + self._set_control_icon(self.icon) + + def _add_event_listeners(self): + """ Set up toolkit-specific bindings for events """ + super()._add_event_listeners() + self.observe(self._icon_updated, "icon", dispatch="ui") + + def _remove_event_listeners(self): + """ Remove toolkit-specific bindings for events """ + self.observe(self._icon_updated, "icon", dispatch="ui", remove=True) + super()._remove_event_listeners() + + # Toolkit control interface --------------------------------------------- + + def _set_control_icon(self, icon): + """ Toolkit specific method to set the control's icon. """ + raise NotImplementedError() + + # Trait change handlers ------------------------------------------------- + + def _icon_updated(self, event): + if self.control is not None: + self._set_control_icon(self.icon) diff --git a/pyface/fields/i_spin_field.py b/pyface/fields/i_spin_field.py index 37127e4a4..35d9e43ad 100644 --- a/pyface/fields/i_spin_field.py +++ b/pyface/fields/i_spin_field.py @@ -11,12 +11,12 @@ """ The spin field interface. """ -from traits.api import HasTraits, Int, Property, Range, Tuple +from traits.api import Bool, HasTraits, Int, Property, Range, Tuple -from pyface.fields.i_field import IField +from pyface.fields.i_editable_field import IEditableField -class ISpinField(IField): +class ISpinField(IEditableField): """ The spin field interface. This is for spinners holding integer values. @@ -34,6 +34,9 @@ class ISpinField(IField): #: The maximum value maximum = Property(Int, observe="bounds") + #: Whether the values wrap around at maximum and minimum. + wrap = Bool() + class MSpinField(HasTraits): @@ -49,6 +52,9 @@ class MSpinField(HasTraits): #: The maximum value for the spinner maximum = Property(Int, observe="bounds") + #: Whether the values wrap around at maximum and minimum. + wrap = Bool() + # ------------------------------------------------------------------------ # object interface # ------------------------------------------------------------------------ @@ -69,21 +75,24 @@ def _initialize_control(self): super()._initialize_control() self._set_control_bounds(self.bounds) self._set_control_value(self.value) + self._set_control_wrap(self.wrap) def _add_event_listeners(self): """ Set up toolkit-specific bindings for events """ super()._add_event_listeners() self.observe(self._bounds_updated, "bounds", dispatch="ui") + self.observe(self._wrap_updated, "wrap", dispatch="ui") if self.control is not None: self._observe_control_value() def _remove_event_listeners(self): """ Remove toolkit-specific bindings for events """ - if self.control is not None: - self._observe_control_value(remove=True) self.observe( self._bounds_updated, "bounds", dispatch="ui", remove=True ) + self.observe( + self._wrap_updated, "wrap", dispatch="ui", remove=True + ) super()._remove_event_listeners() # Toolkit control interface --------------------------------------------- @@ -96,6 +105,14 @@ def _set_control_bounds(self, bounds): """ Toolkit specific method to set the control's bounds. """ raise NotImplementedError() + def _get_control_wrap(self): + """ Toolkit specific method to get whether the control wraps. """ + raise NotImplementedError + + def _set_control_wrap(self, wrap): + """ Toolkit specific method to set whether the control wraps. """ + raise NotImplementedError + # Trait property handlers ----------------------------------------------- def _get_minimum(self): @@ -130,3 +147,7 @@ def _value_default(self): def _bounds_updated(self, event): if self.control is not None: self._set_control_bounds(self.bounds) + + def _wrap_updated(self, event): + if self.control is not None: + self._set_control_wrap(event.new) diff --git a/pyface/fields/i_text_field.py b/pyface/fields/i_text_field.py index eff0066bc..0e6427642 100644 --- a/pyface/fields/i_text_field.py +++ b/pyface/fields/i_text_field.py @@ -13,10 +13,10 @@ from traits.api import Bool, Enum, HasTraits, Str -from pyface.fields.i_field import IField +from pyface.fields.i_editable_field import IEditableField -class ITextField(IField): +class ITextField(IEditableField): """ The text field interface. """ #: The value held by the field. @@ -58,13 +58,11 @@ class MTextField(HasTraits): # ------------------------------------------------------------------------ def _initialize_control(self): + super()._initialize_control() self._set_control_echo(self.echo) - self._set_control_value(self.value) self._set_control_placeholder(self.placeholder) self._set_control_read_only(self.read_only) - super()._initialize_control() - def _add_event_listeners(self): """ Set up toolkit-specific bindings for events """ super()._add_event_listeners() @@ -74,17 +72,15 @@ def _add_event_listeners(self): self.observe(self._read_only_updated, "read_only", dispatch="ui") if self.control is not None: if self.update_text == "editing_finished": + self._observe_control_value(remove=True) self._observe_control_editing_finished() - else: - self._observe_control_value() def _remove_event_listeners(self): """ Remove toolkit-specific bindings for events """ if self.control is not None: if self.update_text == "editing_finished": self._observe_control_editing_finished(remove=True) - else: - self._observe_control_value(remove=True) + self._observe_control_value() self.observe( self._update_text_updated, "update_text", diff --git a/pyface/fields/i_time_field.py b/pyface/fields/i_time_field.py index 18cf40a24..3d63d79b7 100644 --- a/pyface/fields/i_time_field.py +++ b/pyface/fields/i_time_field.py @@ -14,10 +14,10 @@ from traits.api import HasTraits, Time -from pyface.fields.i_field import IField +from pyface.fields.i_editable_field import IEditableField -class ITimeField(IField): +class ITimeField(IEditableField): """ The time field interface. This is for a field that edits a datetime.time value. @@ -37,22 +37,6 @@ class MTimeField(HasTraits): # Private interface # ------------------------------------------------------------------------ - def _initialize_control(self): - super(MTimeField, self)._initialize_control() - self._set_control_value(self.value) - - def _add_event_listeners(self): - """ Set up toolkit-specific bindings for events """ - super(MTimeField, self)._add_event_listeners() - if self.control is not None: - self._observe_control_value() - - def _remove_event_listeners(self): - """ Remove toolkit-specific bindings for events """ - if self.control is not None: - self._observe_control_value(remove=True) - super(MTimeField, self)._remove_event_listeners() - # Trait defaults -------------------------------------------------------- def _value_default(self): diff --git a/pyface/fields/i_toggle_field.py b/pyface/fields/i_toggle_field.py index 017b4f7bd..be68a1228 100644 --- a/pyface/fields/i_toggle_field.py +++ b/pyface/fields/i_toggle_field.py @@ -13,11 +13,11 @@ from traits.api import Bool, HasTraits, Str -from pyface.fields.i_field import IField +from pyface.fields.i_editable_field import IEditableField from pyface.ui_traits import Image -class IToggleField(IField): +class IToggleField(IEditableField): """ The toggle field interface. This is for a toggle between two states, represented by a boolean value. @@ -62,13 +62,9 @@ def _add_event_listeners(self): super()._add_event_listeners() self.observe(self._text_updated, "text", dispatch="ui") self.observe(self._icon_updated, "icon", dispatch="ui") - if self.control is not None: - self._observe_control_value() def _remove_event_listeners(self): """ Remove toolkit-specific bindings for events """ - if self.control is not None: - self._observe_control_value(remove=True) self.observe(self._text_updated, "text", dispatch="ui", remove=True) self.observe(self._icon_updated, "icon", dispatch="ui", remove=True) super()._remove_event_listeners() diff --git a/pyface/fields/image_field.py b/pyface/fields/image_field.py new file mode 100644 index 000000000..3f756a098 --- /dev/null +++ b/pyface/fields/image_field.py @@ -0,0 +1,16 @@ +# (C) Copyright 2005-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! + +""" The image field widget. """ + +# Import the toolkit specific version. +from pyface.toolkit import toolkit_object + +ImageField = toolkit_object("fields.image_field:ImageField") diff --git a/pyface/fields/label_field.py b/pyface/fields/label_field.py new file mode 100644 index 000000000..d1d4dba11 --- /dev/null +++ b/pyface/fields/label_field.py @@ -0,0 +1,16 @@ +# (C) Copyright 2005-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! + +""" The label field widget. """ + +# Import the toolkit specific version. +from pyface.toolkit import toolkit_object + +LabelField = toolkit_object("fields.label_field:LabelField") diff --git a/pyface/fields/tests/field_mixin.py b/pyface/fields/tests/field_mixin.py index 132cfcc0c..80d4421bd 100644 --- a/pyface/fields/tests/field_mixin.py +++ b/pyface/fields/tests/field_mixin.py @@ -14,4 +14,11 @@ class FieldMixin(LayoutWidgetMixin): """ Mixin which provides standard methods for all fields. """ - pass + + def test_text_field_alignment(self): + self._create_widget_control() + + self.widget.alignment = 'right' + self.gui.process_events() + + self.assertEqual(self.widget._get_control_alignment(), 'right') diff --git a/pyface/fields/tests/test_api.py b/pyface/fields/tests/test_api.py new file mode 100644 index 000000000..0b37df01e --- /dev/null +++ b/pyface/fields/tests/test_api.py @@ -0,0 +1,35 @@ +# (C) Copyright 2005-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! + +""" Test for pyface.api """ + +import unittest + + +class TestApi(unittest.TestCase): + """ Test importable items in any environment.""" + + def test_api_importable(self): + # make sure api is importable with the most minimal + # required dependencies, including in the absence of toolkit backends. + from pyface.fields import api # noqa: F401 + + def test_public_attrs(self): + # make sure everything advertised by dir() is available except optional + from pyface.fields import api + + attrs = [ + name + for name in dir(api) + if not name.startswith('_') + ] + for attr in attrs: + with self.subTest(attr=attr): + self.assertIsNotNone(getattr(api, attr, None)) diff --git a/pyface/fields/tests/test_image_field.py b/pyface/fields/tests/test_image_field.py new file mode 100644 index 000000000..7b32c6919 --- /dev/null +++ b/pyface/fields/tests/test_image_field.py @@ -0,0 +1,39 @@ +# (C) Copyright 2005-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! + + +import unittest + +from traits.testing.optional_dependencies import numpy as np, requires_numpy + +from ..image_field import ImageField +from .field_mixin import FieldMixin + + +@requires_numpy +class ImageFieldMixin(FieldMixin, unittest.TestCase): + + def setUp(self): + super().setUp() + self.data = np.full((32, 64, 4), 0xee, dtype='uint8') + + def _create_widget_simple(self, **traits): + traits.setdefault("tooltip", "Dummy") + return ImageField(**traits) + + # Tests ------------------------------------------------------------------ + + def test_image_field(self): + self._create_widget_control() + + self.widget.value = 'splash' + self.gui.process_events() + + self.assertIsNotNone(self.widget._get_control_value()) diff --git a/pyface/fields/tests/test_label_field.py b/pyface/fields/tests/test_label_field.py new file mode 100644 index 000000000..219c0d1ab --- /dev/null +++ b/pyface/fields/tests/test_label_field.py @@ -0,0 +1,43 @@ +# (C) Copyright 2005-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! + + +import unittest + + +from pyface.image_resource import ImageResource +from ..label_field import LabelField +from .field_mixin import FieldMixin + + +class TestLabelField(FieldMixin, unittest.TestCase): + + def _create_widget_simple(self, **traits): + traits.setdefault("value", "Label") + traits.setdefault("tooltip", "Dummy") + return LabelField(**traits) + + # Tests ------------------------------------------------------------------ + + def test_label_field(self): + self._create_widget_control() + + self.widget.value = "Test" + self.gui.process_events() + + self.assertEqual(self.widget._get_control_value(), "Test") + + def test_label_field_image(self): + self._create_widget_control() + image = ImageResource("question") + + # XXX can't validate icon values currently, so just a smoke test + self.widget.image = image + self.gui.process_events() diff --git a/pyface/fields/tests/test_spin_field.py b/pyface/fields/tests/test_spin_field.py index ec7cda343..2ada52e93 100644 --- a/pyface/fields/tests/test_spin_field.py +++ b/pyface/fields/tests/test_spin_field.py @@ -51,3 +51,11 @@ def test_spin_field_bounds(self): self.assertEqual(self.widget._get_control_bounds(), (10, 50)) self.assertEqual(self.widget._get_control_value(), 10) self.assertEqual(self.widget.value, 10) + + def test_spin_field_wrap(self): + self._create_widget_control() + + self.widget.wrap = True + self.gui.process_events() + + self.assertTrue(self.widget._get_control_wrap()) diff --git a/pyface/fields/tests/test_text_field.py b/pyface/fields/tests/test_text_field.py index 53230b4e4..f995e01e4 100644 --- a/pyface/fields/tests/test_text_field.py +++ b/pyface/fields/tests/test_text_field.py @@ -52,7 +52,7 @@ def test_text_field_echo(self): self.assertEqual(self.widget._get_control_echo(), "password") @unittest.skipIf( - is_wx, "Can't change password mode for wx after control " "creation." + is_wx, "Can't change password mode for wx after control creation." ) def test_text_field_echo_change(self): self._create_widget_control() @@ -71,17 +71,6 @@ def test_text_field_placeholder(self): self.assertEqual(self.widget._get_control_placeholder(), "test") def test_text_field_readonly(self): - self.widget.read_only = True - self._create_widget_control() - - self.gui.process_events() - - self.assertEqual(self.widget._get_control_read_only(), True) - - @unittest.skipIf( - is_wx, "Can't change read_only mode for wx after control " "creation." - ) - def test_text_field_readonly_change(self): self._create_widget_control() self.widget.read_only = True diff --git a/pyface/font.py b/pyface/font.py index 5d4434930..7d287f00e 100644 --- a/pyface/font.py +++ b/pyface/font.py @@ -247,6 +247,11 @@ def __init__(self, default_value=NoDefaultSpecified, **metadata): super().__init__(default_value, **metadata) def validate(self, object, name, value): + """Validate the trait + + This is a CFloat trait that also accepts percentage strings. Values + must be in the range 50 to 200 inclusive + """ if isinstance(value, str) and value.endswith('%'): value = value[:-1] value = STRETCHES.get(value, value) @@ -256,6 +261,7 @@ def validate(self, object, name, value): return value def info(self): + """Describe the trait""" info = ( "a float from 50 to 200, " "a value that can convert to a float from 50 to 200, " @@ -284,6 +290,11 @@ def __init__(self, default_value=NoDefaultSpecified, **metadata): super().__init__(default_value, **metadata) def validate(self, object, name, value): + """Validate the trait + + This is a CFloat trait that also accepts strings with 'pt' or 'px' + suffixes. Values must be positive. + """ if ( isinstance(value, str) and (value.endswith('pt') or value.endswith('px')) @@ -296,6 +307,7 @@ def validate(self, object, name, value): return value def info(self): + """Describe the trait""" info = ( "a positive float, a value that can convert to a positive float, " ) @@ -343,7 +355,7 @@ def from_toolkit(cls, toolkit_font): Parameters ---------- - toolkit_font : any + toolkit_font : Any A toolkit font to be converted to a corresponding class instance, within the limitations of the options supported by the class. """ @@ -358,7 +370,7 @@ def to_toolkit(self): Returns ------- - toolkit_font : any + toolkit_font : Any A toolkit font which matches the property of the font as closely as possible given the constraints of the toolkit. """ diff --git a/pyface/gui_application.py b/pyface/gui_application.py index 1f285c948..ee4e9a46c 100644 --- a/pyface/gui_application.py +++ b/pyface/gui_application.py @@ -116,7 +116,7 @@ def create_window(self, **kwargs): Returns ------- - window : IWindow instance or None + window : IWindow or None The new IWindow instance. """ window = self.window_factory(application=self, **kwargs) diff --git a/pyface/i_application_window.py b/pyface/i_application_window.py index 2a5470d7d..5f32ddae1 100644 --- a/pyface/i_application_window.py +++ b/pyface/i_application_window.py @@ -13,8 +13,9 @@ from traits.api import HasTraits, Instance, List - -from pyface.action.api import MenuBarManager, StatusBarManager, ToolBarManager +from pyface.action.i_menu_bar_manager import IMenuBarManager +from pyface.action.i_status_bar_manager import IStatusBarManager +from pyface.action.i_tool_bar_manager import IToolBarManager from pyface.i_window import IWindow from pyface.ui_traits import Image @@ -25,10 +26,10 @@ class IApplicationWindow(IWindow): The application window has support for a menu bar, tool bar and a status bar (all of which are optional). - Usage + Notes ----- - Create a sub-class of this class and override the + To use, create a sub-class of this class and override the :py:meth:`._create_contents` method. """ @@ -37,18 +38,14 @@ class IApplicationWindow(IWindow): #: The window icon. The default is toolkit specific. icon = Image() - #: The menu bar manager (None iff there is no menu bar). - menu_bar_manager = Instance(MenuBarManager) - - #: The status bar manager (None iff there is no status bar). - status_bar_manager = Instance(StatusBarManager) + #: The menu bar manager for the window. + menu_bar_manager = Instance(IMenuBarManager) - #: The tool bar manager (None iff there is no tool bar). - tool_bar_manager = Instance(ToolBarManager) + #: The status bar manager for the window. + status_bar_manager = Instance(IStatusBarManager) - #: If the underlying toolkit supports multiple toolbars, you can use this - #: list instead of the single ToolBarManager instance above. - tool_bar_managers = List(ToolBarManager) + #: The collection of tool bar managers for the window. + tool_bar_managers = List(Instance(IToolBarManager)) # ------------------------------------------------------------------------ # Protected 'IApplicationWindow' interface. @@ -116,6 +113,18 @@ class MApplicationWindow(HasTraits): Implements: destroy(), _create_trim_widgets() """ + #: The icon to display in the application window title bar. + icon = Image() + + #: The menu bar manager for the window. + menu_bar_manager = Instance(IMenuBarManager) + + #: The status bar manager for the window. + status_bar_manager = Instance(IStatusBarManager) + + #: The collection of tool bar managers for the window. + tool_bar_managers = List(Instance(IToolBarManager)) + # ------------------------------------------------------------------------ # 'IWidget' interface. # ------------------------------------------------------------------------ @@ -126,9 +135,6 @@ def destroy(self): if self.menu_bar_manager is not None: self.menu_bar_manager.destroy() - if self.tool_bar_manager is not None: - self.tool_bar_manager.destroy() - if self.status_bar_manager is not None: self.status_bar_manager.destroy() diff --git a/pyface/i_gui.py b/pyface/i_gui.py index b950b617f..9b5fa6771 100644 --- a/pyface/i_gui.py +++ b/pyface/i_gui.py @@ -49,7 +49,7 @@ def __init__(self, splash_screen=None): Parameters ---------- - splash_screen : ISplashScreen instance or None + splash_screen : ISplashScreen or None An optional splash screen that will be displayed until the event loop is started. """ @@ -78,7 +78,7 @@ def invoke_after(cls, millisecs, callable, *args, **kw): ---------- millisecs : float Delay in milliseconds - callable : callable + callable : Callable Callable to be called after the delay args, kwargs : Arguments and keyword arguments to be used when calling. @@ -90,7 +90,7 @@ def invoke_later(cls, callable, *args, **kw): Parameters ---------- - callable : callable + callable : Callable Callable to be called after the delay args, kwargs : Arguments and keyword arguments to be used when calling. @@ -104,11 +104,11 @@ def set_trait_after(cls, millisecs, obj, trait_name, new): ---------- millisecs : float Delay in milliseconds - obj : HasTraits instance + obj : traits.has_traits.HasTraits Object on which the trait is to be set trait_name : str The name of the trait to set - new : any + new : Any The value to set. """ @@ -118,11 +118,11 @@ def set_trait_later(cls, obj, trait_name, new): Parameters ---------- - obj : HasTraits instance + obj : traits.has_traits.HasTraits Object on which the trait is to be set trait_name : str The name of the trait to set - new : any + new : Any The value to set. """ diff --git a/pyface/i_image_resource.py b/pyface/i_image_resource.py index c69315284..b1874198b 100644 --- a/pyface/i_image_resource.py +++ b/pyface/i_image_resource.py @@ -14,7 +14,6 @@ from traits.api import HasTraits, List, Str from pyface.i_image import IImage -from pyface.resource_manager import resource_manager from pyface.resource.resource_path import resource_module, resource_path @@ -125,11 +124,13 @@ def _get_ref(self, size=None): Returns ------- - ref : ImageReference instance + ref : ImageReference The reference to the requested image. """ if self._ref is None: + from pyface.resource_manager import resource_manager + self._ref = resource_manager.locate_image( self.name, self.search_path, size ) @@ -161,7 +162,7 @@ def _get_image_not_found(cls): Returns ------- - not_found : ImageResource instance + not_found : ImageResource An image resource for the the 'not found' image. """ diff --git a/pyface/i_python_shell.py b/pyface/i_python_shell.py index e98582f9c..039f72f64 100644 --- a/pyface/i_python_shell.py +++ b/pyface/i_python_shell.py @@ -38,7 +38,7 @@ def interpreter(self): Returns ------- - interpreter : InteractiveInterpreter instance + interpreter : InteractiveInterpreter Returns the InteractiveInterpreter instance. """ @@ -49,7 +49,7 @@ def bind(self, name, value): ---------- name : str The python idetifier to bind the value to. - value : any + value : Any The python object to be bound into the interpreter's namespace. """ @@ -118,7 +118,7 @@ def bind(self, name, value): ---------- name : str The python idetifier to bind the value to. - value : any + value : Any The python object to be bound into the interpreter's namespace. """ self.interpreter().locals[name] = value diff --git a/pyface/i_system_metrics.py b/pyface/i_system_metrics.py index c86c28245..42382cbdc 100644 --- a/pyface/i_system_metrics.py +++ b/pyface/i_system_metrics.py @@ -11,7 +11,7 @@ """ The interface to system metrics (screen width and height etc). """ -from traits.api import HasTraits, Int, Interface, Tuple +from traits.api import HasTraits, Int, Interface, List, Tuple class ISystemMetrics(Interface): @@ -19,12 +19,15 @@ class ISystemMetrics(Interface): # 'ISystemMetrics' interface ------------------------------------------- - #: The width of the screen in pixels. + #: The width of the main screen in pixels. screen_width = Int() - #: The height of the screen in pixels. + #: The height of the main screen in pixels. screen_height = Int() + #: The height and width of each screen in pixels + screen_sizes = List(Tuple(Int, Int)) + #: Background color of a standard dialog window as a tuple of RGB values #: between 0.0 and 1.0. # FIXME v3: Why isn't this a traits colour? diff --git a/pyface/i_widget.py b/pyface/i_widget.py index 0cd6ad7ad..dd71465cd 100644 --- a/pyface/i_widget.py +++ b/pyface/i_widget.py @@ -77,7 +77,7 @@ def create(self, parent=None): """ Creates the toolkit specific control. This method should create the control and assign it to the - :py:attr:``control`` trait. + :py:attr:`control` trait. """ def destroy(self): diff --git a/pyface/qt/QtCore.py b/pyface/qt/QtCore.py index 1636ef1cc..801fcba10 100644 --- a/pyface/qt/QtCore.py +++ b/pyface/qt/QtCore.py @@ -9,19 +9,7 @@ # Thanks for using Enthought open source! from . import qt_api -if qt_api == "pyqt": - from PyQt4.QtCore import * - - from PyQt4.QtCore import pyqtProperty as Property - from PyQt4.QtCore import pyqtSignal as Signal - from PyQt4.QtCore import pyqtSlot as Slot - from PyQt4.Qt import QCoreApplication - from PyQt4.Qt import Qt - - __version__ = QT_VERSION_STR - __version_info__ = tuple(map(int, QT_VERSION_STR.split("."))) - -elif qt_api == "pyqt5": +if qt_api == "pyqt5": from PyQt5.QtCore import * from PyQt5.QtCore import pyqtProperty as Property diff --git a/pyface/qt/QtGui.py b/pyface/qt/QtGui.py index 25d6c82c8..632cb247d 100644 --- a/pyface/qt/QtGui.py +++ b/pyface/qt/QtGui.py @@ -9,17 +9,7 @@ # Thanks for using Enthought open source! from . import qt_api -if qt_api == "pyqt": - from PyQt4.Qt import QKeySequence, QTextCursor - from PyQt4.QtGui import * - - # forward-compatible font weights - # see https://doc.qt.io/qt-5/qfont.html#Weight-enum - QFont.Weight.ExtraLight = 12 - QFont.Weight.Medium = 57 - QFont.Weight.ExtraBold = 81 - -elif qt_api == "pyqt5": +if qt_api == "pyqt5": from PyQt5.QtGui import * from PyQt5.QtWidgets import * from PyQt5.QtPrintSupport import * diff --git a/pyface/qt/QtNetwork.py b/pyface/qt/QtNetwork.py index e392f878f..0b3b2658d 100644 --- a/pyface/qt/QtNetwork.py +++ b/pyface/qt/QtNetwork.py @@ -9,10 +9,7 @@ # Thanks for using Enthought open source! from . import qt_api -if qt_api == "pyqt": - from PyQt4.QtNetwork import * - -elif qt_api == "pyqt5": +if qt_api == "pyqt5": from PyQt5.QtNetwork import * elif qt_api == "pyqt6": diff --git a/pyface/qt/QtOpenGL.py b/pyface/qt/QtOpenGL.py index a7794f654..9d9b6437f 100644 --- a/pyface/qt/QtOpenGL.py +++ b/pyface/qt/QtOpenGL.py @@ -9,10 +9,7 @@ # Thanks for using Enthought open source! from . import qt_api -if qt_api == "pyqt": - from PyQt4.QtOpenGL import * - -elif qt_api == "pyqt5": +if qt_api == "pyqt5": from PyQt5.QtOpenGL import * from PyQt5.QtWidgets import QOpenGLWidget diff --git a/pyface/qt/QtOpenGLWidgets.py b/pyface/qt/QtOpenGLWidgets.py new file mode 100644 index 000000000..87314a5a2 --- /dev/null +++ b/pyface/qt/QtOpenGLWidgets.py @@ -0,0 +1,24 @@ +# (C) Copyright 2005-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! +from . import qt_api + +if qt_api == "pyqt5": + from PyQt5.QtOpenGL import * + from PyQt5.QtWidgets import QOpenGLWidget + +elif qt_api == "pyqt6": + from PyQt6.QtOpenGLWidgets import * + +elif qt_api == "pyside6": + from PySide6.QtOpenGLWidgets import * + +else: + from PySide2.QtOpenGL import * + from PySide2.QtWidgets import QOpenGLWidget diff --git a/pyface/qt/QtScript.py b/pyface/qt/QtScript.py index 0a9079f06..841087ecc 100644 --- a/pyface/qt/QtScript.py +++ b/pyface/qt/QtScript.py @@ -7,12 +7,8 @@ # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! -from . import qt_api -if qt_api == "pyqt": - from PyQt4.QtScript import * -else: - import warnings +import warnings - warnings.warn(DeprecationWarning("QtScript is not supported in PyQt5/PySide2")) +warnings.warn(DeprecationWarning("QtScript is not supported after Qt4")) diff --git a/pyface/qt/QtSvg.py b/pyface/qt/QtSvg.py index 4c13c77aa..5a3160370 100644 --- a/pyface/qt/QtSvg.py +++ b/pyface/qt/QtSvg.py @@ -9,17 +9,18 @@ # Thanks for using Enthought open source! from . import qt_api -if qt_api == "pyqt": - from PyQt4.QtSvg import * - -elif qt_api == "pyqt5": +if qt_api == "pyqt5": from PyQt5.QtSvg import * elif qt_api == "pyqt6": from PyQt6.QtSvg import * + # Backwards compatibility imports + from PyQt6.QtSvgWidgets import * elif qt_api == "pyside6": from PySide6.QtSvg import * + # Backwards compatibility imports + from PySide6.QtSvgWidgets import * else: from PySide2.QtSvg import * diff --git a/pyface/qt/QtSvgWidgets.py b/pyface/qt/QtSvgWidgets.py new file mode 100644 index 000000000..8ed4d4565 --- /dev/null +++ b/pyface/qt/QtSvgWidgets.py @@ -0,0 +1,24 @@ +# (C) Copyright 2005-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! +from . import qt_api + +if qt_api == "pyqt5": + # Forwards compatibility imports + from PyQt5.QtSvg import * + +elif qt_api == "pyqt6": + from PyQt6.QtSvgWidgets import * + +elif qt_api == "pyside6": + from PySide6.QtSvgWidgets import * + +else: + # Forwards compatibility imports + from PySide2.QtSvg import * diff --git a/pyface/qt/QtTest.py b/pyface/qt/QtTest.py index db3fe2410..e1544b838 100644 --- a/pyface/qt/QtTest.py +++ b/pyface/qt/QtTest.py @@ -9,10 +9,7 @@ # Thanks for using Enthought open source! from . import qt_api -if qt_api == "pyqt": - from PyQt4.QtTest import * - -elif qt_api == "pyqt5": +if qt_api == "pyqt5": from PyQt5.QtTest import * elif qt_api == "pyqt6": diff --git a/pyface/qt/QtWebKit.py b/pyface/qt/QtWebKit.py index c2b186eb9..8ca944811 100644 --- a/pyface/qt/QtWebKit.py +++ b/pyface/qt/QtWebKit.py @@ -9,10 +9,7 @@ # Thanks for using Enthought open source! from . import qt_api -if qt_api == "pyqt": - from PyQt4.QtWebKit import * - -elif qt_api == "pyqt5": +if qt_api == "pyqt5": from PyQt5.QtWidgets import * try: diff --git a/pyface/qt/QtWidgets.py b/pyface/qt/QtWidgets.py new file mode 100644 index 000000000..73656afdf --- /dev/null +++ b/pyface/qt/QtWidgets.py @@ -0,0 +1,22 @@ +# (C) Copyright 2005-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! +from . import qt_api + +if qt_api == "pyqt5": + from PyQt5.QtWidgets import * + +elif qt_api == "pyqt6": + from PyQt6.QtWidgets import * + +elif qt_api == "pyside6": + from PySide6.QtWidgets import * + +else: + from PySide2.QtWidgets import * diff --git a/pyface/qt/__init__.py b/pyface/qt/__init__.py index 8cec68f29..69e7fd673 100644 --- a/pyface/qt/__init__.py +++ b/pyface/qt/__init__.py @@ -18,7 +18,6 @@ ("pyside6", "PySide6"), ("pyqt5", "PyQt5"), ("pyqt6", "PyQt6"), - ("pyqt", "PyQt4"), ] api_names, modules = zip(*QtAPIs) @@ -60,7 +59,7 @@ raise RuntimeError(msg) # useful constants -is_qt4 = qt_api in {"pyqt"} +is_qt4 = False is_qt5 = qt_api in {"pyqt5", "pyside2"} is_qt6 = qt_api in {"pyqt6", "pyside6"} is_pyqt = qt_api in {"pyqt", "pyqt5", "pyqt6"} diff --git a/pyface/single_choice_dialog.py b/pyface/single_choice_dialog.py index c7f8d278c..e187e6475 100644 --- a/pyface/single_choice_dialog.py +++ b/pyface/single_choice_dialog.py @@ -38,7 +38,7 @@ def choose_one(parent, message, choices, title="Choose", cancel=True): Returns ------- - choice : any + choice : Any The selected object, or None if cancelled. """ dialog = SingleChoiceDialog( diff --git a/pyface/splash_screen_log_handler.py b/pyface/splash_screen_log_handler.py index 4ccf4dc78..50ec28c0f 100644 --- a/pyface/splash_screen_log_handler.py +++ b/pyface/splash_screen_log_handler.py @@ -22,7 +22,7 @@ def __init__(self, splash_screen): Parameters ---------- - splash_screen : ISplashScreen instance + splash_screen : ISplashScreen The splash screen being used to display the log messages """ # Base class constructor. @@ -36,7 +36,7 @@ def emit(self, record): Parameters ---------- - record : logging record instance + record : logging record The log record to be displayed. """ self._splash_screen.text = str(record.getMessage()) + "..." diff --git a/pyface/tasks/action/api.py b/pyface/tasks/action/api.py index 5e4a33d97..151e59932 100644 --- a/pyface/tasks/action/api.py +++ b/pyface/tasks/action/api.py @@ -78,7 +78,8 @@ SMenuBar, SToolBar, ) -from .schema_addition import SchemaAddition +# deprecated: will be removed in a future Pyface release +from pyface.action.schema.schema_addition import SchemaAddition from .task_action import ( CentralPaneAction, DockPaneAction, diff --git a/pyface/tasks/action/dock_pane_toggle_group.py b/pyface/tasks/action/dock_pane_toggle_group.py index 603cdfc59..701c2f8f8 100644 --- a/pyface/tasks/action/dock_pane_toggle_group.py +++ b/pyface/tasks/action/dock_pane_toggle_group.py @@ -10,7 +10,6 @@ """ A Group for toggling the visibility of a task's dock panes. """ -from pyface.action.api import Action, ActionItem, Group from traits.api import ( cached_property, Instance, @@ -21,6 +20,7 @@ ) +from pyface.action.api import Action, Group from pyface.tasks.i_dock_pane import IDockPane @@ -123,6 +123,7 @@ def get_manager(self): def _dock_panes_updated(self, event): """Recreate the group items when dock panes have been added/removed. """ + from pyface.action.action_item import ActionItem # Remove the previous group items. self.destroy() diff --git a/pyface/tasks/action/task_action.py b/pyface/tasks/action/task_action.py index 986e1722a..401eb28bc 100644 --- a/pyface/tasks/action/task_action.py +++ b/pyface/tasks/action/task_action.py @@ -12,7 +12,9 @@ from traits.api import Instance, Property, Str, cached_property -from pyface.tasks.api import Editor, Task, TaskPane +from pyface.tasks.i_editor import IEditor +from pyface.tasks.task import Task +from pyface.tasks.i_task_pane import ITaskPane from pyface.action.listening_action import ListeningAction @@ -74,7 +76,7 @@ class CentralPaneAction(TaskAction): # CentralPaneAction interface -----------------------------------------# #: The central pane with which the action is associated. - central_pane = Property(Instance(TaskPane), observe="task") + central_pane = Property(Instance(ITaskPane), observe="task") # ------------------------------------------------------------------------ # Protected interface. @@ -101,7 +103,7 @@ class DockPaneAction(TaskAction): # DockPaneAction interface --------------------------------------------- #: The dock pane with which the action is associated. Set by the framework. - dock_pane = Property(Instance(TaskPane), observe="task") + dock_pane = Property(Instance(ITaskPane), observe="task") #: The ID of the dock pane with which the action is associated. dock_pane_id = Str() @@ -132,7 +134,7 @@ class EditorAction(CentralPaneAction): #: The active editor in the central pane with which the action is associated. active_editor = Property( - Instance(Editor), observe="central_pane.active_editor" + Instance(IEditor), observe="central_pane.active_editor" ) # ------------------------------------------------------------------------ diff --git a/pyface/tasks/action/task_toggle_group.py b/pyface/tasks/action/task_toggle_group.py index 97c4d8001..362b959c6 100644 --- a/pyface/tasks/action/task_toggle_group.py +++ b/pyface/tasks/action/task_toggle_group.py @@ -9,12 +9,11 @@ # Thanks for using Enthought open source! -from pyface.action.api import Action, ActionItem, Group +from pyface.action.api import Action, Group from traits.api import Any, List, Instance, Property, Str, observe from pyface.tasks.task import Task -from pyface.tasks.task_window import TaskWindow class TaskToggleAction(Action): @@ -89,13 +88,15 @@ class TaskToggleGroup(Group): manager = Any() #: The window that contains the group. - window = Instance(TaskWindow) + window = Instance("pyface.tasks.task_window.TaskWindow") # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _get_items(self): + from pyface.action.api import ActionItem + items = [] if len(self.window.tasks) > 1: # at least two tasks, so something to toggle diff --git a/pyface/tasks/action/task_window_toggle_group.py b/pyface/tasks/action/task_window_toggle_group.py index 210450bb9..9a6d70129 100644 --- a/pyface/tasks/action/task_window_toggle_group.py +++ b/pyface/tasks/action/task_window_toggle_group.py @@ -9,9 +9,10 @@ # Thanks for using Enthought open source! -from pyface.action.api import Action, ActionItem, Group from traits.api import Any, Instance, List, Property, Str, observe +from pyface.action.api import Action, Group + class TaskWindowToggleAction(Action): """ An action for activating an application window. @@ -94,6 +95,8 @@ def destroy(self): # ------------------------------------------------------------------------- def _get_items(self): + from pyface.action.action_item import ActionItem + items = [] for window in self.application.windows: active = window == self.application.active_window diff --git a/pyface/tasks/api.py b/pyface/tasks/api.py index bab3d2653..36059a2f5 100644 --- a/pyface/tasks/api.py +++ b/pyface/tasks/api.py @@ -57,14 +57,6 @@ """ -from .advanced_editor_area_pane import AdvancedEditorAreaPane -from .split_editor_area_pane import SplitEditorAreaPane -from .dock_pane import DockPane -from .editor import Editor -from .editor_area_pane import EditorAreaPane -from .enaml_dock_pane import EnamlDockPane -from .enaml_editor import EnamlEditor -from .enaml_task_pane import EnamlTaskPane from .i_dock_pane import IDockPane from .i_editor import IEditor from .i_editor_area_pane import IEditorAreaPane @@ -79,9 +71,72 @@ HSplitter, VSplitter, ) -from .task_pane import TaskPane -from .task_window import TaskWindow from .task_window_layout import TaskWindowLayout -from .traits_dock_pane import TraitsDockPane -from .traits_editor import TraitsEditor -from .traits_task_pane import TraitsTaskPane + + +# ---------------------------------------------------------------------------- +# Deferred imports +# ---------------------------------------------------------------------------- + +# These imports have the side-effect of performing toolkit selection + +_toolkit_imports = { + 'AdvancedEditorAreaPane': "advanced_editor_area_pane", + 'DockPane': "dock_pane", + 'EditorAreaPane': "editor_area_pane", + 'Editor': "editor", + 'SplitEditorAreaPane': "split_editor_area_pane", + 'TaskPane': "task_pane", +} + +# These are pyface.* imports that have selection as a side-effect +# TODO: refactor to delay imports where possible +_relative_imports = { + 'EnamlDockPane': "enaml_dock_pane", + 'EnamlEditor': "enaml_editor", + 'EnamlTaskPane': "enaml_task_pane", + 'TaskWindow': "task_window", + 'TraitsDockPane': "traits_dock_pane", + 'TraitsEditor': "traits_editor", + 'TraitsTaskPane': "traits_task_pane", +} + + +def __getattr__(name): + """Lazily load attributes with side-effects + + In particular, lazily load toolkit backend names. For efficiency, lazily + loaded objects are injected into the module namespace + """ + # sentinel object for no result + not_found = object() + result = not_found + + if name in _relative_imports: + from importlib import import_module + source = _relative_imports[name] + module = import_module(f"pyface.tasks.{source}") + result = getattr(module, name) + + elif name in _toolkit_imports: + from pyface.toolkit import toolkit_object + source = _toolkit_imports[name] + result = toolkit_object(f"tasks.{source}:{name}") + + if result is not_found: + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + globals()[name] = result + return result + + +# ---------------------------------------------------------------------------- +# Introspection support +# ---------------------------------------------------------------------------- + +# the list of available names we report for introspection purposes +_extra_names = set(_toolkit_imports) | set(_relative_imports) + + +def __dir__(): + return sorted(set(globals()) | _extra_names) diff --git a/pyface/tasks/task.py b/pyface/tasks/task.py index b091d3be6..753ff21be 100644 --- a/pyface/tasks/task.py +++ b/pyface/tasks/task.py @@ -8,7 +8,6 @@ # # Thanks for using Enthought open source! -from pyface.action.api import StatusBarManager from traits.api import Callable, HasTraits, Instance, List, Str @@ -49,7 +48,7 @@ class Task(HasTraits): menu_bar = Instance(MenuBarSchema) #: The (optional) status bar for the task. - status_bar = Instance(StatusBarManager) + status_bar = Instance("pyface.action.status_bar_manager.StatusBarManager") #: The list of tool bars for the tasks. tool_bars = List(ToolBarSchema) diff --git a/pyface/tasks/task_window.py b/pyface/tasks/task_window.py index 854c20b95..6d9fe3aa6 100644 --- a/pyface/tasks/task_window.py +++ b/pyface/tasks/task_window.py @@ -11,8 +11,6 @@ import logging -from pyface.action.api import MenuBarManager, StatusBarManager, ToolBarManager -from pyface.api import ApplicationWindow from traits.api import ( Bool, Callable, @@ -26,13 +24,17 @@ ) +from pyface.action.i_menu_bar_manager import IMenuBarManager +from pyface.action.i_status_bar_manager import IStatusBarManager +from pyface.action.i_tool_bar_manager import IToolBarManager +from pyface.application_window import ApplicationWindow from pyface.tasks.action.task_action_manager_builder import ( TaskActionManagerBuilder, ) from pyface.tasks.i_dock_pane import IDockPane from pyface.tasks.i_task_pane import ITaskPane +from pyface.tasks.i_task_window_backend import ITaskWindowBackend from pyface.tasks.task import Task, TaskLayout -from pyface.tasks.task_window_backend import TaskWindowBackend from pyface.tasks.task_window_layout import TaskWindowLayout # Logging. @@ -81,7 +83,7 @@ class TaskWindow(ApplicationWindow): _active_state = Instance("pyface.tasks.task_window.TaskState") _states = List(Instance("pyface.tasks.task_window.TaskState")) _title = Str() - _window_backend = Instance(TaskWindowBackend) + _window_backend = Instance(ITaskWindowBackend) # ------------------------------------------------------------------------ # 'Widget' interface. @@ -403,6 +405,7 @@ def _get_state(self, id_or_task): # Trait initializers --------------------------------------------------- def __window_backend_default(self): + from pyface.tasks.task_window_backend import TaskWindowBackend return TaskWindowBackend(window=self) # Trait property getters/setters --------------------------------------- @@ -448,15 +451,29 @@ class TaskState(HasStrictTraits): with an attached Task. """ + #: The Task that the state comes from. task = Instance(Task) + + #: The layout of panes in the TaskWindow. layout = Instance(TaskLayout) + + #: Whether the task state has been initialized. initialized = Bool(False) + #: The central pane of the TaskWindow central_pane = Instance(ITaskPane) - dock_panes = List(IDockPane) - menu_bar_manager = Instance(MenuBarManager) - status_bar_manager = Instance(StatusBarManager) - tool_bar_managers = List(ToolBarManager) + + #: The list of all dock panes. + dock_panes = List(Instance(IDockPane)) + + #: The TaskWindow's menu bar manager instance. + menu_bar_manager = Instance(IMenuBarManager) + + #: The TaskWindow's status bar instance. + status_bar_manager = Instance(IStatusBarManager) + + #: The TaskWindow's tool bar instances. + tool_bar_managers = List(Instance(IToolBarManager)) def get_dock_pane(self, id): """ Returns the dock pane with the specified id, or None if no such dock diff --git a/pyface/tasks/tasks_application.py b/pyface/tasks/tasks_application.py index 9f14443d9..510445098 100644 --- a/pyface/tasks/tasks_application.py +++ b/pyface/tasks/tasks_application.py @@ -121,7 +121,7 @@ def create_window(self, layout=None, **kwargs): Parameters ---------- - layout : TaskLayout instance or None + layout : TaskLayout or None The pane layout for the window. **kwargs : dict Additional keyword arguments to pass to the window factory. @@ -129,7 +129,7 @@ def create_window(self, layout=None, **kwargs): Returns ------- - window : ITaskWindow instance or None + window : ITaskWindow or None The new TaskWindow. """ from pyface.tasks.task_window_layout import TaskWindowLayout diff --git a/pyface/tasks/tests/test_api.py b/pyface/tasks/tests/test_api.py new file mode 100644 index 000000000..048795a57 --- /dev/null +++ b/pyface/tasks/tests/test_api.py @@ -0,0 +1,35 @@ +# (C) Copyright 2005-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! + +""" Test for pyface.api """ + +import unittest + + +class TestApi(unittest.TestCase): + """ Test importable items in any environment.""" + + def test_api_importable(self): + # make sure api is importable with the most minimal + # required dependencies, including in the absence of toolkit backends. + from pyface.tasks import api # noqa: F401 + + def test_public_attrs(self): + # make sure everything advertised by dir() is available except optional + from pyface.tasks import api + + attrs = [ + name + for name in dir(api) + if not name.startswith('_') + ] + for attr in attrs: + with self.subTest(attr=attr): + self.assertIsNotNone(getattr(api, attr, None)) diff --git a/pyface/tests/test_api.py b/pyface/tests/test_api.py index 52df08136..abc42b55d 100644 --- a/pyface/tests/test_api.py +++ b/pyface/tests/test_api.py @@ -26,6 +26,19 @@ def test_api_importable(self): # required dependencies, including in the absence of toolkit backends. from pyface import api # noqa: F401 + def test_public_attrs(self): + # make sure everything advertised by dir() is available except optional + from pyface import api + + attrs = [ + name + for name in dir(api) + if not (name.startswith('_') or name in api._optional_imports) + ] + for attr in attrs: + with self.subTest(attr=attr): + self.assertIsNotNone(getattr(api, attr, None)) + @unittest.skipIf(not is_qt, "This test is for qt.") class TestApiQt(unittest.TestCase): @@ -191,7 +204,6 @@ def test_importable_items(self): clipboard, confirm, error, - fix_introspect_bug, information, warning, # Interfaces diff --git a/pyface/tests/test_array_image.py b/pyface/tests/test_array_image.py index 479d2a011..79f5560f7 100644 --- a/pyface/tests/test_array_image.py +++ b/pyface/tests/test_array_image.py @@ -13,7 +13,8 @@ from traits.testing.optional_dependencies import numpy as np, requires_numpy -from ..array_image import ArrayImage +if np is not None: + from ..array_image import ArrayImage @requires_numpy diff --git a/pyface/tests/test_system_metrics.py b/pyface/tests/test_system_metrics.py index fd95d7a9a..2cdc93be0 100644 --- a/pyface/tests/test_system_metrics.py +++ b/pyface/tests/test_system_metrics.py @@ -26,6 +26,13 @@ def test_height(self): height = self.metrics.screen_height self.assertGreaterEqual(height, 0) + def test_screen_sizes(self): + screens = self.metrics.screen_sizes + self.assertTrue(all( + (isinstance(screen, tuple) and len(screen) == 2) + for screen in screens + )) + def test_background_color(self): color = self.metrics.dialog_background_color self.assertIn(len(color), [3, 4]) diff --git a/pyface/timer/api.py b/pyface/timer/api.py index 1832ca818..d944eafeb 100644 --- a/pyface/timer/api.py +++ b/pyface/timer/api.py @@ -27,6 +27,53 @@ """ -from .do_later import do_later, do_after, DoLaterTimer from .i_timer import ICallbackTimer, IEventTimer, ITimer -from .timer import CallbackTimer, EventTimer, Timer + + +# ---------------------------------------------------------------------------- +# Deferred imports +# ---------------------------------------------------------------------------- + +# These imports have the side-effect of performing toolkit selection + +# These are pyface.undo.* imports that have selection as a side-effect +# TODO: refactor to delay imports where possible +_relative_imports = { + 'do_later': 'do_later', + 'do_after': 'do_later', + 'DoLaterTimer': 'do_later', + 'CallbackTimer': 'timer', + 'EventTimer': 'timer', + 'Timer': 'timer', +} + + +def __getattr__(name): + """Lazily load attributes with side-effects + + In particular, lazily load toolkit backend names. For efficiency, lazily + loaded objects are injected into the module namespace + """ + # sentinel object for no result + not_found = object() + result = not_found + + if name in _relative_imports: + from importlib import import_module + source = _relative_imports[name] + module = import_module(f"pyface.timer.{source}") + result = getattr(module, name) + + if result is not_found: + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + globals()[name] = result + return result + + +# ---------------------------------------------------------------------------- +# Introspection support +# ---------------------------------------------------------------------------- + +def __dir__(): + return sorted(set(globals()) | set(_relative_imports)) diff --git a/pyface/timer/do_later.py b/pyface/timer/do_later.py index 92e996e88..07f5cc69a 100644 --- a/pyface/timer/do_later.py +++ b/pyface/timer/do_later.py @@ -34,7 +34,7 @@ def do_later(callable, *args, **kwargs): Parameters ---------- - callable : callable + callable : Callable The callable to call in 50ms time. args, kwargs : tuple, dict Arguments to be passed through to the callable. @@ -51,7 +51,7 @@ def do_after(interval, callable, *args, **kwargs): ---------- interval : float The time interval in milliseconds to wait before calling. - callable : callable + callable : Callable The callable to call. args Positional arguments to be passed through to the callable. diff --git a/pyface/timer/tests/test_api.py b/pyface/timer/tests/test_api.py new file mode 100644 index 000000000..392dd7e50 --- /dev/null +++ b/pyface/timer/tests/test_api.py @@ -0,0 +1,35 @@ +# (C) Copyright 2005-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! + +""" Test for pyface.api """ + +import unittest + + +class TestApi(unittest.TestCase): + """ Test importable items in any environment.""" + + def test_api_importable(self): + # make sure api is importable with the most minimal + # required dependencies, including in the absence of toolkit backends. + from pyface.timer import api # noqa: F401 + + def test_public_attrs(self): + # make sure everything advertised by dir() is available except optional + from pyface.timer import api + + attrs = [ + name + for name in dir(api) + if not name.startswith('_') + ] + for attr in attrs: + with self.subTest(attr=attr): + self.assertIsNotNone(getattr(api, attr, None)) diff --git a/pyface/ui/__init__.py b/pyface/ui/__init__.py index 5cc66c880..002e3d564 100644 --- a/pyface/ui/__init__.py +++ b/pyface/ui/__init__.py @@ -37,7 +37,7 @@ class ShadowedModuleLoader(Loader): new_name : str The full name of the corresponding "real" module. Eg. "pyface.ui.qt.foo" - new_spec : ModuleSpec instance + new_spec : ModuleSpec The spec object for the corresponding "real" module. """ diff --git a/pyface/ui/qt/action/menu_bar_manager.py b/pyface/ui/qt/action/menu_bar_manager.py index adf6cb7a6..3fefdf028 100644 --- a/pyface/ui/qt/action/menu_bar_manager.py +++ b/pyface/ui/qt/action/menu_bar_manager.py @@ -17,13 +17,14 @@ import sys +from traits.api import provides from pyface.qt import QtGui - - from pyface.action.action_manager import ActionManager +from pyface.action.i_menu_bar_manager import IMenuBarManager +@provides(IMenuBarManager) class MenuBarManager(ActionManager): """ A menu bar manager realizes itself in errr, a menu bar control. """ diff --git a/pyface/ui/qt/action/menu_manager.py b/pyface/ui/qt/action/menu_manager.py index 8a1f5641b..d9bf02a61 100644 --- a/pyface/ui/qt/action/menu_manager.py +++ b/pyface/ui/qt/action/menu_manager.py @@ -18,15 +18,17 @@ from pyface.qt import QtCore, QtGui -from traits.api import Instance, List, Str +from traits.api import Instance, List, Str, provides from pyface.action.action_manager import ActionManager from pyface.action.action_manager_item import ActionManagerItem from pyface.action.action_item import _Tool, Action +from pyface.action.i_menu_manager import IMenuManager from pyface.action.group import Group +@provides(IMenuManager) class MenuManager(ActionManager, ActionManagerItem): """ A menu manager realizes itself in a menu control. diff --git a/pyface/ui/qt/action/status_bar_manager.py b/pyface/ui/qt/action/status_bar_manager.py index 72492bfde..5c792d8af 100644 --- a/pyface/ui/qt/action/status_bar_manager.py +++ b/pyface/ui/qt/action/status_bar_manager.py @@ -17,9 +17,12 @@ from pyface.qt import QtGui -from traits.api import Any, Bool, HasTraits, List, Property, Str +from traits.api import Any, Bool, HasTraits, List, Property, Str, provides +from pyface.action.i_status_bar_manager import IStatusBarManager + +@provides(IStatusBarManager) class StatusBarManager(HasTraits): """ A status bar manager realizes itself in a status bar control. """ diff --git a/pyface/ui/qt/action/tool_bar_manager.py b/pyface/ui/qt/action/tool_bar_manager.py index d60bdb07f..15ad782d1 100644 --- a/pyface/ui/qt/action/tool_bar_manager.py +++ b/pyface/ui/qt/action/tool_bar_manager.py @@ -17,13 +17,16 @@ from pyface.qt import QtCore, QtGui -from traits.api import Bool, Enum, Instance, List, Str, Tuple +from traits.api import Bool, Instance, List, Str, Tuple, provides from pyface.image_cache import ImageCache from pyface.action.action_manager import ActionManager +from pyface.action.i_tool_bar_manager import IToolBarManager +from pyface.ui_traits import Orientation +@provides(IToolBarManager) class ToolBarManager(ActionManager): """ A tool bar manager realizes itself in errr, a tool bar control. """ @@ -42,7 +45,7 @@ class ToolBarManager(ActionManager): name = Str("ToolBar") # The orientation of the toolbar. - orientation = Enum("horizontal", "vertical") + orientation = Orientation("horizontal") # Should we display the name of each tool bar tool under its image? show_tool_names = Bool(True) diff --git a/pyface/ui/qt/application_window.py b/pyface/ui/qt/application_window.py index 5528f5576..bd2d64961 100644 --- a/pyface/ui/qt/application_window.py +++ b/pyface/ui/qt/application_window.py @@ -18,17 +18,10 @@ import sys +from traits.api import observe, provides, Str from pyface.qt import QtGui - - -from pyface.action.api import MenuBarManager, StatusBarManager -from pyface.action.api import ToolBarManager -from traits.api import Instance, List, observe, provides, Str - - from pyface.i_application_window import IApplicationWindow, MApplicationWindow -from pyface.ui_traits import Image from .image_resource import ImageResource from .window import Window @@ -39,23 +32,6 @@ class ApplicationWindow(MApplicationWindow, Window): IApplicationWindow interface for the API documentation. """ - # 'IApplicationWindow' interface --------------------------------------- - - #: The icon to display in the application window title bar. - icon = Image() - - #: The menu bar manager for the window. - menu_bar_manager = Instance(MenuBarManager) - - #: The status bar manager for the window. - status_bar_manager = Instance(StatusBarManager) - - #: DEPRECATED: The tool bar manager for the window. - tool_bar_manager = Instance(ToolBarManager) - - #: The collection of tool bar managers for the window. - tool_bar_managers = List(ToolBarManager) - # 'IWindow' interface -------------------------------------------------# #: The window title. @@ -90,7 +66,7 @@ def _create_status_bar(self, parent): status_bar.setVisible(self.status_bar_manager.visible) def _create_tool_bar(self, parent): - tool_bar_managers = self._get_tool_bar_managers() + tool_bar_managers = self.tool_bar_managers visible = self.control.isVisible() for tool_bar_manager in tool_bar_managers: # Add the tool bar and make sure it is visible. @@ -150,18 +126,6 @@ def _create_control(self, parent): # Private interface. # ------------------------------------------------------------------------ - def _get_tool_bar_managers(self): - """ Return all tool bar managers specified for the window. """ - - # fixme: V3 remove the old-style single toolbar option! - if self.tool_bar_manager is not None: - tool_bar_managers = [self.tool_bar_manager] - - else: - tool_bar_managers = self.tool_bar_managers - - return tool_bar_managers - # Trait change handlers ------------------------------------------------ # QMainWindow takes ownership of the menu bar and the status bar upon @@ -177,10 +141,11 @@ def _menu_bar_manager_updated(self, event): def _status_bar_manager_updated(self, event): if self.control is not None: if event.old is not None: + self.control.setStatusBar(None) event.old.destroy() self._create_status_bar(self.control) - @observe("tool_bar_manager, tool_bar_managers.items") + @observe("tool_bar_managers.items") def _update_tool_bar_managers(self, event): if self.control is not None: # Remove the old toolbars. @@ -190,7 +155,8 @@ def _update_tool_bar_managers(self, event): child.deleteLater() # Add the new toolbars. - self._create_tool_bar(self.control) + if event.new is not None: + self._create_status_bar(self.control) @observe("icon") def _icon_updated(self, event): diff --git a/pyface/ui/qt/code_editor/code_widget.py b/pyface/ui/qt/code_editor/code_widget.py index 132bdb765..eee2a54ec 100644 --- a/pyface/ui/qt/code_editor/code_widget.py +++ b/pyface/ui/qt/code_editor/code_widget.py @@ -844,14 +844,6 @@ def _update_replace_all_enabled(self, text): if __name__ == "__main__": - def set_trace(): - from PyQt4.QtCore import pyqtRemoveInputHook - - pyqtRemoveInputHook() - import pdb - - pdb.Pdb().set_trace(sys._getframe().f_back) - app = QtGui.QApplication(sys.argv) window = AdvancedCodeWidget(None) diff --git a/pyface/ui/qt/console/console_widget.py b/pyface/ui/qt/console/console_widget.py index 746aca8fc..9508d24fc 100644 --- a/pyface/ui/qt/console/console_widget.py +++ b/pyface/ui/qt/console/console_widget.py @@ -1874,7 +1874,7 @@ def _readline(self, prompt="", callback=None): prompt : str, optional The prompt to print before reading the line. - callback : callable, optional + callback : Callable, optional A callback to execute with the read line. If not specified, input is read *synchronously* and this method does not return until it has been read. diff --git a/pyface/ui/qt/data_view/data_view_widget.py b/pyface/ui/qt/data_view/data_view_widget.py index a240e8a03..e04610540 100644 --- a/pyface/ui/qt/data_view/data_view_widget.py +++ b/pyface/ui/qt/data_view/data_view_widget.py @@ -12,10 +12,10 @@ from traits.api import Callable, Enum, Instance, observe, provides -from pyface.qt.QtCore import QAbstractItemModel -from pyface.qt.QtGui import ( - QAbstractItemView, QItemSelection, QItemSelectionModel, QTreeView +from pyface.qt.QtCore import ( + QAbstractItemModel, QItemSelection, QItemSelectionModel ) +from pyface.qt.QtGui import QAbstractItemView, QTreeView from pyface.data_view.i_data_view_widget import ( IDataViewWidget, MDataViewWidget ) diff --git a/pyface/ui/qt/fields/combo_field.py b/pyface/ui/qt/fields/combo_field.py index c2ce4dc16..afb15c1b8 100644 --- a/pyface/ui/qt/fields/combo_field.py +++ b/pyface/ui/qt/fields/combo_field.py @@ -19,11 +19,14 @@ from pyface.fields.i_combo_field import IComboField, MComboField from pyface.qt.QtCore import Qt from pyface.qt.QtGui import QComboBox -from .field import Field +from pyface.ui.qt.util.alignment import ( + alignment_to_qalignment, qalignment_to_alignment +) +from .editable_field import EditableField @provides(IComboField) -class ComboField(MComboField, Field): +class ComboField(MComboField, EditableField): """ The Qt-specific implementation of the combo field class """ # ------------------------------------------------------------------------ @@ -41,11 +44,6 @@ def _create_control(self, parent): # Private interface # ------------------------------------------------------------------------ - def _update_value(self, value): - """ Handle a change to the value from user interaction - """ - self.value = self._get_control_value() - # Toolkit control interface --------------------------------------------- def _get_control_value(self): @@ -101,3 +99,19 @@ def _set_control_values(self, values): self._set_control_value(current_value) else: self._set_control_value(self.value) + + def _get_control_alignment(self): + """ Toolkit specific method to get the control's alignment. """ + # only works if combobox is ieditable, which currently is always False + line_edit = self.control.lineEdit() + if line_edit is not None: + return qalignment_to_alignment(line_edit.alignment()) + else: + # no widget; cheat + return self.alignment + + def _set_control_alignment(self, alignment): + """ Toolkit specific method to set the control's alignment. """ + line_edit = self.control.lineEdit() + if line_edit is not None: + return line_edit.setAlignment(alignment_to_qalignment(alignment)) diff --git a/pyface/ui/qt/fields/editable_field.py b/pyface/ui/qt/fields/editable_field.py new file mode 100644 index 000000000..0de04eb58 --- /dev/null +++ b/pyface/ui/qt/fields/editable_field.py @@ -0,0 +1,37 @@ +# (C) Copyright 2005-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! + +""" The Qt-specific implementation of the text field class """ + +from traits.api import provides + +from pyface.fields.i_editable_field import IEditableField, MEditableField +from pyface.ui.qt.fields.field import Field + + +@provides(IEditableField) +class EditableField(MEditableField, Field): + """ The Qt-specific implementation of the field class + + This is an abstract class which is not meant to be instantiated. Because + many concrete QWidgets provide a `value` property, the default control + observer targets this. + """ + + # ------------------------------------------------------------------------ + # Private interface + # ------------------------------------------------------------------------ + + def _observe_control_value(self, remove=False): + """ Toolkit specific method to change the control value observer. """ + if remove: + self.control.valueChanged[int].disconnect(self._update_value) + else: + self.control.valueChanged[int].connect(self._update_value) diff --git a/pyface/ui/qt/fields/field.py b/pyface/ui/qt/fields/field.py index dc3b00aa2..bed8ee3d8 100644 --- a/pyface/ui/qt/fields/field.py +++ b/pyface/ui/qt/fields/field.py @@ -15,14 +15,40 @@ from pyface.fields.i_field import IField, MField from pyface.ui.qt.layout_widget import LayoutWidget +from pyface.ui.qt.util.alignment import ( + alignment_to_qalignment, qalignment_to_alignment +) @provides(IField) class Field(MField, LayoutWidget): """ The Qt-specific implementation of the field class - This is an abstract class which is not meant to be instantiated. + This is an abstract class which is not meant to be instantiated. Because + many concrete QWidgets provide a `value` property, the default getters and + setters target this. """ #: The value held by the field. value = Any() + + # ------------------------------------------------------------------------ + # Private interface + # ------------------------------------------------------------------------ + + def _get_control_value(self): + """ Toolkit specific method to get the control's value. """ + return self.control.value() + + def _set_control_value(self, value): + """ Toolkit specific method to set the control's value. """ + self.control.setValue(value) + + def _get_control_alignment(self): + """ Toolkit specific method to get the control's alignment. """ + # default implementation + return qalignment_to_alignment(self.control.alignment()) + + def _set_control_alignment(self, alignment): + """ Toolkit specific method to set the control's alignment. """ + self.control.setAlignment(alignment_to_qalignment(alignment)) diff --git a/pyface/ui/qt/fields/image_field.py b/pyface/ui/qt/fields/image_field.py new file mode 100644 index 000000000..04cfc8daf --- /dev/null +++ b/pyface/ui/qt/fields/image_field.py @@ -0,0 +1,52 @@ +# (C) Copyright 2005-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! + +""" The Qt-specific implementation of the text field class """ + +from pyface.qt.QtGui import QLabel, QPixmap + +from traits.api import provides + +from pyface.ui.qt.fields.field import Field +from pyface.fields.i_image_field import IImageField, MImageField + + +@provides(IImageField) +class ImageField(MImageField, Field): + """ The Qt-specific implementation of the image field class + """ + + # ------------------------------------------------------------------------ + # IWidget interface + # ------------------------------------------------------------------------ + + def _create_control(self, parent): + """ Create the toolkit-specific control that represents the widget. """ + control = QLabel(parent) + return control + + # ------------------------------------------------------------------------ + # Private interface + # ------------------------------------------------------------------------ + + def _get_control_value(self): + """ Toolkit specific method to get the control's value. """ + # XXX we should really have a ToolkitImage subclass of Image and + # which would be the correct subclass to return. + return self.control.pixmap() + + def _set_control_value(self, value): + """ Toolkit specific method to set the control's value. """ + if value is None: + self._toolkit_value = None + self.control.setPixmap(QPixmap()) + else: + self._toolkit_value = self.value.create_bitmap() + self.control.setPixmap(self._toolkit_value) diff --git a/pyface/ui/qt/fields/label_field.py b/pyface/ui/qt/fields/label_field.py new file mode 100644 index 000000000..8c4ab136b --- /dev/null +++ b/pyface/ui/qt/fields/label_field.py @@ -0,0 +1,49 @@ +# (C) Copyright 2005-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! + +""" The Qt-specific implementation of the label field class """ + + +from traits.api import provides + +from pyface.fields.i_label_field import ILabelField, MLabelField +from pyface.qt.QtGui import QLabel, QPixmap +from .field import Field + + +@provides(ILabelField) +class LabelField(MLabelField, Field): + """ The Qt-specific implementation of the label field class """ + + # ------------------------------------------------------------------------ + # Private interface + # ------------------------------------------------------------------------ + + def _create_control(self, parent): + """ Create the toolkit-specific control that represents the widget. """ + control = QLabel(parent) + return control + + # Toolkit control interface --------------------------------------------- + + def _get_control_value(self): + """ Toolkit specific method to get the control's value. """ + return self.control.text() + + def _set_control_value(self, value): + """ Toolkit specific method to set the control's value. """ + return self.control.setText(value) + + def _set_control_icon(self, icon): + """ Toolkit specific method to set the control's icon. """ + if icon is not None: + self.control.setPixmap(self.icon.create_bitmap()) + else: + self.control.setPixmap(QPixmap()) diff --git a/pyface/ui/qt/fields/spin_field.py b/pyface/ui/qt/fields/spin_field.py index ce1e52db3..e011d9a12 100644 --- a/pyface/ui/qt/fields/spin_field.py +++ b/pyface/ui/qt/fields/spin_field.py @@ -18,11 +18,11 @@ from pyface.fields.i_spin_field import ISpinField, MSpinField from pyface.qt.QtGui import QSpinBox -from .field import Field +from .editable_field import EditableField @provides(ISpinField) -class SpinField(MSpinField, Field): +class SpinField(MSpinField, EditableField): """ The Qt-specific implementation of the spin field class """ # ------------------------------------------------------------------------ @@ -39,21 +39,6 @@ def _create_control(self, parent): # Private interface # ------------------------------------------------------------------------ - def _get_control_value(self): - """ Toolkit specific method to get the control's value. """ - return self.control.value() - - def _set_control_value(self, value): - """ Toolkit specific method to set the control's value. """ - self.control.setValue(value) - - def _observe_control_value(self, remove=False): - """ Toolkit specific method to change the control value observer. """ - if remove: - self.control.valueChanged[int].disconnect(self._update_value) - else: - self.control.valueChanged[int].connect(self._update_value) - def _get_control_bounds(self): """ Toolkit specific method to get the control's bounds. """ return (self.control.minimum(), self.control.maximum()) @@ -61,3 +46,11 @@ def _get_control_bounds(self): def _set_control_bounds(self, bounds): """ Toolkit specific method to set the control's bounds. """ self.control.setRange(*bounds) + + def _get_control_wrap(self): + """ Toolkit specific method to get whether the control wraps. """ + return self.control.wrapping() + + def _set_control_wrap(self, wrap): + """ Toolkit specific method to set whether the control wraps. """ + self.control.setWrapping(wrap) diff --git a/pyface/ui/qt/fields/text_field.py b/pyface/ui/qt/fields/text_field.py index d5e195511..5675b691a 100644 --- a/pyface/ui/qt/fields/text_field.py +++ b/pyface/ui/qt/fields/text_field.py @@ -15,8 +15,7 @@ from pyface.fields.i_text_field import ITextField, MTextField from pyface.qt.QtGui import QLineEdit -from .field import Field - +from .editable_field import EditableField ECHO_TO_QT_ECHO_MODE = { "normal": QLineEdit.EchoMode.Normal, @@ -33,7 +32,7 @@ @provides(ITextField) -class TextField(MTextField, Field): +class TextField(MTextField, EditableField): """ The Qt-specific implementation of the text field class """ #: Display typed text, or one of several hidden "password" modes. diff --git a/pyface/ui/qt/fields/time_field.py b/pyface/ui/qt/fields/time_field.py index 0823e9fd2..07d96df2a 100644 --- a/pyface/ui/qt/fields/time_field.py +++ b/pyface/ui/qt/fields/time_field.py @@ -17,11 +17,11 @@ from pyface.fields.i_time_field import ITimeField, MTimeField from pyface.ui.qt.util.datetime import pytime_to_qtime, qtime_to_pytime -from .field import Field +from .editable_field import EditableField @provides(ITimeField) -class TimeField(MTimeField, Field): +class TimeField(MTimeField, EditableField): """ The Qt-specific implementation of the time field class """ # ------------------------------------------------------------------------ diff --git a/pyface/ui/qt/fields/toggle_field.py b/pyface/ui/qt/fields/toggle_field.py index fb6b60733..facb5d2f1 100644 --- a/pyface/ui/qt/fields/toggle_field.py +++ b/pyface/ui/qt/fields/toggle_field.py @@ -17,11 +17,11 @@ from pyface.qt.QtGui import ( QCheckBox, QIcon, QPushButton, QRadioButton ) -from .field import Field +from .editable_field import EditableField @provides(IToggleField) -class ToggleField(MToggleField, Field): +class ToggleField(MToggleField, EditableField): """ The Qt-specific implementation of the toggle field class """ # ------------------------------------------------------------------------ @@ -60,6 +60,16 @@ def _observe_control_value(self, remove=False): else: self.control.toggled.connect(self._update_value) + def _get_control_alignment(self): + """ Toolkit specific method to get the control's alignment. """ + # dummy implementation + return self.alignment + + def _set_control_alignment(self, alignment): + """ Toolkit specific method to set the control's alignment. """ + # use stylesheet for button alignment + self.control.setStyleSheet(f"text-align: {alignment}") + class CheckBoxField(ToggleField): """ The Qt-specific implementation of the checkbox class """ diff --git a/pyface/ui/qt/font.py b/pyface/ui/qt/font.py index 1c0b2a20f..beb21e789 100644 --- a/pyface/ui/qt/font.py +++ b/pyface/ui/qt/font.py @@ -187,7 +187,7 @@ def map_to_nearest(target, mapping): Returns ------- - value : any + value : Any The value corresponding to the nearest key. In the case of a tie, the first value is returned. """ diff --git a/pyface/ui/qt/system_metrics.py b/pyface/ui/qt/system_metrics.py index 4f230453a..06ade0135 100644 --- a/pyface/ui/qt/system_metrics.py +++ b/pyface/ui/qt/system_metrics.py @@ -11,14 +11,10 @@ # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms described in the PyQt GPL exception also apply - -from pyface.qt import QtGui, is_qt4 - - -from traits.api import HasTraits, Int, Property, provides, Tuple - +from traits.api import HasTraits, Int, List, Property, provides, Tuple from pyface.i_system_metrics import ISystemMetrics, MSystemMetrics +from pyface.qt import QtGui @provides(ISystemMetrics) @@ -29,10 +25,17 @@ class SystemMetrics(MSystemMetrics, HasTraits): # 'ISystemMetrics' interface ------------------------------------------- + #: The width of the main screen in pixels. screen_width = Property(Int) + #: The height of the main screen in pixels. screen_height = Property(Int) + #: The height and width of each screen in pixels + screen_sizes = Property(List(Tuple(Int, Int))) + + #: Background color of a standard dialog window as a tuple of RGB values + #: between 0.0 and 1.0. dialog_background_color = Property(Tuple) # ------------------------------------------------------------------------ @@ -40,26 +43,28 @@ class SystemMetrics(MSystemMetrics, HasTraits): # ------------------------------------------------------------------------ def _get_screen_width(self): - # QDesktopWidget.screenGeometry() is deprecated and Qt docs - # suggest using screens() instead, but screens in not available in qt - # see issue: enthought/pyface#721 - if not is_qt4: - return QtGui.QApplication.instance().screens()[0].availableGeometry().width() + screens = self.screen_sizes + if len(screens) == 0: + return 0 else: - return QtGui.QApplication.instance().desktop().availableGeometry().width() + return screens[0][0] def _get_screen_height(self): - # QDesktopWidget.screenGeometry(int screen) is deprecated and Qt docs - # suggest using screens() instead, but screens in not available in qt - # see issue: enthought/pyface#721 - if not is_qt4: - return ( - QtGui.QApplication.instance().screens()[0].availableGeometry().height() - ) + screens = self.screen_sizes + if len(screens) == 0: + return 0 else: - return ( - QtGui.QApplication.instance().desktop().availableGeometry().height() + return screens[0][1] + + def _get_screen_sizes(self): + screens = QtGui.QApplication.instance().screens() + return [ + ( + screen.availableGeometry().width(), + screen.availableGeometry().height(), ) + for screen in screens + ] def _get_dialog_background_color(self): color = ( diff --git a/pyface/ui/qt/tasks/split_editor_area_pane.py b/pyface/ui/qt/tasks/split_editor_area_pane.py index 9baf03687..889857fc8 100644 --- a/pyface/ui/qt/tasks/split_editor_area_pane.py +++ b/pyface/ui/qt/tasks/split_editor_area_pane.py @@ -472,7 +472,7 @@ class EditorAreaWidget(QtGui.QSplitter): def __init__(self, editor_area, parent=None, tabwidget=None): """ Creates an EditorAreaWidget object. - editor_area : global SplitEditorAreaPane instance + editor_area : global SplitEditorAreaPane parent : parent splitter tabwidget : tabwidget object contained by this splitter @@ -759,7 +759,7 @@ class DraggableTabWidget(QtGui.QTabWidget): def __init__(self, editor_area, parent): """ - editor_area : global SplitEditorAreaPane instance + editor_area : global SplitEditorAreaPane parent : parent of the tabwidget """ super().__init__(parent) diff --git a/pyface/ui/qt/tests/test_qt_imports.py b/pyface/ui/qt/tests/test_qt_imports.py index 3327c61a6..821c99de3 100644 --- a/pyface/ui/qt/tests/test_qt_imports.py +++ b/pyface/ui/qt/tests/test_qt_imports.py @@ -21,10 +21,13 @@ def test_imports(self): import pyface.qt.QtGui # noqa: F401 import pyface.qt.QtNetwork # noqa: F401 import pyface.qt.QtOpenGL # noqa: F401 + import pyface.qt.QtOpenGLWidgets # noqa: F401 import pyface.qt.QtSvg # noqa: F401 + import pyface.qt.QtSvgWidgets # noqa: F401 import pyface.qt.QtTest # noqa: F401 import pyface.qt.QtMultimedia # noqa: F401 import pyface.qt.QtMultimediaWidgets # noqa: F401 + import pyface.qt.QtWidgets # noqa: F401 @unittest.skipIf(sys.version_info > (3, 6), "WebKit is not available") def test_import_web_kit(self): diff --git a/pyface/ui/qt/util/alignment.py b/pyface/ui/qt/util/alignment.py new file mode 100644 index 000000000..e9b701664 --- /dev/null +++ b/pyface/ui/qt/util/alignment.py @@ -0,0 +1,67 @@ +# (C) Copyright 2005-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! + +""" Qt alignment helper functions """ + +from pyface.qt.QtCore import Qt + + +ALIGNMENT_TO_QALIGNMENT = { + "default": Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter, + "left": Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter, + "center": Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter, + "right": Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter, +} +QALIGNMENT_TO_ALIGNMENT = { + 0: "default", + int(Qt.AlignmentFlag.AlignLeft): "left", + int(Qt.AlignmentFlag.AlignHCenter): "center", + int(Qt.AlignmentFlag.AlignRight): "right", +} +ALIGNMENT_MASK = ( + Qt.AlignmentFlag.AlignLeft + | Qt.AlignmentFlag.AlignHCenter + | Qt.AlignmentFlag.AlignRight +) + + +def alignment_to_qalignment(alignment): + """Convert an Alignment trait value to a Qt Alignment + + Parameters + ---------- + alignment : str + An Alignment trait value, one of "default", "left", "center", or + "right". + + Returns + ------- + qalignment : Qt.AlignmentFlag + A Qt.AlignmentFlag value + """ + return ALIGNMENT_TO_QALIGNMENT[alignment] + + +def qalignment_to_alignment(alignment): + """Convert a Qt Alignment value to an Alignment trait + + Parameters + ---------- + qalignment : Qt.AlignmentFlag + A Qt.AlignmentFlag value + + Returns + ------- + alignment : str + An Alignment trait value, one of "default", "left", "center", or + "right". + """ + h_alignment = int(alignment & ALIGNMENT_MASK) + return QALIGNMENT_TO_ALIGNMENT[h_alignment] diff --git a/pyface/ui/qt/util/event_loop_helper.py b/pyface/ui/qt/util/event_loop_helper.py index a863bf7ac..18676ce08 100644 --- a/pyface/ui/qt/util/event_loop_helper.py +++ b/pyface/ui/qt/util/event_loop_helper.py @@ -110,7 +110,7 @@ def event_loop_until_condition(self, condition, timeout=10.0): Parameters ---------- - condition : callable + condition : Callable A callable to determine if the stop criteria have been met. This should accept no arguments. diff --git a/pyface/ui/qt/util/gui_test_assistant.py b/pyface/ui/qt/util/gui_test_assistant.py index 12e3bdcec..620cc05b9 100644 --- a/pyface/ui/qt/util/gui_test_assistant.py +++ b/pyface/ui/qt/util/gui_test_assistant.py @@ -159,7 +159,7 @@ def event_loop_until_condition(self, condition, timeout=10.0): Parameters ---------- - condition : callable + condition : Callable A callable to determine if the stop criteria have been met. This should accept no arguments. timeout : float @@ -197,7 +197,7 @@ def assertEventuallyTrueInGui(self, condition, timeout=10.0): Parameters ---------- - condition : callable() -> bool + condition : Callable() -> bool Callable accepting no arguments and returning a bool. timeout : float Maximum length of time to wait for the condition to become @@ -224,11 +224,11 @@ def assertTraitChangesInEventLoop( Parameters ---------- - obj : HasTraits + obj : traits.has_traits.HasTraits The HasTraits instance whose trait will change. trait : str The extended trait name of trait changes to listen to. - condition : callable + condition : Callable A callable to determine if the stop criteria have been met. This takes obj as the only argument. count : int @@ -269,7 +269,7 @@ def event_loop_until_traits_change(self, traits_object, *traits, **kw): Paramaters ---------- - traits_object : HasTraits instance + traits_object : traits.has_traits.HasTraits The object on which to listen for a trait events traits : one or more str The names of the traits to listen to for events @@ -345,7 +345,7 @@ def find_qt_widget(self, start, type_, test=None): type_ : type A subclass of QWidget to use for an initial type filter while walking the tree - test : callable + test : Callable A filter function that takes one argument (the current widget being evaluated) and returns either True or False to determine if the widget matches the required criteria. diff --git a/pyface/ui/qt/util/modal_dialog_tester.py b/pyface/ui/qt/util/modal_dialog_tester.py index 00782fde3..b4fbf0cdb 100644 --- a/pyface/ui/qt/util/modal_dialog_tester.py +++ b/pyface/ui/qt/util/modal_dialog_tester.py @@ -28,7 +28,7 @@ class ModalDialogTester(object): """ Test helper for code that open a traits ui or QDialog window. - Usage + Notes ----- :: @@ -86,7 +86,7 @@ def open_and_run(self, when_opened, *args, **kwargs): Parameters ---------- - when_opened : callable + when_opened : Callable A callable to be called when the dialog has been created and opened. The callable with be called with the tester instance as argument. @@ -142,7 +142,7 @@ def open_and_wait(self, when_opened, *args, **kwargs): Parameters ---------- - when_opened : callable + when_opened : Callable A callable to be called when the dialog has been created and opened. The callable with be called with the tester instance as argument. diff --git a/pyface/ui/qt/util/testing.py b/pyface/ui/qt/util/testing.py index cfdf36368..c35276d97 100644 --- a/pyface/ui/qt/util/testing.py +++ b/pyface/ui/qt/util/testing.py @@ -128,7 +128,7 @@ def find_qt_widget(start, type_, test=None): type_ : type A subclass of QWidget to use for an initial type filter while walking the tree - test : callable + test : Callable A filter function that takes one argument (the current widget being evaluated) and returns either True or False to determine if the widget matches the required criteria. diff --git a/pyface/ui/qt/workbench/split_tab_widget.py b/pyface/ui/qt/workbench/split_tab_widget.py index 3536ae13a..83d858ace 100644 --- a/pyface/ui/qt/workbench/split_tab_widget.py +++ b/pyface/ui/qt/workbench/split_tab_widget.py @@ -19,8 +19,7 @@ import sys import warnings - -from pyface.qt import QtCore, QtGui, qt_api +from pyface.qt import QtCore, QtGui from pyface.image_resource import ImageResource @@ -300,15 +299,6 @@ def _set_focus(self): def _focus_changed(self, old, new): """ Handle a change in focus that affects the current tab. """ - # It is possible for the C++ layer of this object to be deleted between - # the time when the focus change signal is emitted and time when the - # slots are dispatched by the Qt event loop. This may be a bug in PyQt4. - if qt_api == "pyqt": - import sip - - if sip.isdeleted(self): - return - if self._repeat_focus_changes: self.focus_changed.emit(old, new) diff --git a/pyface/ui/wx/action/menu_bar_manager.py b/pyface/ui/wx/action/menu_bar_manager.py index 143d91bac..efe8d8d49 100644 --- a/pyface/ui/wx/action/menu_bar_manager.py +++ b/pyface/ui/wx/action/menu_bar_manager.py @@ -15,10 +15,13 @@ import wx +from traits.api import provides from pyface.action.action_manager import ActionManager +from pyface.action.i_menu_bar_manager import IMenuBarManager +@provides(IMenuBarManager) class MenuBarManager(ActionManager): """ A menu bar manager realizes itself in errr, a menu bar control. """ diff --git a/pyface/ui/wx/action/menu_manager.py b/pyface/ui/wx/action/menu_manager.py index 2ed052fac..eef36da3f 100644 --- a/pyface/ui/wx/action/menu_manager.py +++ b/pyface/ui/wx/action/menu_manager.py @@ -16,14 +16,16 @@ import wx -from traits.api import Str, Bool +from traits.api import Str, Bool, provides from pyface.action.action_manager import ActionManager from pyface.action.action_manager_item import ActionManagerItem from pyface.action.group import Group +from pyface.action.i_menu_manager import IMenuManager +@provides(IMenuManager) class MenuManager(ActionManager, ActionManagerItem): """ A menu manager realizes itself in a menu control. diff --git a/pyface/ui/wx/action/status_bar_manager.py b/pyface/ui/wx/action/status_bar_manager.py index a268492f5..a766a7778 100644 --- a/pyface/ui/wx/action/status_bar_manager.py +++ b/pyface/ui/wx/action/status_bar_manager.py @@ -16,9 +16,12 @@ import wx -from traits.api import Any, HasTraits, List, Property, Str +from traits.api import Any, HasTraits, List, Property, Str, provides +from pyface.action.i_status_bar_manager import IStatusBarManager + +@provides(IStatusBarManager) class StatusBarManager(HasTraits): """ A status bar manager realizes itself in a status bar control. """ diff --git a/pyface/ui/wx/action/tool_bar_manager.py b/pyface/ui/wx/action/tool_bar_manager.py index b2fb60912..41584639f 100644 --- a/pyface/ui/wx/action/tool_bar_manager.py +++ b/pyface/ui/wx/action/tool_bar_manager.py @@ -15,15 +15,16 @@ import wx - -from traits.api import Bool, Enum, Instance, Str, Tuple - +from traits.api import Bool, Instance, Str, Tuple, provides from pyface.wx.aui import aui as AUI from pyface.image_cache import ImageCache from pyface.action.action_manager import ActionManager +from pyface.action.i_tool_bar_manager import IToolBarManager +from pyface.ui_traits import Orientation +@provides(IToolBarManager) class ToolBarManager(ActionManager): """ A tool bar manager realizes itself in errr, a tool bar control. """ @@ -42,7 +43,7 @@ class ToolBarManager(ActionManager): name = Str("ToolBar") # The orientation of the toolbar. - orientation = Enum("horizontal", "vertical") + orientation = Orientation("horizontal") # Should we display the name of each tool bar tool under its image? show_tool_names = Bool(True) diff --git a/pyface/ui/wx/application_window.py b/pyface/ui/wx/application_window.py index 659f9c7af..dcac63a49 100644 --- a/pyface/ui/wx/application_window.py +++ b/pyface/ui/wx/application_window.py @@ -14,14 +14,9 @@ import wx -from traits.api import Instance, List, Str, observe, provides - -from pyface.action.api import MenuBarManager, StatusBarManager -from pyface.action.api import ToolBarManager -from pyface.i_application_window import ( - IApplicationWindow, MApplicationWindow, -) -from pyface.ui_traits import Image +from traits.api import Str, observe, provides + +from pyface.i_application_window import IApplicationWindow, MApplicationWindow from pyface.wx.aui import aui, PyfaceAuiManager from .image_resource import ImageResource from .window import Window @@ -33,30 +28,8 @@ class ApplicationWindow(MApplicationWindow, Window): IApplicationWindow interface for the API documentation. """ - # 'IApplicationWindow' interface --------------------------------------- - - icon = Image() - - menu_bar_manager = Instance(MenuBarManager) - - status_bar_manager = Instance(StatusBarManager) - - tool_bar_manager = Instance(ToolBarManager) - - # If the underlying toolkit supports multiple toolbars then you can use - # this list instead. - tool_bar_managers = List(ToolBarManager) - # 'IWindow' interface -------------------------------------------------# - # fixme: We can't set the default value of the actual 'size' trait here as - # in the toolkit-specific event handlers for window size and position - # changes, we set the value of the shadow '_size' trait. The problem is - # that by doing that traits never knows that the trait has been set and - # hence always returns the default value! Using a trait initializer method - # seems to work however (e.g. 'def _size_default'). Hmmmm.... - ## size = (800, 600) - title = Str("Pyface") # ------------------------------------------------------------------------ @@ -79,7 +52,7 @@ def _create_status_bar(self, parent): self.control.SetStatusBar(status_bar) def _create_tool_bar(self, parent): - tool_bar_managers = self._get_tool_bar_managers() + tool_bar_managers = self.tool_bar_managers if len(tool_bar_managers) > 0: for tool_bar_manager in reversed(tool_bar_managers): tool_bar = tool_bar_manager.create_tool_bar(parent, aui=True) @@ -174,18 +147,6 @@ def _get_tool_bar_pane_info(self, tool_bar): return info - def _get_tool_bar_managers(self): - """ Return all tool bar managers specified for the window. """ - - # fixme: V3 remove the old-style single toolbar option! - if self.tool_bar_manager is not None: - tool_bar_managers = [self.tool_bar_manager] - - else: - tool_bar_managers = self.tool_bar_managers - - return tool_bar_managers - def _wx_enable_tool_bar(self, tool_bar, enabled): """ Enable/Disablea tool bar. """ @@ -225,7 +186,7 @@ def _status_bar_manager_changed(self, old, new): old.destroy() self._create_status_bar(self.control) - @observe("tool_bar_manager, tool_bar_managers.items") + @observe("tool_bar_managers.items") def _update_tool_bar_managers(self, event): if self.control is not None: self._create_tool_bar(self.control) diff --git a/pyface/ui/wx/fields/combo_field.py b/pyface/ui/wx/fields/combo_field.py index 73bf80b2a..9193c6418 100644 --- a/pyface/ui/wx/fields/combo_field.py +++ b/pyface/ui/wx/fields/combo_field.py @@ -16,11 +16,11 @@ from traits.api import provides from pyface.fields.i_combo_field import IComboField, MComboField -from .field import Field +from .editable_field import EditableField @provides(IComboField) -class ComboField(MComboField, Field): +class ComboField(MComboField, EditableField): """ The Wx-specific implementation of the combo field class """ # ------------------------------------------------------------------------ diff --git a/pyface/ui/wx/fields/editable_field.py b/pyface/ui/wx/fields/editable_field.py new file mode 100644 index 000000000..6ead41ef8 --- /dev/null +++ b/pyface/ui/wx/fields/editable_field.py @@ -0,0 +1,25 @@ +# (C) Copyright 2005-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! + +""" The Wx-specific implementation of the text field class """ + + +from traits.api import provides + +from pyface.fields.i_editable_field import IEditableField, MEditableField +from pyface.ui.wx.fields.field import Field + + +@provides(IEditableField) +class EditableField(MEditableField, Field): + """ The Wx-specific implementation of the EditableField class + + This is an abstract class which is not meant to be instantiated. + """ diff --git a/pyface/ui/wx/fields/field.py b/pyface/ui/wx/fields/field.py index d2f484297..8a7f5ee0c 100644 --- a/pyface/ui/wx/fields/field.py +++ b/pyface/ui/wx/fields/field.py @@ -11,10 +11,13 @@ """ The Wx-specific implementation of the text field class """ -from traits.api import Any, provides +from traits.api import provides from pyface.fields.i_field import IField, MField from pyface.ui.wx.layout_widget import LayoutWidget +from pyface.ui.wx.util.alignment import ( + get_alignment_style, set_alignment_style +) @provides(IField) @@ -24,5 +27,17 @@ class Field(MField, LayoutWidget): This is an abstract class which is not meant to be instantiated. """ - #: The value held by the field. - value = Any() + # ------------------------------------------------------------------------ + # Private interface + # ------------------------------------------------------------------------ + + def _get_control_alignment(self): + """ Toolkit specific method to get the control's read_only state. """ + return get_alignment_style(self.control.GetWindowStyle()) + + def _set_control_alignment(self, alignment): + """ Toolkit specific method to set the control's read_only state. """ + old_style = self.control.GetWindowStyle() + new_style = set_alignment_style(alignment, old_style) + self.control.SetWindowStyle(new_style) + self.control.Refresh() diff --git a/pyface/ui/wx/fields/image_field.py b/pyface/ui/wx/fields/image_field.py new file mode 100644 index 000000000..5da484250 --- /dev/null +++ b/pyface/ui/wx/fields/image_field.py @@ -0,0 +1,51 @@ +# (C) Copyright 2005-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! + +""" The Qt-specific implementation of the text field class """ + +from traits.api import provides + +from pyface.ui.wx.fields.field import Field +from pyface.fields.i_image_field import IImageField, MImageField +from pyface.wx.image_control import ImageControl + + +@provides(IImageField) +class ImageField(MImageField, Field): + """ The Wx-specific implementation of the image field class + + """ + + # ------------------------------------------------------------------------ + # IWidget interface + # ------------------------------------------------------------------------ + + def _create_control(self, parent): + """ Create the toolkit-specific control that represents the widget. """ + control = ImageControl(parent) + return control + + # ------------------------------------------------------------------------ + # Private interface + # ------------------------------------------------------------------------ + + def _get_control_value(self): + """ Toolkit specific method to get the control's value. """ + # XXX we should really have a ToolkitImage subclass of Image and + # which would be the correct subclass to return. + return self.control.GetBitmap() + + def _set_control_value(self, value): + """ Toolkit specific method to set the control's value. """ + if value is None: + self._toolkit_value = None + else: + self._toolkit_value = self.value.create_bitmap() + self.control.SetBitmap(self._toolkit_value) diff --git a/pyface/ui/wx/fields/label_field.py b/pyface/ui/wx/fields/label_field.py new file mode 100644 index 000000000..46a69aff4 --- /dev/null +++ b/pyface/ui/wx/fields/label_field.py @@ -0,0 +1,47 @@ +# (C) Copyright 2005-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! + +""" The Wx-specific implementation of the label field class """ + +import wx + +from traits.api import provides + +from pyface.fields.i_label_field import ILabelField, MLabelField +from .field import Field + + +@provides(ILabelField) +class LabelField(MLabelField, Field): + """ The Wx-specific implementation of the label field class """ + + def _create_control(self, parent): + """ Create the toolkit-specific control that represents the widget. """ + control = wx.StaticText(parent) + return control + + # ------------------------------------------------------------------------ + # Private interface + # ------------------------------------------------------------------------ + + # Toolkit control interface --------------------------------------------- + + def _get_control_value(self): + """ Toolkit specific method to get the control's value. """ + return self.control.GetLabel() + + def _set_control_value(self, value): + """ Toolkit specific method to set the control's value. """ + self.control.SetLabel(value) + + def _set_control_icon(self, icon): + """ Toolkit specific method to set the control's icon. """ + # don't support icons on Wx for now + pass diff --git a/pyface/ui/wx/fields/spin_field.py b/pyface/ui/wx/fields/spin_field.py index 78e02fc47..37ed16bc2 100644 --- a/pyface/ui/wx/fields/spin_field.py +++ b/pyface/ui/wx/fields/spin_field.py @@ -19,11 +19,11 @@ from traits.api import provides from pyface.fields.i_spin_field import ISpinField, MSpinField -from .field import Field +from .editable_field import EditableField @provides(ISpinField) -class SpinField(MSpinField, Field): +class SpinField(MSpinField, EditableField): """ The Wx-specific implementation of the spin field class """ # ------------------------------------------------------------------------ @@ -65,3 +65,13 @@ def _get_control_bounds(self): def _set_control_bounds(self, bounds): """ Toolkit specific method to set the control's bounds. """ self.control.SetRange(*bounds) + + def _get_control_wrap(self): + """ Toolkit specific method to get whether the control wraps. """ + return bool(self.control.GetWindowStyle() & wx.SP_WRAP) + + def _set_control_wrap(self, wrap): + """ Toolkit specific method to set whether the control wraps. """ + if wrap != self._get_control_wrap(): + self.control.ToggleWindowStyle(wx.SP_WRAP) + self.control.Refresh() diff --git a/pyface/ui/wx/fields/text_field.py b/pyface/ui/wx/fields/text_field.py index 0f745ccbf..efdf4c6f8 100644 --- a/pyface/ui/wx/fields/text_field.py +++ b/pyface/ui/wx/fields/text_field.py @@ -16,11 +16,11 @@ from traits.api import provides from pyface.fields.i_text_field import ITextField, MTextField -from .field import Field +from .editable_field import EditableField @provides(ITextField) -class TextField(MTextField, Field): +class TextField(MTextField, EditableField): """ The Wx-specific implementation of the text field class """ # ------------------------------------------------------------------------ diff --git a/pyface/ui/wx/fields/time_field.py b/pyface/ui/wx/fields/time_field.py index e310c58d0..ce146856f 100644 --- a/pyface/ui/wx/fields/time_field.py +++ b/pyface/ui/wx/fields/time_field.py @@ -17,11 +17,11 @@ from traits.api import provides from pyface.fields.i_time_field import ITimeField, MTimeField -from .field import Field +from .editable_field import EditableField @provides(ITimeField) -class TimeField(MTimeField, Field): +class TimeField(MTimeField, EditableField): """ The Wx-specific implementation of the time field class """ # ------------------------------------------------------------------------ diff --git a/pyface/ui/wx/fields/toggle_field.py b/pyface/ui/wx/fields/toggle_field.py index 69209b005..bb025cf64 100644 --- a/pyface/ui/wx/fields/toggle_field.py +++ b/pyface/ui/wx/fields/toggle_field.py @@ -15,11 +15,11 @@ from traits.api import provides from pyface.fields.i_toggle_field import IToggleField, MToggleField -from .field import Field +from .editable_field import EditableField @provides(IToggleField) -class ToggleField(MToggleField, Field): +class ToggleField(MToggleField, EditableField): """ The Wx-specific implementation of the toggle field class """ # ------------------------------------------------------------------------ diff --git a/pyface/ui/wx/init.py b/pyface/ui/wx/init.py index bc7c08898..a94f1ead8 100644 --- a/pyface/ui/wx/init.py +++ b/pyface/ui/wx/init.py @@ -36,3 +36,7 @@ if ui_handler is None: # Tell the traits notification handlers to use this UI handler set_ui_handler(GUI.invoke_later) + +# Fix for broken Pycrust introspect module. Imported to patch pycrust. +# CJW: is this still needed? Has been in for years. +from pyface.util import fix_introspect_bug # noqa: F401, E402 diff --git a/pyface/ui/wx/layout_widget.py b/pyface/ui/wx/layout_widget.py index 665fa44e0..dce282f31 100644 --- a/pyface/ui/wx/layout_widget.py +++ b/pyface/ui/wx/layout_widget.py @@ -94,7 +94,7 @@ def _size_to_wx_size(size): Returns ------- - wx_size : wx.Size instance + wx_size : wx.Size A corresponding wx Size instance. """ return wx.Size(*( @@ -108,7 +108,7 @@ def _wx_size_to_size(wx_size): Parameters ---------- - wx_size : wx.Size instance + wx_size : wx.Size A wx Size instance. Returns diff --git a/pyface/ui/wx/system_metrics.py b/pyface/ui/wx/system_metrics.py index 16c308f16..78730b445 100644 --- a/pyface/ui/wx/system_metrics.py +++ b/pyface/ui/wx/system_metrics.py @@ -15,12 +15,9 @@ import sys - import wx - -from traits.api import HasTraits, Int, Property, provides, Tuple - +from traits.api import HasTraits, Int, List, Property, provides, Tuple from pyface.i_system_metrics import ISystemMetrics, MSystemMetrics @@ -33,10 +30,17 @@ class SystemMetrics(MSystemMetrics, HasTraits): # 'ISystemMetrics' interface ------------------------------------------- + #: The width of the main screen in pixels. screen_width = Property(Int) + #: The height of the main screen in pixels. screen_height = Property(Int) + #: The height and width of each screen in pixels + screen_sizes = Property(List(Tuple(Int, Int))) + + #: Background color of a standard dialog window as a tuple of RGB values + #: between 0.0 and 1.0. dialog_background_color = Property(Tuple) # ------------------------------------------------------------------------ @@ -49,6 +53,9 @@ def _get_screen_width(self): def _get_screen_height(self): return wx.SystemSettings.GetMetric(wx.SYS_SCREEN_Y) + def _get_screen_sizes(self): + return [(self.screen_width, self.screen_height)] + def _get_dialog_background_color(self): if sys.platform == "darwin": # wx lies. diff --git a/pyface/ui/wx/util/alignment.py b/pyface/ui/wx/util/alignment.py new file mode 100644 index 000000000..589cfe103 --- /dev/null +++ b/pyface/ui/wx/util/alignment.py @@ -0,0 +1,41 @@ +# (C) Copyright 2005-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! +# +# Author: Enthought, Inc. +# Description: + +""" Utilities for handling alignment """ + +import wx + + +ALIGNMENT_TO_WX_ALIGNMENT = { + 'default': wx.TE_LEFT, + 'left': wx.TE_LEFT, + 'center': wx.TE_CENTRE, + 'right': wx.TE_RIGHT, +} +WX_ALIGNMENT_TO_ALIGNMENT = { + 0: 'default', + wx.TE_LEFT: 'left', + wx.TE_CENTRE: 'center', + wx.TE_RIGHT: 'right', +} +ALIGNMENT_MASK = wx.TE_LEFT | wx.TE_CENTRE | wx.TE_RIGHT + + +def get_alignment_style(style_flags): + alignment_flag = style_flags & ALIGNMENT_MASK + return WX_ALIGNMENT_TO_ALIGNMENT[alignment_flag] + + +def set_alignment_style(alignment, style_flags): + other_flags = style_flags & ~ALIGNMENT_MASK + return other_flags | ALIGNMENT_TO_WX_ALIGNMENT[alignment] diff --git a/pyface/ui_traits.py b/pyface/ui_traits.py index e78274123..a968e8ad8 100644 --- a/pyface/ui_traits.py +++ b/pyface/ui_traits.py @@ -149,7 +149,7 @@ class PyfaceColor(TraitType): """ A Trait which casts strings and tuples to a Pyface Color value. """ - #: The default value should be a tuple (factory, args, kwargs) + #: The default value should be a tuple (factory, args, kwargs). default_value_type = DefaultValue.callable_and_args def __init__(self, value=None, **metadata): @@ -161,6 +161,11 @@ def __init__(self, value=None, **metadata): super().__init__(default_value, **metadata) def validate(self, object, name, value): + """Validate the trait + + This accepts, Color values, parseable strings and RGB(A) sequences + (including numpy arrays). + """ if isinstance(value, Color): return value if isinstance(value, str): @@ -182,6 +187,7 @@ def validate(self, object, name, value): self.error(object, name, value) def info(self): + """Describe the trait""" return ( "a Pyface Color, a #-hexadecimal rgb or rgba string, a standard " "color name, or a sequence of RGBA or RGB values between 0 and 1" @@ -225,6 +231,10 @@ def __init__(self, value=None, *, parser=simple_parser, **metadata): super().__init__(default_value, **metadata) def validate(self, object, name, value): + """Validate the trait + + This accepts, Font values and parseable strings. + """ if isinstance(value, Font): return value if isinstance(value, str): @@ -236,6 +246,7 @@ def validate(self, object, name, value): self.error(object, name, value) def info(self): + """Describe the trait""" return ( "a Pyface Font, or a string describing a Pyface Font" ) @@ -247,15 +258,18 @@ def info(self): class BaseMB(ABCHasStrictTraits): - def __init__(self, *args, **traits): - """ Map posiitonal arguments to traits. + """ Base class for Margins and Borders - If one value is provided it is taken as the value for all sides. - If two values are provided, then the first argument is used for - left and right, while the second is used for top and bottom. - If 4 values are provided, then the arguments are mapped to - left, right, top, and bottom, respectively. - """ + The constructor of this class maps posiitonal arguments to traits. + + - If one value is provided it is taken as the value for all sides. + - If two values are provided, then the first argument is used for + left and right, while the second is used for top and bottom. + - If 4 values are provided, then the arguments are mapped to + left, right, top, and bottom, respectively. + """ + + def __init__(self, *args, **traits): n = len(args) if n > 0: if n == 1: @@ -277,32 +291,34 @@ def __init__(self, *args, **traits): class Margin(BaseMB): + """A HasTraits class that holds margin sizes.""" - # The amount of padding/margin at the top: + #: The amount of padding/margin at the top. top = Range(-32, 32, 0) - # The amount of padding/margin at the bottom: + #: The amount of padding/margin at the bottom. bottom = Range(-32, 32, 0) - # The amount of padding/margin on the left: + #: The amount of padding/margin on the left. left = Range(-32, 32, 0) - # The amount of padding/margin on the right: + #: The amount of padding/margin on the right. right = Range(-32, 32, 0) class Border(BaseMB): + """A HasTraits class that holds border thicknesses.""" - # The amount of border at the top: + #: The amount of border at the top. top = Range(0, 32, 0) - # The amount of border at the bottom: + #: The amount of border at the bottom. bottom = Range(0, 32, 0) - # The amount of border on the left: + #: The amount of border on the left. left = Range(0, 32, 0) - # The amount of border on the right: + #: The amount of border on the right. right = Range(0, 32, 0) @@ -311,13 +327,13 @@ class HasMargin(TraitType): tuple value that can be converted to one. """ - # The desired value class: + #: The desired value class. klass = Margin - # Define the default value for the trait: + #: Define the default value for the trait. default_value = Margin(0) - # A description of the type of value this trait accepts: + #: A description of the type of value this trait accepts. info_text = ( "a Margin instance, or an integer in the range from -32 to 32 " "or a tuple with 1, 2 or 4 integers in that range that can be " @@ -371,13 +387,13 @@ class HasBorder(HasMargin): or tuple value that can be converted to one. """ - # The desired value class: + #: The desired value class. klass = Border - # Define the default value for the trait: + #: Define the default value for the trait. default_value = Border(0) - # A description of the type of value this trait accepts: + #: A description of the type of value this trait accepts. info_text = ( "a Border instance, or an integer in the range from 0 to 32 " "or a tuple with 1, 2 or 4 integers in that range that can be " diff --git a/pyface/util/testing.py b/pyface/util/testing.py index ce154809f..d55069e8f 100644 --- a/pyface/util/testing.py +++ b/pyface/util/testing.py @@ -11,9 +11,15 @@ import re from unittest import TestSuite +try: + from importlib.metadata import version +except: + from importlib_metadata import version + from packaging.version import Version -from traits import __version__ as TRAITS_VERSION + +TRAITS_VERSION = version("traits") def filter_tests(test_suite, exclusion_pattern): diff --git a/pyface/wizard/api.py b/pyface/wizard/api.py index 3f1954541..777b39faa 100644 --- a/pyface/wizard/api.py +++ b/pyface/wizard/api.py @@ -27,14 +27,65 @@ """ +from .chained_wizard_controller import ChainedWizardController from .i_wizard_page import IWizardPage -from .wizard_page import WizardPage - from .i_wizard import IWizard -from .wizard import Wizard - from .i_wizard_controller import IWizardController from .wizard_controller import WizardController -from .chained_wizard import ChainedWizard -from .chained_wizard_controller import ChainedWizardController + +# ---------------------------------------------------------------------------- +# Deferred imports +# ---------------------------------------------------------------------------- + +# These imports have the side-effect of performing toolkit selection + +_toolkit_imports = { + 'Wizard': 'wizard', + 'WizardPage': 'wizard_page', +} + +# These are pyface.* imports that have selection as a side-effect +_relative_imports = { + 'ChainedWizard': "chained_wizard", +} + + +def __getattr__(name): + """Lazily load attributes with side-effects + + In particular, lazily load toolkit backend names. For efficiency, lazily + loaded objects are injected into the module namespace + """ + # sentinel object for no result + not_found = object() + result = not_found + + if name in _relative_imports: + from importlib import import_module + source = _relative_imports[name] + module = import_module(f"pyface.wizard.{source}") + result = getattr(module, name) + + elif name in _toolkit_imports: + from pyface.toolkit import toolkit_object + source = _toolkit_imports[name] + result = toolkit_object(f"wizard.{source}:{name}") + + if result is not_found: + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + globals()[name] = result + return result + + +# ---------------------------------------------------------------------------- +# Introspection support +# ---------------------------------------------------------------------------- + +# the list of available names we report for introspection purposes +_extra_names = set(_toolkit_imports) | set(_relative_imports) + + +def __dir__(): + return sorted(set(globals()) | _extra_names) diff --git a/pyface/workbench/action/action_controller.py b/pyface/workbench/action/action_controller.py index 0d03c8e07..28620553d 100644 --- a/pyface/workbench/action/action_controller.py +++ b/pyface/workbench/action/action_controller.py @@ -10,12 +10,12 @@ """ The action controller for workbench menu and tool bars. """ -from pyface.action.api import ActionController +from pyface.action.api import ActionController as PyfaceActionController from pyface.workbench.api import WorkbenchWindow from traits.api import Instance -class ActionController(ActionController): +class ActionController(PyfaceActionController): """ The action controller for workbench menu and tool bars. The controller is used to 'hook' the invocation of every action on the menu diff --git a/pyface/wx/image_control.py b/pyface/wx/image_control.py new file mode 100644 index 000000000..251294178 --- /dev/null +++ b/pyface/wx/image_control.py @@ -0,0 +1,59 @@ +# (C) Copyright 2004-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! + +""" Defines a wxPython ImageControl widget that is used by various Widgets. +""" + + +import wx + + +class ImageControl(wx.Window): + """A wxPython control that displays an image. + """ + + def __init__(self, parent, bitmap=None, padding=10): + """Initializes the object.""" + if bitmap is not None: + size = wx.Size( + bitmap.GetWidth() + padding, bitmap.GetHeight() + padding + ) + else: + size = wx.Size(32 + padding, 32 + padding) + + wx.Window.__init__(self, parent, -1, size=size,) + self._bitmap = bitmap + + # Set up the 'paint' event handler: + self.Bind(wx.EVT_PAINT, self._on_paint) + + def SetBitmap(self, bitmap=None): + """Sets the bitmap image.""" + if bitmap is not None: + if bitmap != self._bitmap: + self._bitmap = bitmap + self.Refresh() + else: + self._bitmap = None + + def GetBitmap(self): + """Gets the bitmap image.""" + return self._bitmap + + def _on_paint(self, event=None): + """Handles the control being re-painted.""" + wdc = wx.PaintDC(self) + wdx, wdy = self.GetClientSize() + bitmap = self._bitmap + if bitmap is None: + return + bdx = bitmap.GetWidth() + bdy = bitmap.GetHeight() + wdc.DrawBitmap(bitmap, (wdx - bdx) // 2, (wdy - bdy) // 2, True) diff --git a/pyproject.toml b/pyproject.toml index b0687a01f..b91cc3c31 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ dependencies = [ 'importlib-resources>=1.1.0; python_version<"3.9"', 'traits>=6.2', ] -version = '8.0.0.dev0' +version = '8.0.0' license = {file = "LICENSE.txt"} [project.entry-points.'pyface.toolkits'] diff --git a/shell_window.png b/shell_window.png new file mode 100644 index 000000000..12b18a063 Binary files /dev/null and b/shell_window.png differ