dokibox 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.
- dokibox/__init__.py +7 -0
- dokibox/_base.py +150 -0
- dokibox/choicebox.py +264 -0
- dokibox/dialogbox.py +507 -0
- dokibox/garbled.py +67 -0
- dokibox/msgbox.py +161 -0
- dokibox/ynbox.py +214 -0
- dokibox-1.0.0.dist-info/METADATA +150 -0
- dokibox-1.0.0.dist-info/RECORD +11 -0
- dokibox-1.0.0.dist-info/WHEEL +5 -0
- dokibox-1.0.0.dist-info/top_level.txt +1 -0
dokibox/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""dokibox -- DDLC-style dialog library made by 99"""
|
|
3
|
+
from dokibox.ynbox import ynbox
|
|
4
|
+
from dokibox.choicebox import choicebox
|
|
5
|
+
from dokibox.msgbox import msgbox
|
|
6
|
+
from dokibox.dialogbox import dialogbox
|
|
7
|
+
from dokibox.garbled import garbled
|
dokibox/_base.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""dokibox internal base class -- window / gradient border / stroked text / dragging"""
|
|
3
|
+
import tkinter as tk
|
|
4
|
+
import math
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# --- default colors ---
|
|
8
|
+
BORDER_COLOR = "#FFBBE3"
|
|
9
|
+
BODY_COLOR = "#FEE6F4"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _get_dpi_scale():
|
|
13
|
+
try:
|
|
14
|
+
import ctypes
|
|
15
|
+
hdc = ctypes.windll.user32.GetDC(0)
|
|
16
|
+
dpi = ctypes.windll.gdi32.GetDeviceCaps(hdc, 88)
|
|
17
|
+
ctypes.windll.user32.ReleaseDC(0, hdc)
|
|
18
|
+
return dpi / 96.0
|
|
19
|
+
except Exception:
|
|
20
|
+
return 1.0
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# --- shared Tk root (NEVER destroyed, for Python 3.9 compat) ---
|
|
24
|
+
_root_instance = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _get_root():
|
|
28
|
+
global _root_instance
|
|
29
|
+
if _root_instance is None:
|
|
30
|
+
_root_instance = tk.Tk()
|
|
31
|
+
_root_instance.withdraw()
|
|
32
|
+
return _root_instance
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class _DokiBase:
|
|
36
|
+
"""Dialog base class. Subclasses only need to implement _calc_size / _draw_content / _on_click"""
|
|
37
|
+
|
|
38
|
+
BORDER_W = 12
|
|
39
|
+
|
|
40
|
+
def __init__(self, msg, title="", pinned=True):
|
|
41
|
+
self.result = None
|
|
42
|
+
self._px = 0
|
|
43
|
+
self._py = 0
|
|
44
|
+
self._ox = 0
|
|
45
|
+
self._oy = 0
|
|
46
|
+
|
|
47
|
+
self.root = tk.Toplevel(_get_root())
|
|
48
|
+
self.root.overrideredirect(True)
|
|
49
|
+
self.root.attributes('-topmost', pinned)
|
|
50
|
+
|
|
51
|
+
self.w, self.h = self._calc_size(msg)
|
|
52
|
+
sw = self.root.winfo_screenwidth()
|
|
53
|
+
sh = self.root.winfo_screenheight()
|
|
54
|
+
x = (sw - self.w) // 2
|
|
55
|
+
y = (sh - self.h) // 2
|
|
56
|
+
self.root.geometry(f"{self.w}x{self.h}+{x}+{y}")
|
|
57
|
+
|
|
58
|
+
self.cv = tk.Canvas(
|
|
59
|
+
self.root, width=self.w, height=self.h,
|
|
60
|
+
bg=BODY_COLOR, highlightthickness=0
|
|
61
|
+
)
|
|
62
|
+
self.cv.pack()
|
|
63
|
+
|
|
64
|
+
self._draw_gradient_border()
|
|
65
|
+
self._draw_content(msg)
|
|
66
|
+
|
|
67
|
+
self.root.bind("<Escape>", lambda e: self._done(False))
|
|
68
|
+
self._make_draggable()
|
|
69
|
+
self.root.focus_force()
|
|
70
|
+
|
|
71
|
+
# ========== subclass must implement ==========
|
|
72
|
+
|
|
73
|
+
def _calc_size(self, msg):
|
|
74
|
+
raise NotImplementedError
|
|
75
|
+
|
|
76
|
+
def _draw_content(self, msg):
|
|
77
|
+
raise NotImplementedError
|
|
78
|
+
|
|
79
|
+
def _on_click(self, event):
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
# ========== shared drawing utilities ==========
|
|
83
|
+
|
|
84
|
+
@staticmethod
|
|
85
|
+
def _hex_to_rgb(hex_color):
|
|
86
|
+
h = hex_color.lstrip('#')
|
|
87
|
+
return tuple(int(h[i:i + 2], 16) for i in (0, 2, 4))
|
|
88
|
+
|
|
89
|
+
def _draw_gradient_border(self):
|
|
90
|
+
br, bg, bb = self._hex_to_rgb(BORDER_COLOR)
|
|
91
|
+
er, eg, eb = self._hex_to_rgb(BODY_COLOR)
|
|
92
|
+
bw = self.BORDER_W
|
|
93
|
+
for i in range(bw):
|
|
94
|
+
t = (i / max(bw - 1, 1)) ** 3
|
|
95
|
+
r = int(br + (er - br) * t)
|
|
96
|
+
g = int(bg + (eg - bg) * t)
|
|
97
|
+
b = int(bb + (eb - bb) * t)
|
|
98
|
+
color = f'#{r:02x}{g:02x}{b:02x}'
|
|
99
|
+
self.cv.create_rectangle(i, i, self.w - i, self.h - i,
|
|
100
|
+
outline=color, width=1)
|
|
101
|
+
|
|
102
|
+
def _draw_stroked_text(self, x, y, text, font, fill, stroke, stroke_w):
|
|
103
|
+
for step in range(24):
|
|
104
|
+
angle = 2 * math.pi * step / 24
|
|
105
|
+
dx = stroke_w * math.cos(angle)
|
|
106
|
+
dy = stroke_w * math.sin(angle)
|
|
107
|
+
self.cv.create_text(x + dx, y + dy, text=text,
|
|
108
|
+
font=font, fill=stroke, anchor="center")
|
|
109
|
+
self.cv.create_text(x, y, text=text, font=font,
|
|
110
|
+
fill=fill, anchor="center")
|
|
111
|
+
|
|
112
|
+
# ========== dragging ==========
|
|
113
|
+
|
|
114
|
+
def _make_draggable(self):
|
|
115
|
+
self.cv.bind("<ButtonPress-1>", self._on_press, add='+')
|
|
116
|
+
self.cv.bind("<B1-Motion>", self._on_motion, add='+')
|
|
117
|
+
self.cv.bind("<ButtonRelease-1>", self._on_release, add='+')
|
|
118
|
+
|
|
119
|
+
def _on_press(self, event):
|
|
120
|
+
self._px = event.x_root
|
|
121
|
+
self._py = event.y_root
|
|
122
|
+
self._ox = event.x_root - self.root.winfo_x()
|
|
123
|
+
self._oy = event.y_root - self.root.winfo_y()
|
|
124
|
+
|
|
125
|
+
def _on_motion(self, event):
|
|
126
|
+
new_x = event.x_root - self._ox
|
|
127
|
+
new_y = event.y_root - self._oy
|
|
128
|
+
self.root.geometry(f"+{new_x}+{new_y}")
|
|
129
|
+
|
|
130
|
+
def _on_release(self, event):
|
|
131
|
+
dx = abs(event.x_root - self._px)
|
|
132
|
+
dy = abs(event.y_root - self._py)
|
|
133
|
+
if dx < 5 and dy < 5:
|
|
134
|
+
self._on_click(event)
|
|
135
|
+
|
|
136
|
+
# ========== lifecycle ==========
|
|
137
|
+
|
|
138
|
+
def _done(self, value):
|
|
139
|
+
self.result = value
|
|
140
|
+
try:
|
|
141
|
+
self.root.destroy()
|
|
142
|
+
_get_root().quit()
|
|
143
|
+
except tk.TclError:
|
|
144
|
+
pass
|
|
145
|
+
|
|
146
|
+
@classmethod
|
|
147
|
+
def show(cls, *args, **kwargs):
|
|
148
|
+
dialog = cls(*args, **kwargs)
|
|
149
|
+
_get_root().mainloop()
|
|
150
|
+
return dialog.result
|
dokibox/choicebox.py
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""dokibox.choicebox -- DDLC-style multi-choice dialog (floating windows per option)"""
|
|
3
|
+
import tkinter as tk
|
|
4
|
+
import tkinter.font as tkfont
|
|
5
|
+
from typing import Optional, List
|
|
6
|
+
from dokibox._base import _get_root
|
|
7
|
+
|
|
8
|
+
BORDER_COLOR = "#FFBBE3"
|
|
9
|
+
BODY_COLOR = "#FEE6F4"
|
|
10
|
+
OPT_FILL_COLOR = "#000000"
|
|
11
|
+
OPT_HOVER_COLOR = "#999999"
|
|
12
|
+
|
|
13
|
+
BORDER_W = 12
|
|
14
|
+
OPT_FONT_SIZE = 24
|
|
15
|
+
OPT_PAD_X = 80
|
|
16
|
+
OPT_PAD_Y = 4
|
|
17
|
+
OPT_GAP = 40
|
|
18
|
+
UNIFIED_MIN_W = 600
|
|
19
|
+
MSG_FONT_SIZE = 20
|
|
20
|
+
MSG_PAD_Y = 16
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _hex_to_rgb(hex_color):
|
|
24
|
+
h = hex_color.lstrip('#')
|
|
25
|
+
return tuple(int(h[i:i + 2], 16) for i in (0, 2, 4))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class _Panel:
|
|
29
|
+
|
|
30
|
+
def __init__(self, master, text, index, pw, on_select, tooltip=False, pinned=True):
|
|
31
|
+
self.index = index
|
|
32
|
+
self.text = text
|
|
33
|
+
self._on_select = on_select
|
|
34
|
+
self._tooltip = tooltip
|
|
35
|
+
|
|
36
|
+
self.win = tk.Toplevel(master)
|
|
37
|
+
self.win.overrideredirect(True)
|
|
38
|
+
self.win.attributes('-topmost', pinned)
|
|
39
|
+
|
|
40
|
+
f_opt = tkfont.Font(family="Microsoft YaHei", size=OPT_FONT_SIZE, weight="normal")
|
|
41
|
+
th = f_opt.metrics('linespace')
|
|
42
|
+
self._font = ("Microsoft YaHei", OPT_FONT_SIZE, "normal")
|
|
43
|
+
|
|
44
|
+
self.pw = int(pw)
|
|
45
|
+
self.ph = int(th + OPT_PAD_Y * 2 + BORDER_W * 2)
|
|
46
|
+
|
|
47
|
+
self.win.geometry(f"{self.pw}x{self.ph}")
|
|
48
|
+
|
|
49
|
+
self.cv = tk.Canvas(self.win, width=self.pw, height=self.ph,
|
|
50
|
+
bg=BODY_COLOR, highlightthickness=0)
|
|
51
|
+
self.cv.pack()
|
|
52
|
+
|
|
53
|
+
self._draw_gradient_border()
|
|
54
|
+
self._draw_option(text)
|
|
55
|
+
|
|
56
|
+
self.cv.bind("<Enter>", lambda e: self._set_hover(True))
|
|
57
|
+
self.cv.bind("<Leave>", lambda e: self._set_hover(False))
|
|
58
|
+
self.cv.bind("<Button-1>", lambda e: self._on_select(self.index))
|
|
59
|
+
|
|
60
|
+
if self._tooltip:
|
|
61
|
+
self._add_tooltip()
|
|
62
|
+
|
|
63
|
+
def _add_tooltip(self):
|
|
64
|
+
tip = [None]
|
|
65
|
+
|
|
66
|
+
def show(event):
|
|
67
|
+
if tip[0]:
|
|
68
|
+
return
|
|
69
|
+
tw = tk.Toplevel(self.win)
|
|
70
|
+
tw.overrideredirect(True)
|
|
71
|
+
tw.attributes('-topmost', True)
|
|
72
|
+
label = tk.Label(tw, text=self.text, bg=BODY_COLOR, fg='#000000',
|
|
73
|
+
font=("Microsoft YaHei", 12),
|
|
74
|
+
relief='solid', bd=1, padx=6, pady=2)
|
|
75
|
+
label.pack()
|
|
76
|
+
x = event.x_root + 15
|
|
77
|
+
y = event.y_root + 15
|
|
78
|
+
tw.geometry(f"+{x}+{y}")
|
|
79
|
+
tip[0] = tw
|
|
80
|
+
|
|
81
|
+
def hide(event):
|
|
82
|
+
if tip[0]:
|
|
83
|
+
tip[0].destroy()
|
|
84
|
+
tip[0] = None
|
|
85
|
+
|
|
86
|
+
self.cv.bind("<Enter>", show, add='+')
|
|
87
|
+
self.cv.bind("<Leave>", hide, add='+')
|
|
88
|
+
|
|
89
|
+
def _draw_gradient_border(self):
|
|
90
|
+
br, bg, bb = _hex_to_rgb(BORDER_COLOR)
|
|
91
|
+
er, eg, eb = _hex_to_rgb(BODY_COLOR)
|
|
92
|
+
bw = BORDER_W
|
|
93
|
+
for i in range(bw):
|
|
94
|
+
t = (i / max(bw - 1, 1)) ** 3
|
|
95
|
+
r = int(br + (er - br) * t)
|
|
96
|
+
g = int(bg + (eg - bg) * t)
|
|
97
|
+
b = int(bb + (eb - bb) * t)
|
|
98
|
+
color = f'#{r:02x}{g:02x}{b:02x}'
|
|
99
|
+
self.cv.create_rectangle(i, i, self.pw - i, self.ph - i,
|
|
100
|
+
outline=color, width=1)
|
|
101
|
+
|
|
102
|
+
def _draw_option(self, text):
|
|
103
|
+
cx = self.pw // 2
|
|
104
|
+
cy = self.ph // 2
|
|
105
|
+
self.cv.create_text(cx, cy, text=text, font=self._font,
|
|
106
|
+
fill=OPT_FILL_COLOR, anchor="center",
|
|
107
|
+
tags=("opt", "opt_fill"))
|
|
108
|
+
|
|
109
|
+
def _set_hover(self, hover):
|
|
110
|
+
items = self.cv.find_withtag("opt_fill")
|
|
111
|
+
color = OPT_HOVER_COLOR if hover else OPT_FILL_COLOR
|
|
112
|
+
for item in items:
|
|
113
|
+
self.cv.itemconfig(item, fill=color)
|
|
114
|
+
|
|
115
|
+
def set_position(self, x, y):
|
|
116
|
+
self.win.geometry(f"+{x}+{y}")
|
|
117
|
+
|
|
118
|
+
def destroy(self):
|
|
119
|
+
try:
|
|
120
|
+
self.win.destroy()
|
|
121
|
+
except tk.TclError:
|
|
122
|
+
pass
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class _ChoiceManager:
|
|
126
|
+
|
|
127
|
+
def __init__(self, msg, choices, title, tooltip=False, force=None, pinned=True):
|
|
128
|
+
self.result = None
|
|
129
|
+
self._tooltip = tooltip
|
|
130
|
+
self._force = force
|
|
131
|
+
self._pinned = pinned
|
|
132
|
+
self.root = _get_root()
|
|
133
|
+
|
|
134
|
+
f_opt = tkfont.Font(family="Microsoft YaHei", size=OPT_FONT_SIZE, weight="normal")
|
|
135
|
+
opt_widths = [f_opt.measure(c) for c in choices]
|
|
136
|
+
max_opt_w = max(opt_widths) if opt_widths else 0
|
|
137
|
+
unified_w = max(int(max_opt_w + OPT_PAD_X * 2 + BORDER_W * 2), UNIFIED_MIN_W)
|
|
138
|
+
screen_w = self.root.winfo_screenwidth()
|
|
139
|
+
unified_w = min(unified_w, screen_w - BORDER_W * 2)
|
|
140
|
+
self._unified_w = unified_w
|
|
141
|
+
|
|
142
|
+
self._panels = []
|
|
143
|
+
for i, choice in enumerate(choices):
|
|
144
|
+
panel = _Panel(self.root, choice, i, unified_w, self._on_select, self._tooltip, pinned=pinned)
|
|
145
|
+
self._panels.append(panel)
|
|
146
|
+
|
|
147
|
+
if msg.strip():
|
|
148
|
+
self._create_msg_label(msg)
|
|
149
|
+
|
|
150
|
+
self._layout(msg)
|
|
151
|
+
|
|
152
|
+
if force is not None and 0 <= force < len(choices):
|
|
153
|
+
self._force_index = force
|
|
154
|
+
p = self._panels[force]
|
|
155
|
+
cx = p.win.winfo_x() + p.pw // 2
|
|
156
|
+
cy = p.win.winfo_y() + p.ph // 2
|
|
157
|
+
self.root.after(50, lambda: p.win.event_generate(
|
|
158
|
+
'<Motion>', warp=True, x=p.pw // 2, y=p.ph // 2))
|
|
159
|
+
|
|
160
|
+
def _on_select(self, index):
|
|
161
|
+
self.result = index
|
|
162
|
+
for p in self._panels:
|
|
163
|
+
p.destroy()
|
|
164
|
+
if hasattr(self, '_msg_win'):
|
|
165
|
+
try:
|
|
166
|
+
self._msg_win.destroy()
|
|
167
|
+
except tk.TclError:
|
|
168
|
+
pass
|
|
169
|
+
try:
|
|
170
|
+
_get_root().quit()
|
|
171
|
+
except tk.TclError:
|
|
172
|
+
pass
|
|
173
|
+
|
|
174
|
+
def _create_msg_label(self, msg):
|
|
175
|
+
f = tkfont.Font(family="Microsoft YaHei", size=MSG_FONT_SIZE, weight="normal")
|
|
176
|
+
max_lbl_w = max(self._unified_w - 40, 200)
|
|
177
|
+
|
|
178
|
+
raw_lines = msg.split('\n')
|
|
179
|
+
wrapped_lines = []
|
|
180
|
+
for line in raw_lines:
|
|
181
|
+
if f.measure(line) <= max_lbl_w:
|
|
182
|
+
wrapped_lines.append(line)
|
|
183
|
+
else:
|
|
184
|
+
current = ""
|
|
185
|
+
for ch in line:
|
|
186
|
+
test = current + ch
|
|
187
|
+
if f.measure(test) <= max_lbl_w:
|
|
188
|
+
current = test
|
|
189
|
+
else:
|
|
190
|
+
if current:
|
|
191
|
+
wrapped_lines.append(current)
|
|
192
|
+
current = ch
|
|
193
|
+
if current:
|
|
194
|
+
wrapped_lines.append(current)
|
|
195
|
+
|
|
196
|
+
line_h = f.metrics('linespace')
|
|
197
|
+
total_h = line_h * len(wrapped_lines) + MSG_PAD_Y * 2
|
|
198
|
+
text_w = max(f.measure(line) for line in wrapped_lines)
|
|
199
|
+
|
|
200
|
+
lbl = tk.Toplevel(self.root)
|
|
201
|
+
lbl.overrideredirect(True)
|
|
202
|
+
lbl.attributes('-topmost', self._pinned)
|
|
203
|
+
self._msg_win = lbl
|
|
204
|
+
self._msg_w = max(int(text_w + 40), self._unified_w)
|
|
205
|
+
self._msg_h = int(total_h)
|
|
206
|
+
lbl.geometry(f"{self._msg_w}x{self._msg_h}")
|
|
207
|
+
|
|
208
|
+
cv = tk.Canvas(lbl, width=self._msg_w, height=self._msg_h,
|
|
209
|
+
bg=BODY_COLOR, highlightthickness=0)
|
|
210
|
+
cv.pack()
|
|
211
|
+
cv.create_rectangle(0, 0, self._msg_w, self._msg_h,
|
|
212
|
+
outline=BORDER_COLOR, width=4)
|
|
213
|
+
for j, line in enumerate(wrapped_lines):
|
|
214
|
+
y = MSG_PAD_Y + line_h // 2 + j * line_h
|
|
215
|
+
cv.create_text(self._msg_w // 2, y, text=line,
|
|
216
|
+
font=("Microsoft YaHei", MSG_FONT_SIZE, "normal"),
|
|
217
|
+
fill="#000000", anchor="center")
|
|
218
|
+
|
|
219
|
+
def _layout(self, msg):
|
|
220
|
+
sw = self.root.winfo_screenwidth()
|
|
221
|
+
sh = self.root.winfo_screenheight()
|
|
222
|
+
|
|
223
|
+
if not self._panels:
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
total_h = sum(p.ph for p in self._panels) + OPT_GAP * (len(self._panels) - 1)
|
|
227
|
+
if hasattr(self, '_msg_win'):
|
|
228
|
+
total_h += self._msg_h + OPT_GAP
|
|
229
|
+
|
|
230
|
+
start_y = (sh - total_h) // 2
|
|
231
|
+
|
|
232
|
+
if hasattr(self, '_msg_win'):
|
|
233
|
+
msg_x = (sw - self._msg_w) // 2
|
|
234
|
+
self._msg_win.geometry(f"+{msg_x}+{start_y}")
|
|
235
|
+
start_y += self._msg_h + OPT_GAP
|
|
236
|
+
|
|
237
|
+
for panel in self._panels:
|
|
238
|
+
px = (sw - panel.pw) // 2
|
|
239
|
+
panel.set_position(px, start_y)
|
|
240
|
+
start_y += panel.ph + OPT_GAP
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def choicebox(msg: str = "", choices: Optional[List[str]] = None, title: str = "", tooltip: bool = False, force: Optional[int] = None, pinned: bool = True) -> Optional[str]:
|
|
244
|
+
"""DDLC-style multi-choice dialog. Each option is a floating window. Returns the selected text, or None if cancelled.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
msg: prompt text displayed above the options. No label shown if empty.
|
|
248
|
+
choices: list of option strings to display.
|
|
249
|
+
title: window title (unused in borderless mode).
|
|
250
|
+
tooltip: show a floating tooltip when hovering over an option.
|
|
251
|
+
force: pre-select an option by index (0-based). The mouse warps to its center.
|
|
252
|
+
pinned: keep the windows always on top of other windows.
|
|
253
|
+
|
|
254
|
+
Usage:
|
|
255
|
+
import dokibox
|
|
256
|
+
text = dokibox.choicebox("Choose a character", ["Sayori", "Yuri", "Natsuki"], force=1)
|
|
257
|
+
"""
|
|
258
|
+
from dokibox.dialogbox import _destroy_box
|
|
259
|
+
_destroy_box()
|
|
260
|
+
if not choices:
|
|
261
|
+
return None
|
|
262
|
+
mgr = _ChoiceManager(msg, choices, title, tooltip, force, pinned=pinned)
|
|
263
|
+
_get_root().mainloop()
|
|
264
|
+
return choices[mgr.result] if mgr.result is not None else None
|