From 3dcfb9e55bff985ddcdd6aacf226e7f4d0a51c45 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Fri, 8 Apr 2022 10:45:37 +0100 Subject: [PATCH 1/9] 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. --- .../demo/enable/editors/font_editor.py | 46 +++++ enable/label.py | 33 +--- .../tests/trait_defs/test_kiva_font_editor.py | 184 ++++++++++++++++++ enable/trait_defs/ui/api.py | 2 + enable/trait_defs/ui/editor_with_component.py | 133 +++++++++++++ enable/trait_defs/ui/kiva_font_editor.py | 128 ++++++++++++ 6 files changed, 502 insertions(+), 24 deletions(-) create mode 100644 enable/examples/demo/enable/editors/font_editor.py create mode 100644 enable/tests/trait_defs/test_kiva_font_editor.py create mode 100644 enable/trait_defs/ui/editor_with_component.py create mode 100644 enable/trait_defs/ui/kiva_font_editor.py diff --git a/enable/examples/demo/enable/editors/font_editor.py b/enable/examples/demo/enable/editors/font_editor.py new file mode 100644 index 000000000..b55b5e80f --- /dev/null +++ b/enable/examples/demo/enable/editors/font_editor.py @@ -0,0 +1,46 @@ +# (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! +from traits.api import HasStrictTraits +from traitsui.api import View, Item + +from enable.api import Container, TextField, font_trait +from enable.trait_defs.ui.api import KivaFontEditor +from enable.examples._example_support import demo_main + +from kiva.api import Font +from kiva.constants import ITALIC, SWISS, WEIGHT_BOLD +from kiva.trait_defs.api import KivaFont + +size = (500, 200) + +sample_text = "Sphinx of black quartz, judge my vow." + + +class Demo(HasStrictTraits): + """ An example which shows the KivaFontEditor's variations. """ + + font = font_trait(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"), + resizable=True, + width=size[0], + height=size[1], + ) + + +if __name__ == "__main__": + # Save demo so that it doesn't get garbage collected when run within + # existing event loop (i.e. from ipython). + demo = demo_main(Demo, size=size) diff --git a/enable/label.py b/enable/label.py index fe452dca9..0782927fe 100644 --- a/enable/label.py +++ b/enable/label.py @@ -1,4 +1,4 @@ -# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX +# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD @@ -17,7 +17,7 @@ # Enthought library imports from kiva.api import FILL, STROKE from kiva.trait_defs.api import KivaFont -from traits.api import Bool, Enum, Float, HasTraits, Int, List, Str +from traits.api import Bool, Enum, Float, HasTraits, Int, List, Str, observe # Local, relative imports from .colors import black_color_trait, transparent_color_trait @@ -92,11 +92,11 @@ def _calc_line_positions(self, gc): for line in self.text.split("\n")[::-1]: if line != "": ( - width, - height, descent, leading, - ) = gc.get_full_text_extent(line) + width, + height, + ) = gc.get_text_extent(line) if width > max_width: max_width = width new_y_pos = ( @@ -191,25 +191,6 @@ def _draw_mainlayer(self, gc, view_bounds=None, mode="normal"): with gc: gc.translate_ctm(*self.position) - # Draw border and fill background - width, height = self._bounding_box - if self.bgcolor != "transparent": - gc.set_fill_color(self.bgcolor_) - gc.draw_rect((0, 0, width, height), FILL) - if self.border_width > 0: - gc.set_stroke_color(self.border_color_) - gc.set_line_width(self.border_width) - border_offset = (self.border_width - 1) / 2.0 - gc.draw_rect( - ( - border_offset, - border_offset, - width - 2 * border_offset, - height - 2 * border_offset, - ), - STROKE, - ) - gc.set_fill_color(self.color_) gc.set_stroke_color(self.color_) gc.set_font(self.font) @@ -254,3 +235,7 @@ def _text_changed(self): def _rotate_angle_changed(self): self._position_cache_valid = False + + @observe('bounds.items') + def _update_bounds(self, event): + self._position_cache_valid = False diff --git a/enable/tests/trait_defs/test_kiva_font_editor.py b/enable/tests/trait_defs/test_kiva_font_editor.py new file mode 100644 index 000000000..f84968b8c --- /dev/null +++ b/enable/tests/trait_defs/test_kiva_font_editor.py @@ -0,0 +1,184 @@ +# (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! +""" Test the interaction between traitsui and enable's ComponentEditor. +""" +import unittest +from unittest import mock + +from pyface.font import Font as PyfaceFont +import pyface.font_dialog +from traits.api import Any, HasTraits +from traits.testing.api import UnittestTools +from traitsui.api import Item, View + +from kiva.api import Font +from enable.enable_traits import font_trait +from enable.trait_defs.ui.kiva_font_editor import KivaFontEditor +from enable.tests._testing import ( + get_dialog_size, skip_if_null, skip_if_not_qt, skip_if_not_wx +) + +ITEM_WIDTH, ITEM_HEIGHT = 700, 200 + + +class KivaFontView(HasTraits): + """ View containing an item with ComponentEditor. """ + + font = font_trait() + + traits_view = View( + Item("font", editor=KivaFontEditor()), + resizable=True, + ) + + +class TestKivaFontEditor(UnittestTools, unittest.TestCase): + + @skip_if_null + def test_readonly_default_view(self): + obj = KivaFontView() + + ui = obj.edit_traits( + view = View( + Item("font", editor=KivaFontEditor(), style='readonly'), + resizable=True, + ) + ) + try: + # check initial state + editor = ui.info.font + component = editor.component + self.assertIs(editor.value, obj.font) + self.assertIs(editor.font, obj.font) + self.assertIs(component.font, obj.font) + self.assertEqual(editor.str_value, "10 point") + self.assertEqual(component.text, "10 point") + + # check a change + new_font = Font( + face_name="Helvetica", + size=24, + weight=700, + style=2, + ) + + obj.font = new_font + + self.assertIs(editor.value, new_font) + self.assertIs(component.font, new_font) + self.assertEqual(editor.str_value, "24 point Helvetica Bold Italic") + self.assertEqual(component.text, "24 point Helvetica Bold Italic") + finally: + ui.dispose() + + @skip_if_null + def test_simple_default_view(self): + obj = KivaFontView() + + ui = obj.edit_traits( + view = View( + Item("font", editor=KivaFontEditor()), + resizable=True, + ) + ) + try: + editor = ui.info.font + component = editor.component + self.assertIs(editor.value, obj.font) + self.assertIs(editor.font, obj.font) + self.assertIs(component.font, obj.font) + self.assertEqual(editor.str_value, "10 point") + self.assertEqual(component.text, "10 point") + finally: + ui.dispose() + + @skip_if_null + def test_simple_default_object_change(self): + obj = KivaFontView() + + ui = obj.edit_traits( + view = View( + Item("font", editor=KivaFontEditor()), + resizable=True, + ) + ) + try: + editor = ui.info.font + component = editor.component + + new_font = Font( + face_name="Helvetica", + size=24, + weight=700, + style=2, + ) + obj.font = new_font + + 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(component.text, "24 point Helvetica Bold Italic") + finally: + ui.dispose() + + @skip_if_null + def test_simple_default_editor_change(self): + obj = KivaFontView() + + ui = obj.edit_traits( + view = View( + Item("font", editor=KivaFontEditor()), + resizable=True, + ) + ) + try: + editor = ui.info.font + component = editor.component + + new_font = Font( + face_name="Helvetica", + size=24, + weight=700, + style=2, + ) + editor.update_object(new_font) + + self.assertIs(obj.font, new_font) + 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(component.text, "24 point Helvetica Bold Italic") + finally: + ui.dispose() + + @skip_if_null + def test_sample_text(self): + # this is a smoke test + obj = KivaFontView() + + ui = obj.edit_traits( + view = View( + Item( + "font", + editor=KivaFontEditor(sample_text="sample text"), + style='readonly', + ), + resizable=True, + ) + ) + try: + editor = ui.info.font + component = editor.component + self.assertEqual(editor.str_value, "sample text") + self.assertEqual(component.text, "sample text") + finally: + ui.dispose() diff --git a/enable/trait_defs/ui/api.py b/enable/trait_defs/ui/api.py index ffb5971a2..0ae71b3e1 100644 --- a/enable/trait_defs/ui/api.py +++ b/enable/trait_defs/ui/api.py @@ -7,4 +7,6 @@ # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! + +from .kiva_font_editor import KivaFontEditor from .rgba_color_editor import RGBAColorEditor diff --git a/enable/trait_defs/ui/editor_with_component.py b/enable/trait_defs/ui/editor_with_component.py new file mode 100644 index 000000000..16dec991d --- /dev/null +++ b/enable/trait_defs/ui/editor_with_component.py @@ -0,0 +1,133 @@ +# (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! + +from traits.api import Bool, Instance, Str, observe +from traitsui.api import BasicEditorFactory, Editor as BaseEditor, toolkit_object + +from enable.component import Component +from enable.label import Label +from enable.window import Window +from enable.enable_traits import font_trait + + +Editor = toolkit_object("editor:Editor") +if not issubclass(Editor, BaseEditor): + Editor = object + + +class EditorWithComponent(Editor): + """Base class for editors which hold a Component that displays the value. + + This is distinct from CompononentEditor in that the value is not the + Component. + """ + + #: The window that displays the component. + window = Instance(Window) + + #: The component that is created by the UI. + component = Instance(Component) + + def init(self, parent): + self.component = self.create_component() + size = self._get_initial_size() + self.window = self.create_window(parent, size) + self.control = self.window.control + self._parent = parent + + def create_component(self): + raise NotImplementedError() + + def create_window(self, parent, size): + window = Window( + parent, + size=size, + component=self.component, + high_resolution=self.factory.high_resolution, + bgcolor='sys_window', + ) + return window + + def dispose(self): + if self.window is not None: + self.window.cleanup() + self.component = None + self.window = None + self._parent = None + super().dispose() + + +class EditorWithLabelComponent(EditorWithComponent): + """A class that creates a Label component. + + By default it displays the string representation of the value. + """ + + #: The font to use for the label. + font = font_trait() + + def create_component(self): + """Creates the label component.""" + component = Label( + hjustify="center", + vjustify="center", + resizable='hv', + text=self.str_value, + font=self.font, + ) + return component + + def update_editor(self): + if self.component is not None: + self.component.text = self.str_value + self.component.invalidate_and_redraw() + + @observe('font') + def update_font(self, event): + if self.component is not None: + self.component.font = self.font + self.component.invalidate_and_redraw() + + def _get_initial_size(self): + width = self.item.width + height = self.item.height + + if width < 0: + width = 200 + if height < 0: + height = 50 + + return width, height + + def set_size_policy(self, direction, resizable, springy, stretch): + """Set the size policy of the editor's component. + + This is only used by the Qt backend. This is always spring and + resizable. + """ + from pyface.qt import QtGui + + policy = self.window.control.sizePolicy() + + if direction == QtGui.QBoxLayout.Direction.LeftToRight: + policy.setHorizontalStretch(stretch) + policy.setHorizontalPolicy(QtGui.QSizePolicy.Policy.Expanding) + policy.setVerticalStretch(stretch) + policy.setVerticalPolicy(QtGui.QSizePolicy.Policy.Expanding) + + else: # TopToBottom + policy.setVerticalStretch(stretch) + policy.setVerticalPolicy(QtGui.QSizePolicy.Policy.Expanding) + policy.setHorizontalStretch(stretch) + policy.setHorizontalPolicy(QtGui.QSizePolicy.Policy.Expanding) + + self.window.control.setSizePolicy(policy) + if self.window.control is not self.control: + super().set_size_policy() diff --git a/enable/trait_defs/ui/kiva_font_editor.py b/enable/trait_defs/ui/kiva_font_editor.py new file mode 100644 index 000000000..177339a11 --- /dev/null +++ b/enable/trait_defs/ui/kiva_font_editor.py @@ -0,0 +1,128 @@ +# (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! + +from pyface.font import Font as PyfaceFont +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 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 + + +def face_name(font): + """ Returns a Font's typeface name. + """ + face_name = font.face_name + if isinstance(face_name, SequenceTypes): + face_name = face_name[0] + + return face_name + + +def str_font(font): + """ Returns the text representation of the specified font trait value + """ + + weight = " Bold" if font.is_bold() else "" + 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() + + +class ReadOnlyEditor(EditorWithLabelComponent): + """An Editor which displays a label using the font.""" + + def init(self, parent): + self.font = self.value + super().init(parent) + + def update_editor(self): + self.font = self.value + super().update_editor() + + def string_value(self, value, format_func=None): + if self.factory.sample_text: + return self.factory.sample_text + + return super().string_value(value, str_font) + + +class SimpleEditor(ReadOnlyEditor): + """An Editor which displays a label using the font, click for font dialog. + """ + + button = Instance(ButtonTool) + + def create_component(self): + component = super().create_component() + # add a grey border to indicate interactivity + component.border_visible = True + component.border_width = 1 + component.border_color = (0.5, 0.5, 0.5, 1.0) + + # add a button tool to make the label respond to clicks + self.button = ButtonTool(component=component) + component.tools.append(self.button) + return component + + def update_object(self, value): + """Handle changes to the font due to user action. + """ + self.value = value + # force a refresh of the component's settings + self.update_editor() + + @observe('button:clicked') + def button_clicked(self, event): + if self.window is None: + return + 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', + size=self.value.size, + ) + pyface_font = get_font(self.window.control, pyface_font) + if pyface_font is not None: + font = Font( + face_name=pyface_font.family[0], + weight=pyface_font.weight_, + style=kc.ITALIC if pyface_font.style == 'italic' else kc.NORMAL, + size=int(pyface_font.size), + ) + self.update_object(font) + + +class KivaFontEditor(EditorFactory): + """Editor factory for KivaFontEditors + """ + + #: Alternative text to display instead of the font description. + sample_text = Str() + + #: Switch to turn off high resolution rendering if needed. + high_resolution = Bool(True) + + #: The default format func displays a description of the font. + format_func = Callable(str_font) + + def _get_simple_editor_class(self): + return SimpleEditor + + def _get_readonly_editor_class(self): + return ReadOnlyEditor From d588a713ac2dc1bc823730e43e6928a0292efd4a Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Fri, 8 Apr 2022 17:50:06 +0100 Subject: [PATCH 2/9] 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 --- docs/source/enable/traits.rst | 2 +- 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 | 14 +- enable/gadgets/vu_meter.py | 2 +- enable/label.py | 2 +- .../tests/trait_defs/test_kiva_font_trait.py | 92 ++++++++ enable/text_field_style.py | 2 +- enable/text_grid.py | 2 +- enable/tools/toolbars/toolbar_buttons.py | 2 +- enable/trait_defs/api.py | 1 + enable/trait_defs/kiva_font_trait.py | 131 +++++++++++ enable/trait_defs/ui/kiva_font_editor.py | 16 +- kiva/fonttools/font.py | 213 +++++++++++++----- 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 +- 20 files changed, 604 insertions(+), 257 deletions(-) create mode 100644 enable/tests/trait_defs/test_kiva_font_trait.py create mode 100644 enable/trait_defs/kiva_font_trait.py diff --git a/docs/source/enable/traits.rst b/docs/source/enable/traits.rst index 9ba422d00..07ccce0e6 100644 --- a/docs/source/enable/traits.rst +++ b/docs/source/enable/traits.rst @@ -18,7 +18,7 @@ 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`. +: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` 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 b55b5e80f..8cb69f643 100644 --- a/enable/examples/demo/enable/editors/font_editor.py +++ b/enable/examples/demo/enable/editors/font_editor.py @@ -10,13 +10,13 @@ from traits.api import HasStrictTraits from traitsui.api import View, Item -from enable.api import Container, TextField, font_trait +from enable.api import Container, TextField +from enable.trait_defs.api import KivaFont from enable.trait_defs.ui.api import KivaFontEditor from enable.examples._example_support import demo_main from kiva.api import Font from kiva.constants import ITALIC, SWISS, WEIGHT_BOLD -from kiva.trait_defs.api import KivaFont size = (500, 200) @@ -26,13 +26,13 @@ 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', 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], 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 0782927fe..5900afd73 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/trait_defs/test_kiva_font_trait.py b/enable/tests/trait_defs/test_kiva_font_trait.py new file mode 100644 index 000000000..a112fc325 --- /dev/null +++ b/enable/tests/trait_defs/test_kiva_font_trait.py @@ -0,0 +1,92 @@ +# (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 traits.api import HasTraits, TraitError + +from kiva.fonttools.font import Font, FAMILIES +from kiva import constants +from enable.trait_defs.kiva_font_trait import ( + KivaFont, font_families, font_styles, font_weights +) + + +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 font_weights.items(): + expected_outcomes[weight] = Font(weight=kiva_weight, size=10, family=constants.DEFAULT) + + for style, kiva_style in font_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( + "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) + example = FontExample(font=font) + + result = example.font + + # test we get expected font + self.assertIsInstance(result, Font) + self.assertIs(result, font) + + def test_font_trait_default(self): + example = FontExample() + + self.assertIsInstance(example.font, Font) + self.assertEqual(example.font, Font(size=12, family=constants.MODERN)) + + def test_font_trait_none(self): + with self.assertRaises(TraitError): + FontExample(font=None) 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..c9f9aaad5 100644 --- a/enable/trait_defs/api.py +++ b/enable/trait_defs/api.py @@ -13,3 +13,4 @@ """ from .rgba_color_trait import RGBAColor +from .kiva_font_trait import KivaFont diff --git a/enable/trait_defs/kiva_font_trait.py b/enable/trait_defs/kiva_font_trait.py new file mode 100644 index 000000000..f671b69c3 --- /dev/null +++ b/enable/trait_defs/kiva_font_trait.py @@ -0,0 +1,131 @@ +# (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, simple_parser + + +font_attrs = [ + 'face_name', 'size', 'family', 'weight', 'style', 'underline', 'encoding', +] + +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 is '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.). + parser = None + + def __init__(self, default_value=None, *, parser=simple_parser, **metadata): + 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): + new = super().clone(NoDefaultSpecified, **metadata) + if default_value is not NoDefaultSpecified: + new.default_value = self._get_default_value(default_value) + 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( + "expected " + self.info() + + f", 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/kiva_font_editor.py b/enable/trait_defs/ui/kiva_font_editor.py index 177339a11..77cbceb8b 100644 --- a/enable/trait_defs/ui/kiva_font_editor.py +++ b/enable/trait_defs/ui/kiva_font_editor.py @@ -23,6 +23,20 @@ from .editor_with_component import EditorWithLabelComponent +WEIGHTS = { + kc.WEIGHT_THIN: ' Thin', + kc.WEIGHT_EXTRALIGHT: ' Extra-light', + kc.WEIGHT_LIGHT: ' Light', + kc.WEIGHT_NORMAL: '', + kc.WEIGHT_MEDIUM: ' Medium', + kc.WEIGHT_SEMIBOLD: ' Demi-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,7 +51,7 @@ 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 "" diff --git a/kiva/fonttools/font.py b/kiva/fonttools/font.py index bf8d0508e..3746c320b 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, + 'fantasy': DECORATIVE, + 'decorative': DECORATIVE, + 'serif': ROMAN, + 'roman': ROMAN, + 'cursive': SCRIPT, + 'script': SCRIPT, + 'sans-serif': SWISS, + 'swiss': SWISS, + 'monospace': MODERN, + 'modern': MODERN, + 'typewriter': TELETYPE, + 'teletype': 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, + 'demi-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', 'demi-bold', 'bold', 'extra-bold', + 'heavy', 'extra-heavy'. + + Style + The following style terms are accepted: 'italic', 'oblique'. + + Decorations + The following decoration terms is accepted: 'underline' + + Generic Families + The following generic family terms are accepted: 'default', 'fantasy', + 'decorative', 'serif', 'roman', 'cursive', 'script', 'sans-serif', + 'swiss', 'monospace', 'modern', 'typewriter', 'teletype'. + + In addtion, 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.lower() + 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,13 +193,27 @@ 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)): + 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"Excected size to be an int, got {size!r}") + if not isinstance(family, int): + raise RuntimeError( + f"Excected family to be an int, got {family!r}") + if not isinstance(weight, int): + raise RuntimeError( + f"Excected weight to be an int, got {weight!r}") + if not isinstance(style, int): + raise RuntimeError( + f"Excected style to be an int, got {style!r}") + if not isinstance(underline, int): + raise RuntimeError( + f"Excected underline to be a int, got {underline!r}") + if not isinstance(encoding, int): + raise RuntimeError( + f"Excected encoding to be a bool, got {encoding!r}") raise RuntimeError("Bad value in Font() constructor.") self.face_name = face_name @@ -112,7 +221,7 @@ def __init__(self, face_name="", size=12, family=SWISS, 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): From 3fc6f67c36493db96c5be12ec210dbcad5f3b3c9 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Tue, 26 Apr 2022 17:55:29 +0100 Subject: [PATCH 3/9] Fix long line. --- enable/examples/demo/enable/editors/font_editor.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/enable/examples/demo/enable/editors/font_editor.py b/enable/examples/demo/enable/editors/font_editor.py index 186be0785..5065a2084 100644 --- a/enable/examples/demo/enable/editors/font_editor.py +++ b/enable/examples/demo/enable/editors/font_editor.py @@ -34,7 +34,12 @@ class Demo(HasStrictTraits): 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"), + Item( + 'font', + editor=KivaFontEditor(sample_text=sample_text), + style='readonly', + label="sample text", + ), resizable=True, width=size[0], height=size[1], From 32a3b36568ecf7868a77e9dd3cca70efff15b34e Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Tue, 26 Apr 2022 17:59:29 +0100 Subject: [PATCH 4/9] Save all files before finishing merge! --- enable/label.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/enable/label.py b/enable/label.py index 7d10c4e80..5900afd73 100644 --- a/enable/label.py +++ b/enable/label.py @@ -16,11 +16,7 @@ # Enthought library imports from kiva.api import FILL, STROKE -<<<<<<< HEAD from enable.trait_defs.kiva_font_trait import KivaFont -======= -from kiva.trait_defs.api import KivaFont ->>>>>>> main from traits.api import Bool, Enum, Float, HasTraits, Int, List, Str, observe # Local, relative imports From 555419ebed475cf5aeac406f7f74996e7f82c828 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Wed, 27 Apr 2022 09:56:11 +0100 Subject: [PATCH 5/9] Fix tests. --- enable/tests/drawing/__init__.py | 0 enable/tests/trait_defs/__init__.py | 0 .../tests/trait_defs/test_kiva_font_trait.py | 39 ++++++++++-------- .../tests/trait_defs/test_rgba_color_trait.py | 41 ++++++++----------- 4 files changed, 40 insertions(+), 40 deletions(-) create mode 100644 enable/tests/drawing/__init__.py create mode 100644 enable/tests/trait_defs/__init__.py 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_trait.py b/enable/tests/trait_defs/test_kiva_font_trait.py index a112fc325..6653c36d1 100644 --- a/enable/tests/trait_defs/test_kiva_font_trait.py +++ b/enable/tests/trait_defs/test_kiva_font_trait.py @@ -10,13 +10,12 @@ 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 kiva.fonttools.font import Font, FAMILIES -from kiva import constants -from enable.trait_defs.kiva_font_trait import ( - KivaFont, font_families, font_styles, font_weights -) +from enable.trait_defs.kiva_font_trait import KivaFont class FontExample(HasTraits): @@ -30,13 +29,16 @@ def test_validate_str(self): expected_outcomes = {} expected_outcomes[""] = Font(size=10, family=constants.DEFAULT) - for weight, kiva_weight in font_weights.items(): - expected_outcomes[weight] = Font(weight=kiva_weight, 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 font_styles.items(): - expected_outcomes[style] = Font(style=kiva_style, 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["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) @@ -45,9 +47,11 @@ def test_validate_str(self): 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( + 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, ) @@ -72,20 +76,21 @@ def test_validate_font(self): self.assertIs(result, font) def test_validate_pyface_font(self): - font = Font("Comic Sans", 18) - example = FontExample(font=font) + 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.assertIs(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.MODERN)) + self.assertEqual(example.font, Font(size=12, family=constants.SWISS)) def test_font_trait_none(self): with self.assertRaises(TraitError): diff --git a/enable/tests/trait_defs/test_rgba_color_trait.py b/enable/tests/trait_defs/test_rgba_color_trait.py index c88c515ab..c560a9de2 100644 --- a/enable/tests/trait_defs/test_rgba_color_trait.py +++ b/enable/tests/trait_defs/test_rgba_color_trait.py @@ -10,10 +10,8 @@ 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 @@ -42,6 +40,7 @@ ('blue', "uint8"), ]) + class ColorClass(HasTraits): color = RGBAColor() @@ -53,6 +52,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 +65,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 +77,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 +101,7 @@ def test_init_invalid(self): ] for value in values: with self.subTest(value=value): - with self.assertRaises(TraitError): + with self.assertRaises((ValueError, AttributeError)): RGBAColor(value) def test_validate(self): @@ -116,11 +116,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 +128,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 +177,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 +189,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,7 +211,6 @@ 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): @@ -230,6 +225,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") From cb2b1da09d0c534f1bef83cd44873c2f163375c1 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Wed, 27 Apr 2022 10:21:15 +0100 Subject: [PATCH 6/9] Apply suggestions from code review Co-authored-by: Mark Dickinson --- enable/label.py | 2 +- enable/trait_defs/kiva_font_trait.py | 7 +++---- enable/trait_defs/ui/kiva_font_editor.py | 2 +- kiva/fonttools/font.py | 19 +++++++++---------- 4 files changed, 14 insertions(+), 16 deletions(-) diff --git a/enable/label.py b/enable/label.py index 5900afd73..cadac5d9a 100644 --- a/enable/label.py +++ b/enable/label.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/enable/trait_defs/kiva_font_trait.py b/enable/trait_defs/kiva_font_trait.py index f671b69c3..60b6ab9a1 100644 --- a/enable/trait_defs/kiva_font_trait.py +++ b/enable/trait_defs/kiva_font_trait.py @@ -14,7 +14,7 @@ from traits.api import DefaultValue, TraitError, TraitType, NoDefaultSpecified import kiva.constants as kc -from kiva.fonttools.font import Font, simple_parser +from kiva.fonttools.font import Font, FontParseError, simple_parser font_attrs = [ @@ -63,7 +63,7 @@ def pyface_font_to_font(font): family = kc.DEFAULT size = int(font.size) weight = font.weight_ - style = kc.NORMAL if font.style is 'normal' else kc.ITALIC + style = kc.NORMAL if font.style == 'normal' else kc.ITALIC underline = 'underline' in font.decorations return Font(face_name, size, family, weight, style, underline) @@ -120,8 +120,7 @@ def _get_default_value(self, default_value): font = self.validate(None, None, default_value) except TraitError: raise ValueError( - "expected " + self.info() - + f", but got {default_value!r}" + f"expected {self.info()}, but got {default_value!r}" ) klass = font.__class__ kwargs = {attr: getattr(font, attr) for attr in font_attrs} diff --git a/enable/trait_defs/ui/kiva_font_editor.py b/enable/trait_defs/ui/kiva_font_editor.py index 3e4db2200..c07d25ff8 100644 --- a/enable/trait_defs/ui/kiva_font_editor.py +++ b/enable/trait_defs/ui/kiva_font_editor.py @@ -29,7 +29,7 @@ kc.WEIGHT_LIGHT: ' Light', kc.WEIGHT_NORMAL: '', kc.WEIGHT_MEDIUM: ' Medium', - kc.WEIGHT_SEMIBOLD: ' Demi-bold', + kc.WEIGHT_SEMIBOLD: ' Semi-bold', kc.WEIGHT_BOLD: ' Bold', kc.WEIGHT_EXTRABOLD: ' Extra-bold', kc.WEIGHT_HEAVY: ' Heavy', diff --git a/kiva/fonttools/font.py b/kiva/fonttools/font.py index 3746c320b..abb1d6530 100644 --- a/kiva/fonttools/font.py +++ b/kiva/fonttools/font.py @@ -80,14 +80,14 @@ def simple_parser(description): The following style terms are accepted: 'italic', 'oblique'. Decorations - The following decoration terms is accepted: 'underline' + The following decoration term is accepted: 'underline' Generic Families The following generic family terms are accepted: 'default', 'fantasy', 'decorative', 'serif', 'roman', 'cursive', 'script', 'sans-serif', 'swiss', 'monospace', 'modern', 'typewriter', 'teletype'. - In addtion, the parser ignores the terms 'pt', 'point', 'px', and 'family'. + 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. @@ -123,7 +123,7 @@ def simple_parser(description): style = NORMAL underline = False for word in description.split(): - lower_word = word.lower() + lower_word = word.casefold() if lower_word in NOISE: continue elif lower_word in FAMILIES: @@ -198,23 +198,22 @@ def __init__(self, face_name="", size=12, family=SWISS, f"Expected face name to be a str, got {face_name!r}") if not isinstance(size, int): raise RuntimeError( - f"Excected size to be an int, got {size!r}") + f"Expected size to be an int, got {size!r}") if not isinstance(family, int): raise RuntimeError( - f"Excected family to be an int, got {family!r}") + f"Expected family to be an int, got {family!r}") if not isinstance(weight, int): raise RuntimeError( - f"Excected weight to be an int, got {weight!r}") + f"Expected weight to be an int, got {weight!r}") if not isinstance(style, int): raise RuntimeError( - f"Excected style to be an int, got {style!r}") + f"Expected style to be an int, got {style!r}") if not isinstance(underline, int): raise RuntimeError( - f"Excected underline to be a int, got {underline!r}") + f"Expected underline to be a int, got {underline!r}") if not isinstance(encoding, int): raise RuntimeError( - f"Excected encoding to be a bool, got {encoding!r}") - raise RuntimeError("Bad value in Font() constructor.") + f"Expected encoding to be an int, got {encoding!r}") self.face_name = face_name self.size = size From 82ea9fde213f1f6e540753c0ec1a712960b0599c Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Wed, 27 Apr 2022 10:30:36 +0100 Subject: [PATCH 7/9] Handle color editor tests for null backend. --- enable/tests/trait_defs/test_rgba_color_trait.py | 5 ++++- enable/trait_defs/ui/rgba_color_editor.py | 11 ++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/enable/tests/trait_defs/test_rgba_color_trait.py b/enable/tests/trait_defs/test_rgba_color_trait.py index c560a9de2..663c5a175 100644 --- a/enable/tests/trait_defs/test_rgba_color_trait.py +++ b/enable/tests/trait_defs/test_rgba_color_trait.py @@ -15,8 +15,10 @@ 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"), @@ -101,7 +103,7 @@ def test_init_invalid(self): ] for value in values: with self.subTest(value=value): - with self.assertRaises((ValueError, AttributeError)): + with self.assertRaises(Exception): RGBAColor(value) def test_validate(self): @@ -216,6 +218,7 @@ def test_trait_set_invalid(self): with self.assertRaises(TraitError): ColorClass(color=value) + @skip_if_null def test_get_editor(self): trait = RGBAColor() editor = trait.get_editor() 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,)) From 01a4109f0263a7ffa786bf3cf145b38b70425a1f Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Wed, 27 Apr 2022 13:17:10 +0100 Subject: [PATCH 8/9] Fixes from PR review. --- docs/source/enable/traits.rst | 5 +++-- .../demo/enable/editors/font_editor.py | 1 - enable/trait_defs/api.py | 3 ++- enable/trait_defs/kiva_font_trait.py | 7 ++++++- enable/trait_defs/ui/kiva_font_editor.py | 7 ++----- kiva/fonttools/font.py | 20 +++++++++---------- kiva/trait_defs/ui/wx/kiva_font_editor.py | 4 ++-- kiva/trait_defs/ui/wx/tests/__init__.py | 0 8 files changed, 25 insertions(+), 22 deletions(-) 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 07ccce0e6..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:`enable.trait_defs.kiva_font_trait.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/enable/examples/demo/enable/editors/font_editor.py b/enable/examples/demo/enable/editors/font_editor.py index 5065a2084..0177601b2 100644 --- a/enable/examples/demo/enable/editors/font_editor.py +++ b/enable/examples/demo/enable/editors/font_editor.py @@ -10,7 +10,6 @@ from traits.api import HasStrictTraits from traitsui.api import View, Item -from enable.api import Container, TextField from enable.trait_defs.api import KivaFont from enable.trait_defs.ui.api import KivaFontEditor from enable.examples._example_support import demo_main diff --git a/enable/trait_defs/api.py b/enable/trait_defs/api.py index c9f9aaad5..24ee21279 100644 --- a/enable/trait_defs/api.py +++ b/enable/trait_defs/api.py @@ -9,8 +9,9 @@ # Thanks for using Enthought open source! """ API for enable.trait_defs subpackage. +- :attr:`~.KivaFont` - :attr:`~.RGBAColor` """ -from .rgba_color_trait import 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 index 60b6ab9a1..fef829efe 100644 --- a/enable/trait_defs/kiva_font_trait.py +++ b/enable/trait_defs/kiva_font_trait.py @@ -17,10 +17,12 @@ 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, @@ -77,7 +79,8 @@ class KivaFont(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.). + #: "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): @@ -108,9 +111,11 @@ def get_editor(self, trait): 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): diff --git a/enable/trait_defs/ui/kiva_font_editor.py b/enable/trait_defs/ui/kiva_font_editor.py index c07d25ff8..93b55d78a 100644 --- a/enable/trait_defs/ui/kiva_font_editor.py +++ b/enable/trait_defs/ui/kiva_font_editor.py @@ -12,14 +12,11 @@ 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 @@ -55,7 +52,7 @@ def str_font(font): 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): diff --git a/kiva/fonttools/font.py b/kiva/fonttools/font.py index abb1d6530..26ddce35c 100644 --- a/kiva/fonttools/font.py +++ b/kiva/fonttools/font.py @@ -24,18 +24,18 @@ FAMILIES = { 'default': DEFAULT, - 'fantasy': DECORATIVE, + 'cursive': SCRIPT, 'decorative': DECORATIVE, - 'serif': ROMAN, + 'fantasy': DECORATIVE, + 'modern': MODERN, + 'monospace': MODERN, 'roman': ROMAN, - 'cursive': SCRIPT, - 'script': SCRIPT, 'sans-serif': SWISS, + 'script': SCRIPT, + 'serif': ROMAN, 'swiss': SWISS, - 'monospace': MODERN, - 'modern': MODERN, - 'typewriter': TELETYPE, 'teletype': TELETYPE, + 'typewriter': TELETYPE, } WEIGHTS = { 'thin': WEIGHT_THIN, @@ -83,9 +83,9 @@ def simple_parser(description): The following decoration term is accepted: 'underline' Generic Families - The following generic family terms are accepted: 'default', 'fantasy', - 'decorative', 'serif', 'roman', 'cursive', 'script', 'sans-serif', - 'swiss', 'monospace', 'modern', 'typewriter', 'teletype'. + 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 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 From ea3235389b6d1f85d9550ca3e136fb5ebad1db4e Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Wed, 27 Apr 2022 13:55:24 +0100 Subject: [PATCH 9/9] Some more small fixes coming from PR discussion. --- enable/tests/trait_defs/test_kiva_font_editor.py | 9 ++++++--- enable/trait_defs/kiva_font_trait.py | 2 +- enable/trait_defs/ui/editor_with_component.py | 10 ++++------ enable/trait_defs/ui/kiva_font_editor.py | 6 ++++-- kiva/fonttools/font.py | 4 ++-- 5 files changed, 17 insertions(+), 14 deletions(-) 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/trait_defs/kiva_font_trait.py b/enable/trait_defs/kiva_font_trait.py index fef829efe..cdcde72e1 100644 --- a/enable/trait_defs/kiva_font_trait.py +++ b/enable/trait_defs/kiva_font_trait.py @@ -83,7 +83,7 @@ class KivaFont(TraitType): #: should raise FontParseError. parser = None - def __init__(self, default_value=None, *, parser=simple_parser, **metadata): + 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) 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 93b55d78a..b11f5e9de 100644 --- a/enable/trait_defs/ui/kiva_font_editor.py +++ b/enable/trait_defs/ui/kiva_font_editor.py @@ -20,6 +20,7 @@ from .editor_with_component import EditorWithLabelComponent +#: A mapping of Kiva weight constants to strings. WEIGHTS = { kc.WEIGHT_THIN: ' Thin', kc.WEIGHT_EXTRALIGHT: ' Extra-light', @@ -83,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): @@ -113,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) @@ -121,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/kiva/fonttools/font.py b/kiva/fonttools/font.py index 26ddce35c..2db2221ee 100644 --- a/kiva/fonttools/font.py +++ b/kiva/fonttools/font.py @@ -43,7 +43,7 @@ 'light': WEIGHT_LIGHT, 'regular': WEIGHT_NORMAL, 'medium': WEIGHT_MEDIUM, - 'demi-bold': WEIGHT_SEMIBOLD, + 'semi-bold': WEIGHT_SEMIBOLD, 'bold': WEIGHT_BOLD, 'extra-bold': WEIGHT_EXTRABOLD, 'heavy': WEIGHT_HEAVY, @@ -73,7 +73,7 @@ def simple_parser(description): Weight The following weight terms are accepted: 'thin', 'extra-light', - 'light', 'regular', 'medium', 'demi-bold', 'bold', 'extra-bold', + 'light', 'regular', 'medium', 'semi-bold', 'bold', 'extra-bold', 'heavy', 'extra-heavy'. Style