1"""
2
3.. moduleauthor:: easygui developers and Stephen Raymond Ferg
4.. default-domain:: py
5.. highlight:: python
6
7Version |release|
8"""
9
10
11import sys
12
13try:
14    from . import global_state
15except (SystemError, ValueError, ImportError):
16    import global_state
17
18try:
19    import tkinter as tk  # python 3
20    import tkinter.font as tk_Font
21except:
22    import Tkinter as tk  # python 2
23    import tkFont as tk_Font
24
25
26def demo_textbox():
27    demo_1()
28    Demo2()
29    Demo3()
30
31
32def demo_1():
33
34    title = "Demo of textbox: Classic box"
35
36    gnexp = ("This is a demo of the classic textbox call, "
37             "you can see it closes when ok is pressed.\n\n")
38
39    challenge = "INSERT A TEXT WITH MORE THAN TWO PARAGRAPHS"
40
41    text = "Insert your text here\n"
42
43    msg = gnexp + challenge
44
45    finished = False
46    while True:
47
48        text = textbox(msg, title, text)
49        escaped = not text
50        if escaped or finished:
51            break
52
53        if text.count("\n") >= 2:
54            msg = (u"You did it right! Press OK")
55            finished = True
56        else:
57            msg = u"You did it wrong! Try again!\n" + challenge
58
59
60class Demo2(object):
61
62    """ Program that challenges the user to write 5 a's """
63
64    def __init__(self):
65        """ Set and run the program """
66
67        title = "Demo of textbox: Classic box with callback"
68
69        gnexp = ("This is a demo of the textbox with a callback, "
70                 "it doesn't flicker!.\n\n")
71
72        msg = "INSERT A TEXT WITH FIVE OR MORE A\'s"
73
74        text_snippet = "Insert your text here"
75
76        self.finished = False
77
78        textbox(gnexp + msg, title, text_snippet, False,
79                callback=self.check_answer, run=True)
80
81    def check_answer(self, box):
82        """ Callback from TextBox
83
84        Parameters
85        -----------
86        box: object
87            object containing parameters and methods to communicate with the ui
88
89        Returns
90        -------
91        nothing:
92            its return is through the box object
93        """
94
95        if self.finished:
96            box.stop()
97
98        if box.text.lower().count("a") >= 5:
99            box.msg = u"\n\nYou did it right! Press OK button to continue."
100            box.stop()
101            self.finished
102        else:
103            box.msg = u"\n\nMore a's are needed!"
104
105
106class Demo3(object):
107
108    """ Program that challenges the user to find a typo """
109
110    def __init__(self):
111        """ Set and run the program """
112
113        self.finished = False
114
115        title = "Demo of textbox: Object with callback"
116
117        msg = ("This is a demo of the textbox set as "
118               "an object with a callback, "
119               "you can configure it and when you are finished, "
120               "you run it.\n\nThere is a typo in it. Find and correct it.")
121
122        text_snippet = "Hello"  # This text wont show
123
124        box = textbox(
125            msg, title, text_snippet, False, callback=self.check_answer, run=False)
126
127        box.text = (
128            "It was the west of times, and it was the worst of times. "
129            "The  rich ate cake, and the poor had cake recommended to them, "
130            "but wished only for enough cash to buy bread."
131            "The time was ripe for revolution! ")
132
133        box.run()
134
135    def check_answer(self, box):
136        """ Callback from TextBox
137
138        Parameters
139        ----------
140        box: object
141            object containing parameters and methods to communicate with the ui
142
143        Returns
144        -------
145        nothing:
146            its return is through the box object
147        """
148        if self.finished:
149            box.stop()
150
151        if "best" in box.text:
152            box.msg = u"\n\nYou did right! Press OK button to continue."
153            self.finished = True
154        else:
155            box.msg = u"\n\nLook to the west!"
156
157
158def textbox(msg="", title=" ", text="",
159            codebox=False, callback=None, run=True):
160    """ Display a message and a text to edit
161
162    Parameters
163    ----------
164    msg : string
165        text displayed in the message area (instructions...)
166    title : str
167        the window title
168    text: str, list or tuple
169        text displayed in textAreas (editable)
170    codebox: bool
171        if True, don't wrap and width is set to 80 chars
172    callback: function
173        if set, this function will be called when OK is pressed
174    run: bool
175        if True, a box object will be created and returned, but not run
176
177    Returns
178    -------
179    None
180        If cancel is pressed
181    str
182        If OK is pressed returns the contents of textArea
183
184    """
185
186    tb = TextBox(msg=msg, title=title, text=text,
187                 codebox=codebox, callback=callback)
188    if not run:
189        return tb
190    else:
191        reply = tb.run()
192        return reply
193
194
195class TextBox(object):
196
197    """ Display a message and a text to edit
198
199    This object separates user from ui, defines which methods can
200    the user invoke and which properties can he change.
201
202    It also calls the ui in defined ways, so if other gui
203    library can be used (wx, qt) without breaking anything for the user.
204    """
205
206    def __init__(self, msg, title, text, codebox, callback=lambda *args, **kwargs: True):
207        """ Create box object
208
209        Parameters
210        ----------
211        msg : string
212            text displayed in the message area (instructions...)
213        title : str
214            the window title
215        text: str, list or tuple
216            text displayed in textAres (editable)
217        codebox: bool
218            if True, don't wrap and width is set to 80 chars
219        callback: function
220            if set, this function will be called when OK is pressed
221
222        Returns
223        -------
224        object
225            The box object
226        """
227
228        self.callback = callback
229        self.ui = GUItk(msg, title, text, codebox, self.callback_ui)
230        self.text = text
231
232    def run(self):
233        """ Start the ui """
234        self.ui.run()
235        self.ui = None
236        return self._text
237
238    def stop(self):
239        """ Stop the ui """
240        self.ui.stop()
241
242    def callback_ui(self, ui, command, text):
243        """ This method is executed when ok, cancel, or x is pressed in the ui.
244        """
245        if command == 'update':  # OK was pressed
246            self._text = text
247            if self.callback:
248                # If a callback was set, call main process
249                self.callback(self)
250            else:
251                self.stop()
252        elif command == 'x':
253            self.stop()
254            self._text = None
255        elif command == 'cancel':
256            self.stop()
257            self._text = None
258
259    # methods to change properties --------------
260    @property
261    def text(self):
262        """Text in text Area"""
263        return self._text
264
265    @text.setter
266    def text(self, text):
267        self._text = self.to_string(text)
268        self.ui.set_text(self._text)
269
270    @text.deleter
271    def text(self):
272        self._text = ""
273        self.ui.set_text(self._text)
274
275    @property
276    def msg(self):
277        """Text in msg Area"""
278        return self._msg
279
280    @msg.setter
281    def msg(self, msg):
282        self._msg = self.to_string(msg)
283        self.ui.set_msg(self._msg)
284
285    @msg.deleter
286    def msg(self):
287        self._msg = ""
288        self.ui.set_msg(self._msg)
289
290    # Methods to validate what will be sent to ui ---------
291
292    def to_string(self, something):
293        try:
294            basestring  # python 2
295        except NameError:
296            basestring = str  # Python 3
297
298        if isinstance(something, basestring):
299            return something
300        try:
301            text = "".join(something)  # convert a list or a tuple to a string
302        except:
303            textbox(
304                "Exception when trying to convert {} to text in self.textArea"
305                .format(type(something)))
306            sys.exit(16)
307        return text
308
309
310class GUItk(object):
311
312    """ This is the object that contains the tk root object"""
313
314    def __init__(self, msg, title, text, codebox, callback):
315        """ Create ui object
316
317        Parameters
318        ----------
319        msg : string
320            text displayed in the message area (instructions...)
321        title : str
322            the window title
323        text: str, list or tuple
324            text displayed in textAres (editable)
325        codebox: bool
326            if True, don't wrap, and width is set to 80 chars
327        callback: function
328            if set, this function will be called when OK is pressed
329
330        Returns
331        -------
332        object
333            The ui object
334        """
335
336        self.callback = callback
337
338        self.boxRoot = tk.Tk()
339        # self.boxFont = tk_Font.Font(
340        #     family=global_state.PROPORTIONAL_FONT_FAMILY,
341        #     size=global_state.PROPORTIONAL_FONT_SIZE)
342
343        wrap_text = not codebox
344        if wrap_text:
345            self.boxFont = tk_Font.nametofont("TkTextFont")
346            self.width_in_chars = global_state.prop_font_line_length
347        else:
348            self.boxFont = tk_Font.nametofont("TkFixedFont")
349            self.width_in_chars = global_state.fixw_font_line_length
350
351        # default_font.configure(size=global_state.PROPORTIONAL_FONT_SIZE)
352
353        self.configure_root(title)
354
355        self.create_msg_widget(msg)
356
357        self.create_text_area(wrap_text)
358
359        self.create_buttons_frame()
360
361        self.create_cancel_button()
362
363        self.create_ok_button()
364
365    # Run and stop methods ---------------------------------------
366
367    def run(self):
368        self.boxRoot.mainloop()
369        self.boxRoot.destroy()
370
371    def stop(self):
372        # Get the current position before quitting
373        self.get_pos()
374        self.boxRoot.quit()
375
376    # Methods to change content ---------------------------------------
377
378    def set_msg(self, msg):
379        self.messageArea.config(state=tk.NORMAL)
380        self.messageArea.delete(1.0, tk.END)
381        self.messageArea.insert(tk.END, msg)
382        self.messageArea.config(state=tk.DISABLED)
383        # Adjust msg height
384        self.messageArea.update()
385        numlines = self.get_num_lines(self.messageArea)
386        self.set_msg_height(numlines)
387        self.messageArea.update()
388
389    def set_msg_height(self, numlines):
390        self.messageArea.configure(height=numlines)
391
392    def get_num_lines(self, widget):
393        end_position = widget.index(tk.END)  # '4.0'
394        end_line = end_position.split('.')[0]  # 4
395        return int(end_line) + 1  # 5
396
397    def set_text(self, text):
398        self.textArea.delete(1.0, tk.END)
399        self.textArea.insert(tk.END, text, "normal")
400        self.textArea.focus()
401
402    def set_pos(self, pos):
403        self.boxRoot.geometry(pos)
404
405    def get_pos(self):
406        # The geometry() method sets a size for the window and positions it on
407        # the screen. The first two parameters are width and height of
408        # the window. The last two parameters are x and y screen coordinates.
409        # geometry("250x150+300+300")
410        geom = self.boxRoot.geometry()  # "628x672+300+200"
411        global_state.window_position = '+' + geom.split('+', 1)[1]
412
413    def get_text(self):
414        return self.textArea.get(0.0, 'end-1c')
415
416    # Methods executing when a key is pressed -------------------------------
417    def x_pressed(self):
418        self.callback(self, command='x', text=self.get_text())
419
420    def cancel_pressed(self, event):
421        self.callback(self, command='cancel', text=self.get_text())
422
423    def ok_button_pressed(self, event):
424        self.callback(self, command='update', text=self.get_text())
425
426    # Auxiliary methods -----------------------------------------------
427    def calc_character_width(self):
428        char_width = self.boxFont.measure('W')
429        return char_width
430
431    # Initial configuration methods ---------------------------------------
432    # These ones are just called once, at setting.
433
434    def configure_root(self, title):
435
436        self.boxRoot.title(title)
437
438        self.set_pos(global_state.window_position)
439
440        # Quit when x button pressed
441        self.boxRoot.protocol('WM_DELETE_WINDOW', self.x_pressed)
442        self.boxRoot.bind("<Escape>", self.cancel_pressed)
443
444        self.boxRoot.iconname('Dialog')
445
446    def create_msg_widget(self, msg):
447
448        if msg is None:
449            msg = ""
450
451        self.msgFrame = tk.Frame(
452            self.boxRoot,
453            padx=2 * self.calc_character_width(),
454
455        )
456        self.messageArea = tk.Text(
457            self.msgFrame,
458            width=self.width_in_chars,
459            state=tk.DISABLED,
460            padx=(global_state.default_hpad_in_chars) *
461            self.calc_character_width(),
462            pady=global_state.default_hpad_in_chars *
463            self.calc_character_width(),
464            wrap=tk.WORD,
465
466        )
467        self.set_msg(msg)
468
469        self.msgFrame.pack(side=tk.TOP, expand=1, fill='both')
470
471        self.messageArea.pack(side=tk.TOP, expand=1, fill='both')
472
473    def create_text_area(self, wrap_text):
474        """
475        Put a textArea in the top frame
476        Put and configure scrollbars
477        """
478
479        self.textFrame = tk.Frame(
480            self.boxRoot,
481            padx=2 * self.calc_character_width(),
482        )
483
484        self.textFrame.pack(side=tk.TOP)
485        # self.textFrame.grid(row=1, column=0, sticky=tk.EW)
486
487        self.textArea = tk.Text(
488            self.textFrame,
489            padx=global_state.default_hpad_in_chars *
490            self.calc_character_width(),
491            pady=global_state.default_hpad_in_chars *
492            self.calc_character_width(),
493            height=25,  # lines
494            width=self.width_in_chars,   # chars of the current font
495        )
496
497        if wrap_text:
498            self.textArea.configure(wrap=tk.WORD)
499        else:
500            self.textArea.configure(wrap=tk.NONE)
501
502        # some simple keybindings for scrolling
503        self.boxRoot.bind("<Next>", self.textArea.yview_scroll(1, tk.PAGES))
504        self.boxRoot.bind(
505            "<Prior>", self.textArea.yview_scroll(-1, tk.PAGES))
506
507        self.boxRoot.bind("<Right>", self.textArea.xview_scroll(1, tk.PAGES))
508        self.boxRoot.bind("<Left>", self.textArea.xview_scroll(-1, tk.PAGES))
509
510        self.boxRoot.bind("<Down>", self.textArea.yview_scroll(1, tk.UNITS))
511        self.boxRoot.bind("<Up>", self.textArea.yview_scroll(-1, tk.UNITS))
512
513        # add a vertical scrollbar to the frame
514        rightScrollbar = tk.Scrollbar(
515            self.textFrame, orient=tk.VERTICAL, command=self.textArea.yview)
516        self.textArea.configure(yscrollcommand=rightScrollbar.set)
517
518        # add a horizontal scrollbar to the frame
519        bottomScrollbar = tk.Scrollbar(
520            self.textFrame, orient=tk.HORIZONTAL, command=self.textArea.xview)
521        self.textArea.configure(xscrollcommand=bottomScrollbar.set)
522
523        # pack the textArea and the scrollbars.  Note that although
524        # we must define the textArea first, we must pack it last,
525        # so that the bottomScrollbar will be located properly.
526
527        # Note that we need a bottom scrollbar only for code.
528        # Text will be displayed with wordwrap, so we don't need to have
529        # a horizontal scroll for it.
530
531        if not wrap_text:
532            bottomScrollbar.pack(side=tk.BOTTOM, fill=tk.X)
533        rightScrollbar.pack(side=tk.RIGHT, fill=tk.Y)
534
535        self.textArea.pack(side=tk.LEFT, fill=tk.BOTH, expand=tk.YES)
536
537    def create_buttons_frame(self):
538
539        self.buttonsFrame = tk.Frame(self.boxRoot,
540                                     # background="green",
541
542                                     )
543        self.buttonsFrame.pack(side=tk.TOP)
544
545    def create_cancel_button(self):
546        # put the buttons in the buttonsFrame
547        self.cancelButton = tk.Button(
548            self.buttonsFrame, takefocus=tk.YES, text="Cancel",
549            height=1, width=6)
550        self.cancelButton.pack(
551            expand=tk.NO, side=tk.LEFT, padx='2m', pady='1m', ipady="1m",
552            ipadx="2m")
553
554        # for the commandButton, bind activation events to the activation event
555        # handler
556        self.cancelButton.bind("<Return>", self.cancel_pressed)
557        self.cancelButton.bind("<Button-1>", self.cancel_pressed)
558        self.cancelButton.bind("<Escape>", self.cancel_pressed)
559
560    def create_ok_button(self):
561        # put the buttons in the buttonsFrame
562        self.okButton = tk.Button(
563            self.buttonsFrame, takefocus=tk.YES, text="OK", height=1, width=6)
564        self.okButton.pack(
565            expand=tk.NO, side=tk.LEFT, padx='2m', pady='1m', ipady="1m",
566            ipadx="2m")
567
568        # for the commandButton, bind activation events to the activation event
569        # handler
570        self.okButton.bind("<Return>", self.ok_button_pressed)
571        self.okButton.bind("<Button-1>", self.ok_button_pressed)
572
573
574if __name__ == '__main__':
575    demo_textbox()
576