# -*- coding: utf-8 -*- |
|
import copy |
|
import sys |
|
import datetime |
|
import locale |
|
import time |
|
import operator |
|
import re |
|
import warnings |
|
from decimal import Decimal |
|
from decimal import InvalidOperation |
|
import wx |
|
import wx.grid |
|
from wx._core import PyAssertionError |
|
import dabo |
|
from dabo.ui import makeDynamicProperty |
|
if __name__ == "__main__": |
|
dabo.ui.loadUI("wx") |
|
import dabo.dEvents as dEvents |
|
import dabo.dException as dException |
|
from dabo.dLocalize import _, n_ |
|
from dabo.lib.utils import ustr |
|
import dControlMixin as cm |
|
import dKeys |
|
import dUICursors |
|
import dabo.biz |
|
import dabo.dColors as dColors |
|
from dabo.dObject import dObject |
|
import dabo.lib.dates |
|
from dabo.lib.utils import noneSortKey, caseInsensitiveSortKey |
|
from dabo.dBug import loggit |
|
|
|
|
|
# Make this locale-independent |
|
# JK: We can't set this up on module load because locale |
|
# is set not until dApp is completely setup. |
|
decimalPoint = None |
|
|
|
|
|
|
|
class dGridDataTable(wx.grid.PyGridTableBase): |
|
|
|
def __init__(self, parent): |
|
super(dGridDataTable, self).__init__() |
|
self._clearCache() |
|
self.grid = parent |
|
self._initTable() |
|
|
|
def _clearCache(self): |
|
self.__cachedVals = {} |
|
self.__cachedAttrs = {} |
|
|
|
def _initTable(self): |
|
self.colDefs = [] |
|
self._oldRowCount = 0 |
|
self.grid.setTableAttributes(self) |
|
|
|
|
|
def GetAttr(self, row, col, kind=0): |
|
col = self._convertWxColNumToDaboColNum(col) |
|
if col is None: |
|
# Empty grid so far, no biggie: |
|
return self.grid._defaultGridColAttr.Clone() |
|
|
|
cv = self.__cachedAttrs.get((row, col)) |
|
if cv: |
|
diff = time.time() - cv[1] |
|
if diff < 10: ## if it's been less than this # of seconds. |
|
return cv[0].Clone() |
|
|
|
dcol = self.grid.Columns[col] |
|
dcol._updateCellDynamicProps(row) |
|
|
|
if dcol._gridCellAttrs: |
|
attr = dcol._gridCellAttrs.get(row, dcol._gridColAttr).Clone() |
|
else: |
|
attr = dcol._gridColAttr.Clone() |
|
|
|
## Now, override with a custom renderer for this row/col if applicable. |
|
## Note that only the renderer is handled here, as we are segfaulting when |
|
## handling the editor here. |
|
r = dcol.getRendererClassForRow(row) |
|
if r is not None: |
|
rnd = r() |
|
attr.SetRenderer(rnd) |
|
if r in (dcol.floatRendererClass, dcol.decimalRendererClass): |
|
rnd.SetPrecision(dcol.Precision) |
|
|
|
# Now check for alternate row coloration |
|
if self.alternateRowColoring: |
|
attr.SetBackgroundColour((self.rowColorEven, self.rowColorOdd)[row % 2]) |
|
|
|
# Prevents overwriting when a long cell has None in the one next to it. |
|
attr.SetOverflow(False) |
|
self.__cachedAttrs[(row, col)] = (attr.Clone(), time.time()) |
|
return attr |
|
|
|
|
|
def GetRowLabelValue(self, row): |
|
try: |
|
return self.grid.RowLabels[row] |
|
except IndexError: |
|
return "" |
|
|
|
|
|
def GetColLabelValue(self, col): |
|
return "" |
|
|
|
|
|
def setColumns(self, colDefs): |
|
"""Create columns based on passed list of column definitions.""" |
|
if colDefs == self.colDefs: |
|
# Already done, no need to take the time. |
|
return |
|
|
|
for idx, col in enumerate(colDefs): |
|
nm = col.DataField |
|
while not nm: |
|
nm = ustr(idx) |
|
idx += 1 |
|
if nm in colDefs: |
|
nm = "" |
|
colName = "Column_%s" % nm |
|
pos = col._getUserSetting("Order") |
|
if pos is not None: |
|
col.Order = pos |
|
|
|
# If the data types are actual types and not strings, convert |
|
# them to common strings. |
|
if isinstance(col.DataType, type): |
|
typeDict = { |
|
str : "string", |
|
unicode : "unicode", |
|
bool : "bool", |
|
int : "integer", |
|
float : "float", |
|
long : "long", |
|
datetime.date : "date", |
|
datetime.datetime : "datetime", |
|
datetime.time : "time", |
|
Decimal: "decimal"} |
|
try: |
|
col.DataType = typeDict[col.DataType] |
|
except KeyError: |
|
# Not one of the standard types. Extract it from |
|
# the string version of the type |
|
try: |
|
col.DataType = ustr(col.DataType).split("'")[1].lower() |
|
except IndexError: |
|
# Something's odd. Print an error message and move on. |
|
dabo.log.error("Unknown data type found in setColumns(): %s" |
|
% col.DataType) |
|
col.DataType = ustr(col.DataType) |
|
|
|
# Make sure that all cols have an Order set |
|
for num in range(len(colDefs)): |
|
col = colDefs[num] |
|
if col.Order < 0: |
|
col.Order = num |
|
colDefs.sort(self.orderSort) |
|
self.colDefs = copy.copy(colDefs) |
|
|
|
def orderSort(self, col1, col2): |
|
return cmp(col1.Order, col2.Order) |
|
|
|
|
|
def convertType(self, typ): |
|
""" |
|
Convert common types, names and abbreviations for |
|
data types into the constants needed by the wx.grid. |
|
""" |
|
# Default |
|
ret = wx.grid.GRID_VALUE_STRING |
|
if type(typ) == str: |
|
lowtyp = typ.lower() |
|
else: |
|
lowtyp = typ |
|
if typ is Decimal: |
|
lowtyp = "decimal" |
|
if lowtyp in (bool, "bool", "boolean", "logical", "l"): |
|
ret = wx.grid.GRID_VALUE_BOOL |
|
if lowtyp in (int, long, "int", "integer", "bigint", "i", "long"): |
|
ret = wx.grid.GRID_VALUE_NUMBER |
|
elif lowtyp in (str, unicode, "char", "varchar", "text", "c", "s"): |
|
ret = wx.grid.GRID_VALUE_STRING |
|
elif lowtyp in (float, "float", "f", "decimal"): |
|
ret = wx.grid.GRID_VALUE_FLOAT |
|
elif lowtyp in (datetime.date, datetime.datetime, datetime.time, |
|
"date", "datetime", "time", "d", "t"): |
|
ret = wx.grid.GRID_VALUE_DATETIME |
|
return ret |
|
|
|
|
|
def CanGetValueAs(self, row, col, typ): |
|
col = self._convertWxColNumToDaboColNum(col) |
|
if self.grid.useCustomGetValue: |
|
return self.grid.customCanGetValueAs(row, col, typ) |
|
else: |
|
dcol = self.grid.Columns[col] |
|
return typ == self.convertType(dcol.DataType) |
|
|
|
|
|
def CanSetValueAs(self, row, col, typ): |
|
col = self._convertWxColNumToDaboColNum(col) |
|
if self.grid.useCustomSetValue: |
|
return self.grid.customCanSetValueAs(row, col, typ) |
|
else: |
|
dcol = self.grid.Columns[col] |
|
return typ == self.convertType(dcol.DataType) |
|
|
|
|
|
def fillTable(self, force=False): |
|
""" |
|
Fill the grid's data table to match the data set. Returns the number |
|
of rows in the table. |
|
""" |
|
_oldRowCount = self._oldRowCount |
|
|
|
# Get the data from the grid. |
|
bizobj = self.grid.getBizobj() |
|
|
|
if bizobj: |
|
dataSet = bizobj |
|
_newRowCount = dataSet.RowCount |
|
self._bizobj = bizobj |
|
else: |
|
self._bizobj = None |
|
dataSet = self.grid.DataSet |
|
if dataSet is None: |
|
return 0 |
|
_newRowCount = len(dataSet) |
|
if _oldRowCount is None: |
|
## still haven't tracked down why, but bizobj grids needed _oldRowCount |
|
## to be initialized to None, or extra rows would be added. Since we |
|
## aren't a bizobj grid, we need to change that None to 0 here so that |
|
## the rows can get appended below. |
|
_oldRowCount = 0 |
|
|
|
if _oldRowCount == _newRowCount and not force: |
|
return _newRowCount |
|
|
|
self.grid._syncRowCount() |
|
# Column widths come from multiple places. In decreasing precedence: |
|
# 1) dApp user settings, |
|
# 2) col.Width (as set by the Width prop or by the fieldspecs) |
|
# 3) have the grid autosize |
|
|
|
for idx, col in enumerate(self.grid._columns): |
|
gridCol = idx |
|
|
|
# 1) Try to get the column width from the saved user settings: |
|
width = col._getUserSetting("Width") |
|
|
|
if width is None: |
|
# 2) Try to get the column width from the column definition: |
|
width = col.Width |
|
|
|
if width is None or (width < 0): |
|
# 3) Have the grid autosize: |
|
self.grid.autoSizeCol(gridCol) |
|
else: |
|
col.Width = width |
|
|
|
# Show the row labels, if any |
|
for idx, label in enumerate(self.grid.RowLabels): |
|
self.SetRowLabelValue(idx, label) |
|
|
|
self._oldRowCount = _newRowCount |
|
return _newRowCount |
|
|
|
|
|
# The following methods are required by the grid, to find out certain |
|
# important details about the underlying table. |
|
# def GetNumberRows(self): |
|
# bizobj = self.grid.getBizobj() |
|
# if bizobj: |
|
# return bizobj.RowCount |
|
# try: |
|
# num = len(self.grid.DataSet) |
|
# except: |
|
# num = 0 |
|
# return num |
|
|
|
|
|
# def GetNumberCols(self, useNative=False): |
|
# if useNative: |
|
# return super(dGridDataTable, self).GetNumberCols() |
|
# else: |
|
# return self.grid.ColumnCount |
|
|
|
|
|
# def IsEmptyCell(self, row, col): |
|
# if row >= self.grid.RowCount: |
|
# return True |
|
# return False |
|
|
|
|
|
def GetValue(self, row, col, useCache=True, convertNoneToString=True, |
|
dynamicUpdate=True, _fromGridEditor=False): |
|
col = self._convertWxColNumToDaboColNum(col) |
|
if useCache and not _fromGridEditor: |
|
cv = self.__cachedVals.get((row, col)) |
|
if cv: |
|
diff = time.time() - cv[1] |
|
if diff < 10: ## if it's been less than this # of seconds. |
|
return cv[0] |
|
|
|
if col is None: |
|
# No corresponding Dabo column for this column; must be not visible. |
|
return "" |
|
|
|
bizobj = self.grid.getBizobj() |
|
col_obj = self.grid.Columns[col] |
|
field = col_obj.DataField |
|
if dynamicUpdate: |
|
col_obj._updateDynamicProps() |
|
col_obj._updateCellDynamicProps(row) |
|
ret = "" |
|
if bizobj: |
|
if field and (row < bizobj.RowCount): |
|
try: |
|
ret = bizobj.getFieldVal(field, row) |
|
except dException.FieldNotFoundException: |
|
pass |
|
if not _fromGridEditor: |
|
ret = self.getStringValue(ret) |
|
else: |
|
try: |
|
ret = self.grid.DataSet[row][field] |
|
except (TypeError, IndexError, KeyError): |
|
pass |
|
if ret is None and convertNoneToString: |
|
ret = self.grid.NoneDisplay |
|
if not _fromGridEditor: |
|
self.__cachedVals[(row, col)] = (ret, time.time()) |
|
return ret |
|
|
|
|
|
def getStringValue(self, val): |
|
"""Get the string value to display in the grid.""" |
|
if isinstance(val, datetime.datetime): |
|
return dabo.lib.dates.getStringFromDateTime(val) |
|
elif isinstance(val, datetime.date): |
|
return dabo.lib.dates.getStringFromDate(val) |
|
return val |
|
|
|
|
|
def SetValue(self, row, col, value, _fromGridEditor=False): |
|
col = self._convertWxColNumToDaboColNum(col) |
|
self.grid._setCellValue(row, col, value) |
|
if not _fromGridEditor: |
|
# Update the cache |
|
self.__cachedVals[(row, col)] = (value, time.time()) |
|
self.grid.afterCellEdit(row, col) |
|
|
|
|
|
def _convertWxColNumToDaboColNum(self, wxCol): |
|
return self.grid._convertWxColNumToDaboColNum(wxCol) |
|
|
|
|
|
|
|
class GridListEditor(wx.grid.GridCellChoiceEditor): |
|
def __init__(self, *args, **kwargs): |
|
dabo.log.info("GridListEditor: Init ") |
|
dabo.log.info(ustr(args)) |
|
dabo.log.info(ustr(kwargs)) |
|
super(GridListEditor, self).__init__(*args, **kwargs) |
|
|
|
|
|
def Create(self, parent, id, evtHandler, *args, **kwargs): |
|
dabo.log.info("GridListEditor: Create") |
|
dabo.log.info(ustr(args)) |
|
dabo.log.info(ustr(kwargs)) |
|
self.control = dabo.ui.dDropdownList(parent=parent, id=id, |
|
ValueMode="String") |
|
self.SetControl(self.control) |
|
if evtHandler: |
|
self.control.PushEventHandler(evtHandler) |
|
# super(GridListEditor, self).Create(parent, id, evtHandler) |
|
|
|
|
|
def Clone(self): |
|
return self.__class__() |
|
|
|
|
|
def SetParameters(self, paramStr): |
|
dabo.log.info("GridListEditor: SetParameters: %s" % paramStr) |
|
self.control.Choices = eval(paramStr) |
|
|
|
|
|
def BeginEdit(self, row, col, grid): |
|
dabo.log.info("GridListEditor: BeginEdit (%d,%d)" % (row, col)) |
|
self.value = grid.GetTable().GetValue(row, col) |
|
dabo.log.info("GridListEditor: Value=%s" % self.value) |
|
dabo.log.info("GridListEditor: Choices=%s" % self.control.Choices) |
|
try: |
|
self.control.Value = self.value |
|
except ValueError: |
|
dabo.log.info("GridListEditor: Value not in Choices") |
|
self.control.SetFocus() |
|
|
|
|
|
def EndEdit(self, row, col, grid): |
|
dabo.log.info("GridListEditor: EndEdit (%d,%d)" % (row, col)) |
|
changed = False |
|
v = self.control.Value |
|
if v != self.value: |
|
changed = True |
|
if changed: |
|
grid.GetTable().SetValue(row, col, value) |
|
self.value = "" |
|
self.control.Value = self.value |
|
return changed |
|
|
|
|
|
def Reset(self): |
|
dabo.log.info("GridListEditor: Reset") |
|
self.control.Value = self.value |
|
|
|
# def SetSize(self, rectorig): |
|
# dabo.log.info("GridListEditor: SetSize: %s" % rectorig) |
|
# dabo.log.info("GridListEditor: type of rectorig: %s" % type(rectorig)) |
|
# # rect = wx.Rect(rectorig) |
|
# # dabo.log.info("GridListEditor RECT: %s" % rect) |
|
# super(GridListEditor, self).SetSize(rectorig) |
|
|
|
def IsAcceptedKey(self, key): |
|
dabo.log.info("GridListEditor: check key: %d" % (key)) |
|
return true |
|
|
|
|
|
|
|
class dColumn(dabo.ui.dPemMixinBase.dPemMixinBase): |
|
""" |
|
These aren't the actual columns that appear in the grid; rather, |
|
they provide a way to interact with the underlying grid table in a more |
|
straightforward manner. |
|
""" |
|
_call_beforeInit, _call_afterInit, _call_initProperties = False, True, True |
|
|
|
def __init__(self, parent, properties=None, attProperties=None, |
|
*args, **kwargs): |
|
self._isConstructed = False |
|
self._dynamic = {} |
|
# Initialize the attributes for DataField and DataType |
|
self._dataField = "" |
|
self._dataType = "" |
|
self._expand = False |
|
# Default to 2 decimal places |
|
self._precision = 2 |
|
# Do text columns wrap their long text? |
|
self._wordWrap = False |
|
# Is the column shown? |
|
self._visible = True |
|
# Holds the default renderer class for the column |
|
self._rendererClass = None |
|
# Custom editors/renderers |
|
self._customRenderers = {} |
|
self._customEditors = {} |
|
|
|
#Declare Internal Header Attributes |
|
self._headerVerticalAlignment = "Center" |
|
self._headerHorizontalAlignment = "Center" |
|
self._headerForeColor = None |
|
self._headerBackColor = None |
|
|
|
dataFieldSent = "DataField" in kwargs |
|
dataTypeSent = "DataType" in kwargs |
|
precisionSent = "Precision" in kwargs |
|
|
|
self._beforeInit() |
|
kwargs["Parent"] = parent |
|
# dColumn maintains one attr object that the grid table will use for |
|
# setting properties such as ForeColor and Font on the entire column. |
|
att = self._gridColAttr = parent._defaultGridColAttr.Clone() |
|
att.SetFont(self._getDefaultFont()._nativeFont) |
|
|
|
self._gridCellAttrs = {} |
|
|
|
super(dColumn, self).__init__(properties=properties, attProperties=attProperties, |
|
*args, **kwargs) |
|
self._baseClass = dColumn |
|
if dataFieldSent and not dataTypeSent: |
|
implicitPrecision = not precisionSent |
|
self._setDataTypeFromDataField(implicitPrecision) |
|
|
|
|
|
def _beforeInit(self): |
|
# Define the cell renderer and editor classes |
|
import gridRenderers |
|
self.stringRendererClass = wx.grid.GridCellStringRenderer |
|
self.wrapStringRendererClass = wx.grid.GridCellAutoWrapStringRenderer |
|
self.boolRendererClass = gridRenderers.BoolRenderer |
|
self.intRendererClass = wx.grid.GridCellNumberRenderer |
|
self.longRendererClass = wx.grid.GridCellNumberRenderer |
|
self.decimalRendererClass = wx.grid.GridCellFloatRenderer |
|
self.floatRendererClass = wx.grid.GridCellFloatRenderer |
|
self.listRendererClass = wx.grid.GridCellStringRenderer |
|
self.imageRendererClass = gridRenderers.ImageRenderer |
|
self.stringEditorClass = wx.grid.GridCellTextEditor |
|
self.wrapStringEditorClass = wx.grid.GridCellAutoWrapStringEditor |
|
self.boolEditorClass = wx.grid.GridCellBoolEditor |
|
self.intEditorClass = wx.grid.GridCellNumberEditor |
|
self.longEditorClass = wx.grid.GridCellNumberEditor |
|
self.decimalEditorClass = wx.grid.GridCellFloatEditor |
|
self.floatEditorClass = wx.grid.GridCellFloatEditor |
|
self.listEditorClass = wx.grid.GridCellChoiceEditor |
|
# self.listEditorClass = GridListEditor |
|
|
|
self.defaultRenderers = { |
|
"str" : self.stringRendererClass, |
|
"string" : self.stringRendererClass, |
|
"date" : self.stringRendererClass, |
|
"datetime" : self.stringRendererClass, |
|
"bool" : self.boolRendererClass, |
|
"int" : self.intRendererClass, |
|
"long" : self.longRendererClass, |
|
"decimal" : self.decimalRendererClass, |
|
"float" : self.floatRendererClass, |
|
"list" : self.listRendererClass, |
|
str : self.stringRendererClass, |
|
unicode : self.stringRendererClass, |
|
datetime.date : self.stringRendererClass, |
|
datetime.datetime : self.stringRendererClass, |
|
bool : self.boolRendererClass, |
|
int : self.intRendererClass, |
|
long : self.longRendererClass, |
|
float : self.floatRendererClass, |
|
Decimal: self.decimalRendererClass, |
|
list : self.listRendererClass} |
|
self.defaultEditors = { |
|
"str" : self.stringEditorClass, |
|
"string" : self.stringEditorClass, |
|
"date" : self.stringEditorClass, |
|
"datetime" : self.stringEditorClass, |
|
"bool" : self.boolEditorClass, |
|
"int" : self.intEditorClass, |
|
"integer" : self.intEditorClass, |
|
"long" : self.longEditorClass, |
|
"decimal" : self.decimalEditorClass, |
|
"float" : self.floatEditorClass, |
|
"list" : self.listEditorClass, |
|
str : self.stringEditorClass, |
|
unicode : self.stringEditorClass, |
|
datetime.date : self.stringEditorClass, |
|
datetime.datetime : self.stringEditorClass, |
|
bool : self.boolEditorClass, |
|
int : self.intEditorClass, |
|
long : self.longEditorClass, |
|
float : self.floatEditorClass, |
|
Decimal: self.decimalEditorClass, |
|
list : self.listEditorClass} |
|
|
|
# Default to string renderer |
|
self._rendererClass = self.stringRendererClass |
|
super(dColumn, self)._beforeInit() |
|
|
|
|
|
def _afterInit(self): |
|
self._isConstructed = True |
|
super(dColumn, self)._afterInit() |
|
dabo.ui.callAfter(self._restoreFontZoom) |
|
|
|
|
|
def getDataTypeForColumn(self): |
|
try: |
|
typ = self.DataType |
|
except (dException.FieldNotFoundException, dException.NoRecordsException): |
|
typ = None |
|
return typ |
|
|
|
|
|
def _setRenderer(self): |
|
self._setDataTypeFromDataField() |
|
custom = self.CustomRendererClass |
|
if custom: |
|
self._rendererClass = custom |
|
else: |
|
typ = self.getDataTypeForColumn() |
|
self._rendererClass = self.defaultRenderers.get(typ, self.stringRendererClass) |
|
|
|
|
|
@dabo.ui.deadCheck |
|
def _updateDynamicProps(self): |
|
for prop, func in self._dynamic.items(): |
|
if prop[:4] != "Cell": |
|
if isinstance(func, tuple): |
|
args = func[1:] |
|
func = func[0] |
|
else: |
|
args = () |
|
setattr(self, prop, func(*args)) |
|
|
|
|
|
def _updateCellDynamicProps(self, row): |
|
kwargs = {"row": row} |
|
self._cellDynamicRow = row |
|
for prop, func in self._dynamic.items(): |
|
if prop[:4] == "Cell": |
|
if isinstance(func, tuple): |
|
args = func[1:] |
|
func = func[0] |
|
else: |
|
args = () |
|
setattr(self, prop, func(*args, **kwargs)) |
|
del self._cellDynamicRow |
|
|
|
|
|
def _restoreFontZoom(self): |
|
if self.Form and self.Form.SaveRestorePosition: |
|
super(dColumn, self)._restoreFontZoom() |
|
|
|
|
|
def _getDefaultFont(self): |
|
ret = dabo.ui.dFont(Size=10, Bold=False, Italic=False, |
|
Underline=False) |
|
if sys.platform.startswith("win"): |
|
# The wx default is quite ugly |
|
try: |
|
ret.Face = "Arial" |
|
ret.Size = 9 |
|
except dException.FontNotFoundException: |
|
# I had this happen to a customer running Win XP. No idea why Arial |
|
# would be missing. --pkm 2009-10-24 |
|
pass |
|
return ret |
|
|
|
|
|
def _constructed(self): |
|
return self._isConstructed |
|
|
|
|
|
def release(self): |
|
""" |
|
Usually don't need this, but it helps to keep this in |
|
line with other Dabo objects. |
|
""" |
|
try: |
|
self.Parent.removeColumn(self) |
|
except ValueError: |
|
# Will happen when the column has already been removed |
|
pass |
|
|
|
|
|
def _setAbsoluteFontZoom(self, newZoom): |
|
origFontSize = self._origFontSize = getattr(self, "_origFontSize", self.FontSize) |
|
origHeaderFontSize = self._origHeaderFontSize = getattr(self, "_origHeaderFontSize", self.HeaderFontSize) |
|
fontSize = origFontSize + newZoom |
|
headerFontSize = origHeaderFontSize + newZoom |
|
self._currFontZoom = newZoom |
|
if fontSize > 1: |
|
self.FontSize = fontSize |
|
if headerFontSize > 1: |
|
self.HeaderFontSize = headerFontSize |
|
|
|
if self.Form is not None: |
|
dabo.ui.callAfterInterval(200, self.Form.layout) |
|
|
|
|
|
def _setEditor(self, row): |
|
""" |
|
Set the editor for the entire column based on the editor for this row. |
|
|
|
This is a workaround to a problem that is preventing us from setting the |
|
editor for a specific cell at the time the grid needs it. |
|
""" |
|
edClass = self.getEditorClassForRow(row) |
|
attr = self._gridColAttr.Clone() |
|
if edClass: |
|
kwargs = {} |
|
if edClass in (wx.grid.GridCellChoiceEditor,): |
|
kwargs["choices"] = self.getListEditorChoicesForRow(row) |
|
elif edClass in (wx.grid.GridCellFloatEditor,): |
|
kwargs["precision"] = self.Precision |
|
editor = edClass(**kwargs) |
|
attr.SetEditor(editor) |
|
# if edClass is self.floatEditorClass: |
|
# editor.SetPrecision(self.Precision) |
|
self._gridColAttr = attr |
|
|
|
|
|
def getListEditorChoicesForRow(self, row): |
|
"""Return the list of choices for the list editor for the given row.""" |
|
return self.CustomListEditorChoices.get(row, self.ListEditorChoices) |
|
|
|
|
|
def getEditorClassForRow(self, row): |
|
"""Return the cell editor class for the passed row.""" |
|
return self.CustomEditors.get(row, self.EditorClass) |
|
|
|
|
|
def _getValueForRow(self, row): |
|
if self.Parent: |
|
return self.Parent.getColumnValueByRow(self, row) |
|
|
|
|
|
def getRendererClassForRow(self, row): |
|
"""Return the cell renderer class for the passed row.""" |
|
if self._getValueForRow(row) == self.Parent.NoneDisplay: |
|
# Null values in the data should be rendered as strings, |
|
# no matter what type the column is. |
|
return self.stringRendererClass |
|
return self._customRenderers.get(row, self._rendererClass) |
|
|
|
|
|
def _getHeaderRect(self): |
|
"""Return the rect of this header in the header window.""" |
|
grid = self.Parent |
|
height = self.Parent.HeaderHeight |
|
width = self.Width |
|
top = 0 |
|
|
|
# Thanks Roger Binns: |
|
left = -grid.GetViewStart()[0] * grid.GetScrollPixelsPerUnit()[0] |
|
for col in range(self.Parent.ColumnCount): |
|
colObj = self.Parent.Columns[col] |
|
if not colObj.Visible: |
|
continue |
|
if colObj == self: |
|
break |
|
left += colObj.Width |
|
|
|
return wx.Rect(left, top, width, height) |
|
|
|
|
|
def _refreshHeader(self): |
|
"""Refresh just this column's header.""" |
|
if self.Parent: |
|
# This will trigger wx to query GetColLabelValue(), which will in turn |
|
# call paintHeader() on just this column. It's roundabout, but gives the |
|
# best overall results, but risks relying on wx implementation details. |
|
# Other options, in case this starts to fail, are: |
|
# self.Parent.Header.Refresh() |
|
# self.Parent._paintHeader(self._GridColumnIndex) |
|
self.Parent.SetColLabelValue(self.ColumnIndex, "") |
|
|
|
def _refreshGrid(self): |
|
"""Refresh the grid region, not the header region.""" |
|
if self.Parent: |
|
gw = self.Parent.GetGridWindow() |
|
gw.Refresh() |
|
|
|
|
|
def _persist(self, prop): |
|
"""Persist the current prop setting to the user settings table.""" |
|
self._setUserSetting(prop, getattr(self, prop)) |
|
|
|
|
|
def _setDataTypeFromDataField(self, implicitPrecision=True): |
|
""" |
|
When a column has its DataField changed, we need to set the |
|
correct DataType based on the new value. |
|
""" |
|
if self.Parent: |
|
currDT = self.DataType |
|
dt = self.Parent.typeFromDataField(self.DataField, self) |
|
if dt not in (None, type(None)) and (dt != currDT): |
|
self.DataType = dt |
|
if dt is Decimal and implicitPrecision: |
|
self.Precision = self.Parent.precisionFromDataField(self.DataField) |
|
|
|
|
|
def _getUserSetting(self, prop): |
|
"""Get the property value from the user settings table.""" |
|
app = self.Application |
|
grid = self.Parent |
|
form = grid.Form |
|
colName = "column_%s" % self.DataField |
|
|
|
if app is not None and form is not None \ |
|
and not hasattr(grid, "isDesignerControl"): |
|
settingName = "%s.%s.%s.%s" % (form.Name, grid.Name, colName, prop) |
|
return app.getUserSetting(settingName) |
|
return None |
|
|
|
|
|
def _setUserSetting(self, prop, val): |
|
"""Set the property value to the user settings table.""" |
|
app = self.Application |
|
grid = self.Parent |
|
form = grid.Form |
|
colName = "column_%s" % self.DataField |
|
|
|
if app is not None and form is not None \ |
|
and not hasattr(grid, "isDesignerControl"): |
|
settingName = "%s.%s.%s.%s" % (form.Name, grid.Name, colName, prop) |
|
app.setUserSetting(settingName, val) |
|
|
|
|
|
def _getColumnIndex(self): |
|
"""Return our column index in the grid, or -1.""" |
|
try: |
|
return self.Parent.Columns.index(self) |
|
except (ValueError, AttributeError): |
|
return -1 |
|
|
|
|
|
def _updateEditor(self): |
|
"""The Field, DataType, or CustomEditor has changed: set in the attr""" |
|
editorClass = self.EditorClass |
|
if editorClass is None: |
|
editor = None |
|
else: |
|
kwargs = {} |
|
if editorClass in (wx.grid.GridCellChoiceEditor,): |
|
kwargs["choices"] = self.ListEditorChoices |
|
# Fix for editor precision issue. |
|
elif editorClass in (wx.grid.GridCellFloatEditor,): |
|
kwargs["precision"] = self.Precision |
|
editor = editorClass(**kwargs) |
|
self._gridColAttr.SetEditor(editor) |
|
|
|
|
|
def _updateRenderer(self): |
|
"""The Field, DataType, or CustomRenderer has changed: set in the attr""" |
|
self._setRenderer() |
|
rendClass = self.CustomRendererClass or self.RendererClass |
|
if rendClass is None: |
|
renderer = None |
|
else: |
|
renderer = rendClass() |
|
self._gridColAttr.SetRenderer(renderer) |
|
|
|
|
|
def _onFontPropsChanged(self, evt): |
|
# Sent by the dFont object when any props changed. Wx needs to be notified: |
|
self._gridColAttr.SetFont(self.Font._nativeFont) |
|
self._refreshGrid() |
|
|
|
|
|
def _onHeaderFontPropsChanged(self, evt): |
|
# Sent by the dFont object when any props changed. Wx needs to be notified: |
|
self._refreshHeader() |
|
|
|
|
|
def _setCellProp(self, wxPropName, *args, **kwargs): |
|
"""Called from all of the Cell property setters.""" |
|
## dynamic prop uses cellDynamicRow; reg prop uses self.CurrentRow |
|
try: |
|
row = getattr(self, "_cellDynamicRow", self.Parent.CurrentRow) |
|
except dabo.ui.deadObjectException: |
|
# @dabo.ui.deadCheck didn't seem to work... |
|
return |
|
cellAttr = obj = self._gridCellAttrs.get(row, self._gridColAttr.Clone()) |
|
if "." in wxPropName: |
|
# For instance, Font.SetWeight |
|
wxPropName, subObject = wxPropName.split(".") |
|
obj = getattr(cellAttr, wxPropName) |
|
getattr(obj, subObject)(*args, **kwargs) |
|
setattr(cellAttr, wxPropName, obj) |
|
else: |
|
getattr(cellAttr, wxPropName)(*args, **kwargs) |
|
self._gridCellAttrs[row] = cellAttr |
|
|
|
|
|
def _getBackColor(self): |
|
return self._gridColAttr.GetBackgroundColour() |
|
|
|
def _setBackColor(self, val): |
|
if self._constructed(): |
|
if isinstance(val, basestring): |
|
val = dColors.colorTupleFromName(val) |
|
self._gridColAttr.SetBackgroundColour(val) |
|
self._refreshGrid() |
|
else: |
|
self._properties["BackColor"] = val |
|
|
|
|
|
def _getCaption(self): |
|
try: |
|
v = self._caption |
|
except AttributeError: |
|
v = self._caption = "Column" |
|
return v |
|
|
|
def _setCaption(self, val): |
|
if self._constructed(): |
|
self._caption = val |
|
self._refreshHeader() |
|
else: |
|
self._properties["Caption"] = val |
|
|
|
|
|
def _getCellBackColor(self): |
|
row = self.Parent.CurrentRow |
|
cellAttr = self._gridCellAttrs.get(row, False) |
|
if cellAttr: |
|
return cellAttr.GetBackgroundColour() |
|
return self.BackColor |
|
|
|
def _setCellBackColor(self, val): |
|
if self._constructed(): |
|
if isinstance(val, basestring): |
|
val = dColors.colorTupleFromName(val) |
|
self._setCellProp("SetBackgroundColour", val) |
|
else: |
|
self._properties["CellBackColor"] = val |
|
|
|
|
|
def _getCellFontBold(self): |
|
row = self.Parent.CurrentRow |
|
cellAttr = self._gridCellAttrs.get(row, False) |
|
if cellAttr: |
|
return cellAttr.GetFont().GetWeight() == wx.BOLD |
|
return self.FontBold |
|
|
|
def _setCellFontBold(self, val): |
|
if self._constructed(): |
|
if val: |
|
val = wx.FONTWEIGHT_BOLD |
|
else: |
|
val = wx.FONTWEIGHT_NORMAL |
|
self._setCellProp("Font.SetWeight", val) |
|
else: |
|
self._properties["CellFontBold"] = val |
|
|
|
|
|
def _getCellForeColor(self): |
|
row = self.Parent.CurrentRow |
|
cellAttr = self._gridCellAttrs.get(row, False) |
|
if cellAttr: |
|
return cellAttr.GetTextColour() |
|
return self.ForeColor |
|
|
|
def _setCellForeColor(self, val): |
|
if self._constructed(): |
|
if isinstance(val, basestring): |
|
val = dColors.colorTupleFromName(val) |
|
self._setCellProp("SetTextColour", val) |
|
else: |
|
self._properties["CellForeColor"] = val |
|
|
|
|
|
def _getCustomEditorClass(self): |
|
try: |
|
v = self._customEditorClass |
|
except AttributeError: |
|
v = self._customEditorClass = None |
|
return v |
|
|
|
def _setCustomEditorClass(self, val): |
|
if self._constructed(): |
|
self._customEditorClass = val |
|
self._updateEditor() |
|
else: |
|
self._properties["CustomEditorClass"] = val |
|
|
|
|
|
def _getCustomEditors(self): |
|
try: |
|
v = self._customEditors |
|
except AttributeError: |
|
v = self._customEditors = {} |
|
return v |
|
|
|
def _setCustomEditors(self, val): |
|
self._customEditors = val |
|
|
|
|
|
def _getCustomListEditorChoices(self): |
|
try: |
|
v = self._customListEditorChoices |
|
except AttributeError: |
|
v = self._customListEditorChoices = {} |
|
return v |
|
|
|
def _setCustomListEditorChoices(self, val): |
|
self._customListEditorChoices = val |
|
|
|
|
|
def _getCustomRendererClass(self): |
|
try: |
|
v = self._customRendererClass |
|
except AttributeError: |
|
v = self._customRendererClass = None |
|
return v |
|
|
|
def _setCustomRendererClass(self, val): |
|
if self._constructed(): |
|
self._customRendererClass = val |
|
self._updateRenderer() |
|
else: |
|
self._properties["CustomRendererClass"] = val |
|
|
|
|
|
def _getCustomRenderers(self): |
|
try: |
|
v = self._customRenderers |
|
except AttributeError: |
|
v = self._customRenderers = {} |
|
return v |
|
|
|
def _setCustomRenderers(self, val): |
|
self._customRenderers = val |
|
|
|
|
|
def _getDataType(self): |
|
try: |
|
v = self._dataType |
|
except AttributeError: |
|
v = self._dataType = "str" |
|
return v |
|
|
|
def _setDataType(self, val): |
|
if self._constructed(): |
|
if isinstance(val, basestring): |
|
if val.lower().strip() in ("str", "string", "char", "varchar", ""): |
|
val = "str" |
|
if self._dataType == val: |
|
return |
|
self._dataType = val |
|
if "Automatic" in self.HorizontalAlignment: |
|
self._setAutoHorizontalAlignment() |
|
self._updateRenderer() |
|
self._updateEditor() |
|
else: |
|
self._properties["DataType"] = val |
|
|
|
|
|
def _getEditable(self): |
|
return not self._gridColAttr.IsReadOnly() |
|
|
|
def _setEditable(self, val): |
|
if self._constructed(): |
|
self._gridColAttr.SetReadOnly(not val) |
|
if self.Parent: |
|
self.Parent.refresh() |
|
else: |
|
self._properties["Editable"] = val |
|
|
|
|
|
def _getEditorClass(self): |
|
v = self.CustomEditorClass |
|
if v is None: |
|
v = self.defaultEditors.get(self.DataType) |
|
return v |
|
|
|
|
|
def _getExpand(self): |
|
return self._expand |
|
|
|
def _setExpand(self, val): |
|
if self._constructed(): |
|
self._expand = val |
|
else: |
|
self._properties["Expand"] = val |
|
|
|
|
|
def _getDataField(self): |
|
try: |
|
v = self._dataField |
|
except AttributeError: |
|
v = self._dataField = "" |
|
return v |
|
|
|
def _setDataField(self, val): |
|
if self._constructed(): |
|
if self._dataField: |
|
# Use a callAfter, since the parent may not be finished instantiating yet. |
|
dabo.ui.callAfter(self._setDataTypeFromDataField) |
|
self._dataField = val |
|
if not self.Name or self.Name == "?": |
|
self._name = _("col_%s") % val |
|
self._updateRenderer() |
|
self._updateEditor() |
|
else: |
|
self._properties["DataField"] = val |
|
|
|
|
|
def _getFont(self): |
|
if hasattr(self, "_font"): |
|
v = self._font |
|
else: |
|
v = self.Font = dabo.ui.dFont(_nativeFont=self._gridColAttr.GetFont()) |
|
return v |
|
|
|
def _setFont(self, val): |
|
assert isinstance(val, dabo.ui.dFont) |
|
if self._constructed(): |
|
self._font = val |
|
self._gridColAttr.SetFont(val._nativeFont) |
|
val.bindEvent(dEvents.FontPropertiesChanged, self._onFontPropsChanged) |
|
self._refreshGrid() |
|
else: |
|
self._properties["Font"] = val |
|
|
|
|
|
def _getFontBold(self): |
|
return self.Font.Bold |
|
|
|
def _setFontBold(self, val): |
|
if self._constructed(): |
|
self.Font.Bold = val |
|
else: |
|
self._properties["FontBold"] = val |
|
|
|
|
|
def _getFontDescription(self): |
|
return self.Font.Description |
|
|
|
|
|
def _getFontInfo(self): |
|
return self.Font._nativeFont.GetNativeFontInfoDesc() |
|
|
|
|
|
def _getFontItalic(self): |
|
return self.Font.Italic |
|
|
|
def _setFontItalic(self, val): |
|
if self._constructed(): |
|
self.Font.Italic = val |
|
else: |
|
self._properties["FontItalic"] = val |
|
|
|
|
|
def _getFontFace(self): |
|
return self.Font.Face |
|
|
|
def _setFontFace(self, val): |
|
if self._constructed(): |
|
self.Font.Face = val |
|
else: |
|
self._properties["FontFace"] = val |
|
|
|
|
|
def _getFontSize(self): |
|
return self.Font.Size |
|
|
|
def _setFontSize(self, val): |
|
if self._constructed(): |
|
self.Font.Size = val |
|
else: |
|
self._properties["FontSize"] = val |
|
|
|
|
|
def _getFontUnderline(self): |
|
return self.Font.Underline |
|
|
|
def _setFontUnderline(self, val): |
|
if self._constructed(): |
|
self.Font.Underline = val |
|
else: |
|
self._properties["FontUnderline"] = val |
|
|
|
|
|
def _getForeColor(self): |
|
try: |
|
return self._gridColAttr.GetTextColour() |
|
except wx.PyAssertionError: |
|
# Getting the color failed on Mac and win: "no default attr" |
|
default = dColors.colorTupleFromName("black") |
|
self._gridColAttr.SetTextColour(default) |
|
return default |
|
|
|
def _setForeColor(self, val): |
|
if self._constructed(): |
|
if isinstance(val, basestring): |
|
val = dColors.colorTupleFromName(val) |
|
self._gridColAttr.SetTextColour(val) |
|
self._refreshGrid() |
|
else: |
|
self._properties["ForeColor"] = val |
|
|
|
|
|
def _getHeaderFont(self): |
|
if hasattr(self, "_headerFont"): |
|
v = self._headerFont |
|
else: |
|
v = self.HeaderFont = self._getDefaultFont() |
|
v.Bold = True |
|
return v |
|
|
|
def _setHeaderFont(self, val): |
|
assert isinstance(val, dabo.ui.dFont) |
|
if self._constructed(): |
|
self._headerFont = val |
|
val.bindEvent(dEvents.FontPropertiesChanged, self._onHeaderFontPropsChanged) |
|
else: |
|
self._properties["HeaderFont"] = val |
|
|
|
|
|
def _getHeaderFontBold(self): |
|
return self.HeaderFont.Bold |
|
|
|
def _setHeaderFontBold(self, val): |
|
if self._constructed(): |
|
self.HeaderFont.Bold = val |
|
else: |
|
self._properties["HeaderFontBold"] = val |
|
|
|
|
|
def _getHeaderFontDescription(self): |
|
return self.HeaderFont.Description |
|
|
|
|
|
def _getHeaderFontInfo(self): |
|
return self.HeaderFont._nativeFont.GetNativeFontInfoDesc() |
|
|
|
|
|
def _getHeaderFontItalic(self): |
|
return self.HeaderFont.Italic |
|
|
|
def _setHeaderFontItalic(self, val): |
|
if self._constructed(): |
|
self.HeaderFont.Italic = val |
|
else: |
|
self._properties["HeaderFontItalic"] = val |
|
|
|
|
|
def _getHeaderFontFace(self): |
|
return self.HeaderFont.Face |
|
|
|
def _setHeaderFontFace(self, val): |
|
if self._constructed(): |
|
self.HeaderFont.Face = val |
|
else: |
|
self._properties["HeaderFontFace"] = val |
|
|
|
|
|
def _getHeaderFontSize(self): |
|
return self.HeaderFont.Size |
|
|
|
def _setHeaderFontSize(self, val): |
|
if self._constructed(): |
|
self.HeaderFont.Size = val |
|
else: |
|
self._properties["HeaderFontSize"] = val |
|
|
|
|
|
def _getHeaderFontUnderline(self): |
|
return self.HeaderFont.Underline |
|
|
|
def _setHeaderFontUnderline(self, val): |
|
if self._constructed(): |
|
self.HeaderFont.Underline = val |
|
else: |
|
self._properties["HeaderFontUnderline"] = val |
|
|
|
|
|
def _getHeaderBackColor(self): |
|
try: |
|
v = self._headerBackColor |
|
except AttributeError: |
|
v = self._headerBackColor = None |
|
return v |
|
|
|
def _setHeaderBackColor(self, val): |
|
if self._constructed(): |
|
if isinstance(val, basestring): |
|
val = dColors.colorTupleFromName(val) |
|
self._headerBackColor = val |
|
self._refreshHeader() |
|
else: |
|
self._properties["HeaderBackColor"] = val |
|
|
|
|
|
def _getHeaderForeColor(self): |
|
try: |
|
v = self._headerForeColor |
|
except AttributeError: |
|
v = self._headerForeColor = None |
|
return v |
|
|
|
def _setHeaderForeColor(self, val): |
|
if self._constructed(): |
|
if isinstance(val, basestring): |
|
val = dColors.colorTupleFromName(val) |
|
self._headerForeColor = val |
|
self._refreshHeader() |
|
else: |
|
self._properties["HeaderForeColor"] = val |
|
|
|
|
|
def _getHeaderHorizontalAlignment(self): |
|
try: |
|
val = self._headerHorizontalAlignment |
|
except AttributeError: |
|
val = self._headerHorizontalAlignment = None |
|
return val |
|
|
|
def _setHeaderHorizontalAlignment(self, val): |
|
if self._constructed(): |
|
v = self._expandPropStringValue(val, ("Left", "Right", "Center", None)) |
|
self._headerHorizontalAlignment = v |
|
self._refreshHeader() |
|
else: |
|
self._properties["HeaderHorizontalAlignment"] = val |
|
|
|
|
|
def _getHeaderVerticalAlignment(self): |
|
try: |
|
val = self._headerVerticalAlignment |
|
except AttributeError: |
|
val = self._headerVerticalAlignment = None |
|
return val |
|
|
|
def _setHeaderVerticalAlignment(self, val): |
|
if self._constructed(): |
|
v = self._expandPropStringValue(val, ("Top", "Bottom", "Center", None)) |
|
self._headerVerticalAlignment = v |
|
self._refreshHeader() |
|
else: |
|
self._properties["HeaderVerticalAlignment"] = val |
|
|
|
|
|
def _getHorizontalAlignment(self): |
|
try: |
|
auto = self._autoHorizontalAlignment |
|
except AttributeError: |
|
auto = self._autoHorizontalAlignment = True |
|
mapping = {wx.ALIGN_LEFT: "Left", wx.ALIGN_RIGHT: "Right", |
|
wx.ALIGN_CENTRE: "Center"} |
|
wxAlignment = self._gridColAttr.GetAlignment()[0] |
|
try: |
|
val = mapping[wxAlignment] |
|
except KeyError: |
|
val = "Left" |
|
if auto: |
|
val = "%s (Automatic)" % val |
|
return val |
|
|
|
def _setAutoHorizontalAlignment(self): |
|
dt = self.DataType |
|
if isinstance(dt, basestring): |
|
if dt in ("decimal", "float", "long", "integer"): |
|
self._setHorizontalAlignment("Right", _autoAlign=True) |
|
|
|
def _setHorizontalAlignment(self, val, _autoAlign=False): |
|
if self._constructed(): |
|
val = self._expandPropStringValue(val, ("Automatic", "Left", "Right", "Center")) |
|
if val == "Automatic" and not _autoAlign: |
|
self._autoHorizontalAlignment = True |
|
self._setAutoHorizontalAlignment() |
|
return |
|
if val != "Automatic" and not _autoAlign: |
|
self._autoHorizontalAlignment = False |
|
mapping = {"Left": wx.ALIGN_LEFT, "Right": wx.ALIGN_RIGHT, |
|
"Center": wx.ALIGN_CENTRE} |
|
try: |
|
wxHorAlign = mapping[val] |
|
except KeyError: |
|
wxHorAlign = mapping["Left"] |
|
val = "Left" |
|
wxVertAlign = self._gridColAttr.GetAlignment()[1] |
|
self._gridColAttr.SetAlignment(wxHorAlign, wxVertAlign) |
|
self._refreshGrid() |
|
else: |
|
self._properties["HorizontalAlignment"] = val |
|
|
|
|
|
def _getListEditorChoices(self): |
|
try: |
|
v = self._listEditorChoices |
|
except AttributeError: |
|
v = [] |
|
return v |
|
|
|
def _setListEditorChoices(self, val): |
|
if self._constructed(): |
|
self._listEditorChoices = val |
|
else: |
|
self._properties["ListEditorChoices"] = val |
|
|
|
|
|
def _getMovable(self): |
|
return getattr(self, "_movable", True) |
|
|
|
def _setMovable(self, val): |
|
self._movable = bool(val) |
|
|
|
|
|
def _getOrder(self): |
|
try: |
|
v = self._order |
|
except AttributeError: |
|
v = self._order = -1 |
|
return v |
|
|
|
def _setOrder(self, val): |
|
if self._constructed(): |
|
self._order = val |
|
else: |
|
self._properties["Order"] = val |
|
|
|
|
|
def _getPrecision(self): |
|
return self._precision |
|
|
|
def _setPrecision(self, val): |
|
if self._constructed(): |
|
self._precision = val |
|
if self.Parent: |
|
dabo.ui.callAfterInterval(50, self.Parent.refresh) |
|
else: |
|
self._properties["Precision"] = val |
|
|
|
|
|
def _getRendererClass(self): |
|
return self._rendererClass |
|
|
|
|
|
def _getResizable(self): |
|
return getattr(self, "_resizable", True) |
|
|
|
def _setResizable(self, val): |
|
self._resizable = bool(val) |
|
|
|
|
|
def _getSearchable(self): |
|
try: |
|
v = self._searchable |
|
except AttributeError: |
|
v = self._searchable = True |
|
return v |
|
|
|
def _setSearchable(self, val): |
|
if self._constructed(): |
|
self._searchable = bool(val) |
|
else: |
|
self._properties["Searchable"] = val |
|
|
|
|
|
def _getSortable(self): |
|
try: |
|
v = self._sortable |
|
except AttributeError: |
|
v = self._sortable = True |
|
return v |
|
|
|
def _setSortable(self, val): |
|
if self._constructed(): |
|
self._sortable = bool(val) |
|
else: |
|
self._properties["Sortable"] = val |
|
|
|
|
|
def _getValue(self): |
|
grid = self.Parent |
|
if grid is None: |
|
return None |
|
biz = grid.getBizobj() |
|
if self.DataField: |
|
if biz and (grid.CurrentRow < biz.RowCount): |
|
return biz.getFieldVal(self.DataField) |
|
if grid.DataSet: |
|
return grid.DataSet[grid.CurrentRow][self.DataField] |
|
return None |
|
|
|
|
|
def _getVerticalAlignment(self): |
|
mapping = {wx.ALIGN_TOP: "Top", wx.ALIGN_BOTTOM: "Bottom", |
|
wx.ALIGN_CENTRE: "Center"} |
|
wxAlignment = self._gridColAttr.GetAlignment()[1] |
|
try: |
|
val = mapping[wxAlignment] |
|
except KeyError: |
|
val = "Top" |
|
return val |
|
|
|
def _setVerticalAlignment(self, val): |
|
if self._constructed(): |
|
val = self._expandPropStringValue(val, ("Top", "Bottom", "Center")) |
|
mapping = {"Top": wx.ALIGN_TOP, "Bottom": wx.ALIGN_BOTTOM, |
|
"Center": wx.ALIGN_CENTRE} |
|
try: |
|
wxVertAlign = mapping[val] |
|
except KeyError: |
|
wxVertAlign = mapping["Top"] |
|
val = "Top" |
|
wxHorAlign = self._gridColAttr.GetAlignment()[0] |
|
self._gridColAttr.SetAlignment(wxHorAlign, wxVertAlign) |
|
self._refreshGrid() |
|
else: |
|
self._properties["VerticalAlignment"] = val |
|
|
|
|
|
def _getVisible(self): |
|
return self._visible |
|
|
|
def _setVisible(self, val): |
|
if self._constructed(): |
|
self._visible = val |
|
self.Parent.showColumn(self, val) |
|
else: |
|
self._properties["Visible"] = val |
|
|
|
|
|
def _getWidth(self): |
|
try: |
|
v = self._width |
|
except AttributeError: |
|
v = self._width = 150 |
|
if self.Parent: |
|
idx = self.Parent._convertDaboColNumToWxColNum(self.ColumnIndex) |
|
if idx is not None: |
|
# Make sure the grid is in sync: |
|
try: |
|
self.Parent.SetColSize(idx, v) |
|
except wx.PyAssertionError: |
|
# The grid may still be in the process of being created, so pass. |
|
pass |
|
return v |
|
|
|
def _setWidth(self, val): |
|
if self._constructed(): |
|
try: |
|
if val == self._width: |
|
return |
|
except AttributeError: |
|
pass |
|
self._width = val |
|
grd = self.Parent |
|
if grd: |
|
grd._syncColumnCount() |
|
idx = grd._convertDaboColNumToWxColNum(self.ColumnIndex) |
|
if idx is not None: |
|
# Change the size in the wx grid: |
|
grd.SetColSize(idx, val) |
|
else: |
|
self._properties["Width"] = val |
|
|
|
|
|
def _getWordWrap(self): |
|
return self._wordWrap |
|
|
|
def _setWordWrap(self, val): |
|
if self._constructed(): |
|
if val != self._wordWrap: |
|
self._wordWrap = val |
|
if val: |
|
for typ in (unicode, "str", "string"): |
|
self.defaultRenderers[typ] = self.wrapStringRendererClass |
|
self.defaultEditors[typ] = self.wrapStringEditorClass |
|
else: |
|
for typ in (unicode, "str", "string"): |
|
self.defaultRenderers[typ] = self.stringRendererClass |
|
self.defaultEditors[typ] = self.stringEditorClass |
|
self._updateEditor() |
|
self._updateRenderer() |
|
self._refreshGrid() |
|
else: |
|
self._properties["WordWrap"] = val |
|
|
|
|
|
BackColor = property(_getBackColor, _setBackColor, None, |
|
_("Color for the background of each cell in the column.")) |
|
|
|
Caption = property(_getCaption, _setCaption, None, |
|
_("Specifies the caption displayed in this column's header.") ) |
|
|
|
ColumnIndex = property(_getColumnIndex, None, |
|
_("Returns the index of this column in the parent grid.")) |
|
|
|
CellBackColor = property(_getCellBackColor, _setCellBackColor, None, |
|
_("Color for the background of the current cell in the column.")) |
|
|
|
CellFontBold = property(_getCellFontBold, _setCellFontBold, None, |
|
_("Specifies whether the current cell's font is bold-faced.")) |
|
|
|
CellForeColor = property(_getCellForeColor, _setCellForeColor, None, |
|
_("Color for the foreground (text) of the current cell in the column.")) |
|
|
|
CustomEditorClass = property(_getCustomEditorClass, |
|
_setCustomEditorClass, None, |
|
_("""Custom Editor class for this column. Default: None. |
|
|
|
Set this to override the default editor class, which Dabo will |
|
select based on the data type of the field.""")) |
|
|
|
CustomEditors = property(_getCustomEditors, _setCustomEditors, None, |
|
_("""Dictionary of custom editors for this column. Default: {}. |
|
|
|
Set this to override the default editor class on a row-by-row basis. |
|
If there is no custom editor class for a given row in CustomEditors, |
|
the CustomEditor property setting will apply.""")) |
|
|
|
CustomListEditorChoices = property(_getCustomListEditorChoices, |
|
_setCustomListEditorChoices, None, |
|
_("""Dictionary of custom list choices for this column. Default: {}. |
|
|
|
Set this to override the default list choices on a row-by-row basis. |
|
If there is no custom entry for a given row in CustomListEditorChoices, |
|
the ListEditorChoices property setting will apply.""")) |
|
|
|
CustomRendererClass = property(_getCustomRendererClass, |
|
_setCustomRendererClass, None, |
|
_("""Custom Renderer class for this column. Default: None. |
|
|
|
Set this to override the default renderer class, which Dabo will select based |
|
on the data type of the field.""")) |
|
|
|
CustomRenderers = property(_getCustomRenderers, _setCustomRenderers, None, |
|
_("""Dictionary of custom renderers for this column. Default: {}. |
|
|
|
Set this to override the default renderer class on a row-by-row basis. |
|
If there is no custom renderer for a given row in CustomRenderers, the |
|
CustomRendererClass property setting will apply.""")) |
|
|
|
DataType = property(_getDataType, _setDataType, None, |
|
_("Description of the data type for this column (str)") ) |
|
|
|
Editable = property(_getEditable, _setEditable, None, |
|
_("""If True, and if the grid is set as Editable, the cell values in this |
|
column are editable by the user. If False, the cells in this column |
|
cannot be edited no matter what the grid setting is. When editable, |
|
incremental searching will not be enabled, regardless of the |
|
Searchable property setting. (bool)""") ) |
|
|
|
EditorClass = property(_getEditorClass, None, None, |
|
_("""Returns the editor class used for cells in the column. This |
|
will be self.CustomEditorClass if set, or the default editor for the |
|
datatype of the field. (varies)""")) |
|
|
|
Expand = property(_getExpand, _setExpand, None, |
|
_("""Does this column expand/shrink as the grid width changes? |
|
Default=False (bool)""")) |
|
|
|
DataField = property(_getDataField, _setDataField, None, |
|
_("Field key in the data set to which this column is bound. (str)") ) |
|
|
|
Font = property(_getFont, _setFont, None, |
|
_("The font properties of the column's cells. (dFont)") ) |
|
|
|
FontBold = property(_getFontBold, _setFontBold, None, |
|
_("Specifies if the cell font (for all cells in the column) is bold-faced. (bool)") ) |
|
|
|
FontDescription = property(_getFontDescription, None, None, |
|
_("Human-readable description of the column's cell font settings. (str)") ) |
|
|
|
FontFace = property(_getFontFace, _setFontFace, None, |
|
_("Specifies the font face for the column cells. (str)") ) |
|
|
|
FontInfo = property(_getFontInfo, None, None, |
|
_("Specifies the platform-native font info string for the column cells. Read-only. (str)") ) |
|
|
|
FontItalic = property(_getFontItalic, _setFontItalic, None, |
|
_("Specifies whether the column's cell font is italicized. (bool)") ) |
|
|
|
FontSize = property(_getFontSize, _setFontSize, None, |
|
_("Specifies the point size of the column's cell font. (int)") ) |
|
|
|
FontUnderline = property(_getFontUnderline, _setFontUnderline, None, |
|
_("Specifies whether cell text is underlined. (bool)") ) |
|
|
|
ForeColor = property(_getForeColor, _setForeColor, None, |
|
_("Color for the foreground (text) of each cell in the column.")) |
|
|
|
HeaderBackColor = property(_getHeaderBackColor, _setHeaderBackColor, None, |
|
_("Optional color for the background of the column header (str)") ) |
|
|
|
HeaderFont = property(_getHeaderFont, _setHeaderFont, None, |
|
_("The font properties of the column's header. (dFont)") ) |
|
|
|
HeaderFontBold = property(_getHeaderFontBold, _setHeaderFontBold, None, |
|
_("Specifies if the header font is bold-faced. (bool)") ) |
|
|
|
HeaderFontDescription = property(_getHeaderFontDescription, None, None, |
|
_("Human-readable description of the current header font settings. (str)") ) |
|
|
|
HeaderFontFace = property(_getHeaderFontFace, _setHeaderFontFace, None, |
|
_("Specifies the font face for the column header. (str)") ) |
|
|
|
HeaderFontInfo = property(_getHeaderFontInfo, None, None, |
|
_("Specifies the platform-native font info string for the column header. Read-only. (str)") ) |
|
|
|
HeaderFontItalic = property(_getHeaderFontItalic, _setHeaderFontItalic, None, |
|
_("Specifies whether the header font is italicized. (bool)") ) |
|
|
|
HeaderFontSize = property(_getHeaderFontSize, _setHeaderFontSize, None, |
|
_("Specifies the point size of the header font. (int)") ) |
|
|
|
HeaderFontUnderline = property(_getHeaderFontUnderline, _setHeaderFontUnderline, None, |
|
_("Specifies whether column header text is underlined. (bool)") ) |
|
|
|
HeaderForeColor = property(_getHeaderForeColor, _setHeaderForeColor, None, |
|
_("Optional color for the foreground (text) of the column header (str)") ) |
|
|
|
HeaderHorizontalAlignment = property(_getHeaderHorizontalAlignment, _setHeaderHorizontalAlignment, None, |
|
_("Specifies the horizontal alignment of the header caption. ('Left', 'Center', 'Right')")) |
|
|
|
HeaderVerticalAlignment = property(_getHeaderVerticalAlignment, _setHeaderVerticalAlignment, None, |
|
_("Specifies the vertical alignment of the header caption. ('Top', 'Center', 'Bottom')")) |
|
|
|
HorizontalAlignment = property(_getHorizontalAlignment, _setHorizontalAlignment, None, |
|
_("""Horizontal alignment for all cells in this column. (str) |
|
Acceptable values are: |
|
'Automatic': The cell's contents will align right for numeric data, left for text. (default) |
|
'Left' |
|
'Center' |
|
'Right' """)) |
|
|
|
ListEditorChoices = property(_getListEditorChoices, _setListEditorChoices, None, |
|
_("""Specifies the list of choices that will appear in the list. Only applies |
|
if the DataType is set as "list". (list)""")) |
|
|
|
Movable = property(_getMovable, _setMovable, None, |
|
_("""Specifies whether this column is movable by the user. |
|
|
|
Note also the dGrid.MovableColumns property - if that is set |
|
to False, columns will not be movable even if their Movable |
|
property is set to True.""")) |
|
|
|
Order = property(_getOrder, _setOrder, None, |
|
_("""Order of this column. Columns in the grid are arranged according |
|
to their relative Order. (int)""") ) |
|
|
|
Precision = property(_getPrecision, _setPrecision, None, |
|
_("Number of decimal places to display for float and decimal values (int)")) |
|
|
|
RendererClass = property(_getRendererClass, None, None, |
|
_("""Returns the renderer class used for cells in the column. This will be |
|
self.CustomRendererClass if set, or the default renderer class for the |
|
datatype of the field. (varies)""")) |
|
|
|
Resizable = property(_getResizable, _setResizable, None, |
|
_("""Specifies whether this column is resizable by the user. |
|
|
|
Note also the dGrid.ResizableColumns property - if that is set |
|
to False, columns will not be resizable even if their Resizable |
|
property is set to True.""")) |
|
|
|
Searchable = property(_getSearchable, _setSearchable, None, |
|
_("""Specifies whether this column's incremental search is enabled. |
|
Default: True. The grid's Searchable property will override this setting. |
|
(bool)""")) |
|
|
|
Sortable = property(_getSortable, _setSortable, None, |
|
_("""Specifies whether this column can be sorted. Default: True. The grid's |
|
Sortable property will override this setting. (bool)""")) |
|
|
|
Value = property(_getValue, None, None, |
|
_("""Returns the current value of the column from the underlying dataset or bizobj.""")) |
|
|
|
VerticalAlignment = property(_getVerticalAlignment, _setVerticalAlignment, None, |
|
_("""Vertical alignment for all cells in this column. Acceptable values |
|
are 'Top', 'Center', and 'Bottom'. (str)""")) |
|
|
|
Visible = property(_getVisible, _setVisible, None, |
|
_("Controls whether the column is shown or not (bool)")) |
|
|
|
Width = property(_getWidth, _setWidth, None, |
|
_("Width of this column (int)") ) |
|
|
|
WordWrap = property(_getWordWrap, _setWordWrap, None, |
|
_("When True, text longer than the column width will wrap to the next line (bool)")) |
|
|
|
|
|
|
|
|
|
# Dynamic Property Declarations |
|
DynamicBackColor = makeDynamicProperty(BackColor) |
|
DynamicCaption = makeDynamicProperty(Caption) |
|
DynamicCellBackColor = makeDynamicProperty(CellBackColor) |
|
DynamicCellFontBold = makeDynamicProperty(CellFontBold) |
|
DynamicCellForeColor = makeDynamicProperty(CellForeColor) |
|
DynamicCustomEditorClass = makeDynamicProperty(CustomEditorClass) |
|
DynamicCustomEditors = makeDynamicProperty(CustomEditors) |
|
DynamicCustomListEditorChoices = makeDynamicProperty(CustomListEditorChoices) |
|
DynamicCustomRendererClass = makeDynamicProperty(CustomRendererClass) |
|
DynamicCustomRenderers = makeDynamicProperty(CustomRenderers) |
|
DynamicDataField = makeDynamicProperty(DataField) |
|
DynamicDataType = makeDynamicProperty(DataType) |
|
DynamicEditable = makeDynamicProperty(Editable) |
|
DynamicFont = makeDynamicProperty(Font) |
|
DynamicFontBold = makeDynamicProperty(FontBold) |
|
DynamicFontFace = makeDynamicProperty(FontFace) |
|
DynamicFontItalic = makeDynamicProperty(FontItalic) |
|
DynamicFontSize = makeDynamicProperty(FontSize) |
|
DynamicFontUnderline = makeDynamicProperty(FontUnderline) |
|
DynamicForeColor = makeDynamicProperty(ForeColor) |
|
DynamicHeaderBackColor = makeDynamicProperty(HeaderBackColor) |
|
DynamicHeaderFont = makeDynamicProperty(HeaderFont) |
|
DynamicHeaderFontBold = makeDynamicProperty(HeaderFontBold) |
|
DynamicHeaderFontFace = makeDynamicProperty(HeaderFontFace) |
|
DynamicHeaderFontItalic = makeDynamicProperty(HeaderFontItalic) |
|
DynamicHeaderFontSize = makeDynamicProperty(HeaderFontSize) |
|
DynamicHeaderFontUnderline = makeDynamicProperty(HeaderFontUnderline) |
|
DynamicHeaderForeColor = makeDynamicProperty(HeaderForeColor) |
|
DynamicHeaderHorizontalAlignment = makeDynamicProperty(HeaderHorizontalAlignment) |
|
DynamicHeaderVerticalAlignment = makeDynamicProperty(HeaderVerticalAlignment) |
|
DynamicHorizontalAlignment = makeDynamicProperty(HorizontalAlignment) |
|
DynamicListEditorChoices = makeDynamicProperty(ListEditorChoices) |
|
DynamicOrder = makeDynamicProperty(Order) |
|
DynamicSearchable = makeDynamicProperty(Searchable) |
|
DynamicSortable = makeDynamicProperty(Sortable) |
|
DynamicVerticalAlignment = makeDynamicProperty(VerticalAlignment) |
|
DynamicVisible = makeDynamicProperty(Visible) |
|
DynamicWidth = makeDynamicProperty(Width) |
|
|
|
|
|
|
|
class dGrid(cm.dControlMixin, wx.grid.Grid): |
|
""" |
|
Creates a grid, with rows and columns to represent records and fields. |
|
|
|
Grids are powerful controls for allowing reading and writing of data. A |
|
grid can have any number of dColumns, which themselves have lots of properties |
|
to manipulate. The grid is virtual, meaning that large amounts of data can |
|
be accessed efficiently: only the data that needs to be shown on the current |
|
screen is copied and displayed. |
|
""" |
|
|
|
USE_DATASOURCE_BEING_SET_HACK = False |
|
|
|
def __init__(self, parent, properties=None, attProperties=None, *args, **kwargs): |
|
# Update global decimalPoint attribute. |
|
global decimalPoint |
|
if decimalPoint is None: |
|
decimalPoint = locale.localeconv()["decimal_point"] |
|
# Get scrollbar size from system metrics. |
|
self._scrollBarSize = wx.SystemSettings_GetMetric(wx.SYS_VSCROLL_X) |
|
self._baseClass = dGrid |
|
preClass = wx.grid.Grid |
|
|
|
# Internal flag indicates update invoked by grid itself. |
|
self._inUpdate = False |
|
# Internal flag indicates header repaint invoked by the header itself. |
|
self._inHeaderPaint = False |
|
# Internal flag to determine if the prior sort order needs to be restored. |
|
self._sortRestored = False |
|
# Internal flag to determine if the resorting is the result of the DataSet property. |
|
self._settingDataSetFromSort = False |
|
# Internal flag to determine if refresh should be called after sorting. |
|
self._refreshAfterSort = True |
|
# Local count of rows in the data table |
|
self._tableRows = 0 |
|
# List of visible columns |
|
self._daboVisibleColumns = [] |
|
|
|
# When user selects new row, does the form have responsibility for making the change? |
|
self._mediateRowNumberThroughForm = True |
|
|
|
# Used to provide 'data' when the DataSet is empty. |
|
self.emptyRowsToAdd = 0 |
|
|
|
# dColumn maintains its own cell attribute object, but this is the default: |
|
self._defaultGridColAttr = self._getDefaultGridColAttr() |
|
|
|
# Some applications (I'm thinking the UI Designer here) need to be able |
|
# to set Editing = True, but still disallow editing. This attribute does that. |
|
self._vetoAllEditing = False |
|
|
|
# Can the user move the columns around? |
|
self._movableColumns = True |
|
# Can the user re-size the columns or rows? |
|
self._resizableColumns = True |
|
self._resizableRows = True |
|
|
|
# Flag to indicate we are auto-sizing all columns |
|
self._inAutoSizeLoop = False |
|
# Flag to indicate we are in a range selection event |
|
self._inRangeSelect = False |
|
# Flag to indicate we are in a selection update event |
|
self._inUpdateSelection = False |
|
# Flag to avoid record pointer movement during DataSource setting. Only |
|
# applies if dGrid.USE_DATASOURCE_BEING_SET_HACK is True (default False) |
|
self._dataSourceBeingSet = False |
|
|
|
# Do we show row or column labels? |
|
self._showHeaders = True |
|
self._showRowLabels = False |
|
|
|
# Declare Internal Row Attributes |
|
self._rowLabels = [] |
|
self._sameSizeRows = True |
|
|
|
# Declare Internal Column Attributes |
|
self._columnClass = dColumn |
|
self._columns = [] |
|
|
|
#Declare Internal Search And Sort Attributes |
|
self._searchable = True |
|
self._searchDelay = None |
|
self._sortable = True |
|
|
|
#Declare Internal Header Attributes |
|
self._headerVerticalAlignment = "Center" |
|
self._headerHorizontalAlignment = "Center" |
|
self._headerForeColor = None |
|
self._headerBackColor = (232, 232, 232) |
|
self._verticalHeaders = False |
|
self._autoAdjustHeaderHeight = False |
|
self._headerMaxTextHeight = 0 |
|
self._columnMetrics = [(0, 0)] |
|
# What color/size should the little sort indicator arrow be? |
|
self._sortIndicatorColor = "yellow" |
|
self._sortIndicatorSize = 8 |
|
|
|
#Set NoneDisplay attributes |
|
if self.Application: |
|
self.__noneDisplayDefault = self.Application.NoneDisplay |
|
else: |
|
self.__noneDisplayDefault = _("< None >") |
|
self._noneDisplay = self.__noneDisplayDefault |
|
|
|
# These hold the values that affect row/col hiliting |
|
self._selectionForeColor = "black" |
|
self._selectionBackColor = "yellow" |
|
self._selectionMode = "Cell" |
|
self._modeSet = False |
|
self._multipleSelection = True |
|
# Track the last row and col selected |
|
self._lastRow = self._lastCol = None |
|
self._alternateRowColoring = False |
|
self._rowColorEven = "white" |
|
self._rowColorOdd = (212, 255, 212) # very light green |
|
|
|
cm.dControlMixin.__init__(self, preClass, parent, properties=properties, |
|
attProperties=attProperties, *args, **kwargs) |
|
|
|
# Reduces grid flickering on Windows platform. |
|
self._enableDoubleBuffering() |
|
# Need to sync the size reported by wx to the size reported by Dabo: |
|
self.RowHeight = self.RowHeight |
|
self.ShowRowLabels = self.ShowRowLabels |
|
|
|
# Set reasonable minimum size, as the default of (-1,-1) results in something |
|
# in wx calculating the effective minsize based on how much space we need to |
|
# show all the rows: |
|
self.SetMinSize((100, 100)) |
|
|
|
|
|
def _afterInit(self): |
|
# When doing an incremental search, do we stop |
|
# at the nearest matching value? |
|
self.searchNearest = True |
|
# Do we do case-sensitive incremental searches? |
|
self.searchCaseSensitive = False |
|
# How many characters of strings do we display? |
|
self.stringDisplayLen = 64 |
|
|
|
self.currSearchStr = "" |
|
self.incSearchTimer = dabo.ui.dTimer(self) |
|
self.incSearchTimer.bindEvent(dEvents.Hit, self.onIncSearchTimer) |
|
|
|
# By default, row labels are not shown. They can be displayed |
|
# if desired by setting ShowRowLabels = True, and their size |
|
# can be adjusted by setting RowLabelWidth = <width> |
|
self.SetRowLabelSize(self.RowLabelWidth) |
|
self.EnableEditing(self.Editable and not self._vetoAllEditing) |
|
|
|
# These need to be set to True, and custom methods provided, |
|
# if a grid with variable types in a single column is used. |
|
self.useCustomGetValue = False |
|
self.useCustomSetValue = False |
|
|
|
# flags used by mouse motion event handlers: |
|
self._headerDragging = False |
|
self._headerDragFrom = 0 |
|
self._headerDragTo = 0 |
|
self._headerSizing = False |
|
|
|
self.sortedColumn = None |
|
self.sortOrder = None |
|
self.caseSensitiveSorting = False |
|
|
|
# If there is a custom sort method, set this to True |
|
self.customSort = False |
|
|
|
super(dGrid, self)._afterInit() |
|
|
|
# Set the header props/events |
|
self.initHeader() |
|
# Make sure that the columns are sized properly |
|
dabo.ui.callAfter(self._updateColumnWidths) |
|
|
|
|
|
@dabo.ui.deadCheck |
|
def _afterInitAll(self): |
|
super(dGrid, self)._afterInitAll() |
|
for col in self.Columns: |
|
col._setRenderer() |
|
|
|
|
|
def _initEvents(self): |
|
## pkm: Don't do the grid_cell mouse events, because we handle it manually and it |
|
## would result in doubling up the events. |
|
#self.Bind(wx.grid.EVT_GRID_CELL_LEFT_DCLICK, self.__onWxGridCellMouseLeftDoubleClick) |
|
#self.Bind(wx.grid.EVT_GRID_CELL_LEFT_CLICK, self.__onWxGridCellMouseLeftClick) |
|
#self.Bind(wx.grid.EVT_GRID_CELL_RIGHT_CLICK, self.__onWxGridCellMouseRightClick) |
|
self.Bind(wx.grid.EVT_GRID_ROW_SIZE, self.__onWxGridRowSize) |
|
self.Bind(wx.grid.EVT_GRID_SELECT_CELL, self.__onWxGridSelectCell) |
|
self.Bind(wx.grid.EVT_GRID_COL_SIZE, self.__onWxGridColSize) |
|
self.Bind(wx.grid.EVT_GRID_EDITOR_CREATED, self.__onWxGridEditorCreated) |
|
self.Bind(wx.grid.EVT_GRID_EDITOR_SHOWN, self.__onWxGridEditorShown) |
|
self.Bind(wx.grid.EVT_GRID_EDITOR_HIDDEN, self.__onWxGridEditorHidden) |
|
self.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.__onWxGridCellChange) |
|
self.Bind(wx.grid.EVT_GRID_RANGE_SELECT, self.__onWxGridRangeSelect) |
|
self.Bind(wx.EVT_SCROLLWIN, self.__onWxScrollWin) |
|
|
|
# Testing bool cell renderer/editor single-click-toggle: |
|
self.Bind(wx.grid.EVT_GRID_CELL_LEFT_CLICK, self.__onGridCellLeftClick_toggleCB) |
|
|
|
gridWindow = self.GetGridWindow() |
|
|
|
gridWindow.Bind(wx.EVT_MOTION, self.__onWxMouseMotion) |
|
gridWindow.Bind(wx.EVT_LEFT_DCLICK, self.__onWxMouseLeftDoubleClick) |
|
gridWindow.Bind(wx.EVT_LEFT_DOWN, self.__onWxMouseLeftDown) |
|
gridWindow.Bind(wx.EVT_LEFT_UP, self.__onWxMouseLeftUp) |
|
gridWindow.Bind(wx.EVT_RIGHT_DOWN, self.__onWxMouseRightDown) |
|
gridWindow.Bind(wx.EVT_RIGHT_UP, self.__onWxMouseRightUp) |
|
gridWindow.Bind(wx.EVT_CONTEXT_MENU, self.__onWxContextMenu) |
|
|
|
self.bindEvent(dEvents.KeyDown, self._onKeyDown) |
|
self.bindEvent(dEvents.KeyChar, self._onKeyChar) |
|
self.bindEvent(dEvents.GridRowSize, self._onGridRowSize) |
|
self.bindEvent(dEvents.GridCellSelected, self._onGridCellSelected) |
|
self.bindEvent(dEvents.GridColSize, self._onGridColSize) |
|
self.bindEvent(dEvents.GridCellEdited, self._onGridCellEdited) |
|
self.bindEvent(dEvents.GridMouseLeftClick, self._onGridMouseLeftClick) |
|
self.bindEvent(dEvents.MouseWheel, self._onGridMouseWheel) |
|
|
|
## wx.EVT_CONTEXT_MENU doesn't appear to be working for dGrid yet: |
|
# self.bindEvent(dEvents.GridContextMenu, self._onContextMenu) |
|
self.bindEvent(dEvents.GridMouseRightClick, self._onGridMouseRightClick) |
|
self.bindEvent(dEvents.Resize, self._onGridResize) |
|
|
|
self.bindEvent(dEvents.Create, self._onCreate) |
|
self.bindEvent(dEvents.Destroy, self._onDestroy) |
|
|
|
super(dGrid, self)._initEvents() |
|
|
|
|
|
def initHeader(self): |
|
"""Initialize behavior for the grid header region.""" |
|
header = self._getWxHeader() |
|
self.defaultHdrCursor = header.GetCursor() |
|
self._headerNeedsRedraw = False |
|
self._lastHeaderMousePosition = None |
|
self._headerMouseLeftDown, self._headerMouseRightDown = False, False |
|
|
|
header.Bind(wx.EVT_LEFT_DCLICK, self.__onWxHeaderMouseLeftDoubleClick) |
|
header.Bind(wx.EVT_LEFT_DOWN, self.__onWxHeaderMouseLeftDown) |
|
header.Bind(wx.EVT_LEFT_UP, self.__onWxHeaderMouseLeftUp) |
|
header.Bind(wx.EVT_RIGHT_DOWN, self.__onWxHeaderMouseRightDown) |
|
header.Bind(wx.EVT_RIGHT_UP, self.__onWxHeaderMouseRightUp) |
|
header.Bind(wx.EVT_MOTION, self.__onWxHeaderMouseMotion) |
|
header.Bind(wx.EVT_PAINT, self.__onWxHeaderPaint) |
|
header.Bind(wx.EVT_CONTEXT_MENU, self.__onWxHeaderContextMenu) |
|
header.Bind(wx.EVT_ENTER_WINDOW, self.__onWxHeaderMouseEnter) |
|
header.Bind(wx.EVT_LEAVE_WINDOW, self.__onWxHeaderMouseLeave) |
|
header.Bind(wx.EVT_IDLE, self.__onWxHeaderIdle) |
|
|
|
self.bindEvent(dEvents.GridHeaderMouseLeftDown, self._onGridHeaderMouseLeftDown) |
|
self.bindEvent(dEvents.GridHeaderMouseMove, self._onGridHeaderMouseMove) |
|
self.bindEvent(dEvents.GridHeaderMouseLeftUp, self._onGridHeaderMouseLeftUp) |
|
self.bindEvent(dEvents.GridHeaderMouseRightUp, self._onGridHeaderMouseRightUp) |
|
self.bindEvent(dEvents.GridHeaderMouseRightClick, self._onGridHeaderMouseRightClick) |
|
|
|
|
|
def update(self): |
|
""" |
|
Call this when your datasource or dataset has changed to get the grid showing |
|
the proper number of rows with current data. |
|
""" |
|
# We never call the superclass update, because we don't need/want that behavior. |
|
last = getattr(self, "_lastCellSelectedTime", 0) |
|
cur = time.time() |
|
if cur - last < .5: |
|
return |
|
self._syncRowCount() |
|
self._syncCurrentRow() |
|
self.refresh() ## to clear the cache and repaint the cells |
|
|
|
|
|
def _syncAll(self): |
|
self._syncRowCount() |
|
self._syncColumnCount() |
|
self._syncCurrentRow() |
|
|
|
|
|
def refresh(self): |
|
"""Repaint the grid.""" |
|
if getattr(self, "__inRefresh", False): |
|
return |
|
self.__inRefresh = True |
|
self._Table._clearCache() ## Make sure the proper values are filled into the cells |
|
|
|
# Force invisible column dynamic properties to update (possible to make Visible again): |
|
invisible_cols = [c._updateDynamicProps() for c in self.Columns if not c.Visible] |
|
|
|
super(dGrid, self).refresh() |
|
self.__inRefresh = False |
|
|
|
|
|
def _refreshHeader(self): |
|
self._getWxHeader().Refresh() |
|
|
|
|
|
def GetCellValue(self, row, col, useCache=True): |
|
try: |
|
ret = self._Table.GetValue(row, col, useCache=useCache) |
|
except AttributeError: |
|
ret = super(dGrid, self).GetCellValue(row, col) |
|
return ret |
|
|
|
|
|
def GetValue(self, row, col, dynamicUpdate=True): |
|
try: |
|
ret = self._Table.GetValue(row, col, dynamicUpdate=dynamicUpdate) |
|
except (AttributeError, TypeError): |
|
ret = super(dGrid, self).GetValue(row, col) |
|
return ret |
|
|
|
|
|
def SetValue(self, row, col, val): |
|
try: |
|
self._Table.SetValue(row, col, val) |
|
except StandardError, e: |
|
super(dGrid, self).SetCellValue(row, col, val) |
|
# Update the main data source |
|
self._setCellValue(row, col, val) |
|
|
|
|
|
def _setCellValue(self, row, col, val): |
|
try: |
|
column = self.Columns[col] |
|
fld = column.DataField |
|
biz = self.getBizobj() |
|
if isinstance(val, float) and column.DataType == "decimal": |
|
val = Decimal(ustr(val)) |
|
if biz: |
|
biz.RowNumber = row |
|
biz.setFieldVal(fld, val) |
|
else: |
|
self.DataSet[row][fld] = val |
|
except StandardError, e: |
|
dabo.log.error("Cannot update data set: %s" % e) |
|
|
|
|
|
# Wrapper methods to Dabo-ize these calls. |
|
def getValue(self, row=None, col=None): |
|
""" |
|
Returns the value of the specified row and column. |
|
|
|
If no row/col is specified, the current row/col will be used. |
|
""" |
|
if row is None: |
|
row = self.CurrentRow |
|
if col is None: |
|
col = self.CurrentColumn |
|
ret = self.GetValue(row, col, dynamicUpdate=False) |
|
if isinstance(ret, str): |
|
ret = ret.decode(self.Encoding) |
|
return ret |
|
|
|
def setValue(self, row, col, val): |
|
return self.SetValue(row, col, val) |
|
|
|
# These two methods need to be customized if a grid has columns |
|
# with more than one type of data in them. |
|
def customCanGetValueAs(self, row, col, typ): pass |
|
def customCanSetValueAs(self, row, col, typ): pass |
|
|
|
|
|
# Wrap the native wx methods |
|
def setEditorForCell(self, row, col, edt): |
|
## dColumn maintains a dict of overriding editor mappings, but keep this |
|
## function for convenience. |
|
dcol = self.Columns[col] |
|
dcol.CustomEditors[row] = edt |
|
#self.SetCellEditor(row, col, edt) |
|
|
|
|
|
def setRendererForCell(self, row, col, rnd): |
|
## dColumn maintains a dict of overriding renderer mappings, but keep this |
|
## function for convenience. |
|
dcol = self.Columns[col] |
|
dcol.CustomRenderers[row] = rnd |
|
#self.SetCellRenderer(row, col, rnd) |
|
|
|
|
|
def typeFromDataField(self, df, col=None): |
|
""" |
|
When the DataField is set for a column, it needs to set the corresponding |
|
value of its DataType property. Will return the Python data type, or None if |
|
there is no bizobj, or no DataStructure info available in the bizobj. |
|
""" |
|
biz = self.getBizobj() |
|
if biz is None: |
|
if col is not None: |
|
return col.getDataTypeForColumn() |
|
else: |
|
return None |
|
try: |
|
pyType = biz.getDataTypeForField(df) |
|
except ValueError, e: |
|
dabo.log.error(e) |
|
return None |
|
return pyType |
|
|
|
|
|
def precisionFromDataField(self, df): |
|
""" |
|
Return the decimal precision for the passed data field, or the default |
|
precision if this isn't a decimal field or it isn't specified in the |
|
bizobj. |
|
""" |
|
default = 2 |
|
biz = self.getBizobj() |
|
if biz is not None: |
|
ret = biz.getPrecisionForField(df) |
|
if ret is not None: |
|
return ret |
|
return default |
|
|
|
|
|
def getTableClass(cls): |
|
""" |
|
We don't expose the underlying table class to the ui namespace, as it's a |
|
wx-specific implementation detail, but for cases where you need to subclass |
|
the table, this classmethod will return the class reference. |
|
""" |
|
return dGridDataTable |
|
getTableClass = classmethod(getTableClass) |
|
|
|
|
|
def setTableAttributes(self, tbl=None): |
|
"""Set the attributes for table display""" |
|
if tbl is None: |
|
try: |
|
tbl = self._Table |
|
except TypeError: |
|
tbl = None |
|
if tbl is None: |
|
# Still not fully constructed |
|
dabo.ui.callAfter(self.setTableAttributes) |
|
return |
|
tbl.alternateRowColoring = self.AlternateRowColoring |
|
tbl.rowColorOdd = self._getWxColour(self.RowColorOdd) |
|
tbl.rowColorEven = self._getWxColour(self.RowColorEven) |
|
|
|
|
|
def afterCellEdit(self, row, col): |
|
"""Called after a cell has been edited by the user.""" |
|
pass |
|
|
|
|
|
def fillGrid(self, force=False): |
|
"""Refresh the grid to match the data in the data set.""" |
|
# Get the default row size from dApp's user settings |
|
rowSize = self._getUserSetting("RowSize") |
|
if rowSize: |
|
self.SetDefaultRowSize(rowSize) |
|
tbl = self._Table |
|
|
|
if self.emptyRowsToAdd and self.Columns: |
|
# Used for display purposes when no data is present. |
|
self._addEmptyRows() |
|
tbl.setColumns(self.Columns) |
|
self._tableRows = tbl.fillTable(force) |
|
if not self._sortRestored: |
|
dabo.ui.callAfter(self._restoreSort) |
|
self._sortRestored = True |
|
|
|
# This will make sure that the current selection mode is activated. |
|
# We can't do it until after the first time the grid is filled. |
|
if not self._modeSet: |
|
self._modeSet = True |
|
self.SelectionMode = self.SelectionMode |
|
|
|
# I've found that both refresh calls are needed sometimes, especially |
|
# on Linux when manually moving a column header with the mouse. |
|
dabo.ui.callAfterInterval(200, self.refresh) |
|
self.refresh() |
|
|
|
|
|
def _updateDaboVisibleColumns(self): |
|
try: |
|
self._daboVisibleColumns = [e[0] for e in enumerate(self._columns) if e[1].Visible] |
|
except wx._core.PyAssertionError, e: |
|
# Can happen when an editor is active and columns resize |
|
vis = [] |
|
for pos, col in enumerate(self._columns): |
|
if col.Visible: |
|
vis.append(pos) |
|
self._daboVisibleColumns = vis |
|
|
|
|
|
def _convertWxColNumToDaboColNum(self, wxCol): |
|
""" |
|
For the Visible property to work, we need to convert the column number |
|
wx sends to the actual column index in grid.Columns. |
|
|
|
Returns None if there is no corresponding dabo column. |
|
""" |
|
try: |
|
return self._daboVisibleColumns[wxCol] |
|
except IndexError: |
|
return None |
|
|
|
|
|
def _convertDaboColNumToWxColNum(self, daboCol): |
|
""" |
|
For the Visible property to work, we need to convert the column number |
|
dabo uses in grid.Columns to the wx column. |
|
|
|
Returns None if there is no corresponding wx column. |
|
""" |
|
try: |
|
return self._daboVisibleColumns.index(daboCol) |
|
except ValueError: |
|
return None |
|
|
|
|
|
def _restoreSort(self): |
|
if not self.Sortable: |
|
return |
|
self.sortedColumn = self._getUserSetting("sortedColumn") |
|
self.sortOrder = self._getUserSetting("sortOrder") |
|
|
|
if self.sortedColumn is not None: |
|
sortCol = None |
|
for idx, col in enumerate(self.Columns): |
|
if col.DataField == self.sortedColumn: |
|
sortCol = idx |
|
break |
|
if sortCol is not None and col.Sortable: |
|
if self.RowCount > 0: |
|
self.processSort(sortCol, toggleSort=False) |
|
|
|
|
|
def _addEmptyRows(self): |
|
""" |
|
Adds blank rows of data to the grid. Used mostly by |
|
the Designer to display a grid that actually looks like a grid. |
|
""" |
|
# First, get the type and field name for each column, and |
|
# add an empty value to a dict. |
|
colDict = {} |
|
for col in self.Columns: |
|
val = " " * 10 |
|
dt = col.DataType |
|
if dt is "bool": |
|
val = False |
|
elif dt in ("int", "long"): |
|
val = 0 |
|
elif dt in ("float", "decimal"): |
|
val = 0.00 |
|
colDict[col.DataField] = val |
|
# Now add as many rows as specified |
|
ds = [] |
|
for cnt in xrange(self.emptyRowsToAdd): |
|
ds.append(colDict) |
|
|
|
self.emptyRowsToAdd = 0 |
|
self.DataSet = ds |
|
|
|
|
|
def buildFromDataSet(self, ds, keyCaption=None, |
|
includeFields=None, colOrder=None, colWidths=None, colTypes=None, |
|
autoSizeCols=True): |
|
""" |
|
Add columns with properties set based on the passed dataset. |
|
|
|
A dataset is defined as one of: |
|
|
|
+ a sequence of dicts, containing fieldname/fieldvalue pairs. |
|
+ a string, which maps to a bizobj on the form. |
|
|
|
The columns will be taken from the first record of the dataset, with each |
|
column header caption being set to the field name, unless the optional |
|
keyCaption parameter is passed. This parameter is a 1:1 dict containing |
|
the data set keys as its keys, and the desired caption as the |
|
corresponding value. |
|
|
|
If the includeFields parameter is a sequence, the only columns added will |
|
be the fieldnames included in the includeFields sequence. If the |
|
includeFields parameter is None, all fields will be added to the grid. |
|
|
|
The columns will be in the order returned by ds.keys(), unless the |
|
optional colOrder parameter is passed. Like the keyCaption property, |
|
this is a 1:1 dict containing key:order. |
|
""" |
|
if not ds: |
|
return False |
|
|
|
if colOrder is None: |
|
colOrder = {} |
|
|
|
if colWidths is None: |
|
colWidths = {} |
|
|
|
if colTypes is None: |
|
colTypes = {} |
|
|
|
if isinstance(ds, basestring) or isinstance(ds, dabo.biz.dBizobj): |
|
# Assume it is a bizobj datasource. |
|
if self.DataSource != ds: |
|
self.DataSource = ds |
|
else: |
|
self.DataSource = None |
|
self.DataSet = ds |
|
bizobj = self.getBizobj() |
|
|
|
if bizobj: |
|
data = bizobj.getDataSet(rows=1) |
|
if data: |
|
firstRec = data[0] |
|
else: |
|
# Ok, the bizobj doesn't have any records, yet we still want to build |
|
# the grid. We can get enough info from getDataStructureFromDescription(): |
|
try: |
|
structure = bizobj.getDataStructureFromDescription() |
|
except TypeError: |
|
# Well, that call failed... seems that sqlite doesn't define a cursor |
|
# description? I need to test this out. For now, fall back to the old |
|
# code that gets the data structure by executing "select * from table |
|
# where 1=0". The downside to this is that no derived fields will be |
|
# included in the structure. |
|
structure = bizobj.getDataStructure() |
|
firstRec = {} |
|
for field in structure: |
|
firstRec[field[0]] = None |
|
if field[0] not in colTypes: |
|
colTypes[field[0]] = field[1] |
|
else: |
|
# not a bizobj datasource |
|
firstRec = ds[0] |
|
|
|
colKeys = [key for key in firstRec.keys() |
|
if (includeFields is None or key in includeFields)] |
|
|
|
# Add the columns |
|
for colKey in colKeys: |
|
# Use the keyCaption values, if possible |
|
try: |
|
cap = keyCaption[colKey] |
|
except (KeyError, TypeError): |
|
cap = colKey |
|
col = self.addColumn(inBatch=True) |
|
col.Caption = cap |
|
col.DataField = colKey |
|
|
|
## pkm: Get the datatype from what is specified in fieldspecs, not from |
|
## the actual type of the record. |
|
try: |
|
dt = colTypes[colKey] |
|
except KeyError: |
|
# But if it didn't exist in the fieldspecs, use the actual type: |
|
dt = type(firstRec[colKey]) |
|
|
|
if dt is type(None): |
|
if bizobj: |
|
for idx in range(bizobj.RowCount)[1:]: |
|
val = bizobj.getFieldVal(colKey, idx) |
|
if val is not None: |
|
dt = type(val) |
|
break |
|
else: |
|
for rec in ds[1:]: |
|
val = rec[colKey] |
|
if val is not None: |
|
dt = type(val) |
|
break |
|
col.DataType = dt |
|
if dt is type(None): |
|
# Default to string type |
|
dt = col.DataType = str |
|
|
|
# See if any order was specified |
|
if colKey in colOrder: |
|
col.Order = colOrder[colKey] |
|
|
|
# See if any width was specified |
|
if colKey in colWidths: |
|
col.Width = colWidths[colKey] |
|
else: |
|
# Use a default width |
|
col.Width = -1 |
|
|
|
# Populate the grid |
|
self.fillGrid(True) |
|
if autoSizeCols: |
|
self.autoSizeCol("all", True) |
|
return True |
|
|
|
def _onCreate(self, evt): |
|
self.restoreDataSet() |
|
|
|
def _onDestroy(self, evt): |
|
self.saveDataSet() |
|
|
|
def _onGridResize(self, evt): |
|
# Prevent unnecessary event processing. |
|
try: |
|
updCol = (self._lastSize != evt._uiEvent.Size) |
|
except AttributeError: |
|
updCol = True |
|
if updCol: |
|
self._lastSize = evt._uiEvent.Size |
|
dabo.ui.callAfter(self._updateColumnWidths) |
|
|
|
|
|
def _totalContentWidth(self, addScrollBar=False): |
|
ret = sum([col.Width for col in self.Columns]) |
|
if self.ShowRowLabels: |
|
ret += self.RowLabelWidth |
|
if addScrollBar and self.isScrollBarVisible("v"): |
|
ret += self._scrollBarSize |
|
return ret |
|
|
|
|
|
def _totalContentHeight(self, addScrollBar=False): |
|
if self.SameSizeRows: |
|
ret = self.RowHeight * self.RowCount |
|
else: |
|
ret = sum([self.GetRowSize(r) for r in xrange(self.RowCount)]) |
|
if self.ShowHeaders: |
|
ret += self.HeaderHeight |
|
if addScrollBar and self.isScrollBarVisible("h"): |
|
ret += self._scrollBarSize |
|
return ret |
|
|
|
|
|
def isScrollBarVisible(self, which): |
|
whichSide = {"h": wx.HORIZONTAL, "v": wx.VERTICAL}[which[0].lower()] |
|
sr = self.GetScrollRange(whichSide) |
|
if self.Application.Platform in ("Win", "GTK"): |
|
# For some reason, GetScrollRange() returns either 1 or 101 when the scrollbar |
|
# is not visible under Windows or GTK. Under OS X, it returns 0 as expected. |
|
return sr not in (1, 101) |
|
return bool(sr) |
|
|
|
|
|
@dabo.ui.deadCheck |
|
def _updateColumnWidths(self): |
|
""" |
|
See if there are any dynamically-sized columns, and resize them |
|
accordingly. |
|
""" |
|
try: |
|
if self._inColWidthUpdate: |
|
return |
|
except AttributeError: |
|
pass |
|
self._inColWidthUpdate = False |
|
if [col for col in self.Columns if col.Expand]: |
|
dabo.ui.callAfterInterval(10, self._delayedUpdateColumnWidths) |
|
|
|
|
|
def _delayedUpdateColumnWidths(self, redo=False): |
|
def _setFlag(): |
|
self._inColWidthUpdate = True |
|
self.BeginBatch() |
|
def _clearFlag(): |
|
self._inColWidthUpdate = False |
|
self.EndBatch() |
|
|
|
if self._inColWidthUpdate: |
|
return |
|
_setFlag() |
|
dynCols = [col for col in self.Columns |
|
if col.Expand] |
|
dynColCnt = len(dynCols) |
|
colWd = self._totalContentWidth(addScrollBar=True) |
|
rowHt = self._totalContentHeight() |
|
grdWd = self.Width |
|
# Subtract extra pixels to avoid triggering the scroll bar. Again, this |
|
# will probably be OS-dependent |
|
diff = grdWd - colWd - 10 |
|
if redo and not diff: |
|
diff = -10 |
|
if not diff: |
|
dabo.ui.callAfterInterval(5, _clearFlag) |
|
return |
|
if not redo and (diff == self._scrollBarSize): |
|
# This can cause infinite loops as we adjust constantly |
|
diff -= 1 |
|
adj = diff/ dynColCnt |
|
mod = diff % dynColCnt |
|
for col in dynCols: |
|
if mod: |
|
newWidth = col.Width + (adj+1) |
|
mod -= 1 |
|
else: |
|
newWidth = col.Width + adj |
|
# Don't allow the Expand columns to shrink below 24px wide. |
|
col.Width = max(24, newWidth) |
|
# Check to see if we need a further adjustment |
|
adjWd = self._totalContentWidth() |
|
if self.isScrollBarVisible("h") and (adjWd < grdWd): |
|
_clearFlag() |
|
self._delayedUpdateColumnWidths(redo=True) |
|
else: |
|
dabo.ui.callAfter(_clearFlag) |
|
|
|
|
|
def autoSizeCol(self, colNum, persist=False): |
|
""" |
|
Set the column to the minimum width necessary to display its data. |
|
|
|
Set colNum='all' to auto-size all columns. Set persist=True to persist the |
|
new width to the user settings table. |
|
""" |
|
if isinstance(colNum, basestring) and colNum.lower() == "all": |
|
self.BeginBatch() |
|
self._inAutoSizeLoop = True |
|
for ii in range(len(self.Columns)): |
|
self.autoSizeCol(ii, persist=persist) |
|
self._updateColumnWidths() |
|
self.EndBatch() |
|
self._inAutoSizeLoop = False |
|
return |
|
maxWidth = 250 ## limit the width of the column to something reasonable |
|
if not self._inAutoSizeLoop: |
|
# lock the screen |
|
self.lockDisplay() |
|
|
|
## This function will get used in both if/elif below: |
|
def _setColSize(idx): |
|
sortIconSize = self.SortIndicatorSize |
|
sortIconBuffer = sortIconSize / 2 |
|
## breathing room around header caption: |
|
capBuffer = 5 |
|
## add additional room to account for possible sort indicator: |
|
capBuffer += ((2 * sortIconSize) + (2 * sortIconBuffer)) |
|
colObj = self.Columns[idx] |
|
if not colObj.Visible: |
|
## wx knows nothing about Dabo's invisible columns |
|
return |
|
idx = self._convertDaboColNumToWxColNum(idx) |
|
autoWidth = self.GetColSize(idx) |
|
|
|
# Account for the width of the header caption: |
|
cw = dabo.ui.fontMetricFromFont(colObj.Caption, |
|
colObj.HeaderFont._nativeFont)[0] + capBuffer |
|
w = max(autoWidth, cw) |
|
w = min(w, maxWidth) |
|
colObj.Width = w |
|
if persist: |
|
colObj._persist("Width") |
|
|
|
try: |
|
self.AutoSizeColumn(self._convertDaboColNumToWxColNum(colNum), setAsMin=False) |
|
except (TypeError, wx.PyAssertionError): |
|
pass |
|
if colNum > -1: |
|
_setColSize(colNum) |
|
|
|
if not self._inAutoSizeLoop: |
|
self.refresh() |
|
self.unlockDisplay() |
|
self._updateColumnWidths() |
|
|
|
|
|
def _paintHeader(self, updateBox=None): |
|
""" |
|
This method handles all of the display for the header, including writing |
|
the Captions along with any sort indicators. |
|
""" |
|
if self._inHeaderPaint: |
|
return |
|
self._inHeaderPaint = True |
|
w = self._getWxHeader() |
|
w.SetBackgroundColour((255, 255, 255)) |
|
if updateBox is None: |
|
updateBox = w.GetClientRect() |
|
try: |
|
# When called from OnPaint event, there should be PaintDC context. |
|
dc = wx.PaintDC(w) |
|
except wx.PyAssertionError: |
|
dc = wx.ClientDC(w) |
|
textAngle = {True: 90, False: 0}[self.VerticalHeaders] |
|
self._columnMetrics = [] |
|
|
|
for idx, col in enumerate(self._columns): |
|
headerRect = col._getHeaderRect() |
|
intersect = wx.IntersectRect(updateBox, headerRect) |
|
if intersect is None: |
|
# column isn't visible |
|
continue |
|
headerRect[0] -= 1 |
|
headerRect[2] += 1 |
|
|
|
sortIndicator = False |
|
colObj = self.getColByX(intersect[0]) |
|
if not colObj: |
|
# Grid is probably being created or destroyed, so just skip it |
|
continue |
|
dc.SetClippingRegion(*headerRect) |
|
|
|
holdBrush = dc.GetBrush() |
|
holdPen = dc.GetPen() |
|
fcolor = colObj.HeaderForeColor |
|
if fcolor is None: |
|
fcolor = self.HeaderForeColor |
|
if fcolor is None: |
|
fcolor = (0,0,0) |
|
bcolor = colObj.HeaderBackColor |
|
if bcolor is None: |
|
bcolor = self.HeaderBackColor |
|
dc.SetTextForeground(fcolor) |
|
wxNativeFont = colObj.HeaderFont._nativeFont |
|
# draw the col. header background: |
|
if bcolor is not None: |
|
dc.SetBrush(wx.Brush(bcolor, wx.SOLID)) |
|
dc.SetPen(wx.Pen(fcolor, width=0)) |
|
dc.DrawRectangle(*headerRect) |
|
|
|
# draw the col. border: |
|
dc.SetBrush(wx.TRANSPARENT_BRUSH) |
|
dc.SetPen(self.GetDefaultGridLinePen()) |
|
dc.DrawRectangle(*headerRect) |
|
dc.SetPen(holdPen) |
|
dc.SetBrush(holdBrush) |
|
|
|
if colObj.DataField == self.sortedColumn: |
|
sortIndicator = True |
|
sortIconSize = self.SortIndicatorSize |
|
sortIconBuffer = sortIconSize / 2 |
|
# draw a triangle, pointed up or down, at the top left |
|
# of the column. TODO: Perhaps replace with prettier icons |
|
left = headerRect[0] + sortIconBuffer |
|
top = headerRect[1] + sortIconBuffer |
|
brushColor = self.SortIndicatorColor |
|
if isinstance(brushColor, basestring): |
|
brushColor = dColors.colorTupleFromName(brushColor) |
|
dc.SetBrush(wx.Brush(brushColor, wx.SOLID)) |
|
if self.sortOrder == "DESC": |
|
# Down arrow |
|
dc.DrawPolygon([(left, top), (left + sortIconSize, top), |
|
(left + sortIconBuffer, top + sortIconSize)]) |
|
elif self.sortOrder == "ASC": |
|
# Up arrow |
|
dc.DrawPolygon([(left + sortIconBuffer, top), |
|
(left + sortIconSize, top + sortIconSize), |
|
(left, top + sortIconSize)]) |
|
else: |
|
# Column is not sorted, so don't draw. |
|
sortIndicator = False |
|
|
|
dc.SetFont(wxNativeFont) |
|
ah = colObj.HeaderHorizontalAlignment |
|
av = colObj.HeaderVerticalAlignment |
|
if ah is None: |
|
ah = self.HeaderHorizontalAlignment |
|
if av is None: |
|
av = self.HeaderVerticalAlignment |
|
if ah is None: |
|
ah = "Center" |
|
if av is None: |
|
av = "Bottom" |
|
wxah = {"Center": wx.ALIGN_CENTRE_HORIZONTAL, |
|
"Left": wx.ALIGN_LEFT, |
|
"Right": wx.ALIGN_RIGHT}[ah] |
|
wxav = {"Center": wx.ALIGN_CENTRE_VERTICAL, |
|
"Top": wx.ALIGN_TOP, |
|
"Bottom": wx.ALIGN_BOTTOM}[av] |
|
|
|
# Give some more space around the rect - some platforms use a 3d look |
|
# and anyway it looks better if left/right aligned text isn't right on |
|
# the line. |
|
horBuffer = 3 |
|
vertBuffer = 2 |
|
sortBuffer = horBuffer |
|
if sortIndicator: |
|
# If there's a sort indicator, we'll nudge the caption over |
|
sortBuffer += (sortIconSize + sortIconBuffer) |
|
trect = list(headerRect) |
|
trect[0] = trect[0] + sortBuffer |
|
trect[1] = trect[1] + vertBuffer |
|
if ah == "Center": |
|
trect[2] = trect[2] - (2 * sortBuffer) |
|
else: |
|
trect[2] = trect[2] - (horBuffer + sortBuffer) |
|
trect[3] = trect[3] - (2 * vertBuffer) |
|
trect = wx.Rect(*trect) |
|
|
|
twd, tht = dabo.ui.fontMetricFromDC(dc, colObj.Caption) |
|
if self.VerticalHeaders: |
|
# Note that when rotating 90 degrees, the width affect height, |
|
# and vice-versa |
|
twd, tht = tht, twd |
|
self._columnMetrics.append((twd, tht)) |
|
|
|
# Figure out the x,y coordinates to start the text drawing. |
|
left, top, wd, ht = trect |
|
x = left |
|
if ah == "Center": |
|
x += (wd / 2) - (twd / 2) |
|
elif ah == "Right": |
|
x += wd - twd |
|
# Note that we need to adjust for text height when angle is 0. |
|
yadj = 0 |
|
if textAngle == 0: |
|
yadj = tht |
|
y = top + ht - yadj |
|
if av == "Top": |
|
y = top + tht + 2 - yadj |
|
elif av == "Center": |
|
y = top + (ht / 2) + (tht / 2) - yadj |
|
|
|
txt = self.drawText("%s" % colObj.Caption, x, y, angle=textAngle, |
|
persist=False, dc=dc, useDefaults=True) |
|
dc.DestroyClippingRegion() |
|
if self.AutoAdjustHeaderHeight: |
|
self.fitHeaderHeight() |
|
self._inHeaderPaint = False |
|
|
|
|
|
def fitHeaderHeight(self): |
|
""" |
|
Sizes the HeaderHeight to comfortably fit the captions. Primarily used for |
|
vertical captions or multi-line captions. |
|
""" |
|
self._paintHeader() |
|
if self._columnMetrics: |
|
self._headerMaxTextHeight = max([cht for cwd, cht in self._columnMetrics]) |
|
else: |
|
self._headerMaxTextHeight = 0 |
|
diff = (self._headerMaxTextHeight + 20) - self.HeaderHeight |
|
if diff: |
|
self.HeaderHeight += diff |
|
dabo.ui.callAfter(self.refresh) |
|
|
|
|
|
def showColumn(self, col, visible): |
|
""" |
|
If the column is not shown and visible=True, show it. Likewise |
|
but opposite if visible=False. |
|
""" |
|
col = self._resolveColumn(col, logOnly=True) |
|
if col is None: |
|
# Invalid 'col' passed |
|
return |
|
col._visible = visible |
|
self._syncColumnCount() |
|
if getattr(self.Parent, "__inRefresh", False): |
|
self.refresh() |
|
|
|
|
|
def moveColumn(self, colNum, toNum): |
|
"""Move the column to a new position.""" |
|
oldCol = self.Columns[colNum] |
|
self.Columns.remove(oldCol) |
|
if toNum > colNum: |
|
self.Columns.insert(toNum-1, oldCol) |
|
else: |
|
self.Columns.insert(toNum, oldCol) |
|
for col in self.Columns: |
|
col.Order = self.Columns.index(col) * 10 |
|
col._persist("Order") |
|
self.fillGrid(True) |
|
|
|
|
|
def getColumnValueByRow(self, col, row): |
|
"""Returns the value in the given column and row.""" |
|
if isinstance(col, dColumn): |
|
colnum = self.Columns.index(col) |
|
else: |
|
colnum = col |
|
return self.GetValue(row, colnum) |
|
|
|
|
|
def sizeToColumns(self, scrollBarFudge=True): |
|
""" |
|
Set the width of the grid equal to the sum of the widths of the columns. |
|
|
|
If scrollBarFudge is True, additional space will be added to account for |
|
the width of the vertical scrollbar. |
|
""" |
|
fudge = 5 |
|
if scrollBarFudge: |
|
fudge = 18 |
|
self.Width = reduce(operator.add, [col.Width for col in self.Columns]) + fudge |
|
|
|
|
|
def sizeToRows(self, maxHeight=500, scrollBarFudge=True): |
|
""" |
|
Set the height of the grid equal to the sum of the heights of the rows. |
|
|
|
This is intended to be used only when the number of rows is expected to be |
|
low. Set maxHeight to whatever you want the maximum height to be. |
|
""" |
|
fudge = 5 |
|
if scrollBarFudge: |
|
fudge = 18 |
|
self.Height = min(self.RowHeight * self.RowCount, maxHeight) + fudge |
|
|
|
|
|
def onIncSearchTimer(self, evt): |
|
""" |
|
Occurs when the incremental search timer reaches its interval. |
|
It is time to run the search, if there is any search in the buffer. |
|
""" |
|
if self.currSearchStr not in ("", "\n", "\r", "\r\n"): |
|
self.runIncSearch() |
|
else: |
|
self.incSearchTimer.stop() |
|
|
|
|
|
##----------------------------------------------------------## |
|
## begin: user hook methods ## |
|
##----------------------------------------------------------## |
|
|
|
def fillContextMenu(self, menu): |
|
""" |
|
User hook called just before showing the context menu. |
|
|
|
User code can append menu items, or replace/remove the menu entirely. |
|
Return a dMenu or None from this hook. Default: no context menu. |
|
""" |
|
return menu |
|
|
|
|
|
def fillHeaderContextMenu(self, menu): |
|
""" |
|
User hook called just before showing the context menu for the header. |
|
|
|
User code can append menu items, or replace/remove the menu entirely. |
|
Return a dMenu or None from this hook. The default menu includes an |
|
option to autosize the column. |
|
""" |
|
return menu |
|
|
|
##----------------------------------------------------------## |
|
## end: user hook methods ## |
|
##----------------------------------------------------------## |
|
|
|
|
|
def sort(self): |
|
"""Hook method used in subclasses for custom sorting.""" |
|
pass |
|
|
|
|
|
def processSort(self, gridCol=None, toggleSort=True): |
|
""" |
|
Sort the grid column. |
|
|
|
Toggle between ascending and descending. If the grid column index isn't |
|
passed, the currently active grid column will be sorted. |
|
""" |
|
if gridCol is None: |
|
gridCol = self.CurrentColumn |
|
|
|
colObj = self._resolveColumn(gridCol) |
|
canSort = (self.Sortable and colObj.Sortable) |
|
columnToSort = colObj.DataField |
|
sortCol = self.Columns.index(colObj) |
|
dataType = self.Columns[sortCol].DataType |
|
|
|
if not canSort: |
|
# Some columns, especially those with mixed values, |
|
# should not be sorted. |
|
return |
|
|
|
sortOrder="ASC" |
|
if columnToSort == self.sortedColumn: |
|
sortOrder = self.sortOrder |
|
if toggleSort: |
|
if sortOrder == "ASC": |
|
sortOrder = "DESC" |
|
elif sortOrder == "DESC": |
|
columnToSort = None |
|
sortOrder = "ASC" |
|
else: |
|
sortOrder = "ASC" |
|
self.sortOrder = sortOrder |
|
self.sortedColumn = columnToSort |
|
|
|
eventData = {"column": colObj, "sortOrder": sortOrder} |
|
self.raiseEvent(dEvents.GridBeforeSort, eventObject=self, |
|
eventData=eventData) |
|
|
|
biz = self.getBizobj() |
|
if columnToSort is not None: |
|
if self.customSort: |
|
# Grids tied to bizobj cursors may want to use their own sorting. |
|
self.sort() |
|
elif biz: |
|
# Use the default sort() in the bizobj: |
|
try: |
|
biz.sort(columnToSort, sortOrder, self.caseSensitiveSorting) |
|
except dException.NoRecordsException: |
|
# no records to sort: who cares. |
|
pass |
|
else: |
|
# Create the list to hold the rows for sorting |
|
caseSensitive = self.caseSensitiveSorting |
|
sortList = [] |
|
rowNum = 0 |
|
rowlabels = self.RowLabels |
|
if self.DataSet: |
|
for row in self.DataSet: |
|
if rowlabels: |
|
sortList.append([row[columnToSort], row, rowlabels[rowNum]]) |
|
rowNum += 1 |
|
else: |
|
sortList.append([row[columnToSort], row]) |
|
# At this point we have a list consisting of lists. Each of these member |
|
# lists contain the sort value in the zeroth element, and the row as |
|
# the first element. |
|
# First, see if we are comparing strings |
|
if dataType is None: |
|
f = sortList[0][0] |
|
if f is None: |
|
# We are just poking around, trying to glean the datatype, which is prone |
|
# to error. The record we just checked is None, so try the last record and |
|
# then give up. |
|
f = sortList[-1][0] |
|
#pkm: I think grid column DataType properties should store raw python |
|
# types, not string renditions of them. But for now, convert to |
|
# string renditions. I also think that this codeblock should be |
|
# obsolete once all dabo grids use dColumn objects. |
|
if isinstance(f, datetime.date): |
|
dataType = "date" |
|
elif isinstance(f, datetime.datetime): |
|
dataType = "datetime" |
|
elif isinstance(f, unicode): |
|
dataType = "unicode" |
|
elif isinstance(f, str): |
|
dataType = "string" |
|
elif isinstance(f, long): |
|
dataType = "long" |
|
elif isinstance(f, int): |
|
dataType = "int" |
|
elif isinstance(f, Decimal): |
|
dataType = "decimal" |
|
else: |
|
dataType = None |
|
sortingStrings = isinstance(sortList[0][0], basestring) |
|
else: |
|
sortingStrings = dataType in ("unicode", "string") |
|
|
|
if sortingStrings and not caseSensitive: |
|
sortKey = caseInsensitiveSortKey |
|
elif dataType in ("date", "datetime"): |
|
# can't compare NoneType to these types: |
|
sortKey = noneSortKey |
|
else: |
|
sortKey = None |
|
sortList.sort(key=sortKey, reverse=(sortOrder == "DESC")) |
|
|
|
# Extract the rows into a new list, then set the dataSet to the new list |
|
newRows = [] |
|
newLabels = [] |
|
for elem in sortList: |
|
newRows.append(elem[1]) |
|
if self.RowLabels: |
|
newLabels.append(elem[2]) |
|
self.RowLabels = newLabels |
|
# Set this to avoid infinite loops |
|
self._settingDataSetFromSort = True |
|
self.DataSet = newRows |
|
self._settingDataSetFromSort = False |
|
|
|
if biz: |
|
dabo.ui.setAfter(self, "CurrentRow", biz.RowNumber) |
|
|
|
if self._refreshAfterSort: |
|
self.refresh() |
|
|
|
self._setUserSetting("sortedColumn", columnToSort) |
|
self._setUserSetting("sortOrder", sortOrder) |
|
self.raiseEvent(dEvents.GridAfterSort, eventObject=self, |
|
eventData=eventData) |
|
dabo.ui.callAfterInterval(200, self.Form.update) ## rownum in status bar |
|
|
|
|
|
def restoreDataSet(self): |
|
if self.SaveRestoreDataSet: |
|
ds = self.Application.getUserSetting("%s.DataSet" |
|
% self.getAbsoluteName()) |
|
if ds is not None: |
|
self.DataSet = ds |
|
|
|
|
|
def saveDataSet(self): |
|
if self.SaveRestoreDataSet: |
|
self.Application.setUserSetting("%s.DataSet" |
|
% self.getAbsoluteName(), self.DataSet) |
|
|
|
|
|
def runIncSearch(self): |
|
"""Run the incremental search.""" |
|
gridCol = self.CurrentColumn |
|
if gridCol < 0: |
|
gridCol = 0 |
|
fld = self.Columns[gridCol].DataField |
|
if self.RowCount <= 0: |
|
# Nothing to seek within! |
|
return |
|
if not (self.Searchable and self.Columns[gridCol].Searchable): |
|
# Doesn't apply to this column. |
|
self.currSearchStr = "" |
|
return |
|
newRow = self.CurrentRow |
|
biz = self.getBizobj() |
|
srchVal = origSrchStr = self.currSearchStr |
|
self.currSearchStr = "" |
|
near = self.searchNearest |
|
caseSensitive = self.searchCaseSensitive |
|
# Copy the specified field vals and their row numbers to a list, and |
|
# add those lists to the sort list |
|
sortList = [] |
|
for i in range(0, self.RowCount): |
|
if biz: |
|
val = biz.getFieldVal(fld, i, _forceNoCallback=True) |
|
else: |
|
val = self.DataSet[i][fld] |
|
sortList.append( [val, i] ) |
|
|
|
# Determine if we are seeking string values |
|
compString = False |
|
for row in sortList: |
|
if row[0] is not None: |
|
compString = isinstance(row[0], basestring) |
|
break |
|
|
|
if not compString: |
|
# coerce srchVal to be the same type as the field type |
|
listval = sortList[0][0] |
|
if isinstance(listval, int): |
|
try: |
|
srchVal = int(srchVal) |
|
except ValueError: |
|
srchVal = int(0) |
|
elif isinstance(listval, long): |
|
try: |
|
srchVal = long(srchVal) |
|
except ValueError: |
|
srchVal = long(0) |
|
elif isinstance(listval, float): |
|
try: |
|
srchVal = float(srchVal) |
|
except ValueError: |
|
srchVal = float(0) |
|
elif isinstance(listval, (datetime.datetime, datetime.date, datetime.time)): |
|
# We need to convert the sort vals into strings |
|
sortList = [(ustr(vv), i) for vv, i in sortList] |
|
compString = True |
|
|
|
# Now iterate through the list to find the matching value. I know that |
|
# there are more efficient search algorithms, but for this purpose, we'll |
|
# just use brute force |
|
if compString: |
|
if caseSensitive: |
|
mtchs = [vv for vv in sortList |
|
if isinstance(vv[0], basestring) and vv[0].startswith(srchVal)] |
|
else: |
|
srchVal = srchVal.lower() |
|
mtchs = [vv for vv in sortList |
|
if isinstance(vv[0], basestring) and vv[0].lower().startswith(srchVal)] |
|
else: |
|
mtchs = [vv for vv in sortList |
|
if vv[0] == srchVal] |
|
if mtchs: |
|
# The row num is the second element. We want the first row in |
|
# the list, since it will still be sorted. |
|
newRow = mtchs[0][1] |
|
else: |
|
for fldval, row in sortList: |
|
if not compString or caseSensitive: |
|
match = (fldval == srchVal) |
|
else: |
|
# Case-insensitive string search. |
|
match = (isinstance(fldval, basestring) and fldval.lower() == srchVal) |
|
if match: |
|
newRow = row |
|
break |
|
else: |
|
if near: |
|
newRow = row |
|
# If we are doing a near search, see if the row is less than the |
|
# requested matching value. If so, update the value of 'ret'. If not, |
|
# we have passed the matching value, so there's no point in |
|
# continuing the search, but we mu |
|
if compString and not caseSensitive and isinstance(fldval, basestring): |
|
toofar = fldval.lower() > srchVal |
|
else: |
|
toofar = fldval > srchVal |
|
if toofar: |
|
break |
|
self.CurrentRow = newRow |
|
|
|
if self.Form is not None: |
|
# Add a '.' to the status bar to signify that the search is |
|
# done, and clear the search string for next time. |
|
currAutoUpdate = self.Form.AutoUpdateStatusText |
|
self.Form.AutoUpdateStatusText = False |
|
if currAutoUpdate: |
|
dabo.ui.setAfterInterval(1000, self.Form, "AutoUpdateStatusText", True) |
|
self.Form.setStatusText("Search: '%s'." % origSrchStr) |
|
self.currSearchStr = "" |
|
|
|
|
|
def addToSearchStr(self, key): |
|
""" |
|
Add a character to the current incremental search. |
|
|
|
Called by KeyDown when the user pressed an alphanumeric key. Add the |
|
key to the current search and start the timer. |
|
""" |
|
app = self.Application |
|
searchDelay = self.SearchDelay |
|
if searchDelay is None: |
|
if app is not None: |
|
searchDelay = self.Application.SearchDelay |
|
else: |
|
# use a default |
|
searchDelay = 500 |
|
|
|
self.incSearchTimer.stop() |
|
self.currSearchStr = "".join((self.currSearchStr, key)) |
|
|
|
if self.Form is not None: |
|
self.Form.setStatusText("Search: '%s'" % self.currSearchStr) |
|
self.incSearchTimer.start(searchDelay) |
|
|
|
|
|
def findReplace(self, action, findString, replaceString, downwardSearch, |
|
wholeWord, matchCase): |
|
"""Called from the 'Find' dialog.""" |
|
ret = False |
|
rowcol = currRow, currCol = (self.CurrentRow, self.CurrentColumn) |
|
if downwardSearch: |
|
op = operator.gt |
|
else: |
|
op = operator.lt |
|
if wholeWord: |
|
if matchCase: |
|
srch = r"\b%s\b" % findString |
|
findGen = ((r,c) for r in xrange(self.RowCount) for c in xrange(self.ColumnCount) |
|
if op((r,c), rowcol) |
|
and re.search(srch, ustr(self.GetValue(r, c)))) |
|
else: |
|
srch = r"\b%s\b" % findString.lower() |
|
findGen = ((r,c) for r in xrange(self.RowCount) for c in xrange(self.ColumnCount) |
|
if op((r,c), rowcol) |
|
and re.search(srch, ustr(self.GetValue(r, c)).lower())) |
|
else: |
|
if matchCase: |
|
findGen = ((r,c) for r in xrange(self.RowCount) for c in xrange(self.ColumnCount) |
|
if op((r,c), rowcol) |
|
and findString in ustr(self.GetValue(r, c))) |
|
else: |
|
findGen = ((r,c) for r in xrange(self.RowCount) for c in xrange(self.ColumnCount) |
|
if op((r,c), rowcol) |
|
and findString.lower() in ustr(self.GetValue(r, c)).lower()) |
|
if action == "Find": |
|
try: |
|
while True: |
|
newR, newC = findGen.next() |
|
targetVal = self.GetValue(newR, newC) |
|
targetString = ustr(targetVal) |
|
if isinstance(targetVal, (basestring, datetime.datetime, datetime.date)): |
|
# Values can be inexact matches |
|
break |
|
else: |
|
# Needs to be an exact match |
|
if findString == targetString: |
|
break |
|
ret = True |
|
self._lastRow, self._lastCol = newR, newC |
|
self.CurrentRow, self.CurrentColumn = newR, newC |
|
except StopIteration: |
|
ret = False |
|
elif action == "Replace": |
|
val = self.GetValue(currRow, currCol) |
|
if isinstance(val, basestring): |
|
self.SetValue(currRow, currCol, val.replace(findString, replaceString)) |
|
ret = True |
|
elif isinstance(val, bool): |
|
if replaceString.lower() in ("true", "t", "false", "f", "1", "0", "yes", "y", "no", "n"): |
|
newval = replaceString.lower() in ("true", "t", "1", "yes", "y") |
|
self.SetValue(currRow, currCol, newval) |
|
ret = True |
|
else: |
|
dabo.log.error(_("Invalid boolean replacement value: %s") % replaceString) |
|
ret = False |
|
else: |
|
# Try the numeric types |
|
typFunc = type(val) |
|
if typFunc(findString) == val: |
|
# We can replace if replaceString can be the correct type |
|
errors = (ValueError, InvalidOperation) |
|
try: |
|
newval = typFunc(replaceString) |
|
self.SetValue(currRow, currCol, newval) |
|
ret = True |
|
except errors: |
|
dabo.log.error(_("Invalid replacement value: %s") % replaceString) |
|
ret = False |
|
if ret: |
|
self.ForceRefresh() |
|
return ret |
|
|
|
|
|
def getColNumByX(self, x): |
|
"""Given the x-coordinate, return the column index in self.Columns.""" |
|
col = self.XToCol(x + (self.GetViewStart()[0]*self.GetScrollPixelsPerUnit()[0])) |
|
if col == wx.NOT_FOUND: |
|
col = -1 |
|
else: |
|
col = self._convertWxColNumToDaboColNum(col) |
|
return col |
|
|
|
|
|
def getRowNumByY(self, y): |
|
"""Given the y-coordinate, return the row number.""" |
|
row = self.YToRow(y + (self.GetViewStart()[1]*self.GetScrollPixelsPerUnit()[1])) |
|
if row == wx.NOT_FOUND: |
|
row = -1 |
|
return row |
|
|
|
|
|
def getColByX(self, x): |
|
"""Given the x-coordinate, return the column object.""" |
|
colNum = self.getColNumByX(x) |
|
if (colNum < 0) or (colNum > self.ColumnCount-1): |
|
return None |
|
else: |
|
return self.Columns[colNum] |
|
|
|
|
|
def getColByDataField(self, df): |
|
"""Given a DataField value, return the corresponding column.""" |
|
try: |
|
ret = [col for col in self.Columns |
|
if col.DataField == df][0] |
|
except IndexError: |
|
ret = None |
|
return ret |
|
|
|
|
|
def maxColOrder(self): |
|
"""Returns the highest value of Order for all columns.""" |
|
ret = -1 |
|
if len(self.Columns) > 0: |
|
ret = max([cc.Order for cc in self.Columns]) |
|
return ret |
|
|
|
|
|
def addColumns(self, *columns): |
|
""" |
|
Adds a set of columns to the grid. |
|
|
|
Each column in the set should be a dColumn instance. |
|
""" |
|
columns = self._resolveColumns(columns) |
|
for column in columns: |
|
self.addColumn(column, inBatch=True) |
|
self._syncColumnCount() |
|
self.fillGrid(True) |
|
|
|
|
|
def addColumn(self, col=None, inBatch=False, *args, **kwargs): |
|
"""Adds a column to the grid. |
|
|
|
If no col (class or instance) is passed, a blank dColumn is added, which |
|
can be customized later. Any extra keyword arguments are passed to the |
|
constructor of the new dColumn. |
|
""" |
|
if col is None: |
|
col = self.ColumnClass(self, *args, **kwargs) |
|
else: |
|
if not isinstance(col, dColumn): |
|
if issubclass(col, dabo.ui.dColumn): |
|
col = col(self, *args, **kwargs) |
|
else: |
|
raise ValueError(_("col must be a dColumn subclass or instance")) |
|
else: |
|
col.setProperties(**kwargs) |
|
col.Parent = self |
|
|
|
if col.Order == -1: |
|
maxOrd = self.maxColOrder() |
|
if maxOrd < 0: |
|
newOrd = 0 |
|
else: |
|
newOrd = maxOrd + 10 |
|
col.Order = newOrd |
|
self.Columns.append(col) |
|
if not inBatch: |
|
self._syncColumnCount() |
|
self.fillGrid(force=True) |
|
try: |
|
## Set the Width property last, otherwise it won't stick: |
|
if not col.Width: |
|
col.Width = 75 |
|
else: |
|
## If Width was specified in the dColumn subclass or in the constructor, |
|
## it's been set as the property but because it wasn't part of the grid |
|
## yet it hasn't yet taken effect: force it. |
|
col.Width = col.Width |
|
except (wx.PyAssertionError, wx.core.PyAssertionError, wx._core.PyAssertionError): |
|
# If the underlying wx grid doesn't yet know about the column, such |
|
# as when adding columns with inBatch=True, this can throw an error |
|
if not inBatch: |
|
# For now, just log it |
|
dabo.log.info(_("Cannot set width of column %s") % col.Order) |
|
return col |
|
|
|
|
|
def _resolveColumns(self, columns): |
|
if len(columns) == 1 and isinstance(columns[0], (list, tuple, set)): |
|
columns = columns[0] |
|
return [self._resolveColumn(col) for col in columns] |
|
|
|
|
|
def _resolveColumn(self, colOrIdx, returnColumn=True, logOnly=False): |
|
""" |
|
Accepts either a column object or a column index, and returns a column |
|
object. If you need the column's index instead, pass False to the |
|
'returnColumn' parameter. |
|
|
|
Used for cases where a method can accept either type of reference, but |
|
needs to work with the actual column. |
|
|
|
If anything other than a column reference or an integer is passed, a |
|
ValueError will be raised. If you prefer to simply log the error without |
|
raising an exception, pass True to the logOnly parameter (default=False). |
|
""" |
|
if isinstance(colOrIdx, (int, long)): |
|
return self.Columns[colOrIdx] if returnColumn else colOrIdx |
|
elif isinstance(colOrIdx, dColumn): |
|
return colOrIdx if returnColumn else self.Columns.index(colOrIdx) |
|
else: |
|
typcoi = type(colOrIdx) |
|
msg = _("Values must be a dColumn or an int; received '%(colOrIdx)s' " |
|
"(%(typcoi)s)") % locals() |
|
if logOnly: |
|
dabo.log.error(msg) |
|
return None |
|
else: |
|
raise ValueError(msg) |
|
|
|
|
|
def removeColumns(self, *columns): |
|
""" |
|
Removes a set of columns from the grid. |
|
|
|
The passed columns can be indexes or dColumn instances, or both. |
|
""" |
|
columns = self._resolveColumns(columns) |
|
for col in columns: |
|
self.removeColumn(col, inBatch=True) |
|
self._syncColumnCount() |
|
self.fillGrid(True) |
|
|
|
|
|
def removeColumn(self, col=None, inBatch=False): |
|
""" |
|
Removes a column from the grid. |
|
|
|
If no column is passed, the last column is removed. The col argument can |
|
be either a column index or a dColumn instance. |
|
""" |
|
if col is None: |
|
colNum = self.ColumnCount - 1 |
|
else: |
|
colNum = self._resolveColumn(col, returnColumn=False, logOnly=True) |
|
del self.Columns[colNum] |
|
if not inBatch: |
|
self._syncColumnCount() |
|
self.fillGrid(True) |
|
|
|
|
|
def cell(self, row, col): |
|
class GridCell(object): |
|
def __init__(self, parent, row, col): |
|
self.parent = parent |
|
self.row = row |
|
self.col = col |
|
|
|
def _getVal(self): |
|
return self.parent.GetValue(self.row, self.col) |
|
def _setVal(self, val): |
|
self.parent.SetValue(self.row, self.col, val) |
|
Value = property(_getVal, _setVal) |
|
return GridCell(self, row, col) |
|
|
|
|
|
def copy(self): |
|
valSep = dabo.copyValueSeparator |
|
strSep = dabo.copyStringSeparator |
|
lnSep = dabo.copyLineSeparator |
|
|
|
def valEscape(val): |
|
if isinstance(val, basestring): |
|
# Need to escape tabs and newlines |
|
escval = val.replace("\t", "\\t").replace("\n", "\\n") |
|
if strSep: |
|
# Also escape the string separator |
|
escval = escval.replace(strSep, "\\%s" % strSep) |
|
return "%s%s%s" % (strSep, escval, strSep) |
|
else: |
|
ret = str(val) |
|
if isinstance(val, (Decimal, float)): |
|
# We need to convert decimal point accordingly to the locale. |
|
ret = ret.replace(".", decimalPoint) |
|
return ret |
|
|
|
def valuesForRange(rowrange, colrange): |
|
allvals = [] |
|
for row in rowrange: |
|
rowvals = [] |
|
for col in colrange: |
|
val = self.getValue(row, col) |
|
rowvals.append(valEscape(val)) |
|
allvals.append(valSep.join(rowvals)) |
|
return lnSep.join(allvals) |
|
|
|
sel = self.Selection |
|
if not sel: |
|
return None |
|
selmode = self.SelectionMode |
|
copied = [] |
|
txtToCopy = "" |
|
if selmode == "Cell": |
|
copySections = [] |
|
for rangeTuple in sel: |
|
zrow, zcol = zip(*rangeTuple) |
|
rowrange = range(zrow[0], zrow[1] + 1) |
|
colrange = range(zcol[0], zcol[1] + 1) |
|
copySections.append(valuesForRange(rowrange, colrange)) |
|
txtToCopy = lnSep.join(copySections) |
|
else: |
|
if selmode == "Row": |
|
rowrange = sel |
|
colrange = range(0, self.ColumnCount) |
|
else: |
|
rowrange = range(0, self.RowCount) |
|
colrange = sel |
|
txtToCopy = valuesForRange(rowrange, colrange) |
|
self.Application.copyToClipboard(txtToCopy) |
|
|
|
|
|
def getBizobj(self): |
|
""" |
|
Get the bizobj that is controlling this grid. |
|
|
|
Either there was an explicitly-set bizobj reference in |
|
self.DataSource, in which case that is returned, or self.DataSource |
|
is a string, in which case the form hierarchy is walked finding the |
|
first bizobj with the correct DataSource. |
|
|
|
Return None if no bizobj can be located. |
|
""" |
|
ds = self.DataSource |
|
if isinstance(ds, dabo.biz.dBizobj): |
|
return ds |
|
if isinstance(ds, basestring) and self.Form is not None: |
|
form = self.Form |
|
while form is not None: |
|
if hasattr(form, "getBizobj"): |
|
biz = form.getBizobj(ds) |
|
if isinstance(biz, dabo.biz.dBizobj): |
|
return biz |
|
form = form.Form |
|
return None |
|
|
|
|
|
def setRowHeight(self, row, ht): |
|
"""Explicitly set the height of a specific row in the grid. If |
|
SameSizeRows is True, all rows will be affected. |
|
""" |
|
if self.SameSizeRows: |
|
self.RowHeight = ht |
|
else: |
|
if row >= self.RowCount: |
|
rcm = self.RowCount - 1 |
|
dabo.log.error(_("Specified row is out of range for setRowHeight(). " |
|
"Attempted: %(row)s; max row: %(rcm)s") % locals()) |
|
return |
|
self.SetRowSize(row, ht) |
|
|
|
|
|
def _getWxHeader(self): |
|
"""Return the wx grid header window.""" |
|
return self.GetGridColLabelWindow() |
|
|
|
|
|
def _syncCurrentRow(self): |
|
""" |
|
Sync the CurrentRow of the grid to the RowNumber of the bizobj. |
|
|
|
Has no effect if the grid's DataSource isn't a link to a bizobj. |
|
""" |
|
try: |
|
self.CurrentRow = self.getBizobj().RowNumber |
|
except AttributeError: |
|
pass |
|
# On Win, when row is deleted, active row remains unselected. |
|
if self.SelectionMode == "Row": |
|
row = self.CurrentRow |
|
if row not in self.Selection: |
|
self.SelectRow(row) |
|
|
|
|
|
def _syncColumnCount(self): |
|
"""Sync wx's rendition of column count with our self.ColumnCount""" |
|
msg = None |
|
wxColumnCount = self.GetNumberCols() |
|
daboColumnCount = len([col for col in self.Columns if col.Visible]) |
|
diff = daboColumnCount - wxColumnCount |
|
|
|
self.BeginBatch() |
|
if diff < 0: |
|
msg = wx.grid.GridTableMessage(self._Table, |
|
wx.grid.GRIDTABLE_NOTIFY_COLS_DELETED, |
|
0, abs(diff)) |
|
elif diff > 0: |
|
msg = wx.grid.GridTableMessage(self._Table, |
|
wx.grid.GRIDTABLE_NOTIFY_COLS_APPENDED, |
|
diff) |
|
if msg: |
|
self.ProcessTableMessage(msg) |
|
self.EndBatch() |
|
|
|
# Update the visible columns attribute |
|
self._updateDaboVisibleColumns() |
|
|
|
# We need to adjust the Width of visible columns here, in case any |
|
# columns have Visible = False. |
|
for daboCol, colObj in enumerate(self._columns): |
|
wxCol = self._convertDaboColNumToWxColNum(daboCol) |
|
if wxCol is not None: |
|
self.SetColSize(wxCol, colObj.Width) |
|
|
|
|
|
def _syncRowCount(self): |
|
"""Sync wx's rendition of row count with our self.RowCount""" |
|
msg = None |
|
wxRowCount = self.GetNumberRows() |
|
daboRowCount = self.RowCount |
|
diff = daboRowCount - wxRowCount |
|
if diff < 0: |
|
msg = wx.grid.GridTableMessage(self._Table, |
|
wx.grid.GRIDTABLE_NOTIFY_ROWS_DELETED, |
|
0, abs(diff)) |
|
elif diff > 0: |
|
msg = wx.grid.GridTableMessage(self._Table, |
|
wx.grid.GRIDTABLE_NOTIFY_ROWS_APPENDED, |
|
diff) |
|
if msg: |
|
self.ProcessTableMessage(msg) |
|
|
|
|
|
def _getDefaultGridColAttr(self): |
|
"""Return the GridCellAttr that will be used for all columns by default.""" |
|
attr = wx.grid.GridCellAttr() |
|
attr.SetAlignment(wx.ALIGN_TOP, wx.ALIGN_LEFT) |
|
attr.SetReadOnly(True) |
|
return attr |
|
|
|
|
|
def _getUserSetting(self, prop): |
|
"""Get the value of prop from the user settings table.""" |
|
app = self.Application |
|
form = self.Form |
|
ret = None |
|
if app is not None and form is not None \ |
|
and not hasattr(self, "isDesignerControl"): |
|
settingName = "%s.%s.%s" % (form.Name, self.Name, prop) |
|
ret = app.getUserSetting(settingName) |
|
return ret |
|
|
|
|
|
def _setUserSetting(self, prop, val): |
|
"""Persist the value of prop to the user settings table.""" |
|
app = self.Application |
|
form = self.Form |
|
if app is not None and form is not None \ |
|
and not hasattr(self, "isDesignerControl"): |
|
settingName = "%s.%s.%s" % (form.Name, self.Name, prop) |
|
app.setUserSetting(settingName, val) |
|
|
|
|
|
def _enableDoubleBuffering(self): |
|
for win in (self.GetGridWindow(), self.GetGridColLabelWindow()): |
|
if not win.IsDoubleBuffered(): |
|
win.SetDoubleBuffered(True) |
|
|
|
|
|
def _disableDoubleBuffering(self): |
|
for win in (self.GetGridWindow(), self.GetGridColLabelWindow()): |
|
if win.IsDoubleBuffered(): |
|
win.SetDoubleBuffered(False) |
|
|
|
|
|
##----------------------------------------------------------## |
|
## begin: dEvent callbacks for internal use ## |
|
##----------------------------------------------------------## |
|
def _onGridCellEdited(self, evt): |
|
## force cache to update after an edit: |
|
row, col = evt.EventData["row"], evt.EventData["col"] |
|
self.GetCellValue(row, col, useCache=False) |
|
|
|
|
|
def _onGridColSize(self, evt): |
|
"Occurs when the user resizes the width of the column." |
|
colNum = evt.EventData["col"] |
|
col = self.Columns[colNum] |
|
colName = "Column_%s" % col.DataField |
|
# Sync our column object up with what the grid is reporting, and because |
|
# the user made this change, save to the userSettings: |
|
col.Width = self.GetColSize(self._convertDaboColNumToWxColNum(colNum)) |
|
col._persist("Width") |
|
self._disableDoubleBuffering() |
|
self._enableDoubleBuffering() |
|
dabo.ui.callAfterInterval(20, self._updateColumnWidths) |
|
|
|
|
|
def _onGridHeaderMouseMove(self, evt): |
|
curMousePosition = evt.EventData["mousePosition"] |
|
headerIsDragging = self._headerDragging |
|
headerIsSizing = self._headerSizing |
|
dragging = evt.EventData["mouseDown"] and (curMousePosition != self._lastHeaderMousePosition) |
|
header = self._getWxHeader() |
|
|
|
if dragging: |
|
self._lastHeaderMousePosition = evt.EventData["mousePosition"] |
|
x,y = self._lastHeaderMousePosition |
|
|
|
if not headerIsSizing and (self.getColNumByX(x) == self.getColNumByX(x-5) == self.getColNumByX(x+5)): |
|
if not headerIsDragging: |
|
curCol = self.getColByX(x) |
|
if self.MovableColumns and curCol and curCol.Movable: |
|
# A header reposition is beginning |
|
self._headerDragging = True |
|
self._headerDragFrom = (x,y) |
|
else: |
|
# already dragging. |
|
begCol = self.getColNumByX(self._headerDragFrom[0]) |
|
curCol = self.getColNumByX(x) |
|
|
|
# The visual indicators (changing the mouse cursor) isn't currently |
|
# working. It would work without the evt.Skip() below, but that is |
|
# needed for when the column is resized. |
|
uic = dUICursors |
|
if begCol == curCol: |
|
# Give visual indication that a move is initiated |
|
header.SetCursor(uic.getStockCursor(uic.Cursor_Size_WE)) |
|
else: |
|
# Give visual indication that this is an acceptable drop target |
|
header.SetCursor(uic.getStockCursor(uic.Cursor_Bullseye)) |
|
else: |
|
# A size action is happening |
|
self._headerSizing = True |
|
|
|
|
|
def _onGridHeaderMouseLeftUp(self, evt): |
|
""" |
|
Occurs when the left mouse button is released in the grid header. |
|
|
|
Basically, this comes down to two possibilities: the end of a drag |
|
operation, or a single-click operation. If we were dragging, then |
|
it is possible a column needs to change position. If we were clicking, |
|
then it is a sort operation. |
|
""" |
|
x,y = evt.EventData["mousePosition"] |
|
if self._headerDragging: |
|
# A drag action is ending |
|
self._headerDragTo = (x,y) |
|
|
|
begCol = self.getColNumByX(self._headerDragFrom[0]) |
|
curCol = self.getColNumByX(x) |
|
|
|
if begCol != curCol: |
|
if curCol > begCol: |
|
curCol += 1 |
|
self.moveColumn(begCol, curCol) |
|
self._getWxHeader().SetCursor(self.defaultHdrCursor) |
|
elif self._headerSizing: |
|
pass |
|
else: |
|
# we weren't dragging, and the mouse was just released. |
|
# Find out the column we are in based on the x-coord, and |
|
# do a processSort() on that column. |
|
col = self.getColNumByX(x) |
|
self.processSort(col) |
|
self._headerDragging = False |
|
self._headerSizing = False |
|
## pkm: commented out the evt.Continue=False because it doesn't appear |
|
## to be needed, and it prevents the native UI from responding. |
|
#evt.Continue = False |
|
|
|
|
|
def _onGridHeaderMouseRightClick(self, evt): |
|
dabo.ui.callAfter(self._showHeaderContextMenu) |
|
|
|
def _showHeaderContextMenu(self): |
|
# Make the popup menu appear in the location that was clicked. We init |
|
# the menu here, then call the user hook method to optionally fill the |
|
# menu. If we get a menu back from the user hook, we display it. |
|
menu = dabo.ui.dMenu() |
|
|
|
# Fill the default menu item(s): |
|
def _autosizeColumn(evt): |
|
self.autoSizeCol(self.getColNumByX(self.getMousePosition()[0]), persist=True) |
|
def _autosizeAllColumns(evt): |
|
self.autoSizeCol("All") |
|
|
|
if self.ResizableColumns: |
|
menu.append(_("&Autosize Column"), OnHit=_autosizeColumn, |
|
help=_("Autosize the column based on the data in the column.")) |
|
menu.append(_("&Autosize All Columns"), OnHit=_autosizeAllColumns, |
|
help=_("Autosize all columns in the grid.")) |
|
|
|
menu = self.fillHeaderContextMenu(menu) |
|
|
|
if menu is not None and len(menu.Children) > 0: |
|
self.showContextMenu(menu) |
|
|
|
|
|
def _onGridMouseWheel(self, evt): |
|
## Override the default implementation which scrolls too slowly. |
|
evt.stop() |
|
lastWheelTime = getattr(self, "_lastWheelTime", 0) |
|
thisWheelTime = self._lastWheelTime = time.time() |
|
ui = evt._uiEvent |
|
mult = 1 |
|
if ui.GetWheelRotation() > 0: |
|
mult = -1 |
|
linesPerAction = ui.GetLinesPerAction() |
|
scrollAmt = mult * linesPerAction |
|
if thisWheelTime - lastWheelTime > .5: |
|
## Run the first wheel scroll to occur immediately: |
|
self._scrollLines(scrollAmt) |
|
return |
|
## Throttle subsequent rapid-fire wheel scrolls through callAfterInterval, |
|
## otherwise the events pile up resulting in poor performance. |
|
_accumulatedWheelScroll = getattr(self, "_accumulatedWheelScroll", None) |
|
if _accumulatedWheelScroll is None: |
|
dabo.ui.callAfterInterval(50, self._scrollAccumulatedLines) |
|
_accumulatedWheelScroll = 0 |
|
self._accumulatedWheelScroll = _accumulatedWheelScroll + scrollAmt |
|
self._wheelScrollLines = linesPerAction |
|
|
|
|
|
def _scrollAccumulatedLines(self): |
|
scrollAmt = self._accumulatedWheelScroll |
|
if sys.platform.startswith("win") and scrollAmt > self._wheelScrollLines: |
|
# I guess Windows doesn't receive as many wheel events per timeslice |
|
# as Gtk does. This attempts to compensate. |
|
scrollAmt *= (scrollAmt * .5) |
|
self._scrollLines(scrollAmt) |
|
self._accumulatedWheelScroll = None |
|
|
|
|
|
def _scrollLines(self, scrollAmt): |
|
## Without the Freeze/Thaw, performance sucks on Windows as it tries to do |
|
## it smoothly. |
|
self.Freeze() |
|
self.ScrollLines(scrollAmt) |
|
self.Thaw() |
|
|
|
|
|
def _onGridHeaderMouseRightUp(self, evt): |
|
"""Occurs when the right mouse button goes up in the grid header.""" |
|
pass |
|
_onGridHeaderContextMenu = _onGridHeaderMouseRightUp |
|
|
|
|
|
def _onGridMouseRightClick(self, evt): |
|
# Make the popup menu appear in the location that was clicked. We init |
|
# the menu here, then call the user hook method to optionally fill the |
|
# menu. If we get a menu back from the user hook, we display it. |
|
|
|
# First though, make the cell the user right-clicked on the current cell: |
|
if self.MultipleSelection: |
|
# Don't erase the multiple selection if the user clicks on a valid |
|
# row or column: |
|
if "row" in self.SelectionMode.lower() and evt.row not in self.Selection: |
|
self.CurrentRow = evt.row |
|
elif "col" in self.SelectionMode.lower() and evt.col not in self.Selection: |
|
self.CurrentCol = evt.col |
|
elif "cel" in self.SelectionMode.lower(): |
|
self.CurrentRow = evt.row |
|
self.CurrentCol = evt.col |
|
else: |
|
self.CurrentRow = evt.row |
|
self.CurrentColumn = evt.col |
|
|
|
menu = dabo.ui.dMenu() |
|
menu = self.fillContextMenu(menu) |
|
|
|
if menu is not None and len(menu.Children) > 0: |
|
self.showContextMenu(menu) |
|
_onContextMenu = _onGridMouseRightClick |
|
|
|
|
|
def _onGridHeaderMouseLeftDown(self, evt): |
|
# We need to eat this event, because the native wx grid will select all |
|
# rows in the column, which is a spreadsheet-like behavior, not a data- |
|
# aware grid-like behavior. However, let's keep our eyes out for a better |
|
# way to handle this, because eating events could cause some hard-to-debug |
|
# problems later (there could be other, more critical code, that isn't |
|
# being allowed to run). |
|
self._lastHeaderMousePosition = evt.EventData["mousePosition"] |
|
self._headerDragging = False |
|
self._headerSizing = False |
|
evt.Continue = False |
|
|
|
|
|
def _onGridMouseLeftClick(self, evt): |
|
self.ShowCellEditControl() |
|
|
|
|
|
def _onGridRowSize(self, evt): |
|
""" |
|
Occurs when the user sizes the height of the row. If the |
|
property 'SameSizeRows' is True, Dabo overrides the wxPython |
|
default and applies that size change to all rows, not just the row |
|
the user sized. |
|
""" |
|
row = evt.EventData["row"] |
|
if row is None or row < 0 or row > self.RowCount: |
|
# pkm: This has happened but I don't know why. Treat as spurious. |
|
return |
|
|
|
if self.SameSizeRows: |
|
try: |
|
self.RowHeight = self.GetRowSize(row) |
|
except wx._core.PyAssertionError: |
|
# pkm: I don't understand how it could have gotten this far, but |
|
# I got an error report that the c++ assertion row>=0 && row<m_numrows failed. |
|
pass |
|
|
|
|
|
def _onGridCellSelected(self, evt): |
|
"""Occurs when the grid's cell focus has changed.""" |
|
threshold = .2 |
|
last = getattr(self, "_lastCellSelectedTime", 0) |
|
cur = self._lastCellSelectedTime = time.time() |
|
#self._gridCellSelectedNewRowCol = (evt.EventData["row"], evt.EventData["col"]) |
|
if cur - last > threshold: |
|
# Update immediately: |
|
self._gridCellSelectedOldRow = self.CurrentRow |
|
self._updateCellSelection((evt.EventData["row"], evt.EventData["col"])) |
|
return |
|
# Let the grid scroll as fast as possible while rapid-fire keyboard navigation is |
|
# occurring, but <threshold> seconds later, sync up the bizobj and update the selection: |
|
if getattr(self, "_gridCellSelectedOldRow", None) is None: |
|
self._gridCellSelectedOldRow = self.CurrentRow |
|
dabo.ui.callAfterInterval(threshold*1000, self._updateCellSelection) |
|
|
|
|
|
def _updateCellSelection(self, newRowCol=None): |
|
if self._inUpdateSelection: |
|
return |
|
|
|
oldRow = self._gridCellSelectedOldRow |
|
self._gridCellSelectedOldRow = None |
|
if newRowCol is None: |
|
newRowCol = (self.CurrentRow, self.CurrentColumn) |
|
newRow = newRowCol[0] |
|
newCol = self._convertWxColNumToDaboColNum(newRowCol[1]) |
|
try: |
|
col = self.Columns[newCol] |
|
except (IndexError, TypeError): |
|
col = None |
|
|
|
if col and col.Editable and self.Editable: |
|
return ## segfault avoidance |
|
|
|
## pkm 2005-09-28: This works around a nasty segfault: |
|
self.HideCellEditControl() |
|
## but periodically test it. My current version: 2.6.1.1pre |
|
|
|
if col: |
|
## pkm 2005-09-28: Part of the editor segfault workaround. This sets the |
|
## editor for the entire column, at a point in time before |
|
## the grid is actually asking for the editor, and in a |
|
## fashion that ensures the editor instance doesn't go |
|
## out of scope prematurely. |
|
col._setEditor(newRow) |
|
|
|
if col and (self.Editable and col.Editable and not self._vetoAllEditing |
|
and self.ActivateEditorOnSelect): |
|
dabo.ui.callAfter(self.EnableCellEditControl) |
|
if oldRow != newRow: |
|
bizobj = self.getBizobj() |
|
if bizobj and not self._dataSourceBeingSet: |
|
# Don't run any of this code if this is the initial setting of the DataSource |
|
if bizobj.RowCount > newRow and bizobj.RowNumber != newRow: |
|
if self._mediateRowNumberThroughForm and isinstance(self.Form, dabo.ui.dForm): |
|
# run it through the form: |
|
if not self.Form.moveToRowNumber(newRow, bizobj): |
|
dabo.ui.callAfter(self.refresh) |
|
else: |
|
# run it through the bizobj directly: |
|
try: |
|
bizobj.RowNumber = newRow |
|
self.Form.update() |
|
except dException.BusinessRuleViolation, e: |
|
dabo.ui.stop(e) |
|
dabo.ui.callAfter(self.refresh) |
|
else: |
|
# We are probably trying to select row 0 when there are no records |
|
# in the bizobj. |
|
##pkm: the following call causes an assertion on Mac, and appears to be |
|
## unneccesary. |
|
#self.SetGridCursor(0,0) |
|
pass |
|
self._dataSourceBeingSet = False |
|
dabo.ui.callAfterInterval(50, self._updateSelection) |
|
|
|
|
|
def _updateSelection(self): |
|
if self._inUpdateSelection or self.SelectionMode =="Cell": |
|
return |
|
self._inUpdateSelection = True |
|
self.Freeze() |
|
self.ClearSelection() |
|
fnc = {"Row": self.SelectRow, "Col": self.SelectCol}[self.SelectionMode] |
|
for num in self.Selection: |
|
fnc(num, True) |
|
self.Thaw() |
|
self._inUpdateSelection = False |
|
|
|
|
|
def _checkSelectionType(self): |
|
""" |
|
When the SelectionMode or MultipleSelection properties change, |
|
we want to make sure that the selection reflects those settings. |
|
""" |
|
mode = self.SelectionMode |
|
if mode == "Row": |
|
self.SelectRow(self.CurrentRow) |
|
elif mode == "Col": |
|
self.SelectCol(self.CurrentColumn) |
|
else: |
|
self.SelectBlock(self.CurrentRow, self.CurrentColumn, |
|
self.CurrentRow, self.CurrentColumn) |
|
self.refresh() |
|
|
|
|
|
def _onKeyDown(self, evt): |
|
keycode = evt.EventData["keyCode"] |
|
|
|
if keycode == 27: |
|
# esc pressed. Grid will eat it by default. But if we are in a dialog with |
|
# a cancel button, let's runCancel() since that's what the user likely wants: |
|
if hasattr(self.Form, "runCancel"): |
|
self.Form.runCancel() |
|
if keycode == 9 and self.TabNavigates: |
|
evt.stop() |
|
self.Navigate(not evt.EventData["shiftDown"]) |
|
|
|
def _onKeyChar(self, evt): |
|
"""Occurs when the user presses a key inside the grid.""" |
|
columns = self.Columns |
|
current_col = self.CurrentColumn |
|
if not columns or (self.Editable and columns[current_col].Editable |
|
and not self._vetoAllEditing): |
|
# Can't search and edit at the same time |
|
return |
|
|
|
keyCode = evt.EventData["unicodeKey"] |
|
try: |
|
char = unichr(keyCode) |
|
except ValueError: |
|
# keycode not in ascii range |
|
return |
|
|
|
if keyCode in (dKeys.key_Left, dKeys.key_Right, |
|
dKeys.key_Up, dKeys.key_Down, dKeys.key_Pageup, dKeys.key_Pagedown, |
|
dKeys.key_Home, dKeys.key_End, dKeys.key_Prior, dKeys.key_Next) \ |
|
or evt.EventData["hasModifiers"]: |
|
# Enter, Tab, and Arrow Keys shouldn't be searched on. |
|
return |
|
|
|
if (self.Searchable and columns[current_col].Searchable): |
|
self.addToSearchStr(char) |
|
# For some reason, without this the key happens twice |
|
evt.stop() |
|
|
|
##----------------------------------------------------------## |
|
## end: dEvent callbacks for internal use ## |
|
##----------------------------------------------------------## |
|
|
|
|
|
def _calcRanges(self, seq, rowOrCol): |
|
startPoints = [] |
|
nextVal = -1 |
|
maxIdx = len(seq)-1 |
|
for idx,pt in enumerate(seq): |
|
if idx == 0: |
|
startPoints.append(pt) |
|
nextVal = pt+1 |
|
else: |
|
if pt == nextVal: |
|
nextVal += 1 |
|
else: |
|
startPoints.append(pt) |
|
nextVal = pt+1 |
|
|
|
endPoints = [] |
|
for pt in startPoints: |
|
idx = seq.index(pt) |
|
if idx == maxIdx: |
|
endPoints.append(pt) |
|
else: |
|
found = False |
|
while idx < maxIdx: |
|
if seq[idx+1] == pt + 1: |
|
idx += 1 |
|
pt += 1 |
|
else: |
|
endPoints.append(pt) |
|
found = True |
|
break |
|
if not found: |
|
endPoints.append(pt) |
|
|
|
typ = rowOrCol.lower()[:3] |
|
if typ == "row": |
|
cols = self.ColumnCount |
|
rangeStart = [(r, 0) for r in startPoints] |
|
rangeEnd = [(r, cols) for r in endPoints] |
|
elif typ == "col": |
|
rows = self.RowCount |
|
rangeStart = [(0, c) for c in startPoints] |
|
rangeEnd = [(rows, c) for c in endPoints] |
|
return zip(rangeStart, rangeEnd) |
|
|
|
|
|
##----------------------------------------------------------## |
|
## begin: wx callbacks to re-route to dEvents ## |
|
##----------------------------------------------------------## |
|
|
|
## dGrid has to reimplement all of this to augment what dPemMixin does, |
|
## to offer separate events in the grid versus the header region. |
|
def __onWxContextMenu(self, evt): |
|
self.raiseEvent(dEvents.GridContextMenu, evt) |
|
evt.Skip() |
|
|
|
|
|
def __onWxGridColSize(self, evt): |
|
daboCol = self._convertWxColNumToDaboColNum(evt.GetRowOrCol()) |
|
colObj = self.Columns[daboCol] |
|
if self.ResizableColumns and colObj.Resizable: |
|
self.raiseEvent(dEvents.GridColSize, col=daboCol) |
|
else: |
|
# need to reference the Width property for some reason: |
|
colObj.Width |
|
evt.Veto() |
|
self._refreshHeader() |
|
|
|
|
|
def __onWxGridSelectCell(self, evt): |
|
if getattr(self, "_inSelect", False) or getattr(self, "_inUpdateSelection", False): |
|
# Avoid recursion |
|
return |
|
if self.ColumnCount == 0: |
|
# Grid is not fully constructed yet |
|
return |
|
col = self.Columns[evt.GetCol()] |
|
if col.Editable and col.RendererClass == col.boolRendererClass: |
|
# user is clicking on a checkbox |
|
wx.CallAfter(self.EnableCellEditControl) |
|
self._inSelect = True |
|
if evt.Selecting(): |
|
self._updateWxSelection(evt) |
|
self.raiseEvent(dEvents.GridCellSelected, evt) |
|
self._lastRow, self._lastCol = evt.GetRow(), evt.GetCol() |
|
if not sys.platform.startswith("win"): |
|
evt.Skip() |
|
self._inSelect = False |
|
|
|
|
|
def __onWxGridRangeSelect(self, evt): |
|
if self._inRangeSelect: |
|
# avoid recursive events |
|
return |
|
self._inRangeSelect = True |
|
if evt.Selecting(): |
|
self._updateWxSelection(evt) |
|
self.raiseEvent(dEvents.GridRangeSelected, evt) |
|
evt.Skip() |
|
self._inRangeSelect = False |
|
|
|
|
|
def __onWxScrollWin(self, evt): |
|
evtClass = dabo.ui.getScrollWinEventClass(evt) |
|
self.raiseEvent(evtClass, evt) |
|
evt.Skip() |
|
|
|
|
|
def _updateWxSelection(self, evt): |
|
if self.MultipleSelection: |
|
# Nothing to do |
|
return |
|
origRow, origCol = self.CurrentRow, self.CurrentColumn |
|
mode = self.GetSelectionMode() |
|
try: |
|
top, bott = evt.GetTopRow(), evt.GetBottomRow() |
|
except AttributeError: |
|
top = bott = evt.GetRow() |
|
try: |
|
left, right = evt.GetLeftCol(), evt.GetRightCol() |
|
except AttributeError: |
|
left = right = evt.GetCol() |
|
if mode == wx.grid.Grid.wxGridSelectRows: |
|
if (top != bott) or (top != origCol): |
|
# Attempting to select a range |
|
if top == origRow: |
|
row = bott |
|
else: |
|
row = top |
|
if self._lastCol is not None: |
|
self.SetGridCursor(row, self._lastCol) |
|
self.SelectRow(row) |
|
elif mode == wx.grid.Grid.wxGridSelectColumns: |
|
if (left != right) or (left != origCol): |
|
# Attempting to select a range |
|
if left == origCol: |
|
col = right |
|
else: |
|
col = left |
|
self.SetGridCursor(self._lastRow, col) |
|
self.SelectCol(col) |
|
else: |
|
# Cells |
|
chg = False |
|
row, col = origRow, origCol |
|
if top != bott: |
|
chg = True |
|
if top == origRow: |
|
row = bott |
|
else: |
|
row = top |
|
elif top != origRow: |
|
# New row |
|
chg = True |
|
row = top |
|
if left != right: |
|
chg = True |
|
if left == origCol: |
|
col = right |
|
else: |
|
col = left |
|
elif left != origCol: |
|
# New col |
|
chg = True |
|
col = left |
|
if chg: |
|
self.SetGridCursor(row, col) |
|
self.SelectBlock(row, col, row, col) |
|
|
|
|
|
def __onWxGridEditorShown(self, evt): |
|
self.raiseEvent(dEvents.GridCellEditBegin, evt) |
|
evt.Skip() |
|
|
|
|
|
def __onWxGridEditorHidden(self, evt): |
|
self.raiseEvent(dEvents.GridCellEditEnd, evt) |
|
evt.Skip() |
|
|
|
|
|
def _toggleCheckBox(self): |
|
ed = getattr(self, "_activeEditorControl", None) |
|
if ed: |
|
ed.SetValue(not ed.GetValue()) |
|
self._checkBoxToggled(ed) |
|
|
|
|
|
def _checkBoxToggled(self, obj): |
|
# Force the flushing of the value immediately, instead of waiting for the |
|
# editor to lose focus (where the flush will happen a second time). |
|
self._Table.SetValue(self.CurrentRow, self.CurrentColumn, obj.GetValue()) |
|
self.raiseEvent(dEvents.GridCellEditorHit) |
|
|
|
|
|
def __onGridCellLeftClick_toggleCB(self, evt): |
|
col = self.Columns[evt.GetCol()] |
|
if col.RendererClass == col.boolRendererClass: |
|
dabo.ui.callAfterInterval(100, self._toggleCheckBox) |
|
evt.Skip() |
|
|
|
|
|
def __onWxGridEditorCreated(self, evt): |
|
"""Bind the kill focus event to the newly instantiated cell editor """ |
|
editor = evt.GetControl() |
|
editor.Bind(wx.EVT_KILL_FOCUS, self.__onWxGridCellEditorKillFocus) |
|
|
|
col = self.Columns[evt.GetCol()] |
|
if col.RendererClass == col.boolRendererClass: |
|
def onKeyDown(evt): |
|
if evt.KeyCode == wx.WXK_UP: |
|
if self.GetGridCursorRow() > 0: |
|
self.DisableCellEditControl() |
|
self.MoveCursorUp(False) |
|
elif evt.KeyCode == wx.WXK_DOWN: |
|
if self.GetGridCursorRow() < (self.GetNumberRows() - 1): |
|
self.DisableCellEditControl() |
|
self.MoveCursorDown(False) |
|
elif evt.KeyCode == wx.WXK_LEFT: |
|
if self.GetGridCursorCol() > 0: |
|
self.DisableCellEditControl() |
|
self.MoveCursorLeft(False) |
|
elif evt.KeyCode == wx.WXK_RIGHT: |
|
if self.GetGridCursorCol() < (self.GetNumberCols() - 1): |
|
self.DisableCellEditControl() |
|
self.MoveCursorRight(False) |
|
else: |
|
evt.Skip() |
|
|
|
def onHit(evt): |
|
self._checkBoxToggled(editor) |
|
|
|
ed = self._activeEditorControl = evt.GetControl() |
|
style = ed.GetWindowStyle() |
|
style |= wx.WANTS_CHARS |
|
ed.SetWindowStyle(style) |
|
ed.Bind(wx.EVT_KEY_DOWN, onKeyDown) |
|
ed.Bind(wx.EVT_CHECKBOX, onHit) |
|
evt.Skip() |
|
|
|
|
|
def __onWxGridCellEditorKillFocus(self, evt): |
|
# Cell editor's grandparent, the grid GridWindow's parent, is the grid. |
|
self.SaveEditControlValue() |
|
self.HideCellEditControl() |
|
evt.Skip() |
|
|
|
|
|
def __onWxGridCellChange(self, evt): |
|
self.raiseEvent(dEvents.GridCellEdited, evt) |
|
evt.Skip() |
|
|
|
|
|
def __onWxGridRowSize(self, evt): |
|
self.raiseEvent(dEvents.GridRowSize, evt) |
|
evt.Skip() |
|
|
|
|
|
def __onWxHeaderContextMenu(self, evt): |
|
col, row = self._getColRowForPosition(evt.GetPosition()) |
|
self.raiseEvent(dEvents.GridHeaderContextMenu, evt, col=col) |
|
evt.Skip() |
|
|
|
|
|
def __onWxHeaderIdle(self, evt): |
|
self.raiseEvent(dEvents.GridHeaderIdle, evt) |
|
evt.Skip() |
|
|
|
|
|
def __onWxHeaderMouseEnter(self, evt): |
|
col, row = self._getColRowForPosition(evt.GetPosition()) |
|
self.raiseEvent(dEvents.GridHeaderMouseEnter, evt, col=col) |
|
evt.Skip() |
|
|
|
|
|
def __onWxHeaderMouseLeave(self, evt): |
|
col, row = self._getColRowForPosition(evt.GetPosition()) |
|
self._headerMouseLeftDown, self._headerMouseRightDown = False, False |
|
self.raiseEvent(dEvents.GridHeaderMouseLeave, evt, col=col) |
|
evt.Skip() |
|
|
|
|
|
def __onWxHeaderMouseLeftDoubleClick(self, evt): |
|
col, row = self._getColRowForPosition(evt.GetPosition()) |
|
self.raiseEvent(dEvents.GridHeaderMouseLeftDoubleClick, evt, col=col) |
|
evt.Skip() |
|
|
|
|
|
def __onWxHeaderMouseLeftDown(self, evt): |
|
col, row = self._getColRowForPosition(evt.GetPosition()) |
|
self.raiseEvent(dEvents.GridHeaderMouseLeftDown, evt, col=col) |
|
self._headerMouseLeftDown = True |
|
#evt.Skip() #- don't skip or all the rows will be selected. |
|
|
|
|
|
def __onWxHeaderMouseLeftUp(self, evt): |
|
dabo.ui.callAfter(self._enableDoubleBuffering) |
|
col, row = self._getColRowForPosition(evt.GetPosition()) |
|
self.raiseEvent(dEvents.GridHeaderMouseLeftUp, evt, col=col) |
|
if self._headerMouseLeftDown: |
|
# mouse went down and up in the header: send a click: |
|
self.raiseEvent(dEvents.GridHeaderMouseLeftClick, evt, col=col) |
|
self._headerMouseLeftDown = False |
|
evt.Skip() |
|
|
|
|
|
def __onWxHeaderMouseMotion(self, evt): |
|
if dabo.ui.isMouseLeftDown(): |
|
self._disableDoubleBuffering() |
|
col, row = self._getColRowForPosition(evt.GetPosition()) |
|
self.raiseEvent(dEvents.GridHeaderMouseMove, evt, col=col) |
|
evt.Skip() |
|
|
|
|
|
def __onWxHeaderMouseRightDown(self, evt): |
|
col, row = self._getColRowForPosition(evt.GetPosition()) |
|
self.raiseEvent(dEvents.GridHeaderMouseRightDown, evt, col=col) |
|
self._headerMouseRightDown = True |
|
evt.Skip() |
|
|
|
|
|
def __onWxHeaderMouseRightUp(self, evt): |
|
col, row = self._getColRowForPosition(evt.GetPosition()) |
|
self.raiseEvent(dEvents.GridHeaderMouseRightUp, evt, col=col) |
|
if self._headerMouseRightDown: |
|
# mouse went down and up in the header: send a click: |
|
self.raiseEvent(dEvents.GridHeaderMouseRightClick, evt) |
|
self._headerMouseRightDown = False |
|
evt.Skip() |
|
|
|
|
|
def __onWxHeaderPaint(self, evt): |
|
updateBox = self._getWxHeader().GetUpdateRegion().GetBox() |
|
self._paintHeader(updateBox) |
|
|
|
|
|
def _getColRowForPosition(self, pos): |
|
"""Used in the mouse event handlers to stuff the col, row into EventData.""" |
|
col = self.getColNumByX(pos[0]) |
|
row = self.getRowNumByY(pos[1]) |
|
if col < 0 or row < 0: |
|
# click was outside grid cell area |
|
col, row = None, None |
|
return col, row |
|
|
|
|
|
def __onWxMouseLeftDoubleClick(self, evt): |
|
col, row = self._getColRowForPosition(evt.GetPosition()) |
|
self.raiseEvent(dEvents.GridMouseLeftDoubleClick, evt, col=col, row=row) |
|
evt.Skip() |
|
|
|
|
|
def __onWxMouseLeftDown(self, evt): |
|
col, row = self._getColRowForPosition(evt.GetPosition()) |
|
self.raiseEvent(dEvents.GridMouseLeftDown, evt, col=col, row=row) |
|
self._mouseLeftDown = (col, row) |
|
evt.Skip() |
|
|
|
|
|
def __onWxMouseLeftUp(self, evt): |
|
dabo.ui.callAfter(self._enableDoubleBuffering) |
|
col, row = self._getColRowForPosition(evt.GetPosition()) |
|
self.raiseEvent(dEvents.GridMouseLeftUp, evt, col=col, row=row) |
|
if getattr(self, "_mouseLeftDown", (None, None)) == (col, row): |
|
# mouse went down and up in this cell: send a click: |
|
self.raiseEvent(dEvents.GridMouseLeftClick, evt, col=col, row=row) |
|
self._mouseLeftDown = (None, None) |
|
evt.Skip() |
|
|
|
|
|
def __onWxMouseMotion(self, evt): |
|
if dabo.ui.isMouseLeftDown(): |
|
self._disableDoubleBuffering() |
|
col, row = self._getColRowForPosition(evt.GetPosition()) |
|
self.raiseEvent(dEvents.GridMouseMove, evt, col=col, row=row) |
|
evt.Skip() |
|
|
|
|
|
def __onWxMouseRightDown(self, evt): |
|
col, row = self._getColRowForPosition(evt.GetPosition()) |
|
self.raiseEvent(dEvents.GridMouseRightDown, evt, col=col, row=row) |
|
self._mouseRightDown = (col, row) |
|
evt.Skip() |
|
|
|
|
|
def __onWxMouseRightUp(self, evt): |
|
col, row = self._getColRowForPosition(evt.GetPosition()) |
|
self.raiseEvent(dEvents.GridMouseRightUp, evt, col=col, row=row) |
|
if getattr(self, "_mouseRightDown", (None, None)) == (col, row): |
|
# mouse went down and up in this cell: send a click: |
|
self.raiseEvent(dEvents.GridMouseRightClick, evt, col=col, row=row) |
|
self._mouseRightDown = (None, None) |
|
evt.Skip() |
|
|
|
##----------------------------------------------------------## |
|
## end: wx callbacks to re-route to dEvents ## |
|
##----------------------------------------------------------## |
|
|
|
|
|
|
|
##----------------------------------------------------------## |
|
## begin: property definitions ## |
|
##----------------------------------------------------------## |
|
def _getActivateEditorOnSelect(self): |
|
try: |
|
v = self._activateEditorOnSelect |
|
except AttributeError: |
|
v = self._activateEditorOnSelect = True |
|
return v |
|
|
|
def _setActivateEditorOnSelect(self, val): |
|
self._activateEditorOnSelect = bool(val) |
|
|
|
|
|
def _getAlternateRowColoring(self): |
|
return self._alternateRowColoring |
|
|
|
def _setAlternateRowColoring(self, val): |
|
if self._constructed(): |
|
self._alternateRowColoring = val |
|
self.setTableAttributes(self._Table) |
|
self.Refresh() |
|
else: |
|
self._properties["AlternateRowColoring"] = val |
|
|
|
|
|
def _getAutoAdjustHeaderHeight(self): |
|
return self._autoAdjustHeaderHeight |
|
|
|
def _setAutoAdjustHeaderHeight(self, val): |
|
if self._constructed(): |
|
self._autoAdjustHeaderHeight = val |
|
self.refresh() |
|
if val: |
|
self.fitHeaderHeight() |
|
else: |
|
self._properties["AutoAdjustHeaderHeight"] = val |
|
|
|
|
|
def _getCellHighlightWidth(self): |
|
return self.GetCellHighlightPenWidth() |
|
|
|
def _setCellHighlightWidth(self, val): |
|
if self._constructed(): |
|
self.SetCellHighlightPenWidth(val) |
|
self.SetCellHighlightROPenWidth(val) |
|
else: |
|
self._properties["CellHighlightWidth"] = val |
|
|
|
|
|
def _getColumns(self): |
|
return self._columns |
|
|
|
|
|
def _getColumnClass(self): |
|
return self._columnClass |
|
|
|
|
|
def _setColumnClass(self, val): |
|
self._columnClass = val |
|
|
|
|
|
def _getColumnCount(self): |
|
return len(self.Columns) |
|
|
|
def _setColumnCount(self, val): |
|
if self._constructed(): |
|
if val > -1: |
|
colChange = val - self.ColumnCount |
|
if colChange == 0: |
|
# No change |
|
return |
|
elif colChange < 0: |
|
while self.ColumnCount > val: |
|
self.Columns.remove(self.Columns[-1]) |
|
else: |
|
for cc in range(colChange): |
|
self.addColumn(inBatch=True) |
|
self._syncColumnCount() |
|
self.fillGrid(True) |
|
else: |
|
self._properties["ColumnCount"] = val |
|
|
|
|
|
def _getCurrCellVal(self): |
|
return self.GetValue(self.GetGridCursorRow(), self.GetGridCursorCol()) |
|
|
|
def _setCurrCellVal(self, val): |
|
self.SetValue(self.GetGridCursorRow(), self.GetGridCursorCol(), val) |
|
self.refresh() |
|
|
|
|
|
def _getCurrentColumn(self): |
|
return self.GetGridCursorCol() |
|
|
|
def _setCurrentColumn(self, val): |
|
if self._constructed(): |
|
if val > -1: |
|
val = min(val, self.ColumnCount) |
|
rn = self.CurrentRow |
|
self.SetGridCursor(rn, val) |
|
self.MakeCellVisible(rn, val) |
|
else: |
|
self._properties["CurrentColumn"] = val |
|
|
|
|
|
def _getCurrentField(self): |
|
return self.Columns[self.GetGridCursorCol()].DataField |
|
|
|
def _setCurrentField(self, val): |
|
if self._constructed(): |
|
for ii in range(len(self.Columns)): |
|
if self.Columns[ii].DataField == val: |
|
self.CurrentColumn = ii |
|
break |
|
else: |
|
self._properties["CurrentField"] = val |
|
|
|
|
|
def _getCurrentRow(self): |
|
return self.GetGridCursorRow() |
|
|
|
def _setCurrentRow(self, val): |
|
if self._constructed(): |
|
curr = self.GetGridCursorRow() |
|
if val >= self.RowCount: |
|
val = self.RowCount - 1 |
|
if val < 0: |
|
val = 0 |
|
cn = self.CurrentColumn |
|
if curr != val: |
|
# The row is being changed |
|
val = max(0, val) |
|
cn = max(0, cn) |
|
self.SetGridCursor(val, cn) |
|
self.MakeCellVisible(val, cn) |
|
else: |
|
self._properties["CurrentRow"] = val |
|
|
|
|
|
def _getDataSet(self): |
|
if self.DataSource is not None: |
|
ret = None |
|
bo = self.getBizobj() |
|
try: |
|
ret = bo.getDataSet() |
|
except AttributeError: |
|
# See if the DataSource is a reference |
|
try: |
|
ret = eval(self.DataSource) |
|
except StandardError: |
|
# If it fails for any reason, bail. |
|
pass |
|
self._dataSet = ret |
|
else: |
|
try: |
|
ret = self._dataSet |
|
except AttributeError: |
|
ret = self._dataSet = None |
|
return ret |
|
|
|
def _setDataSet(self, val): |
|
if self._constructed(): |
|
if (self.DataSource is not None) and not hasattr(self, "isDesignerControl"): |
|
raise ValueError("Cannot set DataSet: DataSource defined.") |
|
# We must make sure the grid's table is initialized first: |
|
self._Table |
|
if not isinstance(val, dabo.db.dDataSet): |
|
val = dabo.db.dDataSet(val) |
|
self._dataSet = val |
|
self.fillGrid() |
|
self._syncAll() |
|
if not self._settingDataSetFromSort: |
|
# Force the grid to maintain its current sort order |
|
self._restoreSort() |
|
dabo.ui.callAfter(self.refresh) |
|
else: |
|
self._properties["DataSet"] = val |
|
|
|
|
|
def _getDataSource(self): |
|
try: |
|
v = self._dataSource |
|
except AttributeError: |
|
v = self._dataSource = None |
|
return v |
|
|
|
def _setDataSource(self, val): |
|
if self._constructed(): |
|
# We must make sure the grid's table is initialized first: |
|
self._Table |
|
self._dataSet = None |
|
self._dataSource = val |
|
self.fillGrid(True) |
|
biz = self.getBizobj() |
|
if self.USE_DATASOURCE_BEING_SET_HACK: |
|
self._dataSourceBeingSet = True |
|
if biz: |
|
dabo.ui.setAfter(self, "CurrentRow", biz.RowNumber) |
|
else: |
|
self._properties["DataSource"] = val |
|
|
|
|
|
def _getEditable(self): |
|
return self.IsEditable() |
|
|
|
def _setEditable(self, val): |
|
if self._constructed(): |
|
self.EnableEditing(val) |
|
else: |
|
self._properties["Editable"] = val |
|
|
|
|
|
def _getEncoding(self): |
|
try: |
|
ret = self.getBizobj().Encoding |
|
except AttributeError: |
|
ret = dabo.getEncoding() |
|
return ret |
|
|
|
|
|
def _getHeaderBackColor(self): |
|
return self._headerBackColor |
|
|
|
def _setHeaderBackColor(self, val): |
|
if self._constructed(): |
|
if isinstance(val, basestring): |
|
val = dColors.colorTupleFromName(val) |
|
self._headerBackColor = val |
|
self.refresh() |
|
else: |
|
self._properties["HeaderBackColor"] = val |
|
|
|
|
|
def _getHeaderForeColor(self): |
|
return self._headerForeColor |
|
|
|
|
|
def _setHeaderForeColor(self, val): |
|
if self._constructed(): |
|
if isinstance(val, basestring): |
|
val = dColors.colorTupleFromName(val) |
|
self._headerForeColor = val |
|
self.refresh() |
|
else: |
|
self._properties["HeaderForeColor"] = val |
|
|
|
|
|
def _getHeaderHeight(self): |
|
return self.GetColLabelSize() |
|
|
|
def _setHeaderHeight(self, val): |
|
if self._constructed(): |
|
if val <= 0: |
|
self._lastPositiveHeaderHeight = self.GetColLabelSize() |
|
self.SetColLabelSize(val) |
|
else: |
|
self._properties["HeaderHeight"] = val |
|
|
|
|
|
def _getHeaderHorizontalAlignment(self): |
|
return self._headerHorizontalAlignment |
|
|
|
def _setHeaderHorizontalAlignment(self, val): |
|
if self._constructed(): |
|
v = self._expandPropStringValue(val, ("Left", "Right", "Center")) |
|
self._headerHorizontalAlignment = v |
|
self.refresh() |
|
else: |
|
self._properties["HeaderHorizontalAlignment"] = val |
|
|
|
|
|
def _getHeaderVerticalAlignment(self): |
|
return self._headerVerticalAlignment |
|
|
|
def _setHeaderVerticalAlignment(self, val): |
|
if self._constructed(): |
|
v = self._expandPropStringValue(val, ("Top", "Bottom", "Center")) |
|
self._headerVerticalAlignment = v |
|
self.refresh() |
|
else: |
|
self._properties["HeaderVerticalAlignment"] = val |
|
|
|
|
|
def _getHorizontalScrolling(self): |
|
return self.GetScrollPixelsPerUnit()[0] > 0 |
|
|
|
def _setHorizontalScrolling(self, val): |
|
if self._constructed(): |
|
if val: |
|
self.SetScrollRate(20, self.GetScrollPixelsPerUnit()[1]) |
|
else: |
|
self.SetScrollRate(0, self.GetScrollPixelsPerUnit()[1]) |
|
self.refresh() |
|
else: |
|
self._properties["HorizontalScrolling"] = val |
|
|
|
|
|
def _getMovableColumns(self): |
|
return self._movableColumns |
|
|
|
def _setMovableColumns(self, val): |
|
self._movableColumns = val |
|
|
|
|
|
def _getMultipleSelection(self): |
|
return self._multipleSelection |
|
|
|
def _setMultipleSelection(self, val): |
|
if self._constructed(): |
|
if val != self._multipleSelection: |
|
self._multipleSelection = val |
|
self._checkSelectionType() |
|
else: |
|
self._properties["MultipleSelection"] = val |
|
|
|
|
|
def _getNoneDisplay(self): |
|
return self._noneDisplay |
|
|
|
def _setNoneDisplay(self, val): |
|
if val is None: |
|
self._noneDisplay = self.__noneDisplayDefault |
|
else: |
|
assert isinstance(val, basestring) |
|
self._noneDisplay = val |
|
|
|
|
|
def _getResizableColumns(self): |
|
return self._resizableColumns |
|
|
|
def _setResizableColumns(self, val): |
|
self._resizableColumns = val |
|
|
|
|
|
def _getResizableRows(self): |
|
return self._resizableRows |
|
|
|
def _setResizableRows(self, val): |
|
if self._constructed(): |
|
self._resizableRows = val |
|
if val: |
|
self.EnableDragRowSize() |
|
else: |
|
self.DisableDragRowSize() |
|
else: |
|
self._properties["ResizableRows"] = val |
|
|
|
|
|
def _getRowColorEven(self): |
|
return self._rowColorEven |
|
|
|
def _setRowColorEven(self, val): |
|
self._rowColorEven = val |
|
self.setTableAttributes(self._Table) |
|
|
|
|
|
def _getRowColorOdd(self): |
|
return self._rowColorOdd |
|
|
|
def _setRowColorOdd(self, val): |
|
self._rowColorOdd = val |
|
self.setTableAttributes(self._Table) |
|
|
|
|
|
def _getRowCount(self): |
|
try: |
|
self._tableRows = self.getBizobj().RowCount |
|
except AttributeError: |
|
pass |
|
return self._tableRows |
|
|
|
|
|
def _getRowHeight(self): |
|
try: |
|
v = self._rowHeight |
|
except AttributeError: |
|
v = self._rowHeight = self.GetDefaultRowSize() |
|
return v |
|
|
|
def _setRowHeight(self, val): |
|
if self._constructed(): |
|
try: |
|
rh = self._rowHeight |
|
except AttributeError: |
|
rh = self._rowHeight = self.GetDefaultRowSize() |
|
if val != rh: |
|
self._rowHeight = val |
|
self.SetDefaultRowSize(val, True) |
|
self.ForceRefresh() |
|
# Persist the new size: |
|
self._setUserSetting("RowSize", val) |
|
else: |
|
self._properties["RowHeight"] = val |
|
|
|
|
|
def _getRowLabels(self): |
|
return self._rowLabels |
|
|
|
def _setRowLabels(self, val): |
|
self._rowLabels = val |
|
self.fillGrid() |
|
|
|
|
|
def _getRowLabelWidth(self): |
|
try: |
|
v = self._rowLabelWidth |
|
except AttributeError: |
|
v = self._rowLabelWidth = self.GetDefaultRowLabelSize() |
|
return v |
|
|
|
def _setRowLabelWidth(self, val): |
|
if self._constructed(): |
|
self._rowLabelWidth = val |
|
if self.ShowRowLabels: |
|
self.SetRowLabelSize(val) |
|
else: |
|
self._properties["RowLabelWidth"] = val |
|
|
|
|
|
def _getSameSizeRows(self): |
|
return self._sameSizeRows |
|
|
|
def _setSameSizeRows(self, val): |
|
self._sameSizeRows = bool(val) |
|
|
|
|
|
def _getSaveRestoreDataSet(self): |
|
return getattr(self, "_saveRestoreDataSet", False) |
|
|
|
def _setSaveRestoreDataSet(self, val): |
|
self._saveRestoreDataSet = bool(val) |
|
|
|
|
|
def _getSearchable(self): |
|
return self._searchable |
|
|
|
def _setSearchable(self, val): |
|
self._searchable = bool(val) |
|
|
|
|
|
def _getSearchDelay(self): |
|
return self._searchDelay |
|
|
|
def _setSearchDelay(self, val): |
|
self._searchDelay = val |
|
|
|
|
|
def _getSelection(self): |
|
ret = [] |
|
sm = self._selectionMode |
|
tl = self.GetSelectionBlockTopLeft() |
|
br = self.GetSelectionBlockBottomRight() |
|
cols = self.GetSelectedCols() |
|
rows = self.GetSelectedRows() |
|
cells = self.GetSelectedCells() |
|
|
|
if sm == "Row": |
|
ret = rows |
|
# See if anything is returned by the block functions |
|
if tl and br: |
|
for tlz, brz in zip(tl, br): |
|
r1 = tlz[0] |
|
r2 = brz[0] |
|
ret += range(r1, r2+1) |
|
if not ret: |
|
# Only a single cell selected |
|
ret = [self.GetGridCursorRow()] |
|
|
|
elif sm == "Col": |
|
ret = cols |
|
# See if anything is returned by the block functions |
|
if tl and br: |
|
for tlz, brz in zip(tl, br): |
|
c1 = tlz[1] |
|
c2 = brz[1] |
|
ret += range(c1, c2+1) |
|
if not ret: |
|
# Only a single cell selected |
|
ret = [self.GetGridCursorCol()] |
|
|
|
else: |
|
# Cell selection mode |
|
if tl and br: |
|
ret = zip(tl, br) |
|
# Add any selected rows |
|
if rows: |
|
ret += self._calcRanges(rows, "Rows") |
|
# Add any selected columns |
|
if cols: |
|
ret += self._calcRanges(cols, "Cols") |
|
# Add any selected cells |
|
if cells: |
|
ret += [(val, val) for val in cells] |
|
|
|
if not ret: |
|
cell = (self.GetGridCursorRow(), self.GetGridCursorCol()) |
|
ret = [(cell, cell)] |
|
ret.sort() |
|
return ret |
|
|
|
|
|
def _getSelectionBackColor(self): |
|
return self._selectionBackColor |
|
|
|
def _setSelectionBackColor(self, val): |
|
if self._constructed(): |
|
self._selectionBackColor = val |
|
if isinstance(val, basestring): |
|
val = dColors.colorTupleFromName(val) |
|
self.SetSelectionBackground(val) |
|
else: |
|
self._properties["SelectionBackColor"] = val |
|
|
|
|
|
def _getSelectionForeColor(self): |
|
return self._selectionForeColor |
|
|
|
def _setSelectionForeColor(self, val): |
|
if self._constructed(): |
|
self._selectionForeColor = val |
|
if isinstance(val, basestring): |
|
val = dColors.colorTupleFromName(val) |
|
self.SetSelectionForeground(val) |
|
else: |
|
self._properties["SelectionForeColor"] = val |
|
|
|
|
|
def _getSelectionMode(self): |
|
return self._selectionMode |
|
|
|
def _setSelectionMode(self, val): |
|
if self._constructed(): |
|
orig = self._selectionMode |
|
val2 = val.lower().strip()[:2] |
|
if val2 == "ro": |
|
try: |
|
self.SetSelectionMode(wx.grid.Grid.wxGridSelectRows) |
|
self._selectionMode = "Row" |
|
except wx.PyAssertionError: |
|
dabo.ui.callAfter(self._setSelectionMode, val) |
|
elif val2 == "co": |
|
try: |
|
self.SetSelectionMode(wx.grid.Grid.wxGridSelectColumns) |
|
self._selectionMode = "Col" |
|
except wx.PyAssertionError: |
|
dabo.ui.callAfter(self._setSelectionMode, val) |
|
else: |
|
try: |
|
self.SetSelectionMode(wx.grid.Grid.wxGridSelectCells) |
|
self._selectionMode = "Cell" |
|
except wx.PyAssertionError: |
|
dabo.ui.callAfter(self._setSelectionMode, val) |
|
if self._selectionMode != orig: |
|
self._checkSelectionType() |
|
else: |
|
self._properties["SelectionMode"] = val |
|
|
|
|
|
def _getShowCellBorders(self): |
|
return self.GridLinesEnabled() |
|
|
|
def _setShowCellBorders(self, val): |
|
if self._constructed(): |
|
self.EnableGridLines(val) |
|
else: |
|
self._properties["ShowCellBorders"] = val |
|
|
|
|
|
def _getShowColumnLabels(self): |
|
warnings.warn(_("ShowColumnLabels is deprecated. Use ShowHeaders instead"), DeprecationWarning) |
|
return self._showHeaders |
|
|
|
def _setShowColumnLabels(self, val): |
|
if self._constructed(): |
|
warnings.warn(_("ShowColumnLabels is deprecated. Use ShowHeaders instead"), DeprecationWarning) |
|
self._showHeaders = val |
|
if val: |
|
self.SetColLabelSize(self.HeaderHeight) |
|
else: |
|
self.SetColLabelSize(0) |
|
else: |
|
self._properties["ShowColumnLabels"] = val |
|
|
|
|
|
def _getShowHeaders(self): |
|
return self._showHeaders |
|
|
|
def _setShowHeaders(self, val): |
|
if self._constructed(): |
|
self._showHeaders = val |
|
if val: |
|
hh = getattr(self, "_lastPositiveHeaderHeight", None) |
|
if not hh: |
|
# Use current if already positive: |
|
hh = self.GetColLabelSize() |
|
if not hh: |
|
# Set a reasonable default (should never happen) |
|
hh = 32 |
|
self.SetColLabelSize(hh) |
|
else: |
|
curr = self.GetColLabelSize() |
|
if curr > 0: |
|
self._lastPositiveHeaderHeight = curr |
|
self.SetColLabelSize(0) |
|
else: |
|
self._properties["ShowHeaders"] = val |
|
|
|
|
|
def _getShowRowLabels(self): |
|
return self._showRowLabels |
|
|
|
def _setShowRowLabels(self, val): |
|
if self._constructed(): |
|
self._showRowLabels = val |
|
if val: |
|
self.SetRowLabelSize(self.RowLabelWidth) |
|
else: |
|
self.SetRowLabelSize(0) |
|
else: |
|
self._properties["ShowRowLabels"] = val |
|
|
|
|
|
def _getSortable(self): |
|
return self._sortable |
|
|
|
def _setSortable(self, val): |
|
self._sortable = bool(val) |
|
|
|
|
|
def _getSortIndicatorColor(self): |
|
return self._sortIndicatorColor |
|
|
|
def _setSortIndicatorColor(self, val): |
|
if self._constructed(): |
|
self._sortIndicatorColor = val |
|
else: |
|
self._properties["SortIndicatorColor"] = val |
|
|
|
|
|
def _getSortIndicatorSize(self): |
|
return self._sortIndicatorSize |
|
|
|
def _setSortIndicatorSize(self, val): |
|
if self._constructed(): |
|
self._sortIndicatorSize = val |
|
else: |
|
self._properties["SortIndicatorSize"] = val |
|
|
|
|
|
def _getTabNavigates(self): |
|
return getattr(self, "_tabNavigates", True) |
|
|
|
def _setTabNavigates(self, val): |
|
self._tabNavigates = bool(val) |
|
|
|
|
|
def _getVerticalHeaders(self): |
|
return self._verticalHeaders |
|
|
|
def _setVerticalHeaders(self, val): |
|
if self._constructed(): |
|
if val != self._verticalHeaders: |
|
self._verticalHeaders = val |
|
self.refresh() |
|
if self.AutoAdjustHeaderHeight: |
|
dabo.ui.callAfter(self.fitHeaderHeight) |
|
else: |
|
self._properties["VerticalHeaders"] = val |
|
|
|
|
|
def _getVerticalScrolling(self): |
|
return self.GetScrollPixelsPerUnit()[1] > 0 |
|
|
|
def _setVerticalScrolling(self, val): |
|
if self._constructed(): |
|
if val: |
|
self.SetScrollRate(self.GetScrollPixelsPerUnit()[0], 20) |
|
else: |
|
self.SetScrollRate(self.GetScrollPixelsPerUnit()[0], 0) |
|
self.refresh() |
|
else: |
|
self._properties["VerticalScrolling"] = val |
|
|
|
|
|
def _getTable(self): |
|
## pkm: we can't call this until after the grid is fully constructed. Need to fix. |
|
try: |
|
tbl = self.GetTable() |
|
except TypeError: |
|
tbl = None |
|
if not tbl: |
|
try: |
|
tbl = dGridDataTable(self) |
|
self.SetTable(tbl, False) |
|
except TypeError: |
|
tbl = None |
|
return tbl |
|
|
|
def _setTable(self, tbl): |
|
if self._constructed(): |
|
self.SetTable(tbl, True) |
|
else: |
|
self._properties["Table"] = value |
|
|
|
|
|
ActivateEditorOnSelect = property( |
|
_getActivateEditorOnSelect, _setActivateEditorOnSelect, None, |
|
_("Specifies whether the cell editor, if any, is activated upon cell selection.")) |
|
|
|
AlternateRowColoring = property(_getAlternateRowColoring, _setAlternateRowColoring, None, |
|
_("""When True, alternate rows of the grid are colored according to |
|
the RowColorOdd and RowColorEven properties (bool)""")) |
|
|
|
AutoAdjustHeaderHeight = property(_getAutoAdjustHeaderHeight, |
|
_setAutoAdjustHeaderHeight, None, |
|
_("""When True, changing the VerticalHeaders property will adjust the HeaderHeight |
|
to accommodate the rotated labels. Default=False. (bool)""")) |
|
|
|
CellHighlightWidth = property(_getCellHighlightWidth, _setCellHighlightWidth, None, |
|
_("Specifies the width of the cell highlight box.")) |
|
|
|
Children = property(_getColumns, None, None, |
|
_("List of dColumns, same as self.Columns. (list)")) |
|
|
|
Columns = property(_getColumns, None, None, |
|
_("List of dColumns. (list)")) |
|
|
|
ColumnClass = property(_getColumnClass, _setColumnClass, None, |
|
_("""Class to instantiate when a change to ColumnCount requires |
|
additional columns to be created. Default=dColumn. (dColumn subclass)""") ) |
|
|
|
ColumnCount = property(_getColumnCount, _setColumnCount, None, |
|
_("Number of columns in the grid. (int)") ) |
|
|
|
CurrentCellValue = property(_getCurrCellVal, _setCurrCellVal, None, |
|
_("Value of the currently selected grid cell (varies)") ) |
|
|
|
CurrentColumn = property(_getCurrentColumn, _setCurrentColumn, None, |
|
_("Currently selected column index. (int)") ) |
|
|
|
CurrentField = property(_getCurrentField, _setCurrentField, None, |
|
_("Field for the currently selected column (str)") ) |
|
|
|
CurrentRow = property(_getCurrentRow, _setCurrentRow, None, |
|
_("Currently selected row (int)") ) |
|
|
|
DataSet = property(_getDataSet, _setDataSet, None, |
|
_("""The set of data displayed in the grid. (set of dicts) |
|
|
|
When DataSource isn't defined, setting DataSet to a set of dicts, |
|
such as what you get from calling dBizobj.getDataSet(), will |
|
define the source of the data that the grid displays. |
|
|
|
If DataSource is defined, DataSet is read-only and returns the dataSet |
|
from the bizobj.""")) |
|
|
|
DataSource = property(_getDataSource, _setDataSource, None, |
|
_("""The source of the data to display in the grid. (str) |
|
|
|
This corresponds to a bizobj with a matching DataSource on the form, |
|
and setting this makes it impossible to set DataSet.""")) |
|
|
|
Editable = property(_getEditable, _setEditable, None, |
|
_("""This setting enables/disables cell editing globally. (bool) |
|
|
|
When False, no cells will be editable by the user. When True, cells in |
|
columns set as Editable will be editable by the user. Note that grids |
|
and columns are both set with Editable=False by default, so to enable |
|
cell editing you need to turn it on in the appropriate column as well |
|
as in the grid.""") ) |
|
|
|
Encoding = property(_getEncoding, None, None, |
|
_("Name of encoding to use for unicode (str)") ) |
|
|
|
HeaderBackColor = property(_getHeaderBackColor, _setHeaderBackColor, None, |
|
_("""Optional color for the background of the column headers. (str or None) |
|
|
|
This is only the default: setting the corresponding dColumn property will |
|
override.""") ) |
|
|
|
HeaderForeColor = property(_getHeaderForeColor, _setHeaderForeColor, None, |
|
_("""Optional color for the foreground (text) of the column headers. (str or None) |
|
|
|
This is only the default: setting the corresponding dColumn property will |
|
override.""") ) |
|
|
|
HeaderHeight = property(_getHeaderHeight, _setHeaderHeight, None, |
|
_("Height of the column headers. (int)") ) |
|
|
|
HeaderHorizontalAlignment = property(_getHeaderHorizontalAlignment, _setHeaderHorizontalAlignment, None, |
|
_("""The horizontal alignment of the header captions. ('Left', 'Center', 'Right') |
|
|
|
This is only the default: setting the corresponding dColumn property will |
|
override.""") ) |
|
|
|
HeaderVerticalAlignment = property(_getHeaderVerticalAlignment, _setHeaderVerticalAlignment, None, |
|
_("""The vertical alignment of the header captions. ('Top', 'Center', 'Bottom') |
|
|
|
This is only the default: setting the corresponding dColumn property will |
|
override.""") ) |
|
|
|
HorizontalScrolling = property(_getHorizontalScrolling, _setHorizontalScrolling, None, |
|
_("Is scrolling enabled in the horizontal direction? (bool)")) |
|
|
|
MovableColumns = property(_getMovableColumns, _setMovableColumns, None, |
|
_("When False, the user cannot re-order the columns by dragging the headers (bool)")) |
|
|
|
MultipleSelection = property(_getMultipleSelection, _setMultipleSelection, None, |
|
_("When True (default), more than one cell/row/col can be selected at once (bool)")) |
|
|
|
NoneDisplay = property(_getNoneDisplay, _setNoneDisplay, None, |
|
_("Text to display for null (None) values. (str)") ) |
|
|
|
ResizableColumns = property(_getResizableColumns, _setResizableColumns, None, |
|
_("When False, the user cannot resize the columns (bool)")) |
|
|
|
ResizableRows = property(_getResizableRows, _setResizableRows, None, |
|
_("When False, the user cannot resize the rows (bool)")) |
|
|
|
RowColorEven = property(_getRowColorEven, _setRowColorEven, None, |
|
_("""When alternate row coloring is active, controls the color |
|
of the even rows (str or tuple)""")) |
|
|
|
RowColorOdd = property(_getRowColorOdd, _setRowColorOdd, None, |
|
_("""When alternate row coloring is active, controls the color |
|
of the odd rows (str or tuple)""")) |
|
|
|
RowCount = property(_getRowCount, None, None, |
|
_("Number of rows in the grid. (int)") ) |
|
|
|
RowHeight = property(_getRowHeight, _setRowHeight, None, |
|
_("Row Height for all rows of the grid (int)")) |
|
|
|
RowLabels = property(_getRowLabels, _setRowLabels, None, |
|
_("List of the row labels. (list)") ) |
|
|
|
RowLabelWidth = property(_getRowLabelWidth, _setRowLabelWidth, None, |
|
_("""Width of the label on the left side of the rows. This only changes |
|
the grid if ShowRowLabels is True. (int)""")) |
|
|
|
SameSizeRows = property(_getSameSizeRows, _setSameSizeRows, None, |
|
_("""Is every row the same height? (bool)""")) |
|
|
|
SaveRestoreDataSet = property(_getSaveRestoreDataSet, _setSaveRestoreDataSet, None, |
|
_("""Specifies whether the DataSet is persisted to preferences (bool). |
|
|
|
This allows you to build a grid to capture user input of some form, and |
|
instead of saving the row and field values to a database, to save the |
|
entire dataset to a single key in the prefs table. |
|
|
|
Use this sparingly for grids that won't grow too large. |
|
|
|
The default is False.""")) |
|
|
|
SaveRestoreDataSet = property(_getSaveRestoreDataSet, _setSaveRestoreDataSet, None, |
|
_("""Specifies whether the DataSet is persisted to preferences (bool). |
|
|
|
This allows you to build a grid to capture user input of some form, and |
|
instead of saving the row and field values to a database, to save the |
|
entire dataset to a single key in the prefs table. |
|
|
|
Use this sparingly for grids that won't grow too large. |
|
|
|
The default is False.""")) |
|
|
|
Searchable = property(_getSearchable, _setSearchable, None, |
|
_("""Specifies whether the columns can be searched. (bool) |
|
|
|
If True, columns that have their Searchable properties set to True |
|
will be searchable. |
|
|
|
Default: True""")) |
|
|
|
SearchDelay = property(_getSearchDelay, _setSearchDelay, None, |
|
_("""Specifies the delay before incrementeal searching begins. (int or None) |
|
|
|
As the user types, the search string is modified. If the time between |
|
keystrokes exceeds SearchDelay (milliseconds), the search will run and |
|
the search string will be cleared. |
|
|
|
If SearchDelay is set to None (the default), Application.SearchDelay will |
|
be used.""") ) |
|
|
|
Selection = property(_getSelection, None, None, |
|
_("""Returns either a list of row/column numbers if SelectionMode is set to |
|
either 'Row' or 'Column'. If SelectionMode is 'Cell', returns a list of 2-tuples, |
|
where each 2-tuple represents a selected range of cells: the top-left and |
|
bottom-right coordinates for a given range. If only a single cell is selected, |
|
there will be only one 2-tuple in the list, with both values being the same. |
|
If a continuous block of cells is selected, there will be only one 2-tuple in the |
|
list, but the values will differ. If more than one discontinuous range is selected, |
|
there will be as many 2-tuples as there are range blocks. (list)""")) |
|
|
|
SelectionBackColor = property(_getSelectionBackColor, _setSelectionBackColor, None, |
|
_("BackColor of selected cells (str or RGB tuple)")) |
|
|
|
SelectionForeColor = property(_getSelectionForeColor, _setSelectionForeColor, None, |
|
_("ForeColor of selected cells (str or RGB tuple)")) |
|
|
|
SelectionMode = property(_getSelectionMode, _setSelectionMode, None, |
|
_("""Determines how the grid displays selections. (str) |
|
Options are: |
|
Cells/Plain/None - no row/col highlighting (default) |
|
Row - the row of the selected cell is highlighted |
|
Column - the column of the selected cell is highlighted |
|
|
|
The highlight color is determined by the SelectionBackColor and |
|
SelectionForeColor properties. |
|
""")) |
|
|
|
ShowCellBorders = property(_getShowCellBorders, _setShowCellBorders, None, |
|
_("Are borders around cells shown? (bool)") ) |
|
|
|
ShowColumnLabels = property(_getShowColumnLabels, _setShowColumnLabels, None, |
|
_("""Are column labels shown? (bool) |
|
|
|
DEPRECATED: Use ShowHeaders instead.""") ) |
|
|
|
ShowHeaders = property(_getShowHeaders, _setShowHeaders, None, |
|
_("""Are grid column headers shown? (bool)""") ) |
|
|
|
ShowRowLabels = property(_getShowRowLabels, _setShowRowLabels, None, |
|
_("Are row labels shown? (bool)") ) |
|
|
|
Sortable = property(_getSortable, _setSortable, None, |
|
_("""Specifies whether the columns can be sorted. If True, |
|
and if the column's Sortable property is True, the column |
|
will be sortable. Default: True (bool)""")) |
|
|
|
SortIndicatorColor = property(_getSortIndicatorColor, _setSortIndicatorColor, |
|
None, _("""Color of the icon is that identifies a column as being sorted. |
|
Default="yellow". (str or color tuple)""")) |
|
|
|
SortIndicatorSize = property(_getSortIndicatorSize, _setSortIndicatorSize, |
|
None, _("""Determines how large the icon is that identifies a column as |
|
being sorted. Default=8. (int)""")) |
|
|
|
TabNavigates = property(_getTabNavigates, _setTabNavigates, None, |
|
_("""Specifies whether Tab navigates to the next control (True, the default), |
|
or if Tab moves to the next column in the grid (False).""")) |
|
|
|
VerticalHeaders = property(_getVerticalHeaders, _setVerticalHeaders, None, |
|
_("""When True, the column headers' Captions are written vertically. |
|
Default=False. (bool)""")) |
|
|
|
VerticalScrolling = property(_getVerticalScrolling, _setVerticalScrolling, None, |
|
_("Is scrolling enabled in the vertical direction? (bool)")) |
|
|
|
_Table = property(_getTable, _setTable, None, |
|
_("Reference to the internal table class (dGridDataTable)") ) |
|
|
|
|
|
# Dynamic Property Declarations |
|
DynamicActivateEditorOnSelect = makeDynamicProperty(ActivateEditorOnSelect) |
|
DynamicAlternateRowColoring = makeDynamicProperty(AlternateRowColoring) |
|
DynamicCellHighlightWidth = makeDynamicProperty(CellHighlightWidth) |
|
DynamicColumnClass = makeDynamicProperty(ColumnClass) |
|
DynamicColumnCount = makeDynamicProperty(ColumnCount) |
|
DynamicCurrentColumn = makeDynamicProperty(CurrentColumn) |
|
DynamicCurrentField = makeDynamicProperty(CurrentField) |
|
DynamicCurrentRow = makeDynamicProperty(CurrentRow) |
|
DynamicDataSet = makeDynamicProperty(DataSet) |
|
DynamicDataSource = makeDynamicProperty(DataSource) |
|
DynamicEditable = makeDynamicProperty(Editable) |
|
DynamicHeaderBackColor = makeDynamicProperty(HeaderBackColor) |
|
DynamicHeaderForeColor = makeDynamicProperty(HeaderForeColor) |
|
DynamicHeaderHeight = makeDynamicProperty(HeaderHeight) |
|
DynamicHeaderHorizontalAlignment = makeDynamicProperty(HeaderHorizontalAlignment) |
|
DynamicHeaderVerticalAlignment = makeDynamicProperty(HeaderVerticalAlignment) |
|
DynamicHorizontalScrolling = makeDynamicProperty(HorizontalScrolling) |
|
DynamicRowColorEven = makeDynamicProperty(RowColorEven) |
|
DynamicRowColorOdd = makeDynamicProperty(RowColorOdd) |
|
DynamicRowHeight = makeDynamicProperty(RowHeight) |
|
DynamicRowLabels = makeDynamicProperty(RowLabels) |
|
DynamicRowLabelWidth = makeDynamicProperty(RowLabelWidth) |
|
DynamicSameSizeRows = makeDynamicProperty(SameSizeRows) |
|
DynamicSearchable = makeDynamicProperty(Searchable) |
|
DynamicSearchDelay = makeDynamicProperty(SearchDelay) |
|
DynamicSelectionBackColor = makeDynamicProperty(SelectionBackColor) |
|
DynamicSelectionForeColor = makeDynamicProperty(SelectionForeColor) |
|
DynamicSelectionMode = makeDynamicProperty(SelectionMode) |
|
DynamicShowCellBorders = makeDynamicProperty(ShowCellBorders) |
|
DynamicShowColumnLabels = makeDynamicProperty(ShowColumnLabels) |
|
DynamicShowHeaders = makeDynamicProperty(ShowHeaders) |
|
DynamicShowRowLabels = makeDynamicProperty(ShowRowLabels) |
|
DynamicSortable = makeDynamicProperty(Sortable) |
|
DynamicTabNavigates = makeDynamicProperty(TabNavigates) |
|
DynamicVerticalScrolling = makeDynamicProperty(VerticalScrolling) |
|
DynamicVerticalHeaders = makeDynamicProperty(VerticalHeaders) |
|
|
|
|
|
##----------------------------------------------------------## |
|
## end: property definitions ## |
|
##----------------------------------------------------------## |
|
|
|
|
|
class _dGrid_test(dGrid): |
|
def initProperties(self): |
|
thisYear = datetime.datetime.now().year |
|
ds = [ |
|
{"name" : "Ed Leafe", "age" : thisYear - 1957, "coder" : True, "color": "cornsilk"}, |
|
{"name" : "Paul McNett", "age" : thisYear - 1969, "coder" : True, "color": "wheat"}, |
|
{"name" : "Ted Roche", "age" : thisYear - 1958, "coder" : True, "color": "goldenrod"}, |
|
{"name" : "Derek Jeter", "age": thisYear - 1974, "coder" : False, "color": "white"}, |
|
{"name" : "Halle Berry", "age" : thisYear - 1966, "coder" : False, "color": "orange"}, |
|
{"name" : "Steve Wozniak", "age" : thisYear - 1950, "coder" : True, "color": "yellow"}, |
|
{"name" : "LeBron James", "age" : thisYear - 1984, "coder" : False, "color": "gold"}, |
|
{"name" : "Madeline Albright", "age" : thisYear - 1937, "coder" : False, "color": "red"}] |
|
|
|
|
|
for row in range(len(ds)): |
|
for i in range(20): |
|
ds[row]["i_%s" % i] = "sss%s" % i |
|
self.DataSet = ds |
|
|
|
self.TabNavigates = False |
|
self.Width = 360 |
|
self.Height = 150 |
|
self.Editable = False |
|
#self.Sortable = False |
|
#self.Searchable = False |
|
|
|
def afterInit(self): |
|
super(_dGrid_test, self).afterInit() |
|
|
|
self.addColumn(Name="Geek", DataField="coder", Caption="Geek?", |
|
Order=10, DataType="bool", Width=60, Sortable=False, |
|
Searchable=False, Editable=True, HeaderFontBold=False, |
|
HorizontalAlignment="Center", VerticalAlignment="Center", |
|
Resizable=False) |
|
|
|
col = dColumn(self, Name="Person", Order=20, DataField="name", |
|
DataType="string", Width=200, Caption="Celebrity Name", |
|
Sortable=True, Searchable=True, Editable=True, Expand=False) |
|
self.addColumn(col) |
|
|
|
col.HeaderFontItalic = True |
|
col.HeaderBackColor = "peachpuff" |
|
col.HeaderVerticalAlignment = "Top" |
|
col.HeaderHorizontalAlignment = "Left" |
|
|
|
# Let's make a custom editor for the name |
|
class ColoredText(dabo.ui.dTextBox): |
|
def initProperties(self): |
|
self.ForeColor = "blue" |
|
self.FontItalic = True |
|
self.FontSize = 24 |
|
def onKeyChar(self, evt): |
|
self.ForeColor = dColors.randomColor() |
|
self.FontItalic = not self.FontItalic |
|
# Since we're using a big font, set a minimum height for the editor |
|
col.CustomEditorClass = dabo.ui.makeGridEditor(ColoredText, minHeight=40) |
|
|
|
self.addColumn(Name="Age", Order=30, DataField="age", |
|
DataType="integer", Width=40, Caption="Age", |
|
Sortable=True, Searchable=True, Editable=True) |
|
|
|
col = dColumn(self, Name="Color", Order=40, DataField="color", |
|
DataType="string", Width=40, Caption="Favorite Color", |
|
Sortable=True, Searchable=True, Editable=True, Expand=False) |
|
self.addColumn(col) |
|
|
|
col.ListEditorChoices = dColors.colors |
|
col.CustomEditorClass = col.listEditorClass |
|
|
|
col.HeaderVerticalAlignment = "Bottom" |
|
col.HeaderHorizontalAlignment = "Right" |
|
col.HeaderForeColor = "brown" |
|
|
|
for i in range(1): |
|
# Can't test Expand with so many columns! Just add one. |
|
self.addColumn(DataField="i_%s" % i, Caption="i_%s" % i) |
|
|
|
def onScrollLineUp(self, evt): |
|
print "LINE UP orientation =", evt.orientation, " scrollpos =", evt.scrollpos |
|
def onScrollLineDown(self, evt): |
|
print "LINE DOWN orientation =", evt.orientation, " scrollpos =", evt.scrollpos |
|
def onScrollPageUp(self, evt): |
|
print "PAGE UP orientation =", evt.orientation, " scrollpos =", evt.scrollpos |
|
def onScrollPageDown(self, evt): |
|
print "PAGE DOWN orientation =", evt.orientation, " scrollpos =", evt.scrollpos |
|
def onScrollThumbDrag(self, evt): |
|
print "DRAG orientation =", evt.orientation, " scrollpos =", evt.scrollpos |
|
def onScrollThumbRelease(self, evt): |
|
print "THUMB RELEASE orientation =", evt.orientation, " scrollpos =", evt.scrollpos |
|
|
|
if __name__ == '__main__': |
|
from dabo.dApp import dApp |
|
class TestForm(dabo.ui.dForm): |
|
def afterInit(self): |
|
self.BackColor = "khaki" |
|
g = self.grid = _dGrid_test(self, RegID="sampleGrid") |
|
self.Sizer.append(g, 1, "x", border=0, borderSides="all") |
|
self.Sizer.appendSpacer(10) |
|
gsz = dabo.ui.dGridSizer(HGap=50) |
|
|
|
chk = dabo.ui.dCheckBox(self, Caption="Allow Editing?", RegID="gridEdit", |
|
DataSource=self.grid, DataField="Editable") |
|
chk.update() |
|
gsz.append(chk, row=0, col=0) |
|
|
|
chk = dabo.ui.dCheckBox(self, Caption="Show Headers", |
|
RegID="showHeaders", DataSource=self.grid, |
|
DataField="ShowHeaders") |
|
gsz.append(chk, row=1, col=0) |
|
chk.update() |
|
|
|
chk = dabo.ui.dCheckBox(self, Caption="Allow Multiple Selection", |
|
RegID="multiSelect", DataSource=self.grid, |
|
DataField="MultipleSelection") |
|
chk.update() |
|
gsz.append(chk, row=2, col=0) |
|
|
|
chk = dabo.ui.dCheckBox(self, Caption="Vertical Headers", |
|
RegID="verticalHeaders", DataSource=self.grid, |
|
DataField="VerticalHeaders") |
|
chk.update() |
|
gsz.append(chk, row=3, col=0) |
|
|
|
chk = dabo.ui.dCheckBox(self, Caption="Auto-adjust Header Height", |
|
RegID="autoAdjust", DataSource=self.grid, |
|
DataField="AutoAdjustHeaderHeight") |
|
chk.update() |
|
gsz.append(chk, row=4, col=0) |
|
|
|
radSelect = dabo.ui.dRadioList(self, Choices=["Row", "Col", "Cell"], |
|
ValueMode="string", Caption="Sel Mode", BackColor=self.BackColor, |
|
DataSource=self.grid, DataField="SelectionMode", RegID="radSelect") |
|
radSelect.refresh() |
|
gsz.append(radSelect, row=0, col=1, rowSpan=3) |
|
|
|
def setVisible(evt): |
|
col = g.getColByDataField("name") |
|
but = evt.EventObject |
|
col.Visible = not col.Visible |
|
if col.Visible: |
|
but.Caption = "Make Celebrity Invisible" |
|
else: |
|
but.Caption = "Make Celebrity Visible" |
|
butVisible = dabo.ui.dButton(self, Caption="Toggle Celebrity Visibility", |
|
OnHit=setVisible) |
|
gsz.append(butVisible, row=5, col=0) |
|
|
|
self.Sizer.append(gsz, halign="Center", border=10) |
|
gsz.setColExpand(True, 1) |
|
self.layout() |
|
|
|
self.fitToSizer(20, 20) |
|
|
|
|
|
app = dApp(MainFormClass=TestForm) |
|
app.setup() |
|
app.MainForm.radSelect.setFocus() |
|
app.start() |