Загрузка данных
# ==========================================
# dukora\modules\project\project_event_ui.py
# ==========================================
import customtkinter as ctk
from tkinter import ttk
import tkinter as tk
import datetime
import os
import sqlite3
from .project_event_logic import ProjectEventLogic
from tkcalendar import DateEntry
from tkinter import messagebox
# ==========================================
# ОСНОВНОЙ КЛАСС ОКНА СОБЫТИЙ
# ==========================================
class ProjectEventUI(ctk.CTkFrame):
EDIT_MODE_COLOR = "#8B4513"
def _build_indicator_choices(self, indicators):
"""
Принимает сырой список индикаторов [(name, ), ...] или [{'name': ..., 'code': ...}, ...]
Возвращает два объекта:
1. choices_list - то, что увидит пользователь в списке (с \n и отступами)
2. display_to_code_map - скрытый словарь {строка_из_списка: чистый_db_code}
"""
import textwrap
# Сначала достаем данные из БД уже отсортированными ПО КОДУ (это мы поправим в логике позже)
# Пока считаем, что indicators это просто список имен строкой
# Если на вход пришел список объектов/кортежей, вытащим имена
if indicators and not isinstance(indicators[0], str):
names = []
codes = []
for item in indicators:
# Проверяем структуру данных
if isinstance(item, dict):
names.append(item.get('name', ''))
codes.append(item.get('code', ''))
elif isinstance(item, tuple) and len(item) >= 2:
names.append(item[0])
codes.append(item[1]) # Предполагаем, что второй элемент это code
else:
names.append(str(item[0]))
codes.append("") # Кода нет
else:
names = indicators if indicators else []
codes = [""] * len(names)
# Сортируем их вместе по названию (или по коду, если коды есть), чтобы порядок был стабильным
# ВАЖНО: Логика в load_indicators_for_group должна вернуть сортировку по CODE!
combined = sorted(zip(names, codes), key=lambda x: x[0])
choices_list = ["Все показатели"]
display_to_code_map = {"": ""} # Пустая строка для "Все показатели"
wrapper = textwrap.TextWrapper(width=65, subsequent_indent=' ') # 10 пробелов под код + 1 пробел
for name, code in combined:
if not name.strip():
continue
lines = wrapper.wrap(name)
formatted_text = f"{code:<10} {' \\n '.join(lines)}"
choices_list.append(formatted_text)
display_to_code_map[formatted_text] = code
return choices_list, display_to_code_map
def __init__(self, master, window_ref, logic: ProjectEventLogic):
super().__init__(master, fg_color="transparent")
self.window_ref = window_ref
self.logic = logic
self.group_names_list = []
self.indicator_names_list = []
self.group_names_to_codes = {}
self.indicator_cache = {}
self.editing_event_code = None
self._setup_widgets()
self._bind_global_events()
self.update_project_dependent_lists()
self.after(50, self._initial_load)
def _initial_load(self):
print(f"[INIT LOAD] Waiting for UI sync...")
current_proj_code = getattr(self.window_ref, 'current_selected_project_code', "")
if current_proj_code and current_proj_code != "Проектов нет":
self._refresh_event_table(current_proj_code)
def _setup_widgets(self):
main_form = ctk.CTkFrame(self, fg_color="transparent")
main_form.pack(fill="both", expand=True, padx=10, pady=10)
main_form.grid_columnconfigure(1, weight=1)
lbl_group = ctk.CTkLabel(main_form, text="Выбрать группу:", anchor="w")
lbl_group.grid(row=0, column=0, sticky="w", pady=(0, 5), padx=(0, 10))
self.cmb_group = ctk.CTkComboBox(
main_form,
state="readonly",
width=250,
command=self._on_group_change
)
self.cmb_group.grid(row=0, column=1, sticky="ew", pady=(0, 5))
lbl_indicator = ctk.CTkLabel(main_form, text="Выбрать показатель:", anchor="w")
lbl_indicator.grid(row=1, column=0, sticky="w", pady=(0, 5), padx=(0, 10))
# --- КНОПКА ВЫБОРА ПОКАЗАТЕЛЯ ---
self.btn_show_indicators = ctk.CTkButton(
main_form,
text="Выбрать показатель...",
width=250,
anchor="w",
fg_color="#F0F0F0",
hover_color="#E0E0E0",
text_color="black",
command=self._toggle_indicator_list
)
self.btn_show_indicators.grid(row=1, column=1, sticky="ew", pady=(0, 10))
mono_font = ctk.CTkFont(family="Courier New", size=12)
self.btn_show_indicators.configure(font=mono_font)
# --- СКРЫТОЕ ОКНО СО СПИСКОМ (создаем как атрибут объекта) ---
self.indicator_popup_frame = ctk.CTkFrame(
main_form,
width=300,
height=200,
border_width=1,
border_color="gray"
)
# Создаем таблицу с фиксированной шириной первого (и единственного) столбца
self.tree_indicators = ttk.Treeview(
self.indicator_popup_frame,
columns=("indicator"),
show="headings",
height=8,
selectmode="browse"
)
# Явно задаем ширину для колонки "indicator" (280 пикселей под текст + место под скролл)
self.tree_indicators.column("indicator", width=280, minwidth=280, stretch=False, anchor="w")
self.tree_indicators.pack(fill="both", expand=True, side="left")
# Прокрутка для таблицы
scrollbar_i = ctk.CTkScrollbar(
self.indicator_popup_frame,
orientation="vertical",
command=self.tree_indicators.yview
)
scrollbar_i.pack(side="right", fill="y")
self.tree_indicators.configure(yscrollcommand=scrollbar_i.set)
# Событие клика по строке списка
self.tree_indicators.bind("<ButtonRelease-1>", self._on_indicator_select)
lbl_event = ctk.CTkLabel(main_form, text="Содержание события:", anchor="w")
lbl_event.grid(row=2, column=0, sticky="nw", pady=(0, 5), padx=(0, 10))
event_container = ctk.CTkFrame(main_form, fg_color="#F0F0F0", border_width=1, border_color="#A9A9A9")
event_container.grid(row=2, column=1, sticky="ew", pady=(0, 5), ipadx=2, ipady=2)
self.txt_event = ctk.CTkTextbox(event_container, height=80, wrap="word", fg_color="white")
self.txt_event.pack(fill="both", expand=True, padx=2, pady=2)
paste_btn = ctk.CTkButton(main_form, text="Вставить", fg_color="#2B2B2B", hover_color="#3C3C3C", command=self._paste_from_clipboard)
paste_btn.grid(row=3, column=1, sticky="e", pady=(0, 10), ipadx=10)
meta_frame = ctk.CTkFrame(main_form, fg_color="transparent")
meta_frame.grid(row=4, column=0, columnspan=2, sticky="ew", pady=(0, 10))
meta_frame.grid_columnconfigure(1, weight=1)
ttk.Label(meta_frame, text="Дата события:").grid(row=0, column=0, sticky="w", padx=(0, 10))
self.date_entry = DateEntry(
master=meta_frame,
width=12,
background='darkblue',
foreground='white',
bordercolor='gray',
headersbackground='darkgray',
normalbackground='white',
normalforeground='black',
weekendbackground='lightblue',
weekendforeground='red',
date_pattern='dd.MM.yyyy'
)
today = datetime.datetime.now()
self.date_entry.set_date(today)
self.date_entry.grid(row=0, column=1, sticky="w")
ttk.Label(meta_frame, text="Направленность:").grid(row=0, column=2, sticky="w", padx=(20, 10))
self.dir_var = tk.IntVar(value=1)
rb_plus = ctk.CTkRadioButton(meta_frame, text="(+)", variable=self.dir_var, value=1)
rb_plus.grid(row=0, column=3, sticky="w")
rb_minus = ctk.CTkRadioButton(meta_frame, text="(-)", variable=self.dir_var, value=-1)
rb_minus.grid(row=0, column=4, sticky="w")
ctrl_frame = ctk.CTkFrame(main_form, fg_color="transparent")
ctrl_frame.grid(row=5, column=0, columnspan=2, sticky="ew", pady=(0, 10))
for i in range(4): ctrl_frame.grid_columnconfigure(i, weight=1)
self.btn_add = ctk.CTkButton(ctrl_frame, text="Добавить", fg_color="#3B82F6", command=self._add_event)
self.btn_add.grid(row=0, column=0, sticky="ew", padx=2)
self.btn_edit = ctk.CTkButton(ctrl_frame, text="Редактировать", fg_color="#10B981", command=self._enter_edit_mode)
self.btn_edit.grid(row=0, column=1, sticky="ew", padx=2)
self.btn_save = ctk.CTkButton(ctrl_frame, text="Сохранить", state="disabled", fg_color=self.EDIT_MODE_COLOR, command=self._save_edited_event)
self.btn_save.grid(row=0, column=2, sticky="ew", padx=2)
self.btn_del = ctk.CTkButton(ctrl_frame, text="Удалить", fg_color="#EF4444", command=self._delete_selected_event)
self.btn_del.grid(row=0, column=3, sticky="ew", padx=2)
tree_container = ctk.CTkFrame(main_form, fg_color="transparent")
tree_container.grid(row=6, column=0, columnspan=2, sticky="nsew", pady=(10, 0))
tree_container.grid_rowconfigure(0, weight=1)
tree_container.grid_columnconfigure(0, weight=1)
# --- НАСТРОЙКА СТИЛЯ ДЛЯ ВЫСОТЫ СТРОК ---
style = ttk.Style()
# Берем текущую тему CustomTkinter, чтобы цвета скроллбаров совпадали
theme_name = style.theme_use()
# Конфигурируем стиль. rowheight — это высота одной строки в пикселях.
# Для шрифта 12px значение 35-40 позволит вместить переносы текста без наложения.
style.configure("Custom.Treeview",
font=("Courier New", 11),
rowheight=38)
# Применяем наш кастомный стиль к таблице
self.tree = ttk.Treeview(
tree_container,
columns=columns,
show="headings",
selectmode="browse",
height=8,
style="Custom.Treeview" # Подключаем увеличенную высоту
)
self.tree.heading("num", text="№п/п"); self.tree.column("num", width=40, anchor="center", stretch=False)
self.tree.heading("code", text="Код"); self.tree.column("code", width=100, anchor="center", stretch=False)
self.tree.heading("date", text="Дата"); self.tree.column("date", width=110, anchor="center", stretch=False)
self.tree.heading("sign", text="+/-"); self.tree.column("sign", width=50, anchor="center", stretch=False)
self.tree.heading("event", text="Событие"); self.tree.column("event", minwidth=200, stretch=True)
self.tree.pack(side="left", fill="both", expand=True)
scrollbar = ctk.CTkScrollbar(tree_container, orientation="vertical", command=self.tree.yview)
scrollbar.pack(side="right", fill="y")
self.tree.configure(yscrollcommand=scrollbar.set)
search_frame = ctk.CTkFrame(main_form, fg_color="transparent")
search_frame.grid(row=7, column=0, columnspan=2, sticky="ew", pady=(10, 0))
search_frame.grid_columnconfigure(1, weight=1)
ttk.Label(search_frame, text="Наберите код:").grid(row=0, column=0, sticky="w", padx=(0, 5))
self.search_entry = ctk.CTkEntry(search_frame)
self.search_entry.grid(row=0, column=1, sticky="ew", padx=(0, 5))
find_btn = ctk.CTkButton(search_frame, text="Искать", fg_color="#3B82F6", command=self._search_by_code)
find_btn.grid(row=0, column=2, sticky="e")
def update_project_dependent_lists(self, forced_project_code=None):
proj_code = forced_project_code or getattr(self.window_ref, 'current_selected_project_code', "")
if not proj_code:
placeholder_groups = ["Все группы"]
self.cmb_group.configure(values=placeholder_groups)
self.cmb_group.set(placeholder_groups[0])
# Вместо cmb_indicator меняем ТЕКСТ КНОПКИ
self.btn_show_indicators.configure(text="Выбрать показатель...")
return
groups_raw, name_to_code_map = self.logic.load_groups_and_indicators()
safe_groups = []
if groups_raw is not None:
try:
if hasattr(groups_raw[0], 'name'):
safe_groups = sorted(list(set([item.name for item in groups_raw])))
elif isinstance(groups_raw[0], tuple) and len(groups_raw[0]) > 1:
safe_groups = sorted(list(set([item[1] for item in groups_raw])))
elif isinstance(groups_raw[0], str):
safe_groups = sorted(list(set(groups_raw)))
except Exception:
pass
final_group_list = ["Все группы"] + safe_groups if safe_groups else ["Все группы"]
self.group_names_list = final_group_list
self.group_names_to_codes = {"": ""}
if isinstance(name_to_code_map, dict):
self.group_names_to_codes.update(name_to_code_map)
self.cmb_group.configure(values=final_group_list)
self.cmb_group.set(final_group_list[0])
# Загрузка индикаторов для первой выбранной группы или "Все"
all_indicators = []
seen_names = set()
for cached_list in self.indicator_cache.values():
for item in cached_list:
name = item[0]
if name not in seen_names:
all_indicators.append(item)
seen_names.add(name)
all_indicators.sort(key=lambda x: x[0])
ind_names = ["Все показатели"] + [item[0] for item in all_indicators]
self.indicator_names_list = ind_names
# Обновляем наши новые виджеты вместо старых combobox
choices_list, mapping = self._build_indicator_choices(all_indicators)
self.populate_indicator_tree(choices_list)
if len(ind_names) > 1:
self.btn_show_indicators.configure(text=ind_names[1])
else:
self.btn_show_indicators.configure(text="Выбрать показатель...")
self.after_idle(lambda code=proj_code: (
self.window_ref._update_control_buttons_state(True),
self._refresh_event_table(code)
))
def _on_group_change(self, event=None):
group_name = self.cmb_group.get()
if group_name == "Все группы":
# Сбрасываем кнопку индикаторов на заглушку
self.btn_show_indicators.configure(text="Выбрать показатель...")
# Очищаем дерево (список) внутри всплывающего окна
self.populate_indicator_tree(["Все показатели"])
# Сохраняем пустой кэш для этой группы, чтобы не было ошибок при поиске кода
self.indicator_cache[group_name] = ([], {})
return
group_code = self.group_names_to_codes.get(group_name)
if not group_code:
self.btn_show_indicators.configure(text="Выбрать показатель...")
self.populate_indicator_tree(["Все показатели"])
self.indicator_cache[group_code] = ([], {})
return
raw_indicators = self.logic.load_indicators_for_group(group_code)
final_list, mapping = self._build_indicator_choices(raw_indicators)
# Обновляем интерфейс: заполняем дерево и текст кнопки
self.populate_indicator_tree(final_list)
if len(final_list) > 1:
self.btn_show_indicators.configure(text=final_list[1])
else:
self.btn_show_indicators.configure(text="Выбрать показатель...")
# Сохраняем данные в кэш
self.indicator_cache[group_code] = (raw_indicators, mapping)
def _update_indicator_combobox(self, group_name: str):
self.cmb_indicator.set("")
if not group_name or group_name == "Все группы":
placeholder_list = ["Все показатели"]
self.indicator_names_list = placeholder_list
self.cmb_indicator.configure(values=placeholder_list)
self.cmb_indicator.set("Все показатели")
self.indicator_cache[group_name] = ([], {})
return
group_code = self.group_names_to_codes.get(group_name)
if not group_code:
self.cmb_indicator.configure(values=["Все показатели"])
self.cmb_indicator.set("Все показатели")
self.indicator_cache[group_code] = ([], {})
return
raw_indicators = self.logic.load_indicators_for_group(group_code)
final_list, mapping = self._build_indicator_choices(raw_indicators)
# Сохраняем маппинг глобально для доступа в других методах
self.indicator_cache[group_code] = (raw_indicators, mapping)
# Обновляем текст кнопки по умолчанию
if len(final_list) > 1:
self.btn_show_indicators.configure(text=final_list[1])
else:
self.btn_show_indicators.configure(text="Выбрать показатель...")
# Наполняем наше новое древовидное окно
self.populate_indicator_tree(final_list)
def _collect_current_selection(self) -> dict:
proj_code = self.window_ref.current_selected_project_code
group_name = self.cmb_group.get()
group_code = self.group_names_to_codes.get(group_name, "") if group_name else ""
full_display_text = self.btn_show_indicators.cget("text")
# Если выбрано "Все показатели" или заглушка
if full_display_text == "Выбрать показатель..." or full_display_text == "Все показатели":
indicator_name = ""
indicator_code = ""
else:
# Берем сохраненный словарь
_, mapping = self.indicator_cache.get(group_code, ([], {}))
indicator_code = mapping.get(full_display_text, "")
# Имя берем без кода и переносов для записи в БД
clean_name = full_display_text.split('\n')[0].replace(f"{indicator_code:<10}", "").strip()
indicator_name = clean_name
selected_display = self.cmb_indicator.get()
# Если выбран конкретный показатель (не заглушка)
if selected_display and selected_display != "Все показатели" and group_code:
# Берем сохраненный ранее словарь для этой группы
_, mapping = self.indicator_cache.get(group_code, ([], {}))
# Достаем чистый код базы данных через отображаемую строку
indicator_code = mapping.get(selected_display, "")
# Фикс названия для БД (убираем лишние переносы, если они туда попадут)
clean_name = selected_display.split('\n')[0].replace(f"{indicator_code:<10}", "").strip()
indicator_name = clean_name
return {
"project_code": proj_code,
"group_name": group_name,
"group_code": group_code,
"indicator_name": indicator_name,
"indicator_code": indicator_code
}
def _add_event(self):
data = self._collect_current_selection()
if not all([data['project_code'], data['group_code'], data['indicator_code']]):
msg_parts = []
if not data['project_code']: msg_parts.append("Проект")
if not data['group_code']: msg_parts.append("Группа")
if not data['indicator_code']: msg_parts.append("Показатель")
messagebox.showwarning("Ошибка", f"Заполнены не все обязательные поля.\nОтсутствуют: {', '.join(msg_parts)}")
return
text = self.txt_event.get("1.0", "end").strip()
if not text:
messagebox.showwarning("Ошибка", "Поле 'Содержание события' пустое.")
return
try:
date_str = self.date_entry.get_date().strftime('%Y-%m-%d') # Принудительно YYYY-MM-DD
except Exception:
date_str = datetime.datetime.now().strftime('%Y-%m-%d')
sign = self.dir_var.get()
success, msg = self.logic.add_event_to_db(data, text, date_str, sign)
messagebox.showinfo("Результат", msg)
if success:
self.txt_event.delete("1.0", "end")
proj_code = self.window_ref.current_selected_project_code
self.cmb_group.set("Все группы")
self.cmb_indicator.set("Все показатели")
self._refresh_event_table(proj_code)
def _refresh_event_table(self, project_code: str = None):
# Очистка таблицы
for row in self.tree.get_children():
self.tree.delete(row)
if not project_code:
return
db_path = os.path.join(self.logic.project_dbs_folder, f"{project_code}.db")
if not os.path.exists(db_path):
return
try:
conn = sqlite3.connect(db_path)
cur = conn.cursor()
query = "SELECT event_code, event_data, event_sign, event_text_content FROM project_data"
params = ()
conditions = []
current_group = self.cmb_group.get()
current_indicator = self.cmb_indicator.get()
if current_group and current_group != "Все группы":
group_code = self.group_names_to_codes.get(current_group, "")
if group_code:
conditions.append("group_code = ?")
params += (group_code,)
if current_indicator and current_indicator != "Все показатели":
conditions.append("indicator_name = ?")
params += (current_indicator,)
if conditions:
query += " WHERE " + " AND ".join(conditions)
query += " ORDER BY id DESC LIMIT 50"
print(f"[SQL EXEC] Query: {query} | Params: {params}")
cur.execute(query, params)
rows = cur.fetchall()
conn.close()
print(f"[TABLE DEBUG] Fetched {len(rows)} items from disk.")
# Предварительная очистка выделения перед заполнением новой таблицей
self.tree.selection_remove(self.tree.selection())
# Вставка строк с привязкой клика к полю txt_event
for idx, row in enumerate(rows, start=1):
sign_char = "+" if row[2] == 1 else "-"
full_text = row[3]
iid = f"row_{idx}"
self.tree.insert("", "end", iid=iid, values=(idx, row[0], row[1], sign_char, full_text))
except Exception as e:
print(f"[CRITICAL TABLE FAIL]: {e}")
def _on_tree_select(self, event):
"""
Срабатывает ПРИ ЛЮБОМ ВЫБОРЕ СТРОКИ.
Заполняет форму данными выбранной строки.
"""
selected = self.tree.selection()
if not selected:
return
item_id = selected[0]
vals = self.tree.item(item_id)["values"]
if len(vals) < 5:
return
text_content = vals[4]
# Блокируем модифицированные события, чтобы избежать зацикливания
self.txt_event.unbind("<<Modified>>")
# Очищаем и заполняем поле текстом из таблицы
self.txt_event.delete("1.0", "end")
self.txt_event.insert("1.0", text_content)
# Заполняем дату и знак (+/-)
try:
date_obj = datetime.datetime.strptime(str(vals[2]), '%Y-%m-%d')
self.date_entry.set_date(date_obj)
except ValueError:
# Если вдруг формат пришел другой (например dd.mm.YYYY)
try:
date_obj = datetime.datetime.strptime(str(vals[2]), '%d.%m.%Y')
self.date_entry.set_date(date_obj)
except Exception:
pass
sign_val = int(vals[3].replace('+', '1').replace('-', '-1'))
self.dir_var.set(sign_val)
# Устанавливаем режим редактирования для этой строки
self.editing_event_code = vals[1]
# Включаем визуальную индикацию (желтый фон удален согласно ТЗ по очистке NativeToolTip)
self._set_edit_mode_visuals(True)
# Возвращаем отслеживание изменений текста
self.after(100, lambda: self.txt_event.bind("<<Modified>>", self._on_text_modified))
def _set_edit_mode_visuals(self, is_active: bool):
"""Меняет цвет фона поля текста и состояние кнопок"""
color = "#FFF9C4" if is_active else "#F0F0F0" # Желтый или стандартный
self.txt_event.configure(background=color)
# Кнопки согласно ТЗ: Редактировать блокируется, Сохранить активна
self.btn_edit.configure(state="disabled")
self.btn_save.configure(state="normal")
# Разблокируем поля даты и знака для изменения
self.date_entry.config(state="normal")
def _get_text_by_iid(self, item_id: str) -> str:
vals = self.tree.item(item_id)["values"]
if len(vals) > 4:
return vals[4]
return ""
def _enter_edit_mode(self):
sel = self.tree.selection()
if not sel:
messagebox.showwarning("Ошибка", "Выберите событие в таблице."); return
self._enter_edit_mode_by_iid(sel[0])
def _enter_edit_mode_by_iid(self, item_id: str):
vals = self.tree.item(item_id)["values"]
if len(vals) < 5:
return
self.editing_event_code = vals[1]
# Заполняем поля формы данными выбранной строки
self.txt_event.delete("1.0", "end")
self.txt_event.insert("1.0", vals[4])
# Дата события
try:
date_obj = datetime.datetime.strptime(str(vals[2]), '%Y-%m-%d')
self.date_entry.set_date(date_obj)
except Exception:
pass
# Направленность (+/-)
sign_val = int(vals[3].replace('+', '1').replace('-', '-1'))
self.dir_var.set(sign_val)
# === РЕЖИМ РЕДАКТИРОВАНИЯ (КНОПКИ И ПОЛЯ) ===
# Блокируем неизменяемые фильтры
self.btn_add.configure(state="disabled")
self.btn_del.configure(state="disabled")
self.cmb_group.configure(state="disabled")
self.cmb_indicator.configure(state="disabled")
# Кнопка Редактировать становится серой (заблокирована)
self.btn_edit.configure(state="disabled")
# Кнопка Сохранить остается активной (оранжевой), как вы просили НЕ скрывать её
# Если она была disabled - включаем
self.btn_save.configure(state="normal")
# Разблокируем поля ввода для изменений (ТЗ п.3)
self.txt_event.configure(state="normal")
self.date_entry.config(state="normal")
# Радиокнопки dir_var обычно доступны всегда, если frame активен
def _save_edited_event(self):
if not getattr(self, 'editing_event_code', None):
return
new_text = self.txt_event.get("1.0", "end").strip()
if not new_text:
# Используем тот же messagebox, который мы только что импортировали
messagebox.showwarning("Ошибка", "Текст не может быть пустым.")
return
try:
date_str = self.date_entry.get_date().strftime('%Y-%m-%d')
except Exception:
date_str = datetime.datetime.now().strftime('%Y-%m-%d')
sign_val = self.dir_var.get()
db_path = os.path.join(self.logic.project_dbs_folder, f"{self._collect_current_selection()['project_code']}.db")
try:
conn = sqlite3.connect(db_path)
cur = conn.cursor()
cur.execute(
'''UPDATE project_data
SET event_text_content = ?, event_data = ?, event_sign = ?
WHERE event_code = ?''',
(new_text, date_str, sign_val, self.editing_event_code)
)
conn.commit()
conn.close()
# Если дошли до сюда без ошибок — показываем успех
messagebox.showinfo("Готово", "Изменения сохранены.")
self._reset_controls()
proj_code = self._collect_current_selection()['project_code']
self._refresh_event_table(proj_code)
except Exception as e:
# Если упало подключение к БД или SQL-запрос — показываем ошибку
messagebox.showerror("Ошибка записи", f"Не удалось сохранить изменения: {e}")
def _delete_selected_event(self):
sel = self.tree.selection()
if not sel: return
if not messagebox.askyesno("Подтверждение", "Удалить выбранное событие?"): return
event_code = self.tree.item(sel[0])["values"][1]
db_path = os.path.join(self.logic.project_dbs_folder, f"{self._collect_current_selection()['project_code']}.db")
conn = sqlite3.connect(db_path)
cur = conn.cursor()
cur.execute("DELETE FROM project_data WHERE event_code = ?", (event_code,))
conn.commit()
conn.close()
self._refresh_event_table(self._collect_current_selection()['project_code'])
def _reset_controls(self):
self.editing_event_code = None
# Возвращаем базовые состояния кнопок
self.btn_add.configure(state="normal")
self.btn_del.configure(state="normal")
self.cmb_group.configure(state="readonly")
# Вместо cmb_indicator разблокируем нашу кнопку выбора (состояние normal у кнопки уже есть по умолчанию)
# Если кнопка была заблокирована в режиме редактирования, включаем её
if self.btn_show_indicators.cget("state") == "disabled":
self.btn_show_indicators.configure(state="normal")
# Кнопка Редактировать снова зеленая и активная
self.btn_edit.configure(state="normal")
# Кнопка Сохранить гаснет (становится недоступной)
self.btn_save.configure(state="disabled")
# Очищаем поле текста
self.txt_event.delete("1.0", "end")
# Сбрасываем текст кнопки индикаторов на подсказку
self.btn_show_indicators.configure(text="Выбрать показатель...")
def _paste_from_clipboard(self):
try:
txt = self.clipboard_get()
self.txt_event.insert(tk.INSERT, txt)
except tk.TclError:
pass
def _bind_global_events(self):
self.txt_event.bind("<Button-3>", lambda e: (self._paste_from_clipboard(), "break"))
def _search_by_code(self):
target = self.search_entry.get().strip()
if not target:
messagebox.showwarning("Поиск", "Введите код."); return
items = self.tree.get_children()
found = None
for item in items:
vals = self.tree.item(item)["values"]
if len(vals) > 1 and vals[1] == target:
found = item
break
if found:
self.tree.selection_set(found)
self.tree.see(found)
self.tree.focus(found)
else:
messagebox.showinfo("Поиск", "Событие с таким кодом не найдено.")
def _toggle_indicator_list(self, event=None):
"""Показывает или прячет всплывающее окно со списком"""
if self.indicator_popup_frame.winfo_viewable():
self.indicator_popup_frame.place_forget()
else:
x = self.btn_show_indicators.winfo_rootx() - self.winfo_rootx()
y = self.btn_show_indicators.winfo_rooty() - self.winfo_rooty() + self.btn_show_indicators.winfo_height()
# Поднимаем окно НАД всеми остальными виджетами ДО размещения
self.indicator_popup_frame.lift()
# Позиционируем панель строго под кнопкой
self.indicator_popup_frame.place(
in_=self.btn_show_indicators,
x=x,
y=y,
relwidth=1.0,
bordermode="outside" # Чтобы рамка окна считалась от края кнопки
)
self.focus_set()
def _on_indicator_select(self, event):
"""Срабатывает при выборе показателя в кастомном списке"""
selected = self.tree_indicators.selection()
if not selected:
return
item_id = selected[0]
display_text = self.tree_indicators.item(item_id)["values"][0]
# Обновляем текст на кнопке
self.btn_show_indicators.configure(text=display_text)
# Прячем окно
self.indicator_popup_frame.place_forget()
# Принудительно обновляем таблицу событий, так как сменился фильтр
proj_code = self.window_ref.current_selected_project_code
if proj_code:
self._refresh_event_table(proj_code)
def populate_indicator_tree(self, choices_list):
"""Очищает и заполняет дерево индикаторов новыми данными"""
# Очистка
for row in self.tree_indicators.get_children():
self.tree_indicators.delete(row)
# Заполнение (пропускаем первый элемент "Все показатели" для дерева, оставим его только текстом кнопки)
start_index = 1 if len(choices_list) > 1 and choices_list[0] == "Все показатели" else 0
for i, text in enumerate(choices_list[start_index:], start=1):
self.tree_indicators.insert("", "end", iid=f"ind_{i}", values=(text,))