|
5 | 5 | from pathlib import Path |
6 | 6 | from typing import List, Dict, Any |
7 | 7 | import sys |
| 8 | +from tkinter import PhotoImage |
8 | 9 |
|
9 | 10 | class TodoApp: |
10 | 11 | def __init__(self, root: tk.Tk): |
@@ -42,7 +43,7 @@ def setup_ui(self): |
42 | 43 |
|
43 | 44 | self.listbox = tk.Listbox(self.main_frame, selectmode=tk.EXTENDED, bd=0, highlightthickness=0, |
44 | 45 | activestyle='none', font=self.get_system_font(), width=40, height=10) |
45 | | - self.listbox.grid(row=0, column=0, columnspan=4, sticky="nsew", padx=10, pady=(10, 5)) |
| 46 | + self.listbox.grid(row=0, column=0, columnspan=4, sticky="nsew", padx=10, pady=(8, 5)) |
46 | 47 | self.main_frame.grid_rowconfigure(0, weight=1) |
47 | 48 | self.main_frame.grid_columnconfigure(0, weight=1) |
48 | 49 |
|
@@ -95,13 +96,28 @@ def setup_bindings(self): |
95 | 96 |
|
96 | 97 |
|
97 | 98 | def on_ctrl_click(self, event): |
98 | | - """Handle Ctrl-click to select multiple tasks.""" |
| 99 | + """Handle robust Ctrl-click to toggle selection of individual tasks.""" |
99 | 100 | index = self.listbox.nearest(event.y) |
100 | 101 | if index in self.listbox.curselection(): |
101 | 102 | self.listbox.selection_clear(index) |
102 | 103 | else: |
103 | 104 | self.listbox.selection_set(index) |
104 | 105 |
|
| 106 | + return 'break' |
| 107 | + |
| 108 | + def on_ctrl_click(self, event): |
| 109 | + """Handle robust Ctrl-click to toggle selection of individual tasks.""" |
| 110 | + index = self.listbox.nearest(event.y) |
| 111 | + if index in self.listbox.curselection(): |
| 112 | + self.listbox.selection_clear(index) |
| 113 | + else: |
| 114 | + self.listbox.selection_set(index) |
| 115 | + |
| 116 | + # Update buttons state after selection change |
| 117 | + self.update_buttons_state() |
| 118 | + |
| 119 | + return 'break' |
| 120 | + |
105 | 121 | def on_shift_click(self, event): |
106 | 122 | """Handle Shift-click to select a range of tasks.""" |
107 | 123 | index = self.listbox.nearest(event.y) |
@@ -281,8 +297,6 @@ def update_buttons_state(self, event=None): |
281 | 297 | for button in ["➖", "✔"]: |
282 | 298 | self.buttons[button]['state'] = 'normal' if has_selection else 'disabled' |
283 | 299 |
|
284 | | - |
285 | | - |
286 | 300 | def adjust_window_size(self): |
287 | 301 | num_tasks = len(self.tasks) |
288 | 302 | new_height = max(100, min(800, 100 + (num_tasks * 18))) |
@@ -371,11 +385,20 @@ def show_about_dialog(self, event=None): |
371 | 385 | about_window.title("About") |
372 | 386 | about_window.resizable(False, False) |
373 | 387 |
|
| 388 | + # Set window icon |
374 | 389 | self.set_window_icon(about_window) |
375 | 390 |
|
| 391 | + # Load and display app icon |
| 392 | + icon_path = Path(__file__).parent / 'app_logo.png' |
| 393 | + if icon_path.is_file(): |
| 394 | + app_icon = PhotoImage(file=icon_path) |
| 395 | + icon_label = tk.Label(about_window, image=app_icon) |
| 396 | + icon_label.image = app_icon # Keep a reference to avoid garbage collection |
| 397 | + icon_label.pack(pady=(5, 5)) # Add some padding around the icon |
| 398 | + |
376 | 399 | about_text = ( |
377 | | - "To-Do App 0.2.0\n\n" |
378 | | - "2024 Jens Lettkemann <jltk@pm.me>\n\n" |
| 400 | + "To-Do App 0.2.1\n\n" |
| 401 | + "© 2024 Jens Lettkemann <jltk@pm.me>\n\n" |
379 | 402 | "This software is licensed under GPLv3+.\n" |
380 | 403 | ) |
381 | 404 | github_link = "https://github.com/jltk/todo-app\n" |
@@ -454,22 +477,61 @@ def edit_task(self): |
454 | 477 | selected_indices = self.listbox.curselection() |
455 | 478 | if not selected_indices: |
456 | 479 | return |
457 | | - |
| 480 | + |
458 | 481 | index = selected_indices[0] |
459 | 482 | current_task = self.tasks[index] |
460 | | - |
461 | | - new_name = simpledialog.askstring( |
462 | | - "Edit Task", |
463 | | - "Edit task name:", |
464 | | - initialvalue=current_task['name'], |
465 | | - parent=self.root |
466 | | - ) |
467 | | - if new_name is not None: |
468 | | - self.tasks[index]['name'] = new_name |
469 | | - self.populate_listbox() |
470 | | - self.save_tasks() |
471 | | - self.update_buttons_state() |
472 | | - self.update_title() |
| 483 | + |
| 484 | + # Create a Toplevel window for editing the task |
| 485 | + edit_window = tk.Toplevel(self.root) |
| 486 | + edit_window.title("Edit Task") |
| 487 | + edit_window.geometry("200x120") # Larger size for the edit window |
| 488 | + edit_window.transient(self.root) # Make the edit window stay on top of the root window |
| 489 | + edit_window.grab_set() # Make the edit window modal |
| 490 | + |
| 491 | + self.set_window_icon(edit_window) # Set the icon for the edit window |
| 492 | + |
| 493 | + # Create a frame to contain the widgets |
| 494 | + frame = tk.Frame(edit_window, padx=20, pady=20) |
| 495 | + frame.pack(fill="both", expand=True) |
| 496 | + |
| 497 | + # Create a Text widget for task editing |
| 498 | + text_entry = tk.Text(frame, wrap='word', height=2, width=28) |
| 499 | + text_entry.insert(tk.END, current_task['name']) |
| 500 | + text_entry.pack(fill="both", expand=True) |
| 501 | + |
| 502 | + def on_save(event=None): |
| 503 | + new_name = text_entry.get("1.0", "end-1c").strip() |
| 504 | + if new_name: |
| 505 | + self.tasks[index]['name'] = new_name |
| 506 | + self.populate_listbox() |
| 507 | + self.save_tasks() |
| 508 | + self.update_buttons_state() |
| 509 | + self.update_title() |
| 510 | + edit_window.destroy() |
| 511 | + |
| 512 | + def on_cancel(): |
| 513 | + edit_window.destroy() |
| 514 | + |
| 515 | + # Bind Enter key to the on_save function |
| 516 | + text_entry.bind("<Return>", on_save) |
| 517 | + |
| 518 | + # Create Save and Cancel buttons |
| 519 | + button_frame = tk.Frame(frame) |
| 520 | + button_frame.pack(fill="x", pady=(10, 0)) |
| 521 | + |
| 522 | + save_button = tk.Button(button_frame, text="Save", command=on_save) |
| 523 | + save_button.pack(side="left", padx=0) # Align to the left side of the frame |
| 524 | + |
| 525 | + cancel_button = tk.Button(button_frame, text="Cancel", command=on_cancel) |
| 526 | + cancel_button.pack(side="left", padx=5) # Align to the left side of the frame |
| 527 | + |
| 528 | + # Center the button_frame horizontally |
| 529 | + button_frame.pack(side="bottom", pady=(10, 0), fill="x") |
| 530 | + |
| 531 | + edit_window.protocol("WM_DELETE_WINDOW", on_cancel) |
| 532 | + self.center_window_over_window(edit_window) |
| 533 | + |
| 534 | + |
473 | 535 |
|
474 | 536 | def show_window(self): |
475 | 537 | if self.initial_geometry: |
@@ -521,7 +583,7 @@ def center_window(self, default_size=None): |
521 | 583 |
|
522 | 584 | @staticmethod |
523 | 585 | def get_system_font(): |
524 | | - return ('Segoe UI', 10) if sys.platform == 'win32' else ('San Francisco', 11) if sys.platform == 'darwin' else ('Segoe UI', 10) |
| 586 | + return ('Segoe UI', 10) if sys.platform == 'win32' else ('San Francisco', 11) if sys.platform == 'darwin' else ('Arial', 10) |
525 | 587 |
|
526 | 588 | def update_bulk_selection_mode(self): |
527 | 589 | if self.shift_pressed: |
|
0 commit comments