Skip to content

WxAgg NavigationToolbar2 coordinate display not working as expected #18561

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

Closed
jameskeaveney opened this issue Sep 24, 2020 · 4 comments
Closed
Labels

Comments

@jameskeaveney
Copy link

Bug report

Bug summary

Related to issue #18212, the coordinate display added in 3.3 is not working as expected when the toolbar is placed inside a horizontal BoxSizer, such as when adding extra wx controls to the 'empty' space to the right of the toolbar.

Code for reproduction

import wx
import matplotlib.pyplot as plt
from matplotlib.backends.backend_wxagg import NavigationToolbar2WxAgg, FigureCanvasWxAgg


class PlotFrame(wx.Frame):
    def __init__(self, parent, *args, **kwargs):
        wx.Frame.__init__(self, parent, *args, **kwargs)

        self.fig = plt.figure(figsize=(5, 4), dpi=60)
        self.subplot1 = self.fig.add_subplot(111)
        self.canvas = FigureCanvasWxAgg(self, wx.ID_ANY, self.fig)

        self.toolbar = NavigationToolbar2WxAgg(self.canvas)

        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.canvas, 1, wx.EXPAND)

        # Add more wx controls to the same area as the toolbar
        export_data_btn = wx.Button(self, label="Export Data")
        some_other_control = wx.TextCtrl(self, value="TextCtrl")

        tool_sizer = wx.BoxSizer(wx.HORIZONTAL)

        # Option 1: Allowing the toolbar to expand to fill the empty space doesn't display correctly
        tool_sizer.Add(self.toolbar, 1, wx.EXPAND)
        tool_sizer.Add((10, -1), 0, wx.EXPAND)

        # Option 2: Fixed toolbar size and expandable empty space doesn't display correctly either
        # tool_sizer.Add(self.toolbar, 0, wx.EXPAND)
        # tool_sizer.Add((10, -1), 1, wx.EXPAND)

        # doesn't matter whether these are added or not, just wanted to highlight the use case
        tool_sizer.Add(export_data_btn, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5)  
        tool_sizer.Add(some_other_control, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5)

        sizer.Add(tool_sizer, 0, wx.EXPAND)

        self.SetSizer(sizer)
        self.Layout()
        self.CenterOnScreen()

        # self.toolbar.Realize()  # trying to realize the toolbar after layout is complete also doesn't work


if __name__ == '__main__':
    app = wx.App(False)
    frame = PlotFrame(None, title='Test Frame', size=(800, 600))
    app.SetTopWindow(frame)
    frame.Center()
    frame.Show()
    app.MainLoop()

Actual outcome

The coordinate display clips off the side of the toolbar, as shown

Capture

Suggested fix

Adding a keyword argument to NavigationToolbar2WxAgg to remove the AddStretchableSpace() from the toolbar init method is one work-around, but the coordinates then appear next to the toolbar buttons. I haven't found a way to keep the coordinates right-aligned and displayed correctly.

Matplotlib version

  • Operating system: Windows 10 64-bit
  • Matplotlib version: 3.3.2
  • Matplotlib backend (print(matplotlib.get_backend())): WxAgg
  • Python version: 3.8.1 64-bit
@tacaswell tacaswell added this to the v3.3.3 milestone Sep 24, 2020
@timhoffm
Copy link
Member

Not an expert in wx, but this rather seems like an ill-defined layout. AddStretchableSpace() is only relevant in the sense that it moves content to the right side of the toolbar. The underlying problem is that the new controls are drawn on top of the toolbar. I doubt that there is anything we can do from the matplotlib side.

@jameskeaveney
Copy link
Author

I don't think the controls are drawn on top of the toolbar (although I agree this example does look like that) - the horizontal box sizer should sort out their placement next to each other. It's maybe more clear if you take option 2 from the code above (toolbar followed by expanding empty space followed by controls), then I get the following output:
Capture2

Here you see the toolbar's size, and the attempt to add the coordinate text at the end of the toolbar again. Now if for some reason the extra controls were drawn on top of the toolbar as you suggested, you would expect there to be no visible text on the toolbar, but there still is.

After quite a bit of playing around with it, I found that if the StaticText object that contains the coordinates is initialized with a fixed size then it works ok. Code to reproduce is the same as above, but swap out the line
self.toolbar = NavigationToolbar2WxAgg(self.canvas)
with
self.toolbar = NewNavigationToolbar2WxAgg(canvas, spacer='sep')

where the new class is the following

class NewNavigationToolbar2WxAgg(NavigationToolbar2WxAgg):
    def __init__(self, canvas, coordinates=True,
                      spacer='stretch', coord_width=120):
        """
        Parameters
        ----------
        canvas : FigureCanvasWxAgg
            Instance of canvas that the toolbar connects to.
        coordinates : bool
            Enable coordinate display when mouse is hovered over an axes instance in the figure.
            Defaults to True.
        spacer : str
            Option to change the default behaviour of stretching the coordinate display to the far
            right of the toolbar. Only relevant when coordinates = True.
            Valid values are 'stretch' (place coordinate in right corner) or 'sep' (add a separator then place
            coordinate text next to the toolbar buttons).
            Defaults to 'stretch'
        coord_width : int
            Width of the StaticText box showing the coordinates.
            Only relevant when coordinates=True and spacer='sep'.
            Defaults to 120.
        """
        wx.ToolBar.__init__(self, canvas.GetParent(), -1)

        if 'wxMac' in wx.PlatformInfo:
            self.SetToolBitmapSize((24, 24))
        self.wx_ids = {}
        for text, tooltip_text, image_file, callback in self.toolitems:
            if text is None:
                self.AddSeparator()
                continue
            self.wx_ids[text] = (
                self.AddTool(
                    -1,
                    bitmap=self._icon(f"{image_file}.png"),
                    bmpDisabled=wx.NullBitmap,
                    label=text, shortHelp=tooltip_text,
                    kind=(wx.ITEM_CHECK if text in ["Pan", "Zoom"]
                          else wx.ITEM_NORMAL))
                .Id)
            self.Bind(wx.EVT_TOOL, getattr(self, callback),
                      id=self.wx_ids[text])

        self._coordinates = coordinates
        if self._coordinates:
            if spacer == 'stretch':
                self.AddStretchableSpace()
                self._label_text = wx.StaticText(self)
            elif spacer == 'sep':
                self.AddSeparator()
                self._label_text = wx.StaticText(self, style=wx.ALIGN_CENTER, size=(coord_width, -1))
            else:
                raise ValueError(f"Invalid selection for spacer keyword. Should be 'stretch' or 'sep'")
            self.AddControl(self._label_text)

        self.Realize()

        NavigationToolbar2.__init__(self, canvas)
        self._idle = True
        self._prevZoomRect = None
        # for now, use alternate zoom-rectangle drawing on all
        # Macs. N.B. In future versions of wx it may be possible to
        # detect Retina displays with window.GetContentScaleFactor()
        # and/or dc.GetContentScaleFactor()
        self._retinaFix = 'wxMac' in wx.PlatformInfo

For me, this produces the following:

Capture3

@tacaswell
Copy link
Member

NavigationToolbar2Wx inherits from wx.ToolBar, which looks like it has different semantics at the wx layer than other widgets. Could you use toolbar.AddTool to inject your button and text into the toolbar rather than trying to nest the toolbar in a layout?

@jameskeaveney
Copy link
Author

jameskeaveney commented Sep 28, 2020

Ah, OK, that makes a bit more sense now. It still has issues with drawing the coordinates on top of the other controls, but that's an easy fix by removing the self.AddStretchableSpace line in the toolbar init method.

FWIW, the more general approach to add extra UI elements of any type is to use toolbar.AddControl, rather than .AddTool

I think that we should maybe add an option to place the coordinates elsewhere on the toolbar, but I'm happy to close this issue now.

Thanks for all the help and advice

Full working code, just in case its useful for anyone:

import wx
import matplotlib.pyplot as plt
from matplotlib.backends.backend_wxagg import NavigationToolbar2WxAgg, FigureCanvasWxAgg
from matplotlib.backends.backend_wx import NavigationToolbar2

class NewNavigationToolbar2WxAgg(NavigationToolbar2WxAgg):
    def __init__(self, canvas, coordinates=True, spacer='stretch'):
        """
        Parameters
        ----------
        canvas : FigureCanvasWxAgg
            Instance of canvas that the toolbar connects to.
        coordinates : bool
            Enable coordinate display when mouse is hovered over an axes instance in the figure.
            Defaults to True.
        spacer : str
            Option to change the default behaviour of stretching the coordinate display to the far
            right of the toolbar. Only relevant when coordinates = True.
            Valid values are 'stretch' (place coordinate in right corner) or 'sep' (add a separator then place
            coordinate text next to the toolbar buttons).
            Defaults to 'stretch'
        """
        wx.ToolBar.__init__(self, canvas.GetParent(), -1)

        if 'wxMac' in wx.PlatformInfo:
            self.SetToolBitmapSize((24, 24))
        self.wx_ids = {}
        for text, tooltip_text, image_file, callback in self.toolitems:
            if text is None:
                self.AddSeparator()
                continue
            self.wx_ids[text] = (
                self.AddTool(
                    -1,
                    bitmap=self._icon(f"{image_file}.png"),
                    bmpDisabled=wx.NullBitmap,
                    label=text, shortHelp=tooltip_text,
                    kind=(wx.ITEM_CHECK if text in ["Pan", "Zoom"]
                          else wx.ITEM_NORMAL))
                .Id)
            self.Bind(wx.EVT_TOOL, getattr(self, callback),
                      id=self.wx_ids[text])

        self._coordinates = coordinates
        if self._coordinates:
            if spacer == 'stretch':
                self.AddStretchableSpace()
            elif spacer == 'sep':
                self.AddSeparator()
            else:
                raise ValueError(f"Invalid selection for spacer keyword. Should be 'stretch' or 'sep'")
            self._label_text = wx.StaticText(self)  #, style=wx.ALIGN_CENTER, size=(coord_width, -1))
            self.AddControl(self._label_text)

        self.Realize()

        NavigationToolbar2.__init__(self, canvas)
        self._idle = True
        self._prevZoomRect = None
        # for now, use alternate zoom-rectangle drawing on all
        # Macs. N.B. In future versions of wx it may be possible to
        # detect Retina displays with window.GetContentScaleFactor()
        # and/or dc.GetContentScaleFactor()
        self._retinaFix = 'wxMac' in wx.PlatformInfo


class PlotFrame(wx.Frame):
    def __init__(self, parent, *args, **kwargs):
        wx.Frame.__init__(self, parent, *args, **kwargs)

        self.fig = plt.figure(figsize=(5, 4), dpi=60)
        self.subplot1 = self.fig.add_subplot(111)
        self.canvas = FigureCanvasWxAgg(self, wx.ID_ANY, self.fig)
        self.toolbar = NewNavigationToolbar2WxAgg(self.canvas, spacer='sep')

        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.canvas, 1, wx.EXPAND)

        # Add more wx controls to the same area as the toolbar
        export_data_btn = wx.Button(self.toolbar, label="Export Data")
        some_other_control = wx.TextCtrl(self.toolbar, value="TextCtrl")

        sizer.Add(self.toolbar, 0, wx.EXPAND)

        self.toolbar.AddStretchableSpace()  # right align the new controls
        self.toolbar.AddControl(export_data_btn)
        self.toolbar.AddControl(some_other_control, label="Enter text: ")
        self.toolbar.Realize()

        self.SetSizer(sizer)
        self.Layout()
        self.CenterOnScreen()


if __name__ == '__main__':
    app = wx.App(False)
    frame = PlotFrame(None, title='Test Frame', size=(800, 600))
    app.SetTopWindow(frame)
    frame.Center()
    frame.Show()
    app.MainLoop()

which produces:

Capture

@QuLogic QuLogic removed this from the v3.3.3 milestone Nov 12, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants