UI refinements

This commit is contained in:
Ircama 2024-07-27 16:36:43 +02:00
parent eac2b41375
commit 6c5a631d97
4 changed files with 151 additions and 26 deletions

View file

@ -36,9 +36,14 @@ git clone https://github.com/Ircama/epson_print_conf
pip3 install pyyaml pip3 install pyyaml
pip3 install pyasn1==0.4.8 pip3 install pyasn1==0.4.8
pip3 install git+https://github.com/etingof/pysnmp.git pip3 install git+https://github.com/etingof/pysnmp.git
pip3 install tkcalendar
pip3 install pyperclip
cd epson_print_conf cd epson_print_conf
``` ```
With Python 12, also: `pip3 install pyasyncore`.
Notes (at the time of writing): Notes (at the time of writing):
- [before pysnmp, install pyasn1 with version 0.4.8 and not 0.5](https://github.com/etingof/pysnmp/issues/440#issuecomment-1544341598) - [before pysnmp, install pyasn1 with version 0.4.8 and not 0.5](https://github.com/etingof/pysnmp/issues/440#issuecomment-1544341598)

View file

@ -53,12 +53,14 @@ class PrinterScanner:
return {"ip": ip, "hostname": hostname, "name": "Unknown"} return {"ip": ip, "hostname": hostname, "name": "Unknown"}
return None return None
def get_all_printers(self, ip_addr=""): def get_all_printers(self, ip_addr="", local=False):
if ip_addr: if ip_addr:
result = self.scan_ip(ip_addr) result = self.scan_ip(ip_addr)
if result: if result:
return [result] return [result]
local_device_ip_list = socket.gethostbyname_ex(socket.gethostname())[2] local_device_ip_list = socket.gethostbyname_ex(socket.gethostname())[2]
if local:
return local_device_ip_list
printers = [] printers = []
for local_device_ip in local_device_ip_list: for local_device_ip in local_device_ip_list:
if ip_addr and not local_device_ip.startswith(ip_addr): if ip_addr and not local_device_ip.startswith(ip_addr):

View file

@ -2,4 +2,6 @@ PyYAML
git+https://github.com/etingof/pysnmp.git@master#egg=pysnmp git+https://github.com/etingof/pysnmp.git@master#egg=pysnmp
pyasyncore;python_version>="3.12" pyasyncore;python_version>="3.12"
tkcalendar tkcalendar
pyperclip

160
ui.py
View file

@ -8,13 +8,76 @@ from tkinter import ttk, Menu
from tkinter.scrolledtext import ScrolledText from tkinter.scrolledtext import ScrolledText
import tkinter.font as tkfont import tkinter.font as tkfont
from tkcalendar import DateEntry # Ensure you have: pip install tkcalendar from tkcalendar import DateEntry # Ensure you have: pip install tkcalendar
from tkinter import messagebox
import pyperclip
from epson_print_conf import EpsonPrinter from epson_print_conf import EpsonPrinter
from find_printers import PrinterScanner from find_printers import PrinterScanner
VERSION = "2.0" VERSION = "2.0"
class ToolTip:
def __init__(self, widget, text='widget info', wrap_length=10):
self.widget = widget
self.text = text
self.wrap_length = wrap_length
self.tooltip_window = None
widget.bind("<Enter>", self.enter)
widget.bind("<Leave>", self.leave)
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)
# Calculate the position for the tooltip
screen_width = self.widget.winfo_screenwidth()
screen_height = self.widget.winfo_screenheight()
tw.geometry(f"+{x}+{y + height + 2}") # Default position below the widget
label = tk.Label(tw, text=self.wrap_text(self.text), justify='left',
background='yellow', relief='solid', borderwidth=1)
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()
if x + tw_width > screen_width: # If tooltip goes beyond screen width
x = screen_width - tw_width - 5
if y + height + tw_height > screen_height: # If tooltip goes below screen height
y = y - tw_height - height - 2 # Position above the widget
tw.geometry(f"+{x}+{y}")
def leave(self, event=None):
tw = self.tooltip_window
self.tooltip_window = None
if tw:
tw.destroy()
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:
lines.append(' '.join(current_line))
current_line = [word]
if current_line:
lines.append(' '.join(current_line))
return '\n'.join(lines)
class EpsonPrinterUI(tk.Tk): class EpsonPrinterUI(tk.Tk):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@ -22,6 +85,8 @@ class EpsonPrinterUI(tk.Tk):
self.geometry("450x500") self.geometry("450x500")
self.minsize(450, 500) self.minsize(450, 500)
self.printer_scanner=PrinterScanner() self.printer_scanner=PrinterScanner()
self.ip_list = []
self.ip_list_cycle = None
# configure the main window to be resizable # configure the main window to be resizable
self.columnconfigure(0, weight=1) self.columnconfigure(0, weight=1)
@ -36,32 +101,45 @@ class EpsonPrinterUI(tk.Tk):
main_frame = ttk.Frame(self, padding=FRAME_PAD) 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.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
main_frame.columnconfigure(0, weight=1) main_frame.columnconfigure(0, weight=1)
main_frame.rowconfigure(4, weight=1) # Number of elements main_frame.rowconfigure(3, weight=1) # Number of rows
row_n = 0
# [0] printer model selection # [row 0] Container frame for the two LabelFrames Power-off timer and TI Received Time
model_frame = ttk.LabelFrame(main_frame, text="Printer Model", padding=PAD) model_ip_frame = ttk.Frame(main_frame, padding=PAD)
model_frame.grid(row=0, column=0, pady=PADY, sticky=(tk.W, tk.E)) 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
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))
model_frame.columnconfigure(0, weight=0)
model_frame.columnconfigure(1, weight=1) model_frame.columnconfigure(1, weight=1)
self.model_var = tk.StringVar() self.model_var = tk.StringVar()
ttk.Label(model_frame, text="Select Printer Model:").grid(row=0, column=0, sticky=tk.W, padx=PADX) 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) self.model_dropdown = ttk.Combobox(model_frame, textvariable=self.model_var)
self.model_dropdown['values'] = sorted(EpsonPrinter().valid_printers) self.model_dropdown['values'] = sorted(EpsonPrinter().valid_printers)
self.model_dropdown.grid(row=0, column=1, pady=PADY, padx=PADX, sticky=(tk.W, tk.E)) 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'.")
# [1] IP address entry # IP address entry
ip_frame = ttk.LabelFrame(main_frame, text="Printer IP Address", padding=PAD) ip_frame = ttk.LabelFrame(model_ip_frame, text="Printer IP Address", padding=PAD)
ip_frame.grid(row=1, column=0, pady=PADY, sticky=(tk.W, tk.E)) ip_frame.grid(row=0, column=1, pady=PADY, padx=(PADX, 0), sticky=(tk.W, tk.E))
ip_frame.columnconfigure(0, weight=0)
ip_frame.columnconfigure(1, weight=1) ip_frame.columnconfigure(1, weight=1)
self.ip_var = tk.StringVar() self.ip_var = tk.StringVar()
ttk.Label(ip_frame, text="Enter Printer IP Address:").grid(row=0, column=0, sticky=tk.W, padx=PADX) 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) self.ip_entry = ttk.Entry(ip_frame, textvariable=self.ip_var)
self.ip_entry.grid(row=0, column=1, pady=PADY, padx=PADX, sticky=(tk.W, tk.E)) self.ip_entry.grid(row=0, column=1, pady=PADY, padx=PADX, sticky=(tk.W, tk.E))
self.ip_entry.bind("<F2>", self.next_ip)
ToolTip(self.ip_entry, "Enter the IP address, or press 'Detect printers' (enter part of it to speed up the detection), or press F2 to get the next local IP address, which can then be edited.")
# [2] Container frame for the two LabelFrames Power-off timer and TI Received Time # [row 1] Container frame for the two LabelFrames Power-off timer and TI Received Time
row_n += 1
container_frame = ttk.Frame(main_frame, padding=PAD) container_frame = ttk.Frame(main_frame, padding=PAD)
container_frame.grid(row=2, column=0, pady=PADY, sticky=(tk.W, tk.E)) container_frame.grid(row=row_n, column=0, pady=PADY, sticky=(tk.W, tk.E))
container_frame.columnconfigure(0, weight=1) # Allow column to expand container_frame.columnconfigure(0, weight=1) # Allow column to expand
container_frame.columnconfigure(1, weight=1) # Allow column to expand container_frame.columnconfigure(1, weight=1) # Allow column to expand
@ -76,37 +154,41 @@ class EpsonPrinterUI(tk.Tk):
validate_cmd = self.register(self.validate_number_input) validate_cmd = self.register(self.validate_number_input)
self.po_timer_var = tk.StringVar() self.po_timer_var = tk.StringVar()
self.po_timer_entry = ttk.Entry(po_timer_frame, textvariable=self.po_timer_var, validate='all', validatecommand=(validate_cmd, "%P"), width=6) 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)) self.po_timer_entry.grid(row=0, column=1, pady=PADY, padx=PADX, sticky=(tk.W, tk.E))
ToolTip(self.po_timer_entry, "Enter a number of minutes.")
get_po_minutes = ttk.Button(po_timer_frame, text="Get", width=6, command=self.get_po_mins) button_width = 7
get_po_minutes = ttk.Button(po_timer_frame, text="Get", width=button_width, command=self.get_po_mins)
get_po_minutes.grid(row=0, column=0, padx=PADX, pady=PADY, sticky=tk.W) get_po_minutes.grid(row=0, column=0, padx=PADX, pady=PADY, sticky=tk.W)
set_po_minutes = ttk.Button(po_timer_frame, text="Set", width=6, command=self.set_po_mins) set_po_minutes = ttk.Button(po_timer_frame, text="Set", width=button_width, command=self.set_po_mins)
set_po_minutes.grid(row=0, column=2, padx=PADX, pady=PADY, sticky=tk.E) set_po_minutes.grid(row=0, column=2, padx=PADX, pady=PADY, sticky=tk.E)
# TI Received Time # TI Received Time
ti_received_frame = ttk.LabelFrame(container_frame, text="TI Received Time (date)", padding=PAD) 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)) ti_received_frame.grid(row=0, column=1, pady=PADY, padx=(PADX, 0), sticky=(tk.W, tk.E))
ti_received_frame.columnconfigure(0, weight=0) # Button column on the left ti_received_frame.columnconfigure(0, weight=0) # Button column on the left
ti_received_frame.columnconfigure(1, weight=0) # Calendar column ti_received_frame.columnconfigure(1, weight=1) # Calendar column
ti_received_frame.columnconfigure(2, weight=0) # Button column on the right ti_received_frame.columnconfigure(2, weight=0) # Button column on the right
# TI Received Time Calendar Widget # TI Received Time Calendar Widget
self.date_entry = DateEntry(ti_received_frame, date_pattern="yy-mm-dd", width=10, borderwidth=2) self.date_entry = DateEntry(ti_received_frame, date_pattern="yy-mm-dd", width=10, borderwidth=2)
self.date_entry.grid(row=0, column=1, padx=PADX, pady=PADY, sticky=(tk.W, tk.E)) self.date_entry.grid(row=0, column=1, padx=PADX, pady=PADY, sticky=(tk.W, tk.E))
self.date_entry.delete(0,"end") self.date_entry.delete(0,"end")
ToolTip(self.date_entry, "Enter a valid date with format YY-MM-DD.")
# TI Received Time Buttons # TI Received Time Buttons
get_ti_received = ttk.Button(ti_received_frame, text="Get", width=6, command=self.get_ti_date) get_ti_received = ttk.Button(ti_received_frame, text="Get", width=button_width, command=self.get_ti_date)
get_ti_received.grid(row=0, column=0, padx=PADX, pady=PADY, sticky=tk.W) get_ti_received.grid(row=0, column=0, padx=PADX, pady=PADY, sticky=tk.W)
set_ti_received = ttk.Button(ti_received_frame, text="Set", width=6, command=self.set_ti_date) set_ti_received = ttk.Button(ti_received_frame, text="Set", width=button_width, command=self.set_ti_date)
set_ti_received.grid(row=0, column=2, padx=PADX, pady=PADY, sticky=tk.E) set_ti_received.grid(row=0, column=2, padx=PADX, pady=PADY, sticky=tk.E)
# [3] Buttons # [row 2] Buttons
row_n += 1
button_frame = ttk.Frame(main_frame, padding=PAD) button_frame = ttk.Frame(main_frame, padding=PAD)
button_frame.grid(row=3, column=0, pady=PADY, sticky=(tk.W, tk.E)) button_frame.grid(row=row_n, column=0, pady=PADY, sticky=(tk.W, tk.E))
button_frame.columnconfigure((0, 1, 2), weight=1) button_frame.columnconfigure((0, 1, 2), weight=1)
self.detect_button = ttk.Button(button_frame, text="Detect Printers", command=self.start_detect_printers) self.detect_button = ttk.Button(button_frame, text="Detect Printers", command=self.start_detect_printers)
@ -118,9 +200,10 @@ class EpsonPrinterUI(tk.Tk):
self.reset_button = ttk.Button(button_frame, text="Reset Waste Ink Levels", command=self.reset_waste_ink) 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)) self.reset_button.grid(row=0, column=2, padx=PADX, pady=PADX, sticky=(tk.W, tk.E))
# [4] Status display # [row 3] Status display
row_n += 1
status_frame = ttk.LabelFrame(main_frame, text="Status", padding=PAD) status_frame = ttk.LabelFrame(main_frame, text="Status", padding=PAD)
status_frame.grid(row=4, column=0, pady=PADY, sticky=(tk.W, tk.E, tk.N, tk.S)) 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.columnconfigure(0, weight=1)
status_frame.rowconfigure(0, weight=1) status_frame.rowconfigure(0, weight=1)
@ -128,6 +211,7 @@ class EpsonPrinterUI(tk.Tk):
self.status_text = ScrolledText(status_frame, wrap=tk.WORD, font=("TkDefaultFont")) 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)) self.status_text.grid(row=0, column=0, pady=PADY, padx=PADY, sticky=(tk.W, tk.E, tk.N, tk.S))
self.status_text.bind("<Key>", lambda e: "break") # disable editing text self.status_text.bind("<Key>", lambda e: "break") # disable editing text
self.status_text.bind("<Control-c>", lambda event: self.copy_to_clipboard(self.status_text))
# self.status_text.bind("<Button-1>", lambda e: "break") # also disable the mouse # self.status_text.bind("<Button-1>", lambda e: "break") # also disable the mouse
# Create a frame to contain the Treeview and its scrollbar # Create a frame to contain the Treeview and its scrollbar
@ -174,6 +258,26 @@ class EpsonPrinterUI(tk.Tk):
# Hide the Treeview initially # Hide the Treeview initially
self.tree_frame.grid_remove() self.tree_frame.grid_remove()
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"
def get_po_mins(self): def get_po_mins(self):
self.show_status_text_view() self.show_status_text_view()
model = self.model_var.get() model = self.model_var.get()
@ -204,7 +308,11 @@ class EpsonPrinterUI(tk.Tk):
self.status_text.insert(tk.END, "[ERROR] Please Use a valid value for minutes.\n") self.status_text.insert(tk.END, "[ERROR] Please Use a valid value for minutes.\n")
return return
self.status_text.insert(tk.END, f"[INFO] Set Power off timer: {po_timer} minutes.\n") 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:
printer.write_poweroff_timer(int(po_timer)) printer.write_poweroff_timer(int(po_timer))
else:
self.status_text.insert(tk.END, f"[WARNING] Set Power off timer aborted.\n")
except Exception as e: except Exception as e:
self.status_text.insert(tk.END, f"[ERROR] {e}: Cannot set 'Power off timer'; missing configuration\n") self.status_text.insert(tk.END, f"[ERROR] {e}: Cannot set 'Power off timer'; missing configuration\n")
@ -235,7 +343,11 @@ class EpsonPrinterUI(tk.Tk):
date_string = datetime.strptime(printer.stats()['stats']['First TI received time'], '%d %b %Y').strftime('%y-%m-%d') date_string = datetime.strptime(printer.stats()['stats']['First TI received time'], '%d %b %Y').strftime('%y-%m-%d')
date_string = self.date_entry.get_date() date_string = self.date_entry.get_date()
self.status_text.insert(tk.END, f"[INFO] Set 'First TI received time' (YY-MM-DD) to: {date_string.strftime('%Y-%m-%d')}.\n") self.status_text.insert(tk.END, f"[INFO] Set 'First TI received time' (YY-MM-DD) to: {date_string.strftime('%Y-%m-%d')}.\n")
#printer.write_first_ti_received_time(date_string.year, date_string.month, date_string.day) response = messagebox.askyesno("Confirm Action", "Are you sure you want to proceed?")
if response:
printer.write_first_ti_received_time(date_string.year, date_string.month, date_string.day)
else:
self.status_text.insert(tk.END, f"[WARNING] Change of 'First TI received time' aborted.\n")
except Exception as e: except Exception as e:
self.status_text.insert(tk.END, f"[ERROR] {e}: Cannot set 'First TI received time'; missing configuration\n") self.status_text.insert(tk.END, f"[ERROR] {e}: Cannot set 'First TI received time'; missing configuration\n")
@ -284,8 +396,12 @@ class EpsonPrinterUI(tk.Tk):
return return
printer = EpsonPrinter(model=model, hostname=ip_address) printer = EpsonPrinter(model=model, hostname=ip_address)
try: try:
response = messagebox.askyesno("Confirm Action", "Are you sure you want to proceed?")
if response:
printer.reset_waste_ink_levels() printer.reset_waste_ink_levels()
self.status_text.insert(tk.END, "[INFO] Waste ink levels have been reset.\n") self.status_text.insert(tk.END, "[INFO] Waste ink levels have been reset.\n")
else:
self.status_text.insert(tk.END, f"[WARNING] Waste ink levels reset aborted.\n")
except Exception as e: except Exception as e:
self.status_text.insert(tk.END, f"[ERROR] {e}\n") self.status_text.insert(tk.END, f"[ERROR] {e}\n")