1527 lines
48 KiB
Python
1527 lines
48 KiB
Python
# ===============================================================================
|
|
#
|
|
# ToDo: Bug - when selecting close on a tab, sometimes the tab to the right is
|
|
# selected, most likely due to determination of mouse position
|
|
# ToDo: Tab Selection seems overly complicated. OnLeftDown finds tab at
|
|
# position, and then call's CheckTabSelected which calls TabHitTest (when
|
|
# we are already aware it will return due to FindTabAtPos)
|
|
# ToDo: Perhaps a better way of finding tabs at position instead of looping
|
|
# through them and getting their regions. Perhaps some smart trickery with
|
|
# mouse pos x (all tabs have same width, so we divide x by width to find
|
|
# tab index?). This will also help with finding close buttons.
|
|
# ToDo: Fix page preview code (PFNotebookPagePreview)
|
|
#
|
|
# ===============================================================================
|
|
|
|
|
|
import math
|
|
from functools import lru_cache
|
|
|
|
import wx
|
|
import wx.lib.newevent
|
|
|
|
from gui.bitmap_loader import BitmapLoader
|
|
from gui.utils import color as color_utils, draw, fonts
|
|
from service.fit import Fit
|
|
|
|
_t = wx.GetTranslation
|
|
_PageChanging, EVT_NOTEBOOK_PAGE_CHANGING = wx.lib.newevent.NewEvent()
|
|
_PageChanged, EVT_NOTEBOOK_PAGE_CHANGED = wx.lib.newevent.NewEvent()
|
|
_PageAdding, EVT_NOTEBOOK_PAGE_ADDING = wx.lib.newevent.NewEvent()
|
|
_PageClosing, EVT_NOTEBOOK_PAGE_CLOSING = wx.lib.newevent.NewEvent()
|
|
PageAdded, EVT_NOTEBOOK_PAGE_ADDED = wx.lib.newevent.NewEvent()
|
|
PageClosed, EVT_NOTEBOOK_PAGE_CLOSED = wx.lib.newevent.NewEvent()
|
|
|
|
|
|
class VetoAble:
|
|
def __init__(self):
|
|
self.__vetoed = False
|
|
|
|
def Veto(self):
|
|
self.__vetoed = True
|
|
|
|
def isVetoed(self):
|
|
return self.__vetoed
|
|
|
|
|
|
class NotebookTabChangeEvent:
|
|
def __init__(self, old, new):
|
|
self.__old = old
|
|
self.__new = new
|
|
|
|
def GetOldSelection(self):
|
|
return self.__old
|
|
|
|
def GetSelection(self):
|
|
return self.__new
|
|
|
|
OldSelection = property(GetOldSelection)
|
|
Selection = property(GetSelection)
|
|
|
|
|
|
class PageChanging(_PageChanging, NotebookTabChangeEvent, VetoAble):
|
|
def __init__(self, old, new):
|
|
_PageChanging.__init__(self)
|
|
NotebookTabChangeEvent.__init__(self, old, new)
|
|
VetoAble.__init__(self)
|
|
|
|
|
|
class PageChanged(_PageChanged, NotebookTabChangeEvent):
|
|
|
|
def __init__(self, old, new, *args, **kwargs):
|
|
_PageChanged.__init__(self, *args, **kwargs)
|
|
NotebookTabChangeEvent.__init__(self, old, new)
|
|
|
|
|
|
class PageClosing(_PageClosing, VetoAble):
|
|
def __init__(self, i):
|
|
_PageClosing.__init__(self)
|
|
self.__index = i
|
|
VetoAble.__init__(self)
|
|
self.Selection = property(self.GetSelection)
|
|
|
|
def GetSelection(self):
|
|
return self.__index
|
|
|
|
|
|
class PageAdding(_PageAdding, VetoAble):
|
|
def __init__(self):
|
|
_PageAdding.__init__(self)
|
|
VetoAble.__init__(self)
|
|
|
|
|
|
class ChromeNotebook(wx.Panel):
|
|
|
|
def __init__(self, parent, can_add=True, tabWidthMode=0):
|
|
"""
|
|
Instance of Notebook. Initializes general layout, includes methods
|
|
for setting current page, replacing pages, any public function for the
|
|
notebook
|
|
|
|
width modes:
|
|
- 0: legacy (all tabs have equal width)
|
|
- 1: all tabs take just enough space to fit text
|
|
"""
|
|
super().__init__(parent, wx.ID_ANY, size=(-1, -1))
|
|
|
|
self._pages = []
|
|
self._active_page = None
|
|
|
|
main_sizer = wx.BoxSizer(wx.VERTICAL)
|
|
|
|
tabs_sizer = wx.BoxSizer(wx.VERTICAL)
|
|
self.tabs_container = _TabsContainer(self, can_add=can_add, tabWidthMode=tabWidthMode)
|
|
tabs_sizer.Add(self.tabs_container, 0, wx.EXPAND)
|
|
|
|
if 'wxMSW' in wx.PlatformInfo:
|
|
style = wx.DOUBLE_BORDER
|
|
else:
|
|
style = wx.SIMPLE_BORDER
|
|
|
|
back_color = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW)
|
|
|
|
content_sizer = wx.BoxSizer(wx.VERTICAL)
|
|
self.page_container = wx.Panel(self, style=style)
|
|
self.page_container.SetBackgroundColour(back_color)
|
|
content_sizer.Add(self.page_container, 1, wx.EXPAND, 5)
|
|
|
|
main_sizer.Add(tabs_sizer, 0, wx.EXPAND, 5)
|
|
main_sizer.Add(content_sizer, 1, wx.EXPAND | wx.BOTTOM, 2)
|
|
|
|
self.SetSizer(main_sizer)
|
|
self.Bind(wx.EVT_SIZE, self.OnSize)
|
|
self.Layout()
|
|
|
|
def GetPage(self, i):
|
|
return self._pages[i]
|
|
|
|
def SetPage(self, i, page):
|
|
if i >= len(self._pages) or i is None or page is None:
|
|
return
|
|
|
|
old_page = self._pages[i]
|
|
self._pages[i] = page
|
|
if old_page == self._active_page:
|
|
old_page.Destroy()
|
|
self._active_page = page
|
|
else:
|
|
old_page.Destroy()
|
|
|
|
page.Reparent(self.page_container)
|
|
|
|
if self._active_page == page:
|
|
self.ShowActive()
|
|
|
|
def GetBorders(self):
|
|
"""Gets border widths to better determine page size in ShowActive()"""
|
|
|
|
bx = wx.SystemSettings.GetMetric(wx.SYS_BORDER_X)
|
|
by = wx.SystemSettings.GetMetric(wx.SYS_BORDER_Y)
|
|
|
|
if bx < 0:
|
|
bx = 1
|
|
if by < 0:
|
|
by = 1
|
|
|
|
return bx, by
|
|
|
|
def ReplaceActivePage(self, page):
|
|
self.SetPage(self.GetSelection(), page)
|
|
|
|
def GetSelectedPage(self):
|
|
return self._active_page
|
|
|
|
def GetPageIndex(self, page):
|
|
return self._pages.index(page) if page in self._pages else None
|
|
|
|
def GetSelection(self):
|
|
return self.GetPageIndex(self._active_page)
|
|
|
|
def GetCurrentPage(self):
|
|
return self._active_page
|
|
|
|
def GetPageCount(self):
|
|
return len(self._pages)
|
|
|
|
def NextPage(self):
|
|
"""Used with keyboard shortcut for next page navigation"""
|
|
current_page = self.GetSelection()
|
|
|
|
if current_page is None:
|
|
return
|
|
|
|
if current_page < self.GetPageCount() - 1:
|
|
self.SetSelection(current_page + 1)
|
|
new_page = current_page + 1
|
|
else:
|
|
self.SetSelection(0)
|
|
new_page = 0
|
|
|
|
wx.PostEvent(self, PageChanged(current_page, new_page))
|
|
|
|
def PrevPage(self):
|
|
"""Used with keyboard shortcut for previous page navigation"""
|
|
current_page = self.GetSelection()
|
|
|
|
if current_page is None:
|
|
return
|
|
|
|
if current_page > 0:
|
|
self.SetSelection(current_page - 1)
|
|
new_page = current_page - 1
|
|
else:
|
|
self.SetSelection(self.GetPageCount() - 1)
|
|
new_page = self.GetPageCount() - 1
|
|
|
|
wx.PostEvent(self, PageChanged(current_page, new_page))
|
|
|
|
def AddPage(self, win=None, title=None, image: wx.Image=None, closeable=True):
|
|
title = title or "Empty Tab"
|
|
if self._active_page:
|
|
self._active_page.Hide()
|
|
|
|
if not win:
|
|
win = wx.Panel(self)
|
|
|
|
win.Reparent(self.page_container)
|
|
|
|
self.page_container.Layout()
|
|
|
|
self._pages.append(win)
|
|
self.tabs_container.AddTab(title, image, closeable)
|
|
|
|
self._active_page = win
|
|
self.ShowActive(True)
|
|
|
|
def DisablePage(self, page, toggle):
|
|
idx = self.GetPageIndex(page)
|
|
|
|
if toggle and page == self._active_page:
|
|
try:
|
|
# Set page to the first non-disabled page
|
|
self.SetSelection(next(i for i, _ in enumerate(self._pages) if not self.tabs_container.tabs[i].disabled))
|
|
except StopIteration:
|
|
self.SetSelection(0)
|
|
|
|
self.tabs_container.DisableTab(idx, toggle)
|
|
|
|
def SetSelection(self, page, focus=True):
|
|
old_selection = self.GetSelection()
|
|
if old_selection != page:
|
|
self._active_page.Hide()
|
|
self._active_page = self._pages[page]
|
|
self.tabs_container.SetSelected(page)
|
|
self.ShowActive()
|
|
if focus:
|
|
self._active_page.SetFocus()
|
|
wx.PostEvent(self, PageChanged(old_selection, page))
|
|
|
|
def DeletePage(self, n):
|
|
page = self._pages[n]
|
|
self._pages.remove(page)
|
|
page.Destroy()
|
|
|
|
self.tabs_container.DeleteTab(n)
|
|
|
|
selection = self.tabs_container.GetSelected()
|
|
if selection is not None:
|
|
self._active_page = self._pages[selection]
|
|
self.ShowActive()
|
|
wx.PostEvent(self, PageChanged(-1, selection))
|
|
else:
|
|
self._active_page = None
|
|
|
|
def SwitchPages(self, src, dst):
|
|
self._pages[src], self._pages[dst] = self._pages[dst], self._pages[src]
|
|
|
|
def ShowActive(self, resize_only=False):
|
|
"""
|
|
Sets the size of the page and shows. The sizing logic adjusts for some
|
|
minor sizing errors (scrollbars going beyond bounds)
|
|
|
|
resize_only
|
|
if we are not interested in showing the page, only setting the size
|
|
|
|
@todo: is resize_only still needed? Was introduced with 8b8b97 in mid
|
|
2011 to fix a resizing bug with blank _pages, cannot reproduce
|
|
13Sept2014
|
|
"""
|
|
|
|
ww, wh = self.page_container.GetSize()
|
|
bx, by = self.GetBorders()
|
|
ww -= bx * 4
|
|
wh -= by * 4
|
|
self._active_page.SetSize((max(ww, -1), max(wh, -1)))
|
|
self._active_page.SetPosition((0, 0))
|
|
|
|
if not resize_only:
|
|
self._active_page.Show()
|
|
|
|
self.Layout()
|
|
|
|
def IsActive(self, page):
|
|
return self._active_page == page
|
|
|
|
def SetPageTitle(self, i, text, refresh=True):
|
|
tab = self.tabs_container.tabs[i]
|
|
tab.baseText = text
|
|
if refresh:
|
|
self.tabs_container.AdjustTabsSize()
|
|
self.Refresh()
|
|
|
|
def SetPageTitleExtra(self, i, text, refresh=True):
|
|
tab = self.tabs_container.tabs[i]
|
|
tab.extraText = text
|
|
if refresh:
|
|
self.tabs_container.AdjustTabsSize()
|
|
self.Refresh()
|
|
|
|
def SetPageIcon(self, i, icon, refresh=True):
|
|
tab = self.tabs_container.tabs[i]
|
|
tab.tab_img = icon
|
|
if refresh:
|
|
self.tabs_container.AdjustTabsSize()
|
|
self.Refresh()
|
|
|
|
def SetPageTextIcon(self, i, text=wx.EmptyString, icon=None):
|
|
self.SetPageTitle(i, text, False)
|
|
self.SetPageIcon(i, icon, False)
|
|
self.tabs_container.AdjustTabsSize()
|
|
self.Refresh()
|
|
|
|
def Refresh(self):
|
|
self.tabs_container.Refresh()
|
|
|
|
def OnSize(self, event):
|
|
w, h = self.GetSize()
|
|
self.tabs_container.SetSize((w, -1))
|
|
self.tabs_container.UpdateSize()
|
|
self.tabs_container.Refresh()
|
|
self.Layout()
|
|
|
|
if self._active_page:
|
|
self.ShowActive()
|
|
event.Skip()
|
|
|
|
|
|
class _TabRenderer:
|
|
def __init__(self, size=(36, 24), text=wx.EmptyString, img: wx.Image=None,
|
|
closeable=True):
|
|
|
|
# tab left/right zones inclination
|
|
self.ctab_left = BitmapLoader.getImage("ctableft", "gui")
|
|
self.ctab_middle = BitmapLoader.getImage("ctabmiddle", "gui")
|
|
self.ctab_right = BitmapLoader.getImage("ctabright", "gui")
|
|
self.ctab_close = BitmapLoader.getImage("ctabclose", "gui")
|
|
|
|
self.left_width = self.ctab_left.GetWidth()
|
|
self.right_width = self.ctab_right.GetWidth()
|
|
self.middle_width = self.ctab_middle.GetWidth()
|
|
self.close_btn_width = self.ctab_close.GetWidth()
|
|
|
|
width, height = size
|
|
|
|
self.min_width = self.left_width + self.right_width + self.middle_width
|
|
self.min_height = self.ctab_middle.GetHeight()
|
|
|
|
# set minimum width and height to what is allotted to images
|
|
width = max(width, self.min_width)
|
|
height = max(height, self.min_height)
|
|
|
|
self._disabled = False
|
|
self.baseText = text
|
|
self.extraText = ''
|
|
self.tab_size = (width, height)
|
|
self.closeable = closeable
|
|
self.selected = False
|
|
self.close_btn_hovering = False
|
|
self.tab_bitmap = None
|
|
self.tab_back_bitmap = None
|
|
self.padding = 4
|
|
self.font = wx.Font(fonts.NORMAL, wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False)
|
|
|
|
self.tab_img = img
|
|
self.position = (0, 0) # Not used internally for rendering - helper for tab container
|
|
self.InitTab()
|
|
|
|
@property
|
|
def disabled(self):
|
|
return self._disabled
|
|
|
|
@disabled.setter
|
|
def disabled(self, value):
|
|
if value == self._disabled: # Avoid unnecessary re-rendering
|
|
return
|
|
|
|
self._disabled = value
|
|
self._Render()
|
|
|
|
@property
|
|
def text(self):
|
|
return self.baseText + self.extraText
|
|
|
|
def SetPosition(self, position):
|
|
self.position = position
|
|
|
|
def GetPosition(self):
|
|
return self.position
|
|
|
|
def GetSize(self):
|
|
return self.tab_size
|
|
|
|
def SetSize(self, size):
|
|
width, height = size
|
|
|
|
width = max(width, self.min_width)
|
|
height = max(height, self.min_height)
|
|
|
|
cur_width, cur_height = self.tab_size
|
|
if (width == cur_width) and (height == cur_height):
|
|
return
|
|
|
|
self.tab_size = (width, height)
|
|
self.InitTab()
|
|
|
|
def SetSelected(self, sel=True):
|
|
self.selected = sel
|
|
self.InitTab()
|
|
|
|
def GetSelected(self):
|
|
return self.selected
|
|
|
|
def IsSelected(self):
|
|
return self.selected
|
|
|
|
def ShowCloseButtonHovering(self, hover=True):
|
|
if self.close_btn_hovering != hover:
|
|
self.close_btn_hovering = hover
|
|
self._Render()
|
|
|
|
def GetCloseButtonHoverStatus(self):
|
|
return self.close_btn_hovering
|
|
|
|
def GetTabRegion(self):
|
|
new_region = self.CopyRegion(self.tab_region)
|
|
new_region.Subtract(self.close_region) if self.closeable else self.tab_region
|
|
return new_region
|
|
|
|
def GetCloseButtonRegion(self):
|
|
return self.CopyRegion(self.close_region)
|
|
|
|
def GetMinSize(self):
|
|
ebmp = wx.Bitmap(1, 1)
|
|
mdc = wx.MemoryDC()
|
|
mdc.SelectObject(ebmp)
|
|
mdc.SetFont(self.font)
|
|
textSizeX, textSizeY = mdc.GetTextExtent(self.text)
|
|
totalSize = self.left_width + self.right_width + textSizeX + self.close_btn_width / 2 + 16 + self.padding * 2
|
|
mdc.SelectObject(wx.NullBitmap)
|
|
return totalSize, self.tab_height
|
|
|
|
def SetTabImage(self, img):
|
|
self.tab_img = img
|
|
|
|
def CopyRegion(self, region):
|
|
rect = region.GetBox()
|
|
|
|
newRegion = wx.Region(rect.X, rect.Y, rect.Width, rect.Height)
|
|
newRegion.Intersect(region)
|
|
|
|
return newRegion
|
|
|
|
def InitTab(self):
|
|
self.tab_width, self.tab_height = self.tab_size
|
|
|
|
self.content_width = self.tab_width - self.left_width - self.right_width
|
|
self.tab_region = None
|
|
self.close_region = None
|
|
|
|
self.InitColors()
|
|
self.InitBitmaps()
|
|
|
|
self.ComposeTabBack()
|
|
self.InitTabRegions()
|
|
self._Render()
|
|
|
|
def InitBitmaps(self):
|
|
"""
|
|
Creates bitmap for tab
|
|
|
|
Takes the bitmaps already set and replaces a known color (black) with
|
|
the needed color, while also considering selected state. Color dependant
|
|
on platform -- see InitColors().
|
|
"""
|
|
if self.selected:
|
|
tr, tg, tb, ta = self.selected_color
|
|
else:
|
|
tr, tg, tb, ta = self.inactive_color
|
|
|
|
ctab_left = self.ctab_left.Copy()
|
|
ctab_right = self.ctab_right.Copy()
|
|
ctab_middle = self.ctab_middle.Copy()
|
|
|
|
ctab_left.Replace(0, 0, 0, tr, tg, tb)
|
|
ctab_right.Replace(0, 0, 0, tr, tg, tb)
|
|
ctab_middle.Replace(0, 0, 0, tr, tg, tb)
|
|
|
|
self.ctab_left_bmp = wx.Bitmap(ctab_left)
|
|
self.ctab_right_bmp = wx.Bitmap(ctab_right)
|
|
self.ctab_middle_bmp = wx.Bitmap(ctab_middle)
|
|
self.ctab_close_bmp = wx.Bitmap(self.ctab_close)
|
|
|
|
def ComposeTabBack(self):
|
|
"""
|
|
Creates the tab background bitmap based upon calculated dimension values
|
|
and modified bitmaps via InitBitmaps()
|
|
"""
|
|
bk_bmp = wx.Bitmap(round(self.tab_width), round(self.tab_height))
|
|
|
|
mdc = wx.MemoryDC()
|
|
mdc.SelectObject(bk_bmp)
|
|
mdc.Clear()
|
|
|
|
# draw the left bitmap
|
|
mdc.DrawBitmap(self.ctab_left_bmp, 0, 0)
|
|
|
|
# convert middle bitmap and scale to tab width
|
|
cm = self.ctab_middle_bmp.ConvertToImage()
|
|
mimg = cm.Scale(round(self.content_width), round(self.ctab_middle.GetHeight()),
|
|
wx.IMAGE_QUALITY_NORMAL)
|
|
mbmp = wx.Bitmap(mimg)
|
|
|
|
# draw middle bitmap, offset by left
|
|
mdc.DrawBitmap(mbmp, round(self.left_width), 0)
|
|
|
|
# draw right bitmap offset by left + middle
|
|
mdc.DrawBitmap(self.ctab_right_bmp,
|
|
round(self.content_width + self.left_width), 0)
|
|
|
|
mdc.SelectObject(wx.NullBitmap)
|
|
|
|
if self.tab_back_bitmap:
|
|
del self.tab_back_bitmap
|
|
|
|
self.tab_back_bitmap = bk_bmp
|
|
|
|
def InitTabRegions(self):
|
|
"""
|
|
Initializes regions for tab, which makes it easier to determine if
|
|
given coordinates are included in a region
|
|
"""
|
|
self.tab_region = wx.Region(self.tab_back_bitmap)
|
|
self.close_region = wx.Region(self.ctab_close_bmp)
|
|
|
|
x_offset = self.content_width \
|
|
+ self.left_width \
|
|
- self.ctab_close_bmp.GetWidth() / 2
|
|
y_offset = (self.tab_height - self.ctab_close_bmp.GetHeight()) / 2
|
|
self.close_region.Offset(round(x_offset), round(y_offset))
|
|
|
|
def InitColors(self):
|
|
"""Determines colors used for tab, based on system settings"""
|
|
self.tab_color = wx.SystemSettings.GetColour(wx.SYS_COLOUR_3DFACE)
|
|
self.inactive_color = color_utils.GetSuitable(self.tab_color, 0.25)
|
|
self.selected_color = color_utils.GetSuitable(self.tab_color, 0.10)
|
|
|
|
def Render(self):
|
|
return self.tab_bitmap
|
|
|
|
def _Render(self):
|
|
"""Renders the tab, complete with the icon, text, and close button"""
|
|
if self.tab_bitmap:
|
|
del self.tab_bitmap
|
|
|
|
height = self.tab_height
|
|
|
|
canvas = wx.Bitmap(round(self.tab_width), round(self.tab_height), 24)
|
|
|
|
mdc = wx.MemoryDC()
|
|
|
|
mdc.SelectObject(canvas)
|
|
mdc.Clear()
|
|
|
|
mdc.DrawBitmap(self.tab_back_bitmap, 0, 0, True)
|
|
|
|
# draw the tab icon
|
|
if self.tab_img:
|
|
bmp = wx.Bitmap(self.tab_img.ConvertToGreyscale() if self.disabled else self.tab_img)
|
|
# @todo: is this conditional relevant anymore?
|
|
if self.content_width > 16:
|
|
# Draw tab icon
|
|
mdc.DrawBitmap(
|
|
bmp,
|
|
round(self.left_width + self.padding - bmp.GetWidth() / 2),
|
|
round((height - bmp.GetHeight()) / 2))
|
|
|
|
# draw close button
|
|
if self.closeable:
|
|
if self.close_btn_hovering:
|
|
cbmp = self.ctab_close_bmp
|
|
else:
|
|
cimg = self.ctab_close_bmp.ConvertToImage()
|
|
cimg = cimg.AdjustChannels(0.7, 0.7, 0.7, 0.3)
|
|
cbmp = wx.Bitmap(cimg)
|
|
|
|
mdc.DrawBitmap(
|
|
cbmp,
|
|
round(self.content_width + self.left_width - cbmp.GetWidth() / 2),
|
|
round((height - cbmp.GetHeight()) / 2))
|
|
|
|
mdc.SelectObject(wx.NullBitmap)
|
|
|
|
canvas.SetMaskColour((0x12, 0x23, 0x32))
|
|
img = canvas.ConvertToImage()
|
|
|
|
if not img.HasAlpha():
|
|
img.InitAlpha()
|
|
|
|
bmp = wx.Bitmap(img)
|
|
self.tab_bitmap = bmp
|
|
|
|
# We draw the text separately in order to draw it directly on the native DC, rather than a memory one, because
|
|
# drawing text on a memory DC draws it blurry on HD/Retina screens
|
|
def DrawText(self, dc):
|
|
height = self.tab_height
|
|
dc.SetFont(self.font)
|
|
|
|
if self.tab_img:
|
|
text_start = self.left_width + self.padding + self.tab_img.GetWidth() / 2
|
|
else:
|
|
text_start = self.left_width
|
|
|
|
maxsize = self.tab_width \
|
|
- text_start \
|
|
- self.right_width \
|
|
- self.padding * 4
|
|
color = self.selected_color if self.selected else self.inactive_color
|
|
|
|
dc.SetTextForeground(color_utils.GetSuitable(color, 1))
|
|
|
|
# draw text (with no ellipses)
|
|
text = draw.GetPartialText(dc, self.text, maxsize, "")
|
|
tx, ty = dc.GetTextExtent(text)
|
|
dc.DrawText(text, round(text_start + self.padding), round(height / 2 - ty / 2))
|
|
|
|
def __repr__(self):
|
|
return "_TabRenderer(text={}, disabled={}) at {}".format(
|
|
self.text, self.disabled, hex(id(self))
|
|
)
|
|
|
|
|
|
class _AddRenderer:
|
|
def __init__(self):
|
|
"""Renders the add tab button"""
|
|
self.add_img = BitmapLoader.getImage("ctabadd", "gui")
|
|
self.width = self.add_img.GetWidth()
|
|
self.height = self.add_img.GetHeight()
|
|
|
|
self.region = None
|
|
self.tbmp = wx.Bitmap(self.add_img)
|
|
self.add_bitmap = None
|
|
|
|
self.position = (0, 0)
|
|
self.highlighted = False
|
|
|
|
self.InitRenderer()
|
|
|
|
def GetPosition(self):
|
|
return self.position
|
|
|
|
def SetPosition(self, pos):
|
|
self.position = pos
|
|
|
|
def GetSize(self):
|
|
return self.width, self.height
|
|
|
|
def GetHeight(self):
|
|
return self.height
|
|
|
|
def GetWidth(self):
|
|
return self.width
|
|
|
|
def InitRenderer(self):
|
|
self.region = self.CreateRegion()
|
|
self._Render()
|
|
|
|
def CreateRegion(self):
|
|
region = wx.Region(self.tbmp)
|
|
return region
|
|
|
|
def CopyRegion(self, region):
|
|
rect = region.GetBox()
|
|
|
|
new_region = wx.Region(rect.X, rect.Y, rect.Width, rect.Height)
|
|
new_region.Intersect(region)
|
|
|
|
return new_region
|
|
|
|
def GetRegion(self):
|
|
return self.CopyRegion(self.region)
|
|
|
|
def Highlight(self, highlight=False):
|
|
self.highlighted = highlight
|
|
self._Render()
|
|
|
|
def IsHighlighted(self):
|
|
return self.highlighted
|
|
|
|
def Render(self):
|
|
return self.add_bitmap
|
|
|
|
def _Render(self):
|
|
if self.add_bitmap:
|
|
del self.add_bitmap
|
|
|
|
alpha = 1 if self.highlighted else 0.3
|
|
|
|
img = self.add_img.AdjustChannels(1, 1, 1, alpha)
|
|
bmp = wx.Bitmap(img)
|
|
self.add_bitmap = bmp
|
|
|
|
|
|
class _TabsContainer(wx.Panel):
|
|
def __init__(self, parent, pos=(50, 0), size=(100, 22), id=wx.ID_ANY,
|
|
can_add=True, tabWidthMode=0):
|
|
"""
|
|
Defines the tab container. Handles functions such as tab selection and
|
|
dragging, and defines minimum width of tabs (all tabs are of equal
|
|
width, which is determined via widest tab). Also handles the tab
|
|
preview, if any.
|
|
"""
|
|
super().__init__(parent, id, pos, size)
|
|
self.tabWidthMode = tabWidthMode
|
|
|
|
self.tabs = []
|
|
self.width, self.height = size
|
|
self.container_height = self.height
|
|
self.start_drag = False
|
|
self.dragging = False
|
|
|
|
# amount of overlap of tabs?
|
|
self.inclination = 7
|
|
|
|
if can_add:
|
|
self.reserved = 48
|
|
else:
|
|
self.reserved = self.inclination * 4
|
|
|
|
# pixel distance to drag before we actually start dragging
|
|
self.drag_trail = 5
|
|
|
|
self.dragx = 0
|
|
self.dragy = 0
|
|
self.dragged_tab = None
|
|
self.drag_trigger = self.drag_trail
|
|
|
|
self.show_add_button = can_add
|
|
|
|
self.tab_container_width = self.width - self.reserved
|
|
self.fxBmps = {}
|
|
|
|
self.add_button = _AddRenderer()
|
|
self.add_bitmap = self.add_button.Render()
|
|
|
|
self.preview_timer = None
|
|
self.preview_timer_id = wx.ID_ANY
|
|
self.preview_wnd = None
|
|
self.preview_bmp = None
|
|
self.preview_pos = None
|
|
self.preview_tab = None
|
|
|
|
self.Bind(wx.EVT_TIMER, self.OnTimer)
|
|
self.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeaveWindow)
|
|
|
|
self.Bind(wx.EVT_PAINT, self.OnPaint)
|
|
self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnErase)
|
|
self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
|
|
self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp)
|
|
self.Bind(wx.EVT_MIDDLE_UP, self.OnMiddleUp)
|
|
self.Bind(wx.EVT_MOTION, self.OnMotion)
|
|
self.Bind(wx.EVT_SIZE, self.OnSize)
|
|
self.Bind(wx.EVT_SYS_COLOUR_CHANGED, self.OnSysColourChanged)
|
|
|
|
self.SetBackgroundStyle(wx.BG_STYLE_PAINT)
|
|
|
|
def OnSysColourChanged(self, event):
|
|
for tab in self.tabs:
|
|
tab.InitTab()
|
|
self.Refresh()
|
|
|
|
def OnSize(self, event):
|
|
self.UpdateSize()
|
|
event.Skip()
|
|
|
|
def UpdateSize(self):
|
|
"""Update tab sizes based on new tab container size"""
|
|
width, _ = self.GetSize()
|
|
if width != self.width:
|
|
self.width = width
|
|
self.tab_container_width = self.width - self.reserved
|
|
self.AdjustTabsSize()
|
|
|
|
def OnLeftDown(self, event):
|
|
""" Select tab on mouse down and start dragging logic """
|
|
mposx, mposy = event.GetPosition()
|
|
if not self.start_drag:
|
|
tab = self.FindTabAtPos(mposx, mposy)
|
|
if tab:
|
|
self.CheckTabSelected(tab, mposx, mposy)
|
|
if self.show_add_button:
|
|
# If we can add tabs, we can drag them. Set flag
|
|
self.start_drag = True
|
|
tx, ty = tab.GetPosition()
|
|
self.dragx = mposx - tx
|
|
self.dragy = self.container_height - self.height
|
|
self.Refresh()
|
|
|
|
self.dragged_tab = tab
|
|
|
|
def OnMiddleUp(self, event):
|
|
mposx, mposy = event.GetPosition()
|
|
|
|
tab = self.FindTabAtPos(mposx, mposy)
|
|
|
|
if tab is None or not tab.closeable: # if not able to close, return False
|
|
return False
|
|
|
|
index = self.tabs.index(tab)
|
|
ev = PageClosing(index)
|
|
wx.PostEvent(self.Parent, ev)
|
|
|
|
if ev.isVetoed():
|
|
return False
|
|
|
|
index = self.GetTabIndex(tab)
|
|
self.Parent.DeletePage(index)
|
|
wx.PostEvent(self.Parent, PageClosed(index=index))
|
|
|
|
sel = self.GetSelected()
|
|
if sel is not None:
|
|
wx.PostEvent(self.Parent, PageChanged(-1, sel))
|
|
|
|
def OnMotion(self, event):
|
|
"""
|
|
Determines what happens when the mouse moves. This handles primarily
|
|
dragging (region tab can be dragged) as well as checking if we are over
|
|
an actionable button.
|
|
"""
|
|
mposx, mposy = event.GetPosition()
|
|
|
|
if self.start_drag:
|
|
if not self.dragging:
|
|
if self.drag_trigger < 0:
|
|
self.dragging = True
|
|
self.drag_trigger = self.drag_trail
|
|
self.CaptureMouse()
|
|
else:
|
|
self.drag_trigger -= 1
|
|
if self.dragging:
|
|
# we wish to keep tab within tab container boundaries. To do
|
|
# this, we must detect when mouse moves outside of boundaries.
|
|
# Set hard limits to 0 and the size of tab container.
|
|
dtx = mposx - self.dragx
|
|
w, h = self.dragged_tab.GetSize()
|
|
|
|
dtx = max(dtx, 0)
|
|
|
|
if dtx + w > self.tab_container_width + self.inclination * 2:
|
|
dtx = self.tab_container_width - w + self.inclination * 2
|
|
|
|
self.dragged_tab.SetPosition((dtx, self.dragy))
|
|
|
|
# we must modify the surrounding tabs
|
|
index = self.GetTabIndex(self.dragged_tab)
|
|
|
|
left_tab = self.GetTabAtLeft(index)
|
|
right_tab = self.GetTabAtRight(index)
|
|
|
|
if left_tab:
|
|
lw, lh = left_tab.GetSize()
|
|
lx, ly = left_tab.GetPosition()
|
|
|
|
if lx + lw / 2 - self.inclination * 2 > dtx:
|
|
self.SwitchTabs(index - 1, index, self.dragged_tab)
|
|
|
|
if right_tab:
|
|
rw, rh = right_tab.GetSize()
|
|
rx, ry = right_tab.GetPosition()
|
|
|
|
if rx + rw / 2 + self.inclination * 2 < dtx + w:
|
|
self.SwitchTabs(index + 1, index, self.dragged_tab)
|
|
|
|
self.UpdateTabsPosition(self.dragged_tab)
|
|
self.Refresh()
|
|
return
|
|
|
|
# If we aren't dragging, check for actionable buttons under mouse
|
|
self.CheckCloseHighlighted(mposx, mposy)
|
|
self.CheckAddHighlighted(mposx, mposy)
|
|
self.CheckTabPreview(mposx, mposy)
|
|
|
|
event.Skip()
|
|
|
|
def OnLeftUp(self, event):
|
|
"""Determines what happens when user left clicks (up)"""
|
|
mposx, mposy = event.GetPosition()
|
|
if self.start_drag and self.dragging:
|
|
self.dragging = False
|
|
self.start_drag = False
|
|
self.dragged_tab = None
|
|
self.drag_trigger = self.drag_trail
|
|
self.UpdateTabsPosition()
|
|
self.Refresh()
|
|
|
|
if self.HasCapture():
|
|
self.ReleaseMouse()
|
|
|
|
return
|
|
|
|
if self.start_drag:
|
|
self.start_drag = False
|
|
self.drag_trigger = self.drag_trail
|
|
|
|
# Checks if we selected the add button and, if True, returns
|
|
if self.CheckAddButton(mposx, mposy):
|
|
return
|
|
|
|
# If there are no tabs, don't waste time
|
|
if self.GetTabsCount() == 0:
|
|
return
|
|
|
|
# Gets selected tab (was set when user down clicked)
|
|
sel_tab = self.GetSelectedTab()
|
|
|
|
# Check if we selected close button for selected tab
|
|
if self.CheckTabClose(sel_tab, mposx, mposy):
|
|
return
|
|
|
|
# Check if we selected close button for all others
|
|
for tab in self.tabs:
|
|
if self.CheckTabClose(tab, mposx, mposy):
|
|
return
|
|
|
|
def DisableTab(self, tab, disabled=True):
|
|
tb_renderer = self.tabs[tab]
|
|
tb_renderer.disabled = disabled
|
|
|
|
self.Refresh()
|
|
|
|
def GetSelectedTab(self):
|
|
for tab in self.tabs:
|
|
if tab.GetSelected():
|
|
return tab
|
|
return None
|
|
|
|
def GetSelected(self):
|
|
for tab in self.tabs:
|
|
if tab.GetSelected():
|
|
return self.tabs.index(tab)
|
|
return None
|
|
|
|
def SetSelected(self, tabIndex):
|
|
"""Set tab as selected given its index"""
|
|
old_sel_tab = self.GetSelectedTab()
|
|
old_sel_tab.SetSelected(False)
|
|
self.tabs[tabIndex].SetSelected(True)
|
|
self.Refresh()
|
|
|
|
def CheckTabSelected(self, tab, x, y):
|
|
"""
|
|
Selects the tab at x, y. If the tab at x, y is already selected, simply
|
|
return true. Otherwise, perform TabHitTest and set tab at position to
|
|
selected
|
|
"""
|
|
old_sel_tab = self.GetSelectedTab()
|
|
if old_sel_tab == tab:
|
|
return True
|
|
|
|
if self.TabHitTest(tab, x, y):
|
|
if tab.disabled:
|
|
return
|
|
tab.SetSelected(True)
|
|
old_sel_tab.SetSelected(False)
|
|
|
|
ev = PageChanging(self.tabs.index(old_sel_tab), self.tabs.index(tab))
|
|
wx.PostEvent(self.Parent, ev)
|
|
|
|
if ev.isVetoed():
|
|
return False
|
|
|
|
self.Refresh()
|
|
sel_tab = self.tabs.index(tab)
|
|
self.Parent.SetSelection(sel_tab)
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
def CheckTabClose(self, tab, x, y):
|
|
"""
|
|
Determines if close button was selected for the given tab by comparing
|
|
x and y position with known close button region
|
|
"""
|
|
if not tab.closeable: # if not able to close, return False
|
|
return False
|
|
|
|
region = tab.GetCloseButtonRegion()
|
|
posx, posy = tab.GetPosition()
|
|
region.Offset(round(posx), round(posy))
|
|
|
|
if region.Contains(x, y):
|
|
index = self.tabs.index(tab)
|
|
ev = PageClosing(index)
|
|
wx.PostEvent(self.Parent, ev)
|
|
|
|
if ev.isVetoed():
|
|
return False
|
|
|
|
index = self.GetTabIndex(tab)
|
|
self.Parent.DeletePage(index)
|
|
wx.PostEvent(self.Parent, PageClosed(index=index))
|
|
|
|
sel = self.GetSelected()
|
|
if sel is not None:
|
|
wx.PostEvent(self.Parent, PageChanged(-1, sel))
|
|
|
|
return True
|
|
return False
|
|
|
|
def CheckAddButton(self, x, y):
|
|
"""
|
|
Determines if add button was selected by comparing x and y position with
|
|
add button region
|
|
"""
|
|
if not self.show_add_button: # if not able to add, return False
|
|
return
|
|
|
|
region = self.add_button.GetRegion()
|
|
ax, ay = self.add_button.GetPosition()
|
|
region.Offset(round(ax), round(ay))
|
|
|
|
if region.Contains(x, y):
|
|
ev = PageAdding()
|
|
wx.PostEvent(self.Parent, ev)
|
|
if ev.isVetoed():
|
|
return False
|
|
|
|
self.Parent.AddPage()
|
|
wx.PostEvent(self.Parent, PageAdded())
|
|
return True
|
|
|
|
def CheckCloseHighlighted(self, x, y):
|
|
"""
|
|
Checks if mouse pos at x, y is over a close button. If so, set the
|
|
close hovering flag for that tab
|
|
"""
|
|
dirty = False
|
|
|
|
for tab in self.tabs:
|
|
region = tab.GetCloseButtonRegion()
|
|
posx, posy = tab.GetPosition()
|
|
region.Offset(round(posx), round(posy))
|
|
|
|
if region.Contains(x, y):
|
|
if not tab.GetCloseButtonHoverStatus():
|
|
tab.ShowCloseButtonHovering(True)
|
|
dirty = True
|
|
else:
|
|
if tab.GetCloseButtonHoverStatus():
|
|
tab.ShowCloseButtonHovering(False)
|
|
dirty = True
|
|
if dirty:
|
|
self.Refresh()
|
|
break
|
|
|
|
def FindTabAtPos(self, x, y):
|
|
if self.GetTabsCount() == 0:
|
|
return None
|
|
|
|
# test current tab first
|
|
sel_tab = self.GetSelectedTab()
|
|
if self.TabHitTest(sel_tab, x, y):
|
|
return sel_tab
|
|
|
|
# test all other tabs next
|
|
for tab in self.tabs:
|
|
if self.TabHitTest(tab, x, y):
|
|
return tab
|
|
|
|
return None
|
|
|
|
def TabHitTest(self, tab, x, y):
|
|
""" Test if x and y are contained within a tabs region """
|
|
tabRegion = tab.GetTabRegion()
|
|
tabPos = tab.GetPosition()
|
|
tabPosX, tabPosY = tabPos
|
|
tabRegion.Offset(round(tabPosX), round(tabPosY))
|
|
|
|
if tabRegion.Contains(x, y):
|
|
return True
|
|
|
|
return False
|
|
|
|
def GetTabAtLeft(self, tab_index):
|
|
return self.tabs[tab_index - 1] if tab_index > 0 else None
|
|
|
|
def GetTabAtRight(self, tab_index):
|
|
if tab_index < self.GetTabsCount() - 1:
|
|
return self.tabs[tab_index + 1]
|
|
else:
|
|
return None
|
|
|
|
def SwitchTabs(self, src, dst, dragged_tab=None):
|
|
self.tabs[src], self.tabs[dst] = self.tabs[dst], self.tabs[src]
|
|
self.UpdateTabsPosition(dragged_tab)
|
|
self.Parent.SwitchPages(src, dst)
|
|
self.Refresh()
|
|
|
|
def GetTabIndex(self, tab):
|
|
return self.tabs.index(tab)
|
|
|
|
def CheckTabPreview(self, mposx, mposy):
|
|
"""
|
|
Checks to see if we have a tab preview and sets up the timer for it
|
|
to display
|
|
"""
|
|
sFit = Fit.getInstance()
|
|
if not sFit.serviceFittingOptions["showTooltip"] or False:
|
|
return
|
|
|
|
if self.preview_timer:
|
|
if self.preview_timer.IsRunning():
|
|
if self.preview_wnd:
|
|
self.preview_timer.Stop()
|
|
return
|
|
|
|
if self.preview_wnd:
|
|
self.preview_wnd.Show(False)
|
|
del self.preview_wnd
|
|
self.preview_wnd = None
|
|
|
|
for tab in self.tabs:
|
|
if not tab.GetSelected():
|
|
if self.TabHitTest(tab, mposx, mposy):
|
|
try:
|
|
page = self.Parent.GetPage(self.GetTabIndex(tab))
|
|
if hasattr(page, "Snapshot"):
|
|
if not self.preview_timer:
|
|
self.preview_timer = wx.Timer(
|
|
self, self.preview_timer_id)
|
|
|
|
self.preview_tab = tab
|
|
self.preview_timer.Start(500, True)
|
|
break
|
|
except (KeyboardInterrupt, SystemExit):
|
|
raise
|
|
except:
|
|
pass
|
|
|
|
def CheckAddHighlighted(self, x, y):
|
|
"""
|
|
Checks to see if x, y are in add button region, and sets the highlight
|
|
flag
|
|
"""
|
|
if not self.show_add_button:
|
|
return
|
|
|
|
region = self.add_button.GetRegion()
|
|
ax, ay = self.add_button.GetPosition()
|
|
region.Offset(round(ax), round(ay))
|
|
|
|
if region.Contains(x, y):
|
|
if not self.add_button.IsHighlighted():
|
|
self.add_button.Highlight(True)
|
|
self.Refresh()
|
|
else:
|
|
if self.add_button.IsHighlighted():
|
|
self.add_button.Highlight(False)
|
|
self.Refresh()
|
|
|
|
def OnPaint(self, event):
|
|
mdc = wx.AutoBufferedPaintDC(self)
|
|
|
|
# if 'wxMac' in wx.PlatformInfo:
|
|
# color = wx.Colour(0, 0, 0)
|
|
# brush = wx.Brush(color)
|
|
# # @todo: what needs to be changed with wxPheonix?
|
|
# from Carbon.Appearance import kThemeBrushDialogBackgroundActive
|
|
# brush.MacSetTheme(kThemeBrushDialogBackgroundActive)
|
|
# else:
|
|
color = wx.SystemSettings.GetColour(wx.SYS_COLOUR_3DFACE)
|
|
brush = wx.Brush(color)
|
|
|
|
if "wxGTK" not in wx.PlatformInfo:
|
|
mdc.SetBackground(brush)
|
|
mdc.Clear()
|
|
|
|
selected = None
|
|
|
|
if self.show_add_button:
|
|
ax, ay = self.add_button.GetPosition()
|
|
mdc.DrawBitmap(self.add_button.Render(), round(ax), round(ay), True)
|
|
|
|
for i in range(len(self.tabs) - 1, -1, -1):
|
|
tab = self.tabs[i]
|
|
posx, posy = tab.GetPosition()
|
|
|
|
if not tab.IsSelected():
|
|
# drop shadow first
|
|
mdc.DrawBitmap(self.fxBmps[tab], round(posx), (posy), True)
|
|
bmp = tab.Render()
|
|
img = bmp.ConvertToImage()
|
|
img = img.AdjustChannels(1, 1, 1, 0.85)
|
|
bmp = wx.Bitmap(img)
|
|
mdc.DrawBitmap(bmp, round(posx), (posy), True)
|
|
|
|
mdc.SetDeviceOrigin(posx, posy)
|
|
tab.DrawText(mdc)
|
|
mdc.SetDeviceOrigin(0, 0)
|
|
else:
|
|
selected = tab
|
|
|
|
# we draw selected tab separately (instead of in else) so as to not hide
|
|
# the front bit behind the preceding tab
|
|
if selected:
|
|
posx, posy = selected.GetPosition()
|
|
# drop shadow first
|
|
mdc.DrawBitmap(self.fxBmps[selected], round(posx), round(posy), True)
|
|
|
|
bmp = selected.Render()
|
|
|
|
if self.dragging:
|
|
img = bmp.ConvertToImage()
|
|
img = img.AdjustChannels(1.2, 1.2, 1.2, 0.7)
|
|
bmp = wx.Bitmap(img)
|
|
|
|
mdc.DrawBitmap(bmp, round(posx), round(posy), True)
|
|
|
|
mdc.SetDeviceOrigin(posx, posy)
|
|
selected.DrawText(mdc)
|
|
mdc.SetDeviceOrigin(0, 0)
|
|
|
|
def OnErase(self, event):
|
|
pass
|
|
|
|
def UpdateTabFX(self):
|
|
""" Updates tab drop shadow bitmap """
|
|
self.fxBmps.clear()
|
|
for tab in self.tabs:
|
|
tabW, tabH = tab.tab_size
|
|
self.fxBmps[tab] = self.GetTabFx(tabW, self.height + 1)
|
|
|
|
@lru_cache(maxsize=50)
|
|
def GetTabFx(self, width, height):
|
|
renderer = _TabRenderer((width, height))
|
|
fx_bmp = renderer.Render()
|
|
img = fx_bmp.ConvertToImage()
|
|
if not img.HasAlpha():
|
|
img.InitAlpha()
|
|
img = img.Blur(2)
|
|
img = img.AdjustChannels(0.3, 0.3, 0.3, 0.35)
|
|
return wx.Bitmap(img)
|
|
|
|
def AddTab(self, title=wx.EmptyString, img=None, closeable=False):
|
|
self.ClearTabsSelected()
|
|
|
|
tab_renderer = _TabRenderer((200, self.height), title, img, closeable)
|
|
tab_renderer.SetSelected(True)
|
|
|
|
self.tabs.append(tab_renderer)
|
|
self.AdjustTabsSize()
|
|
self.Refresh()
|
|
|
|
def ClearTabsSelected(self):
|
|
for tab in self.tabs:
|
|
tab.SetSelected(False)
|
|
|
|
def DeleteTab(self, tab):
|
|
tab_renderer = self.tabs[tab]
|
|
was_selected = tab_renderer.GetSelected()
|
|
self.tabs.remove(tab_renderer)
|
|
|
|
if tab_renderer:
|
|
del tab_renderer
|
|
|
|
# determine our new selection
|
|
if was_selected and self.GetTabsCount() > 0:
|
|
if tab > self.GetTabsCount() - 1:
|
|
self.tabs[self.GetTabsCount() - 1].SetSelected(True)
|
|
else:
|
|
self.tabs[tab].SetSelected(True)
|
|
|
|
self.AdjustTabsSize()
|
|
self.Refresh()
|
|
|
|
def GetTabsCount(self):
|
|
return len(self.tabs)
|
|
|
|
def AdjustTabsSize(self):
|
|
"""
|
|
Adjust tab sizes to ensure that they are all consistent and can fit into
|
|
the tab container.
|
|
"""
|
|
if self.tabWidthMode == 1:
|
|
if self.GetTabsCount() > 0:
|
|
availableW = self.tab_container_width
|
|
overlapSavedW = max(0, len(self.tabs)) * self.inclination * 2
|
|
tabsGrouped = {}
|
|
for tab in self.tabs:
|
|
tabW, _ = tab.GetMinSize()
|
|
tabsGrouped.setdefault(math.ceil(tabW), []).append(tab)
|
|
clippedTabs = []
|
|
clipW = max(tabsGrouped, default=0)
|
|
|
|
def getUnclippedW():
|
|
unclippedW = 0
|
|
for w, tabs in tabsGrouped.items():
|
|
unclippedW += w * len(tabs)
|
|
return unclippedW
|
|
while tabsGrouped:
|
|
# Check if we're within width limit
|
|
neededW = 0
|
|
for w, tabs in tabsGrouped.items():
|
|
neededW += w * len(tabs)
|
|
if clippedTabs:
|
|
neededW += clipW * len(clippedTabs)
|
|
if neededW <= availableW + overlapSavedW:
|
|
break
|
|
# If we're not, extract widest tab group and mark it for clipping
|
|
currentTabs = tabsGrouped.pop(max(tabsGrouped))
|
|
clippedTabs.extend(currentTabs)
|
|
proposedClipWidth = math.floor((availableW + overlapSavedW - getUnclippedW()) / len(clippedTabs))
|
|
if not tabsGrouped or proposedClipWidth >= max(tabsGrouped, default=0):
|
|
clipW = max(0, proposedClipWidth)
|
|
break
|
|
else:
|
|
clipW = max(tabsGrouped)
|
|
# Assign width for unclipped tabs
|
|
for w, tabs in tabsGrouped.items():
|
|
for tab in tabs:
|
|
tab.SetSize((w, self.height))
|
|
if clippedTabs:
|
|
# Some width remains to be used due to rounding to integer
|
|
extraWTotal = availableW + overlapSavedW - getUnclippedW() - clipW * len(clippedTabs)
|
|
extraWPerTab = math.ceil(extraWTotal / len(clippedTabs))
|
|
# Assign width for clipped tabs
|
|
for tab in clippedTabs:
|
|
extraW = min(extraWTotal, extraWPerTab)
|
|
extraWTotal -= extraW
|
|
tab.SetSize((clipW + extraW, self.height))
|
|
else:
|
|
# first we loop through our tabs and calculate the the largest tab. This
|
|
# is the size that we will base our calculations off
|
|
max_width = 100 # Tab should be at least 100
|
|
for tab in self.tabs:
|
|
mw, _ = tab.GetMinSize() # Tab min size includes tab contents
|
|
max_width = max(mw, max_width)
|
|
|
|
tabWidth = 0
|
|
# Divide tab container by number of tabs and add inclination. This will
|
|
# return the ideal max size for the containers size
|
|
if self.GetTabsCount() > 0:
|
|
dx = self.tab_container_width / self.GetTabsCount() + self.inclination * 2
|
|
tabWidth = min(dx, max_width)
|
|
|
|
# Apply new size to all tabs
|
|
for tab in self.tabs:
|
|
tab.SetSize((tabWidth, self.height))
|
|
|
|
# update drop shadow based on new sizes
|
|
self.UpdateTabFX()
|
|
self.UpdateTabsPosition()
|
|
|
|
def UpdateTabsPosition(self, skip_tab=None):
|
|
tabsWidth = 0
|
|
for tab in self.tabs:
|
|
tabsWidth += tab.tab_width - self.inclination * 2
|
|
|
|
pos = tabsWidth
|
|
selected = None
|
|
for i in range(len(self.tabs) - 1, -1, -1):
|
|
tab = self.tabs[i]
|
|
width = tab.tab_width - self.inclination * 2
|
|
pos -= width
|
|
if not tab.IsSelected():
|
|
tab.SetPosition((pos, self.container_height - self.height))
|
|
else:
|
|
selected = tab
|
|
selpos = pos
|
|
|
|
if selected is not skip_tab:
|
|
selected.SetPosition((selpos, self.container_height - self.height))
|
|
|
|
self.add_button.SetPosition((round(tabsWidth) + self.inclination * 2,
|
|
self.container_height - self.height / 2 - self.add_button.GetHeight() / 3))
|
|
|
|
def OnLeaveWindow(self, event):
|
|
if self.start_drag and not self.dragging:
|
|
self.dragging = False
|
|
self.start_drag = False
|
|
self.dragged_tab = None
|
|
self.drag_trigger = self.drag_trail
|
|
if self.HasCapture():
|
|
self.ReleaseMouse()
|
|
|
|
if self.preview_wnd:
|
|
self.preview_wnd.Show(False)
|
|
del self.preview_wnd
|
|
self.preview_wnd = None
|
|
event.Skip()
|
|
|
|
def OnTimer(self, event):
|
|
mposx, mposy = wx.GetMousePosition()
|
|
cposx, cposy = self.ScreenToClient((mposx, mposy))
|
|
|
|
if self.FindTabAtPos(cposx, cposy) == self.preview_tab:
|
|
if not self.preview_tab.GetSelected():
|
|
page = self.Parent.GetPage(self.GetTabIndex(self.preview_tab))
|
|
if page.Snapshot():
|
|
self.preview_wnd = PFNotebookPagePreview(
|
|
self,
|
|
(mposx + 3, mposy + 3),
|
|
page.Snapshot(),
|
|
self.preview_tab.text)
|
|
self.preview_wnd.Show()
|
|
event.Skip()
|
|
|
|
|
|
class PFNotebookPagePreview(wx.Frame):
|
|
def __init__(self, parent, pos, bitmap, title):
|
|
super().__init__(parent, id=wx.ID_ANY, title=wx.EmptyString, pos=pos,
|
|
size=wx.DefaultSize, style=wx.NO_BORDER |
|
|
wx.FRAME_NO_TASKBAR |
|
|
wx.STAY_ON_TOP)
|
|
self.SetBackgroundStyle(wx.BG_STYLE_PAINT)
|
|
self.title = title
|
|
self.bitmap = bitmap
|
|
self.SetSize((bitmap.GetWidth(), bitmap.GetHeight()))
|
|
self.Bind(wx.EVT_PAINT, self.OnWindowPaint)
|
|
self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnWindowEraseBk)
|
|
self.Bind(wx.EVT_TIMER, self.OnTimer)
|
|
|
|
self.timer = wx.Timer(self, wx.ID_ANY)
|
|
self.timerSleep = None
|
|
self.timerSleepId = wx.NewId()
|
|
self.direction = 1
|
|
self.padding = 15
|
|
self.transp = 0
|
|
|
|
hfont = wx.Font(fonts.NORMAL, wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False)
|
|
self.SetFont(hfont)
|
|
|
|
tx, ty = self.GetTextExtent(self.title)
|
|
tx += self.padding * 2
|
|
|
|
if bitmap.GetWidth() < tx:
|
|
width = tx
|
|
else:
|
|
width = bitmap.GetWidth()
|
|
|
|
self.SetSize((width, bitmap.GetHeight() + 16))
|
|
|
|
self.SetTransparent(0)
|
|
self.Refresh()
|
|
|
|
def OnTimer(self, event):
|
|
self.transp += 20 * self.direction
|
|
|
|
if self.transp > 220:
|
|
self.transp = 220
|
|
self.timer.Stop()
|
|
|
|
if self.transp < 0:
|
|
self.transp = 0
|
|
self.timer.Stop()
|
|
super().Show(False)
|
|
self.Destroy()
|
|
return
|
|
self.SetTransparent(self.transp)
|
|
|
|
def RaiseParent(self):
|
|
wnd = self
|
|
lastwnd = None
|
|
while wnd is not None:
|
|
lastwnd = wnd
|
|
wnd = wnd.Parent
|
|
if lastwnd:
|
|
lastwnd.Raise()
|
|
|
|
def Show(self, showWnd=True):
|
|
if showWnd:
|
|
super().Show(showWnd)
|
|
self.RaiseParent()
|
|
self.direction = 1
|
|
self.timer.Start(10)
|
|
else:
|
|
self.direction = -1
|
|
self.timer.Start(10)
|
|
|
|
def OnWindowEraseBk(self, event):
|
|
pass
|
|
|
|
def OnWindowPaint(self, event):
|
|
rect = self.GetRect()
|
|
canvas = wx.Bitmap(round(rect.width), round(rect.height))
|
|
mdc = wx.BufferedPaintDC(self)
|
|
mdc.SelectObject(canvas)
|
|
color = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW)
|
|
mdc.SetBackground(wx.Brush(color))
|
|
mdc.Clear()
|
|
|
|
font = wx.Font(11, wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False)
|
|
mdc.SetFont(font)
|
|
|
|
x, y = mdc.GetTextExtent(self.title)
|
|
|
|
mdc.SetBrush(wx.Brush(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOWTEXT)))
|
|
mdc.DrawRectangle(0, 0, round(rect.width), 16)
|
|
|
|
mdc.SetTextForeground(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW))
|
|
|
|
mdc.DrawBitmap(self.bitmap, 0, 16)
|
|
|
|
mdc.SetPen(wx.Pen("#000000", width=1))
|
|
mdc.SetBrush(wx.TRANSPARENT_BRUSH)
|
|
|
|
mdc.DrawRectangle(0, 16, round(rect.width), round(rect.height - 16))
|