Exemplo n.º 1
0
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())
Exemplo n.º 2
0
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()
Exemplo n.º 3
0
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()