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