execsql2 2.13.2__py3-none-any.whl → 2.15.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.
- execsql/gui/base.py +52 -1
- execsql/gui/console.py +86 -9
- execsql/gui/desktop.py +261 -39
- execsql/gui/tui.py +325 -51
- execsql/metacommands/connect.py +5 -1
- execsql/metacommands/dispatch.py +49 -6
- execsql/metacommands/io_export.py +2 -2
- execsql/metacommands/prompt.py +6 -11
- execsql/metacommands/upsert.py +125 -17
- execsql/utils/gui.py +2 -2
- {execsql2-2.13.2.dist-info → execsql2-2.15.0.dist-info}/METADATA +3 -3
- {execsql2-2.13.2.dist-info → execsql2-2.15.0.dist-info}/RECORD +31 -31
- {execsql2-2.13.2.data → execsql2-2.15.0.data}/data/execsql2_extras/README.md +0 -0
- {execsql2-2.13.2.data → execsql2-2.15.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.13.2.data → execsql2-2.15.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.13.2.data → execsql2-2.15.0.data}/data/execsql2_extras/execsql.conf +0 -0
- {execsql2-2.13.2.data → execsql2-2.15.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.13.2.data → execsql2-2.15.0.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.13.2.data → execsql2-2.15.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.13.2.data → execsql2-2.15.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.13.2.data → execsql2-2.15.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.13.2.data → execsql2-2.15.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.13.2.data → execsql2-2.15.0.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.13.2.data → execsql2-2.15.0.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.13.2.data → execsql2-2.15.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.13.2.data → execsql2-2.15.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.13.2.data → execsql2-2.15.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.13.2.dist-info → execsql2-2.15.0.dist-info}/WHEEL +0 -0
- {execsql2-2.13.2.dist-info → execsql2-2.15.0.dist-info}/entry_points.txt +0 -0
- {execsql2-2.13.2.dist-info → execsql2-2.15.0.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.13.2.dist-info → execsql2-2.15.0.dist-info}/licenses/NOTICE +0 -0
execsql/gui/desktop.py
CHANGED
|
@@ -50,6 +50,27 @@ def _center_window(win: tk.Tk | tk.Toplevel, width: int = 600, height: int = 400
|
|
|
50
50
|
win.geometry(f"{width}x{height}+{x}+{y}")
|
|
51
51
|
|
|
52
52
|
|
|
53
|
+
from execsql.gui.base import compare_stats as _compare_stats
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _add_help_button(frame: tk.Frame, url: str | None) -> None:
|
|
57
|
+
"""Add a Help button to the top-right of *frame* that opens *url* in the system browser.
|
|
58
|
+
|
|
59
|
+
Uses ``place()`` so the button overlays the top-right corner without
|
|
60
|
+
consuming vertical space in the pack/grid layout.
|
|
61
|
+
"""
|
|
62
|
+
if url:
|
|
63
|
+
import webbrowser
|
|
64
|
+
|
|
65
|
+
btn = ttk.Button(frame, text="Help", command=lambda: webbrowser.open(url))
|
|
66
|
+
btn.place(relx=1.0, x=-4, y=4, anchor="ne")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _row_count_text(n: int) -> str:
|
|
70
|
+
"""Return a human-readable row count string, e.g. '3 rows' or '1 row'."""
|
|
71
|
+
return f"{n:,} row{'s' if n != 1 else ''}"
|
|
72
|
+
|
|
73
|
+
|
|
53
74
|
def _populate_treeview(tree: ttk.Treeview, headers: list, rows: list) -> None:
|
|
54
75
|
"""Fill a ttk.Treeview with column headers and data rows."""
|
|
55
76
|
tree["columns"] = [str(h) for h in headers]
|
|
@@ -91,7 +112,10 @@ class MsgDialog:
|
|
|
91
112
|
frame = ttk.Frame(win, padding=12)
|
|
92
113
|
frame.pack(fill=tk.BOTH, expand=True)
|
|
93
114
|
|
|
94
|
-
ttk.Label(frame, text=args.get("message", ""), wraplength=480).pack(
|
|
115
|
+
ttk.Label(frame, text=args.get("message", ""), wraplength=480, anchor="w", justify="left").pack(
|
|
116
|
+
anchor="w",
|
|
117
|
+
pady=(0, 12),
|
|
118
|
+
)
|
|
95
119
|
|
|
96
120
|
headers = args.get("column_headers")
|
|
97
121
|
rows = args.get("rowset")
|
|
@@ -102,9 +126,10 @@ class MsgDialog:
|
|
|
102
126
|
tree.configure(yscrollcommand=vsb.set)
|
|
103
127
|
tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
|
104
128
|
vsb.pack(side=tk.RIGHT, fill=tk.Y)
|
|
129
|
+
ttk.Label(frame, text=_row_count_text(len(rows))).pack(anchor="w")
|
|
105
130
|
|
|
106
131
|
btn_frame = ttk.Frame(frame)
|
|
107
|
-
btn_frame.pack(pady=8)
|
|
132
|
+
btn_frame.pack(pady=8, anchor="e")
|
|
108
133
|
ttk.Button(btn_frame, text="Close", command=lambda: self._close(win, 1)).pack()
|
|
109
134
|
win.bind("<Return>", lambda e: self._close(win, 1))
|
|
110
135
|
win.bind("<Escape>", lambda e: self._close(win, 1))
|
|
@@ -133,7 +158,10 @@ class PauseDialog:
|
|
|
133
158
|
frame = ttk.Frame(win, padding=12)
|
|
134
159
|
frame.pack(fill=tk.BOTH, expand=True)
|
|
135
160
|
|
|
136
|
-
ttk.Label(frame, text=args.get("message", ""), wraplength=480).pack(
|
|
161
|
+
ttk.Label(frame, text=args.get("message", ""), wraplength=480, anchor="w", justify="left").pack(
|
|
162
|
+
anchor="w",
|
|
163
|
+
pady=(0, 12),
|
|
164
|
+
)
|
|
137
165
|
|
|
138
166
|
countdown = args.get("countdown")
|
|
139
167
|
self._remaining = countdown
|
|
@@ -145,7 +173,7 @@ class PauseDialog:
|
|
|
145
173
|
progress.pack(fill=tk.X, pady=4)
|
|
146
174
|
|
|
147
175
|
btn_frame = ttk.Frame(frame)
|
|
148
|
-
btn_frame.pack(pady=8)
|
|
176
|
+
btn_frame.pack(pady=8, anchor="e")
|
|
149
177
|
ttk.Button(btn_frame, text="Continue", command=lambda: self._close(win, False)).pack(side=tk.LEFT, padx=4)
|
|
150
178
|
ttk.Button(btn_frame, text="Cancel", command=lambda: self._close(win, True)).pack(side=tk.LEFT, padx=4)
|
|
151
179
|
win.bind("<Return>", lambda e: self._close(win, False))
|
|
@@ -192,10 +220,14 @@ class DisplayDialog:
|
|
|
192
220
|
|
|
193
221
|
frame = ttk.Frame(win, padding=12)
|
|
194
222
|
frame.pack(fill=tk.BOTH, expand=True)
|
|
223
|
+
_add_help_button(frame, args.get("help_url"))
|
|
195
224
|
|
|
196
225
|
message = args.get("message", "")
|
|
197
226
|
if message:
|
|
198
|
-
ttk.Label(frame, text=message, wraplength=580).pack(
|
|
227
|
+
ttk.Label(frame, text=message, wraplength=580, anchor="w", justify="left").pack(
|
|
228
|
+
anchor="w",
|
|
229
|
+
pady=(0, 8),
|
|
230
|
+
)
|
|
199
231
|
|
|
200
232
|
headers = args.get("column_headers")
|
|
201
233
|
rows = args.get("rowset")
|
|
@@ -212,6 +244,7 @@ class DisplayDialog:
|
|
|
212
244
|
hsb.grid(row=1, column=0, sticky="ew")
|
|
213
245
|
table_frame.rowconfigure(0, weight=1)
|
|
214
246
|
table_frame.columnconfigure(0, weight=1)
|
|
247
|
+
ttk.Label(frame, text=_row_count_text(len(rows))).pack(anchor="w")
|
|
215
248
|
|
|
216
249
|
self._text_var = None
|
|
217
250
|
textentry = args.get("textentry", False)
|
|
@@ -229,7 +262,7 @@ class DisplayDialog:
|
|
|
229
262
|
no_cancel = args.get("no_cancel", False)
|
|
230
263
|
|
|
231
264
|
btn_frame = ttk.Frame(frame)
|
|
232
|
-
btn_frame.pack(pady=8)
|
|
265
|
+
btn_frame.pack(pady=8, anchor="e")
|
|
233
266
|
_add_buttons(btn_frame, button_list, lambda v: self._close(win, v))
|
|
234
267
|
if not no_cancel:
|
|
235
268
|
win.bind("<Escape>", lambda e: self._close(win, None))
|
|
@@ -262,17 +295,22 @@ class EntryFormDialog:
|
|
|
262
295
|
|
|
263
296
|
main_frame = ttk.Frame(win, padding=12)
|
|
264
297
|
main_frame.pack(fill=tk.BOTH, expand=True)
|
|
298
|
+
_add_help_button(main_frame, args.get("help_url"))
|
|
265
299
|
|
|
266
300
|
message = args.get("message", "")
|
|
267
301
|
if message:
|
|
268
|
-
ttk.Label(main_frame, text=message, wraplength=580).pack(
|
|
302
|
+
ttk.Label(main_frame, text=message, wraplength=580, anchor="w", justify="left").pack(
|
|
303
|
+
anchor="w",
|
|
304
|
+
pady=(0, 8),
|
|
305
|
+
)
|
|
269
306
|
|
|
270
307
|
headers = args.get("column_headers")
|
|
271
308
|
rows = args.get("rowset")
|
|
272
309
|
if headers and rows:
|
|
273
310
|
tree = ttk.Treeview(main_frame, height=min(6, len(rows)))
|
|
274
311
|
_populate_treeview(tree, headers, rows)
|
|
275
|
-
tree.pack(fill=tk.BOTH, expand=True, pady=(0,
|
|
312
|
+
tree.pack(fill=tk.BOTH, expand=True, pady=(0, 4))
|
|
313
|
+
ttk.Label(main_frame, text=_row_count_text(len(rows))).pack(anchor="w", pady=(0, 8))
|
|
276
314
|
|
|
277
315
|
specs = args.get("entry_specs", [])
|
|
278
316
|
form_frame = ttk.Frame(main_frame)
|
|
@@ -280,10 +318,16 @@ class EntryFormDialog:
|
|
|
280
318
|
|
|
281
319
|
for i, spec in enumerate(specs):
|
|
282
320
|
etype = (spec.entry_type or "text").lower()
|
|
283
|
-
|
|
321
|
+
# For radiobuttons, the label is semicolon-delimited: first part is the label
|
|
322
|
+
if etype == "radiobuttons":
|
|
323
|
+
parts = (spec.label or "").split(";")
|
|
324
|
+
field_label = parts[0].strip() if parts else spec.varname
|
|
325
|
+
else:
|
|
326
|
+
field_label = spec.label or spec.varname
|
|
327
|
+
ttk.Label(form_frame, text=field_label, anchor="e").grid(
|
|
284
328
|
row=i,
|
|
285
329
|
column=0,
|
|
286
|
-
sticky="
|
|
330
|
+
sticky="ne",
|
|
287
331
|
padx=4,
|
|
288
332
|
pady=2,
|
|
289
333
|
)
|
|
@@ -297,35 +341,108 @@ class EntryFormDialog:
|
|
|
297
341
|
combo = ttk.Combobox(form_frame, textvariable=var, values=spec.lookup_list, state="readonly")
|
|
298
342
|
combo.grid(row=i, column=1, sticky="ew", pady=2)
|
|
299
343
|
self._widgets[spec.varname] = ("dropdown", var)
|
|
344
|
+
elif etype == "listbox" and spec.lookup_list:
|
|
345
|
+
height = spec.default_height or 4
|
|
346
|
+
lb = tk.Listbox(form_frame, selectmode=tk.MULTIPLE, height=height, exportselection=False)
|
|
347
|
+
for item in spec.lookup_list:
|
|
348
|
+
lb.insert(tk.END, item)
|
|
349
|
+
lb.grid(row=i, column=1, sticky="ew", pady=2)
|
|
350
|
+
self._widgets[spec.varname] = ("listbox", lb)
|
|
351
|
+
elif etype == "radiobuttons":
|
|
352
|
+
buttons = parts[1:] if len(parts) > 1 else ["Option"]
|
|
353
|
+
var = tk.IntVar(value=0)
|
|
354
|
+
rb_frame = ttk.Frame(form_frame)
|
|
355
|
+
rb_frame.grid(row=i, column=1, sticky="w", pady=2)
|
|
356
|
+
for j, btn_label in enumerate(buttons):
|
|
357
|
+
ttk.Radiobutton(rb_frame, text=btn_label.strip(), variable=var, value=j).pack(anchor="w")
|
|
358
|
+
self._widgets[spec.varname] = ("radiobuttons", var)
|
|
359
|
+
elif etype == "textarea":
|
|
360
|
+
height = spec.default_height or 5
|
|
361
|
+
ta = tk.Text(form_frame, width=40, height=height, wrap=tk.WORD)
|
|
362
|
+
if spec.initial_value:
|
|
363
|
+
ta.insert("1.0", spec.initial_value)
|
|
364
|
+
ta.grid(row=i, column=1, sticky="ew", pady=2)
|
|
365
|
+
self._widgets[spec.varname] = ("textarea", ta)
|
|
366
|
+
elif etype in ("inputfile", "outputfile"):
|
|
367
|
+
file_frame = ttk.Frame(form_frame)
|
|
368
|
+
file_frame.grid(row=i, column=1, sticky="ew", pady=2)
|
|
369
|
+
var = tk.StringVar(value=spec.initial_value or "")
|
|
370
|
+
ttk.Entry(file_frame, textvariable=var, width=30).pack(side=tk.LEFT, fill=tk.X, expand=True)
|
|
371
|
+
|
|
372
|
+
def _browse(sv=var, mode=etype):
|
|
373
|
+
from tkinter import filedialog
|
|
374
|
+
|
|
375
|
+
if mode == "inputfile":
|
|
376
|
+
fn = filedialog.askopenfilename()
|
|
377
|
+
else:
|
|
378
|
+
fn = filedialog.asksaveasfilename()
|
|
379
|
+
if fn:
|
|
380
|
+
sv.set(fn)
|
|
381
|
+
|
|
382
|
+
ttk.Button(file_frame, text="Browse…", command=_browse).pack(side=tk.LEFT, padx=(4, 0))
|
|
383
|
+
self._widgets[spec.varname] = ("file", var)
|
|
300
384
|
else:
|
|
301
385
|
var = tk.StringVar(value=spec.initial_value or "")
|
|
302
386
|
entry = ttk.Entry(form_frame, textvariable=var, width=40)
|
|
387
|
+
if spec.validation_key_regex:
|
|
388
|
+
import re as _re
|
|
389
|
+
|
|
390
|
+
_pat = _re.compile(spec.validation_key_regex)
|
|
391
|
+
vcmd = (win.register(lambda val, p=_pat: bool(p.match(val))), "%P")
|
|
392
|
+
entry.configure(validate="key", validatecommand=vcmd)
|
|
303
393
|
entry.grid(row=i, column=1, sticky="ew", pady=2)
|
|
304
394
|
self._widgets[spec.varname] = ("text", var)
|
|
305
395
|
form_frame.columnconfigure(1, weight=1)
|
|
306
396
|
|
|
307
397
|
btn_frame = ttk.Frame(main_frame)
|
|
308
|
-
btn_frame.pack(pady=8)
|
|
398
|
+
btn_frame.pack(pady=8, anchor="e")
|
|
309
399
|
ttk.Button(btn_frame, text="OK", command=lambda: self._close(win, specs, True)).pack(side=tk.LEFT, padx=4)
|
|
310
400
|
ttk.Button(btn_frame, text="Cancel", command=lambda: self._close(win, specs, False)).pack(side=tk.LEFT, padx=4)
|
|
311
401
|
win.bind("<Return>", lambda e: self._close(win, specs, True))
|
|
312
402
|
win.bind("<Escape>", lambda e: self._close(win, specs, False))
|
|
313
403
|
|
|
314
404
|
self._specs = specs
|
|
315
|
-
|
|
405
|
+
extra_height = sum(
|
|
406
|
+
(spec.default_height or 5) * 18 if (spec.entry_type or "").lower() in ("textarea", "listbox") else 0
|
|
407
|
+
for spec in specs
|
|
408
|
+
)
|
|
409
|
+
_center_window(win, 500, 120 + len(specs) * 32 + extra_height)
|
|
316
410
|
root.wait_window(win)
|
|
317
411
|
|
|
318
412
|
def _close(self, win: tk.Toplevel, specs: list, ok: bool) -> None:
|
|
319
413
|
if ok:
|
|
414
|
+
import re as _re
|
|
415
|
+
|
|
320
416
|
for spec in specs:
|
|
321
417
|
entry = self._widgets.get(spec.varname)
|
|
322
418
|
if entry is None:
|
|
323
419
|
continue
|
|
324
420
|
kind, var = entry
|
|
325
421
|
if kind == "checkbox":
|
|
326
|
-
spec.value = "
|
|
422
|
+
spec.value = "1" if var.get() else "0"
|
|
423
|
+
elif kind == "listbox":
|
|
424
|
+
selected = [var.get(i) for i in var.curselection()]
|
|
425
|
+
spec.value = ",".join(f"'{v.replace(chr(39), chr(39) + chr(39))}'" for v in selected)
|
|
426
|
+
elif kind == "radiobuttons":
|
|
427
|
+
spec.value = str(var.get() + 1)
|
|
428
|
+
elif kind == "textarea":
|
|
429
|
+
spec.value = var.get("1.0", tk.END).rstrip("\n")
|
|
327
430
|
else:
|
|
328
431
|
spec.value = var.get()
|
|
432
|
+
# Validate required fields and validation_regex
|
|
433
|
+
errors = []
|
|
434
|
+
for spec in specs:
|
|
435
|
+
val = spec.value or ""
|
|
436
|
+
etype = (spec.entry_type or "text").lower()
|
|
437
|
+
if spec.required and not val and etype != "checkbox":
|
|
438
|
+
errors.append(f"{spec.label or spec.varname}: required")
|
|
439
|
+
if spec.validation_regex and val and not _re.fullmatch(spec.validation_regex, val):
|
|
440
|
+
errors.append(f"{spec.label or spec.varname}: does not match pattern")
|
|
441
|
+
if errors:
|
|
442
|
+
from tkinter import messagebox
|
|
443
|
+
|
|
444
|
+
messagebox.showerror("Validation Error", "\n".join(errors), parent=win)
|
|
445
|
+
return
|
|
329
446
|
self.result = {"button": 1, "return_value": specs}
|
|
330
447
|
else:
|
|
331
448
|
self.result = {"button": None, "return_value": specs}
|
|
@@ -348,29 +465,45 @@ class CompareDialog:
|
|
|
348
465
|
|
|
349
466
|
frame = ttk.Frame(win, padding=12)
|
|
350
467
|
frame.pack(fill=tk.BOTH, expand=True)
|
|
468
|
+
_add_help_button(frame, args.get("help_url"))
|
|
351
469
|
|
|
352
470
|
message = args.get("message", "")
|
|
353
471
|
if message:
|
|
354
|
-
ttk.Label(frame, text=message, wraplength=780).pack(
|
|
472
|
+
ttk.Label(frame, text=message, wraplength=780, anchor="w", justify="left").pack(
|
|
473
|
+
anchor="w",
|
|
474
|
+
pady=(0, 8),
|
|
475
|
+
)
|
|
355
476
|
|
|
356
477
|
headers1 = args.get("headers1", [])
|
|
357
478
|
rows1 = args.get("rows1", [])
|
|
358
479
|
headers2 = args.get("headers2", [])
|
|
359
480
|
rows2 = args.get("rows2", [])
|
|
360
481
|
keylist = [str(k) for k in args.get("keylist", [])]
|
|
482
|
+
sidebyside = args.get("sidebyside", True)
|
|
483
|
+
|
|
484
|
+
# Reserve a frame for the diff button (packed before tables, populated after)
|
|
485
|
+
diff_frame = ttk.Frame(frame) if keylist else None
|
|
486
|
+
if diff_frame:
|
|
487
|
+
diff_frame.pack(anchor="w", pady=(0, 4))
|
|
361
488
|
|
|
362
489
|
tables_frame = ttk.Frame(frame)
|
|
363
490
|
tables_frame.pack(fill=tk.BOTH, expand=True)
|
|
364
491
|
|
|
492
|
+
pack_side = tk.LEFT if sidebyside else tk.TOP
|
|
493
|
+
max_tree_height = 12 if sidebyside else 8
|
|
494
|
+
|
|
365
495
|
def _add_tree(parent, label, headers, rows):
|
|
366
496
|
lf = ttk.LabelFrame(parent, text=label)
|
|
367
|
-
lf.pack(side=
|
|
368
|
-
|
|
497
|
+
lf.pack(side=pack_side, fill=tk.BOTH, expand=True, padx=4, pady=2)
|
|
498
|
+
tree_frame = ttk.Frame(lf)
|
|
499
|
+
tree_frame.pack(fill=tk.BOTH, expand=True)
|
|
500
|
+
tree = ttk.Treeview(tree_frame, height=min(max_tree_height, len(rows)))
|
|
369
501
|
_populate_treeview(tree, headers, rows)
|
|
370
|
-
vsb = ttk.Scrollbar(
|
|
502
|
+
vsb = ttk.Scrollbar(tree_frame, orient=tk.VERTICAL, command=tree.yview)
|
|
371
503
|
tree.configure(yscrollcommand=vsb.set)
|
|
372
504
|
tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
|
373
505
|
vsb.pack(side=tk.RIGHT, fill=tk.Y)
|
|
506
|
+
ttk.Label(lf, text=_row_count_text(len(rows))).pack(anchor="w")
|
|
374
507
|
return tree
|
|
375
508
|
|
|
376
509
|
tree1 = _add_tree(tables_frame, "Table 1", headers1, rows1)
|
|
@@ -414,13 +547,63 @@ class CompareDialog:
|
|
|
414
547
|
tree1.bind("<ButtonRelease-1>", _on_click1)
|
|
415
548
|
tree2.bind("<ButtonRelease-1>", _on_click2)
|
|
416
549
|
|
|
550
|
+
# --- Highlight Diffs button (populate the pre-created diff_frame) ---
|
|
551
|
+
if keylist and diff_frame is not None:
|
|
552
|
+
tree1.tag_configure("diff_match", background="#a3d9a5", foreground="#1a3a1a")
|
|
553
|
+
tree1.tag_configure("diff_changed", background="#f5d98e", foreground="#3a2e00")
|
|
554
|
+
tree1.tag_configure("diff_only", background="#f5a3a3", foreground="#3a0a0a")
|
|
555
|
+
tree2.tag_configure("diff_match", background="#a3d9a5", foreground="#1a3a1a")
|
|
556
|
+
tree2.tag_configure("diff_changed", background="#f5d98e", foreground="#3a2e00")
|
|
557
|
+
tree2.tag_configure("diff_only", background="#f5a3a3", foreground="#3a0a0a")
|
|
558
|
+
_diff_on = [False]
|
|
559
|
+
|
|
560
|
+
def _toggle_diffs():
|
|
561
|
+
_diff_on[0] = not _diff_on[0]
|
|
562
|
+
if not _diff_on[0]:
|
|
563
|
+
for iid in tree1.get_children():
|
|
564
|
+
tree1.item(iid, tags=())
|
|
565
|
+
for iid in tree2.get_children():
|
|
566
|
+
tree2.item(iid, tags=())
|
|
567
|
+
return
|
|
568
|
+
keys1_set = set(iid_to_kv1.values())
|
|
569
|
+
keys2_set = set(iid_to_kv2.values())
|
|
570
|
+
for iid, kv in iid_to_kv1.items():
|
|
571
|
+
if kv not in keys2_set:
|
|
572
|
+
tree1.item(iid, tags=("diff_only",))
|
|
573
|
+
else:
|
|
574
|
+
r1 = [str(v) if v is not None else "" for v in rows1[list(iids1).index(iid)]]
|
|
575
|
+
match_iid = kv_to_iid2.get(kv)
|
|
576
|
+
if match_iid:
|
|
577
|
+
r2 = [str(v) if v is not None else "" for v in rows2[list(iids2).index(match_iid)]]
|
|
578
|
+
tree1.item(iid, tags=("diff_match",) if r1 == r2 else ("diff_changed",))
|
|
579
|
+
for iid, kv in iid_to_kv2.items():
|
|
580
|
+
if kv not in keys1_set:
|
|
581
|
+
tree2.item(iid, tags=("diff_only",))
|
|
582
|
+
else:
|
|
583
|
+
r2 = [str(v) if v is not None else "" for v in rows2[list(iids2).index(iid)]]
|
|
584
|
+
match_iid = kv_to_iid1.get(kv)
|
|
585
|
+
if match_iid:
|
|
586
|
+
r1 = [str(v) if v is not None else "" for v in rows1[list(iids1).index(match_iid)]]
|
|
587
|
+
tree2.item(iid, tags=("diff_match",) if r1 == r2 else ("diff_changed",))
|
|
588
|
+
|
|
589
|
+
ttk.Button(diff_frame, text="Highlight Diffs", command=_toggle_diffs).pack(side=tk.LEFT)
|
|
590
|
+
ttk.Label(diff_frame, text=" ").pack(side=tk.LEFT)
|
|
591
|
+
tk.Label(diff_frame, text=" Match ", bg="#a3d9a5", fg="#1a3a1a", padx=4).pack(side=tk.LEFT, padx=2)
|
|
592
|
+
tk.Label(diff_frame, text=" Changed ", bg="#f5d98e", fg="#3a2e00", padx=4).pack(side=tk.LEFT, padx=2)
|
|
593
|
+
tk.Label(diff_frame, text=" Only in one ", bg="#f5a3a3", fg="#3a0a0a", padx=4).pack(side=tk.LEFT, padx=2)
|
|
594
|
+
|
|
595
|
+
summary = _compare_stats(headers1, rows1, headers2, rows2, keylist)
|
|
596
|
+
if summary:
|
|
597
|
+
ttk.Label(frame, text=summary).pack(anchor="w", pady=(4, 0))
|
|
598
|
+
|
|
417
599
|
button_list = args.get("button_list", [("Continue", 1, "<Return>")])
|
|
418
600
|
btn_frame = ttk.Frame(frame)
|
|
419
|
-
btn_frame.pack(pady=8)
|
|
601
|
+
btn_frame.pack(pady=8, anchor="e")
|
|
420
602
|
_add_buttons(btn_frame, button_list, lambda v: self._close(win, v))
|
|
421
603
|
win.bind("<Escape>", lambda e: self._close(win, None))
|
|
422
604
|
|
|
423
|
-
|
|
605
|
+
win_height = 700 if not sidebyside else 500
|
|
606
|
+
_center_window(win, 900, win_height)
|
|
424
607
|
root.wait_window(win)
|
|
425
608
|
|
|
426
609
|
def _close(self, win: tk.Toplevel, value: int | None) -> None:
|
|
@@ -444,11 +627,18 @@ class SelectRowsDialog:
|
|
|
444
627
|
|
|
445
628
|
frame = ttk.Frame(win, padding=12)
|
|
446
629
|
frame.pack(fill=tk.BOTH, expand=True)
|
|
630
|
+
_add_help_button(frame, args.get("help_url"))
|
|
447
631
|
|
|
448
632
|
message = args.get("message", "")
|
|
449
633
|
if message:
|
|
450
|
-
ttk.Label(frame, text=message, wraplength=780).pack(
|
|
451
|
-
|
|
634
|
+
ttk.Label(frame, text=message, wraplength=780, anchor="w", justify="left").pack(
|
|
635
|
+
anchor="w",
|
|
636
|
+
pady=(0, 4),
|
|
637
|
+
)
|
|
638
|
+
ttk.Label(frame, text="Double-click a row to copy it to the destination table.").pack(
|
|
639
|
+
anchor="w",
|
|
640
|
+
pady=(0, 8),
|
|
641
|
+
)
|
|
452
642
|
|
|
453
643
|
headers1 = args.get("headers1", [])
|
|
454
644
|
rows1 = args.get("rows1", [])
|
|
@@ -461,12 +651,15 @@ class SelectRowsDialog:
|
|
|
461
651
|
def _add_tree_frame(parent, label, headers, rows):
|
|
462
652
|
lf = ttk.LabelFrame(parent, text=label)
|
|
463
653
|
lf.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=4)
|
|
464
|
-
|
|
654
|
+
tree_frame = ttk.Frame(lf)
|
|
655
|
+
tree_frame.pack(fill=tk.BOTH, expand=True)
|
|
656
|
+
tree = ttk.Treeview(tree_frame, height=min(12, max(len(rows1), len(rows2))))
|
|
465
657
|
_populate_treeview(tree, headers, rows)
|
|
466
|
-
vsb = ttk.Scrollbar(
|
|
658
|
+
vsb = ttk.Scrollbar(tree_frame, orient=tk.VERTICAL, command=tree.yview)
|
|
467
659
|
tree.configure(yscrollcommand=vsb.set)
|
|
468
660
|
tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
|
469
661
|
vsb.pack(side=tk.RIGHT, fill=tk.Y)
|
|
662
|
+
ttk.Label(lf, text=_row_count_text(len(rows))).pack(anchor="w")
|
|
470
663
|
return tree
|
|
471
664
|
|
|
472
665
|
src_tree = _add_tree_frame(tables_frame, "Source", headers1, rows1)
|
|
@@ -484,7 +677,7 @@ class SelectRowsDialog:
|
|
|
484
677
|
|
|
485
678
|
button_list = args.get("button_list", [("Continue", 1, "<Return>")])
|
|
486
679
|
btn_frame = ttk.Frame(frame)
|
|
487
|
-
btn_frame.pack(pady=8)
|
|
680
|
+
btn_frame.pack(pady=8, anchor="e")
|
|
488
681
|
_add_buttons(btn_frame, button_list, lambda v: self._close(win, v))
|
|
489
682
|
win.bind("<Escape>", lambda e: self._close(win, None))
|
|
490
683
|
|
|
@@ -512,20 +705,27 @@ class SelectSubDialog:
|
|
|
512
705
|
|
|
513
706
|
frame = ttk.Frame(win, padding=12)
|
|
514
707
|
frame.pack(fill=tk.BOTH, expand=True)
|
|
708
|
+
_add_help_button(frame, args.get("help_url"))
|
|
515
709
|
|
|
516
710
|
message = args.get("message", "")
|
|
517
711
|
if message:
|
|
518
|
-
ttk.Label(frame, text=message, wraplength=580).pack(
|
|
712
|
+
ttk.Label(frame, text=message, wraplength=580, anchor="w", justify="left").pack(
|
|
713
|
+
anchor="w",
|
|
714
|
+
pady=(0, 8),
|
|
715
|
+
)
|
|
519
716
|
|
|
520
717
|
headers = args.get("headers", [])
|
|
521
718
|
rows = args.get("rows", [])
|
|
522
719
|
|
|
523
|
-
|
|
720
|
+
tree_frame = ttk.Frame(frame)
|
|
721
|
+
tree_frame.pack(fill=tk.BOTH, expand=True)
|
|
722
|
+
tree = ttk.Treeview(tree_frame, height=min(10, len(rows)))
|
|
524
723
|
_populate_treeview(tree, headers, rows)
|
|
525
|
-
vsb = ttk.Scrollbar(
|
|
724
|
+
vsb = ttk.Scrollbar(tree_frame, orient=tk.VERTICAL, command=tree.yview)
|
|
526
725
|
tree.configure(yscrollcommand=vsb.set)
|
|
527
726
|
tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
|
528
727
|
vsb.pack(side=tk.RIGHT, fill=tk.Y)
|
|
728
|
+
ttk.Label(frame, text=_row_count_text(len(rows))).pack(anchor="w")
|
|
529
729
|
|
|
530
730
|
def _select():
|
|
531
731
|
sel = tree.selection()
|
|
@@ -536,7 +736,7 @@ class SelectSubDialog:
|
|
|
536
736
|
win.destroy()
|
|
537
737
|
|
|
538
738
|
btn_frame = ttk.Frame(frame)
|
|
539
|
-
btn_frame.pack(pady=8)
|
|
739
|
+
btn_frame.pack(pady=8, anchor="e")
|
|
540
740
|
ttk.Button(btn_frame, text="OK", command=_select).pack(side=tk.LEFT, padx=4)
|
|
541
741
|
ttk.Button(btn_frame, text="Cancel", command=lambda: self._close(win)).pack(side=tk.LEFT, padx=4)
|
|
542
742
|
tree.bind("<Double-1>", lambda e: _select())
|
|
@@ -566,17 +766,22 @@ class ActionDialog:
|
|
|
566
766
|
|
|
567
767
|
frame = ttk.Frame(win, padding=12)
|
|
568
768
|
frame.pack(fill=tk.BOTH, expand=True)
|
|
769
|
+
_add_help_button(frame, args.get("help_url"))
|
|
569
770
|
|
|
570
771
|
message = args.get("message", "")
|
|
571
772
|
if message:
|
|
572
|
-
ttk.Label(frame, text=message, wraplength=580).pack(
|
|
773
|
+
ttk.Label(frame, text=message, wraplength=580, anchor="w", justify="left").pack(
|
|
774
|
+
anchor="w",
|
|
775
|
+
pady=(0, 8),
|
|
776
|
+
)
|
|
573
777
|
|
|
574
778
|
headers = args.get("column_headers")
|
|
575
779
|
rows = args.get("rowset")
|
|
576
780
|
if headers and rows:
|
|
577
781
|
tree = ttk.Treeview(frame, height=min(6, len(rows)))
|
|
578
782
|
_populate_treeview(tree, headers, rows)
|
|
579
|
-
tree.pack(fill=tk.BOTH, expand=True, pady=(0,
|
|
783
|
+
tree.pack(fill=tk.BOTH, expand=True, pady=(0, 4))
|
|
784
|
+
ttk.Label(frame, text=_row_count_text(len(rows))).pack(anchor="w", pady=(0, 8))
|
|
580
785
|
|
|
581
786
|
button_specs = args.get("button_specs", [])
|
|
582
787
|
for i, spec in enumerate(button_specs):
|
|
@@ -619,22 +824,31 @@ class MapDialog:
|
|
|
619
824
|
|
|
620
825
|
message = args.get("message", "")
|
|
621
826
|
if message:
|
|
622
|
-
ttk.Label(frame, text=message, wraplength=580).pack(
|
|
623
|
-
|
|
827
|
+
ttk.Label(frame, text=message, wraplength=580, anchor="w", justify="left").pack(
|
|
828
|
+
anchor="w",
|
|
829
|
+
pady=(0, 4),
|
|
830
|
+
)
|
|
831
|
+
ttk.Label(frame, text="(Interactive map requires tkintermapview; showing tabular data)").pack(
|
|
832
|
+
anchor="w",
|
|
833
|
+
pady=(0, 8),
|
|
834
|
+
)
|
|
624
835
|
|
|
625
836
|
headers = args.get("headers", [])
|
|
626
837
|
rows = args.get("rows", [])
|
|
627
838
|
if headers and rows:
|
|
628
|
-
|
|
839
|
+
tree_frame = ttk.Frame(frame)
|
|
840
|
+
tree_frame.pack(fill=tk.BOTH, expand=True)
|
|
841
|
+
tree = ttk.Treeview(tree_frame, height=min(12, len(rows)))
|
|
629
842
|
_populate_treeview(tree, headers, rows)
|
|
630
|
-
vsb = ttk.Scrollbar(
|
|
843
|
+
vsb = ttk.Scrollbar(tree_frame, orient=tk.VERTICAL, command=tree.yview)
|
|
631
844
|
tree.configure(yscrollcommand=vsb.set)
|
|
632
845
|
tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
|
633
846
|
vsb.pack(side=tk.RIGHT, fill=tk.Y)
|
|
847
|
+
ttk.Label(frame, text=_row_count_text(len(rows))).pack(anchor="w")
|
|
634
848
|
|
|
635
849
|
button_list = args.get("button_list", [("Continue", 1, "<Return>")])
|
|
636
850
|
btn_frame = ttk.Frame(frame)
|
|
637
|
-
btn_frame.pack(pady=8)
|
|
851
|
+
btn_frame.pack(pady=8, anchor="e")
|
|
638
852
|
_add_buttons(btn_frame, button_list, lambda v: self._close(win, v))
|
|
639
853
|
|
|
640
854
|
_center_window(win, 600, 450)
|
|
@@ -661,10 +875,14 @@ class CredentialsDialog:
|
|
|
661
875
|
|
|
662
876
|
frame = ttk.Frame(win, padding=12)
|
|
663
877
|
frame.pack(fill=tk.BOTH, expand=True)
|
|
878
|
+
_add_help_button(frame, args.get("help_url"))
|
|
664
879
|
|
|
665
880
|
message = args.get("message", "")
|
|
666
881
|
if message:
|
|
667
|
-
ttk.Label(frame, text=message, wraplength=380).pack(
|
|
882
|
+
ttk.Label(frame, text=message, wraplength=380, anchor="w", justify="left").pack(
|
|
883
|
+
anchor="w",
|
|
884
|
+
pady=(0, 8),
|
|
885
|
+
)
|
|
668
886
|
|
|
669
887
|
user_var = tk.StringVar()
|
|
670
888
|
pw_var = tk.StringVar()
|
|
@@ -677,7 +895,7 @@ class CredentialsDialog:
|
|
|
677
895
|
frame.columnconfigure(1, weight=1)
|
|
678
896
|
|
|
679
897
|
btn_frame = ttk.Frame(frame)
|
|
680
|
-
btn_frame.grid(row=2, column=0, columnspan=2, pady=8)
|
|
898
|
+
btn_frame.grid(row=2, column=0, columnspan=2, pady=8, sticky="e")
|
|
681
899
|
ttk.Button(btn_frame, text="OK", command=lambda: self._close(win, user_var.get(), pw_var.get())).pack(
|
|
682
900
|
side=tk.LEFT,
|
|
683
901
|
padx=4,
|
|
@@ -722,10 +940,14 @@ class ConnectDialog:
|
|
|
722
940
|
|
|
723
941
|
frame = ttk.Frame(win, padding=12)
|
|
724
942
|
frame.pack(fill=tk.BOTH, expand=True)
|
|
943
|
+
_add_help_button(frame, args.get("help_url"))
|
|
725
944
|
|
|
726
945
|
message = args.get("message", "")
|
|
727
946
|
if message:
|
|
728
|
-
ttk.Label(frame, text=message, wraplength=480).pack(
|
|
947
|
+
ttk.Label(frame, text=message, wraplength=480, anchor="w", justify="left").pack(
|
|
948
|
+
anchor="w",
|
|
949
|
+
pady=(0, 8),
|
|
950
|
+
)
|
|
729
951
|
|
|
730
952
|
type_var = tk.StringVar(value="p")
|
|
731
953
|
server_var = tk.StringVar()
|
|
@@ -756,7 +978,7 @@ class ConnectDialog:
|
|
|
756
978
|
ttk.Entry(form, textvariable=user_var, width=35).grid(row=3, column=1, sticky="ew", pady=4)
|
|
757
979
|
|
|
758
980
|
btn_frame = ttk.Frame(frame)
|
|
759
|
-
btn_frame.pack(pady=8)
|
|
981
|
+
btn_frame.pack(pady=8, anchor="e")
|
|
760
982
|
|
|
761
983
|
def _on_connect():
|
|
762
984
|
idx = type_combo.current()
|