1# --------------------------------------------------------------------------- #
2# TOASTERBOX wxPython IMPLEMENTATION
3# Ported And Enhanced From wxWidgets Contribution (Aj Bommarito) By:
4#
5# Andrea Gavana, @ 16 September 2005
6# Latest Revision: 27 Dec 2012, 21.00 GMT
7#
8#
9# TODO/Caveats List
10#
11# 1. Any Idea?
12#
13#
14# For All Kind Of Problems, Requests Of Enhancements And Bug Reports, Please
15# Write To Me At:
16#
17# andrea.gavana@gmail.com
18# andrea.gavana@maerskoil.com
19#
20# Or, Obviously, To The wxPython Mailing List!!!
21#
22# Tags:         phoenix-port, documented, unittest, py3-port
23#
24# End Of Comments
25# --------------------------------------------------------------------------- #
26
27
28"""
29:class:`~wx.lib.agw.toasterbox.ToasterBox` is a cross-platform widget to make the creation of MSN style "toaster"
30popups easier.
31
32
33Description
34===========
35
36ToasterBox is a cross-platform widget to make the creation of MSN style "toaster"
37popups easier. The syntax is really easy especially if you are familiar with the
38syntax of wxPython.
39
40It has 2 main styles:
41
42- ``TB_SIMPLE``: using this style, you will be able to specify a background image for
43  ToasterBox, text properties as text colour, font and label;
44
45- ``TB_COMPLEX``: this style will allow you to put almost any control inside a
46  ToasterBox. You can add a panel in which you can put all the controls you like.
47
48Both styles support the setting of ToasterBox position (on screen coordinates),
49size, the time after which the ToasterBox is destroyed (linger), and the scroll
50speed of ToasterBox.
51
52
53Usage
54=====
55
56Usage example::
57
58    import wx
59    import wx.lib.agw.toasterbox as TB
60
61    class MyFrame(wx.Frame):
62
63        def __init__(self, parent):
64
65            wx.Frame.__init__(self, parent, -1, "ToasterBox Demo")
66
67            toaster = TB.ToasterBox(self, tbstyle=TB.TB_COMPLEX)
68            toaster.SetPopupPauseTime(3000)
69
70            tbpanel = toaster.GetToasterBoxWindow()
71            panel = wx.Panel(tbpanel, -1)
72            sizer = wx.BoxSizer(wx.VERTICAL)
73
74            button = wx.Button(panel, wx.ID_ANY, "Simple button")
75            sizer.Add(button, 0, wx.EXPAND)
76
77            panel.SetSizer(sizer)
78            toaster.AddPanel(panel)
79
80            wx.CallLater(1000, toaster.Play)
81
82
83    # our normal wxApp-derived class, as usual
84
85    app = wx.App(0)
86
87    frame = MyFrame(None)
88    app.SetTopWindow(frame)
89    frame.Show()
90
91    app.MainLoop()
92
93
94
95Supported Platforms
96===================
97
98ToasterBox has been tested on the following platforms:
99
100- Windows (verified on Windows XP, 2000)
101- Linux
102- Mac
103
104
105Window Styles
106=============
107
108This class supports the following window styles:
109
110==================== =========== ==================================================
111Window Styles        Hex Value   Description
112==================== =========== ==================================================
113``TB_SIMPLE``                0x1 A simple `ToasterBox`, with background image and text customization can be created.
114``TB_ONTIME``                0x1 `ToasterBox` will close after a specified amount of time.
115``TB_COMPLEX``               0x2 ToasterBoxes with different degree of complexity can be created. You can add as  many controls as you want, provided that you call the :meth:`~ToasterBox.AddPanel` method and pass  to it a dummy frame and a :class:`Panel`. See the demo for details.
116``TB_ONCLICK``               0x2 `ToasterBox` can be closed by clicking anywhere on the `ToasterBox` frame.
117``TB_DEFAULT_STYLE``   0x2008002 Default window style for `ToasterBox`, with no caption nor close box.
118``TB_CAPTION``        0x22009806 `ToasterBox` will have a caption, with the possibility to set a title for the `ToasterBox` frame, and a close box.
119==================== =========== ==================================================
120
121
122Events Processing
123=================
124
125`No custom events are available for this class.`
126
127
128License And Version
129===================
130
131ToasterBox is distributed under the wxPython license.
132
133Latest revision: Andrea Gavana @ 27 Dec 2012, 21.00 GMT
134
135Version 0.3
136
137"""
138
139import textwrap
140import time
141import wx
142
143
144# Define Window List, We Use It Globally
145winlist = []
146""" Globally defined window list. """
147
148TB_SIMPLE = 1
149""" A simple ToasterBox, with background image and text customization can be created. """
150TB_COMPLEX = 2
151""" ToasterBoxes with different degree of complexity can be created. You can add as  many controls as you want, provided that you call the AddPanel() method and pass to it a dummy frame and a wx.Panel. See the demo for details. """
152TB_DEFAULT_STYLE = wx.SIMPLE_BORDER | wx.STAY_ON_TOP | wx.FRAME_NO_TASKBAR
153""" Default window style for `ToasterBox`, with no caption nor close box. """
154TB_CAPTION = TB_DEFAULT_STYLE | wx.CAPTION | wx.SYSTEM_MENU | wx.CLOSE_BOX | wx.FRAME_NO_TASKBAR
155""" `ToasterBox` will have a caption, with the possibility to set a title for the `ToasterBox` frame, and a close box. """
156TB_ONTIME = 1
157""" `ToasterBox` will close after a specified amount of time. """
158TB_ONCLICK = 2
159""" `ToasterBox` can be closed by clicking anywhere on the `ToasterBox` frame. """
160
161# scroll from up to down
162TB_SCR_TYPE_UD = 1
163""" Scroll from up to down. """
164# scroll from down to up
165TB_SCR_TYPE_DU = 2
166""" Scroll from down to up. """
167# fade in/out
168TB_SCR_TYPE_FADE = 4
169""" Fade in and out. """
170
171
172# ------------------------------------------------------------------------------ #
173# Class ToasterBox
174#    Main Class Implementation. It Is Basically A wx.Timer. It Creates And
175#    Displays Popups And Handles The "Stacking".
176# ------------------------------------------------------------------------------ #
177
178class ToasterBox(wx.Timer):
179    """
180    ToasterBox is a cross-platform widget to make the creation of MSN style "toaster"
181    popups easier.
182    """
183
184    def __init__(self, parent, tbstyle=TB_SIMPLE, windowstyle=TB_DEFAULT_STYLE,
185                 closingstyle=TB_ONTIME, scrollType=TB_SCR_TYPE_DU):
186        """
187        Default class constructor.
188
189        :param `parent`: the window parent;
190        :param `tbstyle`: the :class:`ToasterBox` main style. Can be one of the following
191         bits:
192
193         ====================== ======= ================================
194         `ToasterBox` Style      Value  Description
195         ====================== ======= ================================
196         ``TB_SIMPLE``              0x1 A simple :class:`ToasterBox`, with background image and text customization can be created
197         ``TB_COMPLEX``             0x2 `ToasterBoxes` with different degree of complexity can be created. You can add as many controls as you want, provided that you call the :meth:`~ToasterBox.AddPanel` method and pass to it a dummy frame and a :class:`Panel`.
198         ====================== ======= ================================
199
200        :param `windowstyle`: this parameter influences the visual appearance of
201         :class:`ToasterBox`, and can be one of the following styles:
202
203         ====================== ========== ================================
204         Window Style           Hex Value  Description
205         ====================== ========== ================================
206         ``TB_DEFAULT_STYLE``   0x2008002  Default window style for :class:`ToasterBox`, with no caption nor close box.
207         ``TB_CAPTION``         0x22009806 :class:`ToasterBox` will have a caption, with the possibility to set a title for the :class:`ToasterBox` frame, and a close box.
208         ====================== ========== ================================
209
210        :param `closingstyle`: the closing style for :class:`ToasterBox`. Can be one of the
211         following bits:
212
213         ==================== =========== ==================================================
214         Closing Styles       Hex Value   Description
215         ==================== =========== ==================================================
216         ``TB_ONTIME``                0x1 :class:`ToasterBox` will close after a specified amount of time.
217         ``TB_ONCLICK``               0x2 :class:`ToasterBox` can be closed by clicking anywhere on the :class:`ToasterBox` frame.
218         ==================== =========== ==================================================
219
220        :param `scrollType`: the scrolling direction for :class:`ToasterBox`. Can be one of the
221         following bits:
222
223         ==================== =========== ==================================================
224         Scroll Styles        Hex Value   Description
225         ==================== =========== ==================================================
226         ``TB_SCR_TYPE_UD``           0x1 :class:`ToasterBox` will scroll from up to down
227         ``TB_SCR_TYPE_DU``           0x2 :class:`ToasterBox` will scroll from down to up
228         ``TB_SCR_TYPE_FADE``         0x4 :class:`ToasterBox` will fade in/out (without scrolling).
229         ==================== =========== ==================================================
230
231        """
232
233        self._parent = parent
234        self._sleeptime = 10
235        self._pausetime = 1700
236        self._popuptext = "default"
237        self._popupposition = wx.Point(100,100)
238        self._popuptop = wx.Point(0,0)
239        self._popupsize = wx.Size(150, 170)
240        self._usefocus = True
241        self._originalfocus = wx.Window.FindFocus()
242
243        self._backgroundcolour = wx.WHITE
244        self._foregroundcolour = wx.BLACK
245        self._textfont = wx.Font(8, wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, "Verdana")
246
247        self._bitmap = None
248
249        self._tbstyle = tbstyle
250        self._windowstyle = windowstyle
251        self._closingstyle = closingstyle
252        self._scrollType = scrollType
253
254        self._panel = None
255
256        self._bottomright = wx.Point(wx.GetDisplaySize().GetWidth(),
257                                     wx.GetDisplaySize().GetHeight())
258
259        if parent is not None:
260            parent.Bind(wx.EVT_ICONIZE, lambda evt: [w.Hide() for w in winlist])
261            self._moveTimer = wx.Timer(parent, -1)
262            parent.Bind(wx.EVT_TIMER, self.OnMoveTimer, self._moveTimer)
263
264        self._tb = ToasterBoxWindow(self._parent, self, self._tbstyle, self._windowstyle,
265                                    self._closingstyle, scrollType=self._scrollType)
266
267    def SetPopupPosition(self, pos):
268        """
269        Sets the :class:`ToasterBox` position on screen.
270
271        :param `pos`: the widget position, an instance of :class:`wx.Point`.
272        """
273
274        self._popupposition = pos
275
276
277    def SetPopupPositionByInt(self, pos):
278        """
279        Sets the :class:`ToasterBox` position on screen, at one of the screen corners.
280
281        :param `pos`: an integer specifying the screen corner, namely:
282
283         ============= ========================================
284         Corner Number Position
285         ============= ========================================
286               0       Top left screen corner
287               1       Top right screen corner
288               2       Bottom left screen corner
289               3       Bottom right screen corner
290         ============= ========================================
291
292        """
293
294        w, h = wx.GetDisplaySize()
295        self._bottomright = wx.Point(w, h)
296
297        # top left
298        if pos == 0:
299            popupposition = wx.Point(0,0)
300        # top right
301        elif pos == 1:
302            popupposition = wx.Point(w - self._popupsize[0], 0)
303        # bottom left
304        elif pos == 2:
305            popupposition = wx.Point(0, h - self._popupsize[1])
306        # bottom right
307        elif pos == 3:
308            popupposition = wx.Point(self._bottomright.x - self._popupsize[0],
309                                     self._bottomright.y - self._popupsize[1])
310
311        self._bottomright = wx.Point(popupposition.x + self._popupsize[0],
312                                     popupposition.y + self._popupsize[1])
313
314        self._popupposition = popupposition
315
316
317    def CenterOnParent(self, direction=wx.BOTH):
318        """
319        Centres the window on its parent (if any). If the :class:`ToasterBox` parent is ``None``,
320        it calls :meth:`~ToasterBox.CenterOnScreen`.
321
322        :param `direction`: specifies the direction for the centering. May be ``wx.HORIZONTAL``,
323         ``wx.VERTICAL`` or ``wx.BOTH``.
324
325        :note: This methods provides for a way to center :class:`ToasterBox` over their parents instead of the
326         entire screen. If there is no parent, then behaviour is the same as :meth:`~ToasterBox.CenterOnScreen`.
327
328        :see: :meth:`~ToasterBox.CenterOnScreen`.
329        """
330
331        if not self._parent:
332            self.CenterOnScreen(direction)
333            return
334
335        parent = self._parent
336        screenrect = parent.GetScreenRect()
337        toast_width, toast_height = self._popupsize
338        x, y = screenrect.GetX(), screenrect.GetY()
339        width, height = screenrect.GetWidth(), screenrect.GetHeight()
340
341        if direction == wx.VERTICAL:
342            pos = wx.Point(x, (y + (height/2) - (toast_height/2)))
343        elif direction == wx.HORIZONTAL:
344            pos = wx.Point((x + (width/2) - (toast_width/2)), y)
345        else:
346            pos = wx.Point((x + (width/2) - (toast_width/2)), (y + (height/2) - (toast_height/2)))
347
348        self.SetPopupPosition(pos)
349
350
351    CentreOnParent = CenterOnParent
352
353
354    def CenterOnScreen(self, direction=wx.BOTH):
355        """
356        Centres the :class:`ToasterBox` on screen.
357
358        :param `direction`: specifies the direction for the centering. May be ``wx.HORIZONTAL``,
359         ``wx.VERTICAL`` or ``wx.BOTH``.
360
361        :see: :meth:`~ToasterBox.CenterOnParent`.
362        """
363
364        screenSize = wx.GetDisplaySize()
365        toast_width, toast_height = self._popupsize
366        width, height = screenSize.GetWidth(), screenSize.GetHeight()
367
368        if direction == wx.VERTICAL:
369            pos = wx.Point(0, (height/2) - (toast_height/2))
370        elif direction == wx.HORIZONTAL:
371            pos = wx.Point((width/2) - (toast_width/2), 0)
372        else:
373            pos = wx.Point((width/2) - (toast_width/2), (height/2) - (toast_height/2))
374
375        self.SetPopupPosition(pos)
376
377
378    CentreOnScreen = CenterOnScreen
379
380
381    def SetPopupBackgroundColour(self, colour=None):
382        """
383        Sets the :class:`ToasterBox` background colour.
384
385        :param `colour`: a valid :class:`wx.Colour` object. If defaulted to ``None``, then
386         the background colour will be white.
387
388        :note: Use this method only for a :class:`ToasterBox` created with the ``TB_SIMPLE`` style.
389        """
390
391        if colour is None:
392            colour = wx.WHITE
393
394        colour = wx.Colour(colour)
395        self._backgroundcolour = colour
396        self._tb.SetPopupBackgroundColour(self._backgroundcolour)
397
398
399    def SetPopupTextColour(self, colour=None):
400        """
401        Sets the :class:`ToasterBox` foreground colour.
402
403        :param `colour`: a valid :class:`wx.Colour` object. If defaulted to ``None``, then
404         the background colour will be black.
405
406        :note: Use this method only for a :class:`ToasterBox` created with the ``TB_SIMPLE`` style.
407        """
408
409        if colour is None:
410            colour = wx.BLACK
411
412        colour = wx.Colour(colour)
413        self._foregroundcolour = colour
414
415
416    def SetPopupTextFont(self, font=None):
417        """
418        Sets the :class:`ToasterBox` text font.
419
420        :param `colour`: a valid :class:`wx.Colour` object. If defaulted to ``None``, then
421         a simple generic font will be generated.
422
423        :note: Use this method only for a :class:`ToasterBox` created with the ``TB_SIMPLE`` style.
424        """
425
426        if font is None:
427            font = wx.Font(8, wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False)
428
429        self._textfont = font
430
431
432    def SetPopupSize(self, size):
433        """
434        Sets the :class:`ToasterBox` size.
435
436        :param `size`: the new control size, an instance of :class:`wx.Size`.
437        """
438
439        self._popupsize = size
440
441
442    def SetPopupPauseTime(self, pausetime):
443        """
444        Sets the time after which the :class:`ToasterBox` is destroyed (linger).
445
446        :param `pausetime`: the delay after which the control is destroyed, in seconds.
447        """
448
449        self._pausetime = pausetime
450
451
452    def SetPopupBitmap(self, bitmap=None):
453        """
454        Sets the :class:`ToasterBox` background image.
455
456        :param `bitmap`: a valid :class:`wx.Bitmap` object or filename. If defaulted
457         to ``None``, then no background bitmap is used.
458
459        :note: Use this method only for a :class:`ToasterBox` created with the ``TB_SIMPLE`` style.
460        """
461
462        if bitmap is not None:
463            bitmap = wx.Bitmap(bitmap)
464
465        self._bitmap = bitmap
466
467
468    def SetPopupScrollSpeed(self, speed):
469        """
470        Sets the :class:`ToasterBox` scroll speed.
471
472        :param `speed`: it is the pause time (in milliseconds) for every step in the
473         `ScrollUp` method.
474        """
475
476        self._sleeptime = speed
477
478
479    def SetPopupText(self, text):
480        """
481        Sets the :class:`ToasterBox` text label.
482
483        :param `text`: the widget label.
484
485        :note: Use this method only for a :class:`ToasterBox` created with the ``TB_SIMPLE`` style.
486        """
487
488        self._popuptext = text
489
490
491    def AddPanel(self, panel):
492        """
493        Adds a panel to the :class:`ToasterBox`.
494
495        :param `panel`: an instance of :class:`wx.Window`.
496
497        :note: Use this method only for a :class:`ToasterBox` created with the ``TB_COMPLEX`` style.
498        """
499
500        if not self._tbstyle & TB_COMPLEX:
501            raise Exception("\nERROR: Panel Can Not Be Added When Using TB_SIMPLE ToasterBox Style")
502
503        self._panel = panel
504
505
506    def Play(self):
507        """ Creates the :class:`ToasterBoxWindow`, that does all the job. """
508
509        # create new window
510        self._tb.SetPopupSize((self._popupsize[0], self._popupsize[1]))
511        self._tb.SetPopupPosition((self._popupposition[0], self._popupposition[1]))
512        self._tb.SetPopupPauseTime(self._pausetime)
513        self._tb.SetPopupScrollSpeed(self._sleeptime)
514        self._tb.SetUseFocus(self._usefocus, self._originalfocus)
515
516        if self._tbstyle == TB_SIMPLE:
517            self._tb.SetPopupTextColour(self._foregroundcolour)
518            self._tb.SetPopupBackgroundColour(self._backgroundcolour)
519            self._tb.SetPopupTextFont(self._textfont)
520
521            if self._bitmap is not None:
522                self._tb.SetPopupBitmap(self._bitmap)
523
524            self._tb.SetPopupText(self._popuptext)
525
526        if self._tbstyle == TB_COMPLEX:
527            if self._panel is not None:
528                self._tb.AddPanel(self._panel)
529
530        # clean up the list
531        self.CleanList()
532
533        # check to see if there is already a window displayed
534        # by looking at the linked list
535        if len(winlist) > 0:
536            # there ARE other windows displayed already
537            # reclac where it should display
538            self.MoveAbove(self._tb)
539
540        # shift new window on to the list
541        winlist.append(self._tb)
542
543        if not self._tb.Play():
544            # if we didn't show the window properly, remove it from the list
545            winlist.remove(winlist[-1])
546            # delete the object too
547            self._tb.Destroy()
548            return
549
550
551    def MoveAbove(self, tb):
552        """
553        If a :class:`ToasterBox` already exists, move the new one above the existing one.
554
555        :param `tb`: another instance of :class:`ToasterBox`.
556        """
557
558        # recalc where to place this popup
559
560        self._tb.SetPopupPosition((self._popupposition[0], self._popupposition[1] -
561                                   self._popupsize[1]*len(winlist)))
562
563
564    def GetToasterBoxWindow(self):
565        """ Returns the :class:`ToasterBox` frame. """
566
567        return self._tb
568
569
570    def SetTitle(self, title):
571        """
572        Sets the :class:`ToasterBox` title if it was created with ``TB_CAPTION`` window style.
573
574        :param `title`: the :class:`ToasterBox` caption title.
575        """
576
577        self._tb.SetTitle(title)
578
579
580    def SetUseFocus(self, focus):
581        """
582        If `focus` is ``True``, Instructs :class:`ToasterBox` to steal the focus from the
583        parent application, otherwise it returns the focus to the original owner.
584
585        :param `focus`: ``True`` to set the focus on :class:`ToasterBox`, ``False`` to
586         return it to the original owner.
587        """
588
589        self._usefocus = focus
590
591
592    def GetUseFocus(self):
593        """ Returns whether :class:`ToasterBox` will steal the focus from the parent application. """
594
595        return self._usefocus
596
597
598    def Notify(self):
599        """ It's time to hide a :class:`ToasterBox`. """
600
601        if len(winlist) == 0:
602            return
603
604        # clean the window list
605        self.CleanList()
606
607        # figure out how many blanks we have
608        try:
609            node = winlist[0]
610        except:
611            return
612
613        if not node:
614            return
615
616        self._startPos = node.GetPosition()[1]
617        self._moveTimer.Start(self._sleeptime)
618
619
620    def OnMoveTimer(self, event):
621        """
622        Handles the ``wx.EVT_TIMER`` event for :class:`ToasterBox`, moving the new window
623        on top of the last one created.
624
625        :param `event`: a :class:`TimerEvent` event to be processed.
626        """
627
628        current = self._startPos
629        if current >= self._popupposition[1]:
630            self._moveTimer.Stop()
631
632        # move windows to fill in blank space
633
634        if current > self._popupposition[1]:
635            current = self._popupposition[1]
636
637        # loop through all the windows
638        for j in range(0, len(winlist)):
639            ourNewHeight = current - (j*self._popupsize[1] - 8)
640            tmpTb = winlist[j]
641            # reset where the object THINKS its supposed to be
642            tmpTb.SetPopupPosition((self._popupposition[0], ourNewHeight))
643            # actually move it
644            tmpTb.SetSize(self._popupposition[0], ourNewHeight, tmpTb.GetSize().GetWidth(),
645                                tmpTb.GetSize().GetHeight())
646
647        self._startPos += 4
648
649
650    def CleanList(self):
651        """ Cleans the window list, erasing the stack of :class:`ToasterBox` objects. """
652
653        if len(winlist) == 0:
654            return
655
656        node = winlist[0]
657        while node:
658            if not node.IsShown():
659                winlist.remove(node)
660                node.Close()
661                try:
662                    node = winlist[0]
663                except:
664                    node = 0
665            else:
666                indx = winlist.index(node)
667                try:
668                    node = winlist[indx+1]
669                except:
670                    node = 0
671
672
673# ------------------------------------------------------------------------------ #
674# Class ToasterBoxWindow
675#    This Class Does All The Job, By Handling Background Images, Text Properties
676#    And Panel Adding. Depending On The Style You Choose, ToasterBoxWindow Will
677#    Behave Differently In Order To Handle Widgets Inside It.
678# ------------------------------------------------------------------------------ #
679
680class ToasterBoxWindow(wx.Frame):
681    """
682    This class does all the job, by handling background images, text properties
683    and panel adding. Depending on the style you choose, :class:`ToasterBoxWindow` will
684    behave differently in order to handle widgets inside it.
685    """
686
687    def __init__(self, parent, parent2, tbstyle, windowstyle, closingstyle,
688                 scrollType=TB_SCR_TYPE_DU):
689        """
690        Default class constructor.
691        Used internally. Do not call directly this class in your application!
692
693        :param `parent`: the window parent;
694        :param `parent2`: the :class:`ToasterBox` calling this window;
695        :param `tbstyle`: the :class:`ToasterBoxWindow` main style. Can be one of the following
696         bits:
697
698         ====================== ======= ================================
699         `ToasterBox` Style      Value  Description
700         ====================== ======= ================================
701         ``TB_SIMPLE``              0x1 A simple :class:`ToasterBox`, with background image and text customization can be created
702         ``TB_COMPLEX``             0x2 `ToasterBoxes` with different degree of complexity can be created. You can add as many controls as you want, provided that you call the :meth:`~ToasterBoxWindow.AddPanel` method and pass to it a dummy frame and a :class:`Panel`.
703         ====================== ======= ================================
704
705        :param `windowstyle`: this parameter influences the visual appearance of
706         :class:`ToasterBoxWindow`, and can be one of the following styles:
707
708         ====================== ========== ================================
709         Window Style           Hex Value  Description
710         ====================== ========== ================================
711         ``TB_DEFAULT_STYLE``   0x2008002  Default window style for :class:`ToasterBox`, with no caption nor close box.
712         ``TB_CAPTION``         0x22009806 :class:`ToasterBox` will have a caption, with the possibility to set a title for the :class:`ToasterBox` frame, and a close box.
713         ====================== ========== ================================
714
715        :param `closingstyle`: the closing style for :class:`ToasterBoxWindow`. Can be one of the
716         following bits:
717
718         ==================== =========== ==================================================
719         Closing Styles       Hex Value   Description
720         ==================== =========== ==================================================
721         ``TB_ONTIME``                0x1 :class:`ToasterBox` will close after a specified amount of time.
722         ``TB_ONCLICK``               0x2 :class:`ToasterBox` can be closed by clicking anywhere on the :class:`ToasterBox` frame.
723         ==================== =========== ==================================================
724
725        :param `scrollType`: the scrolling direction for :class:`ToasterBoxWindow`. Can be one of the
726         following bits:
727
728         ==================== =========== ==================================================
729         Scroll Styles        Hex Value   Description
730         ==================== =========== ==================================================
731         ``TB_SCR_TYPE_UD``           0x1 :class:`ToasterBox` will scroll from up to down
732         ``TB_SCR_TYPE_DU``           0x2 :class:`ToasterBox` will scroll from down to up
733         ``TB_SCR_TYPE_FADE``         0x4 :class:`ToasterBox` will fade in/out (without scrolling).
734         ==================== =========== ==================================================
735
736        """
737
738        wx.Frame.__init__(self, parent, wx.ID_ANY, "window", wx.DefaultPosition,
739                         wx.DefaultSize, style=windowstyle | wx.CLIP_CHILDREN)
740
741        self._starttime = int(time.time())
742        self._parent2 = parent2
743        self._parent = parent
744        self._sleeptime = 10
745        self._step = 4
746        self._pausetime = 1700
747        self._textcolour = wx.BLACK
748        self._popuptext = "Change Me!"
749        # the size we want the dialog to be
750        framesize = wx.Size(150, 170)
751        self._count = 1
752        self._tbstyle = tbstyle
753        self._windowstyle = windowstyle
754        self._closingstyle = closingstyle
755        self._backgroundcolour = wx.WHITE
756
757        if tbstyle == TB_COMPLEX:
758            self.sizer = wx.BoxSizer(wx.VERTICAL)
759        else:
760            self._staticbitmap = None
761
762        if self._windowstyle == TB_CAPTION:
763            self.Bind(wx.EVT_CLOSE, self.OnClose)
764            self.SetTitle("")
765
766        if scrollType == TB_SCR_TYPE_FADE and not self.CanSetTransparent():
767            import warnings
768            warnings.warn("The style ``TB_SCR_TYPE_FADE`` is not supported on this platform.")
769            scrollType = TB_SCR_TYPE_DU
770
771        self._scrollType = scrollType
772
773        if self._closingstyle & TB_ONCLICK and self._windowstyle != TB_CAPTION:
774            self.Bind(wx.EVT_LEFT_DOWN, self.OnMouseDown)
775
776        self._bottomright = wx.Point(wx.GetDisplaySize().GetWidth(),
777                                     wx.GetDisplaySize().GetHeight())
778
779        self.SetSize(self._bottomright.x, self._bottomright.y,
780                           framesize.GetWidth(), framesize.GetHeight())
781
782        self._scrollTimer = wx.Timer(self, -1)
783        self._alphaTimer = wx.Timer(self, -1)
784
785        self.Bind(wx.EVT_TIMER, self.OnScrollTimer, self._scrollTimer)
786        self.Bind(wx.EVT_TIMER, self.AlphaCycle, self._alphaTimer)
787
788        if not self._tbstyle & TB_COMPLEX:
789            self.Bind(wx.EVT_PAINT, self.OnPaint)
790            self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM)
791
792
793    def OnClose(self, event):
794        """
795        Handles the ``wx.EVT_CLOSE`` event for :class:`ToasterBoxWindow`.
796
797        :param `event`: a :class:`CloseEvent` event to be processed.
798        """
799
800        self.NotifyTimer(None)
801        event.Skip()
802
803
804    def OnMouseDown(self, event):
805        """
806        Handles the ``wx.EVT_LEFT_DOWN`` event for :class:`ToasterBoxWindow`.
807
808        :param `event`: a :class:`MouseEvent` event to be processed.
809        """
810
811        self.NotifyTimer(None)
812        event.Skip()
813
814
815    def SetPopupBitmap(self, bitmap=None):
816        """
817        Sets the :class:`ToasterBox` background image.
818
819        :param `bitmap`: a valid :class:`wx.Bitmap` object. If defaulted to ``None``, then
820         no background bitmap is used.
821
822        :note: Use this method only for a :class:`ToasterBox` created with the ``TB_SIMPLE`` style.
823        """
824
825        if bitmap is None:
826            self._staticbitmap = None
827        else:
828            bitmap = bitmap.ConvertToImage()
829            xsize, ysize = self.GetSize()
830            bitmap = bitmap.Scale(xsize, ysize)
831            self._staticbitmap = bitmap.ConvertToBitmap()
832
833
834    def SetPopupSize(self, size):
835        """
836        Sets the :class:`ToasterBox` size.
837
838        :param `size`: the new control size, an instance of :class:`wx.Size`.
839        """
840
841        self.SetSize(self._bottomright.x, self._bottomright.y, size[0], size[1])
842
843
844    def SetPopupPosition(self, pos):
845        """
846        Sets the :class:`ToasterBox` position on screen.
847
848        :param `pos`: the widget position, an instance of :class:`wx.Point`.
849        """
850
851        self._bottomright = wx.Point(pos[0] + self.GetSize().GetWidth(),
852                                     pos[1] + self.GetSize().GetHeight())
853        self._dialogtop = pos
854
855
856    def SetPopupPositionByInt(self, pos):
857        """
858        Sets the :class:`ToasterBox` position on screen, at one of the screen corners.
859
860        :param `pos`: an integer specifying the screen corner, namely:
861
862         ============= ========================================
863         Corner Number Position
864         ============= ========================================
865               0       Top left screen corner
866               1       Top right screen corner
867               2       Bottom left screen corner
868               3       Bottom right screen corner
869         ============= ========================================
870
871        """
872
873        w, h = wx.GetDisplaySize()
874        self._bottomright = wx.Point(w, h)
875
876        # top left
877        if pos == 0:
878            popupposition = wx.Point(0, 0)
879        # top right
880        elif pos == 1:
881            popupposition = wx.Point(w - self._popupsize[0], 0)
882        # bottom left
883        elif pos == 2:
884           popupposition = wx.Point(0, h - self._popupsize[1])
885         # bottom right
886        elif pos == 3:
887            popupposition = wx.Point(self._bottomright.x - self._popupsize[0],
888                                     self._bottomright.y - self._popupsize[1])
889
890        self._bottomright = wx.Point(popupposition.x + self._popupsize[0],
891                                     popupposition.y + self._popupsize[1])
892
893        self._dialogtop = popupposition
894
895
896    def SetPopupPauseTime(self, pausetime):
897        """
898        Sets the time after which the :class:`ToasterBox` is destroyed (linger).
899
900        :param `pausetime`: the delay after which the control is destroyed, in seconds.
901        """
902
903        self._pausetime = pausetime
904
905
906    def SetPopupScrollSpeed(self, speed):
907        """
908        Sets the :class:`ToasterBox` scroll speed.
909
910        :param `speed`: it is the pause time (in milliseconds) for every step in the
911         :meth:`~ToasterBoxWindow.ScrollUp` method.
912        """
913
914        self._sleeptime = speed
915
916
917    def AddPanel(self, panel):
918        """
919        Adds a panel to the :class:`ToasterBox`.
920
921        :param `panel`: an instance of :class:`wx.Window`.
922
923        :note: Use this method only for a :class:`ToasterBox` created with the ``TB_COMPLEX`` style.
924        """
925
926        if not self._tbstyle & TB_COMPLEX:
927            raise Exception("\nERROR: Panel Can Not Be Added When Using TB_SIMPLE ToasterBox Style")
928
929        self.sizer.Add(panel, 1, wx.EXPAND)
930        self.SetSizer(self.sizer)
931        self.Layout()
932
933        if self._closingstyle & TB_ONCLICK and self._windowstyle != TB_CAPTION:
934            panel.Bind(wx.EVT_LEFT_DOWN, self.OnMouseDown)
935
936
937    def SetPopupText(self, text):
938        """
939        Sets the :class:`ToasterBox` text label.
940
941        :param `text`: the widget label.
942
943        :note: Use this method only for a :class:`ToasterBox` created with the ``TB_SIMPLE`` style.
944        """
945
946        self._popuptext = text
947
948
949    def SetPopupTextFont(self, font):
950        """
951        Sets the :class:`ToasterBox` text font.
952
953        :param `colour`: a valid :class:`wx.Colour` object. If defaulted to ``None``, then
954         a simple generic font will be generated.
955
956        :note: Use this method only for a :class:`ToasterBox` created with the ``TB_SIMPLE`` style.
957        """
958
959        self._textfont = font
960
961
962    def GetPopupText(self):
963        """
964        Returns the :class:`ToasterBox` text.
965
966        :note: Use this method only for a :class:`ToasterBox` created with the ``TB_SIMPLE`` style.
967        """
968
969        return self._popuptext
970
971
972    def Play(self):
973        """ Creates the :class:`ToasterBoxWindow`, that does all the job. """
974
975        # do some checks to make sure this window is valid
976        if self._bottomright.x < 1 or self._bottomright.y < 1:
977            return False
978
979        if self.GetSize().GetWidth() < 50 or self.GetSize().GetWidth() < 50:
980            # toasterbox launches into a endless loop for some reason
981            # when you try to make the window too small.
982            return False
983
984        self._direction = wx.UP
985        self.SetupPositions()
986        self.ScrollUp()
987        timerid = wx.NewIdRef()
988        self.showtime = wx.Timer(self, timerid)
989        self.showtime.Start(self._pausetime)
990        self.Bind(wx.EVT_TIMER, self.NotifyTimer, id=timerid)
991
992        return True
993
994
995    def NotifyTimer(self, event):
996        """ Hides gradually the :class:`ToasterBoxWindow`. """
997
998        if self._scrollType != TB_SCR_TYPE_FADE:
999            self.showtime.Stop()
1000            del self.showtime
1001
1002        self._direction = wx.DOWN
1003        self.SetupPositions()
1004
1005        self.ScrollDown()
1006
1007
1008    def SetPopupBackgroundColour(self, colour):
1009        """
1010        Sets the :class:`ToasterBox` background colour.
1011
1012        :param `colour`: a valid :class:`wx.Colour` object. If defaulted to ``None``, then
1013         the background colour will be white.
1014
1015        :note: Use this method only for a :class:`ToasterBox` created with the ``TB_SIMPLE`` style.
1016        """
1017
1018        self.SetBackgroundColour(colour)
1019        self._backgroundcolour = colour
1020
1021
1022    def SetPopupTextColour(self, colour):
1023        """
1024        Sets the :class:`ToasterBox` foreground colour.
1025
1026        :param `colour`: a valid :class:`wx.Colour` object. If defaulted to ``None``, then
1027         the background colour will be black.
1028
1029        :note: Use this method only for a :class:`ToasterBox` created with the ``TB_SIMPLE`` style.
1030        """
1031
1032        self._textcolour = colour
1033
1034
1035    def SetUseFocus(self, focus, originalfocus):
1036        """
1037        If `focus` is ``True``, Instructs :class:`ToasterBoxWindow` to steal the focus from the
1038        parent application, otherwise it returns the focus to the original owner.
1039
1040        :param `focus`: ``True`` to set the focus on :class:`ToasterBoxWindow`, ``False`` to
1041         return it to the original owner;
1042        :param `originalfocus`: an instance of :class:`wx.Window`, representing a pointer to
1043         the window which originally had the focus
1044        """
1045
1046        self._usefocus = focus
1047        self._originalfocus = originalfocus
1048
1049
1050    def OnScrollTimer(self, event):
1051        """
1052        Handles the ``wx.EVT_TIMER`` event for :class:`ToasterBoxWindow` scrolling up/down.
1053
1054        :param `event`: a :class:`TimerEvent` event to be processed.
1055        """
1056
1057        if self._direction == wx.UP:
1058            self.TearUp()
1059        else:
1060            self.TearDown()
1061
1062
1063    def TearUp(self):
1064        """ Scrolls the :class:`ToasterBox` up, which means gradually showing it. """
1065
1066        self._windowsize = self._windowsize + self._step
1067        step = self._currentStep
1068
1069        if step < self._dialogtop[1]:
1070            step = self._dialogtop[1]
1071
1072         # checking the type of the scroll (from up to down or from down to up)
1073        if self._scrollType == TB_SCR_TYPE_UD:
1074            dimY = self._dialogtop[1]
1075        elif self._scrollType == TB_SCR_TYPE_DU:
1076            dimY = step
1077
1078        self.SetSize(self._dialogtop[0], dimY, self.GetSize().GetWidth(), self._windowsize)
1079
1080        self.Refresh(False)
1081
1082        self._currentStep += self._scrollStep
1083
1084        if self._currentStep not in list(range(self._start, self._stop, self._scrollStep)):
1085            self._scrollTimer.Stop()
1086            self.Update()
1087
1088            if self._tbstyle == TB_SIMPLE:
1089                self.DrawText()
1090
1091            if self._usefocus:
1092                self.SetFocus()
1093            else:
1094                self._originalfocus.SetFocus()
1095
1096
1097    def TearDown(self):
1098        """ Scrolls the :class:`ToasterBox` down, which means gradually hiding it. """
1099
1100        self._windowsize = self._windowsize - self._step
1101        step = self._currentStep
1102
1103        if step > self._bottomright.y:
1104            step = self._bottomright.y
1105
1106        if self._windowsize > 0:
1107            # checking the type of the scroll (from up to down or from down to up)
1108            if self._scrollType == TB_SCR_TYPE_UD:
1109                dimY = self._dialogtop[1]
1110            elif self._scrollType == TB_SCR_TYPE_DU:
1111                dimY = step
1112
1113            self.SetSize(self._dialogtop[0], dimY,
1114                               self.GetSize().GetWidth(), self._windowsize)
1115
1116            self.Update()
1117            self.Refresh()
1118
1119            self._currentStep += self._scrollStep
1120
1121        else:
1122            self._scrollTimer.Stop()
1123            self.Hide()
1124            if self._parent2:
1125                self._parent2.Notify()
1126
1127
1128    def SetupPositions(self):
1129        """ Sets up the position, size and scrolling step for :class:`ToasterBoxWindow`. """
1130
1131        if self._scrollType == TB_SCR_TYPE_FADE:
1132            self.SetPosition(wx.Point(*self._dialogtop))
1133            return
1134
1135        if self._direction == wx.UP:
1136            # walk the Y value up in a raise motion
1137            self._xpos = self.GetPosition().x
1138            self._ypos = self._bottomright[1]
1139            self._windowsize = 0
1140
1141            # checking the type of the scroll (from up to down or from down to up)
1142            if self._scrollType == TB_SCR_TYPE_UD:
1143                self._start = self._dialogtop[1]
1144                self._stop = self._ypos
1145                self._scrollStep = self._step
1146            elif self._scrollType == TB_SCR_TYPE_DU:
1147                self._start = self._ypos
1148                self._stop = self._dialogtop[1]
1149                self._scrollStep = -self._step
1150
1151        else:
1152
1153            # walk down the Y value
1154            self._windowsize = self.GetSize().GetHeight()
1155
1156            # checking the type of the scroll (from up to down or from down to up)
1157            if self._scrollType == TB_SCR_TYPE_UD:
1158                self._start = self._bottomright.y
1159                self._stop = self._dialogtop[1]
1160                self._scrollStep = -self._step
1161            elif self._scrollType == TB_SCR_TYPE_DU:
1162                self._start = self._dialogtop[1]
1163                self._stop = self._bottomright.y
1164                self._scrollStep = self._step
1165
1166        self._currentStep = self._start
1167
1168
1169    def ScrollUp(self):
1170        """ Scrolls the :class:`ToasterBox` up, which means gradually showing it. """
1171
1172        if self._scrollType == TB_SCR_TYPE_FADE:
1173            self._amount = 0
1174            self._delta = 5
1175            self.SetSize(self.GetSize())
1176            self._alphaTimer.Start(self._sleeptime)
1177        else:
1178            self.Show(True)
1179            self._scrollTimer.Start(self._sleeptime)
1180
1181
1182    def ScrollDown(self):
1183        """ Scrolls the :class:`ToasterBox` down, which means gradually hiding it. """
1184
1185        if self._scrollType == TB_SCR_TYPE_FADE:
1186            self._amount = 255
1187            self._delta = -5
1188            self._alphaTimer.Start(self._sleeptime)
1189        else:
1190            self._scrollTimer.Start(self._sleeptime)
1191
1192
1193    def OnPaint(self, event):
1194        """
1195        Handles the ``wx.EVT_PAINT`` event for :class:`ToasterBoxWindow`.
1196
1197        :param `event`: a :class:`PaintEvent` event to be processed.
1198
1199        :note: This event is handled and processed only if the style ``TB_SIMPLE`` is
1200         given to :class:`ToasterBox`.
1201        """
1202
1203        dc = wx.AutoBufferedPaintDC(self)
1204        self.DrawText(dc)
1205
1206
1207    def DrawText(self, dc=None):
1208        """
1209        Draws the text label for a :class:`ToasterBox` with ``TB_SIMPLE`` style set.
1210
1211        :param `dc`: an instance of :class:`wx.DC`. If defaulted to ``None``, a :class:`ClientDC`
1212         will be created on the fly.
1213        """
1214
1215        if dc is None:
1216            dc = wx.ClientDC(self)
1217
1218        dc.SetBackground(wx.Brush(self._backgroundcolour))
1219        dc.Clear()
1220
1221        if self._staticbitmap:
1222            dc.DrawBitmap(self._staticbitmap, 0, 0)
1223        dc.SetFont(self._textfont)
1224        dc.SetTextForeground(self._textcolour)
1225
1226        if not hasattr(self, "text_coords"):
1227            self._getTextCoords(dc)
1228        dc.DrawTextList(*self.text_coords)
1229
1230
1231    def AlphaCycle(self, event):
1232        """
1233        Handles the ``wx.EVT_TIMER`` event for :class:`ToasterBoxWindow`.
1234
1235        :param `event`: a :class:`TimerEvent` event to be processed.
1236        """
1237
1238        # Increase (or decrease) the alpha channel
1239        self._amount += self._delta
1240
1241        if self._tbstyle == TB_SIMPLE:
1242            self.Refresh(False)
1243
1244        if self._amount > 255 or self._amount < 0:
1245            # We're done, stop the timer
1246            self._alphaTimer.Stop()
1247
1248            if self._amount < 0:
1249                self.Hide()
1250                self.showtime.Stop()
1251                if self._parent2:
1252                    self._parent2.Notify()
1253
1254            elif self._amount > 255:
1255                if self._usefocus:
1256                    self.SetFocus()
1257                else:
1258                    self._originalfocus.SetFocus()
1259
1260            return
1261
1262        # Make the ToasterBoxWindow more or less transparent
1263        self.MakeWindowTransparent(self._amount)
1264        if not self.IsShown():
1265            self.Show()
1266
1267
1268    def MakeWindowTransparent(self, amount):
1269        """
1270        Makes the :class:`ToasterBoxWindow` window transparent.
1271
1272        :param `amount`: the alpha channel value.
1273        """
1274
1275        if not self.CanSetTransparent():
1276            return
1277
1278        self.SetTransparent(amount)
1279
1280
1281    def _getTextCoords(self, dc):
1282        """
1283        Draw the user specified text.
1284
1285        :param `dc`: an instance of :class:`wx.DC`.
1286
1287        :note: Use this method only for a :class:`ToasterBox` created with the ``TB_SIMPLE`` style.
1288        """
1289
1290        # border from sides and top to text (in pixels)
1291        border = 7
1292        # how much space between text lines
1293        textPadding = 2
1294
1295        pText = self.GetPopupText()
1296
1297        max_len = len(pText)
1298
1299        tw, th = self._parent2._popupsize
1300
1301        if self._windowstyle == TB_CAPTION:
1302            th = th - 20
1303
1304        while 1:
1305            lines = textwrap.wrap(pText, max_len)
1306
1307            for line in lines:
1308                w, h = dc.GetTextExtent(line)
1309                if w > tw - border * 2:
1310                    max_len -= 1
1311                    break
1312            else:
1313                break
1314
1315        fh = 0
1316        for line in lines:
1317            w, h = dc.GetTextExtent(line)
1318            fh += h + textPadding
1319        y = (th - fh) / 2; coords = []
1320
1321        for line in lines:
1322            w, h = dc.GetTextExtent(line)
1323            x = (tw - w) / 2
1324            coords.append((x, y))
1325            y += h + textPadding
1326
1327        self.text_coords = (lines, coords)
1328