laurent@428: #!/usr/bin/env python
laurent@428: # -*- coding: utf-8 -*-
laurent@428: 
andrej@1571: # This file is part of Beremiz, a Integrated Development Environment for
andrej@1571: # programming IEC 61131-3 automates supporting plcopen standard and CanFestival.
andrej@1571: #
andrej@1571: # Copyright (C) 2007: Edouard TISSERANT and Laurent BESSARD
andrej@1571: #
andrej@1571: # See COPYING file for copyrights details.
andrej@1571: #
andrej@1571: # This program is free software; you can redistribute it and/or
andrej@1571: # modify it under the terms of the GNU General Public License
andrej@1571: # as published by the Free Software Foundation; either version 2
andrej@1571: # of the License, or (at your option) any later version.
andrej@1571: #
andrej@1571: # This program is distributed in the hope that it will be useful,
andrej@1571: # but WITHOUT ANY WARRANTY; without even the implied warranty of
andrej@1571: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
andrej@1571: # GNU General Public License for more details.
andrej@1571: #
andrej@1571: # You should have received a copy of the GNU General Public License
andrej@1571: # along with this program; if not, write to the Free Software
andrej@1571: # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
laurent@428: 
andrej@1881: 
kinsamanka@3758: import pickle
laurent@428: import wx
laurent@428: 
laurent@428: MAX_ITEM_COUNT = 10
laurent@428: MAX_ITEM_SHOWN = 6
laurent@428: if wx.Platform == '__WXMSW__':
Laurent@1180:     LISTBOX_BORDER_HEIGHT = 2
Laurent@1180:     LISTBOX_INTERVAL_HEIGHT = 0
laurent@428: else:
Laurent@1180:     LISTBOX_BORDER_HEIGHT = 4
Laurent@1180:     LISTBOX_INTERVAL_HEIGHT = 6
Laurent@1180: 
andrej@1736: 
Laurent@1180: class PopupWithListbox(wx.PopupWindow):
andrej@1735: 
andrej@1852:     def __init__(self, parent, choices=None):
Laurent@1180:         wx.PopupWindow.__init__(self, parent, wx.BORDER_SIMPLE)
andrej@1735: 
andrej@1745:         self.ListBox = wx.ListBox(self, -1, style=wx.LB_HSCROLL | wx.LB_SINGLE | wx.LB_SORT)
andrej@1735: 
andrej@1852:         choices = [] if choices is None else choices
laurent@428:         self.SetChoices(choices)
andrej@1735: 
Laurent@1180:         self.ListBox.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
Laurent@1180:         self.ListBox.Bind(wx.EVT_MOTION, self.OnMotion)
andrej@1735: 
laurent@428:     def SetChoices(self, choices):
laurent@428:         max_text_width = 0
laurent@428:         max_text_height = 0
andrej@1735: 
laurent@428:         self.ListBox.Clear()
laurent@428:         for choice in choices:
laurent@428:             self.ListBox.Append(choice)
laurent@428:             w, h = self.ListBox.GetTextExtent(choice)
laurent@428:             max_text_width = max(max_text_width, w)
laurent@428:             max_text_height = max(max_text_height, h)
andrej@1735: 
laurent@428:         itemcount = min(len(choices), MAX_ITEM_SHOWN)
laurent@428:         width = self.Parent.GetSize()[0]
andrej@1767:         height = \
andrej@1767:             max_text_height * itemcount + \
andrej@1767:             LISTBOX_INTERVAL_HEIGHT * max(0, itemcount - 1) + \
andrej@1767:             2 * LISTBOX_BORDER_HEIGHT
laurent@428:         if max_text_width + 10 > width:
laurent@428:             height += 15
laurent@428:         size = wx.Size(width, height)
Laurent@1180:         if wx.Platform == '__WXMSW__':
Laurent@1180:             size.width -= 2
laurent@428:         self.ListBox.SetSize(size)
laurent@428:         self.SetClientSize(size)
andrej@1735: 
laurent@428:     def MoveSelection(self, direction):
laurent@428:         selected = self.ListBox.GetSelection()
laurent@428:         if selected == wx.NOT_FOUND:
laurent@428:             if direction >= 0:
laurent@428:                 selected = 0
laurent@428:             else:
laurent@428:                 selected = self.ListBox.GetCount() - 1
laurent@428:         else:
laurent@428:             selected = (selected + direction) % (self.ListBox.GetCount() + 1)
laurent@428:         if selected == self.ListBox.GetCount():
laurent@428:             selected = wx.NOT_FOUND
laurent@428:         self.ListBox.SetSelection(selected)
andrej@1735: 
laurent@428:     def GetSelection(self):
laurent@428:         return self.ListBox.GetStringSelection()
andrej@1735: 
Laurent@1180:     def OnLeftDown(self, event):
andrej@1498:         selected = self.ListBox.HitTest(wx.Point(event.GetX(), event.GetY()))
Laurent@1180:         parent_size = self.Parent.GetSize()
Laurent@1180:         parent_rect = wx.Rect(0, -parent_size[1], parent_size[0], parent_size[1])
laurent@428:         if selected != wx.NOT_FOUND:
laurent@428:             wx.CallAfter(self.Parent.SetValueFromSelected, self.ListBox.GetString(selected))
edouard@3303:         elif parent_rect.Contains(event.GetX(), event.GetY()):
andrej@1498:             result, x, y = self.Parent.HitTest(wx.Point(event.GetX(), event.GetY() + parent_size[1]))
Laurent@1180:             if result != wx.TE_HT_UNKNOWN:
Laurent@1180:                 self.Parent.SetInsertionPoint(self.Parent.XYToPosition(x, y))
Laurent@1180:         else:
Laurent@1180:             wx.CallAfter(self.Parent.DismissListBox)
Laurent@1180:         event.Skip()
andrej@1735: 
Laurent@1180:     def OnMotion(self, event):
Laurent@1180:         self.ListBox.SetSelection(
andrej@1498:             self.ListBox.HitTest(wx.Point(event.GetX(), event.GetY())))
Laurent@1180:         event.Skip()
andrej@1735: 
andrej@1736: 
laurent@428: class TextCtrlAutoComplete(wx.TextCtrl):
laurent@428: 
andrej@1771:     def __init__(self, parent, choices=None, dropDownClick=True,
andrej@1771:                  element_path=None, **therest):
laurent@428:         """
laurent@428:         Constructor works just like wx.TextCtrl except you can pass in a
laurent@428:         list of choices.  You can also change the choice list at any time
laurent@428:         by calling setChoices.
laurent@428:         """
laurent@428: 
laurent@428:         therest['style'] = wx.TE_PROCESS_ENTER | therest.get('style', 0)
laurent@428: 
laurent@428:         wx.TextCtrl.__init__(self, parent, **therest)
andrej@1735: 
andrej@1733:         # Some variables
laurent@428:         self._dropDownClick = dropDownClick
laurent@428:         self._lastinsertionpoint = None
Laurent@1180:         self._hasfocus = False
andrej@1735: 
laurent@428:         self._screenheight = wx.SystemSettings.GetMetric(wx.SYS_SCREEN_Y)
laurent@428:         self.element_path = element_path
andrej@1735: 
laurent@428:         self.listbox = None
andrej@1735: 
laurent@428:         self.SetChoices(choices)
laurent@428: 
andrej@1782:         # gp = self
andrej@1782:         # while ( gp != None ) :
laurent@428:         #    gp.Bind ( wx.EVT_MOVE , self.onControlChanged, gp )
laurent@428:         #    gp.Bind ( wx.EVT_SIZE , self.onControlChanged, gp )
laurent@428:         #    gp = gp.GetParent()
laurent@428: 
laurent@428:         self.Bind(wx.EVT_KILL_FOCUS, self.OnControlChanged)
laurent@428:         self.Bind(wx.EVT_TEXT_ENTER, self.OnControlChanged)
laurent@428:         self.Bind(wx.EVT_TEXT, self.OnEnteredText)
laurent@428:         self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
laurent@428: 
andrej@1733:         # If need drop down on left click
laurent@428:         if dropDownClick:
laurent@428:             self.Bind(wx.EVT_LEFT_DOWN, self.OnClickToggleDown)
laurent@428:             self.Bind(wx.EVT_LEFT_UP, self.OnClickToggleUp)
laurent@428: 
laurent@428:     def ChangeValue(self, value):
laurent@428:         wx.TextCtrl.ChangeValue(self, value)
laurent@428:         self.RefreshListBoxChoices()
laurent@428: 
laurent@428:     def OnEnteredText(self, event):
laurent@428:         wx.CallAfter(self.RefreshListBoxChoices)
laurent@428:         event.Skip()
laurent@428: 
laurent@428:     def OnKeyDown(self, event):
laurent@428:         """ Do some work when the user press on the keys:
laurent@428:             up and down: move the cursor
laurent@428:         """
laurent@428:         keycode = event.GetKeyCode()
laurent@428:         if keycode in [wx.WXK_DOWN, wx.WXK_UP]:
laurent@428:             self.PopupListBox()
laurent@428:             if keycode == wx.WXK_DOWN:
laurent@428:                 self.listbox.MoveSelection(1)
laurent@428:             else:
laurent@428:                 self.listbox.MoveSelection(-1)
laurent@428:         elif keycode in [wx.WXK_LEFT, wx.WXK_RIGHT, wx.WXK_RETURN] and self.listbox is not None:
Laurent@1180:             selected = self.listbox.GetSelection()
Laurent@1180:             if selected != "":
Laurent@1180:                 self.SetValueFromSelected(selected)
Laurent@1180:             else:
Laurent@1180:                 event.Skip()
laurent@428:         elif event.GetKeyCode() == wx.WXK_ESCAPE:
laurent@428:             self.DismissListBox()
laurent@428:         else:
laurent@428:             event.Skip()
laurent@428: 
laurent@428:     def OnClickToggleDown(self, event):
laurent@428:         self._lastinsertionpoint = self.GetInsertionPoint()
laurent@428:         event.Skip()
laurent@428: 
laurent@428:     def OnClickToggleUp(self, event):
Laurent@1180:         if not self._hasfocus:
Laurent@1180:             self._hasfocus = True
Laurent@1180:         elif self.GetInsertionPoint() == self._lastinsertionpoint:
laurent@428:             wx.CallAfter(self.PopupListBox)
laurent@428:         self._lastinsertionpoint = None
laurent@428:         event.Skip()
laurent@428: 
laurent@428:     def OnControlChanged(self, event):
laurent@428:         res = self.GetValue()
laurent@428:         config = wx.ConfigBase.Get()
kinsamanka@3758:         listentries = pickle.loads(config.Read(self.element_path,
kinsamanka@3758:                                                pickle.dumps([], 0).decode()
kinsamanka@3758:                                               ).encode())
laurent@428:         if res and res not in listentries:
laurent@428:             listentries = (listentries + [res])[-MAX_ITEM_COUNT:]
kinsamanka@3758:             config.Write(self.element_path, pickle.dumps(listentries, 0))
laurent@428:             config.Flush()
laurent@428:             self.SetChoices(listentries)
laurent@428:         self.DismissListBox()
Laurent@1180:         self._hasfocus = False
laurent@428:         event.Skip()
andrej@1735: 
laurent@428:     def SetChoices(self, choices):
laurent@428:         self._choices = choices
laurent@428:         self.RefreshListBoxChoices()
andrej@1735: 
laurent@428:     def GetChoices(self):
laurent@428:         return self._choices
andrej@1735: 
laurent@428:     def SetValueFromSelected(self, selected):
Laurent@1180:         """
Laurent@1180:         Sets the wx.TextCtrl value from the selected wx.ListCtrl item.
Laurent@1180:         Will do nothing if no item is selected in the wx.ListCtrl.
Laurent@1180:         """
Laurent@1180:         if selected != "":
laurent@428:             self.SetValue(selected)
Laurent@1180:         self.DismissListBox()
andrej@1735: 
laurent@428:     def RefreshListBoxChoices(self):
laurent@428:         if self.listbox is not None:
laurent@428:             text = self.GetValue()
laurent@428:             choices = [choice for choice in self._choices if choice.startswith(text)]
laurent@428:             self.listbox.SetChoices(choices)
laurent@428: 
laurent@428:     def PopupListBox(self):
laurent@428:         if self.listbox is None:
laurent@428:             self.listbox = PopupWithListbox(self)
andrej@1735: 
laurent@428:             # Show the popup right below or above the button
laurent@428:             # depending on available screen space...
laurent@428:             pos = self.ClientToScreen((0, 0))
laurent@428:             sz = self.GetSize()
Laurent@1180:             if wx.Platform == '__WXMSW__':
Laurent@1180:                 pos.x -= 2
Laurent@1180:                 pos.y -= 2
laurent@428:             self.listbox.Position(pos, (0, sz[1]))
andrej@1735: 
laurent@428:             self.RefreshListBoxChoices()
andrej@1735: 
Laurent@1180:             self.listbox.Show()
laurent@428: 
laurent@428:     def DismissListBox(self):
laurent@428:         if self.listbox is not None:
Laurent@1180:             if self.listbox.ListBox.HasCapture():
Laurent@1180:                 self.listbox.ListBox.ReleaseMouse()
Laurent@1180:             self.listbox.Destroy()
laurent@428:             self.listbox = None