-
Notifications
You must be signed in to change notification settings - Fork 14
/
UI_functions.py
280 lines (217 loc) · 10.3 KB
/
UI_functions.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
"""
UI functions: user interface functions used throughout the application.
"""
import tkinter as tk
from pathlib import Path
from tkinter import filedialog
from typing import Optional, Union, Callable
def clear_screen(num_lines: int = 50) -> None:
clear_seq = '\n' * num_lines
print(clear_seq)
# User input and string processing functions:
def input_is_essentially_blank(subject_string: str) -> bool:
"""
Return True if string is empty or primarily composed of spaces, underscores,
special characters (eg brackets, punctuation).
Uses similar method to clean_for_filename to remove all except alphanumeric
characters, then returns True if string is empty (is no numbers or letters.
:param subject_string: str
:return: bool
"""
cleaned_string = "".join([c for c in subject_string if c.isalnum()]).rstrip()
if cleaned_string == '':
return True
# else:
return False
def clean_for_filename(some_string: str) -> str:
"""
Cleans a string for use as a filename.
eg student or classlist name
Returns a string with only alphanumeric characters and underscores.
# Possibly equivalent to: ''.join([c for c in text if re.match(r'\w', c)])
:param some_string: str
:return: str
"""
cleaner_filename = scrub_candidate_filename(some_string)
cleaned_filename = cleaner_filename.replace(' ', '_') # no slashes either
return cleaned_filename
def scrub_candidate_filename(dirty_string: str) -> str:
"""
Cleans string of non-alpha-numeric characters, but leaves spaces, dashes,
and underscores, stripping trailing spaces.
Replace disallowed characters with underscores to preserve form.
>>> scrub_candidate_filename(r"Les méµoires¬de¬M. d'Ar∫@gnåñ /\/\ 'abc∂éåß®∆˚__˙©¬ñ√ƒµ©∆∫ø'")
'Les méµoires_de_M_ d_Ar__gnåñ ____ _abc_éåß________ñ_ƒµ___ø_'
# Which at least makes distinct words clear, rather than:
'Les méµoiresdeM dArgnåñ abcéåßñƒµø' # Which is completely unreadable.
:param dirty_string: str
:return: str
"""
allowed_special_characters = [' ', '_', '-', ]
return "".join([c if c.isalnum()
or c in allowed_special_characters
else '_'
for c in dirty_string
]).rstrip()
def ask_user_bool(question: str, invalid_input_response: str|None = None) -> bool:
"""
Get user input, return a bool response.
Optional additional instruction on invalid input.
:param question: str
:param invalid_input_response: str
:return: bool
"""
valid_responses = {"Y": True,
"YES": True,
"N": False,
"NO": False,
}
response = get_user_input(prompt=question,
validation=lambda user_input: user_input.upper() in valid_responses,
validation_error_msg=invalid_input_response)
return valid_responses[response.upper()]
def save_as_dialogue(title_str: str|None = None,
default_file_extension: str |None= None,
filetypes: list[tuple[str, str]]|None = None,
suggested_filename: str|None = None,
start_dir: Union[Path, str] = '..'
) -> Optional[Path]:
"""
Prompts user to select a directory and filename to save a file to.
Calls tkinter filedialog.asksaveasfilename with title (if provided), and
filetypes argument (if provided) eg '*.png'.
title_str is string to be displayed in popup's title bar.
NB if none provided, or is None - title displayed is "Save as".
default_filetype is a string to be added as an extension (eg '.png, but can
be anything (eg 'dead_parrot', '_chart.png' in the event user does not
input a name with an extension.
If no default_filetype is provided, but optional filetypes are provided,
convention will be for default extension to be listed first.
eg if all files option: [('.png', '*.png'), ("all files", "*.*")]
- .png will be default extension
eg if default filetypes display (the first to appear in drop down) is to
be all files, user must pass a default extension.
start_dir is a path string for the folder to start the dialogue box in.
filetypes is a list of tuples with 2 values, a label and a pattern
eg for png and all files: [('.png', '*.png'), ("all files", "*.*")]
suggested_filename is a string displayed in popup as suggested filename,
user can change/alter as desired.
Returns None instead of empty string if no file is selected.
Default starting directory is directory above application directory.
If start_dir is unresolvable, or '' or None, the dialog will default to
starting at the last directory selected.
See https://www.tcl.tk/man/tcl8.6/TkCmd/chooseDirectory.htm
NB When default_file_extension is given, but not in filetypes, if
the first filetype in filetypes is NOT ("all files", "*.*"), that
first filetypes in filetypes will by appended rather than the default
extension. If ("all files", "*.*") is the first filetype, the
default_file_extension will be appended as expected.
:param title_str: str
:param default_file_extension: list
:param suggested_filename: str
:param filetypes: list[tuple[str, str]]
:param start_dir: Path or str
:return: Path
"""
root = tk.Tk()
root.withdraw()
if filetypes and not default_file_extension:
# Make extension of first listed filetypes default save extension.
first_extension_without_wildcard = filetypes[0][1].strip('*')
if first_extension_without_wildcard != '.':
default_file_extension = first_extension_without_wildcard
default_filetypes = [("all files", "*.*")]
if not filetypes:
filetypes = default_filetypes
filepath_str = filedialog.asksaveasfilename(title=title_str,
defaultextension=default_file_extension,
filetypes=filetypes,
initialfile=suggested_filename,
initialdir=start_dir,
)
if not filepath_str:
return None
return Path(filepath_str)
def select_file_dialogue(title_str: str|None = None,
filetypes: list[tuple[str, str]]|None = None,
start_dir: Union[Path, str] = '..',
) -> Optional[Path]:
"""
Prompt user to select a file.
Prompts user to select a file. Calls tkinter
filedialog.askopenfilename with title (if provided), and filetypes
argument (if provided) eg '*.png'.
filetypes is a list of tuples with 2 values, a label and a pattern
eg for png and all files: [('.png', '*.png'), ("all files", "*.*")]
Default starting directory is directory above application directory.
If start_dir is unresolvable, or '' or None, the dialog will default
to starting at the last directory selected on recent versions of
Windows, current working directory on old Windows/other OS.
See https://www.tcl.tk/man/tcl8.6/TkCmd/chooseDirectory.htm
Returns None instead of empty string if no file is selected.
NB If no title is passed to filedialog.askopenfilename, the window
title will be "Open".
:param title_str: str
:param filetypes: list[tuple[str, str]]
:param start_dir: str
:return: Path or None
"""
root = tk.Tk()
root.withdraw()
default_filetypes = [("all files", "*.*")]
if not filetypes:
filetypes = default_filetypes
filepath_str = filedialog.askopenfilename(title=title_str,
filetypes=filetypes,
initialdir=start_dir,
)
if not filepath_str:
return None
return Path(filepath_str)
def select_folder_dialogue(title_str: str|None = None, start_dir: Union[Path, str] = '..') -> Optional[Path]:
"""
Prompt user to select a directory.
Prompts user to select a file. Calls tkinter
filedialog.askopenfilename with title (if provided), and filetypes
argument (if provided) eg '*.png'.
Default starting directory is directory above application directory.
If start_dir is unresolvable, or '' or None, the dialog will default
to starting at the last directory selected on recent versions of
Windows, current working directory on old Windows/other OS.
See https://www.tcl.tk/man/tcl8.6/TkCmd/chooseDirectory.htm
Returns None instead of empty string if no file is selected.
:param title_str: str
:param start_dir: Path or str - Path for dialogue to start in.
:return: Path or None
"""
root = tk.Tk()
root.withdraw()
dir_path_str = filedialog.askdirectory(initialdir=start_dir, title=title_str)
if not dir_path_str:
return None
return Path(dir_path_str)
def get_user_input(prompt: str,
validation: Callable,
validation_error_msg: Union[str, Callable]|None = None):
"""
Generic function for getting user input.
Supply desired user text prompt (eg `>>> ` or `Name: `).
Must supply a callable to validate input, which takes the user input string
as it's only argument, returning a bool according to validity of the input.
eg `lambda x: return True` if no validation desired/validate any input.
An optional validation_error_msg can be supplied, as a string or callable
taking the user input string as it's only argument. This allows a
responsive error message
eg: `lambda x: f'{x} is not the messiah, it's a very naughty boy!'`
:param prompt: str
:param validation: Callable - function to validate input.
:param validation_error_msg: Union[str, Callable] - error message.
:return:
"""
while not validation(user_input := input(prompt)):
if validation_error_msg is not None:
if isinstance(validation_error_msg, str):
print(validation_error_msg)
elif callable(validation_error_msg):
print(validation_error_msg(user_input))
return user_input