# Copyright 2008-2015 Nokia Networks
# Copyright 2016- Robot Framework Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import time
import tkinter as tk
from importlib.resources import read_binary
from robot.utils import WINDOWS
if WINDOWS:
# A hack to override the default taskbar icon on Windows. See, for example:
# https://stackoverflow.com/questions/1551605/how-to-set-applications-taskbar-icon-in-windows-7/1552105
from ctypes import windll
windll.shell32.SetCurrentProcessExplicitAppUserModelID("robot.dialogs")
[docs]
class TkDialog(tk.Toplevel):
left_button = "OK"
right_button = "Cancel"
font = (None, 12)
padding = 8 if WINDOWS else 16
background = None # Can be used to change the dialog background.
def __init__(self, message, value=None, **config):
super().__init__(self._get_root())
self._button_bindings = {}
self._initialize_dialog()
self.widget = self._create_body(message, value, **config)
self._create_buttons()
self._finalize_dialog()
self._result = None
self._closed = False
def _get_root(self) -> tk.Tk:
root = tk.Tk()
root.withdraw()
icon = tk.PhotoImage(master=root, data=read_binary("robot", "logo.png"))
root.iconphoto(True, icon)
return root
def _initialize_dialog(self):
self.withdraw() # Remove from display until finalized.
self.title("Robot Framework")
self.configure(padx=self.padding, background=self.background)
self.protocol("WM_DELETE_WINDOW", self._close)
self.bind("<Escape>", self._close)
if self.left_button == TkDialog.left_button:
self.bind("<Return>", self._left_button_clicked)
def _finalize_dialog(self):
self.update() # Needed to get accurate dialog size.
screen_width = self.winfo_screenwidth()
screen_height = self.winfo_screenheight()
min_width = screen_width // 5
min_height = screen_height // 8
width = max(self.winfo_reqwidth(), min_width)
height = max(self.winfo_reqheight(), min_height)
x = (screen_width - width) // 2
y = (screen_height - height) // 2
self.geometry(f"{width}x{height}+{x}+{y}")
self.lift()
self.deiconify()
if self.widget:
self.widget.focus_set()
def _create_body(self, message, value, **config) -> "tk.Entry|tk.Listbox|None":
frame = tk.Frame(self, background=self.background)
max_width = self.winfo_screenwidth() // 2
label = tk.Label(
frame,
text=message,
anchor=tk.W,
justify=tk.LEFT,
wraplength=max_width,
pady=self.padding,
background=self.background,
font=self.font,
)
label.pack(fill=tk.BOTH)
widget = self._create_widget(frame, value, **config)
if widget:
widget.pack(fill=tk.BOTH, pady=self.padding)
frame.pack(expand=1, fill=tk.BOTH)
return widget
def _create_widget(self, frame, value) -> "tk.Entry|tk.Listbox|None":
return None
def _create_buttons(self):
frame = tk.Frame(self, pady=self.padding, background=self.background)
self._create_button(frame, self.left_button, self._left_button_clicked)
self._create_button(frame, self.right_button, self._right_button_clicked)
frame.pack()
def _create_button(self, parent, label, callback):
if label:
button = tk.Button(
parent,
text=label,
command=callback,
width=10,
underline=0,
font=self.font,
)
button.pack(side=tk.LEFT, padx=self.padding)
for char in label[0].upper(), label[0].lower():
self.bind(char, callback)
self._button_bindings[char] = callback
def _left_button_clicked(self, event=None):
if self._validate_value():
self._result = self._get_value()
self._close()
def _validate_value(self) -> bool:
return True
def _get_value(self) -> "str|list[str]|bool|None":
return None
def _right_button_clicked(self, event=None):
self._result = self._get_right_button_value()
self._close()
def _get_right_button_value(self) -> "str|list[str]|bool|None":
return None
def _close(self, event=None):
self._closed = True
[docs]
def show(self) -> "str|list[str]|bool|None":
# Use a loop with `update()` instead of `wait_window()` to allow
# timeouts and signals stop execution.
try:
while not self._closed:
time.sleep(0.1)
self.update()
finally:
self.destroy()
self.update() # Needed on Linux to close the dialog (#1466, #4993)
return self._result
[docs]
class MessageDialog(TkDialog):
right_button = None
[docs]
class SelectionDialog(TkDialog):
def __init__(self, message, values, default=None):
super().__init__(message, values, default=default)
def _create_widget(self, parent, values, default=None) -> tk.Listbox:
widget = tk.Listbox(parent, font=self.font)
for item in values:
widget.insert(tk.END, item)
if default is not None:
index = self._get_default_value_index(default, values)
widget.select_set(index)
widget.activate(index)
widget.config(width=0)
return widget
def _get_default_value_index(self, default, values) -> int:
if default in values:
return values.index(default)
try:
index = int(default) - 1
except ValueError:
raise ValueError(f"Invalid default value '{default}'.")
if index < 0 or index >= len(values):
raise ValueError("Default value index is out of bounds.")
return index
def _validate_value(self) -> bool:
return bool(self.widget.curselection())
def _get_value(self) -> str:
return self.widget.get(self.widget.curselection())
[docs]
class MultipleSelectionDialog(TkDialog):
def _create_widget(self, parent, values) -> tk.Listbox:
widget = tk.Listbox(parent, selectmode="multiple", font=self.font)
for item in values:
widget.insert(tk.END, item)
widget.config(width=0)
return widget
def _get_value(self) -> "list[str]":
return [self.widget.get(i) for i in self.widget.curselection()]
[docs]
class PassFailDialog(TkDialog):
left_button = "PASS"
right_button = "FAIL"
def _get_value(self) -> bool:
return True
def _get_right_button_value(self) -> bool:
return False