OK, so it looks like I have it all working.
The code below is 239 lines long a bit much, but it has everything in it. My actual code will be split up between different files.
from itertools import chain
import wx
class GBSRowSwapping:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def gbs_swap_rows(self, gbs, row0, row1, flag=0, border=0):
"""
Swap any two rows in a GridBagSizer keeping most parameters.
"""
rows = gbs.GetRows()
cols = gbs.GetCols()
assert row0 != row1, (f"row0 ({row0}) and row1 ({row1}) cannot "
"be the same.")
assert -1 < row0 < rows, ("The row0 value is invalid can only be "
f"between 0 and {rows-1}, found {row0}")
assert -1 < row1 < rows, ("The row1 value is invalid can only be "
f"between 0 and {rows-1}, found {row1}")
assert -1 < cols, f"The number of columns must be >= 0, found {cols}."
# Get GBS positions.
positions = [[(r, c) for c in range(cols)] for r in (row0, row1)]
# Remove the GBSizerItem in both rows.
sizer_items = [[gbs.FindItemAtPosition(rc) for rc in row]
for row in positions]
# Get list of windows (widgets).
windows = [[(item.GetWindow(), item.GetSpan()) for item in row]
for row in sizer_items]
# Remove GBSizerItem in both rows.
[gbs.Remove(self.get_sizer_item_index(gbs, item))
for item in list(chain(*sizer_items)) if item]
# Add the widgets objects to the GridBagSizer with swapped positions.
[[gbs.Add(item[0], rpos[idx], item[1], flag=flag, border=border)
for idx, item in enumerate(windows[row])]
for row, rpos in enumerate(reversed(positions))]
def get_sizer_item_index(self, sizer, item):
"""
Determines the index of an item in a sizer.
:params sizer: The sizer to search.
:type sizer: wx.Sizer
:param item: The item to find.
:type item: wx.SizerItem
:returns: The index of the item in the sizer, or -1 if the item
is not in the sizer.
:rtype: int
"""
index = -1
for idx, child in enumerate(sizer.GetChildren()):
if child == item:
index = idx
break
return index
def highlight_row(self, gbs, row, color=None):
if row is not None and color:
cols = gbs.GetCols()
positions = [(row, c) for c in range(cols)]
widgets = [gbs.FindItemAtPosition(pos).GetWindow()
for pos in positions]
[w.SetBackgroundColour(color) for w in widgets]
class _ClickPosition:
"""
A borg pattern to hold new widget type ID.
"""
_shared_state = {}
_new_types = {}
def __init__(self):
self.__dict__ = self._shared_state
def get_new_event_type(self, w_name):
return self._new_types.setdefault(w_name, wx.NewEventType())
def get_click_position(self, w_name):
assert w_name in self._new_types, ("The 'get_new_event_type' must "
"be called first.")
return wx.PyEventBinder(self._new_types[w_name], 1)
class WidgetEvent(wx.PyCommandEvent):
"""
For some reason wx.PyCommandEvent screws up the use of properties,
bummer.
"""
def __init__(self, evt_type, id):
super().__init__(evt_type, id)
self.__value = None
def get_value(self):
return self.__value
def set_value(self, value):
self.__value = value
class EventStaticText(wx.StaticText):
__type_name = 'event_static_text'
_cp = _ClickPosition()
_type_id = _cp.get_new_event_type(__type_name)
def __init__(self, parent=None, id=wx.ID_ANY, label="",
pos=wx.DefaultPosition, size=wx.DefaultSize,
style=0, name=wx.StaticTextNameStr):
super().__init__(parent=parent, id=id, label=label, pos=pos,
size=size, style=style, name=name)
self.Bind(wx.EVT_LEFT_DOWN, self.on_left_down)
@property
def new_event_type(self):
return self._cp.get_new_event_type(self.__type_name)
@property
def event_click_position(self):
return self._cp.get_click_position(self.__type_name)
def on_left_down(self, event):
obj = event.GetEventObject()
sizer = obj.GetContainingSizer()
pos = None
if isinstance(sizer, wx.GridBagSizer):
item = sizer.FindItem(obj)
pos = item.GetPos()
elif isinstance(sizer, wx.BoxSizer):
item = sizer.GetItem(obj)
pos = item.GetPosition()
evt = WidgetEvent(self._type_id, self.GetId())
evt.set_value(pos)
self.GetEventHandler().ProcessEvent(evt)
event.Skip()
class MyFrame(GBSRowSwapping, wx.Frame):
__previous_row = None
__cl = None
def __init__(self, parent, *args, **kwargs):
super().__init__(parent, *args, **kwargs)
# Create the sizer objects.
sizer = wx.BoxSizer(wx.VERTICAL)
self.SetSizer(sizer)
bg_color = wx.Colour(*(128, 128, 128))
self.SetBackgroundColour(bg_color)
button = wx.SpinCtrl(self, wx.ID_ANY, "")
sizer.Add(button, 0, wx.CENTER | wx.ALL, 0)
grid_bag_sizer = wx.GridBagSizer()
sizer.Add(grid_bag_sizer, 0, wx.CENTER | wx.ALL, 6)
self.Bind(wx.EVT_SPINCTRL,
self.swap_rows_closure(grid_bag_sizer, bg_color))
# Add the widgets to the GridBagSizer.
self.create_widgets(grid_bag_sizer, button, bg_color)
button.SetRange(0, grid_bag_sizer.GetRows()-1)
self.Show()
def create_widgets(self, gbs, sb, bg_color):
num = -1
for idx in range(15):
dec = idx % 3
if not dec: num += 1
label = f"Widget {num}.{dec}"
widget = EventStaticText(self, -1, label, style=0)
widget.SetBackgroundColour(bg_color)
pos, span = (num, dec), (1, 1)
gbs.Add(widget, pos, span, wx.ALIGN_CENTER | wx.ALL, 6)
self.Bind(widget.event_click_position,
self.test_event_closure(gbs, sb, bg_color),
id=widget.GetId())
def swap_rows_closure(self, gbs, orig_color):
"""
Event to swap the two rows.
"""
def swap_rows(event):
if self.__previous_row is not None:
row0 = self.__previous_row
obj = event.GetEventObject()
row1 = obj.GetValue()
self.__previous_row = row1
if row0 != row1:
self.stop_call_later()
self.gbs_swap_rows(gbs, row0, row1,
wx.ALIGN_CENTER | wx.ALL, 6)
self.Layout()
self.__cl = wx.CallLater(4000, self.turn_off_highlight,
gbs, orig_color)
return swap_rows
def test_event_closure(self, gbs, sb, orig_color=None, color='blue'):
"""
Event to highlight the GBS row when a widget is clicked.
"""
def test_event(event):
self.stop_call_later()
pos = event.get_value()
row, col = pos
self.highlight_row(gbs, self.__previous_row, color=orig_color)
self.highlight_row(gbs, row, color=color)
self.__previous_row = row
sb.SetValue(row)
self.__cl = wx.CallLater(4000, self.turn_off_highlight,
gbs, orig_color)
return test_event
def turn_off_highlight(self, gbs, orig_color):
for row in range(gbs.GetRows()):
self.__previous_row = None
self.highlight_row(gbs, row, color=orig_color)
self.Layout()
def stop_call_later(self):
if self.__cl and self.__cl.IsRunning():
self.__cl.Stop()
self.__cl = None
if __name__ == "__main__":
app = wx.App()
frame = MyFrame(None, title="Swap Widgets in a GridBagSizer")
app.MainLoop()