mcpower-proxy 0.0.58__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.
- main.py +112 -0
- mcpower_proxy-0.0.58.dist-info/METADATA +250 -0
- mcpower_proxy-0.0.58.dist-info/RECORD +43 -0
- mcpower_proxy-0.0.58.dist-info/WHEEL +5 -0
- mcpower_proxy-0.0.58.dist-info/entry_points.txt +2 -0
- mcpower_proxy-0.0.58.dist-info/licenses/LICENSE +201 -0
- mcpower_proxy-0.0.58.dist-info/top_level.txt +3 -0
- modules/__init__.py +1 -0
- modules/apis/__init__.py +1 -0
- modules/apis/security_policy.py +322 -0
- modules/logs/__init__.py +1 -0
- modules/logs/audit_trail.py +162 -0
- modules/logs/logger.py +128 -0
- modules/redaction/__init__.py +13 -0
- modules/redaction/constants.py +38 -0
- modules/redaction/gitleaks_rules.py +1268 -0
- modules/redaction/pii_rules.py +271 -0
- modules/redaction/redactor.py +599 -0
- modules/ui/__init__.py +1 -0
- modules/ui/classes.py +48 -0
- modules/ui/confirmation.py +200 -0
- modules/ui/simple_dialog.py +104 -0
- modules/ui/xdialog/__init__.py +249 -0
- modules/ui/xdialog/constants.py +13 -0
- modules/ui/xdialog/mac_dialogs.py +190 -0
- modules/ui/xdialog/tk_dialogs.py +78 -0
- modules/ui/xdialog/windows_custom_dialog.py +426 -0
- modules/ui/xdialog/windows_dialogs.py +250 -0
- modules/ui/xdialog/windows_structs.py +183 -0
- modules/ui/xdialog/yad_dialogs.py +236 -0
- modules/ui/xdialog/zenity_dialogs.py +156 -0
- modules/utils/__init__.py +1 -0
- modules/utils/cli.py +46 -0
- modules/utils/config.py +193 -0
- modules/utils/copy.py +36 -0
- modules/utils/ids.py +160 -0
- modules/utils/json.py +120 -0
- modules/utils/mcp_configs.py +48 -0
- wrapper/__init__.py +1 -0
- wrapper/__version__.py +6 -0
- wrapper/middleware.py +750 -0
- wrapper/schema.py +227 -0
- wrapper/server.py +78 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
|
|
5
|
+
from .constants import *
|
|
6
|
+
|
|
7
|
+
def osascript(*code: str):
|
|
8
|
+
proc = subprocess.Popen(
|
|
9
|
+
["osascript", "-e", " ".join(code)],
|
|
10
|
+
stdout=subprocess.PIPE,
|
|
11
|
+
stderr=subprocess.PIPE,
|
|
12
|
+
shell=False
|
|
13
|
+
)
|
|
14
|
+
stdout, stderr = proc.communicate()
|
|
15
|
+
|
|
16
|
+
return (proc.returncode, stdout.decode('utf-8'), stderr.decode('utf-8'))
|
|
17
|
+
|
|
18
|
+
def quote(text: str):
|
|
19
|
+
return '"' + text.replace("\\", "\\\\").replace('"', '\\"') + '"'
|
|
20
|
+
|
|
21
|
+
def dialog(title, message, icon, buttons=["OK"], default_button=None):
|
|
22
|
+
script = [
|
|
23
|
+
'display dialog', quote(message),
|
|
24
|
+
'with icon', icon,
|
|
25
|
+
'buttons', "{" + ",".join(quote(btn) for btn in buttons) + "}",
|
|
26
|
+
]
|
|
27
|
+
if title: script.append('with title ' + quote(title))
|
|
28
|
+
|
|
29
|
+
# Set default button if specified
|
|
30
|
+
if default_button is not None and 0 <= default_button < len(buttons):
|
|
31
|
+
script.append('default button ' + quote(buttons[default_button]))
|
|
32
|
+
|
|
33
|
+
code, out, err = osascript(*script)
|
|
34
|
+
if code: return ''
|
|
35
|
+
else: return out[out.index(":")+1:].strip("\r\n")
|
|
36
|
+
|
|
37
|
+
def open_file(title, filetypes, multiple=False):
|
|
38
|
+
script = ['choose file']
|
|
39
|
+
if title: script.append('with prompt ' + quote(title))
|
|
40
|
+
if filetypes:
|
|
41
|
+
oftype = []
|
|
42
|
+
for _, exts in filetypes:
|
|
43
|
+
for ext in exts.split():
|
|
44
|
+
if ext == "*": break
|
|
45
|
+
if ext[:2] == "*.": oftype.append(quote(ext[2:]))
|
|
46
|
+
else:
|
|
47
|
+
if oftype: script.append("of type {" + ",".join(oftype) + "}")
|
|
48
|
+
|
|
49
|
+
if multiple:
|
|
50
|
+
script.append("multiple selections allowed true")
|
|
51
|
+
code, out, err = osascript(f'set ps to ({" ".join(script)})\rrepeat with p in ps\r log (POSIX path of p)\rend repeat')
|
|
52
|
+
if code: return []
|
|
53
|
+
|
|
54
|
+
return err.strip("\r\n").splitlines()
|
|
55
|
+
else:
|
|
56
|
+
code, out, err = osascript(f'POSIX path of ({" ".join(script)})')
|
|
57
|
+
if code: return ''
|
|
58
|
+
|
|
59
|
+
return out.strip("\r\n")
|
|
60
|
+
|
|
61
|
+
def save_file(title, filetypes):
|
|
62
|
+
script = ['choose file name']
|
|
63
|
+
if title: script.append('with prompt ' + quote(title))
|
|
64
|
+
if filetypes:
|
|
65
|
+
for filetype, exts in filetypes:
|
|
66
|
+
for ext in exts.split():
|
|
67
|
+
if ext == "*": continue
|
|
68
|
+
if ext[:2] == "*.":
|
|
69
|
+
script.append(f'default name "{filetype}.{ext[2:]}"')
|
|
70
|
+
break
|
|
71
|
+
|
|
72
|
+
code, out, err = osascript(f'POSIX path of ({" ".join(script)})')
|
|
73
|
+
if code: return ''
|
|
74
|
+
|
|
75
|
+
return out.strip("\r\n")
|
|
76
|
+
|
|
77
|
+
def directory(title):
|
|
78
|
+
script = ['choose folder']
|
|
79
|
+
if title: script.append('with prompt ' + quote(title))
|
|
80
|
+
|
|
81
|
+
code, out, err = osascript(f'POSIX path of ({" ".join(script)})')
|
|
82
|
+
if code: return ''
|
|
83
|
+
|
|
84
|
+
return out.strip("\r\n")
|
|
85
|
+
|
|
86
|
+
def info(title, message):
|
|
87
|
+
dialog(title, message, "note")
|
|
88
|
+
|
|
89
|
+
def warning(title, message):
|
|
90
|
+
dialog(title, message, "caution")
|
|
91
|
+
|
|
92
|
+
def error(title, message):
|
|
93
|
+
dialog(title, message, "stop")
|
|
94
|
+
|
|
95
|
+
def yesno(title, message):
|
|
96
|
+
out = dialog(title, message, "caution", ["No", "Yes"])
|
|
97
|
+
if not out or out == "No": return NO
|
|
98
|
+
elif out == "Yes": return YES
|
|
99
|
+
|
|
100
|
+
def yesno_always(title, message, yes_always=False, no_always=False):
|
|
101
|
+
"""
|
|
102
|
+
Enhanced yes/no dialog with optional always buttons
|
|
103
|
+
Button order: No, No Always (if enabled), Yes, Yes Always (if enabled)
|
|
104
|
+
"""
|
|
105
|
+
buttons = []
|
|
106
|
+
|
|
107
|
+
# Build button list in order: No, No Always, Yes, Yes Always
|
|
108
|
+
buttons.append("No")
|
|
109
|
+
if no_always:
|
|
110
|
+
buttons.append("No (Always)")
|
|
111
|
+
buttons.append("Yes")
|
|
112
|
+
if yes_always:
|
|
113
|
+
buttons.append("Yes (Always)")
|
|
114
|
+
|
|
115
|
+
out = dialog(title, message, "caution", buttons)
|
|
116
|
+
|
|
117
|
+
if not out or out == "No":
|
|
118
|
+
return NO
|
|
119
|
+
elif out == "No (Always)":
|
|
120
|
+
return NO_ALWAYS
|
|
121
|
+
elif out == "Yes":
|
|
122
|
+
return YES
|
|
123
|
+
elif out == "Yes (Always)":
|
|
124
|
+
return YES_ALWAYS
|
|
125
|
+
else:
|
|
126
|
+
return NO # Fallback
|
|
127
|
+
|
|
128
|
+
def yesnocancel(title, message):
|
|
129
|
+
out = dialog(title, message, "note", ["Cancel", "No", "Yes"])
|
|
130
|
+
if not out or out == "Cancel": return CANCEL
|
|
131
|
+
elif out == "No": return NO
|
|
132
|
+
elif out == "Yes": return YES
|
|
133
|
+
|
|
134
|
+
def retrycancel(title, message):
|
|
135
|
+
out = dialog(title, message, "note", ["Cancel", "Retry"])
|
|
136
|
+
if not out or out == "Cancel": return CANCEL
|
|
137
|
+
elif out == "Retry": return RETRY
|
|
138
|
+
|
|
139
|
+
def okcancel(title, message):
|
|
140
|
+
out = dialog(title, message, "note", ["Cancel", "OK"])
|
|
141
|
+
if not out or out == "Cancel": return CANCEL
|
|
142
|
+
elif out == "OK": return OK
|
|
143
|
+
|
|
144
|
+
def generic_dialog(title, message, buttons, default_button, icon):
|
|
145
|
+
"""
|
|
146
|
+
Generic dialog with custom buttons and icon
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
title (str): Dialog title
|
|
150
|
+
message (str): Dialog message
|
|
151
|
+
buttons (list): List of button text strings
|
|
152
|
+
default_button (int): Index of default button (0-based)
|
|
153
|
+
icon (str): Icon type (ICON_QUESTION, ICON_WARNING, ICON_ERROR, ICON_INFO)
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
int: Index of clicked button (0-based)
|
|
157
|
+
"""
|
|
158
|
+
from .constants import ICON_QUESTION, ICON_WARNING, ICON_ERROR, ICON_INFO
|
|
159
|
+
|
|
160
|
+
# Map icon constants to AppleScript icons
|
|
161
|
+
icon_map = {
|
|
162
|
+
ICON_QUESTION: "caution",
|
|
163
|
+
ICON_WARNING: "caution",
|
|
164
|
+
ICON_ERROR: "stop",
|
|
165
|
+
ICON_INFO: "note"
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if icon not in icon_map:
|
|
169
|
+
raise ValueError(f"Unsupported icon: {icon}")
|
|
170
|
+
|
|
171
|
+
if not isinstance(buttons, list) or len(buttons) == 0:
|
|
172
|
+
raise ValueError("buttons must be a non-empty list")
|
|
173
|
+
|
|
174
|
+
if not isinstance(default_button, int) or default_button < 0 or default_button >= len(buttons):
|
|
175
|
+
raise ValueError(f"default_button must be a valid index (0-{len(buttons)-1})")
|
|
176
|
+
|
|
177
|
+
# Call AppleScript dialog with default button
|
|
178
|
+
out = dialog(title, message, icon_map[icon], buttons, default_button)
|
|
179
|
+
|
|
180
|
+
# Map response back to button index
|
|
181
|
+
if not out:
|
|
182
|
+
# Dialog was dismissed/cancelled - return default button index
|
|
183
|
+
return default_button
|
|
184
|
+
|
|
185
|
+
# Find the button that was clicked
|
|
186
|
+
try:
|
|
187
|
+
return buttons.index(out)
|
|
188
|
+
except ValueError:
|
|
189
|
+
# This should not happen unless AppleScript returns unexpected text
|
|
190
|
+
raise RuntimeError(f"Unexpected dialog result: {out}, expected one of {buttons}")
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from .constants import *
|
|
2
|
+
|
|
3
|
+
from tkinter import messagebox
|
|
4
|
+
from tkinter import filedialog
|
|
5
|
+
|
|
6
|
+
def open_file(title, filetypes, multiple=False):
|
|
7
|
+
if multiple:
|
|
8
|
+
return filedialog.askopenfilenames(title=title, filetypes=filetypes)
|
|
9
|
+
else:
|
|
10
|
+
return filedialog.askopenfilename(title=title) or ''
|
|
11
|
+
|
|
12
|
+
def save_file(title, filetypes):
|
|
13
|
+
return filedialog.asksaveasfilename(title=title, filetypes=filetypes) or ''
|
|
14
|
+
|
|
15
|
+
def directory(title):
|
|
16
|
+
return filedialog.askdirectory(mustexist=True) or ''
|
|
17
|
+
|
|
18
|
+
info = messagebox.showinfo
|
|
19
|
+
warning = messagebox.showwarning
|
|
20
|
+
error = messagebox.showerror
|
|
21
|
+
|
|
22
|
+
def yesno(title, message):
|
|
23
|
+
if messagebox.askyesno(title, message):
|
|
24
|
+
return YES
|
|
25
|
+
else:
|
|
26
|
+
return NO
|
|
27
|
+
|
|
28
|
+
def yesno_always(title, message, yes_always=False, no_always=False):
|
|
29
|
+
"""
|
|
30
|
+
Enhanced yes/no dialog with optional always buttons
|
|
31
|
+
Button order: No, No Always (if enabled), Yes, Yes Always (if enabled)
|
|
32
|
+
|
|
33
|
+
Note: Tkinter messagebox doesn't support custom buttons, so we fall back to yesno
|
|
34
|
+
for now. Full implementation would require custom Tkinter dialog creation.
|
|
35
|
+
"""
|
|
36
|
+
# TODO: Implement custom Tkinter dialog with proper button layout
|
|
37
|
+
# For now, fall back to standard yesno dialog
|
|
38
|
+
return yesno(title, message)
|
|
39
|
+
|
|
40
|
+
def yesnocancel(title, message):
|
|
41
|
+
r = messagebox.askyesnocancel(title, message)
|
|
42
|
+
if r is None:
|
|
43
|
+
return CANCEL
|
|
44
|
+
elif r:
|
|
45
|
+
return YES
|
|
46
|
+
else:
|
|
47
|
+
return NO
|
|
48
|
+
|
|
49
|
+
def retrycancel(title, message):
|
|
50
|
+
if messagebox.askretrycancel(title, message):
|
|
51
|
+
return RETRY
|
|
52
|
+
else:
|
|
53
|
+
return CANCEL
|
|
54
|
+
|
|
55
|
+
def okcancel(title, message):
|
|
56
|
+
if messagebox.askokcancel(title, message):
|
|
57
|
+
return OK
|
|
58
|
+
else:
|
|
59
|
+
return CANCEL
|
|
60
|
+
|
|
61
|
+
def generic_dialog(title, message, buttons, default_button, icon):
|
|
62
|
+
"""
|
|
63
|
+
Generic dialog with custom buttons and icon
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
title (str): Dialog title
|
|
67
|
+
message (str): Dialog message
|
|
68
|
+
buttons (list): List of button text strings
|
|
69
|
+
default_button (int): Index of default button (0-based)
|
|
70
|
+
icon (str): Icon type (ICON_QUESTION, ICON_WARNING, ICON_ERROR, ICON_INFO)
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
int: Index of clicked button (0-based)
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
NotImplementedError: Tkinter messagebox doesn't support custom buttons
|
|
77
|
+
"""
|
|
78
|
+
raise NotImplementedError("generic_dialog is not supported on Tkinter - custom buttons require custom dialog implementation")
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Windows Custom Dialog Module for MCPower
|
|
3
|
+
A module to display Windows dialogs with 4 custom buttons:
|
|
4
|
+
No, No (Always), Yes, Yes (Always)
|
|
5
|
+
|
|
6
|
+
This module is specifically designed for MCPower's
|
|
7
|
+
user confirmation dialogs on Windows platforms.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import ctypes
|
|
11
|
+
from ctypes import wintypes
|
|
12
|
+
import sys
|
|
13
|
+
from .constants import NO, NO_ALWAYS, YES, YES_ALWAYS
|
|
14
|
+
|
|
15
|
+
# Custom button IDs (internal use)
|
|
16
|
+
_BUTTON_NO = 100
|
|
17
|
+
_BUTTON_NO_ALWAYS = 101
|
|
18
|
+
_BUTTON_YES = 102
|
|
19
|
+
_BUTTON_YES_ALWAYS = 103
|
|
20
|
+
|
|
21
|
+
# Global result storage (used internally)
|
|
22
|
+
_dialog_result = None
|
|
23
|
+
|
|
24
|
+
# Define WNDCLASSW structure
|
|
25
|
+
class _WNDCLASSW(ctypes.Structure):
|
|
26
|
+
"""Windows WNDCLASSW structure for window class registration"""
|
|
27
|
+
_fields_ = [
|
|
28
|
+
("style", ctypes.c_uint),
|
|
29
|
+
("lpfnWndProc", ctypes.c_void_p),
|
|
30
|
+
("cbClsExtra", ctypes.c_int),
|
|
31
|
+
("cbWndExtra", ctypes.c_int),
|
|
32
|
+
("hInstance", wintypes.HANDLE),
|
|
33
|
+
("hIcon", wintypes.HANDLE),
|
|
34
|
+
("hCursor", wintypes.HANDLE),
|
|
35
|
+
("hbrBackground", wintypes.HANDLE),
|
|
36
|
+
("lpszMenuName", ctypes.c_wchar_p),
|
|
37
|
+
("lpszClassName", ctypes.c_wchar_p)
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
def _create_dialog_window(title, main_text, buttons_config, default_button_index=0):
|
|
41
|
+
"""
|
|
42
|
+
Internal function to create the Windows dialog with native styling
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
title (str): Window title
|
|
46
|
+
main_text (str): Main message text
|
|
47
|
+
buttons_config (list): List of (text, button_id) tuples for buttons to show
|
|
48
|
+
default_button_index (int): Index of default button (0-based)
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
int: Button result mapped to xdialog constants
|
|
52
|
+
"""
|
|
53
|
+
global _dialog_result
|
|
54
|
+
_dialog_result = None
|
|
55
|
+
|
|
56
|
+
user32 = ctypes.windll.user32
|
|
57
|
+
kernel32 = ctypes.windll.kernel32
|
|
58
|
+
gdi32 = ctypes.windll.gdi32
|
|
59
|
+
|
|
60
|
+
# Set proper argument types and return types for Windows API functions
|
|
61
|
+
user32.DefWindowProcW.argtypes = [wintypes.HWND, ctypes.c_uint, wintypes.WPARAM, wintypes.LPARAM]
|
|
62
|
+
user32.DefWindowProcW.restype = wintypes.LPARAM
|
|
63
|
+
|
|
64
|
+
user32.CreateWindowExW.argtypes = [wintypes.DWORD, ctypes.c_wchar_p, ctypes.c_wchar_p, wintypes.DWORD,
|
|
65
|
+
ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_int,
|
|
66
|
+
wintypes.HWND, wintypes.HMENU, wintypes.HINSTANCE, wintypes.LPVOID]
|
|
67
|
+
user32.CreateWindowExW.restype = wintypes.HWND
|
|
68
|
+
|
|
69
|
+
user32.RegisterClassW.argtypes = [ctypes.POINTER(_WNDCLASSW)]
|
|
70
|
+
user32.RegisterClassW.restype = wintypes.ATOM
|
|
71
|
+
|
|
72
|
+
user32.GetMessageW.argtypes = [ctypes.POINTER(wintypes.MSG), wintypes.HWND, ctypes.c_uint, ctypes.c_uint]
|
|
73
|
+
user32.GetMessageW.restype = ctypes.c_int
|
|
74
|
+
|
|
75
|
+
kernel32.GetModuleHandleW.argtypes = [ctypes.c_wchar_p]
|
|
76
|
+
kernel32.GetModuleHandleW.restype = wintypes.HMODULE
|
|
77
|
+
|
|
78
|
+
# Get system fonts for native appearance
|
|
79
|
+
def get_message_font():
|
|
80
|
+
"""Get the system font used for dialogs/message boxes"""
|
|
81
|
+
try:
|
|
82
|
+
# Get the font used by the system for message boxes
|
|
83
|
+
ncm_size = 504 if sys.getwindowsversion().major >= 6 else 440
|
|
84
|
+
ncm = ctypes.create_string_buffer(ncm_size)
|
|
85
|
+
ctypes.windll.user32.SystemParametersInfoW(0x0029, ncm_size, ncm, 0) # SPI_GETNONCLIENTMETRICS
|
|
86
|
+
# Extract font info from NONCLIENTMETRICS
|
|
87
|
+
return gdi32.CreateFontW(
|
|
88
|
+
-14, # Height (negative for character height) - increased from -11
|
|
89
|
+
0, # Width
|
|
90
|
+
0, # Escapement
|
|
91
|
+
0, # Orientation
|
|
92
|
+
400, # Weight (FW_NORMAL)
|
|
93
|
+
0, # Italic
|
|
94
|
+
0, # Underline
|
|
95
|
+
0, # StrikeOut
|
|
96
|
+
1, # CharSet (DEFAULT_CHARSET)
|
|
97
|
+
0, # OutPrecision
|
|
98
|
+
0, # ClipPrecision
|
|
99
|
+
5, # Quality (CLEARTYPE_QUALITY)
|
|
100
|
+
0, # PitchAndFamily
|
|
101
|
+
"Segoe UI" # Face name
|
|
102
|
+
)
|
|
103
|
+
except:
|
|
104
|
+
# Fallback to default GUI font
|
|
105
|
+
return gdi32.GetStockObject(17) # DEFAULT_GUI_FONT
|
|
106
|
+
|
|
107
|
+
system_font = get_message_font()
|
|
108
|
+
|
|
109
|
+
# Window procedure
|
|
110
|
+
def WndProc(hwnd, msg, wparam, lparam):
|
|
111
|
+
global _dialog_result
|
|
112
|
+
|
|
113
|
+
WM_COMMAND = 0x0111
|
|
114
|
+
WM_CLOSE = 0x0010
|
|
115
|
+
WM_DESTROY = 0x0002
|
|
116
|
+
|
|
117
|
+
if msg == WM_COMMAND:
|
|
118
|
+
button_id = wparam & 0xFFFF
|
|
119
|
+
if button_id in [_BUTTON_NO, _BUTTON_NO_ALWAYS, _BUTTON_YES, _BUTTON_YES_ALWAYS]:
|
|
120
|
+
_dialog_result = button_id
|
|
121
|
+
user32.DestroyWindow(hwnd)
|
|
122
|
+
return 0
|
|
123
|
+
|
|
124
|
+
elif msg == WM_CLOSE:
|
|
125
|
+
_dialog_result = _BUTTON_NO # Default to No on close
|
|
126
|
+
user32.DestroyWindow(hwnd)
|
|
127
|
+
return 0
|
|
128
|
+
|
|
129
|
+
elif msg == WM_DESTROY:
|
|
130
|
+
user32.PostQuitMessage(0)
|
|
131
|
+
return 0
|
|
132
|
+
|
|
133
|
+
# Let Windows handle the default background and colors
|
|
134
|
+
|
|
135
|
+
return user32.DefWindowProcW(hwnd, msg, wparam, lparam)
|
|
136
|
+
|
|
137
|
+
# Window procedure callback
|
|
138
|
+
WNDPROC = ctypes.WINFUNCTYPE(wintypes.LPARAM, wintypes.HWND, ctypes.c_uint, wintypes.WPARAM, wintypes.LPARAM)
|
|
139
|
+
wndproc_callback = WNDPROC(WndProc)
|
|
140
|
+
|
|
141
|
+
# Get instance handle
|
|
142
|
+
hInstance = kernel32.GetModuleHandleW(None)
|
|
143
|
+
className = "MCPSecurityDialog"
|
|
144
|
+
|
|
145
|
+
# Register class with proper styling
|
|
146
|
+
wc = _WNDCLASSW()
|
|
147
|
+
wc.style = 0x0008 | 0x0020 # CS_DBLCLKS | CS_CLASSDC
|
|
148
|
+
wc.lpfnWndProc = ctypes.cast(wndproc_callback, ctypes.c_void_p)
|
|
149
|
+
wc.cbClsExtra = 0
|
|
150
|
+
wc.cbWndExtra = 0
|
|
151
|
+
wc.hInstance = hInstance
|
|
152
|
+
wc.hIcon = user32.LoadIconW(None, 32514) # IDI_QUESTION
|
|
153
|
+
wc.hCursor = user32.LoadCursorW(None, 32512) # IDC_ARROW
|
|
154
|
+
wc.hbrBackground = user32.GetSysColorBrush(15) # COLOR_3DFACE
|
|
155
|
+
wc.lpszMenuName = None
|
|
156
|
+
wc.lpszClassName = className
|
|
157
|
+
|
|
158
|
+
atom = user32.RegisterClassW(ctypes.byref(wc))
|
|
159
|
+
if not atom:
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
# Calculate proper dialog dimensions with padding
|
|
163
|
+
padding = 25
|
|
164
|
+
text_padding = 20
|
|
165
|
+
button_padding = 20
|
|
166
|
+
|
|
167
|
+
# Estimate text dimensions (more generous calculation)
|
|
168
|
+
text_lines = main_text.count('\n') + 1
|
|
169
|
+
# Use a more generous calculation for text height
|
|
170
|
+
char_height = 18 # Larger character height
|
|
171
|
+
line_height = 24 # More generous line spacing
|
|
172
|
+
text_height = max(text_lines * line_height + text_padding * 2, 120)
|
|
173
|
+
|
|
174
|
+
# Calculate button area
|
|
175
|
+
button_count = len(buttons_config)
|
|
176
|
+
btn_width = 100 # Wider buttons to fit text like "Yes (Always)"
|
|
177
|
+
btn_height = 26 # Slightly taller buttons
|
|
178
|
+
btn_spacing = 8 # More space between buttons
|
|
179
|
+
|
|
180
|
+
total_btn_width = button_count * btn_width + (button_count - 1) * btn_spacing
|
|
181
|
+
button_area_height = btn_height + button_padding * 2
|
|
182
|
+
|
|
183
|
+
# Calculate window dimensions
|
|
184
|
+
min_width = max(total_btn_width + padding * 2, 550) # Wider minimum dialog width
|
|
185
|
+
content_width = min_width - padding * 2
|
|
186
|
+
window_width = min_width
|
|
187
|
+
window_height = text_height + button_area_height + padding + 40 # Extra space for title bar and margins
|
|
188
|
+
|
|
189
|
+
# Calculate center position
|
|
190
|
+
screen_width = user32.GetSystemMetrics(0)
|
|
191
|
+
screen_height = user32.GetSystemMetrics(1)
|
|
192
|
+
x = (screen_width - window_width) // 2
|
|
193
|
+
y = (screen_height - window_height) // 2
|
|
194
|
+
|
|
195
|
+
# Window styles for a proper dialog
|
|
196
|
+
WS_POPUP = 0x80000000
|
|
197
|
+
WS_VISIBLE = 0x10000000
|
|
198
|
+
WS_CAPTION = 0x00C00000
|
|
199
|
+
WS_SYSMENU = 0x00080000
|
|
200
|
+
WS_DLGFRAME = 0x00400000
|
|
201
|
+
WS_EX_DLGMODALFRAME = 0x00000001
|
|
202
|
+
WS_EX_TOPMOST = 0x00000008
|
|
203
|
+
|
|
204
|
+
# Create main window with dialog styling
|
|
205
|
+
hwnd = user32.CreateWindowExW(
|
|
206
|
+
WS_EX_DLGMODALFRAME | WS_EX_TOPMOST,
|
|
207
|
+
className,
|
|
208
|
+
title,
|
|
209
|
+
WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_DLGFRAME | WS_VISIBLE,
|
|
210
|
+
x, y, window_width, window_height,
|
|
211
|
+
None, # Parent window
|
|
212
|
+
None, # Menu
|
|
213
|
+
hInstance,
|
|
214
|
+
None # Additional data
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
if not hwnd:
|
|
218
|
+
try:
|
|
219
|
+
user32.UnregisterClassW(ctypes.c_wchar_p(className), wintypes.HINSTANCE(hInstance))
|
|
220
|
+
except:
|
|
221
|
+
pass
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
# Create controls with proper styling
|
|
225
|
+
WS_CHILD = 0x40000000
|
|
226
|
+
WS_VISIBLE = 0x10000000
|
|
227
|
+
WS_TABSTOP = 0x00010000
|
|
228
|
+
SS_CENTER = 0x00000001
|
|
229
|
+
SS_CENTERIMAGE = 0x00000200
|
|
230
|
+
BS_PUSHBUTTON = 0x00000000
|
|
231
|
+
BS_DEFPUSHBUTTON = 0x00000001
|
|
232
|
+
|
|
233
|
+
# Create static text with better positioning
|
|
234
|
+
text_x = padding
|
|
235
|
+
text_y = padding
|
|
236
|
+
text_w = content_width
|
|
237
|
+
text_h = text_height - padding
|
|
238
|
+
|
|
239
|
+
# Create static text with left alignment instead of center
|
|
240
|
+
SS_LEFT = 0x00000000 # Left-aligned text
|
|
241
|
+
text_hwnd = user32.CreateWindowExW(
|
|
242
|
+
0, "STATIC", main_text,
|
|
243
|
+
WS_VISIBLE | WS_CHILD | SS_LEFT,
|
|
244
|
+
text_x, text_y, text_w, text_h,
|
|
245
|
+
hwnd, None, hInstance, None
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
# Set font for text
|
|
249
|
+
if text_hwnd and system_font:
|
|
250
|
+
user32.SendMessageW(text_hwnd, 0x0030, system_font, 1) # WM_SETFONT
|
|
251
|
+
|
|
252
|
+
# Create buttons with proper spacing and styling
|
|
253
|
+
btn_y = text_height + button_padding + 10 # Extra margin from text
|
|
254
|
+
start_x = (window_width - total_btn_width) // 2
|
|
255
|
+
|
|
256
|
+
button_hwnds = []
|
|
257
|
+
for i, (btn_text, btn_id) in enumerate(buttons_config):
|
|
258
|
+
btn_x = start_x + i * (btn_width + btn_spacing)
|
|
259
|
+
|
|
260
|
+
# Use default button style for the specified default button
|
|
261
|
+
is_default = (i == default_button_index)
|
|
262
|
+
btn_style = BS_DEFPUSHBUTTON if is_default else BS_PUSHBUTTON
|
|
263
|
+
|
|
264
|
+
btn_hwnd = user32.CreateWindowExW(
|
|
265
|
+
0, "BUTTON", btn_text,
|
|
266
|
+
WS_VISIBLE | WS_CHILD | WS_TABSTOP | btn_style,
|
|
267
|
+
btn_x, btn_y, btn_width, btn_height,
|
|
268
|
+
hwnd, btn_id, hInstance, None
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
# Set font for button
|
|
272
|
+
if btn_hwnd and system_font:
|
|
273
|
+
user32.SendMessageW(btn_hwnd, 0x0030, system_font, 0) # WM_SETFONT
|
|
274
|
+
|
|
275
|
+
button_hwnds.append(btn_hwnd)
|
|
276
|
+
|
|
277
|
+
# Set focus to default button
|
|
278
|
+
if is_default and btn_hwnd:
|
|
279
|
+
user32.SetFocus(btn_hwnd)
|
|
280
|
+
|
|
281
|
+
# Make dialog modal and bring to front
|
|
282
|
+
user32.SetWindowPos(hwnd, -1, 0, 0, 0, 0, 0x0010 | 0x0002 | 0x0001) # HWND_TOPMOST | SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE
|
|
283
|
+
user32.SetForegroundWindow(hwnd)
|
|
284
|
+
user32.BringWindowToTop(hwnd)
|
|
285
|
+
|
|
286
|
+
# Enable the parent window to be disabled (modal behavior)
|
|
287
|
+
parent = user32.GetWindow(hwnd, 4) # GW_OWNER
|
|
288
|
+
if parent:
|
|
289
|
+
user32.EnableWindow(parent, False)
|
|
290
|
+
|
|
291
|
+
# Simple message loop
|
|
292
|
+
msg = wintypes.MSG()
|
|
293
|
+
while True:
|
|
294
|
+
bRet = user32.GetMessageW(ctypes.byref(msg), None, 0, 0)
|
|
295
|
+
if bRet == 0: # WM_QUIT
|
|
296
|
+
break
|
|
297
|
+
elif bRet == -1: # Error
|
|
298
|
+
break
|
|
299
|
+
else:
|
|
300
|
+
user32.TranslateMessage(ctypes.byref(msg))
|
|
301
|
+
user32.DispatchMessageW(ctypes.byref(msg))
|
|
302
|
+
|
|
303
|
+
# Re-enable parent window
|
|
304
|
+
if parent:
|
|
305
|
+
user32.EnableWindow(parent, True)
|
|
306
|
+
user32.SetForegroundWindow(parent)
|
|
307
|
+
|
|
308
|
+
# Cleanup
|
|
309
|
+
if system_font:
|
|
310
|
+
gdi32.DeleteObject(system_font)
|
|
311
|
+
try:
|
|
312
|
+
user32.UnregisterClassW(ctypes.c_wchar_p(className), wintypes.HINSTANCE(hInstance))
|
|
313
|
+
except:
|
|
314
|
+
pass # Ignore cleanup errors
|
|
315
|
+
|
|
316
|
+
return _dialog_result
|
|
317
|
+
|
|
318
|
+
def show_confirmation_dialog(title, message, yes_always=False, no_always=False):
|
|
319
|
+
"""
|
|
320
|
+
Show a Windows confirmation dialog with configurable buttons.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
title (str): Window title
|
|
324
|
+
message (str): Main message text
|
|
325
|
+
yes_always (bool): Whether to show "Yes (Always)" button
|
|
326
|
+
no_always (bool): Whether to show "No (Always)" button
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
int: One of the xdialog constants (NO, NO_ALWAYS, YES, YES_ALWAYS)
|
|
330
|
+
NO: If dialog was cancelled or closed
|
|
331
|
+
"""
|
|
332
|
+
# Check if we're on Windows
|
|
333
|
+
if sys.platform != "win32":
|
|
334
|
+
raise OSError("This module only works on Windows")
|
|
335
|
+
|
|
336
|
+
# Build button configuration based on parameters
|
|
337
|
+
# Order: No, No (Always), Yes, Yes (Always)
|
|
338
|
+
buttons_config = []
|
|
339
|
+
|
|
340
|
+
buttons_config.append(("No", _BUTTON_NO))
|
|
341
|
+
if no_always:
|
|
342
|
+
buttons_config.append(("No (Always)", _BUTTON_NO_ALWAYS))
|
|
343
|
+
buttons_config.append(("Yes", _BUTTON_YES))
|
|
344
|
+
if yes_always:
|
|
345
|
+
buttons_config.append(("Yes (Always)", _BUTTON_YES_ALWAYS))
|
|
346
|
+
|
|
347
|
+
# Try to create the dialog
|
|
348
|
+
try:
|
|
349
|
+
result = _create_dialog_window(title, message, buttons_config, 2) # Default to "Yes" button
|
|
350
|
+
|
|
351
|
+
# Map internal button IDs to xdialog constants
|
|
352
|
+
mapping = {
|
|
353
|
+
_BUTTON_NO: NO,
|
|
354
|
+
_BUTTON_NO_ALWAYS: NO_ALWAYS,
|
|
355
|
+
_BUTTON_YES: YES,
|
|
356
|
+
_BUTTON_YES_ALWAYS: YES_ALWAYS
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return mapping.get(result, NO) # Default to NO if unknown result
|
|
360
|
+
|
|
361
|
+
except Exception:
|
|
362
|
+
# Fallback to NO on any error
|
|
363
|
+
return NO
|
|
364
|
+
|
|
365
|
+
def is_available():
|
|
366
|
+
"""
|
|
367
|
+
Check if the custom dialog is available on this platform.
|
|
368
|
+
|
|
369
|
+
Returns:
|
|
370
|
+
bool: True if custom dialogs can be used
|
|
371
|
+
"""
|
|
372
|
+
return sys.platform == "win32"
|
|
373
|
+
|
|
374
|
+
def show_generic_dialog(title, message, buttons, default_button, icon):
|
|
375
|
+
"""
|
|
376
|
+
Show a Windows generic dialog with custom buttons and icon.
|
|
377
|
+
|
|
378
|
+
Args:
|
|
379
|
+
title (str): Window title
|
|
380
|
+
message (str): Main message text
|
|
381
|
+
buttons (list): List of button text strings
|
|
382
|
+
default_button (int): Index of default button (0-based)
|
|
383
|
+
icon (str): Icon type (ICON_QUESTION, ICON_WARNING, ICON_ERROR, ICON_INFO)
|
|
384
|
+
|
|
385
|
+
Returns:
|
|
386
|
+
int: Index of clicked button (0-based), or default_button if dismissed
|
|
387
|
+
"""
|
|
388
|
+
from .constants import ICON_QUESTION, ICON_WARNING, ICON_ERROR, ICON_INFO
|
|
389
|
+
|
|
390
|
+
# Check if we're on Windows
|
|
391
|
+
if sys.platform != "win32":
|
|
392
|
+
raise OSError("This module only works on Windows")
|
|
393
|
+
|
|
394
|
+
# Map icon constants (currently not used in UI, but validated)
|
|
395
|
+
icon_map = {
|
|
396
|
+
ICON_QUESTION: "question",
|
|
397
|
+
ICON_WARNING: "warning",
|
|
398
|
+
ICON_ERROR: "error",
|
|
399
|
+
ICON_INFO: "info"
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if icon not in icon_map:
|
|
403
|
+
raise ValueError(f"Unsupported icon: {icon}")
|
|
404
|
+
|
|
405
|
+
# Build button configuration with custom button IDs starting from 100
|
|
406
|
+
buttons_config = []
|
|
407
|
+
for i, button_text in enumerate(buttons):
|
|
408
|
+
buttons_config.append((button_text, 100 + i))
|
|
409
|
+
|
|
410
|
+
# Try to create the dialog
|
|
411
|
+
try:
|
|
412
|
+
result = _create_dialog_window(title, message, buttons_config, default_button)
|
|
413
|
+
|
|
414
|
+
# Map internal button IDs back to indices
|
|
415
|
+
if result is not None and result >= 100:
|
|
416
|
+
button_index = result - 100
|
|
417
|
+
if 0 <= button_index < len(buttons):
|
|
418
|
+
return button_index
|
|
419
|
+
|
|
420
|
+
# Dialog was dismissed or unexpected result - return default button
|
|
421
|
+
return default_button
|
|
422
|
+
|
|
423
|
+
except Exception:
|
|
424
|
+
# Return default button on any error
|
|
425
|
+
return default_button
|
|
426
|
+
|