class UIColourChannelEditor(UIElement):
    """
    This colour picker specific element lets us edit a single colour channel (Red, Green, Blue,
    Hue etc). It's bundled along with the colour picker class because I don't see much use for it
    outside of a colour picker, but it still seemed sensible to make a class for a pattern in the
    colour picker that is repeated six times.

    :param relative_rect: The relative rectangle for sizing and positioning the element, relative
                          to the anchors.
    :param manager: The UI manager for the UI system.
    :param name: Name for this colour channel, (e.g 'R:' or 'B:'). Used for the label.
    :param channel_index: Index for the colour channel (e.g. red is 0, blue is 1, hue is also 0,
                          saturation is 1)
    :param value_range: Range of values for this channel (0 to 255 for R,G,B - 0 to 360 for hue, 0
                        to 100 for the rest)
    :param initial_value: Starting value for this colour channel.
    :param container: UI container for this element.
    :param parent_element: An element to parent this element, used for theming hierarchies and
                           events.
    :param object_id: A specific theming/event ID for this element.
    :param anchors: A dictionary of anchors used for setting up what this element's relative_rect
                    is relative to.
    :param visible: Whether the element is visible by default. Warning - container visibility
                    may override this.
    """
    def __init__(self,
                 relative_rect: pygame.Rect,
                 manager: IUIManagerInterface,
                 name: str,
                 channel_index: int,
                 value_range: Tuple[int, int],
                 initial_value: int,
                 container: Union[IContainerLikeInterface, None] = None,
                 parent_element: UIElement = None,
                 object_id: Union[ObjectID, str, None] = None,
                 anchors: Dict[str, str] = None,
                 visible: int = 1):

        super().__init__(relative_rect,
                         manager,
                         container,
                         starting_height=1,
                         layer_thickness=1,
                         anchors=anchors,
                         visible=visible)

        self._create_valid_ids(container=container,
                               parent_element=parent_element,
                               object_id=object_id,
                               element_id='colour_channel_editor')

        self.range = value_range
        self.current_value = initial_value
        self.channel_index = channel_index

        self.set_image(self.ui_manager.get_universal_empty_surface())

        self.element_container = UIContainer(relative_rect,
                                             self.ui_manager,
                                             container=self.ui_container,
                                             parent_element=self,
                                             anchors=anchors,
                                             visible=self.visible)

        default_sizes = {
            'space_between': 3,
            'label_width': 17,
            'entry_width': 43,
            'line_height': 29,
            'slider_height': 21,
            'slider_vert_space': 4
        }

        self.label = UILabel(pygame.Rect(0, 0, -1,
                                         default_sizes['line_height']),
                             text=name,
                             manager=self.ui_manager,
                             container=self.element_container,
                             parent_element=self,
                             anchors={
                                 'left': 'left',
                                 'right': 'left',
                                 'top': 'top',
                                 'bottom': 'bottom'
                             })

        self.entry = UITextEntryLine(pygame.Rect(-default_sizes['entry_width'],
                                                 0,
                                                 default_sizes['entry_width'],
                                                 default_sizes['line_height']),
                                     manager=self.ui_manager,
                                     container=self.element_container,
                                     parent_element=self,
                                     anchors={
                                         'left': 'right',
                                         'right': 'right',
                                         'top': 'top',
                                         'bottom': 'bottom'
                                     })

        slider_width = (self.entry.rect.left - self.label.rect.right) - (
            2 * default_sizes['space_between'])

        self.slider = UIHorizontalSlider(pygame.Rect(
            (self.label.get_abs_rect().width + default_sizes['space_between']),
            default_sizes['slider_vert_space'], slider_width,
            default_sizes['slider_height']),
                                         start_value=initial_value,
                                         value_range=value_range,
                                         manager=self.ui_manager,
                                         container=self.element_container,
                                         parent_element=self,
                                         anchors={
                                             'left': 'left',
                                             'right': 'right',
                                             'top': 'top',
                                             'bottom': 'bottom'
                                         })

        self.entry.set_allowed_characters('numbers')
        self.entry.set_text(str(initial_value))
        self.entry.set_text_length_limit(3)

    def process_event(self, event: pygame.event.Event) -> bool:
        """
        Handles events that this UI element is interested in. In this case we are responding to the
        slider being moved and the user finishing entering text in the text entry element.

        :param event: The pygame Event to process.

        :return: True if event is consumed by this element and should not be passed on to other
                 elements.

        """
        consumed_event = super().process_event(event)
        if event.type == UI_TEXT_ENTRY_FINISHED and event.ui_element == self.entry:
            int_value = self.current_value
            try:
                int_value = int(self.entry.get_text())
            except ValueError:
                int_value = 0
            finally:
                self._set_value_from_entry(int_value)

        if event.type == UI_HORIZONTAL_SLIDER_MOVED and event.ui_element == self.slider:
            int_value = self.current_value
            try:
                int_value = int(self.slider.get_current_value())
            except ValueError:
                int_value = 0
            finally:
                self._set_value_from_slider(int_value)

        return consumed_event

    def _set_value_from_slider(self, new_value: int):
        """
        For updating the value in the text entry element when we've moved the slider. Also sends
        out an event for the color picker.

        :param new_value: The new value to set.

        """
        clipped_value = min(self.range[1], max(self.range[0], new_value))
        if clipped_value != self.current_value:
            self.current_value = clipped_value
            self.entry.set_text(str(self.current_value))
            # old event - to be removed in 0.8.0
            event_data = {
                'user_type': OldType(UI_COLOUR_PICKER_COLOUR_CHANNEL_CHANGED),
                'value': self.current_value,
                'channel_index': self.channel_index,
                'ui_element': self,
                'ui_object_id': self.most_specific_combined_id
            }
            pygame.event.post(pygame.event.Event(pygame.USEREVENT, event_data))

            # new event
            event_data = {
                'value': self.current_value,
                'channel_index': self.channel_index,
                'ui_element': self,
                'ui_object_id': self.most_specific_combined_id
            }
            pygame.event.post(
                pygame.event.Event(UI_COLOUR_PICKER_COLOUR_CHANNEL_CHANGED,
                                   event_data))

    def _set_value_from_entry(self, new_value: int):
        """
        For updating the value the slider element is set to when we've edited the text entry. The
        slider may have much less precision than the text entry depending on it's available width
        so we need to be careful to make the change one way. Also sends out an event for the color
        picker and clips the value to within the allowed value range.

        :param new_value: The new value to set.

        """
        clipped_value = min(self.range[1], max(self.range[0], new_value))
        if clipped_value != new_value:
            self.entry.set_text(str(clipped_value))
        if clipped_value != self.current_value:
            self.current_value = clipped_value
            self.slider.set_current_value(self.current_value)

            # old event - to be removed in 0.8.0
            event_data = {
                'user_type': OldType(UI_COLOUR_PICKER_COLOUR_CHANNEL_CHANGED),
                'value': self.current_value,
                'channel_index': self.channel_index,
                'ui_element': self,
                'ui_object_id': self.most_specific_combined_id
            }
            colour_channel_changed_event = pygame.event.Event(
                pygame.USEREVENT, event_data)
            pygame.event.post(colour_channel_changed_event)

            event_data = {
                'value': self.current_value,
                'channel_index': self.channel_index,
                'ui_element': self,
                'ui_object_id': self.most_specific_combined_id
            }
            colour_channel_changed_event = pygame.event.Event(
                UI_COLOUR_PICKER_COLOUR_CHANNEL_CHANGED, event_data)
            pygame.event.post(colour_channel_changed_event)

    def set_value(self, new_value: int):
        """
        For when we need to set the value of the colour channel from outside, usually from
        adjusting the colour elsewhere in the colour picker. Makes sure the new value is within the
        allowed range.

        :param new_value: Value to set.

        """
        clipped_value = min(self.range[1], max(self.range[0], new_value))
        if clipped_value != self.current_value:
            self.current_value = clipped_value
            self.entry.set_text(str(self.current_value))
            self.slider.set_current_value(self.current_value)

    def set_position(self, position: Union[pygame.math.Vector2,
                                           Tuple[int, int], Tuple[float,
                                                                  float]]):
        """
        Sets the absolute screen position of this channel, updating all subordinate elements at the
        same time.

        :param position: The absolute screen position to set.

        """
        super().set_position(position)
        self.element_container.set_relative_position(
            self.relative_rect.topleft)

    def set_relative_position(self, position: Union[pygame.math.Vector2,
                                                    Tuple[int, int],
                                                    Tuple[float, float]]):
        """
        Sets the relative screen position of this channel, updating all subordinate elements at the
        same time.

        :param position: The relative screen position to set.

        """
        super().set_relative_position(position)
        self.element_container.set_relative_position(
            self.relative_rect.topleft)

    def set_dimensions(self, dimensions: Union[pygame.math.Vector2,
                                               Tuple[int, int], Tuple[float,
                                                                      float]]):
        """
        Method to directly set the dimensions of an element.

        :param dimensions: The new dimensions to set.

        """
        super().set_dimensions(dimensions)
        self.element_container.set_dimensions(self.relative_rect.size)

    def show(self):
        """
        In addition to the base UIElement.show() - call show() of the element_container
        - which will propagate to the sub-elements - label, entry and slider.
        """
        super().show()

        self.element_container.show()

    def hide(self):
        """
        In addition to the base UIElement.hide() - call hide() of the element_container
        - which will propagate to the sub-elements - label, entry and slider.
        """
        super().hide()

        self.element_container.hide()
Exemple #2
0
class Chat(UIContainer):
    # region Docstring
    """
    Class to display the game chat
    """

    # endregion

    def __init__(
        self,
        size: tuple[int, int],
        manager: IUIManagerInterface,
        udp: UDP_P2P,
        username: str,
    ) -> None:
        # region Docstring
        """
        Creates a `Chat` object

        ### Arguments
            `size {(int, int)}`:
                `summary`: the size of the chat window
            `manager {UIManager}`:
                `summary`: the UIManager that manages this element.
            `udp {UDP_P2P}`:
                `summary`: the udp object
            `username {str}`:
                `summary`: the username of this host
        """
        # endregion
        super().__init__(relative_rect=Rect((Game.SIZE[0], 0), size),
                         manager=manager)

        # region Element creation

        self.textbox = UITextBox(
            html_text="",
            relative_rect=Rect((0, 0), (size[0], size[1] - 25)),
            manager=manager,
            container=self,
        )

        self.entryline = UITextEntryLine(
            relative_rect=Rect((0, size[1] - 28), (size[0] - 50, 20)),
            manager=manager,
            container=self,
        )
        self.entryline.set_text_length_limit(100)

        self.enterbtn = UIButton(
            text="Enter",
            relative_rect=Rect((size[0] - 50, size[1] - 28), (50, 30)),
            manager=manager,
            container=self,
        )

        # endregion

        self.record = ""
        self.size = size
        self.udp = udp
        self.username = username

    def process_event(self, event: Event) -> Union[bool, None]:
        # region Docstring
        """
        Overridden method to handle the gui events

        ### Arguments
            `event {Event}`:
                `summary`: the fired event

        ### Returns
            `bool | None`: return if the event has been handled
        """
        # endregion

        handled = super().process_event(event)
        if event.type != USEREVENT:
            return

        if (event.user_type == UI_TEXT_ENTRY_FINISHED and event.ui_element
                == self.entryline) or (event.user_type == UI_BUTTON_PRESSED
                                       and event.ui_element == self.enterbtn):
            self.__send()
            handled = True

        return handled

    def __send(self) -> None:
        # region Docstring
        """
        Handles the press of the enter `UIButton`, sending the user message.\n
        """
        # endregion

        if len(self.entryline.get_text().strip()) > 0:
            self.udp.transmission("CHA", "01", self.username,
                                  self.entryline.get_text().strip())
            self.__addmsg(
                f"<b>(YOU): </b><br>{self.entryline.get_text().strip()}<br>")
            self.entryline.set_text("")

    def receive(self, data: Packet, addr: Tuple[str, int],
                time: datetime) -> None:
        # region Docstring
        """
        Method that handles the received data, address and time of reception

        ### Arguments
            `data {Packet}`:
                `summary`: the data received
            `addr {Tuple[str, int]}`:
                `summary`: the source address and port
                example: (192.168.0.1, 6000)
            `time {datetime}`:
                `summary`: the time of the packet reception
        """
        # endregion

        n = UDP_P2P.latency(
            datetime.strptime(time.strftime("%H%M%S%f"), "%H%M%S%f"),
            datetime.strptime(data.time + "000", "%H%M%S%f"),
        )

        self.record += f"<b>({data.nick.strip()} - {addr[0]} - {n}ms): </b><br>{data.msg.strip()}<br>"

    def update(self, time_delta: float) -> None:
        # region Docstring
        """
        Overridden method to update the element

        ### Arguments
            `time_delta {float}`:
                `summary`: the time passed between frames, measured in seconds.
        """
        # endregion

        super().update(time_delta)
        if self.record != self.textbox.html_text:
            self.textbox.kill()
            self.textbox = UITextBox(
                html_text=self.record,
                relative_rect=Rect((0, 0), (self.size[0], self.size[1] - 25)),
                container=self,
                manager=self.ui_manager,
            )

    def __addmsg(self, msg: str) -> None:
        # region Docstring
        """
        Method to insert text in the `UITextBox` element

        ### Arguments
            `msg {str}`:
                `summary`: the text to insert
        """
        # endregion
        self.record += msg
        self.textbox.kill()
        self.textbox = UITextBox(
            html_text=self.record,
            relative_rect=Rect((0, 0), (self.size[0], self.size[1] - 25)),
            container=self,
            manager=self.ui_manager,
        )