From 8a391a4a6335a88e8c8403231d5bd12a72cb577f Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Tue, 30 Nov 2021 12:15:54 +0000 Subject: [PATCH 01/12] Initial check-in of font trait and simple parser. --- pyface/font.py | 2 +- pyface/ui/wx/font.py | 3 -- pyface/ui_traits.py | 52 ++++++++++++++++++- pyface/util/font_parser.py | 103 +++++++++++++++++++++++++++++++++++++ 4 files changed, 155 insertions(+), 5 deletions(-) create mode 100644 pyface/util/font_parser.py diff --git a/pyface/font.py b/pyface/font.py index 09499428a..ee94b7c35 100644 --- a/pyface/font.py +++ b/pyface/font.py @@ -368,7 +368,7 @@ def to_toolkit(self): return font_to_toolkit_font(self) def __str__(self): - """ Produce a CSS-style representation of the font. """ + """ Produce a CSS2-style representation of the font. """ terms = [] if self.style != 'normal': terms.append(self.style) diff --git a/pyface/ui/wx/font.py b/pyface/ui/wx/font.py index 296d929dc..82bfd6ff2 100644 --- a/pyface/ui/wx/font.py +++ b/pyface/ui/wx/font.py @@ -18,9 +18,6 @@ import wx -from pyface.font import Font - - # font weight and size features changed in wxPython 4.1/wxWidgets 3.1 wx_python_4_1 = (wx.VERSION >= (4, 1)) diff --git a/pyface/ui_traits.py b/pyface/ui_traits.py index 2b136f320..a703818a2 100644 --- a/pyface/ui_traits.py +++ b/pyface/ui_traits.py @@ -29,8 +29,10 @@ from traits.trait_base import get_resource_path from pyface.color import Color +from pyface.font import Font from pyface.i_image import IImage from pyface.util.color_parser import ColorParseError +from pyface.util.font_parser import simple_parser, FontParseError logger = logging.getLogger(__name__) @@ -143,7 +145,7 @@ def create_editor(self): class PyfaceColor(TraitType): - """ A Trait which casts strings and tuples to a PyfaceColor value. + """ A Trait which casts strings and tuples to a Pyface Color value. """ #: The default value should be a tuple (factory, args, kwargs) @@ -185,6 +187,54 @@ def info(self): ) +# ------------------------------------------------------------------------------- +# Font +# ------------------------------------------------------------------------------- + + +class PyfaceFont(TraitType): + """ A Trait which casts strings to a Pyface Font value. + """ + + #: The default value should be a tuple (factory, args, kwargs) + default_value_type = DefaultValue.callable_and_args + + #: The parser to use when converting text to keyword args. This should + #: accept a string and return a dictionary of Font class trait values (ie. + #: "family", "size", "weight", etc.). + parser = simple_parser + + def __init__(self, value=None, *, parser=None, **metadata): + if parser is not None: + self.parser = parser + if value is not None: + font = self.validate(value) + default_value = ( + Font, + (), + font.trait_get(transient=lambda x: not x), + ) + else: + default_value = (Font, (), {}) + super().__init__(default_value, **metadata) + + def validate(self, object, name, value): + if isinstance(value, Font): + return value + if isinstance(value, str): + try: + return Font(**self.parser(value)) + except FontParseError: + self.error(object, name, value) + + self.error(object, name, value) + + def info(self): + return ( + "a Pyface Font, or a string describing a Pyface Font" + ) + + # ------------------------------------------------------------------------------- # Borders, Margins and Layout # ------------------------------------------------------------------------------- diff --git a/pyface/util/font_parser.py b/pyface/util/font_parser.py new file mode 100644 index 000000000..885562bbc --- /dev/null +++ b/pyface/util/font_parser.py @@ -0,0 +1,103 @@ +# (C) Copyright 2005-2021 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! + + +GENERIC_FAMILIES = { + 'default', 'fantasy', 'decorative', 'serif', 'roman', 'cursive', 'script', + 'sans-serif', 'swiss', 'monospace', 'modern', 'typewriter', 'teletype' +} +WEIGHTS = {'thin', 'extra-light', 'light', 'regular', 'medium', 'demi-bold', + 'bold', 'extra-bold', 'heavy', 'extra-heavy'} +STRETCHES = {'ultra-condensed', 'extra-condensed', 'condensed', + 'semi-condensed', 'semi-expanded', 'expanded', 'extra-expanded', + 'ultra-expanded'} +STYLES = {'italic', 'oblique'} +VARIANTS = {'small-caps'} +DECORATIONS = {'underline', 'strikethrough', 'overline'} +NOISE = {'pt', 'point', 'px', 'family'} + + +class FontParseError(ValueError): + """An exception raised when font parsing fails.""" + pass + + +def simple_parser(description): + """An extremely simple font description parser. + + This is roughly compatible with the various ad-hoc parsers in TraitsUI + and Kiva, allowing for the slight differences between them and adding + support for additional options supported by Pyface fonts, such as stretch + and variants. + + Parameters + ---------- + description : str + The font description to be parsed. + + Returns + ------- + properties : dict + Font properties suitable for use in creating a Pyface Font. + + Notes + ----- + This is not a particularly good parser, as it will fail to properly + parse something like "10 pt times new roman" or "14 pt computer modern" + since they have generic font names as part of the font face name. + """ + face = [] + generic_family = "" + size = None + weight = "normal" + stretch = "normal" + style = "normal" + variants = set() + decorations = set() + for word in description.lower().split(): + if word in NOISE: + continue + elif word in GENERIC_FAMILIES: + generic_family = word + elif word in WEIGHTS: + weight = word + elif word in STRETCHES: + stretch = word + elif word in STYLES: + style = word + elif word in VARIANTS: + variants.add(word) + elif word in DECORATIONS: + decorations.add(word) + else: + if size is not None: + try: + size = int(word) + except ValueError: + pass + face.append(word) + + family = [] + if face: + family.append(" ".join(face)) + if generic_family: + family.append(generic_family) + if not family: + family = ["default"] + + return { + 'family': family, + 'size': size, + 'weight': weight, + 'stretch': stretch, + 'style': style, + 'variants': variants, + 'decorations': decorations, + } From 12bf8e2bb85cc6aefa2a10d151a9e7380145c8ea Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Mon, 20 Dec 2021 16:32:00 +0000 Subject: [PATCH 02/12] Tests for simple parser. --- pyface/util/font_parser.py | 3 +- pyface/util/tests/test_font_parser.py | 199 ++++++++++++++++++++++++++ 2 files changed, 201 insertions(+), 1 deletion(-) create mode 100644 pyface/util/tests/test_font_parser.py diff --git a/pyface/util/font_parser.py b/pyface/util/font_parser.py index 885562bbc..261f0b481 100644 --- a/pyface/util/font_parser.py +++ b/pyface/util/font_parser.py @@ -77,9 +77,10 @@ def simple_parser(description): elif word in DECORATIONS: decorations.add(word) else: - if size is not None: + if size is None: try: size = int(word) + continue except ValueError: pass face.append(word) diff --git a/pyface/util/tests/test_font_parser.py b/pyface/util/tests/test_font_parser.py new file mode 100644 index 000000000..38af7d2a3 --- /dev/null +++ b/pyface/util/tests/test_font_parser.py @@ -0,0 +1,199 @@ +# (C) Copyright 2005-2021 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 itertools import chain, combinations +from unittest import TestCase + +from ..font_parser import ( + DECORATIONS, + GENERIC_FAMILIES, + NOISE, + STRETCHES, + STYLES, + VARIANTS, + WEIGHTS, + FontParseError, + simple_parser, +) + + +class TestSimpleParser(TestCase): + + def test_empty(self): + properties = simple_parser("") + self.assertEqual( + properties, + { + 'family': ["default"], + 'size': None, + 'weight': "normal", + 'stretch': "normal", + 'style': "normal", + 'variants': set(), + 'decorations': set(), + }, + ) + + def test_typical(self): + properties = simple_parser( + "10 pt bold condensed italic underline Helvetica sans-serif") + self.assertEqual( + properties, + { + 'family': ["helvetica", "sans-serif"], + 'size': 10, + 'weight': "bold", + 'stretch': "condensed", + 'style': "italic", + 'variants': set(), + 'decorations': {"underline"}, + }, + ) + + def test_noise(self): + for noise in NOISE: + with self.subTest(noise=noise): + properties = simple_parser(noise) + self.assertEqual( + properties, + { + 'family': ["default"], + 'size': None, + 'weight': "normal", + 'stretch': "normal", + 'style': "normal", + 'variants': set(), + 'decorations': set(), + }, + ) + + def test_generic_families(self): + for family in GENERIC_FAMILIES: + with self.subTest(family=family): + properties = simple_parser(family) + self.assertEqual( + properties, + { + 'family': [family], + 'size': None, + 'weight': "normal", + 'stretch': "normal", + 'style': "normal", + 'variants': set(), + 'decorations': set(), + }, + ) + + def test_size(self): + for size in [12, 24]: + with self.subTest(size=size): + properties = simple_parser(str(size)) + self.assertEqual( + properties, + { + 'family': ["default"], + 'size': size, + 'weight': "normal", + 'stretch': "normal", + 'style': "normal", + 'variants': set(), + 'decorations': set(), + }, + ) + + def test_weight(self): + for weight in WEIGHTS: + with self.subTest(weight=weight): + properties = simple_parser(weight) + self.assertEqual( + properties, + { + 'family': ["default"], + 'size': None, + 'weight': weight, + 'stretch': "normal", + 'style': "normal", + 'variants': set(), + 'decorations': set(), + }, + ) + + def test_stretch(self): + for stretch in STRETCHES: + with self.subTest(stretch=stretch): + properties = simple_parser(stretch) + self.assertEqual( + properties, + { + 'family': ["default"], + 'size': None, + 'weight': "normal", + 'stretch': stretch, + 'style': "normal", + 'variants': set(), + 'decorations': set(), + }, + ) + + def test_style(self): + for style in STYLES: + with self.subTest(style=style): + properties = simple_parser(style) + self.assertEqual( + properties, + { + 'family': ["default"], + 'size': None, + 'weight': "normal", + 'stretch': "normal", + 'style': style, + 'variants': set(), + 'decorations': set(), + }, + ) + + def test_variant(self): + for variant in VARIANTS: + with self.subTest(variant=variant): + properties = simple_parser(variant) + self.assertEqual( + properties, + { + 'family': ["default"], + 'size': None, + 'weight': "normal", + 'stretch': "normal", + 'style': "normal", + 'variants': {variant}, + 'decorations': set(), + }, + ) + + def test_decorations(self): + # get powerset iterator of DECORATIONS + all_decorations = chain.from_iterable( + combinations(DECORATIONS, n) + for n in range(len(DECORATIONS) + 1) + ) + for decorations in all_decorations: + with self.subTest(decorations=decorations): + properties = simple_parser(" ".join(decorations)) + self.assertEqual( + properties, + { + 'family': ["default"], + 'size': None, + 'weight': "normal", + 'stretch': "normal", + 'style': "normal", + 'variants': set(), + 'decorations': set(decorations), + }, + ) From bd7f3dd94f3f7224ced590d096ac3b75c238e932 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Mon, 20 Dec 2021 17:23:09 +0000 Subject: [PATCH 03/12] Add tests for PyfaceFont trait; fix default size in parser. --- pyface/tests/test_ui_traits.py | 133 ++++++++++++++++++++++++++ pyface/ui_traits.py | 6 +- pyface/util/font_parser.py | 4 +- pyface/util/tests/test_font_parser.py | 20 ++-- 4 files changed, 149 insertions(+), 14 deletions(-) diff --git a/pyface/tests/test_ui_traits.py b/pyface/tests/test_ui_traits.py index 69d10e03c..4a0991ab1 100644 --- a/pyface/tests/test_ui_traits.py +++ b/pyface/tests/test_ui_traits.py @@ -12,6 +12,8 @@ import os import unittest +from pyface.util.font_parser import simple_parser + # importlib.resources is new in Python 3.7, and importlib.resources.files is # new in Python 3.9, so for Python < 3.9 we must rely on the 3rd party # importlib_resources package. @@ -30,6 +32,7 @@ from traits.testing.api import UnittestTools from ..color import Color +from ..font import Font from ..image_resource import ImageResource from ..ui_traits import ( Border, @@ -38,6 +41,7 @@ Image, Margin, PyfaceColor, + PyfaceFont, image_resource_cache, image_bitmap_cache, ) @@ -56,6 +60,11 @@ class ColorClass(HasTraits): color = PyfaceColor() +class FontClass(HasTraits): + + font = PyfaceFont() + + class HasMarginClass(HasTraits): margin = HasMargin @@ -354,6 +363,130 @@ def test_set_structured_dtype(self): self.assertEqual(color_class.color, color) +class TestPyfaceFont(unittest.TestCase): + + def test_init(self): + trait = PyfaceFont() + self.assertEqual(trait.default_value, (Font, (), {})) + self.assertEqual( + trait.default_value_type, + DefaultValue.callable_and_args, + ) + + def test_init_empty_string(self): + trait = PyfaceFont("") + self.assertEqual( + trait.default_value, + ( + Font, + (), + { + 'family': ["default"], + 'size': 12.0, + 'weight': "normal", + 'stretch': 100, + 'style': "normal", + 'variants': set(), + 'decorations': set(), + }, + ) + ) + + def test_init_empty_string(self): + trait = PyfaceFont( + "10 pt bold condensed italic underline Helvetica sans-serif") + self.assertEqual( + trait.default_value, + ( + Font, + (), + { + 'family': ["helvetica", "sans-serif"], + 'size': 10.0, + 'weight': "bold", + 'stretch': 75.0, + 'style': "italic", + 'variants': set(), + 'decorations': {"underline"}, + }, + ) + ) + + def test_init_font(self): + font = Font( + family=["helvetica", "sans-serif"], + size=10.0, + weight="bold", + stretch=75.0, + style="italic", + variants=set(), + decorations={"underline"}, + ) + trait = PyfaceFont(font) + self.assertEqual( + trait.default_value, + ( + Font, + (), + { + 'family': ["helvetica", "sans-serif"], + 'size': 10.0, + 'weight': "bold", + 'stretch': 75.0, + 'style': "italic", + 'variants': set(), + 'decorations': {"underline"}, + }, + ) + ) + + def test_set_empty_string(self): + font_class = FontClass() + font_class.font = "" + self.assertFontEqual(font_class.font, Font()) + + def test_set_typical_string(self): + font_class = FontClass() + font_class.font = "10 pt bold condensed italic underline Helvetica sans-serif" # noqa: E501 + self.assertFontEqual( + font_class.font, + Font( + family=["helvetica", "sans-serif"], + size=10.0, + weight="bold", + stretch=75.0, + style="italic", + variants=set(), + decorations={"underline"}, + ), + ) + + def test_set_font(self): + font_class = FontClass() + font = Font( + family=["helvetica", "sans-serif"], + size=10.0, + weight="bold", + stretch=75.0, + style="italic", + variants=set(), + decorations={"underline"}, + ) + font_class.font = font + self.assertIs(font_class.font, font) + + def test_set_failure(self): + font_class = FontClass() + + with self.assertRaises(TraitError): + font_class.font = None + + def assertFontEqual(self, font1, font2): + state1 = font1.trait_get(transient=lambda x: not x) + state2 = font2.trait_get(transient=lambda x: not x) + self.assertEqual(state1, state2) + + class TestHasMargin(unittest.TestCase, UnittestTools): def test_defaults(self): has_margin = HasMarginClass() diff --git a/pyface/ui_traits.py b/pyface/ui_traits.py index 01e6f534f..63aebd94a 100644 --- a/pyface/ui_traits.py +++ b/pyface/ui_traits.py @@ -203,13 +203,13 @@ class PyfaceFont(TraitType): #: The parser to use when converting text to keyword args. This should #: accept a string and return a dictionary of Font class trait values (ie. #: "family", "size", "weight", etc.). - parser = simple_parser + parser = None - def __init__(self, value=None, *, parser=None, **metadata): + def __init__(self, value=None, *, parser=simple_parser, **metadata): if parser is not None: self.parser = parser if value is not None: - font = self.validate(value) + font = self.validate(None, None, value) default_value = ( Font, (), diff --git a/pyface/util/font_parser.py b/pyface/util/font_parser.py index 261f0b481..c917b8a25 100644 --- a/pyface/util/font_parser.py +++ b/pyface/util/font_parser.py @@ -79,7 +79,7 @@ def simple_parser(description): else: if size is None: try: - size = int(word) + size = float(word) continue except ValueError: pass @@ -92,6 +92,8 @@ def simple_parser(description): family.append(generic_family) if not family: family = ["default"] + if size is None: + size = 12 return { 'family': family, diff --git a/pyface/util/tests/test_font_parser.py b/pyface/util/tests/test_font_parser.py index 38af7d2a3..1d2dd5d3a 100644 --- a/pyface/util/tests/test_font_parser.py +++ b/pyface/util/tests/test_font_parser.py @@ -32,7 +32,7 @@ def test_empty(self): properties, { 'family': ["default"], - 'size': None, + 'size': 12.0, 'weight': "normal", 'stretch': "normal", 'style': "normal", @@ -48,7 +48,7 @@ def test_typical(self): properties, { 'family': ["helvetica", "sans-serif"], - 'size': 10, + 'size': 10.0, 'weight': "bold", 'stretch': "condensed", 'style': "italic", @@ -65,7 +65,7 @@ def test_noise(self): properties, { 'family': ["default"], - 'size': None, + 'size': 12.0, 'weight': "normal", 'stretch': "normal", 'style': "normal", @@ -82,7 +82,7 @@ def test_generic_families(self): properties, { 'family': [family], - 'size': None, + 'size': 12.0, 'weight': "normal", 'stretch': "normal", 'style': "normal", @@ -92,7 +92,7 @@ def test_generic_families(self): ) def test_size(self): - for size in [12, 24]: + for size in [12, 24, 12.5]: with self.subTest(size=size): properties = simple_parser(str(size)) self.assertEqual( @@ -116,7 +116,7 @@ def test_weight(self): properties, { 'family': ["default"], - 'size': None, + 'size': 12.0, 'weight': weight, 'stretch': "normal", 'style': "normal", @@ -133,7 +133,7 @@ def test_stretch(self): properties, { 'family': ["default"], - 'size': None, + 'size': 12.0, 'weight': "normal", 'stretch': stretch, 'style': "normal", @@ -150,7 +150,7 @@ def test_style(self): properties, { 'family': ["default"], - 'size': None, + 'size': 12.0, 'weight': "normal", 'stretch': "normal", 'style': style, @@ -167,7 +167,7 @@ def test_variant(self): properties, { 'family': ["default"], - 'size': None, + 'size': 12.0, 'weight': "normal", 'stretch': "normal", 'style': "normal", @@ -189,7 +189,7 @@ def test_decorations(self): properties, { 'family': ["default"], - 'size': None, + 'size': 12.0, 'weight': "normal", 'stretch': "normal", 'style': "normal", From e375248dfbfe996a829f5c82b6d1e29b4eca3bcc Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Wed, 22 Dec 2021 11:07:13 +0000 Subject: [PATCH 04/12] Add support for font selection dialogs for Font class. --- docs/source/standard_dialogs.rst | 9 +++ examples/dialogs/font_dialog.py | 16 ++++++ pyface/font_dialog.py | 41 ++++++++++++++ pyface/i_font_dialog.py | 24 ++++++++ pyface/tests/test_font_dialog.py | 97 ++++++++++++++++++++++++++++++++ pyface/ui/qt4/font_dialog.py | 61 ++++++++++++++++++++ pyface/ui/wx/font_dialog.py | 63 +++++++++++++++++++++ 7 files changed, 311 insertions(+) create mode 100644 examples/dialogs/font_dialog.py create mode 100644 pyface/font_dialog.py create mode 100644 pyface/i_font_dialog.py create mode 100644 pyface/tests/test_font_dialog.py create mode 100644 pyface/ui/qt4/font_dialog.py create mode 100644 pyface/ui/wx/font_dialog.py diff --git a/docs/source/standard_dialogs.rst b/docs/source/standard_dialogs.rst index dd290f012..00ca62166 100644 --- a/docs/source/standard_dialogs.rst +++ b/docs/source/standard_dialogs.rst @@ -83,6 +83,13 @@ Typical usage for a save dialog looks like this:: print(f"Save to {dialog.path}") +Font Dialog +=========== + +A dialog that takes a |Font| object and displays a standard OS font selection. +A convenience function |get_font| opens the dialog and returns a font value. + + Message Dialog ============== @@ -195,6 +202,7 @@ user's selection or ``None`` if nothing is selected. .. |CANCEL| replace:: :py:obj:`~pyface.constants.CANCEL` .. |Color| replace:: :py:class:`~pyface.color.Color` .. |Executor| replace:: :py:class:`~concurrent.futures.Executor` +.. |Font| replace:: :py:class:`~pyface.font.Font` .. |IDialog| replace:: :py:class:`~pyface.i_dialog.IDialog` .. |NO| replace:: :py:obj:`~pyface.constants.NO` .. |OK| replace:: :py:obj:`~pyface.constants.OK` @@ -203,6 +211,7 @@ user's selection or ``None`` if nothing is selected. .. |confirm| replace:: :py:func:`~pyface.confirmation_dialog.confirm` .. |error| replace:: :py:func:`~pyface.message_dialog.error` .. |get_color| replace:: :py:func:`~pyface.color_dialog.get_color` +.. |get_font| replace:: :py:func:`~pyface.font_dialog.get_font` .. |information| replace:: :py:func:`~pyface.message_dialog.information` .. |open| replace:: :py:meth:`~pyface.i_dialog.IDialog.open` .. |warning| replace:: :py:func:`~pyface.message_dialog.warning` diff --git a/examples/dialogs/font_dialog.py b/examples/dialogs/font_dialog.py new file mode 100644 index 000000000..798374228 --- /dev/null +++ b/examples/dialogs/font_dialog.py @@ -0,0 +1,16 @@ +# (C) Copyright 2005-2021 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 pyface.font_dialog import get_font + +# display a font dialog and get the result +font = get_font(None, font='12 pt Helvetica sans-serif') +if font is not None: + print(font) diff --git a/pyface/font_dialog.py b/pyface/font_dialog.py new file mode 100644 index 000000000..aa8c9b70e --- /dev/null +++ b/pyface/font_dialog.py @@ -0,0 +1,41 @@ +# (C) Copyright 2005-2021 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 implementation of a dialog that allows the user to select a color. +""" + +from .constant import OK +from .toolkit import toolkit_object + + +FontDialog = toolkit_object("font_dialog:FontDialog") + + +def get_font(parent, font): + """ Convenience function that displays a color dialog. + + Parameters + ---------- + parent : toolkit control + The parent toolkit control for the modal dialog. + font : Font or font description + The initial Font object or string describing the font. + + Returns + ------- + font : Font or None + The selected font, or None if the user made no selection. + """ + dialog = FontDialog(parent=parent, font=font) + result = dialog.open() + if result == OK: + return dialog.font + else: + return None diff --git a/pyface/i_font_dialog.py b/pyface/i_font_dialog.py new file mode 100644 index 000000000..ae038bb58 --- /dev/null +++ b/pyface/i_font_dialog.py @@ -0,0 +1,24 @@ +# (C) Copyright 2005-2021 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 dialog that allows the user to select a color. """ + +from pyface.ui_traits import PyfaceFont +from pyface.i_dialog import IDialog + + +class IFontDialog(IDialog): + """ The interface for a dialog that allows the user to choose a font. + """ + + # 'IFontDialog' interface ---------------------------------------------# + + #: The font in the dialog. + font = PyfaceFont() diff --git a/pyface/tests/test_font_dialog.py b/pyface/tests/test_font_dialog.py new file mode 100644 index 000000000..f03d72635 --- /dev/null +++ b/pyface/tests/test_font_dialog.py @@ -0,0 +1,97 @@ +# (C) Copyright 2005-2021 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 ..font import Font +from ..font_dialog import FontDialog, get_font +from ..toolkit import toolkit_object +from ..util.font_parser import simple_parser + +GuiTestAssistant = toolkit_object("util.gui_test_assistant:GuiTestAssistant") +no_gui_test_assistant = GuiTestAssistant.__name__ == "Unimplemented" + +ModalDialogTester = toolkit_object( + "util.modal_dialog_tester:ModalDialogTester" +) +no_modal_dialog_tester = ModalDialogTester.__name__ == "Unimplemented" + + +@unittest.skipIf(no_gui_test_assistant, "No GuiTestAssistant") +class TestFontDialog(unittest.TestCase, GuiTestAssistant): + + def setUp(self): + GuiTestAssistant.setUp(self) + self.dialog = FontDialog(font="12 Helvetica sans-serif") + + def tearDown(self): + if self.dialog.control is not None: + with self.delete_widget(self.dialog.control): + self.dialog.destroy() + del self.dialog + GuiTestAssistant.tearDown(self) + + def test_font(self): + # test that fonts are translated as expected + self.dialog.font = "10 bold condensed Helvetica sans-serif" + + self.assertFontEqual( + self.dialog.font, + Font(**simple_parser("10 bold condensed Helvetica sans-serif")), + ) + + def test_create(self): + # test that creation and destruction works as expected + with self.event_loop(): + self.dialog._create() + with self.event_loop(): + self.dialog.destroy() + + def test_destroy(self): + # test that destroy works even when no control + with self.event_loop(): + self.dialog.destroy() + + def test_close(self): + # test that close works + with self.event_loop(): + self.dialog._create() + with self.event_loop(): + self.dialog.close() + + def assertFontEqual(self, font1, font2): + state1 = font1.trait_get(transient=lambda x: not x) + state2 = font2.trait_get(transient=lambda x: not x) + self.assertEqual(state1, state2) + + +@unittest.skipIf(no_gui_test_assistant, "No GuiTestAssistant") +class TestGetFont(unittest.TestCase, GuiTestAssistant): + + @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") + def test_close(self): + # test that cancel works as expected + tester = ModalDialogTester( + lambda: get_font(None, "12 Helvetica sans-serif") + ) + tester.open_and_run(when_opened=lambda x: x.close(accept=False)) + + self.assertEqual(tester.result, None) + + @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") + def test_close_show_alpha(self): + # test that cancel works as expected + tester = ModalDialogTester( + lambda: get_font(None, "12 Helvetica sans-serif") + ) + tester.open_and_run(when_opened=lambda x: x.close(accept=False)) + + self.assertEqual(tester.result, None) diff --git a/pyface/ui/qt4/font_dialog.py b/pyface/ui/qt4/font_dialog.py new file mode 100644 index 000000000..078c1c9bd --- /dev/null +++ b/pyface/ui/qt4/font_dialog.py @@ -0,0 +1,61 @@ +# (C) Copyright 2005-2021 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! + +""" A dialog that allows the user to select a font. """ + +from pyface.qt import QtGui + +from traits.api import Bool, provides + +from pyface.font import Font +from pyface.ui_traits import PyfaceFont +from pyface.i_font_dialog import IFontDialog +from .dialog import Dialog + + +@provides(IFontDialog) +class FontDialog(Dialog): + """ A dialog that allows the user to choose a font. + """ + + # 'IFontDialog' interface ---------------------------------------------- + + #: The font in the dialog. + font = PyfaceFont() + + # ------------------------------------------------------------------------ + # 'IDialog' interface. + # ------------------------------------------------------------------------ + + def _create_contents(self, parent): + # In PyQt this is a canned dialog so there are no contents. + pass + + # ------------------------------------------------------------------------ + # 'IWindow' interface. + # ------------------------------------------------------------------------ + + def close(self): + if self.control.result() == QtGui.QDialog.Accepted: + qfont = self.control.selectedFont() + print(qfont.family()) + print(qfont.families()) + print(qfont.defaultFamily()) + self.font = Font.from_toolkit(qfont) + return super(FontDialog, self).close() + + # ------------------------------------------------------------------------ + # 'IWindow' interface. + # ------------------------------------------------------------------------ + + def _create_control(self, parent): + qfont = self.font.to_toolkit() + dialog = QtGui.QFontDialog(qfont, parent) + return dialog diff --git a/pyface/ui/wx/font_dialog.py b/pyface/ui/wx/font_dialog.py new file mode 100644 index 000000000..35f477759 --- /dev/null +++ b/pyface/ui/wx/font_dialog.py @@ -0,0 +1,63 @@ +# (C) Copyright 2005-2021 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 dialog that allows the user to select a font. """ + +import wx + +from traits.api import Bool, provides + +from pyface.font import Font +from pyface.ui_traits import PyfaceFont +from pyface.i_font_dialog import IFontDialog +from .dialog import Dialog + +# The WxPython version in a convenient to compare form. +wx_version = tuple(int(x) for x in wx.__version__.split('.')[:3]) + + +@provides(IFontDialog) +class FontDialog(Dialog): + """ A dialog for selecting fonts. + """ + + # 'IFontDialog' interface ---------------------------------------------- + + #: The font in the dialog. + font = PyfaceFont() + + # ------------------------------------------------------------------------ + # 'IDialog' interface. + # ------------------------------------------------------------------------ + + def _create_contents(self, parent): + # In wx this is a canned dialog. + pass + + # ------------------------------------------------------------------------ + # 'IWindow' interface. + # ------------------------------------------------------------------------ + + def close(self): + font_data = self.control.GetFontData() + wx_font = font_data.GetChosenFont() + self.font = Font.from_toolkit(wx_font) + super(FontDialog, self).close() + + # ------------------------------------------------------------------------ + # 'IWidget' interface. + # ------------------------------------------------------------------------ + + def _create_control(self, parent): + wx_font = self.font.to_toolkit() + data = wx.FontData() + data.SetInitialFont(wx_font) + dialog = wx.FontDialog(parent, data) + return dialog From 05224c5be7f22b803b65a24406e22a30f454946d Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Fri, 7 Jan 2022 11:42:49 +0000 Subject: [PATCH 05/12] Missed merge conflict. --- pyface/tests/test_ui_traits.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pyface/tests/test_ui_traits.py b/pyface/tests/test_ui_traits.py index 15e9ce5dd..278ef6000 100644 --- a/pyface/tests/test_ui_traits.py +++ b/pyface/tests/test_ui_traits.py @@ -392,11 +392,7 @@ def test_init_empty_string(self): ) ) -<<<<<<< HEAD - def test_init_empty_string(self): -======= def test_init_typical_string(self): ->>>>>>> main trait = PyfaceFont( "10 pt bold condensed italic underline Helvetica sans-serif") self.assertEqual( From dc47cc3ec2b8d482492aeaf6a75b23ec0f2f72b6 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Fri, 7 Jan 2022 12:23:40 +0000 Subject: [PATCH 06/12] Update copyright dates for new files. --- examples/dialogs/font_dialog.py | 2 +- pyface/font_dialog.py | 2 +- pyface/i_font_dialog.py | 2 +- pyface/tests/test_font_dialog.py | 2 +- pyface/ui/qt4/font_dialog.py | 2 +- pyface/ui/wx/font_dialog.py | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/dialogs/font_dialog.py b/examples/dialogs/font_dialog.py index 798374228..2abfeeea7 100644 --- a/examples/dialogs/font_dialog.py +++ b/examples/dialogs/font_dialog.py @@ -1,4 +1,4 @@ -# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX +# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD diff --git a/pyface/font_dialog.py b/pyface/font_dialog.py index aa8c9b70e..1035496a6 100644 --- a/pyface/font_dialog.py +++ b/pyface/font_dialog.py @@ -1,4 +1,4 @@ -# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX +# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD diff --git a/pyface/i_font_dialog.py b/pyface/i_font_dialog.py index ae038bb58..f7d30b46b 100644 --- a/pyface/i_font_dialog.py +++ b/pyface/i_font_dialog.py @@ -1,4 +1,4 @@ -# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX +# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD diff --git a/pyface/tests/test_font_dialog.py b/pyface/tests/test_font_dialog.py index f03d72635..237990512 100644 --- a/pyface/tests/test_font_dialog.py +++ b/pyface/tests/test_font_dialog.py @@ -1,4 +1,4 @@ -# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX +# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD diff --git a/pyface/ui/qt4/font_dialog.py b/pyface/ui/qt4/font_dialog.py index 078c1c9bd..75d7db654 100644 --- a/pyface/ui/qt4/font_dialog.py +++ b/pyface/ui/qt4/font_dialog.py @@ -1,4 +1,4 @@ -# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX +# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD diff --git a/pyface/ui/wx/font_dialog.py b/pyface/ui/wx/font_dialog.py index 35f477759..04a82405d 100644 --- a/pyface/ui/wx/font_dialog.py +++ b/pyface/ui/wx/font_dialog.py @@ -1,4 +1,4 @@ -# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX +# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD From c98d8b599964c92451dce38a97c7fbf2629fbad0 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Mon, 10 Jan 2022 17:04:01 +0000 Subject: [PATCH 07/12] Fix unused imports. --- pyface/ui/qt4/font_dialog.py | 2 +- pyface/ui/wx/font_dialog.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyface/ui/qt4/font_dialog.py b/pyface/ui/qt4/font_dialog.py index 75d7db654..fc120c74b 100644 --- a/pyface/ui/qt4/font_dialog.py +++ b/pyface/ui/qt4/font_dialog.py @@ -12,7 +12,7 @@ from pyface.qt import QtGui -from traits.api import Bool, provides +from traits.api import provides from pyface.font import Font from pyface.ui_traits import PyfaceFont diff --git a/pyface/ui/wx/font_dialog.py b/pyface/ui/wx/font_dialog.py index 04a82405d..f288ade09 100644 --- a/pyface/ui/wx/font_dialog.py +++ b/pyface/ui/wx/font_dialog.py @@ -12,7 +12,7 @@ import wx -from traits.api import Bool, provides +from traits.api import provides from pyface.font import Font from pyface.ui_traits import PyfaceFont From a2103604e34331e750e3cdc99fbc0e3384edc456 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Mon, 10 Jan 2022 17:06:26 +0000 Subject: [PATCH 08/12] Correct docstrings. --- pyface/font_dialog.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyface/font_dialog.py b/pyface/font_dialog.py index 1035496a6..484f27256 100644 --- a/pyface/font_dialog.py +++ b/pyface/font_dialog.py @@ -8,7 +8,7 @@ # # Thanks for using Enthought open source! -""" The implementation of a dialog that allows the user to select a color. +""" The implementation of a dialog that allows the user to select a font. """ from .constant import OK @@ -19,7 +19,7 @@ def get_font(parent, font): - """ Convenience function that displays a color dialog. + """ Convenience function that displays a font dialog. Parameters ---------- From df28a60ce8fb28c363e2467cd2b9130b54abdc6a Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Mon, 17 Jan 2022 09:41:03 +0000 Subject: [PATCH 09/12] Clean up debugging prints. --- pyface/ui/qt4/font_dialog.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pyface/ui/qt4/font_dialog.py b/pyface/ui/qt4/font_dialog.py index fc120c74b..d37b4764d 100644 --- a/pyface/ui/qt4/font_dialog.py +++ b/pyface/ui/qt4/font_dialog.py @@ -45,9 +45,6 @@ def _create_contents(self, parent): def close(self): if self.control.result() == QtGui.QDialog.Accepted: qfont = self.control.selectedFont() - print(qfont.family()) - print(qfont.families()) - print(qfont.defaultFamily()) self.font = Font.from_toolkit(qfont) return super(FontDialog, self).close() From 565d35d2beceb145711eac1c3daf74f79a890281 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Tue, 25 Jan 2022 16:44:44 +0000 Subject: [PATCH 10/12] Remove spurious test case. --- pyface/tests/test_font_dialog.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/pyface/tests/test_font_dialog.py b/pyface/tests/test_font_dialog.py index 237990512..b274f1215 100644 --- a/pyface/tests/test_font_dialog.py +++ b/pyface/tests/test_font_dialog.py @@ -85,13 +85,3 @@ def test_close(self): tester.open_and_run(when_opened=lambda x: x.close(accept=False)) self.assertEqual(tester.result, None) - - @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") - def test_close_show_alpha(self): - # test that cancel works as expected - tester = ModalDialogTester( - lambda: get_font(None, "12 Helvetica sans-serif") - ) - tester.open_and_run(when_opened=lambda x: x.close(accept=False)) - - self.assertEqual(tester.result, None) From 9864f65a489012bbe5fb3f9c91b50f3a6ecfcf82 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Wed, 26 Jan 2022 12:39:41 +0000 Subject: [PATCH 11/12] Quick fix for Pyside6 on MacOS - use older PySide6 --- etstool.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/etstool.py b/etstool.py index 18afd55a0..de624c1ac 100644 --- a/etstool.py +++ b/etstool.py @@ -84,6 +84,7 @@ supported_combinations = { "3.6": {"pyqt5", "pyside2", "pyside6", "wx"}, + "3.8": {"pyside6"}, } # Traits version requirement (empty string to mean no specific requirement). @@ -211,11 +212,16 @@ def install(edm, runtime, toolkit, environment, editable, source): ] ) elif toolkit == "pyside6": - commands.extend( - [ - "{edm} run -e {environment} -- pip install pyside6", - "{edm} run -e {environment} -- pip install pillow", - ] + if sys.platform == 'darwin': + commands.append( + "{edm} run -e {environment} -- pip install pyside6<6.2.2'" + ) + else: + commands.append( + "{edm} run -e {environment} -- pip install pyside6'" + ) + commands.append( + "{edm} run -e {environment} -- pip install pillow" ) elif toolkit == "wx": if sys.platform == "darwin": From 6acd57b43a91d61b7105d6d1801cf2a4d5a6ca33 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Wed, 26 Jan 2022 12:51:40 +0000 Subject: [PATCH 12/12] Fix version string. --- etstool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etstool.py b/etstool.py index de624c1ac..23866eb81 100644 --- a/etstool.py +++ b/etstool.py @@ -218,7 +218,7 @@ def install(edm, runtime, toolkit, environment, editable, source): ) else: commands.append( - "{edm} run -e {environment} -- pip install pyside6'" + "{edm} run -e {environment} -- pip install pyside6" ) commands.append( "{edm} run -e {environment} -- pip install pillow"