Ejemplo n.º 1
0
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)
Ejemplo n.º 2
0
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)
Ejemplo n.º 3
0
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
Ejemplo n.º 4
0
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
Ejemplo n.º 5
0
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