robotframework-dialogsplus 0.1.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.
- DialogsPlus/__init__.py +5 -0
- DialogsPlus/dialogsplus.py +90 -0
- DialogsPlus/py.typed +0 -0
- DialogsPlus/utils/config.py +83 -0
- DialogsPlus/widgets/assets/robot.ico +0 -0
- DialogsPlus/widgets/base.py +169 -0
- DialogsPlus/widgets/styling.py +337 -0
- DialogsPlus/widgets/wrappers.py +118 -0
- robotframework_dialogsplus-0.1.0.dist-info/METADATA +37 -0
- robotframework_dialogsplus-0.1.0.dist-info/RECORD +11 -0
- robotframework_dialogsplus-0.1.0.dist-info/WHEEL +4 -0
DialogsPlus/__init__.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from robot.api.deco import keyword
|
|
2
|
+
import os
|
|
3
|
+
from DialogsPlus.utils.config import DialogConfig
|
|
4
|
+
from DialogsPlus.widgets.wrappers import ( GetValueFromUserDialog,
|
|
5
|
+
ExecuteManualStepDialog,
|
|
6
|
+
CountdownDialogRunner,
|
|
7
|
+
GetConfirmationFromUser,
|
|
8
|
+
MultiValueInput,
|
|
9
|
+
ChooseFromFileDialog,
|
|
10
|
+
ChooseFolderDialog,
|
|
11
|
+
ConfirmWithCheckbox,
|
|
12
|
+
SelectOptionsWithCheckboxes)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
ROBOT_LIBRARY_SCOPE = 'SUITE'
|
|
16
|
+
|
|
17
|
+
class DialogsPlus:
|
|
18
|
+
|
|
19
|
+
def __init__(self, config=None):
|
|
20
|
+
|
|
21
|
+
if config and os.path.exists(config):
|
|
22
|
+
self.config = DialogConfig.from_yaml(config)
|
|
23
|
+
else:
|
|
24
|
+
self.config = DialogConfig() # use defaults
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@keyword
|
|
28
|
+
def get_value_from_user(self, prompt="Enter value:", default=""):
|
|
29
|
+
return GetValueFromUserDialog.show(prompt,default,config=self.config)
|
|
30
|
+
|
|
31
|
+
@keyword
|
|
32
|
+
def run_manual_steps(self, steps):
|
|
33
|
+
ExecuteManualStepDialog.run_steps(steps, config=self.config)
|
|
34
|
+
|
|
35
|
+
@keyword
|
|
36
|
+
def count_down(self, seconds):
|
|
37
|
+
CountdownDialogRunner.show(int(seconds), config=self.config)
|
|
38
|
+
|
|
39
|
+
@keyword
|
|
40
|
+
def get_confirmation(self, message):
|
|
41
|
+
return GetConfirmationFromUser.show(message=message,config=self.config)
|
|
42
|
+
|
|
43
|
+
@keyword
|
|
44
|
+
def get_multi_value(self, fields, default=None):
|
|
45
|
+
fields_list = fields if isinstance(fields, list) else [fields]
|
|
46
|
+
calculated_height = 150 + (len(fields_list) * 40) + 60
|
|
47
|
+
#max_field_length = max(len(field) for field in fields_list)
|
|
48
|
+
max_field_length = 20
|
|
49
|
+
calculated_width = 300 + (max_field_length * 8)
|
|
50
|
+
self.config.height = calculated_height
|
|
51
|
+
self.config.width = calculated_width
|
|
52
|
+
return MultiValueInput.run_multival(fields=fields,defaults=default,config=self.config)
|
|
53
|
+
|
|
54
|
+
@keyword
|
|
55
|
+
def choose_file(self, message="", filetypes=None, multiple=False):
|
|
56
|
+
return ChooseFromFileDialog.show( message, filetypes, multiple, self.config)
|
|
57
|
+
|
|
58
|
+
@keyword
|
|
59
|
+
def choose_folder(self, message):
|
|
60
|
+
return ChooseFolderDialog.show(message, self.config)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@keyword
|
|
64
|
+
def confirm_with_checkbox(self, message, checkbox_text="I agree"):
|
|
65
|
+
return ConfirmWithCheckbox.show(message, checkbox_text, self.config)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@keyword
|
|
69
|
+
def select_options_with_checkboxes(self, message, options, defaults=None):
|
|
70
|
+
"""Show multiple checkboxes and return selected options as dictionary."""
|
|
71
|
+
|
|
72
|
+
options_list = options if isinstance(options, list) else options.split('|')
|
|
73
|
+
|
|
74
|
+
# Base sizing
|
|
75
|
+
num_options = len(options_list)
|
|
76
|
+
max_option_length = max(len(opt) for opt in options_list)
|
|
77
|
+
message_length = len(message)
|
|
78
|
+
|
|
79
|
+
# Height: base + checkboxes + some buffer
|
|
80
|
+
calculated_height = 180 + (num_options * 40)
|
|
81
|
+
|
|
82
|
+
# Width: consider both message and longest option
|
|
83
|
+
width_from_message = min(600, max(300, message_length * 7))
|
|
84
|
+
width_from_options = max(300, 200 + (max_option_length * 8))
|
|
85
|
+
calculated_width = max(width_from_message, width_from_options)
|
|
86
|
+
|
|
87
|
+
self.config.height = calculated_height
|
|
88
|
+
self.config.width = calculated_width
|
|
89
|
+
|
|
90
|
+
return SelectOptionsWithCheckboxes.show(message, options, defaults, self.config)
|
DialogsPlus/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# dialog_config.py
|
|
2
|
+
|
|
3
|
+
import yaml
|
|
4
|
+
|
|
5
|
+
class DialogConfig:
|
|
6
|
+
def __init__(
|
|
7
|
+
self,
|
|
8
|
+
title="Robot Framework",
|
|
9
|
+
width=400,
|
|
10
|
+
height=150,
|
|
11
|
+
theme="blue",
|
|
12
|
+
appearance_mode="system",
|
|
13
|
+
|
|
14
|
+
button_width=120,
|
|
15
|
+
button_height=32,
|
|
16
|
+
label_font=("Courier New", 16, "bold"),
|
|
17
|
+
entry_width= 200,
|
|
18
|
+
entry_height=28,
|
|
19
|
+
spacing=10,
|
|
20
|
+
button_fg_color="#06bdb1",
|
|
21
|
+
button_font=("Courier New", 16, "bold"),
|
|
22
|
+
label_text_color="white",
|
|
23
|
+
button_text_color="black",
|
|
24
|
+
button_hover_color="#57b7b0",
|
|
25
|
+
entry_font=("Courier New", 14),
|
|
26
|
+
entry_text_color="white",
|
|
27
|
+
entry_fg_color="#212121",
|
|
28
|
+
entry_border_color="#46fff4",
|
|
29
|
+
frame_fg_color = "transparent",
|
|
30
|
+
progress_bar_width = 300,
|
|
31
|
+
progress_bar_height = 12,
|
|
32
|
+
progress_bar_color = "#00c0b5",
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
):
|
|
36
|
+
self.title = title
|
|
37
|
+
self.width = width
|
|
38
|
+
self.height = height
|
|
39
|
+
self.theme = theme
|
|
40
|
+
self.appearance_mode = appearance_mode
|
|
41
|
+
|
|
42
|
+
self.button_width = button_width
|
|
43
|
+
self.button_height = button_height
|
|
44
|
+
self.label_font = label_font
|
|
45
|
+
self.entry_width = entry_width
|
|
46
|
+
self.entry_height = entry_height
|
|
47
|
+
self.spacing = spacing
|
|
48
|
+
self.button_fg_color=button_fg_color
|
|
49
|
+
self.button_font = button_font
|
|
50
|
+
self.label_text_color = label_text_color
|
|
51
|
+
self.button_text_color = button_text_color
|
|
52
|
+
self.button_hover_color = button_hover_color
|
|
53
|
+
self.entry_font = entry_font
|
|
54
|
+
self.entry_text_color = entry_text_color
|
|
55
|
+
self.entry_fg_color = entry_fg_color
|
|
56
|
+
self.entry_border_color = entry_border_color
|
|
57
|
+
self.frame_fg_color = frame_fg_color
|
|
58
|
+
self.progress_bar_width = progress_bar_width
|
|
59
|
+
self.progress_bar_height = progress_bar_height
|
|
60
|
+
self.progress_bar_color = progress_bar_color
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def from_yaml(cls, path: str):
|
|
65
|
+
print(f"[DEBUG] Received config path: {path}")
|
|
66
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
67
|
+
data = yaml.safe_load(f)
|
|
68
|
+
|
|
69
|
+
return cls(
|
|
70
|
+
title=data.get("title", "Dialog"),
|
|
71
|
+
width=data.get("width", 300),
|
|
72
|
+
height=data.get("height", 150),
|
|
73
|
+
theme=data.get("theme", "blue"),
|
|
74
|
+
appearance_mode=data.get("appearance_mode", "system"),
|
|
75
|
+
|
|
76
|
+
button_width=data.get("button_width", 120),
|
|
77
|
+
button_height=data.get("button_height", 32),
|
|
78
|
+
label_font=tuple(data.get("label_font", ("Arial", 12))),
|
|
79
|
+
entry_height=data.get("entry_height", 28),
|
|
80
|
+
entry_width=data.get("entry_width",60),
|
|
81
|
+
spacing=data.get("spacing", 10),
|
|
82
|
+
button_fg_color=data.get("button_fg_color", "#06bdb1")
|
|
83
|
+
)
|
|
Binary file
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import customtkinter as ctk
|
|
2
|
+
from customtkinter import BooleanVar, IntVar
|
|
3
|
+
from DialogsPlus.utils.config import DialogConfig
|
|
4
|
+
from tkinter import filedialog
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# Export for use in other modules
|
|
9
|
+
__all__ = [
|
|
10
|
+
'BaseDialogRunner',
|
|
11
|
+
'BaseDialog',
|
|
12
|
+
'ctk',
|
|
13
|
+
'BooleanVar',
|
|
14
|
+
'filedialog',
|
|
15
|
+
'IntVar'
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
class BaseDialogRunner:
|
|
19
|
+
_theme_initialized = False
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
def _center_window(app, config):
|
|
23
|
+
app.update_idletasks()
|
|
24
|
+
|
|
25
|
+
screen_width = app.winfo_screenwidth()
|
|
26
|
+
screen_height = app.winfo_screenheight()
|
|
27
|
+
|
|
28
|
+
x = (screen_width - config.width) // 2
|
|
29
|
+
y = (screen_height - config.height) // 2
|
|
30
|
+
app.geometry(f"{config.width}x{config.height}+{x}+{y}")
|
|
31
|
+
|
|
32
|
+
@staticmethod
|
|
33
|
+
def create_app(config: DialogConfig):
|
|
34
|
+
# Initialize theme once on first dialog creation
|
|
35
|
+
if not BaseDialogRunner._theme_initialized:
|
|
36
|
+
ctk.set_appearance_mode(config.appearance_mode)
|
|
37
|
+
ctk.set_default_color_theme(config.theme)
|
|
38
|
+
BaseDialogRunner._theme_initialized = True
|
|
39
|
+
|
|
40
|
+
app = ctk.CTk()
|
|
41
|
+
app.title(config.title)
|
|
42
|
+
|
|
43
|
+
icon_path = os.path.join(os.path.dirname(__file__), "assets", "robot.ico")
|
|
44
|
+
try:
|
|
45
|
+
app.iconbitmap(icon_path)
|
|
46
|
+
except (FileNotFoundError, Exception):
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
BaseDialogRunner._center_window(app, config)
|
|
50
|
+
return app
|
|
51
|
+
|
|
52
|
+
@staticmethod
|
|
53
|
+
def run_dialog(ui_builder_func, config: DialogConfig):
|
|
54
|
+
app = BaseDialogRunner.create_app(config)
|
|
55
|
+
ui_builder_func(app)
|
|
56
|
+
app.mainloop()
|
|
57
|
+
|
|
58
|
+
app.withdraw()
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
after_ids = app.tk.call('after', 'info')
|
|
62
|
+
for after_id in after_ids:
|
|
63
|
+
try:
|
|
64
|
+
app.after_cancel(after_id)
|
|
65
|
+
except:
|
|
66
|
+
pass
|
|
67
|
+
except:
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
app.update_idletasks()
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
app.quit()
|
|
74
|
+
except:
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
app.destroy()
|
|
79
|
+
except:
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class BaseDialog:
|
|
84
|
+
|
|
85
|
+
def __init__(self, config=None):
|
|
86
|
+
self.config = config if config else DialogConfig()
|
|
87
|
+
self.result = {}
|
|
88
|
+
|
|
89
|
+
def create_button(self, parent, text, command, **kwargs):
|
|
90
|
+
"""Create a button with config styling"""
|
|
91
|
+
return ctk.CTkButton(
|
|
92
|
+
parent,
|
|
93
|
+
text=text,
|
|
94
|
+
command=command,
|
|
95
|
+
font=self.config.button_font,
|
|
96
|
+
fg_color=self.config.button_fg_color,
|
|
97
|
+
text_color=self.config.button_text_color,
|
|
98
|
+
hover_color=self.config.button_hover_color,
|
|
99
|
+
width=kwargs.get('width', self.config.button_width),
|
|
100
|
+
height=kwargs.get('height', self.config.button_height),
|
|
101
|
+
**{k: v for k, v in kwargs.items() if k not in ['width', 'height']}
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def create_label(self, parent, text, **kwargs):
|
|
105
|
+
"""Create a label with config styling"""
|
|
106
|
+
return ctk.CTkLabel(
|
|
107
|
+
parent,
|
|
108
|
+
text=text,
|
|
109
|
+
font=self.config.label_font,
|
|
110
|
+
text_color=self.config.label_text_color,
|
|
111
|
+
anchor=kwargs.get('anchor', "center"),
|
|
112
|
+
justify=kwargs.get('justify', "left"),
|
|
113
|
+
wraplength=kwargs.get('wraplength', 0),
|
|
114
|
+
**{k: v for k, v in kwargs.items() if k not in ['anchor', 'justify', 'wraplength']}
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
def create_entry(self, parent, **kwargs):
|
|
118
|
+
"""Create an entry with consistent styling from config"""
|
|
119
|
+
return ctk.CTkEntry(
|
|
120
|
+
parent,
|
|
121
|
+
width=kwargs.get('width', self.config.entry_width),
|
|
122
|
+
height=kwargs.get('height', self.config.entry_height),
|
|
123
|
+
font=kwargs.get('font', self.config.entry_font),
|
|
124
|
+
text_color=kwargs.get('text_color', self.config.entry_text_color),
|
|
125
|
+
fg_color=kwargs.get('fg_color', self.config.entry_fg_color),
|
|
126
|
+
border_color=kwargs.get('border_color', self.config.entry_border_color),
|
|
127
|
+
**{k: v for k, v in kwargs.items() if k not in ['width', 'height', 'font', 'text_color', 'fg_color', 'border_color']}
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def create_frame(self, parent, **kwargs):
|
|
131
|
+
return ctk.CTkFrame(
|
|
132
|
+
parent,
|
|
133
|
+
fg_color=kwargs.get('fg_color', self.config.frame_fg_color),
|
|
134
|
+
**{k: v for k, v in kwargs.items() if k not in ['fg_color']} # ← Add this
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
def create_progress_bar(self, parent, **kwargs):
|
|
138
|
+
return ctk.CTkProgressBar(
|
|
139
|
+
parent,
|
|
140
|
+
width=kwargs.get('width', self.config.progress_bar_width),
|
|
141
|
+
height=kwargs.get('height', self.config.progress_bar_height),
|
|
142
|
+
progress_color=kwargs.get('progress_color', self.config.progress_bar_color),
|
|
143
|
+
**{k: v for k, v in kwargs.items() if k not in ['width', 'height', 'progress_color']}
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def create_checkbox(self, parent, text="", **kwargs):
|
|
147
|
+
"""Create a checkbox with consistent styling from config"""
|
|
148
|
+
return ctk.CTkCheckBox(
|
|
149
|
+
parent,
|
|
150
|
+
text=text,
|
|
151
|
+
font=kwargs.get('font', self.config.label_font),
|
|
152
|
+
text_color=kwargs.get('text_color', self.config.label_text_color),
|
|
153
|
+
fg_color=kwargs.get('fg_color', self.config.button_fg_color),
|
|
154
|
+
hover_color=kwargs.get('hover_color', self.config.button_hover_color),
|
|
155
|
+
**{k: v for k, v in kwargs.items() if k not in ['font', 'text_color', 'fg_color', 'hover_color']}
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# testing this shit
|
|
160
|
+
|
|
161
|
+
def show(self):
|
|
162
|
+
def ui(app):
|
|
163
|
+
self.build_ui(app)
|
|
164
|
+
|
|
165
|
+
BaseDialogRunner.run_dialog(ui, self.config)
|
|
166
|
+
return self.result
|
|
167
|
+
|
|
168
|
+
def build_ui(self, app):
|
|
169
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
from DialogsPlus.widgets.base import BaseDialog, filedialog, BooleanVar, IntVar
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class InputDialog(BaseDialog):
|
|
7
|
+
|
|
8
|
+
def __init__(self, prompt, default="", config=None, is_error=False):
|
|
9
|
+
super().__init__(config)
|
|
10
|
+
self.prompt = prompt
|
|
11
|
+
self.default = default
|
|
12
|
+
self.is_error = is_error
|
|
13
|
+
|
|
14
|
+
def build_ui(self, app):
|
|
15
|
+
frame = self.create_frame(app)
|
|
16
|
+
frame.pack(pady=8)
|
|
17
|
+
|
|
18
|
+
label = self.create_label(frame, text=self.prompt)
|
|
19
|
+
label.pack()
|
|
20
|
+
|
|
21
|
+
entry_frame = self.create_frame(app)
|
|
22
|
+
entry_frame.pack(pady=8)
|
|
23
|
+
entry = self.create_entry(entry_frame)
|
|
24
|
+
entry.insert(0, self.default)
|
|
25
|
+
entry.pack()
|
|
26
|
+
|
|
27
|
+
def on_submit():
|
|
28
|
+
self.result['value'] = entry.get()
|
|
29
|
+
app.quit()
|
|
30
|
+
|
|
31
|
+
app.protocol("WM_DELETE_WINDOW", app.quit)
|
|
32
|
+
app.bind('<Return>', lambda e: on_submit())
|
|
33
|
+
app.bind('<Escape>', lambda e: app.quit())
|
|
34
|
+
|
|
35
|
+
entry.focus_set()
|
|
36
|
+
|
|
37
|
+
button_frame = self.create_frame(app)
|
|
38
|
+
button_frame.pack(pady=10)
|
|
39
|
+
self.create_button(button_frame, text="Submit", command=on_submit).pack()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ManualStepDialog(BaseDialog):
|
|
43
|
+
|
|
44
|
+
def __init__(self, message, config=None):
|
|
45
|
+
super().__init__(config)
|
|
46
|
+
self.message = message
|
|
47
|
+
|
|
48
|
+
def build_ui(self, app):
|
|
49
|
+
def on_pass():
|
|
50
|
+
self.result["status"] = "pass"
|
|
51
|
+
app.quit()
|
|
52
|
+
|
|
53
|
+
def on_fail():
|
|
54
|
+
self.result["status"] = "fail"
|
|
55
|
+
app.quit()
|
|
56
|
+
|
|
57
|
+
app.protocol("WM_DELETE_WINDOW", app.quit)
|
|
58
|
+
app.bind('<Escape>', lambda e: app.quit())
|
|
59
|
+
|
|
60
|
+
self.create_label(app, text=self.message).pack(pady=25)
|
|
61
|
+
|
|
62
|
+
button_frame = self.create_frame(app)
|
|
63
|
+
button_frame.pack(pady=(10, self.config.spacing), expand=True)
|
|
64
|
+
|
|
65
|
+
self.create_button(
|
|
66
|
+
button_frame,
|
|
67
|
+
text="PASS",
|
|
68
|
+
command=on_pass).pack(side="left", padx=10)
|
|
69
|
+
|
|
70
|
+
self.create_button(
|
|
71
|
+
button_frame,
|
|
72
|
+
text="FAIL",
|
|
73
|
+
command=on_fail).pack(side="left", padx=10)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class CountdownDialog(BaseDialog):
|
|
77
|
+
|
|
78
|
+
def __init__(self, seconds, message="Please wait...", config=None):
|
|
79
|
+
super().__init__(config)
|
|
80
|
+
self.seconds = seconds
|
|
81
|
+
self.message = message
|
|
82
|
+
|
|
83
|
+
def build_ui(self, app):
|
|
84
|
+
|
|
85
|
+
label = self.create_label(app, text="")
|
|
86
|
+
label.place(relx=0.5, rely=0.4, anchor="center")
|
|
87
|
+
|
|
88
|
+
progress = self.create_progress_bar(app)
|
|
89
|
+
progress.place(relx=0.5, rely=0.8, anchor="center")
|
|
90
|
+
progress.set(0)
|
|
91
|
+
|
|
92
|
+
start_time = time.perf_counter()
|
|
93
|
+
|
|
94
|
+
def update():
|
|
95
|
+
elapsed = time.perf_counter() - start_time
|
|
96
|
+
remaining = self.seconds - elapsed
|
|
97
|
+
|
|
98
|
+
if remaining > 0:
|
|
99
|
+
mins, secs = divmod(int(remaining), 60)
|
|
100
|
+
label.configure(text=f"{self.message}\n{mins:02}:{secs:02}")
|
|
101
|
+
progress.set(min(elapsed / self.seconds, 1))
|
|
102
|
+
app.after(100, update)
|
|
103
|
+
else:
|
|
104
|
+
progress.set(1)
|
|
105
|
+
label.configure(text=f"{self.message}\n00:00")
|
|
106
|
+
app.quit()
|
|
107
|
+
|
|
108
|
+
update()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class ConfirmationDialog(BaseDialog):
|
|
112
|
+
def __init__(self, message, default="Yes", config=None):
|
|
113
|
+
super().__init__(config)
|
|
114
|
+
self.message = message
|
|
115
|
+
self.default = default
|
|
116
|
+
|
|
117
|
+
def build_ui(self, app):
|
|
118
|
+
|
|
119
|
+
def on_yes():
|
|
120
|
+
self.result["status"] = "yes"
|
|
121
|
+
app.quit()
|
|
122
|
+
|
|
123
|
+
def on_no():
|
|
124
|
+
self.result["status"] = "no"
|
|
125
|
+
app.quit()
|
|
126
|
+
|
|
127
|
+
def on_cancel():
|
|
128
|
+
self.result["status"] = "cancel"
|
|
129
|
+
app.quit()
|
|
130
|
+
|
|
131
|
+
app.protocol("WM_DELETE_WINDOW", app.quit)
|
|
132
|
+
app.bind('<Escape>', lambda e: app.quit())
|
|
133
|
+
|
|
134
|
+
self.create_label(app, text=self.message).pack(pady=25)
|
|
135
|
+
|
|
136
|
+
button_frame = self.create_frame(app)
|
|
137
|
+
button_frame.pack(pady=(10, self.config.spacing), expand=True)
|
|
138
|
+
|
|
139
|
+
self.create_button(
|
|
140
|
+
button_frame,
|
|
141
|
+
text="Yes",
|
|
142
|
+
command=on_yes).pack(side="left", padx=5)
|
|
143
|
+
|
|
144
|
+
self.create_button(
|
|
145
|
+
button_frame,
|
|
146
|
+
text="No",
|
|
147
|
+
command=on_no).pack(side="left", padx=5)
|
|
148
|
+
|
|
149
|
+
self.create_button(
|
|
150
|
+
button_frame,
|
|
151
|
+
text="Cancel",
|
|
152
|
+
command=on_cancel).pack(side="left", padx=5)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class MultiValueInputDialog(BaseDialog):
|
|
158
|
+
def __init__(self, fields, defaults=None, config=None):
|
|
159
|
+
super().__init__(config)
|
|
160
|
+
|
|
161
|
+
# Normalize string input to list
|
|
162
|
+
self.fields = fields if isinstance(fields, list) else [fields]
|
|
163
|
+
self.defaults = defaults or {}
|
|
164
|
+
self.entries = {}
|
|
165
|
+
|
|
166
|
+
def build_ui(self, app):
|
|
167
|
+
def on_submit():
|
|
168
|
+
self.result = {field: self.entries[field].get() for field in self.fields}
|
|
169
|
+
self.result["status"] = "pass"
|
|
170
|
+
app.quit()
|
|
171
|
+
|
|
172
|
+
def on_cancel():
|
|
173
|
+
self.result["status"] = "fail"
|
|
174
|
+
app.quit()
|
|
175
|
+
|
|
176
|
+
app.protocol("WM_DELETE_WINDOW", on_cancel)
|
|
177
|
+
app.bind('<Escape>', lambda e: on_cancel())
|
|
178
|
+
app.bind('<Return>', lambda e: on_submit())
|
|
179
|
+
|
|
180
|
+
main_frame = self.create_frame(app)
|
|
181
|
+
main_frame.pack(fill="both", expand=True, padx=10, pady=10)
|
|
182
|
+
|
|
183
|
+
title = self.create_label(main_frame, text="Enter values")
|
|
184
|
+
title.pack(pady=25)
|
|
185
|
+
|
|
186
|
+
fields_frame = self.create_frame(main_frame)
|
|
187
|
+
fields_frame.pack(fill="both", expand=True)
|
|
188
|
+
|
|
189
|
+
for field in self.fields:
|
|
190
|
+
row_frame = self.create_frame(fields_frame)
|
|
191
|
+
row_frame.pack(fill="x", pady=5)
|
|
192
|
+
|
|
193
|
+
label = self.create_label(row_frame, text=field)
|
|
194
|
+
label.pack(side="left", padx=(0, 10))
|
|
195
|
+
|
|
196
|
+
entry = self.create_entry(row_frame)
|
|
197
|
+
entry.insert(0, self.defaults.get(field, ""))
|
|
198
|
+
entry.pack(side="left", fill="x", expand=True)
|
|
199
|
+
|
|
200
|
+
self.entries[field] = entry
|
|
201
|
+
|
|
202
|
+
# Buttons
|
|
203
|
+
button_frame = self.create_frame(app)
|
|
204
|
+
button_frame.place(relx=0.5, rely=0.8, anchor="center")
|
|
205
|
+
|
|
206
|
+
submit_btn = self.create_button(button_frame, text="Submit", command=on_submit)
|
|
207
|
+
submit_btn.pack(side="left", padx=5)
|
|
208
|
+
|
|
209
|
+
cancel_btn = self.create_button(button_frame, text="Cancel", command=on_cancel)
|
|
210
|
+
cancel_btn.pack(side="left", padx=5)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class FileDialog(BaseDialog):
|
|
217
|
+
def __init__(self, message="", filetypes=None, multiple=False, config=None):
|
|
218
|
+
super().__init__(config)
|
|
219
|
+
self.title = self.config.title
|
|
220
|
+
self.message = message
|
|
221
|
+
self.filetypes = filetypes or [("All files", "*.*")]
|
|
222
|
+
self.multiple = multiple
|
|
223
|
+
|
|
224
|
+
def build_ui(self, app):
|
|
225
|
+
# app.withdraw() # Hide the main window
|
|
226
|
+
|
|
227
|
+
def on_browse():
|
|
228
|
+
if self.multiple:
|
|
229
|
+
files = filedialog.askopenfilenames(title=self.title, filetypes=self.filetypes)
|
|
230
|
+
self.result['files'] = list(files) if files else None
|
|
231
|
+
|
|
232
|
+
else:
|
|
233
|
+
file = filedialog.askopenfilename(title=self.title, filetypes=self.filetypes)
|
|
234
|
+
self.result['file'] = file if file else None
|
|
235
|
+
|
|
236
|
+
app.quit()
|
|
237
|
+
|
|
238
|
+
app.protocol("WM_DELETE_WINDOW", app.quit)
|
|
239
|
+
app.bind('<Escape>', lambda e: app.quit())
|
|
240
|
+
|
|
241
|
+
self.create_label(app, text=self.message).pack(pady=25)
|
|
242
|
+
|
|
243
|
+
button_frame = self.create_frame(app)
|
|
244
|
+
button_frame.pack(pady=(10, self.config.spacing), expand=True)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
self.create_button(
|
|
248
|
+
button_frame,
|
|
249
|
+
text="Browse",
|
|
250
|
+
command=on_browse).pack(side="bottom", padx=10)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class FolderDialog(BaseDialog):
|
|
254
|
+
def __init__(self, message, config=None):
|
|
255
|
+
super().__init__(config)
|
|
256
|
+
self.title = self.config.title
|
|
257
|
+
self.message = message
|
|
258
|
+
|
|
259
|
+
def build_ui(self, app):
|
|
260
|
+
#app.withdraw() # Hide the main window
|
|
261
|
+
|
|
262
|
+
def on_browser_folder():
|
|
263
|
+
folder = filedialog.askdirectory(title=self.title)
|
|
264
|
+
self.result['folder'] = folder if folder else None
|
|
265
|
+
app.quit()
|
|
266
|
+
|
|
267
|
+
app.protocol("WM_DELETE_WINDOW", app.quit)
|
|
268
|
+
app.bind('<Escape>', lambda e: app.quit())
|
|
269
|
+
|
|
270
|
+
self.create_label(app, text=self.message).pack(pady=25)
|
|
271
|
+
|
|
272
|
+
button_frame = self.create_frame(app)
|
|
273
|
+
button_frame.pack(pady=(10, self.config.spacing), expand=True)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
self.create_button(
|
|
277
|
+
button_frame,
|
|
278
|
+
text="Browse",
|
|
279
|
+
command=on_browser_folder).pack(side="bottom", padx=10)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class CheckboxConfirmationDialog(BaseDialog):
|
|
283
|
+
def __init__(self, message, checkbox_text="I agree", config=None):
|
|
284
|
+
super().__init__(config)
|
|
285
|
+
self.message = message
|
|
286
|
+
self.checkbox_text = checkbox_text
|
|
287
|
+
|
|
288
|
+
def build_ui(self, app):
|
|
289
|
+
checkbox_var = BooleanVar(value=False)
|
|
290
|
+
|
|
291
|
+
def on_submit():
|
|
292
|
+
self.result['confirmed'] = checkbox_var.get()
|
|
293
|
+
app.quit()
|
|
294
|
+
|
|
295
|
+
app.protocol("WM_DELETE_WINDOW", app.quit)
|
|
296
|
+
app.bind('<Escape>', lambda e: app.quit())
|
|
297
|
+
app.bind('<Return>', lambda e: on_submit())
|
|
298
|
+
|
|
299
|
+
self.create_label(app, text=self.message).pack(pady=10)
|
|
300
|
+
|
|
301
|
+
self.create_checkbox(app, text=self.checkbox_text, variable=checkbox_var).pack(pady=10)
|
|
302
|
+
|
|
303
|
+
self.create_button(app, text="Submit", command=on_submit).pack(pady=10)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class MultiCheckboxDialog(BaseDialog):
|
|
307
|
+
def __init__(self, message, options, defaults=None, config=None):
|
|
308
|
+
super().__init__(config)
|
|
309
|
+
self.message = message
|
|
310
|
+
self.options = options if isinstance(options, list) else options.split('|')
|
|
311
|
+
self.defaults = defaults or []
|
|
312
|
+
self.checkbox_vars = {}
|
|
313
|
+
|
|
314
|
+
def build_ui(self, app):
|
|
315
|
+
def on_submit():
|
|
316
|
+
self.result = {option: var.get() for option, var in self.checkbox_vars.items()}
|
|
317
|
+
app.quit()
|
|
318
|
+
|
|
319
|
+
app.protocol("WM_DELETE_WINDOW", app.quit)
|
|
320
|
+
app.bind('<Escape>', lambda e: app.quit())
|
|
321
|
+
app.bind('<Return>', lambda e: on_submit())
|
|
322
|
+
|
|
323
|
+
self.create_label(app, text=self.message).pack(pady=20)
|
|
324
|
+
|
|
325
|
+
# Checkboxes frame
|
|
326
|
+
checkbox_frame = self.create_frame(app)
|
|
327
|
+
checkbox_frame.pack(pady=10, padx=20, fill="both", expand=True)
|
|
328
|
+
|
|
329
|
+
for option in self.options:
|
|
330
|
+
var = BooleanVar(value=(option in self.defaults))
|
|
331
|
+
self.checkbox_vars[option] = var
|
|
332
|
+
|
|
333
|
+
self.create_checkbox(checkbox_frame, text=option, variable=var).pack(
|
|
334
|
+
anchor="w", pady=5, padx=10
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
self.create_button(app, text="Submit", command=on_submit).pack(pady=20)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
from DialogsPlus.widgets.styling import ( InputDialog,
|
|
2
|
+
ManualStepDialog,
|
|
3
|
+
CountdownDialog,
|
|
4
|
+
ConfirmationDialog,
|
|
5
|
+
MultiValueInputDialog,
|
|
6
|
+
FileDialog,
|
|
7
|
+
FolderDialog,
|
|
8
|
+
CheckboxConfirmationDialog,
|
|
9
|
+
MultiCheckboxDialog)
|
|
10
|
+
from robot.api import logger
|
|
11
|
+
from robot.errors import ExecutionFailed
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class GetValueFromUserDialog:
|
|
15
|
+
@staticmethod
|
|
16
|
+
def show(prompt="Enter value:", default="", config=None):
|
|
17
|
+
dialog = InputDialog(prompt, default, config)
|
|
18
|
+
return dialog.show().get('value')
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ExecuteManualStepDialog:
|
|
22
|
+
@staticmethod
|
|
23
|
+
def show(message="Please perform the step and confirm.", config=None):
|
|
24
|
+
logger.info(message)
|
|
25
|
+
|
|
26
|
+
dialog = ManualStepDialog(message, config)
|
|
27
|
+
result = dialog.show()
|
|
28
|
+
|
|
29
|
+
if result.get("status") == "pass":
|
|
30
|
+
return
|
|
31
|
+
else:
|
|
32
|
+
failure_dialog = InputDialog("Test Failed - Reason:", "", config, is_error=True)
|
|
33
|
+
reason = failure_dialog.show().get('value', 'No reason provided')
|
|
34
|
+
logger.error(f"{message} | Reason: {reason}")
|
|
35
|
+
raise ExecutionFailed(reason)
|
|
36
|
+
|
|
37
|
+
@staticmethod
|
|
38
|
+
def run_steps(steps, config=None):
|
|
39
|
+
if isinstance(steps, str):
|
|
40
|
+
ExecuteManualStepDialog.show(steps, config)
|
|
41
|
+
elif isinstance(steps, list):
|
|
42
|
+
for step in steps:
|
|
43
|
+
ExecuteManualStepDialog.show(step, config)
|
|
44
|
+
else:
|
|
45
|
+
raise ExecutionFailed("Invalid input: must be a string or a list of strings.")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class CountdownDialogRunner:
|
|
49
|
+
@staticmethod
|
|
50
|
+
def show(seconds=10, message="Please wait...", config=None):
|
|
51
|
+
logger.info(f"Starting countdown for {seconds} seconds...")
|
|
52
|
+
dialog = CountdownDialog(seconds, message, config)
|
|
53
|
+
dialog.show()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class GetConfirmationFromUser:
|
|
57
|
+
@staticmethod
|
|
58
|
+
def show(message="Are you sure?", default="Yes", config=None):
|
|
59
|
+
logger.info(message)
|
|
60
|
+
dialog = ConfirmationDialog(message, default, config)
|
|
61
|
+
dialog.config.width = 450
|
|
62
|
+
result = dialog.show().get("status")
|
|
63
|
+
|
|
64
|
+
if result == "yes":
|
|
65
|
+
return True
|
|
66
|
+
elif result == "no":
|
|
67
|
+
return False
|
|
68
|
+
else: # cancel
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class MultiValueInput:
|
|
73
|
+
@staticmethod
|
|
74
|
+
def show(fields, defaults=None, config=None):
|
|
75
|
+
logger.info(f"Showing input dialog for: {fields}")
|
|
76
|
+
dialog = MultiValueInputDialog(fields, defaults=defaults, config=config)
|
|
77
|
+
result = dialog.show()
|
|
78
|
+
if result.get("status") == "pass":
|
|
79
|
+
return result
|
|
80
|
+
else:
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
@staticmethod
|
|
84
|
+
def run_multival(fields, config=None, defaults=None):
|
|
85
|
+
return MultiValueInput.show(fields, defaults=defaults, config=config)
|
|
86
|
+
|
|
87
|
+
class ChooseFromFileDialog:
|
|
88
|
+
@staticmethod
|
|
89
|
+
def show(message="", filetypes=None, multiple=False, config=None):
|
|
90
|
+
logger.info(f"Opening file picker: {message}")
|
|
91
|
+
dialog = FileDialog(message, filetypes, multiple, config)
|
|
92
|
+
result = dialog.show()
|
|
93
|
+
return result.get('files' if multiple else 'file')
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class ChooseFolderDialog:
|
|
97
|
+
@staticmethod
|
|
98
|
+
def show(message, config=None):
|
|
99
|
+
logger.info(f"Opening folder picker: {message}")
|
|
100
|
+
dialog = FolderDialog(message, config)
|
|
101
|
+
return dialog.show().get('folder')
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class ConfirmWithCheckbox:
|
|
105
|
+
@staticmethod
|
|
106
|
+
def show(message, checkbox_text="I agree", config=None):
|
|
107
|
+
logger.info(f"Showing checkbox confirmation: {message}")
|
|
108
|
+
dialog = CheckboxConfirmationDialog(message, checkbox_text, config)
|
|
109
|
+
result = dialog.show()
|
|
110
|
+
return result.get('confirmed', False)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class SelectOptionsWithCheckboxes:
|
|
114
|
+
@staticmethod
|
|
115
|
+
def show(message, options, defaults=None, config=None):
|
|
116
|
+
logger.info(f"Showing multi-checkbox dialog: {message}")
|
|
117
|
+
dialog = MultiCheckboxDialog(message, options, defaults, config)
|
|
118
|
+
return dialog.show()
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: robotframework-dialogsplus
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A drop-in enhancement for Robot Framework's Dialogs library with modern UI and extended user interaction keywords.
|
|
5
|
+
Author-email: Alpha-Centauri-00 <Alpha@Centauri.c0m>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Requires-Dist: customtkinter>=5.2.2
|
|
9
|
+
Requires-Dist: pyyaml>=6.0.3
|
|
10
|
+
Requires-Dist: robotframework>=7.3.2
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# DialogsPlus
|
|
14
|
+
A drop-in enhancement for Robot Framework's Dialogs library with modern UI and extended user interaction keywords.
|
|
15
|
+
|
|
16
|
+
### ⚠️ Known Limitations
|
|
17
|
+
|
|
18
|
+
- Not supported in headless environments such as CI/CD pipelines (e.g., Jenkins, GitHub Actions)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
**DialogsPlus** is a user-friendly and fully customizable dialog library for [Robot Framework](https://robotframework.org/), built on top of [`customtkinter`](https://github.com/TomSchimansky/CustomTkinter). It extends the built-in dialog functionality with rich GUI dialogs that are **stylish**, **modern**, and **configurable** via a simple `config.yaml` file.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## ✨ Features
|
|
26
|
+
|
|
27
|
+
- Easy-to-use dialogs for user interaction during test runs
|
|
28
|
+
- Full GUI-based interface using `customtkinter`
|
|
29
|
+
- Customizable look and feel (colors, fonts, sizes, etc.) via `config.yaml`
|
|
30
|
+
- Drop-in replacement for standard Robot Framework dialogs
|
|
31
|
+
- Supports dynamic sizing based on input fields or options
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
Pull requests are welcome! More info coming soon.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
DialogsPlus/__init__.py,sha256=bj2en4W8oOadu0BgP-BMmhOB1woGUFfNYRyQIffTCWI,102
|
|
2
|
+
DialogsPlus/dialogsplus.py,sha256=EEZpMYvu8Evu3V_Xy-0hNl5UsaCMa4VWJUycm59wZG0,3630
|
|
3
|
+
DialogsPlus/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
DialogsPlus/utils/config.py,sha256=aPoNgC5RPOtp6yEZiNQkj-qhyLTo391eEscqjL2w9_M,2864
|
|
5
|
+
DialogsPlus/widgets/base.py,sha256=lyTnRtnE1aI0rCrqG6a17kIjcFNcacTZr3rmQ0zmCm0,5929
|
|
6
|
+
DialogsPlus/widgets/styling.py,sha256=LL-O-gPUw3I_QIqn05BK02SEE4qnVt9beNV_nQjmMXk,10981
|
|
7
|
+
DialogsPlus/widgets/wrappers.py,sha256=oVa-ScnDNr8IdGv2aR_HKflL5DhCeqVlO-Ix5C5snS8,4352
|
|
8
|
+
DialogsPlus/widgets/assets/robot.ico,sha256=Ikez3We-xWB3jvB7WMXB5mpgxUx57ebZbR1hcmBkoZk,7744
|
|
9
|
+
robotframework_dialogsplus-0.1.0.dist-info/METADATA,sha256=ZfSv2NzmNoYgbrl71LrEOKpsDSlEBn9uQi5yE6F-mlA,1443
|
|
10
|
+
robotframework_dialogsplus-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
11
|
+
robotframework_dialogsplus-0.1.0.dist-info/RECORD,,
|