# coding: utf-8
# python 2 only

# Copyright (c) 2026 TormachTips.com. All rights reserved.
# Licensed under the TormachTips Personal Use License.
# Permission is granted only for private personal use and private personal modification.
# No sharing, publication, distribution, resale, sublicensing, screenshots, code excerpts,
# benchmarks, or videos are permitted without prior written permission.
# Requests:         tormach.1100m@gmail.com
# Information page: https://tormachtips.com/plugins.htm

#####################################
##                                 ##
##       File Favorites 1.11       ##
##       www.tormachtips.com       ##
##                                 ##
#####################################

# 1.11 - speeds Favorite row rendering by resolving the known GFile URI column directly and using the broader model scan only as fallback. - 9/10/2026
# 1.10 - fixes Favorite highlight drift during native filtering by isolating row-path cache entries per filter state and clearing cache before chooser filter changes. - 9/10/2026

import os
import gtk
import glib
import urllib
import subprocess
import constants
import singletons
import tormach_file_util
from ui_hooks import plugin

CURRENT_VER            = "1.11"
SCRIPT_NAME            = "File Favorites"
DESCRIPTION            = "Highlights favorite files and adds a live filename filter plus Edit, Rename, and Delete commands."
ENABLED                = 1
DEV_MACHINE            = 1
DEV_MACHINE_FLAG       = "/home/operator/gcode/python/dev_machine.txt"
FAVORITES_FILE         = "/home/operator/gcode/python/file_tab_favorites.txt"
FAVORITE_BACKGROUND    = "#F4A742"
FAVORITE_FOREGROUND    = "#000000"
POPUP_RETRY_MS         = 25
POPUP_RETRY_COUNT      = 6
FILTER_LABEL           = "Filter:"
FILTER_WIDTH_CHARS     = 20
FILTER_ENTRY_WIDTH     = 180
FILTER_ENTRY_HEIGHT    = 30
FILTER_NAME_COLUMN     = 0
FILTER_SIZE_COLUMN     = 8
FILTER_MODIFIED_COLUMN = 9
FILTER_URI_COLUMN      = 3
FILE_PAGE_NAME         = "notebook_file_util_wrapping_eventbox"

_ORIGINAL_HD_FILE_CHOOSER_INIT = None

def tt_favorites_filter_escape_glob(text):
    # GtkFileFilter.add_pattern() uses shell-style glob syntax. Build an ASCII
    # case-insensitive literal substring pattern without using a callback.
    escaped = ""
    for char in text:
        lower_char = char.lower()
        upper_char = char.upper()
        if lower_char != upper_char and len(lower_char) == 1 and len(upper_char) == 1:
            escaped += "[" + lower_char + upper_char + "]"
        elif char == "*":
            escaped += "[*]"
        elif char == "?":
            escaped += "[?]"
        elif char == "[":
            escaped += "[[]"
        else:
            escaped += char
    return escaped

def tt_favorites_filter_remove_native(chooser_owner):
    native_filter = getattr(chooser_owner, "tt_favorites_native_filter", None)
    if native_filter is None:
        return
    tt_favorites_clear_row_path_cache(chooser_owner, reset_column=False)
    try:
        chooser_owner.file_chooser.remove_filter(native_filter)
    except:
        pass
    chooser_owner.tt_favorites_native_filter = None
    tt_favorites_clear_row_path_cache(chooser_owner, reset_column=False)

def tt_favorites_filter_apply_native(chooser_owner):
    filter_text = getattr(chooser_owner, "tt_favorites_filter_text", "")
    last_applied_text = getattr(chooser_owner, "tt_favorites_last_applied_filter_text", None)
    native_filter = getattr(chooser_owner, "tt_favorites_native_filter", None)
    if filter_text == last_applied_text:
        if filter_text == "" and native_filter is None:
            return False
        if filter_text != "" and native_filter is not None:
            return False
    tt_favorites_filter_remove_native(chooser_owner)
    if filter_text == "":
        chooser_owner.tt_favorites_last_applied_filter_text = ""
        try:
            chooser_owner.tt_favorites_treeview.queue_resize()
            chooser_owner.tt_favorites_treeview.queue_draw()
        except:
            pass
        return False
    pattern = "*" + tt_favorites_filter_escape_glob(filter_text) + "*"
    tt_favorites_clear_row_path_cache(chooser_owner, reset_column=False)
    try:
        native_filter = gtk.FileFilter()
        native_filter.set_name("TormachTips File Filter")
        native_filter.add_pattern(pattern)
        chooser_owner.file_chooser.add_filter(native_filter)
        chooser_owner.file_chooser.set_filter(native_filter)
        chooser_owner.tt_favorites_native_filter = native_filter
        chooser_owner.tt_favorites_last_applied_filter_text = filter_text
        tt_favorites_clear_row_path_cache(chooser_owner, reset_column=False)
    except Exception as e:
        chooser_owner.error_handler.write("[%s] Native filter error: %s" % (SCRIPT_NAME, str(e)), constants.ALARM_LEVEL_MEDIUM)
    try:
        chooser_owner.tt_favorites_treeview.queue_resize()
        chooser_owner.tt_favorites_treeview.queue_draw()
    except:
        pass
    return False

def tt_favorites_normalize_path(path):
    if path is None:
        return ""
    try:
        path = os.path.abspath(path)
    except:
        pass
    try:
        path = os.path.normpath(path)
    except:
        pass
    return path

def tt_favorites_load():
    favorites = set()
    if os.path.isfile(FAVORITES_FILE) == False:
        return favorites
    try:
        handle = open(FAVORITES_FILE, "r")
        try:
            for raw_line in handle:
                path = raw_line.rstrip("\r\n")
                if path:
                    favorites.add(tt_favorites_normalize_path(path))
        finally:
            handle.close()
    except:
        pass
    return favorites

def tt_favorites_save(favorites):
    directory = os.path.dirname(FAVORITES_FILE)
    if directory and os.path.isdir(directory) == False:
        os.makedirs(directory)
    temp_path = FAVORITES_FILE + ".tmp"
    handle = open(temp_path, "w")
    try:
        for path in sorted(favorites):
            handle.write(path + "\n")
    finally:
        handle.close()
    os.rename(temp_path, FAVORITES_FILE)

def tt_favorites_find_file_treeview(widget):
    if isinstance(widget, gtk.TreeView):
        try:
            for column in widget.get_columns():
                title = str(column.get_title() or "").strip().lower()
                if title == "name":
                    return widget
        except:
            pass
    if isinstance(widget, gtk.Container):
        try:
            for child in widget.get_children():
                result = tt_favorites_find_file_treeview(child)
                if result is not None:
                    return result
        except:
            pass
    return None

def tt_favorites_get_column_renderers(column):
    try:
        return column.get_cell_renderers()
    except:
        pass
    try:
        return column.get_cells()
    except:
        pass
    return []

def tt_favorites_clear_row_path_cache(chooser_owner, reset_column=False):
    chooser_owner.tt_favorites_row_path_cache = {}
    if reset_column:
        chooser_owner.tt_favorites_preferred_path_column = None

def tt_favorites_row_cache_key(chooser_owner, model, tree_iter):
    current_folder = chooser_owner.file_chooser.get_current_folder()
    if current_folder is None:
        current_folder = ""
    current_folder = tt_favorites_normalize_path(current_folder)
    filter_text = getattr(chooser_owner, "tt_favorites_filter_text", "")
    try:
        tree_path = model.get_path(tree_iter)
        try:
            row_key = tree_path.to_string()
        except:
            row_key = str(tree_path)
    except:
        return None
    return (id(model), current_folder, filter_text, row_key)

def tt_favorites_cache_row_path(chooser_owner, model, tree_iter, path):
    cache_key = tt_favorites_row_cache_key(chooser_owner, model, tree_iter)
    if cache_key is None:
        return
    cache = getattr(chooser_owner, "tt_favorites_row_path_cache", None)
    if cache is None:
        cache = {}
        chooser_owner.tt_favorites_row_path_cache = cache
    cache[cache_key] = tt_favorites_normalize_path(path)

def tt_favorites_candidate_path(current_folder, value):
    if isinstance(value, basestring) == False:
        return ""
    candidate_name = value.strip()
    if candidate_name == "":
        return ""
    if candidate_name.startswith("file://"):
        try:
            candidate_path = urllib.unquote(candidate_name[7:])
        except:
            candidate_path = candidate_name[7:]
        candidate_path = tt_favorites_normalize_path(candidate_path)
        if os.path.exists(candidate_path):
            return candidate_path
    if os.path.isabs(candidate_name):
        candidate_path = tt_favorites_normalize_path(candidate_name)
        if os.path.exists(candidate_path):
            return candidate_path
    if current_folder:
        candidate_path = tt_favorites_normalize_path(os.path.join(current_folder, candidate_name))
        if os.path.exists(candidate_path):
            return candidate_path
    return ""

def tt_favorites_model_row_path(chooser_owner, model, tree_iter):
    # GTK can ask every renderer to redraw the same row repeatedly. Resolve a row's
    # filesystem path once, then reuse it until the chooser model/folder changes.
    cache_key = tt_favorites_row_cache_key(chooser_owner, model, tree_iter)
    cache = getattr(chooser_owner, "tt_favorites_row_path_cache", None)
    if cache is None:
        cache = {}
        chooser_owner.tt_favorites_row_path_cache = cache
    if cache_key is not None and cache_key in cache:
        return cache[cache_key]

    current_folder = chooser_owner.file_chooser.get_current_folder()
    if current_folder is None:
        current_folder = ""
    current_folder = tt_favorites_normalize_path(current_folder)

    # PathPilot's FileSystemModel column 3 is the row's GFile URI. Use that
    # directly during normal painting so Favorite highlighting avoids scanning
    # every model column and avoids repeated os.path.exists() calls.
    try:
        uri_value = model.get_value(tree_iter, FILTER_URI_COLUMN)
        if isinstance(uri_value, basestring) and uri_value.startswith("file://"):
            try:
                direct_path = urllib.unquote(uri_value[7:])
            except:
                direct_path = uri_value[7:]
            direct_path = tt_favorites_normalize_path(direct_path)
            if direct_path:
                chooser_owner.tt_favorites_preferred_path_column = FILTER_URI_COLUMN
                if cache_key is not None:
                    cache[cache_key] = direct_path
                return direct_path
    except:
        pass

    # Fallback for PathPilot builds/model states where the expected URI column is
    # unavailable or does not contain a file:// value.
    try:
        column_count = model.get_n_columns()
    except:
        if cache_key is not None:
            cache[cache_key] = ""
        return ""

    preferred_column = getattr(chooser_owner, "tt_favorites_preferred_path_column", None)
    column_indexes = []

    if preferred_column is not None and preferred_column >= 0 and preferred_column < column_count:
        column_indexes.append(preferred_column)

    for column_index in range(column_count):
        if column_index != preferred_column:
            column_indexes.append(column_index)

    for column_index in column_indexes:
        try:
            value = model.get_value(tree_iter, column_index)
        except:
            continue

        candidate_path = tt_favorites_candidate_path(current_folder, value)

        if candidate_path:
            chooser_owner.tt_favorites_preferred_path_column = column_index
            if cache_key is not None:
                cache[cache_key] = candidate_path
            return candidate_path

    if cache_key is not None:
        cache[cache_key] = ""

    return ""

def tt_favorites_renderer_text_for_column(column, model, tree_iter):
    try:
        title = str(column.get_title() or "").strip().lower()
    except:
        title = ""
    if title == "name":
        model_column = FILTER_NAME_COLUMN
    elif title == "size":
        model_column = FILTER_SIZE_COLUMN
    elif title == "modified":
        model_column = FILTER_MODIFIED_COLUMN
    else:
        return None
    try:
        value = model.get_value(tree_iter, model_column)
    except:
        value = None
    if value is None:
        return ""
    try:
        return value
    except:
        return str(value)

def tt_favorites_row_cell_data(column, renderer, model, tree_iter, chooser_owner):
    path = tt_favorites_model_row_path(chooser_owner, model, tree_iter)
    favorites = getattr(chooser_owner, "tt_favorites_paths", set())
    if path and path in favorites:
        try:
            renderer.set_property("cell-background", FAVORITE_BACKGROUND)
            renderer.set_property("cell-background-set", True)
        except:
            pass
        try:
            renderer.set_property("foreground", FAVORITE_FOREGROUND)
            renderer.set_property("foreground-set", True)
        except:
            pass
    else:
        try:
            renderer.set_property("cell-background-set", False)
        except:
            pass
        try:
            renderer.set_property("foreground-set", False)
        except:
            pass
    text = tt_favorites_renderer_text_for_column(column, model, tree_iter)
    if text is not None:
        try:
            renderer.set_property("text", text)
        except:
            pass

def tt_favorites_install_row_renderer(chooser_owner, treeview):
    installed_count = 0
    try:
        for column in treeview.get_columns():
            for renderer in tt_favorites_get_column_renderers(column):
                if isinstance(renderer, gtk.CellRendererText):
                    column.set_cell_data_func(renderer, tt_favorites_row_cell_data, chooser_owner)
                    installed_count += 1
    except:
        pass
    return installed_count > 0

def tt_favorites_toggle(chooser_owner, path):
    path = tt_favorites_normalize_path(path)
    if path == "" or os.path.isfile(path) == False:
        chooser_owner.error_handler.write("[%s] Could not mark favorite because the selected file path is invalid: %s" % (SCRIPT_NAME, path), constants.ALARM_LEVEL_MEDIUM)
        return
    favorites = tt_favorites_load()
    if path in favorites:
        favorites.remove(path)
        action_text = "Unmarked favorite"
    else:
        favorites.add(path)
        action_text = "Marked favorite"
    try:
        tt_favorites_save(favorites)
    except Exception as e:
        chooser_owner.error_handler.write("[%s] Could not save favorites: %s" % (SCRIPT_NAME, str(e)), constants.ALARM_LEVEL_MEDIUM)
        return
    chooser_owner.tt_favorites_paths = favorites
    chooser_owner.error_handler.write("[%s] %s: %s" % (SCRIPT_NAME, action_text, os.path.basename(path)), constants.ALARM_LEVEL_QUIET)
    try:
        chooser_owner.tt_favorites_treeview.queue_draw()
    except:
        pass

def tt_favorites_select_only_path(chooser_owner, path):
    path = tt_favorites_normalize_path(path)
    if path == "" or os.path.exists(path) == False:
        return False
    try:
        chooser_owner.file_chooser.unselect_all()
        chooser_owner.file_chooser.select_filename(path)
        return True
    except:
        return False

def tt_favorites_remove_missing_paths(chooser_owner):
    favorites = tt_favorites_load()
    cleaned = set()
    changed = False
    for path in favorites:
        if os.path.isfile(path):
            cleaned.add(path)
        else:
            changed = True
    if changed:
        try:
            tt_favorites_save(cleaned)
        except Exception as e:
            chooser_owner.error_handler.write("[%s] Could not update favorites after file operation: %s" % (SCRIPT_NAME, str(e)), constants.ALARM_LEVEL_MEDIUM)
            return
    chooser_owner.tt_favorites_paths = cleaned
    try:
        chooser_owner.tt_favorites_treeview.queue_draw()
    except:
        pass

def tt_favorites_find_renamed_path(old_path, old_stat):
    # A normal rename on the same filesystem keeps the inode. Use that instead of
    # assuming GTK leaves the newly renamed row selected after the OEM dialog closes.
    parent_dir = os.path.dirname(old_path)
    if parent_dir == "" or os.path.isdir(parent_dir) == False:
        return ""
    try:
        names = os.listdir(parent_dir)
    except:
        return ""
    for name in names:
        candidate = tt_favorites_normalize_path(os.path.join(parent_dir, name))
        if candidate == old_path:
            continue
        try:
            candidate_stat = os.stat(candidate)
        except:
            continue
        if candidate_stat.st_dev == old_stat.st_dev and candidate_stat.st_ino == old_stat.st_ino:
            return candidate
    return ""

def tt_favorites_rename(chooser_owner, path):
    path = tt_favorites_normalize_path(path)
    if tt_favorites_select_only_path(chooser_owner, path) == False:
        return
    favorites = tt_favorites_load()
    was_favorite = path in favorites
    try:
        old_stat = os.stat(path)
    except:
        old_stat = None
    try:
        chooser_owner.rename_item(None)
    except Exception as e:
        chooser_owner.error_handler.write("[%s] Rename failed: %s" % (SCRIPT_NAME, str(e)), constants.ALARM_LEVEL_MEDIUM)
        return
    if was_favorite and old_stat is not None and os.path.exists(path) == False:
        new_path = tt_favorites_find_renamed_path(path, old_stat)
        if new_path and os.path.isfile(new_path):
            try:
                favorites = tt_favorites_load()
                if path in favorites:
                    favorites.remove(path)
                favorites.add(new_path)
                tt_favorites_save(favorites)
                chooser_owner.tt_favorites_paths = favorites
            except Exception as e:
                chooser_owner.error_handler.write("[%s] Renamed file, but could not move its Favorite entry: %s" % (SCRIPT_NAME, str(e)), constants.ALARM_LEVEL_MEDIUM)
        else:
            # Do not silently point the favorite at a guessed filename. Remove only
            # the now-missing old entry if the OEM rename succeeded but cannot be traced.
            tt_favorites_remove_missing_paths(chooser_owner)
    else:
        chooser_owner.tt_favorites_paths = tt_favorites_load()
    tt_favorites_clear_row_path_cache(chooser_owner, reset_column=False)
    try:
        chooser_owner.tt_favorites_treeview.queue_draw()
    except:
        pass

def tt_favorites_edit(chooser_owner, path):
    path = tt_favorites_normalize_path(path)
    if os.path.isfile(path) == False:
        return
    if tt_favorites_select_only_path(chooser_owner, path) == False:
        return
    try:
        chooser_owner.error_handler.write("Opening %s in editor." % path, constants.ALARM_LEVEL_QUIET)
        subprocess.Popen(['editscript', path])
    except Exception as e:
        chooser_owner.error_handler.write("[%s] Could not open file in editor: %s" % (SCRIPT_NAME, str(e)), constants.ALARM_LEVEL_MEDIUM)

def tt_favorites_delete(chooser_owner, path):
    path = tt_favorites_normalize_path(path)
    if tt_favorites_select_only_path(chooser_owner, path) == False:
        return
    try:
        chooser_owner.delete_selected_items()
    except Exception as e:
        chooser_owner.error_handler.write("[%s] Delete failed: %s" % (SCRIPT_NAME, str(e)), constants.ALARM_LEVEL_MEDIUM)
        return
    tt_favorites_remove_missing_paths(chooser_owner)
    tt_favorites_clear_row_path_cache(chooser_owner, reset_column=False)

def tt_favorites_make_action_item(text, chooser_owner, selected_path, callback, files_only=False):
    selected_path = tt_favorites_normalize_path(selected_path)
    menu_item = gtk.MenuItem(text)
    path_valid = selected_path != "" and os.path.exists(selected_path)
    if files_only and path_valid:
        path_valid = os.path.isfile(selected_path)
    if path_valid:
        menu_item.connect("activate", lambda widget, owner=chooser_owner, path=selected_path: callback(owner, path))
    else:
        menu_item.set_sensitive(False)
    return menu_item

def tt_favorites_make_menu_item(chooser_owner, selected_path):
    selected_path = tt_favorites_normalize_path(selected_path)
    favorites = getattr(chooser_owner, "tt_favorites_paths", set())
    if selected_path and selected_path in favorites:
        menu_text = "Unmark Favorite"
    else:
        menu_text = "Mark as Favorite"
    menu_item = gtk.MenuItem(menu_text)
    if selected_path == "" or os.path.isfile(selected_path) == False:
        menu_item.set_sensitive(False)
    else:
        menu_item.connect("activate", lambda widget, owner=chooser_owner, path=selected_path: tt_favorites_toggle(owner, path))
    return menu_item

def tt_favorites_find_grabbed_menu():
    try:
        widget = gtk.grab_get_current()
    except:
        widget = None
    while widget is not None:
        if isinstance(widget, gtk.Menu):
            return widget
        try:
            widget = widget.get_parent()
        except:
            widget = None
    return None

def tt_favorites_build_context_items(chooser_owner, selected_path):
    return [
        tt_favorites_make_menu_item(chooser_owner, selected_path),
        gtk.SeparatorMenuItem(),
        tt_favorites_make_action_item("Rename", chooser_owner, selected_path, tt_favorites_rename),
        tt_favorites_make_action_item("Edit", chooser_owner, selected_path, tt_favorites_edit, files_only=True),
        tt_favorites_make_action_item("Delete", chooser_owner, selected_path, tt_favorites_delete)]

def tt_favorites_remove_old_context_items(menu):
    old_items = getattr(menu, "tt_favorites_context_items", [])
    for old_item in old_items:
        try:
            menu.remove(old_item)
            old_item.destroy()
        except:
            pass
    menu.tt_favorites_context_items = []

def tt_favorites_append_to_oem_menu(menu, chooser_owner, selected_path):
    try:
        # GtkFileChooser commonly reuses the same Menu object. Rebuild every custom
        # item so no action remains bound to a path from an earlier right-click.
        tt_favorites_remove_old_context_items(menu)
        leading_separator = gtk.SeparatorMenuItem()
        custom_items = [leading_separator] + tt_favorites_build_context_items(chooser_owner, selected_path)
        for menu_item in custom_items:
            menu.append(menu_item)
            menu_item.show()
        menu.tt_favorites_context_items = custom_items
        return True
    except:
        return False

def tt_favorites_popup_fallback(chooser_owner, button, event_time, selected_path):
    menu = gtk.Menu()
    for menu_item in tt_favorites_build_context_items(chooser_owner, selected_path):
        menu.append(menu_item)
        menu_item.show()
    menu.popup(None, None, None, button, event_time)

def tt_favorites_try_oem_menu(chooser_owner, button, event_time, selected_path, attempt_state):
    menu = tt_favorites_find_grabbed_menu()
    if menu is not None:
        if tt_favorites_append_to_oem_menu(menu, chooser_owner, selected_path):
            return False
    attempt_state[0] += 1
    if attempt_state[0] < POPUP_RETRY_COUNT:
        return True
    tt_favorites_popup_fallback(chooser_owner, button, event_time, selected_path)
    return False

def tt_favorites_get_path_at_event(treeview, event, chooser_owner):
    try:
        result = treeview.get_path_at_pos(int(event.x), int(event.y))
    except:
        return ""
    if result is None:
        return ""
    tree_path = result[0]
    try:
        treeview.set_cursor(tree_path)
    except:
        pass
    # Prefer GtkFileChooser's public API after selecting the right-clicked row.
    # It returns the real absolute path and stays correct in nested directories.
    try:
        selected = chooser_owner.file_chooser.get_filenames()
        if len(selected) == 1:
            selected_path = tt_favorites_normalize_path(selected[0])
            if os.path.isfile(selected_path):
                try:
                    model = treeview.get_model()
                    tree_iter = model.get_iter(tree_path)
                    tt_favorites_cache_row_path(chooser_owner, model, tree_iter, selected_path)
                except:
                    pass
                return selected_path
    except:
        pass
    try:
        model = treeview.get_model()
        tree_iter = model.get_iter(tree_path)
    except:
        return ""
    return tt_favorites_model_row_path(chooser_owner, model, tree_iter)

def tt_favorites_get_keyboard_selected_path(treeview, chooser_owner):
    # Keyboard navigation moves the TreeView cursor without generating a mouse event.
    # Resolve the file from the current keyboard cursor/selection so the context-menu
    # action never reuses the path from the last row clicked with the mouse.
    try:
        cursor_path, cursor_column = treeview.get_cursor()
    except:
        cursor_path = None
    if cursor_path is not None:
        try:
            model = treeview.get_model()
            tree_iter = model.get_iter(cursor_path)
        except:
            model = None
            tree_iter = None
        if model is not None and tree_iter is not None:
            # GtkFileChooser's public selection is the best absolute-path source when
            # exactly one file is selected. Cache it against the current cursor row.
            try:
                selected = chooser_owner.file_chooser.get_filenames()
                if len(selected) == 1:
                    selected_path = tt_favorites_normalize_path(selected[0])
                    if os.path.isfile(selected_path):
                        tt_favorites_cache_row_path(chooser_owner, model, tree_iter, selected_path)
                        return selected_path
            except:
                pass
            selected_path = tt_favorites_model_row_path(chooser_owner, model, tree_iter)
            if selected_path:
                return selected_path
    try:
        selected = chooser_owner.file_chooser.get_filenames()
        if len(selected) == 1:
            selected_path = tt_favorites_normalize_path(selected[0])
            if os.path.isfile(selected_path):
                return selected_path
    except:
        pass
    return ""

def tt_favorites_filter_entry_changed(entry, chooser_owner):
    try:
        chooser_owner.tt_favorites_filter_text = entry.get_text().strip()
        glib.idle_add(tt_favorites_filter_apply_native, chooser_owner)
    except Exception as e:
        chooser_owner.error_handler.write("[%s] Filter refresh error: %s" % (SCRIPT_NAME, str(e)), constants.ALARM_LEVEL_MEDIUM)

def tt_favorites_filter_entry_key_press(entry, event, chooser_owner):
    if event.keyval == gtk.keysyms.Escape:
        entry.set_text("")
        return True
    return False

def tt_favorites_filter_entry_button_press(entry, event, chooser_owner):
    # Keep Filter out of normal notebook/tab traversal, but allow a deliberate
    # mouse click to make it focusable before GTK processes the click.
    try:
        entry.set_can_focus(True)
    except:
        pass
    return False

def tt_favorites_filter_entry_focus_out(entry, event, chooser_owner):
    # Once the user leaves Filter, remove it from normal tab traversal again.
    try:
        entry.set_can_focus(False)
    except:
        pass
    return False

def tt_favorites_filter_main_window_event(widget, event, chooser_owner):
    if event.type != gtk.gdk.KEY_PRESS:
        return False
    entry = getattr(chooser_owner, "tt_favorites_filter_entry", None)
    if entry is None:
        return False
    # PathPilot attaches its own global key_press_event handler to the main
    # window. Its handler consumes Escape as STOP, so a normal key-press-event
    # connection on the entry never gets a chance to use Escape. The generic
    # GTK "event" signal fires before PathPilot's specific key_press_event
    # handler, which lets us reserve Escape only while our filter entry has focus.
    if event.keyval == gtk.keysyms.Escape:
        try:
            if entry.has_focus():
                if len(entry.get_text()) > 0:
                    entry.set_text("")
                    chooser_owner.parentwindow.set_focus(None)
                    return True
                # Empty filter: do not consume Escape. Let PathPilot's normal
                # global handler receive this same key press as STOP.
                chooser_owner.parentwindow.set_focus(None)
                return False
        except:
            pass
        return False
    is_ctrl_f = (
        event.keyval in (gtk.keysyms.f, gtk.keysyms.F) and
        (event.state & gtk.gdk.CONTROL_MASK))
    if is_ctrl_f == False:
        return False
    try:
        entry.set_can_focus(True)
        entry.grab_focus()
        entry.select_region(0, -1)
        return True
    except:
        return False

def tt_favorites_filter_install_entry(chooser_owner):
    try:
        toolbar = chooser_owner.builder.get_object("top_toolbar_hbox")
    except:
        toolbar = None
    if toolbar is None:
        chooser_owner.error_handler.write("[%s] Could not find top_toolbar_hbox for live filter." % SCRIPT_NAME, constants.ALARM_LEVEL_MEDIUM)
        return False
    row = gtk.HBox(False, 5)
    label = gtk.Label(FILTER_LABEL)
    entry = gtk.Entry()
    entry.set_width_chars(FILTER_WIDTH_CHARS)
    entry.set_size_request(FILTER_ENTRY_WIDTH, FILTER_ENTRY_HEIGHT)
    entry.set_tooltip_text("Type part of a filename to filter the File tab. Matching is case-insensitive. Press Escape to clear.")
    row.pack_start(label, False, False, 0)
    row.pack_start(entry, False, False, 0)
    toolbar.pack_end(row, False, False, 0)
    chooser_owner.tt_favorites_filter_text = ""
    chooser_owner.tt_favorites_filter_row = row
    chooser_owner.tt_favorites_filter_entry = entry
    entry.set_can_focus(False)
    entry.connect("changed", tt_favorites_filter_entry_changed, chooser_owner)
    entry.connect("key-press-event", tt_favorites_filter_entry_key_press, chooser_owner)
    entry.connect("button-press-event", tt_favorites_filter_entry_button_press, chooser_owner)
    entry.connect("focus-out-event", tt_favorites_filter_entry_focus_out, chooser_owner)
    row.show_all()
    return True

def tt_favorites_on_tree_button_press(treeview, event, chooser_owner):
    if event.type not in (gtk.gdk.BUTTON_PRESS, gtk.gdk._2BUTTON_PRESS):
        return False
    if event.type != gtk.gdk.BUTTON_PRESS:
        return False
    if event.button != 3:
        return False
    selected_path = tt_favorites_get_path_at_event(treeview, event, chooser_owner)
    chooser_owner.tt_favorites_paths = tt_favorites_load()
    attempt_state = [0]
    glib.timeout_add(POPUP_RETRY_MS, tt_favorites_try_oem_menu, chooser_owner, event.button, event.time, selected_path, attempt_state)
    return False

def tt_favorites_on_tree_key_press(treeview, event, chooser_owner):
    is_menu_key = event.keyval == gtk.keysyms.Menu
    is_shift_f10 = event.keyval == gtk.keysyms.F10 and (event.state & gtk.gdk.SHIFT_MASK)
    if is_menu_key == False and is_shift_f10 == False:
        return False
    selected_path = tt_favorites_get_keyboard_selected_path(treeview, chooser_owner)
    chooser_owner.tt_favorites_paths = tt_favorites_load()
    attempt_state = [0]
    # Return False so GtkFileChooser still creates its normal keyboard context menu.
    # Our delayed hook then appends the favorite action to that newly opened menu.
    glib.timeout_add(POPUP_RETRY_MS, tt_favorites_try_oem_menu, chooser_owner, 0, event.time, selected_path, attempt_state)
    return False

def tt_favorites_on_notebook_switch_page(notebook, page, page_num, chooser_owner):
    try:
        current_page = notebook.get_nth_page(page_num)
        page_id = gtk.Buildable.get_name(current_page)
    except:
        return
    # Clear filtering only after switching away from the File tab. The File page
    # is already hidden at this point, so neither the text reset nor list refresh
    # is visible when the user returns.
    if page_id == FILE_PAGE_NAME:
        return
    entry = getattr(chooser_owner, "tt_favorites_filter_entry", None)
    if entry is None:
        return
    try:
        entry.set_can_focus(False)
    except:
        pass
    try:
        if entry.get_text() != "":
            entry.set_text("")
            tt_favorites_filter_apply_native(chooser_owner)
        elif getattr(chooser_owner, "tt_favorites_native_filter", None) is not None:
            chooser_owner.tt_favorites_filter_text = ""
            tt_favorites_filter_apply_native(chooser_owner)
    except:
        pass

def tt_favorites_on_folder_changed(widget, chooser_owner):
    # GTK reuses the same private model while changing directories. Row indexes now
    # represent different files, so discard only the row/path cache and redraw.
    chooser_owner.tt_favorites_paths = tt_favorites_load()
    tt_favorites_clear_row_path_cache(chooser_owner, reset_column=True)
    try:
        glib.idle_add(chooser_owner.tt_favorites_treeview.queue_draw)
    except:
        pass

def tt_favorites_on_model_changed(model, chooser_owner):
    # File creation/deletion or sorting can move a different file into an existing
    # TreePath without changing folders. The model schema is unchanged, so retain
    # the preferred path column but discard cached row identities.
    tt_favorites_clear_row_path_cache(chooser_owner, reset_column=False)

def tt_favorites_attach_to_chooser(chooser_owner):
    if getattr(chooser_owner, "tt_favorites_installed", False):
        return False
    treeview = tt_favorites_find_file_treeview(chooser_owner.file_chooser)
    if treeview is None:
        chooser_owner.error_handler.write("[%s] Could not find the File-tab file list TreeView." % SCRIPT_NAME, constants.ALARM_LEVEL_MEDIUM)
        return False
    chooser_owner.tt_favorites_treeview = treeview
    # Disable the GtkFileChooser file-row hover tooltip. PathPilot's click
    # behavior and context menu remain unchanged.
    try:
        treeview.set_has_tooltip(False)
    except:
        pass
    chooser_owner.tt_favorites_paths = tt_favorites_load()
    chooser_owner.tt_favorites_native_filter = None
    chooser_owner.tt_favorites_last_applied_filter_text = None
    chooser_owner.tt_favorites_row_path_cache = {}
    chooser_owner.tt_favorites_preferred_path_column = None
    chooser_owner.tt_favorites_installed = True
    treeview.connect("button-press-event", tt_favorites_on_tree_button_press, chooser_owner)
    treeview.connect("key-press-event", tt_favorites_on_tree_key_press, chooser_owner)
    # Catch Ctrl+F and filter-entry Escape before PathPilot's global
    # main-window key_press_event handler.
    try:
        chooser_owner.tt_favorites_main_window_event_handler = chooser_owner.parentwindow.connect(
            "event",
            tt_favorites_filter_main_window_event,
            chooser_owner)
    except Exception as e:
        chooser_owner.error_handler.write("[%s] Could not install main-window filter shortcut handler: %s" % (SCRIPT_NAME, str(e)), constants.ALARM_LEVEL_MEDIUM)
    chooser_owner.file_chooser.connect("current-folder-changed", tt_favorites_on_folder_changed, chooser_owner)
    # Clear the live filter after the user leaves the File tab.
    try:
        ui = getattr(singletons, "g_Machine", None)
        notebook = getattr(ui, "notebook", None)
        if notebook is not None:
            chooser_owner.tt_favorites_notebook_switch_handler = notebook.connect_after(
                "switch-page",
                tt_favorites_on_notebook_switch_page,
                chooser_owner)
    except Exception as e:
        chooser_owner.error_handler.write("[%s] Could not install File-tab filter reset handler: %s" % (SCRIPT_NAME, str(e)), constants.ALARM_LEVEL_MEDIUM)
    try:
        model = treeview.get_model()
        model.connect("row-inserted", lambda model, path, tree_iter, owner=chooser_owner: tt_favorites_on_model_changed(model, owner))
        model.connect("row-deleted", lambda model, path, owner=chooser_owner: tt_favorites_on_model_changed(model, owner))
        model.connect("rows-reordered", lambda model, path, tree_iter, new_order, owner=chooser_owner: tt_favorites_on_model_changed(model, owner))
    except:
        pass
    filter_entry_installed = tt_favorites_filter_install_entry(chooser_owner)
    if tt_favorites_install_row_renderer(chooser_owner, treeview):
        if filter_entry_installed:
            chooser_owner.error_handler.write("[%s] Installed. Favorites use orange row highlighting and native filename filtering." % SCRIPT_NAME, constants.ALARM_LEVEL_QUIET)
        else:
            chooser_owner.error_handler.write("[%s] Favorites installed, but the live filter entry could not be added." % SCRIPT_NAME, constants.ALARM_LEVEL_MEDIUM)
    else:
        chooser_owner.error_handler.write("[%s] Installed menu handling, but could not attach row highlighting/filter rendering." % SCRIPT_NAME, constants.ALARM_LEVEL_MEDIUM)
    return False

def tt_favorites_hd_file_chooser_init(self, *args, **kwargs):
    _ORIGINAL_HD_FILE_CHOOSER_INIT(self, *args, **kwargs)
    glib.idle_add(tt_favorites_attach_to_chooser, self)

def tt_favorites_patch():
    global _ORIGINAL_HD_FILE_CHOOSER_INIT
    if getattr(tormach_file_util.hd_file_chooser, "tt_favorites_patched", False):
        return False
    _ORIGINAL_HD_FILE_CHOOSER_INIT = tormach_file_util.hd_file_chooser.__init__
    tormach_file_util.hd_file_chooser.__init__ = tt_favorites_hd_file_chooser_init
    tormach_file_util.hd_file_chooser.tt_favorites_patched = True
    return True

class UserPlugin(plugin):
    def __init__(self):
        plugin.__init__(self, "%s %s" % (SCRIPT_NAME, CURRENT_VER))
        dev_machine_found = os.path.exists(DEV_MACHINE_FLAG)
        if dev_machine_found:
            plugin_enabled = DEV_MACHINE
        else:
            plugin_enabled = ENABLED
        if plugin_enabled:
            if tt_favorites_patch():
                self.error_handler.write("[%s] File chooser hook installed." % SCRIPT_NAME, constants.ALARM_LEVEL_QUIET)
            else:
                self.error_handler.write("[%s] File chooser hook was already installed." % SCRIPT_NAME, constants.ALARM_LEVEL_QUIET)
            return
        if dev_machine_found:
            self.error_handler.write("[%s] Dev machine found. Plugin loaded, but disabled by DEV_MACHINE." % SCRIPT_NAME, constants.ALARM_LEVEL_QUIET)
        else:
            self.error_handler.write("[%s] Plugin loaded, but is disabled." % SCRIPT_NAME, constants.ALARM_LEVEL_QUIET)
DESCRIPTION_LONG = """This script will 
    allow you to select Favorite files. The idea being, if you have a folder 
    with probing ops, top ops, bottom ops and more, you can highlight which ones 
    you use the most often. It also adds a live filename filter at the top-right 
    of the File tab. Filtering uses GtkFileChooser's native GtkFileFilter API,
    so non-matching files are removed by the chooser itself without replacing
    the OEM file model. </font></p>
    <p><font face="Verdana" size="2">This is all on right-click and you also get 
    Rename, Delete and Edit options, too.</font></p>
    <p><a href="images/favoriter1.jpg">
    <img border="2" src="images/favoriter1_small.jpg" xthumbnail-orig-image="images/favoriter1.jpg" width="200" height="150"></a>
    <a href="images/favoriter2.jpg">
    <img border="2" src="images/favoriter2_small.jpg" xthumbnail-orig-image="images/favoriter2.jpg" width="200" height="150">"""            