From c8db431c068671b244d776861261162aadedef50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A9sir=C3=A9=20Werner=20Menrath?= Date: Tue, 5 Aug 2025 18:29:58 +0200 Subject: [PATCH] commit 57 --- __pycache__/cfd_app_config.cpython-312.pyc | Bin 5856 -> 5880 bytes __pycache__/cfd_ui_setup.cpython-312.pyc | Bin 32927 -> 34322 bytes .../custom_file_dialog.cpython-312.pyc | Bin 95607 -> 97581 bytes cfd_app_config.py | 3 +- cfd_ui_setup.py | 136 ++++++++-------- custom_file_dialog.py | 145 ++++++++++-------- layout_beschreibung.md | 82 ++++++++++ 7 files changed, 228 insertions(+), 138 deletions(-) create mode 100644 layout_beschreibung.md diff --git a/__pycache__/cfd_app_config.cpython-312.pyc b/__pycache__/cfd_app_config.cpython-312.pyc index ec0a9b733faed899dd1222db084ea9b583c7e24e..f32f12bf59e11b8ea4d518eb670b3f3afa2933a7 100644 GIT binary patch delta 652 zcmaE$`$L!aG%qg~0}%XtGcm(tBd-Pr7bl1d1fMG=+jBTurEt#Sh~h})OyO!_SPhY3 zh~fhBxsmwXKt2x=pQn;ZlXvnijxOd9O^(TKoE1X11d39VON)v#%TnWuQxl7lGdAzz ze9FY=x_JqYA9KAK(BvX>Afd@!FEKr}NDrhXU~`{ zrBIw$mYJ8XP@Gy)0;G$r6g1h7F0S6=4TSXz0BSaJ# zYbRHUnDgF{QTQmxDam$$Mf8K%f(RuL0Scm`a2Sy_ zd4s48FNmwjUnB++b)5WDG*cd|09Dg34x8Nkl+v73yQ28XjbiQ+#*Bs^8Gyw197aw? LwoeQ|0&E2Uw2hk8 delta 532 zcmeyN`#_iXG%qg~0}$LXo0uWAkynF*ivz?3g3o1>?Kzw+=CDU`q;jTkwlJ)Qh%-cS z0r^}=d~P708;Q?T$)w3Mc@IYylP3FQPtJYbIU>8V9}AQSyJFBY(85;XxS zhP$Q60>rhP{9Q=F3e_1!jvyIt5CO8c$OTAfvViTn#ZjDEQj(dMUR>k`l5(FMD6GdA zH@QK0k0DqCC?tzOp$qX9*el%0If=!^xv3=?`6*!k7lGVb6g)XsM3J#-a=(Z<*GGO1 zNwy0tq8~&iUluuR6b_PS0TBuyLLNj!fCxnpp#&n7K|~mg$eesa)P@(t)#NV{1Bp6J n78J{544YgnCJS`mOfh!}BSyoI3_#+079%Gk+b0Gf0X7N%3toBh diff --git a/__pycache__/cfd_ui_setup.cpython-312.pyc b/__pycache__/cfd_ui_setup.cpython-312.pyc index 9e4eecde25a7c666fca164f2333e8710d8cfbfeb..8492131da49a5e9d1d0b141688b3ac069d9908ea 100644 GIT binary patch delta 4078 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 o!)-x7fsz@4g%1u1MD(EtDd diff --git a/__pycache__/custom_file_dialog.cpython-312.pyc b/__pycache__/custom_file_dialog.cpython-312.pyc index 90746cad8258df4b7a13f76636a110e83d4453ad..cd630bb737ce954f5f98649e7f8b5d79429146ed 100644 GIT binary patch delta 19566 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 delta 18407 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 diff --git a/cfd_app_config.py b/cfd_app_config.py index ef5a71b..42a5ec1 100755 --- a/cfd_app_config.py +++ b/cfd_app_config.py @@ -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 diff --git a/cfd_ui_setup.py b/cfd_ui_setup.py index a634853..53ed086 100644 --- a/cfd_ui_setup.py +++ b/cfd_ui_setup.py @@ -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("", 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( - "<>", self.dialog.on_filter_change) + self.filter_combobox.bind("<>", 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( - "<>", self.dialog.on_filter_change) + self.filter_combobox.bind("<>", self.dialog.on_filter_change) self.filter_combobox.set(self.dialog.filetypes[0][0]) diff --git a/custom_file_dialog.py b/custom_file_dialog.py index 723f978..49907ec 100644 --- a/custom_file_dialog.py +++ b/custom_file_dialog.py @@ -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( "", self.handle_path_entry_return) + + + + self.bind("", self.show_search_bar) # Bind the delete key only in "save" mode if self.dialog_mode == "save": self.bind("", 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("", self.execute_search) + self.widget_manager.search_entry.bind("", 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("", self.execute_search) + self.widget_manager.filename_entry.bind("", 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( - "", 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("") - - # 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("", 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() diff --git a/layout_beschreibung.md b/layout_beschreibung.md new file mode 100644 index 0000000..9487a0d --- /dev/null +++ b/layout_beschreibung.md @@ -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.