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 ADDED
@@ -0,0 +1,71 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ FlexGUI — thư viện GUI Python kéo-thả-thiết-kế trên nền tkinter.
4
+
5
+ pip install flexgui
6
+
7
+ ĐỒNG GÓI 2 PHẦN TRONG 1 THƯ VIỆN:
8
+
9
+ (A) THƯ VIỆN RUNTIME — cú pháp gọn, định vị tuyệt đối bằng place(x, y):
10
+
11
+ import flexgui
12
+ from flexgui import App, Label, Button, Input, Checkbox, Text
13
+
14
+ app = App("Ứng dụng của tôi", 860, 560)
15
+ Label("Xin chào!", 40, 30)
16
+ Button("Bấm tôi", 40, 80)
17
+ Input(40, 130, width=26)
18
+ Checkbox("Đồng ý", 40, 180)
19
+ Text(40, 230, width=42, height=7)
20
+ app.run()
21
+
22
+ (B) CHẾ ĐỘ THIẾT KẾ (FlexGUI Studio) — mở bằng MỘT trong các cách:
23
+
24
+ import flexgui
25
+ flexgui.setup() # cách 1 — mở Studio ngay
26
+
27
+ from flexgui import design # cách 2 — tương đương
28
+ design.mode = True
29
+
30
+ python myapp.py --design # cách 3 — cờ dòng lệnh
31
+
32
+ → Chạy file ở design mode: Studio tự tạo GIAO DIỆN TRỐNG (hoặc nạp
33
+ myapp.flexgui.json đã lưu). Kéo thả thành phần, chỉnh thuộc tính,
34
+ viết trigger bằng mini VS Code ngay trong Studio HOẶC mở file
35
+ myapp.triggers.py bằng VS Code thật — cả hai sửa CÙNG một file.
36
+
37
+ → Chạy thật: đổi setup() thành flexgui.run(__file__) rồi chạy lại.
38
+
39
+ Truy cập widget theo ID (tự động, tái sử dụng ID khi xóa):
40
+ from flexgui import label, button, input, checkbox, text
41
+ label[1].set_text("Xin chào!") # đọc/ghi thuộc tính trực tiếp
42
+
43
+ Chi tiết: README.md đi kèm package.
44
+ """
45
+
46
+ from .mode import design
47
+ from .registry import label, button, input, checkbox, text
48
+ from .triggers import (
49
+ on_click, on_change, on_enter, when, every, on_init,
50
+ refresh, set_title, set_size, set_bg, alert, confirm, prompt, close_app,
51
+ )
52
+ from .widgets import Label, Button, Input, Checkbox, Text
53
+ from .app import App
54
+ from .runtime import run, setup, run_embedded
55
+
56
+ __version__ = "1.0.0"
57
+ __author__ = "FlexGUI Team"
58
+
59
+ __all__ = [
60
+ # cửa sổ & widget
61
+ "App", "Label", "Button", "Input", "Checkbox", "Text",
62
+ # vùng tên theo ID
63
+ "label", "button", "input", "checkbox", "text",
64
+ # chế độ thiết kế & chạy
65
+ "design", "setup", "run", "run_embedded",
66
+ # decorator trigger
67
+ "on_click", "on_change", "on_enter", "when", "every", "on_init",
68
+ # lệnh app
69
+ "refresh", "set_title", "set_size", "set_bg",
70
+ "alert", "confirm", "prompt", "close_app",
71
+ ]
flexgui/app.py ADDED
@@ -0,0 +1,221 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ App — cửa sổ ứng dụng + đăng ký widget (ID tái sử dụng) + vòng đời chạy.
4
+
5
+ from flexgui import App, Label, Button
6
+
7
+ app = App("Ứng dụng của tôi", 860, 560)
8
+ Label("Xin chào!", 40, 30)
9
+ app.run() # chạy thật
10
+ app.run(design_mode=True) # chuyển sang FlexGUI Studio
11
+
12
+ ID tái sử dụng (từ phiên bản trước, giữ nguyên): widget bị xóa trả ID
13
+ về pool; widget tạo kế tiếp nhận ID nhỏ nhất vừa rảnh — ví dụ xóa
14
+ input[3] rồi thêm widget mới thì widget mới đúng là [3].
15
+ """
16
+
17
+ import heapq
18
+ import sys
19
+ from pathlib import Path
20
+
21
+ import tkinter as tk
22
+
23
+ from . import registry as _reg
24
+ from .mode import design
25
+ from .theme import MAU_APP
26
+ from .triggers import TriggerEngine
27
+
28
+
29
+ def _doc_co_design(argv=None):
30
+ """Đọc cờ --design từ dòng lệnh (giữ tương thích thói quen cũ)."""
31
+ argv = sys.argv if argv is None else argv
32
+ return any(str(a).lower() in ("--design", "-design") for a in argv)
33
+
34
+
35
+ class App:
36
+ """Cửa sổ ứng dụng.
37
+
38
+ Tham số:
39
+ title, width, height : tiêu đề + kích thước cửa sổ
40
+ bg : màu nền (mặc định #f5f6fa)
41
+ goc : None = cửa sổ chính (tk.Tk)
42
+ tk widget = cửa sổ con (Toplevel) — dùng khi
43
+ Studio mở bản XEM TRƯỚC, không chặn Studio.
44
+ """
45
+
46
+ def __init__(self, title="Ứng dụng FlexGUI", width=860, height=560,
47
+ bg=None, goc=None):
48
+ self.title = title
49
+ self.width = int(width)
50
+ self.height = int(height)
51
+ self._nen = bg or MAU_APP["nen_mac_dinh"]
52
+ self.xem_truoc = goc is not None # True = bản xem trước từ Studio
53
+ self.dang_thiet_ke = False
54
+
55
+ if goc is None:
56
+ self.root = tk.Tk()
57
+ else:
58
+ self.root = tk.Toplevel(goc)
59
+ self.root.title(title)
60
+ self.root.geometry(f"{self.width}x{self.height}")
61
+ self.root.minsize(320, 200)
62
+ self.root.configure(bg=self._nen)
63
+ self.khung = tk.Frame(self.root, bg=self._nen)
64
+ self.khung.pack(fill="both", expand=True)
65
+
66
+ # Sổ đăng ký widget
67
+ self.widgets = {} # id -> widget (mọi loại)
68
+ self.theo_loai = { # loại -> {id -> widget}
69
+ "label": {}, "button": {}, "input": {}, "checkbox": {}, "text": {},
70
+ }
71
+ self._id_tiep_theo = 1 # ID mới kế tiếp (pool rỗng)
72
+ self._id_tu_do = [] # pool ID đã xóa — TÁI SỬ DỤNG
73
+
74
+ # Hệ thống trigger + cổng chặn khi ở chế độ thiết kế
75
+ self.dong_co = TriggerEngine(self)
76
+ self.trigger_nguon = {}
77
+ self._cho_phep_thu_kich = False
78
+ self.designer = None
79
+
80
+ _reg._dat_app(self)
81
+
82
+ # ------------------------------------------------------------ vòng đời
83
+ def con_song(self):
84
+ """Cửa sổ còn tồn tại không (để registry tự dọn app chết)."""
85
+ try:
86
+ return bool(self.root.winfo_exists())
87
+ except Exception:
88
+ return False
89
+
90
+ # ------------------------------------------------------------ đăng ký ID
91
+ def _dang_ky(self, wgt, id=None):
92
+ """Cấp ID duy nhất — ưu tiên ID nhỏ nhất trong pool TÁI SỬ DỤNG."""
93
+ if id is None:
94
+ if self._id_tu_do:
95
+ id = heapq.heappop(self._id_tu_do)
96
+ else:
97
+ id = self._id_tiep_theo
98
+ id = int(id)
99
+ if id in self.widgets:
100
+ raise ValueError(f"ID {id} đã được widget khác sử dụng!")
101
+ if id in self._id_tu_do: # id tường minh trùng id đang rảnh
102
+ self._id_tu_do.remove(id)
103
+ heapq.heapify(self._id_tu_do)
104
+ if id >= self._id_tiep_theo:
105
+ self._id_tiep_theo = id + 1
106
+ self.widgets[id] = wgt
107
+ self.theo_loai[wgt.loai][id] = wgt
108
+ return id
109
+
110
+ def _huy_dang_ky(self, wgt):
111
+ """Gỡ widget khỏi sổ đăng ký — ID trả về pool TÁI SỬ DỤNG."""
112
+ self.widgets.pop(wgt.id, None)
113
+ self.theo_loai.get(wgt.loai, {}).pop(wgt.id, None)
114
+ self.trigger_nguon.pop(wgt.id, None)
115
+ self.dong_co._click.pop(wgt.id, None)
116
+ self.dong_co._change.pop(wgt.id, None)
117
+ self.dong_co._enter.pop(wgt.id, None)
118
+ if wgt.id is not None and wgt.id not in self._id_tu_do:
119
+ heapq.heappush(self._id_tu_do, wgt.id)
120
+
121
+ # ------------------------------------------------------------ tiện ích
122
+ def bg(self, mau):
123
+ """Đổi màu nền cửa sổ."""
124
+ self._nen = mau
125
+ self.root.config(bg=mau)
126
+ self.khung.config(bg=mau)
127
+ for w in self.widgets.values():
128
+ try:
129
+ if w._mau_nen is None and w.loai in ("label", "checkbox"):
130
+ w.tkw.config(bg=mau)
131
+ if w.loai == "checkbox":
132
+ w.tkw.config(activebackground=mau)
133
+ except Exception:
134
+ pass
135
+
136
+ def dat_trigger_chung(self, code):
137
+ """Gán đoạn code trigger CHUNG (giữ tương thích bản cũ)."""
138
+ self.trigger_nguon["global"] = code or ""
139
+
140
+ def lay_thiet_ke(self):
141
+ """Chụp toàn bộ app thành dict thiết kế (dùng khi --design trên
142
+ app viết bằng code thuần: chuyển code thành thiết kế chỉnh được)."""
143
+ return {
144
+ "window": {
145
+ "title": self.title,
146
+ "width": self.width,
147
+ "height": self.height,
148
+ "bg": self._nen,
149
+ },
150
+ "components": [w.lay_du_lieu()
151
+ for w in self.widgets.values()],
152
+ }
153
+
154
+ # ------------------------------------------------------------ chạy
155
+ def run(self, design_mode=None, chay_mainloop=True):
156
+ """Khởi động ứng dụng.
157
+
158
+ design_mode=None : tự đọc design.mode (flexgui.design) + cờ --design
159
+ design_mode=True : chuyển sang FlexGUI Studio (mang theo toàn bộ
160
+ widget đang có — thiết kế tiếp bằng kéo thả)
161
+ chay_mainloop=False : chỉ kích hoạt engine, không vào mainloop
162
+ (dùng cho bản XEM TRƯỚC của Studio).
163
+ """
164
+ if design_mode is None:
165
+ design_mode = design.mode or _doc_co_design()
166
+ if design_mode:
167
+ from .designer import Studio
168
+ thiet_ke = self.lay_thiet_ke()
169
+ design._danh_dau_da_mo()
170
+ # file .py của người dùng (nếu chạy python myapp.py --design)
171
+ file_py = None
172
+ try:
173
+ if sys.argv and sys.argv[0].endswith(".py") \
174
+ and Path(sys.argv[0]).exists():
175
+ file_py = Path(sys.argv[0]).resolve()
176
+ except Exception:
177
+ pass
178
+ # Đóng cửa sổ code thuần (chưa vào mainloop nên chỉ cần destroy)
179
+ self.dong_co.dung()
180
+ try:
181
+ self.root.destroy()
182
+ except Exception:
183
+ pass
184
+ _reg._dat_app(None)
185
+ st = Studio(duong_dan=file_py, thiet_ke_ban_dau=thiet_ke)
186
+ st.chay()
187
+ return st
188
+
189
+ self._cho_phep_thu_kich = True
190
+ self.dong_co.bat_dau()
191
+
192
+ if self.xem_truoc:
193
+ # Bản xem trước từ Studio: đóng = dọn dẹp + destroy Toplevel
194
+ self.root.protocol("WM_DELETE_WINDOW", self._dong_xem_truoc)
195
+ else:
196
+ self.root.protocol("WM_DELETE_WINDOW", self._dong_app)
197
+
198
+ if chay_mainloop:
199
+ self.root.mainloop()
200
+
201
+ def _dong_app(self):
202
+ """Đóng ứng dụng thật: dừng engine, thoát mainloop rồi hủy."""
203
+ self.dong_co.dung()
204
+ try:
205
+ self.root.quit()
206
+ except Exception:
207
+ pass
208
+ try:
209
+ self.root.destroy()
210
+ except Exception:
211
+ pass
212
+
213
+ def _dong_xem_truoc(self):
214
+ """Đóng bản XEM TRƯỚC (Studio vẫn sống)."""
215
+ self.dong_co.dung()
216
+ try:
217
+ self.root.destroy()
218
+ except Exception:
219
+ pass
220
+ if _reg._app_hien_tai() is self:
221
+ _reg._dat_app(None)
flexgui/builder.py ADDED
@@ -0,0 +1,211 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Builder — xuất file chạy đơn & đóng gói .exe bằng PyInstaller.
4
+
5
+ • xuat_file_dung() : gộp thiết kế + trigger thành MỘT file .py duy nhất
6
+ (chạy được ngay: cần đã cài pip install flexgui)
7
+ • TrinhDongGoi : chạy pyinstaller trong thread nền, log realtime,
8
+ tự cài pyinstaller nếu thiếu
9
+ """
10
+
11
+ import os
12
+ import queue
13
+ import subprocess
14
+ import sys
15
+ import threading
16
+
17
+ import tkinter as tk
18
+ from tkinter import ttk, messagebox
19
+
20
+ from .theme import MAU, font_ui, font_mono
21
+
22
+
23
+ def xuat_file_dung(duong_dan, thiet_ke, nguon_trigger):
24
+ """Ghi file .py tự chạy (nhúng toàn bộ thiết kế + trigger).
25
+
26
+ duong_dan : file .py đích (ví dụ myapp_app.py)
27
+ thiet_ke : dict thiết kế (window + components)
28
+ nguon_trigger : chuỗi code trigger (file .triggers.py)
29
+ Trả về đường dẫn đã ghi.
30
+ """
31
+ import pprint
32
+
33
+ ten = os.path.splitext(os.path.basename(duong_dan))[0]
34
+ body = (
35
+ f"# {'=' * 74}\n"
36
+ f"# {ten} — sinh bởi FlexGUI Studio\n"
37
+ f"# Chạy: python {os.path.basename(duong_dan)}\n"
38
+ f"# Cần: pip install flexgui\n"
39
+ f"# {'=' * 74}\n"
40
+ f"import flexgui\n"
41
+ f"\n"
42
+ f"_THIET_KE = "
43
+ )
44
+ # pprint.pformat sinh literal PYTHON đúng (None/True/False, chuỗi '...')
45
+ body += pprint.pformat(thiet_ke, width=68)
46
+ body += "\n\n_TRIGGERS = "
47
+ body += pprint.pformat(nguon_trigger or "", width=68)
48
+ body += "\n\nflexgui.run_embedded(_THIET_KE, _TRIGGERS)\n"
49
+
50
+ with open(duong_dan, "w", encoding="utf-8") as f:
51
+ f.write(body)
52
+ return duong_dan
53
+
54
+
55
+ class TrinhDongGoi:
56
+ """Đóng gói file .py thành file thực thi duy nhất (.exe trên Windows).
57
+
58
+ Quy trình tự động trong thread nền (không đơ giao diện):
59
+ 1. Kiểm tra PyInstaller → thiếu thì pip install tự động
60
+ 2. pyinstaller --onefile --windowed --name <tên> <file.py>
61
+ 3. Kết quả: dist/<tên>.exe — log hiển thị trực tiếp trong cửa sổ
62
+ """
63
+
64
+ def __init__(self, goc, ten="ung_dung"):
65
+ self.goc = goc # cửa sổ cha (root Studio)
66
+ self.ten = ten
67
+ self.hang_doi = queue.Queue()
68
+ self._xong = False
69
+ self._thanh_cong = False
70
+ self._duong_dan_dist = None
71
+
72
+ # ------------------------------------------------------------ giao diện
73
+ def chay(self, duong_dan):
74
+ """Mở cửa sổ build rồi chạy build trong thread nền."""
75
+ cs = tk.Toplevel(self.goc)
76
+ cs.title(f"Đóng gói .exe — {self.ten}")
77
+ cs.geometry("780x460")
78
+ cs.configure(bg=MAU["nen_panel"])
79
+ cs.transient(self.goc)
80
+ self.cua_so = cs
81
+
82
+ tk.Label(cs, text="⏳ PyInstaller đang làm việc — có thể mất 1–3 phút "
83
+ "lần đầu (nếu phải cài đặt).",
84
+ bg=MAU["nen_panel"], fg=MAU["chu"], anchor="w",
85
+ font=font_ui(10)).pack(fill="x", padx=14, pady=(12, 6))
86
+
87
+ self.thanh_tien = ttk.Progressbar(cs, mode="indeterminate")
88
+ self.thanh_tien.pack(fill="x", padx=14)
89
+ self.thanh_tien.start(14)
90
+
91
+ self.nhat_ky = tk.Text(cs, bg="#141414", fg=MAU["chu"], bd=0,
92
+ font=font_mono(10), wrap="none",
93
+ state="disabled")
94
+ self.nhat_ky.pack(fill="both", expand=True, padx=14, pady=(6, 6))
95
+
96
+ duoi = tk.Frame(cs, bg=MAU["nen_panel"])
97
+ duoi.pack(fill="x", padx=14, pady=(0, 12))
98
+ tk.Button(duoi, text="📂 Mở thư mục dist", state="disabled",
99
+ bg=MAU["nen_nut"], fg=MAU["chu_mo"], bd=0,
100
+ padx=12, pady=4, font=font_ui(9), cursor="hand2",
101
+ command=self._mo_thu_muc).pack(side="right")
102
+
103
+ threading.Thread(target=self._thuc_thi, args=(duong_dan,),
104
+ daemon=True).start()
105
+ self._do_hang()
106
+
107
+ def _ghi(self, s):
108
+ """Thread nền ghi log vào queue (thread-safe)."""
109
+ self.hang_doi.put(s)
110
+
111
+ def _do_hang(self):
112
+ """Vòng poll 150ms: rút log từ queue, phát hiện trạng thái kết thúc."""
113
+ try:
114
+ while True:
115
+ dong = self.hang_doi.get_nowait()
116
+ self.nhat_ky.config(state="normal")
117
+ self.nhat_ky.insert("end", dong)
118
+ self.nhat_ky.see("end")
119
+ self.nhat_ky.config(state="disabled")
120
+ except queue.Empty:
121
+ pass
122
+ if self._xong:
123
+ self.thanh_tien.stop()
124
+ if self._thanh_cong:
125
+ messagebox.showinfo(
126
+ "Build xong", "Đã tạo file thực thi thành công!\n\n"
127
+ f"{self._duong_dan_dist}", parent=self.cua_so)
128
+ else:
129
+ self.cua_so.after(150, self._do_hang)
130
+
131
+ def _mo_thu_muc(self):
132
+ """Mở thư mục chứa kết quả (đa nền tảng)."""
133
+ thu_muc = os.path.dirname(self._duong_dan_dist or "")
134
+ try:
135
+ if sys.platform.startswith("win"):
136
+ os.startfile(thu_muc) # type: ignore[attr-defined]
137
+ elif sys.platform == "darwin":
138
+ subprocess.Popen(["open", thu_muc])
139
+ else:
140
+ subprocess.Popen(["xdg-open", thu_muc])
141
+ except Exception:
142
+ messagebox.showinfo("Thư mục", thu_muc, parent=self.cua_so)
143
+
144
+ # ------------------------------------------------------------ quy trình
145
+ def _moi_truong_build(self):
146
+ """Môi trường cho lệnh build.
147
+
148
+ Trên Linux với Python "di động" (uv, pyenv...), libtcl/libtk nằm
149
+ ngoài LD_LIBRARY_PATH mặc định nên PyInstaller không nhúng được —
150
+ thêm thư mục đó vào để file chạy được trên mọi máy.
151
+ """
152
+ import glob
153
+ mt = os.environ.copy()
154
+ if os.name == "posix":
155
+ thu_muc = os.path.join(sys.base_prefix, "lib")
156
+ if os.path.isdir(thu_muc) and \
157
+ glob.glob(os.path.join(thu_muc, "libtcl*")):
158
+ mt["LD_LIBRARY_PATH"] = (thu_muc + os.pathsep +
159
+ mt.get("LD_LIBRARY_PATH", ""))
160
+ return mt
161
+
162
+ def _thuc_thi(self, duong_dan):
163
+ """Toàn bộ pipeline build — chạy trong thread nền."""
164
+ try:
165
+ self._ghi("► Bước 1/3: Kiểm tra PyInstaller...\n")
166
+ r = subprocess.run([sys.executable, "-m", "PyInstaller",
167
+ "--version"], capture_output=True, text=True)
168
+ if r.returncode != 0:
169
+ self._ghi(" Chưa có PyInstaller → tự cài bằng pip...\n")
170
+ p = subprocess.run(
171
+ [sys.executable, "-m", "pip", "install", "--quiet",
172
+ "pyinstaller"], capture_output=True, text=True)
173
+ self._ghi((p.stdout or "") + (p.stderr or "") + "\n")
174
+ if p.returncode != 0:
175
+ self._ghi("✘ Cài PyInstaller thất bại — kiểm tra mạng "
176
+ "hoặc cài thủ công: pip install pyinstaller\n")
177
+ return
178
+ else:
179
+ self._ghi(f" Đã có PyInstaller {r.stdout.strip()}\n")
180
+
181
+ self._ghi("► Bước 2/3: Đang build (thường 1–3 phút)...\n")
182
+ thu_muc = os.path.dirname(os.path.abspath(duong_dan))
183
+ # --paths: bảo đảm PyInstaller luôn tìm thấy package flexgui
184
+ # (cần khi cài kiểu editable / pip install -e)
185
+ duong_goi = os.path.dirname(
186
+ os.path.dirname(os.path.abspath(__file__)))
187
+ lenh = [sys.executable, "-m", "PyInstaller", "--onefile",
188
+ "--windowed", "--name", self.ten,
189
+ "--paths", duong_goi,
190
+ os.path.basename(duong_dan)]
191
+ tien = subprocess.Popen(lenh, cwd=thu_muc,
192
+ stdout=subprocess.PIPE,
193
+ stderr=subprocess.STDOUT, text=True,
194
+ bufsize=1, env=self._moi_truong_build())
195
+ for dong in tien.stdout:
196
+ self._ghi(dong)
197
+ ma = tien.wait()
198
+
199
+ ten_duoi = self.ten + (".exe" if os.name == "nt" else "")
200
+ self._duong_dan_dist = os.path.join(thu_muc, "dist", ten_duoi)
201
+ if ma == 0:
202
+ self._thanh_cong = True
203
+ self._ghi(f"\n✔✔✔ ĐÃ XONG: {self._duong_dan_dist}\n")
204
+ else:
205
+ self._ghi(f"\n✘ BUILD THẤT BẠI (mã thoát {ma}) — xem log "
206
+ "phía trên.\n")
207
+ except Exception:
208
+ import traceback
209
+ self._ghi("\n✘ LỖI BẤT NGỜ:\n" + traceback.format_exc() + "\n")
210
+ finally:
211
+ self._xong = True