diff --git a/gestione_pickinglist.py b/gestione_pickinglist.py index 7a99142..cfe6963 100644 --- a/gestione_pickinglist.py +++ b/gestione_pickinglist.py @@ -70,6 +70,14 @@ from busy_overlay import InlineBusyOverlay from gestione_aree import AsyncRunner from locale_text import load_locale_catalog, text as loc_text from ui_theme import theme_color, theme_font, theme_section, theme_value +from ui_tables import ( + TABLE_HEADER_BG, + TABLE_HEADER_FG, + TABLE_ROW_EVEN, + TABLE_ROW_ODD, + apply_tksheet_visual_style, + apply_tksheet_zebra, +) from user_session import UserSession from version_info import module_version, versioned_title from window_placement import place_window_fullsize_below_parent_later @@ -391,13 +399,14 @@ class ScrollTable(ctk.CTkFrame): self._sort_key: Optional[str] = None self._sort_reverse = False self.total_w = sum(c.width for c in self.columns) + self._vbar_visible = False self.grid_rowconfigure(1, weight=1) self.grid_columnconfigure(0, weight=1) # header - self.h_canvas = tk.Canvas(self, height=ROW_H, highlightthickness=0, bd=0) - self.h_inner = ctk.CTkFrame(self.h_canvas, fg_color="#f3f3f3", + self.h_canvas = tk.Canvas(self, height=ROW_H, highlightthickness=0, bd=0, bg=TABLE_HEADER_BG) + self.h_inner = ctk.CTkFrame(self.h_canvas, fg_color=TABLE_HEADER_BG, height=ROW_H, width=self.total_w) self.h_canvas.create_window((0,0), window=self.h_inner, anchor="nw", width=self.total_w, height=ROW_H) @@ -414,7 +423,6 @@ class ScrollTable(ctk.CTkFrame): # scrollbars self.vbar = tk.Scrollbar(self, orient="vertical", command=self.b_canvas.yview) self.xbar = tk.Scrollbar(self, orient="horizontal", command=self._xscroll_both) - self.vbar.grid(row=1, column=1, sticky="ns") self.xbar.grid(row=2, column=0, sticky="ew") # link scroll @@ -436,14 +444,14 @@ class ScrollTable(ctk.CTkFrame): for w in self.h_inner.winfo_children(): w.destroy() - row = ctk.CTkFrame(self.h_inner, fg_color="#f3f3f3", + row = ctk.CTkFrame(self.h_inner, fg_color=TABLE_HEADER_BG, height=ROW_H, width=self.total_w) row.pack(fill="x", expand=False) row.pack_propagate(False) for col in self.columns: holder = ctk.CTkFrame( - row, fg_color="#f3f3f3", + row, fg_color=TABLE_HEADER_BG, width=col.width, height=ROW_H, border_width=1, border_color=self.GRID_COLOR ) @@ -454,7 +462,7 @@ class ScrollTable(ctk.CTkFrame): if col.key == self._sort_key: header_text = f"{col.title} {'↓' if self._sort_reverse else '↑'}" - lbl = ctk.CTkLabel(holder, text=header_text, anchor="w") + lbl = ctk.CTkLabel(holder, text=header_text, anchor="w", text_color=TABLE_HEADER_FG) lbl.pack(fill="both", padx=(self.PADX_L, self.PADX_R), pady=self.PADY) if self.on_header_click and col.key != "__check__": @@ -475,10 +483,27 @@ class ScrollTable(ctk.CTkFrame): """Keep the scroll region aligned with the current body content width.""" self.b_canvas.itemconfigure(self.body_window, width=self.total_w) sr = self.b_canvas.bbox("all") + content_height = int(sr[3]) if sr else 0 if sr: self.b_canvas.configure(scrollregion=(0,0,max(self.total_w, sr[2]), sr[3])) else: self.b_canvas.configure(scrollregion=(0,0,self.total_w,0)) + self._update_vertical_scrollbar(content_height) + + def _update_vertical_scrollbar(self, content_height: int): + """Show the vertical scrollbar only when body rows exceed the visible area.""" + try: + visible_height = max(1, int(self.b_canvas.winfo_height())) + except Exception: + visible_height = 1 + needs_scroll = content_height > visible_height + 2 + if needs_scroll and not self._vbar_visible: + self.vbar.grid(row=1, column=1, sticky="ns") + self._vbar_visible = True + elif not needs_scroll and self._vbar_visible: + self.vbar.grid_remove() + self._vbar_visible = False + self.b_canvas.yview_moveto(0) def _on_body_configure(self): """React to body resize events by syncing dimensions and header scroll.""" @@ -513,8 +538,11 @@ class ScrollTable(ctk.CTkFrame): delta = getattr(event, "delta", 0) if delta == 0: return + if not self._vbar_visible: + return "break" step = -1 if delta > 0 else 1 self.b_canvas.yview_scroll(step, "units") + return "break" def clear_rows(self): """Remove all rendered body rows.""" @@ -530,14 +558,15 @@ class ScrollTable(ctk.CTkFrame): checkbox_builder: Optional[Callable[[tk.Widget], ctk.CTkCheckBox]] = None, ): """Append one row to the table body.""" - row = ctk.CTkFrame(self.b_inner, fg_color="transparent", + row_bg = TABLE_ROW_EVEN if row_index % 2 == 0 else TABLE_ROW_ODD + row = ctk.CTkFrame(self.b_inner, fg_color=row_bg, height=ROW_H, width=self.total_w) row.pack(fill="x", expand=False) row.pack_propagate(False) for i, col in enumerate(self.columns): holder = ctk.CTkFrame( - row, fg_color="transparent", + row, fg_color=row_bg, width=col.width, height=ROW_H, border_width=1, border_color=self.GRID_COLOR ) @@ -549,10 +578,10 @@ class ScrollTable(ctk.CTkFrame): cb = checkbox_builder(holder) cb.pack(padx=(self.PADX_L, self.PADX_R), pady=self.PADY, anchor="w") else: - ctk.CTkLabel(holder, text="").pack(fill="both") + ctk.CTkLabel(holder, text="", fg_color=row_bg).pack(fill="both") else: anchor = (anchors[i] if anchors else col.anchor) - ctk.CTkLabel(holder, text=values[i], anchor=anchor).pack( + ctk.CTkLabel(holder, text=values[i], anchor=anchor, fg_color=row_bg, text_color="#111827").pack( fill="both", padx=(self.PADX_L, self.PADX_R), pady=self.PADY ) @@ -696,6 +725,7 @@ class GestionePickingListFrame(ctk.CTkFrame): self.detail_sheet.change_theme("light green") self.detail_sheet.enable_bindings("all") self.detail_sheet.headers(self._detail_headers(), redraw=False) + apply_tksheet_visual_style(self.detail_sheet) self.detail_sheet.bind("", self._on_detail_sheet_left_click, add="+") self.detail_sheet.grid(row=0, column=0, sticky="nsew") @@ -736,8 +766,10 @@ class GestionePickingListFrame(ctk.CTkFrame): data, reset_col_positions=True, reset_row_positions=True, - redraw=True, + redraw=False, ) + apply_tksheet_visual_style(self.detail_sheet) + apply_tksheet_zebra(self.detail_sheet, len(data)) self.detail_sheet.set_all_column_widths() def _detail_sort_value(self, row: Dict[str, Any], key: str): diff --git a/gestione_scarico.py b/gestione_scarico.py index 1d3371d..e1e723e 100644 --- a/gestione_scarico.py +++ b/gestione_scarico.py @@ -20,6 +20,7 @@ from audit_log import log_user_action from busy_overlay import InlineBusyOverlay from locale_text import load_locale_catalog, text as loc_text from ui_theme import theme_color, theme_font, theme_section, theme_value +from ui_tables import style_treeview, zebra_tag from user_session import UserSession from version_info import module_version, versioned_title @@ -511,10 +512,6 @@ class ScaricoDialog(ctk.CTkToplevel): tree_host.grid_rowconfigure(0, weight=1) tree_host.grid_columnconfigure(0, weight=1) - style = ttk.Style(self) - style.configure("Scarico.Treeview", rowheight=28, font=theme_font(self._theme, "tree_body_font", ("Segoe UI", 10))) - style.configure("Scarico.Treeview.Heading", font=theme_font(self._theme, "tree_heading_font", ("Segoe UI", 10, "bold"))) - self.rows_tree = ttk.Treeview( tree_host, columns=("sel", "udc", "last", "diag"), @@ -522,6 +519,13 @@ class ScaricoDialog(ctk.CTkToplevel): style="Scarico.Treeview", selectmode="none", ) + style_treeview( + self.rows_tree, + style_name="Scarico.Treeview", + rowheight=28, + font=theme_font(self._theme, "tree_body_font", ("Segoe UI", 10)), + heading_font=theme_font(self._theme, "tree_heading_font", ("Segoe UI", 10, "bold")), + ) self.rows_tree.heading("sel", text="Sel") self.rows_tree.heading("udc", text=loc_text("scarico.col.udc", catalog=self._locale_catalog, default="UDC")) self.rows_tree.heading("last", text=loc_text("scarico.col.last_insert", catalog=self._locale_catalog, default="Ultimo inserimento")) @@ -575,6 +579,7 @@ class ScaricoDialog(ctk.CTkToplevel): row.last_event_at, row.diagnostic_note or "", ), + tags=(zebra_tag(idx),), ) self.update_idletasks() diff --git a/login_window.py b/login_window.py index cad9045..56d84e3 100644 --- a/login_window.py +++ b/login_window.py @@ -68,7 +68,7 @@ class LoginWindow(tk.Toplevel): self._clear_topmost_after_id: str | None = None self.title(versioned_title(loc_text("login.msg.title", catalog=self._locale_catalog, default="Login"), __name__)) - self.geometry("170x145+0+0" if self.compact else str(theme_value(self._theme, "window_geometry", "165x155+0+0"))) + self.geometry("170x148+0+0" if self.compact else str(theme_value(self._theme, "window_geometry", "165x170+0+0"))) self.resizable(False, False) try: if parent is not None and parent.winfo_viewable(): @@ -91,53 +91,59 @@ class LoginWindow(tk.Toplevel): def _build_ui(self) -> None: """Build the compact operator login form.""" - body = ttk.Frame(self, padding=8 if self.compact else 8) + body = ttk.Frame(self, padding=6 if self.compact else 8) body.pack(fill="both", expand=True) body.columnconfigure(1, weight=0) row_offset = 0 - ttk.Label(body, text="User:").grid(row=row_offset, column=0, sticky="w", padx=(0, 4), pady=4) + ttk.Label(body, text="User:").grid(row=row_offset, column=0, sticky="w", padx=(0, 4), pady=(2, 3)) self.login_entry = ttk.Entry(body, textvariable=self.login_var, width=10, font=("Segoe UI", 10 if self.compact else 9)) - self.login_entry.grid(row=row_offset, column=1, sticky="w", pady=4) + self.login_entry.grid(row=row_offset, column=1, sticky="w", pady=(2, 3)) - ttk.Label(body, text="Pwd:").grid(row=row_offset + 1, column=0, sticky="w", padx=(0, 4), pady=4) + ttk.Label(body, text="Pwd:").grid(row=row_offset + 1, column=0, sticky="w", padx=(0, 4), pady=(2, 2)) self.password_entry = ttk.Entry(body, textvariable=self.password_var, width=10, show="*", font=("Segoe UI", 10 if self.compact else 9)) - self.password_entry.grid(row=row_offset + 1, column=1, sticky="w", pady=4) + self.password_entry.grid(row=row_offset + 1, column=1, sticky="w", pady=(2, 2)) if self.compact: actions = ttk.Frame(body) - actions.grid(row=row_offset + 2, column=0, columnspan=2, sticky="ew", pady=(6, 0)) - self._cancel_button = ttk.Button( - actions, - text="Annulla", - command=self._on_cancel, - ) - self._cancel_button.grid(row=1, column=0, sticky="ew", pady=(4, 0)) + actions.grid(row=row_offset + 2, column=0, columnspan=2, sticky="ew", pady=(3, 0)) self._login_button = ttk.Button( actions, text="OK", command=self._on_login, ) self._login_button.grid(row=0, column=0, sticky="ew") - else: - self.status_label = ttk.Label(body, textvariable=self._status_var, foreground="#555555") - self.status_label.grid(row=2, column=0, columnspan=2, sticky="w", pady=(2, 2)) - - actions = ttk.Frame(body) - actions.grid(row=3, column=0, columnspan=2, sticky="w", pady=(6, 0)) self._cancel_button = ttk.Button( actions, - text=loc_text("login.button.cancel", catalog=self._locale_catalog, default="Annulla"), + text="Annulla", command=self._on_cancel, ) - self._cancel_button.grid(row=1, column=0, sticky="ew", pady=(4, 0)) + self._cancel_button.grid(row=1, column=0, sticky="ew", pady=(3, 0)) + else: + self.status_label = ttk.Label(body, textvariable=self._status_var, foreground="#555555") + self.status_label.grid(row=2, column=0, columnspan=2, sticky="w", pady=(1, 1)) + + actions = ttk.Frame(body) + actions.grid(row=3, column=0, columnspan=2, sticky="w", pady=(3, 0)) self._login_button = ttk.Button( actions, text=loc_text("login.button.submit", catalog=self._locale_catalog, default="OK"), command=self._on_login, ) self._login_button.grid(row=0, column=0, sticky="ew") + self._cancel_button = ttk.Button( + actions, + text=loc_text("login.button.cancel", catalog=self._locale_catalog, default="Annulla"), + command=self._on_cancel, + ) + self._cancel_button.grid(row=1, column=0, sticky="ew", pady=(3, 0)) + + for widget in (self.login_entry, self.password_entry, self._login_button, self._cancel_button): + try: + widget.configure(takefocus=True) + except Exception: + pass self.bind("", lambda _e: self._on_login()) self.bind("", lambda _e: self._on_cancel()) diff --git a/reset_corsie.py b/reset_corsie.py index c442d9c..ed1ea89 100644 --- a/reset_corsie.py +++ b/reset_corsie.py @@ -23,7 +23,8 @@ from gestione_aree import AsyncRunner from gestione_scarico import move_pallet_async from locale_text import load_locale_catalog, text as loc_text from tooltips import WidgetToolTip, load_tooltip_catalog, tooltip_text -from ui_theme import theme_color, theme_font, theme_padding, theme_section, theme_value +from ui_theme import theme_color, theme_font, theme_section, theme_value +from ui_tables import style_treeview, zebra_tag from version_info import module_version, versioned_title from window_placement import place_window_fullsize_below_parent_later @@ -301,35 +302,6 @@ class ResetCorsieWindow(ctk.CTkToplevel): def _setup_tree_style(self): """Apply a denser, spreadsheet-like style to the main result grid.""" - style = ttk.Style(self) - style.configure( - "ResetCorsie.Treeview.Heading", - font=theme_font(self._theme, "tree_heading_font", ("Segoe UI", 10, "bold")), - background=theme_value(self._theme, "tree_heading_bg", "#b8c7db"), - foreground=theme_value(self._theme, "tree_heading_fg", "#10243e"), - relief="flat", - padding=theme_padding(self._theme, "tree_heading_padding", (8, 6)), - ) - style.map( - "ResetCorsie.Treeview.Heading", - background=[("active", theme_value(self._theme, "tree_heading_bg_active", "#aebfd6"))], - relief=[("pressed", "groove"), ("!pressed", "flat")], - ) - style.configure( - "ResetCorsie.Treeview", - font=theme_font(self._theme, "tree_body_font", ("Segoe UI", 10)), - rowheight=int(theme_value(self._theme, "tree_row_height", 30)), - background=theme_value(self._theme, "tree_body_bg", "#ffffff"), - fieldbackground=theme_value(self._theme, "tree_body_bg", "#ffffff"), - foreground=theme_value(self._theme, "tree_body_fg", "#1f1f1f"), - borderwidth=0, - ) - style.map( - "ResetCorsie.Treeview", - background=[("selected", theme_value(self._theme, "tree_selected_bg", "#cfe4ff"))], - foreground=[("selected", theme_value(self._theme, "tree_selected_fg", "#10243e"))], - ) - @_log_call() def _build_ui(self): """Create selectors, summary widgets and the occupied-cell grid.""" @@ -414,8 +386,13 @@ class ResetCorsieWindow(ctk.CTkToplevel): width=int(theme_value(self._theme, "tree_col_num_udc_width", 160)), anchor=str(theme_value(self._theme, "tree_col_num_udc_anchor", "center")), ) - self.tree.tag_configure("odd", background=theme_value(self._theme, "tree_row_odd_bg", "#ffffff")) - self.tree.tag_configure("even", background=theme_value(self._theme, "tree_row_even_bg", "#f3f6fb")) + style_treeview( + self.tree, + style_name="ResetCorsie.Treeview", + rowheight=int(theme_value(self._theme, "tree_row_height", 30)), + font=theme_font(self._theme, "tree_body_font", ("Segoe UI", 10)), + heading_font=theme_font(self._theme, "tree_heading_font", ("Segoe UI", 10, "bold")), + ) sy = ttk.Scrollbar(mid, orient="vertical", command=self.tree.yview) sx = ttk.Scrollbar(mid, orient="horizontal", command=self.tree.xview) @@ -532,7 +509,7 @@ class ResetCorsieWindow(ctk.CTkToplevel): for item in self.tree.get_children(): self.tree.delete(item) for idx, (_idc, ubi, n) in enumerate(det_rows): - tag = "even" if idx % 2 else "odd" + tag = zebra_tag(idx) self.tree.insert("", "end", values=(ubi, n), tags=(tag,)) except Exception as ex: _MODULE_LOGGER.exception(f"Errore UI refresh reset corsie corsia={corsia}: {ex}") diff --git a/search_pallets.py b/search_pallets.py index 493790a..d1217dd 100644 --- a/search_pallets.py +++ b/search_pallets.py @@ -11,6 +11,13 @@ from busy_overlay import InlineBusyOverlay from gestione_aree import AsyncRunner from locale_text import load_locale_catalog, text as loc_text from ui_theme import theme_color, theme_font, theme_section, theme_value +from ui_tables import ( + apply_tksheet_visual_style, + apply_tksheet_zebra, + merge_tags, + style_treeview, + zebra_tag, +) from version_info import module_version, versioned_title from window_placement import place_window_fullsize_below_parent_later from tooltips import WidgetToolTip, load_tooltip_catalog, tooltip_text @@ -177,17 +184,25 @@ class SearchWindow(ctk.CTkToplevel): self.use_sheet = False cols = ("IDCella", "Ubicazione", "UDC", "Lotto", "Codice", "Descrizione") self.tree = ttk.Treeview(wrap, columns=cols, show="headings") - self._style = ttk.Style(self) - try: - self._style.theme_use(self._style.theme_use()) - except Exception: - pass - self._style.configure("Search.Treeview", rowheight=22, font=("", 9)) - self._style.configure("Search.Treeview.Heading", font=("", 9, "bold"), background="#F3F4F6") - self._style.map("Search.Treeview", background=[("selected", "#DCEBFF")]) - self.tree.configure(style="Search.Treeview") - self.tree.tag_configure("even", background="#FFFFFF") - self.tree.tag_configure("odd", background="#F7F9FC") + headings = { + "IDCella": ("IDCella", 90, "e"), + "Ubicazione": ("Ubicazione", 150, "w"), + "UDC": ("UDC / Barcode", 130, "w"), + "Lotto": ("Lotto", 130, "w"), + "Codice": ("Codice prodotto", 150, "w"), + "Descrizione": ("Descrizione prodotto", 340, "w"), + } + for col in cols: + text, width, anchor = headings[col] + self.tree.heading(col, text=text) + self.tree.column(col, width=width, anchor=anchor, stretch=True) + self._style = style_treeview( + self.tree, + style_name="Search.Treeview", + rowheight=22, + font=("", 9), + heading_font=("", 9, "bold"), + ) self.tree.tag_configure("id9999", background="#FFECEC", foreground="#B00020") sy = ttk.Scrollbar(wrap, orient="vertical", command=self.tree.yview) @@ -212,7 +227,7 @@ class SearchWindow(ctk.CTkToplevel): is9999 = int(vals[0]) == 9999 except Exception: is9999 = False - tags = ("id9999", zebra) if is9999 else (zebra,) + tags = merge_tags(zebra, "id9999" if is9999 else "") self.tree.item(iid, tags=tags) def _export_xlsx(self): @@ -417,6 +432,8 @@ class SearchWindow(ctk.CTkToplevel): hdrs = list(headers) hdrs[c] = hdrs[c] + arrow self.sheet.headers(hdrs) + apply_tksheet_visual_style(self.sheet) + apply_tksheet_zebra(self.sheet, len(data)) except Exception: pass @@ -452,7 +469,10 @@ class SearchWindow(ctk.CTkToplevel): for row in rows: idc, ubi, udc_v, lot_v, cod_v, desc_v = row data.append([idc, ubi, udc_v, lot_v, cod_v, desc_v]) + self.sheet.headers(["IDCella", "Ubicazione", "UDC", "Lotto", "Codice", "Descrizione"]) + apply_tksheet_visual_style(self.sheet) self.sheet.set_sheet_data(data) + apply_tksheet_zebra(self.sheet, len(data)) self.sheet.set_all_cell_sizes_to_text() except Exception: self.use_sheet = False @@ -461,12 +481,11 @@ class SearchWindow(ctk.CTkToplevel): self.tree.delete(iid) for idx, row in enumerate(rows): idc, ubi, udc_v, lot_v, cod_v, desc_v = row - zebra = "even" if idx % 2 == 0 else "odd" try: is9999 = int(idc) == 9999 except Exception: is9999 = False - tags = ("id9999", zebra) if is9999 else (zebra,) + tags = merge_tags(zebra_tag(idx), "id9999" if is9999 else "") self.tree.insert("", "end", values=(idc, ubi, udc_v, lot_v, cod_v, desc_v), tags=tags) if not rows: diff --git a/storico_pickinglist.py b/storico_pickinglist.py index e70d9a6..aa46185 100644 --- a/storico_pickinglist.py +++ b/storico_pickinglist.py @@ -13,6 +13,7 @@ from busy_overlay import InlineBusyOverlay from gestione_aree import AsyncRunner from locale_text import load_locale_catalog, text as loc_text from ui_theme import theme_color, theme_font, theme_section, theme_value +from ui_tables import merge_tags, style_treeview, zebra_tag from version_info import module_version, versioned_title from window_placement import place_window_fullsize_below_parent_later @@ -251,8 +252,10 @@ class StoricoPickingListWindow(ctk.CTkToplevel): for col in columns: tree.heading(col, text=col) tree.column(col, width=widths.get(col, 100), anchor="w") + style_treeview(tree, style_name="HistoryPicking.Treeview", rowheight=24) tree.tag_configure("done", background="#ECECEC") tree.tag_configure("active", background="#EAF7EA") + tree.tag_configure("warning", background="#FFE3B3") sy = ttk.Scrollbar(wrap, orient="vertical", command=tree.yview) sx = ttk.Scrollbar(wrap, orient="horizontal", command=tree.xview) tree.configure(yscrollcommand=sy.set, xscrollcommand=sx.set) @@ -298,7 +301,19 @@ class StoricoPickingListWindow(ctk.CTkToplevel): self.detail_tree.delete(*self.detail_tree.get_children("")) for index, row in enumerate(rows): stato = str(row.get("StatoOperativo") or "") - tag = "done" if stato in {"Chiusa", "Esaurita"} else "active" if int(row.get("IDStato") or 0) == 1 else "" + is_open_with_shipped = ( + str(row.get("StatoDocumento") or "") == "P" + and int(row.get("RigheSpedite") or 0) > 0 + ) + tag = ( + "warning" + if is_open_with_shipped + else "done" + if stato in {"Chiusa", "Esaurita"} + else "active" + if int(row.get("IDStato") or 0) == 1 + else "" + ) self.master_tree.insert( "", "end", @@ -314,7 +329,7 @@ class StoricoPickingListWindow(ctk.CTkToplevel): stato, row.get("IDStato", ""), ), - tags=(tag,) if tag else (), + tags=merge_tags(zebra_tag(index), tag), ) def _on_master_select(self, _event=None) -> None: @@ -357,8 +372,10 @@ class StoricoPickingListWindow(ctk.CTkToplevel): def _fill_detail(self, rows: list[dict[str, Any]]) -> None: self.detail_tree.delete(*self.detail_tree.get_children("")) - for row in rows: + for index, row in enumerate(rows): + is_open_shipped = str(row.get("StatoDocumento") or "") == "P" and int(row.get("Cella") or 0) == 9999 done = str(row.get("StatoDocumento") or "") == "D" or int(row.get("Cella") or 0) == 9999 + tag = "warning" if is_open_shipped else "done" if done else "" self.detail_tree.insert( "", "end", @@ -374,7 +391,7 @@ class StoricoPickingListWindow(ctk.CTkToplevel): row.get("Ubicazione", ""), row.get("Ordinamento", ""), ), - tags=("done",) if done else (), + tags=merge_tags(zebra_tag(index), tag), ) diff --git a/storico_udc.py b/storico_udc.py index 7eb877e..53dd1fa 100644 --- a/storico_udc.py +++ b/storico_udc.py @@ -12,6 +12,7 @@ from busy_overlay import InlineBusyOverlay from gestione_aree import AsyncRunner from locale_text import load_locale_catalog, text as loc_text from ui_theme import theme_color, theme_font, theme_section, theme_value +from ui_tables import merge_tags, style_treeview, zebra_tag from version_info import module_version, versioned_title from window_placement import place_window_fullsize_below_parent_later @@ -221,6 +222,7 @@ class StoricoUDCWindow(ctk.CTkToplevel): for col in cols: self.tree.heading(col, text=col) self.tree.column(col, width=90, anchor="w") + style_treeview(self.tree, style_name="HistoryUDC.Treeview", rowheight=24) self.tree.column("ID", width=70, anchor="e") self.tree.column("Tipo", width=55, anchor="center") self.tree.column("Rif", width=70, anchor="e") @@ -283,7 +285,7 @@ class StoricoUDCWindow(ctk.CTkToplevel): value = row.get(name, "") return "" if value is None else value - for row in rows: + for index, row in enumerate(rows): tipo = str(row.get("Tipo") or "") tag = "versamento" if tipo == "V" else "prelievo" if tipo == "P" else "spedita" if tipo == "SPED" else "" self.tree.insert( @@ -302,7 +304,7 @@ class StoricoUDCWindow(ctk.CTkToplevel): _value(row, "ModUtente"), _value(row, "ModDataOra"), ), - tags=(tag,) if tag else (), + tags=merge_tags(zebra_tag(index), tag), ) diff --git a/ui_tables.py b/ui_tables.py new file mode 100644 index 0000000..fe25388 --- /dev/null +++ b/ui_tables.py @@ -0,0 +1,108 @@ +"""Shared visual helpers for data grids.""" + +from __future__ import annotations + +from tkinter import ttk +from typing import Any + +TABLE_ROW_EVEN = "#FFFFFF" +TABLE_ROW_ODD = "#F4F6F8" +TABLE_HEADER_BG = "#D1D5DB" +TABLE_HEADER_FG = "#111827" +TABLE_SELECTED_BG = "#DCEBFF" +TABLE_SELECTED_FG = "#111827" + + +def style_treeview( + tree: ttk.Treeview, + *, + style_name: str, + rowheight: int = 24, + font: Any = ("Segoe UI", 9), + heading_font: Any = ("Segoe UI", 9, "bold"), +) -> ttk.Style: + """Apply a consistent high-contrast header and zebra-ready style.""" + + style = ttk.Style(tree) + style.configure(style_name, rowheight=rowheight, font=font) + style.configure( + f"{style_name}.Heading", + font=heading_font, + background=TABLE_HEADER_BG, + foreground=TABLE_HEADER_FG, + relief="flat", + ) + style.map( + f"{style_name}.Heading", + background=[("active", TABLE_HEADER_BG), ("pressed", TABLE_HEADER_BG)], + foreground=[("active", TABLE_HEADER_FG), ("pressed", TABLE_HEADER_FG)], + ) + style.map( + style_name, + background=[("selected", TABLE_SELECTED_BG)], + foreground=[("selected", TABLE_SELECTED_FG)], + ) + tree.configure(style=style_name) + configure_treeview_zebra_tags(tree) + return style + + +def configure_treeview_zebra_tags(tree: ttk.Treeview) -> None: + """Register alternating row color tags on a Treeview.""" + + tree.tag_configure("even", background=TABLE_ROW_EVEN) + tree.tag_configure("odd", background=TABLE_ROW_ODD) + + +def zebra_tag(index: int) -> str: + """Return the alternating row tag for the given zero-based index.""" + + return "even" if index % 2 == 0 else "odd" + + +def merge_tags(*tags: str) -> tuple[str, ...]: + """Return non-empty tags preserving order.""" + + return tuple(tag for tag in tags if tag) + + +def apply_tksheet_visual_style(sheet: Any) -> None: + """Apply best-effort header contrast and zebra rows to a tksheet widget.""" + + try: + sheet.set_options( + header_bg=TABLE_HEADER_BG, + header_fg=TABLE_HEADER_FG, + header_selected_cells_bg=TABLE_HEADER_BG, + header_selected_cells_fg=TABLE_HEADER_FG, + table_bg=TABLE_ROW_EVEN, + table_fg="#111827", + selected_rows_bg=TABLE_SELECTED_BG, + selected_rows_fg=TABLE_SELECTED_FG, + selected_cells_bg=TABLE_SELECTED_BG, + selected_cells_fg=TABLE_SELECTED_FG, + ) + except Exception: + pass + + +def apply_tksheet_zebra(sheet: Any, row_count: int) -> None: + """Apply alternating row colors to a tksheet widget when supported.""" + + try: + sheet.dehighlight_rows(redraw=False) + except Exception: + pass + for row_index in range(row_count): + try: + sheet.highlight_rows( + rows=[row_index], + bg=TABLE_ROW_EVEN if row_index % 2 == 0 else TABLE_ROW_ODD, + redraw=False, + ) + except Exception: + break + try: + sheet.redraw() + except Exception: + pass diff --git a/ui_theme.json b/ui_theme.json index 781f510..5af92fa 100644 --- a/ui_theme.json +++ b/ui_theme.json @@ -164,7 +164,7 @@ } }, "login_window": { - "window_geometry": "165x155+0+0", + "window_geometry": "165x170+0+0", "overlay_cover_fg_color": ["#d9d9d9", "#4a4a4a"], "overlay_panel_fg_color": ["#f2f2f2", "#353535"], "overlay_panel_corner_radius": 10, diff --git a/view_celle_multi_udc.py b/view_celle_multi_udc.py index 84d642e..9228b97 100644 --- a/view_celle_multi_udc.py +++ b/view_celle_multi_udc.py @@ -22,6 +22,7 @@ from gestione_scarico import move_pallet_async from locale_text import load_locale_catalog, text as loc_text from tooltips import WidgetToolTip, load_tooltip_catalog, tooltip_text from ui_theme import theme_color, theme_font, theme_section, theme_value +from ui_tables import merge_tags, style_treeview, zebra_tag from version_info import module_version, versioned_title from window_placement import place_window_fullsize_below_parent_later @@ -510,6 +511,7 @@ class CelleMultipleWindow(ctk.CTkToplevel): self.tree.column("col2", width=250, anchor="w") self.tree.column("col3", width=120, anchor="w") self.tree.column("col4", width=260, anchor="w") + style_treeview(self.tree, style_name="MultiUDC.Treeview", rowheight=24) y = ttk.Scrollbar(frame, orient="vertical", command=self.tree.yview) x = ttk.Scrollbar(frame, orient="horizontal", command=self.tree.xview) self.tree.configure(yscrollcommand=y.set, xscrollcommand=x.set) @@ -545,6 +547,7 @@ class CelleMultipleWindow(ctk.CTkToplevel): ): self.sum_tbl.heading(key, text=title) self.sum_tbl.column(key, width=width, anchor=anchor) + style_treeview(self.sum_tbl, style_name="MultiUDCSummary.Treeview", rowheight=24) y2 = ttk.Scrollbar(inner, orient="vertical", command=self.sum_tbl.yview) x2 = ttk.Scrollbar(inner, orient="horizontal", command=self.sum_tbl.xview) self.sum_tbl.configure(yscrollcommand=y2.set, xscrollcommand=x2.set) @@ -663,7 +666,7 @@ class CelleMultipleWindow(ctk.CTkToplevel): """Populate root tree nodes after the aisle query completes.""" rows = _json_obj(res).get("rows", []) _log_dataset("multi_udc_corsie", rows) - for row in rows: + for index, row in enumerate(rows): corsia = row.get("Corsia") if not corsia: continue @@ -675,7 +678,7 @@ class CelleMultipleWindow(ctk.CTkToplevel): text=self._format_corsia_text(corsia), values=("", "", ""), open=False, - tags=("corsia",), + tags=merge_tags(zebra_tag(index), "corsia"), ) self.tree.insert(node_id, "end", iid=f"{node_id}::lazy", text="...", values=("", "", "")) @@ -727,7 +730,7 @@ class CelleMultipleWindow(ctk.CTkToplevel): if not rows: self.tree.insert(parent_iid, "end", text="(nessuna cella con >1 UDC)", values=("", "", "")) return - for row in rows: + for index, row in enumerate(rows): idc = row["IDCella"] ubi = row["Ubicazione"] corsia = row.get("Corsia") @@ -744,7 +747,7 @@ class CelleMultipleWindow(ctk.CTkToplevel): text=label, values=(f"IDCella {idc}", "", ""), open=False, - tags=("cella", f"corsia:{corsia}"), + tags=merge_tags(zebra_tag(index), "cella", f"corsia:{corsia}"), ) if not any(child.endswith("::lazy") for child in self.tree.get_children(node_id)): self.tree.insert(node_id, "end", iid=f"{node_id}::lazy", text="...", values=("", "", "")) @@ -784,7 +787,7 @@ class CelleMultipleWindow(ctk.CTkToplevel): idcella_txt = self.tree.item(parent_iid, "values")[0] idcella_num = int(idcella_txt.split()[-1]) if idcella_txt else None - for row in rows: + for index, row in enumerate(rows): pallet = row.get("Pallet", "") desc = row.get("Descrizione", "") lotto = row.get("Lotto", "") @@ -816,7 +819,13 @@ class CelleMultipleWindow(ctk.CTkToplevel): iid=leaf_id, text=self._format_pallet_text(str(pallet), leaf_id in self.selected_udc_keys), values=(desc, lotto, causale), - tags=("pallet", f"corsia:{corsia_val}", f"ubicazione:{cella_ubi}", f"idcella:{idcella_num}"), + tags=merge_tags( + zebra_tag(index), + "pallet", + f"corsia:{corsia_val}", + f"ubicazione:{cella_ubi}", + f"idcella:{idcella_num}", + ), ) @_log_call() @@ -990,11 +999,12 @@ class CelleMultipleWindow(ctk.CTkToplevel): _log_dataset("multi_udc_riepilogo", rows) for item in self.sum_tbl.get_children(): self.sum_tbl.delete(item) - for row in rows: + for index, row in enumerate(rows): self.sum_tbl.insert( "", "end", values=(row.get("Corsia"), row.get("TotCelle", 0), row.get("CelleMultiple", 0), f"{row.get('Percentuale', 0):.2f}"), + tags=(zebra_tag(index),), ) def expand_all(self):