add custom TextCtrl allowed to enter long integer with bounds checking
authorAndrey Skvortsov <andrej.skvortzov@gmail.com>
Fri, 15 Sep 2017 20:01:21 +0300
changeset 1811 4e3c78a84c64
parent 1810 70768bd1dab3
child 1812 8626a4948d5e
add custom TextCtrl allowed to enter long integer with bounds checking

It's needed because wx.SpinCtrl supports only 'int' type. For UDINT
and (U)LINT long type is necessary.
controls/CustomIntCtrl.py
controls/__init__.py
tests/tools/test_CustomIntCtrl.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/controls/CustomIntCtrl.py	Fri Sep 15 20:01:21 2017 +0300
@@ -0,0 +1,88 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# This file is part of Beremiz, a Integrated Development Environment for
+# programming IEC 61131-3 automates supporting plcopen standard and CanFestival.
+#
+# Copyright (C) 2017: Andrey Skvortsov
+#
+# See COPYING file for copyrights details.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+
+import wx
+from wx.lib.intctrl import IntCtrl
+
+
+class CustomIntUpdatedEvent(wx.PyCommandEvent):
+    def __init__(self, id, value=0, object=None):
+        wx.PyCommandEvent.__init__(self, CustomIntCtrl.wxEVT_COMMAND_CUSTOM_INT_UPDATED, id)
+
+        self.__value = value
+        self.SetEventObject(object)
+
+    def GetValue(self):
+        """Retrieve the value of the control at the time
+        this event was generated."""
+        return self.__value
+
+
+class CustomIntCtrl(wx.lib.intctrl.IntCtrl):
+    """
+    This class provides a control that takes and returns long as
+    value, and provides bounds support and optional value limiting.
+
+    It handles entering negative numbers more user-friendly than
+    original wx.lib.intctrl.IntCtrl.
+
+    It applies limits as focus is changed to other control and
+    sends event afterwards to signal that editing is done.
+    """
+
+    # Used to trap events indicating that the current
+    # integer value of the control has been changed.
+    wxEVT_COMMAND_CUSTOM_INT_UPDATED = wx.NewEventType()
+    EVT_CUSTOM_INT = wx.PyEventBinder(wxEVT_COMMAND_CUSTOM_INT_UPDATED, 1)
+
+    def __init__(self, *args, **kwargs):
+        wx.lib.intctrl.IntCtrl.__init__(self, *args, **kwargs)
+        self.Bind(wx.EVT_KILL_FOCUS, self.UpdateValue)
+        self.SetLongAllowed(True)
+        self.SetLimited(False)
+
+    def GetValue(self):
+        """
+        Returns integer (long) value of the control,
+        but handles entering negative numbers
+        """
+        s = wx.TextCtrl.GetValue(self)
+        if s == '-':
+            s = ''
+        return self._fromGUI(s)
+
+    def GetValueStr(self):
+        """Returns string value of TextCtrl"""
+        return wx.TextCtrl.GetValue(self)
+
+    def UpdateValue(self, event):
+        self.SetLimited(True)
+        self.SetLimited(False)
+        try:
+            self.GetEventHandler().ProcessEvent(
+                CustomIntUpdatedEvent(self.GetId(), self.GetValue(), self))
+        except ValueError:
+            return
+        event.Skip()
--- a/controls/__init__.py	Fri Sep 15 19:53:49 2017 +0300
+++ b/controls/__init__.py	Fri Sep 15 20:01:21 2017 +0300
@@ -28,6 +28,7 @@
 from CustomGrid import CustomGrid
 from CustomTable import CustomTable
 from CustomTree import CustomTree
+from CustomIntCtrl import CustomIntCtrl
 from DebugVariablePanel import DebugVariablePanel
 from DurationCellEditor import DurationCellEditor
 from LibraryPanel import LibraryPanel
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/tools/test_CustomIntCtrl.py	Fri Sep 15 20:01:21 2017 +0300
@@ -0,0 +1,136 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# This file is part of Beremiz, a Integrated Development Environment for
+# programming IEC 61131-3 automates supporting plcopen standard and CanFestival.
+#
+# Copyright (C) 2017: Andrey Skvortsov
+#
+# See COPYING file for copyrights details.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+import unittest
+import wx
+import time
+
+import conftest
+import controls.CustomIntCtrl
+
+
+class TestCustomIntCtrl(unittest.TestCase):
+    def setUp(self):
+        self.app = wx.App()
+        self.frame = wx.Frame(None)
+
+    def tearDown(self):
+        self.frame.Destroy()
+        wx.CallAfter(self.app.Exit)
+        self.app.MainLoop()
+
+    def testMaxLimit(self):
+        """Test working upper bound"""
+        self.AddControls()
+        self.int_ctrl.SetValue(self.max_val + 100)
+        self.ProcessEvents()
+
+        self.txt_ctrl.SetFocus()
+        self.ProcessEvents()
+        self.assertEqual(self.int_ctrl.GetValue(), self.max_val)
+
+    def testMinLimit(self):
+        """Test working lower bound"""
+        self.AddControls()
+        self.int_ctrl.SetValue(self.min_val - 100)
+        self.ProcessEvents()
+
+        self.txt_ctrl.SetFocus()
+        self.ProcessEvents()
+
+        self.assertEqual(self.int_ctrl.GetValue(), self.min_val)
+
+    def testCorrectValue(self):
+        """Test case if no limiting is necessary"""
+        self.AddControls()
+        val = (self.max_val + self.min_val) / 2
+        self.int_ctrl.SetValue(val)
+        self.ProcessEvents()
+
+        self.txt_ctrl.SetFocus()
+        self.ProcessEvents()
+
+        self.assertEqual(self.int_ctrl.GetValue(), val)
+
+    def testEventBinding(self):
+        """Test event sending after edit and bound checks are done"""
+        self.AddControls()
+        self.event_happend = False
+
+        def EventHandler(event):
+            self.event_happend = True
+            event.Skip()
+
+        self.int_ctrl.Bind(controls.CustomIntCtrl.EVT_CUSTOM_INT, EventHandler)
+
+        val = (self.max_val + self.min_val) / 2
+
+        self.int_ctrl.SetValue(val)
+        self.ProcessEvents()
+        self.txt_ctrl.SetFocus()
+
+        self.ProcessEvents()
+        self.txt_ctrl.SetFocus()
+        self.ProcessEvents()
+
+        self.assertEqual(self.int_ctrl.GetValue(), val)
+        self.assertTrue(self.event_happend)
+
+    def testLongNumbers(self):
+        """Test support of long integer"""
+        self.AddControls()
+        val = 40000000000
+        self.int_ctrl.SetMax(val)
+        self.int_ctrl.SetValue(val)
+        self.ProcessEvents()
+
+        self.txt_ctrl.SetFocus()
+        self.ProcessEvents()
+
+        self.assertEqual(val, val)
+
+    def ProcessEvents(self):
+        for i in range(0, 10):
+            wx.Yield()
+            time.sleep(0.01)
+
+    def AddControls(self):
+        vs = wx.BoxSizer(wx.VERTICAL)
+        self.int_ctrl = controls.CustomIntCtrl(self.frame)
+        self.txt_ctrl = wx.TextCtrl(self.frame)
+        vs.Add(self.int_ctrl, 0, wx.ALIGN_CENTRE | wx.ALL, 5)
+        vs.Add(self.txt_ctrl, 0, wx.ALIGN_CENTRE | wx.ALL, 5)
+        self.frame.SetSizer(vs)
+        vs.Fit(self.frame)
+        self.frame.Show()
+        self.frame.Raise()
+
+        self.min_val = 50
+        self.max_val = 100
+        self.int_ctrl.SetBounds(self.min_val, self.max_val)
+        self.ProcessEvents()
+
+
+if __name__ == '__main__':
+    unittest.main()