class SpeakSpell(pygame_gui.elements.UIWindow): speakthrd = None def __init__(self, pos, manager): super().__init__( pygame.Rect(pos, (400,128)), manager=manager, window_display_title='speaknspell', object_id='#speaknspell' ) self.label = UILabel( relative_rect=pygame.Rect(-20, 10, 400, 20), text='', manager=manager, container=self ) self.input = UITextEntryLine( relative_rect=pygame.Rect(0, 40, 368, 30), manager=manager, container=self ) self.engine = pyttsx3.init() self.engine.setProperty('rate', 150) self.speakthrd = None self.speak('Hello, thank you for using snakeware!') def speak(self, text): if self.speakthrd is not None and self.speakthrd.is_alive(): return if text == '': return self.engine.say(text) self.speakthrd = threading.Thread(target=self.engine.runAndWait, args=()) self.speakthrd.start() self.label.set_text(text) self.input.set_text('') def process_event(self, event): super().process_event(event) if event.type == pygame.USEREVENT and event.ui_element == self.input: if event.user_type == pygame_gui.UI_TEXT_ENTRY_FINISHED: self.speak(self.input.get_text())
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 UIFileDialog(UIWindow): """ A dialog window for handling file selection operations. The dialog will let you pick a file from a file system but won't do anything with it once you have, the path will just be returned leaving it up to the rest of the application to decide what to do with it. :param rect: The size and position of the file dialog window. Includes the size of shadow, border and title bar. :param manager: The manager for the whole of the UI. :param window_title: The title for the window, defaults to 'File Dialog' :param initial_file_path: The initial path to open the file dialog at. :param object_id: The object ID for the window, used for theming - defaults to '#file_dialog' :param visible: Whether the element is visible by default. """ def __init__(self, rect: pygame.Rect, manager: IUIManagerInterface, window_title: str = 'File Dialog', initial_file_path: Union[str, None] = None, object_id: Union[ObjectID, str] = ObjectID('#file_dialog', None), allow_existing_files_only: bool = False, allow_picking_directories: bool = False, visible: int = 1): super().__init__(rect, manager, window_display_title=window_title, object_id=object_id, resizable=True, visible=visible) minimum_dimensions = (260, 300) if rect.width < minimum_dimensions[ 0] or rect.height < minimum_dimensions[1]: warn_string = ("Initial size: " + str(rect.size) + " is less than minimum dimensions: " + str(minimum_dimensions)) warnings.warn(warn_string, UserWarning) self.set_minimum_dimensions(minimum_dimensions) self.allow_existing_files_only = allow_existing_files_only self.allow_picking_directories = allow_picking_directories self.delete_confirmation_dialog = None # type: Union[UIConfirmationDialog, None] self.current_file_path = None # type: Union[Path, None] if initial_file_path is not None: pathed_initial_file_path = Path(initial_file_path) if pathed_initial_file_path.exists( ) and not pathed_initial_file_path.is_file(): self.current_directory_path = str( pathed_initial_file_path.resolve()) if self.allow_picking_directories: self.current_file_path = self.current_directory_path elif pathed_initial_file_path.exists( ) and pathed_initial_file_path.is_file(): self.current_file_path = pathed_initial_file_path.resolve() self.current_directory_path = str( pathed_initial_file_path.parent.resolve()) elif pathed_initial_file_path.parent.exists(): self.current_directory_path = str( pathed_initial_file_path.parent.resolve()) self.current_file_path = ( Path(initial_file_path).parent.resolve() / Path(initial_file_path).name) else: self.current_directory_path = str(Path('.').resolve()) self.last_valid_directory_path = self.current_directory_path self.current_file_list = None # type: Union[List[str], None] self.update_current_file_list() self.ok_button = UIButton(relative_rect=pygame.Rect( -220, -40, 100, 30), text='OK', manager=self.ui_manager, container=self, object_id='#ok_button', anchors={ 'left': 'right', 'right': 'right', 'top': 'bottom', 'bottom': 'bottom' }) if not self._validate_file_path(self.current_file_path): self.ok_button.disable() self.cancel_button = UIButton(relative_rect=pygame.Rect( -110, -40, 100, 30), text='Cancel', manager=self.ui_manager, container=self, object_id='#cancel_button', anchors={ 'left': 'right', 'right': 'right', 'top': 'bottom', 'bottom': 'bottom' }) self.home_button = UIButton(relative_rect=pygame.Rect(10, 10, 20, 20), text='⌂', tool_tip_text='Home Directory', manager=self.ui_manager, container=self, object_id='#home_icon_button', anchors={ 'left': 'left', 'right': 'left', 'top': 'top', 'bottom': 'top' }) self.delete_button = UIButton(relative_rect=pygame.Rect( 32, 10, 20, 20), text='⌧', tool_tip_text='Delete', manager=self.ui_manager, container=self, object_id='#delete_icon_button', anchors={ 'left': 'left', 'right': 'left', 'top': 'top', 'bottom': 'top' }) if not self._validate_path_exists_and_of_allowed_type( self.current_file_path, allow_directories=False): self.delete_button.disable() self.parent_directory_button = UIButton( relative_rect=pygame.Rect(54, 10, 20, 20), text='↑', tool_tip_text='Parent Directory', manager=self.ui_manager, container=self, object_id='#parent_icon_button', anchors={ 'left': 'left', 'right': 'left', 'top': 'top', 'bottom': 'top' }) self.refresh_button = UIButton(relative_rect=pygame.Rect( 76, 10, 20, 20), text='⇪', tool_tip_text='Refresh Directory', manager=self.ui_manager, container=self, object_id='#refresh_icon_button', anchors={ 'left': 'left', 'right': 'left', 'top': 'top', 'bottom': 'top' }) text_line_rect = pygame.Rect(10, 40, self.get_container().get_size()[0] - 20, 25) self.file_path_text_line = UITextEntryLine( relative_rect=text_line_rect, manager=self.ui_manager, container=self, object_id='#file_path_text_line', anchors={ 'left': 'left', 'right': 'right', 'top': 'top', 'bottom': 'top' }) if self.current_file_path is not None: self.file_path_text_line.set_text(str(self.current_file_path)) self._highlight_file_name_for_editing() else: self.file_path_text_line.set_text(str(self.current_directory_path)) file_selection_rect = pygame.Rect( 10, 80, self.get_container().get_size()[0] - 20, self.get_container().get_size()[1] - 130) self.file_selection_list = UISelectionList( relative_rect=file_selection_rect, item_list=self.current_file_list, manager=self.ui_manager, container=self, object_id='#file_display_list', anchors={ 'left': 'left', 'right': 'right', 'top': 'top', 'bottom': 'bottom' }) def _highlight_file_name_for_editing(self): # try highlighting the file name if self.current_file_path is None or self.allow_existing_files_only: return highlight_start = self.file_path_text_line.get_text().find( self.current_file_path.stem) highlight_end = highlight_start + len(self.current_file_path.stem) self.file_path_text_line.select_range[0] = highlight_start self.file_path_text_line.select_range[1] = highlight_end self.file_path_text_line.cursor_has_moved_recently = True self.file_path_text_line.edit_position = highlight_end text_clip_width = (self.file_path_text_line.rect.width - (self.file_path_text_line.padding[0] * 2) - (self.file_path_text_line.shape_corner_radius * 2) - (self.file_path_text_line.border_width * 2) - (self.file_path_text_line.shadow_width * 2)) text_width = self.file_path_text_line.font.size( self.file_path_text_line.get_text())[0] self.file_path_text_line.start_text_offset = max( 0, text_width - text_clip_width) if not self.file_path_text_line.is_focused: self.file_path_text_line.focus() def update_current_file_list(self): """ Updates the currently displayed list of files and directories. Usually called when the directory path has changed. """ try: directories_on_path = [ f.name for f in Path(self.current_directory_path).iterdir() if not f.is_file() ] directories_on_path = sorted(directories_on_path, key=str.casefold) directories_on_path_tuples = [(f, '#directory_list_item') for f in directories_on_path] files_on_path = [ f.name for f in Path(self.current_directory_path).iterdir() if f.is_file() ] files_on_path = sorted(files_on_path, key=str.casefold) files_on_path_tuples = [(f, '#file_list_item') for f in files_on_path] self.current_file_list = directories_on_path_tuples + files_on_path_tuples except (PermissionError, FileNotFoundError): self.current_directory_path = self.last_valid_directory_path self.update_current_file_list() else: self.last_valid_directory_path = self.current_directory_path def _validate_file_path(self, path_to_validate: Path) -> bool: if self.allow_existing_files_only: return self._validate_path_exists_and_of_allowed_type( path_to_validate, self.allow_picking_directories) else: return self._validate_path_in_existing_directory(path_to_validate) @staticmethod def _validate_path_in_existing_directory(path_to_validate: Path) -> bool: """ Checks the selected path is valid. :return: True if valid. """ if path_to_validate is None: return False return len( path_to_validate.name) > 0 and path_to_validate.parent.exists() @staticmethod def _validate_path_exists_and_of_allowed_type( path_to_validate: Path, allow_directories: bool) -> bool: """ Checks the selected path is valid. :return: True if valid. """ if path_to_validate is None: return False if allow_directories: valid_type = (path_to_validate.is_file() or path_to_validate.is_dir()) else: valid_type = path_to_validate.is_file() return path_to_validate.exists() and valid_type def process_event(self, event: pygame.event.Event) -> bool: """ Handles events that this UI element is interested in. There are a lot of buttons in the file dialog. :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. """ handled = super().process_event(event) self._process_ok_cancel_events(event) self._process_confirmation_dialog_events(event) self._process_mini_file_operation_button_events(event) self._process_file_path_entry_events(event) self._process_file_list_events(event) return handled def _process_file_path_entry_events(self, event): """ Handle events coming from text entry element which displays the current file path. :param event: event to check. """ if (event.type != pygame.USEREVENT or event.user_type not in [UI_TEXT_ENTRY_FINISHED, UI_TEXT_ENTRY_CHANGED] or event.ui_element != self.file_path_text_line): return entered_file_path = Path( self.file_path_text_line.get_text()).absolute() if self._validate_file_path(entered_file_path): if len(entered_file_path.name) > 0 and ( entered_file_path.is_file() or not entered_file_path.exists()): self.current_file_path = entered_file_path if self._validate_path_exists_and_of_allowed_type( self.current_file_path, allow_directories=False): self.delete_button.enable() else: self.delete_button.disable() self.ok_button.enable() else: self.current_directory_path = str(entered_file_path) self.current_file_path = None self.delete_button.disable() self.ok_button.disable() if event.user_type == UI_TEXT_ENTRY_FINISHED: if len(entered_file_path.name ) > 0 and entered_file_path.is_dir(): self.current_directory_path = str(entered_file_path) elif len(entered_file_path.name) > 0 and ( entered_file_path.is_file() or not entered_file_path.exists()): self.current_directory_path = str( entered_file_path.parent.absolute()) else: self.current_directory_path = str(entered_file_path) self.update_current_file_list() self.file_selection_list.set_item_list(self.current_file_list) if self.current_file_path is not None: self.file_path_text_line.set_text( str(self.current_file_path)) else: self.file_path_text_line.set_text( self.current_directory_path) else: self.current_directory_path = self.last_valid_directory_path self.current_file_path = None self.delete_button.disable() self.ok_button.disable() def _process_file_list_events(self, event): """ Handle events coming from the file/folder list. :param event: event to check. """ if (event.type == pygame.USEREVENT and event.user_type == UI_SELECTION_LIST_NEW_SELECTION and event.ui_element == self.file_selection_list): new_selection_file_path = Path( self.current_directory_path) / event.text if self._validate_path_exists_and_of_allowed_type( new_selection_file_path, self.allow_picking_directories): self.current_file_path = new_selection_file_path self.file_path_text_line.set_text(str(self.current_file_path)) self._highlight_file_name_for_editing() self.ok_button.enable() if self._validate_path_exists_and_of_allowed_type( self.current_file_path, allow_directories=False): self.delete_button.enable() else: self.delete_button.disable() else: self.ok_button.disable() self.delete_button.disable() if (event.type == pygame.USEREVENT and event.user_type == UI_SELECTION_LIST_DOUBLE_CLICKED_SELECTION and event.ui_element == self.file_selection_list): new_directory_file_path = Path( self.current_directory_path) / event.text self._change_directory_path(new_directory_file_path) def _change_directory_path(self, new_directory_path: Path): """ Change the current directory path and update everything that needs to update when that happens. :param new_directory_path: The new path to change to. """ if not new_directory_path.exists() or new_directory_path.is_file(): return self.current_directory_path = str(new_directory_path.resolve()) self.update_current_file_list() self.file_selection_list.set_item_list(self.current_file_list) if self.current_file_path is not None and not self.allow_existing_files_only: self.current_file_path = (new_directory_path / self.current_file_path.name).resolve() self.file_path_text_line.set_text(str(self.current_file_path)) self._highlight_file_name_for_editing() self.ok_button.enable() if self._validate_path_exists_and_of_allowed_type( self.current_file_path, allow_directories=False): self.delete_button.enable() else: self.delete_button.disable() else: self.current_file_path = None self.file_path_text_line.set_text(self.current_directory_path) self.delete_button.disable() self.ok_button.disable() def _process_confirmation_dialog_events(self, event): """ Handle any events coming from the confirmation dialog if that's up. :param event: event to check. """ if (event.type != pygame.USEREVENT or event.user_type != UI_CONFIRMATION_DIALOG_CONFIRMED or event.ui_element != self.delete_confirmation_dialog): return try: self.current_file_path.unlink() except (PermissionError, FileNotFoundError): pass else: self.current_file_path = None self.delete_button.disable() self.ok_button.disable() self.update_current_file_list() self.file_path_text_line.set_text(self.current_directory_path) self.file_selection_list.set_item_list(self.current_file_list) def _process_mini_file_operation_button_events(self, event): """ Handle what happens when you press one of the tiny file/folder operation buttons. :param event: event to check. """ if (event.type == pygame.USEREVENT and event.user_type == UI_BUTTON_PRESSED and event.ui_element == self.delete_button): confirmation_rect = pygame.Rect(0, 0, 300, 200) confirmation_rect.center = self.rect.center selected_file_name = self.current_file_path.name long_desc = "Delete " + str(selected_file_name) + "?" self.delete_confirmation_dialog = UIConfirmationDialog( rect=confirmation_rect, manager=self.ui_manager, action_long_desc=long_desc, action_short_name='Delete', window_title='Delete') if (event.type == pygame.USEREVENT and event.user_type == UI_BUTTON_PRESSED and event.ui_element == self.parent_directory_button): self._change_directory_path( Path(self.current_directory_path).parent) if (event.type == pygame.USEREVENT and event.user_type == UI_BUTTON_PRESSED and event.ui_element == self.refresh_button): self._change_directory_path(Path(self.current_directory_path)) if (event.type == pygame.USEREVENT and event.user_type == UI_BUTTON_PRESSED and event.ui_element == self.home_button): self._change_directory_path(Path.home()) def _process_ok_cancel_events(self, event): """ Handle what happens when you press OK and Cancel. :param event: event to check. """ if (event.type == pygame.USEREVENT and event.user_type == UI_BUTTON_PRESSED and event.ui_element == self.cancel_button): self.kill() if (event.type == pygame.USEREVENT and event.user_type == UI_BUTTON_PRESSED and event.ui_element == self.ok_button and self._validate_file_path(self.current_file_path)): event_data = { 'user_type': UI_FILE_DIALOG_PATH_PICKED, 'text': str(self.current_file_path), 'ui_element': self, 'ui_object_id': self.most_specific_combined_id } new_file_chosen_event = pygame.event.Event(pygame.USEREVENT, event_data) pygame.event.post(new_file_chosen_event) self.kill()
class UIFileDialog(UIWindow): """ A dialog window for handling file selection operations. The dialog will let you pick a file from a file system but won't do anything with it once you have, the path will just be returned leaving it up to the rest of the application to decide what to do with it. TODO: This works fine for loading files, but can it be adjusted to allow for saving files? :param rect: The size and position of the file dialog window. Includes the size of shadow, border and title bar. :param manager: The manager for the whole of the UI. :param window_title: The title for the window, defaults to 'File Dialog' :param initial_file_path: The initial path to open the file dialog at. :param object_id: The object ID for the window, used for theming - defaults to '#file_dialog' """ def __init__(self, rect: pygame.Rect, manager: IUIManagerInterface, window_title: str = 'File Dialog', initial_file_path: Union[str, None] = None, object_id: str = '#file_dialog'): super().__init__(rect, manager, window_display_title=window_title, object_id=object_id, resizable=True) minimum_dimensions = (260, 300) if rect.width < minimum_dimensions[ 0] or rect.height < minimum_dimensions[1]: warn_string = ("Initial size: " + str(rect.size) + " is less than minimum dimensions: " + str(minimum_dimensions)) warnings.warn(warn_string, UserWarning) self.set_minimum_dimensions(minimum_dimensions) self.delete_confirmation_dialog = None # type: Union[None, UIConfirmationDialog] if initial_file_path is not None: if exists(initial_file_path): self.current_directory_path = abspath(initial_file_path) else: self.current_directory_path = abspath('.') self.last_valid_path = None self.selected_file_path = None self.current_file_list = None # type: Union[None, List[str]] self.update_current_file_list() self.ok_button = UIButton(relative_rect=pygame.Rect( -220, -40, 100, 30), text='OK', manager=self.ui_manager, container=self, object_id='#ok_button', anchors={ 'left': 'right', 'right': 'right', 'top': 'bottom', 'bottom': 'bottom' }) self.ok_button.disable() self.cancel_button = UIButton(relative_rect=pygame.Rect( -110, -40, 100, 30), text='Cancel', manager=self.ui_manager, container=self, object_id='#cancel_button', anchors={ 'left': 'right', 'right': 'right', 'top': 'bottom', 'bottom': 'bottom' }) self.home_button = UIButton(relative_rect=pygame.Rect(10, 10, 20, 20), text='⌂', tool_tip_text='Home Directory', manager=self.ui_manager, container=self, object_id='#home_icon_button', anchors={ 'left': 'left', 'right': 'left', 'top': 'top', 'bottom': 'top' }) self.delete_button = UIButton(relative_rect=pygame.Rect( 32, 10, 20, 20), text='⌧', tool_tip_text='Delete', manager=self.ui_manager, container=self, object_id='#delete_icon_button', anchors={ 'left': 'left', 'right': 'left', 'top': 'top', 'bottom': 'top' }) self.delete_button.disable() self.parent_directory_button = UIButton( relative_rect=pygame.Rect(54, 10, 20, 20), text='↑', tool_tip_text='Parent Directory', manager=self.ui_manager, container=self, object_id='#parent_icon_button', anchors={ 'left': 'left', 'right': 'left', 'top': 'top', 'bottom': 'top' }) self.refresh_button = UIButton(relative_rect=pygame.Rect( 76, 10, 20, 20), text='⇪', tool_tip_text='Refresh Directory', manager=self.ui_manager, container=self, object_id='#refresh_icon_button', anchors={ 'left': 'left', 'right': 'left', 'top': 'top', 'bottom': 'top' }) text_line_rect = pygame.Rect( 10, 40, self.get_container().relative_rect.width - 20, 25) self.file_path_text_line = UITextEntryLine( relative_rect=text_line_rect, manager=self.ui_manager, container=self, object_id='#file_path_text_line', anchors={ 'left': 'left', 'right': 'right', 'top': 'top', 'bottom': 'top' }) self.file_path_text_line.set_text(self.current_directory_path) file_selection_rect = pygame.Rect( 10, 80, self.get_container().relative_rect.width - 20, self.get_container().relative_rect.height - 130) self.file_selection_list = UISelectionList( relative_rect=file_selection_rect, item_list=self.current_file_list, manager=self.ui_manager, container=self, object_id='#file_display_list', anchors={ 'left': 'left', 'right': 'right', 'top': 'top', 'bottom': 'bottom' }) def update_current_file_list(self): """ Updates the currently displayed list of files and directories. Usually called when the directory path has changed. """ try: directories_on_path = [ f for f in listdir(self.current_directory_path) if not isfile(join(self.current_directory_path, f)) ] directories_on_path = sorted(directories_on_path, key=str.casefold) directories_on_path_tuples = [(f, '#directory_list_item') for f in directories_on_path] files_on_path = [ f for f in listdir(self.current_directory_path) if isfile(join(self.current_directory_path, f)) ] files_on_path = sorted(files_on_path, key=str.casefold) files_on_path_tuples = [(f, '#file_list_item') for f in files_on_path] self.current_file_list = directories_on_path_tuples + files_on_path_tuples except (PermissionError, FileNotFoundError): self.current_directory_path = self.last_valid_path self.update_current_file_list() else: self.last_valid_path = self.current_directory_path def _validate_selected_path(self) -> bool: """ Checks the selected path is valid. :return: True if valid. """ if self.selected_file_path is None: return False return exists(self.selected_file_path) and isfile( self.selected_file_path) def process_event(self, event: pygame.event.Event) -> bool: """ Handles events that this UI element is interested in. There are a lot of buttons in the file dialog. :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. """ handled = super().process_event(event) self._process_ok_cancel_events(event) self._process_confirmation_dialog_events(event) self._process_mini_file_operation_button_events(event) self._process_file_path_entry_events(event) self._process_file_list_events(event) return handled def _process_file_path_entry_events(self, event): """ Handle events coming from text entry element which displays the current file path. :param event: event to check. """ if (event.type == pygame.USEREVENT and event.user_type == UI_TEXT_ENTRY_FINISHED and event.ui_element == self.file_path_text_line): entered_file_path = self.file_path_text_line.get_text() if exists(entered_file_path): self.current_directory_path = abspath(entered_file_path) self.update_current_file_list() self.file_path_text_line.set_text(self.current_directory_path) self.file_selection_list.set_item_list(self.current_file_list) self.delete_button.disable() self.ok_button.disable() def _process_file_list_events(self, event): """ Handle events coming from the file/folder list. :param event: event to check. """ if (event.type == pygame.USEREVENT and event.user_type == UI_SELECTION_LIST_NEW_SELECTION and event.ui_element == self.file_selection_list): new_selection_file_path = join(self.current_directory_path, event.text) if exists(new_selection_file_path) and isfile( new_selection_file_path): self.selected_file_path = new_selection_file_path self.ok_button.enable() self.delete_button.enable() else: self.ok_button.disable() self.delete_button.disable() if (event.type == pygame.USEREVENT and event.user_type == UI_SELECTION_LIST_DOUBLE_CLICKED_SELECTION and event.ui_element == self.file_selection_list): new_directory_file_path = join(self.current_directory_path, event.text) if exists(new_directory_file_path ) and not isfile(new_directory_file_path): self.current_directory_path = abspath(new_directory_file_path) self.update_current_file_list() self.file_path_text_line.set_text(self.current_directory_path) self.file_selection_list.set_item_list(self.current_file_list) self.delete_button.disable() self.ok_button.disable() def _process_confirmation_dialog_events(self, event): """ Handle any events coming from the confirmation dialog if that's up. :param event: event to check. """ if (event.type == pygame.USEREVENT and event.user_type == UI_CONFIRMATION_DIALOG_CONFIRMED and event.ui_element == self.delete_confirmation_dialog): try: Path(self.selected_file_path).unlink() self.delete_button.disable() self.update_current_file_list() self.file_path_text_line.set_text(self.current_directory_path) self.file_selection_list.set_item_list(self.current_file_list) except (PermissionError, FileNotFoundError): pass def _process_mini_file_operation_button_events(self, event): """ Handle what happens when you press one of the tiny file/folder operation buttons. :param event: event to check. """ if (event.type == pygame.USEREVENT and event.user_type == UI_BUTTON_PRESSED and event.ui_element == self.delete_button): confirmation_rect = pygame.Rect(0, 0, 300, 200) confirmation_rect.center = self.rect.center selected_file_name = Path(self.selected_file_path).name long_desc = "Delete " + str(selected_file_name) + "?" self.delete_confirmation_dialog = UIConfirmationDialog( rect=confirmation_rect, manager=self.ui_manager, action_long_desc=long_desc, action_short_name='Delete', window_title='Delete') if (event.type == pygame.USEREVENT and event.user_type == UI_BUTTON_PRESSED and event.ui_element == self.parent_directory_button): self.current_directory_path = str( Path(self.current_directory_path).parent) self.update_current_file_list() self.file_path_text_line.set_text(self.current_directory_path) self.file_selection_list.set_item_list(self.current_file_list) self.delete_button.disable() self.ok_button.disable() if (event.type == pygame.USEREVENT and event.user_type == UI_BUTTON_PRESSED and event.ui_element == self.refresh_button): self.update_current_file_list() self.file_path_text_line.set_text(self.current_directory_path) self.file_selection_list.set_item_list(self.current_file_list) self.delete_button.disable() self.ok_button.disable() if (event.type == pygame.USEREVENT and event.user_type == UI_BUTTON_PRESSED and event.ui_element == self.home_button): self.current_directory_path = str(Path.home()) self.update_current_file_list() self.file_path_text_line.set_text(self.current_directory_path) self.file_selection_list.set_item_list(self.current_file_list) self.delete_button.disable() self.ok_button.disable() def _process_ok_cancel_events(self, event): """ Handle what happens when you press OK and Cancel. :param event: event to check. """ if (event.type == pygame.USEREVENT and event.user_type == UI_BUTTON_PRESSED and event.ui_element == self.cancel_button): self.kill() if (event.type == pygame.USEREVENT and event.user_type == UI_BUTTON_PRESSED and event.ui_element == self.ok_button and self._validate_selected_path()): event_data = { 'user_type': UI_FILE_DIALOG_PATH_PICKED, 'text': self.selected_file_path, 'ui_element': self, 'ui_object_id': self.most_specific_combined_id } new_file_chosen_event = pygame.event.Event(pygame.USEREVENT, event_data) pygame.event.post(new_file_chosen_event) self.kill()
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, )
class UIConsoleWindow(UIWindow): """ Create a basic console window. By default it doesn't do anything except log commands entered into the console in the text box log. There is an event and a few methods however that allow you to hook this console window up to do whatever you would like with the entered text commands. See the pygame GUI examples repository for an example using it to run the Python Interactive Shell. :param rect: A rect determining the size and position of the console window. :param manager: The UI manager. :param window_title: The title displayed in the windows title bar. :param object_id: The object ID for the window, used for theming - defaults to '#console_window' :param visible: Whether the element is visible by default. """ def __init__(self, rect: pygame.Rect, manager: IUIManagerInterface, window_title: str = 'pygame-gui.console_title_bar', object_id: Union[ObjectID, str] = ObjectID('#console_window', None), visible: int = 1, preload_bold_log_font: bool = True): super().__init__(rect, manager, window_display_title=window_title, object_id=object_id, resizable=True, visible=visible) self.default_log_prefix = '> ' self.log_prefix = self.default_log_prefix self.should_logged_commands_escape_html = True self.logged_commands_above = [] self.current_logged_command = None self.logged_commands_below = [] self.command_entry = UITextEntryLine(relative_rect=pygame.rect.Rect( (2, -32), (self.get_container().get_size()[0] - 4, 30)), manager=self.ui_manager, container=self, object_id='#command_entry', anchors={ 'left': 'left', 'right': 'right', 'top': 'bottom', 'bottom': 'bottom' }) self.log = UITextBox(html_text="", relative_rect=pygame.rect.Rect( (2, 2), (self.get_container().get_size()[0] - 4, self.get_container().get_size()[1] - 36)), manager=manager, container=self, object_id='#log', anchors={ 'left': 'left', 'right': 'right', 'top': 'top', 'bottom': 'bottom' }) if preload_bold_log_font: # Would be better to load this font during UIManager setup, but this is probably # second best place. We can't know if someone will use the console window in advance # but if they do and use the default bold output setup for it, it would be a good idea # to load the bold font at th the same time th other UI stuff is loaded. log_font_info = self.ui_theme.get_font_info( self.log.combined_element_ids) font_dict = self.ui_manager.get_theme().get_font_dictionary() bold_font_id = font_dict.create_font_id(log_font_info['size'], log_font_info['name'], bold=True, italic=False) if not font_dict.check_font_preloaded(bold_font_id): font_dict.preload_font(log_font_info['size'], log_font_info['name'], bold=True) def set_log_prefix(self, prefix: str) -> None: """ Set the prefix to add before commands when they are displayed in the log. This defaults to '> '. :param prefix: A string that is prepended to commands before they are added to the log text box. """ self.log_prefix = prefix def restore_default_prefix(self) -> None: """ Restore the console log prefix to it's default value (which is: '> ') """ self.log_prefix = self.default_log_prefix def set_logged_commands_escape_html(self, should_escape: bool) -> None: """ Sets whether commands should have any HTML characters escaped before being added to the log. This is because the log uses an HTML Text box and most of the time we don't want to assume that every entered > or < is part of an HTML tag. By default HTML is escaped for commands added to the log. :param should_escape: A bool to switch escaping HTML on commands on or off. """ self.should_logged_commands_escape_html = should_escape def add_output_line_to_log(self, text_to_add: str, is_bold: bool = True, remove_line_break: bool = False, escape_html: bool = True) -> None: """ Adds a single line of text to the log text box. This is intended as a hook to add output/responses to commands entered into the console. :param text_to_add: The single line of text to add to the log. :param is_bold: Determines whether the output is shown in bold or not. Defaults to True. :param remove_line_break: Set this to True to remove the automatic line break at the end of the line of text. Sometimes you might want to add some output all on a single line (e.g. a console style 'progress bar') :param escape_html: Determines whether to escape any HTML in this line of text. Defaults to True, as most people won't be expecting every > or < to be processed as HTML. """ output_to_log = html.escape( text_to_add) if escape_html else text_to_add line_ending = '' if remove_line_break else '<br>' if is_bold: self.log.append_html_text('<b>' + output_to_log + '</b>' + line_ending) else: self.log.append_html_text(output_to_log + line_ending) def process_event(self, event: pygame.event.Event) -> bool: """ See if we need to handle an event passed down by the UI manager. Returns True if the console window dealt with this event. :param event: The event to handle """ handled = super().process_event(event) if (self.command_entry.is_focused and event.type == pygame.KEYDOWN): if event.key == pygame.K_DOWN: if len(self.logged_commands_below) > 0: popped_command = self.logged_commands_below.pop() if self.current_logged_command is not None: self.logged_commands_above.append( self.current_logged_command) self.current_logged_command = popped_command self.command_entry.set_text(self.current_logged_command) elif event.key == pygame.K_UP: if len(self.logged_commands_above) > 0: popped_command = self.logged_commands_above.pop() if self.current_logged_command is not None: self.logged_commands_below.append( self.current_logged_command) self.current_logged_command = popped_command self.command_entry.set_text(self.current_logged_command) if event.type == UI_TEXT_ENTRY_FINISHED and event.ui_element == self.command_entry: handled = True command = self.command_entry.get_text() command_for_log = command self._restore_command_log_to_end() self.logged_commands_above.append(command_for_log) if self.should_logged_commands_escape_html: command_for_log = html.escape(command_for_log) self.log.append_html_text(self.log_prefix + command_for_log + "<br>") self.command_entry.set_text("") event_data = { 'command': command, 'ui_element': self, 'ui_object_id': self.most_specific_combined_id } command_entered_event = pygame.event.Event( UI_CONSOLE_COMMAND_ENTERED, event_data) pygame.event.post(command_entered_event) if event.type == UI_TEXT_ENTRY_CHANGED and event.ui_element == self.command_entry: self._restore_command_log_to_end() return handled def _restore_command_log_to_end(self): if self.current_logged_command is not None: self.logged_commands_above.append(self.current_logged_command) self.current_logged_command = None while len(self.logged_commands_below) > 0: self.logged_commands_above.append(self.logged_commands_below.pop())
class SpeakSpell(pygame_gui.elements.UIWindow): speakthrd = None def __init__(self, pos, manager): super().__init__( pygame.Rect(pos, (400, 200)), manager=manager, window_display_title="speaknspell", object_id="#speaknspell", resizable=True, ) self.box = UITextBox( "", relative_rect=pygame.Rect(0, 0, 368, 100), manager=manager, container=self, anchors={ "left": "left", "right": "right", "top": "top", "bottom": "bottom", }, ) self.input = UITextEntryLine( relative_rect=pygame.Rect(0, -35, 368, 30), manager=manager, container=self, anchors={ "left": "left", "right": "right", "top": "bottom", "bottom": "bottom", }, ) self.engine = pyttsx3.init() self.engine.setProperty("rate", 150) self.speakthrd = None self.speak("Hello, thank you for using snakeware!") self.input.focus() def speak(self, text): if self.speakthrd is not None and self.speakthrd.is_alive(): return if text == "": return text = text.replace("\n", "<br>") spoken = re.sub(r"<(.*?)>", "", text) self.engine.say(spoken) self.speakthrd = threading.Thread(target=self.engine.runAndWait, args=()) self.speakthrd.start() self.box.html_text = text self.box.rebuild() self.input.set_text("") def process_event(self, event): super().process_event(event) if event.type == pygame.USEREVENT and event.ui_element == self.input: if event.user_type == pygame_gui.UI_TEXT_ENTRY_FINISHED: self.speak(self.input.get_text())
class SpeakSpell(pygame_gui.elements.UIWindow): speakthrd = None def __init__(self, pos, manager): super().__init__( pygame.Rect(pos, (400, 200)), manager=manager, window_display_title="speaknspell", object_id="#speaknspell", resizable=True, ) self.box = UITextBox( "", relative_rect=pygame.Rect(0, 0, 368, 100), manager=manager, container=self, anchors={ "left": "left", "right": "right", "top": "top", "bottom": "bottom", }, ) self.input = UITextEntryLine( relative_rect=pygame.Rect(0, -35, 368, 30), manager=manager, container=self, anchors={ "left": "left", "right": "right", "top": "bottom", "bottom": "bottom", }, ) self.engine = pyttsx3.init() self.engine.setProperty("rate", 150) self.speakthrd = None self.speak("Hello, thank you for using snakeware!") self.input.focus() # history attributes self.histsize = 100 self.histindex = -1 self.history = ["Hello, thank you for using snakeware!"] self.cached_command = "" def speak(self, text): if self.speakthrd is not None and self.speakthrd.is_alive(): return if text == "": return text = re.sub(r"(\\r)?\\n", "<br>", text) spoken = re.sub(r"<(.*?)>", "", text) self.engine.say(spoken) self.speakthrd = threading.Thread(target=self.engine.runAndWait, args=()) self.speakthrd.start() self.box.html_text = text self.box.rebuild() self.input.set_text("") def cache_command(self): self.cached_command = self.input.get_text() def flush_command_cache(self): self.cached_command = "" def set_histindex(self, increment): try: # self.history[self.histindex + increment] self.histindex += increment except IndexError: pass return self.histindex def set_from_history(self): if self.histindex > -1: self.input.set_text(self.history[self.histindex]) else: self.input.set_text(self.cached_command) self.input.edit_position = len(self.input.get_text()) def add_to_history(self, text): self.history = [text] + self.history if len(self.history) > self.histsize: del self.history[-1] def process_event(self, event): super().process_event(event) if event.type == pygame.USEREVENT and event.ui_element == self.input: if event.user_type == pygame_gui.UI_TEXT_ENTRY_FINISHED: text = self.input.get_text() self.add_to_history(text) self.histindex = -1 self.flush_command_cache() self.speak(text) elif event.type == pygame.KEYUP and event.key in (pygame.K_UP, pygame.K_DOWN): increment = 1 if event.key == pygame.K_UP else -1 if self.histindex == -1: self.cache_command() self.set_histindex(increment) self.set_from_history()