Files
pyfa/gui/pyfa_gauge.py
Ryan Holmes 729c46ab00 Revert "Remove EVT_ERASE_BACKGROUND events - pretty sure these were there for older versions of wx where the background wasn't erased properly"
This does help prevent flickering on resizing. Need to research more before removing them

This reverts commit 8c5c7fba29.
2017-06-14 03:57:57 -04:00

434 lines
14 KiB
Python

#===============================================================================
# PyfaGauge is a generic Gauge implementation tailored for pyfa (the Python
# Fitting Assistant). It uses the easeOutQuad equation from
# caurina.transitions.Tweener to do animations
#
# ToDo: make SetGradient(<value, colour start, colour end)
# ToDo: make a solid gradient (not to->from and not dependant on value)
# ToDo: fix 0 range (currently resets range to 0.01, but this causes problems if
# we really set range at 0.01). Perhaps make it -1 and test percentage as
# a negativeor something.
# ToDo: possibly devise a way to determine transition percents on init
# (currently hardcoded)
#
#===============================================================================
import copy
import wx
from gui.utils import color as color_utils
from gui.utils import draw, anim_effects
class PyGauge(wx.Window):
def __init__(self, parent, font, max_range=100, size=(-1, 30), *args,
**kargs):
super().__init__(parent, size=size, *args, **kargs)
self._size = size
self._border_colour = wx.BLACK
self._bar_colour = None
self._bar_gradient = None
self._border_padding = 0
self._max_range = max_range
self._value = 0
self._fraction_digits = 0
self._timer_id = wx.NewId()
self._timer = None
self._oldValue = 0
self._anim_duration = 500
self._anim_step = 0
self._period = 20
self._anim_value = 0
self._anim_direction = 0
self.anim_effect = anim_effects.OUT_QUAD
# transition colors used based on how full (or overfilled) the gauge is.
self.transition_colors = [
(wx.Colour(191, 191, 191), wx.Colour(96, 191, 0)), # < 0-100%
(wx.Colour(191, 167, 96), wx.Colour(255, 191, 0)), # < 100-101%
(wx.Colour(255, 191, 0), wx.Colour(255, 128, 0)), # < 101-103%
(wx.Colour(255, 128, 0), wx.Colour(255, 0, 0)) # < 103-105%
]
self.gradient_effect = -35
self._percentage = 0
self._old_percentage = 0
self._show_remaining = False
self.font = font
self.SetBackgroundColour(wx.Colour(51, 51, 51))
self._tooltip = wx.ToolTip("0.00/100.00")
self.SetToolTip(self._tooltip)
self.Bind(wx.EVT_PAINT, self.OnPaint)
self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground)
self.Bind(wx.EVT_TIMER, self.OnTimer)
self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground)
self.Bind(wx.EVT_ENTER_WINDOW, self.OnWindowEnter)
self.Bind(wx.EVT_LEAVE_WINDOW, self.OnWindowLeave)
def OnEraseBackground(self, event):
pass
def OnWindowEnter(self, event):
self._show_remaining = True
self.Refresh()
def OnWindowLeave(self, event):
self._show_remaining = False
self.Refresh()
def GetBorderColour(self):
return self._border_colour
def SetBorderColour(self, colour: wx.Colour):
self._border_colour = colour
def GetBarColour(self):
return self._bar_colour
def SetBarColour(self, colour: wx.Colour=None):
self._bar_colour = colour
def SetFractionDigits(self, digits):
self._fraction_digits = digits
def GetBarGradient(self):
if self._bar_gradient is None:
return None
return self._bar_gradient[0]
def SetBarGradient(self, gradient=None):
if gradient is None:
self._bar_gradient = None
else:
if not isinstance(gradient, list):
self._bar_gradient = [gradient]
else:
self._bar_gradient = list(gradient)
def GetBorderPadding(self) -> int:
return self._border_padding
def SetBorderPadding(self, padding):
self._border_padding = padding
def GetRange(self):
""" Returns the maximum value of the gauge. """
return self._max_range
def Animate(self):
if not self._timer:
self._timer = wx.Timer(self, self._timer_id)
self._anim_step = 0
self._timer.Start(self._period)
def SetRange(self, range, reinit=False, animate=True):
"""
Sets the range of the gauge. The gauge length is its
value as a proportion of the range.
"""
if self._max_range == range:
return
# we cannot have a range of zero (laws of physics, etc), so we set it
if range <= 0:
self._max_range = 0.01
else:
self._max_range = range
if reinit is False:
self._old_percentage = self._percentage
self._percentage = (self._value/self._max_range) * 100
else:
self._old_percentage = self._percentage
self._percentage = 0
self._value = 0
if animate:
self.Animate()
self._tooltip.SetTip("%.2f/%.2f" % (self._value, self._max_range if self._max_range >0.01 else 0))
def GetValue(self):
return self._value
def SetValue(self, value, animate=True):
""" Sets the current position of the gauge. """
if self._value == value:
return
self._old_percentage = self._percentage
self._value = value
if value < 0:
self._value = 0
self._percentage = (self._value/self._max_range) * 100
if animate:
self.Animate()
self._tooltip.SetTip("%.2f/%.2f" % (self._value, self._max_range))
def SetValueRange(self, value, range, reinit=False):
""" Set both value and range of the gauge. """
self.SetRange(range, reinit, animate=False)
self.SetValue(value, animate=False)
self.Animate()
self._tooltip.SetTip("%.2f/%.2f" %
(self._value,
self._max_range if self._max_range > 0.01 else 0))
def OnEraseBackground(self, event):
pass
def OnPaint(self, event):
dc = wx.BufferedPaintDC(self)
rect = self.GetClientRect()
dc.SetBackground(wx.Brush(self.GetBackgroundColour()))
dc.Clear()
colour = self.GetBackgroundColour()
dc.SetBrush(wx.Brush(colour))
dc.SetPen(wx.Pen(colour))
dc.DrawRectangle(rect)
value = self._percentage
if self._timer:
if self._timer.IsRunning():
value = self._anim_value
if self._border_colour:
dc.SetPen(wx.Pen(self.GetBorderColour()))
dc.DrawRectangle(rect)
pad = 1 + self.GetBorderPadding()
rect.Deflate(pad, pad)
if self.GetBarColour():
# if we have a bar color set, then we will use this
colour = self.GetBarColour()
dc.SetBrush(wx.Brush(colour))
dc.SetPen(wx.Pen(colour))
# calculate width of bar and draw it
if value > 100:
w = rect.width
else:
w = rect.width * (float(value) / 100)
r = copy.copy(rect)
r.width = w
dc.DrawRectangle(r)
else:
# if bar color is not set, then we use pre-defined transitions
# for the colors based on the percentage value
# calculate width of bar
if value > 100:
w = rect.width
else:
w = rect.width * (float(value) / 100)
r = copy.copy(rect)
r.width = w
# determine transition range number and calculate xv (which is the
# progress between the two transition ranges)
pv = value
if pv <= 100:
xv = pv/100
transition = 0
elif pv <= 101:
xv = pv - 100
transition = 1
elif pv <= 103:
xv = (pv - 101)/2
transition = 2
elif pv <= 105:
xv = (pv - 103)/2
transition = 3
else:
pv = 106
xv = pv - 100
transition = -1
if transition != -1:
start_color, end_color = self.transition_colors[transition]
color = color_utils.CalculateTransition(start_color, end_color,
xv)
else:
color = wx.Colour(191, 48, 48) # dark red
color_factor = self.gradient_effect / 100
mid_factor = (self.gradient_effect / 2) / 100
if self.gradient_effect > 0:
gradient_color = color_utils.Brighten(color, color_factor)
gradient_mid = color_utils.Brighten(color, mid_factor)
else:
gradient_color = color_utils.Darken(color, color_factor * -1)
gradient_mid = color_utils.Darken(color, mid_factor * -1)
# draw bar
gradient_bitmap = draw.DrawGradientBar(
r.width,
r.height,
gradient_mid,
color,
gradient_color
)
dc.DrawBitmap(gradient_bitmap, r.left, r.top)
# font stuff begins here
dc.SetFont(self.font)
# determine shadow position
r = copy.copy(rect)
r.left += 1
r.top += 1
if self._max_range == 0.01 and self._value > 0:
format = u'\u221e' # infinity symbol
# drop shadow
dc.SetTextForeground(wx.Colour(80, 80, 80)) # dark grey
dc.DrawLabel(format, r, wx.ALIGN_CENTER)
# text
dc.SetTextForeground(wx.WHITE)
dc.DrawLabel(format, rect, wx.ALIGN_CENTER)
else:
if not self.GetBarColour() and self._show_remaining:
# we only do these for gradients with mouse over
range = self._max_range if self._max_range > 0.01 else 0
value = range - self._value
if value < 0:
label = "over"
value = -value
else:
label = "left"
format = "{0:." + str(self._fraction_digits) + "f} " + label
else:
format = "{0:." + str(self._fraction_digits) + "f}%"
# drop shadow
dc.SetTextForeground(wx.Colour(80, 80, 80))
dc.DrawLabel(format.format(value), r, wx.ALIGN_CENTER)
# text
dc.SetTextForeground(wx.WHITE)
dc.DrawLabel(format.format(value), rect, wx.ALIGN_CENTER)
def OnTimer(self, event):
old_value = self._old_percentage
value = self._percentage
start = 0
# -1 = left direction, 1 = right direction
direction = 1 if old_value < value else -1
end = direction * (value - old_value)
self._anim_direction = direction
step = self.anim_effect(self._anim_step, start, end, self._anim_duration)
self._anim_step += self._period
if self._timer_id == event.GetId():
stop_timer = False
if self._anim_step > self._anim_duration:
stop_timer = True
# add new value to the animation if we haven't reached our goal
# otherwise, stop animation
if direction == 1:
if old_value + step < value:
self._anim_value = old_value + step
else:
stop_timer = True
else:
if old_value - step > value:
self._anim_value = old_value - step
else:
stop_timer = True
if stop_timer:
self._timer.Stop()
self.Refresh()
if __name__ == "__main__":
def frange(x, y, jump):
while x < y:
yield x
x += jump
class TestPanel(wx.Panel):
def __init__(self, parent, size=(500, 500)):
wx.Panel.__init__(self, parent, size=size)
box = wx.BoxSizer(wx.VERTICAL)
# tests the colors of transition based on percentage used
# list of test values (from 99 -> 106 in increments of 0.5)
tests = [x for x in frange(99, 106.5, .5)]
font = wx.Font(9, wx.SWISS, wx.NORMAL, wx.NORMAL, False)
for i, value in enumerate(tests):
self.gauge = PyGauge(self, font, size=(100, 25))
self.gauge.SetValueRange(value, 100)
self.gauge.SetFractionDigits(2)
box.Add(self.gauge, 0, wx.ALL, 2)
gauge = PyGauge(self, font, size=(100, 25))
gauge.SetBackgroundColour(wx.Colour(52, 86, 98))
gauge.SetBarColour(wx.Colour(38, 133, 198))
gauge.SetValue(59)
gauge.SetFractionDigits(1)
box.Add(gauge, 0, wx.ALL, 2)
self.SetSizer(box)
self.Layout()
# see animation going backwards with last gauge
wx.CallLater(2000, self.ChangeValues)
def ChangeValues(self):
self.gauge.SetValueRange(4, 100)
class Frame(wx.Frame):
def __init__(self, title, size=(500, 800)):
wx.Frame.__init__(self, None, title=title, size=size)
self.statusbar = self.CreateStatusBar()
main_sizer = wx.BoxSizer(wx.VERTICAL)
panel = TestPanel(self, size=size)
main_sizer.Add(panel)
self.SetSizer(main_sizer)
app = wx.App(redirect=False) # Error messages go to popup window
top = Frame("Test Chrome Tabs")
top.Show()
app.MainLoop()