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..aaf5553ed --- /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 + + +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..3e388cdad 100644 --- a/enable/label.py +++ b/enable/label.py @@ -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..50f3362fe --- /dev/null +++ b/enable/tests/trait_defs/test_kiva_font_editor.py @@ -0,0 +1,180 @@ +# (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 traits.api import 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 skip_if_null + + +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..25f8af473 --- /dev/null +++ b/enable/trait_defs/ui/editor_with_component.py @@ -0,0 +1,134 @@ +# (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): + # if the toolkit is "null" make these at least instantiatable + 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 springy 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..3878798aa --- /dev/null +++ b/enable/trait_defs/ui/kiva_font_editor.py @@ -0,0 +1,140 @@ +# (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): + """Initialize the editor. + + The Label font should match the value for a font editor. + """ + self.font = self.value + super().init(parent) + + def update_editor(self): + """Handle the content of the editor changing.""" + self.font = self.value + super().update_editor() + + def string_value(self, value, format_func=None): + """Get a string value to display in the editor. + + If the factory provides sample text, use that, otherwise follow the + usual path, but default to using the `str_font` function. + """ + 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): + """Create and configure the Label component.""" + 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): + """Display a Pyface FontDialog when the button tool is clicked.""" + 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