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()
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, )