1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473
|
'''
Task Coach - Your friendly task manager
Copyright (C) 2004-2014 Task Coach developers <developers@taskcoach.org>
Task Coach 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 3 of the License, or
(at your option) any later version.
Task Coach 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, see <http://www.gnu.org/licenses/>.
'''
''' Base classes for controls with items, such as ListCtrl, TreeCtrl,
and TreeListCtrl. ''' # pylint: disable=W0105
import wx, draganddrop, autowidth, tooltip, inspect
from taskcoachlib.thirdparty import hypertreelist
class _CtrlWithItemsMixin(object):
''' Base class for controls with items, such as ListCtrl, TreeCtrl,
TreeListCtrl, etc. '''
def _itemIsOk(self, item):
try:
return item.IsOk() # for Tree(List)Ctrl
except AttributeError:
return item != wx.NOT_FOUND # for ListCtrl
def _objectBelongingTo(self, item):
if not self._itemIsOk(item):
return None
try:
return self.GetItemPyData(item) # TreeListCtrl
except AttributeError:
return self.getItemWithIndex(item) # ListCtrl
def SelectItem(self, item, *args, **kwargs):
try:
# Tree(List)Ctrl:
super(_CtrlWithItemsMixin, self).SelectItem(item, *args, **kwargs)
except AttributeError:
# ListCtrl:
select = kwargs.get('select', True)
newState = wx.LIST_STATE_SELECTED
if not select:
newState = ~newState
self.SetItemState(item, newState, wx.LIST_STATE_SELECTED)
class _CtrlWithPopupMenuMixin(_CtrlWithItemsMixin):
''' Base class for controls with popupmenu's. '''
@staticmethod
def _attachPopupMenu(eventSource, eventTypes, eventHandler):
for eventType in eventTypes:
eventSource.Bind(eventType, eventHandler)
class _CtrlWithItemPopupMenuMixin(_CtrlWithPopupMenuMixin):
''' Popupmenu's on items. '''
def __init__(self, *args, **kwargs):
self.__popupMenu = kwargs.pop('itemPopupMenu')
super(_CtrlWithItemPopupMenuMixin, self).__init__(*args, **kwargs)
if self.__popupMenu is not None:
self._attachPopupMenu(self,
(wx.EVT_TREE_ITEM_RIGHT_CLICK, wx.EVT_CONTEXT_MENU),
self.onItemPopupMenu)
def onItemPopupMenu(self, event):
# Make sure the window this control is in has focus:
try:
window = event.GetEventObject().MainWindow
except AttributeError:
window = event.GetEventObject()
window.SetFocus()
if hasattr(event, 'GetPoint'):
# Make sure the item under the mouse is selected because that
# is what users expect and what is most user-friendly. Not all
# widgets do this by default, e.g. the TreeListCtrl does not.
item = self.HitTest(event.GetPoint())[0]
if not self._itemIsOk(item):
return
if not self.IsSelected(item):
self.UnselectAll()
self.SelectItem(item)
self.PopupMenu(self.__popupMenu)
class _CtrlWithColumnPopupMenuMixin(_CtrlWithPopupMenuMixin):
''' This class enables a right-click popup menu on column headers. The
popup menu should expect a public property columnIndex to be set so
that the control can tell the menu which column the user clicked to
popup the menu. '''
def __init__(self, *args, **kwargs):
self.__popupMenu = kwargs.pop('columnPopupMenu')
super(_CtrlWithColumnPopupMenuMixin, self).__init__(*args, **kwargs)
if self.__popupMenu is not None:
self._attachPopupMenu(self, [wx.EVT_LIST_COL_RIGHT_CLICK],
self.onColumnPopupMenu)
def onColumnPopupMenu(self, event):
# We store the columnIndex in the menu, because it's near to
# impossible for commands in the menu to determine on what column the
# menu was popped up.
columnIndex = event.GetColumn()
self.__popupMenu.columnIndex = columnIndex
# Because right-clicking on column headers does not automatically give
# focus to the control, we force the focus:
try:
window = event.GetEventObject().GetMainWindow()
except AttributeError:
window = event.GetEventObject()
window.SetFocus()
self.PopupMenuXY(self.__popupMenu, *event.GetPosition())
event.Skip(False)
class _CtrlWithDropTargetMixin(_CtrlWithItemsMixin):
''' Control that accepts files, e-mails or URLs being dropped onto items. '''
def __init__(self, *args, **kwargs):
self.__onDropURLCallback = kwargs.pop('onDropURL', None)
self.__onDropFilesCallback = kwargs.pop('onDropFiles', None)
self.__onDropMailCallback = kwargs.pop('onDropMail', None)
super(_CtrlWithDropTargetMixin, self).__init__(*args, **kwargs)
if self.__onDropURLCallback or self.__onDropFilesCallback or self.__onDropMailCallback:
dropTarget = draganddrop.DropTarget(self.onDropURL,
self.onDropFiles,
self.onDropMail,
self.onDragOver)
self.GetMainWindow().SetDropTarget(dropTarget)
def onDropURL(self, x, y, url):
item = self.HitTest((x, y))[0]
if self.__onDropURLCallback:
self.__onDropURLCallback(self._objectBelongingTo(item), url)
def onDropFiles(self, x, y, filenames):
item = self.HitTest((x, y))[0]
if self.__onDropFilesCallback:
self.__onDropFilesCallback(self._objectBelongingTo(item), filenames)
def onDropMail(self, x, y, mail):
item = self.HitTest((x, y))[0]
if self.__onDropMailCallback:
self.__onDropMailCallback(self._objectBelongingTo(item), mail)
def onDragOver(self, x, y, defaultResult):
item, flags = self.HitTest((x, y))[:2]
if self._itemIsOk(item):
if flags & wx.TREE_HITTEST_ONITEMBUTTON:
self.Expand(item)
return defaultResult
def GetMainWindow(self):
try:
return super(_CtrlWithDropTargetMixin, self).GetMainWindow()
except AttributeError:
return self
class CtrlWithToolTipMixin(_CtrlWithItemsMixin, tooltip.ToolTipMixin):
''' Control that has a different tooltip for each item '''
def __init__(self, *args, **kwargs):
super(CtrlWithToolTipMixin, self).__init__(*args, **kwargs)
self.__tip = tooltip.SimpleToolTip(self)
def OnBeforeShowToolTip(self, x, y):
item, _, column = self.HitTest(wx.Point(x, y))
domainObject = self._objectBelongingTo(item)
if domainObject:
tooltipData = self.getItemTooltipData(domainObject)
doShow = any([data[1] for data in tooltipData])
if doShow:
self.__tip.SetData(tooltipData)
return self.__tip
return None
class CtrlWithItemsMixin(_CtrlWithItemPopupMenuMixin, _CtrlWithDropTargetMixin):
pass
class Column(object):
def __init__(self, name, columnHeader, *eventTypes, **kwargs):
self.__name = name
self.__columnHeader = columnHeader
self.width = kwargs.pop('width', hypertreelist._DEFAULT_COL_WIDTH) # pylint: disable=W0212
# The event types to use for registering an observer that is
# interested in changes that affect this column:
self.__eventTypes = eventTypes
self.__sortCallback = kwargs.pop('sortCallback', None)
self.__renderCallback = kwargs.pop('renderCallback',
self.defaultRenderer)
self.__resizeCallback = kwargs.pop('resizeCallback', None)
self.__alignment = kwargs.pop('alignment', wx.LIST_FORMAT_LEFT)
self.__hasImages = 'imageIndicesCallback' in kwargs
self.__imageIndicesCallback = kwargs.pop('imageIndicesCallback',
self.defaultImageIndices) or self.defaultImageIndices
# NB: because the header image is needed for sorting a fixed header
# image cannot be combined with a sortable column
self.__headerImageIndex = kwargs.pop('headerImageIndex', -1)
self.__editCallback = kwargs.get('editCallback', None)
self.__editControlClass = kwargs.get('editControl', None)
self.__parse = kwargs.get('parse', lambda value: value)
self.__settings = kwargs.get('settings', None) # FIXME: Column shouldn't need to know about settings
def name(self):
return self.__name
def header(self):
return self.__columnHeader
def headerImageIndex(self):
return self.__headerImageIndex
def eventTypes(self):
return self.__eventTypes
def setWidth(self, width):
self.width = width
if self.__resizeCallback:
self.__resizeCallback(self, width)
def sort(self, *args, **kwargs):
if self.__sortCallback:
self.__sortCallback(*args, **kwargs)
def __filterArgs(self, func, kwargs):
actualKwargs = dict()
argNames = inspect.getargspec(func).args
return dict([(name, value) for name, value in kwargs.items() if name in argNames])
def render(self, *args, **kwargs):
return self.__renderCallback(*args, **self.__filterArgs(self.__renderCallback, kwargs))
def defaultRenderer(self, *args, **kwargs): # pylint: disable=W0613
return unicode(args[0])
def alignment(self):
return self.__alignment
def defaultImageIndices(self, *args, **kwargs): # pylint: disable=W0613
return {wx.TreeItemIcon_Normal: -1}
def imageIndices(self, *args, **kwargs):
return self.__imageIndicesCallback(*args, **kwargs)
def hasImages(self):
return self.__hasImages
def isEditable(self):
return self.__editControlClass != None and self.__editCallback != None
def onEndEdit(self, item, newValue):
self.__editCallback(item, newValue)
def editControl(self, parent, item, columnIndex, domainObject):
value = self.value(domainObject)
kwargs = dict(settings=self.__settings) if self.__settings else dict()
# pylint: disable=W0142
return self.__editControlClass(parent, wx.ID_ANY, item, columnIndex,
parent, value, **kwargs)
def parse(self, value):
return self.__parse(value)
def value(self, domainObject):
return getattr(domainObject, self.name())()
def __eq__(self, other):
return self.name() == other.name()
class _BaseCtrlWithColumnsMixin(object):
''' A base class for all controls with columns. Note that this class and
its subclasses do not support addition or deletion of columns after
the initial setting of columns. '''
def __init__(self, *args, **kwargs):
self.__allColumns = kwargs.pop('columns')
super(_BaseCtrlWithColumnsMixin, self).__init__(*args, **kwargs)
# This is used to keep track of which column has which
# index. The only other way would be (and was) find a column
# using its header, which causes problems when several columns
# have the same header. It's a list of (index, column) tuples.
self.__indexMap = []
self._setColumns()
def _setColumns(self):
for columnIndex, column in enumerate(self.__allColumns):
self._insertColumn(columnIndex, column)
def _insertColumn(self, columnIndex, column):
newMap = []
for colIndex, col in self.__indexMap:
if colIndex >= columnIndex:
newMap.append((colIndex + 1, col))
else:
newMap.append((colIndex, col))
newMap.append((columnIndex, column))
self.__indexMap = newMap
self.InsertColumn(columnIndex, column.header() if column.headerImageIndex() == -1 else u'',
format=column.alignment(), width=column.width)
columnInfo = self.GetColumn(columnIndex)
columnInfo.SetImage(column.headerImageIndex())
self.SetColumn(columnIndex, columnInfo)
def _deleteColumn(self, columnIndex):
newMap = []
for colIndex, col in self.__indexMap:
if colIndex > columnIndex:
newMap.append((colIndex - 1, col))
elif colIndex < columnIndex:
newMap.append((colIndex, col))
self.__indexMap = newMap
self.DeleteColumn(columnIndex)
def _allColumns(self):
return self.__allColumns
def _getColumn(self, columnIndex):
for colIndex, col in self.__indexMap:
if colIndex == columnIndex:
return col
raise IndexError
def _getColumnHeader(self, columnIndex):
''' The currently displayed column header in the column with index
columnIndex. '''
return self.GetColumn(columnIndex).GetText()
def _getColumnIndex(self, column):
''' The current column index of the column 'column'. '''
try:
return self.__allColumns.index(column) # Uses overriden __eq__
except ValueError:
raise ValueError, '%s: unknown column' % column.name()
class _CtrlWithHideableColumnsMixin(_BaseCtrlWithColumnsMixin):
''' This class supports hiding columns. '''
def showColumn(self, column, show=True):
''' showColumn shows or hides the column for column.
The column is actually removed or inserted into the control because
although TreeListCtrl supports hiding columns, ListCtrl does not.
'''
columnIndex = self._getColumnIndex(column)
if show and not self.isColumnVisible(column):
self._insertColumn(columnIndex, column)
elif not show and self.isColumnVisible(column):
self._deleteColumn(columnIndex)
def isColumnVisible(self, column):
return column in self._visibleColumns()
def _getColumnIndex(self, column):
''' _getColumnIndex returns the actual columnIndex of the column if it
is visible, or the position it would have if it were visible. '''
columnIndexWhenAllColumnsVisible = super(_CtrlWithHideableColumnsMixin, self)._getColumnIndex(column)
for columnIndex, visibleColumn in enumerate(self._visibleColumns()):
if super(_CtrlWithHideableColumnsMixin, self)._getColumnIndex(visibleColumn) >= columnIndexWhenAllColumnsVisible:
return columnIndex
return self.GetColumnCount() # Column header not found
def _visibleColumns(self):
return [self._getColumn(columnIndex) for columnIndex in range(self.GetColumnCount())]
class _CtrlWithSortableColumnsMixin(_BaseCtrlWithColumnsMixin):
''' This class adds sort indicators and clickable column headers that
trigger callbacks to (re)sort the contents of the control. '''
def __init__(self, *args, **kwargs):
super(_CtrlWithSortableColumnsMixin, self).__init__(*args, **kwargs)
self.Bind(wx.EVT_LIST_COL_CLICK, self.onColumnClick)
self.__currentSortColumn = self._getColumn(0)
self.__currentSortImageIndex = -1
def onColumnClick(self, event):
event.Skip(False)
# Make sure the window this control is in has focus:
try:
window = event.GetEventObject().GetMainWindow()
except AttributeError:
window = event.GetEventObject()
window.SetFocus()
columnIndex = event.GetColumn()
if 0 <= columnIndex < self.GetColumnCount():
column = self._getColumn(columnIndex)
# Use CallAfter to make sure the window this control is in is
# activated before we process the column click:
wx.CallAfter(column.sort, event)
def showSortColumn(self, column):
if column != self.__currentSortColumn:
self._clearSortImage()
self.__currentSortColumn = column
self._showSortImage()
def showSortOrder(self, imageIndex):
self.__currentSortImageIndex = imageIndex
self._showSortImage()
def _clearSortImage(self):
self.__setSortColumnImage(-1)
def _showSortImage(self):
self.__setSortColumnImage(self.__currentSortImageIndex)
def _currentSortColumn(self):
return self.__currentSortColumn
def __setSortColumnImage(self, imageIndex):
columnIndex = self._getColumnIndex(self.__currentSortColumn)
columnInfo = self.GetColumn(columnIndex)
if columnInfo.GetImage() == imageIndex:
pass # The column is already showing the right image, so we're done
else:
columnInfo.SetImage(imageIndex)
self.SetColumn(columnIndex, columnInfo)
class _CtrlWithAutoResizedColumnsMixin(autowidth.AutoColumnWidthMixin):
def __init__(self, *args, **kwargs):
super(_CtrlWithAutoResizedColumnsMixin, self).__init__(*args, **kwargs)
self.Bind(wx.EVT_LIST_COL_END_DRAG, self.onEndColumnResize)
def onEndColumnResize(self, event):
''' Save the column widths after the user did a resize. '''
for index, column in enumerate(self._visibleColumns()):
column.setWidth(self.GetColumnWidth(index))
event.Skip()
class CtrlWithColumnsMixin(_CtrlWithAutoResizedColumnsMixin,
_CtrlWithHideableColumnsMixin,
_CtrlWithSortableColumnsMixin,
_CtrlWithColumnPopupMenuMixin):
''' CtrlWithColumnsMixin combines the functionality of its four parent
classes: automatic resizing of columns, hideable columns, columns with
sort indicators, and column popup menu's. '''
def showColumn(self, column, show=True):
super(CtrlWithColumnsMixin, self).showColumn(column, show)
# Show sort indicator if the column that was just made visible is being sorted on
if show and column == self._currentSortColumn():
self._showSortImage()
def _clearSortImage(self):
# Only clear the sort image if the column in question is visible
if self.isColumnVisible(self._currentSortColumn()):
super(CtrlWithColumnsMixin, self)._clearSortImage()
def _showSortImage(self):
# Only show the sort image if the column in question is visible
if self.isColumnVisible(self._currentSortColumn()):
super(CtrlWithColumnsMixin, self)._showSortImage()
|