"""
ユーザーがファイルまたはフォルダを選択するための Kivy 用ダイアログボックス

    Last modified: 2025/09/29

    Copyright (c) 2025 toshifumi tsutsui
    Released under the MIT license
    https://wpandora8.net/the_mit_license.html
"""

import os
import platform
from typing import Callable, Literal

from kivy.core.window import Keyboard, Window
from kivy.graphics import Color, Rectangle
from kivy.lang import Builder
from kivy.metrics import dp
from kivy.properties import (
    BooleanProperty,
    DictProperty,
    ObjectProperty,
    StringProperty,
)
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.filechooser import FileChooser
from kivy.uix.popup import Popup
from kivy.uix.screenmanager import Screen, ScreenManager
from kivy.uix.scrollview import ScrollView
from kivy.uix.spinner import Spinner
from kivy.uix.textinput import TextInput
from my_dialogs.input_box import InputBox
from my_dialogs.message_box import MessageBox
from my_dialogs.textinput4ja import TextInput_JA
from my_dialogs.yes_no_box import YesNoBox

textinput: str = "TextInput_JA" if platform.system() == "Darwin" else "TextInput"

Builder.load_string(f"""
#: import os os

<_DialogLayout>:
    button_decision: button_decision
    button_to_prev: button_to_prev
    filechooser: filechooser
    label_drive: label_drive
    spinner_drive: spinner_drive
    spinner_type: spinner_type
    text_current_dir: text_current_dir
    text_filename: text_filename

    size: root.size
    pos: root.pos
    orientation: "vertical"
    BoxLayout:
        orientation: 'horizontal'
        size_hint_y: dp(48)/root.height
        padding: dp(16), dp(8)
        spacing: dp(6)
        Label:
            id: label_drive
            size_hint_x: dp(90)/root.width
            text: 'ドライブ名：'
            disabled: root.default_dir_path[0] == '/'
        Spinner:
            id: spinner_drive
            size_hint_x: dp(50)/root.width
            sync_height: True
            disabled: root.default_dir_path[0] == '/'
            text: root.default_dir_path[0:2] if root.default_dir_path[0] != '/' else ''
            values: [f'{{d}}:' for d in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' if os.path.exists(f'{{d}}:')]
        BoxLayout:
            size_hint_x: dp(10)/root.width
        Button:
            id: button_to_prev
            size_hint_x: dp(80)/root.width
            text: '< 戻る'
            disabled: True
            on_release: root.to_previouse_path()
        Button:
            size_hint_x: dp(150)/root.width
            text: 'ホームフォルダ'
            on_release: root.to_home_dir()
        BoxLayout:
            size_hint_x: (1.0-dp(90+6+50+6+10+6+80+6+150+6+140+6+110)/root.width)/2.0
        Button:
            id: button_make_dir
            size_hint_x: dp(140)/root.width
            text: 'フォルダ作成'
            disabled: (root.type == 'open')
            on_release: root.make_directory()
        BoxLayout:
            size_hint_x: (1.0-dp(90+6+50+6+10+6+80+6+150+6+140+6+110)/root.width)/2.0
        Button:
            id: button_display
            size_hint_x: dp(110)/root.width
            text: '表示切替'
            on_release: root.change_display()
    BoxLayout:
        orientation: 'horizontal'
        size_hint_y: dp(48)/root.height
        padding: dp(16), dp(8)
        spacing: dp(12)
        Label:
            size_hint_x: dp(60)/root.width
            text: '場所：'
        {textinput}:
        # TextInput:
            id: text_current_dir
            size_hint_x: 1.0-dp(60+12)/root.width
            padding: dp(8), dp(8)
            use_bubble: True
            multiline: False
            write_tab: False
            on_text_validate: root.on_text_current_dir_validate()
    AnchorLayout:
        size_hint_y: 1.0-dp(48*4+72)/root.height
        padding: dp(16), dp(8)
        FileChooser:
            id: filechooser
            multiselect: root.multiselect
            on_selection: root.change_status()
            canvas.before:
                Color:
                    rgba: 29/255, 49/255, 86/255, 1.0
                Rectangle:
                    size: self.size
            FileChooserListLayout
            FileChooserIconLayout
    BoxLayout:
        orientation: 'horizontal'
        size_hint_y: dp(48)/root.height
        padding: dp(16), dp(8)
        Label:
            size_hint_x: dp(140)/root.width
            text: 'フォルダ名：' if (root.type == 'folder') else 'ファイル名：'
        {textinput}:
        # TextInput:
            id: text_filename
            size_hint_x: 1.0-dp(140)/root.width
            padding: dp(8), dp(8)
            use_bubble: True
            multiline: False
            write_tab: False
            disabled: (root.type != 'save')
            background_disabled_normal: self.background_normal
            disabled_foreground_color: self.foreground_color
            text: root.default_file_name
            cursor: (0, 0)
            on_text_validate: root.on_text_filename_validate()
    BoxLayout:
        orientation: 'horizontal'
        size_hint_y: dp(48)/root.height
        padding: dp(16), dp(8)
        Label:
            size_hint_x: dp(140)/root.width
            height: dp(32)
            text: 'ファイルの種類：'
            disabled: (root.type == 'folder')
        Spinner:
            id: spinner_type
            size_hint_x: 1.0-dp(140)/root.width
            text_size: self.size
            sync_height: True
            halign: 'left'
            valign: 'center'
            padding: dp(16), 0
            disabled: (root.type == 'folder')
            on_text: filechooser.filters = root.filters[self.text]
    StackLayout:
        orientation: 'rl-tb'
        size_hint_y: dp(72)/root.height
        padding: dp(16), dp(12)
        spacing: dp(16)
        Button:
            id: button_decision
            text: root.button_decision_text
            size_hint_x: dp(140)/root.width
            disabled: len(text_filename.text) == 0
            on_release: root.button_decision_click()
        Button:
            text: 'キャンセル'
            size_hint_x: dp(140)/root.width
            on_release: root.button_cancel_click()
""")


class FileSelectionDialog:
    """ファイルまたはフォルダを選択するダイアログボックスを定義したクラス。"""

    def __init__(
        self,
        type: Literal["open", "save", "folder"],
        callback: Callable[[list[str]], None],
        on_cancel_callback: Callable | None,
        title: str,
        button_decision_text: str,
        confirm_overwrite: bool,
        size_hint: tuple[float | None, float | None],
        size: tuple[float, float],
        multiselect: bool,
        default_dir_path: str,
        default_file_name: str,
        with_extension: str,
        filters: dict[str, list[str]],
    ) -> None:
        """ファイルまたはフォルダを選択するダイアログボックスのインスタンスを作成して返す。

        Args:
            type (Literal[&#39;open&#39;, &#39;save&#39;, &#39;folder&#39;]): ダイアログボックスの種類。
            callback (Callable[[list[str]], None]): 決定ボタンがクリックされたときの処理。
            on_cancel_callback (Callable | None): 「キャンセル」ボタンがクリックされたときの処理。
            title (str): ダイアログボックスのタイトル。
            button_decision_text (str): 決定ボタンの表示名。
            confirm_overwrite (bool): 同名のファイルが存在するとき上書きの確認を行う場合は True。
            size_hint (tuple[float | None, float | None]): 親ウィンドウに対するダイアログボックスのサイズの比率。
            size (tuple[float, float]): ダイアログボックスのサイズ（size_hint にはそれぞれ None を指定）。
            multiselect (bool): 複数のファイルを選択する場合は True。
            default_dir_path (str): 最初に開くディレクトリのパス。
            default_file_name (str): デフォルトのファイル名。
            with_extension (str): 自動的にファイル名の末尾に追加する拡張子。
            filters (dict[str, list[str]]): 表示するファイルのフィルタ名とパターンの dict。
        """

        self._callback: Callable[[list[str]], None] = callback
        self._cancel_callback: Callable | None = on_cancel_callback
        self._type: str = type
        dir_path: str = self._get_default_dir_path(default_dir_path)
        content: _DialogLayout = _DialogLayout(
            on_decision=lambda s: self._on_decision(s),
            on_cancel=self._on_cancel,
            button_decision_text=button_decision_text,
            default_dir_path=dir_path,
            default_file_name=default_file_name,
            filters=filters,
            multiselect=multiselect,
            confirm_overwrite=confirm_overwrite,
            with_extension=with_extension,
            type=type,
        )
        self._popup: Popup = Popup(
            title=title,
            content=content,
            size_hint=size_hint,
            size=size,
            auto_dismiss=False,
        )
        self.is_alive: bool = True
        """このウィジェットのインスタンスが有効であれば True"""

    def show(self) -> None:
        """ダイアログボックスを表示する。"""

        self._popup.open()

    def _get_default_dir_path(self, default_dir_path: str) -> str:
        """default_dir_path で指定されたディレクトリのパスを検証して、適切なパスを返す。

        Args:
            default_dir_path (str): 検証するディレクトリのパス。

        Returns:
            str: 適切なパス。
        """

        path: str = os.path.abspath(default_dir_path) if default_dir_path else ""
        if os.path.isdir(path):
            return path
        elif os.path.isfile(path):
            return os.path.dirname(path)
        elif platform.system() == "Darwin":
            return os.path.expanduser("~")
        else:
            return os.path.abspath(".")

    def _on_decision(self, selection: list[str]) -> None:
        """決定ボタンがクリックされたとき。

        Args:
            selection (list[str]): 選択されたファイルまたはフォルダのパスの list。
        """

        if selection:
            self._execute_process(selection)
        else:
            self._on_cancel()

    def _execute_process(self, selection: list[str]) -> None:
        """ダイアログボックスで選択されたファイルまたはフォルダに対して処理を実行する。

        Args:
            selection (list[str]): 処理するファイルまたはフォルダのパスの list。
        """

        self.is_alive = False
        self._popup.dismiss()

        self._callback(selection)

    def _on_cancel(self) -> None:
        """「キャンセル」ボタンがクリックされたとき。"""

        self.is_alive = False
        self._popup.dismiss()

        if self._cancel_callback is not None:
            self._cancel_callback()


class _DialogLayout(BoxLayout):
    """ファイルまたはフォルダを選択するダイアログボックスのレイアウト。"""

    button_decision_text: str = StringProperty("")
    confirm_overwrite: bool = BooleanProperty(True)
    default_dir_path: str = StringProperty("")
    default_file_name: str = StringProperty("")
    filters = DictProperty({})
    multiselect: bool = BooleanProperty(False)
    type: str = StringProperty("")
    with_extension: str = StringProperty("")

    text_filename: TextInput = ObjectProperty(None)
    text_current_dir: TextInput = ObjectProperty(None)
    filechooser: FileChooser = ObjectProperty(None)
    spinner_type: Spinner = ObjectProperty(None)
    spinner_drive: Spinner = ObjectProperty(None)
    button_decision: Button = ObjectProperty(None)
    button_to_prev: Button = ObjectProperty(None)

    def __init__(
        self, on_decision: Callable[[list[str]], None], on_cancel: Callable, **kwargs
    ) -> None:
        """ファイルまたはフォルダを選択するダイアログボックスのレイアウト。

        Args:
            on_decision (Callable[[list[str]], None]): 決定ボタンがクリックされたときの処理。
            on_cancel (Callable): 「キャンセル」ボタンがクリックされたときの処理。
        """

        super().__init__(**kwargs)

        self._on_decision: Callable[[list[str]], None] = on_decision
        self._on_cancel: Callable = on_cancel
        self._prev_dir: str = self.default_dir_path
        self._history: list[str] = []

        self.text_current_dir.text = self.default_dir_path
        self.filechooser.path = self.default_dir_path
        self.text_current_dir.cursor = (0, 0)

        self._display_filename()
        self._set_filter()
        self._initialize_filechooser()
        self._set_access_key()
        self._bind_functions()

    def _set_filter(self) -> None:
        """ファイルの表示フィルターに関わるウィジェットを設定する。"""

        def is_dir(dir_path: str, filename: str) -> bool:
            file_path: str = os.path.join(dir_path, filename)
            return os.path.isdir(file_path)

        if self.type == "folder":
            self.filechooser.filters = [is_dir]
        else:
            self.spinner_type.text = list(self.filters)[0]
            self.spinner_type.values = list(self.filters)
            self.filechooser.filters = self.filters[self.spinner_type.text]

    def _initialize_filechooser(self) -> None:
        """FileChooser の初期設定を行う。"""

        sm: ScreenManager = self.filechooser.manager
        listview_screen: Screen = sm.get_screen("listview")
        listview_screen.bind(on_enter=self._reset_color)  # type: ignore
        self.filechooser.bind(path=self._dir_changed)  # type: ignore

        iconview_screen: Screen = sm.get_screen("iconview")
        sv_icon: ScrollView = iconview_screen.children[0].children[0]
        sv_list: ScrollView = listview_screen.children[0].children[0].children[0]
        sv_list.bar_width = sv_icon.bar_width = dp(16)
        sv_list.bar_inactive_color = (0.9, 0.9, 0.9, 0.4)
        sv_icon.bar_inactive_color = (0.9, 0.9, 0.9, 0.4)
        sv_list.scroll_type = sv_icon.scroll_type = ["bars", "content"]

    def _bind_functions(self) -> None:
        """各ウィジェットにイベントをバインドする。"""

        self.spinner_drive.bind(text=self._drive_changed)  # type: ignore
        self.text_current_dir.bind(focus=self._on_focus)  # type: ignore
        self.text_filename.bind(focus=self._on_focus)  # type: ignore

    def _set_access_key(self) -> None:
        """アクセスキーを設定する。"""

        def keyboard_closed():
            self._keyboard.unbind(on_key_down=self.on_keyboard_down)

        self._keyboard: Keyboard = Window.request_keyboard(keyboard_closed, self)
        self._keyboard.bind(on_key_down=self.on_keyboard_down)

    def on_text_current_dir_validate(self) -> None:
        """text_current_dir にフォーカスがある状態で Enter キーが押されたとき。"""

        self._change_directory()
        length: int = len(self.text_current_dir.text)
        self.text_current_dir.cursor = (length, 0)

    def on_text_filename_validate(self) -> None:
        """text_filename にフォーカスがある状態で Enter キーが押されたとき。"""

        if not self.button_decision.disabled:
            self.button_decision_click()

    def on_keyboard_down(
        self,
        window: Keyboard,
        keycode: tuple[int, str],
        text: str,
        modifiers: list[str],
    ) -> None:
        """押下されたキーにより処理を行う。

        Args:
            window (Keyboard): バインドされた Keyboard のインスタンス。
            keycode (tupl[int, str]): 押されたキーのキーコードと文字の tuple。
            text (str): 押されたキーのテキスト。
            modifiers (list[str]): 同時に押された補助キー名の list。
        """

        if keycode[1] == "escape":
            self.button_cancel_click()
        elif keycode[1] == "enter":
            if not self.button_decision.disabled:
                self.button_decision_click()

    def _on_focus(self, instance: TextInput, value: bool) -> None:
        """TextInput のフォーカスの状態が変わったとき。

        Args:
            instance (TextInput): 対象となる TextInput のインスタンス。
            value (bool): フォーカスが外れた場合は False。
        """

        # TextInput のフォーカスが外れたときに、再度 Keyboard にアクセスキーをバインドする。
        if not value:
            self._keyboard.bind(on_key_down=self.on_keyboard_down)

    def _drive_changed(self, instance: Spinner, value: str) -> None:
        """「ドライブ名」スピナーの値が変更されたとき。

        Args:
            instance (Spinner): バインドされた Spinner のインスタンス。
            value (str): 選択された値。
        """

        self._prev_dir = self.filechooser.path
        self.filechooser.path = f"{self.spinner_drive.text}\\"

    def to_previouse_path(self) -> None:
        """「戻る」ボタンがクリックされたとき。"""

        self.filechooser.unbind(path=self._dir_changed)  # type: ignore
        self.filechooser.path = self._history.pop()
        self.filechooser.bind(path=self._dir_changed)  # type: ignore
        self.change_status()

    def to_home_dir(self) -> None:
        """「ホームフォルダ」ボタンがクリックされたとき。"""

        self._prev_dir = self.filechooser.path
        self.filechooser.path = os.path.expanduser("~")

    def make_directory(self) -> None:
        """「フォルダ作成」ボタンがクリックされたとき。"""

        def on_cancel() -> None:
            self._keyboard.bind(on_key_down=self.on_keyboard_down)

        self._unbind_functions()

        InputBox(
            message="作成するフォルダの名前を入力してください。",
            on_ok_callback=lambda s: self._make_dir(s),
            on_cancel_callback=on_cancel,
        ).show()

    def _make_dir(self, name: str) -> None:
        """指定された名前のディレクトリを作成する。

        Args:
            name (str): 作成するディレクトリの名前。
        """

        def after_closing() -> None:
            self._keyboard.bind(on_key_down=self.on_keyboard_down)

        current_path: str = self.filechooser.path
        dir_path: str = os.path.join(current_path, name)

        try:
            os.makedirs(dir_path)
            self.filechooser.path = dir_path
            self.filechooser.path = current_path

        except FileExistsError:
            MessageBox(
                message="既に同名のフォルダが存在します。",
                icon="error",
                callback=after_closing,
            ).show()
        except OSError:
            MessageBox(
                message="フォルダ名に使用できない文字が含まれています。",
                icon="error",
                callback=after_closing,
            ).show()
        except Exception:
            MessageBox(
                message="フォルダが作成できませんでした。",
                icon="error",
                callback=after_closing,
            ).show()
        else:
            self._keyboard.bind(on_key_down=self.on_keyboard_down)

    def change_display(self) -> None:
        """「表示切替」ボタンがクリックされたとき。"""

        if self.filechooser.view_mode == "list":
            self.filechooser.view_mode = "icon"
        else:
            self.filechooser.view_mode = "list"

    def _dir_changed(self, instance: FileChooser, value: str) -> None:
        """FileChooser の path の値が変更されたとき。

        Args:
            instance (FileChooser): バインドされた FileChooser のインスタンス。
            value (str): 現在の path の値。
        """

        self._history.append(self._prev_dir)
        self.change_status()

    def _change_directory(self) -> None:
        """「場所」に入力されているパスに移動する。"""

        self._prev_dir = self.filechooser.path

        path: str = self.text_current_dir.text.replace('"', "")
        path = path.rstrip(os.sep) if (path[-1] == os.sep) else path
        if os.path.isdir(path):
            self.filechooser.path = path
        elif os.path.isfile(path):
            dir_path: str = os.path.dirname(path)
            self.filechooser.path = self.text_current_dir.text = dir_path
        else:
            self.text_current_dir.text = self.filechooser.path

    def button_decision_click(self) -> None:
        """決定ボタンがクリックされたとき。"""

        def on_decision(selection: list[str]) -> None:
            self._on_decision(selection)

        def on_cancel() -> None:
            self._bind_functions()
            self._keyboard.bind(on_key_down=self.on_keyboard_down)

        self._unbind_functions()

        if self.type == "save":
            dir_path: str = self.filechooser.path
            filename: str = self.text_filename.text
            file_path: str = os.path.join(dir_path, filename)
            ext: str = "." + self.with_extension

            if self.with_extension and (os.path.splitext(file_path)[1] != ext):
                file_path += ext

            if os.path.isfile(file_path) and self.confirm_overwrite:
                msg: str = f'"{os.path.basename(file_path)}"\nは既に存在します。'
                msg += "上書きしますか？"
                YesNoBox(
                    msg,
                    on_yes_callback=lambda: on_decision([file_path]),
                    on_no_callback=on_cancel,
                    size_hint=(None, None),
                    size=(dp(640), dp(300)),
                ).show()
            else:
                on_decision([file_path])

        elif self.type == "open":
            on_decision(self.filechooser.selection)
        else:
            on_decision([self.filechooser.path])

    def button_cancel_click(self) -> None:
        """「キャンセル」ボタンがクリックされたとき。"""

        self._unbind_functions()

        self._on_cancel()

    def _unbind_functions(self) -> None:
        """イベントにバインドされた関数を解除する。"""

        self.text_current_dir.unbind(focus=self._on_focus)  # type: ignore
        self.text_filename.unbind(focus=self._on_focus)  # type: ignore
        self._keyboard.unbind(on_key_down=self.on_keyboard_down)

    def change_status(self) -> None:
        """各ウィジェットの状態を変更する。"""

        current_dir: str = self.filechooser.path
        current_drive: str = current_dir[0]

        self._reset_color()
        self._display_filename()

        self.spinner_drive.unbind(text=self._drive_changed)  # type: ignore

        if current_drive == "/":
            self.spinner_drive.text = ""
        else:
            self.spinner_drive.text = current_dir[0:2]

        self.spinner_drive.bind(text=self._drive_changed)  # type: ignore

        self.text_current_dir.text = current_dir
        self.text_current_dir.cursor = (0, 0)
        self._prev_dir = current_dir
        self.button_to_prev.disabled = not any(self._history)

    def _display_filename(self) -> None:
        """「ファイル名」または「フォルダ名」を表示する。"""

        selection: list[str] = self.filechooser.selection

        if self.type == "save":
            if any(selection):
                self.text_filename.text = os.path.basename(selection[0])
        elif self.type == "open":
            if not any(selection):
                self.text_filename.text = ""
            elif len(selection) == 1:
                self.text_filename.text = os.path.basename(selection[0])
            else:
                text: str = " ".join([f'"{os.path.basename(f)}"' for f in selection])
                self.text_filename.text = text
        else:
            self.text_filename.text = os.path.basename(self.filechooser.path)

        self.text_filename.cursor = (0, 0)

    def _reset_color(self, instance: Screen | None = None, value=None):
        """FileChooser の ListView で、複数のファイルが選択された状態を表示する。

        Args:
            instance (Screen | None, optional): イベントがバインドされた Screen のインスタンス。
            value (_type_, optional): _description_. 不明。
        """

        if self.filechooser.view_mode == "list":
            tv = self.filechooser.children[0].children[0].children[0].ids.treeview
            for i in tv.children:
                with i.canvas.before:
                    odd_color: tuple = (29 / 255, 49 / 255, 86 / 255, 1)
                    even_color: tuple = (39 / 255, 57 / 255, 90 / 255, 1)
                    Color(rgba=odd_color if i.odd else even_color)

                    if i.parent:
                        size: tuple = (i.parent.size[0], i.size[1])
                        pos: tuple = (i.parent.x, i.y)
                    else:
                        size: tuple = (0, 0)
                        pos: tuple = (1, 1)

                    Rectangle(size=size, pos=pos)

                if hasattr(i, "selected") and i.selected:
                    with i.canvas.before:
                        Color(90 / 255, 100 / 255, 120 / 255, 1.0)

                        if i.parent:
                            size: tuple = (i.parent.size[0], i.size[1])
                            pos: tuple = (i.parent.x, i.y)
                        else:
                            size: tuple = (0, 0)
                            pos: tuple = (1, 1)

                        Rectangle(size=size, pos=pos)
                else:
                    pass
        else:
            pass


class SaveFileDialog(FileSelectionDialog):
    """「ファイルを保存」ダイアログボックスを定義したクラス。"""

    def __init__(
        self,
        callback: Callable[[list[str]], None],
        on_cancel_callback: Callable | None = None,
        title: str = "ファイルを保存",
        button_decision_text: str = "保存",
        confirm_overwrite: bool = True,
        size_hint: tuple[float | None, float | None] = (0.9, 0.9),
        size: tuple[float, float] = (720, 540),
        default_dir_path: str = "",
        default_file_name: str = "",
        with_extension: str = "",
        filters: dict[str, list[str]] = {"すべてのファイル": ["*"]},
    ) -> None:
        """「ファイルを保存」ダイアログボックスのインスタンスを作成して、ダイアログボックスを表示する。

        Args:
            callback (Callable[[list[str]], None]): 決定ボタンがクリックされたときの処理。
            on_cancel_callback (Callable | None, optional): 「キャンセル」ボタンがクリックされたときの処理。
            title (str, optional): ダイアログボックスのタイトル。デフォルトは「ファイルを保存」。
            button_decision_text (str, optional): 決定ボタンの表示名。デフォルトは「保存」。
            confirm_overwrite (bool, optional): 同名のファイルが存在するとき上書きの確認を行う場合は True。
            size_hint (tuple[float | None, float | None]): 親ウィンドウに対するダイアログボックスのサイズの比率。
            size (tuple[float, float]): ダイアログボックスのサイズ（size_hint にはそれぞれ None を指定）。
            default_dir_path (str, optional): 最初に開くディレクトリのパス。空白の場合は既定のディレクトリ。
            default_file_name (str, optional): デフォルトのファイル名。
            with_extension (str, optional): 自動的にファイル名の末尾に追加する拡張子。
            filters (dict[str, list[str]]): 表示するファイルのフィルタ名とパターンの dict。
        """

        super().__init__(
            type="save",
            callback=callback,
            on_cancel_callback=on_cancel_callback,
            title=title,
            button_decision_text=button_decision_text,
            confirm_overwrite=confirm_overwrite,
            size_hint=size_hint,
            size=size,
            multiselect=False,
            default_dir_path=default_dir_path,
            default_file_name=default_file_name,
            with_extension=with_extension,
            filters=filters,
        )


class OpenFileDialog(FileSelectionDialog):
    """「ファイルを開く」ダイアログボックスを定義したクラス。"""

    def __init__(
        self,
        callback: Callable[[list[str]], None],
        on_cancel_callback: Callable | None = None,
        title: str = "ファイルを開く",
        button_decision_text: str = "開く",
        size_hint: tuple[float | None, float | None] = (0.9, 0.9),
        size: tuple[float, float] = (720, 540),
        multiselect: bool = False,
        default_dir_path: str = "",
        filters: dict[str, list[str]] = {"すべてのファイル": ["*"]},
    ) -> None:
        """「ファイルを開く」ダイアログボックスのインスタンスを作成して、ダイアログボックスを表示する。

        Args:
            callback (Callable[[list[str]], None]): 決定ボタンがクリックされたときの処理。
            on_cancel_callback (Callable | None, optional): 「キャンセル」ボタンがクリックされたときの処理。
            title (str, optional): ダイアログボックスのタイトル。デフォルトは「ファイルを開く」。
            button_decision_text (str, optional): 決定ボタンの表示名。デフォルトは「開く」。
            size_hint (tuple[float | None, float | None]): 親ウィンドウに対するダイアログボックスのサイズの比率。
            size (tuple[float, float]): ダイアログボックスのサイズ（size_hint にはそれぞれ None を指定）。
            multiselect (bool, optional): 複数のファイルを選択する場合は True。
            default_dir_path (str, optional): 最初に開くディレクトリのパス。空白の場合は既定のディレクトリ。
            filters (dict[str, list[str]]): 表示するファイルのフィルタ名とパターンの dict。
        """

        super().__init__(
            type="open",
            callback=callback,
            on_cancel_callback=on_cancel_callback,
            title=title,
            button_decision_text=button_decision_text,
            confirm_overwrite=False,
            size_hint=size_hint,
            size=size,
            multiselect=multiselect,
            default_dir_path=default_dir_path,
            default_file_name="",
            with_extension="",
            filters=filters,
        )


class FolderSelectionDialog(FileSelectionDialog):
    """「フォルダを選択」ダイアログボックスを定義したクラス。"""

    def __init__(
        self,
        callback: Callable[[list[str]], None],
        on_cancel_callback: Callable | None = None,
        title: str = "フォルダを選択",
        button_decision_text: str = "選択",
        size_hint: tuple[float | None, float | None] = (0.9, 0.9),
        size: tuple[float, float] = (720, 540),
        default_dir_path: str = "",
    ) -> None:
        """「フォルダを選択」ダイアログボックスのインスタンスを作成して、ダイアログボックスを表示する。

        Args:
            callback (Callable[[list[str]], None]): 決定ボタンがクリックされたときの処理。
            on_cancel_callback (Callable | None, optional): 「キャンセル」ボタンがクリックされたときの処理。
            title (str, optional): ダイアログボックスのタイトル。デフォルトは「フォルダを選択」。
            button_decision_text (str, optional): 決定ボタンの表示名。デフォルトは「選択」。
            size_hint (tuple[float | None, float | None]): 親ウィンドウに対するダイアログボックスのサイズの比率。
            size (tuple[float, float]): ダイアログボックスのサイズ（size_hint にはそれぞれ None を指定）。
            default_dir_path (str, optional): 最初に開くディレクトリのパス。空白の場合は既定のディレクトリ。
        """

        super().__init__(
            type="folder",
            callback=callback,
            on_cancel_callback=on_cancel_callback,
            title=title,
            button_decision_text=button_decision_text,
            confirm_overwrite=False,
            size_hint=size_hint,
            size=size,
            multiselect=False,
            default_dir_path=default_dir_path,
            default_file_name="",
            with_extension="",
            filters={},
        )
