"""
Contains the identity swap dialog. Displayed in the stripped down version of DIPLOMAT's UI (shown when running
:cli:`diplomat tweak`).
"""
import wx
from typing import List, Iterable, Tuple
from dataclasses import dataclass
from diplomat.wx_gui.point_edit import WxDotShapeDrawer
[docs]
class IdSwapDialog(wx.Dialog):
"""
Identity swap dialog. Provides a dialog which can get reordered parts and individuals.
"""
[docs]
def __init__(
self,
*args,
num_outputs: int,
labels: List[str],
colors: List[Tuple[int, int, int, int]],
shapes: List[str],
**kwargs,
):
super().__init__(*args, **kwargs)
self.SetWindowStyle(self.GetWindowStyle() | wx.RESIZE_BORDER)
self._num_outputs = num_outputs
self._labels = labels
self._outer_sizer = wx.BoxSizer(wx.VERTICAL)
self._scroller = wx.ScrolledWindow(
self, wx.ID_ANY, size=wx.Size(200, 200), style=wx.VSCROLL
)
self._outer_sizer.Add(self._scroller, 1, wx.ALL | wx.EXPAND)
self._sizer = wx.BoxSizer(wx.VERTICAL)
# Build the dialog....
self._individuals = DragZone(
num_outputs, labels, shapes, colors, parent=self._scroller, id=wx.ID_ANY
)
self._sizer.Add(self._individuals, 1, wx.EXPAND | wx.CENTER)
self._btn_sizer = self.CreateButtonSizer(wx.OK | wx.CANCEL)
if self._btn_sizer is not None:
self._outer_sizer.Add(self._btn_sizer, 0, wx.ALL | wx.EXPAND)
self._scroller.SetSizer(self._sizer)
self._scroller.EnableScrolling(True, True)
self._scroller.SetScrollbars(1, 1, 1, 1)
self.SetSizerAndFit(self._outer_sizer)
def get_proposed_order(self) -> List[int]:
return [part.index for part in self._individuals.iter_parts()]
_PADDING = 3
[docs]
@dataclass
class Part:
x: int
y: int
index: int
name: str
shape: str
color: Tuple[int, int, int, int]
def __post_init__(self):
self.dragging = False
self.hover = False
self.highlight = False
self.background_color = (0, 0, 0, 0)
def draw(self, dc: wx.DC, canvas_width: int, canvas_height: int):
w, h = dc.GetTextExtent(self.name)
highlight_color = wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHT)
background_color = (
highlight_color
if (self.dragging or self.highlight)
else wx.Colour(*self.background_color)
)
dc.SetBrush(wx.Brush(background_color))
border_color = (
wx.SYS_COLOUR_BTNSHADOW
if (not self.hover or self.dragging)
else wx.SYS_COLOUR_HIGHLIGHT
)
dc.SetPen(
wx.Pen(wx.SystemSettings.GetColour(border_color), 1, wx.PENSTYLE_SOLID)
)
dc.DrawRoundedRectangle(self.x, self.y, w + int(h * 2.5), h * 2, _PADDING)
dc.DrawText(self.name, self.x + h * 2, self.y + h // 2)
dc.SetBrush(wx.Brush(wx.Colour(*self.color), wx.BRUSHSTYLE_SOLID))
dc.SetPen(wx.TRANSPARENT_PEN)
WxDotShapeDrawer(dc)[self.shape](self.x + h, self.y + h, h // 2)
def size(self, dc: wx.DC, canvas_width: int, canvas_height: int) -> Tuple[int, int]:
w, h = dc.GetTextExtent(self.name)
return w + int(h * 2.5), h * 2
def get_mouseover(
self, dc: wx.DC, canvas_width: int, canvas_height: int, mx: int, my: int
):
w, h = self.size(dc, canvas_width, canvas_height)
if (self.x <= mx <= self.x + w) and (self.y <= my <= self.y + h):
return self
return None
[docs]
@dataclass
class Body:
x: int
y: int
name: str
shape: str
parts: List[Part]
def __post_init__(self):
self.dragging = False
self.hover = False
self.highlight = False
self.background_color = (0, 0, 0, 0)
def draw(self, dc: wx.DC, canvas_width: int, canvas_height: int):
w, h = dc.GetTextExtent(self.name)
x_cur, y_cur = self.x + _PADDING, self.y + _PADDING + h * 2
h_jump = 0
# Part layout engine code...
for part in self.parts:
pw, ph = part.size(dc, canvas_width, canvas_height)
if (x_cur + pw + _PADDING) > (self.x + canvas_width):
y_cur += h_jump + _PADDING
x_cur = self.x + _PADDING
if not part.dragging:
part.x = x_cur
part.y = y_cur
h_jump = max(h_jump, ph)
x_cur += pw + _PADDING
highlight_color = wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHT)
background_color = (
highlight_color
if (self.dragging or self.highlight)
else wx.Colour(*self.background_color)
)
dc.SetBrush(wx.Brush(background_color))
border_color = (
wx.SYS_COLOUR_BTNSHADOW
if (not self.hover or self.dragging)
else wx.SYS_COLOUR_HIGHLIGHT
)
dc.SetPen(
wx.Pen(wx.SystemSettings.GetColour(border_color), 1, wx.PENSTYLE_SOLID)
)
dc.DrawRoundedRectangle(
self.x, self.y, canvas_width, y_cur + h_jump + _PADDING - self.y, _PADDING
)
dc.DrawText(self.name, self.x + canvas_width // 2 - w // 2, self.y + h // 2)
dc.SetBrush(wx.Brush(dc.GetTextForeground(), wx.BRUSHSTYLE_SOLID))
dc.SetPen(wx.TRANSPARENT_PEN)
WxDotShapeDrawer(dc)[self.shape](
self.x + canvas_width // 2 - w // 2 - h // 2 - _PADDING, self.y + h, h // 2
)
for part in sorted(self.parts, key=lambda a: a.dragging):
part.draw(dc, canvas_width, canvas_height)
def size(self, dc: wx.DC, canvas_width: int, canvas_height: int):
w, h = dc.GetTextExtent(self.name)
x_cur, y_cur = _PADDING, _PADDING
h_jump = 0
for part in self.parts:
pw, ph = part.size(dc, canvas_width, canvas_height)
if (x_cur + pw + _PADDING) > canvas_width:
y_cur += h_jump + _PADDING
x_cur = _PADDING
h_jump = max(h_jump, ph)
x_cur += pw + _PADDING
return canvas_width, y_cur + h * 2 + h_jump + _PADDING
def get_mouseover(
self, dc: wx.DC, canvas_width: int, canvas_height: int, mx: int, my: int
):
for part in self.parts:
obj = part.get_mouseover(dc, canvas_width, canvas_height, mx, my)
if obj is not None:
return obj
w, h = self.size(dc, canvas_width, canvas_height)
if (self.x <= mx <= self.x + w) and (self.y <= my <= self.y + h):
return self
return None
[docs]
class DragZone(wx.Control):
[docs]
def __init__(
self,
num_outputs: int,
parts: List[str],
shapes: List[str],
colors: List,
*args,
**kwargs,
):
super().__init__(*args, **kwargs)
self._bodies = []
self._part_count = len(parts)
self._num_outputs = num_outputs
self._pressed_obj = None
self._owner_body = None
self._hover_obj = None
self._highlight_obj = None
self._press_offset = (0, 0)
self._offset_shapes = []
for i in range(num_outputs):
body = Body(0, 0, f"Body {i}", shapes[i], [])
self._offset_shapes.append(shapes[i])
body.background_color = self.GetBackgroundColour()
for j in range(i, len(parts), num_outputs):
part = Part(0, 0, j, parts[j], shapes[j], colors[j])
part.background_color = self.GetBackgroundColour()
body.parts.append(part)
self._bodies.append(body)
self.SetMinSize(self._get_min_size(200, 200))
self.Bind(wx.EVT_PAINT, self._on_paint)
self.Bind(wx.EVT_LEFT_DOWN, self._on_press)
self.Bind(wx.EVT_LEFT_UP, self._on_release)
self.Bind(wx.EVT_MOTION, self._on_move)
self.Bind(wx.EVT_SIZE, self._on_size)
def iter_parts(self) -> Iterable[Part]:
for j in range(self._part_count // self._num_outputs):
for i in range(self._num_outputs):
yield self._bodies[i].parts[j]
def _on_paint(self, evt: wx.PaintEvent):
dc = wx.PaintDC(self)
self._on_draw(dc)
def _on_size(self, evt: wx.SizeEvent):
w, h = self._get_min_size(*evt.GetSize())
self.SetSizeHints(w, h)
def _get_min_size(self, w, h):
dc = wx.ClientDC(self)
new_h = _PADDING
for body in self._bodies:
bw, bh = body.size(dc, *self._get_internal_size(w, h))
new_h += bh + _PADDING
return w, new_h
def _get_internal_size(self, w: int, h: int) -> Tuple[int, int]:
__, fh = self.GetFont().GetPixelSize()
return (w - _PADDING * 4 - fh, h - _PADDING * 2)
def _on_draw(self, dc: wx.DC):
width, height = self.GetClientSize()
y_off = _PADDING
__, fh = self.GetFont().GetPixelSize()
y_info = []
for body in self._bodies:
if not body.dragging:
body.x = _PADDING
body.y = y_off
bw, bh = body.size(dc, *self._get_internal_size(width, height))
y_info.append((y_off, bh))
y_off += bh + _PADDING
for (by_off, bh), orig_shape in zip(y_info, self._offset_shapes):
dc.SetPen(wx.Pen(dc.GetTextForeground(), _PADDING // 2, wx.PENSTYLE_SOLID))
dc.DrawLine(
width - fh - _PADDING * 2,
by_off + _PADDING,
width - fh - _PADDING * 2,
by_off + bh - _PADDING * 2,
)
dc.SetPen(wx.TRANSPARENT_PEN)
dc.SetBrush(wx.Brush(dc.GetTextForeground(), wx.BRUSHSTYLE_SOLID))
WxDotShapeDrawer(dc)[orig_shape](
width - fh // 2 - _PADDING, by_off + bh // 2, fh // 2
)
for body in sorted(
self._bodies, key=lambda a: a.dragging or any(b.dragging for b in a.parts)
):
body.draw(dc, *self._get_internal_size(width, height))
def _on_press(self, evt: wx.MouseEvent):
if self._pressed_obj is None:
dc = wx.ClientDC(self)
w, h = self.GetClientSize()
mx, my = evt.GetPosition()
for body in self._bodies:
res = body.get_mouseover(dc, *self._get_internal_size(w, h), mx, my)
if res is not None:
self._pressed_obj = res
self._owner_body = body
self._press_offset = (mx - res.x, my - res.y)
res.dragging = True
def _handle_drag(
self, dc: wx.DC, w: int, h: int, x: int, y: int, is_release: bool = False
):
if self._highlight_obj is not None:
self._highlight_obj.highlight = False
self._highlight_obj = None
if isinstance(self._pressed_obj, Part):
for body in self._bodies:
res = body.get_mouseover(dc, *self._get_internal_size(w, h), x, y)
if res is not None and res is not self._pressed_obj:
index = self._pressed_obj.index // self._num_outputs
if is_release:
self._owner_body.parts[index] = body.parts[index]
body.parts[index] = self._pressed_obj
else:
body.parts[index].highlight = True
self._highlight_obj = body.parts[index]
return
elif isinstance(self._pressed_obj, Body):
body = None
i = 0
for i, body in enumerate(self._bodies):
if body is self._pressed_obj:
continue
h = body.size(dc, *self._get_internal_size(w, h))[1]
end_h = body.y + h
if end_h >= y:
break
if body is None or body.y > y:
return
if is_release:
j = self._bodies.index(self._pressed_obj)
self._bodies[j] = self._bodies[i]
self._bodies[i] = self._pressed_obj
else:
self._bodies[i].highlight = True
self._highlight_obj = self._bodies[i]
def _on_release(self, evt: wx.MouseEvent):
if self._pressed_obj is not None:
dc = wx.ClientDC(self)
w, h = self.GetClientSize()
x, y = evt.GetPosition()
self._handle_drag(dc, w, h, x, y, True)
self._pressed_obj.dragging = False
self._pressed_obj = None
self._owner_body = None
if self._highlight_obj is not None:
self._highlight_obj.highlight = False
self._highlight_obj = None
self.Refresh()
def _on_move(self, evt: wx.MouseEvent):
old_hover = self._hover_obj
mx, my = evt.GetPosition()
dc = wx.ClientDC(self)
w, h = self.GetClientSize()
if self._pressed_obj is not None:
if self._hover_obj is not None:
self._hover_obj.hover = False
self._hover_obj = None
self._pressed_obj.x = mx - self._press_offset[0]
self._pressed_obj.y = my - self._press_offset[1]
self._handle_drag(dc, w, h, mx, my)
else:
for body in self._bodies:
res = body.get_mouseover(dc, *self._get_internal_size(w, h), mx, my)
if res is not None:
if self._hover_obj is not None:
self._hover_obj.hover = False
self._hover_obj = res
res.hover = True
break
else:
if self._hover_obj is not None:
self._hover_obj.hover = False
self._hover_obj = None
if self._pressed_obj is not None or old_hover is not self._hover_obj:
self.Refresh()
def _main():
from diplomat.utils.colormaps import DiplomatColormap
app = wx.App()
dlg = IdSwapDialog(
None,
wx.ID_ANY,
num_outputs=3,
labels=[
"Nose 1",
"Nose 2",
"Nose 3",
"Back 1",
"Back 2",
"Back 3",
"Tail 1",
"Tail 2",
"Tail 3",
],
colors=[
tuple(int(v * 255) for v in DiplomatColormap.to_rgba_optional(c))
for c in [
"red",
"red",
"red",
"green",
"green",
"green",
"blue",
"blue",
"blue",
]
],
shapes=[
"circle",
"triangle",
"square",
"circle",
"triangle",
"square",
"circle",
"triangle",
"square",
],
)
with dlg as dlg:
if dlg.ShowModal() == wx.ID_OK:
print(dlg.get_proposed_order())
print([dlg._labels[i] for i in dlg.get_proposed_order()])
else:
print("Canceled...")
if __name__ == "__main__":
_main()