Update Helper Methods
parent
405075c4eb
commit
8c0fcc4c0f
1 changed files with 288 additions and 182 deletions
|
|
@ -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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue