mirror of
https://github.com/hypervortex/VH-Bombsquad-Modded-Server-Files
synced 2025-11-07 17:36:08 +00:00
456 lines
16 KiB
Python
456 lines
16 KiB
Python
# Released under the MIT License. See LICENSE for details.
|
|
#
|
|
"""UI functionality for selecting files."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import threading
|
|
import time
|
|
from typing import TYPE_CHECKING
|
|
|
|
import ba
|
|
import ba.internal
|
|
|
|
if TYPE_CHECKING:
|
|
from typing import Any, Callable, Sequence
|
|
|
|
|
|
class FileSelectorWindow(ba.Window):
|
|
"""Window for selecting files."""
|
|
|
|
def __init__(
|
|
self,
|
|
path: str,
|
|
callback: Callable[[str | None], Any] | None = None,
|
|
show_base_path: bool = True,
|
|
valid_file_extensions: Sequence[str] | None = None,
|
|
allow_folders: bool = False,
|
|
):
|
|
if valid_file_extensions is None:
|
|
valid_file_extensions = []
|
|
uiscale = ba.app.ui.uiscale
|
|
self._width = 700 if uiscale is ba.UIScale.SMALL else 600
|
|
self._x_inset = x_inset = 50 if uiscale is ba.UIScale.SMALL else 0
|
|
self._height = 365 if uiscale is ba.UIScale.SMALL else 418
|
|
self._callback = callback
|
|
self._base_path = path
|
|
self._path: str | None = None
|
|
self._recent_paths: list[str] = []
|
|
self._show_base_path = show_base_path
|
|
self._valid_file_extensions = [
|
|
'.' + ext for ext in valid_file_extensions
|
|
]
|
|
self._allow_folders = allow_folders
|
|
self._subcontainer: ba.Widget | None = None
|
|
self._subcontainerheight: float | None = None
|
|
self._scroll_width = self._width - (80 + 2 * x_inset)
|
|
self._scroll_height = self._height - 170
|
|
self._r = 'fileSelectorWindow'
|
|
super().__init__(
|
|
root_widget=ba.containerwidget(
|
|
size=(self._width, self._height),
|
|
transition='in_right',
|
|
scale=(
|
|
2.23
|
|
if uiscale is ba.UIScale.SMALL
|
|
else 1.4
|
|
if uiscale is ba.UIScale.MEDIUM
|
|
else 1.0
|
|
),
|
|
stack_offset=(0, -35)
|
|
if uiscale is ba.UIScale.SMALL
|
|
else (0, 0),
|
|
)
|
|
)
|
|
ba.textwidget(
|
|
parent=self._root_widget,
|
|
position=(self._width * 0.5, self._height - 42),
|
|
size=(0, 0),
|
|
color=ba.app.ui.title_color,
|
|
h_align='center',
|
|
v_align='center',
|
|
text=ba.Lstr(resource=self._r + '.titleFolderText')
|
|
if (allow_folders and not valid_file_extensions)
|
|
else ba.Lstr(resource=self._r + '.titleFileText')
|
|
if not allow_folders
|
|
else ba.Lstr(resource=self._r + '.titleFileFolderText'),
|
|
maxwidth=210,
|
|
)
|
|
|
|
self._button_width = 146
|
|
self._cancel_button = ba.buttonwidget(
|
|
parent=self._root_widget,
|
|
position=(35 + x_inset, self._height - 67),
|
|
autoselect=True,
|
|
size=(self._button_width, 50),
|
|
label=ba.Lstr(resource='cancelText'),
|
|
on_activate_call=self._cancel,
|
|
)
|
|
ba.widget(edit=self._cancel_button, left_widget=self._cancel_button)
|
|
|
|
b_color = (0.6, 0.53, 0.63)
|
|
|
|
self._back_button = ba.buttonwidget(
|
|
parent=self._root_widget,
|
|
button_type='square',
|
|
position=(43 + x_inset, self._height - 113),
|
|
color=b_color,
|
|
textcolor=(0.75, 0.7, 0.8),
|
|
enable_sound=False,
|
|
size=(55, 35),
|
|
label=ba.charstr(ba.SpecialChar.LEFT_ARROW),
|
|
on_activate_call=self._on_back_press,
|
|
)
|
|
|
|
self._folder_tex = ba.gettexture('folder')
|
|
self._folder_color = (1.1, 0.8, 0.2)
|
|
self._file_tex = ba.gettexture('file')
|
|
self._file_color = (1, 1, 1)
|
|
self._use_folder_button: ba.Widget | None = None
|
|
self._folder_center = self._width * 0.5 + 15
|
|
self._folder_icon = ba.imagewidget(
|
|
parent=self._root_widget,
|
|
size=(40, 40),
|
|
position=(40, self._height - 117),
|
|
texture=self._folder_tex,
|
|
color=self._folder_color,
|
|
)
|
|
self._path_text = ba.textwidget(
|
|
parent=self._root_widget,
|
|
position=(self._folder_center, self._height - 98),
|
|
size=(0, 0),
|
|
color=ba.app.ui.title_color,
|
|
h_align='center',
|
|
v_align='center',
|
|
text=self._path,
|
|
maxwidth=self._width * 0.9,
|
|
)
|
|
self._scrollwidget: ba.Widget | None = None
|
|
ba.containerwidget(
|
|
edit=self._root_widget, cancel_button=self._cancel_button
|
|
)
|
|
self._set_path(path)
|
|
|
|
def _on_up_press(self) -> None:
|
|
self._on_entry_activated('..')
|
|
|
|
def _on_back_press(self) -> None:
|
|
if len(self._recent_paths) > 1:
|
|
ba.playsound(ba.getsound('swish'))
|
|
self._recent_paths.pop()
|
|
self._set_path(self._recent_paths.pop())
|
|
else:
|
|
ba.playsound(ba.getsound('error'))
|
|
|
|
def _on_folder_entry_activated(self) -> None:
|
|
ba.containerwidget(edit=self._root_widget, transition='out_right')
|
|
if self._callback is not None:
|
|
assert self._path is not None
|
|
self._callback(self._path)
|
|
|
|
def _on_entry_activated(self, entry: str) -> None:
|
|
# pylint: disable=too-many-branches
|
|
new_path = None
|
|
try:
|
|
assert self._path is not None
|
|
if entry == '..':
|
|
chunks = self._path.split('/')
|
|
if len(chunks) > 1:
|
|
new_path = '/'.join(chunks[:-1])
|
|
if new_path == '':
|
|
new_path = '/'
|
|
else:
|
|
ba.playsound(ba.getsound('error'))
|
|
else:
|
|
if self._path == '/':
|
|
test_path = self._path + entry
|
|
else:
|
|
test_path = self._path + '/' + entry
|
|
if os.path.isdir(test_path):
|
|
ba.playsound(ba.getsound('swish'))
|
|
new_path = test_path
|
|
elif os.path.isfile(test_path):
|
|
if self._is_valid_file_path(test_path):
|
|
ba.playsound(ba.getsound('swish'))
|
|
ba.containerwidget(
|
|
edit=self._root_widget, transition='out_right'
|
|
)
|
|
if self._callback is not None:
|
|
self._callback(test_path)
|
|
else:
|
|
ba.playsound(ba.getsound('error'))
|
|
else:
|
|
print(
|
|
(
|
|
'Error: FileSelectorWindow found non-file/dir:',
|
|
test_path,
|
|
)
|
|
)
|
|
except Exception:
|
|
ba.print_exception(
|
|
'Error in FileSelectorWindow._on_entry_activated().'
|
|
)
|
|
|
|
if new_path is not None:
|
|
self._set_path(new_path)
|
|
|
|
class _RefreshThread(threading.Thread):
|
|
def __init__(
|
|
self, path: str, callback: Callable[[list[str], str | None], Any]
|
|
):
|
|
super().__init__()
|
|
self._callback = callback
|
|
self._path = path
|
|
|
|
def run(self) -> None:
|
|
try:
|
|
starttime = time.time()
|
|
files = os.listdir(self._path)
|
|
duration = time.time() - starttime
|
|
min_time = 0.1
|
|
|
|
# Make sure this takes at least 1/10 second so the user
|
|
# has time to see the selection highlight.
|
|
if duration < min_time:
|
|
time.sleep(min_time - duration)
|
|
ba.pushcall(
|
|
ba.Call(self._callback, files, None), from_other_thread=True
|
|
)
|
|
except Exception as exc:
|
|
# Ignore permission-denied.
|
|
if 'Errno 13' not in str(exc):
|
|
ba.print_exception()
|
|
nofiles: list[str] = []
|
|
ba.pushcall(
|
|
ba.Call(self._callback, nofiles, str(exc)),
|
|
from_other_thread=True,
|
|
)
|
|
|
|
def _set_path(self, path: str, add_to_recent: bool = True) -> None:
|
|
self._path = path
|
|
if add_to_recent:
|
|
self._recent_paths.append(path)
|
|
self._RefreshThread(path, self._refresh).start()
|
|
|
|
def _refresh(self, file_names: list[str], error: str | None) -> None:
|
|
# pylint: disable=too-many-statements
|
|
# pylint: disable=too-many-branches
|
|
# pylint: disable=too-many-locals
|
|
if not self._root_widget:
|
|
return
|
|
|
|
scrollwidget_selected = (
|
|
self._scrollwidget is None
|
|
or self._root_widget.get_selected_child() == self._scrollwidget
|
|
)
|
|
|
|
in_top_folder = self._path == self._base_path
|
|
hide_top_folder = in_top_folder and self._show_base_path is False
|
|
|
|
if hide_top_folder:
|
|
folder_name = ''
|
|
elif self._path == '/':
|
|
folder_name = '/'
|
|
else:
|
|
assert self._path is not None
|
|
folder_name = os.path.basename(self._path)
|
|
|
|
b_color = (0.6, 0.53, 0.63)
|
|
b_color_disabled = (0.65, 0.65, 0.65)
|
|
|
|
if len(self._recent_paths) < 2:
|
|
ba.buttonwidget(
|
|
edit=self._back_button,
|
|
color=b_color_disabled,
|
|
textcolor=(0.5, 0.5, 0.5),
|
|
)
|
|
else:
|
|
ba.buttonwidget(
|
|
edit=self._back_button,
|
|
color=b_color,
|
|
textcolor=(0.75, 0.7, 0.8),
|
|
)
|
|
|
|
max_str_width = 300.0
|
|
str_width = min(
|
|
max_str_width,
|
|
ba.internal.get_string_width(folder_name, suppress_warning=True),
|
|
)
|
|
ba.textwidget(
|
|
edit=self._path_text, text=folder_name, maxwidth=max_str_width
|
|
)
|
|
ba.imagewidget(
|
|
edit=self._folder_icon,
|
|
position=(
|
|
self._folder_center - str_width * 0.5 - 40,
|
|
self._height - 117,
|
|
),
|
|
opacity=0.0 if hide_top_folder else 1.0,
|
|
)
|
|
|
|
if self._scrollwidget is not None:
|
|
self._scrollwidget.delete()
|
|
|
|
if self._use_folder_button is not None:
|
|
self._use_folder_button.delete()
|
|
ba.widget(edit=self._cancel_button, right_widget=self._back_button)
|
|
|
|
self._scrollwidget = ba.scrollwidget(
|
|
parent=self._root_widget,
|
|
position=(
|
|
(self._width - self._scroll_width) * 0.5,
|
|
self._height - self._scroll_height - 119,
|
|
),
|
|
size=(self._scroll_width, self._scroll_height),
|
|
)
|
|
|
|
if scrollwidget_selected:
|
|
ba.containerwidget(
|
|
edit=self._root_widget, selected_child=self._scrollwidget
|
|
)
|
|
|
|
# show error case..
|
|
if error is not None:
|
|
self._subcontainer = ba.containerwidget(
|
|
parent=self._scrollwidget,
|
|
size=(self._scroll_width, self._scroll_height),
|
|
background=False,
|
|
)
|
|
ba.textwidget(
|
|
parent=self._subcontainer,
|
|
color=(1, 1, 0, 1),
|
|
text=error,
|
|
maxwidth=self._scroll_width * 0.9,
|
|
position=(
|
|
self._scroll_width * 0.48,
|
|
self._scroll_height * 0.57,
|
|
),
|
|
size=(0, 0),
|
|
h_align='center',
|
|
v_align='center',
|
|
)
|
|
|
|
else:
|
|
file_names = [f for f in file_names if not f.startswith('.')]
|
|
file_names.sort(key=lambda x: x[0].lower())
|
|
|
|
entries = file_names
|
|
entry_height = 35
|
|
folder_entry_height = 100
|
|
show_folder_entry = False
|
|
show_use_folder_button = self._allow_folders and not in_top_folder
|
|
|
|
self._subcontainerheight = entry_height * len(entries) + (
|
|
folder_entry_height if show_folder_entry else 0
|
|
)
|
|
v = self._subcontainerheight - (
|
|
folder_entry_height if show_folder_entry else 0
|
|
)
|
|
|
|
self._subcontainer = ba.containerwidget(
|
|
parent=self._scrollwidget,
|
|
size=(self._scroll_width, self._subcontainerheight),
|
|
background=False,
|
|
)
|
|
|
|
ba.containerwidget(
|
|
edit=self._scrollwidget,
|
|
claims_left_right=False,
|
|
claims_tab=False,
|
|
)
|
|
ba.containerwidget(
|
|
edit=self._subcontainer,
|
|
claims_left_right=False,
|
|
claims_tab=False,
|
|
selection_loops=False,
|
|
print_list_exit_instructions=False,
|
|
)
|
|
ba.widget(edit=self._subcontainer, up_widget=self._back_button)
|
|
|
|
if show_use_folder_button:
|
|
self._use_folder_button = btn = ba.buttonwidget(
|
|
parent=self._root_widget,
|
|
position=(
|
|
self._width - self._button_width - 35 - self._x_inset,
|
|
self._height - 67,
|
|
),
|
|
size=(self._button_width, 50),
|
|
label=ba.Lstr(
|
|
resource=self._r + '.useThisFolderButtonText'
|
|
),
|
|
on_activate_call=self._on_folder_entry_activated,
|
|
)
|
|
ba.widget(
|
|
edit=btn,
|
|
left_widget=self._cancel_button,
|
|
down_widget=self._scrollwidget,
|
|
)
|
|
ba.widget(edit=self._cancel_button, right_widget=btn)
|
|
ba.containerwidget(edit=self._root_widget, start_button=btn)
|
|
|
|
folder_icon_size = 35
|
|
for num, entry in enumerate(entries):
|
|
cnt = ba.containerwidget(
|
|
parent=self._subcontainer,
|
|
position=(0, v - entry_height),
|
|
size=(self._scroll_width, entry_height),
|
|
root_selectable=True,
|
|
background=False,
|
|
click_activate=True,
|
|
on_activate_call=ba.Call(self._on_entry_activated, entry),
|
|
)
|
|
if num == 0:
|
|
ba.widget(edit=cnt, up_widget=self._back_button)
|
|
is_valid_file_path = self._is_valid_file_path(entry)
|
|
assert self._path is not None
|
|
is_dir = os.path.isdir(self._path + '/' + entry)
|
|
if is_dir:
|
|
ba.imagewidget(
|
|
parent=cnt,
|
|
size=(folder_icon_size, folder_icon_size),
|
|
position=(
|
|
10,
|
|
0.5 * entry_height - folder_icon_size * 0.5,
|
|
),
|
|
draw_controller=cnt,
|
|
texture=self._folder_tex,
|
|
color=self._folder_color,
|
|
)
|
|
else:
|
|
ba.imagewidget(
|
|
parent=cnt,
|
|
size=(folder_icon_size, folder_icon_size),
|
|
position=(
|
|
10,
|
|
0.5 * entry_height - folder_icon_size * 0.5,
|
|
),
|
|
opacity=1.0 if is_valid_file_path else 0.5,
|
|
draw_controller=cnt,
|
|
texture=self._file_tex,
|
|
color=self._file_color,
|
|
)
|
|
ba.textwidget(
|
|
parent=cnt,
|
|
draw_controller=cnt,
|
|
text=entry,
|
|
h_align='left',
|
|
v_align='center',
|
|
position=(10 + folder_icon_size * 1.05, entry_height * 0.5),
|
|
size=(0, 0),
|
|
maxwidth=self._scroll_width * 0.93 - 50,
|
|
color=(1, 1, 1, 1)
|
|
if (is_valid_file_path or is_dir)
|
|
else (0.5, 0.5, 0.5, 1),
|
|
)
|
|
v -= entry_height
|
|
|
|
def _is_valid_file_path(self, path: str) -> bool:
|
|
return any(
|
|
path.lower().endswith(ext) for ext in self._valid_file_extensions
|
|
)
|
|
|
|
def _cancel(self) -> None:
|
|
ba.containerwidget(edit=self._root_widget, transition='out_right')
|
|
if self._callback is not None:
|
|
self._callback(None)
|