commit 57

This commit is contained in:
2025-08-05 18:29:58 +02:00
parent 160d8acafb
commit c8db431c06
7 changed files with 228 additions and 138 deletions

View File

@@ -62,7 +62,8 @@ class CfdConfigManager:
"default_view_mode": "icons", # 'icons' or 'list'
"search_hidden_files": False, # True or False
"use_trash": False, # True or False
"confirm_delete": False # True or False
"confirm_delete": False, # True or False
"recursive_search": True
}
@classmethod

View File

@@ -175,11 +175,6 @@ class WidgetManager:
# Function to create search widgets
def create_search_widgets(parent_frame):
container = ttk.Frame(parent_frame, style='Accent.TFrame')
self.search_button = ttk.Button(container, image=self.dialog.icon_manager.get_icon(
'search_small'), command=self.dialog.toggle_search_mode, style="Header.TButton.Borderless.Round")
self.search_button.pack(side="left")
Tooltip(self.search_button, "Suchen")
self.recursive_button = ttk.Button(container, image=self.dialog.icon_manager.get_icon(
'recursive_small'), command=self.dialog.toggle_recursive_search, style="Header.TButton.Active.Round")
self.recursive_button.pack(side="left", padx=2)
@@ -400,92 +395,87 @@ class WidgetManager:
self.file_list_frame.grid(row=0, column=0, sticky="nsew")
self.dialog.bind("<Configure>", self.dialog.on_window_resize)
# --- Bottom Bar ---
# This frame will contain the action buttons and status bar
self.action_status_frame = ttk.Frame(
content_frame, style="AccentBottom.TFrame")
self.action_status_frame.grid(
row=1, column=0, sticky="ew", pady=(5, 10), padx=10)
self.action_status_frame = ttk.Frame(content_frame, style="AccentBottom.TFrame")
self.action_status_frame.grid(row=1, column=0, sticky="ew", pady=(5, 10), padx=10)
button_box_pos = self.settings.get("button_box_pos", "left")
# Create three main containers for alignment
self.left_container = ttk.Frame(self.action_status_frame, style="AccentBottom.TFrame")
self.center_container = ttk.Frame(self.action_status_frame, style="AccentBottom.TFrame")
self.right_container = ttk.Frame(self.action_status_frame, style="AccentBottom.TFrame")
# Configure columns for the action_status_frame
if button_box_pos == 'left':
self.action_status_frame.grid_columnconfigure(1, weight=1)
else:
self.action_status_frame.grid_columnconfigure(1, weight=1)
# Configure the containers' expansion behavior
self.action_status_frame.grid_columnconfigure(0, weight=0) # Left container
self.action_status_frame.grid_columnconfigure(1, weight=1) # Center container (expands)
self.action_status_frame.grid_columnconfigure(2, weight=0) # Right container
# Status bar will be placed inside the action_status_frame
self.status_bar = ttk.Label(
self.action_status_frame, text="", anchor="w", style="AccentBottom.TLabel")
self.left_container.grid(row=0, column=0, sticky='w')
self.center_container.grid(row=0, column=1, sticky='ew')
self.right_container.grid(row=0, column=2, sticky='e')
self.settings_button = ttk.Button(self.action_status_frame, image=self.dialog.icon_manager.get_icon(
'settings-2_small'), command=self.dialog.open_settings_dialog, style="Bottom.TButton.Borderless.Round")
self.trash_button = ttk.Button(self.action_status_frame, image=self.dialog.icon_manager.get_icon(
'trash_small2'), command=self.dialog.delete_selected_item, style="Bottom.TButton.Borderless.Round")
# --- Define Widgets (All belong to action_status_frame) ---
self.status_bar = ttk.Label(self.action_status_frame, text="", anchor="w", style="AccentBottom.TLabel")
self.search_entry = ttk.Entry(self.action_status_frame)
self.search_status_label = ttk.Label(self.action_status_frame, text="", style="AccentBottom.TLabel")
self.settings_button = ttk.Button(self.action_status_frame, image=self.dialog.icon_manager.get_icon('settings-2_small'),
command=self.dialog.open_settings_dialog, style="Bottom.TButton.Borderless.Round")
self.trash_button = ttk.Button(self.action_status_frame, image=self.dialog.icon_manager.get_icon('trash_small2'),
command=self.dialog.delete_selected_item, style="Bottom.TButton.Borderless.Round")
Tooltip(self.trash_button, "Ausgewähltes Element löschen/verschieben")
# --- Arrange Widgets based on Mode and Settings ---
button_box_pos = self.settings.get("button_box_pos", "left")
if self.dialog.dialog_mode == "save":
self.filename_entry = ttk.Entry(self.action_status_frame)
save_button = ttk.Button(
self.action_status_frame, text="Speichern", command=self.dialog.on_save)
cancel_button = ttk.Button(
self.action_status_frame, text="Abbrechen", command=self.dialog.on_cancel)
self.filter_combobox = ttk.Combobox(self.action_status_frame, values=[
ft[0] for ft in self.dialog.filetypes], state="readonly")
save_button = ttk.Button(self.action_status_frame, text="Speichern", command=self.dialog.on_save)
cancel_button = ttk.Button(self.action_status_frame, text="Abbrechen", command=self.dialog.on_cancel)
self.filter_combobox = ttk.Combobox(self.action_status_frame, values=[ft[0] for ft in self.dialog.filetypes], state="readonly")
# Pack widgets into their respective containers
self.filename_entry.pack(in_=self.center_container, side="top", fill="x", expand=True)
if button_box_pos == 'left':
save_button.grid(row=0, column=0, sticky="w", padx=(0, 10))
self.filename_entry.grid(
row=0, column=1, sticky="ew", padx=(0, 5))
cancel_button.grid(row=1, column=0, sticky="w",
pady=(5, 0), padx=(0, 10))
self.filter_combobox.grid(
row=1, column=1, sticky="w", pady=(5, 0), padx=(0, 5))
self.settings_button.grid(row=0, column=3, sticky="e")
self.trash_button.grid(
row=1, column=3, sticky="se", padx=(5, 0))
save_button.pack(in_=self.left_container, side="left", padx=(0, 5))
cancel_button.pack(in_=self.left_container, side="left")
self.filter_combobox.pack(in_=self.center_container, side="left", pady=(5,0), padx=(5,0))
self.search_status_label.pack(in_=self.center_container, side="left", pady=(5,0), padx=(5,0))
self.trash_button.pack(in_=self.right_container, side="right", padx=(5,0))
self.settings_button.pack(in_=self.right_container, side="right")
else: # right
self.trash_button.grid(
row=1, column=0, sticky="sw", padx=(0, 5))
self.filename_entry.grid(
row=0, column=1, sticky="ew", padx=(0, 5))
self.filter_combobox.grid(
row=1, column=1, sticky="e", pady=(5, 0), padx=(0, 5))
save_button.grid(row=0, column=2, sticky="e", padx=5)
cancel_button.grid(row=1, column=2, sticky="e",
padx=5, pady=(5, 0))
self.settings_button.grid(row=0, column=3, sticky="e")
self.trash_button.pack(in_=self.left_container, side="left")
self.search_status_label.pack(in_=self.center_container, side="left", pady=(5,0), padx=(5,0))
self.filter_combobox.pack(in_=self.center_container, side="right", pady=(5,0), padx=(0,5))
save_button.pack(in_=self.right_container, side="left", padx=(5, 5))
cancel_button.pack(in_=self.right_container, side="left")
self.settings_button.pack(in_=self.right_container, side="right")
self.filter_combobox.bind(
"<<ComboboxSelected>>", self.dialog.on_filter_change)
self.filter_combobox.bind("<<ComboboxSelected>>", self.dialog.on_filter_change)
self.filter_combobox.set(self.dialog.filetypes[0][0])
else: # Open mode
open_button = ttk.Button(
self.action_status_frame, text="Öffnen", command=self.dialog.on_open)
cancel_button = ttk.Button(
self.action_status_frame, text="Abbrechen", command=self.dialog.on_cancel)
self.filter_combobox = ttk.Combobox(self.action_status_frame, values=[
ft[0] for ft in self.dialog.filetypes], state="readonly")
open_button = ttk.Button(self.action_status_frame, text="Öffnen", command=self.dialog.on_open)
cancel_button = ttk.Button(self.action_status_frame, text="Abbrechen", command=self.dialog.on_cancel)
self.filter_combobox = ttk.Combobox(self.action_status_frame, values=[ft[0] for ft in self.dialog.filetypes], state="readonly")
# Pack status bar and search entry into the center
self.status_bar.pack(in_=self.center_container, side="top", fill="x")
self.search_entry.pack(in_=self.center_container, side="top", fill="x")
self.search_entry.pack_forget() # Initially hidden
if button_box_pos == 'left':
open_button.grid(row=0, column=0, sticky="w", padx=(0, 5))
self.status_bar.grid(row=0, column=1, sticky="w", padx=5)
cancel_button.grid(row=1, column=0, sticky="w", pady=(5, 0))
self.filter_combobox.grid(
row=1, column=1, sticky="w", pady=(5, 0), padx=(5, 0))
self.settings_button.grid(row=0, column=2, sticky="e")
open_button.pack(in_=self.left_container, side="left", padx=(0, 5))
cancel_button.pack(in_=self.left_container, side="left")
self.filter_combobox.pack(in_=self.center_container, side="left", pady=(5,0), padx=(5,0))
self.search_status_label.pack(in_=self.center_container, side="left", pady=(5,0), padx=(5,0))
self.settings_button.pack(in_=self.right_container, side="right")
else: # right
self.status_bar.grid(row=0, column=0, sticky="e", padx=10)
open_button.grid(row=0, column=1, sticky="e", padx=5)
cancel_button.grid(row=1, column=1, sticky="e",
padx=5, pady=(5, 0))
self.filter_combobox.grid(
row=1, column=0, sticky="e", pady=(5, 0))
self.settings_button.grid(row=0, column=2, sticky="e")
self.search_status_label.pack(in_=self.center_container, side="left", pady=(5,0))
self.filter_combobox.pack(in_=self.center_container, side="right", pady=(5,0), padx=(0,5))
open_button.pack(in_=self.right_container, side="left", padx=(0, 5))
cancel_button.pack(in_=self.right_container, side="left")
self.settings_button.pack(in_=self.right_container, side="right")
self.filter_combobox.bind(
"<<ComboboxSelected>>", self.dialog.on_filter_change)
self.filter_combobox.bind("<<ComboboxSelected>>", self.dialog.on_filter_change)
self.filter_combobox.set(self.dialog.filetypes[0][0])

View File

@@ -38,6 +38,8 @@ class SettingsDialog(tk.Toplevel):
value=self.settings.get("default_view_mode", "icons"))
self.search_hidden_files = tk.BooleanVar(
value=self.settings.get("search_hidden_files", False))
self.recursive_search = tk.BooleanVar(
value=self.settings.get("recursive_search", True))
self.use_trash = tk.BooleanVar(
value=self.settings.get("use_trash", False))
self.confirm_delete = tk.BooleanVar(
@@ -89,6 +91,8 @@ class SettingsDialog(tk.Toplevel):
search_hidden_frame.pack(fill="x", pady=5)
ttk.Checkbutton(search_hidden_frame, text="Versteckte Dateien und Ordner durchsuchen",
variable=self.search_hidden_files).pack(anchor="w")
ttk.Checkbutton(search_hidden_frame, text="Rekursiv suchen",
variable=self.recursive_search).pack(anchor="w")
# Deletion Settings
delete_frame = ttk.LabelFrame(
@@ -134,6 +138,7 @@ class SettingsDialog(tk.Toplevel):
"window_size_preset": self.window_size_preset.get(),
"default_view_mode": self.default_view_mode.get(),
"search_hidden_files": self.search_hidden_files.get(),
"recursive_search": self.recursive_search.get(),
"use_trash": self.use_trash.get(),
"confirm_delete": self.confirm_delete.get()
}
@@ -148,6 +153,7 @@ class SettingsDialog(tk.Toplevel):
self.window_size_preset.set(defaults["window_size_preset"])
self.default_view_mode.set(defaults["default_view_mode"])
self.search_hidden_files.set(defaults["search_hidden_files"])
self.recursive_search.set(defaults["recursive_search"])
self.use_trash.set(defaults["use_trash"])
self.confirm_delete.set(defaults["confirm_delete"])
@@ -194,6 +200,7 @@ class CustomFileDialog(tk.Toplevel):
self.items_to_load_per_batch = 250
self.item_path_map = {}
self.responsive_buttons_hidden = None # State for responsive buttons
self.search_job = None
self.icon_manager = IconManager()
self.style_manager = StyleManager(self)
@@ -217,31 +224,63 @@ class CustomFileDialog(tk.Toplevel):
# Bind the intelligent return handler
self.widget_manager.path_entry.bind(
"<Return>", self.handle_path_entry_return)
self.bind("<Key>", self.show_search_bar)
# Bind the delete key only in "save" mode
if self.dialog_mode == "save":
self.bind("<Delete>", self.delete_selected_item)
def handle_path_entry_return(self, event):
"""Intelligently handles the Enter key in the path entry.
If the text is a valid directory, it navigates there.
Otherwise, if in search mode, it executes a search.
def show_search_bar(self, event=None):
# Ignore key presses if they are coming from an entry widget or have no character
if isinstance(event.widget, (ttk.Entry, tk.Entry)) or not event.char.strip():
return
self.search_mode = True
if self.dialog_mode == "open":
self.widget_manager.status_bar.pack_forget()
self.widget_manager.search_entry.pack(side="top", fill="x", in_=self.widget_manager.center_container)
self.widget_manager.search_entry.focus_set()
self.widget_manager.search_entry.insert(0, event.char)
self.widget_manager.search_entry.bind("<Return>", self.execute_search)
self.widget_manager.search_entry.bind("<Escape>", self.hide_search_bar)
else: # save mode
self.widget_manager.filename_entry.focus_set()
self.widget_manager.filename_entry.insert(tk.END, event.char)
self.widget_manager.filename_entry.bind("<Return>", self.execute_search)
self.widget_manager.filename_entry.bind("<Escape>", self.hide_search_bar)
def hide_search_bar(self, event=None):
self.search_mode = False
if self.dialog_mode == "open":
self.widget_manager.search_entry.pack_forget()
self.widget_manager.status_bar.pack(side="top", fill="x", in_=self.widget_manager.center_container)
self.widget_manager.search_entry.delete(0, tk.END)
else: # save mode
self.widget_manager.filename_entry.delete(0, tk.END)
self.widget_manager.search_status_label.config(text="")
self.populate_files()
def toggle_search_mode(self, event=None):
if self.search_mode:
self.hide_search_bar(event)
else:
self.show_search_bar(event)
def handle_path_entry_return(self, event):
"""Handles the Enter key in the path entry to navigate.
Search is handled by on_path_entry_key_release.
"""
path_text = self.widget_manager.path_entry.get().strip()
# Try to interpret as a path first
# Expand user-home and resolve relative paths
potential_path = os.path.realpath(os.path.expanduser(path_text))
if os.path.isdir(potential_path):
# If search was active, turn it off before navigating
if self.search_mode:
self.toggle_search_mode()
self.navigate_to(potential_path)
elif self.search_mode:
# If not a valid path and in search mode, execute search
self.execute_search(event)
# If not a directory, do nothing on Enter. Search is triggered on key release.
def load_settings(self):
self.settings = CfdConfigManager.load()
@@ -435,36 +474,7 @@ class CustomFileDialog(tk.Toplevel):
widget_y - buffer <= y <= widget_y + widget_height + buffer):
self.widget_manager.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.widget_manager.path_entry.get()
self.widget_manager.path_entry.delete(0, tk.END)
self.widget_manager.path_entry.insert(0, "Suchbegriff eingeben...")
# Use after() to ensure the focus is set after the UI has updated
self.after(10, lambda: self.widget_manager.path_entry.focus_set())
self.after(
20, lambda: self.widget_manager.path_entry.select_range(0, tk.END))
self.widget_manager.path_entry.bind(
"<FocusIn>", self.clear_search_placeholder)
# Show search options
self.widget_manager.recursive_button.pack(side="left", padx=5)
else:
# Exit search mode
self.search_mode = False
self.widget_manager.path_entry.delete(0, tk.END)
self.widget_manager.path_entry.insert(0, self.original_path_text)
self.widget_manager.path_entry.unbind("<FocusIn>")
# Hide search options
self.widget_manager.recursive_button.pack_forget()
# Return to normal file view
self.populate_files()
def toggle_recursive_search(self):
"""Toggle recursive search on/off and update button style"""
@@ -502,17 +512,21 @@ class CustomFileDialog(tk.Toplevel):
self._update_view_mode_buttons()
self.populate_files()
def clear_search_placeholder(self, event):
"""Clear placeholder text when focus enters search field"""
if self.widget_manager.path_entry.get() == "Suchbegriff eingeben...":
self.widget_manager.path_entry.delete(0, tk.END)
def execute_search(self, event):
"""Execute search when Enter is pressed in search mode"""
search_term = self.widget_manager.path_entry.get().strip()
if not search_term or search_term == "Suchbegriff eingeben...":
def execute_search(self, event=None):
if self.dialog_mode == "open":
search_term = self.widget_manager.search_entry.get().strip()
else:
search_term = self.widget_manager.filename_entry.get().strip()
if not search_term:
self.hide_search_bar()
return
self.widget_manager.search_status_label.config(text=f"Suche nach '{search_term}'...")
self.update_idletasks()
# Clear previous search results
self.search_results.clear()
@@ -550,12 +564,12 @@ class CustomFileDialog(tk.Toplevel):
os.chdir(search_dir)
# Build find command based on recursive setting (use . for current directory)
if self.widget_manager.recursive_search.get():
# Find both files and directories
find_cmd = ['find', '.', '-iname', f'*{search_term}*']
if self.settings.get("recursive_search", True):
# Find both files and directories, following symlinks
find_cmd = ['find', '-L', '.', '-iname', f'*{search_term}*']
else:
# Find both files and directories, but only in the current level
find_cmd = ['find', '.', '-maxdepth', '1',
# Find both files and directories, but only in the current level, following symlinks
find_cmd = ['find', '-L', '.', '-maxdepth', '1',
'-iname', f'*{search_term}*']
result = subprocess.run(
@@ -604,7 +618,11 @@ class CustomFileDialog(tk.Toplevel):
# Show search results in TreeView
if self.search_results:
self.show_search_results_treeview()
folder_count = sum(1 for p in self.search_results if os.path.isdir(p))
file_count = len(self.search_results) - folder_count
self.widget_manager.search_status_label.config(text=f"{folder_count} Ordner und {file_count} Dateien gefunden.")
else:
self.widget_manager.search_status_label.config(text=f"Keine Ergebnisse für '{search_term}'.")
MessageDialog(
message_type="info",
text=f"Keine Dateien mit '{search_term}' gefunden.",
@@ -718,11 +736,10 @@ class CustomFileDialog(tk.Toplevel):
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()
# Exit search mode and navigate to the file's location
self.hide_search_bar()
self.navigate_to(directory, file_to_select=filename)
search_tree.bind("<Double-1>", on_search_double_click)
@@ -751,7 +768,7 @@ class CustomFileDialog(tk.Toplevel):
directory = item['values'][0]
# Exit search mode and navigate to the directory
self.toggle_search_mode() # To restore normal view
self.hide_search_bar()
self.navigate_to(directory)
# Select the file in the list
@@ -1171,7 +1188,7 @@ class CustomFileDialog(tk.Toplevel):
break
self.populate_files()
def navigate_to(self, path):
def navigate_to(self, path, file_to_select=None):
try:
real_path = os.path.realpath(
os.path.abspath(os.path.expanduser(path)))
@@ -1189,7 +1206,7 @@ class CustomFileDialog(tk.Toplevel):
if not self.history or self.history[-1] != self.current_dir:
self.history.append(self.current_dir)
self.history_pos = len(self.history) - 1
self.populate_files()
self.populate_files(item_to_select=file_to_select)
self.update_nav_buttons()
self.update_status_bar()
self.update_action_buttons_state()

82
layout_beschreibung.md Normal file
View File

@@ -0,0 +1,82 @@
# Beschreibung der Layout-Struktur (Unterer Bereich)
Selbstverständlich. Sie haben absolut recht, eine gute Dokumentation ist wichtig, besonders nach einer solchen Umstrukturierung. Ich werde die neue Logik des unteren Bereichs detailliert aufschlüsseln.
### Das Grundkonzept: Drei flexible Spalten
Stellen Sie sich den gesamten unteren Bereich (`action_status_frame`) wie eine Tabelle mit **einer Zeile und drei Spalten** vor:
| `left_container` | `center_container` (flexibel) | `right_container` |
| :--- | :--- | :--- |
| (linksbündig) | (füllt den Platz) | (rechtsbündig) |
- **`action_status_frame`**: Dies ist der Haupt-Frame für den gesamten unteren Bereich. Er enthält die drei Container.
- **`left_container`**: Alle Widgets, die linksbündig sein sollen, kommen hier hinein.
- **`right_container`**: Alle Widgets, die rechtsbündig sein sollen, kommen hier hinein.
- **`center_container`**: Dieser Container ist der flexible Teil. Er dehnt sich aus, um den gesamten verfügbaren Platz zwischen dem linken und rechten Container zu füllen. Hier platzieren wir die Statusleiste oder das Eingabefeld für den Dateinamen.
Gesteuert wird dieses Verhalten durch diese Zeilen:
```python
# Die drei Container werden im Haupt-Frame platziert
left_container.grid(row=0, column=0, sticky='w')
center_container.grid(row=0, column=1, sticky='ew')
right_container.grid(row=0, column=2, sticky='e')
# Dem Grid wird gesagt, dass nur die mittlere Spalte (1) wachsen soll
self.action_status_frame.grid_columnconfigure(1, weight=1)
```
### Die zwei "Zeilen": Widgets in den Containern
Innerhalb dieser drei Spalten-Container organisieren wir die Widgets in zwei logischen Zeilen.
- **Obere Zeile**: Dies ist meist ein einzelnes, breites Widget.
- Im **Open-Modus**: Die `status_bar` (oder das `search_entry`).
- Im **Save-Modus**: Das `filename_entry`.
Diese werden direkt in den `center_container` gepackt und füllen dessen Breite aus.
- **Untere Zeile**: Hier liegen die meisten Aktions-Widgets (Buttons, Combobox). Um die horizontale Anordnung innerhalb der drei Spalten beizubehalten, wird für diese Zeile **in jeden Haupt-Container ein weiterer kleiner Frame** gepackt:
- `row2_left` (im `left_container`)
- `row2_center` (im `center_container`)
- `row2_right` (im `right_container`)
### Platzierung der Widgets: Wer kommt wohin?
Jetzt wird nur noch entschieden, welches Widget in welchen Container kommt.
---
#### **Im `Open-Modus`**
- **`status_bar` / `search_entry`**: Immer in der oberen Zeile des `center_container`.
- **Wenn `button_box_pos == 'left'` (Buttons Links):**
- `left_container`: Enthält `open_button` und `cancel_button`.
- `center_container`: Enthält `filter_combobox` und `search_status_label`.
- `right_container`: Enthält den `settings_button`.
- **Wenn `button_box_pos == 'right'` (Buttons Rechts):**
- `left_container`: Ist leer.
- `center_container`: Enthält `search_status_label` (linksbündig darin) und `filter_combobox` (rechtsbündig darin).
- `right_container`: Enthält `open_button`, `cancel_button` und `settings_button`.
---
#### **Im `Save-Modus`**
- **`filename_entry`**: Immer in der oberen Zeile des `center_container`.
- **Wenn `button_box_pos == 'left'` (Buttons Links):**
- `left_container`: Enthält `save_button`, `cancel_button` und den `trash_button`.
- `center_container`: Enthält `filter_combobox` und `search_status_label`.
- `right_container`: Enthält den `settings_button`.
- **Wenn `button_box_pos == 'right'` (Buttons Rechts):**
- `left_container`: Enthält nur den `trash_button`.
- `center_container`: Enthält `search_status_label` (linksbündig) und `filter_combobox` (rechtsbündig).
- `right_container`: Enthält `save_button`, `cancel_button` und `settings_button`.
---
**Der entscheidende Vorteil ist:** Wenn Sie jetzt ein Widget verschieben oder hinzufügen wollen, müssen Sie nur noch entscheiden, in welchen der drei Haupt-Container (`left`, `center`, `right`) und in welche "Zeile" (direkt oder in den `row2`-Frame) es gehört. Das komplexe Jonglieren mit Spalten- und Zeilenindizes im Grid entfällt vollständig.
Ich hoffe, diese Erklärung macht die neue Struktur klarer und die Wartung für Sie einfacher.