thonny-ikode 1.0.2__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.
Potentially problematic release.
This version of thonny-ikode might be problematic. Click here for more details.
- thonny_ikode-1.0.2.dist-info/METADATA +69 -0
- thonny_ikode-1.0.2.dist-info/RECORD +9 -0
- thonny_ikode-1.0.2.dist-info/WHEEL +5 -0
- thonny_ikode-1.0.2.dist-info/licenses/LICENSE +19 -0
- thonny_ikode-1.0.2.dist-info/top_level.txt +1 -0
- thonnycontrib/ikode/__init__.py +1213 -0
- thonnycontrib/ikode/res/logo.png +0 -0
- thonnycontrib/ikode/res/qt_16.png +0 -0
- thonnycontrib/ikode/res/qt_32.png +0 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: thonny-ikode
|
|
3
|
+
Version: 1.0.2
|
|
4
|
+
Summary: Thonny plugin by IKODE: generate PyQt5 loadUi code and preview Qt Designer .ui forms
|
|
5
|
+
Author-email: Bejaoui Dhia <admin@ikode.tn>
|
|
6
|
+
Maintainer-email: IKODE <admin@ikode.tn>
|
|
7
|
+
License-Expression: LicenseRef-Proprietary
|
|
8
|
+
Project-URL: Homepage, https://ikode.tn
|
|
9
|
+
Keywords: thonny,thonny-plugin,PyQt5,Qt Designer,education,IKODE
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: X11 Applications
|
|
12
|
+
Classifier: Environment :: Win32 (MS Windows)
|
|
13
|
+
Classifier: Environment :: MacOS X
|
|
14
|
+
Classifier: Intended Audience :: Education
|
|
15
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
16
|
+
Classifier: Natural Language :: French
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
20
|
+
Classifier: Topic :: Education
|
|
21
|
+
Classifier: Topic :: Software Development :: User Interfaces
|
|
22
|
+
Requires-Python: >=3.8
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: thonny>=4.0
|
|
26
|
+
Provides-Extra: preview
|
|
27
|
+
Requires-Dist: Pillow; extra == "preview"
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# thonny-ikode
|
|
31
|
+
|
|
32
|
+
Thonny plugin by IKODE for working with PyQt5 and Qt Designer `.ui` files.
|
|
33
|
+
|
|
34
|
+
## Features
|
|
35
|
+
|
|
36
|
+
- **PyQt** button (`Ctrl+Shift+B`): pick a `.ui` file, choose the form variable name, and generate the `loadUi` code with a `*_click` handler for every `QPushButton`.
|
|
37
|
+
- **Éléments UI** button: interactive live preview of a `.ui` form. Hover a widget to highlight it, click to copy `form.widgetName`.
|
|
38
|
+
- **Tools > Oublier le nom du formulaire**: resets the remembered form name.
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
From Thonny: *Tools > Open system shell*, then:
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
pip install thonny-ikode
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
or from this folder / a wheel:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
pip install .
|
|
52
|
+
pip install thonny_ikode-1.0.2-py3-none-any.whl
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Optional (sharper image previews): `pip install Pillow`
|
|
56
|
+
|
|
57
|
+
Restart Thonny afterwards.
|
|
58
|
+
|
|
59
|
+
## Notes
|
|
60
|
+
|
|
61
|
+
- Generated code needs `PyQt5` in the interpreter used to run scripts.
|
|
62
|
+
- On startup the plugin sets the working folder to `Desktop/IKODE` and does not reopen previous files.
|
|
63
|
+
|
|
64
|
+
## Build
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
python -m pip install build
|
|
68
|
+
python -m build
|
|
69
|
+
```
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
thonny_ikode-1.0.2.dist-info/licenses/LICENSE,sha256=Tcxk4taP5vz_Yn3tjrYqSUN2cJxsNhqs_XMOquEpcfc,942
|
|
2
|
+
thonnycontrib/ikode/__init__.py,sha256=njPomNx-MG7d-SRfGeQJsTjQxj709CImwWWA4BCd3cA,48911
|
|
3
|
+
thonnycontrib/ikode/res/logo.png,sha256=cmVSlHt2Ak8VDQ6r6IMjsBBx3ZECUxDkYmJIoGlny-Y,106009
|
|
4
|
+
thonnycontrib/ikode/res/qt_16.png,sha256=zRGwNxrYXruCHJTbv85ki1RWwbU0jTO_rC_APqzUV2I,732
|
|
5
|
+
thonnycontrib/ikode/res/qt_32.png,sha256=SxhSrjvi9r8EeRt90HhJcsKzN9r2uaPEkHLNc_h3Bow,1272
|
|
6
|
+
thonny_ikode-1.0.2.dist-info/METADATA,sha256=mE_LDh-anUPO6bRbr8rbcT0hdn9U0vs7oU5gWgImAIk,2212
|
|
7
|
+
thonny_ikode-1.0.2.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
8
|
+
thonny_ikode-1.0.2.dist-info/top_level.txt,sha256=thF9WQtY_lcNKOOR4SQeeWLAVWb4i8XhdGC7pEMAOHY,14
|
|
9
|
+
thonny_ikode-1.0.2.dist-info/RECORD,,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2026 Bejaoui Dhia / IKODE (admin@ikode.tn). All rights reserved.
|
|
2
|
+
|
|
3
|
+
This software ("thonny-ikode") is provided free of charge for personal and
|
|
4
|
+
educational use with the Thonny IDE.
|
|
5
|
+
|
|
6
|
+
You MAY:
|
|
7
|
+
- install and use it on any number of computers, including school labs;
|
|
8
|
+
- share the unmodified package (as downloaded from PyPI or ikode.tn).
|
|
9
|
+
|
|
10
|
+
You MAY NOT, without prior written permission from IKODE (admin@ikode.tn):
|
|
11
|
+
- modify, decompile, or create derivative works from it;
|
|
12
|
+
- remove or alter the IKODE name, logo, credits, or copyright notices;
|
|
13
|
+
- sell, sublicense, or redistribute it as part of another product.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL IKODE
|
|
18
|
+
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY ARISING FROM THE USE OF
|
|
19
|
+
THE SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
thonnycontrib
|
|
@@ -0,0 +1,1213 @@
|
|
|
1
|
+
__version__ = "1.0.2"
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import subprocess
|
|
6
|
+
import keyword
|
|
7
|
+
import tempfile
|
|
8
|
+
import tkinter as tk
|
|
9
|
+
import webbrowser
|
|
10
|
+
from tkinter import messagebox, ttk
|
|
11
|
+
from thonny import get_workbench
|
|
12
|
+
from thonny.languages import tr
|
|
13
|
+
from thonny.ui_utils import select_sequence,askopenfilename
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
from xml.dom import minidom
|
|
17
|
+
global qt_ui_file
|
|
18
|
+
qt_ui_file =""
|
|
19
|
+
qt_form_name = ""
|
|
20
|
+
FORM_NAME_OPTION = "tunisiaschools.form_name"
|
|
21
|
+
_toolbar_logo_path = None
|
|
22
|
+
_live_preview_dialog = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _make_toolbar_logo(logo_path):
|
|
26
|
+
global _toolbar_logo_path
|
|
27
|
+
if _toolbar_logo_path and os.path.isfile(_toolbar_logo_path):
|
|
28
|
+
return _toolbar_logo_path
|
|
29
|
+
try:
|
|
30
|
+
logo = tk.PhotoImage(file=logo_path)
|
|
31
|
+
factor = max(1, (logo.width() + 23) // 24, (logo.height() + 23) // 24)
|
|
32
|
+
if factor == 1:
|
|
33
|
+
return logo_path
|
|
34
|
+
small_logo = logo.subsample(factor, factor)
|
|
35
|
+
icon_path = os.path.join(tempfile.gettempdir(), "thonny_tunisiaschools_logo_24.png")
|
|
36
|
+
small_logo.write(icon_path, format="png")
|
|
37
|
+
_toolbar_logo_path = icon_path
|
|
38
|
+
return icon_path
|
|
39
|
+
except (tk.TclError, OSError):
|
|
40
|
+
return logo_path
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def ask_form_name(parent):
|
|
44
|
+
workbench = get_workbench()
|
|
45
|
+
saved_form_name = workbench.get_option(FORM_NAME_OPTION, "")
|
|
46
|
+
if saved_form_name:
|
|
47
|
+
return saved_form_name
|
|
48
|
+
|
|
49
|
+
dialog = tk.Toplevel(parent)
|
|
50
|
+
dialog.title("Nom du formulaire")
|
|
51
|
+
dialog.resizable(False, False)
|
|
52
|
+
dialog.transient(parent)
|
|
53
|
+
dialog.configure(bg="#f4f6fb")
|
|
54
|
+
|
|
55
|
+
parent.update_idletasks()
|
|
56
|
+
screen_width = dialog.winfo_screenwidth()
|
|
57
|
+
screen_height = dialog.winfo_screenheight()
|
|
58
|
+
width = min(410, screen_width - 32)
|
|
59
|
+
height = screen_height - 64
|
|
60
|
+
|
|
61
|
+
card = tk.Frame(dialog, bg="white", padx=20, pady=16)
|
|
62
|
+
card.pack(fill="both", expand=True, padx=10, pady=10)
|
|
63
|
+
tk.Label(
|
|
64
|
+
card, text="Nom du formulaire", bg="white", fg="#172033",
|
|
65
|
+
font=("Segoe UI", 17, "bold")
|
|
66
|
+
).pack(anchor="w")
|
|
67
|
+
tk.Label(
|
|
68
|
+
card, text="Choisissez le nom de la variable Python à générer.",
|
|
69
|
+
bg="white", fg="#667085", font=("Segoe UI", 10),
|
|
70
|
+
wraplength=width - 80, justify="left"
|
|
71
|
+
).pack(anchor="w", pady=(4, 10))
|
|
72
|
+
|
|
73
|
+
name_var = tk.StringVar(value="win")
|
|
74
|
+
name_entry = ttk.Entry(card, textvariable=name_var, font=("Segoe UI", 11))
|
|
75
|
+
name_entry.pack(fill="x", ipady=7)
|
|
76
|
+
remember_var = tk.BooleanVar(value=False)
|
|
77
|
+
tk.Checkbutton(
|
|
78
|
+
card, text="Ne plus me demander (réutiliser ce nom)",
|
|
79
|
+
variable=remember_var, bg="white", fg="#344054",
|
|
80
|
+
activebackground="white", selectcolor="white",
|
|
81
|
+
font=("Segoe UI", 9), cursor="hand2"
|
|
82
|
+
).pack(anchor="w", pady=(8, 0))
|
|
83
|
+
error_label = tk.Label(card, text="", bg="white", fg="#c0392b", font=("Segoe UI", 9))
|
|
84
|
+
error_label.pack(anchor="w", pady=(5, 0))
|
|
85
|
+
|
|
86
|
+
result = {"name": None}
|
|
87
|
+
|
|
88
|
+
def validate_form_name(show_success=False):
|
|
89
|
+
form_name = name_var.get().strip()
|
|
90
|
+
is_valid = form_name.isidentifier() and not keyword.iskeyword(form_name)
|
|
91
|
+
if is_valid:
|
|
92
|
+
error_label.config(
|
|
93
|
+
text="Nom valide." if show_success else "", fg="#27864a"
|
|
94
|
+
)
|
|
95
|
+
else:
|
|
96
|
+
error_label.config(
|
|
97
|
+
text="Entrez un nom de variable Python valide.", fg="#c0392b"
|
|
98
|
+
)
|
|
99
|
+
return is_valid
|
|
100
|
+
|
|
101
|
+
def submit(event=None):
|
|
102
|
+
form_name = name_var.get().strip()
|
|
103
|
+
if not validate_form_name():
|
|
104
|
+
name_entry.focus_set()
|
|
105
|
+
return
|
|
106
|
+
result["name"] = form_name
|
|
107
|
+
if remember_var.get():
|
|
108
|
+
workbench.set_option(FORM_NAME_OPTION, form_name)
|
|
109
|
+
dialog.destroy()
|
|
110
|
+
|
|
111
|
+
name_var.trace_add("write", lambda *_args: validate_form_name(show_success=True))
|
|
112
|
+
|
|
113
|
+
buttons = tk.Frame(card, bg="white")
|
|
114
|
+
buttons.pack(fill="x", pady=(10, 0))
|
|
115
|
+
tk.Button(
|
|
116
|
+
buttons, text="Annuler", command=dialog.destroy, relief="flat",
|
|
117
|
+
bg="#eef1f6", fg="#344054", activebackground="#e2e7ef",
|
|
118
|
+
font=("Segoe UI", 10), padx=15, pady=8, cursor="hand2"
|
|
119
|
+
).pack(side="right")
|
|
120
|
+
tk.Button(
|
|
121
|
+
buttons, text="Confirmer", command=submit, relief="flat",
|
|
122
|
+
bg="#3659d9", fg="white", activebackground="#2948bc",
|
|
123
|
+
activeforeground="white", font=("Segoe UI", 10, "bold"),
|
|
124
|
+
padx=16, pady=8, cursor="hand2"
|
|
125
|
+
).pack(side="right", padx=(0, 9))
|
|
126
|
+
|
|
127
|
+
dialog.update_idletasks()
|
|
128
|
+
actual_width = min(max(width, dialog.winfo_reqwidth()), screen_width - 32)
|
|
129
|
+
actual_height = min(max(270, dialog.winfo_reqheight()), height)
|
|
130
|
+
x = parent.winfo_rootx() + (parent.winfo_width() - actual_width) // 2
|
|
131
|
+
y = parent.winfo_rooty() + (parent.winfo_height() - actual_height) // 2
|
|
132
|
+
x = min(max(x, 0), screen_width - actual_width)
|
|
133
|
+
y = min(max(y, 0), screen_height - actual_height)
|
|
134
|
+
dialog.geometry(f"{actual_width}x{actual_height}+{x}+{y}")
|
|
135
|
+
dialog.minsize(min(actual_width, 340), min(actual_height, 240))
|
|
136
|
+
|
|
137
|
+
dialog.bind("<Return>", submit)
|
|
138
|
+
dialog.bind("<Escape>", lambda event: dialog.destroy())
|
|
139
|
+
dialog.protocol("WM_DELETE_WINDOW", dialog.destroy)
|
|
140
|
+
dialog.grab_set()
|
|
141
|
+
name_entry.focus_set()
|
|
142
|
+
parent.wait_window(dialog)
|
|
143
|
+
return result["name"]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def confirm_replace_editor(parent):
|
|
147
|
+
dialog = tk.Toplevel(parent)
|
|
148
|
+
dialog.title("Remplacer le contenu ?")
|
|
149
|
+
dialog.resizable(False, False)
|
|
150
|
+
dialog.transient(parent)
|
|
151
|
+
dialog.configure(bg="#f4f6fb")
|
|
152
|
+
|
|
153
|
+
card = tk.Frame(dialog, bg="white", padx=22, pady=18)
|
|
154
|
+
card.pack(fill="both", expand=True, padx=10, pady=10)
|
|
155
|
+
tk.Label(
|
|
156
|
+
card, text="L’éditeur n’est pas vide", bg="white", fg="#172033",
|
|
157
|
+
font=("Segoe UI", 15, "bold")
|
|
158
|
+
).pack(anchor="w")
|
|
159
|
+
tk.Label(
|
|
160
|
+
card,
|
|
161
|
+
text="Effacer son contenu et insérer le code du formulaire ?",
|
|
162
|
+
bg="white", fg="#667085", font=("Segoe UI", 10),
|
|
163
|
+
wraplength=340, justify="left"
|
|
164
|
+
).pack(anchor="w", pady=(7, 16))
|
|
165
|
+
|
|
166
|
+
result = {"replace": False}
|
|
167
|
+
|
|
168
|
+
def accept():
|
|
169
|
+
result["replace"] = True
|
|
170
|
+
dialog.destroy()
|
|
171
|
+
|
|
172
|
+
buttons = tk.Frame(card, bg="white")
|
|
173
|
+
buttons.pack(fill="x")
|
|
174
|
+
tk.Button(
|
|
175
|
+
buttons, text="Non, conserver", command=dialog.destroy,
|
|
176
|
+
relief="flat", bg="#eef1f6", fg="#344054",
|
|
177
|
+
activebackground="#e2e7ef", font=("Segoe UI", 10),
|
|
178
|
+
padx=13, pady=8, cursor="hand2"
|
|
179
|
+
).pack(side="right")
|
|
180
|
+
tk.Button(
|
|
181
|
+
buttons, text="Effacer et insérer", command=accept,
|
|
182
|
+
relief="flat", bg="#3659d9", fg="white",
|
|
183
|
+
activebackground="#2948bc", activeforeground="white",
|
|
184
|
+
font=("Segoe UI", 10, "bold"), padx=14, pady=8,
|
|
185
|
+
cursor="hand2"
|
|
186
|
+
).pack(side="right", padx=(0, 8))
|
|
187
|
+
|
|
188
|
+
dialog.update_idletasks()
|
|
189
|
+
width, height = dialog.winfo_reqwidth(), dialog.winfo_reqheight()
|
|
190
|
+
screen_width, screen_height = dialog.winfo_screenwidth(), dialog.winfo_screenheight()
|
|
191
|
+
x = parent.winfo_rootx() + (parent.winfo_width() - width) // 2
|
|
192
|
+
y = parent.winfo_rooty() + (parent.winfo_height() - height) // 2
|
|
193
|
+
x = min(max(x, 0), screen_width - width)
|
|
194
|
+
y = min(max(y, 0), screen_height - height)
|
|
195
|
+
dialog.geometry(f"{width}x{height}+{x}+{y}")
|
|
196
|
+
dialog.bind("<Escape>", lambda event: dialog.destroy())
|
|
197
|
+
dialog.protocol("WM_DELETE_WINDOW", dialog.destroy)
|
|
198
|
+
dialog.grab_set()
|
|
199
|
+
parent.wait_window(dialog)
|
|
200
|
+
return result["replace"]
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _widget_property(widget, property_name):
|
|
204
|
+
for prop in widget.getElementsByTagName("property"):
|
|
205
|
+
if prop.parentNode is widget and prop.getAttribute("name") == property_name:
|
|
206
|
+
for value_node in prop.childNodes:
|
|
207
|
+
if value_node.nodeType == value_node.ELEMENT_NODE:
|
|
208
|
+
return "".join(
|
|
209
|
+
child.data for child in value_node.childNodes
|
|
210
|
+
if child.nodeType in (child.TEXT_NODE, child.CDATA_SECTION_NODE)
|
|
211
|
+
).strip()
|
|
212
|
+
return ""
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _widget_preview_colors(widget):
|
|
216
|
+
stylesheet = _widget_property(widget, "styleSheet")
|
|
217
|
+
colors = {}
|
|
218
|
+
for property_name, option in (("background-color", "background"), ("color", "foreground")):
|
|
219
|
+
match = re.search(
|
|
220
|
+
rf"(?:^|;)\s*{property_name}\s*:\s*(#[0-9a-fA-F]{{3,12}}|rgba?\([^)]*\)|[a-zA-Z]+)",
|
|
221
|
+
stylesheet,
|
|
222
|
+
re.IGNORECASE,
|
|
223
|
+
)
|
|
224
|
+
if match:
|
|
225
|
+
value = match.group(1).strip().lower()
|
|
226
|
+
if value.startswith("rgb"):
|
|
227
|
+
channels = re.findall(r"\d+(?:\.\d+)?", value)
|
|
228
|
+
if len(channels) >= 3:
|
|
229
|
+
rgb = [max(0, min(255, round(float(channel)))) for channel in channels[:3]]
|
|
230
|
+
colors[option] = "#%02x%02x%02x" % tuple(rgb)
|
|
231
|
+
elif value.startswith("#"):
|
|
232
|
+
digits = value[1:]
|
|
233
|
+
if len(digits) == 3:
|
|
234
|
+
digits = "".join(character * 2 for character in digits)
|
|
235
|
+
elif len(digits) == 4:
|
|
236
|
+
digits = "".join(character * 2 for character in digits[1:])
|
|
237
|
+
elif len(digits) == 8:
|
|
238
|
+
digits = digits[2:]
|
|
239
|
+
elif len(digits) == 12:
|
|
240
|
+
digits = digits[::4]
|
|
241
|
+
if len(digits) == 6:
|
|
242
|
+
colors[option] = "#" + digits
|
|
243
|
+
elif value in {
|
|
244
|
+
"black", "white", "red", "green", "blue", "yellow", "cyan",
|
|
245
|
+
"magenta", "gray", "grey", "orange", "purple", "pink",
|
|
246
|
+
"navy", "teal", "lime", "silver", "maroon", "olive",
|
|
247
|
+
}:
|
|
248
|
+
colors[option] = value
|
|
249
|
+
return colors
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _widget_font_size(widget, default_size=9):
|
|
253
|
+
font_spec = _widget_property(widget, "styleSheet")
|
|
254
|
+
match = re.search(r"font:\s*[^;]*?(\d+(?:\.\d+)?)pt", font_spec)
|
|
255
|
+
if match:
|
|
256
|
+
try:
|
|
257
|
+
return float(match.group(1))
|
|
258
|
+
except ValueError:
|
|
259
|
+
pass
|
|
260
|
+
return default_size
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _list_widget_items(widget):
|
|
264
|
+
values = []
|
|
265
|
+
for node in widget.childNodes:
|
|
266
|
+
if getattr(node, "tagName", None) != "item":
|
|
267
|
+
continue
|
|
268
|
+
for prop in node.getElementsByTagName("property"):
|
|
269
|
+
if prop.getAttribute("name") == "text":
|
|
270
|
+
strings = prop.getElementsByTagName("string")
|
|
271
|
+
if strings and strings[0].firstChild:
|
|
272
|
+
values.append(strings[0].firstChild.data)
|
|
273
|
+
return values
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _resolve_preview_image(widget, base_directory):
|
|
277
|
+
image_path = _widget_property(widget, "pixmap").strip()
|
|
278
|
+
if not image_path or image_path.startswith(":/"):
|
|
279
|
+
return None
|
|
280
|
+
if image_path.startswith("file://"):
|
|
281
|
+
image_path = image_path[7:]
|
|
282
|
+
if not os.path.isabs(image_path):
|
|
283
|
+
image_path = os.path.join(base_directory, image_path)
|
|
284
|
+
image_path = os.path.normpath(image_path)
|
|
285
|
+
return image_path if os.path.isfile(image_path) else None
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _loaded_ui_path(source_text, editor_path):
|
|
289
|
+
match = re.search(
|
|
290
|
+
r"\bloadUi\s*\(\s*(['\"])(.*?)\1",
|
|
291
|
+
source_text,
|
|
292
|
+
re.DOTALL,
|
|
293
|
+
)
|
|
294
|
+
if not match:
|
|
295
|
+
return None
|
|
296
|
+
referenced_path = match.group(2).strip()
|
|
297
|
+
if not referenced_path:
|
|
298
|
+
return None
|
|
299
|
+
if not os.path.isabs(referenced_path):
|
|
300
|
+
editor_directory = os.path.dirname(editor_path) if editor_path else ""
|
|
301
|
+
candidates = [
|
|
302
|
+
os.path.join(editor_directory, referenced_path) if editor_directory else "",
|
|
303
|
+
os.path.join(os.getcwd(), referenced_path),
|
|
304
|
+
]
|
|
305
|
+
else:
|
|
306
|
+
candidates = [referenced_path]
|
|
307
|
+
for candidate in candidates:
|
|
308
|
+
if candidate and os.path.isfile(candidate):
|
|
309
|
+
return os.path.normpath(candidate)
|
|
310
|
+
return None
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _widget_geometry(widget):
|
|
314
|
+
for prop in widget.getElementsByTagName("property"):
|
|
315
|
+
if prop.parentNode is not widget or prop.getAttribute("name") != "geometry":
|
|
316
|
+
continue
|
|
317
|
+
rects = prop.getElementsByTagName("rect")
|
|
318
|
+
if not rects:
|
|
319
|
+
continue
|
|
320
|
+
rect = rects[0]
|
|
321
|
+
values = {}
|
|
322
|
+
for key in ("x", "y", "width", "height"):
|
|
323
|
+
nodes = rect.getElementsByTagName(key)
|
|
324
|
+
if nodes and nodes[0].firstChild:
|
|
325
|
+
try:
|
|
326
|
+
values[key] = int(nodes[0].firstChild.data)
|
|
327
|
+
except ValueError:
|
|
328
|
+
values[key] = 0
|
|
329
|
+
if "width" in values and "height" in values:
|
|
330
|
+
return values
|
|
331
|
+
return None
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _widget_parent(widget):
|
|
335
|
+
node = widget.parentNode
|
|
336
|
+
while node is not None:
|
|
337
|
+
if getattr(node, "tagName", None) == "widget":
|
|
338
|
+
return node
|
|
339
|
+
node = node.parentNode
|
|
340
|
+
return None
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _layout_items(layout):
|
|
344
|
+
items = []
|
|
345
|
+
for node in layout.childNodes:
|
|
346
|
+
if getattr(node, "tagName", None) != "item":
|
|
347
|
+
continue
|
|
348
|
+
child = next(
|
|
349
|
+
(entry for entry in node.childNodes
|
|
350
|
+
if getattr(entry, "tagName", None) in ("widget", "layout", "spacer")),
|
|
351
|
+
None,
|
|
352
|
+
)
|
|
353
|
+
if child is not None:
|
|
354
|
+
items.append((node, child))
|
|
355
|
+
return items
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _layout_margins(layout):
|
|
359
|
+
values = {}
|
|
360
|
+
for prop in layout.getElementsByTagName("property"):
|
|
361
|
+
if prop.parentNode is not layout:
|
|
362
|
+
continue
|
|
363
|
+
name = prop.getAttribute("name")
|
|
364
|
+
if name in ("leftMargin", "topMargin", "rightMargin", "bottomMargin", "spacing"):
|
|
365
|
+
numbers = prop.getElementsByTagName("number")
|
|
366
|
+
if numbers and numbers[0].firstChild:
|
|
367
|
+
try:
|
|
368
|
+
values[name] = int(numbers[0].firstChild.data)
|
|
369
|
+
except ValueError:
|
|
370
|
+
pass
|
|
371
|
+
return values
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _widget_size_hint(widget):
|
|
375
|
+
geometry = _widget_geometry(widget) or {}
|
|
376
|
+
width = geometry.get("width", 0)
|
|
377
|
+
height = geometry.get("height", 0)
|
|
378
|
+
minimum = widget.getElementsByTagName("minimumSize")
|
|
379
|
+
if minimum:
|
|
380
|
+
for key in ("width", "height"):
|
|
381
|
+
nodes = minimum[0].getElementsByTagName(key)
|
|
382
|
+
if nodes and nodes[0].firstChild:
|
|
383
|
+
try:
|
|
384
|
+
value = int(nodes[0].firstChild.data)
|
|
385
|
+
if key == "width":
|
|
386
|
+
width = max(width, value)
|
|
387
|
+
else:
|
|
388
|
+
height = max(height, value)
|
|
389
|
+
except ValueError:
|
|
390
|
+
pass
|
|
391
|
+
text = _widget_property(widget, "text")
|
|
392
|
+
style = _widget_property(widget, "styleSheet")
|
|
393
|
+
font_match = re.search(r"font:\s*[^;]*?(\d+(?:\.\d+)?)pt", style)
|
|
394
|
+
font_px = float(font_match.group(1)) * 4 / 3 if font_match else 14
|
|
395
|
+
text_width = max(24, int(len(text) * font_px * 0.56 + 18))
|
|
396
|
+
kind = widget.getAttribute("class")
|
|
397
|
+
defaults = {
|
|
398
|
+
"QLabel": (text_width, int(font_px + 8)),
|
|
399
|
+
"QLineEdit": (110, int(font_px + 14)),
|
|
400
|
+
"QComboBox": (110, int(font_px + 14)),
|
|
401
|
+
"QPushButton": (max(80, text_width + 16), int(font_px + 18)),
|
|
402
|
+
"QCheckBox": (text_width + 18, int(font_px + 8)),
|
|
403
|
+
"QRadioButton": (text_width + 18, int(font_px + 8)),
|
|
404
|
+
"QListWidget": (160, 120),
|
|
405
|
+
}
|
|
406
|
+
default_width, default_height = defaults.get(kind, (90, 28))
|
|
407
|
+
return max(width, default_width), max(height, default_height)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _layout_widget_rects(container):
|
|
411
|
+
"""Approximate Qt box-layout placement using item order and size hints."""
|
|
412
|
+
container_geometry = _widget_geometry(container) or {}
|
|
413
|
+
width = container_geometry.get("width", 320)
|
|
414
|
+
height = container_geometry.get("height", 240)
|
|
415
|
+
layouts = []
|
|
416
|
+
for layout in container.getElementsByTagName("layout"):
|
|
417
|
+
node = layout.parentNode
|
|
418
|
+
nested_layout = False
|
|
419
|
+
while node is not None and node is not container:
|
|
420
|
+
if getattr(node, "tagName", None) == "layout":
|
|
421
|
+
nested_layout = True
|
|
422
|
+
break
|
|
423
|
+
node = node.parentNode
|
|
424
|
+
if node is container and not nested_layout:
|
|
425
|
+
layouts.append(layout)
|
|
426
|
+
if not layouts:
|
|
427
|
+
return {}
|
|
428
|
+
rects = {}
|
|
429
|
+
|
|
430
|
+
def measure(entry):
|
|
431
|
+
if entry.tagName == "widget":
|
|
432
|
+
return _widget_size_hint(entry)
|
|
433
|
+
children = _layout_items(entry)
|
|
434
|
+
if not children:
|
|
435
|
+
return 20, 20
|
|
436
|
+
sizes = [measure(child) for _item, child in children]
|
|
437
|
+
vertical = "VBox" in entry.getAttribute("class")
|
|
438
|
+
spacing = _layout_margins(entry).get("spacing", 6)
|
|
439
|
+
layout_margin = 18 if entry is layouts[0] else 0
|
|
440
|
+
if vertical:
|
|
441
|
+
return max(size[0] for size in sizes) + layout_margin, sum(size[1] for size in sizes) + spacing * (len(sizes) - 1) + layout_margin
|
|
442
|
+
return sum(size[0] for size in sizes) + spacing * (len(sizes) - 1) + layout_margin, max(size[1] for size in sizes) + layout_margin
|
|
443
|
+
|
|
444
|
+
def place(layout, x, y, area_width, area_height):
|
|
445
|
+
children = _layout_items(layout)
|
|
446
|
+
if not children:
|
|
447
|
+
return
|
|
448
|
+
margins = _layout_margins(layout)
|
|
449
|
+
default_margin = 9 if layout is layouts[0] else 0
|
|
450
|
+
left_margin = margins.get("leftMargin", default_margin)
|
|
451
|
+
top_margin = margins.get("topMargin", default_margin)
|
|
452
|
+
right_margin = margins.get("rightMargin", default_margin)
|
|
453
|
+
bottom_margin = margins.get("bottomMargin", default_margin)
|
|
454
|
+
spacing = margins.get("spacing", 6)
|
|
455
|
+
inner_x = x + left_margin
|
|
456
|
+
inner_y = y + top_margin
|
|
457
|
+
inner_width = max(area_width - left_margin - right_margin, 1)
|
|
458
|
+
inner_height = max(area_height - top_margin - bottom_margin, 1)
|
|
459
|
+
vertical = "VBox" in layout.getAttribute("class")
|
|
460
|
+
hints = [measure(child) for _item, child in children]
|
|
461
|
+
available = (inner_height if vertical else inner_width) - spacing * (len(children) - 1)
|
|
462
|
+
preferred = [size[1] if vertical else size[0] for size in hints]
|
|
463
|
+
total_preferred = sum(preferred)
|
|
464
|
+
stretch_weights = []
|
|
465
|
+
for item, child in children:
|
|
466
|
+
stretch_node = item.getElementsByTagName("property")
|
|
467
|
+
stretch = 0
|
|
468
|
+
for prop in stretch_node:
|
|
469
|
+
if prop.parentNode is item and prop.getAttribute("name") == "stretch":
|
|
470
|
+
numbers = prop.getElementsByTagName("number")
|
|
471
|
+
if numbers and numbers[0].firstChild:
|
|
472
|
+
try:
|
|
473
|
+
stretch = int(numbers[0].firstChild.data)
|
|
474
|
+
except ValueError:
|
|
475
|
+
pass
|
|
476
|
+
expandable = (
|
|
477
|
+
child.tagName == "layout"
|
|
478
|
+
or child.getAttribute("class") in (
|
|
479
|
+
"QLineEdit", "QComboBox", "QListWidget", "QTextEdit",
|
|
480
|
+
"QPlainTextEdit", "QPushButton",
|
|
481
|
+
)
|
|
482
|
+
)
|
|
483
|
+
stretch_weights.append(stretch if stretch else (1 if expandable and not vertical else 0))
|
|
484
|
+
extra = max(available - total_preferred, 0)
|
|
485
|
+
weights_total = sum(stretch_weights)
|
|
486
|
+
sizes = [
|
|
487
|
+
max(1, preferred[index] + (
|
|
488
|
+
extra * stretch_weights[index] / weights_total if weights_total else 0
|
|
489
|
+
))
|
|
490
|
+
for index in range(len(children))
|
|
491
|
+
]
|
|
492
|
+
if total_preferred > available and total_preferred:
|
|
493
|
+
sizes = [max(1, available * value / total_preferred) for value in preferred]
|
|
494
|
+
cursor = inner_y if vertical else inner_x
|
|
495
|
+
for index, (_item, child) in enumerate(children):
|
|
496
|
+
child_size = sizes[index]
|
|
497
|
+
if vertical:
|
|
498
|
+
child_x, child_y = inner_x, cursor
|
|
499
|
+
child_width, child_height = inner_width, child_size
|
|
500
|
+
else:
|
|
501
|
+
child_x, child_y = cursor, inner_y
|
|
502
|
+
child_width, child_height = child_size, inner_height
|
|
503
|
+
if child.tagName == "widget":
|
|
504
|
+
rects[child] = {
|
|
505
|
+
"x": child_x, "y": child_y,
|
|
506
|
+
"width": child_width, "height": child_height,
|
|
507
|
+
}
|
|
508
|
+
else:
|
|
509
|
+
place(child, child_x, child_y, child_width, child_height)
|
|
510
|
+
cursor += child_size + spacing
|
|
511
|
+
|
|
512
|
+
place(layouts[0], 0, 0, width, height)
|
|
513
|
+
return rects
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def _draw_form_preview(canvas, root_widget, preview_widgets, on_hover=None, on_click=None):
|
|
517
|
+
canvas.delete("all")
|
|
518
|
+
canvas._preview_images = []
|
|
519
|
+
canvas.update_idletasks()
|
|
520
|
+
available_width = max(canvas.winfo_width(), 240)
|
|
521
|
+
available_height = max(canvas.winfo_height(), 180)
|
|
522
|
+
root_geometry = _widget_geometry(root_widget) or {}
|
|
523
|
+
widget_right = max(
|
|
524
|
+
(item["geometry"].get("x", 0) + item["geometry"].get("width", 0)
|
|
525
|
+
for item in preview_widgets if item["geometry"]),
|
|
526
|
+
default=0,
|
|
527
|
+
)
|
|
528
|
+
widget_bottom = max(
|
|
529
|
+
(item["geometry"].get("y", 0) + item["geometry"].get("height", 0)
|
|
530
|
+
for item in preview_widgets if item["geometry"]),
|
|
531
|
+
default=0,
|
|
532
|
+
)
|
|
533
|
+
form_width = max(root_geometry.get("width", 480), widget_right + 12, 320)
|
|
534
|
+
form_height = max(root_geometry.get("height", 320), widget_bottom + 12, 220)
|
|
535
|
+
margin = min(10, max(3, int(min(available_width, available_height) * 0.015)))
|
|
536
|
+
titlebar_height = 26
|
|
537
|
+
form_area_width = max(available_width - margin * 2, 1)
|
|
538
|
+
form_area_height = max(available_height - margin * 2 - titlebar_height, 1)
|
|
539
|
+
scale = min(
|
|
540
|
+
form_area_width / form_width,
|
|
541
|
+
form_area_height / form_height,
|
|
542
|
+
)
|
|
543
|
+
detail_scale = max(0.5, scale)
|
|
544
|
+
text_size = max(6, min(24, int(round(9 * min(scale, 2.4)))))
|
|
545
|
+
rendered_width = form_width * scale
|
|
546
|
+
rendered_height = form_height * scale
|
|
547
|
+
root_colors = _widget_preview_colors(root_widget)
|
|
548
|
+
left = margin + max((form_area_width - rendered_width) / 2, 0)
|
|
549
|
+
top = margin + max((form_area_height - rendered_height) / 2, 0)
|
|
550
|
+
|
|
551
|
+
canvas.create_rectangle(
|
|
552
|
+
left + 3, top + 5, left + rendered_width + 3,
|
|
553
|
+
top + rendered_height + titlebar_height + 5,
|
|
554
|
+
fill="#cbd5e1", outline=""
|
|
555
|
+
)
|
|
556
|
+
canvas.create_rectangle(
|
|
557
|
+
left, top, left + rendered_width, top + rendered_height + titlebar_height,
|
|
558
|
+
fill=root_colors.get("background", "#ffffff"),
|
|
559
|
+
outline="#d7dee9", width=1
|
|
560
|
+
)
|
|
561
|
+
canvas.create_rectangle(
|
|
562
|
+
left, top, left + rendered_width, top + titlebar_height,
|
|
563
|
+
fill="#f8fafc", outline=""
|
|
564
|
+
)
|
|
565
|
+
canvas.create_rectangle(
|
|
566
|
+
left, top + titlebar_height - 3, left + rendered_width,
|
|
567
|
+
top + titlebar_height, fill="#5578e8", outline=""
|
|
568
|
+
)
|
|
569
|
+
canvas.create_text(
|
|
570
|
+
left + 13, top + titlebar_height / 2,
|
|
571
|
+
text=(_widget_property(root_widget, "windowTitle") or
|
|
572
|
+
root_widget.getAttribute("class") or "Form Preview"),
|
|
573
|
+
anchor="w", fill="#1e293b", font=("Segoe UI", 10, "bold")
|
|
574
|
+
)
|
|
575
|
+
for offset, color in enumerate(("#f28b82", "#fbbc04", "#34a853")):
|
|
576
|
+
canvas.create_oval(
|
|
577
|
+
left + rendered_width - 44 + offset * 13, top + 12,
|
|
578
|
+
left + rendered_width - 36 + offset * 13, top + 20,
|
|
579
|
+
fill=color, outline=""
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
if not preview_widgets:
|
|
583
|
+
canvas.create_text(
|
|
584
|
+
left + rendered_width / 2,
|
|
585
|
+
top + titlebar_height + rendered_height / 2,
|
|
586
|
+
text="Aucun widget à prévisualiser", fill="#98a2b3",
|
|
587
|
+
font=("Segoe UI", text_size)
|
|
588
|
+
)
|
|
589
|
+
return
|
|
590
|
+
|
|
591
|
+
content_top = top + titlebar_height
|
|
592
|
+
fallback_index = 0
|
|
593
|
+
for item in preview_widgets:
|
|
594
|
+
geometry = item["geometry"]
|
|
595
|
+
if geometry:
|
|
596
|
+
x = geometry.get("x", 0) * scale
|
|
597
|
+
y = geometry.get("y", 0) * scale
|
|
598
|
+
widget_width = max(geometry.get("width", 110) * scale, 1)
|
|
599
|
+
widget_height = max(geometry.get("height", 30) * scale, 1)
|
|
600
|
+
else:
|
|
601
|
+
fallback_index += 1
|
|
602
|
+
x = 14 * scale
|
|
603
|
+
y = (16 + (fallback_index - 1) * 44) * scale
|
|
604
|
+
widget_width = min(form_width - 28, 180) * scale
|
|
605
|
+
widget_height = 30 * scale
|
|
606
|
+
|
|
607
|
+
x1 = min(max(left + x, left + 4), left + rendered_width - 4)
|
|
608
|
+
y1 = min(max(content_top + y, content_top + 4),
|
|
609
|
+
content_top + rendered_height - 4)
|
|
610
|
+
x2 = min(x1 + widget_width, left + rendered_width - 5)
|
|
611
|
+
y2 = min(y1 + widget_height, content_top + rendered_height - 5)
|
|
612
|
+
if x2 - x1 < 2 or y2 - y1 < 2:
|
|
613
|
+
continue
|
|
614
|
+
widget_type = item["type"]
|
|
615
|
+
name = item["name"]
|
|
616
|
+
widget = item.get("widget")
|
|
617
|
+
widget_font_size = max(
|
|
618
|
+
5,
|
|
619
|
+
min(
|
|
620
|
+
28,
|
|
621
|
+
int(round(_widget_font_size(widget) * scale))
|
|
622
|
+
if widget is not None
|
|
623
|
+
else max(5, min(text_size, int(max(y2 - y1, 1) * 0.55))),
|
|
624
|
+
),
|
|
625
|
+
)
|
|
626
|
+
caption = (item["text"] or item["placeholder"]).replace("&", "")
|
|
627
|
+
if widget_type in ("QListWidget", "Line"):
|
|
628
|
+
caption = item["text"].replace("&", "")
|
|
629
|
+
if widget_type == "QComboBox" and not caption:
|
|
630
|
+
combo_items = item.get("list_items", [])
|
|
631
|
+
caption = combo_items[0] if combo_items else ""
|
|
632
|
+
label = caption or name
|
|
633
|
+
if widget_type in ("QLineEdit", "QTextEdit", "QPlainTextEdit", "QComboBox", "QListWidget", "Line"):
|
|
634
|
+
label = caption
|
|
635
|
+
available_text_width = max(x2 - x1 - 14, 1)
|
|
636
|
+
max_label_chars = max(1, int(available_text_width / 5.5))
|
|
637
|
+
if len(label) > max_label_chars:
|
|
638
|
+
label = label[:max_label_chars - 1] + "…"
|
|
639
|
+
colors = _widget_preview_colors(widget) if widget is not None else {}
|
|
640
|
+
border = "#c5cedd"
|
|
641
|
+
fill = colors.get("background", "#ffffff")
|
|
642
|
+
foreground = colors.get("foreground", "#334155")
|
|
643
|
+
tag = f"widget-{name}"
|
|
644
|
+
hover_area = canvas.create_rectangle(
|
|
645
|
+
x1, y1, x2, y2, fill="#f8faff", outline="",
|
|
646
|
+
tags=(tag, f"hover-area-{name}")
|
|
647
|
+
)
|
|
648
|
+
hover_shapes = []
|
|
649
|
+
|
|
650
|
+
if widget_type == "QLabel":
|
|
651
|
+
image_path = item.get("image_path")
|
|
652
|
+
image_rendered = False
|
|
653
|
+
if image_path:
|
|
654
|
+
try:
|
|
655
|
+
from PIL import Image, ImageTk
|
|
656
|
+
|
|
657
|
+
with Image.open(image_path) as source_image:
|
|
658
|
+
source_image = source_image.convert("RGBA")
|
|
659
|
+
image_width = max(1, int(x2 - x1 - 4))
|
|
660
|
+
image_height = max(1, int(y2 - y1 - 4))
|
|
661
|
+
resampling = getattr(Image, "Resampling", Image).LANCZOS
|
|
662
|
+
source_image.thumbnail(
|
|
663
|
+
(image_width, image_height), resampling
|
|
664
|
+
)
|
|
665
|
+
preview_image = ImageTk.PhotoImage(
|
|
666
|
+
source_image, master=canvas
|
|
667
|
+
)
|
|
668
|
+
canvas._preview_images.append(preview_image)
|
|
669
|
+
canvas.create_image(
|
|
670
|
+
(x1 + x2) / 2, (y1 + y2) / 2,
|
|
671
|
+
image=preview_image, anchor="center", tags=(tag,)
|
|
672
|
+
)
|
|
673
|
+
image_rendered = True
|
|
674
|
+
except (ImportError, OSError, tk.TclError, ValueError):
|
|
675
|
+
pass
|
|
676
|
+
if not image_rendered:
|
|
677
|
+
canvas.create_text(
|
|
678
|
+
x1 + 4, (y1 + y2) / 2, text=label, anchor="w",
|
|
679
|
+
width=max(x2 - x1 - 8, 1), fill=foreground,
|
|
680
|
+
font=("Segoe UI", widget_font_size), tags=(tag,)
|
|
681
|
+
)
|
|
682
|
+
elif widget_type in ("QCheckBox", "QRadioButton"):
|
|
683
|
+
indicator_size = min(
|
|
684
|
+
14 * detail_scale,
|
|
685
|
+
max(y2 - y1 - 4, 2),
|
|
686
|
+
max(x2 - x1 - 4, 2),
|
|
687
|
+
)
|
|
688
|
+
indicator_inset = max((y2 - y1 - indicator_size) / 2, 1)
|
|
689
|
+
label_space = max(x2 - x1 - indicator_size - 8, 1)
|
|
690
|
+
if widget_type == "QRadioButton":
|
|
691
|
+
checkbox_shape = canvas.create_oval(
|
|
692
|
+
x1 + 2, y1 + indicator_inset,
|
|
693
|
+
x1 + 2 + indicator_size, y1 + indicator_inset + indicator_size,
|
|
694
|
+
fill="#ffffff", outline="#94a3b8", width=1, tags=(tag,)
|
|
695
|
+
)
|
|
696
|
+
else:
|
|
697
|
+
checkbox_shape = canvas.create_rectangle(
|
|
698
|
+
x1 + 2, y1 + indicator_inset,
|
|
699
|
+
x1 + 2 + indicator_size, y1 + indicator_inset + indicator_size,
|
|
700
|
+
fill="#ffffff", outline="#94a3b8", width=1, tags=(tag,)
|
|
701
|
+
)
|
|
702
|
+
hover_shapes.append((checkbox_shape, "#ffffff", "#bfdbfe"))
|
|
703
|
+
label_x = x1 + indicator_size + 6
|
|
704
|
+
canvas.create_text(
|
|
705
|
+
label_x, (y1 + y2) / 2, text=label, anchor="w",
|
|
706
|
+
width=label_space, fill=foreground,
|
|
707
|
+
font=("Segoe UI", widget_font_size), tags=(tag,)
|
|
708
|
+
)
|
|
709
|
+
elif widget_type in ("QLineEdit", "QTextEdit", "QPlainTextEdit", "QComboBox"):
|
|
710
|
+
input_shape = canvas.create_rectangle(
|
|
711
|
+
x1, y1, x2, y2, fill=fill, outline=border,
|
|
712
|
+
width=1, tags=(tag,)
|
|
713
|
+
)
|
|
714
|
+
hover_shapes.append((input_shape, "#ffffff", "#bfdbfe"))
|
|
715
|
+
text_width = max(
|
|
716
|
+
x2 - x1 - (22 if widget_type == "QComboBox" else 14), 1
|
|
717
|
+
)
|
|
718
|
+
canvas.create_text(
|
|
719
|
+
x1 + 7, (y1 + y2) / 2, text=label, anchor="w",
|
|
720
|
+
width=text_width, fill=foreground if caption else "#94a3b8",
|
|
721
|
+
font=("Segoe UI", widget_font_size), tags=(tag,)
|
|
722
|
+
)
|
|
723
|
+
if widget_type == "QComboBox":
|
|
724
|
+
canvas.create_text(x2 - 10, (y1 + y2) / 2, text="⌄",
|
|
725
|
+
fill="#667085", font=("Segoe UI", 9), tags=(tag,))
|
|
726
|
+
elif widget_type == "QListWidget":
|
|
727
|
+
list_shape = canvas.create_rectangle(
|
|
728
|
+
x1, y1, x2, y2, fill=fill, outline=border,
|
|
729
|
+
width=1, tags=(tag,)
|
|
730
|
+
)
|
|
731
|
+
hover_shapes.append((list_shape, fill, "#f1f5ff"))
|
|
732
|
+
rows = item.get("list_items", [])
|
|
733
|
+
row_height = max(22 * scale, (y2 - y1) / max(len(rows), 1))
|
|
734
|
+
for row_index, row_text in enumerate(rows):
|
|
735
|
+
row_y = y1 + row_index * row_height
|
|
736
|
+
if row_y + row_height > y2:
|
|
737
|
+
break
|
|
738
|
+
if row_index:
|
|
739
|
+
canvas.create_line(
|
|
740
|
+
x1 + 1, row_y, x2 - 1, row_y,
|
|
741
|
+
fill="#edf0f5", width=1, tags=(tag,)
|
|
742
|
+
)
|
|
743
|
+
canvas.create_text(
|
|
744
|
+
x1 + 8, row_y + row_height / 2, text=row_text,
|
|
745
|
+
anchor="w", width=max(x2 - x1 - 16, 1),
|
|
746
|
+
fill=foreground, font=("Segoe UI", widget_font_size), tags=(tag,)
|
|
747
|
+
)
|
|
748
|
+
elif widget_type == "Line":
|
|
749
|
+
orientation = _widget_property(widget, "orientation") if widget is not None else ""
|
|
750
|
+
if "Vertical" in orientation:
|
|
751
|
+
center_x = (x1 + x2) / 2
|
|
752
|
+
line_shape = canvas.create_line(
|
|
753
|
+
center_x, y1, center_x, y2,
|
|
754
|
+
fill="#94a3b8", width=max(1, scale), tags=(tag,)
|
|
755
|
+
)
|
|
756
|
+
else:
|
|
757
|
+
center_y = (y1 + y2) / 2
|
|
758
|
+
line_shape = canvas.create_line(
|
|
759
|
+
x1, center_y, x2, center_y,
|
|
760
|
+
fill="#94a3b8", width=max(1, scale), tags=(tag,)
|
|
761
|
+
)
|
|
762
|
+
hover_shapes.append((line_shape, "#94a3b8", "#5578e8"))
|
|
763
|
+
elif widget_type == "QProgressBar":
|
|
764
|
+
progress_track = canvas.create_rectangle(
|
|
765
|
+
x1, y1, x2, y2, fill="#e2e8f0", outline="", tags=(tag,)
|
|
766
|
+
)
|
|
767
|
+
progress_fill = canvas.create_rectangle(
|
|
768
|
+
x1, y1, x1 + (x2 - x1) * .62, y2,
|
|
769
|
+
fill="#6481e8", outline="", tags=(tag,)
|
|
770
|
+
)
|
|
771
|
+
hover_shapes.extend((
|
|
772
|
+
(progress_track, "#e2e8f0", "#bfdbfe"),
|
|
773
|
+
(progress_fill, "#6481e8", "#93b4fb"),
|
|
774
|
+
))
|
|
775
|
+
canvas.create_text(
|
|
776
|
+
(x1 + x2) / 2, (y1 + y2) / 2, text=label,
|
|
777
|
+
width=max(x2 - x1 - 8, 1), fill="#334155",
|
|
778
|
+
font=("Segoe UI", widget_font_size), tags=(tag,)
|
|
779
|
+
)
|
|
780
|
+
elif widget_type == "QPushButton":
|
|
781
|
+
button_fill = colors.get("background", "#eef2f7")
|
|
782
|
+
button_text = colors.get("foreground", "#334155")
|
|
783
|
+
button_shape = canvas.create_rectangle(
|
|
784
|
+
x1, y1, x2, y2, fill=button_fill, outline="#cbd5e1",
|
|
785
|
+
width=1, tags=(tag,)
|
|
786
|
+
)
|
|
787
|
+
hover_shapes.append((button_shape, button_fill, "#dce7ff"))
|
|
788
|
+
canvas.create_text(
|
|
789
|
+
(x1 + x2) / 2, (y1 + y2) / 2, text=label,
|
|
790
|
+
width=max(x2 - x1 - 12, 1), fill=button_text,
|
|
791
|
+
font=("Segoe UI", widget_font_size, "bold"), tags=(tag,)
|
|
792
|
+
)
|
|
793
|
+
else:
|
|
794
|
+
widget_shape = canvas.create_rectangle(
|
|
795
|
+
x1, y1, x2, y2, fill=fill, outline=border,
|
|
796
|
+
width=1, tags=(tag,)
|
|
797
|
+
)
|
|
798
|
+
hover_shapes.append((widget_shape, "#ffffff", "#bfdbfe"))
|
|
799
|
+
canvas.create_text(
|
|
800
|
+
(x1 + x2) / 2, (y1 + y2) / 2, text=label,
|
|
801
|
+
width=max(x2 - x1 - 12, 1), fill="#334155",
|
|
802
|
+
font=("Segoe UI", widget_font_size), tags=(tag,)
|
|
803
|
+
)
|
|
804
|
+
|
|
805
|
+
hover_outline = canvas.create_rectangle(
|
|
806
|
+
x1, y1, x2, y2, fill="", outline="", width=3,
|
|
807
|
+
tags=(tag, f"hover-outline-{name}")
|
|
808
|
+
)
|
|
809
|
+
|
|
810
|
+
def highlight_widget(
|
|
811
|
+
_event, active, background=hover_area,
|
|
812
|
+
outline=hover_outline, shapes=tuple(hover_shapes)
|
|
813
|
+
):
|
|
814
|
+
canvas.itemconfigure(
|
|
815
|
+
background, fill="#dce8ff" if active else "#f8faff"
|
|
816
|
+
)
|
|
817
|
+
canvas.itemconfigure(
|
|
818
|
+
outline, outline="#2458e8" if active else ""
|
|
819
|
+
)
|
|
820
|
+
for shape, normal_fill, hover_fill in shapes:
|
|
821
|
+
canvas.itemconfigure(shape, fill=hover_fill if active else normal_fill)
|
|
822
|
+
|
|
823
|
+
canvas.tag_bind(
|
|
824
|
+
tag, "<Enter>",
|
|
825
|
+
lambda event, widget_name=name, highlight=highlight_widget: (
|
|
826
|
+
highlight(event, True), on_hover(widget_name)
|
|
827
|
+
) if on_hover else highlight(event, True)
|
|
828
|
+
)
|
|
829
|
+
canvas.tag_bind(
|
|
830
|
+
tag, "<Button-1>",
|
|
831
|
+
lambda event, widget_name=name: on_click(widget_name)
|
|
832
|
+
if on_click else (on_hover(widget_name) if on_hover else "")
|
|
833
|
+
)
|
|
834
|
+
canvas.tag_bind(
|
|
835
|
+
tag, "<Leave>",
|
|
836
|
+
lambda event, highlight=highlight_widget: highlight(event, False)
|
|
837
|
+
)
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
def show_ui_elements():
|
|
841
|
+
global qt_ui_file, qt_form_name, _live_preview_dialog
|
|
842
|
+
parent = get_workbench()
|
|
843
|
+
if _live_preview_dialog is not None:
|
|
844
|
+
try:
|
|
845
|
+
if _live_preview_dialog.winfo_exists():
|
|
846
|
+
_live_preview_dialog.destroy()
|
|
847
|
+
except tk.TclError:
|
|
848
|
+
pass
|
|
849
|
+
_live_preview_dialog = None
|
|
850
|
+
|
|
851
|
+
try:
|
|
852
|
+
ui_document = None
|
|
853
|
+
display_name = ""
|
|
854
|
+
source_ui_path = None
|
|
855
|
+
editor = parent.get_editor_notebook().get_current_editor()
|
|
856
|
+
editor_path = None
|
|
857
|
+
editor_text = ""
|
|
858
|
+
if editor is not None:
|
|
859
|
+
for attr in ("get_filename", "get_file_path"):
|
|
860
|
+
get_path = getattr(editor, attr, None)
|
|
861
|
+
if callable(get_path):
|
|
862
|
+
editor_path = get_path()
|
|
863
|
+
if editor_path:
|
|
864
|
+
break
|
|
865
|
+
|
|
866
|
+
editor_text = editor.get_code_view().text.get("1.0", "end-1c")
|
|
867
|
+
if "<ui" in editor_text and "</ui>" in editor_text:
|
|
868
|
+
candidate = minidom.parseString(editor_text.encode("utf-8"))
|
|
869
|
+
if candidate.documentElement.tagName == "ui":
|
|
870
|
+
ui_document = candidate
|
|
871
|
+
if editor_path:
|
|
872
|
+
qt_ui_file = editor_path
|
|
873
|
+
source_ui_path = editor_path
|
|
874
|
+
display_name = os.path.basename(editor_path)
|
|
875
|
+
else:
|
|
876
|
+
display_name = "Interface ouverte dans l’éditeur"
|
|
877
|
+
|
|
878
|
+
# Some Thonny editor implementations don't expose the .ui buffer
|
|
879
|
+
# as XML text. If the active tab is a UI file, load that opened file.
|
|
880
|
+
if (
|
|
881
|
+
ui_document is None
|
|
882
|
+
and editor_path
|
|
883
|
+
and os.path.splitext(editor_path)[1].lower() == ".ui"
|
|
884
|
+
):
|
|
885
|
+
qt_ui_file = editor_path
|
|
886
|
+
source_ui_path = editor_path
|
|
887
|
+
ui_document = minidom.parse(editor_path)
|
|
888
|
+
display_name = os.path.basename(editor_path)
|
|
889
|
+
|
|
890
|
+
if ui_document is None:
|
|
891
|
+
loaded_ui_path = _loaded_ui_path(editor_text, editor_path)
|
|
892
|
+
if loaded_ui_path:
|
|
893
|
+
qt_ui_file = loaded_ui_path
|
|
894
|
+
source_ui_path = loaded_ui_path
|
|
895
|
+
ui_document = minidom.parse(loaded_ui_path)
|
|
896
|
+
display_name = os.path.basename(loaded_ui_path)
|
|
897
|
+
|
|
898
|
+
# Prefer the exact interface selected in the plugin even when there
|
|
899
|
+
# is no current editor. Generated loadUi calls can use a relative path.
|
|
900
|
+
if ui_document is None and qt_ui_file:
|
|
901
|
+
remembered_path = os.path.abspath(qt_ui_file)
|
|
902
|
+
if os.path.isfile(remembered_path):
|
|
903
|
+
qt_ui_file = remembered_path
|
|
904
|
+
source_ui_path = remembered_path
|
|
905
|
+
ui_document = minidom.parse(remembered_path)
|
|
906
|
+
display_name = os.path.basename(remembered_path)
|
|
907
|
+
|
|
908
|
+
# If the active editor is not showing a .ui document, ask for a file
|
|
909
|
+
# on every preview launch. This picker choice is temporary; the UI
|
|
910
|
+
# selected by “Ajouter code” or found in loadUi remains remembered.
|
|
911
|
+
if ui_document is None:
|
|
912
|
+
source_ui_path = askopenfilename(
|
|
913
|
+
filetypes=[("Fichiers UI", ".ui"), (tr("Tous les fichiers"), ".*")],
|
|
914
|
+
parent=parent,
|
|
915
|
+
)
|
|
916
|
+
if not source_ui_path:
|
|
917
|
+
return
|
|
918
|
+
ui_document = minidom.parse(source_ui_path)
|
|
919
|
+
display_name = os.path.basename(source_ui_path)
|
|
920
|
+
|
|
921
|
+
ui_widgets = list(ui_document.getElementsByTagName("widget"))
|
|
922
|
+
if not ui_widgets:
|
|
923
|
+
raise ValueError("Le fichier ne contient aucun widget Qt.")
|
|
924
|
+
root_widget = ui_widgets[0]
|
|
925
|
+
preview_widgets = []
|
|
926
|
+
layout_rects = {
|
|
927
|
+
widget: _layout_widget_rects(widget) for widget in ui_widgets
|
|
928
|
+
}
|
|
929
|
+
for widget in ui_widgets:
|
|
930
|
+
widget_name = widget.getAttribute("name") or "(sans nom)"
|
|
931
|
+
widget_type = widget.getAttribute("class") or "Type inconnu"
|
|
932
|
+
geometry = _widget_geometry(widget)
|
|
933
|
+
parent_widget = _widget_parent(widget)
|
|
934
|
+
if geometry is None and parent_widget is not None:
|
|
935
|
+
geometry = layout_rects.get(parent_widget, {}).get(widget)
|
|
936
|
+
if widget is not root_widget and geometry:
|
|
937
|
+
geometry = dict(geometry)
|
|
938
|
+
parent_node = parent_widget
|
|
939
|
+
while parent_node is not None and parent_node is not root_widget:
|
|
940
|
+
parent_geometry = _widget_geometry(parent_node)
|
|
941
|
+
if parent_geometry is None:
|
|
942
|
+
ancestor = _widget_parent(parent_node)
|
|
943
|
+
parent_geometry = layout_rects.get(ancestor, {}).get(parent_node, {})
|
|
944
|
+
geometry["x"] += parent_geometry.get("x", 0)
|
|
945
|
+
geometry["y"] += parent_geometry.get("y", 0)
|
|
946
|
+
parent_node = _widget_parent(parent_node)
|
|
947
|
+
item = {
|
|
948
|
+
"name": widget_name,
|
|
949
|
+
"type": widget_type,
|
|
950
|
+
"geometry": geometry,
|
|
951
|
+
"text": _widget_property(widget, "text"),
|
|
952
|
+
"placeholder": _widget_property(widget, "placeholderText"),
|
|
953
|
+
"widget": widget,
|
|
954
|
+
"image_path": _resolve_preview_image(
|
|
955
|
+
widget,
|
|
956
|
+
os.path.dirname(source_ui_path) if source_ui_path else os.getcwd(),
|
|
957
|
+
),
|
|
958
|
+
"list_items": _list_widget_items(widget),
|
|
959
|
+
}
|
|
960
|
+
has_layout = bool(widget.getElementsByTagName("layout"))
|
|
961
|
+
if widget is not root_widget and not (
|
|
962
|
+
widget_type == "QWidget" and has_layout
|
|
963
|
+
):
|
|
964
|
+
preview_widgets.append(item)
|
|
965
|
+
except Exception as error:
|
|
966
|
+
messagebox.showerror(
|
|
967
|
+
"Formulaire UI illisible",
|
|
968
|
+
f"Impossible de lire le fichier :\n{error}",
|
|
969
|
+
parent=parent,
|
|
970
|
+
)
|
|
971
|
+
return
|
|
972
|
+
|
|
973
|
+
dialog = tk.Toplevel(parent)
|
|
974
|
+
_live_preview_dialog = dialog
|
|
975
|
+
dialog.title("Aperçu live du formulaire")
|
|
976
|
+
dialog.transient(parent)
|
|
977
|
+
dialog.configure(bg="#edf1f7")
|
|
978
|
+
dialog.update_idletasks()
|
|
979
|
+
screen_width = dialog.winfo_screenwidth()
|
|
980
|
+
screen_height = dialog.winfo_screenheight()
|
|
981
|
+
root_geometry = _widget_geometry(root_widget) or {}
|
|
982
|
+
ui_width = max(
|
|
983
|
+
root_geometry.get("width", 480),
|
|
984
|
+
max(
|
|
985
|
+
(item["geometry"].get("x", 0) + item["geometry"].get("width", 0)
|
|
986
|
+
for item in preview_widgets if item["geometry"]),
|
|
987
|
+
default=0,
|
|
988
|
+
) + 12,
|
|
989
|
+
)
|
|
990
|
+
ui_height = max(
|
|
991
|
+
root_geometry.get("height", 320),
|
|
992
|
+
max(
|
|
993
|
+
(item["geometry"].get("y", 0) + item["geometry"].get("height", 0)
|
|
994
|
+
for item in preview_widgets if item["geometry"]),
|
|
995
|
+
default=0,
|
|
996
|
+
) + 12,
|
|
997
|
+
)
|
|
998
|
+
max_width = max(480, min(1040, screen_width - 24))
|
|
999
|
+
max_height = max(360, screen_height - 36)
|
|
1000
|
+
width = min(max(760, ui_width + 40), max_width)
|
|
1001
|
+
height = min(max(620, ui_height + 120), max_height)
|
|
1002
|
+
x = parent.winfo_rootx() + (parent.winfo_width() - width) // 2
|
|
1003
|
+
y = parent.winfo_rooty() + (parent.winfo_height() - height) // 2
|
|
1004
|
+
x = min(max(x, 0), screen_width - width)
|
|
1005
|
+
y = min(max(y, 0), screen_height - height)
|
|
1006
|
+
dialog.geometry(f"{width}x{height}+{x}+{y}")
|
|
1007
|
+
dialog.minsize(min(720, width), min(480, height))
|
|
1008
|
+
|
|
1009
|
+
card = tk.Frame(dialog, bg="#f8fafc", padx=10, pady=8)
|
|
1010
|
+
card.pack(fill="both", expand=True, padx=5, pady=5)
|
|
1011
|
+
header = tk.Frame(card, bg="#f8fafc")
|
|
1012
|
+
header.pack(fill="x", pady=(0, 5))
|
|
1013
|
+
logo_path = os.path.join(os.path.dirname(__file__), "res", "logo.png")
|
|
1014
|
+
try:
|
|
1015
|
+
try:
|
|
1016
|
+
from PIL import Image, ImageTk
|
|
1017
|
+
|
|
1018
|
+
source_logo = Image.open(logo_path).convert("RGBA")
|
|
1019
|
+
source_logo.thumbnail((76, 76), Image.Resampling.LANCZOS)
|
|
1020
|
+
preview_logo = ImageTk.PhotoImage(source_logo, master=dialog)
|
|
1021
|
+
except ImportError:
|
|
1022
|
+
# Keep the original pixels rather than producing a jagged Tk
|
|
1023
|
+
# subsample when Pillow is unavailable.
|
|
1024
|
+
preview_logo = tk.PhotoImage(file=logo_path, master=dialog)
|
|
1025
|
+
logo_label = tk.Label(
|
|
1026
|
+
header, image=preview_logo, bg="#f8fafc", cursor="hand2"
|
|
1027
|
+
)
|
|
1028
|
+
logo_label.pack(side="right", padx=(12, 2), pady=2)
|
|
1029
|
+
logo_label.bind(
|
|
1030
|
+
"<Button-1>", lambda _event: webbrowser.open("https://ikode.tn", new=2)
|
|
1031
|
+
)
|
|
1032
|
+
dialog.preview_logo = preview_logo
|
|
1033
|
+
except tk.TclError:
|
|
1034
|
+
pass
|
|
1035
|
+
heading = tk.Frame(header, bg="#f8fafc")
|
|
1036
|
+
heading.pack(side="left", fill="x", expand=True)
|
|
1037
|
+
tk.Label(
|
|
1038
|
+
heading, text="APERÇU INTERACTIF", bg="#f8fafc", fg="#4565c7",
|
|
1039
|
+
font=("Segoe UI", 9, "bold")
|
|
1040
|
+
).pack(anchor="w")
|
|
1041
|
+
tk.Label(
|
|
1042
|
+
heading, text=display_name, bg="#f8fafc",
|
|
1043
|
+
fg="#172033", font=("Segoe UI", 15, "bold")
|
|
1044
|
+
).pack(anchor="w", pady=(3, 0))
|
|
1045
|
+
tk.Label(
|
|
1046
|
+
heading, text="Survolez un élément pour le repérer · Cliquez pour copier son nom",
|
|
1047
|
+
bg="#f8fafc", fg="#64748b", font=("Segoe UI", 9)
|
|
1048
|
+
).pack(anchor="w", pady=(3, 0))
|
|
1049
|
+
preview_canvas = tk.Canvas(
|
|
1050
|
+
card, bg="#eef2f7", highlightthickness=1,
|
|
1051
|
+
highlightbackground="#dce3ed", bd=0
|
|
1052
|
+
)
|
|
1053
|
+
preview_canvas.pack(fill="both", expand=True)
|
|
1054
|
+
hovered_name = tk.StringVar(
|
|
1055
|
+
value="Prêt · Sélectionnez un composant dans l’aperçu"
|
|
1056
|
+
)
|
|
1057
|
+
tk.Label(
|
|
1058
|
+
card, textvariable=hovered_name, bg="#edf2ff", fg="#294cae",
|
|
1059
|
+
font=("Segoe UI", 9, "bold"), anchor="w", padx=12, pady=7,
|
|
1060
|
+
highlightthickness=1, highlightbackground="#dce5ff"
|
|
1061
|
+
).pack(fill="x", pady=(4, 0))
|
|
1062
|
+
|
|
1063
|
+
def show_hovered_widget(widget_name):
|
|
1064
|
+
widget = next(
|
|
1065
|
+
(item for item in preview_widgets if item["name"] == widget_name),
|
|
1066
|
+
None,
|
|
1067
|
+
)
|
|
1068
|
+
widget_type = widget["type"] if widget else "Widget"
|
|
1069
|
+
hovered_name.set(f"{widget_name} · {widget_type} · Cliquez pour copier")
|
|
1070
|
+
|
|
1071
|
+
def copy_widget_name(widget_name):
|
|
1072
|
+
form_name = qt_form_name or parent.get_option(FORM_NAME_OPTION, "win")
|
|
1073
|
+
qualified_name = f"{form_name}.{widget_name}"
|
|
1074
|
+
try:
|
|
1075
|
+
dialog.clipboard_clear()
|
|
1076
|
+
dialog.clipboard_append(qualified_name)
|
|
1077
|
+
hovered_name.set(f"Nom copié : {qualified_name}")
|
|
1078
|
+
except tk.TclError:
|
|
1079
|
+
hovered_name.set(f"Élément sélectionné : {qualified_name}")
|
|
1080
|
+
|
|
1081
|
+
redraw_job = {"id": None}
|
|
1082
|
+
|
|
1083
|
+
def redraw_preview():
|
|
1084
|
+
redraw_job["id"] = None
|
|
1085
|
+
_draw_form_preview(
|
|
1086
|
+
preview_canvas, root_widget, preview_widgets,
|
|
1087
|
+
show_hovered_widget, copy_widget_name
|
|
1088
|
+
)
|
|
1089
|
+
|
|
1090
|
+
def schedule_preview_redraw(_event=None):
|
|
1091
|
+
if redraw_job["id"] is not None:
|
|
1092
|
+
try:
|
|
1093
|
+
dialog.after_cancel(redraw_job["id"])
|
|
1094
|
+
except tk.TclError:
|
|
1095
|
+
pass
|
|
1096
|
+
redraw_job["id"] = dialog.after(35, redraw_preview)
|
|
1097
|
+
|
|
1098
|
+
preview_canvas.bind("<Configure>", schedule_preview_redraw)
|
|
1099
|
+
|
|
1100
|
+
_draw_form_preview(
|
|
1101
|
+
preview_canvas, root_widget, preview_widgets,
|
|
1102
|
+
show_hovered_widget, copy_widget_name
|
|
1103
|
+
)
|
|
1104
|
+
|
|
1105
|
+
def close_preview(event=None):
|
|
1106
|
+
global _live_preview_dialog
|
|
1107
|
+
if _live_preview_dialog is dialog:
|
|
1108
|
+
_live_preview_dialog = None
|
|
1109
|
+
try:
|
|
1110
|
+
dialog.destroy()
|
|
1111
|
+
except tk.TclError:
|
|
1112
|
+
pass
|
|
1113
|
+
|
|
1114
|
+
dialog.bind("<Escape>", close_preview)
|
|
1115
|
+
dialog.protocol("WM_DELETE_WINDOW", close_preview)
|
|
1116
|
+
parent.wait_window(dialog)
|
|
1117
|
+
|
|
1118
|
+
|
|
1119
|
+
def add_pyqt_code():
|
|
1120
|
+
global qt_ui_file, qt_form_name
|
|
1121
|
+
btnstxt = ""
|
|
1122
|
+
mytxt = ""
|
|
1123
|
+
path = askopenfilename(
|
|
1124
|
+
filetypes=[("Fichiers UI", ".ui"), (tr("Tous les fichiers"), ".*")], parent=get_workbench()
|
|
1125
|
+
)
|
|
1126
|
+
if path:
|
|
1127
|
+
form_name = ask_form_name(get_workbench())
|
|
1128
|
+
if form_name is None:
|
|
1129
|
+
return
|
|
1130
|
+
qt_form_name = form_name
|
|
1131
|
+
|
|
1132
|
+
qt_ui_file = os.path.abspath(path)
|
|
1133
|
+
file = minidom.parse(qt_ui_file)
|
|
1134
|
+
widgets = file.getElementsByTagName('widget')
|
|
1135
|
+
for w in widgets:
|
|
1136
|
+
if w.attributes['class'].value == "QPushButton" : #Bouton
|
|
1137
|
+
btnstxt = btnstxt + form_name+"."+w.attributes['name'].value +".clicked.connect ( "+ w.attributes['name'].value +"_click )\n"
|
|
1138
|
+
mytxt = mytxt + "def "+ w.attributes['name'].value +"_click():\n pass\n"
|
|
1139
|
+
|
|
1140
|
+
generated_code = (
|
|
1141
|
+
'#Updated By IKODE\n#Credits : Mr Selmen Arous\n' + 'from PyQt5.uic import loadUi\n'+
|
|
1142
|
+
'from PyQt5.QtWidgets import *\n'+
|
|
1143
|
+
'\n'+mytxt+'\n'+
|
|
1144
|
+
'app = QApplication([])\n'+
|
|
1145
|
+
form_name+' = loadUi (".'+ path[path.rfind("/"):] +'")\n'+
|
|
1146
|
+
form_name+'.show()\n'+
|
|
1147
|
+
btnstxt+'\n'
|
|
1148
|
+
'app.exec_()'
|
|
1149
|
+
)
|
|
1150
|
+
editor = get_workbench().get_editor_notebook().get_current_editor()
|
|
1151
|
+
if editor is None:
|
|
1152
|
+
return
|
|
1153
|
+
text = editor.get_code_view().text
|
|
1154
|
+
if text.get("1.0", "end-1c").strip() and not confirm_replace_editor(get_workbench()):
|
|
1155
|
+
return
|
|
1156
|
+
text.delete("1.0", "end")
|
|
1157
|
+
text.insert("1.0", generated_code)
|
|
1158
|
+
|
|
1159
|
+
|
|
1160
|
+
def forget_form_name():
|
|
1161
|
+
get_workbench().set_option(FORM_NAME_OPTION, "")
|
|
1162
|
+
|
|
1163
|
+
|
|
1164
|
+
def load_plugin():
|
|
1165
|
+
|
|
1166
|
+
image_path = os.path.join(os.path.dirname(__file__), "res", "qt_16.png")
|
|
1167
|
+
logo_image_path = os.path.join(os.path.dirname(__file__), "res", "logo.png")
|
|
1168
|
+
toolbar_logo_path = _make_toolbar_logo(logo_image_path)
|
|
1169
|
+
|
|
1170
|
+
get_workbench().add_command(
|
|
1171
|
+
"selmen_command",
|
|
1172
|
+
"tools",
|
|
1173
|
+
tr("Ajouter code PyQt5"),
|
|
1174
|
+
add_pyqt_code,
|
|
1175
|
+
default_sequence=select_sequence("<Control-Shift-B>", "<Command-Shift-B>"),
|
|
1176
|
+
include_in_toolbar=True,
|
|
1177
|
+
caption="PyQt",
|
|
1178
|
+
image=image_path
|
|
1179
|
+
)
|
|
1180
|
+
|
|
1181
|
+
get_workbench().add_command(
|
|
1182
|
+
"show_ui_elements_command",
|
|
1183
|
+
"tools",
|
|
1184
|
+
tr("Afficher les éléments du formulaire UI"),
|
|
1185
|
+
show_ui_elements,
|
|
1186
|
+
include_in_toolbar=True,
|
|
1187
|
+
caption="Éléments UI",
|
|
1188
|
+
image=toolbar_logo_path,
|
|
1189
|
+
)
|
|
1190
|
+
|
|
1191
|
+
get_workbench().add_command(
|
|
1192
|
+
"forget_form_name_command",
|
|
1193
|
+
"tools",
|
|
1194
|
+
tr("Oublier le nom du formulaire"),
|
|
1195
|
+
forget_form_name,
|
|
1196
|
+
)
|
|
1197
|
+
|
|
1198
|
+
# Dossier de sauvegarde : Desktop/IKODE
|
|
1199
|
+
desktop = os.path.join(os.path.expanduser("~"), "Desktop")
|
|
1200
|
+
cwd = os.path.join(desktop, "IKODE")
|
|
1201
|
+
if os.path.exists(cwd) and not os.path.isdir(cwd):
|
|
1202
|
+
cwd = os.path.join(desktop, "IKODE_QT")
|
|
1203
|
+
suffix = 2
|
|
1204
|
+
while os.path.exists(cwd) and not os.path.isdir(cwd):
|
|
1205
|
+
cwd = os.path.join(desktop, f"IKODE_QT_{suffix}")
|
|
1206
|
+
suffix += 1
|
|
1207
|
+
os.makedirs(cwd, exist_ok=True)
|
|
1208
|
+
get_workbench().set_local_cwd(cwd)
|
|
1209
|
+
|
|
1210
|
+
# Ne pas ouvrir les derniers fichiers
|
|
1211
|
+
|
|
1212
|
+
get_workbench().set_option("file.current_file", "")
|
|
1213
|
+
get_workbench().set_option("file.open_files", "")
|
|
Binary file
|
|
Binary file
|
|
Binary file
|