epson_print_conf/ui.py

951 lines
34 KiB
Python
Raw Normal View History

2024-08-08 20:41:19 -04:00
import sys
2024-07-27 07:53:03 -04:00
import re
import threading
import ipaddress
2024-07-28 13:15:10 -04:00
import inspect
2024-07-27 07:53:03 -04:00
from datetime import datetime
import tkinter as tk
2024-07-25 16:08:05 -04:00
from tkinter import ttk, Menu
from tkinter.scrolledtext import ScrolledText
2024-07-25 16:08:05 -04:00
import tkinter.font as tkfont
2024-07-27 07:53:03 -04:00
from tkcalendar import DateEntry # Ensure you have: pip install tkcalendar
2024-07-27 10:36:43 -04:00
from tkinter import messagebox
2024-07-25 16:08:05 -04:00
2024-07-27 10:36:43 -04:00
import pyperclip
2024-07-27 07:53:03 -04:00
from epson_print_conf import EpsonPrinter
from find_printers import PrinterScanner
2024-07-28 13:15:10 -04:00
VERSION = "2.1"
NO_CONF_ERROR = (
"[ERROR] Please select a printer model and a valid IP address, or press 'Detect Printers'.\n"
)
def get_printer_models(input_string):
# Tokenize the string
tokens = re.split(" |/", input_string)
if not len(tokens):
return []
# Define the words to remove (uppercase, then case insensitive)
remove_tokens = {"EPSON", "SERIES"}
# Process tokens
processed_tokens = []
non_numeric_part = ""
pre_model = ""
for token in tokens:
upper_token = token.upper()
# Remove tokens that match remove_tokens
if any(word == upper_token for word in remove_tokens):
continue
if not any(char.isdigit() for char in token): # no alphanum inside
pre_model = pre_model + token + " "
continue
# Identify the non-numeric part of the first token
if not token.isnumeric() and not non_numeric_part:
non_numeric_part = "".join(c for c in token if not c.isdigit())
# if token is numeric, prepend the non-numeric part
if token.isnumeric():
processed_tokens.append(f"{pre_model}{non_numeric_part}{token}")
else:
processed_tokens.append(f"{pre_model}{token}")
if not processed_tokens and pre_model:
processed_tokens.append(pre_model.strip())
return processed_tokens
2024-07-27 10:36:43 -04:00
class ToolTip:
2024-07-28 13:15:10 -04:00
def __init__(self, widget, text="widget info", wrap_length=10):
2024-07-27 10:36:43 -04:00
self.widget = widget
self.text = text
self.wrap_length = wrap_length
self.tooltip_window = None
2024-07-28 13:15:10 -04:00
widget.bind("<Enter>", self.enter, "+") # Show the tooltip on hover
widget.bind("<Leave>", self.leave, "+") # Hide the tooltip on leave
widget.bind("<Button-1>", self.leave, "+") # Hide tooltip on mouse click
2024-07-27 10:36:43 -04:00
def enter(self, event=None):
if self.tooltip_window or not self.text:
return
x, y, width, height = self.widget.bbox("insert")
x += self.widget.winfo_rootx() + 20
y += self.widget.winfo_rooty() + 20
self.tooltip_window = tw = tk.Toplevel(self.widget)
tw.wm_overrideredirect(True)
2024-07-28 13:15:10 -04:00
2024-07-27 10:36:43 -04:00
# Calculate the position for the tooltip
screen_width = self.widget.winfo_screenwidth()
screen_height = self.widget.winfo_screenheight()
2024-07-28 13:15:10 -04:00
2024-07-27 10:36:43 -04:00
tw.geometry(f"+{x}+{y + height + 2}") # Default position below the widget
2024-07-28 13:15:10 -04:00
label = tk.Label(
tw,
text=self.wrap_text(self.text),
justify="left",
background="LightYellow",
relief="solid",
borderwidth=1,
)
2024-07-27 10:36:43 -04:00
label.pack(ipadx=1)
# Check if the tooltip goes off the screen
tw.update_idletasks() # Ensures the tooltip size is calculated
tw_width = tw.winfo_width()
tw_height = tw.winfo_height()
2024-07-28 13:15:10 -04:00
2024-07-27 10:36:43 -04:00
if x + tw_width > screen_width: # If tooltip goes beyond screen width
x = screen_width - tw_width - 5
2024-07-28 13:15:10 -04:00
if (y + height + tw_height > screen_height): # If tooltip goes below screen height
2024-07-27 10:36:43 -04:00
y = y - tw_height - height - 2 # Position above the widget
tw.geometry(f"+{x}+{y}")
def leave(self, event=None):
2024-07-28 13:15:10 -04:00
if self.tooltip_window:
self.tooltip_window.destroy()
self.tooltip_window = None
2024-07-27 10:36:43 -04:00
def wrap_text(self, text):
words = text.split()
lines = []
current_line = []
for word in words:
if len(current_line) + len(word.split()) <= self.wrap_length:
current_line.append(word)
else:
2024-07-28 13:15:10 -04:00
lines.append(" ".join(current_line))
2024-07-27 10:36:43 -04:00
current_line = [word]
if current_line:
2024-07-28 13:15:10 -04:00
lines.append(" ".join(current_line))
return "\n".join(lines)
2024-07-27 10:36:43 -04:00
2024-08-08 20:41:19 -04:00
class BugFixedDateEntry(DateEntry):
"""
Fixes a bug on the calendar that does not accept mouse selection with Linux
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def drop_down(self):
super().drop_down()
if self._top_cal is not None and not self._calendar.winfo_ismapped():
self._top_cal.lift()
class EpsonPrinterUI(tk.Tk):
2024-07-28 21:29:30 -04:00
def __init__(self, conf_dict={}, replace_conf=False):
super().__init__()
2024-07-27 07:53:03 -04:00
self.title("Epson Printer Configuration - v" + VERSION)
2024-08-07 20:32:03 -04:00
self.geometry("500x500")
self.minsize(500, 500)
2024-07-28 13:15:10 -04:00
self.printer_scanner = PrinterScanner()
2024-07-27 10:36:43 -04:00
self.ip_list = []
self.ip_list_cycle = None
2024-07-28 21:29:30 -04:00
self.conf_dict = conf_dict
self.replace_conf = replace_conf
2024-07-26 22:01:38 -04:00
# configure the main window to be resizable
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
2024-07-27 07:53:03 -04:00
FRAME_PAD = 10
2024-07-28 13:15:10 -04:00
PAD = (3, 0)
PADX = 4
2024-07-27 07:53:03 -04:00
PADY = 5
2024-07-28 13:15:10 -04:00
# main Frame
2024-07-27 07:53:03 -04:00
main_frame = ttk.Frame(self, padding=FRAME_PAD)
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
main_frame.columnconfigure(0, weight=1)
2024-07-27 10:36:43 -04:00
main_frame.rowconfigure(3, weight=1) # Number of rows
row_n = 0
# [row 0] Container frame for the two LabelFrames Power-off timer and TI Received Time
model_ip_frame = ttk.Frame(main_frame, padding=PAD)
model_ip_frame.grid(row=row_n, column=0, pady=PADY, sticky=(tk.W, tk.E))
model_ip_frame.columnconfigure(0, weight=1) # Allow column to expand
model_ip_frame.columnconfigure(1, weight=1) # Allow column to expand
# printer model selection
2024-07-28 13:15:10 -04:00
model_frame = ttk.LabelFrame(
model_ip_frame, text="Printer Model", padding=PAD
)
model_frame.grid(
row=0, column=0, pady=PADY, padx=(0, PADX), sticky=(tk.W, tk.E)
)
2024-07-27 10:36:43 -04:00
model_frame.columnconfigure(0, weight=0)
model_frame.columnconfigure(1, weight=1)
2024-07-28 13:15:10 -04:00
self.model_var = tk.StringVar()
2024-07-28 21:29:30 -04:00
if (
"internal_data" in conf_dict
and "default_model" in conf_dict["internal_data"]
):
self.model_var.set(conf_dict["internal_data"]["default_model"])
2024-07-28 13:15:10 -04:00
ttk.Label(model_frame, text="Model:").grid(
row=0, column=0, sticky=tk.W, padx=PADX
)
self.model_dropdown = ttk.Combobox(
model_frame, textvariable=self.model_var, state="readonly"
)
2024-07-28 21:29:30 -04:00
self.model_dropdown["values"] = sorted(EpsonPrinter(
conf_dict=self.conf_dict,
replace_conf=self.replace_conf
).valid_printers)
2024-07-28 13:15:10 -04:00
self.model_dropdown.grid(
row=0, column=1, pady=PADY, padx=PADX, sticky=(tk.W, tk.E)
)
ToolTip(
self.model_dropdown,
"Select the model of the printer, or press 'Detect Printers'.",
)
2024-07-27 10:36:43 -04:00
# IP address entry
2024-07-28 13:15:10 -04:00
ip_frame = ttk.LabelFrame(
model_ip_frame, text="Printer IP Address", padding=PAD
)
ip_frame.grid(
row=0, column=1, pady=PADY, padx=(PADX, 0), sticky=(tk.W, tk.E)
)
2024-07-27 10:36:43 -04:00
ip_frame.columnconfigure(0, weight=0)
ip_frame.columnconfigure(1, weight=1)
2024-07-28 13:15:10 -04:00
self.ip_var = tk.StringVar()
2024-07-28 21:29:30 -04:00
if (
"internal_data" in conf_dict
and "hostname" in conf_dict["internal_data"]
):
self.ip_var.set(conf_dict["internal_data"]["hostname"])
2024-07-28 13:15:10 -04:00
ttk.Label(ip_frame, text="IP Address:").grid(
row=0, column=0, sticky=tk.W, padx=PADX
)
self.ip_entry = ttk.Entry(ip_frame, textvariable=self.ip_var)
2024-07-28 13:15:10 -04:00
self.ip_entry.grid(
row=0, column=1, pady=PADY, padx=PADX, sticky=(tk.W, tk.E)
)
2024-07-27 10:36:43 -04:00
self.ip_entry.bind("<F2>", self.next_ip)
2024-07-28 13:15:10 -04:00
ToolTip(
self.ip_entry,
"Enter the IP address, or press 'Detect Printers' (you can also enter part of the IP address to speed up the detection), or press F2 more times to get the next local IP address, which can then be edited (by removing the last part before pressing 'Detect Printers').",
)
2024-07-27 07:53:03 -04:00
2024-07-27 10:36:43 -04:00
# [row 1] Container frame for the two LabelFrames Power-off timer and TI Received Time
row_n += 1
2024-07-27 07:53:03 -04:00
container_frame = ttk.Frame(main_frame, padding=PAD)
2024-07-28 13:15:10 -04:00
container_frame.grid(
row=row_n, column=0, pady=PADY, sticky=(tk.W, tk.E)
)
2024-07-27 07:53:03 -04:00
container_frame.columnconfigure(0, weight=1) # Allow column to expand
container_frame.columnconfigure(1, weight=1) # Allow column to expand
# Power-off timer
2024-07-28 13:15:10 -04:00
po_timer_frame = ttk.LabelFrame(
container_frame, text="Power-off timer (minutes)", padding=PAD
)
po_timer_frame.grid(
row=0, column=0, pady=PADY, padx=(0, PADX), sticky=(tk.W, tk.E)
)
2024-07-27 07:53:03 -04:00
po_timer_frame.columnconfigure(0, weight=0) # Button column on the left
po_timer_frame.columnconfigure(1, weight=1) # Entry column
po_timer_frame.columnconfigure(2, weight=0) # Button column on the right
2024-07-28 13:15:10 -04:00
2024-07-27 07:53:03 -04:00
# Configure validation command for numeric entry
validate_cmd = self.register(self.validate_number_input)
self.po_timer_var = tk.StringVar()
2024-07-28 13:15:10 -04:00
self.po_timer_entry = ttk.Entry(
po_timer_frame,
textvariable=self.po_timer_var,
validate="all",
validatecommand=(validate_cmd, "%P"),
width=6,
justify="center",
)
self.po_timer_entry.grid(
row=0, column=1, pady=PADY, padx=PADX, sticky=(tk.W, tk.E)
)
2024-07-27 10:36:43 -04:00
ToolTip(self.po_timer_entry, "Enter a number of minutes.")
2024-07-27 07:53:03 -04:00
2024-07-27 10:36:43 -04:00
button_width = 7
2024-07-28 13:15:10 -04:00
get_po_minutes = ttk.Button(
po_timer_frame,
text="Get",
width=button_width,
command=self.get_po_mins,
)
2024-07-27 07:53:03 -04:00
get_po_minutes.grid(row=0, column=0, padx=PADX, pady=PADY, sticky=tk.W)
2024-07-28 13:15:10 -04:00
set_po_minutes = ttk.Button(
po_timer_frame,
text="Set",
width=button_width,
command=self.set_po_mins,
)
2024-07-27 07:53:03 -04:00
set_po_minutes.grid(row=0, column=2, padx=PADX, pady=PADY, sticky=tk.E)
# TI Received Time
2024-07-28 13:15:10 -04:00
ti_received_frame = ttk.LabelFrame(
container_frame, text="TI Received Time (date)", padding=PAD
)
ti_received_frame.grid(
row=0, column=1, pady=PADY, padx=(PADX, 0), sticky=(tk.W, tk.E)
)
2024-07-27 07:53:03 -04:00
ti_received_frame.columnconfigure(0, weight=0) # Button column on the left
2024-07-27 10:36:43 -04:00
ti_received_frame.columnconfigure(1, weight=1) # Calendar column
2024-07-27 07:53:03 -04:00
ti_received_frame.columnconfigure(2, weight=0) # Button column on the right
# TI Received Time Calendar Widget
2024-08-08 20:41:19 -04:00
self.date_entry = BugFixedDateEntry(
2024-07-28 13:15:10 -04:00
ti_received_frame, date_pattern="yyyy-mm-dd"
)
self.date_entry.grid(
row=0, column=1, padx=PADX, pady=PADY, sticky=(tk.W, tk.E)
)
2024-08-08 20:41:19 -04:00
self.date_entry.delete(0, "end") # blank the field removing the current date
2024-07-28 13:15:10 -04:00
ToolTip(self.date_entry, "Enter a valid date with format YYYY-MM-DD.")
2024-07-27 07:53:03 -04:00
# TI Received Time Buttons
2024-07-28 13:15:10 -04:00
get_ti_received = ttk.Button(
ti_received_frame,
text="Get",
width=button_width,
command=self.get_ti_date,
)
2024-07-27 07:53:03 -04:00
get_ti_received.grid(row=0, column=0, padx=PADX, pady=PADY, sticky=tk.W)
2024-07-28 13:15:10 -04:00
set_ti_received = ttk.Button(
ti_received_frame,
text="Set",
width=button_width,
command=self.set_ti_date,
)
2024-07-27 07:53:03 -04:00
set_ti_received.grid(row=0, column=2, padx=PADX, pady=PADY, sticky=tk.E)
2024-07-27 10:36:43 -04:00
# [row 2] Buttons
row_n += 1
2024-07-27 07:53:03 -04:00
button_frame = ttk.Frame(main_frame, padding=PAD)
2024-07-27 10:36:43 -04:00
button_frame.grid(row=row_n, column=0, pady=PADY, sticky=(tk.W, tk.E))
button_frame.columnconfigure((0, 1, 2), weight=1)
2024-07-28 13:15:10 -04:00
self.detect_button = ttk.Button(
button_frame,
text="Detect Printers",
command=self.start_detect_printers,
)
self.detect_button.grid(
row=0, column=0, padx=PADX, pady=PADX, sticky=(tk.W, tk.E)
)
self.status_button = ttk.Button(
button_frame, text="Printer Status", command=self.printer_status
)
self.status_button.grid(
row=0, column=1, padx=PADX, pady=PADY, sticky=(tk.W, tk.E)
)
self.reset_button = ttk.Button(
button_frame,
text="Reset Waste Ink Levels",
command=self.reset_waste_ink,
)
self.reset_button.grid(
row=0, column=2, padx=PADX, pady=PADX, sticky=(tk.W, tk.E)
)
2024-07-27 07:53:03 -04:00
2024-07-27 10:36:43 -04:00
# [row 3] Status display
row_n += 1
2024-07-27 07:53:03 -04:00
status_frame = ttk.LabelFrame(main_frame, text="Status", padding=PAD)
2024-07-28 13:15:10 -04:00
status_frame.grid(
row=row_n, column=0, pady=PADY, sticky=(tk.W, tk.E, tk.N, tk.S)
)
status_frame.columnconfigure(0, weight=1)
status_frame.rowconfigure(0, weight=1)
2024-07-28 13:15:10 -04:00
2024-07-25 16:08:05 -04:00
# ScrolledText widget
2024-07-28 13:15:10 -04:00
self.status_text = ScrolledText(
status_frame, wrap=tk.WORD, font=("TkDefaultFont")
)
self.status_text.grid(
row=0,
column=0,
pady=PADY,
padx=PADY,
sticky=(tk.W, tk.E, tk.N, tk.S),
)
2024-07-27 07:53:03 -04:00
self.status_text.bind("<Key>", lambda e: "break") # disable editing text
2024-07-28 13:15:10 -04:00
self.status_text.bind(
"<Control-c>",
lambda event: self.copy_to_clipboard(self.status_text),
)
2024-07-27 07:53:03 -04:00
# self.status_text.bind("<Button-1>", lambda e: "break") # also disable the mouse
2024-07-25 16:08:05 -04:00
2024-07-27 07:53:03 -04:00
# Create a frame to contain the Treeview and its scrollbar
self.tree_frame = tk.Frame(status_frame)
self.tree_frame.grid(column=0, row=0, sticky=(tk.W, tk.E, tk.N, tk.S))
self.tree_frame.columnconfigure(0, weight=1)
self.tree_frame.rowconfigure(0, weight=1)
# Style configuration for the treeview
2024-07-25 16:08:05 -04:00
style = ttk.Style(self)
2024-07-27 07:53:03 -04:00
treeview_font = style.lookup("Treeview.Heading", "font")
2024-07-25 16:08:05 -04:00
2024-07-27 07:53:03 -04:00
# For the treeview, if the treeview_font is a tuple, split into components
if isinstance(treeview_font, tuple):
2024-07-28 13:15:10 -04:00
treeview_font_name, treeview_font_size = (
treeview_font[0],
treeview_font[1],
)
2024-07-25 16:08:05 -04:00
else:
# If font is not a tuple, it might be a font string or other format.
2024-07-28 13:15:10 -04:00
treeview_font_name, treeview_font_size = tkfont.Font().actual(
"family"
), tkfont.Font().actual("size")
style.configure(
"Treeview.Heading",
font=(treeview_font_name, treeview_font_size - 4, "bold"),
background="lightblue",
foreground="darkblue",
)
2024-07-25 16:08:05 -04:00
# Create and configure the Treeview widget
2024-07-27 07:53:03 -04:00
self.tree = ttk.Treeview(self.tree_frame, style="Treeview")
2024-07-28 13:15:10 -04:00
self.tree.heading("#0", text="Status Information", anchor="w")
2024-07-25 16:08:05 -04:00
self.tree.grid(column=0, row=0, sticky=(tk.W, tk.E, tk.N, tk.S))
# Create a vertical scrollbar for the Treeview
2024-07-28 13:15:10 -04:00
tree_scrollbar = ttk.Scrollbar(
self.tree_frame, orient="vertical", command=self.tree.yview
)
2024-07-25 16:08:05 -04:00
tree_scrollbar.grid(column=1, row=0, sticky=(tk.N, tk.S))
# Configure the Treeview to use the scrollbar
self.tree.configure(yscrollcommand=tree_scrollbar.set)
# Create a context menu
self.context_menu = Menu(self, tearoff=0)
2024-07-28 13:15:10 -04:00
self.context_menu.add_command(
label="Copy", command=self.copy_selected_item
)
2024-07-25 16:08:05 -04:00
# Bind the right-click event to the Treeview
self.tree.bind("<Button-3>", self.show_context_menu)
2024-07-27 07:53:03 -04:00
# Hide the Treeview initially
self.tree_frame.grid_remove()
2024-07-27 10:36:43 -04:00
def next_ip(self, event):
ip = self.ip_var.get()
if self.ip_list_cycle == None:
self.ip_list = self.printer_scanner.get_all_printers(local=True)
self.ip_list_cycle = 0
if not self.ip_list:
return
self.ip_var.set(self.ip_list[self.ip_list_cycle])
self.ip_list_cycle += 1
if self.ip_list_cycle >= len(self.ip_list):
self.ip_list_cycle = None
def copy_to_clipboard(self, text_widget):
try:
text = text_widget.selection_get()
pyperclip.copy(text)
except tk.TclError:
pass
return "break"
2024-07-28 13:15:10 -04:00
def get_po_mins(self, cursor=True):
if cursor:
self.config(cursor="watch")
self.update()
current_function_name = inspect.stack()[0][3]
method_to_call = getattr(self, current_function_name)
self.after(100, lambda: method_to_call(cursor=False))
return
2024-07-27 07:53:03 -04:00
self.show_status_text_view()
model = self.model_var.get()
ip_address = self.ip_var.get()
if not model or not self._is_valid_ip(ip_address):
2024-07-28 13:15:10 -04:00
self.status_text.insert(tk.END, NO_CONF_ERROR)
self.config(cursor="")
self.update()
2024-07-27 07:53:03 -04:00
return
2024-07-28 21:29:30 -04:00
printer = EpsonPrinter(
conf_dict=self.conf_dict,
replace_conf=self.replace_conf,
model=model,
hostname=ip_address
)
2024-08-08 20:41:19 -04:00
if not printer.parm.get("stats", {}).get("Power off timer"):
self.status_text.insert(
tk.END,
f"[ERROR]: Missing 'Power off timer' in configuration\n",
)
self.config(cursor="")
self.update_idletasks()
return
2024-07-27 07:53:03 -04:00
try:
2024-07-28 13:15:10 -04:00
po_timer = printer.stats()["stats"]["Power off timer"]
self.status_text.insert(
tk.END, f"[INFO] Power off timer: {po_timer} minutes.\n"
)
2024-07-27 07:53:03 -04:00
self.po_timer_var.set(po_timer)
except Exception as e:
2024-08-08 20:41:19 -04:00
self.status_text.insert(tk.END, f"[ERROR] {e}\n")
2024-07-28 13:15:10 -04:00
finally:
self.config(cursor="")
self.update_idletasks()
def set_po_mins(self, cursor=True):
if cursor:
self.config(cursor="watch")
self.update()
current_function_name = inspect.stack()[0][3]
method_to_call = getattr(self, current_function_name)
self.after(100, lambda: method_to_call(cursor=False))
return
2024-07-27 07:53:03 -04:00
self.show_status_text_view()
model = self.model_var.get()
ip_address = self.ip_var.get()
if not model or not self._is_valid_ip(ip_address):
2024-07-28 13:15:10 -04:00
self.status_text.insert(tk.END, NO_CONF_ERROR)
self.config(cursor="")
self.update_idletasks()
2024-07-27 07:53:03 -04:00
return
2024-07-28 21:29:30 -04:00
printer = EpsonPrinter(
conf_dict=self.conf_dict,
replace_conf=self.replace_conf,
model=model,
hostname=ip_address
)
2024-08-08 20:41:19 -04:00
if not printer.parm.get("stats", {}).get("Power off timer"):
self.status_text.insert(
tk.END,
f"[ERROR]: Missing 'Power off timer' in configuration\n",
)
2024-07-28 13:15:10 -04:00
self.config(cursor="")
self.update_idletasks()
2024-08-08 20:41:19 -04:00
return
po_timer = self.po_timer_var.get()
self.config(cursor="")
self.update_idletasks()
if not po_timer.isnumeric():
2024-07-28 13:15:10 -04:00
self.status_text.insert(
2024-08-08 20:41:19 -04:00
tk.END, "[ERROR] Please Use a valid value for minutes.\n"
2024-07-28 13:15:10 -04:00
)
2024-08-08 20:41:19 -04:00
return
self.status_text.insert(
tk.END, f"[INFO] Set Power off timer: {po_timer} minutes.\n"
)
response = messagebox.askyesno(
"Confirm Action", "Are you sure you want to proceed?"
)
if response:
try:
2024-07-27 10:36:43 -04:00
printer.write_poweroff_timer(int(po_timer))
2024-08-08 20:41:19 -04:00
except Exception as e:
self.status_text.insert(tk.END, f"[ERROR] {e}\n")
else:
2024-07-28 13:15:10 -04:00
self.status_text.insert(
2024-08-08 20:41:19 -04:00
tk.END, f"[WARNING] Set Power off timer aborted.\n"
2024-07-28 13:15:10 -04:00
)
2024-08-08 20:41:19 -04:00
self.config(cursor="")
self.update_idletasks()
2024-07-28 13:15:10 -04:00
def get_ti_date(self, cursor=True):
if cursor:
self.config(cursor="watch")
self.update()
current_function_name = inspect.stack()[0][3]
method_to_call = getattr(self, current_function_name)
self.after(100, lambda: method_to_call(cursor=False))
return
2024-07-27 07:53:03 -04:00
self.show_status_text_view()
model = self.model_var.get()
ip_address = self.ip_var.get()
if not model or not self._is_valid_ip(ip_address):
2024-07-28 13:15:10 -04:00
self.status_text.insert(tk.END, NO_CONF_ERROR)
self.config(cursor="")
self.update_idletasks()
2024-07-27 07:53:03 -04:00
return
2024-07-28 21:29:30 -04:00
printer = EpsonPrinter(
conf_dict=self.conf_dict,
replace_conf=self.replace_conf,
model=model,
hostname=ip_address
)
2024-08-08 20:41:19 -04:00
if not printer.parm.get("stats", {}).get("First TI received time"):
self.status_text.insert(
tk.END,
f"[ERROR]: Missing 'First TI received time' in configuration\n",
)
self.config(cursor="")
self.update_idletasks()
return
2024-07-27 07:53:03 -04:00
try:
2024-07-28 13:15:10 -04:00
date_string = datetime.strptime(
printer.stats()["stats"]["First TI received time"], "%d %b %Y"
).strftime("%Y-%m-%d")
self.status_text.insert(
tk.END,
f"[INFO] First TI received time (YYYY-MM-DD): {date_string}.\n",
)
2024-07-27 07:53:03 -04:00
self.date_entry.set_date(date_string)
except Exception as e:
2024-08-08 20:41:19 -04:00
self.status_text.insert(tk.END, f"[ERROR] {e}\n")
2024-07-28 13:15:10 -04:00
finally:
self.config(cursor="")
self.update_idletasks()
def set_ti_date(self, cursor=True):
if cursor:
self.config(cursor="watch")
self.update()
current_function_name = inspect.stack()[0][3]
method_to_call = getattr(self, current_function_name)
self.after(100, lambda: method_to_call(cursor=False))
return
2024-07-27 07:53:03 -04:00
self.show_status_text_view()
model = self.model_var.get()
ip_address = self.ip_var.get()
if not model or not self._is_valid_ip(ip_address):
2024-07-28 13:15:10 -04:00
self.status_text.insert(tk.END, NO_CONF_ERROR)
self.config(cursor="")
self.update_idletasks()
2024-07-27 07:53:03 -04:00
return
2024-07-28 21:29:30 -04:00
printer = EpsonPrinter(
conf_dict=self.conf_dict,
replace_conf=self.replace_conf,
model=model,
hostname=ip_address
)
2024-08-08 20:41:19 -04:00
if not printer.parm.get("stats", {}).get("First TI received time"):
2024-07-28 13:15:10 -04:00
self.status_text.insert(
tk.END,
2024-08-08 20:41:19 -04:00
f"[ERROR]: Missing 'First TI received time' in configuration\n",
2024-07-28 13:15:10 -04:00
)
2024-08-08 20:41:19 -04:00
self.config(cursor="")
self.update_idletasks()
return
date_string = self.date_entry.get_date()
self.status_text.insert(
tk.END,
f"[INFO] Set 'First TI received time' (YYYY-MM-DD) to: {date_string.strftime('%Y-%m-%d')}.\n",
)
response = messagebox.askyesno(
"Confirm Action", "Are you sure you want to proceed?"
)
if response:
try:
2024-07-28 13:15:10 -04:00
printer.write_first_ti_received_time(
date_string.year, date_string.month, date_string.day
)
2024-08-08 20:41:19 -04:00
except Exception as e:
self.status_text.insert(tk.END, f"[ERROR] {e}\n")
else:
2024-07-28 13:15:10 -04:00
self.status_text.insert(
tk.END,
2024-08-08 20:41:19 -04:00
f"[WARNING] Change of 'First TI received time' aborted.\n",
2024-07-28 13:15:10 -04:00
)
2024-08-08 20:41:19 -04:00
self.config(cursor="")
self.update_idletasks()
2024-07-27 07:53:03 -04:00
def validate_number_input(self, new_value):
# This function will be called with the new input value
if new_value == "" or new_value.isdigit():
return True
else:
return False
2024-07-25 16:08:05 -04:00
def show_status_text_view(self):
"""Show the status frame and hide the Treeview."""
2024-07-27 07:53:03 -04:00
self.tree_frame.grid_remove()
2024-07-25 16:08:05 -04:00
self.status_text.grid()
def show_treeview(self):
"""Show the Treeview and hide the status frame."""
self.status_text.grid_remove()
2024-07-27 07:53:03 -04:00
self.tree_frame.grid()
2024-07-25 16:08:05 -04:00
2024-07-28 13:15:10 -04:00
def printer_status(self, cursor=True):
if cursor:
self.config(cursor="watch")
self.update()
current_function_name = inspect.stack()[0][3]
method_to_call = getattr(self, current_function_name)
self.after(100, lambda: method_to_call(cursor=False))
return
2024-07-25 16:08:05 -04:00
self.show_status_text_view()
model = self.model_var.get()
ip_address = self.ip_var.get()
2024-08-07 20:32:03 -04:00
if not self._is_valid_ip(ip_address):
self.status_text.insert(
tk.END,
"[ERROR] Please enter a valid IP address, or press 'Detect Printers'.\n"
)
2024-07-28 13:15:10 -04:00
self.config(cursor="")
self.update_idletasks()
return
2024-07-28 21:29:30 -04:00
printer = EpsonPrinter(
conf_dict=self.conf_dict,
replace_conf=self.replace_conf,
model=model,
hostname=ip_address
)
try:
2024-07-25 16:08:05 -04:00
self.show_treeview()
2024-07-28 13:15:10 -04:00
# Configure tags
self.tree.tag_configure("key", foreground="black")
self.tree.tag_configure("key_value", foreground="dark blue")
self.tree.tag_configure("value", foreground="blue")
2024-07-25 16:08:05 -04:00
# Populate the Treeview
2024-07-28 13:15:10 -04:00
self.populate_treeview("", self.tree, printer.stats())
2024-07-25 16:08:05 -04:00
# Expand all nodes
self.expand_all(self.tree)
except Exception as e:
2024-07-25 16:08:05 -04:00
self.show_status_text_view()
self.status_text.insert(tk.END, f"[ERROR] {e}\n")
2024-07-28 13:15:10 -04:00
finally:
self.config(cursor="")
self.update_idletasks()
def reset_waste_ink(self, cursor=True):
if cursor:
self.config(cursor="watch")
self.update()
current_function_name = inspect.stack()[0][3]
method_to_call = getattr(self, current_function_name)
self.after(100, lambda: method_to_call(cursor=False))
return
2024-07-25 16:08:05 -04:00
self.show_status_text_view()
model = self.model_var.get()
ip_address = self.ip_var.get()
if not model or not self._is_valid_ip(ip_address):
2024-07-28 13:15:10 -04:00
self.status_text.insert(tk.END, NO_CONF_ERROR)
self.config(cursor="")
self.update_idletasks()
return
2024-07-28 21:29:30 -04:00
printer = EpsonPrinter(
conf_dict=self.conf_dict,
replace_conf=self.replace_conf,
model=model,
hostname=ip_address
)
2024-08-08 20:41:19 -04:00
response = messagebox.askyesno(
"Confirm Action", "Are you sure you want to proceed?"
)
if response:
try:
2024-07-27 10:36:43 -04:00
printer.reset_waste_ink_levels()
2024-07-28 13:15:10 -04:00
self.status_text.insert(
tk.END, "[INFO] Waste ink levels have been reset.\n"
)
2024-08-08 20:41:19 -04:00
except Exception as e:
self.status_text.insert(tk.END, f"[ERROR] {e}\n")
else:
self.status_text.insert(
tk.END, f"[WARNING] Waste ink levels reset aborted.\n"
)
self.config(cursor="")
self.update_idletasks()
2024-07-28 13:15:10 -04:00
def start_detect_printers(self):
2024-07-25 16:08:05 -04:00
self.show_status_text_view()
2024-07-28 13:15:10 -04:00
self.status_text.insert(
tk.END, "[INFO] Detecting printers... (this might take a while)\n"
)
# run printer detection in new thread, as it can take a while
2024-07-28 13:15:10 -04:00
threading.Thread(target=self.detect_printers_thread).start()
def detect_printers_thread(self, cursor=True):
if cursor:
self.config(cursor="watch")
self.update()
current_function_name = inspect.stack()[0][3]
method_to_call = getattr(self, current_function_name)
self.after(100, lambda: method_to_call(cursor=False))
return
self.detect_button.config(state=tk.DISABLED) # disable button while processing
2024-07-25 16:08:05 -04:00
self.show_status_text_view()
try:
2024-07-28 13:15:10 -04:00
printers = self.printer_scanner.get_all_printers(
self.ip_var.get().strip()
)
if len(printers) > 0:
2024-07-26 22:01:38 -04:00
if len(printers) == 1:
2024-07-28 13:15:10 -04:00
self.status_text.insert(
tk.END,
f"[INFO] Found printer '{printers[0]['name']}' at {printers[0]['ip']} (hostname: {printers[0]['hostname']})\n",
)
self.ip_var.set(printers[0]["ip"])
for model in get_printer_models(printers[0]["name"]):
2024-07-28 21:29:30 -04:00
if model in EpsonPrinter(
conf_dict=self.conf_dict,
replace_conf=self.replace_conf
).valid_printers:
2024-07-26 22:01:38 -04:00
self.model_var.set(model)
break
else:
2024-07-28 13:15:10 -04:00
self.status_text.insert(
tk.END, f"[INFO] Found {len(printers)} printers:\n"
)
2024-07-26 22:01:38 -04:00
for printer in printers:
2024-07-28 13:15:10 -04:00
self.status_text.insert(
tk.END,
f"[INFO] {printer['name']} found at {printer['ip']} (hostname: {printer['hostname']})\n",
)
else:
self.status_text.insert(tk.END, "[WARN] No printers found.\n")
except Exception as e:
self.status_text.insert(tk.END, f"[ERROR] {e}\n")
finally:
2024-07-28 13:15:10 -04:00
self.detect_button.config(state=tk.NORMAL) # enable button after processing
self.config(cursor="")
self.update_idletasks()
2024-07-28 13:15:10 -04:00
def _is_valid_ip(self, ip):
try:
ip = ipaddress.ip_address(ip)
return True
except ValueError:
return False
2024-07-25 16:08:05 -04:00
def is_simple_type(self, data):
return isinstance(data, (str, int, float, bool))
def contains_parentheses(self, data):
"""Check if a string representation contains parentheses."""
if isinstance(data, (list, tuple, set)):
for item in data:
if isinstance(item, (tuple, list, set)):
return True
2024-07-28 13:15:10 -04:00
if isinstance(item, str) and ("(" in item or ")" in item):
2024-07-25 16:08:05 -04:00
return True
return False
def populate_treeview(self, parent, treeview, data):
if isinstance(data, dict):
for key, value in data.items():
if isinstance(value, (dict, list, set, tuple)):
2024-07-28 13:15:10 -04:00
node = treeview.insert(
parent, "end", text=key, tags=("key",)
)
2024-07-25 16:08:05 -04:00
self.populate_treeview(node, treeview, value)
else:
2024-07-28 13:15:10 -04:00
treeview.insert(
parent,
"end",
text=f"{key}: {value}",
tags=("key_value"),
)
2024-07-25 16:08:05 -04:00
elif isinstance(data, list):
2024-07-28 13:15:10 -04:00
if all(
self.is_simple_type(item) for item in data
) and not self.contains_parentheses(data):
treeview.insert(
parent,
"end",
text=", ".join(map(str, data)),
tags=("value",),
)
2024-07-25 16:08:05 -04:00
else:
for item in data:
if isinstance(item, (dict, list, set, tuple)):
self.populate_treeview(parent, treeview, item)
else:
2024-07-28 13:15:10 -04:00
treeview.insert(
parent, "end", text=str(item), tags=("value",)
)
2024-07-25 16:08:05 -04:00
elif isinstance(data, set):
if not self.contains_parentheses(data):
2024-07-28 13:15:10 -04:00
treeview.insert(
parent,
"end",
text=", ".join(map(str, data)),
tags=("value",),
)
2024-07-25 16:08:05 -04:00
else:
for item in data:
2024-07-28 13:15:10 -04:00
treeview.insert(
parent, "end", text=str(item), tags=("value",)
)
2024-07-25 16:08:05 -04:00
elif isinstance(data, tuple):
2024-07-28 13:15:10 -04:00
treeview.insert(parent, "end", text=str(data), tags=("value",))
2024-07-25 16:08:05 -04:00
else:
2024-07-28 13:15:10 -04:00
treeview.insert(parent, "end", text=str(data), tags=("value",))
2024-07-25 16:08:05 -04:00
def expand_all(self, treeview):
def recursive_expand(item):
treeview.item(item, open=True)
children = treeview.get_children(item)
for child in children:
recursive_expand(child)
root_children = treeview.get_children()
for child in root_children:
recursive_expand(child)
def show_context_menu(self, event):
"""Show the context menu."""
# Select the item under the cursor
item = self.tree.identify_row(event.y)
if item:
self.tree.selection_set(item)
self.context_menu.post(event.x_root, event.y_root)
def copy_selected_item(self):
"""Copy the selected Treeview item text to the clipboard."""
selected_item = self.tree.selection()
if selected_item:
item_text = self.tree.item(selected_item[0], "text")
self.clipboard_clear()
self.clipboard_append(item_text)
2024-07-26 22:01:38 -04:00
if __name__ == "__main__":
2024-07-28 21:29:30 -04:00
import argparse
import pickle
parser = argparse.ArgumentParser(
epilog='epson_print_conf GUI'
)
parser.add_argument(
'-P',
"--pickle",
dest='pickle',
type=argparse.FileType('rb'),
2024-07-29 19:18:02 -04:00
help="Load a pickle configuration archive saved by parse_devices.py",
2024-07-28 21:29:30 -04:00
default=None,
nargs=1,
metavar='PICKLE_FILE'
)
parser.add_argument(
'-O',
"--override",
dest='override',
action='store_true',
2024-07-29 19:18:02 -04:00
help="Replace the default configuration with the one in the pickle "
"file instead of merging (default is to merge)",
2024-07-28 21:29:30 -04:00
)
args = parser.parse_args()
conf_dict = {}
if args.pickle:
conf_dict = pickle.load(args.pickle[0])
app = EpsonPrinterUI(conf_dict=conf_dict, replace_conf=args.override)
2024-08-08 20:41:19 -04:00
try:
app.mainloop()
except:
print("\nInterrupted.")
sys.exit(0)