From f57e7cf1ecdd84e44af030ec0e64673fab2071e5 Mon Sep 17 00:00:00 2001 From: blitzmann Date: Wed, 26 Apr 2017 21:08:46 -0400 Subject: [PATCH] Implement FloatSpin as well as changing sec status via character editor --- gui/characterEditor.py | 52 ++ gui/utils/floatspin.py | 1662 ++++++++++++++++++++++++++++++++++++++++ service/character.py | 5 + 3 files changed, 1719 insertions(+) create mode 100644 gui/utils/floatspin.py diff --git a/gui/characterEditor.py b/gui/characterEditor.py index e36919e84..7312fdc68 100644 --- a/gui/characterEditor.py +++ b/gui/characterEditor.py @@ -20,6 +20,7 @@ # noinspection PyPackageRequirements import wx +from utils.floatspin import FloatSpin # noinspection PyPackageRequirements import wx.lib.newevent # noinspection PyPackageRequirements @@ -284,11 +285,19 @@ class SkillTreeView(wx.Panel): tree.SetColumnWidth(0, 500) + self.btnSecStatus = wx.Button(self, wx.ID_ANY, "Sec Status: {0:.2f}".format(char.secStatus or 0.0)) + self.btnSecStatus.Bind(wx.EVT_BUTTON, self.onSecStatus) + self.populateSkillTree() tree.Bind(wx.EVT_TREE_ITEM_EXPANDING, self.expandLookup) tree.Bind(wx.EVT_TREE_ITEM_RIGHT_CLICK, self.scheduleMenu) + bSizerButtons = wx.BoxSizer(wx.HORIZONTAL) + + bSizerButtons.Add(self.btnSecStatus, 0, wx.ALL, 5) + pmainSizer.Add(bSizerButtons, 0, wx.EXPAND, 5) + # bind the Character selection event self.charEditor.entityEditor.Bind(wx.EVT_CHOICE, self.charChanged) self.charEditor.Bind(GE.CHAR_LIST_UPDATED, self.populateSkillTree) @@ -322,6 +331,17 @@ class SkillTreeView(wx.Panel): self.Layout() + def onSecStatus(self, event): + sChar = Character.getInstance() + char = self.charEditor.entityEditor.getActiveEntity() + myDlg = SecStatusDialog(self, char.secStatus or 0.0) + res = myDlg.ShowModal() + if res == wx.ID_OK: + value = myDlg.floatSpin.GetValue() + sChar.setSecStatus(char, value) + self.btnSecStatus.SetLabel("Sec Status: {0:.2f}".format(value)) + myDlg.Destroy() + def cloneChanged(self, event): sChar = Character.getInstance() sChar.setAlphaClone(self.charEditor.entityEditor.getActiveEntity(), event.ClientData) @@ -334,6 +354,8 @@ class SkillTreeView(wx.Panel): if char.alphaCloneID == cloneID: self.clonesChoice.SetSelection(i) + self.btnSecStatus.SetLabel("Sec Status: {0:.2f}".format(char.secStatus or 0.0)) + self.populateSkillTree(event) def populateSkillTree(self, event=None): @@ -343,8 +365,10 @@ class SkillTreeView(wx.Panel): if char.name in ("All 0", "All 5"): self.clonesChoice.Disable() + self.btnSecStatus.Disable() else: self.clonesChoice.Enable() + self.btnSecStatus.Enable() groups = sChar.getSkillGroups() imageId = self.skillBookImageId @@ -700,3 +724,31 @@ class SaveCharacterAs(wx.Dialog): event.Skip() self.Close() + + +class SecStatusDialog(wx.Dialog): + + def __init__(self, parent, sec): + wx.Dialog.__init__(self, parent, title="Set Security Status", size=(275, 175)) + + self.SetSizeHintsSz(wx.DefaultSize, wx.DefaultSize) + + bSizer1 = wx.BoxSizer(wx.VERTICAL) + + self.m_staticText1 = wx.StaticText(self, wx.ID_ANY, + u"Security Status is used in some CONCORD hull calculations; you can set the characters security status here", + wx.DefaultPosition, wx.DefaultSize, 0) + self.m_staticText1.Wrap(-1) + bSizer1.Add(self.m_staticText1, 1, wx.ALL | wx.EXPAND, 5) + + + self.floatSpin = FloatSpin(self, value=sec, min_val=-5.0, max_val=5.0, increment=0.1, digits=2, size=(100, -1)) + bSizer1.Add(self.floatSpin, 1, wx.ALIGN_CENTER | wx.ALL, 5) + + btnOk = wx.Button(self, wx.ID_OK) + bSizer1.Add(btnOk, 0, wx.ALIGN_CENTER | wx.ALIGN_RIGHT | wx.ALL, 5) + + self.SetSizer(bSizer1) + self.Layout() + + self.Centre(wx.BOTH) diff --git a/gui/utils/floatspin.py b/gui/utils/floatspin.py new file mode 100644 index 000000000..b2e21f178 --- /dev/null +++ b/gui/utils/floatspin.py @@ -0,0 +1,1662 @@ +# --------------------------------------------------------------------------- # +# 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!!! +# +# +# 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:`PyControl`, 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 0.9 + + +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 + +# 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.PyCommandEvent): + """ 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.PyCommandEvent.__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.PyControl): + """ + :class:`FloatSpin` implements a floating point :class:`SpinCtrl`. It is built using a custom + :class:`PyControl`, 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.PyControl.__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(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.SetBestSize((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:`PyControl`. + """ + + 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_PRIOR: + + 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_NEXT: + + 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.GetSizeTuple() + + spin_width, _ = self._spinbutton.GetSizeTuple() + + 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() `, + 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, type(42)) or isinstance(value, type(42L)): + 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 = 0L + # 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, 1L << -e) + if value < 0: + n = -n + self.n = n + return + + if isinstance(value, type(42 - 42j)): + raise TypeError("can't convert complex to FixedPoint: " + + `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: " + `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: " + + `precision`) + if p < 0: + raise ValueError("precision must be >= 0: " + `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" + `(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 __cmp__(self, other): + + if (other == None): + return 1 + xn, yn, p = _norm(self, other) + return cmp(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 __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 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] = 10L ** 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 + + +# 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[-+])? + ( + (?P\d+) (\. (?P\d*))? + | + \. (?P\d+) + ) + ([eE](?P[-+]? \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: " + `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 diff --git a/service/character.py b/service/character.py index c6436b11e..ea5391781 100644 --- a/service/character.py +++ b/service/character.py @@ -279,6 +279,11 @@ class Character(object): char.alphaCloneID = cloneID eos.db.commit() + @staticmethod + def setSecStatus(char, secStatus): + char.secStatus = secStatus + eos.db.commit() + @staticmethod def getSkillDescription(itemID): return eos.db.getItem(itemID).description