Source code for diplomat.wx_gui.id_swap_dialog

"""
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 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 to_rgba 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 to_rgba(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()