# 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

#######################################
##                                   ##
##        G-code Edit Tab 0.96       ##
##        www.tormachtips.com        ##
##                                   ##
#######################################

# 0.96 - Edit tab now copies the installed PathPilot View Options tab styling. - 9/6/2026
# 0.95 - Public beta. Adds an EDIT command tab beside G-code and View Options. - 9/6/2026

import os
import gtk
import glib
import pango
import subprocess
import constants
import singletons
from ui_hooks import plugin

CURRENT_VER      = "0.96"
SCRIPT_NAME      = "G-code Edit Tab"
DESCRIPTION      = "Adds an EDIT command tab beside G-code and View Options for the loaded NC program."
ENABLED          = 1
DEV_MACHINE      = 1
DEV_MACHINE_FLAG = "/home/operator/gcode/python/dev_machine.txt"
EDIT_TAB_TEXT    = "Edit G-Code"
EDIT_TAB_FONT    = "Roboto Condensed 10"
START_DELAY_MS   = 1500
BOOTSTRAP_MS     = 250
BOOTSTRAP_LIMIT  = 40
STATE_POLL_MS    = 500
EDIT_PAGE_NAME   = "tt_gcode_edit_command_page"
EDIT_TAB_NAME    = "tt_gcode_edit_command_tab"

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 == 0:
            self.ShowMsg("Plugin loaded, but disabled.", constants.ALARM_LEVEL_QUIET)
            return
        self.ui = None
        self.notebook = None
        self.edit_page = None
        self.edit_tab_label = None
        self.edit_page_num = -1
        self.last_real_page_num = 0
        self.switch_handler_id = None
        self._bootstrap_count = 0
        glib.timeout_add(START_DELAY_MS, self.start_process)

    def start_process(self):
        try:
            self.ui = singletons.g_Machine
            if self.ui:
                pass
            else:
                glib.timeout_add(BOOTSTRAP_MS, self.bootstrap_edit_tab)
                return False
            if self.install_edit_tab():
                glib.timeout_add(STATE_POLL_MS, self.update_edit_tab_state)
                self.error_handler.write("[%s] %s %s started." % (SCRIPT_NAME, SCRIPT_NAME, CURRENT_VER), constants.ALARM_LEVEL_QUIET)
            else:
                glib.timeout_add(BOOTSTRAP_MS, self.bootstrap_edit_tab)
        except Exception as e:
            self.error_handler.write("[%s] Startup error: %s" % (SCRIPT_NAME, str(e)), constants.ALARM_LEVEL_LOW)
        return False

    def bootstrap_edit_tab(self):
        try:
            self._bootstrap_count += 1
            if self.ui:
                pass
            else:
                self.ui = singletons.g_Machine
            if self.install_edit_tab():
                glib.timeout_add(STATE_POLL_MS, self.update_edit_tab_state)
                self.error_handler.write("[%s] %s %s started." % (SCRIPT_NAME, SCRIPT_NAME, CURRENT_VER), constants.ALARM_LEVEL_QUIET)
                return False
            if self._bootstrap_count >= BOOTSTRAP_LIMIT:
                self.error_handler.write("[%s] Could not find the G-code options notebook." % SCRIPT_NAME, constants.ALARM_LEVEL_LOW)
                return False
        except Exception as e:
            self.error_handler.write("[%s] Bootstrap error: %s" % (SCRIPT_NAME, str(e)), constants.ALARM_LEVEL_LOW)
            return False
        return True

    def get_gcode_options_notebook(self):
        try:
            if self.ui and hasattr(self.ui, "gcode_options_notebook"):
                notebook = self.ui.gcode_options_notebook
                if notebook:
                    return notebook
            if self.ui and hasattr(self.ui, "builder"):
                notebook = self.ui.builder.get_object("gcode_options_notebook")
                if notebook:
                    return notebook
        except Exception as e:
            self.error_handler.write("[%s] Notebook lookup error: %s" % (SCRIPT_NAME, str(e)), constants.ALARM_LEVEL_LOW)
        return None

    def find_existing_edit_page(self, notebook):
        try:
            page_count = notebook.get_n_pages()
            for page_num in range(page_count):
                page = notebook.get_nth_page(page_num)
                if page:
                    try:
                        if page.get_name() == EDIT_PAGE_NAME:
                            return page_num, page
                    except:
                        pass
                    try:
                        tab_label = notebook.get_tab_label(page)
                        if tab_label and tab_label.get_name() == EDIT_TAB_NAME:
                            return page_num, page
                    except:
                        pass
        except:
            pass
        return -1, None

    def get_reference_tab_label(self, notebook):
        # PathPilot adds View Options immediately before this plugin adds its page.
        # Use that live OEM label as the style source so this follows the installed
        # PathPilot version instead of hard-coding a color, weight, or font size.
        try:
            page_count = notebook.get_n_pages()
            for page_num in range(page_count - 1, -1, -1):
                page = notebook.get_nth_page(page_num)
                if page is self.edit_page:
                    continue
                tab_label = notebook.get_tab_label(page)
                if isinstance(tab_label, gtk.Label):
                    return tab_label
        except:
            pass
        return None

    def copy_tab_label_style(self, source_label, target_label):
        if source_label:
            pass
        else:
            return False
        copied = False
        # set_markup() styling used by PathPilot is carried by the label's Pango
        # layout attributes. Copy those attributes so 2.9.1, 2.14.4, and later
        # versions can each supply their own font, weight, and foreground color.
        try:
            attributes = source_label.get_layout().get_attributes()
            if attributes:
                try:
                    attributes = attributes.copy()
                except:
                    pass
                target_label.set_attributes(attributes)
                copied = True
        except:
            pass
        # Also inherit the GTK widget style for any theme-driven properties that
        # are not part of the Pango markup.
        try:
            target_label.set_style(source_label.get_style().copy())
            copied = True
        except:
            pass
        try:
            xalign, yalign = source_label.get_alignment()
            target_label.set_alignment(xalign, yalign)
        except:
            pass
        try:
            xpad, ypad = source_label.get_padding()
            target_label.set_padding(xpad, ypad)
        except:
            pass
        return copied

    def make_edit_tab_label(self, notebook):
        label = gtk.Label()
        label.set_name(EDIT_TAB_NAME)
        label.set_text(EDIT_TAB_TEXT)
        source_label = self.get_reference_tab_label(notebook)
        if self.copy_tab_label_style(source_label, label):
            pass
        else:
            # Defensive fallback matching PathPilot 2.9.1 if an OEM label cannot
            # be obtained for some unexpected layout/version.
            label.set_markup('<span weight="regular" font_desc="%s" foreground="black">%s</span>' % (EDIT_TAB_FONT, EDIT_TAB_TEXT))
        label.show()
        return label

    def install_edit_tab(self):
        try:
            notebook = self.get_gcode_options_notebook()
            if notebook:
                pass
            else:
                return False
            self.notebook = notebook
            existing_page_num, existing_page = self.find_existing_edit_page(notebook)
            if existing_page_num >= 0:
                self.edit_page_num = existing_page_num
                self.edit_page = existing_page
                self.edit_tab_label = notebook.get_tab_label(existing_page)
                if isinstance(self.edit_tab_label, gtk.Label):
                    self.edit_tab_label.set_text(EDIT_TAB_TEXT)
                    self.copy_tab_label_style(self.get_reference_tab_label(notebook), self.edit_tab_label)
                if self.switch_handler_id is None:
                    self.switch_handler_id = notebook.connect("switch-page", self.on_gcode_options_switch_page)
                self.capture_current_real_page()
                self.update_edit_tab_state()
                return True
            # Wait until the OEM View Options page has been appended. The normal
            # PathPilot layout then has G-code at page 0 and View Options at page 1.
            if notebook.get_n_pages() < 2:
                return False
            self.capture_current_real_page()
            self.edit_page = gtk.Fixed()
            self.edit_page.set_name(EDIT_PAGE_NAME)
            self.edit_tab_label = self.make_edit_tab_label(notebook)
            self.edit_page_num = notebook.append_page(self.edit_page, self.edit_tab_label)
            self.edit_page.show()
            self.switch_handler_id = notebook.connect("switch-page", self.on_gcode_options_switch_page)
            self.update_edit_tab_state()
            return True
        except Exception as e:
            self.error_handler.write("[%s] Install error: %s" % (SCRIPT_NAME, str(e)), constants.ALARM_LEVEL_LOW)
            return False

    def capture_current_real_page(self):
        try:
            if self.notebook:
                current_page_num = self.notebook.get_current_page()
                if current_page_num >= 0 and current_page_num != self.edit_page_num:
                    self.last_real_page_num = current_page_num
        except:
            pass

    def get_loaded_program_path(self):
        try:
            if self.ui:
                pass
            else:
                return ""
            if hasattr(self.ui, "get_current_gcode_path"):
                path = self.ui.get_current_gcode_path()
                if path and os.path.isfile(path):
                    return path
            if hasattr(self.ui, "current_gcode_file_path"):
                path = getattr(self.ui, "current_gcode_file_path", "") or ""
                if path and os.path.isfile(path):
                    return path
        except Exception as e:
            self.error_handler.write("[%s] Loaded program lookup error: %s" % (SCRIPT_NAME, str(e)), constants.ALARM_LEVEL_LOW)
        return ""

    def update_edit_tab_state(self):
        try:
            if self.edit_tab_label:
                program_path = self.get_loaded_program_path()
                self.edit_tab_label.set_sensitive(bool(program_path and os.path.isfile(program_path)))
        except:
            pass
        return True

    def restore_previous_page(self):
        try:
            if self.notebook:
                page_count = self.notebook.get_n_pages()
                page_num = self.last_real_page_num
                if page_num < 0 or page_num >= page_count or page_num == self.edit_page_num:
                    page_num = 0
                self.notebook.set_current_page(page_num)
        except Exception as e:
            self.error_handler.write("[%s] Could not restore the previous G-code tab: %s" % (SCRIPT_NAME, str(e)), constants.ALARM_LEVEL_LOW)
        return False

    def open_loaded_program(self):
        program_path = self.get_loaded_program_path()
        if program_path and os.path.isfile(program_path):
            try:
                subprocess.Popen(["editscript", program_path])
            except Exception as e:
                self.error_handler.write("[%s] Could not open %s in editor: %s" % (SCRIPT_NAME, os.path.basename(program_path), str(e)), constants.ALARM_LEVEL_LOW)
        else:
            self.error_handler.write("[%s] No loaded NC program to edit." % SCRIPT_NAME, constants.ALARM_LEVEL_LOW)

    def on_gcode_options_switch_page(self, notebook, page_ptr_unused, page_num):
        try:
            if page_num == self.edit_page_num:
                self.open_loaded_program()
                glib.idle_add(self.restore_previous_page)
            else:
                self.last_real_page_num = page_num
        except Exception as e:
            self.error_handler.write("[%s] EDIT tab error: %s" % (SCRIPT_NAME, str(e)), constants.ALARM_LEVEL_LOW)
            
DESCRIPTION_LONG = """This script adds a third &quot;tab&quot; to the G-Code 
    Preview Pane. The &quot;tab&quot; is actually an Edit G-Code button that will open the 
    currently-loaded program for editing.</font></p>
    <p><font face="Verdana" size="2">It's the same thing as ALT + E, except the 
    hard button can be used while the program is running.</font></p>
    <p><font face="Verdana" size="2">There is no harm in editing a running 
    program, it will not &quot;update&quot; in real-time. When the current job is finished 
    (or aborted), PathPilot will <i>then</i> ask you to refresh the file. </font>
    </p>
    <p><a href="images/edittab.jpg">
    <img border="2" src="images/edittab_small.jpg" xthumbnail-orig-image="images/edittab.jpg" width="200" height="150">"""            