Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend button editor to allow dynamically changing button image #1566

Merged
merged 14 commits into from
Apr 26, 2021
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions traitsui/editors/button_editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ class ToolkitEditorFactory(EditorFactory):
# (Optional) Image to display on the button
image = Image

# The name of the external object trait that the button image is synced to
aaronayres35 marked this conversation as resolved.
Show resolved Hide resolved
aaronayres35 marked this conversation as resolved.
Show resolved Hide resolved
image_value = Str()

# Extra padding to add to both the left and the right sides
width_padding = Range(0, 31, 7)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# (C) Copyright 2004-2021 Enthought, Inc., Austin, TX
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE.txt and may be redistributed only under
# the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!

"""
This demo shows how to dynamically change the label or image of a button. Note
in this demo clicking the button itself does nothing.

Please refer to the `ButtonEditor API docs`_ for further information.

.. _ButtonEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.button_editor.html#traitsui.editors.button_editor.ButtonEditor
"""
from pyface.api import Image, ImageResource
from traits.api import Button, Enum, HasTraits, Instance, List, Str

from traitsui.api import (
ButtonEditor,
Group,
ImageEditor,
InstanceChoice,
InstanceEditor,
Item,
UItem,
View
)


class ImageChoice(InstanceChoice):
def get_view(self):
return View(
UItem('name', editor=ImageEditor(image=self.object))
)
Comment on lines +35 to +39
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went with the InstanceEditor approach because trying to use ImageEnumEditor was giving me trouble/ I couldn't get things working how I wanted them to.
This demo is now a little bit showcasing the secret InstanceEditor Api as well.

Also, relating to my other comment, it seems that if I had used the Image trait correctly here, this code would be un needed as the Image trait sets the editor to ImageEditor for the ImageResource that is stored.



class ButtonEditorDemo(HasTraits):
my_button = Button()

my_button_label = Str("Initial Label")

my_button_image = Image()

my_button_image_options = List(
Image(),
value=[
ImageResource("run"),
ImageResource("previous"),
ImageResource("next"),
ImageResource("parent"),
ImageResource("reload")
]
)

def _my_button_image_default(self):
return self.my_button_image_options[0]

traits_view = View(
Item(
"my_button",
style="custom",
editor=ButtonEditor(
label_value="my_button_label",
image_value="my_button_image",
orientation="horizontal"
)
),
Item("my_button_label"),
Item(
"my_button_image",
editor=InstanceEditor(
name="my_button_image_options",
adapter=ImageChoice
),
style="custom"
)
)

# Create the demo:
demo = ButtonEditorDemo()

# Run the demo (if invoked from the command line):
if __name__ == '__main__':
demo.configure_traits()
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions traitsui/examples/demo/Advanced/images/image_LICENSE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
The icons are mostly derived work from other icons. As such they are
licensed accordingly to the original license:

Project License File
----------------------------------------------------------------------------
Nuvola LGPL image_LICENSE_Nuvola.txt

Unless stated in this file, icons are the work of Enthought, and are
released under a 3 clause BSD license.

Files and orginal authors:
----------------------------------------------------------------------------
images:
next.png | Nuvola
aaronayres35 marked this conversation as resolved.
Show resolved Hide resolved
parent.png | Nuvola
previous.png | Nuvola
reload.png | Nuvola
run.png | Nuvola
aaronayres35 marked this conversation as resolved.
Show resolved Hide resolved
Binary file added traitsui/examples/demo/Advanced/images/next.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added traitsui/examples/demo/Advanced/images/parent.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added traitsui/examples/demo/Advanced/images/previous.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added traitsui/examples/demo/Advanced/images/reload.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added traitsui/examples/demo/Advanced/images/run.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 10 additions & 1 deletion traitsui/qt4/button_editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@


from pyface.qt import QtCore, QtGui
from pyface.ui_traits import Image
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if this doesn't exist in pyface.api, let's create an issue in pyface.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i dont see an issue for this in pyface - not sure if you had written this down on your personal to-do list.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It actually is available from the api, I've updated the import statement on this PR


from traits.api import Str, List, Str, on_trait_change
from traits.api import Str, List, Str, observe, on_trait_change

# FIXME: ToolkitEditorFactory is a proxy class defined here just for backward
# compatibility. The class has been moved to the
Expand Down Expand Up @@ -146,6 +147,8 @@ def update_editor(self):
class CustomEditor(SimpleEditor):
""" Custom style editor for a button, which can contain an image.
"""
#: The button image
image = Image()

#: The mapping of button styles to Qt classes.
_STYLE_MAP = {
Expand Down Expand Up @@ -173,9 +176,15 @@ def init(self, parent):
self.control.setIcon(factory.image.create_icon())

self.sync_value(self.factory.label_value, "label", "from")
self.sync_value(self.factory.image_value, "image", "from")
self.control.clicked.connect(self.update_object)
self.set_tooltip()

@observe("image")
def _image_updated(self, event):
image = event.new
self.control.setIcon(image.create_icon())

def dispose(self):
""" Disposes of the contents of an editor.
"""
Expand Down
Binary file added traitsui/tests/editors/images/enthought-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions traitsui/tests/editors/images/image_LICENSE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
The icons are mostly derived work from other icons. As such they are
licensed accordingly to the original license:

Project License File
----------------------------------------------------------------------------
Nuvola LGPL image_LICENSE_Nuvola.txt

Unless stated in this file, icons are the work of Enthought, and are
released under a 3 clause BSD license.

Files and orginal authors:
----------------------------------------------------------------------------
images:
next.png | Nuvola
aaronayres35 marked this conversation as resolved.
Show resolved Hide resolved
parent.png | Nuvola
previous.png | Nuvola
reload.png | Nuvola
run.png | Nuvola
Binary file added traitsui/tests/editors/images/next.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added traitsui/tests/editors/images/parent.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added traitsui/tests/editors/images/previous.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added traitsui/tests/editors/images/reload.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added traitsui/tests/editors/images/run.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 31 additions & 0 deletions traitsui/tests/editors/test_button_editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@

import unittest

from pyface.api import ImageResource
from pyface.ui_traits import Image
from traits.api import Bool, Button, HasTraits, List, Str
from traits.testing.api import UnittestTools

from traitsui.api import ButtonEditor, Item, UItem, View
from traitsui.tests._tools import (
BaseTestMixin,
Expand All @@ -33,6 +36,10 @@ class ButtonTextEdit(HasTraits):

play_button_label = Str("I'm a play button")

play_button_image = Image(
ImageResource(name="run")
)

values = List()

button_enabled = Bool(True)
Expand Down Expand Up @@ -60,6 +67,13 @@ class ButtonTextEdit(HasTraits):
)


custom_image_view = View(
UItem("play_button", editor=ButtonEditor(image_value="play_button_image")),
resizable=True,
style="custom",
)


@requires_toolkit([ToolkitName.qt, ToolkitName.wx])
class TestButtonEditor(BaseTestMixin, unittest.TestCase, UnittestTools):

Expand Down Expand Up @@ -145,6 +159,23 @@ def test_simple_button_editor_disabled(self):
def test_custom_button_editor_disabled(self):
self.check_button_disabled("custom")

def test_custom_image_value(self):
button_text_edit = ButtonTextEdit()

tester = UITester()
with tester.create_ui(button_text_edit, dict(view=custom_image_view)) \
as ui:
button = tester.find_by_name(ui, "play_button")
default_image = button._target.image
self.assertIsInstance(default_image, ImageResource)

button_text_edit.play_button_image = ImageResource(
name='next',
search_path='traitsui/examples/demos/Advanced/images'
aaronayres35 marked this conversation as resolved.
Show resolved Hide resolved
)
self.assertIsInstance(button._target.image, ImageResource)
self.assertIsNot(button._target.image, default_image)


@requires_toolkit([ToolkitName.qt])
class TestButtonEditorValuesTrait(BaseTestMixin, unittest.TestCase):
Expand Down
15 changes: 13 additions & 2 deletions traitsui/wx/button_editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@

import wx

from traits.api import Str
from pyface.ui_traits import Image
from traits.api import Str, observe

# FIXME: ToolkitEditorFactory is a proxy class defined here just for backward
# compatibility. The class has been moved to the
Expand Down Expand Up @@ -79,6 +80,8 @@ def dispose(self):
class CustomEditor(SimpleEditor):
""" Custom style editor for a button, which can contain an image.
"""
#: The button image
image = Image()

def init(self, parent):
""" Finishes initializing the editor by creating the underlying toolkit
Expand Down Expand Up @@ -106,9 +109,17 @@ def init(self, parent):
self.update_object, "clicked", dispatch="ui"
)
self.sync_value(self.factory.label_value, "label", "from")

self.sync_value(self.factory.image_value, "image", "from")
self.set_tooltip()

def _label_changed(self, label):
self._control.label = self.string_value(label)

@observe("image")
def _image_updated(self, event):
image = event.new
self._control.image = image
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem right - should it be image.get_icon() or image.get_image() or something like that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So for Qt, image.create_icon() returns a QtGui.QIcon which can then be passed as the argument of the setIcon method on the control.

In this case on wx, the _control is a pyface.ui.wx.image_button.ImageButton and control is the control of that (ie self.control = self._control.control) which is a wx.Window. Neither of which have "icon" related methods, etc. ImageButton simply has an image = Instance(ImageResource, allow_none=True) trait, and so I set it directly to the ImageResource instance from the image trait here.

I agree it seems incorrect, and in fact it ends up working not exactly right, see the following screenshot:
Screen Shot 2021-04-20 at 9 22 20 AM

ImageButton has an _image_changed method which sets

self._img = image.create_image()
self._image = self._img.ConvertToBitmap()

which seems like maybe we would want to use create_icon instead or something? I am not sure I was unable to figure this out. There is code in ImageButton.__init__ method which looks as though it is trying to figure out the size of the image and the size of the label (and that seems to work correctly - as in the above image looks as though it has the perfect button size to fit the image and label side by side). However, it seems both the image and label are getting drawn at the center which likely isn't intentional (?)
I suspect this is might be a problem in Imagebutton._on_paint

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW: I tried setting orientation="horizontal" and now I see this

Screen Shot 2021-04-20 at 9 48 37 AM

I think we may be using some calculated layout value in the wrong place or something of the sort. Looking into it now

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also don't know how I'm just seeing this, but the dynamic label isn't working either!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The label can be fixed by overwriting the _label_changed method in the traitsui.wx.button_editor.CustomEditor class to be self._control.label = self.string_value(label).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay I have successfully fixed this locally.

Screen Shot 2021-04-20 at 10 15 22 AM

I will push the changes to this PR and open a separate PR in pyface

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've opened enthought/pyface#932


def dispose(self):
""" Disposes of the contents of an editor.
"""
Expand Down