Update Helper Methods

robert 2026-01-07 23:03:20 +01:00
parent 405075c4eb
commit 8c0fcc4c0f

@ -1,22 +1,13 @@
## Helper Functions Documentation ## Helper Functions Documentation v2.3.0
### State Management Helpers ### State Management Helpers
#### `mark_as_modified()` #### mark_as_modified()
```python
def mark_as_modified(self): **Purpose**: Tracks when metadata has been modified but not saved
"""Mark the document as having unsaved changes"""
if not self.has_unsaved_changes:
self.has_unsaved_changes = True
self.update_title()
self.changes_label.config(text="Unsaved changes")
```
**Purpose**:
- Tracks when metadata has been modified but not saved
- Prevents duplicate change notifications
**Behavior**: **Behavior**:
1. Sets `has_unsaved_changes` flag to True 1. Sets has_unsaved_changes flag to True
2. Updates window title with asterisk (*) 2. Updates window title with asterisk (*)
3. Shows "Unsaved changes" indicator in UI 3. Shows "Unsaved changes" indicator in UI
4. Only triggers once per modification session 4. Only triggers once per modification session
@ -28,20 +19,12 @@ self.metadata_dict["NewKey"] = "Value"
self.mark_as_modified() self.mark_as_modified()
``` ```
#### `mark_as_saved()` #### mark_as_saved()
```python
def mark_as_saved(self): **Purpose**: Resets the unsaved changes indicator and updates UI to reflect saved state
"""Mark the document as saved"""
self.has_unsaved_changes = False
self.update_title()
self.changes_label.config(text="")
```
**Purpose**:
- Resets the unsaved changes indicator
- Updates UI to reflect saved state
**Behavior**: **Behavior**:
1. Clears `has_unsaved_changes` flag 1. Clears has_unsaved_changes flag
2. Removes asterisk from window title 2. Removes asterisk from window title
3. Hides unsaved changes indicator 3. Hides unsaved changes indicator
@ -54,139 +37,242 @@ self.set_status("Metadata saved successfully", color="green")
### Display Formatting Helpers ### Display Formatting Helpers
#### `format_value_for_display(value)` #### format_value_for_display(value) - UPDATED v2.3.0
```python
def format_value_for_display(self, value): **Purpose**: Intelligently formats metadata values for display in editor
"""Try to parse as JSON if possible, otherwise return as-is"""
try:
# Try to parse as JSON
parsed = json.loads(value)
# If successful, return pretty-printed JSON
return json.dumps(parsed, indent=2, ensure_ascii=False)
except (json.JSONDecodeError, TypeError):
# Not JSON, return original value
return value
```
**Purpose**:
- Intelligently formats metadata values for display
- Handles both JSON and plain text values
**Behavior**: **Behavior**:
1. Attempts to parse value as JSON 1. Attempts to parse value as JSON
2. If successful, returns pretty-printed with 2-space indentation 2. If successful, returns pretty-printed with 2-space indentation
3. Falls back to string representation if parsing fails 3. Falls back to original value if parsing fails
4. Preserves Unicode characters with `ensure_ascii=False` 4. Preserves Unicode characters with ensure_ascii=False
**v2.3.0 Note**: This function formats for display only. Values are automatically flattened before saving via flatten_json_if_valid().
**Example Usage**: **Example Usage**:
```python ```python
# JSON-formatted metadata # JSON-formatted metadata
json_value = '{"camera":"Canon","iso":100}' json_value = '{"camera":"Canon","iso":100}'
formatted = app.format_value_for_display(json_value) formatted = app.format_value_for_display(json_value)
# Returns: {"camera": "Canon", "iso": 100} # Returns pretty-printed JSON
# Plain text metadata # Plain text metadata
text_value = "Simple string value" text_value = "Simple string value"
formatted = app.format_value_for_display(text_value) formatted = app.format_value_for_display(text_value)
# Returns: Simple string value # Returns: "Simple string value"
``` ```
#### `truncate_value(value, max_length=80)` #### flatten_json_if_valid(value) - NEW v2.3.0
```python
def truncate_value(self, value, max_length=80): **Purpose**: Preserves original compact JSON format when saving
"""Truncate long values for tree display"""
value_str = str(value)
if len(value_str) > max_length:
return value_str[:max_length] + "..."
return value_str
```
**Purpose**:
- Shortens long values for compact tree view display
- Maintains readability while preventing UI overflow
**Behavior**: **Behavior**:
1. Converts value to string 1. Attempts to parse value as JSON
2. Checks if length exceeds `max_length` (default 80) 2. If successful, returns flattened single-line JSON
3. Truncates with ellipsis (...) if too long 3. Uses separators=(',', ':') for compact format (no spaces)
4. Returns full value if within length limit 4. Falls back to original value if parsing fails
5. Preserves Unicode characters
**Parameters**: **Key Feature**: Automatically called during save operations to maintain the original compact format of JSON metadata.
- `value`: The metadata value to truncate
- `max_length` (optional): Maximum length before truncation (default: 80) **Example Usage**:
```python
# Pretty-printed JSON from editor
formatted = '{\n "prompt": "a cat",\n "steps": 20\n}'
flattened = app.flatten_json_if_valid(formatted)
# Returns: '{"prompt":"a cat","steps":20}'
# Plain text (non-JSON)
text = "Simple value"
flattened = app.flatten_json_if_valid(text)
# Returns: "Simple value" (unchanged)
```
**Workflow**:
```
User edits -> format_value_for_display() -> Pretty JSON shown
User saves -> flatten_json_if_valid() -> Compact JSON saved
```
#### truncate_value(value, max_length=80)
**Purpose**: Shortens long values for compact tree view display
**Parameters**:
- value: The metadata value to truncate
- max_length (optional): Maximum length before truncation (default: 80)
**Behavior**:
1. Converts value to string
2. Checks if length exceeds max_length
3. Truncates with ellipsis (...) if too long
4. Returns full value if within length limit
**Example Usage**: **Example Usage**:
```python ```python
# Long metadata value
long_value = "This is a very long string that exceeds the maximum display length" long_value = "This is a very long string that exceeds the maximum display length"
truncated = app.truncate_value(long_value) truncated = app.truncate_value(long_value)
# Returns: "This is a very long string that exceeds the maximum displa..." # Returns truncated version with "..."
```
# Short metadata value ### Theme System Helpers - NEW v2.3.0
short_value = "Short"
truncated = app.truncate_value(short_value) #### detect_dark_mode()
# Returns: "Short" (unchanged)
**Purpose**: Automatically detects OS-level dark mode setting for adaptive UI theming
**Behavior**:
1. Checks platform (macOS, Windows, Linux)
2. Executes platform-specific command/registry read
3. Returns True for dark mode, False for light mode
4. Falls back to light mode on any error
5. Has 1-second timeout to prevent hanging
**Platform Detection**:
- **macOS**: Reads AppleInterfaceStyle system preference
- **Windows**: Reads AppsUseLightTheme registry key (0=dark, 1=light)
- **Linux**: Checks GTK theme name for "dark" keyword
**Example Usage**:
```python
is_dark = app.detect_dark_mode()
if is_dark:
print("System is in dark mode")
```
#### get_theme_colors()
**Purpose**: Provides consistent color scheme based on system theme
**Returns**: Dictionary with keys:
- canvas_bg: Canvas background color
- frame_bg: Frame background color
- text_fg: Text foreground color
- loading_fg: Loading indicator color
**Color Schemes**:
**Dark Mode**:
- canvas_bg: #2b2b2b (dark gray)
- frame_bg: #2b2b2b
- text_fg: #ffffff (white)
- loading_fg: #6bb6ff (light blue)
**Light Mode**:
- canvas_bg: #f0f0f0 (light gray)
- frame_bg: #f0f0f0
- text_fg: #000000 (black)
- loading_fg: #4a90d9 (blue)
**Example Usage**:
```python
colors = app.get_theme_colors()
canvas = tk.Canvas(root, bg=colors['canvas_bg'])
label = tk.Label(root, fg=colors['text_fg'])
```
### Image Browser Helpers - NEW v2.3.0
#### scroll_to_thumbnail(filepath)
**Purpose**: Automatically scrolls image browser to show selected thumbnail
**Behavior**:
1. Checks if filepath has a thumbnail frame
2. Calculates frame position and canvas dimensions
3. Computes center position for thumbnail
4. Normalizes position to 0-1 range
5. Scrolls canvas to target position using yview_moveto()
**Example Usage**:
```python
# After loading a file
app.load_file_from_path(filepath)
app.scroll_to_thumbnail(filepath) # Auto-scroll to it
```
#### bind_mousewheel(widget)
**Purpose**: Enables trackpad and mousewheel scrolling for image browser
**Behavior**:
1. Detects operating system platform
2. Binds appropriate scroll events for platform
3. Sets focus on mouse enter for scroll capture
4. Restores focus on mouse leave
**Platform Events**:
- **macOS**: MouseWheel, Button-4, Button-5
- **Windows**: MouseWheel only
- **Linux**: Button-4 (scroll up), Button-5 (scroll down)
**Example Usage**:
```python
# During UI setup
self.bind_mousewheel(self.image_canvas)
```
#### on_mousewheel(event)
**Purpose**: Processes scroll events with platform-specific calculations
**Behavior**:
1. Detects platform from event
2. Calculates scroll delta based on platform
3. Scrolls canvas by appropriate units
**Platform Deltas**:
- **macOS**: Direct delta value (trackpad-friendly)
- **Windows**: Delta divided by 120 (standard wheel units)
- **Linux**: +1/-1 for button events
#### on_thumb_frame_configure(event)
**Purpose**: Dynamically updates scroll region as thumbnails are added
**Behavior**:
1. Triggered when thumbnail container changes size
2. Calculates bounding box of all canvas items
3. Updates scrollable region to encompass all content
**Example Usage**:
```python
# Bound to thumbnail container during setup
self.thumb_frame.bind("<Configure>", self.on_thumb_frame_configure)
``` ```
### UI Management Helpers ### UI Management Helpers
#### `set_status(message, duration=3000, color="")` #### set_status(message, duration=3000, color="")
```python
def set_status(self, message, duration=3000, color=""):
"""Set status bar message that auto-clears after duration (ms)"""
# Cancel any existing timer
if self.status_timer:
self.root.after_cancel(self.status_timer)
# Set the message **Purpose**: Provides temporary feedback to users with auto-clear
self.status_label.config(text=message, foreground=color)
# Schedule clearing the message **Parameters**:
if duration > 0: - message: Text to display in status bar
self.status_timer = self.root.after(duration, lambda: self.status_label.config(text="Ready", foreground="")) - duration (optional): Time before auto-clear in ms (default: 3000)
``` - color (optional): Text color like "green" or "red"
**Purpose**:
- Provides temporary feedback to users
- Automatically clears messages after specified duration
**Behavior**: **Behavior**:
1. Cancels any existing status timer 1. Cancels any existing status timer
2. Updates status label with new message and color 2. Updates status label with new message and color
3. Schedules automatic clearing after duration (ms) 3. Schedules automatic clearing after duration
4. Defaults to 3000ms (3 seconds) timeout 4. Duration=0 means permanent message
**Parameters**:
- `message`: Text to display in status bar
- `duration` (optional): Time before auto-clear (ms), 0 for permanent (default: 3000)
- `color` (optional): Text color for the message
**Example Usage**: **Example Usage**:
```python ```python
# Temporary success message # Temporary success message
app.set_status("File loaded successfully", color="green") app.set_status("File loaded successfully", color="green")
# Permanent error message (no auto-clear) # Permanent error message
app.set_status("Error: File not found", duration=0, color="red") app.set_status("Error: File not found", duration=0, color="red")
# Custom duration message # Custom duration
app.set_status("Processing...", duration=5000) # 5 seconds app.set_status("Processing...", duration=5000) # 5 seconds
``` ```
#### `update_title()` #### update_title()
```python
def update_title(self): **Purpose**: Maintains consistent window title format with unsaved indicator
"""Update window title with unsaved indicator"""
base_title = f"{APP_NAME} v{APP_VERSION}"
if self.current_file:
filename = Path(self.current_file).name
if self.has_unsaved_changes:
self.root.title(f"{base_title} - {filename} *")
else:
self.root.title(f"{base_title} - {filename}")
else:
self.root.title(base_title)
```
**Purpose**:
- Maintains consistent window title format
- Indicates unsaved changes with asterisk (*)
**Behavior**: **Behavior**:
1. Constructs base title with app name and version 1. Constructs base title with app name and version
@ -196,52 +282,28 @@ def update_title(self):
**Example Usage**: **Example Usage**:
```python ```python
# When opening a file
app.current_file = "example.png" app.current_file = "example.png"
app.update_title() app.update_title()
# Window title: "PNG Metadata Editor v1.0.0 - example.png" # Window title: "PNG Metadata Editor v2.3.0 - example.png"
# After making changes
app.has_unsaved_changes = True app.has_unsaved_changes = True
app.update_title() app.update_title()
# Window title: "PNG Metadata Editor v1.0.0 - example.png *" # Window title: "PNG Metadata Editor v2.3.0 - example.png *"
``` ```
### Event Handling Helpers ### Event Handling Helpers
#### `on_selection_change(event)` #### on_selection_change(event)
```python
def on_selection_change(self, event):
"""Update detail view when selection changes"""
selected = self.tree.selection()
if selected:
item = selected[0]
key, _ = self.tree.item(item, "values")
if key in self.metadata_dict: **Purpose**: Synchronizes detail view with tree selection
value = self.metadata_dict[key]
formatted_value = self.format_value_for_display(value)
# Update detail text
self.detail_text.config(state=tk.NORMAL)
self.detail_text.delete(1.0, tk.END)
self.detail_text.insert(1.0, formatted_value)
self.detail_text.config(state=tk.DISABLED)
```
**Purpose**:
- Synchronizes detail view with tree selection
- Handles both JSON and plain text formatting
**Behavior**: **Behavior**:
1. Gets currently selected item from tree 1. Gets currently selected item from tree
2. Extracts key from selection values 2. Extracts key from selection values
3. Retrieves full value from metadata_dict 3. Retrieves full value from metadata_dict
4. Formats value for display (JSON pretty-printing) 4. Formats value for display via format_value_for_display()
5. Updates detail text widget with formatted value 5. Updates detail text widget
6. Disables editing of detail view 6. Disables editing of detail view (read-only)
**Parameters**:
- `event`: Tkinter event object (automatically passed)
**Example Usage**: **Example Usage**:
```python ```python
@ -251,57 +313,101 @@ self.tree.bind("<<TreeviewSelect>>", self.on_selection_change)
### Utility Helpers ### Utility Helpers
#### `show_about()` #### show_about()
```python
def show_about(self):
"""Show application about dialog"""
about_text = f"{APP_NAME} v{APP_VERSION}\n\n" \
"A graphical tool for viewing and editing metadata in PNG files.\n\n" \
"Author: Robert Tusa\n" \
"License: MIT"
messagebox.showinfo( **Purpose**: Displays application information dialog
f"About {APP_NAME}",
about_text,
parent=self.root
)
```
**Purpose**:
- Displays application information dialog
- Provides version, description, author, and license info
**Behavior**: **Behavior**:
1. Constructs about text with app metadata 1. Constructs about text with app metadata
2. Shows modal dialog using `messagebox.showinfo()` 2. Shows modal dialog using messagebox.showinfo()
3. Includes standard application information 3. Includes v2.3.0 feature highlights
**Example Usage**: **Example Usage**:
```python ```python
# Called when About button is clicked
app.show_about() app.show_about()
``` ```
## Helper Function Best Practices ## Helper Function Best Practices v2.3.0
1. **State Management**: ### State Management
- Always call `mark_as_modified()` after any metadata change - Always call mark_as_modified() after any metadata change
- Call `mark_as_saved()` only after successful save operations - Call mark_as_saved() only after successful save operations
- Ensure title updates reflect current state
2. **Display Formatting**: ### JSON Handling (NEW)
- Use `format_value_for_display()` for all detail view updates - Use format_value_for_display() for editor and detail views
- Apply `truncate_value()` only to tree view values - Always call flatten_json_if_valid() before saving to file
- Workflow: display formatted -> edit -> save flattened
- Never skip flattening step to preserve original format
3. **UI Feedback**: ### Display Formatting
- Use `set_status()` for temporary user feedback - Use format_value_for_display() for all detail view updates
- Include appropriate color coding (green=success, red=error) - Apply truncate_value() only to tree view values
- Keep messages concise and action-oriented - Keep tree view compact, detail view readable
4. **Event Handling**: ### Theme Integration (NEW)
- Bind `on_selection_change()` to treeview selection events - Call detect_dark_mode() once during initialization
- Ensure detail view is properly updated on every selection change - Use get_theme_colors() for all theme-dependent widgets
- Apply theme colors to canvas, frames, and labels
- Fallback to light mode on detection failure
5. **Title Management**: ### Image Browser (NEW)
- Call `update_title()` after any state change that affects title - Always call scroll_to_thumbnail() after loading files
- Maintain consistent formatting across all title states - Bind mousewheel events to enable smooth scrolling
- Update scroll region after adding/removing thumbnails
- Handle platform-specific scroll events correctly
These helper functions form the backbone of the application's functionality, handling everything from state management to UI updates. Each function has a specific purpose and follows consistent patterns for maintainability. ### UI Feedback
- Use set_status() for temporary user feedback
- Include appropriate color coding (green=success, red=error)
- Keep messages concise and action-oriented
- Use 3-second default duration for most messages
### File Handle Management (NEW)
- Always close PIL Image objects after reading
- Critical for multi-file loading in image browser
- Prevents file locking issues on Windows
## v2.3.0 Helper Function Summary
### New Helpers
- flatten_json_if_valid() - Preserves compact JSON format
- detect_dark_mode() - Cross-platform theme detection
- get_theme_colors() - Adaptive color schemes
- scroll_to_thumbnail() - Auto-scroll to selected image
- bind_mousewheel() - Platform-specific scroll binding
- on_mousewheel() - Scroll event processing
- on_thumb_frame_configure() - Dynamic scroll region
### Updated Helpers
- format_value_for_display() - Enhanced for v2.3.0 JSON workflow
- show_about() - Updated with v2.3.0 features
### Critical Workflows
**JSON Editing Workflow**:
```
1. Select field -> on_selection_change()
2. Display -> format_value_for_display() (pretty JSON)
3. User edits in dialog
4. Save -> flatten_json_if_valid() (compact JSON)
5. Store in metadata_dict
```
**Theme Detection Workflow**:
```
1. App init -> detect_dark_mode()
2. Get colors -> get_theme_colors()
3. Apply to canvas/frames
4. Use consistently throughout UI
```
**Image Loading Workflow**:
```
1. User clicks thumbnail
2. load_file_from_path(filepath)
3. Open image -> Extract metadata -> img.close()
4. Update UI -> scroll_to_thumbnail(filepath)
```
These helper functions form the backbone of v2.3.0's enhanced functionality, handling everything from intelligent JSON management to adaptive theming and smooth image browsing.