1""" 2 3.. moduleauthor:: easygui developers and Stephen Raymond Ferg 4.. default-domain:: py 5.. highlight:: python 6 7Version |release| 8""" 9 10try: 11 from . import global_state 12except: 13 import global_state 14 15try: 16 import tkinter as tk # python 3 17except: 18 import Tkinter as tk # python 2 19 20# ----------------------------------------------------------------------- 21# multpasswordbox 22# ----------------------------------------------------------------------- 23 24 25def multpasswordbox(msg="Fill in values for the fields.", 26 title=" ", fields=tuple(), values=tuple(), 27 callback=None, run=True): 28 r""" 29 Same interface as multenterbox. But in multpassword box, 30 the last of the fields is assumed to be a password, and 31 is masked with asterisks. 32 33 :param str msg: the msg to be displayed. 34 :param str title: the window title 35 :param list fields: a list of fieldnames. 36 :param list values: a list of field values 37 :return: String 38 39 **Example** 40 41 Here is some example code, that shows how values returned from 42 multpasswordbox can be checked for validity before they are accepted:: 43 44 msg = "Enter logon information" 45 title = "Demo of multpasswordbox" 46 fieldNames = ["Server ID", "User ID", "Password"] 47 fieldValues = [] # we start with blanks for the values 48 fieldValues = multpasswordbox(msg,title, fieldNames) 49 50 # make sure that none of the fields was left blank 51 while 1: 52 if fieldValues is None: break 53 errmsg = "" 54 for i in range(len(fieldNames)): 55 if fieldValues[i].strip() == "": 56 errmsg = errmsg + ('"%s" is a required field.\n\n' % 57 fieldNames[i]) 58 if errmsg == "": break # no problems found 59 fieldValues = multpasswordbox(errmsg, title, 60 fieldNames, fieldValues) 61 62 print("Reply was: %s" % str(fieldValues)) 63 64 """ 65 if run: 66 mb = MultiBox(msg, title, fields, values, mask_last=True, 67 callback=callback) 68 69 reply = mb.run() 70 71 return reply 72 73 else: 74 75 mb = MultiBox(msg, title, fields, values, mask_last=True, 76 callback=callback) 77 78 return mb 79 80 81# ------------------------------------------------------------------- 82# multenterbox 83# ------------------------------------------------------------------- 84# TODO RL: Should defaults be list constructors. 85# i think after multiple calls, the value is retained. 86# TODO RL: Rename/alias to multienterbox? 87# default should be None and then in the logic create an empty liglobal_state. 88def multenterbox(msg="Fill in values for the fields.", title=" ", 89 fields=[], values=[], callback=None, run=True): 90 r""" 91 Show screen with multiple data entry fields. 92 93 If there are fewer values than names, the list of values is padded with 94 empty strings until the number of values is the same as the number 95 of names. 96 97 If there are more values than names, the list of values 98 is truncated so that there are as many values as names. 99 100 Returns a list of the values of the fields, 101 or None if the user cancels the operation. 102 103 Here is some example code, that shows how values returned from 104 multenterbox can be checked for validity before they are accepted:: 105 106 msg = "Enter your personal information" 107 title = "Credit Card Application" 108 fieldNames = ["Name","Street Address","City","State","ZipCode"] 109 fieldValues = [] # we start with blanks for the values 110 fieldValues = multenterbox(msg,title, fieldNames) 111 112 # make sure that none of the fields was left blank 113 while 1: 114 if fieldValues is None: break 115 errmsg = "" 116 for i in range(len(fieldNames)): 117 if fieldValues[i].strip() == "": 118 errmsg += ('"%s" is a required field.\n\n' % fieldNames[i]) 119 if errmsg == "": 120 break # no problems found 121 fieldValues = multenterbox(errmsg, title, fieldNames, fieldValues) 122 123 print("Reply was: %s" % str(fieldValues)) 124 125 :param str msg: the msg to be displayed. 126 :param str title: the window title 127 :param list fields: a list of fieldnames. 128 :param list values: a list of field values 129 :return: String 130 """ 131 if run: 132 mb = MultiBox(msg, title, fields, values, mask_last=False, 133 callback=callback) 134 reply = mb.run() 135 return reply 136 else: 137 mb = MultiBox(msg, title, fields, values, mask_last=False, 138 callback=callback) 139 return mb 140 141 142class MultiBox(object): 143 144 """ Show multiple data entry fields 145 146 This object does a number of things: 147 148 - chooses a GUI framework (wx, qt) 149 - checks the data sent to the GUI 150 - performs the logic (button ok should close the window?) 151 - defines what methods the user can invoke and 152 what properties he can change. 153 - calls the ui in defined ways, so other gui 154 frameworks can be used without breaking anything to the user 155 """ 156 157 def __init__(self, msg, title, fields, values, mask_last, callback): 158 """ Create box object 159 160 Parameters 161 ---------- 162 msg : string 163 text displayed in the message area (instructions...) 164 title : str 165 the window title 166 fields: list 167 names of fields 168 values: list 169 initial values 170 callback: function 171 if set, this function will be called when OK is pressed 172 run: bool 173 if True, a box object will be created and returned, but not run 174 175 Returns 176 ------- 177 self 178 The MultiBox object 179 """ 180 181 self.callback = callback 182 183 self.fields, self.values = self.check_fields(fields, values) 184 185 self.ui = GUItk(msg, title, self.fields, self.values, 186 mask_last, self.callback_ui) 187 188 def run(self): 189 """ Start the ui """ 190 self.ui.run() 191 self.ui = None 192 return self.values 193 194 def stop(self): 195 """ Stop the ui """ 196 self.ui.stop() 197 198 def callback_ui(self, ui, command, values): 199 """ This method is executed when ok, cancel, or x is pressed in the ui. 200 """ 201 if command == 'update': # OK was pressed 202 self.values = values 203 if self.callback: 204 # If a callback was set, call main process 205 self.callback(self) 206 else: 207 self.stop() 208 elif command == 'x': 209 self.stop() 210 self.values = None 211 elif command == 'cancel': 212 self.stop() 213 self.values = None 214 215 # methods to change properties -------------- 216 217 @property 218 def msg(self): 219 """Text in msg Area""" 220 return self._msg 221 222 @msg.setter 223 def msg(self, msg): 224 self.ui.set_msg(msg) 225 226 @msg.deleter 227 def msg(self): 228 self._msg = "" 229 self.ui.set_msg(self._msg) 230 231 # Methods to validate what will be sent to ui --------- 232 233 def check_fields(self, fields, values): 234 if len(fields) == 0: 235 return None 236 237 fields = list(fields[:]) # convert possible tuples to a list 238 values = list(values[:]) # convert possible tuples to a list 239 240 # TODO RL: The following seems incorrect when values>fields. Replace 241 # below with zip? 242 if len(values) == len(fields): 243 pass 244 elif len(values) > len(fields): 245 fields = fields[0:len(values)] 246 else: 247 while len(values) < len(fields): 248 values.append("") 249 250 return fields, values 251 252 253class GUItk(object): 254 255 """ This object contains the tk root object. 256 It draws the window, waits for events and communicates them 257 to MultiBox, together with the entered values. 258 259 The position in wich it is drawn comes from a global variable. 260 261 It also accepts commands from Multibox to change its message. 262 """ 263 264 def __init__(self, msg, title, fields, values, mask_last, callback): 265 266 self.callback = callback 267 268 self.boxRoot = tk.Tk() 269 270 self.create_root(title) 271 272 self.set_pos(global_state.window_position) # GLOBAL POSITION 273 274 self.create_msg_widget(msg) 275 276 self.create_entryWidgets(fields, values, mask_last) 277 278 self.create_buttons() 279 280 self.entryWidgets[0].focus_force() # put the focus on the entryWidget 281 282 # Run and stop methods --------------------------------------- 283 284 def run(self): 285 self.boxRoot.mainloop() # run it! 286 self.boxRoot.destroy() # Close the window 287 288 def stop(self): 289 # Get the current position before quitting 290 self.get_pos() 291 292 self.boxRoot.quit() 293 294 def x_pressed(self): 295 self.callback(self, command='x', values=self.get_values()) 296 297 def cancel_pressed(self, event): 298 self.callback(self, command='cancel', values=self.get_values()) 299 300 def ok_pressed(self, event): 301 self.callback(self, command='update', values=self.get_values()) 302 303 # Methods to change content --------------------------------------- 304 305 def set_msg(self, msg): 306 self.messageWidget.configure(text=msg) 307 self.entryWidgets[0].focus_force() # put the focus on the entryWidget 308 309 def set_pos(self, pos): 310 self.boxRoot.geometry(pos) 311 312 def get_pos(self): 313 # The geometry() method sets a size for the window and positions it on 314 # the screen. The first two parameters are width and height of 315 # the window. The last two parameters are x and y screen coordinates. 316 # geometry("250x150+300+300") 317 geom = self.boxRoot.geometry() # "628x672+300+200" 318 global_state.window_position = '+' + geom.split('+', 1)[1] 319 320 def get_values(self): 321 values = [] 322 for entryWidget in self.entryWidgets: 323 values.append(entryWidget.get()) 324 return values 325 326 # Initial configuration methods --------------------------------------- 327 # These ones are just called once, at setting. 328 329 def create_root(self, title): 330 331 self.boxRoot.protocol('WM_DELETE_WINDOW', self.x_pressed) 332 self.boxRoot.title(title) 333 self.boxRoot.iconname('Dialog') 334 self.boxRoot.bind("<Escape>", self.cancel_pressed) 335 336 def create_msg_widget(self, msg): 337 # -------------------- the msg widget ---------------------------- 338 self.messageWidget = tk.Message(self.boxRoot, width="4.5i", text=msg) 339 self.messageWidget.configure( 340 font=(global_state.PROPORTIONAL_FONT_FAMILY, global_state.PROPORTIONAL_FONT_SIZE)) 341 self.messageWidget.pack( 342 side=tk.TOP, expand=1, fill=tk.BOTH, padx='3m', pady='3m') 343 344 def create_entryWidgets(self, fields, values, mask_last): 345 346 self.entryWidgets = [] 347 348 lastWidgetIndex = len(fields) - 1 349 350 for widgetIndex in range(len(fields)): 351 name = fields[widgetIndex] 352 value = values[widgetIndex] 353 entryFrame = tk.Frame(master=self.boxRoot) 354 entryFrame.pack(side=tk.TOP, fill=tk.BOTH) 355 356 # --------- entryWidget ------------------------------------------- 357 labelWidget = tk.Label(entryFrame, text=name) 358 labelWidget.pack(side=tk.LEFT) 359 360 entryWidget = tk.Entry(entryFrame, width=40, highlightthickness=2) 361 self.entryWidgets.append(entryWidget) 362 entryWidget.configure( 363 font=(global_state.PROPORTIONAL_FONT_FAMILY, global_state.TEXT_ENTRY_FONT_SIZE)) 364 entryWidget.pack(side=tk.RIGHT, padx="3m") 365 366 self.bindArrows(entryWidget) 367 368 entryWidget.bind("<Return>", self.ok_pressed) 369 entryWidget.bind("<Escape>", self.cancel_pressed) 370 371 # for the last entryWidget, if this is a multpasswordbox, 372 # show the contents as just asterisks 373 if widgetIndex == lastWidgetIndex: 374 if mask_last: 375 self.entryWidgets[widgetIndex].configure(show="*") 376 377 # put text into the entryWidget 378 if value is None: 379 value = '' 380 self.entryWidgets[widgetIndex].insert( 381 0, '{}'.format(value)) 382 383 def create_buttons(self): 384 self.buttonsFrame = tk.Frame(master=self.boxRoot) 385 self.buttonsFrame.pack(side=tk.BOTTOM) 386 387 self.create_cancel_button() 388 self.create_ok_button() 389 390 def create_ok_button(self): 391 392 okButton = tk.Button(self.buttonsFrame, takefocus=1, text="OK") 393 self.bindArrows(okButton) 394 okButton.pack(expand=1, side=tk.LEFT, padx='3m', pady='3m', 395 ipadx='2m', ipady='1m') 396 397 # for the commandButton, bind activation events to the activation event 398 # handler 399 commandButton = okButton 400 handler = self.ok_pressed 401 for selectionEvent in global_state.STANDARD_SELECTION_EVENTS: 402 commandButton.bind("<%s>" % selectionEvent, handler) 403 404 def create_cancel_button(self): 405 406 cancelButton = tk.Button(self.buttonsFrame, takefocus=1, text="Cancel") 407 self.bindArrows(cancelButton) 408 cancelButton.pack(expand=1, side=tk.LEFT, padx='3m', pady='3m', 409 ipadx='2m', ipady='1m') 410 411 # for the commandButton, bind activation events to the activation event 412 # handler 413 commandButton = cancelButton 414 handler = self.cancel_pressed 415 for selectionEvent in global_state.STANDARD_SELECTION_EVENTS: 416 commandButton.bind("<%s>" % selectionEvent, handler) 417 418 def bindArrows(self, widget): 419 420 widget.bind("<Down>", self.tabRight) 421 widget.bind("<Up>", self.tabLeft) 422 423 widget.bind("<Right>", self.tabRight) 424 widget.bind("<Left>", self.tabLeft) 425 426 def tabRight(self, event): 427 self.boxRoot.event_generate("<Tab>") 428 429 def tabLeft(self, event): 430 self.boxRoot.event_generate("<Shift-Tab>") 431 432 433def demo1(): 434 msg = "Enter your personal information" 435 title = "Credit Card Application" 436 fieldNames = ["Name", "Street Address", "City", "State", "ZipCode"] 437 fieldValues = [] # we start with blanks for the values 438 439 # make sure that none of the fields was left blank 440 while True: 441 442 fieldValues = multenterbox(msg, title, fieldNames, fieldValues) 443 cancelled = fieldValues is None 444 errors = [] 445 if cancelled: 446 pass 447 else: # check for errors 448 for name, value in zip(fieldNames, fieldValues): 449 if value.strip() == "": 450 errors.append('"{}" is a required field.'.format(name)) 451 452 all_ok = not errors 453 454 if cancelled or all_ok: 455 break # no problems found 456 457 msg = "\n".join(errors) 458 459 print("Reply was: {}".format(fieldValues)) 460 461 462class Demo2(): 463 464 def __init__(self): 465 msg = "Without flicker. Enter your personal information" 466 title = "Credit Card Application" 467 fieldNames = ["Name", "Street Address", "City", "State", "ZipCode"] 468 fieldValues = [] # we start with blanks for the values 469 470 fieldValues = multenterbox(msg, title, fieldNames, fieldValues, 471 callback=self.check_for_blank_fields) 472 print("Reply was: {}".format(fieldValues)) 473 474 def check_for_blank_fields(self, box): 475 # make sure that none of the fields was left blank 476 cancelled = box.values is None 477 errors = [] 478 if cancelled: 479 pass 480 else: # check for errors 481 for name, value in zip(box.fields, box.values): 482 if value.strip() == "": 483 errors.append('"{}" is a required field.'.format(name)) 484 485 all_ok = not errors 486 487 if cancelled or all_ok: 488 box.stop() # no problems found 489 490 box.msg = "\n".join(errors) 491 492 493if __name__ == '__main__': 494 demo1() 495 Demo2() 496