# 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

##################################################
##                                              ##
##            Hobbs All Viewer 0.97             ##
##         https://www.tormachtips.com          ##
##                                              ##
###################################################

# 0.97 - Adds a spot for Download & Update Manager - 4/13/2026
# 0.96 adds disabled / enabled flag. 4/12/2026

import os
import sys
import subprocess
import Tkinter as tk
import tkMessageBox

CURRENT_VER                = "0.97"
SCRIPT_NAME                = "Hobbs All Scripts Viewer"
DESCRIPTION                = "A launcher for all the Spindle Hobbs / Tool Life scripts."
WINDOW_TITLE               = "Hobbs Program Launcher"
WINDOW_GEOMETRY            = "900x620"
WINDOW_MIN_WIDTH           = 760
WINDOW_MIN_HEIGHT          = 520
START_FULLSCREEN           = True
SHOW_HEADER                = False
HEADER_TITLE               = "Machine Utility Programs"
HEADER_DESCRIPTION         = "Select a program below to launch it."
QUIT_BUTTON_TEXT           = "Quit"
MISSING_SUFFIX             = "\n[NOT INSTALLED]"
BASE_DIR                   = "/home/operator/gcode/python"
GRID_COLUMNS               = 2
GRID_ROWS                  = 5
PROGRAMS_PER_PAGE          = GRID_COLUMNS * GRID_ROWS
OUTER_PAD_X                = 12
OUTER_PAD_Y                = 10
COLUMN_PAD_X               = 8
ROW_PAD_Y                  = 6
BUTTON_HEIGHT              = 2
BUTTON_WRAP_LENGTH         = 260
DESCRIPTION_WRAP_LENGTH    = 280
CARD_HEIGHT                = 108
CARD_INNER_PAD_X           = 10
CARD_TOP_PAD_Y             = 8
CARD_MIDDLE_PAD_Y          = 4
CARD_BOTTOM_PAD_Y          = 8
QUIT_PAD_TOP               = 6
FONT_HEADER                = ("Helvetica", 18, "bold")
FONT_SUBHEADER             = ("Helvetica", 10)
FONT_BUTTON                = ("Helvetica", 11, "bold")
FONT_DESCRIPTION           = ("Helvetica", 9)
FONT_QUIT                  = ("Helvetica", 11, "bold")
COLOR_WINDOW_BG            = "#f2f4f7"
COLOR_HEADER_BG            = "#f2f4f7"
COLOR_CARD_BG              = "#ffffff"
COLOR_TEXT_PRIMARY         = "#1f2933"
COLOR_TEXT_SECONDARY       = "#52606d"
COLOR_BUTTON_BG            = "#2f6db3"
COLOR_BUTTON_FG            = "#ffffff"
COLOR_BUTTON_ACTIVE_BG     = "#24578f"
COLOR_BUTTON_ACTIVE_FG     = "#ffffff"
COLOR_BUTTON_DISABLED_BG   = "#d5dbe1"
COLOR_BUTTON_DISABLED_FG   = "#7b8794"
COLOR_DESCRIPTION_DISABLED = "#9aa5b1"
COLOR_FOCUS_BG             = "#16324f"
COLOR_QUIT_BG              = "#7a1f1f"
COLOR_QUIT_FG              = "#ffffff"
COLOR_QUIT_ACTIVE_BG       = "#5e1717"
COLOR_QUIT_ACTIVE_FG       = "#ffffff"

PROGRAMS = [
    {
        "title": "Tool Life Manager",
        "filename": "hobbs_db_viewer.py",
        "description": "Tool History, with Age and Usage."
    },
    {
        "title": "Daily Tool Use",
        "filename": "hobbs_daily.py",
        "description": "Show daily usage summaries."
    },
    {
        "title": "Spindle Time, by Month",
        "filename": "hobbs_monthly.py",
        "description": "Review spindle totals by month."
    },
    {
        "title": "Spindle Time, by Hour",
        "filename": "hobbs_hourly.py",
        "description": "Display hour-by-hour spindle usage."
    },
    {
        "title": "Quick Spindle Total Time",
        "filename": "hobbs_box.py",
        "description": "Quick popup of total spindle time."
    },
    {
        "title": "File Load History",
        "filename": "load_history.py",
        "description": "History of completed NC programs."
    },
    {
        "title": "Download & Update Manager",
        "filename": "download_manager.py",
        "description": "Download & Update Manager"
    },
    {
        "title": "Spindle Load Graphing",
        "filename": "load_meter_standalone_graph.py",
        "description": "Choose spindle load txt file to create SVG."
    },
    {
        "title": "Alarm History Review",
        "filename": "alarm_history.py",
        "description": "List recent machine alarms."
    },    
    {
        "title": "Job Runtime Estimator",
        "filename": "runtime_estimator.py",
        "description": "Estimate cycle timing from prior runs."
    }
]

class ScriptLauncher(object):
    def __init__(self, master):
        self.master = master
        self.master.title(WINDOW_TITLE)
        self.master.geometry(WINDOW_GEOMETRY)
        self.master.minsize(WINDOW_MIN_WIDTH, WINDOW_MIN_HEIGHT)
        self.master.configure(bg=COLOR_WINDOW_BG)
        if START_FULLSCREEN:
            self.master.attributes("-fullscreen", True)
        self.program_buttons = []
        self.program_enabled = []
        self.button_positions = []
        self.quit_button = None
        self.current_index = 0
        self._build_ui()
        self._bind_keys()
        self._focus_first_available()
    
    def _build_ui(self):
        outer = tk.Frame(self.master, bg=COLOR_WINDOW_BG)
        outer.pack(fill="both", expand=True, padx=OUTER_PAD_X, pady=OUTER_PAD_Y)
        if SHOW_HEADER:
            header_frame = tk.Frame(outer, bg=COLOR_HEADER_BG)
            header_frame.pack(fill="x", pady=(0, 8))
            header_label = tk.Label(
                header_frame,
                text=HEADER_TITLE,
                font=FONT_HEADER,
                bg=COLOR_HEADER_BG,
                fg=COLOR_TEXT_PRIMARY,
                anchor="w"
            )
            header_label.pack(fill="x")
            description_label = tk.Label(
                header_frame,
                text=HEADER_DESCRIPTION,
                font=FONT_SUBHEADER,
                bg=COLOR_HEADER_BG,
                fg=COLOR_TEXT_SECONDARY,
                justify="left",
                anchor="w",
                wraplength=900
            )
            description_label.pack(fill="x", pady=(4, 0))
        grid_frame = tk.Frame(outer, bg=COLOR_WINDOW_BG)
        grid_frame.pack(fill="both", expand=True)
        for col in range(GRID_COLUMNS):
            grid_frame.grid_columnconfigure(col, weight=1, uniform="program_column")
        for row in range(GRID_ROWS):
            grid_frame.grid_rowconfigure(row, weight=1, uniform="program_row")
        for index, program in enumerate(PROGRAMS[:PROGRAMS_PER_PAGE]):
            row = index % GRID_ROWS
            col = index // GRID_ROWS
            card = tk.Frame(
                grid_frame,
                bg=COLOR_CARD_BG,
                bd=1,
                relief="solid",
                highlightthickness=0,
                height=CARD_HEIGHT
            )
            card.grid(
                row=row,
                column=col,
                sticky="nsew",
                padx=COLUMN_PAD_X,
                pady=ROW_PAD_Y
            )
            card.pack_propagate(False)
            card.grid_propagate(False)
            title = program["title"]
            filename = program["filename"]
            description = program["description"]
            script_path = os.path.join(BASE_DIR, filename)
            is_installed = os.path.isfile(script_path)
            button_text = title
            if not is_installed:
                button_text += MISSING_SUFFIX
            btn = tk.Button(
                card,
                text=button_text,
                font=FONT_BUTTON,
                height=BUTTON_HEIGHT,
                wraplength=BUTTON_WRAP_LENGTH,
                relief="flat",
                bd=0,
                padx=6,
                pady=2,
                takefocus=1,
                command=lambda f=filename: self.run_script(f)
            )
            btn.pack(fill="x", padx=CARD_INNER_PAD_X, pady=(CARD_TOP_PAD_Y, CARD_MIDDLE_PAD_Y))
            desc_color = COLOR_TEXT_SECONDARY
            if is_installed:
                btn.config(
                    bg=COLOR_BUTTON_BG,
                    fg=COLOR_BUTTON_FG,
                    activebackground=COLOR_BUTTON_ACTIVE_BG,
                    activeforeground=COLOR_BUTTON_ACTIVE_FG,
                    state="normal",
                    cursor="hand2"
                )
            else:
                btn.config(
                    bg=COLOR_BUTTON_DISABLED_BG,
                    fg=COLOR_BUTTON_DISABLED_FG,
                    activebackground=COLOR_BUTTON_DISABLED_BG,
                    activeforeground=COLOR_BUTTON_DISABLED_FG,
                    state="disabled",
                    disabledforeground=COLOR_BUTTON_DISABLED_FG,
                    cursor="arrow"
                )
                desc_color = COLOR_DESCRIPTION_DISABLED
            desc = tk.Label(
                card,
                text=description,
                font=FONT_DESCRIPTION,
                bg=COLOR_CARD_BG,
                fg=desc_color,
                justify="left",
                anchor="nw",
                wraplength=DESCRIPTION_WRAP_LENGTH
            )
            desc.pack(fill="both", expand=True, padx=CARD_INNER_PAD_X, pady=(0, CARD_BOTTOM_PAD_Y))
            self.program_buttons.append(btn)
            self.program_enabled.append(is_installed)
            self.button_positions.append((row, col))
        bottom_frame = tk.Frame(outer, bg=COLOR_WINDOW_BG)
        bottom_frame.pack(fill="x", pady=(QUIT_PAD_TOP, 0))
        self.quit_button = tk.Button(
            bottom_frame,
            text=QUIT_BUTTON_TEXT,
            font=FONT_QUIT,
            height=1,
            relief="flat",
            bd=0,
            padx=6,
            pady=4,
            takefocus=1,
            bg=COLOR_QUIT_BG,
            fg=COLOR_QUIT_FG,
            activebackground=COLOR_QUIT_ACTIVE_BG,
            activeforeground=COLOR_QUIT_ACTIVE_FG,
            command=self.master.quit
        )
        self.quit_button.pack(fill="x")
    
    def _bind_keys(self):
        self.master.bind("<Up>", self.move_up)
        self.master.bind("<Down>", self.move_down)
        self.master.bind("<Left>", self.move_left)
        self.master.bind("<Right>", self.move_right)
        self.master.bind("<Return>", self.activate_focused)
        self.master.bind("<KP_Enter>", self.activate_focused)
        self.master.bind("<Escape>", self.exit_fullscreen)
    
    def exit_fullscreen(self, event=None):
        self.master.attributes("-fullscreen", False)
    
    def _focus_first_available(self):
        first_enabled = self._first_enabled_index()
        if first_enabled is not None:
            self.current_index = first_enabled
        else:
            self.current_index = len(self.program_buttons)
        self.update_button_styles()
        self._focus_current()
    
    def _first_enabled_index(self):
        for i, enabled in enumerate(self.program_enabled):
            if enabled:
                return i
        return None
    
    def _focus_current(self):
        if self.current_index < len(self.program_buttons):
            self.program_buttons[self.current_index].focus_set()
        else:
            self.quit_button.focus_set()
    
    def update_button_styles(self):
        for i, btn in enumerate(self.program_buttons):
            enabled = self.program_enabled[i]
            if i == self.current_index:
                if enabled:
                    btn.config(
                        bg=COLOR_FOCUS_BG,
                        fg=COLOR_BUTTON_FG,
                        activebackground=COLOR_FOCUS_BG,
                        activeforeground=COLOR_BUTTON_FG,
                        highlightthickness=0
                    )
                else:
                    btn.config(
                        bg=COLOR_BUTTON_DISABLED_BG,
                        fg=COLOR_BUTTON_DISABLED_FG,
                        activebackground=COLOR_BUTTON_DISABLED_BG,
                        activeforeground=COLOR_BUTTON_DISABLED_FG,
                        highlightthickness=0
                    )
            else:
                if enabled:
                    btn.config(
                        bg=COLOR_BUTTON_BG,
                        fg=COLOR_BUTTON_FG,
                        activebackground=COLOR_BUTTON_ACTIVE_BG,
                        activeforeground=COLOR_BUTTON_ACTIVE_FG,
                        highlightthickness=0
                    )
                else:
                    btn.config(
                        bg=COLOR_BUTTON_DISABLED_BG,
                        fg=COLOR_BUTTON_DISABLED_FG,
                        activebackground=COLOR_BUTTON_DISABLED_BG,
                        activeforeground=COLOR_BUTTON_DISABLED_FG,
                        highlightthickness=0
                    )
        if self.current_index == len(self.program_buttons):
            self.quit_button.config(
                bg=COLOR_FOCUS_BG,
                fg=COLOR_QUIT_FG,
                activebackground=COLOR_FOCUS_BG,
                activeforeground=COLOR_QUIT_FG,
                highlightthickness=0
            )
        else:
            self.quit_button.config(
                bg=COLOR_QUIT_BG,
                fg=COLOR_QUIT_FG,
                activebackground=COLOR_QUIT_ACTIVE_BG,
                activeforeground=COLOR_QUIT_ACTIVE_FG,
                highlightthickness=0
            )
    
    def _program_index_at_position(self, row, col):
        for i, position in enumerate(self.button_positions):
            if position == (row, col):
                return i
        return None
    
    def move_up(self, event=None):
        if self.current_index == len(self.program_buttons):
            return
        row, col = self.button_positions[self.current_index]
        new_row = (row - 1) % GRID_ROWS
        new_index = self._program_index_at_position(new_row, col)
        if new_index is not None:
            self.current_index = new_index
            self.update_button_styles()
            self._focus_current()
    
    def move_down(self, event=None):
        if self.current_index == len(self.program_buttons):
            return
        row, col = self.button_positions[self.current_index]
        new_row = (row + 1) % GRID_ROWS
        new_index = self._program_index_at_position(new_row, col)
        if new_index is not None:
            self.current_index = new_index
            self.update_button_styles()
            self._focus_current()
    
    def move_left(self, event=None):
        if self.current_index == len(self.program_buttons):
            first_enabled = self._first_enabled_index()
            if first_enabled is not None:
                self.current_index = first_enabled
                self.update_button_styles()
                self._focus_current()
            return
        row, col = self.button_positions[self.current_index]
        new_col = (col - 1) % GRID_COLUMNS
        new_index = self._program_index_at_position(row, new_col)
        if new_index is not None:
            self.current_index = new_index
            self.update_button_styles()
            self._focus_current()
    
    def move_right(self, event=None):
        if self.current_index == len(self.program_buttons):
            return
        row, col = self.button_positions[self.current_index]
        new_col = (col + 1) % GRID_COLUMNS
        new_index = self._program_index_at_position(row, new_col)
        if new_index is not None:
            self.current_index = new_index
            self.update_button_styles()
            self._focus_current()
    
    def activate_focused(self, event=None):
        if self.current_index < len(self.program_buttons):
            if self.program_enabled[self.current_index]:
                self.program_buttons[self.current_index].invoke()
        else:
            self.quit_button.invoke()
    
    def run_script(self, filename):
        script_path = os.path.join(BASE_DIR, filename)
        if not os.path.isfile(script_path):
            tkMessageBox.showerror(
                "File Not Found",
                "Script not found:\n%s" % script_path
            )
            return
        try:
            subprocess.Popen([sys.executable, script_path])
        except Exception as e:
            tkMessageBox.showerror(
                "Launch Error",
                "Could not run script:\n%s\n\n%s" % (script_path, str(e))
            )

if __name__ == "__main__":
    root = tk.Tk()
    app = ScriptLauncher(root)
    root.mainloop()
