class Window(): texture = None def __init__(self, title_text, scale, parent=None, child=None, transparent=False, owner=None): self.title_text = title_text self.scale = scale self.title_size = settings.ui_font_size self.owner = owner self.child = None self.last_pos = None self.title_color = (1, 1, 1, 1) self.title_pad = tuple(self.scale * 2) if parent is None: parent = aspect2d self.parent = parent if transparent: frameColor = (0, 0, 0, 0) else: frameColor = (0.5, 0.5, 0.5, 1) self.pad = 0 self.event_handler = DirectObject() self.button_thrower = base.buttonThrowers[0].node() self.event_handler.accept("wheel_up-up", self.mouse_wheel_event, extraArgs = [-1]) self.event_handler.accept("wheel_down-up", self.mouse_wheel_event, extraArgs = [1]) self.scrollers = [] # if Window.texture is None: # Window.texture = loader.loadTexture('textures/futureui1.png') # image_scale = (scale[0] * Window.texture.get_x_size(), 1, scale[1] * Window.texture.get_y_size()) self.frame = DirectFrame(parent=parent, state=DGG.NORMAL, frameColor=frameColor)#, image=self.texture, image_scale=image_scale) self.title_frame = DirectFrame(parent=self.frame, state=DGG.NORMAL, frameColor=(.5, .5, .5, 1)) self.title = OnscreenText(text=self.title_text, style=Plain, fg=self.title_color, scale=tuple(self.scale * self.title_size), parent=self.title_frame, pos=(0, 0), align=TextNode.ALeft, font=None, mayChange=True) bounds = self.title.getTightBounds() self.title_frame['frameSize'] = [0, bounds[1][0] - bounds[0][0] + self.title_pad[0] * 2, 0, bounds[1][2] - bounds[0][2] + self.title_pad[1] * 2] self.title.setPos( -bounds[0][0] + self.title_pad[0], -bounds[0][2] + self.title_pad[1]) self.close_frame = DirectFrame(parent=self.frame, state=DGG.NORMAL, frameColor=(.5, .5, .5, 1)) self.close = OnscreenText(text='X', style=Plain, fg=self.title_color, scale=tuple(self.scale * self.title_size), parent=self.close_frame, pos=(0, 0), align=TextNode.ACenter, font=None, mayChange=True) bounds = self.close.getTightBounds() self.close_frame['frameSize'] = [0, bounds[1][0] - bounds[0][0] + self.title_pad[0] * 2, self.title_frame['frameSize'][2], self.title_frame['frameSize'][3]] self.close.setPos( -bounds[0][0] + self.title_pad[0], -bounds[0][2] + self.title_pad[1]) self.frame.setPos(0, 0, 0) self.title_frame.bind(DGG.B1PRESS, self.start_drag) self.title_frame.bind(DGG.B1RELEASE, self.stop_drag) self.close_frame.bind(DGG.B1PRESS, self.close_window) self.set_child(child) def set_child(self, child): if child is not None: self.child = child child.reparent_to(self.frame) self.update() def update(self): if self.child is not None: frame_size = list(self.child.frame['frameSize']) if frame_size is not None: frame_size[0] -= self.pad frame_size[1] += self.pad frame_size[2] += self.pad frame_size[3] -= self.pad self.frame['frameSize'] = frame_size if self.frame['frameSize'] is not None: width = self.frame['frameSize'][1] - self.frame['frameSize'][0] title_size = self.title_frame['frameSize'] title_size[0] = 0 title_size[1] = width self.title_frame['frameSize'] = title_size self.close_frame.setPos(width - self.close_frame['frameSize'][1], 0, 0) def register_scroller(self, scroller): self.scrollers.append(scroller) def mouse_wheel_event(self, dir): # If the user is scrolling a scroll-bar, don't try to scroll the scrolled-frame too. region = base.mouseWatcherNode.getOverRegion() if region is not None: widget = base.render2d.find("**/*{0}".format(region.name)) if widget.is_empty() or isinstance(widget.node(), PGSliderBar) or isinstance(widget.getParent().node(), PGSliderBar): return # Get the mouse-position if not base.mouseWatcherNode.hasMouse(): return mouse_pos = base.mouseWatcherNode.getMouse() found_scroller = None # Determine whether any of the scrolled-frames are under the mouse-pointer for scroller in self.scrollers: bounds = scroller['frameSize'] pos = scroller.get_relative_point(base.render2d, Point3(mouse_pos.get_x() ,0, mouse_pos.get_y())) if pos.x > bounds[0] and pos.x < bounds[1] and \ pos.z > bounds[2] and pos.z < bounds[3]: found_scroller = scroller break if found_scroller is not None: if not found_scroller.verticalScroll.isHidden(): self.do_mouse_scroll(found_scroller.verticalScroll, dir, None) else: self.do_mouse_scroll(found_scroller.horizontalScroll, dir, None) def do_mouse_scroll(self, obj, dir, data): if isinstance(obj, DirectSlider) or isinstance(obj, DirectScrollBar): obj.setValue(obj.getValue() + dir * obj["pageSize"] * 0.1) def start_drag(self, event): if base.mouseWatcherNode.has_mouse(): mpos = base.mouseWatcherNode.get_mouse() self.drag_start = self.frame.parent.get_relative_point(render2d, Point3(mpos.get_x() ,0, mpos.get_y())) - self.frame.getPos() taskMgr.add(self.drag, "drag", -1) def drag(self, task): if base.mouseWatcherNode.has_mouse(): mpos = base.mouseWatcherNode.get_mouse() current_pos = self.frame.parent.get_relative_point(render2d, Point3(mpos.get_x() ,0, mpos.get_y())) self.frame.set_pos(current_pos - self.drag_start) return task.again def close_window(self, event=None): if self.owner is not None: self.owner.window_closed(self) self.destroy() def stop_drag(self, event): taskMgr.remove("drag") self.last_pos = self.frame.getPos() def destroy(self): if self.frame is not None: self.frame.destroy() self.frame = None self.scrollers = [] self.event_handler.ignore_all() def getPos(self): return self.frame.getPos() def setPos(self, pos): self.frame.setPos(pos)
class Window(): texture = None def __init__(self, title, scale, parent=None, child=None, transparent=False, owner=None): self.scale = scale self.owner = owner self.last_pos = None self.title_text = title self.title_color = (1, 1, 1, 1) self.title_pad = tuple(self.scale * 2) if parent is None: parent = aspect2d self.parent = parent if transparent: frameColor = (0, 0, 0, 0) else: frameColor = (0, 0, 0, 1) self.pad = 0 # if Window.texture is None: # Window.texture = loader.loadTexture('textures/futureui1.png') # image_scale = (scale[0] * Window.texture.get_x_size(), 1, scale[1] * Window.texture.get_y_size()) self.frame = DirectFrame( parent=parent, state=DGG.NORMAL, frameColor=frameColor ) #, image=self.texture, image_scale=image_scale) self.title_frame = DirectFrame(parent=self.frame, state=DGG.NORMAL, frameColor=(.5, .5, .5, 1)) self.title = OnscreenText(text=self.title_text, style=Plain, fg=self.title_color, scale=tuple(self.scale * 14), parent=self.title_frame, pos=(0, 0), align=TextNode.ALeft, font=None, mayChange=True) bounds = self.title.getTightBounds() self.title_frame['frameSize'] = [ 0, bounds[1][0] - bounds[0][0] + self.title_pad[0] * 2, 0, bounds[1][2] - bounds[0][2] + self.title_pad[1] * 2 ] self.title.setPos(-bounds[0][0] + self.title_pad[0], -bounds[0][2] + self.title_pad[1]) self.close_frame = DirectFrame(parent=self.frame, state=DGG.NORMAL, frameColor=(.5, .5, .5, 1)) self.close = OnscreenText(text='X', style=Plain, fg=self.title_color, scale=tuple(self.scale * 14), parent=self.close_frame, pos=(0, 0), align=TextNode.ACenter, font=None, mayChange=True) bounds = self.close.getTightBounds() self.close_frame['frameSize'] = [ 0, bounds[1][0] - bounds[0][0] + self.title_pad[0] * 2, self.title_frame['frameSize'][2], self.title_frame['frameSize'][3] ] self.close.setPos(-bounds[0][0] + self.title_pad[0], -bounds[0][2] + self.title_pad[1]) self.frame.setPos(0, 0, 0) self.title_frame.bind(DGG.B1PRESS, self.start_drag) self.title_frame.bind(DGG.B1RELEASE, self.stop_drag) self.close_frame.bind(DGG.B1PRESS, self.close_window) self.set_child(child) def set_child(self, child): if child is not None: self.child = child child.reparent_to(self.frame) self.update() def update(self): if self.child is not None: frame_size = self.child.frame['frameSize'] if frame_size is not None: frame_size[0] -= self.pad frame_size[1] += self.pad frame_size[2] += self.pad frame_size[3] -= self.pad self.frame['frameSize'] = frame_size if self.frame['frameSize'] is not None: width = self.frame['frameSize'][1] - self.frame['frameSize'][0] title_size = self.title_frame['frameSize'] title_size[0] = 0 title_size[1] = width self.title_frame['frameSize'] = title_size self.close_frame.setPos(width - self.close_frame['frameSize'][1], 0, 0) def start_drag(self, event): if base.mouseWatcherNode.has_mouse(): mpos = base.mouseWatcherNode.get_mouse() self.drag_start = self.frame.parent.get_relative_point( render2d, Point3(mpos.get_x(), 0, mpos.get_y())) - self.frame.getPos() taskMgr.add(self.drag, "drag", -1) def drag(self, task): if base.mouseWatcherNode.has_mouse(): mpos = base.mouseWatcherNode.get_mouse() current_pos = self.frame.parent.get_relative_point( render2d, Point3(mpos.get_x(), 0, mpos.get_y())) self.frame.set_pos(current_pos - self.drag_start) return task.again def close_window(self, event=None): if self.owner is not None: self.owner.window_closed(self) self.destroy() def stop_drag(self, event): taskMgr.remove("drag") self.last_pos = self.frame.getPos() def destroy(self): if self.frame is not None: self.frame.destroy() self.frame = None def getPos(self): return self.frame.getPos() def setPos(self, pos): self.frame.setPos(pos)
class ViewEditor(): __services: Services __base: ShowBase __window: Window __colour_picker: AdvancedColourPicker __elems: List[ViewElement] def __init__(self, services: Services, mouse1_press_callbacks: List[Callable[[], None]]): self.__services = services self.__services.ev_manager.register_listener(self) self.__base = self.__services.graphics.window self.__window = Window(self.__base, "view_editor", mouse1_press_callbacks, borderWidth=(0.0, 0.0), frameColor=WINDOW_BG_COLOUR, pos=(1.1, 0.5, 0.5), frameSize=(-1.1, 1.1, -5.79, 1.56)) self.__colour_picker = AdvancedColourPicker( self.__base, self.__window.frame, self.__colour_picker_callback, mouse1_press_callbacks) # spacers DirectFrame(parent=self.__window.frame, borderWidth=(.0, .0), frameColor=WIDGET_BG_COLOUR, frameSize=(-1., 1., -0.01, 0.01), pos=(0.0, 0.0, -1.75)) DirectFrame(parent=self.__window.frame, borderWidth=(.0, .0), frameColor=WIDGET_BG_COLOUR, frameSize=(-1., 1., -0.01, 0.01), pos=(0.0, 0.0, 1.1)) DirectFrame(parent=self.__window.frame, borderWidth=(.0, .0), frameColor=WIDGET_BG_COLOUR, frameSize=(-1., 1., -0.01, 0.01), pos=(0.0, 0.0, -4.1)) # ViewElements listing self.__elems_frame = DirectScrolledFrame(parent=self.__window.frame, frameColor=WINDOW_BG_COLOUR, frameSize=(-1, 1, -1.125, 1.1), pos=(0, 0, -2.9), autoHideScrollBars=True) # selected colour view frame self.__selected_cv_outline = DirectFrame( parent=self.__elems_frame.getCanvas(), relief=DGG.SUNKEN, frameColor=WHITE, borderWidth=(0.15, 0.15), frameSize=(-0.62, 0.62, -0.54, 0.54), scale=(0.18, 1.0, 0.18)) # selected view frame self.__selected_view_outline = DirectFrame(parent=self.__window.frame, relief=DGG.SUNKEN, frameColor=WHITE, borderWidth=(0.15, 0.15), frameSize=(-0.62, 0.62, -0.54, 0.54), scale=(0.3, 2.0, 0.35)) self.heading = DirectLabel(parent=self.__window.frame, text="View Editor", text_fg=WHITE, text_bg=WINDOW_BG_COLOUR, frameColor=WINDOW_BG_COLOUR, borderWidth=(.0, .0), pos=(-0.42, 0.0, 1.27), scale=(0.2, 3, 0.2)) self.__save_outline = DirectFrame(parent=self.__window.frame, frameColor=WHITE, pos=(-0.57, 0, -5.45), borderWidth=(0.25, 0.15), frameSize=(-0.62, 0.62, -0.54, 0.54), scale=(0.50, 2.1, 0.25)) self.__restore_outline = DirectFrame(parent=self.__window.frame, frameColor=WHITE, pos=(0.50, 0, -5.45), borderWidth=(0.25, 0.15), frameSize=(-0.62, 0.62, -0.54, 0.54), scale=(0.65, 2.1, 0.25)) # save and restore self.btn_s = DirectButton(text="Save", text_fg=(0.3, 0.3, 0.3, 1.0), pressEffect=1, command=self.__save, pos=(-0.575, 0, -5.5), parent=self.__window.frame, scale=(0.20, 2.1, 0.15), frameColor=TRANSPARENT) self.btn_r = DirectButton(text="Restore", text_fg=(0.3, 0.3, 0.3, 1.0), pressEffect=1, command=self.__reset, pos=(0.50, 0, -5.5), parent=self.__window.frame, scale=(0.20, 2.1, 0.15), frameColor=TRANSPARENT) # zoom window in / out self.btn_zoom_out = DirectButton(text="-", text_fg=WHITE, pressEffect=1, command=self.__window.zoom_out, pos=(0.5, 0., 1.25), parent=self.__window.frame, scale=(0.38, 4.25, 0.45), frameColor=TRANSPARENT) self.btn_zoom_in = DirectButton(text="+", text_fg=WHITE, pressEffect=1, command=self.__window.zoom_in, pos=(0.71, 0., 1.28), parent=self.__window.frame, scale=(0.35, 4.19, 0.38), frameColor=TRANSPARENT) # Quit button self.btn = DirectButton(text='x', text_fg=WHITE, command=self.__window.toggle_visible, pos=(0.91, 0.4, 1.3), parent=self.__window.frame, scale=(0.3, 2.9, 0.2), pressEffect=1, frameColor=TRANSPARENT) # Creating view selectors self.__view_selectors = [] for i in range(0, PersistentStateViews.MAX_VIEWS): num = os.path.join(GUI_DATA_PATH, str(i + 1) + ".png") self.__view_selectors.append( DirectButton(image=num, pos=(-0.7 + (i % 3) * 0.7, 0.4, -4.4 - 0.5 * (i // 3)), parent=self.__window.frame, scale=0.16, frameColor=TRANSPARENT, command=lambda v: setattr(self, "view_idx", v), extraArgs=[i])) self.__elems = [] self.__reset() def __reset(self) -> None: self.__services.state.views.restore_effective_view() ps_colours = self.__services.state.views.effective_view.colours for e in self.__elems: e.destroy() self.__elems.clear() for name, dc in ps_colours.items(): self.__elems.append( ViewElement(name, self.__update_visibility, self.__update_colour, self.__select_cv, self.__elems_frame.getCanvas(), dc.visible, dc.colour)) for i in range(len(self.__elems)): self.__elems[i].frame.set_pos((0.15, 0.0, -0.2 * i)) self.__elems_frame["canvasSize"] = (-0.95, 0.95, -0.2 * len(self.__elems) + 0.1, 0.1) self.__cur_view_idx = self.view_idx self.__selected_view_outline.set_pos( (-0.7 + (self.view_idx % 3) * 0.7, 0.4, -4.4 - 0.5 * (self.view_idx // 3))) self.__select_cv(self.__elems[0] if self.__elems else None) def __save(self) -> None: self.__services.state.views.apply_effective_view() def __update_visibility(self, name: str, visible: bool) -> None: self.__services.state.views.effective_view.colours[ name].visible = visible def __update_colour(self, name: str, colour: Colour) -> None: self.__services.state.views.effective_view.colours[ name].colour = colour def __colour_picker_callback(self, colour: Colour) -> None: self.__window.focus() if self.__selected_cv_elem is not None: self.__selected_cv_elem.colour = colour # calls self.__update_colour() def __select_cv(self, e: ViewElement): self.__window.focus() self.__selected_cv_elem = e if e is None: self.__selected_cv_outline.hide() self.__colour_picker.enabled = False else: self.__colour_picker.enabled = True self.__selected_cv_outline.show() self.__selected_cv_outline.set_pos( (-0.495, 1.0, -0.2 * self.__elems.index(e))) self.__colour_picker.colour = e.colour def notify(self, event: Event) -> None: if isinstance( event, NewColourEvent ) or self.__services.state.views.view_idx != self.__cur_view_idx: self.__reset() elif isinstance(event, ToggleViewEvent): self.__window.toggle_visible() @property def view_idx(self) -> str: return 'view_idx' @view_idx.setter def view_idx(self, idx: int) -> None: self.__services.state.views.view_idx = idx self.__reset() @view_idx.getter def view_idx(self) -> int: return self.__services.state.views.view_idx
class TransformComponent(Component): def __init__(self, entity): super().__init__(entity) self.pos_x = NetworkedFloat(0, "pos_x") self.pos_y = NetworkedFloat(0, "pos_y") self.rotation = 0 if VISUAL_DEBUGGING: self._frames_debug = [] self._position_indicators = [] ps = 0.04 self._start_interpolator = DirectFrame(pos=(0, 0, 0), frameColor=(0, 1, 0, 0.8), hpr=(0, 0, 45), frameSize=(-ps, ps, ps, -ps)) self._end_interpolator = DirectFrame(pos=(0, 0, 0), frameColor=(0, 0, 1, 0.8), hpr=(0, 0, 45), frameSize=(-ps, ps, ps, -ps)) self._start_interpolator.hide() self._end_interpolator.hide() self._server_pos = DirectFrame(pos=(0, 0, 0), frameColor=(1, 1, 1, 0.5), frameSize=(-0.05, 0.05, -0.05, 0.05)) self._server_pos.hide() def serialize(self): return {"pos": (self.pos_x.v, self.pos_y.v), "rotation": self.rotation} def load(self, data): self.pos_x.v = data["pos"][0] self.pos_y.v = data["pos"][1] self.rotation = data["rotation"] self.pos_x.updated() self.pos_y.updated() def get_changed_vars(self, keep_update_status=False): result = {} if self.pos_x.changed: result["pos_x"] = self.pos_x.v if not keep_update_status: self.pos_x.updated() if self.pos_y.changed: result["pos_y"] = self.pos_y.v if not keep_update_status: self.pos_y.updated() return result def load_delta(self, json, timestamp, index): self.pos_x.add_point(json["pos_x"], timestamp, index) self.pos_y.add_point(json["pos_y"], timestamp, index) if VISUAL_DEBUGGING: ps = 0.015 self._frames_debug.append( (DirectFrame(pos=(json["pos_x"], 0, json["pos_y"]), frameColor=(1, 0, 0, 0.45), frameSize=(-ps, ps, ps, -ps)), OnscreenText(pos=(json["pos_x"], json["pos_y"] + 0.06), text="{:6.1f}".format(timestamp * 1000.0), fg=(1, 0.3, 1, 1), scale=0.05))) def correct_delta(self, json, index): self.pos_x.correct_delta(json["pos_x"], index) self.pos_y.correct_delta(json["pos_y"], index) def correct(self, dt): self.pos_x.correct(dt) self.pos_y.correct(dt) if VISUAL_DEBUGGING: self._server_pos.show() self._server_pos.set_pos( self.pos_x._value + self.pos_x._required_correction, 0, self.pos_y._value + self.pos_y._required_correction) def save_delta(self, index): self.pos_x.save_delta(index) self.pos_y.save_delta(index) def interpolate(self, timestamp, index): pxs, pxe = self.pos_x.interpolate(timestamp, index) pys, pye = self.pos_y.interpolate(timestamp, index) if VISUAL_DEBUGGING: if pxs == 0 or pys == 0: self._start_interpolator.hide() self._end_interpolator.hide() return self._start_interpolator.show() self._end_interpolator.show() self._start_interpolator.set_pos(pxs, 0, pys) self._end_interpolator.set_pos(pxe, 0, pye) px, py = self.pos_x.v, self.pos_y.v if not self._position_indicators or abs( px - self._position_indicators[-1].get_x()) > 0.0005 or abs( py - self._position_indicators[-1].get_y()) > 0.0005: ps = 0.01 self._position_indicators.append( DirectFrame(pos=(px, 0, py), frameColor=(0, 1, 1, 0.45), frameSize=(-ps, ps, ps, -ps))) def has_changes(self): return self.pos_x.changed or self.pos_y.changed
class ColourPicker: pick_colour_callback: Callable[[Tuple[float, float, float, float]], None] __base: ShowBase __palette_img: PNMImage __palette_size: Tuple[int, int] __palette_frame: DirectFrame __marker: DirectFrame __marker_center: DirectFrame enabled: bool def __init__(self, base: ShowBase, pick_colour_callback: Callable[ [Tuple[float, float, float, float]], None], **kwargs) -> None: self.__base = base self.pick_colour_callback = pick_colour_callback self.enabled = True # PALETTE # palette_filename = os.path.join(GUI_DATA_PATH, "colour_palette.png") self.__palette_img = PNMImage( Filename.fromOsSpecific(palette_filename)) self.__palette_size = (self.__palette_img.getReadXSize(), self.__palette_img.getReadYSize()) self.__palette_frame = DirectFrame(image=palette_filename, **kwargs) self.__palette_frame['state'] = DGG.NORMAL self.__palette_frame.bind(DGG.B1PRESS, command=self.__pick) # MARKER # self.__marker = DirectFrame(parent=self.__palette_frame, frameColor=(0.0, 0.0, 0.0, 1.0), frameSize=(-0.08, .08, -.08, .08), pos=(0.0, 0.0, 0.0)) self.__marker_center = DirectFrame(parent=self.__marker, frameSize=(-0.03, 0.03, -0.03, 0.03)) self.__marker.hide() def __colour_at( self, x: float, y: float) -> Union[Tuple[float, float, float, float], None]: w, h = self.__palette_size screen = self.__base.pixel2d img_scale = self.__palette_frame['image_scale'] sx = self.__palette_frame.getSx(screen) * img_scale[0] sy = self.__palette_frame.getSz(screen) * img_scale[2] x -= self.__palette_frame.getX(screen) y -= self.__palette_frame.getZ(screen) x = (0.5 + x / (2.0 * sx)) * w y = (0.5 - y / (2.0 * sy)) * h if 0 <= x < w and 0 <= y < h: return (*self.__palette_img.getXel(int(x), int(y)), 1.0) else: return None def __update_marker_colour(self) -> Tuple[float, float, float, float]: c = self.colour_under_marker() if c is None: c = self.__marker_center['frameColor'] else: self.__marker_center['frameColor'] = c return c def __update_marker_pos(self) -> None: if not self.__base.mouseWatcherNode.hasMouse(): return None pointer = self.__base.win.get_pointer(0) x, y = pointer.getX(), -pointer.getY() w, h = self.__palette_size screen = self.__base.pixel2d img_scale = self.__palette_frame['image_scale'] sx = self.__palette_frame.getSx(screen) * img_scale[0] sy = self.__palette_frame.getSz(screen) * img_scale[2] x -= self.__palette_frame.getX(screen) y -= self.__palette_frame.getZ(screen) x /= sx y /= sy x = max(-0.92, min(0.92, x)) y = max(-0.92, min(0.92, y)) self.__marker.set_pos(x, 0.0, y) self.__marker.show() def colour_under_marker( self) -> Union[Tuple[float, float, float, float], None]: x, _, y = self.__marker.get_pos() w, h = self.__palette_size screen = self.__base.pixel2d img_scale = self.__palette_frame['image_scale'] sx = self.__palette_frame.getSx(screen) * img_scale[0] sy = self.__palette_frame.getSz(screen) * img_scale[2] x *= sx y *= sy x += self.__palette_frame.getX(screen) y += self.__palette_frame.getZ(screen) return self.__colour_at(x, y) def colour_under_mouse( self) -> Union[Tuple[float, float, float, float], None]: if not self.__base.mouseWatcherNode.hasMouse(): return None pointer = self.__base.win.get_pointer(0) return self.__colour_at(pointer.getX(), -pointer.getY()) def __pick(self, *args): if self.enabled: self.__update_marker_pos() self.pick_colour_callback(self.__update_marker_colour()) @property def frame(self) -> DirectFrame: return self.__palette_frame @property def marker(self) -> DirectFrame: return self.__marker