# 12/21/2003 - Jeff Grimmett (grimmtooth@softhome.net)
#
# o wxColumnSorterMixin -> ColumnSorterMixin
-# o wxListCtrlAutoWidthMixin -> ListCtrlAutoWidthMixin
+# o wxListCtrlAutoWidthMixin -> ListCtrlAutoWidthMixin
+# ...
+# 13/10/2004 - Pim Van Heuven (pim@think-wize.com)
+# o wxTextEditMixin: Support Horizontal scrolling when TAB is pressed on long
+# ListCtrls, support for WXK_DOWN, WXK_UP, performance improvements on
+# very long ListCtrls, Support for virtual ListCtrls
+#
+# 15-Oct-2004 - Robin Dunn
+# o wxTextEditMixin: Added Shift-TAB support
#
import locale
list = self.GetListCtrl()
if not list:
raise ValueError, "No wx.ListCtrl available"
- self.Bind(wx.EVT_LIST_COL_CLICK, self.__OnColClick, list)
+ list.Bind(wx.EVT_LIST_COL_CLICK, self.__OnColClick, list)
def SetColumnCount(self, newNumColumns):
_OnResize method is called.
This mix-in class was written by Erik Westra <ewestra@wave.co.nz>
-"""
+ """
def __init__(self):
""" Standard initialiser.
"""
- self._lastColMinWidth = None
-
+ self._resizeColMinWidth = None
+ self._resizeColStyle = "LAST"
+ self._resizeCol = 0
self.Bind(wx.EVT_SIZE, self._onResize)
self.Bind(wx.EVT_LIST_COL_END_DRAG, self._onResize, self)
+ def setResizeColumn(self, col):
+ """
+ Specify which column that should be autosized. Pass either
+ 'LAST' or the column number. Default is 'LAST'.
+ """
+ if col == "LAST":
+ self._resizeColStyle = "LAST"
+ else:
+ self._resizeColStyle = "COL"
+ self._resizeCol = col
+
+
def resizeLastColumn(self, minWidth):
""" Resize the last column appropriately.
'minWidth' is the preferred minimum width for the last column.
"""
- self._lastColMinWidth = minWidth
+ self.resizeColumn(minWidth)
+
+
+ def resizeColumn(self, minWidth):
+ self._resizeColMinWidth = minWidth
self._doResize()
+
# =====================
# == Private Methods ==
We automatically resize the last column in the list.
"""
- wx.CallAfter(self._doResize)
+ if 'gtk2' in wx.PlatformInfo:
+ self._doResize()
+ else:
+ wx.CallAfter(self._doResize)
event.Skip()
or calculated a minimum width. This ensure that repeated calls to
_doResize() don't cause the last column to size itself too large.
"""
+
+ if not self: # avoid a PyDeadObject error
+ return
+
numCols = self.GetColumnCount()
if numCols == 0: return # Nothing to resize.
- if self._lastColMinWidth == None:
- self._lastColMinWidth = self.GetColumnWidth(numCols - 1)
+ if(self._resizeColStyle == "LAST"):
+ resizeCol = self.GetColumnCount()
+ else:
+ resizeCol = self._resizeCol
+
+ if self._resizeColMinWidth == None:
+ self._resizeColMinWidth = self.GetColumnWidth(resizeCol - 1)
# We're showing the vertical scrollbar -> allow for scrollbar width
# NOTE: on GTK, the scrollbar is included in the client size, but on
listWidth = listWidth - scrollWidth
totColWidth = 0 # Width of all columns except last one.
- for col in range(numCols-1):
- totColWidth = totColWidth + self.GetColumnWidth(col)
+ for col in range(numCols):
+ if col != (resizeCol-1):
+ totColWidth = totColWidth + self.GetColumnWidth(col)
- lastColWidth = self.GetColumnWidth(numCols - 1)
+ resizeColWidth = self.GetColumnWidth(resizeCol - 1)
- if totColWidth + self._lastColMinWidth > listWidth:
+ if totColWidth + self._resizeColMinWidth > listWidth:
# We haven't got the width to show the last column at its minimum
# width -> set it to its minimum width and allow the horizontal
# scrollbar to show.
- self.SetColumnWidth(numCols-1, self._lastColMinWidth)
+ self.SetColumnWidth(resizeCol-1, self._resizeColMinWidth)
return
# Resize the last column to take up the remaining available space.
- self.SetColumnWidth(numCols-1, listWidth - totColWidth)
+ self.SetColumnWidth(resizeCol-1, listWidth - totColWidth)
+
Works with both single-select and multi-select lists."""
ctrl = event.GetEventObject()
- if isinstance(ctrl, wxListCtrl):
+ if isinstance(ctrl, wx.ListCtrl):
n, flags = ctrl.HitTest(event.GetPosition())
if n >= 0:
if not ctrl.GetItemState(n, wx.LIST_STATE_SELECTED):
# ctrl.SetItemState(i, 0, SEL_FOC)
ctrl.SetItemState(n, SEL_FOC, SEL_FOC)
+
def getListCtrlSelection(listctrl, state=wx.LIST_STATE_SELECTED):
""" Returns list of item indexes of given state (selected by defaults) """
res = []
wxEVT_DOPOPUPMENU = wx.NewEventType()
EVT_DOPOPUPMENU = wx.PyEventBinder(wxEVT_DOPOPUPMENU, 0)
+
class ListCtrlSelectionManagerMix:
"""Mixin that defines a platform independent selection policy
self.Bind(EVT_DOPOPUPMENU, self.OnLCSMDoPopup)
# self.Connect(-1, -1, self.wxEVT_DOPOPUPMENU, self.OnLCSMDoPopup)
+
def getPopupMenu(self):
""" Override to implement dynamic menus (create) """
return self._menu
+
def setPopupMenu(self, menu):
""" Must be set for default behaviour """
self._menu = menu
+
def afterPopupMenu(self, menu):
""" Override to implement dynamic menus (destroy) """
pass
+
def getSelection(self):
res = getListCtrlSelection(self)
if self.GetWindowStyleFlag() & wx.LC_SINGLE_SEL:
else:
return res
+
def OnLCSMRightDown(self, event):
selectBeforePopup(event)
event.Skip()
evt.pos = event.GetPosition()
wx.PostEvent(self, evt)
+
def OnLCSMDoPopup(self, event):
self.PopupMenu(event.menu, event.pos)
self.afterPopupMenu(event.menu)
-#----------------------------------------------------------------------
+
+#----------------------------------------------------------------------------
+from bisect import bisect
+
+
+class TextEditMixin:
+ """
+ A mixin class that enables any text in any column of a
+ multi-column listctrl to be edited by clicking on the given row
+ and column. You close the text editor by hitting the ENTER key or
+ clicking somewhere else on the listctrl. You switch to the next
+ column by hiting TAB.
+
+ To use the mixin you have to include it in the class definition
+ and call the __init__ function::
+
+ class TestListCtrl(wx.ListCtrl, TextEditMixin):
+ def __init__(self, parent, ID, pos=wx.DefaultPosition,
+ size=wx.DefaultSize, style=0):
+ wx.ListCtrl.__init__(self, parent, ID, pos, size, style)
+ TextEditMixin.__init__(self)
+
+
+ Authors: Steve Zatz, Pim Van Heuven (pim@think-wize.com)
+ """
+
+ editorBgColour = wx.Colour(255,255,175) # Yellow
+ editorFgColour = wx.Colour(0,0,0) # black
+
+ def __init__(self):
+ #editor = wx.TextCtrl(self, -1, pos=(-1,-1), size=(-1,-1),
+ # style=wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB \
+ # |wx.TE_RICH2)
+
+ self.make_editor()
+ self.Bind(wx.EVT_TEXT_ENTER, self.CloseEditor)
+ self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
+ self.Bind(wx.EVT_LEFT_DCLICK, self.OnLeftDown)
+ self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnItemSelected)
+
+
+ def make_editor(self, col_style=wx.LIST_FORMAT_LEFT):
+ editor = wx.PreTextCtrl()
+
+ style =wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB|wx.TE_RICH2
+ style |= {wx.LIST_FORMAT_LEFT: wx.TE_LEFT,
+ wx.LIST_FORMAT_RIGHT: wx.TE_RIGHT,
+ wx.LIST_FORMAT_CENTRE : wx.TE_CENTRE
+ }[col_style]
+
+ editor.Create(self, -1, style=style)
+ editor.SetBackgroundColour(self.editorBgColour)
+ editor.SetForegroundColour(self.editorFgColour)
+ font = self.GetFont()
+ editor.SetFont(font)
+
+ self.curRow = 0
+ self.curCol = 0
+
+ editor.Hide()
+ self.editor = editor
+
+ self.col_style = col_style
+ self.editor.Bind(wx.EVT_CHAR, self.OnChar)
+ self.editor.Bind(wx.EVT_KILL_FOCUS, self.CloseEditor)
+
+
+ def OnItemSelected(self, evt):
+ self.curRow = evt.GetIndex()
+ evt.Skip()
+
+
+ def OnChar(self, event):
+ ''' Catch the TAB, Shift-TAB, cursor DOWN/UP key code
+ so we can open the editor at the next column (if any).'''
+
+ keycode = event.GetKeyCode()
+ if keycode == wx.WXK_TAB and event.ShiftDown():
+ self.CloseEditor()
+ if self.curCol-1 >= 0:
+ self.OpenEditor(self.curCol-1, self.curRow)
+
+ elif keycode == wx.WXK_TAB:
+ self.CloseEditor()
+ if self.curCol+1 < self.GetColumnCount():
+ self.OpenEditor(self.curCol+1, self.curRow)
+
+ elif keycode == wx.WXK_ESCAPE:
+ self.CloseEditor()
+
+ elif keycode == wx.WXK_DOWN:
+ self.CloseEditor()
+ if self.curRow+1 < self.GetItemCount():
+ self._SelectIndex(self.curRow+1)
+ self.OpenEditor(self.curCol, self.curRow)
+
+ elif keycode == wx.WXK_UP:
+ self.CloseEditor()
+ if self.curRow > 0:
+ self._SelectIndex(self.curRow-1)
+ self.OpenEditor(self.curCol, self.curRow)
+
+ else:
+ event.Skip()
+
+
+ def OnLeftDown(self, evt=None):
+ ''' Examine the click and double
+ click events to see if a row has been click on twice. If so,
+ determine the current row and columnn and open the editor.'''
+
+ if self.editor.IsShown():
+ self.CloseEditor()
+
+ x,y = evt.GetPosition()
+ row,flags = self.HitTest((x,y))
+
+ if row != self.curRow: # self.curRow keeps track of the current row
+ evt.Skip()
+ return
+
+ # the following should really be done in the mixin's init but
+ # the wx.ListCtrl demo creates the columns after creating the
+ # ListCtrl (generally not a good idea) on the other hand,
+ # doing this here handles adjustable column widths
+
+ self.col_locs = [0]
+ loc = 0
+ for n in range(self.GetColumnCount()):
+ loc = loc + self.GetColumnWidth(n)
+ self.col_locs.append(loc)
+
+
+ col = bisect(self.col_locs, x+self.GetScrollPos(wx.HORIZONTAL)) - 1
+ self.OpenEditor(col, row)
+
+
+ def OpenEditor(self, col, row):
+ ''' Opens an editor at the current position. '''
+
+ if self.GetColumn(col).m_format != self.col_style:
+ self.make_editor(self.GetColumn(col).m_format)
+
+ x0 = self.col_locs[col]
+ x1 = self.col_locs[col+1] - x0
+
+ scrolloffset = self.GetScrollPos(wx.HORIZONTAL)
+
+ # scroll forward
+ if x0+x1-scrolloffset > self.GetSize()[0]:
+ if wx.Platform == "__WXMSW__":
+ # don't start scrolling unless we really need to
+ offset = x0+x1-self.GetSize()[0]-scrolloffset
+ # scroll a bit more than what is minimum required
+ # so we don't have to scroll everytime the user presses TAB
+ # which is very tireing to the eye
+ addoffset = self.GetSize()[0]/4
+ # but be careful at the end of the list
+ if addoffset + scrolloffset < self.GetSize()[0]:
+ offset += addoffset
+
+ self.ScrollList(offset, 0)
+ scrolloffset = self.GetScrollPos(wx.HORIZONTAL)
+ else:
+ # Since we can not programmatically scroll the ListCtrl
+ # close the editor so the user can scroll and open the editor
+ # again
+ self.editor.SetValue(self.GetItem(row, col).GetText())
+ self.curRow = row
+ self.curCol = col
+ self.CloseEditor()
+ return
+
+ y0 = self.GetItemRect(row)[1]
+
+ editor = self.editor
+ editor.SetDimensions(x0-scrolloffset,y0, x1,-1)
+
+ editor.SetValue(self.GetItem(row, col).GetText())
+ editor.Show()
+ editor.Raise()
+ editor.SetSelection(-1,-1)
+ editor.SetFocus()
+
+ self.curRow = row
+ self.curCol = col
+
+
+ # FIXME: this function is usually called twice - second time because
+ # it is binded to wx.EVT_KILL_FOCUS. Can it be avoided? (MW)
+ def CloseEditor(self, evt=None):
+ ''' Close the editor and save the new value to the ListCtrl. '''
+ text = self.editor.GetValue()
+ self.editor.Hide()
+ self.SetFocus()
+
+ # post wxEVT_COMMAND_LIST_END_LABEL_EDIT
+ # Event can be vetoed. It doesn't has SetEditCanceled(), what would
+ # require passing extra argument to CloseEditor()
+ evt = wx.ListEvent(wx.wxEVT_COMMAND_LIST_END_LABEL_EDIT, self.GetId())
+ evt.m_itemIndex = self.curRow
+ evt.m_col = self.curCol
+ item = self.GetItem(self.curRow, self.curCol)
+ evt.m_item.SetId(item.GetId())
+ evt.m_item.SetColumn(item.GetColumn())
+ evt.m_item.SetData(item.GetData())
+ evt.m_item.SetText(text) #should be empty string if editor was canceled
+ ret = self.GetEventHandler().ProcessEvent(evt)
+ if not ret or evt.IsAllowed():
+ if self.IsVirtual():
+ # replace by whather you use to populate the virtual ListCtrl
+ # data source
+ self.SetVirtualData(self.curRow, self.curCol, text)
+ else:
+ self.SetStringItem(self.curRow, self.curCol, text)
+ self.RefreshItem(self.curRow)
+
+ def _SelectIndex(self, row):
+ listlen = self.GetItemCount()
+ if row < 0 and not listlen:
+ return
+ if row > (listlen-1):
+ row = listlen -1
+
+ self.SetItemState(self.curRow, ~wx.LIST_STATE_SELECTED,
+ wx.LIST_STATE_SELECTED)
+ self.EnsureVisible(row)
+ self.SetItemState(row, wx.LIST_STATE_SELECTED,
+ wx.LIST_STATE_SELECTED)
+
+
+
+#----------------------------------------------------------------------------