clan-app: Change tkinter file dialogue to gtk4 file dialogue
This commit is contained in:
@@ -1,96 +0,0 @@
|
|||||||
# ruff: noqa: N801
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from tkinter import Tk, filedialog
|
|
||||||
|
|
||||||
from clan_cli.api import ApiError, ErrorDataClass, SuccessDataClass
|
|
||||||
from clan_cli.api.directory import FileFilter, FileRequest
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def _apply_filters(filters: FileFilter | None) -> list[tuple[str, str]]:
|
|
||||||
if not filters:
|
|
||||||
return []
|
|
||||||
|
|
||||||
filter_patterns = []
|
|
||||||
|
|
||||||
if filters.mime_types:
|
|
||||||
# Tkinter does not directly support MIME types, so this section can be adjusted
|
|
||||||
# if you wish to handle them differently
|
|
||||||
filter_patterns.extend(filters.mime_types)
|
|
||||||
|
|
||||||
if filters.patterns:
|
|
||||||
filter_patterns.extend(filters.patterns)
|
|
||||||
|
|
||||||
if filters.suffixes:
|
|
||||||
suffix_patterns = [f"*.{suffix}" for suffix in filters.suffixes]
|
|
||||||
filter_patterns.extend(suffix_patterns)
|
|
||||||
|
|
||||||
filter_title = filters.title if filters.title else "Custom Files"
|
|
||||||
|
|
||||||
return [(filter_title, " ".join(filter_patterns))]
|
|
||||||
|
|
||||||
|
|
||||||
def open_file(
|
|
||||||
file_request: FileRequest, *, op_key: str
|
|
||||||
) -> SuccessDataClass[list[str] | None] | ErrorDataClass:
|
|
||||||
try:
|
|
||||||
root = Tk()
|
|
||||||
root.withdraw() # Hide the main window
|
|
||||||
root.attributes("-topmost", True) # Bring the dialogs to the front
|
|
||||||
|
|
||||||
file_path: str = ""
|
|
||||||
multiple_files: list[str] = []
|
|
||||||
|
|
||||||
if file_request.mode == "open_file":
|
|
||||||
file_path = filedialog.askopenfilename(
|
|
||||||
title=file_request.title,
|
|
||||||
initialdir=file_request.initial_folder,
|
|
||||||
initialfile=file_request.initial_file,
|
|
||||||
filetypes=_apply_filters(file_request.filters),
|
|
||||||
)
|
|
||||||
|
|
||||||
elif file_request.mode == "select_folder":
|
|
||||||
file_path = filedialog.askdirectory(
|
|
||||||
title=file_request.title, initialdir=file_request.initial_folder
|
|
||||||
)
|
|
||||||
|
|
||||||
elif file_request.mode == "save":
|
|
||||||
file_path = filedialog.asksaveasfilename(
|
|
||||||
title=file_request.title,
|
|
||||||
initialdir=file_request.initial_folder,
|
|
||||||
initialfile=file_request.initial_file,
|
|
||||||
filetypes=_apply_filters(file_request.filters),
|
|
||||||
)
|
|
||||||
|
|
||||||
elif file_request.mode == "open_multiple_files":
|
|
||||||
tresult = filedialog.askopenfilenames(
|
|
||||||
title=file_request.title,
|
|
||||||
initialdir=file_request.initial_folder,
|
|
||||||
filetypes=_apply_filters(file_request.filters),
|
|
||||||
)
|
|
||||||
multiple_files = list(tresult)
|
|
||||||
|
|
||||||
if len(file_path) == 0 and len(multiple_files) == 0:
|
|
||||||
msg = "No file selected"
|
|
||||||
raise ValueError(msg) # noqa: TRY301
|
|
||||||
|
|
||||||
multiple_files = [file_path] if len(multiple_files) == 0 else multiple_files
|
|
||||||
return SuccessDataClass(op_key, status="success", data=multiple_files)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
log.exception("Error opening file")
|
|
||||||
return ErrorDataClass(
|
|
||||||
op_key=op_key,
|
|
||||||
status="error",
|
|
||||||
errors=[
|
|
||||||
ApiError(
|
|
||||||
message=e.__class__.__name__,
|
|
||||||
description=str(e),
|
|
||||||
location=["open_file"],
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
finally:
|
|
||||||
root.destroy()
|
|
||||||
200
pkgs/clan-app/clan_app/api/file_gtk.py
Normal file
200
pkgs/clan-app/clan_app/api/file_gtk.py
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
# ruff: noqa: N801
|
||||||
|
import gi
|
||||||
|
|
||||||
|
gi.require_version("Gtk", "4.0")
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from clan_cli.api import ApiError, ErrorDataClass, SuccessDataClass
|
||||||
|
from clan_cli.api.directory import FileRequest
|
||||||
|
from gi.repository import Gio, GLib, Gtk
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_none(_list: list) -> list:
|
||||||
|
return [i for i in _list if i is not None]
|
||||||
|
|
||||||
|
|
||||||
|
RESULT: dict[str, SuccessDataClass[list[str] | None] | ErrorDataClass] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def open_file(
|
||||||
|
file_request: FileRequest, *, op_key: str
|
||||||
|
) -> SuccessDataClass[list[str] | None] | ErrorDataClass:
|
||||||
|
GLib.idle_add(gtk_open_file, file_request, op_key)
|
||||||
|
|
||||||
|
while RESULT.get(op_key) is None:
|
||||||
|
time.sleep(0.2)
|
||||||
|
response = RESULT[op_key]
|
||||||
|
del RESULT[op_key]
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def gtk_open_file(file_request: FileRequest, op_key: str) -> bool:
|
||||||
|
def returns(data: SuccessDataClass | ErrorDataClass) -> None:
|
||||||
|
global RESULT
|
||||||
|
RESULT[op_key] = data
|
||||||
|
|
||||||
|
def on_file_select(file_dialog: Gtk.FileDialog, task: Gio.Task) -> None:
|
||||||
|
try:
|
||||||
|
gfile = file_dialog.open_finish(task)
|
||||||
|
if gfile:
|
||||||
|
selected_path = remove_none([gfile.get_path()])
|
||||||
|
returns(
|
||||||
|
SuccessDataClass(
|
||||||
|
op_key=op_key, data=selected_path, status="success"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
log.exception("Error opening file")
|
||||||
|
returns(
|
||||||
|
ErrorDataClass(
|
||||||
|
op_key=op_key,
|
||||||
|
status="error",
|
||||||
|
errors=[
|
||||||
|
ApiError(
|
||||||
|
message=e.__class__.__name__,
|
||||||
|
description=str(e),
|
||||||
|
location=["open_file"],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def on_file_select_multiple(file_dialog: Gtk.FileDialog, task: Gio.Task) -> None:
|
||||||
|
try:
|
||||||
|
gfiles: Any = file_dialog.open_multiple_finish(task)
|
||||||
|
if gfiles:
|
||||||
|
selected_paths = remove_none([gfile.get_path() for gfile in gfiles])
|
||||||
|
returns(
|
||||||
|
SuccessDataClass(
|
||||||
|
op_key=op_key, data=selected_paths, status="success"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
returns(SuccessDataClass(op_key=op_key, data=None, status="success"))
|
||||||
|
except Exception as e:
|
||||||
|
log.exception("Error opening file")
|
||||||
|
returns(
|
||||||
|
ErrorDataClass(
|
||||||
|
op_key=op_key,
|
||||||
|
status="error",
|
||||||
|
errors=[
|
||||||
|
ApiError(
|
||||||
|
message=e.__class__.__name__,
|
||||||
|
description=str(e),
|
||||||
|
location=["open_file"],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def on_folder_select(file_dialog: Gtk.FileDialog, task: Gio.Task) -> None:
|
||||||
|
try:
|
||||||
|
gfile = file_dialog.select_folder_finish(task)
|
||||||
|
if gfile:
|
||||||
|
selected_path = remove_none([gfile.get_path()])
|
||||||
|
returns(
|
||||||
|
SuccessDataClass(
|
||||||
|
op_key=op_key, data=selected_path, status="success"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
returns(SuccessDataClass(op_key=op_key, data=None, status="success"))
|
||||||
|
except Exception as e:
|
||||||
|
log.exception("Error opening file")
|
||||||
|
returns(
|
||||||
|
ErrorDataClass(
|
||||||
|
op_key=op_key,
|
||||||
|
status="error",
|
||||||
|
errors=[
|
||||||
|
ApiError(
|
||||||
|
message=e.__class__.__name__,
|
||||||
|
description=str(e),
|
||||||
|
location=["open_file"],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def on_save_finish(file_dialog: Gtk.FileDialog, task: Gio.Task) -> None:
|
||||||
|
try:
|
||||||
|
gfile = file_dialog.save_finish(task)
|
||||||
|
if gfile:
|
||||||
|
selected_path = remove_none([gfile.get_path()])
|
||||||
|
returns(
|
||||||
|
SuccessDataClass(
|
||||||
|
op_key=op_key, data=selected_path, status="success"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
returns(SuccessDataClass(op_key=op_key, data=None, status="success"))
|
||||||
|
except Exception as e:
|
||||||
|
log.exception("Error opening file")
|
||||||
|
returns(
|
||||||
|
ErrorDataClass(
|
||||||
|
op_key=op_key,
|
||||||
|
status="error",
|
||||||
|
errors=[
|
||||||
|
ApiError(
|
||||||
|
message=e.__class__.__name__,
|
||||||
|
description=str(e),
|
||||||
|
location=["open_file"],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
dialog = Gtk.FileDialog()
|
||||||
|
|
||||||
|
if file_request.title:
|
||||||
|
dialog.set_title(file_request.title)
|
||||||
|
|
||||||
|
if file_request.filters:
|
||||||
|
filters = Gio.ListStore.new(Gtk.FileFilter)
|
||||||
|
file_filters = Gtk.FileFilter()
|
||||||
|
|
||||||
|
if file_request.filters.title:
|
||||||
|
file_filters.set_name(file_request.filters.title)
|
||||||
|
|
||||||
|
if file_request.filters.mime_types:
|
||||||
|
for mime in file_request.filters.mime_types:
|
||||||
|
file_filters.add_mime_type(mime)
|
||||||
|
filters.append(file_filters)
|
||||||
|
|
||||||
|
if file_request.filters.patterns:
|
||||||
|
for pattern in file_request.filters.patterns:
|
||||||
|
file_filters.add_pattern(pattern)
|
||||||
|
|
||||||
|
if file_request.filters.suffixes:
|
||||||
|
for suffix in file_request.filters.suffixes:
|
||||||
|
file_filters.add_suffix(suffix)
|
||||||
|
|
||||||
|
filters.append(file_filters)
|
||||||
|
dialog.set_filters(filters)
|
||||||
|
|
||||||
|
if file_request.initial_file:
|
||||||
|
p = Path(file_request.initial_file).expanduser()
|
||||||
|
f = Gio.File.new_for_path(str(p))
|
||||||
|
dialog.set_initial_file(f)
|
||||||
|
|
||||||
|
if file_request.initial_folder:
|
||||||
|
p = Path(file_request.initial_folder).expanduser()
|
||||||
|
f = Gio.File.new_for_path(str(p))
|
||||||
|
dialog.set_initial_folder(f)
|
||||||
|
|
||||||
|
# if select_folder
|
||||||
|
if file_request.mode == "select_folder":
|
||||||
|
dialog.select_folder(callback=on_folder_select)
|
||||||
|
if file_request.mode == "open_multiple_files":
|
||||||
|
dialog.open_multiple(callback=on_file_select_multiple)
|
||||||
|
elif file_request.mode == "open_file":
|
||||||
|
dialog.open(callback=on_file_select)
|
||||||
|
elif file_request.mode == "save":
|
||||||
|
dialog.save(callback=on_save_finish)
|
||||||
|
|
||||||
|
return GLib.SOURCE_REMOVE
|
||||||
@@ -12,7 +12,7 @@ from pathlib import Path
|
|||||||
from clan_cli.api import API
|
from clan_cli.api import API
|
||||||
from clan_cli.custom_logger import setup_logging
|
from clan_cli.custom_logger import setup_logging
|
||||||
|
|
||||||
from clan_app.api.file import open_file
|
from clan_app.api.file_gtk import open_file
|
||||||
from clan_app.deps.webview.webview import Size, SizeHint, Webview
|
from clan_app.deps.webview.webview import Size, SizeHint, Webview
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,9 @@
|
|||||||
webview-lib,
|
webview-lib,
|
||||||
fontconfig,
|
fontconfig,
|
||||||
pythonRuntime,
|
pythonRuntime,
|
||||||
|
wrapGAppsHook4,
|
||||||
|
gobject-introspection,
|
||||||
|
gtk4,
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
source = ./.;
|
source = ./.;
|
||||||
@@ -20,13 +23,14 @@ let
|
|||||||
mimeTypes = [ "x-scheme-handler/clan" ];
|
mimeTypes = [ "x-scheme-handler/clan" ];
|
||||||
};
|
};
|
||||||
|
|
||||||
# Runtime binary dependencies required by the application
|
|
||||||
runtimeDependencies = [
|
runtimeDependencies = [
|
||||||
|
gobject-introspection
|
||||||
|
gtk4
|
||||||
];
|
];
|
||||||
|
|
||||||
pyDeps = ps: [
|
pyDeps = ps: [
|
||||||
ps.tkinter
|
ps.pygobject3
|
||||||
|
ps.pygobject-stubs
|
||||||
];
|
];
|
||||||
|
|
||||||
# Dependencies required for running tests
|
# Dependencies required for running tests
|
||||||
@@ -70,7 +74,10 @@ pythonRuntime.pkgs.buildPythonApplication {
|
|||||||
(pythonRuntime.withPackages (ps: [ ps.setuptools ]))
|
(pythonRuntime.withPackages (ps: [ ps.setuptools ]))
|
||||||
copyDesktopItems
|
copyDesktopItems
|
||||||
fontconfig
|
fontconfig
|
||||||
];
|
|
||||||
|
# gtk4 deps
|
||||||
|
wrapGAppsHook4
|
||||||
|
] ++ runtimeDependencies;
|
||||||
|
|
||||||
# The necessity of setting buildInputs and propagatedBuildInputs to the
|
# The necessity of setting buildInputs and propagatedBuildInputs to the
|
||||||
# same values for your Python package within Nix largely stems from ensuring
|
# same values for your Python package within Nix largely stems from ensuring
|
||||||
@@ -120,6 +127,7 @@ pythonRuntime.pkgs.buildPythonApplication {
|
|||||||
# Additional pass-through attributes
|
# Additional pass-through attributes
|
||||||
passthru.desktop-file = desktop-file;
|
passthru.desktop-file = desktop-file;
|
||||||
passthru.devshellPyDeps = ps: (pyTestDeps ps) ++ (pyDeps ps);
|
passthru.devshellPyDeps = ps: (pyTestDeps ps) ++ (pyDeps ps);
|
||||||
|
passthru.runtimeDeps = runtimeDependencies;
|
||||||
passthru.pythonRuntime = pythonRuntime;
|
passthru.pythonRuntime = pythonRuntime;
|
||||||
|
|
||||||
postInstall = ''
|
postInstall = ''
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ mkShell {
|
|||||||
|
|
||||||
inputsFrom = [ self'.devShells.default ];
|
inputsFrom = [ self'.devShells.default ];
|
||||||
|
|
||||||
|
inherit (clan-app) nativeBuildInputs propagatedBuildInputs;
|
||||||
|
|
||||||
buildInputs = [
|
buildInputs = [
|
||||||
(clan-app.pythonRuntime.withPackages (
|
(clan-app.pythonRuntime.withPackages (
|
||||||
ps:
|
ps:
|
||||||
@@ -22,7 +24,7 @@ mkShell {
|
|||||||
]
|
]
|
||||||
++ (clan-app.devshellPyDeps ps)
|
++ (clan-app.devshellPyDeps ps)
|
||||||
))
|
))
|
||||||
];
|
] ++ clan-app.runtimeDeps;
|
||||||
|
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
export GIT_ROOT=$(git rev-parse --show-toplevel)
|
export GIT_ROOT=$(git rev-parse --show-toplevel)
|
||||||
|
|||||||
Reference in New Issue
Block a user