1# -*- coding: utf-8 -*-
2#----------------------------------------------------------------------------
3# Name:         panel.py
4# Purpose:
5#
6# Author:       Andrea Gavana <andrea.gavana@gmail.com>
7#
8# Created:
9# Version:
10# Date:
11# Licence:      wxWindows license
12# Tags:         phoenix-port, unittest, documented, py3-port
13#----------------------------------------------------------------------------
14"""
15Serves as a container for a group of (ribbon) controls.
16
17
18Description
19===========
20
21A :class:`RibbonPanel` will typically have panels for children, with the controls for that
22page placed on the panels. A panel adds a border and label to a group of controls,
23and can be minimised (either automatically to conserve space, or manually by the user).
24
25
26Window Styles
27=============
28
29This class supports the following window styles:
30
31================================= =========== =================================
32Window Styles                     Hex Value   Description
33================================= =========== =================================
34``RIBBON_PANEL_DEFAULT_STYLE``            0x0 Defined as no other flags set.
35``RIBBON_PANEL_NO_AUTO_MINIMISE``         0x1 Prevents the panel from automatically minimising to conserve screen space.
36``RIBBON_PANEL_EXT_BUTTON``               0x8 Causes an extension button to be shown in the panel's chrome (if the bar in which it is contained has ``RIBBON_BAR_SHOW_PANEL_EXT_BUTTONS`` set). The behaviour of this button is application controlled, but typically will show an extended drop-down menu relating to the panel.
37``RIBBON_PANEL_MINIMISE_BUTTON``         0x10 Causes a (de)minimise button to be shown in the panel's chrome (if the bar in which it is contained has the ``RIBBON_BAR_SHOW_PANEL_MINIMISE_BUTTONS`` style set). This flag is typically combined with ``RIBBON_PANEL_NO_AUTO_MINIMISE`` to make a panel which the user always has manual control over when it minimises.
38``RIBBON_PANEL_STRETCH``                 0x20 Allows a single panel to stretch to fill the parent page.
39``RIBBON_PANEL_FLEXIBLE``                0x40 Allows toolbars to wrap, taking up the optimum amount of space when used in a vertical palette.
40================================= =========== =================================
41
42
43Events Processing
44=================
45
46This class processes the following events:
47
48======================================= ===================================
49Event Name                              Description
50======================================= ===================================
51``EVT_RIBBONPANEL_EXTBUTTON_ACTIVATED`` Triggered when the user activate the panel extension button.
52======================================= ===================================
53
54See Also
55========
56
57:class:`~wx.lib.agw.ribbon.page.RibbonPage`
58
59"""
60
61import wx
62
63from .control import RibbonControl
64
65from .art import *
66
67
68wxEVT_COMMAND_RIBBONPANEL_EXTBUTTON_ACTIVATED = wx.NewEventType()
69EVT_RIBBONPANEL_EXTBUTTON_ACTIVATED = wx.PyEventBinder(wxEVT_COMMAND_RIBBONPANEL_EXTBUTTON_ACTIVATED, 1)
70
71
72def IsAncestorOf(ancestor, window):
73
74    while window is not None:
75        parent = window.GetParent()
76        if parent == ancestor:
77            return True
78        else:
79            window = parent
80
81    return False
82
83
84class RibbonPanelEvent(wx.PyCommandEvent):
85    """ Handles events related to :class:`RibbonPanel`. """
86
87    def __init__(self, command_type=None, win_id=0, panel=None):
88        """
89        Default class constructor.
90
91        :param integer `command_type`: the event type;
92        :param integer `win_id`: the event identifier;
93        :param `panel`: an instance of :class:`RibbonPanel`;
94        """
95
96        wx.PyCommandEvent.__init__(self, command_type, win_id)
97
98        self._panel = panel
99
100
101    def GetPanel(self):
102        """ Returns the panel which the event relates to. """
103
104        return self._panel
105
106
107    def SetPanel(self, panel):
108        """
109        Sets the panel relating to this event.
110
111        :param `panel`: an instance of :class:`RibbonPanel`.
112        """
113
114        self._panel = panel
115
116
117class RibbonPanel(RibbonControl):
118    """ This is the main implementation of :class:`RibbonPanel`. """
119
120    def __init__(self, parent, id=wx.ID_ANY, label="", minimised_icon=wx.NullBitmap,
121                 pos=wx.DefaultPosition, size=wx.DefaultSize, agwStyle=RIBBON_PANEL_DEFAULT_STYLE,
122                 name="RibbonPanel"):
123        """
124        Default class constructor.
125
126        :param `parent`: pointer to a parent window, typically a :class:`~wx.lib.agw.ribbon.page.RibbonPage`, though
127         it can be any window;
128        :param `id`: window identifier. If ``wx.ID_ANY``, will automatically create
129         an identifier;
130        :param `label`: label of the new button;
131        :param `minimised_icon`: the bitmap to be used in place of the panel children
132         when it is minimised;
133        :param `pos`: window position. ``wx.DefaultPosition`` indicates that wxPython
134         should generate a default position for the window;
135        :param `size`: window size. ``wx.DefaultSize`` indicates that wxPython should
136         generate a default size for the window. If no suitable size can be found, the
137         window will be sized to 20x20 pixels so that the window is visible but obviously
138         not correctly sized;
139        :param `agwStyle`: the AGW-specific window style. This can be one of the following
140         bits:
141
142         ================================= =========== =================================
143         Window Styles                     Hex Value   Description
144         ================================= =========== =================================
145         ``RIBBON_PANEL_DEFAULT_STYLE``            0x0 Defined as no other flags set.
146         ``RIBBON_PANEL_NO_AUTO_MINIMISE``         0x1 Prevents the panel from automatically minimising to conserve screen space.
147         ``RIBBON_PANEL_EXT_BUTTON``               0x8 Causes an extension button to be shown in the panel's chrome (if the bar in which it is contained has ``RIBBON_BAR_SHOW_PANEL_EXT_BUTTONS`` set). The behaviour of this button is application controlled, but typically will show an extended drop-down menu relating to the panel.
148         ``RIBBON_PANEL_MINIMISE_BUTTON``         0x10 Causes a (de)minimise button to be shown in the panel's chrome (if the bar in which it is contained has the ``RIBBON_BAR_SHOW_PANEL_MINIMISE_BUTTONS`` style set). This flag is typically combined with ``RIBBON_PANEL_NO_AUTO_MINIMISE`` to make a panel which the user always has manual control over when it minimises.
149         ``RIBBON_PANEL_STRETCH``                 0x20 Allows a single panel to stretch to fill the parent page.
150         ``RIBBON_PANEL_FLEXIBLE``                0x40 Allows toolbars to wrap, taking up the optimum amount of space when used in a vertical palette.
151         ================================= =========== =================================
152
153        :param `name`: the window name.
154        """
155
156        RibbonControl.__init__(self, parent, id, pos, size, wx.BORDER_NONE, name=name)
157        self.CommonInit(label, minimised_icon, agwStyle)
158
159        self.Bind(wx.EVT_ENTER_WINDOW, self.OnMouseEnter)
160        self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground)
161        self.Bind(wx.EVT_KILL_FOCUS, self.OnKillFocus)
162        self.Bind(wx.EVT_LEAVE_WINDOW, self.OnMouseLeave)
163        self.Bind(wx.EVT_LEFT_DOWN, self.OnMouseClick)
164        self.Bind(wx.EVT_PAINT, self.OnPaint)
165        self.Bind(wx.EVT_SIZE, self.OnSize)
166        self.Bind(wx.EVT_MOTION, self.OnMotion)
167
168
169    def __del__(self):
170
171        if self._expanded_panel:
172            self._expanded_panel._expanded_dummy = None
173            self._expanded_panel.GetParent().Destroy()
174
175
176    def IsExtButtonHovered(self):
177
178        return self._ext_button_hovered
179
180
181    def SetArtProvider(self, art):
182        """
183        Set the art provider to be used.
184
185        Normally called automatically by :class:`~wx.lib.agw.ribbon.page.RibbonPage` when the panel is created, or the
186        art provider changed on the page. The new art provider will be propagated to the
187        children of the panel.
188
189        Reimplemented from :class:`~wx.lib.agw.ribbon.control.RibbonControl`.
190
191        :param `art`: an art provider.
192
193        """
194
195        self._art = art
196        for child in self.GetChildren():
197            if isinstance(child, RibbonControl):
198                child.SetArtProvider(art)
199
200        if self._expanded_panel:
201            self._expanded_panel.SetArtProvider(art)
202
203
204    def CommonInit(self, label, icon, agwStyle):
205
206        self.SetName(label)
207        self.SetLabel(label)
208
209        self._minimised_size = wx.Size(-1, -1) # Unknown / none
210        self._smallest_unminimised_size = wx.Size(-1, -1) # Unknown / none
211        self._preferred_expand_direction = wx.SOUTH
212        self._expanded_dummy = None
213        self._expanded_panel = None
214        self._flags = agwStyle
215        self._minimised_icon = icon
216        self._minimised = False
217        self._hovered = False
218        self._ext_button_hovered = False
219        self._ext_button_rect = wx.Rect()
220
221        if self._art == None:
222            parent = self.GetParent()
223            if isinstance(parent, RibbonControl):
224                self._art = parent.GetArtProvider()
225
226        self.SetAutoLayout(True)
227        self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM)
228        self.SetMinSize(wx.Size(20, 20))
229
230
231    def IsMinimised(self, at_size=None):
232        """
233        Query if the panel would be minimised at a given size.
234
235        :param `at_size`: an instance of :class:`wx.Size`, giving the size at which the
236         panel should be tested for minimisation.
237        """
238
239        if at_size is None:
240            return self.IsMinimised1()
241
242        return self.IsMinimised2(wx.Size(*at_size))
243
244
245    def IsMinimised1(self):
246        """ Query if the panel is currently minimised. """
247
248        return self._minimised
249
250
251    def IsHovered(self):
252        """
253        Query is the mouse is currently hovered over the panel.
254
255        :returns: ``True`` if the cursor is within the bounds of the panel (i.e.
256         hovered over the panel or one of its children), ``False`` otherwise.
257        """
258
259        return self._hovered
260
261
262    def OnMouseEnter(self, event):
263        """
264        Handles the ``wx.EVT_ENTER_WINDOW`` event for :class:`RibbonPanel`.
265
266        :param `event`: a :class:`MouseEvent` event to be processed.
267        """
268
269        self.TestPositionForHover(event.GetPosition())
270
271
272    def OnMouseEnterChild(self, event):
273        """
274        Handles the ``wx.EVT_ENTER_WINDOW`` event for children of :class:`RibbonPanel`.
275
276        :param `event`: a :class:`MouseEvent` event to be processed.
277        """
278
279        pos = event.GetPosition()
280        child = event.GetEventObject()
281
282        if child:
283            pos += child.GetPosition()
284            self.TestPositionForHover(pos)
285
286        event.Skip()
287
288
289    def OnMouseLeave(self, event):
290        """
291        Handles the ``wx.EVT_LEAVE_WINDOW`` event for :class:`RibbonPanel`.
292
293        :param `event`: a :class:`MouseEvent` event to be processed.
294        """
295
296        self.TestPositionForHover(event.GetPosition())
297
298
299    def OnMouseLeaveChild(self, event):
300        """
301        Handles the ``wx.EVT_LEAVE_WINDOW`` event for children of :class:`RibbonPanel`.
302
303        :param `event`: a :class:`MouseEvent` event to be processed.
304        """
305
306        pos = event.GetPosition()
307        child = event.GetEventObject()
308
309        if child:
310            pos += child.GetPosition()
311            self.TestPositionForHover(pos)
312
313        event.Skip()
314
315
316    def TestPositionForHover(self, pos):
317
318        hovered = ext_button_hovered = False
319
320        if pos.x >= 0 and pos.y >= 0:
321            size = self.GetSize()
322            if pos.x < size.GetWidth() and pos.y < size.GetHeight():
323                hovered = True
324
325        if hovered:
326            if self.HasExtButton():
327                ext_button_hovered = self._ext_button_rect.Contains(pos)
328
329        if hovered != self._hovered or ext_button_hovered != self._ext_button_hovered:
330            self._hovered = hovered
331            self._ext_button_hovered = ext_button_hovered
332            self.Refresh(False)
333
334
335    def HasExtButton(self):
336
337        bar = self.GetGrandParent()
338        return (self._flags & RIBBON_PANEL_EXT_BUTTON) and (bar.GetAGWWindowStyleFlag() & RIBBON_BAR_SHOW_PANEL_EXT_BUTTONS)
339
340
341    def AddChild(self, child):
342
343        RibbonControl.AddChild(self, child)
344
345        # Window enter / leave events count for only the window in question, not
346        # for children of the window. The panel wants to be in the hovered state
347        # whenever the mouse cursor is within its boundary, so the events need to
348        # be attached to children too.
349        child.Bind(wx.EVT_ENTER_WINDOW, self.OnMouseEnterChild)
350        child.Bind(wx.EVT_LEAVE_WINDOW, self.OnMouseLeaveChild)
351
352
353    def RemoveChild(self, child):
354
355        child.Unbind(wx.EVT_ENTER_WINDOW)
356        child.Unbind(wx.EVT_LEAVE_WINDOW)
357
358        RibbonControl.RemoveChild(self, child)
359
360
361    def OnMotion(self, event):
362        """
363        Handles the ``wx.EVT_MOTION`` event for :class:`RibbonPanel`.
364
365        :param `event`: a :class:`MouseEvent` event to be processed.
366        """
367
368        self.TestPositionForHover(event.GetPosition())
369
370
371    def OnSize(self, event):
372        """
373        Handles the ``wx.EVT_SIZE`` event for :class:`RibbonPanel`.
374
375        :param `event`: a :class:`wx.SizeEvent` event to be processed.
376        """
377
378        if self.GetAutoLayout():
379            self.Layout()
380
381        event.Skip()
382
383
384    def DoSetSize(self, x, y, width, height, sizeFlags=wx.SIZE_AUTO):
385        """
386        Sets the size of the window in pixels.
387
388        :param integer `x`: required `x` position in pixels, or ``wx.DefaultCoord`` to
389         indicate that the existing value should be used;
390        :param integer `y`: required `y` position in pixels, or ``wx.DefaultCoord`` to
391         indicate that the existing value should be used;
392        :param integer `width`: required width in pixels, or ``wx.DefaultCoord`` to
393         indicate that the existing value should be used;
394        :param integer `height`: required height in pixels, or ``wx.DefaultCoord`` to
395         indicate that the existing value should be used;
396        :param integer `sizeFlags`: indicates the interpretation of other parameters.
397         It is a bit list of the following:
398
399         * ``wx.SIZE_AUTO_WIDTH``: a ``wx.DefaultCoord`` width value is taken to indicate a
400           wxPython-supplied default width.
401         * ``wx.SIZE_AUTO_HEIGHT``: a ``wx.DefaultCoord`` height value is taken to indicate a
402           wxPython-supplied default height.
403         * ``wx.SIZE_AUTO``: ``wx.DefaultCoord`` size values are taken to indicate a wxPython-supplied
404           default size.
405         * ``wx.SIZE_USE_EXISTING``: existing dimensions should be used if ``wx.DefaultCoord`` values are supplied.
406         * ``wx.SIZE_ALLOW_MINUS_ONE``: allow negative dimensions (i.e. value of ``wx.DefaultCoord``)
407           to be interpreted as real dimensions, not default values.
408         * ``wx.SIZE_FORCE``: normally, if the position and the size of the window are already
409           the same as the parameters of this function, nothing is done. but with this flag a window
410           resize may be forced even in this case (supported in wx 2.6.2 and later and only implemented
411           for MSW and ignored elsewhere currently).
412        """
413
414        # At least on MSW, changing the size of a window will cause GetSize() to
415        # report the new size, but a size event may not be handled immediately.
416        # If self minimised check was performed in the OnSize handler, then
417        # GetSize() could return a size much larger than the minimised size while
418        # IsMinimised() returns True. This would then affect layout, as the panel
419        # will refuse to grow any larger while in limbo between minimised and non.
420
421        minimised = (self._flags & RIBBON_PANEL_NO_AUTO_MINIMISE) == 0 and self.IsMinimised(wx.Size(width, height))
422
423        if minimised != self._minimised:
424            self._minimised = minimised
425
426            for child in self.GetChildren():
427                child.Show(not minimised)
428
429            self.Refresh()
430
431        RibbonControl.DoSetSize(self, x, y, width, height, sizeFlags)
432
433
434    def IsMinimised2(self, at_size):
435        """
436        Query if the panel would be minimised at a given size.
437
438        :param `at_size`: an instance of :class:`wx.Size`, giving the size at which the
439         panel should be tested for minimisation.
440        """
441
442        if self.GetSizer():
443            # we have no information on size change direction
444            # so check both
445            size = self.GetMinNotMinimisedSize()
446            if size.x > at_size.x or size.y > at_size.y:
447                return True
448
449            return False
450
451        if not self._minimised_size.IsFullySpecified():
452            return False
453
454        return (at_size.x <= self._minimised_size.x and \
455                at_size.y <= self._minimised_size.y) or \
456                at_size.x < self._smallest_unminimised_size.x or \
457                at_size.y < self._smallest_unminimised_size.y
458
459
460    def OnEraseBackground(self, event):
461        """
462        Handles the ``wx.EVT_ERASE_BACKGROUND`` event for :class:`RibbonPanel`.
463
464        :param `event`: a :class:`EraseEvent` event to be processed.
465        """
466
467        # All painting done in main paint handler to minimise flicker
468        pass
469
470
471    def OnPaint(self, event):
472        """
473        Handles the ``wx.EVT_PAINT`` event for :class:`RibbonPanel`.
474
475        :param `event`: a :class:`PaintEvent` event to be processed.
476        """
477
478        dc = wx.AutoBufferedPaintDC(self)
479
480        if self._art != None:
481            if self.IsMinimised():
482                self._art.DrawMinimisedPanel(dc, self, wx.Rect(0, 0, *self.GetSize()), self._minimised_icon_resized)
483            else:
484                self._art.DrawPanelBackground(dc, self, wx.Rect(0, 0, *self.GetSize()))
485
486
487    def IsSizingContinuous(self):
488        """
489        Returns ``True`` if this window can take any size (greater than its minimum size),
490        ``False`` if it can only take certain sizes.
491
492        :see: :meth:`RibbonControl.GetNextSmallerSize() <lib.agw.ribbon.control.RibbonControl.GetNextSmallerSize>`,
493         :meth:`RibbonControl.GetNextLargerSize() <lib.agw.ribbon.control.RibbonControl.GetNextLargerSize>`
494        """
495
496        # A panel never sizes continuously, even if all of its children can,
497        # as it would appear out of place along side non-continuous panels.
498
499        # JS 2012-03-09: introducing wxRIBBON_PANEL_STRETCH to allow
500        # the panel to fill its parent page. For example we might have
501        # a list of styles in one of the pages, which should stretch to
502        # fill available space.
503        return self._flags & RIBBON_PANEL_STRETCH
504
505
506    def GetBestSizeForParentSize(self, parentSize):
507        """ Finds the best width and height given the parent's width and height. """
508
509        if len(self.GetChildren()) == 1:
510            win = self.GetChildren()[0]
511
512            if isinstance(win, RibbonControl):
513                temp_dc = wx.ClientDC(self)
514                childSize = win.GetBestSizeForParentSize(parentSize)
515                clientParentSize = self._art.GetPanelClientSize(temp_dc, self, wx.Size(*parentSize), None)
516                overallSize = self._art.GetPanelSize(temp_dc, self, wx.Size(*clientParentSize), None)
517                return overallSize
518
519        return self.GetSize()
520
521
522    def DoGetNextSmallerSize(self, direction, relative_to):
523        """
524        Implementation of :meth:`RibbonControl.GetNextSmallerSize() <lib.agw.ribbon.control.RibbonControl.GetNextSmallerSize>`.
525
526        Controls which have non-continuous sizing must override this virtual function
527        rather than :meth:`RibbonControl.GetNextSmallerSize() <lib.agw.ribbon.control.RibbonControl.GetNextSmallerSize>`.
528        """
529
530        if self._expanded_panel != None:
531            # Next size depends upon children, who are currently in the
532            # expanded panel
533            return self._expanded_panel.DoGetNextSmallerSize(direction, relative_to)
534
535        if self._art is not None:
536
537            dc = wx.ClientDC(self)
538            child_relative, dummy = self._art.GetPanelClientSize(dc, self, wx.Size(*relative_to), None)
539            smaller = wx.Size(-1, -1)
540            minimise = False
541
542            if self.GetSizer():
543
544                # Get smallest non minimised size
545                smaller = self.GetMinSize()
546
547                # and adjust to child_relative for parent page
548                if self._art.GetFlags() & RIBBON_BAR_FLOW_VERTICAL:
549                     minimise = child_relative.y <= smaller.y
550                     if smaller.x < child_relative.x:
551                        smaller.x = child_relative.x
552                else:
553                    minimise = child_relative.x <= smaller.x
554                    if smaller.y < child_relative.y:
555                        smaller.y = child_relative.y
556
557            elif len(self.GetChildren()) == 1:
558
559                # Simple (and common) case of single ribbon child or Sizer
560                ribbon_child = self.GetChildren()[0]
561                if isinstance(ribbon_child, RibbonControl):
562                    smaller = ribbon_child.GetNextSmallerSize(direction, child_relative)
563                    minimise = smaller == child_relative
564
565            if minimise:
566                if self.CanAutoMinimise():
567                    minimised = wx.Size(*self._minimised_size)
568
569                    if direction == wx.HORIZONTAL:
570                        minimised.SetHeight(relative_to.GetHeight())
571                    elif direction == wx.VERTICAL:
572                        minimised.SetWidth(relative_to.GetWidth())
573
574                    return minimised
575
576                else:
577                    return relative_to
578
579            elif smaller.IsFullySpecified(): # Use fallback if !(sizer/child = 1)
580                return self._art.GetPanelSize(dc, self, wx.Size(*smaller), None)
581
582        # Fallback: Decrease by 20% (or minimum size, whichever larger)
583        current = wx.Size(*relative_to)
584        minimum = wx.Size(*self.GetMinSize())
585
586        if direction & wx.HORIZONTAL:
587            current.x = (current.x * 4) / 5
588            if current.x < minimum.x:
589                current.x = minimum.x
590
591        if direction & wx.VERTICAL:
592            current.y = (current.y * 4) / 5
593            if current.y < minimum.y:
594                current.y = minimum.y
595
596        return current
597
598
599    def DoGetNextLargerSize(self, direction, relative_to):
600        """
601        Implementation of :meth:`RibbonControl.GetNextLargerSize() <lib.agw.ribbon.control.RibbonControl.GetNextLargerSize>`.
602
603        Controls which have non-continuous sizing must override this virtual function
604        rather than :meth:`RibbonControl.GetNextLargerSize() <lib.agw.ribbon.control.RibbonControl.GetNextLargerSize>`.
605        """
606
607        if self._expanded_panel != None:
608            # Next size depends upon children, who are currently in the
609            # expanded panel
610            return self._expanded_panel.DoGetNextLargerSize(direction, relative_to)
611
612        if self.IsMinimised(relative_to):
613            current = wx.Size(*relative_to)
614            min_size = wx.Size(*self.GetMinNotMinimisedSize())
615
616            if direction == wx.HORIZONTAL:
617                if min_size.x > current.x and min_size.y == current.y:
618                    return min_size
619
620            elif direction == wx.VERTICAL:
621                if min_size.x == current.x and min_size.y > current.y:
622                    return min_size
623
624            elif direction == wx.BOTH:
625                if min_size.x > current.x and min_size.y > current.y:
626                    return min_size
627
628        if self._art is not None:
629
630            dc = wx.ClientDC(self)
631            child_relative, dummy = self._art.GetPanelClientSize(dc, self, wx.Size(*relative_to), None)
632            larger = wx.Size(-1, -1)
633
634            if self.GetSizer():
635
636                # We could just let the sizer expand in flow direction but see comment
637                # in IsSizingContinuous()
638                larger = self.GetPanelSizerBestSize()
639
640                # and adjust for page in non flow direction
641                if self._art.GetFlags() & RIBBON_BAR_FLOW_VERTICAL:
642                     if larger.x != child_relative.x:
643                        larger.x = child_relative.x
644
645                elif larger.y != child_relative.y:
646                    larger.y = child_relative.y
647
648            elif len(self.GetChildren()) == 1:
649
650                # Simple (and common) case of single ribbon child
651                ribbon_child = self.GetChildren()[0]
652                if isinstance(ribbon_child, RibbonControl):
653                    larger = ribbon_child.GetNextLargerSize(direction, child_relative)
654
655            if larger.IsFullySpecified(): # Use fallback if !(sizer/child = 1)
656                if larger == child_relative:
657                    return relative_to
658                else:
659                    return self._art.GetPanelSize(dc, self, wx.Size(*larger), None)
660
661
662        # Fallback: Increase by 25% (equal to a prior or subsequent 20% decrease)
663        # Note that due to rounding errors, this increase may not exactly equal a
664        # matching decrease - an ideal solution would not have these errors, but
665        # avoiding them is non-trivial unless an increase is by 100% rather than
666        # a fractional amount. This would then be non-ideal as the resizes happen
667        # at very large intervals.
668        current = wx.Size(*relative_to)
669
670        if direction & wx.HORIZONTAL:
671            current.x = (current.x * 5 + 3) / 4
672
673        if direction & wx.VERTICAL:
674            current.y = (current.y * 5 + 3) / 4
675
676        return current
677
678
679
680    def CanAutoMinimise(self):
681        """ Query if the panel can automatically minimise itself at small sizes. """
682
683        return (self._flags & RIBBON_PANEL_NO_AUTO_MINIMISE) == 0 \
684               and self._minimised_size.IsFullySpecified()
685
686
687    def GetMinSize(self):
688        """
689        Returns the minimum size of the window, an indication to the sizer layout mechanism
690        that this is the minimum required size.
691
692        This method normally just returns the value set by `SetMinSize`, but it can be
693        overridden to do the calculation on demand.
694        """
695
696        if self._expanded_panel != None:
697            # Minimum size depends upon children, who are currently in the
698            # expanded panel
699            return self._expanded_panel.GetMinSize()
700
701        if self.CanAutoMinimise():
702            return wx.Size(*self._minimised_size)
703        else:
704            return self.GetMinNotMinimisedSize()
705
706
707    def GetMinNotMinimisedSize(self):
708
709        # Ask sizer if present
710        if self.GetSizer():
711            dc = wx.ClientDC(self)
712            return self._art.GetPanelSize(dc, self, wx.Size(*self.GetPanelSizerMinSize()), None)
713
714        # Common case of no sizer and single child taking up the entire panel
715        elif len(self.GetChildren()) == 1:
716            child = self.GetChildren()[0]
717            dc = wx.ClientDC(self)
718            return self._art.GetPanelSize(dc, self, wx.Size(*child.GetMinSize()), None)
719
720        return wx.Size(*RibbonControl.GetMinSize(self))
721
722
723    def GetPanelSizerMinSize(self):
724
725        # Called from Realize() to set self._smallest_unminimised_size and from other
726        # functions to get the minimum size.
727        # The panel will be invisible when minimised and sizer calcs will be 0
728        # Uses self._smallest_unminimised_size in preference to self.GetSizer().CalcMin()
729        # to eliminate flicker.
730
731        # Check if is visible and not previously calculated
732        if self.IsShown() and not self._smallest_unminimised_size.IsFullySpecified():
733             return self.GetSizer().CalcMin()
734
735        # else use previously calculated self._smallest_unminimised_size
736        dc = wx.ClientDC(self)
737        return self._art.GetPanelClientSize(dc, self, wx.Size(*self._smallest_unminimised_size), None)[0]
738
739
740    def GetPanelSizerBestSize(self):
741
742        size = self.GetPanelSizerMinSize()
743        # TODO allow panel to increase its size beyond minimum size
744        # by steps similarly to ribbon control panels (preferred for aesthetics)
745        # or continuously.
746        return size
747
748
749    def DoGetBestSize(self):
750        """
751        Gets the size which best suits the window: for a control, it would be the
752        minimal size which doesn't truncate the control, for a panel - the same size
753        as it would have after a call to `Fit()`.
754
755        :return: An instance of :class:`wx.Size`.
756
757        :note: Overridden from :class:`wx.Control`.
758        """
759
760        # Ask sizer if present
761        if self.GetSizer():
762            dc = wx.ClientDC(self)
763            return self._art.GetPanelSize(dc, self, wx.Size(*self.GetPanelSizerBestSize()), None)
764
765        # Common case of no sizer and single child taking up the entire panel
766        elif len(self.GetChildren()) == 1:
767            child = self.GetChildren()[0]
768            dc = wx.ClientDC(self)
769            return self._art.GetPanelSize(dc, self, wx.Size(*child.GetBestSize()), None)
770
771        return wx.Size(*RibbonControl.DoGetBestSize(self))
772
773
774    def Realize(self):
775        """
776        Realize all children of the panel.
777
778        :note: Reimplemented from :class:`~wx.lib.agw.ribbon.control.RibbonControl`.
779        """
780
781        status = True
782        children = self.GetChildren()
783
784        for child in children:
785            if not isinstance(child, RibbonControl):
786                continue
787
788            if not child.Realize():
789                status = False
790
791        minimum_children_size = wx.Size(0, 0)
792
793        # Ask sizer if there is one present
794        if self.GetSizer():
795            minimum_children_size = wx.Size(*self.GetPanelSizerMinSize())
796        elif len(children) == 1:
797            minimum_children_size = wx.Size(*children[0].GetMinSize())
798
799        if self._art != None:
800            temp_dc = wx.ClientDC(self)
801            self._smallest_unminimised_size = self._art.GetPanelSize(temp_dc, self, wx.Size(*minimum_children_size), None)
802
803            panel_min_size = self.GetMinNotMinimisedSize()
804            self._minimised_size, bitmap_size, self._preferred_expand_direction = self._art.GetMinimisedPanelMinimumSize(temp_dc, self, 1, 1)
805
806            if self._minimised_icon.IsOk() and self._minimised_icon.GetSize() != bitmap_size:
807                img = self._minimised_icon.ConvertToImage()
808                img.Rescale(bitmap_size.GetWidth(), bitmap_size.GetHeight(), wx.IMAGE_QUALITY_HIGH)
809                self._minimised_icon_resized = wx.Bitmap(img)
810            else:
811                self._minimised_icon_resized = self._minimised_icon
812
813            if self._minimised_size.x > panel_min_size.x and self._minimised_size.y > panel_min_size.y:
814                # No point in having a minimised size which is larger than the
815                # minimum size which the children can go to.
816                self._minimised_size = wx.Size(-1, -1)
817            else:
818                if self._art.GetFlags() & RIBBON_BAR_FLOW_VERTICAL:
819                    self._minimised_size.x = panel_min_size.x
820                else:
821                    self._minimised_size.y = panel_min_size.y
822
823        else:
824            self._minimised_size = wx.Size(-1, -1)
825
826        return self.Layout() and status
827
828
829    def Layout(self):
830
831        if self.IsMinimised():
832            # Children are all invisible when minimised
833            return True
834
835        dc = wx.ClientDC(self)
836        size, position = self._art.GetPanelClientSize(dc, self, wx.Size(*self.GetSize()), wx.Point())
837
838        children = self.GetChildren()
839
840        if self.GetSizer():
841            self.GetSizer().SetDimension(position.x, position.y, size.x, size.y) # SetSize and Layout()
842        elif len(children) == 1:
843           # Common case of no sizer and single child taking up the entire panel
844             children[0].SetSize(position.x, position.y, size.GetWidth(), size.GetHeight())
845
846        if self.HasExtButton():
847            self._ext_button_rect = self._art.GetPanelExtButtonArea(dc, self, self.GetSize())
848
849        return True
850
851
852    def OnMouseClick(self, event):
853        """
854        Handles the ``wx.EVT_LEFT_DOWN`` event for :class:`RibbonPanel`.
855
856        :param `event`: a :class:`MouseEvent` event to be processed.
857        """
858
859        if self.IsMinimised():
860            if self._expanded_panel != None:
861                self.HideExpanded()
862            else:
863                self.ShowExpanded()
864
865        elif self.IsExtButtonHovered():
866            notification = RibbonPanelEvent(wxEVT_COMMAND_RIBBONPANEL_EXTBUTTON_ACTIVATED, self.GetId())
867            notification.SetEventObject(self)
868            notification.SetPanel(self)
869            self.ProcessEvent(notification)
870
871
872    def GetExpandedDummy(self):
873        """
874        Get the dummy panel of an expanded panel.
875
876        :note: This should be called on an expanded panel to get the dummy associated
877         with it - it will return ``None`` when called on the dummy itself.
878
879        :see: :meth:`~RibbonPanel.ShowExpanded`, :meth:`~RibbonPanel.GetExpandedPanel`
880        """
881
882        return self._expanded_dummy
883
884
885    def GetExpandedPanel(self):
886        """
887        Get the expanded panel of a dummy panel.
888
889        :note: This should be called on a dummy panel to get the expanded panel
890         associated with it - it will return ``None`` when called on the expanded panel
891         itself.
892
893        :see: :meth:`~RibbonPanel.ShowExpanded`, :meth:`~RibbonPanel.GetExpandedDummy`
894        """
895
896        return self._expanded_panel
897
898
899    def ShowExpanded(self):
900        """
901        Show the panel externally expanded.
902
903        When a panel is minimised, it can be shown full-size in a pop-out window, which
904        is refered to as being (externally) expanded.
905
906        :returns: ``True`` if the panel was expanded, ``False`` if it was not (possibly
907         due to it not being minimised, or already being expanded).
908
909        :note: When a panel is expanded, there exist two panels - the original panel
910         (which is refered to as the dummy panel) and the expanded panel. The original
911         is termed a dummy as it sits in the ribbon bar doing nothing, while the expanded
912         panel holds the panel children.
913
914        :see: :meth:`~RibbonPanel.HideExpanded`, :meth:`~RibbonPanel.GetExpandedPanel`
915        """
916
917        if not self.IsMinimised():
918            return False
919
920        if self._expanded_dummy != None or self._expanded_panel != None:
921            return False
922
923        size = self.GetBestSize()
924        pos = self.GetExpandedPosition(wx.Rect(self.GetScreenPosition(), self.GetSize()), size, self._preferred_expand_direction).GetTopLeft()
925
926        # Need a top-level frame to contain the expanded panel
927        container = wx.Frame(None, wx.ID_ANY, self.GetLabel(), pos, size, wx.FRAME_NO_TASKBAR | wx.BORDER_NONE)
928
929        self._expanded_panel = RibbonPanel(container, wx.ID_ANY, self.GetLabel(), self._minimised_icon, wx.Point(0, 0), size, self._flags)
930        self._expanded_panel.SetArtProvider(self._art)
931        self._expanded_panel._expanded_dummy = self
932
933        # Move all children to the new panel.
934        # Conceptually it might be simpler to reparent self entire panel to the
935        # container and create a new panel to sit in its place while expanded.
936        # This approach has a problem though - when the panel is reinserted into
937        # its original parent, it'll be at a different position in the child list
938        # and thus assume a new position.
939        # NB: Children iterators not used as behaviour is not well defined
940        # when iterating over a container which is being emptied
941
942        for child in self.GetChildren():
943            child.Reparent(self._expanded_panel)
944            child.Show()
945
946
947        # Move sizer to new panel
948        if self.GetSizer():
949            sizer = self.GetSizer()
950            self.SetSizer(None, False)
951            self._expanded_panel.SetSizer(sizer)
952
953        self._expanded_panel.Realize()
954        self.Refresh()
955        container.Show()
956        self._expanded_panel.SetFocus()
957
958        return True
959
960
961    def ShouldSendEventToDummy(self, event):
962
963        # For an expanded panel, filter events between being sent up to the
964        # floating top level window or to the dummy panel sitting in the ribbon
965        # bar.
966
967        # Child focus events should not be redirected, as the child would not be a
968        # child of the window the event is redirected to. All other command events
969        # seem to be suitable for redirecting.
970        return event.IsCommandEvent() and event.GetEventType() != wx.wxEVT_CHILD_FOCUS
971
972
973    def TryAfter(self, event):
974
975        if self._expanded_dummy and self.ShouldSendEventToDummy(event):
976            propagateOnce = wx.PropagateOnce(event)
977            return self._expanded_dummy.GetEventHandler().ProcessEvent(event)
978        else:
979            return RibbonControl.TryAfter(self, event)
980
981
982    def OnKillFocus(self, event):
983        """
984        Handles the ``wx.EVT_KILL_FOCUS`` event for :class:`RibbonPanel`.
985
986        :param `event`: a :class:`FocusEvent` event to be processed.
987        """
988
989        if self._expanded_dummy:
990            receiver = event.GetWindow()
991
992            if IsAncestorOf(self, receiver):
993                self._child_with_focus = receiver
994                receiver.Bind(wx.EVT_KILL_FOCUS, self.OnChildKillFocus)
995
996            elif receiver is None or receiver != self._expanded_dummy:
997                self.HideExpanded()
998
999
1000    def OnChildKillFocus(self, event):
1001        """
1002        Handles the ``wx.EVT_KILL_FOCUS`` event for children of :class:`RibbonPanel`.
1003
1004        :param `event`: a :class:`FocusEvent` event to be processed.
1005        """
1006
1007        if self._child_with_focus == None:
1008            return # Should never happen, but a check can't hurt
1009
1010        self._child_with_focus.Bind(wx.EVT_KILL_FOCUS, None)
1011        self._child_with_focus = None
1012
1013        receiver = event.GetWindow()
1014        if receiver == self or IsAncestorOf(self, receiver):
1015            self._child_with_focus = receiver
1016            receiver.Bind(wx.EVT_KILL_FOCUS, self.OnChildKillFocus)
1017            event.Skip()
1018
1019        elif receiver == None or receiver != self._expanded_dummy:
1020            self.HideExpanded()
1021            # Do not skip event, as the panel has been de-expanded, causing the
1022            # child with focus to be reparented (and hidden). If the event
1023            # continues propogation then bad things happen.
1024
1025        else:
1026            event.Skip()
1027
1028
1029    def HideExpanded(self):
1030        """
1031        Hide the panel's external expansion.
1032
1033        :returns: ``True`` if the panel was un-expanded, ``False`` if it was not
1034         (normally due to it not being expanded in the first place).
1035
1036        :see: :meth:`~RibbonPanel.HideExpanded`, :meth:`~RibbonPanel.GetExpandedPanel`
1037        """
1038
1039        if self._expanded_dummy == None:
1040            if self._expanded_panel:
1041                return self._expanded_panel.HideExpanded()
1042            else:
1043                return False
1044
1045        # Move children back to original panel
1046        # NB: Children iterators not used as behaviour is not well defined
1047        # when iterating over a container which is being emptied
1048        for child in self.GetChildren():
1049            child.Reparent(self._expanded_dummy)
1050            child.Hide()
1051
1052        # TODO: Move sizer back
1053        self._expanded_dummy._expanded_panel = None
1054        self._expanded_dummy.Realize()
1055        self._expanded_dummy.Refresh()
1056        parent = self.GetParent()
1057        self.Destroy()
1058        parent.Destroy()
1059
1060        return True
1061
1062
1063    def GetExpandedPosition(self, panel, expanded_size, direction):
1064
1065        # Strategy:
1066        # 1) Determine primary position based on requested direction
1067        # 2) Move the position so that it sits entirely within a display
1068        #    (for single monitor systems, this moves it into the display region,
1069        #     but for multiple monitors, it does so without splitting it over
1070        #     more than one display)
1071        # 2.1) Move in the primary axis
1072        # 2.2) Move in the secondary axis
1073
1074        primary_x = False
1075        secondary_x = secondary_y = 0
1076        pos = wx.Point()
1077
1078        if direction == wx.NORTH:
1079            pos.x = panel.GetX() + (panel.GetWidth() - expanded_size.GetWidth()) / 2
1080            pos.y = panel.GetY() - expanded_size.GetHeight()
1081            primary_x = True
1082            secondary_y = 1
1083
1084        elif direction == wx.EAST:
1085            pos.x = panel.GetRight()
1086            pos.y = panel.GetY() + (panel.GetHeight() - expanded_size.GetHeight()) / 2
1087            secondary_x = -1
1088
1089        elif direction == wx.SOUTH:
1090            pos.x = panel.GetX() + (panel.GetWidth() - expanded_size.GetWidth()) / 2
1091            pos.y = panel.GetBottom()
1092            primary_x = True
1093            secondary_y = -1
1094
1095        else:
1096            pos.x = panel.GetX() - expanded_size.GetWidth()
1097            pos.y = panel.GetY() + (panel.GetHeight() - expanded_size.GetHeight()) / 2
1098            secondary_x = 1
1099
1100        expanded = wx.Rect(pos, expanded_size)
1101        best = wx.Rect(*expanded)
1102        best_distance = 10000
1103
1104        display_n = wx.Display.GetCount()
1105
1106        for display_i in range(display_n):
1107            display = wx.Display(display_i).GetGeometry()
1108            if display.Contains(expanded):
1109                return expanded
1110
1111            elif display.Intersects(expanded):
1112                new_rect = wx.Rect(*expanded)
1113                distance = 0
1114
1115                if primary_x:
1116                    if expanded.GetRight() > display.GetRight():
1117                        distance = expanded.GetRight() - display.GetRight()
1118                        new_rect.x -= distance
1119
1120                    elif expanded.GetLeft() < display.GetLeft():
1121                        distance = display.GetLeft() - expanded.GetLeft()
1122                        new_rect.x += distance
1123
1124                else:
1125                    if expanded.GetBottom() > display.GetBottom():
1126                        distance = expanded.GetBottom() - display.GetBottom()
1127                        new_rect.y -= distance
1128
1129                    elif expanded.GetTop() < display.GetTop():
1130                        distance = display.GetTop() - expanded.GetTop()
1131                        new_rect.y += distance
1132
1133                if not display.Contains(new_rect):
1134                    # Tried moving in primary axis, but failed.
1135                    # Hence try moving in the secondary axis.
1136                    dx = secondary_x * (panel.GetWidth() + expanded_size.GetWidth())
1137                    dy = secondary_y * (panel.GetHeight() + expanded_size.GetHeight())
1138                    new_rect.x += dx
1139                    new_rect.y += dy
1140
1141                    # Squaring makes secondary moves more expensive (and also
1142                    # prevents a negative cost)
1143                    distance += dx * dx + dy * dy
1144
1145                if display.Contains(new_rect) and distance < best_distance:
1146                    best = new_rect
1147                    best_distance = distance
1148
1149        return best
1150
1151
1152    def GetMinimisedIcon(self):
1153        """
1154        Get the bitmap to be used in place of the panel children when it is minimised.
1155        """
1156
1157        return self._minimised_icon
1158
1159
1160    def GetDefaultBorder(self):
1161        """ Returns the default border style for :class:`RibbonPanel`. """
1162
1163        return wx.BORDER_NONE
1164
1165
1166    def GetFlags(self):
1167        """ Returns the AGW-specific window style for :class:`RibbonPanel`. """
1168
1169        return self._flags
1170
1171