From 74536e064be70d2387fcf143e2364a07f9685f3a Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Thu, 28 Apr 2022 08:55:16 +0100 Subject: [PATCH] New, more modern KivaFont trait (#929) * Add a new font-editor that uses Enable to draw fonts This font editor is laregly toolkit independent - it embeds a Window with a Label component to display the font and the simple editor uses the Pyface font dialog to get new font values. Some functionality is broken out into experimental base classes for building editors with components. * New more modern KivaFont trait This creates a KivaFont TraitType, which is: - in enable, rather than Kiva, meaning Kiva can potentially not need traits - has a better font parser that supports more weight types - the font parser can be swapped out for something better easily - uses the new KivaFontEditor, which is available for Qt - converts Pyface Fonts to Kiva Fonts * Fix long line. * Save all files before finishing merge! * Fix tests. * Apply suggestions from code review Co-authored-by: Mark Dickinson * Handle color editor tests for null backend. * Fixes from PR review. * Some more small fixes coming from PR discussion. Co-authored-by: Mark Dickinson --- docs/source/enable/traits.rst | 5 +- docs/source/kiva/drawing_details.rst | 6 +- enable/drawing/drawing_canvas.py | 2 +- enable/enable_traits.py | 2 +- enable/examples/demo/enable/container_demo.py | 2 +- .../demo/enable/editors/font_editor.py | 19 +- enable/gadgets/vu_meter.py | 2 +- enable/label.py | 2 +- enable/tests/drawing/__init__.py | 0 enable/tests/trait_defs/__init__.py | 0 .../tests/trait_defs/test_kiva_font_editor.py | 9 +- .../tests/trait_defs/test_kiva_font_trait.py | 97 ++++++++ .../tests/trait_defs/test_rgba_color_trait.py | 44 ++-- enable/text_field_style.py | 2 +- enable/text_grid.py | 2 +- enable/tools/toolbars/toolbar_buttons.py | 2 +- enable/trait_defs/api.py | 2 + enable/trait_defs/kiva_font_trait.py | 135 +++++++++++ enable/trait_defs/ui/editor_with_component.py | 10 +- enable/trait_defs/ui/kiva_font_editor.py | 33 ++- enable/trait_defs/ui/rgba_color_editor.py | 11 +- kiva/fonttools/font.py | 214 +++++++++++++----- kiva/fonttools/tests/test_font.py | 158 ++++++++++++- kiva/trait_defs/api.py | 8 + kiva/trait_defs/kiva_font_trait.py | 180 +-------------- kiva/trait_defs/tests/test_kiva_font_trait.py | 24 +- kiva/trait_defs/ui/wx/kiva_font_editor.py | 4 +- kiva/trait_defs/ui/wx/tests/__init__.py | 0 28 files changed, 670 insertions(+), 305 deletions(-) create mode 100644 enable/tests/drawing/__init__.py create mode 100644 enable/tests/trait_defs/__init__.py create mode 100644 enable/tests/trait_defs/test_kiva_font_trait.py create mode 100644 enable/trait_defs/kiva_font_trait.py create mode 100644 kiva/trait_defs/ui/wx/tests/__init__.py diff --git a/docs/source/enable/traits.rst b/docs/source/enable/traits.rst index 9ba422d00..abba94325 100644 --- a/docs/source/enable/traits.rst +++ b/docs/source/enable/traits.rst @@ -18,8 +18,9 @@ the form of an HTML color name ("blue" or "#0000FF"). font_trait ---------- -:class:`~.font_trait` is a synonym for :class:`kiva.trait_defs.api.KivaFont`. -The trait maps a font-description string to a valid :class:`kiva.fonttools.Font` +:class:`~.font_trait` is a synonym for +:class:`enable.trait_defs.kiva_font_trait.KivaFont`. The trait maps a +font-description string to a valid :class:`kiva.fonttools.Font` instance which can be passed to :py:meth:`AbstractGraphicsContext.set_font` LineStyle diff --git a/docs/source/kiva/drawing_details.rst b/docs/source/kiva/drawing_details.rst index 88d6e1bb6..356a94392 100644 --- a/docs/source/kiva/drawing_details.rst +++ b/docs/source/kiva/drawing_details.rst @@ -280,9 +280,9 @@ The ``KivaFont`` trait and ``set_font`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If you're already doing your drawing within an application using traits, you can -use the :class:`~kiva.trait_defs.kiva_font_trait.KivaFont` trait. +use the :class:`~enable.trait_defs.kiva_font_trait.KivaFont` trait. -:class:`~kiva.trait_defs.kiva_font_trait.KivaFont` traits are initialized with +:class:`~enable.trait_defs.kiva_font_trait.KivaFont` traits are initialized with a string which describes the font: "Times Italic 18", "Courier Bold 10", etc. The *value* of the trait is a :class:`~kiva.fonttools.font.Font` instance which can be passed to the :py:meth:`~.AbstractGraphicsContext.set_font` method. @@ -290,7 +290,7 @@ can be passed to the :py:meth:`~.AbstractGraphicsContext.set_font` method. *Supported backends*: all backends .. note:: - The :class:`~kiva.trait_defs.kiva_font_trait.KivaFont` parser is very + The :class:`~enable.trait_defs.kiva_font_trait.KivaFont` parser is very simplistic and special-cases some words. For example "roman" means a generic serif-style font family, so for example a face name of "Times New Roman" will not resolve as expected. In these cases, use a diff --git a/enable/drawing/drawing_canvas.py b/enable/drawing/drawing_canvas.py index 21642613d..62f26298a 100644 --- a/enable/drawing/drawing_canvas.py +++ b/enable/drawing/drawing_canvas.py @@ -9,7 +9,7 @@ # Thanks for using Enthought open source! from enable.api import Container, Component, ColorTrait from kiva.api import FILL, FILL_STROKE -from kiva.trait_defs.api import KivaFont +from enable.trait_defs.kiva_font_trait import KivaFont from traits.api import Any, Bool, Delegate, Enum, Instance, Int, List, Str diff --git a/enable/enable_traits.py b/enable/enable_traits.py index 6b617b4e8..4e3b87ee9 100644 --- a/enable/enable_traits.py +++ b/enable/enable_traits.py @@ -15,7 +15,7 @@ from numpy import array, ndarray # Enthought library imports -from kiva.trait_defs.api import KivaFont +from enable.trait_defs.kiva_font_trait import KivaFont from traits.api import ( BaseFloat, List, Map, PrefixList, PrefixMap, Range, TraitType, Union, ) diff --git a/enable/examples/demo/enable/container_demo.py b/enable/examples/demo/enable/container_demo.py index 346f8fad3..373d8dc0c 100644 --- a/enable/examples/demo/enable/container_demo.py +++ b/enable/examples/demo/enable/container_demo.py @@ -13,7 +13,7 @@ from enable.api import ColorTrait from enable.examples._example_support import DemoFrame, demo_main from enable.tools.api import DragTool -from kiva.trait_defs.api import KivaFont +from enable.trait_defs.api import KivaFont class Region(PlotComponent, DragTool): diff --git a/enable/examples/demo/enable/editors/font_editor.py b/enable/examples/demo/enable/editors/font_editor.py index aaf5553ed..0177601b2 100644 --- a/enable/examples/demo/enable/editors/font_editor.py +++ b/enable/examples/demo/enable/editors/font_editor.py @@ -10,7 +10,7 @@ from traits.api import HasStrictTraits from traitsui.api import View, Item -from enable.api import Container, TextField, font_trait +from enable.trait_defs.api import KivaFont from enable.trait_defs.ui.api import KivaFontEditor from enable.examples._example_support import demo_main @@ -26,14 +26,19 @@ class Demo(HasStrictTraits): """ An example which shows the KivaFontEditor's variations. """ - font = font_trait(Font("Times", 24, SWISS, WEIGHT_BOLD, ITALIC)) + font = KivaFont(Font("Times", 24, SWISS, WEIGHT_BOLD, ITALIC)) view = View( - Item('font', editor=KivaFontEditor(), style='simple', label="Simple"), - Item('font', editor=KivaFontEditor(), style='custom', label="Custom"), - Item('font', editor=KivaFontEditor(), style='text', label="Text"), - Item('font', editor=KivaFontEditor(), style='readonly', label="Readonly"), - Item('font', editor=KivaFontEditor(sample_text=sample_text), style='readonly', label="sample text"), + Item('font', style='simple', label="Simple"), + Item('font', style='custom', label="Custom"), + Item('font', style='text', label="Text"), + Item('font', style='readonly', label="Readonly"), + Item( + 'font', + editor=KivaFontEditor(sample_text=sample_text), + style='readonly', + label="sample text", + ), resizable=True, width=size[0], height=size[1], diff --git a/enable/gadgets/vu_meter.py b/enable/gadgets/vu_meter.py index 96097569d..ebadce875 100644 --- a/enable/gadgets/vu_meter.py +++ b/enable/gadgets/vu_meter.py @@ -11,7 +11,7 @@ from traits.api import Float, Property, List, Str, Range from enable.api import Component -from kiva.trait_defs.api import KivaFont +from enable.trait_defs.kiva_font_trait import KivaFont from kiva import affine diff --git a/enable/label.py b/enable/label.py index 3e388cdad..cadac5d9a 100644 --- a/enable/label.py +++ b/enable/label.py @@ -16,7 +16,7 @@ # Enthought library imports from kiva.api import FILL, STROKE -from kiva.trait_defs.api import KivaFont +from enable.trait_defs.kiva_font_trait import KivaFont from traits.api import Bool, Enum, Float, HasTraits, Int, List, Str, observe # Local, relative imports diff --git a/enable/tests/drawing/__init__.py b/enable/tests/drawing/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/enable/tests/trait_defs/__init__.py b/enable/tests/trait_defs/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/enable/tests/trait_defs/test_kiva_font_editor.py b/enable/tests/trait_defs/test_kiva_font_editor.py index 50f3362fe..037d72937 100644 --- a/enable/tests/trait_defs/test_kiva_font_editor.py +++ b/enable/tests/trait_defs/test_kiva_font_editor.py @@ -69,7 +69,8 @@ def test_readonly_default_view(self): self.assertIs(editor.value, new_font) self.assertIs(component.font, new_font) - self.assertEqual(editor.str_value, "24 point Helvetica Bold Italic") + self.assertEqual( + editor.str_value, "24 point Helvetica Bold Italic") self.assertEqual(component.text, "24 point Helvetica Bold Italic") finally: ui.dispose() @@ -120,7 +121,8 @@ def test_simple_default_object_change(self): self.assertIs(editor.value, new_font) self.assertIs(editor.font, new_font) self.assertIs(component.font, new_font) - self.assertEqual(editor.str_value, "24 point Helvetica Bold Italic") + self.assertEqual( + editor.str_value, "24 point Helvetica Bold Italic") self.assertEqual(component.text, "24 point Helvetica Bold Italic") finally: ui.dispose() @@ -151,7 +153,8 @@ def test_simple_default_editor_change(self): self.assertIs(editor.value, new_font) self.assertIs(editor.font, new_font) self.assertIs(component.font, new_font) - self.assertEqual(editor.str_value, "24 point Helvetica Bold Italic") + self.assertEqual( + editor.str_value, "24 point Helvetica Bold Italic") self.assertEqual(component.text, "24 point Helvetica Bold Italic") finally: ui.dispose() diff --git a/enable/tests/trait_defs/test_kiva_font_trait.py b/enable/tests/trait_defs/test_kiva_font_trait.py new file mode 100644 index 000000000..6653c36d1 --- /dev/null +++ b/enable/tests/trait_defs/test_kiva_font_trait.py @@ -0,0 +1,97 @@ +# (C) Copyright 2008-2022 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 kiva import constants +from kiva.fonttools.font import Font, FAMILIES, WEIGHTS, STYLES +from pyface.font import Font as PyfaceFont +from traits.api import HasTraits, TraitError + +from enable.trait_defs.kiva_font_trait import KivaFont + + +class FontExample(HasTraits): + + font = KivaFont() + + +class TestKivaFont(unittest.TestCase): + + def test_validate_str(self): + expected_outcomes = {} + expected_outcomes[""] = Font(size=10, family=constants.DEFAULT) + + for weight, kiva_weight in WEIGHTS.items(): + expected_outcomes[weight] = Font( + weight=kiva_weight, size=10, family=constants.DEFAULT) + + for style, kiva_style in STYLES.items(): + expected_outcomes[style] = Font( + style=kiva_style, size=10, family=constants.DEFAULT) + + expected_outcomes["underline"] = Font( + underline=True, size=10, family=constants.DEFAULT) + + expected_outcomes["18"] = Font(size=18, family=constants.DEFAULT) + expected_outcomes["18 pt"] = Font(size=18, family=constants.DEFAULT) + expected_outcomes["18 point"] = Font(size=18, family=constants.DEFAULT) + + for family, kiva_family in FAMILIES.items(): + expected_outcomes[family] = Font(family=kiva_family, size=10) + + expected_outcomes["Courier"] = Font( + "Courier", size=10, family=constants.DEFAULT) + expected_outcomes["Comic Sans"] = Font( + "Comic Sans", size=10, family=constants.DEFAULT) + expected_outcomes["18 pt Bold Italic Underline Comic Sans script"] = Font( # noqa: E501 + "Comic Sans", 18, constants.SCRIPT, weight=constants.WEIGHT_BOLD, + style=constants.ITALIC, underline=True, + ) + + for name, expected in expected_outcomes.items(): + with self.subTest(name=name): + example = FontExample(font=name) + result = example.font + + # test we get expected font + self.assertIsInstance(result, Font) + self.assertEqual(result, expected) + + def test_validate_font(self): + font = Font("Comic Sans", 18) + example = FontExample(font=font) + + result = example.font + + # test we get expected font + self.assertIsInstance(result, Font) + self.assertIs(result, font) + + def test_validate_pyface_font(self): + font = Font("Comic Sans", 18, constants.DEFAULT) + pyface_font = PyfaceFont(family=["Comic Sans"], size=18) + example = FontExample(font=pyface_font) + + result = example.font + + # test we get expected font + self.assertIsInstance(result, Font) + self.assertEqual(result, font) + + def test_font_trait_default(self): + example = FontExample() + + self.assertIsInstance(example.font, Font) + self.assertEqual(example.font, Font(size=12, family=constants.SWISS)) + + def test_font_trait_none(self): + with self.assertRaises(TraitError): + FontExample(font=None) diff --git a/enable/tests/trait_defs/test_rgba_color_trait.py b/enable/tests/trait_defs/test_rgba_color_trait.py index c88c515ab..663c5a175 100644 --- a/enable/tests/trait_defs/test_rgba_color_trait.py +++ b/enable/tests/trait_defs/test_rgba_color_trait.py @@ -10,15 +10,15 @@ import unittest -import numpy as np - from pyface.color import Color -from traits.api import DefaultValue, HasTraits, TraitError +from traits.api import HasTraits, TraitError from traits.testing.optional_dependencies import numpy as np, requires_numpy from traitsui.api import EditorFactory +from enable.tests._testing import skip_if_null from enable.trait_defs.rgba_color_trait import RGBAColor + rgba_float_dtype = np.dtype([ ('red', "float64"), ('green', "float64"), @@ -42,6 +42,7 @@ ('blue', "uint8"), ]) + class ColorClass(HasTraits): color = RGBAColor() @@ -53,6 +54,7 @@ def test_init(self): trait = RGBAColor() self.assertEqual(trait.default_value, (1.0, 1.0, 1.0, 1.0)) + @requires_numpy def test_default_value(self): values = [ "rebeccapurple", @@ -65,11 +67,11 @@ def test_default_value(self): (0.4, 0.2, 0.6, 1.0), [0.4, 0.2, 0.6, 1.0], np.array([0.4, 0.2, 0.6, 1.0]), - np.array((0.4, 0.2, 0.6, 1.0), dtype=rgba_float_dtype), + np.array([(0.4, 0.2, 0.6, 1.0)], dtype=rgba_float_dtype)[0], (0x66, 0x33, 0x99, 0xff), [0x66, 0x33, 0x99, 0xff], np.array([0x66, 0x33, 0x99, 0xff], dtype='uint8'), - np.array((0x66, 0x33, 0x99, 0xff), dtype=rgba_int_dtype), + np.array([(0x66, 0x33, 0x99, 0xff)], dtype=rgba_uint8_dtype)[0], "#666633339999", "#663399", "#639", @@ -77,11 +79,11 @@ def test_default_value(self): (0.4, 0.2, 0.6), [0.4, 0.2, 0.6], np.array([0.4, 0.2, 0.6]), - np.array((0.4, 0.2, 0.6), dtype=rgb_float_dtype), + np.array([(0.4, 0.2, 0.6)], dtype=rgb_float_dtype)[0], (0x66, 0x33, 0x99), [0x66, 0x33, 0x99], np.array([0x66, 0x33, 0x99], dtype='uint8'), - np.array((0x66, 0x33, 0x99), dtype=rgb_int_dtype), + np.array([(0x66, 0x33, 0x99)], dtype=rgb_uint8_dtype)[0], ] for value in values: with self.subTest(value=value): @@ -101,7 +103,7 @@ def test_init_invalid(self): ] for value in values: with self.subTest(value=value): - with self.assertRaises(TraitError): + with self.assertRaises(Exception): RGBAColor(value) def test_validate(self): @@ -116,11 +118,11 @@ def test_validate(self): (0.4, 0.2, 0.6, 1.0), [0.4, 0.2, 0.6, 1.0], np.array([0.4, 0.2, 0.6, 1.0]), - np.array((0.4, 0.2, 0.6, 1.0), dtype=rgba_float_dtype), + np.array([(0.4, 0.2, 0.6, 1.0)], dtype=rgba_float_dtype)[0], (0x66, 0x33, 0x99, 0xff), [0x66, 0x33, 0x99, 0xff], np.array([0x66, 0x33, 0x99, 0xff], dtype='uint8'), - np.array((0x66, 0x33, 0x99, 0xff), dtype=rgba_int_dtype), + np.array([(0x66, 0x33, 0x99, 0xff)], dtype=rgba_uint8_dtype)[0], "#666633339999", "#663399", "#639", @@ -128,11 +130,11 @@ def test_validate(self): (0.4, 0.2, 0.6), [0.4, 0.2, 0.6], np.array([0.4, 0.2, 0.6]), - np.array((0.4, 0.2, 0.6), dtype=rgb_float_dtype), + np.array([(0.4, 0.2, 0.6)], dtype=rgb_float_dtype)[0], (0x66, 0x33, 0x99), [0x66, 0x33, 0x99], np.array([0x66, 0x33, 0x99], dtype='uint8'), - np.array((0x66, 0x33, 0x99), dtype=rgb_int_dtype), + np.array([(0x66, 0x33, 0x99)], dtype=rgb_uint8_dtype)[0], ] trait = RGBAColor() for value in values: @@ -177,11 +179,11 @@ def test_trait_set(self): (0.4, 0.2, 0.6, 1.0), [0.4, 0.2, 0.6, 1.0], np.array([0.4, 0.2, 0.6, 1.0]), - np.array((0.4, 0.2, 0.6, 1.0), dtype=rgba_float_dtype), + np.array([(0.4, 0.2, 0.6, 1.0)], dtype=rgba_float_dtype)[0], (0x66, 0x33, 0x99, 0xff), [0x66, 0x33, 0x99, 0xff], np.array([0x66, 0x33, 0x99, 0xff], dtype='uint8'), - np.array((0x66, 0x33, 0x99, 0xff), dtype=rgba_int_dtype), + np.array([(0x66, 0x33, 0x99, 0xff)], dtype=rgba_uint8_dtype)[0], "#666633339999", "#663399", "#639", @@ -189,21 +191,17 @@ def test_trait_set(self): (0.4, 0.2, 0.6), [0.4, 0.2, 0.6], np.array([0.4, 0.2, 0.6]), - np.array((0.4, 0.2, 0.6), dtype=rgb_float_dtype), + np.array([(0.4, 0.2, 0.6)], dtype=rgb_float_dtype)[0], (0x66, 0x33, 0x99), [0x66, 0x33, 0x99], np.array([0x66, 0x33, 0x99], dtype='uint8'), - np.array((0x66, 0x33, 0x99), dtype=rgb_int_dtype), + np.array([(0x66, 0x33, 0x99)], dtype=rgb_uint8_dtype)[0], ] - trait = RGBAColor() for value in values: with self.subTest(value=value): color_class = ColorClass(color=value) self.assertEqual(color_class.color, (0.4, 0.2, 0.6, 1.0)) - color_class = ColorClass(color=arr[0]) - self.assertEqual(color_class.color, color) - def test_trait_set_invalid(self): values = [ (0.4, 0.2), @@ -215,12 +213,12 @@ def test_trait_set_invalid(self): (0, -1, 250, 255), None, ] - trait = RGBAColor() for value in values: with self.subTest(value=value): with self.assertRaises(TraitError): ColorClass(color=value) + @skip_if_null def test_get_editor(self): trait = RGBAColor() editor = trait.get_editor() @@ -230,6 +228,6 @@ def test_get_editor(self): def test_sys_window_color(self): trait = RGBAColor() # smoke-test: value depends on system and user preferences - trait.validate("syswindow") + trait.validate(None, None, "syswindow") # older code used with an underscore is also OK - trait.validate("sys_window") + trait.validate(None, None, "sys_window") diff --git a/enable/text_field_style.py b/enable/text_field_style.py index f112a236f..91a2e0ffb 100644 --- a/enable/text_field_style.py +++ b/enable/text_field_style.py @@ -10,7 +10,7 @@ # Enthought library imports from traits.api import HasTraits, Int, Bool -from kiva.trait_defs.api import KivaFont +from enable.trait_defs.kiva_font_trait import KivaFont from enable.colors import ColorTrait diff --git a/enable/text_grid.py b/enable/text_grid.py index e33db1316..0f9f7d0c0 100644 --- a/enable/text_grid.py +++ b/enable/text_grid.py @@ -18,7 +18,7 @@ from traits.api import ( Any, Array, Bool, Int, List, Property, Tuple, observe, ) -from kiva.trait_defs.api import KivaFont +from enable.trait_defs.kiva_font_trait import KivaFont # Relative imports from .component import Component diff --git a/enable/tools/toolbars/toolbar_buttons.py b/enable/tools/toolbars/toolbar_buttons.py index 084240347..c2e73564e 100644 --- a/enable/tools/toolbars/toolbar_buttons.py +++ b/enable/tools/toolbars/toolbar_buttons.py @@ -11,7 +11,7 @@ # Enthought library imports from enable.api import ColorTrait, Component from enable.font_metrics_provider import font_metrics_provider -from kiva.trait_defs.api import KivaFont +from enable.trait_defs.kiva_font_trait import KivaFont from traits.api import Bool, Enum, Int, Str, Tuple diff --git a/enable/trait_defs/api.py b/enable/trait_defs/api.py index b8d004b90..24ee21279 100644 --- a/enable/trait_defs/api.py +++ b/enable/trait_defs/api.py @@ -9,7 +9,9 @@ # Thanks for using Enthought open source! """ API for enable.trait_defs subpackage. +- :attr:`~.KivaFont` - :attr:`~.RGBAColor` """ +from .kiva_font_trait import KivaFont from .rgba_color_trait import RGBAColor diff --git a/enable/trait_defs/kiva_font_trait.py b/enable/trait_defs/kiva_font_trait.py new file mode 100644 index 000000000..cdcde72e1 --- /dev/null +++ b/enable/trait_defs/kiva_font_trait.py @@ -0,0 +1,135 @@ +# (C) Copyright 2005-2022 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! +""" Trait definition for a wxPython-based Kiva font. +""" + +from pyface.font import Font as PyfaceFont +from traits.api import DefaultValue, TraitError, TraitType, NoDefaultSpecified + +import kiva.constants as kc +from kiva.fonttools.font import Font, FontParseError, simple_parser + + +#: Expected attributes on the Font class. +font_attrs = [ + 'face_name', 'size', 'family', 'weight', 'style', 'underline', 'encoding', +] + +#: Mapping from Pyface Font generic family names to corresponding constants. +pyface_family_to_kiva_family = { + 'default': kc.DEFAULT, + 'fantasy': kc.DECORATIVE, + 'decorative': kc.DECORATIVE, + 'serif': kc.ROMAN, + 'roman': kc.ROMAN, + 'cursive': kc.SCRIPT, + 'script': kc.SCRIPT, + 'sans-serif': kc.SWISS, + 'swiss': kc.SWISS, + 'monospace': kc.MODERN, + 'modern': kc.MODERN, + 'typewriter': kc.TELETYPE, + 'teletype': kc.TELETYPE, +} + + +def pyface_font_to_font(font): + """Convert a Pyface font to an equivalent Kiva Font. + + This ignores stretch and some options like small caps and strikethrough + as the Kiva font object can't represent these at the moment. + + Parameters + ---------- + font : Pyface Font instance + The font to convert. + + Returns + ------- + font : Kiva Font instance + The resulting Kiva Font object. + """ + face_name = font.family[0] + for face in font.family: + if face in pyface_family_to_kiva_family: + family = pyface_family_to_kiva_family[face] + break + else: + family = kc.DEFAULT + size = int(font.size) + weight = font.weight_ + style = kc.NORMAL if font.style == 'normal' else kc.ITALIC + underline = 'underline' in font.decorations + return Font(face_name, size, family, weight, style, underline) + + +class KivaFont(TraitType): + """ A Trait which casts strings to a Kiva 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.). If it can't parse the string, it + #: should raise FontParseError. + parser = None + + def __init__(self, default_value=None, *, parser=simple_parser, **metadata): # noqa: E501 + self.parser = parser + default_value = self._get_default_value(default_value) + super().__init__(default_value, **metadata) + + def validate(self, object, name, value): + if isinstance(value, Font): + return value + if isinstance(value, PyfaceFont): + return pyface_font_to_font(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 Kiva Font, a Pyface Font, or a string describing a font" + ) + + def get_editor(self, trait): + from enable.trait_defs.ui.kiva_font_editor import KivaFontEditor + return KivaFontEditor() + + def clone(self, default_value=NoDefaultSpecified, **metadata): + # Need to override clone due to Traits issue #1629 + new = super().clone(NoDefaultSpecified, **metadata) + if default_value is not NoDefaultSpecified: + new.default_value = self._get_default_value(default_value) + new.default_value_type = DefaultValue.callable_and_args + return new + + def _get_default_value(self, default_value): + """Construct a default value suitable for callable_and_args.""" + if default_value is not None: + try: + font = self.validate(None, None, default_value) + except TraitError: + raise ValueError( + f"expected {self.info()}, but got {default_value!r}" + ) + klass = font.__class__ + kwargs = {attr: getattr(font, attr) for attr in font_attrs} + else: + klass = Font + kwargs = {} + return (klass, (), kwargs) diff --git a/enable/trait_defs/ui/editor_with_component.py b/enable/trait_defs/ui/editor_with_component.py index 25f8af473..3d197377a 100644 --- a/enable/trait_defs/ui/editor_with_component.py +++ b/enable/trait_defs/ui/editor_with_component.py @@ -8,19 +8,17 @@ # # Thanks for using Enthought open source! -from traits.api import Bool, Instance, Str, observe -from traitsui.api import BasicEditorFactory, Editor as BaseEditor, toolkit_object +from traits.api import Instance, observe +from traitsui.api import toolkit_object from enable.component import Component +from enable.enable_traits import font_trait from enable.label import Label from enable.window import Window -from enable.enable_traits import font_trait +#: The toolkit's Editor base class Editor = toolkit_object("editor:Editor") -if not issubclass(Editor, BaseEditor): - # if the toolkit is "null" make these at least instantiatable - Editor = object class EditorWithComponent(Editor): diff --git a/enable/trait_defs/ui/kiva_font_editor.py b/enable/trait_defs/ui/kiva_font_editor.py index 3878798aa..b11f5e9de 100644 --- a/enable/trait_defs/ui/kiva_font_editor.py +++ b/enable/trait_defs/ui/kiva_font_editor.py @@ -12,17 +12,29 @@ from pyface.font_dialog import get_font from traits.api import Bool, Callable, Instance, Str, observe from traits.trait_base import SequenceTypes -from traitsui.api import EditorFactory, Editor as BaseEditor, toolkit_object +from traitsui.api import EditorFactory from kiva.fonttools.font import Font import kiva.constants as kc -from enable.component import Component -from enable.label import Label from enable.tools.button_tool import ButtonTool -from enable.window import Window from .editor_with_component import EditorWithLabelComponent +#: A mapping of Kiva weight constants to strings. +WEIGHTS = { + kc.WEIGHT_THIN: ' Thin', + kc.WEIGHT_EXTRALIGHT: ' Extra-light', + kc.WEIGHT_LIGHT: ' Light', + kc.WEIGHT_NORMAL: '', + kc.WEIGHT_MEDIUM: ' Medium', + kc.WEIGHT_SEMIBOLD: ' Semi-bold', + kc.WEIGHT_BOLD: ' Bold', + kc.WEIGHT_EXTRABOLD: ' Extra-bold', + kc.WEIGHT_HEAVY: ' Heavy', + kc.WEIGHT_EXTRAHEAVY: ' Extra-heavy', +} + + def face_name(font): """ Returns a Font's typeface name. """ @@ -37,21 +49,17 @@ def str_font(font): """ Returns the text representation of the specified font trait value """ - weight = " Bold" if font.is_bold() else "" + weight = WEIGHTS[font.weight] style = " Italic" if font.style in kc.italic_styles else "" underline = " Underline" if font.underline else "" - return f"{font.size} point {face_name(font)}{weight}{style}{underline}".strip() + return f"{font.size} point {face_name(font)}{weight}{style}{underline}".strip() # noqa: E501 class ReadOnlyEditor(EditorWithLabelComponent): """An Editor which displays a label using the font.""" def init(self, parent): - """Initialize the editor. - - The Label font should match the value for a font editor. - """ self.font = self.value super().init(parent) @@ -76,6 +84,7 @@ class SimpleEditor(ReadOnlyEditor): """An Editor which displays a label using the font, click for font dialog. """ + #: Button tool connected to the Label component. button = Instance(ButtonTool) def create_component(self): @@ -106,7 +115,7 @@ def button_clicked(self, event): pyface_font = PyfaceFont( family=[self.value.face_name], weight=str(self.value.weight), - style='italic' if self.value.style in kc.italic_styles else 'normal', + style='italic' if self.value.style in kc.italic_styles else 'normal', # noqa: E501 size=self.value.size, ) pyface_font = get_font(self.window.control, pyface_font) @@ -114,7 +123,7 @@ def button_clicked(self, event): font = Font( face_name=pyface_font.family[0], weight=pyface_font.weight_, - style=kc.ITALIC if pyface_font.style == 'italic' else kc.NORMAL, + style=kc.ITALIC if pyface_font.style == 'italic' else kc.NORMAL, # noqa: E501 size=int(pyface_font.size), ) self.update_object(font) diff --git a/enable/trait_defs/ui/rgba_color_editor.py b/enable/trait_defs/ui/rgba_color_editor.py index fc9e8ded2..6e58d9cbb 100644 --- a/enable/trait_defs/ui/rgba_color_editor.py +++ b/enable/trait_defs/ui/rgba_color_editor.py @@ -14,4 +14,13 @@ elif toolkit().toolkit.startswith("qt"): from .qt4.rgba_color_editor import RGBAColorEditor else: - RGBAColorEditor = None + class RGBAColorEditor(object): + """ An unimplemented toolkit object + + This is returned if an object isn't implemented by the selected + toolkit. It raises an exception if it is ever instantiated. + """ + + def __init__(self, *args, **kwargs): + msg = "the %s backend doesn't implement RGBAColorEditor" + raise NotImplementedError(msg % (toolkit().toolkit,)) diff --git a/kiva/fonttools/font.py b/kiva/fonttools/font.py index bf8d0508e..2db2221ee 100644 --- a/kiva/fonttools/font.py +++ b/kiva/fonttools/font.py @@ -14,62 +14,157 @@ import warnings from kiva.constants import ( - BOLD, DECORATIVE, DEFAULT, ITALIC, MODERN, NORMAL, ROMAN, - SCRIPT, SWISS, TELETYPE, WEIGHT_BOLD, WEIGHT_MEDIUM, WEIGHT_NORMAL, - bold_styles, italic_styles + BOLD, DECORATIVE, DEFAULT, ITALIC, MODERN, NORMAL, ROMAN, SCRIPT, SWISS, + TELETYPE, WEIGHT_BOLD, WEIGHT_EXTRABOLD, WEIGHT_EXTRAHEAVY, + WEIGHT_EXTRALIGHT, WEIGHT_HEAVY, WEIGHT_LIGHT, WEIGHT_MEDIUM, + WEIGHT_NORMAL, WEIGHT_SEMIBOLD, WEIGHT_THIN, bold_styles, italic_styles, ) from kiva.fonttools._query import FontQuery from kiva.fonttools.font_manager import default_font_manager -# Various maps used by str_to_font -font_families = { - "default": DEFAULT, - "decorative": DECORATIVE, - "roman": ROMAN, - "script": SCRIPT, - "swiss": SWISS, - "modern": MODERN, +FAMILIES = { + 'default': DEFAULT, + 'cursive': SCRIPT, + 'decorative': DECORATIVE, + 'fantasy': DECORATIVE, + 'modern': MODERN, + 'monospace': MODERN, + 'roman': ROMAN, + 'sans-serif': SWISS, + 'script': SCRIPT, + 'serif': ROMAN, + 'swiss': SWISS, + 'teletype': TELETYPE, + 'typewriter': TELETYPE, } -font_styles = {"italic": ITALIC} -font_weights = {"bold": WEIGHT_BOLD} -font_noise = {"pt", "point", "family"} +WEIGHTS = { + 'thin': WEIGHT_THIN, + 'extra-light': WEIGHT_EXTRALIGHT, + 'light': WEIGHT_LIGHT, + 'regular': WEIGHT_NORMAL, + 'medium': WEIGHT_MEDIUM, + 'semi-bold': WEIGHT_SEMIBOLD, + 'bold': WEIGHT_BOLD, + 'extra-bold': WEIGHT_EXTRABOLD, + 'heavy': WEIGHT_HEAVY, + 'extra-heavy': WEIGHT_EXTRAHEAVY +} +STYLES = { + 'italic': ITALIC, + 'oblique': ITALIC, +} +DECORATIONS = {'underline'} +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. + + The parser is simple, and works by splitting the description on whitespace + and examining each resulting token for understood terms: + + Size + The first numeric term is treated as the font size. + + Weight + The following weight terms are accepted: 'thin', 'extra-light', + 'light', 'regular', 'medium', 'semi-bold', 'bold', 'extra-bold', + 'heavy', 'extra-heavy'. + + Style + The following style terms are accepted: 'italic', 'oblique'. + + Decorations + The following decoration term is accepted: 'underline' + + Generic Families + The following generic family terms are accepted: 'default', 'cursive', + 'decorative', 'fantasy', 'modern', 'monospace', 'roman', 'sans-serif', + 'script', 'serif', 'swiss', 'teletype', 'typewriter'. + + In addition, the parser ignores the terms 'pt', 'point', 'px', and 'family'. + Any remaining terms are combined into the typeface name. There is no + expected order to the terms. + + This parser 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. -def str_to_font(fontspec): + 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. + + This is derived from Pyface's equivalent simple_parser. Eventually both + will be replaced by better parsers that can parse something closer to a + CSS font definition. + """ + face = [] + family = DEFAULT + size = None + weight = WEIGHT_NORMAL + style = NORMAL + underline = False + for word in description.split(): + lower_word = word.casefold() + if lower_word in NOISE: + continue + elif lower_word in FAMILIES: + family = FAMILIES[lower_word] + elif lower_word in WEIGHTS: + weight = WEIGHTS[lower_word] + elif lower_word in STYLES: + style = STYLES[lower_word] + elif lower_word in DECORATIONS: + underline = True + else: + if size is None: + try: + size = int(lower_word) + continue + except ValueError: + pass + face.append(word) + + face_name = " ".join(face) + if size is None: + size = 10 + + return { + 'face_name': face_name, + 'size': size, + 'family': family, + 'weight': weight, + 'style': style, + 'underline': underline, + } + + +def str_to_font(fontspec, parser=simple_parser): """ Converts a string specification of a font into a Font instance. string specifications are of the form: "modern 12", "9 roman italic", and so on. """ - point_size = 10 - family = DEFAULT - style = NORMAL - weight = WEIGHT_NORMAL - underline = 0 - facename = [] - for word in fontspec.split(): - lword = word.lower() - if lword in font_families: - family = font_families[lword] - elif lword in font_styles: - style = font_styles[lword] - elif lword in font_weights: - weight = font_weights[lword] - elif lword == "underline": - underline = 1 - elif lword not in font_noise: - try: - point_size = int(lword) - except Exception: - facename.append(word) - return Font( - size=point_size, - family=family, - weight=weight, - style=style, - underline=underline, - face_name=" ".join(facename), - ) + font_properties = parser(fontspec) + return Font(**font_properties) class Font(object): @@ -98,21 +193,34 @@ class Font(object): def __init__(self, face_name="", size=12, family=SWISS, weight=WEIGHT_NORMAL, style=NORMAL, underline=0, encoding=DEFAULT): - if (not isinstance(face_name, str) - or not isinstance(size, int) - or not isinstance(family, int) - or not isinstance(weight, int) - or not isinstance(style, int) - or not isinstance(underline, int) - or not isinstance(encoding, int)): - raise RuntimeError("Bad value in Font() constructor.") + if not isinstance(face_name, str): + raise RuntimeError( + f"Expected face name to be a str, got {face_name!r}") + if not isinstance(size, int): + raise RuntimeError( + f"Expected size to be an int, got {size!r}") + if not isinstance(family, int): + raise RuntimeError( + f"Expected family to be an int, got {family!r}") + if not isinstance(weight, int): + raise RuntimeError( + f"Expected weight to be an int, got {weight!r}") + if not isinstance(style, int): + raise RuntimeError( + f"Expected style to be an int, got {style!r}") + if not isinstance(underline, int): + raise RuntimeError( + f"Expected underline to be a int, got {underline!r}") + if not isinstance(encoding, int): + raise RuntimeError( + f"Expected encoding to be an int, got {encoding!r}") self.face_name = face_name self.size = size self.family = family self.weight = weight self.style = style - self.underline = underline + self.underline = bool(underline) self.encoding = encoding # correct the style and weight if needed (can be removed in Enable 7) diff --git a/kiva/fonttools/tests/test_font.py b/kiva/fonttools/tests/test_font.py index 0bce6944a..d28d00d3f 100644 --- a/kiva/fonttools/tests/test_font.py +++ b/kiva/fonttools/tests/test_font.py @@ -9,18 +9,23 @@ # Thanks for using Enthought open source! """ Tests for kiva.fonttools.font """ +from itertools import chain, combinations import os import unittest -from kiva.api import ( - BOLD, BOLD_ITALIC, Font, ITALIC, MODERN, NORMAL, ROMAN, WEIGHT_BOLD, - WEIGHT_LIGHT +from kiva.constants import ( + BOLD, BOLD_ITALIC, DEFAULT, ITALIC, MODERN, NORMAL, ROMAN, WEIGHT_BOLD, + WEIGHT_LIGHT, WEIGHT_NORMAL, SWISS, ) -from kiva.fonttools import str_to_font from kiva.fonttools.tests._testing import patch_global_font_manager +from kiva.fonttools.font import ( + DECORATIONS, FAMILIES, NOISE, STYLES, WEIGHTS, Font, str_to_font, + simple_parser, +) class TestFont(unittest.TestCase): + def setUp(self): # Invalidate the global font manager cache to avoid test interaction # as well as catching erroneous assumption on an existing cache. @@ -92,6 +97,18 @@ def test_str_to_font(self): ) self.assertEqual(from_ctor, from_str) + # Using extra font weights + from_str = str_to_font("Times roman light italic underline 72") + from_ctor = Font( + "Times", + family=ROMAN, + weight=WEIGHT_LIGHT, + style=ITALIC, + size=72, + underline=1, + ) + self.assertEqual(from_ctor, from_str) + def test_is_bold_false(self): for weight in range(100, 501, 100): with self.subTest(weight=weight): @@ -175,3 +192,136 @@ def test_font_query_warnings(self): query = font._make_font_query() self.assertEqual(query.get_weight(), WEIGHT_LIGHT) self.assertEqual(query.get_style(), "italic") + + +class TestSimpleParser(unittest.TestCase): + + def test_empty(self): + properties = simple_parser("") + self.assertEqual( + properties, + { + 'face_name': "", + 'family': DEFAULT, + 'size': 10, + 'weight': WEIGHT_NORMAL, + 'style': NORMAL, + 'underline': False, + }, + ) + + def test_typical(self): + properties = simple_parser( + "10 pt bold italic underline Helvetica sans-serif") + self.assertEqual( + properties, + { + 'face_name': "Helvetica", + 'family': SWISS, + 'size': 10, + 'weight': WEIGHT_BOLD, + 'style': ITALIC, + 'underline': True, + }, + ) + + def test_noise(self): + for noise in NOISE: + with self.subTest(noise=noise): + properties = simple_parser(noise) + self.assertEqual( + properties, + { + 'face_name': "", + 'family': DEFAULT, + 'size': 10, + 'weight': WEIGHT_NORMAL, + 'style': NORMAL, + 'underline': False, + }, + ) + + def test_generic_families(self): + for family, constant in FAMILIES.items(): + with self.subTest(family=family): + properties = simple_parser(family) + self.assertEqual( + properties, + { + 'face_name': "", + 'family': constant, + 'size': 10, + 'weight': WEIGHT_NORMAL, + 'style': NORMAL, + 'underline': False, + }, + ) + + def test_size(self): + for size in [12, 24]: + with self.subTest(size=size): + properties = simple_parser(str(size)) + self.assertEqual( + properties, + { + 'face_name': "", + 'family': DEFAULT, + 'size': size, + 'weight': WEIGHT_NORMAL, + 'style': NORMAL, + 'underline': False, + }, + ) + + def test_weight(self): + for weight, constant in WEIGHTS.items(): + with self.subTest(weight=weight): + properties = simple_parser(weight) + self.assertEqual( + properties, + { + 'face_name': "", + 'family': DEFAULT, + 'size': 10, + 'weight': constant, + 'style': NORMAL, + 'underline': False, + }, + ) + + def test_style(self): + for style, constant in STYLES.items(): + with self.subTest(style=style): + properties = simple_parser(style) + self.assertEqual( + properties, + { + 'face_name': "", + 'family': DEFAULT, + 'size': 10, + 'weight': WEIGHT_NORMAL, + 'style': constant, + 'underline': False, + }, + ) + + 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, + { + 'face_name': "", + 'family': DEFAULT, + 'size': 10, + 'weight': WEIGHT_NORMAL, + 'style': NORMAL, + 'underline': 'underline' in decorations, + }, + ) diff --git a/kiva/trait_defs/api.py b/kiva/trait_defs/api.py index f12e9aa2d..e9ab583e9 100644 --- a/kiva/trait_defs/api.py +++ b/kiva/trait_defs/api.py @@ -7,4 +7,12 @@ # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! + +from warnings import warn + from .kiva_font_trait import KivaFont + +warn( + "KivaFont should be imported from enable.trait_defs.api", + DeprecationWarning, +) diff --git a/kiva/trait_defs/kiva_font_trait.py b/kiva/trait_defs/kiva_font_trait.py index 44a4d4fef..d051ede45 100644 --- a/kiva/trait_defs/kiva_font_trait.py +++ b/kiva/trait_defs/kiva_font_trait.py @@ -10,181 +10,7 @@ """ Trait definition for a wxPython-based Kiva font. """ -import logging +from enable.trait_defs.kiva_font_trait import KivaFont as _KivaFont -from traits.api import Trait, TraitError, TraitHandler, TraitFactory - -logger = logging.getLogger(__name__) - -try: - from traitsui.api import toolkit -except ImportError: - toolkit = None - logger.exception("Could not import TraitsUI, KivaFontTrait has no editor") - - -KivaFontEditor = None - -if toolkit is not None: - if toolkit().toolkit == "wx": - from .ui.wx.kiva_font_editor import KivaFontEditor - elif toolkit().toolkit.startswith("qt"): - # FIXME - # from .ui.qt4.kiva_font_editor import KivaFontEditor - pass - - -# ----------------------------------------------------------------------------- -# Convert a string into a valid 'Font' object (if possible): -# ----------------------------------------------------------------------------- - -# Strings to ignore in text representations of fonts -font_noise = ["pt", "point", "family"] - -font_families = font_styles = font_weights = DEFAULT = NORMAL = WEIGHT_NORMAL = None - - -def init_constants(): - """ Dynamically load Kiva constants to avoid import dependencies. - """ - global font_families, font_styles, font_weights, default_face - global DEFAULT, NORMAL, WEIGHT_NORMAL - - if font_families is not None: - return - - import kiva.constants as kc - - DEFAULT = kc.DEFAULT - NORMAL = kc.NORMAL - WEIGHT_NORMAL = kc.WEIGHT_NORMAL - - # Mapping of strings to valid Kiva font families: - font_families = { - "default": kc.DEFAULT, - "decorative": kc.DECORATIVE, - "roman": kc.ROMAN, - "script": kc.SCRIPT, - "swiss": kc.SWISS, - "modern": kc.MODERN, - } - - # Mapping of strings to Kiva font styles: - font_styles = {"italic": kc.ITALIC} - - # Mapping of strings to Kiva font weights: - font_weights = {"bold": kc.WEIGHT_BOLD} - - default_face = { - kc.SWISS: "Arial", - kc.ROMAN: "Times", - kc.MODERN: "Courier", - kc.SCRIPT: "Zapfino", - kc.DECORATIVE: "Zapfino", # need better choice for this - } - - -# Strings to ignore in text representations of fonts -font_noise = ["pt", "point", "family"] - -# ----------------------------------------------------------------------------- -# 'TraitKivaFont' class' -# ----------------------------------------------------------------------------- - - -class TraitKivaFont(TraitHandler): - """ Ensures that values assigned to a trait attribute are valid font - descriptor strings for Kiva fonts; the value actually assigned is the - corresponding Kiva font. - """ - - # ------------------------------------------------------------------------- - # Validates that the value is a valid font: - # ------------------------------------------------------------------------- - - def validate(self, object, name, value): - """ Validates that the value is a valid font. - """ - from kiva.fonttools import Font - - if isinstance(value, Font): - return value - - # Make sure all Kiva related data is loaded: - init_constants() - - try: - point_size = 10 - family = DEFAULT - style = NORMAL - weight = WEIGHT_NORMAL - underline = 0 - facename = [] - for word in value.split(): - lword = word.lower() - if lword in font_families: - family = font_families[lword] - elif lword in font_styles: - style = font_styles[lword] - elif lword in font_weights: - weight = font_weights[lword] - elif lword == "underline": - underline = 1 - elif lword not in font_noise: - try: - point_size = int(lword) - except Exception: - facename.append(word) - - if facename == "": - facename = default_face.get(family, "") - # FIXME: The above if clause never happens, the below should - # be correct though it results in loading weird fonts. - # if facename == []: - # facename = [default_face.get(family, "")] - return Font( - face_name=" ".join(facename), - size=point_size, - family=family, - weight=weight, - style=style, - underline=underline, - ) - except Exception: - pass - - raise TraitError(object, name, "a font descriptor string", repr(value)) - - def info(self): - return ( - "a string describing a font (e.g. '12 pt bold italic " - "swiss family Arial' or 'default 12')" - ) - - -fh = TraitKivaFont() -if KivaFontEditor is not None: - KivaFontTrait = Trait( - fh.validate(None, None, "modern 12"), fh, editor=KivaFontEditor - ) -else: - KivaFontTrait = Trait(fh.validate(None, None, "modern 12"), fh) - - -def KivaFontFunc(*args, **metadata): - """ Returns a trait whose value must be a GUI toolkit-specific font. - - Description: - For wxPython, the returned trait accepts any of the following: - - * an kiva.fonttools.Font instance - * a string describing the font, including one or more of the font family, - size, weight, style, and typeface name. - - Default Value: - For wxPython, 'Arial 10' - """ - return KivaFontTrait(*args, **metadata) - - -KivaFont = TraitFactory(KivaFontFunc) +# old KivaFont defaulted to "modern" family rather than "default" +KivaFont = _KivaFont("modern 12") diff --git a/kiva/trait_defs/tests/test_kiva_font_trait.py b/kiva/trait_defs/tests/test_kiva_font_trait.py index f0616f5ad..9b85aa230 100644 --- a/kiva/trait_defs/tests/test_kiva_font_trait.py +++ b/kiva/trait_defs/tests/test_kiva_font_trait.py @@ -14,10 +14,26 @@ from kiva.fonttools.font import Font from kiva import constants -from ..kiva_font_trait import ( - KivaFont, TraitKivaFont, font_families, font_styles, font_weights -) - +from ..kiva_font_trait import KivaFont + +# XXX This test is kept to validate backwards compatibility of the new +# KivaTrait added in Enable and will eventually be removed. + +# Mapping of strings to valid Kiva font families: +font_families = { + "default": constants.DEFAULT, + "decorative": constants.DECORATIVE, + "roman": constants.ROMAN, + "script": constants.SCRIPT, + "swiss": constants.SWISS, + "modern": constants.MODERN, +} + +# Mapping of strings to Kiva font styles: +font_styles = {"italic": constants.ITALIC} + +# Mapping of strings to Kiva font weights: +font_weights = {"bold": constants.WEIGHT_BOLD} class FontExample(HasTraits): diff --git a/kiva/trait_defs/ui/wx/kiva_font_editor.py b/kiva/trait_defs/ui/wx/kiva_font_editor.py index 48c72306c..867112b69 100644 --- a/kiva/trait_defs/ui/wx/kiva_font_editor.py +++ b/kiva/trait_defs/ui/wx/kiva_font_editor.py @@ -117,7 +117,7 @@ def str_font(self, font): import kiva.constants as kc weight = " Bold" if font.is_bold() else "" - style = " Italic" if font.style in italic_styles else "" + style = " Italic" if font.style in kc.italic_styles else "" underline = " Underline" if font.underline else "" return "%s point %s%s%s%s" % ( @@ -136,7 +136,7 @@ def all_facenames(self): """ Returns a list of all available font typeface names. """ font_manager = default_font_manager() - return sorted({f.name for f in font_manager.ttflist}) + return sorted({f.fname for f in font_manager.ttf_db._entries}) def KivaFontEditor(*args, **traits): diff --git a/kiva/trait_defs/ui/wx/tests/__init__.py b/kiva/trait_defs/ui/wx/tests/__init__.py new file mode 100644 index 000000000..e69de29bb