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
#### `mark_as_modified()`
```python
def mark_as_modified(self):
"""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
#### mark_as_modified()
**Purpose**: Tracks when metadata has been modified but not saved
**Behavior**:
1. Sets `has_unsaved_changes` flag to True
1. Sets has_unsaved_changes flag to True
2. Updates window title with asterisk (*)
3. Shows "Unsaved changes" indicator in UI
4. Only triggers once per modification session
@ -28,20 +19,12 @@ self.metadata_dict["NewKey"] = "Value"
self.mark_as_modified()
```
#### `mark_as_saved()`
```python
def mark_as_saved(self):
"""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
#### mark_as_saved()
**Purpose**: Resets the unsaved changes indicator and updates UI to reflect saved state
**Behavior**:
1. Clears `has_unsaved_changes` flag
1. Clears has_unsaved_changes flag
2. Removes asterisk from window title
3. Hides unsaved changes indicator
@ -54,139 +37,242 @@ self.set_status("Metadata saved successfully", color="green")
### Display Formatting Helpers
#### `format_value_for_display(value)`
```python
def format_value_for_display(self, value):
"""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
#### format_value_for_display(value) - UPDATED v2.3.0
**Purpose**: Intelligently formats metadata values for display in editor
**Behavior**:
1. Attempts to parse value as JSON
2. If successful, returns pretty-printed with 2-space indentation
3. Falls back to string representation if parsing fails
4. Preserves Unicode characters with `ensure_ascii=False`
3. Falls back to original value if parsing fails
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**:
```python
# JSON-formatted metadata
json_value = '{"camera":"Canon","iso":100}'
formatted = app.format_value_for_display(json_value)
# Returns: {"camera": "Canon", "iso": 100}
# Returns pretty-printed JSON
# Plain text metadata
text_value = "Simple string value"
formatted = app.format_value_for_display(text_value)
# Returns: Simple string value
# Returns: "Simple string value"
```
#### `truncate_value(value, max_length=80)`
```python
def truncate_value(self, value, max_length=80):
"""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
#### flatten_json_if_valid(value) - NEW v2.3.0
**Purpose**: Preserves original compact JSON format when saving
**Behavior**:
1. Converts value to string
2. Checks if length exceeds `max_length` (default 80)
3. Truncates with ellipsis (...) if too long
4. Returns full value if within length limit
1. Attempts to parse value as JSON
2. If successful, returns flattened single-line JSON
3. Uses separators=(',', ':') for compact format (no spaces)
4. Falls back to original value if parsing fails
5. Preserves Unicode characters
**Parameters**:
- `value`: The metadata value to truncate
- `max_length` (optional): Maximum length before truncation (default: 80)
**Key Feature**: Automatically called during save operations to maintain the original compact format of JSON metadata.
**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**:
```python
# Long metadata value
long_value = "This is a very long string that exceeds the maximum display length"
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
short_value = "Short"
truncated = app.truncate_value(short_value)
# Returns: "Short" (unchanged)
### Theme System Helpers - NEW v2.3.0
#### detect_dark_mode()
**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
#### `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_status(message, duration=3000, color="")
# Set the message
self.status_label.config(text=message, foreground=color)
**Purpose**: Provides temporary feedback to users with auto-clear
# Schedule clearing the message
if duration > 0:
self.status_timer = self.root.after(duration, lambda: self.status_label.config(text="Ready", foreground=""))
```
**Purpose**:
- Provides temporary feedback to users
- Automatically clears messages after specified duration
**Parameters**:
- message: Text to display in status bar
- duration (optional): Time before auto-clear in ms (default: 3000)
- color (optional): Text color like "green" or "red"
**Behavior**:
1. Cancels any existing status timer
2. Updates status label with new message and color
3. Schedules automatic clearing after duration (ms)
4. Defaults to 3000ms (3 seconds) timeout
**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
3. Schedules automatic clearing after duration
4. Duration=0 means permanent message
**Example Usage**:
```python
# Temporary success message
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")
# Custom duration message
# Custom duration
app.set_status("Processing...", duration=5000) # 5 seconds
```
#### `update_title()`
```python
def update_title(self):
"""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 (*)
#### update_title()
**Purpose**: Maintains consistent window title format with unsaved indicator
**Behavior**:
1. Constructs base title with app name and version
@ -196,52 +282,28 @@ def update_title(self):
**Example Usage**:
```python
# When opening a file
app.current_file = "example.png"
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.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
#### `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")
#### on_selection_change(event)
if key in self.metadata_dict:
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
**Purpose**: Synchronizes detail view with tree selection
**Behavior**:
1. Gets currently selected item from tree
2. Extracts key from selection values
3. Retrieves full value from metadata_dict
4. Formats value for display (JSON pretty-printing)
5. Updates detail text widget with formatted value
6. Disables editing of detail view
**Parameters**:
- `event`: Tkinter event object (automatically passed)
4. Formats value for display via format_value_for_display()
5. Updates detail text widget
6. Disables editing of detail view (read-only)
**Example Usage**:
```python
@ -251,57 +313,101 @@ self.tree.bind("<<TreeviewSelect>>", self.on_selection_change)
### Utility Helpers
#### `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"
#### show_about()
messagebox.showinfo(
f"About {APP_NAME}",
about_text,
parent=self.root
)
```
**Purpose**:
- Displays application information dialog
- Provides version, description, author, and license info
**Purpose**: Displays application information dialog
**Behavior**:
1. Constructs about text with app metadata
2. Shows modal dialog using `messagebox.showinfo()`
3. Includes standard application information
2. Shows modal dialog using messagebox.showinfo()
3. Includes v2.3.0 feature highlights
**Example Usage**:
```python
# Called when About button is clicked
app.show_about()
```
## Helper Function Best Practices
## Helper Function Best Practices v2.3.0
1. **State Management**:
- Always call `mark_as_modified()` after any metadata change
- Call `mark_as_saved()` only after successful save operations
### State Management
- Always call mark_as_modified() after any metadata change
- Call mark_as_saved() only after successful save operations
- Ensure title updates reflect current state
2. **Display Formatting**:
- Use `format_value_for_display()` for all detail view updates
- Apply `truncate_value()` only to tree view values
### JSON Handling (NEW)
- Use format_value_for_display() for editor and detail views
- 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**:
- Use `set_status()` for temporary user feedback
### Display Formatting
- Use format_value_for_display() for all detail view updates
- Apply truncate_value() only to tree view values
- Keep tree view compact, detail view readable
### Theme Integration (NEW)
- Call detect_dark_mode() once during initialization
- Use get_theme_colors() for all theme-dependent widgets
- Apply theme colors to canvas, frames, and labels
- Fallback to light mode on detection failure
### Image Browser (NEW)
- Always call scroll_to_thumbnail() after loading files
- Bind mousewheel events to enable smooth scrolling
- Update scroll region after adding/removing thumbnails
- Handle platform-specific scroll events correctly
### 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
4. **Event Handling**:
- Bind `on_selection_change()` to treeview selection events
- Ensure detail view is properly updated on every selection change
### 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
5. **Title Management**:
- Call `update_title()` after any state change that affects title
- Maintain consistent formatting across all title states
## v2.3.0 Helper Function Summary
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.
### 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.