commit 36 mit common tools
This commit is contained in:
BIN
__pycache__/common_tools.cpython-312.pyc
Normal file
BIN
__pycache__/common_tools.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
621
common_tools.py
Executable file
621
common_tools.py
Executable file
@@ -0,0 +1,621 @@
|
||||
|
||||
""" Classes Method and Functions for lx Apps """
|
||||
|
||||
import logging
|
||||
import signal
|
||||
import base64
|
||||
from subprocess import CompletedProcess, run
|
||||
import re
|
||||
import sys
|
||||
import shutil
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
import os
|
||||
from typing import Optional, Dict, Any, NoReturn
|
||||
from pathlib import Path
|
||||
from tkinter import Toplevel
|
||||
|
||||
|
||||
class CryptoUtil:
|
||||
"""
|
||||
This class is for the creation of the folders and files
|
||||
required by Wire-Py, as well as for decryption
|
||||
the tunnel from the user's home directory
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def decrypt(user) -> None:
|
||||
"""
|
||||
Starts SSL dencrypt
|
||||
"""
|
||||
process: CompletedProcess[str] = run(
|
||||
["pkexec", "/usr/local/bin/ssl_decrypt.py", "--user", user],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
# Output from Openssl Error
|
||||
if process.stderr:
|
||||
logging.error(process.stderr, exc_info=True)
|
||||
|
||||
if process.returncode == 0:
|
||||
logging.info("Files successfully decrypted...", exc_info=True)
|
||||
else:
|
||||
|
||||
logging.error(
|
||||
f"Error process decrypt: Code {process.returncode}", exc_info=True
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def encrypt(user) -> None:
|
||||
"""
|
||||
Starts SSL encryption
|
||||
"""
|
||||
process: CompletedProcess[str] = run(
|
||||
["pkexec", "/usr/local/bin/ssl_encrypt.py", "--user", user],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
# Output from Openssl Error
|
||||
if process.stderr:
|
||||
logging.error(process.stderr, exc_info=True)
|
||||
|
||||
if process.returncode == 0:
|
||||
logging.info("Files successfully encrypted...", exc_info=True)
|
||||
else:
|
||||
logging.error(
|
||||
f"Error process encrypt: Code {process.returncode}", exc_info=True
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def find_key(key: str = "") -> bool:
|
||||
"""
|
||||
Checks if the private key already exists in the system using an external script.
|
||||
Returns True only if the full key is found exactly (no partial match).
|
||||
"""
|
||||
process: CompletedProcess[str] = run(
|
||||
["pkexec", "/usr/local/bin/match_found.py", key],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if "True" in process.stdout:
|
||||
return True
|
||||
elif "False" in process.stdout:
|
||||
return False
|
||||
logging.error(
|
||||
f"Unexpected output from the external script:\nSTDOUT: {process.stdout}\nSTDERR: {process.stderr}",
|
||||
exc_info=True,
|
||||
)
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def is_valid_base64(key: str) -> bool:
|
||||
"""
|
||||
Validates if the input is a valid Base64 string (WireGuard private key format).
|
||||
Returns True only for non-empty strings that match the expected length.
|
||||
"""
|
||||
# Check for empty string
|
||||
if not key or key.strip() == "":
|
||||
return False
|
||||
|
||||
# Regex pattern to validate Base64: [A-Za-z0-9+/]+={0,2}
|
||||
base64_pattern = r"^[A-Za-z0-9+/]+={0,2}$"
|
||||
if not re.match(base64_pattern, key):
|
||||
return False
|
||||
|
||||
try:
|
||||
# Decode and check length (WireGuard private keys are 32 bytes long)
|
||||
decoded = base64.b64decode(key)
|
||||
if len(decoded) != 32: # 32 bytes = 256 bits
|
||||
return False
|
||||
except Exception as e:
|
||||
logging.error(f"Error on decode Base64: {e}", exc_info=True)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class LxTools:
|
||||
"""
|
||||
Class LinuxTools methods that can also be used for other apps
|
||||
"""
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def center_window_cross_platform(window, width, height):
|
||||
"""
|
||||
Centers a window on the primary monitor in a way that works on both X11 and Wayland
|
||||
|
||||
Args:
|
||||
window: The tkinter window to center
|
||||
width: Window width
|
||||
height: Window height
|
||||
"""
|
||||
# Calculate the position before showing the window
|
||||
|
||||
# First attempt: Try to use GDK if available (works on both X11 and Wayland)
|
||||
try:
|
||||
import gi
|
||||
|
||||
gi.require_version("Gdk", "3.0")
|
||||
from gi.repository import Gdk
|
||||
|
||||
display = Gdk.Display.get_default()
|
||||
monitor = display.get_primary_monitor() or display.get_monitor(0)
|
||||
geometry = monitor.get_geometry()
|
||||
scale_factor = monitor.get_scale_factor()
|
||||
|
||||
# Calculate center position on the primary monitor
|
||||
x = geometry.x + (geometry.width - width // scale_factor) // 2
|
||||
y = geometry.y + (geometry.height - height // scale_factor) // 2
|
||||
|
||||
# Set window geometry
|
||||
window.geometry(f"{width}x{height}+{x}+{y}")
|
||||
return
|
||||
except (ImportError, AttributeError):
|
||||
pass
|
||||
|
||||
# Second attempt: Try xrandr for X11
|
||||
try:
|
||||
import subprocess
|
||||
|
||||
output = subprocess.check_output(
|
||||
["xrandr", "--query"], universal_newlines=True
|
||||
)
|
||||
|
||||
# Parse the output to find the primary monitor
|
||||
primary_info = None
|
||||
for line in output.splitlines():
|
||||
if "primary" in line:
|
||||
parts = line.split()
|
||||
for part in parts:
|
||||
if "x" in part and "+" in part:
|
||||
primary_info = part
|
||||
break
|
||||
break
|
||||
|
||||
if primary_info:
|
||||
# Parse the geometry: WIDTH x HEIGHT+X+Y
|
||||
geometry = primary_info.split("+")
|
||||
dimensions = geometry[0].split("x")
|
||||
primary_width = int(dimensions[0])
|
||||
primary_height = int(dimensions[1])
|
||||
primary_x = int(geometry[1])
|
||||
primary_y = int(geometry[2])
|
||||
|
||||
# Calculate center position on the primary monitor
|
||||
x = primary_x + (primary_width - width) // 2
|
||||
y = primary_y + (primary_height - height) // 2
|
||||
|
||||
# Set window geometry
|
||||
window.geometry(f"{width}x{height}+{x}+{y}")
|
||||
return
|
||||
except (ImportError, IndexError, ValueError):
|
||||
pass
|
||||
|
||||
# Final fallback: Use standard Tkinter method
|
||||
screen_width = window.winfo_screenwidth()
|
||||
screen_height = window.winfo_screenheight()
|
||||
|
||||
# Try to make an educated guess for multi-monitor setups
|
||||
# If screen width is much larger than height, assume multiple monitors side by side
|
||||
if (
|
||||
screen_width > screen_height * 1.8
|
||||
): # Heuristic for detecting multiple monitors
|
||||
# Assume the primary monitor is on the left half
|
||||
screen_width = screen_width // 2
|
||||
|
||||
x = (screen_width - width) // 2
|
||||
y = (screen_height - height) // 2
|
||||
window.geometry(f"{width}x{height}+{x}+{y}")
|
||||
|
||||
@staticmethod
|
||||
def clean_files(tmp_dir: Path = None, file: Path = None) -> None:
|
||||
"""
|
||||
Deletes temporary files and directories for cleanup when exiting the application.
|
||||
|
||||
This method safely removes an optional directory defined by `AppConfig.TEMP_DIR`
|
||||
and a single file to free up resources at the end of the program's execution.
|
||||
All operations are performed securely, and errors such as `FileNotFoundError`
|
||||
are ignored if the target files or directories do not exist.
|
||||
:param tmp_dir: (Path, optional): Path to the temporary directory that should be deleted.
|
||||
If `None`, the value of `AppConfig.TEMP_DIR` is used.
|
||||
:param file: (Path, optional): Path to the file that should be deleted.
|
||||
If `None`, no additional file will be deleted.
|
||||
|
||||
Returns:
|
||||
None: The method does not return any value.
|
||||
"""
|
||||
|
||||
if tmp_dir is not None:
|
||||
shutil.rmtree(tmp_dir, ignore_errors=True)
|
||||
try:
|
||||
if file is not None:
|
||||
Path.unlink(file)
|
||||
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def sigi(file_path: Optional[Path] = None, file: Optional[Path] = None) -> None:
|
||||
"""
|
||||
Function for cleanup after a program interruption
|
||||
|
||||
:param file: Optional - File to be deleted
|
||||
:param file_path: Optional - Directory to be deleted
|
||||
"""
|
||||
|
||||
def signal_handler(signum: int, frame: Any) -> NoReturn:
|
||||
"""
|
||||
Determines clear text names for signal numbers and handles signals
|
||||
|
||||
Args:
|
||||
signum: The signal number
|
||||
frame: The current stack frame
|
||||
|
||||
Returns:
|
||||
NoReturn since the function either exits the program or continues execution
|
||||
"""
|
||||
|
||||
signals_to_names_dict: Dict[int, str] = dict(
|
||||
(getattr(signal, n), n)
|
||||
for n in dir(signal)
|
||||
if n.startswith("SIG") and "_" not in n
|
||||
)
|
||||
|
||||
signal_name: str = signals_to_names_dict.get(
|
||||
signum, f"Unnamed signal: {signum}"
|
||||
)
|
||||
|
||||
# End program for certain signals, report to others only reception
|
||||
if signum in (signal.SIGINT, signal.SIGTERM):
|
||||
exit_code: int = 1
|
||||
logging.error(
|
||||
f"\nSignal {signal_name} {signum} received. => Aborting with exit code {exit_code}.",
|
||||
exc_info=True,
|
||||
)
|
||||
LxTools.clean_files(file_path, file)
|
||||
logging.info("Breakdown by user...")
|
||||
sys.exit(exit_code)
|
||||
else:
|
||||
logging.info(f"Signal {signum} received and ignored.")
|
||||
LxTools.clean_files(file_path, file)
|
||||
logging.error("Process unexpectedly ended...")
|
||||
|
||||
# Register signal handlers for various signals
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
signal.signal(signal.SIGHUP, signal_handler)
|
||||
|
||||
|
||||
# ConfigManager with caching
|
||||
class ConfigManager:
|
||||
"""
|
||||
Universal class for managing configuration files with caching support.
|
||||
|
||||
This class provides a general solution to load, save, and manage configuration
|
||||
files across different projects. It uses a caching system to optimize access efficiency.
|
||||
The `init()` method initializes the configuration file path, while `load()` and `save()`
|
||||
synchronize data between the file and internal memory structures.
|
||||
|
||||
Key Features:
|
||||
- Caching to minimize I/O operations.
|
||||
- Default values for missing or corrupted configuration files.
|
||||
- Reusability across different projects and use cases.
|
||||
|
||||
The class is designed for central application configuration management, working closely
|
||||
with `ThemeManager` to dynamically manage themes or other settings.
|
||||
"""
|
||||
|
||||
_config = None
|
||||
_config_file = None
|
||||
|
||||
@classmethod
|
||||
def init(cls, config_file):
|
||||
"""Initial the Configmanager with the given config file"""
|
||||
cls._config_file = config_file
|
||||
cls._config = None # Reset the cache
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
"""Load the config file and return the config as dict"""
|
||||
if not cls._config:
|
||||
try:
|
||||
lines = Path(cls._config_file).read_text(encoding="utf-8").splitlines()
|
||||
cls._config = {
|
||||
"updates": lines[1].strip(),
|
||||
"theme": lines[3].strip(),
|
||||
"tooltips": lines[5].strip()
|
||||
== "True", # is converted here to boolean!!!
|
||||
"autostart": lines[7].strip() if len(lines) > 7 else "off",
|
||||
"logfile": lines[9].strip(),
|
||||
}
|
||||
except (IndexError, FileNotFoundError):
|
||||
# DeDefault values in case of error
|
||||
cls._config = {
|
||||
"updates": "on",
|
||||
"theme": "light",
|
||||
"tooltips": "True", # Default Value as string!
|
||||
"autostart": "off",
|
||||
"logfile": LOG_FILE_PATH,
|
||||
}
|
||||
return cls._config
|
||||
|
||||
@classmethod
|
||||
def save(cls):
|
||||
"""Save the config to the config file"""
|
||||
if cls._config:
|
||||
lines = [
|
||||
"# Configuration\n",
|
||||
f"{cls._config['updates']}\n",
|
||||
"# Theme\n",
|
||||
f"{cls._config['theme']}\n",
|
||||
"# Tooltips\n",
|
||||
f"{str(cls._config['tooltips'])}\n",
|
||||
"# Autostart\n",
|
||||
f"{cls._config['autostart']}\n",
|
||||
"# Logfile\n",
|
||||
f"{cls._config['logfile']}\n",
|
||||
]
|
||||
Path(cls._config_file).write_text("".join(lines), encoding="utf-8")
|
||||
|
||||
@classmethod
|
||||
def set(cls, key, value):
|
||||
"""Sets a configuration value and saves the change"""
|
||||
cls.load()
|
||||
cls._config[key] = value
|
||||
cls.save()
|
||||
|
||||
@classmethod
|
||||
def get(cls, key, default=None):
|
||||
"""Returns a configuration value"""
|
||||
config = cls.load()
|
||||
return config.get(key, default)
|
||||
|
||||
|
||||
class ThemeManager:
|
||||
"""
|
||||
Class for central theme management and UI customization.
|
||||
|
||||
This static class allows dynamic adjustment of the application's appearance.
|
||||
The method `change_theme()` updates the current theme and saves
|
||||
the selection in the configuration file via `ConfigManager`.
|
||||
It ensures a consistent visual design across the entire project.
|
||||
|
||||
Key Features:
|
||||
- Central control over themes.
|
||||
- Automatic saving of theme settings to the configuration file.
|
||||
- Tight integration with `ConfigManager` for persistent storage of preferences.
|
||||
|
||||
The class is designed to apply themes consistently throughout the application,
|
||||
ensuring that changes are traceable and uniform across all parts of the project.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def change_theme(root, theme_in_use, theme_name=None):
|
||||
"""Change application theme centrally"""
|
||||
root.tk.call("set_theme", theme_in_use)
|
||||
if theme_in_use == theme_name:
|
||||
ConfigManager.set("theme", theme_in_use)
|
||||
|
||||
|
||||
class Tooltip:
|
||||
def __init__(self, widget, text, wraplength=250):
|
||||
self.widget = widget
|
||||
self.text = text
|
||||
self.wraplength = wraplength
|
||||
self.tooltip_window = None
|
||||
self.id = None
|
||||
self.widget.bind("<Enter>", self.enter)
|
||||
self.widget.bind("<Leave>", self.leave)
|
||||
self.widget.bind("<ButtonPress>", self.leave)
|
||||
|
||||
def enter(self, event=None): self.schedule()
|
||||
def leave(self, event=None): self.unschedule(); self.hide_tooltip()
|
||||
|
||||
def schedule(self): self.unschedule(
|
||||
); self.id = self.widget.after(250, self.show_tooltip)
|
||||
|
||||
def unschedule(self):
|
||||
id = self.id
|
||||
self.id = None
|
||||
if id:
|
||||
self.widget.after_cancel(id)
|
||||
|
||||
def show_tooltip(self, event=None):
|
||||
x, y, _, _ = self.widget.bbox("insert")
|
||||
x += self.widget.winfo_rootx() + 25
|
||||
y += self.widget.winfo_rooty() + 20
|
||||
self.tooltip_window = tw = tk.Toplevel(self.widget)
|
||||
tw.wm_overrideredirect(True)
|
||||
tw.wm_geometry(f"+{x}+{y}")
|
||||
label = ttk.Label(tw, text=self.text, justify=tk.LEFT, background="#FFFFE0", foreground="black",
|
||||
relief=tk.SOLID, borderwidth=1, wraplength=self.wraplength, padding=(4, 2, 4, 2))
|
||||
label.pack(ipadx=1)
|
||||
|
||||
def hide_tooltip(self):
|
||||
tw = self.tooltip_window
|
||||
self.tooltip_window = None
|
||||
if tw:
|
||||
tw.destroy()
|
||||
|
||||
|
||||
class LogConfig:
|
||||
@staticmethod
|
||||
def logger(file_path) -> None:
|
||||
|
||||
file_handler = logging.FileHandler(
|
||||
filename=f"{file_path}",
|
||||
mode="a",
|
||||
encoding="utf-8",
|
||||
)
|
||||
formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
|
||||
file_handler.setFormatter(formatter)
|
||||
file_handler.setLevel(logging.DEBUG)
|
||||
|
||||
logger = logging.getLogger()
|
||||
logger.addHandler(file_handler)
|
||||
|
||||
import os
|
||||
|
||||
class IconManager:
|
||||
def __init__(self, base_path='/usr/share/icons/lx-icons'):
|
||||
self.base_path = base_path
|
||||
self.icons = {}
|
||||
self._define_icon_paths()
|
||||
self._load_all()
|
||||
|
||||
def _define_icon_paths(self):
|
||||
self.icon_paths = {
|
||||
# 16x16
|
||||
'settings_16': '16/settings.png',
|
||||
|
||||
# 32x32
|
||||
'back': '32/arrow-left.png',
|
||||
'forward': '32/arrow-right.png',
|
||||
'audio_small': '32/audio.png',
|
||||
'icon_view': '32/carrel.png',
|
||||
'computer_small': '32/computer.png',
|
||||
'device_small': '32/device.png',
|
||||
'file_small': '32/document.png',
|
||||
'download_error_small': '32/download_error.png',
|
||||
'download_small': '32/download.png',
|
||||
'error_small': '32/error.png',
|
||||
'python_small': '32/file-python.png',
|
||||
'documents_small': '32/folder-water-documents.png',
|
||||
'downloads_small': '32/folder-water-download.png',
|
||||
'music_small': '32/folder-water-music.png',
|
||||
'pictures_small': '32/folder-water-pictures.png',
|
||||
'folder_small': '32/folder-water.png',
|
||||
'video_small': '32/folder-water-video.png',
|
||||
'hide': '32/hide.png',
|
||||
'home': '32/home.png',
|
||||
'info_small': '32/info.png',
|
||||
'list_view': '32/list.png',
|
||||
'log_small': '32/log.png',
|
||||
'lunix_tools_small': '32/Lunix_Tools.png',
|
||||
'key_small': '32/lxtools_key.png',
|
||||
'iso_small': '32/media-optical.png',
|
||||
'new_document_small': '32/new-document.png',
|
||||
'new_folder_small': '32/new-folder.png',
|
||||
'pdf_small': '32/pdf.png',
|
||||
'picture_small': '32/picture.png',
|
||||
'question_mark_small': '32/question_mark.png',
|
||||
'recursive_small': '32/recursive.png',
|
||||
'search_small': '32/search.png',
|
||||
'settings_small': '32/settings.png',
|
||||
'archive_small': '32/tar.png',
|
||||
'unhide': '32/unhide.png',
|
||||
'usb_small': '32/usb.png',
|
||||
'video_small_file': '32/video.png',
|
||||
'warning_small': '32/warning.png',
|
||||
'export_small': '32/wg_export.png',
|
||||
'import_small': '32/wg_import.png',
|
||||
'message_small': '32/wg_msg.png',
|
||||
'trash_small': '32/wg_trash.png',
|
||||
'vpn_small': '32/wg_vpn.png',
|
||||
'vpn_start_small': '32/wg_vpn-start.png',
|
||||
'vpn_stop_small': '32/wg_vpn-stop.png',
|
||||
|
||||
# 48x48
|
||||
'back_large': '48/arrow-left.png',
|
||||
'forward_large': '48/arrow-right.png',
|
||||
'icon_view_large': '48/carrel.png',
|
||||
'computer_large': '48/computer.png',
|
||||
'device_large': '48/device.png',
|
||||
'download_error_large': '48/download_error.png',
|
||||
'download_large': '48/download.png',
|
||||
'error_large': '48/error.png',
|
||||
'documents_large': '48/folder-water-documents.png',
|
||||
'downloads_large': '48/folder-water-download.png',
|
||||
'music_large': '48/folder-water-music.png',
|
||||
'pictures_large': '48/folder-water-pictures.png',
|
||||
'folder_large_48': '48/folder-water.png',
|
||||
'video_large_folder': '48/folder-water-video.png',
|
||||
'hide_large': '48/hide.png',
|
||||
'home_large': '48/home.png',
|
||||
'info_large': '48/info.png',
|
||||
'list_view_large': '48/list.png',
|
||||
'log_large': '48/log.png',
|
||||
'lunix_tools_large': '48/Lunix_Tools.png',
|
||||
'new_document_large': '48/new-document.png',
|
||||
'new_folder_large': '48/new-folder.png',
|
||||
'question_mark_large': '48/question_mark.png',
|
||||
'search_large_48': '48/search.png',
|
||||
'settings_large': '48/settings.png',
|
||||
'unhide_large': '48/unhide.png',
|
||||
'usb_large': '48/usb.png',
|
||||
'warning_large_48': '48/warning.png',
|
||||
'export_large': '48/wg_export.png',
|
||||
'import_large': '48/wg_import.png',
|
||||
'message_large': '48/wg_msg.png',
|
||||
'trash_large': '48/wg_trash.png',
|
||||
'vpn_large': '48/wg_vpn.png',
|
||||
'vpn_start_large': '48/wg_vpn-start.png',
|
||||
'vpn_stop_large': '48/wg_vpn-stop.png',
|
||||
|
||||
# 64x64
|
||||
'back_extralarge': '64/arrow-left.png',
|
||||
'forward_extralarge': '64/arrow-right.png',
|
||||
'audio_large': '64/audio.png',
|
||||
'icon_view_extralarge': '64/carrel.png',
|
||||
'computer_extralarge': '64/computer.png',
|
||||
'device_extralarge': '64/device.png',
|
||||
'file_large': '64/document.png',
|
||||
'download_error_extralarge': '64/download_error.png',
|
||||
'download_extralarge': '64/download.png',
|
||||
'error_extralarge': '64/error.png',
|
||||
'python_large': '64/file-python.png',
|
||||
'documents_extralarge': '64/folder-water-documents.png',
|
||||
'downloads_extralarge': '64/folder-water-download.png',
|
||||
'music_extralarge': '64/folder-water-music.png',
|
||||
'pictures_extralarge': '64/folder-water-pictures.png',
|
||||
'folder_large': '64/folder-water.png',
|
||||
'video_extralarge_folder': '64/folder-water-video.png',
|
||||
'hide_extralarge': '64/hide.png',
|
||||
'home_extralarge': '64/home.png',
|
||||
'info_extralarge': '64/info.png',
|
||||
'list_view_extralarge': '64/list.png',
|
||||
'log_extralarge': '64/log.png',
|
||||
'lunix_tools_extralarge': '64/Lunix_Tools.png',
|
||||
'iso_large': '64/media-optical.png',
|
||||
'new_document_extralarge': '64/new-document.png',
|
||||
'new_folder_extralarge': '64/new-folder.png',
|
||||
'pdf_large': '64/pdf.png',
|
||||
'picture_large': '64/picture.png',
|
||||
'question_mark_extralarge': '64/question_mark.png',
|
||||
'recursive_large': '64/recursive.png',
|
||||
'search_large': '64/search.png',
|
||||
'settings_extralarge': '64/settings.png',
|
||||
'archive_large': '64/tar.png',
|
||||
'unhide_extralarge': '64/unhide.png',
|
||||
'usb_extralarge': '64/usb.png',
|
||||
'video_large': '64/video.png',
|
||||
'warning_large': '64/warning.png',
|
||||
'export_extralarge': '64/wg_export.png',
|
||||
'import_extralarge': '64/wg_import.png',
|
||||
'message_extralarge': '64/wg_msg.png',
|
||||
'trash_extralarge': '64/wg_trash.png',
|
||||
'vpn_extralarge': '64/wg_vpn.png',
|
||||
'vpn_start_extralarge': '64/wg_vpn-start.png',
|
||||
'vpn_stop_extralarge': '64/wg_vpn-stop.png',
|
||||
}
|
||||
|
||||
def _load_all(self):
|
||||
for key, rel_path in self.icon_paths.items():
|
||||
full_path = os.path.join(self.base_path, rel_path)
|
||||
try:
|
||||
self.icons[key] = tk.PhotoImage(file=full_path)
|
||||
except tk.TclError as e:
|
||||
print(f"Error loading icon '{key}' from '{full_path}': {e}")
|
||||
size = 32 # Default size
|
||||
if '16' in rel_path: size = 16
|
||||
elif '48' in rel_path: size = 48
|
||||
elif '64' in rel_path: size = 64
|
||||
self.icons[key] = tk.PhotoImage(width=size, height=size)
|
@@ -5,16 +5,14 @@ from tkinter import ttk
|
||||
from datetime import datetime
|
||||
import subprocess
|
||||
import json
|
||||
from shared_libs.message import MessageDialog
|
||||
from shared_libs.common_tools import IconManager, Tooltip
|
||||
|
||||
# Helper to make icon paths robust, so the script can be run from anywhere
|
||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
MAX_ITEMS_TO_DISPLAY = 1000
|
||||
|
||||
|
||||
def get_icon_path(icon_name):
|
||||
return os.path.join(SCRIPT_DIR, icon_name)
|
||||
|
||||
|
||||
def get_xdg_user_dir(dir_key, fallback_name):
|
||||
home = os.path.expanduser("~")
|
||||
fallback_path = os.path.join(home, fallback_name)
|
||||
@@ -38,47 +36,6 @@ def get_xdg_user_dir(dir_key, fallback_name):
|
||||
return fallback_path
|
||||
|
||||
|
||||
class Tooltip:
|
||||
def __init__(self, widget, text, wraplength=250):
|
||||
self.widget = widget
|
||||
self.text = text
|
||||
self.wraplength = wraplength
|
||||
self.tooltip_window = None
|
||||
self.id = None
|
||||
self.widget.bind("<Enter>", self.enter)
|
||||
self.widget.bind("<Leave>", self.leave)
|
||||
self.widget.bind("<ButtonPress>", self.leave)
|
||||
|
||||
def enter(self, event=None): self.schedule()
|
||||
def leave(self, event=None): self.unschedule(); self.hide_tooltip()
|
||||
|
||||
def schedule(self): self.unschedule(
|
||||
); self.id = self.widget.after(250, self.show_tooltip)
|
||||
|
||||
def unschedule(self):
|
||||
id = self.id
|
||||
self.id = None
|
||||
if id:
|
||||
self.widget.after_cancel(id)
|
||||
|
||||
def show_tooltip(self, event=None):
|
||||
x, y, _, _ = self.widget.bbox("insert")
|
||||
x += self.widget.winfo_rootx() + 25
|
||||
y += self.widget.winfo_rooty() + 20
|
||||
self.tooltip_window = tw = tk.Toplevel(self.widget)
|
||||
tw.wm_overrideredirect(True)
|
||||
tw.wm_geometry(f"+{x}+{y}")
|
||||
label = ttk.Label(tw, text=self.text, justify=tk.LEFT, background="#FFFFE0", foreground="black",
|
||||
relief=tk.SOLID, borderwidth=1, wraplength=self.wraplength, padding=(4, 2, 4, 2))
|
||||
label.pack(ipadx=1)
|
||||
|
||||
def hide_tooltip(self):
|
||||
tw = self.tooltip_window
|
||||
self.tooltip_window = None
|
||||
if tw:
|
||||
tw.destroy()
|
||||
|
||||
|
||||
class CustomFileDialog(tk.Toplevel):
|
||||
def __init__(self, parent, initial_dir=None, filetypes=None):
|
||||
super().__init__(parent)
|
||||
@@ -100,56 +57,18 @@ class CustomFileDialog(tk.Toplevel):
|
||||
self.show_hidden_files = tk.BooleanVar(value=False)
|
||||
self.resize_job = None
|
||||
self.last_width = 0
|
||||
self.sidebar_buttons = []
|
||||
self.device_buttons = []
|
||||
self.search_results = [] # Store search results
|
||||
self.search_mode = False # Track if in search mode
|
||||
self.original_path_text = "" # Store original path text
|
||||
|
||||
self.load_icons()
|
||||
self.icon_manager = IconManager()
|
||||
self.icons = self.icon_manager.icons
|
||||
self.create_styles()
|
||||
self.create_widgets()
|
||||
self.navigate_to(self.current_dir)
|
||||
|
||||
def load_icons(self):
|
||||
self.icons = {}
|
||||
icon_files = {
|
||||
'computer_small': '/usr/share/icons/lx-icons/32/computer-32.png',
|
||||
'computer_large': '/usr/share/icons/lx-icons/48/computer-48.png',
|
||||
'device_small': '/usr/share/icons/lx-icons/32/device-32.png',
|
||||
'device_large': '/usr/share/icons/lx-icons/48/device-48.png',
|
||||
'usb_small': '/usr/share/icons/lx-icons/32/usb-32.png',
|
||||
'usb_large': '/usr/share/icons/lx-icons/48/usb-48.png',
|
||||
'downloads_small': '/usr/share/icons/lx-icons/32/folder-water-download-32.png',
|
||||
'downloads_large': '/usr/share/icons/lx-icons/48/folder-water-download-48.png',
|
||||
'documents_small': '/usr/share/icons/lx-icons/32/folder-water-documents-32.png',
|
||||
'documents_large': '/usr/share/icons/lx-icons/48/folder-water-documents-48.png',
|
||||
'pictures_small': '/usr/share/icons/lx-icons/32/folder-water-pictures-32.png',
|
||||
'pictures_large': '/usr/share/icons/lx-icons/48/folder-water-pictures-48.png',
|
||||
'music_small': '/usr/share/icons/lx-icons/32/folder-water-music-32.png',
|
||||
'music_large': '/usr/share/icons/lx-icons/48/folder-water-music-48.png',
|
||||
'video_small': '/usr/share/icons/lx-icons/32/folder-water-video-32.png',
|
||||
'video_large_folder': '/usr/share/icons/lx-icons/48/folder-water-video-48.png',
|
||||
'warning_small': '/usr/share/icons/lx-icons/32/warning.png', 'warning_large': '/usr/share/icons/lx-icons/64/warning.png',
|
||||
'folder_large': '/usr/share/icons/lx-icons/64/folder-water-64.png', 'file_large': '/usr/share/icons/lx-icons/64/document-64.png',
|
||||
'python_large': '/usr/share/icons/lx-icons/64/file-python-64.png', 'pdf_large': '/usr/share/icons/lx-icons/64/pdf-64.png',
|
||||
'archive_large': '/usr/share/icons/lx-icons/64/tar-64.png', 'audio_large': '/usr/share/icons/lx-icons/64/audio-64.png',
|
||||
'video_large': '/usr/share/icons/lx-icons/64/video-64.png', 'picture_large': '/usr/share/icons/lx-icons/64/picture-64.png',
|
||||
'iso_large': '/usr/share/icons/lx-icons/64/media-optical-64.png', 'folder_small': '/usr/share/icons/lx-icons/32/folder-water-32.png',
|
||||
'file_small': '/usr/share/icons/lx-icons/32/document-32.png', 'python_small': '/usr/share/icons/lx-icons/32/file-python-32.png',
|
||||
'pdf_small': '/usr/share/icons/lx-icons/32/pdf-32.png', 'archive_small': '/usr/share/icons/lx-icons/32/tar-32.png',
|
||||
'audio_small': '/usr/share/icons/lx-icons/32/audio-32.png', 'video_small_file': '/usr/share/icons/lx-icons/32/video-32.png',
|
||||
'picture_small': '/usr/share/icons/lx-icons/32/picture-32.png', 'iso_small': '/usr/share/icons/lx-icons/32/media-optical-32.png',
|
||||
'list_view': '/usr/share/icons/lx-icons/32/list-32.png',
|
||||
'icon_view': '/usr/share/icons/lx-icons/32/carrel-32.png',
|
||||
'hide': '/usr/share/icons/lx-icons/32/hide-32.png',
|
||||
'unhide': '/usr/share/icons/lx-icons/32/unhide-32.png',
|
||||
'back': '/usr/share/icons/lx-icons/32/arrow-left-32.png',
|
||||
'forward': '/usr/share/icons/lx-icons/32/arrow-right-32.png',
|
||||
'home': '/usr/share/icons/lx-icons/32/home-32.png'
|
||||
}
|
||||
for key, filename in icon_files.items():
|
||||
try:
|
||||
self.icons[key] = tk.PhotoImage(file=get_icon_path(filename))
|
||||
except tk.TclError:
|
||||
size = 32 if 'small' in key or 'view' in key or 'hide' in key or 'unhide' in key or 'back' in key or 'forward' in key or 'home' in key else 64
|
||||
self.icons[key] = tk.PhotoImage(width=size, height=size)
|
||||
|
||||
def get_file_icon(self, filename, size='large'):
|
||||
ext = os.path.splitext(filename)[1].lower()
|
||||
if ext == '.svg':
|
||||
@@ -207,6 +126,17 @@ class CustomFileDialog(tk.Toplevel):
|
||||
style.map("Header.TButton.Borderless.Round", background=[
|
||||
('active', self.hover_extrastyle)])
|
||||
|
||||
# Style for active/pressed header buttons
|
||||
style.configure("Header.TButton.Active.Round",
|
||||
background=self.selection_color)
|
||||
|
||||
# Copy layout from the base style
|
||||
style.layout("Header.TButton.Active.Round",
|
||||
style.layout("Header.TButton.Borderless.Round"))
|
||||
|
||||
style.map("Header.TButton.Active.Round", background=[
|
||||
('active', self.selection_color)])
|
||||
|
||||
style.configure("Dark.TButton.Borderless", anchor="w",
|
||||
background=self.sidebar_color, foreground=self.color_foreground, padding=(20, 5, 0, 5))
|
||||
|
||||
@@ -278,21 +208,43 @@ class CustomFileDialog(tk.Toplevel):
|
||||
self.path_entry.bind(
|
||||
"<Return>", lambda e: self.navigate_to(self.path_entry.get()))
|
||||
|
||||
# View switch and hidden files button
|
||||
# Search, view switch and hidden files button
|
||||
right_top_bar_frame = ttk.Frame(top_bar, style='Accent.TFrame')
|
||||
right_top_bar_frame.grid(row=0, column=2, sticky="e")
|
||||
|
||||
# Search button and options container
|
||||
search_container = ttk.Frame(
|
||||
right_top_bar_frame, style='Accent.TFrame')
|
||||
search_container.pack(side="left", padx=(0, 10))
|
||||
|
||||
self.search_button = ttk.Button(search_container, image=self.icons['search_small'],
|
||||
command=self.toggle_search_mode, style="Header.TButton.Borderless.Round")
|
||||
self.search_button.pack(side="left")
|
||||
Tooltip(self.search_button, "Suchen")
|
||||
|
||||
# Search options frame (initially hidden, next to search button)
|
||||
self.search_options_frame = ttk.Frame(
|
||||
search_container, style='Accent.TFrame')
|
||||
|
||||
# Recursive search toggle button
|
||||
self.recursive_search = tk.BooleanVar(value=True)
|
||||
self.recursive_button = ttk.Button(self.search_options_frame, image=self.icons['recursive_small'],
|
||||
command=self.toggle_recursive_search,
|
||||
style="Header.TButton.Active.Round")
|
||||
self.recursive_button.pack(side="left", padx=2)
|
||||
Tooltip(self.recursive_button, "Rekursive Suche ein/ausschalten")
|
||||
|
||||
view_switch = ttk.Frame(right_top_bar_frame,
|
||||
padding=(5, 0), style='Accent.TFrame')
|
||||
view_switch.pack(side="left")
|
||||
|
||||
self.icon_view_button = ttk.Button(view_switch, image=self.icons['icon_view'], command=lambda: (
|
||||
self.view_mode.set("icons"), self.populate_files()), style="Header.TButton.Borderless.Round")
|
||||
self.icon_view_button = ttk.Button(view_switch, image=self.icons['icon_view'],
|
||||
command=self.set_icon_view, style="Header.TButton.Active.Round")
|
||||
self.icon_view_button.pack(side="left", padx=(50, 10))
|
||||
Tooltip(self.icon_view_button, "Kachelansicht")
|
||||
|
||||
self.list_view_button = ttk.Button(view_switch, image=self.icons['list_view'], command=lambda: (
|
||||
self.view_mode.set("list"), self.populate_files()), style="Header.TButton.Borderless.Round")
|
||||
self.list_view_button = ttk.Button(view_switch, image=self.icons['list_view'],
|
||||
command=self.set_list_view, style="Header.TButton.Borderless.Round")
|
||||
self.list_view_button.pack(side="left")
|
||||
Tooltip(self.list_view_button, "Listenansicht")
|
||||
|
||||
@@ -312,14 +264,14 @@ class CustomFileDialog(tk.Toplevel):
|
||||
|
||||
# Sidebar
|
||||
sidebar_frame = ttk.Frame(
|
||||
paned_window, style="Sidebar.TFrame", padding=(0, 0, 0, 0))
|
||||
paned_window, style="Sidebar.TFrame", padding=(0, 0, 0, 0), width=200)
|
||||
# Prevent content from resizing the frame
|
||||
# sidebar_frame.grid_propagate(False)
|
||||
sidebar_frame.grid_propagate(False)
|
||||
sidebar_frame.bind("<Configure>", self.on_sidebar_resize)
|
||||
# Use weight=0 to give it a fixed size
|
||||
paned_window.add(sidebar_frame, weight=0)
|
||||
|
||||
sidebar_frame.grid_rowconfigure(2, weight=1)
|
||||
|
||||
# No weight on any row - let storage stay at bottom
|
||||
sidebar_buttons_frame = ttk.Frame(
|
||||
sidebar_frame, style="Sidebar.TFrame", padding=(0, 15, 0, 0))
|
||||
sidebar_buttons_frame.grid(
|
||||
@@ -342,43 +294,121 @@ class CustomFileDialog(tk.Toplevel):
|
||||
btn = ttk.Button(sidebar_buttons_frame, text=f" {config['name']}", image=config['icon'],
|
||||
compound="left", command=lambda p=config['path']: self.navigate_to(p), style="Dark.TButton.Borderless")
|
||||
btn.pack(fill="x", pady=1)
|
||||
self.sidebar_buttons.append((btn, f" {config['name']}"))
|
||||
|
||||
# Horizontal separator
|
||||
separator_color = "#a9a9a9" if self.is_dark else "#7c7c7c"
|
||||
tk.Frame(sidebar_frame, height=1, bg=separator_color).grid(
|
||||
row=1, column=0, sticky="ew", padx=20, pady=15)
|
||||
|
||||
# Mounted devices
|
||||
# Mounted devices with scrollable frame
|
||||
mounted_devices_frame = ttk.Frame(
|
||||
sidebar_frame, style="Sidebar.TFrame")
|
||||
mounted_devices_frame.grid(row=2, column=0, sticky="nsew", padx=10)
|
||||
ttk.Label(mounted_devices_frame, text="Geräte:", background=self.sidebar_color,
|
||||
foreground=self.color_foreground).pack(fill="x", padx=10, pady=(5, 0))
|
||||
# Don't expand devices frame so storage stays in position
|
||||
mounted_devices_frame.grid_columnconfigure(0, weight=1)
|
||||
|
||||
ttk.Label(mounted_devices_frame, text="Geräte:", background=self.sidebar_color,
|
||||
foreground=self.color_foreground).grid(row=0, column=0, sticky="ew", padx=10, pady=(5, 0))
|
||||
|
||||
# Create scrollable canvas for devices
|
||||
self.devices_canvas = tk.Canvas(mounted_devices_frame, highlightthickness=0,
|
||||
bg=self.sidebar_color, height=150, width=180)
|
||||
self.devices_scrollbar = ttk.Scrollbar(mounted_devices_frame, orient="vertical",
|
||||
command=self.devices_canvas.yview)
|
||||
self.devices_canvas.configure(
|
||||
yscrollcommand=self.devices_scrollbar.set)
|
||||
|
||||
self.devices_canvas.grid(row=1, column=0, sticky="nsew")
|
||||
# Scrollbar initially hidden
|
||||
|
||||
# Create scrollable frame inside canvas
|
||||
self.devices_scrollable_frame = ttk.Frame(
|
||||
self.devices_canvas, style="Sidebar.TFrame")
|
||||
self.devices_canvas_window = self.devices_canvas.create_window(
|
||||
(0, 0), window=self.devices_scrollable_frame, anchor="nw")
|
||||
|
||||
# Bind events for showing/hiding scrollbar on hover
|
||||
self.devices_canvas.bind("<Enter>", self._on_devices_enter)
|
||||
self.devices_canvas.bind("<Leave>", self._on_devices_leave)
|
||||
self.devices_scrollable_frame.bind("<Enter>", self._on_devices_enter)
|
||||
self.devices_scrollable_frame.bind("<Leave>", self._on_devices_leave)
|
||||
|
||||
# Bind canvas width to scrollable frame width
|
||||
def _configure_devices_canvas(event):
|
||||
self.devices_canvas.configure(
|
||||
scrollregion=self.devices_canvas.bbox("all"))
|
||||
canvas_width = event.width
|
||||
self.devices_canvas.itemconfig(
|
||||
self.devices_canvas_window, width=canvas_width)
|
||||
|
||||
self.devices_scrollable_frame.bind("<Configure>", lambda e: self.devices_canvas.configure(
|
||||
scrollregion=self.devices_canvas.bbox("all")))
|
||||
self.devices_canvas.bind("<Configure>", _configure_devices_canvas)
|
||||
|
||||
# Mouse wheel scrolling for devices area
|
||||
def _on_devices_mouse_wheel(event):
|
||||
if event.num == 4: # Scroll up on Linux
|
||||
delta = -1
|
||||
elif event.num == 5: # Scroll down on Linux
|
||||
delta = 1
|
||||
else: # MouseWheel event for Windows/macOS
|
||||
delta = -1 * int(event.delta / 120)
|
||||
self.devices_canvas.yview_scroll(delta, "units")
|
||||
|
||||
# Bind mouse wheel to canvas and scrollable frame
|
||||
for widget in [self.devices_canvas, self.devices_scrollable_frame]:
|
||||
widget.bind("<MouseWheel>", _on_devices_mouse_wheel)
|
||||
widget.bind("<Button-4>", _on_devices_mouse_wheel)
|
||||
widget.bind("<Button-5>", _on_devices_mouse_wheel)
|
||||
|
||||
# Populate devices
|
||||
for device_name, mount_point, removable in self._get_mounted_devices():
|
||||
icon = self.icons['usb_small'] if removable else self.icons['device_small']
|
||||
button_text = f" {device_name}"
|
||||
if len(device_name) > 15: # Static wrapping for long names
|
||||
button_text = f" {device_name[:15]}\n{device_name[15:]}"
|
||||
|
||||
btn = ttk.Button(mounted_devices_frame, text=button_text, image=icon,
|
||||
btn = ttk.Button(self.devices_scrollable_frame, text=button_text, image=icon,
|
||||
compound="left", command=lambda p=mount_point: self.navigate_to(p), style="Dark.TButton.Borderless")
|
||||
btn.pack(fill="x", pady=1)
|
||||
self.device_buttons.append((btn, button_text))
|
||||
|
||||
# Bind mouse wheel to device buttons too
|
||||
btn.bind("<MouseWheel>", _on_devices_mouse_wheel)
|
||||
btn.bind("<Button-4>", _on_devices_mouse_wheel)
|
||||
btn.bind("<Button-5>", _on_devices_mouse_wheel)
|
||||
|
||||
# Bind hover events for scrollbar visibility
|
||||
btn.bind("<Enter>", self._on_devices_enter)
|
||||
btn.bind("<Leave>", self._on_devices_leave)
|
||||
|
||||
try:
|
||||
total, used, _ = shutil.disk_usage(mount_point)
|
||||
progress_bar = ttk.Progressbar(
|
||||
mounted_devices_frame, orient="horizontal", length=100, mode="determinate", style='Small.Horizontal.TProgressbar')
|
||||
self.devices_scrollable_frame, orient="horizontal", length=100, mode="determinate", style='Small.Horizontal.TProgressbar')
|
||||
progress_bar.pack(fill="x", pady=(2, 8), padx=25)
|
||||
progress_bar['value'] = (used / total) * 100
|
||||
|
||||
# Bind mouse wheel to progress bars too
|
||||
progress_bar.bind("<MouseWheel>", _on_devices_mouse_wheel)
|
||||
progress_bar.bind("<Button-4>", _on_devices_mouse_wheel)
|
||||
progress_bar.bind("<Button-5>", _on_devices_mouse_wheel)
|
||||
|
||||
# Bind hover events for scrollbar visibility
|
||||
progress_bar.bind("<Enter>", self._on_devices_enter)
|
||||
progress_bar.bind("<Leave>", self._on_devices_leave)
|
||||
except (FileNotFoundError, PermissionError):
|
||||
# In case of errors (e.g., unreadable drive), just skip the progress bar
|
||||
pass
|
||||
|
||||
# Separator before storage
|
||||
tk.Frame(sidebar_frame, height=1, bg=separator_color).grid(
|
||||
row=3, column=0, sticky="ew", padx=20, pady=15)
|
||||
|
||||
# Storage section at bottom - use pack instead of grid to stay at bottom
|
||||
storage_frame = ttk.Frame(sidebar_frame, style="Sidebar.TFrame")
|
||||
storage_frame.grid(row=4, column=0, sticky="ew", padx=10)
|
||||
storage_frame.grid(row=4, column=0, sticky="sew", padx=10, pady=10)
|
||||
self.storage_label = ttk.Label(
|
||||
storage_frame, text="Freier Speicher:", background=self.freespace_background)
|
||||
self.storage_label.pack(fill="x", padx=10)
|
||||
@@ -441,6 +471,280 @@ class CustomFileDialog(tk.Toplevel):
|
||||
self.resize_job = self.after(200, self.populate_files)
|
||||
self.last_width = new_width
|
||||
|
||||
def on_sidebar_resize(self, event):
|
||||
current_width = event.width
|
||||
# Define a threshold for when to hide/show text
|
||||
threshold_width = 100 # Adjust this value as needed
|
||||
|
||||
if current_width < threshold_width:
|
||||
# Hide text, show only icons
|
||||
for btn, original_text in self.sidebar_buttons:
|
||||
btn.config(text="", compound="top")
|
||||
for btn, original_text in self.device_buttons:
|
||||
btn.config(text="", compound="top")
|
||||
else:
|
||||
# Show text
|
||||
for btn, original_text in self.sidebar_buttons:
|
||||
btn.config(text=original_text, compound="left")
|
||||
for btn, original_text in self.device_buttons:
|
||||
btn.config(text=original_text, compound="left")
|
||||
|
||||
def _on_devices_enter(self, event):
|
||||
"""Show scrollbar when mouse enters devices area"""
|
||||
self.devices_scrollbar.grid(row=1, column=1, sticky="ns")
|
||||
|
||||
def _on_devices_leave(self, event):
|
||||
"""Hide scrollbar when mouse leaves devices area"""
|
||||
# Check if mouse is really leaving the devices area
|
||||
x, y = event.x_root, event.y_root
|
||||
widget_x = self.devices_canvas.winfo_rootx()
|
||||
widget_y = self.devices_canvas.winfo_rooty()
|
||||
widget_width = self.devices_canvas.winfo_width()
|
||||
widget_height = self.devices_canvas.winfo_height()
|
||||
|
||||
# Add small buffer to prevent flickering
|
||||
buffer = 5
|
||||
if not (widget_x - buffer <= x <= widget_x + widget_width + buffer and
|
||||
widget_y - buffer <= y <= widget_y + widget_height + buffer):
|
||||
self.devices_scrollbar.grid_remove()
|
||||
|
||||
def toggle_search_mode(self):
|
||||
"""Toggle between search mode and normal mode"""
|
||||
if not self.search_mode:
|
||||
# Enter search mode
|
||||
self.search_mode = True
|
||||
self.original_path_text = self.path_entry.get()
|
||||
self.path_entry.delete(0, tk.END)
|
||||
self.path_entry.insert(0, "Suchbegriff eingeben...")
|
||||
self.path_entry.bind("<Return>", self.execute_search)
|
||||
self.path_entry.bind("<FocusIn>", self.clear_search_placeholder)
|
||||
|
||||
# Show search options
|
||||
self.search_options_frame.pack(side="left", padx=(5, 0))
|
||||
else:
|
||||
# Exit search mode
|
||||
self.search_mode = False
|
||||
self.path_entry.delete(0, tk.END)
|
||||
self.path_entry.insert(0, self.original_path_text)
|
||||
self.path_entry.bind(
|
||||
"<Return>", lambda e: self.navigate_to(self.path_entry.get()))
|
||||
self.path_entry.unbind("<FocusIn>")
|
||||
|
||||
# Hide search options
|
||||
self.search_options_frame.pack_forget()
|
||||
|
||||
# Return to normal file view
|
||||
self.populate_files()
|
||||
|
||||
def toggle_recursive_search(self):
|
||||
"""Toggle recursive search on/off and update button style"""
|
||||
self.recursive_search.set(not self.recursive_search.get())
|
||||
if self.recursive_search.get():
|
||||
self.recursive_button.configure(
|
||||
style="Header.TButton.Active.Round")
|
||||
else:
|
||||
self.recursive_button.configure(
|
||||
style="Header.TButton.Borderless.Round")
|
||||
|
||||
def set_icon_view(self):
|
||||
"""Set icon view and update button styles"""
|
||||
self.view_mode.set("icons")
|
||||
self.icon_view_button.configure(style="Header.TButton.Active.Round")
|
||||
self.list_view_button.configure(
|
||||
style="Header.TButton.Borderless.Round")
|
||||
self.populate_files()
|
||||
|
||||
def set_list_view(self):
|
||||
"""Set list view and update button styles"""
|
||||
self.view_mode.set("list")
|
||||
self.list_view_button.configure(style="Header.TButton.Active.Round")
|
||||
self.icon_view_button.configure(
|
||||
style="Header.TButton.Borderless.Round")
|
||||
self.populate_files()
|
||||
|
||||
def clear_search_placeholder(self, event):
|
||||
"""Clear placeholder text when focus enters search field"""
|
||||
if self.path_entry.get() == "Suchbegriff eingeben...":
|
||||
self.path_entry.delete(0, tk.END)
|
||||
|
||||
def execute_search(self, event):
|
||||
"""Execute search when Enter is pressed in search mode"""
|
||||
search_term = self.path_entry.get().strip()
|
||||
if not search_term or search_term == "Suchbegriff eingeben...":
|
||||
return
|
||||
|
||||
# Clear previous search results
|
||||
self.search_results.clear()
|
||||
|
||||
# Determine search directories
|
||||
search_dirs = [self.current_dir]
|
||||
|
||||
# If searching from home directory, also include XDG directories
|
||||
home_dir = os.path.expanduser("~")
|
||||
if os.path.abspath(self.current_dir) == os.path.abspath(home_dir):
|
||||
xdg_dirs = [
|
||||
get_xdg_user_dir("XDG_DOWNLOAD_DIR", "Downloads"),
|
||||
get_xdg_user_dir("XDG_DOCUMENTS_DIR", "Documents"),
|
||||
get_xdg_user_dir("XDG_PICTURES_DIR", "Pictures"),
|
||||
get_xdg_user_dir("XDG_MUSIC_DIR", "Music"),
|
||||
get_xdg_user_dir("XDG_VIDEO_DIR", "Videos")
|
||||
]
|
||||
# Add XDG directories that exist and are not already in home
|
||||
for xdg_dir in xdg_dirs:
|
||||
if (os.path.exists(xdg_dir) and
|
||||
os.path.abspath(xdg_dir) != os.path.abspath(home_dir) and
|
||||
xdg_dir not in search_dirs):
|
||||
search_dirs.append(xdg_dir)
|
||||
|
||||
try:
|
||||
all_files = []
|
||||
|
||||
# Search in each directory
|
||||
for search_dir in search_dirs:
|
||||
if not os.path.exists(search_dir):
|
||||
continue
|
||||
|
||||
# Change to directory and use relative paths to avoid path issues
|
||||
original_cwd = os.getcwd()
|
||||
try:
|
||||
os.chdir(search_dir)
|
||||
|
||||
# Build find command based on recursive setting (use . for current directory)
|
||||
if self.recursive_search.get():
|
||||
find_cmd = ['find', '.', '-iname',
|
||||
f'*{search_term}*', '-type', 'f']
|
||||
else:
|
||||
find_cmd = ['find', '.', '-maxdepth', '1',
|
||||
'-iname', f'*{search_term}*', '-type', 'f']
|
||||
|
||||
result = subprocess.run(
|
||||
find_cmd, capture_output=True, text=True, timeout=30)
|
||||
|
||||
if result.returncode == 0:
|
||||
files = result.stdout.strip().split('\n')
|
||||
# Convert relative paths back to absolute paths
|
||||
directory_files = []
|
||||
for f in files:
|
||||
if f and f.startswith('./'):
|
||||
abs_path = os.path.join(
|
||||
search_dir, f[2:]) # Remove './' prefix
|
||||
if os.path.isfile(abs_path):
|
||||
directory_files.append(abs_path)
|
||||
all_files.extend(directory_files)
|
||||
|
||||
finally:
|
||||
os.chdir(original_cwd)
|
||||
|
||||
# Remove duplicates while preserving order
|
||||
seen = set()
|
||||
unique_files = []
|
||||
for file_path in all_files:
|
||||
if file_path not in seen:
|
||||
seen.add(file_path)
|
||||
unique_files.append(file_path)
|
||||
|
||||
# Filter based on currently selected filter pattern
|
||||
self.search_results = []
|
||||
for file_path in unique_files:
|
||||
filename = os.path.basename(file_path)
|
||||
if self._matches_filetype(filename):
|
||||
self.search_results.append(file_path)
|
||||
|
||||
# Show search results in TreeView
|
||||
if self.search_results:
|
||||
self.show_search_results_treeview()
|
||||
else:
|
||||
MessageDialog(
|
||||
message_type="info",
|
||||
text=f"Keine Dateien mit '{search_term}' gefunden.",
|
||||
title="Suche",
|
||||
master=self
|
||||
).show()
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
MessageDialog(
|
||||
message_type="error",
|
||||
text="Suche dauert zu lange und wurde abgebrochen.",
|
||||
title="Suche",
|
||||
master=self
|
||||
).show()
|
||||
except Exception as e:
|
||||
MessageDialog(
|
||||
message_type="error",
|
||||
text=f"Fehler bei der Suche: {e}",
|
||||
title="Suchfehler",
|
||||
master=self
|
||||
).show()
|
||||
|
||||
def show_search_results_treeview(self):
|
||||
"""Show search results in TreeView format"""
|
||||
# Clear current file list and replace with search results
|
||||
for widget in self.file_list_frame.winfo_children():
|
||||
widget.destroy()
|
||||
|
||||
# Create TreeView for search results
|
||||
tree_frame = ttk.Frame(self.file_list_frame)
|
||||
tree_frame.pack(fill='both', expand=True)
|
||||
tree_frame.grid_rowconfigure(0, weight=1)
|
||||
tree_frame.grid_columnconfigure(0, weight=1)
|
||||
|
||||
columns = ("path", "size", "modified")
|
||||
search_tree = ttk.Treeview(
|
||||
tree_frame, columns=columns, show="tree headings")
|
||||
|
||||
# Configure columns
|
||||
search_tree.heading("#0", text="Dateiname", anchor="w")
|
||||
search_tree.column("#0", anchor="w", width=200, stretch=True)
|
||||
search_tree.heading("path", text="Pfad", anchor="w")
|
||||
search_tree.column("path", anchor="w", width=300, stretch=True)
|
||||
search_tree.heading("size", text="Größe", anchor="e")
|
||||
search_tree.column("size", anchor="e", width=100, stretch=False)
|
||||
search_tree.heading("modified", text="Geändert am", anchor="w")
|
||||
search_tree.column("modified", anchor="w", width=160, stretch=False)
|
||||
|
||||
# Add scrollbars
|
||||
v_scrollbar = ttk.Scrollbar(
|
||||
tree_frame, orient="vertical", command=search_tree.yview)
|
||||
h_scrollbar = ttk.Scrollbar(
|
||||
tree_frame, orient="horizontal", command=search_tree.xview)
|
||||
search_tree.configure(yscrollcommand=v_scrollbar.set,
|
||||
xscrollcommand=h_scrollbar.set)
|
||||
|
||||
search_tree.grid(row=0, column=0, sticky='nsew')
|
||||
v_scrollbar.grid(row=0, column=1, sticky='ns')
|
||||
h_scrollbar.grid(row=1, column=0, sticky='ew')
|
||||
|
||||
# Populate with search results
|
||||
for file_path in self.search_results:
|
||||
try:
|
||||
filename = os.path.basename(file_path)
|
||||
directory = os.path.dirname(file_path)
|
||||
stat = os.stat(file_path)
|
||||
size = self._format_size(stat.st_size)
|
||||
modified_time = datetime.fromtimestamp(
|
||||
stat.st_mtime).strftime('%d.%m.%Y %H:%M')
|
||||
|
||||
icon = self.get_file_icon(filename, 'small')
|
||||
search_tree.insert("", "end", text=f" {filename}", image=icon,
|
||||
values=(directory, size, modified_time))
|
||||
except (FileNotFoundError, PermissionError):
|
||||
continue
|
||||
|
||||
# Bind double-click to select file
|
||||
def on_search_double_click(event):
|
||||
selection = search_tree.selection()
|
||||
if selection:
|
||||
item = search_tree.item(selection[0])
|
||||
filename = item['text'].strip()
|
||||
directory = item['values'][0]
|
||||
full_path = os.path.join(directory, filename)
|
||||
|
||||
# Select the file and close dialog
|
||||
self.selected_file = full_path
|
||||
self.destroy()
|
||||
|
||||
search_tree.bind("<Double-1>", on_search_double_click)
|
||||
|
||||
def _unbind_mouse_wheel_events(self):
|
||||
# Unbind all mouse wheel events from the root window
|
||||
self.unbind_all("<MouseWheel>")
|
||||
@@ -811,12 +1115,9 @@ class CustomFileDialog(tk.Toplevel):
|
||||
name = block_device.get('name')
|
||||
mountpoint = block_device.get('mountpoint')
|
||||
label = block_device.get('label')
|
||||
# size = block_device.get('size')
|
||||
removable = block_device.get('rm', False)
|
||||
|
||||
display_name = label if label else name
|
||||
# if size:
|
||||
# display_name += f" ({size})"
|
||||
devices.append((display_name, mountpoint, removable))
|
||||
|
||||
# Process children (partitions)
|
||||
@@ -833,12 +1134,9 @@ class CustomFileDialog(tk.Toplevel):
|
||||
name = child_device.get('name')
|
||||
mountpoint = child_device.get('mountpoint')
|
||||
label = child_device.get('label')
|
||||
# size = child_device.get('size')
|
||||
removable = child_device.get('rm', False)
|
||||
|
||||
display_name = label if label else name
|
||||
# if size:
|
||||
# display_name += f" ({size})"
|
||||
devices.append(
|
||||
(display_name, mountpoint, removable))
|
||||
|
||||
|
7
mainwindow.py
Normal file → Executable file
7
mainwindow.py
Normal file → Executable file
@@ -32,10 +32,7 @@ class GlotzMol(tk.Tk):
|
||||
|
||||
dialog = CustomFileDialog(self,
|
||||
initial_dir=os.path.expanduser("~"),
|
||||
filetypes=[("Alle Dateien", "*.*"),
|
||||
("Audio-Dateien", "*.mp3 *.wav"),
|
||||
("Video-Dateien", "*.mkv *.mp4"),
|
||||
("ISO-Images", "*.iso"),
|
||||
filetypes=[("Wireguard Files (.conf)", "*.conf"),
|
||||
])
|
||||
|
||||
# This is the crucial part: wait for the dialog to be closed
|
||||
@@ -58,7 +55,7 @@ if __name__ == "__main__":
|
||||
style = ttk.Style(root)
|
||||
root.tk.call('source', f"{theme_path}/water.tcl")
|
||||
try:
|
||||
root.tk.call('set_theme', 'light')
|
||||
root.tk.call('set_theme', 'dark')
|
||||
except tk.TclError:
|
||||
pass
|
||||
root.mainloop()
|
||||
|
Reference in New Issue
Block a user