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