flexgui 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flexgui/__init__.py +71 -0
- flexgui/app.py +221 -0
- flexgui/builder.py +211 -0
- flexgui/designer.py +1974 -0
- flexgui/editor.py +277 -0
- flexgui/mode.py +95 -0
- flexgui/registry.py +188 -0
- flexgui/runtime.py +190 -0
- flexgui/theme.py +139 -0
- flexgui/triggers.py +467 -0
- flexgui/widgets.py +738 -0
- flexgui-1.0.0.dist-info/METADATA +235 -0
- flexgui-1.0.0.dist-info/RECORD +15 -0
- flexgui-1.0.0.dist-info/WHEEL +4 -0
- flexgui-1.0.0.dist-info/licenses/LICENSE +21 -0
flexgui/designer.py
ADDED
|
@@ -0,0 +1,1974 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
FlexGUI Studio — trình thiết kế trực quan, đồ họa nền tối hiện đại.
|
|
4
|
+
|
|
5
|
+
Bố cục (kiểu Figma/VS Code):
|
|
6
|
+
|
|
7
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
8
|
+
│ ◈ FlexGUI Studio · myapp [Mới][Mở][Lưu] [↶ ↷] [▦ Lưới] │
|
|
9
|
+
│ [Triggers] [Xuất .py] [▲ Build] [▶│
|
|
10
|
+
├──────┬───────────────────────────────────────────┬────────────┤
|
|
11
|
+
│ │ ╔════════════════════════════════╗ │ CỬA SỔ / │
|
|
12
|
+
│ PAL │ ║ ⬜ myapp ─ □ ✕ ║ │ THÀNH PHẦN │
|
|
13
|
+
│ ETTE │ ║────────────────────────────────║ │ (thuộc tính)│
|
|
14
|
+
│ TE │ ║ vùng thiết kế (kéo thả) ║ │ │
|
|
15
|
+
│ │ ╚════════════════════════════════╝ │ [Trigger] │
|
|
16
|
+
├──────┴───────────────────────────────────────────┴────────────┤
|
|
17
|
+
│ ● Sẵn sàng · X: 12 Y: 34 · Chọn: Button #2 · Lưới 10px │
|
|
18
|
+
└──────────────────────────────────────────────────────────────┘
|
|
19
|
+
|
|
20
|
+
Thiết kế vẽ bằng CANVAS ITEM (không phải widget thật) — kéo thả
|
|
21
|
+
mượt tuyệt đối, KHÔNG TÀN ẢNH, resize 8 núm, multi-select quét vùng,
|
|
22
|
+
snap lưới, undo/redo, mini VS Code cho trigger, xem trước, build .exe.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import copy
|
|
26
|
+
import heapq
|
|
27
|
+
import json
|
|
28
|
+
import os
|
|
29
|
+
import re
|
|
30
|
+
import traceback
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
import tkinter as tk
|
|
34
|
+
import tkinter.font as tkfont
|
|
35
|
+
from tkinter import ttk, messagebox, filedialog, colorchooser, simpledialog
|
|
36
|
+
|
|
37
|
+
from . import registry as _reg
|
|
38
|
+
from .theme import MAU, MAU_APP, font_ui, font_mono, vien_bo, mau_luoi
|
|
39
|
+
from .editor import TrinhSoanThao
|
|
40
|
+
from .widgets import _tao_tu_du_lieu
|
|
41
|
+
from .app import App
|
|
42
|
+
from .triggers import nap_trigger, SUKIEN_CHINH
|
|
43
|
+
from . import builder
|
|
44
|
+
|
|
45
|
+
# ==============================================================================
|
|
46
|
+
# HẰNG SỐ BỐ CỤC
|
|
47
|
+
# ==============================================================================
|
|
48
|
+
LE = 36 # khoảng trống quanh cửa sổ thiết kế (canvas)
|
|
49
|
+
H_TIEU_DE = 36 # chiều cao thanh tiêu đề giả của cửa sổ thiết kế
|
|
50
|
+
KICH_THUOC_MAC_DINH = { # kích thước px mặc định khi thả mới
|
|
51
|
+
"label": (None, 26),
|
|
52
|
+
"button": (140, 40),
|
|
53
|
+
"input": (220, 34),
|
|
54
|
+
"checkbox": (170, 28),
|
|
55
|
+
"text": (300, 150),
|
|
56
|
+
}
|
|
57
|
+
CHU_MAC_DINH = {
|
|
58
|
+
"label": "Nhãn mới",
|
|
59
|
+
"button": "Nút bấm",
|
|
60
|
+
"input": "",
|
|
61
|
+
"checkbox": "Lựa chọn",
|
|
62
|
+
"text": "",
|
|
63
|
+
}
|
|
64
|
+
TEN_HIEN_THI = {
|
|
65
|
+
"label": "Label", "button": "Button", "input": "Input",
|
|
66
|
+
"checkbox": "Checkbox", "text": "Text",
|
|
67
|
+
}
|
|
68
|
+
CAC_NUT_BAM = ["nw", "n", "ne", "e", "se", "s", "sw", "w"] # 8 núm resize
|
|
69
|
+
|
|
70
|
+
MUC_CHEN = [ # snippet cho drawer trigger
|
|
71
|
+
("Đếm số lần bấm nút",
|
|
72
|
+
"so_lan = 0\n\n"
|
|
73
|
+
"def on_button_1_click():\n"
|
|
74
|
+
" global so_lan\n"
|
|
75
|
+
" so_lan += 1\n"
|
|
76
|
+
" label[1].set_text(f\"Đã bấm {so_lan} lần\")\n"),
|
|
77
|
+
("Đổi màu + cỡ chữ",
|
|
78
|
+
"def on_button_1_click():\n"
|
|
79
|
+
" label[1].color = '#d63384'\n"
|
|
80
|
+
" label[1].font_size = 16\n"
|
|
81
|
+
" label[1].refresh()\n"),
|
|
82
|
+
("Hiện / ẩn widget",
|
|
83
|
+
"def on_checkbox_2_toggle():\n"
|
|
84
|
+
" if checkbox[2].checked:\n"
|
|
85
|
+
" text[3].show()\n"
|
|
86
|
+
" else:\n"
|
|
87
|
+
" text[3].hide()\n"),
|
|
88
|
+
("Nhập rồi Enter",
|
|
89
|
+
"def on_input_1_enter():\n"
|
|
90
|
+
" ten = input[1].text.strip()\n"
|
|
91
|
+
" if ten:\n"
|
|
92
|
+
" label[1].set_text(\"Xin chào \" + ten)\n"
|
|
93
|
+
" input[1].clear()\n"),
|
|
94
|
+
("Đồng hồ mỗi giây",
|
|
95
|
+
"from datetime import datetime\n\n"
|
|
96
|
+
"@every(1000)\n"
|
|
97
|
+
"def dong_ho():\n"
|
|
98
|
+
" label[1].set_text(datetime.now().strftime(\"%H:%M:%S\"))\n"),
|
|
99
|
+
("Trigger điều kiện",
|
|
100
|
+
"@when(lambda: checkbox[2].checked and input[1].text == \"admin\")\n"
|
|
101
|
+
"def khi_du():\n"
|
|
102
|
+
" alert(\"Đăng nhập hợp lệ!\")\n"),
|
|
103
|
+
("import thư viện ngoài",
|
|
104
|
+
"import random\n\n"
|
|
105
|
+
"def on_button_1_click():\n"
|
|
106
|
+
" label[1].set_text(\"Số may mắn: \" + str(random.randint(1, 99)))\n"),
|
|
107
|
+
("Lệnh app (refresh…)",
|
|
108
|
+
"def on_button_1_click():\n"
|
|
109
|
+
" set_title(\"Tiêu đề mới\")\n"
|
|
110
|
+
" set_bg(\"#fff8e1\")\n"
|
|
111
|
+
" refresh()\n"),
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ==============================================================================
|
|
116
|
+
# Nút phẳng (toolbar) — hover, kiểu accent, kiểu toggle
|
|
117
|
+
# ==============================================================================
|
|
118
|
+
class NutPhang(tk.Label):
|
|
119
|
+
"""Nút phẳng kiểu VS Code — Label + hiệu ứng hover + click."""
|
|
120
|
+
|
|
121
|
+
def __init__(self, cha, chu, lenh=None, accent=False, toggle=False,
|
|
122
|
+
rong=None, co=9, dam=False):
|
|
123
|
+
self._trang_thai_bat = False
|
|
124
|
+
self._lenh = lenh
|
|
125
|
+
self._toggle = toggle
|
|
126
|
+
self._accent = accent
|
|
127
|
+
try:
|
|
128
|
+
nen_cha = cha.cget("bg")
|
|
129
|
+
except Exception:
|
|
130
|
+
nen_cha = MAU["nen"]
|
|
131
|
+
kw = dict(
|
|
132
|
+
bg=MAU["gt"] if accent else nen_cha,
|
|
133
|
+
fg=MAU["chu_sang"] if accent else MAU["chu"],
|
|
134
|
+
font=font_ui(co, dam), padx=12, pady=6, cursor="hand2")
|
|
135
|
+
if rong:
|
|
136
|
+
kw["width"] = rong
|
|
137
|
+
super().__init__(cha, **kw)
|
|
138
|
+
self.bind("<Enter>", self._hover)
|
|
139
|
+
self.bind("<Leave>", self._roi)
|
|
140
|
+
self.bind("<Button-1>", self._bam)
|
|
141
|
+
|
|
142
|
+
# ---------------------------------------------------------------- trạng thái
|
|
143
|
+
def dat_bat(self, bat):
|
|
144
|
+
"""Bật/tắt trạng thái toggle (nút đang được kích hoạt)."""
|
|
145
|
+
self._trang_thai_bat = bool(bat)
|
|
146
|
+
self._ve()
|
|
147
|
+
|
|
148
|
+
def dat_chu(self, chu):
|
|
149
|
+
self.config(text=chu)
|
|
150
|
+
|
|
151
|
+
def dat_lenh(self, lenh):
|
|
152
|
+
self._lenh = lenh
|
|
153
|
+
|
|
154
|
+
# ---------------------------------------------------------------- hiệu ứng
|
|
155
|
+
def _hover(self, _e=None):
|
|
156
|
+
if self._accent:
|
|
157
|
+
self.config(bg=MAU["gt_nghi"])
|
|
158
|
+
elif self._trang_thai_bat:
|
|
159
|
+
self.config(bg="#094771")
|
|
160
|
+
else:
|
|
161
|
+
self.config(bg=MAU["nen_hover"])
|
|
162
|
+
|
|
163
|
+
def _roi(self, _e=None):
|
|
164
|
+
self._ve()
|
|
165
|
+
|
|
166
|
+
def _ve(self):
|
|
167
|
+
if self._accent:
|
|
168
|
+
self.config(bg=MAU["gt"])
|
|
169
|
+
elif self._trang_thai_bat:
|
|
170
|
+
self.config(bg="#094771", fg=MAU["chu_sang"])
|
|
171
|
+
else:
|
|
172
|
+
self.config(bg=self.master.cget("bg"), fg=MAU["chu"])
|
|
173
|
+
|
|
174
|
+
def _bam(self, _e=None):
|
|
175
|
+
if self._toggle:
|
|
176
|
+
self._trang_thai_bat = not self._trang_thai_bat
|
|
177
|
+
self._ve()
|
|
178
|
+
if self._lenh:
|
|
179
|
+
self._lenh()
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# ==============================================================================
|
|
183
|
+
# STUDIO
|
|
184
|
+
# ==============================================================================
|
|
185
|
+
class Studio:
|
|
186
|
+
"""Trình thiết kế trực quan FlexGUI (mọi thứ vẽ trên canvas — mượt)."""
|
|
187
|
+
|
|
188
|
+
def __init__(self, duong_dan=None, thiet_ke_ban_dau=None):
|
|
189
|
+
self.duong_dan = Path(duong_dan) if duong_dan else None
|
|
190
|
+
self.duong_json = None
|
|
191
|
+
self.duong_triggers = None
|
|
192
|
+
if self.duong_dan is not None:
|
|
193
|
+
from . import runtime as _rt
|
|
194
|
+
self.duong_json = _rt.duong_dan_json(self.duong_dan)
|
|
195
|
+
self.duong_triggers = _rt.duong_dan_triggers(self.duong_dan)
|
|
196
|
+
|
|
197
|
+
# ---- dữ liệu thiết kế ----
|
|
198
|
+
self.design = thiet_ke_ban_dau or self._doc_json() or self._thiet_ke_moi()
|
|
199
|
+
self.chon = [] # các id đang chọn (cuối = chính)
|
|
200
|
+
self.undo_ds = [] # chồng undo (ảnh chụp)
|
|
201
|
+
self.redo_ds = [] # chồng redo
|
|
202
|
+
self.ban_dau_undo = None # ảnh chụp trước khi sửa liên tục
|
|
203
|
+
self.dirty = False
|
|
204
|
+
|
|
205
|
+
# ---- quản lý ID (tái sử dụng như runtime) ----
|
|
206
|
+
ids = [c["id"] for c in self.design["components"]]
|
|
207
|
+
self._id_tiep_theo = (max(ids) + 1) if ids else 1
|
|
208
|
+
self._id_tu_do = []
|
|
209
|
+
|
|
210
|
+
# ---- cấu hình lưới ----
|
|
211
|
+
self.luoi_bat = True
|
|
212
|
+
self.buoc_luoi = 10
|
|
213
|
+
|
|
214
|
+
# ---- trạng thái chuột ----
|
|
215
|
+
self._trang_thai = None # None/keo/bam/quet
|
|
216
|
+
self._nhan_x = self._nhan_y = 0
|
|
217
|
+
self._goc = {} # id -> (x, y) lúc bắt đầu kéo
|
|
218
|
+
self._goc_kich_thuoc = None # (x, y, w, h) lúc bắt đầu resize
|
|
219
|
+
self._dda_x = self._dda_y = 0 # tổng dịch đã áp cho canvas.move
|
|
220
|
+
self._quet_hcn = None # item vùng quét
|
|
221
|
+
self._chon_truoc_quet = ()
|
|
222
|
+
self._hover_id = None
|
|
223
|
+
self._them_dem = 0 # so le khi thêm liên tiếp
|
|
224
|
+
|
|
225
|
+
self._xay_dung_giao_dien()
|
|
226
|
+
self._gan_phim()
|
|
227
|
+
|
|
228
|
+
# ================================================================== KHỞI TẠO
|
|
229
|
+
def _xay_dung_giao_dien(self):
|
|
230
|
+
self.root = tk.Tk()
|
|
231
|
+
self.root.title("FlexGUI Studio")
|
|
232
|
+
self.root.geometry("1280x780")
|
|
233
|
+
self.root.minsize(1024, 640)
|
|
234
|
+
self.root.configure(bg=MAU["nen"])
|
|
235
|
+
self._dinh_vi_giua_man_hinh()
|
|
236
|
+
|
|
237
|
+
self._tao_thanh_cong_cu()
|
|
238
|
+
self._tao_than()
|
|
239
|
+
self._tao_thanh_trang_thai()
|
|
240
|
+
self._tao_drawer_trigger(an=True)
|
|
241
|
+
|
|
242
|
+
self.root.protocol("WM_DELETE_WINDOW", self._khi_dong)
|
|
243
|
+
self._ve_lai()
|
|
244
|
+
self._lam_moi_bang_thuoc_tinh()
|
|
245
|
+
# Focus canvas để phím tắt hoạt động ngay
|
|
246
|
+
self.ban_ve.focus_set()
|
|
247
|
+
|
|
248
|
+
def _dinh_vi_giua_man_hinh(self):
|
|
249
|
+
"""Căn Studio ra giữa màn hình."""
|
|
250
|
+
x = (self.root.winfo_screenwidth() - 1280) // 2
|
|
251
|
+
y = (self.root.winfo_screenheight() - 780) // 2
|
|
252
|
+
self.root.geometry(f"+{max(x, 0)}+{max(y, 0)}")
|
|
253
|
+
|
|
254
|
+
# ------------------------------------------------------------- thanh công cụ
|
|
255
|
+
def _tao_thanh_cong_cu(self):
|
|
256
|
+
bar = tk.Frame(self.root, bg=MAU["nen"], pady=6, padx=8)
|
|
257
|
+
bar.pack(fill="x", side="top")
|
|
258
|
+
|
|
259
|
+
tk.Label(bar, text="◈", fg=MAU["gt"], bg=MAU["nen"],
|
|
260
|
+
font=font_ui(15)).pack(side="left")
|
|
261
|
+
tk.Label(bar, text="FlexGUI Studio", fg=MAU["chu_sang"],
|
|
262
|
+
bg=MAU["nen"], font=font_ui(10, True)).pack(side="left",
|
|
263
|
+
padx=(4, 2))
|
|
264
|
+
ten = self.duong_dan.name if self.duong_dan else "chưa lưu"
|
|
265
|
+
self.nhan_ten_file = tk.Label(bar, text="· " + ten, fg=MAU["chu_mo"],
|
|
266
|
+
bg=MAU["nen"], font=font_ui(9))
|
|
267
|
+
self.nhan_ten_file.pack(side="left")
|
|
268
|
+
|
|
269
|
+
tk.Frame(bar, bg=MAU["vien"], width=1).pack(side="left", fill="y",
|
|
270
|
+
padx=8, pady=2)
|
|
271
|
+
NutPhang(bar, "Mới", self._moi, rong=4).pack(side="left", padx=1)
|
|
272
|
+
NutPhang(bar, "Mở…", self._mo_file, rong=4).pack(side="left", padx=1)
|
|
273
|
+
NutPhang(bar, "Lưu", lambda: self._luu_toan_bo(True), rong=4,
|
|
274
|
+
dam=True).pack(side="left", padx=1)
|
|
275
|
+
NutPhang(bar, "↶", self._hoan_tac, rong=2).pack(side="left", padx=1)
|
|
276
|
+
NutPhang(bar, "↷", self._lam_lai, rong=2).pack(side="left", padx=1)
|
|
277
|
+
|
|
278
|
+
tk.Frame(bar, bg=MAU["vien"], width=1).pack(side="left", fill="y",
|
|
279
|
+
padx=8, pady=2)
|
|
280
|
+
self.nut_luoi = NutPhang(bar, "▦ Lưới 10px", None, toggle=True,
|
|
281
|
+
rong=9)
|
|
282
|
+
self.nut_luoi.dat_lenh(self._doi_luoi)
|
|
283
|
+
self.nut_luoi.pack(side="left", padx=1)
|
|
284
|
+
self.nut_luoi.dat_bat(True)
|
|
285
|
+
|
|
286
|
+
# ---- bên phải ----
|
|
287
|
+
self.nut_drawer = NutPhang(bar, "⌨ Triggers", self._dao_drawer,
|
|
288
|
+
toggle=True, rong=10)
|
|
289
|
+
self.nut_drawer.pack(side="right", padx=1)
|
|
290
|
+
NutPhang(bar, "▲ Xuất .py", self._xuat_py, rong=9).pack(
|
|
291
|
+
side="right", padx=1)
|
|
292
|
+
self.nut_build = NutPhang(bar, "📦 Build .exe", self._build_exe,
|
|
293
|
+
rong=9)
|
|
294
|
+
self.nut_build.pack(side="right", padx=1)
|
|
295
|
+
NutPhang(bar, "▶ Xem trước", self._xem_truoc, accent=True,
|
|
296
|
+
rong=10).pack(side="right", padx=(1, 8))
|
|
297
|
+
NutPhang(bar, "?", self._mo_tro_giup, rong=2).pack(side="right",
|
|
298
|
+
padx=1)
|
|
299
|
+
|
|
300
|
+
# ------------------------------------------------------------- thân chính
|
|
301
|
+
def _tao_than(self):
|
|
302
|
+
self.than = tk.Frame(self.root, bg=MAU["nen"])
|
|
303
|
+
self.than.pack(fill="both", expand=True)
|
|
304
|
+
self._tao_palette()
|
|
305
|
+
self._tao_vung_ve()
|
|
306
|
+
self._tao_bang_thuoc_tinh()
|
|
307
|
+
|
|
308
|
+
def _tao_palette(self):
|
|
309
|
+
"""Bảng thành phần bên trái — gợi ý + nhấp để thêm vào giữa."""
|
|
310
|
+
pl = tk.Frame(self.than, bg=MAU["nen_panel"], width=86, padx=6,
|
|
311
|
+
pady=8)
|
|
312
|
+
pl.pack(side="left", fill="y")
|
|
313
|
+
pl.pack_propagate(False)
|
|
314
|
+
tk.Label(pl, text="THÀNH PHẦN", fg=MAU["chu_mo"], bg=MAU["nen_panel"],
|
|
315
|
+
font=font_ui(7, True)).pack(pady=(2, 8))
|
|
316
|
+
for loai in ("label", "button", "input", "checkbox", "text"):
|
|
317
|
+
self._o_palette(pl, loai)
|
|
318
|
+
tk.Label(pl, text="Nhấp để thêm\nvào giữa", fg="#6e6e6e",
|
|
319
|
+
bg=MAU["nen_panel"], font=font_ui(7),
|
|
320
|
+
justify="center").pack(side="bottom", pady=8)
|
|
321
|
+
|
|
322
|
+
def _o_palette(self, cha, loai):
|
|
323
|
+
"""Một ô palette: vẽ mock nhỏ + tên — hover sáng, click thêm."""
|
|
324
|
+
khung = tk.Frame(cha, bg=MAU["nen_panel"], cursor="hand2")
|
|
325
|
+
khung.pack(pady=3, ipadx=2, ipady=2)
|
|
326
|
+
cv = tk.Canvas(khung, width=64, height=40, bg="#ffffff",
|
|
327
|
+
highlightthickness=1, highlightbackground=MAU["vien"])
|
|
328
|
+
cv.pack()
|
|
329
|
+
self._ve_icon(cv, loai)
|
|
330
|
+
tk.Label(khung, text=TEN_HIEN_THI[loai], fg=MAU["chu_mo"],
|
|
331
|
+
bg=MAU["nen_panel"], font=font_ui(7)).pack(pady=(1, 0))
|
|
332
|
+
cv.bind("<Button-1>", lambda e, l=loai: self._them(l))
|
|
333
|
+
cv.bind("<Enter>", lambda e: cv.config(
|
|
334
|
+
highlightbackground=MAU["gt"]))
|
|
335
|
+
cv.bind("<Leave>", lambda e: cv.config(
|
|
336
|
+
highlightbackground=MAU["vien"]))
|
|
337
|
+
|
|
338
|
+
def _ve_icon(self, cv, loai):
|
|
339
|
+
"""Vẽ icon mô phỏng cho ô palette."""
|
|
340
|
+
f = font_ui(8)
|
|
341
|
+
if loai == "label":
|
|
342
|
+
cv.create_text(32, 20, text="Aa", fill="#1f2430", font=f)
|
|
343
|
+
elif loai == "button":
|
|
344
|
+
vien_bo(cv, 10, 11, 54, 29, 6, fill=MAU_APP["gt_mac_dinh"],
|
|
345
|
+
outline="")
|
|
346
|
+
cv.create_text(32, 20, text="OK", fill="#fff", font=f)
|
|
347
|
+
elif loai == "input":
|
|
348
|
+
vien_bo(cv, 8, 13, 56, 27, 5, fill="#ffffff",
|
|
349
|
+
outline=MAU_APP["khung"])
|
|
350
|
+
cv.create_text(14, 20, anchor="w", text="…", fill="#9aa1ab",
|
|
351
|
+
font=f)
|
|
352
|
+
elif loai == "checkbox":
|
|
353
|
+
cv.create_rectangle(11, 12, 23, 24, fill="#ffffff",
|
|
354
|
+
outline="#8a919c")
|
|
355
|
+
cv.create_line(14, 19, 17, 22, 20, 13, fill=MAU_APP["gt_mac_dinh"],
|
|
356
|
+
width=2, capstyle="round", joinstyle="round")
|
|
357
|
+
cv.create_text(29, 18, anchor="w", text="✓", fill="#1f2430",
|
|
358
|
+
font=f)
|
|
359
|
+
else: # text
|
|
360
|
+
cv.create_rectangle(9, 8, 55, 32, fill="#ffffff",
|
|
361
|
+
outline=MAU_APP["khung"])
|
|
362
|
+
for i, y in enumerate((14, 19, 24)):
|
|
363
|
+
cv.create_rectangle(14, y, 14 + (34 if i < 2 else 22), y + 2,
|
|
364
|
+
fill="#e4e8ee", outline="")
|
|
365
|
+
|
|
366
|
+
def _tao_vung_ve(self):
|
|
367
|
+
"""Vùng canvas trung tâm (cuộn được khi cửa sổ thiết kế to)."""
|
|
368
|
+
vung = tk.Frame(self.than, bg=MAU["nen_den"])
|
|
369
|
+
vung.pack(side="left", fill="both", expand=True)
|
|
370
|
+
|
|
371
|
+
self.ban_ve = tk.Canvas(vung, bg=MAU["nen_den"], highlightthickness=0,
|
|
372
|
+
cursor="arrow")
|
|
373
|
+
self.ban_ve.pack(side="left", fill="both", expand=True)
|
|
374
|
+
|
|
375
|
+
# Thanh cuộn tối màu
|
|
376
|
+
self._dinh_dang_ttk()
|
|
377
|
+
sb_y = ttk.Scrollbar(vung, orient="vertical",
|
|
378
|
+
command=self.ban_ve.yview)
|
|
379
|
+
sb_y.pack(side="right", fill="y")
|
|
380
|
+
sb_x = ttk.Scrollbar(vung, orient="horizontal",
|
|
381
|
+
command=self.ban_ve.xview)
|
|
382
|
+
sb_x.pack(side="bottom", fill="x")
|
|
383
|
+
self.ban_ve.configure(xscrollcommand=sb_x.set,
|
|
384
|
+
yscrollcommand=sb_y.set)
|
|
385
|
+
|
|
386
|
+
# ---- các item THƯỜNG TRỰC (không tạo/hủy khi kéo → hết tàn ảnh) ----
|
|
387
|
+
self._vien_chon = self.ban_ve.create_rectangle(
|
|
388
|
+
0, 0, 0, 0, outline=MAU["vien_chon"], width=2, state="hidden")
|
|
389
|
+
self._ds_bam = {}
|
|
390
|
+
for ten in CAC_NUT_BAM:
|
|
391
|
+
self._ds_bam[ten] = self.ban_ve.create_rectangle(
|
|
392
|
+
0, 0, 0, 0, fill=MAU["nen_handle"],
|
|
393
|
+
outline=MAU["vien_chon"], width=1, state="hidden")
|
|
394
|
+
self._vien_hover = self.ban_ve.create_rectangle(
|
|
395
|
+
0, 0, 0, 0, outline=MAU["vien_hover"], state="hidden")
|
|
396
|
+
self._quet_hcn = self.ban_ve.create_rectangle(
|
|
397
|
+
0, 0, 0, 0, fill=MAU["gt"], stipple="gray50",
|
|
398
|
+
outline=MAU["gt"], state="hidden")
|
|
399
|
+
# viên thuốc tọa độ khi kéo
|
|
400
|
+
self._toa_do_nen = self.ban_ve.create_rectangle(
|
|
401
|
+
0, 0, 0, 0, fill="#111418", outline=MAU["gt"], state="hidden")
|
|
402
|
+
self._toa_do_chu = self.ban_ve.create_text(
|
|
403
|
+
0, 0, text="", fill=MAU["chu_sang"], font=font_ui(8),
|
|
404
|
+
state="hidden")
|
|
405
|
+
|
|
406
|
+
# ---- gắn sự kiện chuột ----
|
|
407
|
+
cv = self.ban_ve
|
|
408
|
+
cv.bind("<Button-1>", self._nhan)
|
|
409
|
+
cv.bind("<B1-Motion>", self._keo)
|
|
410
|
+
cv.bind("<ButtonRelease-1>", self._tha)
|
|
411
|
+
cv.bind("<Double-Button-1>", self._nhan_kp)
|
|
412
|
+
cv.bind("<Button-3>", self._menu_phai)
|
|
413
|
+
cv.bind("<Motion>", self._di_chuot)
|
|
414
|
+
cv.bind("<Leave>", lambda e: self._an_toa_do_chuot())
|
|
415
|
+
cv.bind("<Button-1>", lambda e: cv.focus_set(), add="+")
|
|
416
|
+
# cuộn bằng con lăn (bỏ qua khi đang gõ trong editor ô nhập)
|
|
417
|
+
def _lan_doc(e):
|
|
418
|
+
w = self.root.focus_get()
|
|
419
|
+
if w is not None and w.winfo_class() in ("Text", "Entry",
|
|
420
|
+
"TSpinbox", "TEntry"):
|
|
421
|
+
return
|
|
422
|
+
self.ban_ve.yview_scroll(-int(e.delta / 120) * 3, "units")
|
|
423
|
+
|
|
424
|
+
def _lan_ngang(e):
|
|
425
|
+
w = self.root.focus_get()
|
|
426
|
+
if w is not None and w.winfo_class() in ("Text", "Entry",
|
|
427
|
+
"TSpinbox", "TEntry"):
|
|
428
|
+
return
|
|
429
|
+
self.ban_ve.xview_scroll(-int(e.delta / 120) * 3, "units")
|
|
430
|
+
|
|
431
|
+
cv.bind_all("<MouseWheel>", _lan_doc)
|
|
432
|
+
cv.bind_all("<Shift-MouseWheel>", _lan_ngang)
|
|
433
|
+
|
|
434
|
+
def _dinh_dang_ttk(self):
|
|
435
|
+
"""Áp style tối cho widget ttk (scrollbar, progressbar...)."""
|
|
436
|
+
self._phong = ttk.Style()
|
|
437
|
+
try:
|
|
438
|
+
self._phong.theme_use("clam")
|
|
439
|
+
except Exception:
|
|
440
|
+
pass
|
|
441
|
+
self._phong.configure("Vertical.TScrollbar",
|
|
442
|
+
background=MAU["vien"],
|
|
443
|
+
troughcolor=MAU["nen_den"],
|
|
444
|
+
bordercolor=MAU["nen_den"],
|
|
445
|
+
arrowcolor=MAU["chu_mo"],
|
|
446
|
+
arrowsize=11)
|
|
447
|
+
self._phong.configure("Horizontal.TScrollbar",
|
|
448
|
+
background=MAU["vien"],
|
|
449
|
+
troughcolor=MAU["nen_den"],
|
|
450
|
+
bordercolor=MAU["nen_den"],
|
|
451
|
+
arrowcolor=MAU["chu_mo"],
|
|
452
|
+
arrowsize=11)
|
|
453
|
+
|
|
454
|
+
# ------------------------------------------------------------- bảng thuộc tính
|
|
455
|
+
def _tao_bang_thuoc_tinh(self):
|
|
456
|
+
"""Panel phải: thuộc tính cửa sổ HOẶC thành phần đang chọn."""
|
|
457
|
+
self.bang = tk.Frame(self.than, bg=MAU["nen_panel"], width=272)
|
|
458
|
+
self.bang.pack(side="right", fill="y")
|
|
459
|
+
self.bang.pack_propagate(False)
|
|
460
|
+
self.noi_bang = tk.Frame(self.bang, bg=MAU["nen_panel"])
|
|
461
|
+
self.noi_bang.pack(fill="both", expand=True, padx=10, pady=8)
|
|
462
|
+
|
|
463
|
+
def _tao_thanh_trang_thai(self):
|
|
464
|
+
sb = tk.Frame(self.root, bg="#007acc", pady=3, padx=8)
|
|
465
|
+
sb.pack(fill="x", side="bottom")
|
|
466
|
+
f = font_ui(8)
|
|
467
|
+
self.nhan_trang_thai = tk.Label(sb, text="● Sẵn sàng", bg="#007acc",
|
|
468
|
+
fg="#ffffff", font=f)
|
|
469
|
+
self.nhan_trang_thai.pack(side="left")
|
|
470
|
+
self.nhan_toa_do = tk.Label(sb, text="", bg="#007acc", fg="#e8f2fa",
|
|
471
|
+
font=f)
|
|
472
|
+
self.nhan_toa_do.pack(side="left", padx=16)
|
|
473
|
+
self.nhan_chon = tk.Label(sb, text="", bg="#007acc", fg="#e8f2fa",
|
|
474
|
+
font=f)
|
|
475
|
+
self.nhan_chon.pack(side="left")
|
|
476
|
+
self.nhan_thong_tin = tk.Label(
|
|
477
|
+
sb, text="Ctrl+S Lưu · F5 Xem trước · Del Xóa · Ctrl+Z Hoàn tác",
|
|
478
|
+
bg="#007acc", fg="#cfe3f5", font=f)
|
|
479
|
+
self.nhan_thong_tin.pack(side="right")
|
|
480
|
+
|
|
481
|
+
# ------------------------------------------------------------- drawer trigger
|
|
482
|
+
def _tao_drawer_trigger(self, an=True):
|
|
483
|
+
"""Ngăn kéo dưới cùng: mini VS Code cho myapp.triggers.py."""
|
|
484
|
+
self.drawer = tk.Frame(self.root, bg=MAU["nen_panel"], height=320)
|
|
485
|
+
self.drawer.pack_propagate(False)
|
|
486
|
+
|
|
487
|
+
dau = tk.Frame(self.drawer, bg="#2d2d2d", padx=8, pady=3)
|
|
488
|
+
dau.pack(fill="x")
|
|
489
|
+
self.nhan_tab = tk.Label(
|
|
490
|
+
dau, text=" ⌨ " + self._ten_triggers() + " • Python • UTF-8",
|
|
491
|
+
bg="#2d2d2d", fg=MAU["chu_sang"], font=font_ui(8),
|
|
492
|
+
anchor="w")
|
|
493
|
+
self.nhan_tab.pack(side="left")
|
|
494
|
+
NutPhang(dau, "✕", self._an_drawer, rong=2, co=8).pack(
|
|
495
|
+
side="right")
|
|
496
|
+
NutPhang(dau, "Chèn mẫu ▾", self._chen_mau, rong=9, co=8).pack(
|
|
497
|
+
side="right", padx=4)
|
|
498
|
+
NutPhang(dau, "💾 Áp dụng & Lưu", lambda: self._ghi_triggers(True),
|
|
499
|
+
rong=14, co=8).pack(side="right", padx=4)
|
|
500
|
+
|
|
501
|
+
self.editor = TrinhSoanThao(self.drawer)
|
|
502
|
+
self.editor.pack(fill="both", expand=True, padx=8, pady=(0, 8))
|
|
503
|
+
self._doc_triggers()
|
|
504
|
+
|
|
505
|
+
# Ctrl+S trong editor cũng lưu
|
|
506
|
+
self.editor.o.bind("<Control-s>",
|
|
507
|
+
lambda e: (self._luu_toan_bo(True), "break")[1])
|
|
508
|
+
|
|
509
|
+
def _ten_triggers(self):
|
|
510
|
+
if self.duong_triggers:
|
|
511
|
+
return self.duong_triggers.name
|
|
512
|
+
return "triggers.py"
|
|
513
|
+
|
|
514
|
+
# ================================================================== VÒNG ĐỜI
|
|
515
|
+
def chay(self):
|
|
516
|
+
"""Vào mainloop của Studio."""
|
|
517
|
+
self.root.mainloop()
|
|
518
|
+
|
|
519
|
+
def _khi_dong(self):
|
|
520
|
+
"""Đóng Studio — nhắc lưu nếu còn thay đổi."""
|
|
521
|
+
if self.dirty:
|
|
522
|
+
dap = messagebox.askyesnocancel(
|
|
523
|
+
"Thoát FlexGUI Studio",
|
|
524
|
+
"Thiết kế chưa lưu. Lưu trước khi thoát?")
|
|
525
|
+
if dap is None:
|
|
526
|
+
return
|
|
527
|
+
if dap and not self._luu_toan_bo(im_lang=True):
|
|
528
|
+
return
|
|
529
|
+
try:
|
|
530
|
+
self.root.destroy()
|
|
531
|
+
except Exception:
|
|
532
|
+
pass
|
|
533
|
+
|
|
534
|
+
# ==================================================================
|
|
535
|
+
# DỮ LIỆU THIẾT KẾ
|
|
536
|
+
# ==================================================================
|
|
537
|
+
def _thiet_ke_moi(self):
|
|
538
|
+
"""Thiết kế trống — giao diện trắng mặc định 860×560."""
|
|
539
|
+
return {
|
|
540
|
+
"window": {"title": "Ứng dụng FlexGUI", "width": 860,
|
|
541
|
+
"height": 560, "bg": MAU_APP["nen_mac_dinh"]},
|
|
542
|
+
"components": [],
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
def _doc_json(self):
|
|
546
|
+
"""Nạp myapp.flexgui.json (nếu có)."""
|
|
547
|
+
if self.duong_json and Path(self.duong_json).exists():
|
|
548
|
+
try:
|
|
549
|
+
with open(self.duong_json, "r", encoding="utf-8") as f:
|
|
550
|
+
return json.load(f)
|
|
551
|
+
except Exception:
|
|
552
|
+
messagebox.showwarning(
|
|
553
|
+
"Không đọc được file thiết kế",
|
|
554
|
+
f"{self.duong_json}\n\nMở một thiết kế trống mới.")
|
|
555
|
+
return None
|
|
556
|
+
|
|
557
|
+
def _danh_sach(self):
|
|
558
|
+
return self.design["components"]
|
|
559
|
+
|
|
560
|
+
def _comp(self, cid):
|
|
561
|
+
for d in self._danh_sach():
|
|
562
|
+
if d["id"] == cid:
|
|
563
|
+
return d
|
|
564
|
+
return None
|
|
565
|
+
|
|
566
|
+
def _xin_id(self):
|
|
567
|
+
"""ID mới — tái sử dụng ID nhỏ nhất vừa bị xóa (như runtime)."""
|
|
568
|
+
if self._id_tu_do:
|
|
569
|
+
return heapq.heappop(self._id_tu_do)
|
|
570
|
+
self._id_tiep_theo += 1
|
|
571
|
+
return self._id_tiep_theo - 1
|
|
572
|
+
|
|
573
|
+
def _moi(self):
|
|
574
|
+
"""Thiết kế mới (hỏi trước khi xóa)."""
|
|
575
|
+
if self.dirty:
|
|
576
|
+
if not messagebox.askyesno(
|
|
577
|
+
"Thiết kế mới", "Bỏ thay đổi chưa lưu và tạo mới?"):
|
|
578
|
+
return
|
|
579
|
+
self.design = self._thiet_ke_moi()
|
|
580
|
+
self._id_tiep_theo, self._id_tu_do = 1, []
|
|
581
|
+
self.chon = []
|
|
582
|
+
self.undo_ds.clear()
|
|
583
|
+
self.redo_ds.clear()
|
|
584
|
+
self.dirty = False
|
|
585
|
+
self._ve_lai()
|
|
586
|
+
self._lam_moi_bang_thuoc_tinh()
|
|
587
|
+
|
|
588
|
+
def _mo_file(self):
|
|
589
|
+
"""Mở file .flexgui.json khác."""
|
|
590
|
+
duong = filedialog.askopenfilename(
|
|
591
|
+
title="Mở thiết kế FlexGUI",
|
|
592
|
+
filetypes=[("Thiết kế FlexGUI", "*.flexgui.json"),
|
|
593
|
+
("Tất cả", "*.*")])
|
|
594
|
+
if not duong:
|
|
595
|
+
return
|
|
596
|
+
try:
|
|
597
|
+
with open(duong, "r", encoding="utf-8") as f:
|
|
598
|
+
self.design = json.load(f)
|
|
599
|
+
except Exception:
|
|
600
|
+
messagebox.showerror("Lỗi", "Không đọc được file:\n"
|
|
601
|
+
+ traceback.format_exc())
|
|
602
|
+
return
|
|
603
|
+
self.duong_json = Path(duong)
|
|
604
|
+
# file trigger đi kèm: cùng tên, đuôi .triggers.py
|
|
605
|
+
self.duong_triggers = self.duong_json.parent / (
|
|
606
|
+
self.duong_json.name[:-len(".flexgui.json")] + ".triggers.py")
|
|
607
|
+
ids = [c["id"] for c in self._danh_sach()]
|
|
608
|
+
self._id_tiep_theo = (max(ids) + 1) if ids else 1
|
|
609
|
+
self._id_tu_do = []
|
|
610
|
+
self.chon = []
|
|
611
|
+
self.undo_ds.clear()
|
|
612
|
+
self.redo_ds.clear()
|
|
613
|
+
self.dirty = False
|
|
614
|
+
self.nhan_ten_file.config(text="· " + self.duong_json.name)
|
|
615
|
+
self.nhan_tab.config(text=" ⌨ " + self._ten_triggers()
|
|
616
|
+
+ " • Python • UTF-8")
|
|
617
|
+
self._doc_triggers()
|
|
618
|
+
self._ve_lai()
|
|
619
|
+
self._lam_moi_bang_thuoc_tinh()
|
|
620
|
+
|
|
621
|
+
# ==================================================================
|
|
622
|
+
# HỆ TOẠ ĐỘ: canvas <-> thiết kế
|
|
623
|
+
# ==================================================================
|
|
624
|
+
def _khung(self):
|
|
625
|
+
"""Kích thước vùng client của cửa sổ thiết kế."""
|
|
626
|
+
return self.design["window"].get("width", 860), \
|
|
627
|
+
self.design["window"].get("height", 560)
|
|
628
|
+
|
|
629
|
+
def _goc_client(self):
|
|
630
|
+
"""Góc trái-trên vùng client TRÊN CANVAS."""
|
|
631
|
+
return LE, LE + H_TIEU_DE
|
|
632
|
+
|
|
633
|
+
def _sang_canvas(self, x, y):
|
|
634
|
+
gx, gy = self._goc_client()
|
|
635
|
+
return gx + x, gy + y
|
|
636
|
+
|
|
637
|
+
def _sang_thiet_ke(self, cx, cy):
|
|
638
|
+
gx, gy = self._goc_client()
|
|
639
|
+
return cx - gx, cy - gy
|
|
640
|
+
|
|
641
|
+
def _snap(self, v):
|
|
642
|
+
if self.luoi_bat:
|
|
643
|
+
return int(round(v / self.buoc_luoi) * self.buoc_luoi)
|
|
644
|
+
return int(v)
|
|
645
|
+
|
|
646
|
+
# ==================================================================
|
|
647
|
+
# VẼ
|
|
648
|
+
# ==================================================================
|
|
649
|
+
def _ve_lai(self):
|
|
650
|
+
"""Vẽ lại toàn bộ (khung cửa sổ + lưới + mọi thành phần)."""
|
|
651
|
+
cv = self.ban_ve
|
|
652
|
+
cv.delete("cua_so", "luoi", "comp", "goi_y")
|
|
653
|
+
self._ve_cua_so()
|
|
654
|
+
self._ve_luoi()
|
|
655
|
+
for d in self._danh_sach():
|
|
656
|
+
self._ve_comp(d)
|
|
657
|
+
if not self._danh_sach():
|
|
658
|
+
self._ve_goi_y()
|
|
659
|
+
self._cap_nhat_vien_chon()
|
|
660
|
+
self._cap_nhat_trang_thai()
|
|
661
|
+
|
|
662
|
+
def _ve_cua_so(self):
|
|
663
|
+
"""Khung cửa sổ giả: bóng đổ + thanh tiêu đề + nút cửa sổ."""
|
|
664
|
+
cv = self.ban_ve
|
|
665
|
+
w, h = self._khung()
|
|
666
|
+
nen = self.design["window"].get("bg") or MAU_APP["nen_mac_dinh"]
|
|
667
|
+
tieu_de = self.design["window"].get("title", "FlexGUI")
|
|
668
|
+
x0, y0 = LE, LE
|
|
669
|
+
x1, y1 = LE + w, LE + H_TIEU_DE + h
|
|
670
|
+
# bóng đổ 2 lớp
|
|
671
|
+
cv.create_rectangle(x0 + 7, y0 + 7, x1 + 7, y1 + 7,
|
|
672
|
+
fill=MAU["soi_bong_1"], outline="", tags="cua_so")
|
|
673
|
+
cv.create_rectangle(x0 + 3, y0 + 3, x1 + 3, y1 + 3,
|
|
674
|
+
fill=MAU["soi_bong_2"], outline="", tags="cua_so")
|
|
675
|
+
# thân
|
|
676
|
+
cv.create_rectangle(x0, y0, x1, y1, fill=nen,
|
|
677
|
+
outline=MAU["khung_so"], tags="cua_so")
|
|
678
|
+
# thanh tiêu đề
|
|
679
|
+
cv.create_rectangle(x0, y0, x1, y0 + H_TIEU_DE,
|
|
680
|
+
fill=MAU["thanh_tieu_de"], outline="",
|
|
681
|
+
tags="cua_so")
|
|
682
|
+
cv.create_line(x0, y0 + H_TIEU_DE, x1, y0 + H_TIEU_DE,
|
|
683
|
+
fill="#e3e6ea", tags="cua_so")
|
|
684
|
+
vien_bo(cv, x0 + 12, y0 + 9, x0 + 24, y0 + 21, 4,
|
|
685
|
+
fill=MAU_APP["gt_mac_dinh"], outline="", tags="cua_so")
|
|
686
|
+
cv.create_text(x0 + 18, y0 + 15, text="◈", fill="#ffffff",
|
|
687
|
+
font=font_ui(8, True), tags="cua_so")
|
|
688
|
+
cv.create_text(x0 + 34, y0 + H_TIEU_DE / 2, text=tieu_de,
|
|
689
|
+
anchor="w", fill="#3b4048", font=font_ui(9, True),
|
|
690
|
+
tags="cua_so")
|
|
691
|
+
# nút cửa sổ (Windows style)
|
|
692
|
+
for i, chu in enumerate(("—", "☐", "✕")):
|
|
693
|
+
nx = x1 - 96 + i * 32
|
|
694
|
+
cv.create_rectangle(nx, y0, nx + 32, y0 + H_TIEU_DE,
|
|
695
|
+
fill=MAU["thanh_tieu_de"], outline="",
|
|
696
|
+
tags="cua_so")
|
|
697
|
+
cv.create_text(nx + 16, y0 + H_TIEU_DE / 2, text=chu,
|
|
698
|
+
fill="#8a919c", font=font_ui(8), tags="cua_so")
|
|
699
|
+
# vùng cuộn theo kích thước cửa sổ
|
|
700
|
+
cv.configure(scrollregion=(0, 0, x1 + LE, y1 + LE))
|
|
701
|
+
# cuộn về gốc
|
|
702
|
+
cv.xview_moveto(0)
|
|
703
|
+
cv.yview_moveto(0)
|
|
704
|
+
|
|
705
|
+
def _ve_luoi(self):
|
|
706
|
+
"""Lưới điểm gián đoạn trong vùng client (màu tương phản nền)."""
|
|
707
|
+
cv = self.ban_ve
|
|
708
|
+
w, h = self._khung()
|
|
709
|
+
gx, gy = self._goc_client()
|
|
710
|
+
mau = mau_luoi(self.design["window"].get("bg")
|
|
711
|
+
or MAU_APP["nen_mac_dinh"])
|
|
712
|
+
buoc = self.buoc_luoi
|
|
713
|
+
x = gx + buoc
|
|
714
|
+
while x < gx + w:
|
|
715
|
+
cv.create_line(x, gy, x, gy + h, fill=mau, tags="luoi")
|
|
716
|
+
x += buoc * 2 # giãn đôi → nhìn nhẹ hơn
|
|
717
|
+
y = gy + buoc
|
|
718
|
+
while y < gy + h:
|
|
719
|
+
cv.create_line(gx, y, gx + w, y, fill=mau, tags="luoi")
|
|
720
|
+
y += buoc * 2
|
|
721
|
+
|
|
722
|
+
def _ve_goi_y(self):
|
|
723
|
+
"""Gợi ý giữa vùng trống khi chưa có thành phần nào."""
|
|
724
|
+
cv = self.ban_ve
|
|
725
|
+
w, h = self._khung()
|
|
726
|
+
gx, gy = self._goc_client()
|
|
727
|
+
cv.create_text(
|
|
728
|
+
gx + w / 2, gy + h / 2 - 12,
|
|
729
|
+
text="Giao diện trống — nhấp một thành phần ở bảng bên trái\n"
|
|
730
|
+
"để thêm vào đây (kéo thả, snap lưới, resize 8 núm)",
|
|
731
|
+
fill="#aeb4bf", font=font_ui(11), justify="center",
|
|
732
|
+
tags="goi_y")
|
|
733
|
+
|
|
734
|
+
def _doi_luoi(self):
|
|
735
|
+
"""Bật/tắt lưới + snap (nút ▦ trên toolbar)."""
|
|
736
|
+
self.luoi_bat = not self.luoi_bat
|
|
737
|
+
self.nut_luoi.dat_bat(self.luoi_bat)
|
|
738
|
+
if self.luoi_bat:
|
|
739
|
+
self._ve_lai()
|
|
740
|
+
else:
|
|
741
|
+
self.ban_ve.delete("luoi")
|
|
742
|
+
self._cap_nhat_trang_thai(
|
|
743
|
+
"● Tắt lưới — kéo thả hoàn toàn tự do")
|
|
744
|
+
|
|
745
|
+
def _ve_comp(self, d):
|
|
746
|
+
"""Vẽ MỘT thành phần theo đúng loại (đồ họa mô phỏng hiện đại)."""
|
|
747
|
+
cv = self.ban_ve
|
|
748
|
+
X, Y = self._sang_canvas(d["x"], d["y"])
|
|
749
|
+
w, h = d.get("w") or 100, d.get("h") or 30
|
|
750
|
+
tag = ("comp", f"c{d['id']}")
|
|
751
|
+
nen_cua_so = self.design["window"].get("bg") or \
|
|
752
|
+
MAU_APP["nen_mac_dinh"]
|
|
753
|
+
co = d.get("font_size") or 10
|
|
754
|
+
mau_chu = d.get("color") or "#1f2430"
|
|
755
|
+
|
|
756
|
+
# hit-rect trong suốt (đón chuột) — luôn nằm dưới cùng của comp
|
|
757
|
+
cv.create_rectangle(X, Y, X + w, Y + h, fill=nen_cua_so,
|
|
758
|
+
outline="", tags=tag)
|
|
759
|
+
|
|
760
|
+
if d["loai"] == "label":
|
|
761
|
+
cv.create_text(X, Y + h / 2, anchor="w", text=d.get("text") or "",
|
|
762
|
+
fill=mau_chu, font=font_ui(co), tags=tag)
|
|
763
|
+
|
|
764
|
+
elif d["loai"] == "button":
|
|
765
|
+
mau_nen = d.get("bg") or MAU_APP["gt_mac_dinh"]
|
|
766
|
+
vien_bo(cv, X, Y, X + w, Y + h, 8, fill=mau_nen, outline="",
|
|
767
|
+
tags=tag)
|
|
768
|
+
cv.create_text(X + w / 2, Y + h / 2, text=d.get("text") or "",
|
|
769
|
+
fill="#ffffff" if not d.get("color") else mau_chu,
|
|
770
|
+
font=font_ui(co), tags=tag)
|
|
771
|
+
|
|
772
|
+
elif d["loai"] == "input":
|
|
773
|
+
vien_bo(cv, X, Y, X + w, Y + h, 6, fill=d.get("bg") or "#ffffff",
|
|
774
|
+
outline=d.get("bg") or MAU_APP["khung"], tags=tag)
|
|
775
|
+
chu = (d.get("text") or "").strip()
|
|
776
|
+
cv.create_text(X + 8, Y + h / 2, anchor="w",
|
|
777
|
+
text=chu or "Ô nhập…",
|
|
778
|
+
fill=mau_chu if chu else "#b8bec8",
|
|
779
|
+
font=font_ui(co), tags=tag)
|
|
780
|
+
|
|
781
|
+
elif d["loai"] == "checkbox":
|
|
782
|
+
c = 16
|
|
783
|
+
cy = Y + (h - c) / 2
|
|
784
|
+
cv.create_rectangle(X, cy, X + c, cy + c, fill="#ffffff",
|
|
785
|
+
outline="#8a919c", tags=tag)
|
|
786
|
+
if d.get("checked"):
|
|
787
|
+
cv.create_rectangle(X + 2, cy + 2, X + c - 2, cy + c - 2,
|
|
788
|
+
fill=MAU_APP["gt_mac_dinh"], outline="",
|
|
789
|
+
tags=tag)
|
|
790
|
+
cv.create_line(X + 4, cy + 8, X + 7, cy + 11, X + 12,
|
|
791
|
+
cy + 4, fill="#ffffff", width=2,
|
|
792
|
+
capstyle="round", joinstyle="round",
|
|
793
|
+
tags=tag)
|
|
794
|
+
cv.create_text(X + c + 8, Y + h / 2, anchor="w",
|
|
795
|
+
text=d.get("text") or "", fill=mau_chu,
|
|
796
|
+
font=font_ui(co), tags=tag)
|
|
797
|
+
|
|
798
|
+
elif d["loai"] == "text":
|
|
799
|
+
cv.create_rectangle(X, Y, X + w, Y + h,
|
|
800
|
+
fill=d.get("bg") or "#ffffff",
|
|
801
|
+
outline=MAU_APP["khung"], tags=tag)
|
|
802
|
+
# thanh cuộn giả bên phải
|
|
803
|
+
cv.create_rectangle(X + w - 8, Y + 3, X + w - 3, Y + h - 3,
|
|
804
|
+
fill="#eef1f5", outline="", tags=tag)
|
|
805
|
+
noi_dung = (d.get("text") or "").strip()
|
|
806
|
+
if noi_dung:
|
|
807
|
+
for i, dong in enumerate(noi_dung.split("\n")[:3]):
|
|
808
|
+
cv.create_text(X + 8, Y + 8 + i * 16, anchor="w",
|
|
809
|
+
text=dong[:40], fill=mau_chu,
|
|
810
|
+
font=font_ui(co), tags=tag)
|
|
811
|
+
else:
|
|
812
|
+
for i in range(3):
|
|
813
|
+
cv.create_rectangle(X + 8, Y + 10 + i * 16,
|
|
814
|
+
X + 8 + (w - 34 if i < 2 else 20),
|
|
815
|
+
Y + 14 + i * 16,
|
|
816
|
+
fill="#e4e8ee", outline="", tags=tag)
|
|
817
|
+
|
|
818
|
+
# huy hiệu trạng thái đặc biệt
|
|
819
|
+
if not d.get("visible", True):
|
|
820
|
+
cv.create_rectangle(X, Y, X + w, Y + h, fill="#5b21b6",
|
|
821
|
+
stipple="gray25", outline="#8b5cf6",
|
|
822
|
+
dash=(4, 3), tags=tag)
|
|
823
|
+
vien_bo(cv, X + w - 44, Y + 4, X + w - 4, Y + 20, 5,
|
|
824
|
+
fill="#8b5cf6", outline="", tags=tag)
|
|
825
|
+
cv.create_text(X + w - 24, Y + 12, text="ẨN", fill="#ffffff",
|
|
826
|
+
font=font_ui(7, True), tags=tag)
|
|
827
|
+
if not d.get("enabled", True):
|
|
828
|
+
vien_bo(cv, X + 4, Y + h - 20, X + 40, Y + h - 4, 5,
|
|
829
|
+
fill="#6b7280", outline="", tags=tag)
|
|
830
|
+
cv.create_text(X + 22, Y + h - 12, text="KHOÁ", fill="#ffffff",
|
|
831
|
+
font=font_ui(7, True), tags=tag)
|
|
832
|
+
|
|
833
|
+
def _ve_comp_tu_dong(self, cid):
|
|
834
|
+
"""Vẽ lại 1 comp, giữ đúng thứ tự chồng lớp."""
|
|
835
|
+
d = self._comp(cid)
|
|
836
|
+
if not d:
|
|
837
|
+
return
|
|
838
|
+
self.ban_ve.delete(f"c{cid}")
|
|
839
|
+
self._ve_comp(d)
|
|
840
|
+
# nâng lại các comp nằm TRÊN nó trong z-order
|
|
841
|
+
ds = self._danh_sach()
|
|
842
|
+
vi_tri = next(i for i, c in enumerate(ds) if c["id"] == cid)
|
|
843
|
+
for c in ds[vi_tri + 1:]:
|
|
844
|
+
self.ban_ve.tag_raise(f"c{c['id']}")
|
|
845
|
+
|
|
846
|
+
# ----------------------------------------------------- viền chọn + núm
|
|
847
|
+
def _cap_nhat_vien_chon(self):
|
|
848
|
+
"""Cập nhật viền chọn + 8 núm resize (chỉ coords — không tạo mới)."""
|
|
849
|
+
cv = self.ban_ve
|
|
850
|
+
cv.delete("vien_phu")
|
|
851
|
+
if not self.chon:
|
|
852
|
+
cv.itemconfigure(self._vien_chon, state="hidden")
|
|
853
|
+
for it in self._ds_bam.values():
|
|
854
|
+
cv.itemconfigure(it, state="hidden")
|
|
855
|
+
return
|
|
856
|
+
chinh = self._comp(self.chon[-1])
|
|
857
|
+
if chinh is None:
|
|
858
|
+
cv.itemconfigure(self._vien_chon, state="hidden")
|
|
859
|
+
return
|
|
860
|
+
# viền phụ cho các comp chọn thêm (trừ comp chính)
|
|
861
|
+
for cid in self.chon[:-1]:
|
|
862
|
+
d = self._comp(cid)
|
|
863
|
+
if d:
|
|
864
|
+
a, b = self._sang_canvas(d["x"], d["y"])
|
|
865
|
+
r = cv.create_rectangle(
|
|
866
|
+
a - 1, b - 1, a + (d.get("w") or 100) + 1,
|
|
867
|
+
b + (d.get("h") or 30) + 1,
|
|
868
|
+
outline=MAU["vien_chon"], width=1, tags="vien_phu")
|
|
869
|
+
cv.tag_lower(r, self._vien_chon)
|
|
870
|
+
X, Y = self._sang_canvas(chinh["x"], chinh["y"])
|
|
871
|
+
w, h = chinh.get("w") or 100, chinh.get("h") or 30
|
|
872
|
+
cv.coords(self._vien_chon, X - 1, Y - 1, X + w + 1, Y + h + 1)
|
|
873
|
+
cv.itemconfigure(self._vien_chon, state="normal")
|
|
874
|
+
cv.tag_raise(self._vien_chon)
|
|
875
|
+
self._dat_bam(X, Y, w, h)
|
|
876
|
+
self._cap_nhat_chon_trang_thai()
|
|
877
|
+
|
|
878
|
+
def _dat_bam(self, X, Y, w, h):
|
|
879
|
+
"""Đặt 8 núm quanh hình chữ nhật (canvas coords)."""
|
|
880
|
+
cv = self.ban_ve
|
|
881
|
+
r = 4 # nửa kích thước núm
|
|
882
|
+
vi_tri = {
|
|
883
|
+
"nw": (X, Y), "n": (X + w / 2, Y), "ne": (X + w, Y),
|
|
884
|
+
"e": (X + w, Y + h / 2), "se": (X + w, Y + h),
|
|
885
|
+
"s": (X + w / 2, Y + h), "sw": (X, Y + h), "w": (X, Y + h / 2),
|
|
886
|
+
}
|
|
887
|
+
for ten, (x, y) in vi_tri.items():
|
|
888
|
+
it = self._ds_bam[ten]
|
|
889
|
+
cv.coords(it, x - r, y - r, x + r, y + r)
|
|
890
|
+
cv.itemconfigure(it, state="normal")
|
|
891
|
+
cv.tag_raise(it)
|
|
892
|
+
|
|
893
|
+
def _an_bam(self):
|
|
894
|
+
for it in self._ds_bam.values():
|
|
895
|
+
self.ban_ve.itemconfigure(it, state="hidden")
|
|
896
|
+
|
|
897
|
+
# ----------------------------------------------------- trạng thái / toast
|
|
898
|
+
def _cap_nhat_trang_thai(self, chu=None):
|
|
899
|
+
if chu is not None:
|
|
900
|
+
self.nhan_trang_thai.config(text=chu)
|
|
901
|
+
self.nhan_thong_tin.config(
|
|
902
|
+
text=f"{len(self._danh_sach())} thành phần · Lưới "
|
|
903
|
+
f"{'Bật' if self.luoi_bat else 'Tắt'} {self.buoc_luoi}px")
|
|
904
|
+
|
|
905
|
+
def _cap_nhat_chon_trang_thai(self):
|
|
906
|
+
if not self.chon:
|
|
907
|
+
self.nhan_chon.config(text="")
|
|
908
|
+
return
|
|
909
|
+
if len(self.chon) == 1:
|
|
910
|
+
d = self._comp(self.chon[0])
|
|
911
|
+
self.nhan_chon.config(
|
|
912
|
+
text=f"Đang chọn: {TEN_HIEN_THI[d['loai']]} #{d['id']}")
|
|
913
|
+
else:
|
|
914
|
+
self.nhan_chon.config(text=f"Đang chọn: {len(self.chon)} thành phần")
|
|
915
|
+
|
|
916
|
+
def _toast(self, chu, mau="#2d2d30"):
|
|
917
|
+
"""Thông báo nổi dưới đáy canvas (tự ẩn sau 2 giây)."""
|
|
918
|
+
if getattr(self, "_toast_phu", None) is not None:
|
|
919
|
+
try:
|
|
920
|
+
self._toast_phu.destroy()
|
|
921
|
+
except Exception:
|
|
922
|
+
pass
|
|
923
|
+
phu = tk.Frame(self.ban_ve, bg=mau, highlightthickness=1,
|
|
924
|
+
highlightbackground=MAU["vien"])
|
|
925
|
+
tk.Label(phu, text=chu, bg=mau, fg=MAU["chu_sang"],
|
|
926
|
+
font=font_ui(9), padx=14, pady=6).pack()
|
|
927
|
+
phu.place(relx=0.5, rely=1.0, y=-46, anchor="s")
|
|
928
|
+
self._toast_phu = phu
|
|
929
|
+
self.root.after(2000, lambda: phu.destroy()
|
|
930
|
+
if phu.winfo_exists() else None)
|
|
931
|
+
|
|
932
|
+
# ==================================================================
|
|
933
|
+
# TƯƠNG TÁC CHUỘT
|
|
934
|
+
# ==================================================================
|
|
935
|
+
def _tim_comp_tai(self, x, y):
|
|
936
|
+
"""Comp chứa điểm canvas (x, y) — duyệt từ trên xuống."""
|
|
937
|
+
for d in reversed(self._danh_sach()):
|
|
938
|
+
a, b = self._sang_canvas(d["x"], d["y"])
|
|
939
|
+
if a <= x <= a + (d.get("w") or 100) and \
|
|
940
|
+
b <= y <= b + (d.get("h") or 30):
|
|
941
|
+
return d
|
|
942
|
+
return None
|
|
943
|
+
|
|
944
|
+
def _bam_tai(self, x, y):
|
|
945
|
+
"""Tên núm resize tại điểm (nếu có) — đếm cả vùng đệm 6px."""
|
|
946
|
+
if not self.chon:
|
|
947
|
+
return None
|
|
948
|
+
chinh = self._comp(self.chon[-1])
|
|
949
|
+
if not chinh:
|
|
950
|
+
return None
|
|
951
|
+
X, Y = self._sang_canvas(chinh["x"], chinh["y"])
|
|
952
|
+
w, h = chinh.get("w") or 100, chinh.get("h") or 30
|
|
953
|
+
tam = {"nw": (X, Y), "n": (X + w / 2, Y), "ne": (X + w, Y),
|
|
954
|
+
"e": (X + w, Y + h / 2), "se": (X + w, Y + h),
|
|
955
|
+
"s": (X + w / 2, Y + h), "sw": (X, Y + h), "w": (X, Y + h / 2)}
|
|
956
|
+
for ten, (cx, cy) in tam.items():
|
|
957
|
+
if abs(x - cx) <= 7 and abs(y - cy) <= 7:
|
|
958
|
+
return ten
|
|
959
|
+
return None
|
|
960
|
+
|
|
961
|
+
def _nhan(self, e):
|
|
962
|
+
"""Nhấn chuột trái: chọn / bắt đầu kéo / resize / quét vùng."""
|
|
963
|
+
cv = self.ban_ve
|
|
964
|
+
cv.focus_set()
|
|
965
|
+
self._nhan_x, self._nhan_y = e.x, e.y
|
|
966
|
+
self._dda_x = self._dda_y = 0
|
|
967
|
+
|
|
968
|
+
bam = self._bam_tai(e.x, e.y)
|
|
969
|
+
if bam:
|
|
970
|
+
self._trang_thai = "bam"
|
|
971
|
+
chinh = self._comp(self.chon[-1])
|
|
972
|
+
self._goc_kich_thuoc = (chinh["x"], chinh["y"],
|
|
973
|
+
chinh.get("w") or 100,
|
|
974
|
+
chinh.get("h") or 30)
|
|
975
|
+
self._bam_ten = bam
|
|
976
|
+
self._chup_truoc_khi_sua() # undo resize = hình gốc
|
|
977
|
+
cv.configure(cursor="fleur")
|
|
978
|
+
return
|
|
979
|
+
|
|
980
|
+
d = self._tim_comp_tai(e.x, e.y)
|
|
981
|
+
if d is not None:
|
|
982
|
+
them = e.state & 0x0004 # Ctrl
|
|
983
|
+
them_hoac = e.state & 0x0001 # Shift
|
|
984
|
+
if them or them_hoac:
|
|
985
|
+
if d["id"] in self.chon:
|
|
986
|
+
self.chon.remove(d["id"])
|
|
987
|
+
else:
|
|
988
|
+
self.chon.append(d["id"])
|
|
989
|
+
else:
|
|
990
|
+
if d["id"] in self.chon:
|
|
991
|
+
self.chon.remove(d["id"])
|
|
992
|
+
self.chon.append(d["id"])
|
|
993
|
+
self._cap_nhat_vien_chon()
|
|
994
|
+
# bắt đầu kéo cả cụm
|
|
995
|
+
self._trang_thai = "keo"
|
|
996
|
+
self._goc = {cid: (self._comp(cid)["x"], self._comp(cid)["y"])
|
|
997
|
+
for cid in self.chon}
|
|
998
|
+
self._chup_truoc_khi_sua()
|
|
999
|
+
cv.configure(cursor="fleur")
|
|
1000
|
+
return
|
|
1001
|
+
|
|
1002
|
+
# vùng trống → quét vùng chọn
|
|
1003
|
+
if not (e.state & (0x0004 | 0x0001)):
|
|
1004
|
+
self.chon = []
|
|
1005
|
+
self._cap_nhat_vien_chon()
|
|
1006
|
+
self._chon_truoc_quet = tuple(self.chon)
|
|
1007
|
+
self._trang_thai = "quet"
|
|
1008
|
+
cv.itemconfigure(self._quet_hcn, state="normal")
|
|
1009
|
+
cv.coords(self._quet_hcn, e.x, e.y, e.x, e.y)
|
|
1010
|
+
cv.configure(cursor="crosshair")
|
|
1011
|
+
|
|
1012
|
+
def _keo(self, e):
|
|
1013
|
+
"""Kéo chuột: di chuyển cụm / resize / quét vùng."""
|
|
1014
|
+
cv = self.ban_ve
|
|
1015
|
+
self._chuot_x, self._chuot_y = e.x, e.y
|
|
1016
|
+
if self._trang_thai == "keo":
|
|
1017
|
+
dx = e.x - self._nhan_x
|
|
1018
|
+
dy = e.y - self._nhan_y
|
|
1019
|
+
if not self.chon:
|
|
1020
|
+
return
|
|
1021
|
+
chinh = self._comp(self.chon[-1])
|
|
1022
|
+
gx, gy = self._goc.get(chinh["id"], (chinh["x"], chinh["y"]))
|
|
1023
|
+
nx = self._snap(gx + dx)
|
|
1024
|
+
ny = self._snap(gy + dy)
|
|
1025
|
+
ddx, ddy = nx - gx, ny - gy
|
|
1026
|
+
for cid in self.chon:
|
|
1027
|
+
d = self._comp(cid)
|
|
1028
|
+
ox, oy = self._goc.get(cid, (d["x"], d["y"]))
|
|
1029
|
+
d["x"], d["y"] = ox + ddx, oy + ddy
|
|
1030
|
+
# di chuyển item canvas MƯỢT (không vẽ lại → hết tàn ảnh)
|
|
1031
|
+
cv.move(f"c{cid}", ddx - self._dda_x, ddy - self._dda_y)
|
|
1032
|
+
self._dda_x, self._dda_y = ddx, ddy
|
|
1033
|
+
self._cap_nhat_vien_chon()
|
|
1034
|
+
self._hien_toa_do(nx, ny)
|
|
1035
|
+
self.dirty = True
|
|
1036
|
+
|
|
1037
|
+
elif self._trang_thai == "bam":
|
|
1038
|
+
self._keo_bam(e)
|
|
1039
|
+
|
|
1040
|
+
elif self._trang_thai == "quet":
|
|
1041
|
+
cv.coords(self._quet_hcn, self._nhan_x, self._nhan_y,
|
|
1042
|
+
e.x, e.y)
|
|
1043
|
+
x0, x1 = sorted((self._nhan_x, e.x))
|
|
1044
|
+
y0, y1 = sorted((self._nhan_y, e.y))
|
|
1045
|
+
self.chon = list(self._chon_truoc_quet)
|
|
1046
|
+
for d in self._danh_sach():
|
|
1047
|
+
a, b = self._sang_canvas(d["x"], d["y"])
|
|
1048
|
+
if a <= x1 and a + (d.get("w") or 100) >= x0 and \
|
|
1049
|
+
b <= y1 and b + (d.get("h") or 30) >= y0:
|
|
1050
|
+
if d["id"] not in self.chon:
|
|
1051
|
+
self.chon.append(d["id"])
|
|
1052
|
+
self._cap_nhat_vien_chon()
|
|
1053
|
+
|
|
1054
|
+
def _keo_bam(self, e):
|
|
1055
|
+
"""Kéo núm resize — tính từ hình gốc, snap biên, cỡ tối thiểu."""
|
|
1056
|
+
cv = self.ban_ve
|
|
1057
|
+
self._chuot_x, self._chuot_y = e.x, e.y
|
|
1058
|
+
cid = self.chon[-1]
|
|
1059
|
+
d = self._comp(cid)
|
|
1060
|
+
gx, gy, gw, gh = self._goc_kich_thuoc
|
|
1061
|
+
dx = e.x - self._nhan_x
|
|
1062
|
+
dy = e.y - self._nhan_y
|
|
1063
|
+
ten = self._bam_ten
|
|
1064
|
+
x, y, w, h = gx, gy, gw, gh
|
|
1065
|
+
|
|
1066
|
+
if "e" in ten:
|
|
1067
|
+
w = self._snap(gx + gw + dx) - gx
|
|
1068
|
+
if "s" in ten:
|
|
1069
|
+
h = self._snap(gy + gh + dy) - gy
|
|
1070
|
+
if "w" in ten:
|
|
1071
|
+
x_moi = self._snap(gx + dx)
|
|
1072
|
+
w = gx + gw - x_moi
|
|
1073
|
+
x = x_moi
|
|
1074
|
+
if "n" in ten:
|
|
1075
|
+
y_moi = self._snap(gy + dy)
|
|
1076
|
+
h = gy + gh - y_moi
|
|
1077
|
+
y = y_moi
|
|
1078
|
+
w = max(16, w)
|
|
1079
|
+
h = max(14, h)
|
|
1080
|
+
d.update(x=x, y=y, w=w, h=h)
|
|
1081
|
+
self._ve_comp_tu_dong(cid)
|
|
1082
|
+
self._cap_nhat_vien_chon()
|
|
1083
|
+
self._hien_toa_do(x, y, w, h)
|
|
1084
|
+
self.dirty = True
|
|
1085
|
+
|
|
1086
|
+
def _tha(self, e):
|
|
1087
|
+
"""Nhả chuột: kết thúc kéo/resize/quét."""
|
|
1088
|
+
cv = self.ban_ve
|
|
1089
|
+
if self._trang_thai in ("keo", "bam"):
|
|
1090
|
+
self._day_undo()
|
|
1091
|
+
self._lam_moi_bang_thuoc_tinh()
|
|
1092
|
+
elif self._trang_thai == "quet":
|
|
1093
|
+
self._cap_nhat_vien_chon()
|
|
1094
|
+
self._trang_thai = None
|
|
1095
|
+
self._an_toa_do()
|
|
1096
|
+
cv.itemconfigure(self._quet_hcn, state="hidden")
|
|
1097
|
+
cv.configure(cursor="arrow")
|
|
1098
|
+
|
|
1099
|
+
def _di_chuot(self, e):
|
|
1100
|
+
"""Rê chuột: viền hover + tọa độ trên thanh trạng thái."""
|
|
1101
|
+
dx, dy = self._sang_thiet_ke(e.x, e.y)
|
|
1102
|
+
w, h = self._khung()
|
|
1103
|
+
self.nhan_toa_do.config(
|
|
1104
|
+
text=f"X: {min(max(dx, 0), w)} Y: {min(max(dy, 0), h)}")
|
|
1105
|
+
if self._trang_thai:
|
|
1106
|
+
return
|
|
1107
|
+
cv = self.ban_ve
|
|
1108
|
+
bam = self._bam_tai(e.x, e.y)
|
|
1109
|
+
if bam:
|
|
1110
|
+
cv.configure(cursor="fleur")
|
|
1111
|
+
cv.itemconfigure(self._vien_hover, state="hidden")
|
|
1112
|
+
return
|
|
1113
|
+
d = self._tim_comp_tai(e.x, e.y)
|
|
1114
|
+
if d is not None and d["id"] not in self.chon:
|
|
1115
|
+
X, Y = self._sang_canvas(d["x"], d["y"])
|
|
1116
|
+
cv.coords(self._vien_hover, X - 1, Y - 1,
|
|
1117
|
+
X + (d.get("w") or 100) + 1,
|
|
1118
|
+
Y + (d.get("h") or 30) + 1)
|
|
1119
|
+
cv.itemconfigure(self._vien_hover, state="normal")
|
|
1120
|
+
cv.configure(cursor="hand2")
|
|
1121
|
+
self._hover_id = d["id"]
|
|
1122
|
+
else:
|
|
1123
|
+
cv.itemconfigure(self._vien_hover, state="hidden")
|
|
1124
|
+
cv.configure(cursor="arrow")
|
|
1125
|
+
self._hover_id = None
|
|
1126
|
+
|
|
1127
|
+
def _an_toa_do_chuot(self):
|
|
1128
|
+
self.nhan_toa_do.config(text="")
|
|
1129
|
+
|
|
1130
|
+
# ----------------------------------------------------- viên thuốc tọa độ
|
|
1131
|
+
def _hien_toa_do(self, x, y, w=None, h=None):
|
|
1132
|
+
"""Viên thuốc tọa độ bay theo con trỏ khi kéo/resize."""
|
|
1133
|
+
cv = self.ban_ve
|
|
1134
|
+
chu = (f"x: {x} y: {y}" if w is None
|
|
1135
|
+
else f"x: {x} y: {y} w: {w} h: {h}")
|
|
1136
|
+
try:
|
|
1137
|
+
f = tkfont.Font(font=font_ui(8))
|
|
1138
|
+
rong = f.measure(chu)
|
|
1139
|
+
except Exception:
|
|
1140
|
+
rong = 9 * len(chu)
|
|
1141
|
+
mx = getattr(self, "_chuot_x", 0)
|
|
1142
|
+
my = getattr(self, "_chuot_y", 0)
|
|
1143
|
+
cx = min(max(mx + 18, LE + 4), max(LE + 4, cv.winfo_width() - rong - 40))
|
|
1144
|
+
cy = max(6, my - 44)
|
|
1145
|
+
cv.coords(self._toa_do_nen, cx, cy, cx + rong + 18, cy + 22)
|
|
1146
|
+
cv.coords(self._toa_do_chu, cx + 9, cy + 11)
|
|
1147
|
+
cv.itemconfigure(self._toa_do_chu, text=chu, state="normal")
|
|
1148
|
+
cv.itemconfigure(self._toa_do_nen, state="normal")
|
|
1149
|
+
cv.tag_raise(self._toa_do_nen)
|
|
1150
|
+
cv.tag_raise(self._toa_do_chu)
|
|
1151
|
+
|
|
1152
|
+
def _an_toa_do(self):
|
|
1153
|
+
self.ban_ve.itemconfigure(self._toa_do_chu, state="hidden")
|
|
1154
|
+
self.ban_ve.itemconfigure(self._toa_do_nen, state="hidden")
|
|
1155
|
+
|
|
1156
|
+
# ==================================================================
|
|
1157
|
+
# THAO TÁC: THÊM / XÓA / NHÂN BẢN / Z-ORDER
|
|
1158
|
+
# ==================================================================
|
|
1159
|
+
def _them(self, loai, x=None, y=None):
|
|
1160
|
+
"""Thêm thành phần mới (nhấp palette → giữa, so le mỗi lần)."""
|
|
1161
|
+
anh_truoc = self._chup() # ảnh TRƯỚC khi thêm (undo)
|
|
1162
|
+
w, h = KICH_THUOC_MAC_DINH[loai]
|
|
1163
|
+
chu = CHU_MAC_DINH[loai]
|
|
1164
|
+
if loai == "label" and w is None: # đo rộng theo chữ
|
|
1165
|
+
try:
|
|
1166
|
+
f = tkfont.Font(font=font_ui(10))
|
|
1167
|
+
w = f.measure(chu) + 8
|
|
1168
|
+
except Exception:
|
|
1169
|
+
w = 80
|
|
1170
|
+
if x is None or y is None:
|
|
1171
|
+
kw, kh = self._khung()
|
|
1172
|
+
x = (kw - (w or 100)) // 2 + (self._them_dem % 6) * 14
|
|
1173
|
+
y = (kh - (h or 26)) // 2 + (self._them_dem % 6) * 14
|
|
1174
|
+
self._them_dem += 1
|
|
1175
|
+
d = {"loai": loai, "id": self._xin_id(),
|
|
1176
|
+
"x": self._snap(x), "y": self._snap(y),
|
|
1177
|
+
"w": w or 100, "h": h or 26, "text": chu,
|
|
1178
|
+
"color": None, "bg": None, "font_size": None,
|
|
1179
|
+
"visible": True, "enabled": True,
|
|
1180
|
+
"checked": False if loai == "checkbox" else None}
|
|
1181
|
+
self._danh_sach().append(d)
|
|
1182
|
+
self.chon = [d["id"]]
|
|
1183
|
+
self._ve_lai()
|
|
1184
|
+
self._lam_moi_bang_thuoc_tinh()
|
|
1185
|
+
self._day_undo(anh_truoc)
|
|
1186
|
+
self.dirty = True
|
|
1187
|
+
self._cap_nhat_trang_thai(
|
|
1188
|
+
f"● Đã thêm {TEN_HIEN_THI[loai]} #{d['id']}")
|
|
1189
|
+
|
|
1190
|
+
def _xoa_dang_chon(self):
|
|
1191
|
+
"""Xóa các thành phần đang chọn — ID trả về pool TÁI SỬ DỤNG."""
|
|
1192
|
+
if not self.chon:
|
|
1193
|
+
return
|
|
1194
|
+
anh_truoc = self._chup() # ảnh TRƯỚC khi xóa (undo)
|
|
1195
|
+
ds = self._danh_sach()
|
|
1196
|
+
da_xoa = [d for d in ds if d["id"] in self.chon]
|
|
1197
|
+
self.design["components"] = [d for d in ds
|
|
1198
|
+
if d["id"] not in self.chon]
|
|
1199
|
+
for d in da_xoa:
|
|
1200
|
+
heapq.heappush(self._id_tu_do, d["id"]) # trả ID về pool
|
|
1201
|
+
ten = ", ".join(f"{TEN_HIEN_THI[d['loai']]}#{d['id']}"
|
|
1202
|
+
for d in da_xoa)
|
|
1203
|
+
self.chon = []
|
|
1204
|
+
self._ve_lai()
|
|
1205
|
+
self._lam_moi_bang_thuoc_tinh()
|
|
1206
|
+
self._day_undo(anh_truoc)
|
|
1207
|
+
self.dirty = True
|
|
1208
|
+
self._cap_nhat_trang_thai(f"● Đã xóa: {ten}")
|
|
1209
|
+
if da_xoa:
|
|
1210
|
+
self._toast(f"Đã xóa {len(da_xoa)} thành phần — "
|
|
1211
|
+
f"thành phần mới sẽ nhận lại ID vừa xóa")
|
|
1212
|
+
|
|
1213
|
+
def _nhan_ban(self):
|
|
1214
|
+
"""Nhân bản các thành phần đang chọn (ID mới, lệch +14px)."""
|
|
1215
|
+
if not self.chon:
|
|
1216
|
+
return
|
|
1217
|
+
anh_truoc = self._chup()
|
|
1218
|
+
ds = self._danh_sach()
|
|
1219
|
+
moi = []
|
|
1220
|
+
for cid in self.chon:
|
|
1221
|
+
goc = self._comp(cid)
|
|
1222
|
+
if not goc:
|
|
1223
|
+
continue
|
|
1224
|
+
d = copy.deepcopy(goc)
|
|
1225
|
+
d["id"] = self._xin_id()
|
|
1226
|
+
d["x"] += 14
|
|
1227
|
+
d["y"] += 14
|
|
1228
|
+
ds.append(d)
|
|
1229
|
+
moi.append(d["id"])
|
|
1230
|
+
self.chon = moi
|
|
1231
|
+
self._ve_lai()
|
|
1232
|
+
self._lam_moi_bang_thuoc_tinh()
|
|
1233
|
+
self._day_undo(anh_truoc)
|
|
1234
|
+
self.dirty = True
|
|
1235
|
+
|
|
1236
|
+
def _doi_z(self, lenh):
|
|
1237
|
+
"""Đổi thứ tự chồng lớp: len / xuong / tren / duoi."""
|
|
1238
|
+
if not self.chon:
|
|
1239
|
+
return
|
|
1240
|
+
anh_truoc = self._chup()
|
|
1241
|
+
ds = self._danh_sach()
|
|
1242
|
+
chinh = self._comp(self.chon[-1])
|
|
1243
|
+
if not chinh:
|
|
1244
|
+
return
|
|
1245
|
+
i = ds.index(chinh)
|
|
1246
|
+
ds.pop(i)
|
|
1247
|
+
if lenh == "len":
|
|
1248
|
+
ds.insert(min(i + 1, len(ds)), chinh)
|
|
1249
|
+
elif lenh == "xuong":
|
|
1250
|
+
ds.insert(max(i - 1, 0), chinh)
|
|
1251
|
+
elif lenh == "tren":
|
|
1252
|
+
ds.append(chinh)
|
|
1253
|
+
else:
|
|
1254
|
+
ds.insert(0, chinh)
|
|
1255
|
+
self._ve_lai()
|
|
1256
|
+
self._day_undo(anh_truoc)
|
|
1257
|
+
self.dirty = True
|
|
1258
|
+
|
|
1259
|
+
# ==================================================================
|
|
1260
|
+
# UNDO / REDO (ảnh chụp thiết kế)
|
|
1261
|
+
# ==================================================================
|
|
1262
|
+
def _chup(self):
|
|
1263
|
+
return copy.deepcopy(
|
|
1264
|
+
{"window": self.design["window"],
|
|
1265
|
+
"components": self.design["components"]})
|
|
1266
|
+
|
|
1267
|
+
def _chup_truoc_khi_sua(self):
|
|
1268
|
+
"""Lưu ảnh trước khi sửa liên tục (kéo nhiều bước = 1 undo)."""
|
|
1269
|
+
if self.ban_dau_undo is None:
|
|
1270
|
+
self.ban_dau_undo = self._chup()
|
|
1271
|
+
|
|
1272
|
+
def _day_undo(self, anh=None):
|
|
1273
|
+
"""Đưa ảnh TRƯỚC KHI SỬA vào chồng undo — gọi khi thao tác xong.
|
|
1274
|
+
|
|
1275
|
+
Ưu tiên ảnh truyền vào, rồi đến ban_dau_undo (kéo/sửa liên tục),
|
|
1276
|
+
cuối cùng là ảnh hiện tại (fallback an toàn).
|
|
1277
|
+
"""
|
|
1278
|
+
if anh is None and self.ban_dau_undo is not None:
|
|
1279
|
+
anh, self.ban_dau_undo = self.ban_dau_undo, None
|
|
1280
|
+
if anh is None:
|
|
1281
|
+
anh = self._chup()
|
|
1282
|
+
self.undo_ds.append(anh)
|
|
1283
|
+
if len(self.undo_ds) > 80:
|
|
1284
|
+
self.undo_ds.pop(0)
|
|
1285
|
+
self.redo_ds.clear()
|
|
1286
|
+
|
|
1287
|
+
def _phuc_hoi(self, anh):
|
|
1288
|
+
self.design["window"] = copy.deepcopy(anh["window"])
|
|
1289
|
+
self.design["components"] = copy.deepcopy(anh["components"])
|
|
1290
|
+
ids = [c["id"] for c in self.design["components"]]
|
|
1291
|
+
self._id_tiep_theo = (max(ids) + 1) if ids else 1
|
|
1292
|
+
self._id_tu_do = []
|
|
1293
|
+
self.chon = [cid for cid in self.chon if cid in ids]
|
|
1294
|
+
self._ve_lai()
|
|
1295
|
+
self._lam_moi_bang_thuoc_tinh()
|
|
1296
|
+
self.dirty = True
|
|
1297
|
+
|
|
1298
|
+
def _hoan_tac(self):
|
|
1299
|
+
if not self.undo_ds:
|
|
1300
|
+
self._toast("Không còn gì để hoàn tác")
|
|
1301
|
+
return
|
|
1302
|
+
self.redo_ds.append(self._chup())
|
|
1303
|
+
self._phuc_hoi(self.undo_ds.pop())
|
|
1304
|
+
self._cap_nhat_trang_thai("● Đã hoàn tác")
|
|
1305
|
+
|
|
1306
|
+
def _lam_lai(self):
|
|
1307
|
+
if not self.redo_ds:
|
|
1308
|
+
self._toast("Không còn gì để làm lại")
|
|
1309
|
+
return
|
|
1310
|
+
self.undo_ds.append(self._chup())
|
|
1311
|
+
self._phuc_hoi(self.redo_ds.pop())
|
|
1312
|
+
self._cap_nhat_trang_thai("● Đã làm lại")
|
|
1313
|
+
|
|
1314
|
+
# ==================================================================
|
|
1315
|
+
# BẢNG THUỘC TÍNH (phải)
|
|
1316
|
+
# ==================================================================
|
|
1317
|
+
def _lam_moi_bang_thuoc_tinh(self):
|
|
1318
|
+
"""Vẽ lại panel phải theo trạng thái chọn."""
|
|
1319
|
+
for w in self.noi_bang.winfo_children():
|
|
1320
|
+
w.destroy()
|
|
1321
|
+
if self.chon:
|
|
1322
|
+
self._bang_thanh_phan()
|
|
1323
|
+
else:
|
|
1324
|
+
self._bang_cua_so()
|
|
1325
|
+
|
|
1326
|
+
def _tieu_de_bang(self, chu):
|
|
1327
|
+
tk.Label(self.noi_bang, text=chu, fg=MAU["chu_mo"],
|
|
1328
|
+
bg=MAU["nen_panel"], font=font_ui(7, True),
|
|
1329
|
+
anchor="w").pack(fill="x", pady=(10, 4))
|
|
1330
|
+
|
|
1331
|
+
def _hang(self, nhan, widget):
|
|
1332
|
+
"""Một hàng: nhãn trái + control phải."""
|
|
1333
|
+
f = tk.Frame(self.noi_bang, bg=MAU["nen_panel"])
|
|
1334
|
+
f.pack(fill="x", pady=2)
|
|
1335
|
+
tk.Label(f, text=nhan, fg=MAU["chu"], bg=MAU["nen_panel"],
|
|
1336
|
+
font=font_ui(8), width=9, anchor="w").pack(side="left")
|
|
1337
|
+
widget.pack(side="left", fill="x", expand=True)
|
|
1338
|
+
return f
|
|
1339
|
+
|
|
1340
|
+
def _o_nhap(self, gia_tri, lenh, chi_so=False, rong=8):
|
|
1341
|
+
"""Entry tối màu — commit khi rời ô hoặc Enter."""
|
|
1342
|
+
o = tk.Entry(self.noi_bang, bg="#3c3c3c", fg=MAU["chu_sang"],
|
|
1343
|
+
insertbackground=MAU["chu_sang"], relief="flat",
|
|
1344
|
+
highlightthickness=1, font=font_ui(8),
|
|
1345
|
+
highlightbackground=MAU["vien"],
|
|
1346
|
+
highlightcolor=MAU["gt"])
|
|
1347
|
+
o.insert(0, str(gia_tri if gia_tri is not None else ""))
|
|
1348
|
+
o.pack_propagate(False)
|
|
1349
|
+
|
|
1350
|
+
def _cam_ket(_e=None):
|
|
1351
|
+
v = o.get().strip()
|
|
1352
|
+
if chi_so:
|
|
1353
|
+
try:
|
|
1354
|
+
v = int(float(v))
|
|
1355
|
+
except ValueError:
|
|
1356
|
+
return
|
|
1357
|
+
lenh(v)
|
|
1358
|
+
o.bind("<Return>", _cam_ket)
|
|
1359
|
+
o.bind("<FocusOut>", _cam_ket)
|
|
1360
|
+
return o
|
|
1361
|
+
|
|
1362
|
+
def _o_mau(self, mau_hien_tai, lenh):
|
|
1363
|
+
"""Swatch màu + click chọn + chuột phải đặt lại mặc định."""
|
|
1364
|
+
f = tk.Frame(self.noi_bang, bg=MAU["nen_panel"])
|
|
1365
|
+
sw = tk.Label(f, width=3, height=1, bd=0, cursor="hand2",
|
|
1366
|
+
bg=mau_hien_tai or "#3c3c3c",
|
|
1367
|
+
highlightthickness=1,
|
|
1368
|
+
highlightbackground=MAU["vien"])
|
|
1369
|
+
|
|
1370
|
+
def _chon():
|
|
1371
|
+
m = colorchooser.askcolor(parent=self.root)[1]
|
|
1372
|
+
if m:
|
|
1373
|
+
sw.config(bg=m)
|
|
1374
|
+
lenh(m)
|
|
1375
|
+
|
|
1376
|
+
def _dat_lai(_e=None):
|
|
1377
|
+
sw.config(bg="#3c3c3c")
|
|
1378
|
+
lenh(None)
|
|
1379
|
+
sw.bind("<Button-1>", lambda e: _chon())
|
|
1380
|
+
sw.bind("<Button-3>", _dat_lai)
|
|
1381
|
+
sw.pack(side="left")
|
|
1382
|
+
tk.Label(f, text="chuột phải = mặc định", fg=MAU["chu_mo"],
|
|
1383
|
+
bg=MAU["nen_panel"], font=font_ui(7)).pack(side="left",
|
|
1384
|
+
padx=6)
|
|
1385
|
+
return f
|
|
1386
|
+
|
|
1387
|
+
# ----------------------------------------------------- bảng cửa sổ
|
|
1388
|
+
def _bang_cua_so(self):
|
|
1389
|
+
win = self.design["window"]
|
|
1390
|
+
self._tieu_de_bang("CỬA SỔ ỨNG DỤNG")
|
|
1391
|
+
|
|
1392
|
+
def _dat(k, ep_kieu=None):
|
|
1393
|
+
def f(v):
|
|
1394
|
+
self._chup_truoc_khi_sua()
|
|
1395
|
+
if ep_kieu:
|
|
1396
|
+
try:
|
|
1397
|
+
v = ep_kieu(v)
|
|
1398
|
+
except (TypeError, ValueError):
|
|
1399
|
+
return
|
|
1400
|
+
win[k] = v
|
|
1401
|
+
self.dirty = True
|
|
1402
|
+
if k in ("width", "height", "bg"):
|
|
1403
|
+
self._ve_lai()
|
|
1404
|
+
elif k == "title":
|
|
1405
|
+
self._ve_lai()
|
|
1406
|
+
return f
|
|
1407
|
+
|
|
1408
|
+
self._hang("Tiêu đề",
|
|
1409
|
+
self._o_nhap(win.get("title", ""), _dat("title")))
|
|
1410
|
+
self._hang("Rộng (px)",
|
|
1411
|
+
self._o_nhap(win.get("width", 860), _dat("width", int),
|
|
1412
|
+
chi_so=True))
|
|
1413
|
+
self._hang("Cao (px)",
|
|
1414
|
+
self._o_nhap(win.get("height", 560), _dat("height", int),
|
|
1415
|
+
chi_so=True))
|
|
1416
|
+
self._hang("Màu nền",
|
|
1417
|
+
self._o_mau(win.get("bg"), _dat("bg")))
|
|
1418
|
+
self._tieu_de_bang("GỢI Ý")
|
|
1419
|
+
tk.Label(self.noi_bang,
|
|
1420
|
+
text="Chọn một thành phần để sửa thuộc tính.\n"
|
|
1421
|
+
"Vùng trống: quét chuột để chọn nhiều.\n"
|
|
1422
|
+
"Double-click thành phần: sửa chữ nhanh.",
|
|
1423
|
+
fg=MAU["chu_mo"], bg=MAU["nen_panel"], font=font_ui(8),
|
|
1424
|
+
justify="left", anchor="w", wraplength=236).pack(
|
|
1425
|
+
fill="x", pady=(2, 0))
|
|
1426
|
+
|
|
1427
|
+
# ----------------------------------------------------- bảng thành phần
|
|
1428
|
+
def _bang_thanh_phan(self):
|
|
1429
|
+
chinh = self._comp(self.chon[-1])
|
|
1430
|
+
if chinh is None:
|
|
1431
|
+
return
|
|
1432
|
+
loai = chinh["loai"]
|
|
1433
|
+
self._tieu_de_bang(
|
|
1434
|
+
f"{TEN_HIEN_THI[loai].upper()} #{chinh['id']}"
|
|
1435
|
+
+ (f" + {len(self.chon) - 1} khác" if len(self.chon) > 1 else ""))
|
|
1436
|
+
|
|
1437
|
+
def _dat(k, ep_kieu=None, ve_lai=True):
|
|
1438
|
+
def f(v):
|
|
1439
|
+
self._chup_truoc_khi_sua()
|
|
1440
|
+
if ep_kieu:
|
|
1441
|
+
try:
|
|
1442
|
+
v = ep_kieu(v)
|
|
1443
|
+
except (TypeError, ValueError):
|
|
1444
|
+
return
|
|
1445
|
+
if k == "text" and v == "":
|
|
1446
|
+
v = ""
|
|
1447
|
+
chinh[k] = v
|
|
1448
|
+
self.dirty = True
|
|
1449
|
+
if ve_lai:
|
|
1450
|
+
self._ve_comp_tu_dong(chinh["id"])
|
|
1451
|
+
self._cap_nhat_vien_chon()
|
|
1452
|
+
self._chup_ve_cuoi_hang()
|
|
1453
|
+
return f
|
|
1454
|
+
|
|
1455
|
+
# các trường theo loại
|
|
1456
|
+
if loai in ("label", "button", "checkbox"):
|
|
1457
|
+
self._hang("Chữ", self._o_nhap(chinh.get("text") or "",
|
|
1458
|
+
_dat("text")))
|
|
1459
|
+
elif loai == "input":
|
|
1460
|
+
self._hang("Giá trị đầu", self._o_nhap(chinh.get("text") or "",
|
|
1461
|
+
_dat("text")))
|
|
1462
|
+
elif loai == "text":
|
|
1463
|
+
o = tk.Text(self.noi_bang, height=3, bd=0, bg="#3c3c3c",
|
|
1464
|
+
fg=MAU["chu_sang"], insertbackground=MAU["chu_sang"],
|
|
1465
|
+
font=font_ui(8), relief="flat", padx=6, pady=4,
|
|
1466
|
+
highlightthickness=1,
|
|
1467
|
+
highlightbackground=MAU["vien"])
|
|
1468
|
+
o.insert("1.0", chinh.get("text") or "")
|
|
1469
|
+
|
|
1470
|
+
def _cam(_e=None):
|
|
1471
|
+
_dat("text")(o.get("1.0", "end-1c"))
|
|
1472
|
+
o.bind("<FocusOut>", _cam)
|
|
1473
|
+
self._hang("Nội dung", o)
|
|
1474
|
+
|
|
1475
|
+
self._hang("X", self._o_nhap(chinh["x"], _dat("x", int),
|
|
1476
|
+
chi_so=True))
|
|
1477
|
+
self._hang("Y", self._o_nhap(chinh["y"], _dat("y", int),
|
|
1478
|
+
chi_so=True))
|
|
1479
|
+
self._hang("Rộng", self._o_nhap(chinh.get("w") or 100,
|
|
1480
|
+
_dat("w", int), chi_so=True))
|
|
1481
|
+
self._hang("Cao", self._o_nhap(chinh.get("h") or 30,
|
|
1482
|
+
_dat("h", int), chi_so=True))
|
|
1483
|
+
self._hang("Màu chữ", self._o_mau(chinh.get("color"),
|
|
1484
|
+
_dat("color")))
|
|
1485
|
+
self._hang("Màu nền", self._o_mau(chinh.get("bg"), _dat("bg")))
|
|
1486
|
+
self._hang("Cỡ chữ", self._o_nhap(chinh.get("font_size") or 10,
|
|
1487
|
+
_dat("font_size", int),
|
|
1488
|
+
chi_so=True))
|
|
1489
|
+
|
|
1490
|
+
# ô kiểm
|
|
1491
|
+
if loai == "checkbox":
|
|
1492
|
+
self._o_kiem(
|
|
1493
|
+
"Đang chọn (checked)", chinh.get("checked"),
|
|
1494
|
+
lambda: _dat("checked", bool)(not chinh.get("checked")))
|
|
1495
|
+
self._o_kiem(
|
|
1496
|
+
"Hiện khi chạy (visible)", chinh.get("visible", True),
|
|
1497
|
+
lambda: _dat("visible", bool)(
|
|
1498
|
+
not chinh.get("visible", True)))
|
|
1499
|
+
self._o_kiem(
|
|
1500
|
+
"Kích hoạt (enabled)", chinh.get("enabled", True),
|
|
1501
|
+
lambda: _dat("enabled", bool)(
|
|
1502
|
+
not chinh.get("enabled", True)))
|
|
1503
|
+
|
|
1504
|
+
# nút tiện dụng
|
|
1505
|
+
duoi = tk.Frame(self.noi_bang, bg=MAU["nen_panel"])
|
|
1506
|
+
duoi.pack(fill="x", pady=(10, 0))
|
|
1507
|
+
NutPhang(duoi, "⌨ Sửa trigger", lambda: self._sua_trigger(),
|
|
1508
|
+
co=8).pack(side="left")
|
|
1509
|
+
NutPhang(duoi, "⧉ Nhân bản", self._nhan_ban, co=8).pack(
|
|
1510
|
+
side="left", padx=4)
|
|
1511
|
+
NutPhang(duoi, "🗑 Xóa", self._xoa_dang_chon, co=8).pack(
|
|
1512
|
+
side="left", padx=4)
|
|
1513
|
+
|
|
1514
|
+
def _o_kiem(self, nhan, gia_tri, lenh):
|
|
1515
|
+
"""Một ô kiểm (Checkbutton) tối màu trong bảng thuộc tính."""
|
|
1516
|
+
f = tk.Frame(self.noi_bang, bg=MAU["nen_panel"])
|
|
1517
|
+
f.pack(fill="x", pady=2)
|
|
1518
|
+
ck = tk.Checkbutton(f, text=nhan,
|
|
1519
|
+
variable=tk.BooleanVar(value=bool(gia_tri)),
|
|
1520
|
+
bg=MAU["nen_panel"], fg=MAU["chu"],
|
|
1521
|
+
activebackground=MAU["nen_panel"],
|
|
1522
|
+
selectcolor="#2d2d30", font=font_ui(8),
|
|
1523
|
+
anchor="w", command=lenh)
|
|
1524
|
+
ck.pack(side="left")
|
|
1525
|
+
|
|
1526
|
+
def _chup_ve_cuoi_hang(self):
|
|
1527
|
+
"""Hẹn đẩy undo 700ms sau lần sửa cuối (gộp thao tác liên tục)."""
|
|
1528
|
+
if getattr(self, "_hen_undo_cuoi", None):
|
|
1529
|
+
try:
|
|
1530
|
+
self.root.after_cancel(self._hen_undo_cuoi)
|
|
1531
|
+
except Exception:
|
|
1532
|
+
pass
|
|
1533
|
+
self._hen_undo_cuoi = self.root.after(700, self._ket_thuc_sua)
|
|
1534
|
+
|
|
1535
|
+
def _ket_thuc_sua(self):
|
|
1536
|
+
self._hen_undo_cuoi = None
|
|
1537
|
+
self._day_undo()
|
|
1538
|
+
|
|
1539
|
+
# ==================================================================
|
|
1540
|
+
# TRIGGER — file .triggers.py dùng chung (Studio + VS Code)
|
|
1541
|
+
# ==================================================================
|
|
1542
|
+
def _mau_triggers(self):
|
|
1543
|
+
"""Nội dung mẫu khi file trigger chưa tồn tại."""
|
|
1544
|
+
ten = (self.duong_triggers.name if self.duong_triggers
|
|
1545
|
+
else "triggers.py")
|
|
1546
|
+
return (
|
|
1547
|
+
"# " + "=" * 70 + "\n"
|
|
1548
|
+
f"# {ten} — trigger cho ứng dụng\n"
|
|
1549
|
+
"# File này DÙNG CHUNG: sửa ở FlexGUI Studio HOẶC VS Code.\n"
|
|
1550
|
+
"# (label/button/input/checkbox/text đã có sẵn — không cần "
|
|
1551
|
+
"import)\n"
|
|
1552
|
+
"# " + "=" * 70 + "\n"
|
|
1553
|
+
"\n"
|
|
1554
|
+
"def on_start():\n"
|
|
1555
|
+
' """Chạy 1 lần khi app mở."""\n'
|
|
1556
|
+
" pass\n"
|
|
1557
|
+
"\n"
|
|
1558
|
+
)
|
|
1559
|
+
|
|
1560
|
+
def _doc_triggers(self):
|
|
1561
|
+
"""Nạp file trigger vào editor (file cũ hoặc mẫu mới)."""
|
|
1562
|
+
if self.duong_triggers and Path(self.duong_triggers).exists():
|
|
1563
|
+
try:
|
|
1564
|
+
self.editor.dat(
|
|
1565
|
+
Path(self.duong_triggers).read_text(encoding="utf-8"))
|
|
1566
|
+
return
|
|
1567
|
+
except Exception:
|
|
1568
|
+
pass
|
|
1569
|
+
self.editor.dat(self._mau_triggers())
|
|
1570
|
+
|
|
1571
|
+
def _bo_sung_skeleton(self):
|
|
1572
|
+
"""Thêm sườn hàm trigger cho thành phần chưa có — giữ code cũ."""
|
|
1573
|
+
nguon = self.editor.lay()
|
|
1574
|
+
cac_dong = [nguon]
|
|
1575
|
+
for d in self._danh_sach():
|
|
1576
|
+
su_kien = SUKIEN_CHINH.get(d["loai"], "click")
|
|
1577
|
+
ten = f"on_{d['loai']}_{d['id']}_{su_kien}"
|
|
1578
|
+
if re.search(rf"^\s*def\s+{ten}\s*\(", nguon, re.M):
|
|
1579
|
+
continue
|
|
1580
|
+
if su_kien == "toggle":
|
|
1581
|
+
mo_ta = f"{TEN_HIEN_THI[d['loai']]} #{d['id']} được tick/bỏ tick"
|
|
1582
|
+
elif su_kien == "enter":
|
|
1583
|
+
mo_ta = f"nhấn Enter trong Input #{d['id']}"
|
|
1584
|
+
elif su_kien == "change":
|
|
1585
|
+
mo_ta = f"gõ chữ trong Text #{d['id']}"
|
|
1586
|
+
else:
|
|
1587
|
+
mo_ta = f"{TEN_HIEN_THI[d['loai']]} #{d['id']} được nhấn"
|
|
1588
|
+
cac_dong.append(
|
|
1589
|
+
f"\n\ndef {ten}():\n"
|
|
1590
|
+
f' """Chạy khi {mo_ta}."""\n'
|
|
1591
|
+
f" pass\n")
|
|
1592
|
+
if len(cac_dong) > 1:
|
|
1593
|
+
self.editor.dat("".join(cac_dong), dat_con_tro=True)
|
|
1594
|
+
|
|
1595
|
+
def _ghi_triggers(self, bao=False):
|
|
1596
|
+
"""Ghi buffer editor ra file .triggers.py (kèm skeleton)."""
|
|
1597
|
+
if not self.duong_triggers:
|
|
1598
|
+
# chưa gắn file app — hỏi nơi lưu chung với JSON
|
|
1599
|
+
if not self._hoi_duong_luu():
|
|
1600
|
+
return False
|
|
1601
|
+
self._bo_sung_skeleton()
|
|
1602
|
+
try:
|
|
1603
|
+
Path(self.duong_triggers).write_text(self.editor.lay(),
|
|
1604
|
+
encoding="utf-8")
|
|
1605
|
+
if bao:
|
|
1606
|
+
self._toast("✓ Đã lưu " + self._ten_triggers(),
|
|
1607
|
+
"#173f2f")
|
|
1608
|
+
return True
|
|
1609
|
+
except Exception:
|
|
1610
|
+
messagebox.showerror("Không lưu được trigger",
|
|
1611
|
+
traceback.format_exc())
|
|
1612
|
+
return False
|
|
1613
|
+
|
|
1614
|
+
def _sua_trigger(self, cid=None):
|
|
1615
|
+
"""Mở drawer trigger, nhảy tới hàm của thành phần đang chọn."""
|
|
1616
|
+
self._mo_drawer()
|
|
1617
|
+
d = self._comp(cid if cid is not None
|
|
1618
|
+
else (self.chon[-1] if self.chon else None))
|
|
1619
|
+
if not d:
|
|
1620
|
+
return
|
|
1621
|
+
su_kien = SUKIEN_CHINH.get(d["loai"], "click")
|
|
1622
|
+
ten = f"on_{d['loai']}_{d['id']}_{su_kien}"
|
|
1623
|
+
if self.editor.tim_ham(ten) is None:
|
|
1624
|
+
self._bo_sung_skeleton()
|
|
1625
|
+
dong = self.editor.tim_ham(ten)
|
|
1626
|
+
if dong:
|
|
1627
|
+
self.editor.den_dong(dong)
|
|
1628
|
+
|
|
1629
|
+
def _dao_drawer(self):
|
|
1630
|
+
"""Bật/tắt ngăn kéo trigger dưới cùng."""
|
|
1631
|
+
if self.drawer.winfo_ismapped():
|
|
1632
|
+
self._an_drawer()
|
|
1633
|
+
else:
|
|
1634
|
+
self._mo_drawer()
|
|
1635
|
+
|
|
1636
|
+
def _mo_drawer(self):
|
|
1637
|
+
if not self.drawer.winfo_ismapped():
|
|
1638
|
+
self.drawer.pack(fill="x", side="bottom")
|
|
1639
|
+
self.nut_drawer.dat_bat(True)
|
|
1640
|
+
self.editor.o.focus_set()
|
|
1641
|
+
try:
|
|
1642
|
+
self.editor.o.mark_set("insert", "insert")
|
|
1643
|
+
except Exception:
|
|
1644
|
+
pass
|
|
1645
|
+
|
|
1646
|
+
def _an_drawer(self):
|
|
1647
|
+
self.drawer.pack_forget()
|
|
1648
|
+
self.nut_drawer.dat_bat(False)
|
|
1649
|
+
self.ban_ve.focus_set()
|
|
1650
|
+
|
|
1651
|
+
def _chen_mau(self):
|
|
1652
|
+
"""Menu chèn snippet code mẫu tại vị trí con trỏ editor."""
|
|
1653
|
+
menu = tk.Menu(self.root, tearoff=0, bg=MAU["nen_nut"],
|
|
1654
|
+
fg=MAU["chu"], activebackground=MAU["gt"],
|
|
1655
|
+
activeforeground="#ffffff", bd=0)
|
|
1656
|
+
for ten, code in MUC_CHEN:
|
|
1657
|
+
menu.add_command(
|
|
1658
|
+
label=ten,
|
|
1659
|
+
command=lambda c=code: self._chen_code(c))
|
|
1660
|
+
menu.tk_popup(self.root.winfo_pointerx(), self.root.winfo_pointery())
|
|
1661
|
+
|
|
1662
|
+
def _chen_code(self, code):
|
|
1663
|
+
self._mo_drawer()
|
|
1664
|
+
try:
|
|
1665
|
+
self.editor.o.insert("insert", code)
|
|
1666
|
+
self.editor._lam_moi_sau()
|
|
1667
|
+
except Exception:
|
|
1668
|
+
pass
|
|
1669
|
+
|
|
1670
|
+
# ==================================================================
|
|
1671
|
+
# LƯU / XEM TRƯỚC / XUẤT / BUILD
|
|
1672
|
+
# ==================================================================
|
|
1673
|
+
def _hoi_duong_luu(self):
|
|
1674
|
+
"""Hỏi nơi lưu khi chưa có file (chưa gắn với file .py nào)."""
|
|
1675
|
+
duong = filedialog.asksaveasfilename(
|
|
1676
|
+
title="Lưu thiết kế FlexGUI",
|
|
1677
|
+
defaultextension=".flexgui.json",
|
|
1678
|
+
filetypes=[("Thiết kế FlexGUI", "*.flexgui.json")])
|
|
1679
|
+
if not duong:
|
|
1680
|
+
return False
|
|
1681
|
+
self.duong_json = Path(duong)
|
|
1682
|
+
self.duong_triggers = self.duong_json.parent / (
|
|
1683
|
+
self.duong_json.name[:-len(".flexgui.json")] + ".triggers.py")
|
|
1684
|
+
self.nhan_ten_file.config(text="· " + self.duong_json.name)
|
|
1685
|
+
self.nhan_tab.config(text=" ⌨ " + self._ten_triggers()
|
|
1686
|
+
+ " • Python • UTF-8")
|
|
1687
|
+
return True
|
|
1688
|
+
|
|
1689
|
+
def _luu_toan_bo(self, bao=False, im_lang=False):
|
|
1690
|
+
"""Ctrl+S — lưu thiết kế (.json) + trigger (.triggers.py)."""
|
|
1691
|
+
if not self.duong_json and not self._hoi_duong_luu():
|
|
1692
|
+
return False
|
|
1693
|
+
self._bo_sung_skeleton()
|
|
1694
|
+
try:
|
|
1695
|
+
Path(self.duong_json).write_text(
|
|
1696
|
+
json.dumps(self.design, ensure_ascii=False, indent=1),
|
|
1697
|
+
encoding="utf-8")
|
|
1698
|
+
Path(self.duong_triggers).write_text(self.editor.lay(),
|
|
1699
|
+
encoding="utf-8")
|
|
1700
|
+
except Exception:
|
|
1701
|
+
if not im_lang:
|
|
1702
|
+
messagebox.showerror("Không lưu được",
|
|
1703
|
+
traceback.format_exc())
|
|
1704
|
+
return False
|
|
1705
|
+
self.dirty = False
|
|
1706
|
+
if bao:
|
|
1707
|
+
self._toast(f"✓ Đã lưu {self.duong_json.name}", "#173f2f")
|
|
1708
|
+
self._cap_nhat_trang_thai("● Đã lưu")
|
|
1709
|
+
return True
|
|
1710
|
+
|
|
1711
|
+
def _xem_truoc(self):
|
|
1712
|
+
"""F5 — chạy thử THẬT thiết kế hiện tại (không cần lưu file)."""
|
|
1713
|
+
if _reg._app_hien_tai() is not None:
|
|
1714
|
+
messagebox.showinfo("Đang mở",
|
|
1715
|
+
"Hãy đóng cửa sổ xem trước hiện tại trước.")
|
|
1716
|
+
return
|
|
1717
|
+
thiet_ke = copy.deepcopy(self.design)
|
|
1718
|
+
win = thiet_ke["window"]
|
|
1719
|
+
try:
|
|
1720
|
+
app = App(win.get("title", "Xem trước"), win.get("width", 860),
|
|
1721
|
+
win.get("height", 560), bg=win.get("bg"),
|
|
1722
|
+
goc=self.root)
|
|
1723
|
+
for d in thiet_ke["components"]:
|
|
1724
|
+
_tao_tu_du_lieu(app, d)
|
|
1725
|
+
loi = nap_trigger(app, self.editor.lay(),
|
|
1726
|
+
ten_file=str(self.duong_triggers or "<triggers>"))
|
|
1727
|
+
if loi:
|
|
1728
|
+
chi_tiet = "\n\n".join(f"— {b} —\n{t}" for b, t in loi)
|
|
1729
|
+
messagebox.showwarning(
|
|
1730
|
+
"Trigger có lỗi (vẫn xem trước được)",
|
|
1731
|
+
"Một số đoạn trigger không chạy được:\n\n" + chi_tiet,
|
|
1732
|
+
parent=self.root)
|
|
1733
|
+
app.run(chay_mainloop=False)
|
|
1734
|
+
self._cap_nhat_trang_thai("● Đang xem trước…")
|
|
1735
|
+
except Exception:
|
|
1736
|
+
_reg._dat_app(None)
|
|
1737
|
+
messagebox.showerror("Lỗi xem trước", traceback.format_exc(),
|
|
1738
|
+
parent=self.root)
|
|
1739
|
+
|
|
1740
|
+
def _ten_sach(self):
|
|
1741
|
+
"""Tên an toàn cho file build (ASCII)."""
|
|
1742
|
+
ten = (self.duong_json.stem if self.duong_json
|
|
1743
|
+
else (self.duong_dan.stem if self.duong_dan else "ung_dung"))
|
|
1744
|
+
ten = ten[:-len(".flexgui")] if ten.endswith(".flexgui") else ten
|
|
1745
|
+
re.sub(r"[^A-Za-z0-9_-]+", "_", ten)
|
|
1746
|
+
return re.sub(r"[^A-Za-z0-9_-]+", "_", ten) or "ung_dung"
|
|
1747
|
+
|
|
1748
|
+
def _duong_file_dung(self):
|
|
1749
|
+
"""Thư mục + file .py gộp dùng chung cho xuất/build."""
|
|
1750
|
+
if not self.duong_json and not self._hoi_duong_luu():
|
|
1751
|
+
return None
|
|
1752
|
+
thu_muc = self.duong_json.parent / "flexgui_build"
|
|
1753
|
+
thu_muc.mkdir(exist_ok=True)
|
|
1754
|
+
return thu_muc / (self._ten_sach() + ".py")
|
|
1755
|
+
|
|
1756
|
+
def _xuat_py(self):
|
|
1757
|
+
"""Xuất MỘT file .py tự chạy (nhúng thiết kế + trigger)."""
|
|
1758
|
+
if not self._luu_toan_bo(im_lang=True):
|
|
1759
|
+
return
|
|
1760
|
+
duong = self._duong_file_dung()
|
|
1761
|
+
if not duong:
|
|
1762
|
+
return
|
|
1763
|
+
try:
|
|
1764
|
+
builder.xuat_file_dung(duong, self.design, self.editor.lay())
|
|
1765
|
+
self._toast(f"✓ Đã xuất {duong.name} (thư mục flexgui_build)")
|
|
1766
|
+
messagebox.showinfo(
|
|
1767
|
+
"Đã xuất file chạy đơn",
|
|
1768
|
+
f"Đã tạo:\n{duong}\n\nChạy bằng: python {duong.name}\n"
|
|
1769
|
+
f"Cần đã cài: pip install flexgui", parent=self.root)
|
|
1770
|
+
except Exception:
|
|
1771
|
+
messagebox.showerror("Lỗi xuất file", traceback.format_exc(),
|
|
1772
|
+
parent=self.root)
|
|
1773
|
+
|
|
1774
|
+
def _build_exe(self):
|
|
1775
|
+
"""Build .exe bằng PyInstaller (chỉ khi user bấm nút)."""
|
|
1776
|
+
if not self._luu_toan_bo(im_lang=True):
|
|
1777
|
+
return
|
|
1778
|
+
duong = self._duong_file_dung()
|
|
1779
|
+
if not duong:
|
|
1780
|
+
return
|
|
1781
|
+
try:
|
|
1782
|
+
builder.xuat_file_dung(duong, self.design, self.editor.lay())
|
|
1783
|
+
self._dong_goi = builder.TrinhDongGoi(self.root,
|
|
1784
|
+
self._ten_sach())
|
|
1785
|
+
self._dong_goi.chay(duong)
|
|
1786
|
+
except Exception:
|
|
1787
|
+
messagebox.showerror("Lỗi build", traceback.format_exc(),
|
|
1788
|
+
parent=self.root)
|
|
1789
|
+
|
|
1790
|
+
# ==================================================================
|
|
1791
|
+
# MENU CHUỘT PHẢI / DOUBLE-CLICK / TRỢ GIÚP
|
|
1792
|
+
# ==================================================================
|
|
1793
|
+
def _nhan_kp(self, e):
|
|
1794
|
+
"""Double-click: sửa chữ nhanh thành phần."""
|
|
1795
|
+
d = self._tim_comp_tai(e.x, e.y)
|
|
1796
|
+
if not d:
|
|
1797
|
+
return
|
|
1798
|
+
self.chon = [d["id"]]
|
|
1799
|
+
self._cap_nhat_vien_chon()
|
|
1800
|
+
self._lam_moi_bang_thuoc_tinh()
|
|
1801
|
+
if d["loai"] in ("label", "button", "checkbox", "input", "text"):
|
|
1802
|
+
moi = simpledialog.askstring(
|
|
1803
|
+
"Sửa nhanh nội dung",
|
|
1804
|
+
"Nội dung mới cho "
|
|
1805
|
+
f"{TEN_HIEN_THI[d['loai']]} #{d['id']}:",
|
|
1806
|
+
initialvalue=d.get("text") or "", parent=self.root)
|
|
1807
|
+
if moi is not None:
|
|
1808
|
+
self._chup_truoc_khi_sua()
|
|
1809
|
+
d["text"] = moi
|
|
1810
|
+
self._ve_comp_tu_dong(d["id"])
|
|
1811
|
+
self._ket_thuc_sua()
|
|
1812
|
+
self.dirty = True
|
|
1813
|
+
|
|
1814
|
+
def _menu_phai(self, e):
|
|
1815
|
+
menu = tk.Menu(self.root, tearoff=0, bg=MAU["nen_nut"],
|
|
1816
|
+
fg=MAU["chu"], activebackground=MAU["gt"],
|
|
1817
|
+
activeforeground="#ffffff", bd=0,
|
|
1818
|
+
font=font_ui(9))
|
|
1819
|
+
d = self._tim_comp_tai(e.x, e.y)
|
|
1820
|
+
if d is not None:
|
|
1821
|
+
if d["id"] not in self.chon:
|
|
1822
|
+
self.chon = [d["id"]]
|
|
1823
|
+
self._cap_nhat_vien_chon()
|
|
1824
|
+
menu.add_command(label=f"Sửa chữ… (double-click)",
|
|
1825
|
+
command=lambda: self._nhan_kp(e))
|
|
1826
|
+
menu.add_command(label="Sửa trigger…", command=self._sua_trigger)
|
|
1827
|
+
menu.add_command(label="Nhân bản (Ctrl+D)",
|
|
1828
|
+
command=self._nhan_ban)
|
|
1829
|
+
menu.add_separator()
|
|
1830
|
+
menu.add_command(label="Đưa lên một lớp",
|
|
1831
|
+
command=lambda: self._doi_z("len"))
|
|
1832
|
+
menu.add_command(label="Đưa xuống một lớp",
|
|
1833
|
+
command=lambda: self._doi_z("xuong"))
|
|
1834
|
+
menu.add_command(label="Nằm trên cùng",
|
|
1835
|
+
command=lambda: self._doi_z("tren"))
|
|
1836
|
+
menu.add_command(label="Nằm dưới cùng",
|
|
1837
|
+
command=lambda: self._doi_z("duoi"))
|
|
1838
|
+
menu.add_separator()
|
|
1839
|
+
menu.add_command(label="Xóa (Delete)",
|
|
1840
|
+
command=self._xoa_dang_chon)
|
|
1841
|
+
else:
|
|
1842
|
+
menu.add_command(label="Chọn hết (Ctrl+A)",
|
|
1843
|
+
command=lambda: self._chon_het())
|
|
1844
|
+
try:
|
|
1845
|
+
menu.tk_popup(e.x_root, e.y_root)
|
|
1846
|
+
finally:
|
|
1847
|
+
menu.grab_release()
|
|
1848
|
+
|
|
1849
|
+
def _chon_het(self):
|
|
1850
|
+
self.chon = [d["id"] for d in self._danh_sach()]
|
|
1851
|
+
self._cap_nhat_vien_chon()
|
|
1852
|
+
|
|
1853
|
+
def _mo_tro_giup(self):
|
|
1854
|
+
"""F1 — bảng phím tắt + hướng dẫn nhanh."""
|
|
1855
|
+
hop = tk.Toplevel(self.root)
|
|
1856
|
+
hop.title("FlexGUI Studio — Trợ giúp")
|
|
1857
|
+
hop.configure(bg=MAU["nen_panel"])
|
|
1858
|
+
hop.geometry("640x560")
|
|
1859
|
+
hop.transient(self.root)
|
|
1860
|
+
noi_dung = (
|
|
1861
|
+
("PHÍM TẮT", ""),
|
|
1862
|
+
("Ctrl+S", "Lưu thiết kế (.flexgui.json + .triggers.py)"),
|
|
1863
|
+
("F5", "Xem trước — chạy thử app thật (kéo được, trigger chạy)"),
|
|
1864
|
+
("Delete / Backspace", "Xóa các thành phần đang chọn"),
|
|
1865
|
+
("Ctrl+Z / Ctrl+Y", "Hoàn tác / Làm lại"),
|
|
1866
|
+
("Ctrl+D", "Nhân bản thành phần đang chọn"),
|
|
1867
|
+
("Ctrl+A", "Chọn hết"),
|
|
1868
|
+
("Mũi tên (+Shift)", "Dịch 1px (Shift = 10px, tự snap lưới)"),
|
|
1869
|
+
("Escape", "Bỏ chọn hết"),
|
|
1870
|
+
("", ""),
|
|
1871
|
+
("TƯƠNG TÁC", ""),
|
|
1872
|
+
("Kéo", "Di chuyển — giữ Ctrl/Shift + click để chọn nhiều, "
|
|
1873
|
+
"quét vùng chuột trên nền trống"),
|
|
1874
|
+
("8 núm xanh", "Resize — viền + góc, tự snap lưới 10px"),
|
|
1875
|
+
("Double-click", "Sửa chữ nhanh"),
|
|
1876
|
+
("Chuột phải", "Menu: z-order, nhân bản, xóa, trigger"),
|
|
1877
|
+
("", ""),
|
|
1878
|
+
("QUY TRÌNH XUẤT BẢN", ""),
|
|
1879
|
+
("1.", "flexgui.setup() trong file .py → chạy → Studio mở"),
|
|
1880
|
+
("2.", "Kéo thả, chỉnh thuộc tính, viết trigger (nút Triggers)"),
|
|
1881
|
+
("3.", "Xuất .py (1 file tự chạy) hoặc Build .exe (PyInstaller)"),
|
|
1882
|
+
("4.", "Chạy thật: flexgui.run(__file__) thay cho setup()"),
|
|
1883
|
+
)
|
|
1884
|
+
f = font_mono(9)
|
|
1885
|
+
for i, (a, b) in enumerate(noi_dung):
|
|
1886
|
+
mau = MAU["chu_sang"] if b == "" else MAU["chu"]
|
|
1887
|
+
trong = MAU["chu"] if b == "" else MAU["chu_mo"]
|
|
1888
|
+
tk.Label(hop, text=a, fg=mau, bg=MAU["nen_panel"], font=f,
|
|
1889
|
+
width=20, anchor="w").grid(row=i, column=0,
|
|
1890
|
+
sticky="nw", padx=(16, 0),
|
|
1891
|
+
pady=1)
|
|
1892
|
+
tk.Label(hop, text=b, fg=trong, bg=MAU["nen_panel"], font=f,
|
|
1893
|
+
anchor="w", justify="left", wraplength=420).grid(
|
|
1894
|
+
row=i, column=1, sticky="nw", padx=(0, 16), pady=1)
|
|
1895
|
+
NutPhang(hop, "Đã hiểu", hop.destroy, rong=10).grid(
|
|
1896
|
+
row=len(noi_dung) + 1, column=0, columnspan=2, pady=14)
|
|
1897
|
+
|
|
1898
|
+
# ==================================================================
|
|
1899
|
+
# PHÍM TẮT (an toàn bộ gõ — bỏ qua khi đang gõ trong ô nhập)
|
|
1900
|
+
# ==================================================================
|
|
1901
|
+
def _gan_phim(self):
|
|
1902
|
+
self.root.bind("<KeyPress>", self._nhan_phim)
|
|
1903
|
+
self.root.bind("<Control-s>", self._phim_luu)
|
|
1904
|
+
self.root.bind("<F5>", self._phim_xem_truoc)
|
|
1905
|
+
self.editor.o.bind("<Control-s>", self._phim_luu)
|
|
1906
|
+
|
|
1907
|
+
def _phim_luu(self, _e=None):
|
|
1908
|
+
self._luu_toan_bo(True)
|
|
1909
|
+
return "break"
|
|
1910
|
+
|
|
1911
|
+
def _phim_xem_truoc(self, _e=None):
|
|
1912
|
+
self._xem_truoc()
|
|
1913
|
+
return "break"
|
|
1914
|
+
|
|
1915
|
+
@staticmethod
|
|
1916
|
+
def _dang_go_van_ban(w):
|
|
1917
|
+
"""True khi focus đang trong ô nhập/editor — không chặn phím của IME."""
|
|
1918
|
+
return w is not None and w.winfo_class() in (
|
|
1919
|
+
"Text", "Entry", "TSpinbox", "TEntry", "TCombobox")
|
|
1920
|
+
|
|
1921
|
+
def _nhan_phim(self, e):
|
|
1922
|
+
# Đang gõ trong ô nhập / editor → KHÔNG can thiệp (bảo vệ IME)
|
|
1923
|
+
if self._dang_go_van_ban(self.root.focus_get()):
|
|
1924
|
+
return
|
|
1925
|
+
ks = e.keysym.lower()
|
|
1926
|
+
|
|
1927
|
+
if ks in ("delete", "backspace"):
|
|
1928
|
+
self._xoa_dang_chon()
|
|
1929
|
+
return "break"
|
|
1930
|
+
if ks == "escape":
|
|
1931
|
+
self.chon = []
|
|
1932
|
+
self._cap_nhat_vien_chon()
|
|
1933
|
+
self._lam_moi_bang_thuoc_tinh()
|
|
1934
|
+
return
|
|
1935
|
+
if ks in ("left", "right", "up", "down"):
|
|
1936
|
+
self._di_chuyen_bang_phim(ks, e)
|
|
1937
|
+
return "break"
|
|
1938
|
+
if not (e.state & 0x0004):
|
|
1939
|
+
return
|
|
1940
|
+
# ---- tổ hợp Ctrl+... ----
|
|
1941
|
+
if ks == "a":
|
|
1942
|
+
self._chon_het()
|
|
1943
|
+
return "break"
|
|
1944
|
+
if ks == "z":
|
|
1945
|
+
self._hoan_tac()
|
|
1946
|
+
return "break"
|
|
1947
|
+
if ks == "y":
|
|
1948
|
+
self._lam_lai()
|
|
1949
|
+
return "break"
|
|
1950
|
+
if ks == "d":
|
|
1951
|
+
self._nhan_ban()
|
|
1952
|
+
return "break"
|
|
1953
|
+
if ks == "s":
|
|
1954
|
+
self._luu_toan_bo(True)
|
|
1955
|
+
return "break"
|
|
1956
|
+
|
|
1957
|
+
def _di_chuyen_bang_phim(self, ks, e):
|
|
1958
|
+
"""Mũi tên: 1px, Shift = 10px — KHÔNG snap (giữ nguyên độ chính xác)."""
|
|
1959
|
+
if not self.chon:
|
|
1960
|
+
return
|
|
1961
|
+
buoc = 10 if (e.state & 0x0001) else 1
|
|
1962
|
+
d = {"left": (-buoc, 0), "right": (buoc, 0),
|
|
1963
|
+
"up": (0, -buoc), "down": (0, buoc)}[ks]
|
|
1964
|
+
self._chup_truoc_khi_sua()
|
|
1965
|
+
for cid in self.chon:
|
|
1966
|
+
c = self._comp(cid)
|
|
1967
|
+
if c:
|
|
1968
|
+
c["x"] += d[0]
|
|
1969
|
+
c["y"] += d[1]
|
|
1970
|
+
self._ve_comp_tu_dong(cid) # vẽ lại đúng vị trí mới
|
|
1971
|
+
self._cap_nhat_vien_chon()
|
|
1972
|
+
self.dirty = True
|
|
1973
|
+
self._chup_ve_cuoi_hang()
|
|
1974
|
+
|