diff --git a/Helper-Methods.md b/Helper-Methods.md index e3f8f45..dd4613d 100644 --- a/Helper-Methods.md +++ b/Helper-Methods.md @@ -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("", 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("<>", 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 - - Include appropriate color coding (green=success, red=error) - - Keep messages concise and action-oriented +### 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 -4. **Event Handling**: - - Bind `on_selection_change()` to treeview selection events - - Ensure detail view is properly updated on every selection change +### 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 -5. **Title Management**: - - Call `update_title()` after any state change that affects title - - Maintain consistent formatting across all title states +### 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 -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. \ No newline at end of file +### 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. \ No newline at end of file