mirror of
https://github.com/Sneed-Group/Poodletooth-iLand
synced 2024-12-27 21:52:25 -06:00
1777 lines
53 KiB
Python
1777 lines
53 KiB
Python
# --------------------------------------------------------------------------- #
|
|
# FLOATSPIN Control wxPython IMPLEMENTATION
|
|
# Python Code By:
|
|
#
|
|
# Andrea Gavana, @ 16 Nov 2005
|
|
# Latest Revision: 03 Jan 2014, 23.00 GMT
|
|
#
|
|
#
|
|
# TODO List/Caveats
|
|
#
|
|
# 1. Ay Idea?
|
|
#
|
|
# For All Kind Of Problems, Requests Of Enhancements And Bug Reports, Please
|
|
# Write To Me At:
|
|
#
|
|
# andrea.gavana@gmail.com
|
|
# andrea.gavana@maerskoil.com
|
|
#
|
|
# Or, Obviously, To The wxPython Mailing List!!!
|
|
#
|
|
# Tags: phoenix-port, unittest, documented
|
|
#
|
|
# End Of Comments
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
"""
|
|
:class:`FloatSpin` implements a floating point :class:`SpinCtrl`.
|
|
|
|
|
|
Description
|
|
===========
|
|
|
|
:class:`FloatSpin` implements a floating point :class:`SpinCtrl`. It is built using a custom
|
|
:class:`Control`, composed by a :class:`TextCtrl` and a :class:`SpinButton`. In order to
|
|
correctly handle floating points numbers without rounding errors or non-exact
|
|
floating point representations, :class:`FloatSpin` uses the great :class:`FixedPoint` class
|
|
from Tim Peters.
|
|
|
|
What you can do:
|
|
|
|
- Set the number of representative digits for your floating point numbers;
|
|
- Set the floating point format (``%f``, ``%F``, ``%e``, ``%E``, ``%g``, ``%G``);
|
|
- Set the increment of every ``EVT_FLOATSPIN`` event;
|
|
- Set minimum, maximum values for :class:`FloatSpin` as well as its range;
|
|
- Change font and colour for the underline :class:`TextCtrl`.
|
|
|
|
|
|
Usage
|
|
=====
|
|
|
|
Usage example::
|
|
|
|
import wx
|
|
import wx.lib.agw.floatspin as FS
|
|
|
|
class MyFrame(wx.Frame):
|
|
|
|
def __init__(self, parent):
|
|
|
|
wx.Frame.__init__(self, parent, -1, "FloatSpin Demo")
|
|
|
|
panel = wx.Panel(self)
|
|
|
|
floatspin = FS.FloatSpin(panel, -1, pos=(50, 50), min_val=0, max_val=1,
|
|
increment=0.01, value=0.1, agwStyle=FS.FS_LEFT)
|
|
floatspin.SetFormat("%f")
|
|
floatspin.SetDigits(2)
|
|
|
|
|
|
# our normal wxApp-derived class, as usual
|
|
|
|
app = wx.App(0)
|
|
|
|
frame = MyFrame(None)
|
|
app.SetTopWindow(frame)
|
|
frame.Show()
|
|
|
|
app.MainLoop()
|
|
|
|
|
|
|
|
Events
|
|
======
|
|
|
|
:class:`FloatSpin` catches 3 different types of events:
|
|
|
|
1) Spin events: events generated by spinning up/down the spinbutton;
|
|
2) Char events: playing with up/down arrows of the keyboard increase/decrease
|
|
the value of :class:`FloatSpin`;
|
|
3) Mouse wheel event: using the wheel will change the value of :class:`FloatSpin`.
|
|
|
|
In addition, there are some other functionalities:
|
|
|
|
- It remembers the initial value as a default value, call :meth:`~FloatSpin.SetToDefaultValue`, or
|
|
press ``Esc`` to return to it;
|
|
- ``Shift`` + arrow = 2 * increment (or ``Shift`` + mouse wheel);
|
|
- ``Ctrl`` + arrow = 10 * increment (or ``Ctrl`` + mouse wheel);
|
|
- ``Alt`` + arrow = 100 * increment (or ``Alt`` + mouse wheel);
|
|
- Combinations of ``Shift``, ``Ctrl``, ``Alt`` increment the :class:`FloatSpin` value by the
|
|
product of the factors;
|
|
- ``PgUp`` & ``PgDn`` = 10 * increment * the product of the ``Shift``, ``Ctrl``, ``Alt``
|
|
factors;
|
|
- ``Space`` sets the control's value to it's last valid state.
|
|
|
|
|
|
Window Styles
|
|
=============
|
|
|
|
This class supports the following window styles:
|
|
|
|
=============== =========== ==================================================
|
|
Window Styles Hex Value Description
|
|
=============== =========== ==================================================
|
|
``FS_READONLY`` 0x1 Sets :class:`FloatSpin` as read-only control.
|
|
``FS_LEFT`` 0x2 Horizontally align the underlying :class:`TextCtrl` on the left.
|
|
``FS_CENTRE`` 0x4 Horizontally align the underlying :class:`TextCtrl` on center.
|
|
``FS_RIGHT`` 0x8 Horizontally align the underlying :class:`TextCtrl` on the right.
|
|
=============== =========== ==================================================
|
|
|
|
|
|
Events Processing
|
|
=================
|
|
|
|
This class processes the following events:
|
|
|
|
================= ==================================================
|
|
Event Name Description
|
|
================= ==================================================
|
|
``EVT_FLOATSPIN`` Emitted when the user changes the value of :class:`FloatSpin`, either with the mouse or with the keyboard.
|
|
================= ==================================================
|
|
|
|
|
|
License And Version
|
|
===================
|
|
|
|
:class:`FloatSpin` control is distributed under the wxPython license.
|
|
|
|
Latest revision: Andrea Gavana @ 03 Jan 2014, 23.00 GMT
|
|
|
|
Version 1.0
|
|
|
|
|
|
Backward Incompatibilities
|
|
==========================
|
|
|
|
Modifications to allow `min_val` or `max_val` to be ``None`` done by:
|
|
|
|
James Bigler,
|
|
SCI Institute, University of Utah,
|
|
March 14, 2007
|
|
|
|
:note: Note that the changes I made will break backward compatibility,
|
|
because I changed the contructor's parameters from `min` / `max` to
|
|
`min_val` / `max_val` to be consistent with the other functions and to
|
|
eliminate any potential confusion with the built in `min` and `max`
|
|
functions.
|
|
|
|
You specify open ranges like this (you can equally do this in the
|
|
constructor)::
|
|
|
|
SetRange(min_val=1, max_val=None) # [1, ]
|
|
SetRange(min_val=None, max_val=0) # [ , 0]
|
|
|
|
or no range::
|
|
|
|
SetRange(min_val=None, max_val=None) # [ , ]
|
|
|
|
"""
|
|
|
|
|
|
#----------------------------------------------------------------------
|
|
# Beginning Of FLOATSPIN wxPython Code
|
|
#----------------------------------------------------------------------
|
|
|
|
import wx
|
|
import locale
|
|
from math import ceil, floor
|
|
|
|
# Python 2/3 compatibility helper
|
|
import wx.lib.six as six
|
|
if six.PY3:
|
|
long = int
|
|
|
|
# Set The Styles For The Underline wx.TextCtrl
|
|
FS_READONLY = 1
|
|
""" Sets :class:`FloatSpin` as read-only control. """
|
|
FS_LEFT = 2
|
|
""" Horizontally align the underlying :class:`TextCtrl` on the left. """
|
|
FS_CENTRE = 4
|
|
""" Horizontally align the underlying :class:`TextCtrl` on center. """
|
|
FS_RIGHT = 8
|
|
""" Horizontally align the underlying :class:`TextCtrl` on the right. """
|
|
|
|
# Define The FloatSpin Event
|
|
wxEVT_FLOATSPIN = wx.NewEventType()
|
|
|
|
#-----------------------------------#
|
|
# FloatSpinEvent
|
|
#-----------------------------------#
|
|
|
|
EVT_FLOATSPIN = wx.PyEventBinder(wxEVT_FLOATSPIN, 1)
|
|
""" Emitted when the user changes the value of :class:`FloatSpin`, either with the mouse or""" \
|
|
""" with the keyboard. """
|
|
|
|
# ---------------------------------------------------------------------------- #
|
|
# Class FloatSpinEvent
|
|
# ---------------------------------------------------------------------------- #
|
|
|
|
class FloatSpinEvent(wx.CommandEvent):
|
|
""" This event will be sent when a ``EVT_FLOATSPIN`` event is mapped in the parent. """
|
|
|
|
def __init__(self, eventType, eventId=1, nSel=-1, nOldSel=-1):
|
|
"""
|
|
Default class constructor.
|
|
|
|
:param `eventType`: the event type;
|
|
:param `eventId`: the event identifier;
|
|
:param `nSel`: the current selection;
|
|
:param `nOldSel`: the old selection.
|
|
"""
|
|
|
|
wx.CommandEvent.__init__(self, eventType, eventId)
|
|
self._eventType = eventType
|
|
|
|
|
|
def SetPosition(self, pos):
|
|
"""
|
|
Sets event position.
|
|
|
|
:param `pos`: an integer specyfing the event position.
|
|
"""
|
|
|
|
self._position = pos
|
|
|
|
|
|
def GetPosition(self):
|
|
""" Returns event position. """
|
|
|
|
return self._position
|
|
|
|
|
|
#----------------------------------------------------------------------------
|
|
# FloatTextCtrl
|
|
#----------------------------------------------------------------------------
|
|
|
|
|
|
class FloatTextCtrl(wx.TextCtrl):
|
|
"""
|
|
A class which holds a :class:`TextCtrl`, one of the two building blocks
|
|
of :class:`FloatSpin`.
|
|
"""
|
|
|
|
def __init__(self, parent, id=wx.ID_ANY, value="", pos=wx.DefaultPosition,
|
|
size=wx.DefaultSize, style=wx.TE_NOHIDESEL | wx.TE_PROCESS_ENTER,
|
|
validator=wx.DefaultValidator,
|
|
name=wx.TextCtrlNameStr):
|
|
"""
|
|
Default class constructor.
|
|
Used internally. Do not call directly this class in your code!
|
|
|
|
:param `parent`: the :class:`FloatTextCtrl` parent;
|
|
:param `id`: an identifier for the control: a value of -1 is taken to mean a default;
|
|
:param `value`: default text value;
|
|
:param `pos`: the control position. A value of (-1, -1) indicates a default position,
|
|
chosen by either the windowing system or wxPython, depending on platform;
|
|
:param `size`: the control size. A value of (-1, -1) indicates a default size,
|
|
chosen by either the windowing system or wxPython, depending on platform;
|
|
:param `style`: the window style;
|
|
:param `validator`: the window validator;
|
|
:param `name`: the window name.
|
|
|
|
"""
|
|
|
|
wx.TextCtrl.__init__(self, parent, id, value, pos, size, style, validator, name)
|
|
|
|
self._parent = parent
|
|
self.Bind(wx.EVT_WINDOW_DESTROY, self.OnDestroy)
|
|
self.Bind(wx.EVT_CHAR, self.OnChar)
|
|
self.Bind(wx.EVT_KILL_FOCUS, self.OnKillFocus)
|
|
|
|
|
|
def OnDestroy(self, event):
|
|
"""
|
|
Handles the ``wx.EVT_WINDOW_DESTROY`` event for :class:`FloatTextCtrl`.
|
|
|
|
:param `event`: a :class:`WindowDestroyEvent` event to be processed.
|
|
|
|
:note: This method tries to correctly handle the control destruction under MSW.
|
|
"""
|
|
|
|
if self._parent:
|
|
self._parent._textctrl = None
|
|
self._parent = None
|
|
|
|
|
|
def OnChar(self, event):
|
|
"""
|
|
Handles the ``wx.EVT_CHAR`` event for :class:`FloatTextCtrl`.
|
|
|
|
:param `event`: a :class:`KeyEvent` event to be processed.
|
|
"""
|
|
|
|
if self._parent:
|
|
self._parent.OnChar(event)
|
|
|
|
|
|
def OnKillFocus(self, event):
|
|
"""
|
|
Handles the ``wx.EVT_KILL_FOCUS`` event for :class:`FloatTextCtrl`.
|
|
|
|
:param `event`: a :class:`FocusEvent` event to be processed.
|
|
|
|
:note: This method synchronizes the :class:`SpinButton` and the :class:`TextCtrl`
|
|
when focus is lost.
|
|
"""
|
|
|
|
if self._parent:
|
|
self._parent.SyncSpinToText(True)
|
|
|
|
event.Skip()
|
|
|
|
|
|
#---------------------------------------------------------------------------- #
|
|
# FloatSpin
|
|
# This Is The Main Class Implementation
|
|
# ---------------------------------------------------------------------------- #
|
|
|
|
class FloatSpin(wx.Control):
|
|
"""
|
|
:class:`FloatSpin` implements a floating point :class:`SpinCtrl`. It is built using a custom
|
|
:class:`Control`, composed by a :class:`TextCtrl` and a :class:`SpinButton`. In order to
|
|
correctly handle floating points numbers without rounding errors or non-exact
|
|
floating point representations, :class:`FloatSpin` uses the great :class:`FixedPoint` class
|
|
from Tim Peters.
|
|
"""
|
|
|
|
def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition,
|
|
size=(95,-1), style=0, value=0.0, min_val=None, max_val=None,
|
|
increment=1.0, digits=-1, agwStyle=FS_LEFT,
|
|
name="FloatSpin"):
|
|
"""
|
|
Default class constructor.
|
|
|
|
:param `parent`: the :class:`FloatSpin` parent;
|
|
:param `id`: an identifier for the control: a value of -1 is taken to mean a default;
|
|
:param `pos`: the control position. A value of (-1, -1) indicates a default position,
|
|
chosen by either the windowing system or wxPython, depending on platform;
|
|
:param `size`: the control size. A value of (-1, -1) indicates a default size,
|
|
chosen by either the windowing system or wxPython, depending on platform;
|
|
:param `style`: the window style;
|
|
:param `value`: is the current value for :class:`FloatSpin`;
|
|
:param `min_val`: the minimum value, ignored if ``None``;
|
|
:param `max_val`: the maximum value, ignored if ``None``;
|
|
:param `increment`: the increment for every :class:`FloatSpinEvent` event;
|
|
:param `digits`: number of representative digits for your floating point numbers;
|
|
:param `agwStyle`: one of the following bits:
|
|
|
|
=============== =========== ==================================================
|
|
Window Styles Hex Value Description
|
|
=============== =========== ==================================================
|
|
``FS_READONLY`` 0x1 Sets :class:`FloatSpin` as read-only control.
|
|
``FS_LEFT`` 0x2 Horizontally align the underlying :class:`TextCtrl` on the left.
|
|
``FS_CENTRE`` 0x4 Horizontally align the underlying :class:`TextCtrl` on center.
|
|
``FS_RIGHT`` 0x8 Horizontally align the underlying :class:`TextCtrl` on the right.
|
|
=============== =========== ==================================================
|
|
|
|
:param `name`: the window name.
|
|
|
|
"""
|
|
|
|
wx.Control.__init__(self, parent, id, pos, size, style|wx.NO_BORDER|
|
|
wx.NO_FULL_REPAINT_ON_RESIZE | wx.CLIP_CHILDREN,
|
|
wx.DefaultValidator, name)
|
|
|
|
# Don't call SetRange here, because it will try to modify
|
|
# self._value whose value doesn't exist yet.
|
|
self.SetRangeDontClampValue(min_val, max_val)
|
|
self._value = self.ClampValue(FixedPoint(str(value), 20))
|
|
self._defaultvalue = self._value
|
|
self._increment = FixedPoint(str(increment), 20)
|
|
self._spinmodifier = FixedPoint(str(1.0), 20)
|
|
self._digits = digits
|
|
self._snapticks = False
|
|
self._spinbutton = None
|
|
self._textctrl = None
|
|
self._spinctrl_bestsize = wx.Size(-999, -999)
|
|
|
|
# start Philip Semanchuk addition
|
|
# The textbox & spin button are drawn slightly differently
|
|
# depending on the platform. The difference is most pronounced
|
|
# under OS X.
|
|
if "__WXMAC__" in wx.PlatformInfo:
|
|
self._gap = 8
|
|
self._spin_top = 3
|
|
self._text_left = 4
|
|
self._text_top = 4
|
|
elif "__WXMSW__" in wx.PlatformInfo:
|
|
self._gap = 1
|
|
self._spin_top = 0
|
|
self._text_left = 0
|
|
self._text_top = 0
|
|
else:
|
|
# GTK
|
|
self._gap = -1
|
|
self._spin_top = 0
|
|
self._text_left = 0
|
|
self._text_top = 0
|
|
# end Philip Semanchuk addition
|
|
|
|
self.SetLabel(name)
|
|
self.SetForegroundColour(parent.GetForegroundColour())
|
|
|
|
width = size[0]
|
|
height = size[1]
|
|
best_size = self.DoGetBestSize()
|
|
|
|
if width == -1:
|
|
width = best_size.GetWidth()
|
|
if height == -1:
|
|
height = best_size.GetHeight()
|
|
|
|
self._validkeycode = [43, 44, 45, 46, 69, 101, 127, 314]
|
|
self._validkeycode.extend(list(range(48, 58)))
|
|
self._validkeycode.extend([wx.WXK_RETURN, wx.WXK_TAB, wx.WXK_BACK,
|
|
wx.WXK_LEFT, wx.WXK_RIGHT])
|
|
|
|
self._spinbutton = wx.SpinButton(self, wx.ID_ANY, wx.DefaultPosition,
|
|
size=(-1, height),
|
|
style=wx.SP_ARROW_KEYS | wx.SP_VERTICAL |
|
|
wx.SP_WRAP)
|
|
|
|
txtstyle = wx.TE_NOHIDESEL | wx.TE_PROCESS_ENTER
|
|
|
|
if agwStyle & FS_RIGHT:
|
|
txtstyle = txtstyle | wx.TE_RIGHT
|
|
elif agwStyle & FS_CENTRE:
|
|
txtstyle = txtstyle | wx.TE_CENTER
|
|
|
|
if agwStyle & FS_READONLY:
|
|
txtstyle = txtstyle | wx.TE_READONLY
|
|
|
|
self._textctrl = FloatTextCtrl(self, wx.ID_ANY, str(self._value),
|
|
wx.DefaultPosition,
|
|
(width-self._spinbutton.GetSize().GetWidth(), height),
|
|
txtstyle)
|
|
|
|
# start Philip Semanchuk addition
|
|
# Setting the textctrl's size in the ctor also sets its min size.
|
|
# But the textctrl is entirely controlled by the parent floatspin
|
|
# control and should accept whatever size its parent dictates, so
|
|
# here we tell it to forget its min size.
|
|
self._textctrl.SetMinSize(wx.DefaultSize)
|
|
# Setting the spin buttons's size in the ctor also sets its min size.
|
|
# Under OS X that results in a rendering artifact because spin buttons
|
|
# are a little shorter than textboxes.
|
|
# Setting the min size to the default allows OS X to draw the spin
|
|
# button correctly. However, Windows and KDE take the call to
|
|
# SetMinSize() as a cue to size the spin button taller than the
|
|
# textbox, so we avoid the call there.
|
|
if "__WXMAC__" in wx.PlatformInfo:
|
|
self._spinbutton.SetMinSize(wx.DefaultSize)
|
|
# end Philip Semanchuk addition
|
|
|
|
self._mainsizer = wx.BoxSizer(wx.HORIZONTAL)
|
|
# Ensure the spin button is shown, and the text widget takes
|
|
# all remaining free space
|
|
self._mainsizer.Add(self._textctrl, 1)
|
|
self._mainsizer.Add(self._spinbutton, 0)
|
|
self.SetSizer(self._mainsizer)
|
|
self._mainsizer.Layout()
|
|
|
|
self.SetFormat()
|
|
self.SetDigits(digits)
|
|
|
|
# set the value here without generating an event
|
|
|
|
decimal = locale.localeconv()["decimal_point"]
|
|
strs = ("%100." + str(self._digits) + self._textformat[1])%self._value
|
|
strs = strs.replace(".", decimal)
|
|
|
|
strs = strs.strip()
|
|
strs = self.ReplaceDoubleZero(strs)
|
|
|
|
self._textctrl.SetValue(strs)
|
|
|
|
if not (agwStyle & FS_READONLY):
|
|
self.Bind(wx.EVT_SPIN_UP, self.OnSpinUp)
|
|
self.Bind(wx.EVT_SPIN_DOWN, self.OnSpinDown)
|
|
self._spinbutton.Bind(wx.EVT_LEFT_DOWN, self.OnSpinMouseDown)
|
|
|
|
self._textctrl.Bind(wx.EVT_TEXT_ENTER, self.OnTextEnter)
|
|
self._textctrl.Bind(wx.EVT_MOUSEWHEEL, self.OnMouseWheel)
|
|
self._spinbutton.Bind(wx.EVT_MOUSEWHEEL, self.OnMouseWheel)
|
|
|
|
self.Bind(wx.EVT_SET_FOCUS, self.OnFocus)
|
|
self.Bind(wx.EVT_KILL_FOCUS, self.OnKillFocus)
|
|
self.Bind(wx.EVT_SIZE, self.OnSize)
|
|
|
|
# start Philip Semanchuk move
|
|
self.SetInitialSize((width, height))
|
|
# end Philip Semanchuk move
|
|
|
|
|
|
def OnDestroy(self, event):
|
|
"""
|
|
Handles the ``wx.EVT_WINDOW_DESTROY`` event for :class:`FloatSpin`.
|
|
|
|
:param `event`: a :class:`WindowDestroyEvent` event to be processed.
|
|
|
|
:note: This method tries to correctly handle the control destruction under MSW.
|
|
"""
|
|
|
|
# Null This Since MSW Sends KILL_FOCUS On Deletion
|
|
if self._textctrl:
|
|
self._textctrl._parent = None
|
|
self._textctrl.Destroy()
|
|
self._textctrl = None
|
|
|
|
self._spinbutton.Destroy()
|
|
self._spinbutton = None
|
|
|
|
|
|
def DoGetBestSize(self):
|
|
"""
|
|
Gets the size which best suits the window: for a control, it would be the
|
|
minimal size which doesn't truncate the control, for a panel - the same
|
|
size as it would have after a call to `Fit()`.
|
|
|
|
:note: Overridden from :class:`Control`.
|
|
"""
|
|
|
|
if self._spinctrl_bestsize.x == -999:
|
|
|
|
spin = wx.SpinCtrl(self, -1)
|
|
self._spinctrl_bestsize = spin.GetBestSize()
|
|
|
|
# oops something went wrong, set to reasonable value
|
|
if self._spinctrl_bestsize.GetWidth() < 20:
|
|
self._spinctrl_bestsize.SetWidth(95)
|
|
if self._spinctrl_bestsize.GetHeight() < 10:
|
|
self._spinctrl_bestsize.SetHeight(22)
|
|
|
|
spin.Destroy()
|
|
|
|
return self._spinctrl_bestsize
|
|
|
|
|
|
def DoSendEvent(self):
|
|
""" Send the event to the parent. """
|
|
|
|
event = wx.CommandEvent(wx.wxEVT_COMMAND_SPINCTRL_UPDATED, self.GetId())
|
|
event.SetEventObject(self)
|
|
event.SetInt(int(self._value + 0.5))
|
|
|
|
if self._textctrl:
|
|
event.SetString(self._textctrl.GetValue())
|
|
|
|
self.GetEventHandler().ProcessEvent(event)
|
|
|
|
eventOut = FloatSpinEvent(wxEVT_FLOATSPIN, self.GetId())
|
|
eventOut.SetPosition(int(self._value + 0.5))
|
|
eventOut.SetEventObject(self)
|
|
self.GetEventHandler().ProcessEvent(eventOut)
|
|
|
|
|
|
def OnSpinMouseDown(self, event):
|
|
"""
|
|
Handles the ``wx.EVT_LEFT_DOWN`` event for :class:`FloatSpin`.
|
|
|
|
:param `event`: a :class:`MouseEvent` event to be processed.
|
|
|
|
:note: This method works on the underlying :class:`SpinButton`.
|
|
"""
|
|
|
|
modifier = FixedPoint(str(1.0), 20)
|
|
if event.ShiftDown():
|
|
modifier = modifier*2.0
|
|
if event.ControlDown():
|
|
modifier = modifier*10.0
|
|
if event.AltDown():
|
|
modifier = modifier*100.0
|
|
|
|
self._spinmodifier = modifier
|
|
|
|
event.Skip()
|
|
|
|
|
|
def OnSpinUp(self, event):
|
|
"""
|
|
Handles the ``wx.EVT_SPIN_UP`` event for :class:`FloatSpin`.
|
|
|
|
:param `event`: a :class:`SpinEvent` event to be processed.
|
|
"""
|
|
|
|
if self._textctrl and self._textctrl.IsModified():
|
|
self.SyncSpinToText(False)
|
|
|
|
if self.InRange(self._value + self._increment*self._spinmodifier):
|
|
|
|
self._value = self._value + self._increment*self._spinmodifier
|
|
self.SetValue(self._value)
|
|
self.DoSendEvent()
|
|
|
|
|
|
def OnSpinDown(self, event):
|
|
"""
|
|
Handles the ``wx.EVT_SPIN_DOWN`` event for :class:`FloatSpin`.
|
|
|
|
:param `event`: a :class:`SpinEvent` event to be processed.
|
|
"""
|
|
|
|
if self._textctrl and self._textctrl.IsModified():
|
|
self.SyncSpinToText(False)
|
|
|
|
if self.InRange(self._value - self._increment*self._spinmodifier):
|
|
|
|
self._value = self._value - self._increment*self._spinmodifier
|
|
self.SetValue(self._value)
|
|
self.DoSendEvent()
|
|
|
|
|
|
def OnTextEnter(self, event):
|
|
"""
|
|
Handles the ``wx.EVT_TEXT_ENTER`` event for :class:`FloatSpin`.
|
|
|
|
:param `event`: a :class:`KeyEvent` event to be processed.
|
|
|
|
:note: This method works on the underlying :class:`TextCtrl`.
|
|
"""
|
|
|
|
self.SyncSpinToText(True)
|
|
event.Skip()
|
|
|
|
|
|
def OnChar(self, event):
|
|
"""
|
|
Handles the ``wx.EVT_CHAR`` event for :class:`FloatSpin`.
|
|
|
|
:param `event`: a :class:`KeyEvent` event to be processed.
|
|
|
|
:note: This method works on the underlying :class:`TextCtrl`.
|
|
"""
|
|
|
|
modifier = FixedPoint(str(1.0), 20)
|
|
if event.ShiftDown():
|
|
modifier = modifier*2.0
|
|
if event.ControlDown():
|
|
modifier = modifier*10.0
|
|
if event.AltDown():
|
|
modifier = modifier*100.0
|
|
|
|
keycode = event.GetKeyCode()
|
|
|
|
if keycode == wx.WXK_UP:
|
|
|
|
if self._textctrl and self._textctrl.IsModified():
|
|
self.SyncSpinToText(False)
|
|
|
|
self.SetValue(self._value + self._increment*modifier)
|
|
self.DoSendEvent()
|
|
|
|
elif keycode == wx.WXK_DOWN:
|
|
|
|
if self._textctrl and self._textctrl.IsModified():
|
|
self.SyncSpinToText(False)
|
|
|
|
self.SetValue(self._value - self._increment*modifier)
|
|
self.DoSendEvent()
|
|
|
|
elif keycode == wx.WXK_PAGEUP:
|
|
|
|
if self._textctrl and self._textctrl.IsModified():
|
|
self.SyncSpinToText(False)
|
|
|
|
self.SetValue(self._value + 10.0*self._increment*modifier)
|
|
self.DoSendEvent()
|
|
|
|
elif keycode == wx.WXK_PAGEDOWN:
|
|
|
|
if self._textctrl and self._textctrl.IsModified():
|
|
self.SyncSpinToText(False)
|
|
|
|
self.SetValue(self._value - 10.0*self._increment*modifier)
|
|
self.DoSendEvent()
|
|
|
|
elif keycode == wx.WXK_SPACE:
|
|
|
|
self.SetValue(self._value)
|
|
event.Skip(False)
|
|
|
|
elif keycode == wx.WXK_ESCAPE:
|
|
|
|
self.SetToDefaultValue()
|
|
self.DoSendEvent()
|
|
|
|
elif keycode == wx.WXK_TAB:
|
|
|
|
new_event = wx.NavigationKeyEvent()
|
|
new_event.SetEventObject(self.GetParent())
|
|
new_event.SetDirection(not event.ShiftDown())
|
|
# CTRL-TAB changes the (parent) window, i.e. switch notebook page
|
|
new_event.SetWindowChange(event.ControlDown())
|
|
new_event.SetCurrentFocus(self)
|
|
self.GetParent().GetEventHandler().ProcessEvent(new_event)
|
|
|
|
else:
|
|
if keycode not in self._validkeycode:
|
|
return
|
|
|
|
event.Skip()
|
|
|
|
|
|
def OnMouseWheel(self, event):
|
|
"""
|
|
Handles the ``wx.EVT_MOUSEWHEEL`` event for :class:`FloatSpin`.
|
|
|
|
:param `event`: a :class:`MouseEvent` event to be processed.
|
|
"""
|
|
|
|
modifier = FixedPoint(str(1.0), 20)
|
|
if event.ShiftDown():
|
|
modifier = modifier*2.0
|
|
if event.ControlDown():
|
|
modifier = modifier*10.0
|
|
if event.AltDown():
|
|
modifier = modifier*100.0
|
|
|
|
if self._textctrl and self._textctrl.IsModified():
|
|
self.SyncSpinToText(False)
|
|
|
|
if event.GetWheelRotation() > 0:
|
|
self.SetValue(self._value + self._increment*modifier)
|
|
self.DoSendEvent()
|
|
|
|
else:
|
|
|
|
self.SetValue(self._value - self._increment*modifier)
|
|
self.DoSendEvent()
|
|
|
|
|
|
def OnSize(self, event):
|
|
"""
|
|
Handles the ``wx.EVT_SIZE`` event for :class:`FloatSpin`.
|
|
|
|
:param `event`: a :class:`SizeEvent` event to be processed.
|
|
|
|
:note: This method resizes the text control and reposition the spin button when
|
|
resized.
|
|
"""
|
|
# start Philip Semanchuk addition
|
|
event_width = event.GetSize().width
|
|
|
|
self._textctrl.SetPosition((self._text_left, self._text_top))
|
|
|
|
text_width, text_height = self._textctrl.GetSize()
|
|
|
|
spin_width, _ = self._spinbutton.GetSize()
|
|
|
|
text_width = event_width - (spin_width + self._gap + self._text_left)
|
|
|
|
self._textctrl.SetSize(wx.Size(text_width, event.GetSize().height))
|
|
|
|
# The spin button is always snug against the right edge of the
|
|
# control.
|
|
self._spinbutton.SetPosition((event_width - spin_width, self._spin_top))
|
|
|
|
event.Skip()
|
|
# end Philip Semanchuk addition
|
|
|
|
|
|
def ReplaceDoubleZero(self, strs):
|
|
"""
|
|
Replaces the (somewhat) python ugly `+e000` with `+e00`.
|
|
|
|
:param `strs`: a string (possibly) containing a `+e00` substring.
|
|
"""
|
|
|
|
if self._textformat not in ["%g", "%e", "%E", "%G"]:
|
|
return strs
|
|
|
|
if strs.find("e+00") >= 0:
|
|
strs = strs.replace("e+00", "e+0")
|
|
elif strs.find("e-00") >= 0:
|
|
strs = strs.replace("e-00", "e-0")
|
|
elif strs.find("E+00") >= 0:
|
|
strs = strs.replace("E+00", "E+0")
|
|
elif strs.find("E-00") >= 0:
|
|
strs = strs.replace("E-00", "E-0")
|
|
|
|
return strs
|
|
|
|
|
|
def SetValue(self, value):
|
|
"""
|
|
Sets the :class:`FloatSpin` value.
|
|
|
|
:param `value`: the new value.
|
|
"""
|
|
if not self._textctrl or not self.InRange(value):
|
|
return
|
|
|
|
if self._snapticks and self._increment != 0.0:
|
|
|
|
finite, snap_value = self.IsFinite(value)
|
|
|
|
if not finite: # FIXME What To Do About A Failure?
|
|
|
|
if (snap_value - floor(snap_value) < ceil(snap_value) - snap_value):
|
|
value = self._defaultvalue + floor(snap_value)*self._increment
|
|
else:
|
|
value = self._defaultvalue + ceil(snap_value)*self._increment
|
|
|
|
decimal = locale.localeconv()["decimal_point"]
|
|
strs = ("%100." + str(self._digits) + self._textformat[1])%value
|
|
strs = strs.replace(".", decimal)
|
|
strs = strs.strip()
|
|
strs = self.ReplaceDoubleZero(strs)
|
|
|
|
if value != self._value or strs != self._textctrl.GetValue():
|
|
|
|
self._textctrl.SetValue(strs)
|
|
self._textctrl.DiscardEdits()
|
|
self._value = value
|
|
|
|
|
|
def GetValue(self):
|
|
""" Returns the :class:`FloatSpin` value. """
|
|
|
|
return float(self._value)
|
|
|
|
|
|
def SetRangeDontClampValue(self, min_val, max_val):
|
|
"""
|
|
Sets the allowed range.
|
|
|
|
:param `min_val`: the minimum value for :class:`FloatSpin`. If it is ``None`` it is
|
|
ignored;
|
|
:param `max_val`: the maximum value for :class:`FloatSpin`. If it is ``None`` it is
|
|
ignored.
|
|
|
|
:note: This method doesn't modify the current value.
|
|
"""
|
|
|
|
if (min_val != None):
|
|
self._min = FixedPoint(str(min_val), 20)
|
|
else:
|
|
self._min = None
|
|
if (max_val != None):
|
|
self._max = FixedPoint(str(max_val), 20)
|
|
else:
|
|
self._max = None
|
|
|
|
|
|
def SetRange(self, min_val, max_val):
|
|
"""
|
|
Sets the allowed range.
|
|
|
|
:param `min_val`: the minimum value for :class:`FloatSpin`. If it is ``None`` it is
|
|
ignored;
|
|
:param `max_val`: the maximum value for :class:`FloatSpin`. If it is ``None`` it is
|
|
ignored.
|
|
|
|
:note: This method doesn't modify the current value.
|
|
|
|
:note: You specify open ranges like this (you can equally do this in the
|
|
constructor)::
|
|
|
|
SetRange(min_val=1, max_val=None)
|
|
SetRange(min_val=None, max_val=0)
|
|
|
|
|
|
or no range::
|
|
|
|
SetRange(min_val=None, max_val=None)
|
|
|
|
"""
|
|
|
|
self.SetRangeDontClampValue(min_val, max_val)
|
|
|
|
value = self.ClampValue(self._value)
|
|
if (value != self._value):
|
|
self.SetValue(value)
|
|
|
|
|
|
def ClampValue(self, var):
|
|
"""
|
|
Clamps `var` between `_min` and `_max` depending if the range has
|
|
been specified.
|
|
|
|
:param `var`: the value to be clamped.
|
|
|
|
:return: A clamped copy of `var`.
|
|
"""
|
|
|
|
if (self._min != None):
|
|
if (var < self._min):
|
|
var = self._min
|
|
return var
|
|
|
|
if (self._max != None):
|
|
if (var > self._max):
|
|
var = self._max
|
|
|
|
return var
|
|
|
|
|
|
def SetIncrement(self, increment):
|
|
"""
|
|
Sets the increment for every ``EVT_FLOATSPIN`` event.
|
|
|
|
:param `increment`: a floating point number specifying the :class:`FloatSpin` increment.
|
|
"""
|
|
|
|
if increment < 1./10.0**self._digits:
|
|
raise Exception("\nERROR: Increment Should Be Greater Or Equal To 1/(10**digits).")
|
|
|
|
self._increment = FixedPoint(str(increment), 20)
|
|
self.SetValue(self._value)
|
|
|
|
|
|
def GetIncrement(self):
|
|
""" Returns the increment for every ``EVT_FLOATSPIN`` event. """
|
|
|
|
return self._increment
|
|
|
|
|
|
def SetDigits(self, digits=-1):
|
|
"""
|
|
Sets the number of digits to show.
|
|
|
|
:param `digits`: the number of digits to show. If `digits` < 0, :class:`FloatSpin`
|
|
tries to calculate the best number of digits based on input values passed
|
|
in the constructor.
|
|
"""
|
|
|
|
if digits < 0:
|
|
incr = str(self._increment)
|
|
if incr.find(".") < 0:
|
|
digits = 0
|
|
else:
|
|
digits = len(incr[incr.find(".")+1:])
|
|
|
|
self._digits = digits
|
|
|
|
self.SetValue(self._value)
|
|
|
|
|
|
def GetDigits(self):
|
|
""" Returns the number of digits shown. """
|
|
|
|
return self._digits
|
|
|
|
|
|
def SetFormat(self, fmt="%f"):
|
|
"""
|
|
Set the string format to use.
|
|
|
|
:param `fmt`: the new string format to use. One of the following strings:
|
|
|
|
====== =================================
|
|
Format Description
|
|
====== =================================
|
|
'e' Floating point exponential format (lowercase)
|
|
'E' Floating point exponential format (uppercase)
|
|
'f' Floating point decimal format
|
|
'F' Floating point decimal format
|
|
'g' Floating point format. Uses lowercase exponential format if exponent is less than -4 or not less than precision, decimal format otherwise
|
|
'G' Floating point format. Uses uppercase exponential format if exponent is less than -4 or not less than precision, decimal format otherwise
|
|
====== =================================
|
|
|
|
"""
|
|
|
|
if fmt not in ["%f", "%g", "%e", "%E", "%F", "%G"]:
|
|
raise Exception('\nERROR: Bad Float Number Format: ' + repr(fmt) + '. It Should Be ' \
|
|
'One Of "%f", "%g", "%e", "%E", "%F", "%G"')
|
|
|
|
self._textformat = fmt
|
|
|
|
if self._digits < 0:
|
|
self.SetDigits()
|
|
|
|
self.SetValue(self._value)
|
|
|
|
|
|
def GetFormat(self):
|
|
"""
|
|
Returns the string format in use.
|
|
|
|
:see: :meth:`~FloatSpin.SetFormat` for a list of valid string formats.
|
|
"""
|
|
|
|
return self._textformat
|
|
|
|
|
|
def SetDefaultValue(self, defaultvalue):
|
|
"""
|
|
Sets the :class:`FloatSpin` default value.
|
|
|
|
:param `defaultvalue`: a floating point value representing the new default
|
|
value for :class:`FloatSpin`.
|
|
"""
|
|
|
|
if self.InRange(defaultvalue):
|
|
self._defaultvalue = FixedPoint(str(defaultvalue), 20)
|
|
|
|
|
|
def GetDefaultValue(self):
|
|
""" Returns the :class:`FloatSpin` default value. """
|
|
|
|
return self._defaultvalue
|
|
|
|
|
|
def IsDefaultValue(self):
|
|
""" Returns whether the current value is the default value or not. """
|
|
|
|
return self._value == self._defaultvalue
|
|
|
|
|
|
def SetToDefaultValue(self):
|
|
""" Sets :class:`FloatSpin` value to its default value. """
|
|
|
|
self.SetValue(self._defaultvalue)
|
|
|
|
|
|
def SetSnapToTicks(self, forceticks=True):
|
|
"""
|
|
Force the value to always be divisible by the increment. Initially ``False``.
|
|
|
|
:param `forceticks`: ``True`` to force the snap to ticks option, ``False`` otherwise.
|
|
|
|
:note: This uses the default value as the basis, you will get strange results
|
|
for very large differences between the current value and default value
|
|
when the increment is very small.
|
|
"""
|
|
|
|
if self._snapticks != forceticks:
|
|
|
|
self._snapticks = forceticks
|
|
self.SetValue(self._value)
|
|
|
|
|
|
def GetSnapToTicks(self):
|
|
""" Returns whether the snap to ticks option is active or not. """
|
|
|
|
return self._snapticks
|
|
|
|
|
|
def OnFocus(self, event):
|
|
"""
|
|
Handles the ``wx.EVT_SET_FOCUS`` event for :class:`FloatSpin`.
|
|
|
|
:param `event`: a :class:`FocusEvent` event to be processed.
|
|
"""
|
|
|
|
if self._textctrl:
|
|
self._textctrl.SetFocus()
|
|
|
|
event.Skip()
|
|
|
|
|
|
def OnKillFocus(self, event):
|
|
"""
|
|
Handles the ``wx.EVT_KILL_FOCUS`` event for :class:`FloatSpin`.
|
|
|
|
:param `event`: a :class:`FocusEvent` event to be processed.
|
|
"""
|
|
|
|
self.SyncSpinToText(True)
|
|
event.Skip()
|
|
|
|
|
|
def SyncSpinToText(self, send_event=True, force_valid=True):
|
|
"""
|
|
Synchronize the underlying :class:`TextCtrl` with :class:`SpinButton`.
|
|
|
|
:param `send_event`: ``True`` to send a ``EVT_FLOATSPIN`` event, ``False``
|
|
otherwise;
|
|
:param `force_valid`: ``True`` to force a valid value (i.e. inside the
|
|
provided range), ``False`` otherwise.
|
|
"""
|
|
|
|
if not self._textctrl:
|
|
return
|
|
|
|
curr = self._textctrl.GetValue()
|
|
curr = curr.strip()
|
|
decimal = locale.localeconv()["decimal_point"]
|
|
curr = curr.replace(decimal, ".")
|
|
|
|
if curr:
|
|
try:
|
|
curro = float(curr)
|
|
curr = FixedPoint(curr, 20)
|
|
except:
|
|
self.SetValue(self._value)
|
|
return
|
|
|
|
if force_valid or not self.HasRange() or self.InRange(curr):
|
|
|
|
if force_valid and self.HasRange():
|
|
|
|
curr = self.ClampValue(curr)
|
|
|
|
if self._value != curr:
|
|
self.SetValue(curr)
|
|
|
|
if send_event:
|
|
self.DoSendEvent()
|
|
|
|
elif force_valid:
|
|
|
|
# textctrl is out of sync, discard and reset
|
|
self.SetValue(self.GetValue())
|
|
|
|
|
|
def SetFont(self, font=None):
|
|
"""
|
|
Sets the underlying :class:`TextCtrl` font.
|
|
|
|
:param `font`: a valid instance of :class:`Font`.
|
|
"""
|
|
|
|
if font is None:
|
|
font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT)
|
|
|
|
if not self._textctrl:
|
|
return False
|
|
|
|
return self._textctrl.SetFont(font)
|
|
|
|
|
|
def GetFont(self):
|
|
""" Returns the underlying :class:`TextCtrl` font. """
|
|
|
|
if not self._textctrl:
|
|
return self.GetFont()
|
|
|
|
return self._textctrl.GetFont()
|
|
|
|
|
|
def GetMin(self):
|
|
"""
|
|
Returns the minimum value for :class:`FloatSpin`. It can be a
|
|
number or ``None`` if no minimum is present.
|
|
"""
|
|
|
|
return self._min
|
|
|
|
|
|
def GetMax(self):
|
|
"""
|
|
Returns the maximum value for :class:`FloatSpin`. It can be a
|
|
number or ``None`` if no minimum is present.
|
|
"""
|
|
|
|
return self._max
|
|
|
|
|
|
def HasRange(self):
|
|
""" Returns whether :class:`FloatSpin` range has been set or not. """
|
|
|
|
return (self._min != None) or (self._max != None)
|
|
|
|
|
|
def InRange(self, value):
|
|
"""
|
|
Returns whether a value is inside :class:`FloatSpin` range.
|
|
|
|
:param `value`: the value to test.
|
|
"""
|
|
|
|
if (not self.HasRange()):
|
|
return True
|
|
if (self._min != None):
|
|
if (value < self._min):
|
|
return False
|
|
if (self._max != None):
|
|
if (value > self._max):
|
|
return False
|
|
return True
|
|
|
|
|
|
def GetTextCtrl(self):
|
|
""" Returns the underlying :class:`TextCtrl`. """
|
|
|
|
return self._textctrl
|
|
|
|
|
|
def IsFinite(self, value):
|
|
"""
|
|
Tries to determine if a value is finite or infinite/NaN.
|
|
|
|
:param `value`: the value to test.
|
|
"""
|
|
|
|
try:
|
|
snap_value = (value - self._defaultvalue)/self._increment
|
|
finite = True
|
|
except:
|
|
finite = False
|
|
snap_value = None
|
|
|
|
return finite, snap_value
|
|
|
|
|
|
|
|
# Class FixedPoint, version 0.0.4.
|
|
# Released to the public domain 28-Mar-2001,
|
|
# by Tim Peters (tim.one@home.com).
|
|
|
|
# Provided as-is; use at your own risk; no warranty; no promises; enjoy!
|
|
|
|
|
|
# 28-Mar-01 ver 0.0,4
|
|
# Use repr() instead of str() inside __str__, because str(long) changed
|
|
# since this was first written (used to produce trailing "L", doesn't
|
|
# now).
|
|
#
|
|
# 09-May-99 ver 0,0,3
|
|
# Repaired __sub__(FixedPoint, string); was blowing up.
|
|
# Much more careful conversion of float (now best possible).
|
|
# Implemented exact % and divmod.
|
|
#
|
|
# 14-Oct-98 ver 0,0,2
|
|
# Added int, long, frac. Beefed up docs. Removed DECIMAL_POINT
|
|
# and MINUS_SIGN globals to discourage bloating this class instead
|
|
# of writing formatting wrapper classes (or subclasses)
|
|
#
|
|
# 11-Oct-98 ver 0,0,1
|
|
# posted to c.l.py
|
|
|
|
__version__ = 0, 0, 4
|
|
|
|
# The default value for the number of decimal digits carried after the
|
|
# decimal point. This only has effect at compile-time.
|
|
DEFAULT_PRECISION = 2
|
|
""" The default value for the number of decimal digits carried after the decimal point. This only has effect at compile-time. """
|
|
|
|
class FixedPoint(object):
|
|
"""
|
|
FixedPoint objects support decimal arithmetic with a fixed number of
|
|
digits (called the object's precision) after the decimal point. The
|
|
number of digits before the decimal point is variable & unbounded.
|
|
|
|
The precision is user-settable on a per-object basis when a FixedPoint
|
|
is constructed, and may vary across FixedPoint objects. The precision
|
|
may also be changed after construction via `FixedPoint.set_precision(p)`.
|
|
Note that if the precision of a FixedPoint is reduced via :meth:`FixedPoint.set_precision() <FixedPoint.set_precision>`,
|
|
information may be lost to rounding.
|
|
|
|
Example::
|
|
|
|
>>> x = FixedPoint("5.55") # precision defaults to 2
|
|
>>> print(x)
|
|
5.55
|
|
>>> x.set_precision(1) # round to one fraction digit
|
|
>>> print(x)
|
|
5.6
|
|
>>> print(FixedPoint("5.55", 1)) # same thing setting to 1 in constructor
|
|
5.6
|
|
>>> repr(x) # returns constructor string that reproduces object exactly
|
|
"FixedPoint('5.6', 1)"
|
|
>>>
|
|
|
|
|
|
When :class:`FixedPoint` objects of different precision are combined via + - * /,
|
|
the result is computed to the larger of the inputs' precisions, which also
|
|
becomes the precision of the resulting :class:`FixedPoint` object. Example::
|
|
|
|
>>> print FixedPoint("3.42") + FixedPoint("100.005", 3)
|
|
103.425
|
|
>>>
|
|
|
|
|
|
When a :class:`FixedPoint` is combined with other numeric types (ints, floats,
|
|
strings representing a number) via + - * /, then similarly the computation
|
|
is carried out using -- and the result inherits -- the :class:`FixedPoint`'s
|
|
precision. Example::
|
|
|
|
>>> print(FixedPoint(1) / 7)
|
|
0.14
|
|
>>> print(FixedPoint(1, 30) / 7)
|
|
0.142857142857142857142857142857
|
|
>>>
|
|
|
|
|
|
The string produced by `str(x)` (implictly invoked by `print`) always
|
|
contains at least one digit before the decimal point, followed by a
|
|
decimal point, followed by exactly `x.get_precision()` digits. If `x` is
|
|
negative, `str(x)[0] == "-"`.
|
|
|
|
The :class:`FixedPoint` constructor can be passed an int, long, string, float,
|
|
:class:`FixedPoint`, or any object convertible to a float via `float()` or to a
|
|
long via `long()`. Passing a precision is optional; if specified, the
|
|
precision must be a non-negative int. There is no inherent limit on
|
|
the size of the precision, but if very very large you'll probably run
|
|
out of memory.
|
|
|
|
Note that conversion of floats to :class:`FixedPoint` can be surprising, and
|
|
should be avoided whenever possible. Conversion from string is exact
|
|
(up to final rounding to the requested precision), so is greatly
|
|
preferred. Example::
|
|
|
|
>>> print(FixedPoint(1.1e30))
|
|
1099999999999999993725589651456.00
|
|
>>> print(FixedPoint("1.1e30"))
|
|
1100000000000000000000000000000.00
|
|
>>>
|
|
|
|
|
|
"""
|
|
|
|
# the exact value is self.n / 10**self.p;
|
|
# self.n is a long; self.p is an int
|
|
|
|
def __init__(self, value=0, precision=DEFAULT_PRECISION):
|
|
"""
|
|
Default class constructor.
|
|
|
|
:param `value`: the initial value;
|
|
:param `precision`: must be an int >= 0, and defaults to ``DEFAULT_PRECISION``.
|
|
"""
|
|
|
|
self.n = self.p = 0
|
|
self.set_precision(precision)
|
|
p = self.p
|
|
|
|
if isinstance(value, type("42.3e5")):
|
|
n, exp = _string2exact(value)
|
|
# exact value is n*10**exp = n*10**(exp+p)/10**p
|
|
effective_exp = exp + p
|
|
if effective_exp > 0:
|
|
n = n * _tento(effective_exp)
|
|
elif effective_exp < 0:
|
|
n = _roundquotient(n, _tento(-effective_exp))
|
|
self.n = n
|
|
return
|
|
|
|
if isinstance(value, six.integer_types):
|
|
self.n = long(value) * _tento(p)
|
|
return
|
|
|
|
if isinstance(value, FixedPoint):
|
|
temp = value.copy()
|
|
temp.set_precision(p)
|
|
self.n, self.p = temp.n, temp.p
|
|
return
|
|
|
|
if isinstance(value, type(42.0)):
|
|
# XXX ignoring infinities and NaNs and overflows for now
|
|
import math
|
|
f, e = math.frexp(abs(value))
|
|
assert f == 0 or 0.5 <= f < 1.0
|
|
# |value| = f * 2**e exactly
|
|
|
|
# Suck up CHUNK bits at a time; 28 is enough so that we suck
|
|
# up all bits in 2 iterations for all known binary double-
|
|
# precision formats, and small enough to fit in an int.
|
|
CHUNK = 28
|
|
top = 0
|
|
# invariant: |value| = (top + f) * 2**e exactly
|
|
while f:
|
|
f = math.ldexp(f, CHUNK)
|
|
digit = int(f)
|
|
assert digit >> CHUNK == 0
|
|
top = (top << CHUNK) | digit
|
|
f = f - digit
|
|
assert 0.0 <= f < 1.0
|
|
e = e - CHUNK
|
|
|
|
# now |value| = top * 2**e exactly
|
|
# want n such that n / 10**p = top * 2**e, or
|
|
# n = top * 10**p * 2**e
|
|
top = top * _tento(p)
|
|
if e >= 0:
|
|
n = top << e
|
|
else:
|
|
n = _roundquotient(top, 1 << -e)
|
|
if value < 0:
|
|
n = -n
|
|
self.n = n
|
|
return
|
|
|
|
if isinstance(value, type(42-42j)):
|
|
raise TypeError("can't convert complex to FixedPoint: " +
|
|
repr(value))
|
|
|
|
# can we coerce to a float?
|
|
yes = 1
|
|
try:
|
|
asfloat = float(value)
|
|
except:
|
|
yes = 0
|
|
if yes:
|
|
self.__init__(asfloat, p)
|
|
return
|
|
|
|
# similarly for long
|
|
yes = 1
|
|
try:
|
|
aslong = long(value)
|
|
except:
|
|
yes = 0
|
|
if yes:
|
|
self.__init__(aslong, p)
|
|
return
|
|
|
|
raise TypeError("can't convert to FixedPoint: " + repr(value))
|
|
|
|
|
|
def get_precision(self):
|
|
"""
|
|
Return the precision of this :class:`FixedPoint`.
|
|
|
|
:note: The precision is the number of decimal digits carried after
|
|
the decimal point, and is an int >= 0.
|
|
"""
|
|
|
|
return self.p
|
|
|
|
|
|
def set_precision(self, precision=DEFAULT_PRECISION):
|
|
"""
|
|
Change the precision carried by this :class:`FixedPoint` to `precision`.
|
|
|
|
:param `precision`: must be an int >= 0, and defaults to
|
|
``DEFAULT_PRECISION``.
|
|
|
|
:note: If `precision` is less than this :class:`FixedPoint`'s current precision,
|
|
information may be lost to rounding.
|
|
"""
|
|
|
|
try:
|
|
p = int(precision)
|
|
except:
|
|
raise TypeError("precision not convertable to int: " +
|
|
repr(precision))
|
|
if p < 0:
|
|
raise ValueError("precision must be >= 0: " + repr(precision))
|
|
|
|
if p > self.p:
|
|
self.n = self.n * _tento(p - self.p)
|
|
elif p < self.p:
|
|
self.n = _roundquotient(self.n, _tento(self.p - p))
|
|
self.p = p
|
|
|
|
|
|
def __str__(self):
|
|
|
|
n, p = self.n, self.p
|
|
i, f = divmod(abs(n), _tento(p))
|
|
if p:
|
|
frac = repr(f)[:-1]
|
|
frac = "0" * (p - len(frac)) + frac
|
|
else:
|
|
frac = ""
|
|
return "-"[:n<0] + \
|
|
repr(i)[:-1] + \
|
|
"." + frac
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
return "FixedPoint" + repr((str(self), self.p))
|
|
|
|
|
|
def copy(self):
|
|
""" Create a copy of the current :class:`FixedPoint`. """
|
|
|
|
return _mkFP(self.n, self.p)
|
|
|
|
__copy__ = __deepcopy__ = copy
|
|
|
|
def __eq__(self, other):
|
|
if (other == None):
|
|
return False
|
|
xn, yn, p = _norm(self, other)
|
|
return not _cmp(xn, yn)
|
|
|
|
def __ge__(self, other):
|
|
if other is None:
|
|
return False
|
|
xn, yn, p = _norm(self, other)
|
|
return xn >= yn
|
|
|
|
def __le__(self, other):
|
|
if other is None:
|
|
return False
|
|
xn, yn, p = _norm(self, other)
|
|
return xn <= yn
|
|
|
|
def __hash__(self):
|
|
# caution! == values must have equal hashes, and a FixedPoint
|
|
# is essentially a rational in unnormalized form. There's
|
|
# really no choice here but to normalize it, so hash is
|
|
# potentially expensive.
|
|
n, p = self.__reduce()
|
|
|
|
# Obscurity: if the value is an exact integer, p will be 0 now,
|
|
# so the hash expression reduces to hash(n). So FixedPoints
|
|
# that happen to be exact integers hash to the same things as
|
|
# their int or long equivalents. This is Good. But if a
|
|
# FixedPoint happens to have a value exactly representable as
|
|
# a float, their hashes may differ. This is a teensy bit Bad.
|
|
return hash(n) ^ hash(p)
|
|
|
|
def __nonzero__(self):
|
|
return self.n != 0
|
|
|
|
def __bool__(self):
|
|
return self.n != 0
|
|
|
|
def __neg__(self):
|
|
return _mkFP(-self.n, self.p)
|
|
|
|
def __abs__(self):
|
|
if self.n >= 0:
|
|
return self.copy()
|
|
else:
|
|
return -self
|
|
|
|
def __add__(self, other):
|
|
n1, n2, p = _norm(self, other)
|
|
# n1/10**p + n2/10**p = (n1+n2)/10**p
|
|
return _mkFP(n1 + n2, p)
|
|
|
|
__radd__ = __add__
|
|
|
|
def __sub__(self, other):
|
|
if not isinstance(other, FixedPoint):
|
|
other = FixedPoint(other, self.p)
|
|
return self.__add__(-other)
|
|
|
|
def __rsub__(self, other):
|
|
return (-self) + other
|
|
|
|
def __mul__(self, other):
|
|
n1, n2, p = _norm(self, other)
|
|
# n1/10**p * n2/10**p = (n1*n2/10**p)/10**p
|
|
return _mkFP(_roundquotient(n1 * n2, _tento(p)), p)
|
|
|
|
__rmul__ = __mul__
|
|
|
|
def __div__(self, other):
|
|
n1, n2, p = _norm(self, other)
|
|
if n2 == 0:
|
|
raise ZeroDivisionError("FixedPoint division")
|
|
if n2 < 0:
|
|
n1, n2 = -n1, -n2
|
|
# n1/10**p / (n2/10**p) = n1/n2 = (n1*10**p/n2)/10**p
|
|
return _mkFP(_roundquotient(n1 * _tento(p), n2), p)
|
|
|
|
def __rdiv__(self, other):
|
|
n1, n2, p = _norm(self, other)
|
|
return _mkFP(n2, p) / self
|
|
|
|
def __divmod__(self, other):
|
|
n1, n2, p = _norm(self, other)
|
|
if n2 == 0:
|
|
raise ZeroDivisionError("FixedPoint modulo")
|
|
# floor((n1/10**p)/(n2*10**p)) = floor(n1/n2)
|
|
q = n1 / n2
|
|
# n1/10**p - q * n2/10**p = (n1 - q * n2)/10**p
|
|
return q, _mkFP(n1 - q * n2, p)
|
|
|
|
def __rdivmod__(self, other):
|
|
n1, n2, p = _norm(self, other)
|
|
return divmod(_mkFP(n2, p), self)
|
|
|
|
def __mod__(self, other):
|
|
return self.__divmod__(other)[1]
|
|
|
|
def __rmod__(self, other):
|
|
n1, n2, p = _norm(self, other)
|
|
return _mkFP(n2, p).__mod__(self)
|
|
|
|
# caution! float can lose precision
|
|
def __float__(self):
|
|
n, p = self.__reduce()
|
|
return float(n) / float(_tento(p))
|
|
|
|
# XXX should this round instead?
|
|
# XXX note e.g. long(-1.9) == -1L and long(1.9) == 1L in Python
|
|
# XXX note that __int__ inherits whatever __long__ does,
|
|
# XXX and .frac() is affected too
|
|
def __long__(self):
|
|
answer = abs(self.n) // _tento(self.p)
|
|
if self.n < 0:
|
|
answer = -answer
|
|
return answer
|
|
|
|
def __int__(self):
|
|
return int(self.__long__())
|
|
|
|
def __lt__(self, other):
|
|
if other is None:
|
|
return 1
|
|
xn, yn, p = _norm(self, other)
|
|
return xn < yn
|
|
|
|
def __gt__(self, other):
|
|
if other is None:
|
|
return 1
|
|
xn, yn, p = _norm(self, other)
|
|
return xn > yn
|
|
|
|
def frac(self):
|
|
"""
|
|
Returns fractional portion as a :class:`FixedPoint`.
|
|
|
|
:note: In :class:`FixedPoint`,
|
|
|
|
this equality holds true::
|
|
|
|
x = x.frac() + long(x)
|
|
|
|
|
|
"""
|
|
return self - long(self)
|
|
|
|
# return n, p s.t. self == n/10**p and n % 10 != 0
|
|
def __reduce(self):
|
|
n, p = self.n, self.p
|
|
if n == 0:
|
|
p = 0
|
|
while p and n % 10 == 0:
|
|
p = p - 1
|
|
n = n // 10
|
|
return n, p
|
|
|
|
# return 10L**n
|
|
|
|
def _tento(n, cache={}):
|
|
try:
|
|
return cache[n]
|
|
except KeyError:
|
|
answer = cache[n] = 10 ** n
|
|
return answer
|
|
|
|
# return xn, yn, p s.t.
|
|
# p = max(x.p, y.p)
|
|
# x = xn / 10**p
|
|
# y = yn / 10**p
|
|
#
|
|
# x must be FixedPoint to begin with; if y is not FixedPoint,
|
|
# it inherits its precision from x.
|
|
#
|
|
# Note that this is called a lot, so default-arg tricks are helpful.
|
|
|
|
def _norm(x, y, isinstance=isinstance, FixedPoint=FixedPoint,
|
|
_tento=_tento):
|
|
assert isinstance(x, FixedPoint)
|
|
if not isinstance(y, FixedPoint):
|
|
y = FixedPoint(y, x.p)
|
|
xn, yn = x.n, y.n
|
|
xp, yp = x.p, y.p
|
|
if xp > yp:
|
|
yn = yn * _tento(xp - yp)
|
|
p = xp
|
|
elif xp < yp:
|
|
xn = xn * _tento(yp - xp)
|
|
p = yp
|
|
else:
|
|
p = xp # same as yp
|
|
return xn, yn, p
|
|
|
|
def _mkFP(n, p, FixedPoint=FixedPoint):
|
|
f = FixedPoint()
|
|
f.n = n
|
|
f.p = p
|
|
return f
|
|
|
|
# divide x by y, rounding to int via nearest-even
|
|
# y must be > 0
|
|
# XXX which rounding modes are useful?
|
|
|
|
def _roundquotient(x, y):
|
|
assert y > 0
|
|
n, leftover = divmod(x, y)
|
|
c = _cmp(leftover << 1, y)
|
|
# c < 0 <-> leftover < y/2, etc
|
|
if c > 0 or (c == 0 and (n & 1) == 1):
|
|
n = n + 1
|
|
return n
|
|
|
|
|
|
def _cmp(a, b):
|
|
return a - b
|
|
|
|
|
|
# crud for parsing strings
|
|
import re
|
|
|
|
# There's an optional sign at the start, and an optional exponent
|
|
# at the end. The exponent has an optional sign and at least one
|
|
# digit. In between, must have either at least one digit followed
|
|
# by an optional fraction, or a decimal point followed by at least
|
|
# one digit. Yuck.
|
|
|
|
_parser = re.compile(r"""
|
|
\s*
|
|
(?P<sign>[-+])?
|
|
(
|
|
(?P<int>\d+) (\. (?P<frac>\d*))?
|
|
|
|
|
\. (?P<onlyfrac>\d+)
|
|
)
|
|
([eE](?P<exp>[-+]? \d+))?
|
|
\s* $
|
|
""", re.VERBOSE).match
|
|
|
|
del re
|
|
|
|
# return n, p s.t. float string value == n * 10**p exactly
|
|
|
|
def _string2exact(s):
|
|
m = _parser(s)
|
|
if m is None:
|
|
raise ValueError("can't parse as number: " + repr(s))
|
|
|
|
exp = m.group('exp')
|
|
if exp is None:
|
|
exp = 0
|
|
else:
|
|
exp = int(exp)
|
|
|
|
intpart = m.group('int')
|
|
if intpart is None:
|
|
intpart = "0"
|
|
fracpart = m.group('onlyfrac')
|
|
else:
|
|
fracpart = m.group('frac')
|
|
if fracpart is None or fracpart == "":
|
|
fracpart = "0"
|
|
assert intpart
|
|
assert fracpart
|
|
|
|
i, f = long(intpart), long(fracpart)
|
|
nfrac = len(fracpart)
|
|
i = i * _tento(nfrac) + f
|
|
exp = exp - nfrac
|
|
|
|
if m.group('sign') == "-":
|
|
i = -i
|
|
|
|
return i, exp
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
import wx
|
|
|
|
class MyFrame(wx.Frame):
|
|
|
|
def __init__(self, parent):
|
|
|
|
wx.Frame.__init__(self, parent, -1, "FloatSpin Demo")
|
|
|
|
panel = wx.Panel(self)
|
|
|
|
floatspin = FloatSpin(panel, -1, pos=(50, 50), min_val=0, max_val=1,
|
|
increment=0.01, value=0.1, agwStyle=FS_LEFT)
|
|
floatspin.SetFormat("%f")
|
|
floatspin.SetDigits(2)
|
|
|
|
|
|
# our normal wxApp-derived class, as usual
|
|
|
|
app = wx.App(0)
|
|
|
|
frame = MyFrame(None)
|
|
app.SetTopWindow(frame)
|
|
frame.Show()
|
|
|
|
app.MainLoop()
|
|
|