263 lines
9.5 KiB
Python
263 lines
9.5 KiB
Python
# =============================================================================
|
|
# Copyright (C) 2010 Diego Duclos
|
|
#
|
|
# This file is part of pyfa.
|
|
#
|
|
# pyfa is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# pyfa is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
|
|
# =============================================================================
|
|
|
|
|
|
import math
|
|
|
|
# noinspection PyPackageRequirements
|
|
import wx
|
|
|
|
from eos.utils.float import floatUnerr
|
|
|
|
|
|
class VectorPicker(wx.Window):
|
|
|
|
myEVT_VECTOR_CHANGED = wx.NewEventType()
|
|
EVT_VECTOR_CHANGED = wx.PyEventBinder(myEVT_VECTOR_CHANGED, 1)
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self._label = str(kwargs.pop('label', ''))
|
|
self._labelpos = int(kwargs.pop('labelpos', 0))
|
|
self._offset = float(kwargs.pop('offset', 0))
|
|
self._size = max(0, float(kwargs.pop('size', 50)))
|
|
self._fontsize = max(1, float(kwargs.pop('fontsize', 8)))
|
|
self._directionOnly = kwargs.pop('directionOnly', False)
|
|
super().__init__(*args, **kwargs)
|
|
self._font = wx.Font(self._fontsize, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False)
|
|
self._angle = 0
|
|
self.__length = 1
|
|
self._left = False
|
|
self._right = False
|
|
self._savedFocusedWindow = None
|
|
self.SetToolTip(wx.ToolTip(self._tooltip))
|
|
self.Bind(wx.EVT_PAINT, self.OnPaint)
|
|
self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground)
|
|
self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
|
|
self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown)
|
|
self.Bind(wx.EVT_MOUSEWHEEL, self.OnWheel)
|
|
# Allows to focus these widgets on hover, needed to support
|
|
# vector length changing by scrolling
|
|
if 'wxMSW' in wx.PlatformInfo:
|
|
self.Bind(wx.EVT_MOTION, self.OnMouseMove)
|
|
self.Bind(wx.EVT_LEAVE_WINDOW, self.OnMouseLeave)
|
|
|
|
@property
|
|
def _tooltip(self):
|
|
if self._directionOnly:
|
|
return 'Click to set angle\nShift-click or right-click to snap to 15% angle'
|
|
else:
|
|
return 'Click to set angle and velocity\nShift-click or right-click to snap to 15% angle/5% speed increments\nMouse wheel to change velocity only'
|
|
|
|
@property
|
|
def _length(self):
|
|
if self._directionOnly:
|
|
return 1
|
|
else:
|
|
return self.__length
|
|
|
|
@_length.setter
|
|
def _length(self, newLength):
|
|
self.__length = newLength
|
|
|
|
def DoGetBestSize(self):
|
|
return wx.Size(self._size, self._size)
|
|
|
|
def AcceptsFocusFromKeyboard(self):
|
|
return False
|
|
|
|
def GetValue(self):
|
|
return self._angle, self._length
|
|
|
|
def GetAngle(self):
|
|
return self._angle
|
|
|
|
def GetLength(self):
|
|
return self._length
|
|
|
|
def SetValue(self, angle=None, length=None):
|
|
if angle is not None:
|
|
self._angle = min(max(angle, -180), 180)
|
|
if length is not None:
|
|
self._length = min(max(length, 0), 1)
|
|
self.Refresh()
|
|
|
|
def SetAngle(self, angle):
|
|
self.SetValue(angle, None)
|
|
|
|
def SetLength(self, length):
|
|
self.SetValue(None, length)
|
|
|
|
def OnPaint(self, event):
|
|
dc = wx.BufferedPaintDC(self)
|
|
self.Draw(dc)
|
|
|
|
def Draw(self, dc):
|
|
width, height = self.GetClientSize()
|
|
if not width or not height:
|
|
return
|
|
dc.SetBackground(wx.Brush(self.GetBackgroundColour(), wx.BRUSHSTYLE_SOLID))
|
|
dc.Clear()
|
|
dc.SetTextForeground(wx.Colour(0))
|
|
dc.SetFont(self._font)
|
|
|
|
radius = min(width, height) / 2 - 2
|
|
dc.SetBrush(wx.WHITE_BRUSH)
|
|
dc.DrawCircle(radius + 2, radius + 2, radius)
|
|
a = math.radians(self._angle + self._offset)
|
|
x = math.cos(a) * radius
|
|
y = math.sin(a) * radius
|
|
dc.DrawLine(radius + 2, radius + 2, radius + 2 + x * self._length, radius + 2 - y * self._length)
|
|
dc.SetBrush(wx.BLACK_BRUSH)
|
|
dc.DrawCircle(radius + 2 + x * self._length, radius + 2 - y * self._length, 2)
|
|
|
|
if self._label:
|
|
labelText = self._label
|
|
labelTextW, labelTextH = dc.GetTextExtent(labelText)
|
|
labelTextX = (radius * 2 + 4 - labelTextW) if (self._labelpos & 1) else 0
|
|
labelTextY = (radius * 2 + 4 - labelTextH) if (self._labelpos & 2) else 0
|
|
dc.DrawText(labelText, labelTextX, labelTextY)
|
|
|
|
if not self._directionOnly:
|
|
lengthText = '%d%%' % (100 * self._length,)
|
|
lengthTextW, lengthTextH = dc.GetTextExtent(lengthText)
|
|
lengthTextX = radius + 2 + x / 2 - y / 3 - lengthTextW / 2
|
|
lengthTextY = radius + 2 - y / 2 - x / 3 - lengthTextH / 2
|
|
dc.DrawText(lengthText, lengthTextX, lengthTextY)
|
|
|
|
angleText = '%d\u00B0' % (self._angle,)
|
|
angleTextW, angleTextH = dc.GetTextExtent(angleText)
|
|
angleTextX = radius + 2 - x / 2 - angleTextW / 2
|
|
angleTextY = radius + 2 + y / 2 - angleTextH / 2
|
|
dc.DrawText(angleText, angleTextX, angleTextY)
|
|
|
|
def OnEraseBackground(self, event):
|
|
pass
|
|
|
|
def OnLeftDown(self, event):
|
|
self._left = True
|
|
self.SetToolTip(None)
|
|
self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp)
|
|
self.Bind(wx.EVT_MOUSE_CAPTURE_LOST, self.OnLeftUp)
|
|
if not self._right:
|
|
self.Bind(wx.EVT_MOTION, self.OnMotion)
|
|
if not self.HasCapture():
|
|
self.CaptureMouse()
|
|
self.HandleMouseEvent(event)
|
|
|
|
def OnRightDown(self, event):
|
|
self._right = True
|
|
self.SetToolTip(None)
|
|
self.Bind(wx.EVT_RIGHT_UP, self.OnRightUp)
|
|
self.Bind(wx.EVT_MOUSE_CAPTURE_LOST, self.OnRightUp)
|
|
if not self._left:
|
|
self.Bind(wx.EVT_MOTION, self.OnMotion)
|
|
if not self.HasCapture():
|
|
self.CaptureMouse()
|
|
self.HandleMouseEvent(event)
|
|
|
|
def OnLeftUp(self, event):
|
|
self.HandleMouseEvent(event)
|
|
self.Unbind(wx.EVT_LEFT_UP, handler=self.OnLeftUp)
|
|
self.Unbind(wx.EVT_MOUSE_CAPTURE_LOST, handler=self.OnLeftUp)
|
|
self._left = False
|
|
if not self._right:
|
|
self.Unbind(wx.EVT_MOTION, handler=self.OnMotion)
|
|
self.SendChangeEvent()
|
|
self.SetToolTip(wx.ToolTip(self._tooltip))
|
|
if self.HasCapture():
|
|
self.ReleaseMouse()
|
|
|
|
def OnRightUp(self, event):
|
|
self.HandleMouseEvent(event)
|
|
self.Unbind(wx.EVT_RIGHT_UP, handler=self.OnRightUp)
|
|
self.Unbind(wx.EVT_MOUSE_CAPTURE_LOST, handler=self.OnRightUp)
|
|
self._right = False
|
|
if not self._left:
|
|
self.Unbind(wx.EVT_MOTION, handler=self.OnMotion)
|
|
self.SendChangeEvent()
|
|
self.SetToolTip(wx.ToolTip(self._tooltip))
|
|
if self.HasCapture():
|
|
self.ReleaseMouse()
|
|
|
|
def OnMotion(self, event):
|
|
self.HandleMouseEvent(event)
|
|
event.Skip()
|
|
|
|
def OnWheel(self, event):
|
|
amount = 0.1 * event.GetWheelRotation() / event.GetWheelDelta()
|
|
self._length = floatUnerr(min(max(self._length + amount, 0.0), 1.0))
|
|
self.Refresh()
|
|
self.SendChangeEvent()
|
|
|
|
def HandleMouseEvent(self, event):
|
|
width, height = self.GetClientSize()
|
|
if width and height:
|
|
center = min(width, height) / 2
|
|
x, y = event.GetPosition()
|
|
x = x - center
|
|
y = center - y
|
|
angle = self._angle
|
|
length = min((x ** 2 + y ** 2) ** 0.5 / (center - 2), 1.0)
|
|
if length < 0.01:
|
|
length = 0
|
|
else:
|
|
angle = ((math.degrees(math.atan2(y, x)) - self._offset + 180) % 360) - 180
|
|
if (self._right and not self._left) or event.ShiftDown():
|
|
angle = round(angle / 15.0) * 15.0
|
|
# floor() for length to make it easier to hit 0%, can still hit 100% outside the circle
|
|
length = math.floor(length / 0.05) * 0.05
|
|
if (angle != self._angle) or (length != self._length):
|
|
self._angle = angle
|
|
self._length = length
|
|
self.Refresh()
|
|
if (self._right and not self._left) or event.ShiftDown():
|
|
self.SendChangeEvent()
|
|
|
|
# Focus manipulation - otherwise scrolling doesn't work under Windows
|
|
def OnMouseMove(self, event):
|
|
event.Skip()
|
|
if not self.HasFocus():
|
|
self._savedFocusedWindow = self.FindFocus()
|
|
self.SetFocus()
|
|
|
|
def OnMouseLeave(self, event):
|
|
event.Skip()
|
|
if self.HasFocus():
|
|
if self._savedFocusedWindow is not None:
|
|
self._savedFocusedWindow.SetFocus()
|
|
self._savedFocusedWindow = None
|
|
|
|
def SendChangeEvent(self):
|
|
changeEvent = wx.CommandEvent(self.myEVT_VECTOR_CHANGED, self.GetId())
|
|
changeEvent._object = self
|
|
changeEvent._angle = self._angle
|
|
changeEvent._length = self._length
|
|
self.GetEventHandler().ProcessEvent(changeEvent)
|
|
|
|
def SetDirectionOnly(self, val):
|
|
if self._directionOnly is val:
|
|
return
|
|
self._directionOnly = val
|
|
self.GetToolTip().SetTip(self._tooltip)
|
|
|
|
@property
|
|
def IsDirectionOnly(self):
|
|
return self._directionOnly
|
|
|