Platforms to show: All Mac Windows Linux Cross-Platform
/MacControls/Listbox and TableView Demos/ListboxTV drop-in/Hierarchical & Flat/ListBoxTV OutlineView
You find this example project in your Plugins Download as a Xojo project file within the examples folder: /MacControls/Listbox and TableView Demos/ListboxTV drop-in/Hierarchical & Flat/ListBoxTV OutlineView
This example is the version from Sun, 5th Nov 2022.
Project "ListBoxTV OutlineView.xojo_binary_project"
Class App Inherits Application
Const kEditClear = "&Delete"
Const kFileQuit = "&Quit"
Const kFileQuitShortcut = ""
EventHandler Sub Open()
LogWin.Left = ListboxDemoWin.Left + ListboxDemoWin.Width + 30
LogWin.Top = ListboxDemoWin.Top
ListboxDemoWin.Show
End EventHandler
End Class
MenuBar MenuBar1
MenuItem FileMenu = "&File"
MenuItem FileQuit = "#App.kFileQuit"
MenuItem EditMenu = "&Edit"
MenuItem EditUndo = "&Undo"
MenuItem UntitledMenu1 = "-"
MenuItem EditCut = "Cu&t"
MenuItem EditCopy = "&Copy"
MenuItem EditPaste = "&Paste"
MenuItem EditClear = "#App.kEditClear"
MenuItem UntitledMenu0 = "-"
MenuItem EditSelectAll = "Select &All"
End MenuBar
Class ListboxDemoWin Inherits Window
Const StaticListCount = 16
Control cocoaListbox Inherits ListBoxTV
ControlInstance cocoaListbox Inherits ListBoxTV
EventHandler Sub CellAction(row as Integer, column as Integer)
log "cocoa.CellAction ("+Str(row)+", "+Str(column)+")"
End EventHandler
EventHandler Function CellClick(row as Integer, column as Integer, x as Integer, y as Integer) As Boolean
log "cocoa.CellClick ("+Str(row)+", "+Str(column)+", "+Str(x)+", "+Str(y)+"), isCMM: "+Str(IsContextualClick)+")"
return mCellClickResult
End EventHandler
EventHandler Sub Change()
'log "Change: "+Str(me.ListIndex) + " (" + Str(me.SelCount) + ")"
End EventHandler
EventHandler Function CompareRows(cell1 as ListCellTV, cell2 as ListCellTV, row1 as Integer, row2 as Integer, column as Integer, ByRef result as Integer) As Boolean
if column < 2 then
// default sorting for first two columns
return false
else
// custom sorting for 3rd and last col
if column = 2 then
// Use "smart" sorting, which identifies numbers, so that "10" comes after "2"
const NSCaseInsensitiveSearch = 1
const NSLiteralSearch = 2
const NSBackwardsSearch = 4
const NSAnchoredSearch = 8
const NSNumericSearch = 64
const NSDiacriticInsensitiveSearch = 128
const NSWidthInsensitiveSearch = 256
const NSForcedOrderingSearch = 512
static options as Integer = NSCaseInsensitiveSearch or NSNumericSearch or NSDiacriticInsensitiveSearch
result = NSStringCompareMBS (cell1.Text, cell2.Text, options)
else
// Use "dumb" String sorting ( "10" comes before "2")
result = StrComp (me.Cell(row1, column), me.Cell(row2, column), 1)
end if
return true
end if
End EventHandler
EventHandler Function ConstructContextualMenu(base as MenuItem, x as Integer, y as Integer, clickedRow as Integer, clickedColumn as Integer, parentMenu as NSMenuMBS) As Boolean
base.Append new MenuItem ("Item 1", 1)
base.Append new MenuItem ("Item 2", 2)
parentMenu.allowsContextMenuPlugIns = true ' enables System Services
return true
End EventHandler
EventHandler Function ConstructContextualMenuForHeader(base as MenuItem, x as Integer, y as Integer, clickedColumn as Integer, parentMenu as NSMenuMBS) As Boolean
base.Append new MenuItem ("Header 1", 1)
base.Append new MenuItem ("Header 2", 2)
return true
End EventHandler
EventHandler Function ContextualMenuAction(hitItem as MenuItem, clickedRow as Integer, clickedColumn as Integer) As Boolean
if clickedRow < 0 then
MsgBox "You chose "+hitItem.Text+" on column "+Str(clickedColumn)
else
dim allSel() as String
for i as Integer = 0 to me.ListCount-1
if me.Selected (i) then
allSel.Append Str(i)
end if
next
MsgBox "You chose "+hitItem.Text+" on these rows:"+EndOfLine+Join(allSel, ", ")
end if
End EventHandler
EventHandler Function DenyReorderColumn(fromColumn as Integer, toColumn as Integer) As Boolean
return fromColumn = 0 or toColumn = 0 ' this ensures that the first columns always remains the first (leftmost)
End EventHandler
EventHandler Function DragRow(drag as DragItemTV, row as Integer, ByRef operationMask as Integer, ByRef thisAppOnly as Boolean) As Boolean
// Add all selected rows as items to the drag pasteboard
dim n as Integer = me.SelCount
while n > 0 and row < me.ListCount
if me.Selected(row) then
drag.Text = "Row " + Str(row) + ": " + me.List(row)
drag.RawData("Test") = Str(row)
drag.FolderItem = App.ExecutableFile.Parent.Parent.Parent ' just some file
n = n - 1
if n = 0 then exit
drag.AddItem(0,0,20,4)
end if
row = row + 1
wend
return true
End EventHandler
EventHandler Sub DropObject(obj as DragItemTV, operationMask as Integer, aboveRow as Integer, draggingInfo as NSDraggingInfoMBS)
dim s() as String
do
if obj.TextAvailable then
s.Append "Text: "+obj.Text
end
if obj.FolderItemAvailable then
s.Append "FolderItem: "+obj.FolderItem.NativePath
end
loop until not obj.NextItem
if s.Ubound < 0 then
MsgBox "Got something unknown dropped."
else
MsgBox "Received drop"+EndOfLine+EndOfLine+Join(s, EndOfLine)
end if
End EventHandler
EventHandler Sub ExpandRow(row as Integer)
dim elems() as String = Pair(me.RowTag(row)).Right
dim depth as Integer = Pair(me.RowTag(row)).Left
dim n as Integer, expand as Boolean
for each s as String in elems
n = n + 1
if n = 2 and depth <= 3 then
me.AddFolder s
me.RowTag (me.LastIndex) = depth + 1 : elems
if depth <= 2 then
expand = true
end if
else
me.AddRow s
end if
me.Cell (me.LastIndex, 1) = Str (me.LastIndex)
if expand then
me.Expanded(me.LastIndex) = true
end if
next
End EventHandler
EventHandler Function MouseDown(x as Integer, y as Integer) As Boolean
log "MouseDown ("+Str(x) + ", " + Str(y) + ")"
End EventHandler
EventHandler Sub Open()
for row as integer = 0 to StaticListCount-1
if row mod 4 = 2 then
me.AddFolder "f "+Format(row, "#")
me.RowTag (me.LastIndex) = 1 : Array (Str(row)+"a", Str(row)+"b", Str(row)+"c")
else
me.AddRow Format(row, "#.0")
end if
for col as Integer = 1 to me.ColumnCount-1
if col = 1 or col = 2 then
// 2nd and 3rd columns get random values for sort testing
me.Cell (me.LastIndex, col) = Chr(col+65)+Str(Rnd*30,"#")
else
me.Cell (me.LastIndex, col) = Chr(col+65)+Str(row)
end if
next
if row mod 12 = 10 then
me.Expanded(me.LastIndex) = true
end if
next
for row as Integer = 1 to Min (12, StaticListCount-1)
me.CellAlignmentOffset (row, 1) = row * 4
next
me.CellAlignment (1, 0) = ListBox.AlignLeft
me.CellAlignment (2, 0) = ListBox.AlignCenter
me.CellAlignment (3, 0) = ListBox.AlignRight
me.CellAlignment (4, 0) = ListBox.AlignDecimal
me.Cell (1, 0) = "AlignLeft"
me.Cell (2, 0) = "AlignCenter"
me.Cell (3, 0) = "AlignRight"
me.Cell (4, 0) = "AlignDecimal"
me.ColumnAlignment (2) = ListBox.AlignCenter
me.Column(0).MinWidthActual = 40
me.Column(1).MinWidthActual = 60
me.Column(2).MinWidthActual = 30
me.Column(4).MinWidthActual = 10
me.ColumnType(0) = ListBox.TypeEditable
me.CellType(1,1) = ListBox.TypeCheckbox
me.CellType(1,2) = ListBox.TypeCheckbox
me.CellType(2,1) = ListBox.TypeCheckbox
me.CellType(2,2) = ListBox.TypeCheckbox
me.CellType(3,1) = ListBox.TypeEditable
me.CellType(4,1) = ListBox.TypeEditable
me.Cell(1,1) = me.Cell(1,1) + " TypeCheckbox"
me.Cell(1,2) = me.Cell(1,2) + " TypeCheckbox"
me.Cell(2,1) = me.Cell(2,1) + " TypeCheckbox"
me.Cell(2,2) = me.Cell(2,2) + " TypeCheckbox"
me.Cell(3,1) = me.Cell(3,1) + " TypeEditable"
me.Cell(4,1) = me.Cell(4,1) + " TypeEditable"
me.RowPicture(5) = mSampleImage
me.CellType(5,0) = ListBox.TypeEditable
me.Cell(5,0) = "5 - RowPicture, TypeEditable"
for row as Integer = 2 to 4
me.CellTag(row, 1) = &cFFFE2500 : &cF4004500 ' let's use the CellTag to specify their background and text colors
me.Cell(row,1) = me.Cell(row,1) + " BgColor TextColor"
next
me.AcceptRawDataDrop ("Test")
//
// Custom settings that are not available in ListBox
//
for col as Integer = 0 to me.ColumnCount-1
me.Column(col).SetFontAndSize "System", 0
next
for row as Integer = 2 to 4
me.CellStyle(row,1).BackgroundColor = &cFFFE2500
me.CellStyle(row,1).TextColor = &cF4004500
next
me.TableView.usesAlternatingRowBackgroundColors = true
me.TableView.allowsTypeSelect = true
me.Reload ' this loads the listbox immediately - otherwise there may be a little delay (due to the Timer in ListBoxTV._needsReload)
me.Expanded(2) = true
me.Expanded(9) = true
End EventHandler
EventHandler Function SortColumn(column as Integer) As Boolean
log "cocoa.SortColumn "+Str(column)
End EventHandler
EventHandler Function WillDisplayCell(row as Integer, column as Integer, cell as ListCellTV, textFieldCell as NSCellMBS, tableColumn as NSTableColumnMBS) As Boolean
// The last column shall always show the current row
if column = me.ColumnCount-1 then
me.Cell(row, column) = "row " + Str(row)
end if
End EventHandler
End Control
Control xojoListbox Inherits Listbox
ControlInstance xojoListbox Inherits Listbox
EventHandler Sub CellAction(row As Integer, column As Integer)
log "xojo.CellAction ("+Str(row)+", "+Str(column)+")"
End EventHandler
EventHandler Function CellBackgroundPaint(g As Graphics, row As Integer, column As Integer) As Boolean
if row >= me.ListCount then return false
// The last column shall always show the current row
if column = me.ColumnCount-1 then
me.Cell(row, column) = "row " + Str(row)
end if
dim p as Pair = me.CellTag (row, column)
if p <> nil then
g.ForeColor = p.Left
g.FillRect 0, 0, g.Width, g.Height
return true
end if
End EventHandler
EventHandler Function CellClick(row as Integer, column as Integer, x as Integer, y as Integer) As Boolean
log "xojo.CellClick ("+Str(row)+", "+Str(column)+", "+Str(x)+", "+Str(y)+"), isCMM: "+Str(IsContextualClick)
return mCellClickResult
End EventHandler
EventHandler Function CellTextPaint(g As Graphics, row As Integer, column As Integer, x as Integer, y as Integer) As Boolean
dim p as Pair = me.CellTag (row, column)
if p <> nil then
g.ForeColor = p.Right
g.DrawString me.Cell(row, column), 10, 13
return true
end if
End EventHandler
EventHandler Sub Change()
'log "Change: "+Str(me.ListIndex) + " (" + Str(me.SelCount) + ")"
End EventHandler
EventHandler Function CompareRows(row1 as Integer, row2 as Integer, column as Integer, ByRef result as Integer) As Boolean
if column < 2 then
// default sorting for first two columns
return false
else
// custom sorting for 3rd and last col
if column = 2 then
// Use "smart" sorting, which identifies numbers, so that "10" comes after "2"
const NSCaseInsensitiveSearch = 1
const NSLiteralSearch = 2
const NSBackwardsSearch = 4
const NSAnchoredSearch = 8
const NSNumericSearch = 64
const NSDiacriticInsensitiveSearch = 128
const NSWidthInsensitiveSearch = 256
const NSForcedOrderingSearch = 512
static options as Integer = NSCaseInsensitiveSearch or NSNumericSearch or NSDiacriticInsensitiveSearch
result = NSStringCompareMBS (me.Cell(row1, column), me.Cell(row2, column), options)
else
// Use "dumb" String sorting ( "10" comes before "2")
result = StrComp (me.Cell(row1, column), me.Cell(row2, column), 1)
end if
return true
end if
End EventHandler
EventHandler Function ConstructContextualMenu(base as MenuItem, x as Integer, y as Integer) As Boolean
base.Append new MenuItem ("Item 1", 1)
base.Append new MenuItem ("Item 2", 2)
return true
End EventHandler
EventHandler Function ContextualMenuAction(hitItem as MenuItem) As Boolean
dim allSel() as String
for i as Integer = 0 to me.ListCount-1
if me.Selected (i) then
allSel.Append Str(i)
end if
next
MsgBox "You chose "+hitItem.Text+" on these rows:"+EndOfLine+Join(allSel, ", ")
return true
End EventHandler
EventHandler Function DragOver(x As Integer, y As Integer, obj As DragItem, action As Integer) As Boolean
beep
End EventHandler
EventHandler Function DragRow(drag As DragItem, row As Integer) As Boolean
// Add all selected rows as items to the drag pasteboard
dim n as Integer = me.SelCount
while n > 0 and row < me.ListCount
if me.Selected(row) then
drag.Text = "Row " + Str(row) + ": " + me.List(row)
drag.RawData("Test") = Str(row)
drag.FolderItem = App.ExecutableFile.Parent.Parent.Parent ' just some file
n = n - 1
if n = 0 then exit
drag.AddItem(0,0,20,4)
end if
row = row + 1
wend
return true
End EventHandler
EventHandler Sub DropObject(obj As DragItem, action As Integer)
dim s() as String
do
if obj.TextAvailable then
s.Append "Text: "+obj.Text
end
if obj.FolderItemAvailable then
s.Append "FolderItem: "+obj.FolderItem.NativePath
end
loop until not obj.NextItem
if s.Ubound < 0 then
MsgBox "Got something unknown dropped."
else
MsgBox "Received drop"+EndOfLine+EndOfLine+Join(s, EndOfLine)
end if
End EventHandler
EventHandler Sub EnableMenuItems()
log "xojo.EnableMenuItems"
End EventHandler
EventHandler Sub ExpandRow(row As Integer)
dim elems() as String = Pair(me.RowTag(row)).Right
dim depth as Integer = Pair(me.RowTag(row)).Left
dim n as Integer, expand as Boolean
for each s as String in elems
n = n + 1
if n = 2 and depth <= 3 then
me.AddFolder s
me.RowTag (me.LastIndex) = depth + 1 : elems
if depth <= 2 then
expand = true
end if
else
me.AddRow s
end if
me.Cell (me.LastIndex, 1) = Str (me.LastIndex)
if expand then
me.Expanded(me.LastIndex) = true
end if
next
End EventHandler
EventHandler Function MouseDown(x As Integer, y As Integer) As Boolean
log "MouseDown ("+Str(x) + ", " + Str(y) + ")"
End EventHandler
EventHandler Sub Open()
for row as integer = 0 to StaticListCount-1
if row mod 4 = 2 then
me.AddFolder "f "+Format(row, "#")
me.RowTag (me.LastIndex) = 1 : Array (Str(row)+"a", Str(row)+"b", Str(row)+"c")
else
me.AddRow Format(row, "#.0")
end if
for col as Integer = 1 to me.ColumnCount-1
if col = 1 or col = 2 then
// 2nd and 3rd columns get random values for sort testing
me.Cell (me.LastIndex, col) = Chr(col+65)+Str(Rnd*30,"#")
else
me.Cell (me.LastIndex, col) = Chr(col+65)+Str(row)
end if
next
if row mod 12 = 10 then
me.Expanded(me.LastIndex) = true
end if
next
for row as Integer = 1 to Min (12, StaticListCount-1)
me.CellAlignmentOffset (row, 1) = row * 4
next
me.CellAlignment (1, 0) = ListBox.AlignLeft
me.CellAlignment (2, 0) = ListBox.AlignCenter
me.CellAlignment (3, 0) = ListBox.AlignRight
me.CellAlignment (4, 0) = ListBox.AlignDecimal
me.Cell (1, 0) = "AlignLeft"
me.Cell (2, 0) = "AlignCenter"
me.Cell (3, 0) = "AlignRight"
me.Cell (4, 0) = "AlignDecimal"
me.ColumnAlignment (2) = ListBox.AlignCenter
me.Column(0).MinWidthActual = 40
me.Column(1).MinWidthActual = 60
me.Column(2).MinWidthActual = 30
me.Column(4).MinWidthActual = 10
me.ColumnType(0) = ListBox.TypeEditable
me.CellType(1,1) = ListBox.TypeCheckbox
me.CellType(1,2) = ListBox.TypeCheckbox
me.CellType(2,1) = ListBox.TypeCheckbox
me.CellType(2,2) = ListBox.TypeCheckbox
me.CellType(3,1) = ListBox.TypeEditable
me.CellType(4,1) = ListBox.TypeEditable
me.Cell(1,1) = me.Cell(1,1) + " TypeCheckbox"
me.Cell(1,2) = me.Cell(1,2) + " TypeCheckbox"
me.Cell(2,1) = me.Cell(2,1) + " TypeCheckbox"
me.Cell(2,2) = me.Cell(2,2) + " TypeCheckbox"
me.Cell(3,1) = me.Cell(3,1) + " TypeEditable"
me.Cell(4,1) = me.Cell(4,1) + " TypeEditable"
me.RowPicture(5) = mSampleImage
me.CellType(5,0) = ListBox.TypeEditable
me.Cell(5,0) = "5 - RowPicture, TypeEditable"
for row as Integer = 2 to 4
me.CellTag(row, 1) = &cFFFE2500 : &cF4004500 ' let's use the CellTag to specify their background and text colors
me.Cell(row,1) = me.Cell(row,1) + " BgColor TextColor"
next
me.AcceptRawDataDrop ("Test")
// --------
me.Expanded(2) = true
me.Expanded(9) = true
End EventHandler
EventHandler Function SortColumn(column As Integer) As Boolean
log "xojo.SortColumn "+Str(column)
End EventHandler
End Control
Control Timer1 Inherits Timer
ControlInstance Timer1 Inherits Timer
EventHandler Sub Action()
'if Keyboard.AsyncShiftKey then break ' this is for breaking into the debugger, for setting breakpoints (as it doesn't work reliably with the IDE's Pause button)
dim txt as String
'dim o as Variant = self.Focus
'if o = nil then
'txt = "No focus"
'elseif o = xojoListbox then
'txt = "Xojo ListBox has focus"
'elseif o = cocoaListbox then
'txt = "MBS TableView has focus"
'else
'txt = "Unknown focus"
'end if
'txt = txt + _
'", w: " + Str(cocoaListbox.TableView.bounds.Width,"#") + _
'" / " + Str (cocoaListbox.ScrollView.contentSize.Width) + _
'" / " + Str (cocoaListbox.ScrollView.visibleRect.Width)
// show current row/col
dim row, col, x, y as Integer
if self.MouseY >= xojoListbox.Top and self.MouseY < xojoListbox.Top + xojoListbox.Height then
x = self.MouseX - xojoListbox.Left
y = self.MouseY - xojoListbox.Top
row = xojoListbox.RowFromXY (x,y)
col = xojoListbox.ColumnFromXY (x,y)
txt = "In row "+Str(row)+" col "+Str(col)
elseif self.MouseY >= cocoaListbox.Top and self.MouseY < cocoaListbox.Top + cocoaListbox.Height then
x = self.MouseX - cocoaListbox.Left
y = self.MouseY - cocoaListbox.Top
row = cocoaListbox.RowFromXY (x,y)
col = cocoaListbox.ColumnFromXY (x,y)
txt = "In row "+Str(row)+" col "+Str(col)
end if
// show column widths
dim s1(), s2() as String, tw as Double
for i as Integer = 0 to cocoaListbox.ColumnCount-1
s1.Append cocoaListbox.Column(i).WidthExpression
dim w as Double = cocoaListbox.Column(i).WidthActual
s2.Append Str(w)
tw = tw + w
next
dim lbw as Double = cocoaListbox.ScrollView.frameWidth - cocoaListbox.ScrollView.verticalScroller.frame.Width - cocoaListbox.ColumnCount * cocoaListbox.TableView.intercellSpacing.width
txt = txt + " [" + Join (s1, ", ") + "] / [" + Join (s2, ", ") + "] = " + Str(tw) + " (" + Str (lbw) + ")"
Label1.Text = txt.Trim
End EventHandler
End Control
Control Label1 Inherits Label
ControlInstance Label1 Inherits Label
End Control
Control PushButton1 Inherits PushButton
ControlInstance PushButton1 Inherits PushButton
EventHandler Sub Action()
for row as Integer = 0 to cocoaListbox.ListCount-1
cocoaListbox.Selected(row) = not cocoaListbox.Selected(row)
next
for row as Integer = 0 to xojoListbox.ListCount-1
xojoListbox.Selected(row) = not xojoListbox.Selected(row)
next
End EventHandler
End Control
Control PushButton3 Inherits PushButton
ControlInstance PushButton3 Inherits PushButton
EventHandler Sub Action()
cocoaListbox.RecalculateColumnWidths
End EventHandler
End Control
Control PushButton5 Inherits PushButton
ControlInstance PushButton5 Inherits PushButton
EventHandler Sub Action()
cocoaListbox.Sort
xojoListbox.Sort
End EventHandler
End Control
Control CheckBox1 Inherits CheckBox
ControlInstance CheckBox1 Inherits CheckBox
EventHandler Sub Action()
mCellClickResult = me.Value
End EventHandler
End Control
Control CheckBox2 Inherits CheckBox
ControlInstance CheckBox2 Inherits CheckBox
EventHandler Sub Action()
cocoaListbox.HasHeading = me.Value
xojoListbox.HasHeading = me.Value
End EventHandler
EventHandler Sub Open()
me.Value = cocoaListbox.HasHeading
End EventHandler
End Control
Control TextField1 Inherits TextField
ControlInstance TextField1 Inherits TextField
EventHandler Sub TextChange()
dim i as Integer = me.Text.Val
setSortedColumn.Enabled = i >= 1 and i <= cocoaListbox.ColumnCount
End EventHandler
End Control
Control setSortedColumn Inherits PushButton
ControlInstance setSortedColumn Inherits PushButton
EventHandler Sub Action()
dim i as Integer = TextField1.Text.Val
cocoaListbox.SortedColumn = i
xojoListbox.SortedColumn = i
End EventHandler
End Control
Control PushButton6 Inherits PushButton
ControlInstance PushButton6 Inherits PushButton
EventHandler Sub Action()
dim i as Integer = TextField1.Text.Val
cocoaListbox.PressHeader i
xojoListbox.PressHeader i
End EventHandler
End Control
Control PushButton2 Inherits PushButton
ControlInstance PushButton2 Inherits PushButton
EventHandler Sub Action()
self.Width = self.Width + 1
End EventHandler
End Control
Control PushButton7 Inherits PushButton
ControlInstance PushButton7 Inherits PushButton
EventHandler Sub Action()
self.Width = self.Width - 1
End EventHandler
End Control
Control PushButton4 Inherits PushButton
ControlInstance PushButton4 Inherits PushButton
EventHandler Sub Action()
cocoaListbox.updateColumnWidthExpressions
End EventHandler
End Control
Control PushButton8 Inherits PushButton
ControlInstance PushButton8 Inherits PushButton
EventHandler Sub Action()
for row as Integer = cocoaListbox.ListCount-1 downTo 0
if cocoaListbox.Selected (row) then
cocoaListbox.RemoveRow row
end if
next
for row as Integer = xojoListbox.ListCount-1 downTo 0
if xojoListbox.Selected (row) then
xojoListbox.RemoveRow row
end if
next
End EventHandler
End Control
Control cocoaListboxLbl Inherits Label
ControlInstance cocoaListboxLbl Inherits Label
End Control
Control xojoListboxLbl Inherits Label
ControlInstance xojoListboxLbl Inherits Label
End Control
EventHandler Sub Open()
arrangeListBoxes()
End EventHandler
EventHandler Sub Resizing()
arrangeListBoxes
End EventHandler
Sub Constructor()
dim p as new Picture (16, 16)
dim g as Graphics = p.Graphics
g.ForeColor = &cDBE8FB00
g.FillRect 0, 0, g.Width, g.Height
g.ForeColor = &c041DFF00
g.DrawRoundRect 1, 1, g.Width-2, g.Height-2, 8, 8
g.DrawLine 1, 1, g.Width-2, g.Height-2
g.DrawLine 1, g.Height-2, g.Width-2, 1
mSampleImage = p
super.Constructor
End Sub
Protected Sub arrangeListBoxes()
// Make both list boxes share half of the window height each
dim h as Integer = xojoListbox.Height + cocoaListbox.Height
cocoaListbox.Height = h \ 2
dim h2 as Integer = h - cocoaListbox.Height
xojoListbox.Top = xojoListbox.Top - (h2 - xojoListbox.Height)
xojoListbox.Height = h2
xojoListboxLbl.Top = xojoListbox.Top - 20
End Sub
Sub log(msg as String)
LogWin.Log msg
End Sub
Property Private mCellClickResult As Boolean
Property Private mSampleImage As Picture
End Class
Class LogWin Inherits Window
Control TextArea1 Inherits TextArea
ControlInstance TextArea1 Inherits TextArea
End Control
Control PushButton1 Inherits PushButton
ControlInstance PushButton1 Inherits PushButton
EventHandler Sub Action()
TextArea1.Text = ""
End EventHandler
End Control
Sub Log(msg as String)
TextArea1.AppendText msg + EndOfLine
TextArea1.ScrollPosition = 99999
End Sub
End Class
Class ListBoxTV Inherits NSOutlineControlMBS
ComputedProperty AutoHideScrollBars As Boolean
Sub Set()
if not mHadOpenEvent then
mDelayedHideScrollers = value
else
me.autohidesScrollers = value
end if
End Set
Sub Get()
if not mHadOpenEvent then
return mDelayedHideScrollers
else
return me.autohidesScrollers
end if
End Get
End ComputedProperty
ComputedProperty ColumnCount As Integer
Sub Set()
if not mHadOpenEvent then
mDelayedColumnCount = value
else
if value < 1 then value = 1
dim newUbound as Integer = value-1
if newUbound = mCols.Ubound then
// no change
return
end if
// Add columns
while newUbound > mCols.Ubound
dim c as new ListColumnTV (mSelfRef, mCols.Ubound+1)
mCols.Append c
c.title = Str(mCols.Ubound)
mTableView.addTableColumn c
wend
// Remove columns
while mCols.Ubound > newUbound
mTableView.removeTableColumn (mCols.Pop)
wend
// Make first column the one for the outline's expand control
dim cols() as NSTableColumnMBS =mTableView.tableColumns()
mTableView.outlineTableColumn = cols(0) ' use the first column
mDataSource.DataSource_SetColumnCount value
_needsReload
end if
End Set
Sub Get()
if not mHadOpenEvent then
return mDelayedColumnCount
else
return mCols.Ubound+1
end if
End Get
End ComputedProperty
ComputedProperty ColumnWidths As String
Sub Set()
if not mHadOpenEvent then
mDelayedColumnWidths = value
else
dim w() as String
for each s as String in value.Split(",")
w.Append s
next
redim w(mCols.Ubound)
if value = "" or w.Ubound <= 0 then
// User has not set any rules for the widths or has only one column - let's use the TableView's auto resizing
mHasDynamicColumnWidths = false
mTableView.columnAutoresizingStyle = mDefaultColumnAutoresizingStyle
else
// User has set explicit rules for the widths - use our own resizing implementation (see '_recalcColumnWidths')
for i as Integer = 0 to mCols.Ubound
dim col as ListColumnTV = mCols(i)
col.WidthExpression = w(i)
next
end if
end if
End Set
Sub Get()
if not mHadOpenEvent then
return mDelayedColumnWidths
else
dim res() as String
for each col as ListColumnTV in mCols
if mMaintainColumnWidths then
res.Append col.WidthExpression
else
res.Append Str (col.WidthActual,"-#")
end if
next
return Join (res, ",")
end if
End Get
End ComputedProperty
ComputedProperty ColumnsResizable As Boolean
Sub Set()
if not mHadOpenEvent then
mDelayedColumnResizing = value
else
me.allowsColumnResizing = value
end if
End Set
Sub Get()
if not mHadOpenEvent then
return mDelayedColumnResizing
else
return me.allowsColumnResizing
end if
End Get
End ComputedProperty
ComputedProperty DataSource As ListBoxTVDataSource
Sub Set()
if value = nil then
value = new ListBoxTV_Support.ListBoxTVStaticOutlineDataSource (mSelfRef)
end if
mDataSource = value
mDataSource.DataSource_SetColumnCount mCols.Ubound+1
me.Reload() ' needs to reload without delay, or rowCount won't be inquired before a redraw may occur
End Set
Sub Get()
return mDataSource
End Get
End ComputedProperty
ComputedProperty EnableDrag As Boolean
Sub Set()
mEnableDrag = value
End Set
Sub Get()
return mEnableDrag
End Get
End ComputedProperty
ComputedProperty EnableDragReorder As Boolean
Sub Set()
mEnableDragReorder = value
if value then
mTableView.registerForDraggedTypes Array ("private.EnableDragReorder")
end if
End Set
Sub Get()
return mEnableDragReorder
End Get
End ComputedProperty
ComputedProperty GridLinesHorizontal As Integer
Sub Set()
const NSTableViewDashedHorizontalGridLineMask = 8
dim v as Integer
select case value
case ListBox.BorderDefault, ListBox.BorderNone
v = NSTableViewMBS.NSTableViewGridNone
case ListBox.BorderThinDotted
v = NSTableViewMBS.NSTableViewSolidHorizontalGridLineMask or NSTableViewDashedHorizontalGridLineMask
else
v = NSTableViewMBS.NSTableViewSolidHorizontalGridLineMask
end select
me.TableView.gridStyleMask = (me.TableView.gridStyleMask AND NOT (NSTableViewMBS.NSTableViewSolidHorizontalGridLineMask or NSTableViewDashedHorizontalGridLineMask)) OR v
End Set
Sub Get()
const NSTableViewDashedHorizontalGridLineMask = 8
dim v as Integer = me.TableView.gridStyleMask AND (NSTableViewMBS.NSTableViewSolidHorizontalGridLineMask or NSTableViewDashedHorizontalGridLineMask)
if (v AND NSTableViewDashedHorizontalGridLineMask) <> 0 then
return ListBox.BorderThinDotted
elseif v <> 0 then
return ListBox.BorderThinSolid
else
return ListBox.BorderNone
end if
End Get
End ComputedProperty
ComputedProperty GridLinesVertical As Integer
Sub Set()
dim v as Integer
select case value
case ListBox.BorderDefault, ListBox.BorderNone
v = NSTableViewMBS.NSTableViewGridNone
else
v = NSTableViewMBS.NSTableViewSolidVerticalGridLineMask
end select
me.TableView.gridStyleMask = (me.TableView.gridStyleMask AND NOT NSTableViewMBS.NSTableViewSolidVerticalGridLineMask) OR v
End Set
Sub Get()
dim v as Integer = me.TableView.gridStyleMask AND NSTableViewMBS.NSTableViewSolidVerticalGridLineMask
if v <> 0 then
return ListBox.BorderThinSolid
else
return ListBox.BorderNone
end if
End Get
End ComputedProperty
ComputedProperty HasHeading As Boolean
Sub Set()
if value <> me.HasHeading then
if value then
mTableView.headerView = mHeaderView
else
mTableView.headerView = nil
end if
end if
End Set
Sub Get()
return mTableView.headerView <> nil
End Get
End ComputedProperty
ComputedProperty Hierarchical As Boolean
Sub Set()
if value then
mTableView.indentationPerLevel = mOrigIndentationPerLevel
else
mTableView.indentationPerLevel = 0
end
_needsReload()
End Set
Sub Get()
return mTableView.indentationPerLevel <> 0
End Get
End ComputedProperty
ComputedProperty IntercellSpacing As Integer
Sub Set()
dim ss as new NSSizeMBS
ss.Width = value
ss.Height = value
mTableView.intercellSpacing = ss
End Set
End ComputedProperty
ComputedProperty LastIndex As Integer
Sub Get()
return mLastAddedIndex
End Get
End ComputedProperty
ComputedProperty ListCount As Integer
Sub Get()
return mDataSource.DataSource_CurrentRowCount() ' do not call 'mTableView.numberOfRows', as it may not be up-to-date yet
End Get
End ComputedProperty
ComputedProperty ListIndex As Integer
Sub Set()
if value >= 0 and value < me.ListCount then
mTableView.selectRowIndexes (NSIndexSetMBS.indexSetWithIndex(value), false)
else
me.DeselectAll
end if
End Set
Sub Get()
if mSelectionCache.count = 0 then
return -1
else
return mSelectionCache.firstIndex
end if
End Get
End ComputedProperty
ComputedProperty MaintainColumnWidths As Boolean
True: Emulate Xojo's algorithm to keep columns sized based on "%" and "*" values in ColumnWidths or Columns(n).WidthExpression.
False: If ColumnWidths is set, the columns are adjusted once. After that, they are maintained by NSTableView's columnAutoresizingStyle property.
Sub Set()
mMaintainColumnWidths = value
End Set
Sub Get()
return mMaintainColumnWidths
End Get
End ComputedProperty
ComputedProperty RequiresSelection As Boolean
Sub Set()
if not mHadOpenEvent then
mDelayedRequiresSelection = value
else
me.allowsEmptySelection = not value
end if
End Set
Sub Get()
if not mHadOpenEvent then
return mDelayedRequiresSelection
else
return not me.allowsEmptySelection
end if
End Get
End ComputedProperty
ComputedProperty ScrollPosition As Integer
Sub Set()
'
End Set
Sub Get()
return 0
End Get
End ComputedProperty
ComputedProperty ScrollbarHorizontal As Boolean
Sub Set()
if not mHadOpenEvent then
mDelayedHorScroller = value
else
me.hasHorizontalScroller = value
end if
End Set
Sub Get()
if not mHadOpenEvent then
return mDelayedHorScroller
else
return me.hasHorizontalScroller
end if
End Get
End ComputedProperty
ComputedProperty ScrollbarVertical As Boolean
Sub Set()
if not mHadOpenEvent then
mDelayedVerScroller = value
else
me.hasVerticalScroller = value
end if
End Set
Sub Get()
if not mHadOpenEvent then
return mDelayedVerScroller
else
return me.hasVerticalScroller
end if
End Get
End ComputedProperty
ComputedProperty SelectionType As Integer
Sub Set()
if not mHadOpenEvent then
mDelayedSelectionType = value
else
mTableView.allowsMultipleSelection = value <> 0
end if
End Set
Sub Get()
if not mHadOpenEvent then
return mDelayedSelectionType
else
if mTableView.allowsMultipleSelection then
return 1
end if
end if
End Get
End ComputedProperty
ComputedProperty SortedColumn As Integer
Sub Set()
// Note: This method should not depress the header, i.e. not indicate sorting, but it does currently. Not perfect, but should work for most cases.
if not mHadOpenEvent then return
// Re-arrange the existing sort descriptors so that the specified column comes first
dim sorters() as NSSortDescriptorMBS = mTableView.sortDescriptors()
for each sorter as NSSortDescriptorMBS in sorters
dim col as Integer = mTableView.columnWithIdentifier (sorter.key)
if col = value then
// found it
dim i as Integer = sorters.IndexOf (sorter)
if i <> 0 then
sorters.Remove i
sorters.Insert 0, sorter
mTableView.setSortDescriptors sorters
end if
return
end if
next
// If there are no sort descriptors for our column, set it from our ListColumnTV
dim col as ListColumnTV = Column(value)
sorters.Insert 0, col.SortDescriptorPrototype
mTableView.setSortDescriptors sorters
End Set
Sub Get()
dim sorters() as NSSortDescriptorMBS = mTableView.sortDescriptors()
if sorters.Ubound >= 0 then
dim firstSorter as NSSortDescriptorMBS = sorters(0)
return mTableView.columnWithIdentifier (firstSorter.key)
end if
End Get
End ComputedProperty
ComputedProperty TextFont As String
Sub Set()
mTextFont = value
_needsReload
End Set
Sub Get()
return mTextFont
End Get
End ComputedProperty
ComputedProperty TextSize As Integer
Sub Set()
mTextSize = value
_needsReload
End Set
Sub Get()
return mTextSize
End Get
End ComputedProperty
ComputedProperty UseContextualClickEvent As Boolean
Set this to True if you want to use the ContextualMenu events.
Sub Set()
mUseContextualClickEvent = value
if value then
mTableView.menu = mContextMenu
else
mTableView.menu = nil
end if
End Set
Sub Get()
return mUseContextualClickEvent
End Get
End ComputedProperty
ComputedProperty UseFocusRing As Boolean
Sub Set()
if value then
me.ScrollView.focusRingType = NSScrollViewMBS.NSFocusRingTypeExterior
else
me.ScrollView.focusRingType = NSScrollViewMBS.NSFocusRingTypeNone
end if
End Set
Sub Get()
return me.ScrollView.focusRingType = NSScrollViewMBS.NSFocusRingTypeExterior
End Get
End ComputedProperty
Const DragReorderPBType = "private.DragReorder"
Const Support_OSX_10_6 = true
Const Version = 5
Enum columnWidthCalcTypes
actual
minimum
maximum
End Enum
Enum columnWidthTypes
absolute
percentage
remain
End Enum
Event CellAction(row as Integer, column as Integer)
End Event
Event CellBackgroundPaint(g As Graphics, row As Integer, column As Integer, textFrame as NSRectMBS, controlView as NSViewMBS) As Boolean
End Event
Event CellClick(row as Integer, column as Integer, x as Integer, y as Integer) As Boolean
End Event
Event CellTextPaint(g As Graphics, row As Integer, column As Integer, x as Integer, y as Integer, textFrame as NSRectMBS, controlView as NSViewMBS) As Boolean
End Event
Event Change()
End Event
Event CollapseRow(row As Integer)
End Event
Event ColumnsMoved(oldColumn as Integer, newColumn as Integer)
End Event
Event ColumnsResized(column as Integer, oldWidth as Integer)
End Event
Event CompareRows(cell1 as ListCellTV, cell2 as ListCellTV, row1 as Integer, row2 as Integer, column as Integer, ByRef result as Integer) As Boolean
End Event
Event ConstructContextualMenu(base as MenuItem, x as Integer, y as Integer, clickedRow as Integer, clickedColumn as Integer, parentMenu as NSMenuMBS) As Boolean
End Event
Event ConstructContextualMenuForHeader(base as MenuItem, x as Integer, y as Integer, clickedColumn as Integer, parentMenu as NSMenuMBS) As Boolean
End Event
Event ContextualMenuAction(hitItem as MenuItem, clickedRow as Integer, clickedColumn as Integer) As Boolean
End Event
Event DenyReorderColumn(fromColumn as Integer, toColumn as Integer) As Boolean
End Event
Event DidResizeCells()
End Event
Event DragReorderColumns(movedColumn as integer, insertColumn as Integer)
End Event
Event DragReorderRows(newPosition as Integer, parentRow as Integer) As Boolean
End Event
Event DragRow(drag as DragItemTV, row as Integer, ByRef operationMask as Integer, ByRef thisAppOnly as Boolean) As Boolean
End Event
Event DropObject(obj as DragItemTV, operationMask as Integer, aboveRow as Integer, draggingInfo as NSDraggingInfoMBS)
End Event
Event ExpandRow(row as Integer)
End Event
Event HeaderPressed(column as Integer) As Boolean
End Event
Event MouseDown(x as Integer, y as Integer) As Boolean
End Event
Event Open()
End Event
Event SortColumn(column as Integer) As Boolean
End Event
Event WillDisplayCell(row as Integer, column as Integer, cell as ListCellTV, textFieldCell as NSCellMBS, tableColumn as NSTableColumnMBS) As Boolean
End Event
Event didTile()
End Event
EventHandler Sub ColumnDidMove(notification as NSNotificationMBS, OldColumn as Integer, NewColumn as Integer)
mInsideHeaderDrag = false
mIgnoreTileEvents = false
dim indexes() as Integer
for each col as ListColumnTV in mCols
col._updateOwnIndex
indexes.Append col.ColumnIndex
next
indexes.SortWith mCols
mDataSource.DataSource_MoveColumn (oldColumn, newColumn)
RaiseEvent ColumnsMoved(oldColumn, newColumn)
End EventHandler
EventHandler Sub ColumnDidResize(notification as NSNotificationMBS, tableColumn as NSTableColumnMBS, OldWidth as Double)
if mInsideHeaderDrag then
mInsideHeaderDrag = false
mIgnoreTileEvents = false
me.updateColumnWidthExpressions
// Does the cursor remain in "Resizing" state? This happens only with Real Studio 2012, however, but not with Xojo 2016.
end if
if not mIgnoreTileEvents then
RaiseEvent ColumnsResized (ListColumnTV(tableColumn).ColumnIndex, oldWidth)
end
End EventHandler
EventHandler Function ConstructContextualMenu(base as MenuItem, x as Integer, y as Integer) As Boolean
'
End EventHandler
EventHandler Function ContextualMenuAction(hitItem as MenuItem) As Boolean
'
End EventHandler
EventHandler Function DragEnter(obj As DragItem, action As Integer) As Boolean
'
End EventHandler
EventHandler Sub DragExit(obj As DragItem, action As Integer)
'
End EventHandler
EventHandler Function DragOver(x As Integer, y As Integer, obj As DragItem, action As Integer) As Boolean
'
End EventHandler
EventHandler Sub DropObject(obj As DragItem, action As Integer)
break ' this would be unexpected unless the user invoked AcceptDrop... on the Xojo control directly, circumventing our ListBoxTV functions.
End EventHandler
EventHandler Sub ItemDidCollapse(notification as NSNotificationMBS, item as NSOutlineViewItemMBS)
didCollapse ListRowTV(item)
End EventHandler
EventHandler Sub ItemDidExpand(notification as NSNotificationMBS, item as NSOutlineViewItemMBS)
expandEnd ListRowTV(item)
End EventHandler
EventHandler Sub ItemWillExpand(notification as NSNotificationMBS, item as NSOutlineViewItemMBS)
expandBegin ListRowTV(item)
End EventHandler
EventHandler Function MouseDown(x as Integer, y as Integer, Modifiers as Integer) As Boolean
return handleMouseDown (x, y, modifiers)
End EventHandler
EventHandler Sub Open()
me.setDelayedProperties()
me.setupFromInitialValue()
mCurrentRootItem = nil
mLastAddedIndex = -1
RaiseEvent Open()
End EventHandler
EventHandler Sub SelectionDidChange(notification as NSNotificationMBS)
mSelectionCache = mTableView.selectedRowIndexes()
RaiseEvent Change()
End EventHandler
EventHandler Function acceptDrop(info as NSDraggingInfoMBS, item as NSOutlineViewItemMBS, index as Integer) As Boolean
// Finishes the drop
// item is the parent item, and index the child's position where to insert the dropped items
dim pb as NSPasteboardMBS = info.draggingPasteboard
dim operationMask as Integer = info.draggingSourceOperationMask
dim row as Integer
if index >= 0 then
// -> drag between rows
dim childItem as NSOutlineViewItemMBS
childItem = mTableView.child (index, item)
if childItem = nil then
// index is past the last child of the parent item -> append to end of children
row = mTableView.rowForItem (item) + 1 + mTableView.numberOfChildrenOfItem (item)
else
row = mTableView.rowForItem (childItem)
end if
else
// -> drag onto a row
row = mTableView.rowForItem (item)
end if
dim done as Boolean
// Handle "drag reorder"
if mEnableDragReorder and info.draggingSource = mTableView then
dim mb as MemoryBlock = pb.dataForType(DragReorderPBType)
if mb <> nil then
dim srcRows() as Integer
for i as Integer = 0 to mb.Size-1 step 4
srcRows.Append mb.Int32Value(i)
next
ListBoxTV(me).SelectRows srcRows ' makes sure the moved rows are selected because the DragReorderRows event expects that
if not RaiseEvent DragReorderRows (row, -1) then
mDataSource.DataSource_MoveRows (srcRows, row)
mTableView.reloadData // this call is needed as well, or the following redraw might keep showing wrong cell content
self._needsReload // so that every cell's WillDisplayCell event is called in case they moved
end if
done = true ' -> drop was successful
end if
end if
if not done then
// Send it to the DropObject event handler
dim dragItems as new DragItemTV (pb)
RaiseEvent DropObject (dragItems, operationMask, row, info) ' we should pass "Action as Integer" here, but I don't know how that translation from the operationMask is supposed to be done.
end
#if DebugBuild
self._DebugCheck
#endif
return true
End EventHandler
EventHandler Function childOfItem(index as Integer, item as NSOutlineViewItemMBS) As NSOutlineViewItemMBS
return mDataSource.DataSource_ChildItem (ListRowTV(item), index)
End EventHandler
EventHandler Function dataCell(tableColumn as NSTableColumnMBS, item as NSOutlineViewItemMBS) As NSCellMBS
if tableColumn = nil then
return nil
end if
dim tcol as ListColumnTV = ListColumnTV(tableColumn)
dim col as Integer = tcol.ColumnIndex
dim myCell as ListCellTV = _cell(item, col)
dim t as Integer = myCell.Type
if t = ListBox.TypeCheckbox or (t = ListBox.TypeDefault and tcol.Type = ListBox.TypeCheckbox) then
// A checkbox cell
static bc as NSButtonCellMBS
if bc = nil then ' we only need to create this once
bc = new NSButtonCellMBS ("x")
declare sub setButtonType lib "Cocoa" selector "setButtonType:" (h as Integer, t as Integer)
setButtonType (bc.Handle, NSButtonMBS.NSSwitchButton)
bc.wraps = false
bc.lineBreakMode = NSParagraphStyleMBS.NSLineBreakByTruncatingTail
end if
return bc
end if
// We return a custom NSTextFieldCell, in order to perform some custom painting in its drawWithFrame event handler.
// We are lazy and re-use the same cell, because we keep getting this event called for each drawn cell, sequentially.
static cc as ListBoxTV_Support.ListDrawCellHandler
if cc = nil then
cc = new ListBoxTV_Support.ListDrawCellHandler (mSelfRef)
cc.editable = true
cc.wraps = false
cc.lineBreakMode = NSParagraphStyleMBS.NSLineBreakByTruncatingTail
end if
return cc
End EventHandler
EventHandler Sub didClickTableColumn(tableColumn as NSTableColumnMBS)
mInsideHeaderDrag = false
mIgnoreTileEvents = false
dim column as Integer = ListColumnTV(tableColumn).ColumnIndex
if RaiseEvent HeaderPressed (column) then
// handled
return
end if
ListBoxTV(me).SortedColumn = column
_sort
End EventHandler
EventHandler Sub didDragTableColumn(tableColumn as NSTableColumnMBS)
mInsideHeaderDrag = false
mIgnoreTileEvents = false
End EventHandler
EventHandler Sub didTile()
RaiseEvent didTile
if not mIgnoreTileEvents then
if mMaintainColumnWidths then
_recalcColumnWidths()
end if
end if
End EventHandler
EventHandler Function isItemExpandable(item as NSOutlineViewItemMBS) As Boolean
if mTableView.indentationPerLevel = 0 then
' non-hierarchical
return false
end
return mDataSource.DataSource_IsExpandable (ListRowTV (item))
End EventHandler
EventHandler Sub mouseDownInHeaderOfTableColumn(tableColumn as NSTableColumnMBS)
if mMaintainColumnWidths then
mIgnoreTileEvents = true
mInsideHeaderDrag = true
end if
End EventHandler
EventHandler Function numberOfChildrenOfItem(item as NSOutlineViewItemMBS) As Integer
return mDataSource.DataSource_ChildCount (ListRowTV(item))
End EventHandler
EventHandler Function objectValue(tableColumn as NSTableColumnMBS, item as NSOutlineViewItemMBS) As Variant
dim col as Integer = ListColumnTV(tableColumn).ColumnIndex
dim myCell as ListCellTV = _cell (item, col, true)
dim t as Integer = myCell.Type
if t = ListBox.TypeCheckbox or (t = ListBox.TypeDefault and ListColumnTV(tableColumn).Type = ListBox.TypeCheckbox) then
// This is a checkbox - we return the Checkbox value, not its title text
return myCell.Checked
end
return myCell.Text
End EventHandler
EventHandler Sub setObjectValue(tableColumn as NSTableColumnMBS, item as NSOutlineViewItemMBS, value as Variant)
dim col as Integer = ListColumnTV(tableColumn).ColumnIndex
dim row as Integer = mTableView.rowForItem (item)
dim myCell as ListCellTV = _cell (item, col)
dim isCheckbox as Boolean
dim t as Integer = myCell.Type
if t = ListBox.TypeCheckbox or (t = ListBox.TypeDefault and ListColumnTV(tableColumn).Type = ListBox.TypeCheckbox) then
isCheckbox = true
end
mDataSource.DataSource_CellWillUpdate (myCell, ListRowTV(item), col, not isCheckbox, isCheckbox)
if isCheckbox then
myCell.Checked = value.BooleanValue
elseif value isA NSAttributedStringMBS then
myCell.Text = NSAttributedStringMBS(value).text
else
myCell.Text = value.StringValue
end
mDataSource.DataSource_CellDidUpdate (myCell, ListRowTV(item), col, not isCheckbox, isCheckbox)
RaiseEvent CellAction (row, col)
End EventHandler
EventHandler Function shouldEdit(tableColumn as NSTableColumnMBS, item as NSOutlineViewItemMBS) As Boolean
dim t as Integer = ListColumnTV(tableColumn).EffectiveType(item)
return t = ListBox.TypeEditable or t = ListBox.TypeCheckbox
End EventHandler
EventHandler Function shouldReorderColumn(columnIndex as Integer, newColumnIndex as Integer) As Boolean
return not RaiseEvent DenyReorderColumn (columnIndex, newColumnIndex)
End EventHandler
EventHandler Function validateDrop(info as NSDraggingInfoMBS, proposedItem as NSOutlineViewItemMBS, proposedChildIndex as Integer) As Integer
// Determines whether the current row is accepted as a drop target
if mEnableDragReorder then
dim operationMask as Integer = info.draggingSourceOperationMask
// dragging a folder into one of its children is not allowed!
if info.draggingSource = mTableView then
dim pb as NSPasteboardMBS = info.draggingPasteboard
dim mb as MemoryBlock = pb.dataForType(DragReorderPBType)
if mb <> nil then
dim srcRows() as Integer
for i as Integer = 0 to mb.Size-1 step 4
srcRows.Append mb.Int32Value(i)
next
for each row as Integer in srcRows
dim item as NSOutlineViewItemMBS = _rowItemForRow(row)
if itemIsSameOrParentOf (item, proposedItem) then
// disallow this drop
operationMask = 0
exit
end if
next
end if
end if
return operationMask
end if
End EventHandler
EventHandler Sub willDisplayCell(cell as NSCellMBS, tableColumn as NSTableColumnMBS, item as NSOutlineViewItemMBS)
#if Support_OSX_10_6
// We're using outdated NSCells instead of NSViews so that we can keep using this control in OSX 10.6.
// If you prefer to use custom NSViews (e.g. in order to add buttons, input fields, checkboxes or more labels),
// implement the ListBoxTV.view event - if there's code in that event, then this ListBoxTV.willDisplayCell won't
// be invoked, i.e. you end up with a modern NSView based TableView.
dim col as Integer = ListColumnTV(tableColumn).ColumnIndex
dim myCell as ListCellTV = _cell (item, col)
dim tfc as NSTextFieldCellMBS
dim bc as NSButtonCellMBS
if cell isA NSButtonCellMBS then
bc = NSButtonCellMBS(cell)
else
tfc = NSTextFieldCellMBS(cell)
end if
// Preset font
if mTextFont <> "" and mTextFont <> "System" then
cell.font = NSFontMBS.fontWithName (mTextFont, mTextSize)
elseif mTextSize > 0 then
cell.font = NSFontMBS.systemFontOfSize (mTextSize)
end if
dim row as Integer = myCell._internalUse.Left
if col <> myCell._internalUse.Right then break
_suppressReload = true ' in case the event changes some properties
dim handled as Boolean = RaiseEvent WillDisplayCell (row, col, myCell, cell, tableColumn)
_suppressReload = false
if not handled then
// Button title
if bc <> nil then
bc.title = myCell.text
end if
// Cell text color
if tfc <> nil then
if myCell.TextColor <> &c00000000 then
dim xc as Color = myCell.TextColor
dim c as NSColorMBS = NSColorMBS.colorWithCalibratedRGB (xc.Red/255, xc.Green/255, xc.Blue/255, 1-xc.Alpha/255)
tfc.textColor = c
else
tfc.textColor = nil
end if
end if
// Cell background color
if myCell.BackgroundColor <> &cFFFFFFFF then
dim xc as Color = myCell.BackgroundColor
dim c as NSColorMBS = NSColorMBS.colorWithCalibratedRGB (xc.Red/255, xc.Green/255, xc.Blue/255, 1-xc.Alpha/255)
if bc <> nil then
bc.backgroundColor = c
else
tfc.backgroundColor = c
#if false ' we handle this in ListDrawCellHandler.drawWithFrame so that it also works with indented text
tfc.drawsBackground = true
#endif
end if
elseif bc <> nil then
bc.backgroundColor = nil
else
tfc.backgroundColor = nil
tfc.drawsBackground = false
end if
// Cell text alignment
dim align as Integer
if myCell.alignment <> ListBox.AlignDefault then
align = myCell.alignment
else
align = ListColumnTV(tableColumn).Alignment
end if
select case align
case ListBox.AlignDefault
align = NSTextMBS.NSNaturalTextAlignment
case ListBox.AlignLeft
align = NSTextMBS.NSLeftTextAlignment
case ListBox.AlignRight
align = NSTextMBS.NSRightTextAlignment
case ListBox.AlignCenter
align = NSTextMBS.NSCenterTextAlignment
case ListBox.AlignDecimal
align = NSTextMBS.NSJustifiedTextAlignment
end select
cell.alignment = align
// Cell text indentation and background drawing (handled by ListDrawCellHandler.drawWithFrame)
if tfc <> nil then
if tfc isA ListBoxTV_Support.ListDrawCellHandler then
ListBoxTV_Support.ListDrawCellHandler(tfc).cell = myCell
end if
else
' unfortunately, we cannot indent checkbox cells
end if
end if
#endif
End EventHandler
EventHandler Function writeItems(items() as NSOutlineViewItemMBS, pasteboard as NSPasteboardMBS) As Boolean
dim didAdd as Boolean
call pasteboard.clearContents
dim rows() as Integer = rowsFromItems (items)
if rows.Ubound < 0 then return false
if mEnableDrag then
ListBoxTV(me).SelectRows rows ' makes sure the dragged rows are selected because the DragRow event expects that
dim drag as new DragItemTV (pasteboard)
dim operationMask as Integer = NSDraggingInfoMBS.NSDragOperationEvery // Xojo uses NSDragOperationEvery, Real Studio used NSDragOperationAll_Obsolete
dim locally as Boolean = false
if RaiseEvent DragRow (drag, rows(0), operationMask, locally) then
drag.FinishAddedItems
mTableView.setDraggingSourceOperationMask (operationMask, locally)
didAdd = true
end if
end if
if mEnableDragReorder then
// Put the moved rows into a MemoryBlock and pass that as a private data item in the pasteboard
dim mb as new MemoryBlock (4 * (rows.Ubound+1))
for i as Integer = 0 to rows.Ubound
mb.Int32Value (4*i) = rows(i)
next
pasteboard.dataForType (DragReorderPBType) = mb
if not didAdd then
me.View.setDraggingSourceOperationMask (NSDraggingInfoMBS.NSDragOperationMove, true)
end
didAdd = true
end if
return didAdd
End EventHandler
Sub AcceptFileDrop(type As String)
mTableView.registerForDraggedTypes Array (NSPasteboardMBS.NSFilenamesPboardType, "public.file-url")
End Sub
Sub AcceptMacDataDrop(type As String)
me.AcceptRawDataDrop (type)
End Sub
Sub AcceptPictureDrop()
break ' missing (needs methods in DragItemTV, too)
End Sub
Sub AcceptRawDataDrop(type As String)
mTableView.registerForDraggedTypes Array (ListBoxTV_Support.convertOSType(type))
End Sub
Sub AcceptTextDrop()
mTableView.registerForDraggedTypes Array (NSPasteboardMBS.NSStringPboardType, NSPasteboardMBS.NSPasteboardTypeString)
End Sub
Sub AddFolder(txt as String)
addItem txt, true
End Sub
Sub AddRow(items() as String)
addItems items
End Sub
Sub AddRow(ParamArray items as String)
addItems items
End Sub
Sub BeginUpdates()
mTableView.beginUpdates
End Sub
Function Cell(row as Integer, column as Integer) As String
return _cell(row,column).text
End Function
Sub Cell(row as Integer, column as Integer, assigns v as String)
_cell(row,column).text = v
_needsReload()
End Sub
Function CellAlignment(row as Integer, column as Integer) As Integer
dim cell as ListCellTV = _cell(row, column)
return cell.alignment
End Function
Sub CellAlignment(row as Integer, column as Integer, assigns v as Integer)
dim cell as ListCellTV = _cell(row, column)
if cell.alignment <> v then
cell.alignment = v
_needsReload()
end if
End Sub
Function CellAlignmentOffset(row as Integer, column as Integer) As Integer
dim cell as ListCellTV = _cell(row, column)
return cell.Indentation
End Function
Sub CellAlignmentOffset(row as Integer, column as Integer, assigns v as Integer)
dim cell as ListCellTV = _cell(row, column)
if cell.Indentation <> v then
cell.Indentation = v
_needsReload()
end if
End Sub
Function CellBold(row as Integer, column as Integer) As Boolean
return _cell(row, column).Bold
End Function
Sub CellBold(row as Integer, column as Integer, assigns v as Boolean)
if _cell(row, column).Bold <> v then
_cell(row, column).Bold = v
_needsReload()
end if
End Sub
Function CellCheck(row as Integer, column as Integer) As Boolean
return _cell(row, column).Checked
End Function
Sub CellCheck(row as Integer, column as Integer, assigns v as Boolean)
if _cell(row, column).Checked <> v then
_cell(row, column).Checked = v
_needsReload()
end if
End Sub
Function CellHelpTag(row as Integer, column as Integer) As String
return _cell(row, column).ToolTip
End Function
Sub CellHelpTag(row as Integer, column as Integer, assigns v as String)
_cell(row, column).ToolTip = v
End Sub
Function CellItalic(row as Integer, column as Integer) As Boolean
return _cell(row, column).Italic
End Function
Sub CellItalic(row as Integer, column as Integer, assigns v as Boolean)
if _cell(row, column).Italic <> v then
_cell(row, column).Italic = v
_needsReload()
end if
End Sub
Function CellStyle(row as Integer, column as Integer) As ListCellTV
return _cell(row,column)
End Function
Function CellTag(row as Integer, column as Integer) As Variant
return _cell(row,column).tag
End Function
Sub CellTag(row as Integer, column as Integer, assigns v as Variant)
_cell(row,column).tag = v
End Sub
Function CellType(row as Integer, column as Integer) As Integer
dim cell as ListCellTV = _cell(row, column)
return cell.Type
End Function
Sub CellType(row as Integer, column as Integer, assigns v as Integer)
dim cell as ListCellTV = _cell(row, column)
if cell.Type <> v then
cell.Type = v
_needsReload()
end if
End Sub
Function Column(col as Integer) As ListColumnTV
return mCols(col)
End Function
Function ColumnAlignment(column as Integer) As Integer
return mCols(column).Alignment
End Function
Sub ColumnAlignment(column as Integer, assigns v as Integer)
mCols(column).Alignment = v
End Sub
Function ColumnAlignmentOffset(column as Integer) As Integer
return mCols(column).AlignmentOffset
End Function
Sub ColumnAlignmentOffset(column as Integer, assigns v as Integer)
mCols(column).AlignmentOffset = v
End Sub
Function ColumnFromXY(x as Integer, y as Integer) As Integer
dim loc0 as new NSPointMBS (x, y)
dim loc as NSPointMBS = mTableView.convertPointFromView (loc0, me.ScrollView)
return mTableView.columnAtPoint (loc)
End Function
Function ColumnSortDirection(column as Integer) As Integer
return mCols(column).SortDirection
End Function
Sub ColumnSortDirection(column as Integer, assigns dir as Integer)
mCols(column).SortDirection = dir
End Sub
Function ColumnType(column as Integer) As Integer
return mCols(column).Type
End Function
Sub ColumnType(column as Integer, assigns v as Integer)
mCols(column).Type = v
End Sub
Sub Constructor()
// We create a weak ref to ourselves so that we can pass that to our chiild classes instead of them creating more WeakRefs (this is a performance optimization)
mSelfRef = new WeakRef (me)
// Our separate class for handling listbox content (can be replaced by user)
mDataSource = new ListBoxTV_Support.ListBoxTVStaticOutlineDataSource (mSelfRef)
// Set up some local properties for cleaner access to some tableview properties
mTableView = me.View
mHeaderView = mTableView.headerView
// Context Menu preparation for Cells
mContextMenu = new ListBoxTV_Support.ListBoxTVContextMenu (mSelfRef)
// Context Menu preparation for the Header, see http://stackoverflow.com/a/3850215/43615
mTableView.headerView.menu = mContextMenu
// Let's preset the columns to automatic resizing - can be overwritten by code in Open() event or later, or disabled by setting ColumnWidths to a non-empty string.
mDefaultColumnAutoresizingStyle = NSTableViewMBS.NSTableViewUniformColumnAutoresizingStyle
mTableView.columnAutoresizingStyle = mDefaultColumnAutoresizingStyle
// We cache the selection for performance reasons (not sure if that's necessary, though)
mSelectionCache = NSIndexSetMBS.indexSet()
mOrigIndentationPerLevel = mTableView.indentationPerLevel
mTableView.indentationPerLevel = 0 ' set this to zero for non-hierarchical lists
mTableView.autoresizesOutlineColumn = false
super.Constructor
End Sub
Sub DeleteAllRows()
mDataSource.DataSource_DeleteAllRows
me.ScrollPosition = 0
mCurrentRootItem = nil
mLastAddedIndex = -1
me.Reload() ' needs to reload without delay, or rowCount won't be inquired before a redraw may occur
End Sub
Sub DeselectAll()
mTableView.deselectAll
End Sub
Sub EndUpdates()
mTableView.endUpdates
End Sub
Function Expanded(row as Integer) As Boolean
dim rowItem as ListRowTV = _rowItemForRow (row)
return mTableView.isItemExpanded (rowItem)
End Function
Sub Expanded(row as Integer, assigns expand as Boolean)
dim rowItem as ListRowTV = _rowItemForRow (row)
if mTableView.isExpandable (rowItem) then
if expand then
expand rowItem
else
mTableView.collapseItem rowItem, false
end if
end if
End Sub
Function HeaderHeight() As Integer
return mTableView.headerView.frame.Height
End Function
Function Heading(col as Integer) As String
return mCols(col).headerCell.stringValue
End Function
Sub Heading(col as Integer, assigns txt as String)
mCols(col).headerCell.stringValue = txt
End Sub
Sub InvalidateCell(row as Integer, column as Integer)
// We're lazy and simply reload the entire view's cells.
// Ideally, we're determine the rect of the row/col and then call "setNeedsDisplayInRect:" instead.
_needsReload()
End Sub
Function List(row as Integer) As String
return me.Cell (row, 0)
End Function
Sub List(row as Integer, assigns v as String)
me.Cell (row, 0) = v
End Sub
Sub PressHeader(col as Integer)
// This method, contrary to just setting SortedColumn and calling Sort, should also depress the header, whereas SortedColumn should not. Doesn't work right yet, though
me.SortedColumn = col
me.Sort
End Sub
Sub RecalculateColumnWidths()
// Call this after a column or the entire control got resized and you want to keep the "*" or "%" column widths re-applied
_recalcColumnWidths()
End Sub
Sub Reload()
_reloadNow(nil)
End Sub
Sub RemoveRow(row as Integer, animate as Boolean = false)
if mDataSource isA ListBoxTV_Support.ListBoxTVStaticOutlineDataSource then
dim animationOptions as Integer
if animate then
const NSTableViewAnimationEffectFade = 1
const NSTableViewAnimationSlideUp = 16
animationOptions = NSTableViewAnimationEffectFade or NSTableViewAnimationSlideUp
end if
mTableView.beginUpdates
ListBoxTV_Support.ListBoxTVStaticOutlineDataSource(mDataSource).RemoveRow (row, animationOptions)
mTableView.endUpdates
mLastAddedIndex = -1
else
break // If you have assigned your own DataSource, then you cannot use RemoveRow() as that makes little sense
end if
_needsReload ' we reload lazily so that multiple calls to AddRow won't cause many reloads
End Sub
Function RowForItem(rowItem as ListRowTV) As Integer
return mTableView.rowForItem (rowItem)
End Function
Function RowFromXY(x as Integer, y as Integer) As Integer
dim loc0 as new NSPointMBS (x, y)
dim loc as NSPointMBS = mTableView.convertPointFromView (loc0, me.ScrollView)
return mTableView.rowAtPoint (loc)
End Function
Function RowHeight() As Integer
return Round (mTableView.rowHeight)
End Function
Function RowPicture(row as Integer) As Picture
return _cell(row,0).picture
End Function
Sub RowPicture(row as Integer, assigns pic as Picture)
_cell(row,0).picture = pic
End Sub
Function RowTag(row as Integer) As Variant
return ListRowTV(_rowItemForRow(row)).tag
End Function
Sub RowTag(row as Integer, assigns tag as Variant)
ListRowTV(_rowItemForRow(row)).tag = tag
End Sub
Function SelCount() As Integer
return mSelectionCache.count
End Function
Sub SelectRows(rows() as Integer)
dim set as new NSMutableIndexSetMBS
for each row as Integer in rows
set.addIndex row
next
mSelectionCache = set
mTableView.selectRowIndexes mSelectionCache, false
End Sub
Function Selected(row as Integer) As Boolean
return mSelectionCache.containsIndex (row)
End Function
Sub Selected(row as Integer, assigns sel as Boolean)
dim isSelected as Boolean = mSelectionCache.containsIndex (row)
if isSelected <> sel then
mSelectionCache = mSelectionCache.mutableCopy
if sel then
NSMutableIndexSetMBS(mSelectionCache).addIndex row
else
NSMutableIndexSetMBS(mSelectionCache).removeIndex row
end if
mTableView.selectRowIndexes mSelectionCache, false
end if
End Sub
Function SelectedRows() As Integer()
return mSelectionCache.Values
End Function
Sub Sort()
_sort
End Sub
Function TableView() As NSOutlineViewMBS
return mTableView
End Function
Sub UpdateColumnWidthExpressions()
// Takes the current column widths and adjusts the ListColumnTV.WidthExpression values accordingly
_suppressReload = true
dim colSpacing as Integer = mTableView.intercellSpacing.Width // extra space that every visible column occupies
// Determine total width first
dim totalWidth as Integer
for col as Integer = 0 to mCols.Ubound
dim c as ListColumnTV = mCols(col)
dim w as Integer = Round (c.width)
if w > 0 then w = w + colSpacing
totalWidth = totalWidth + w
next
// Now update the width expressions (absolute and percentage)
dim remainWidth as Integer = totalWidth
dim asteriskValues() as Double
dim lowestAsteriskWidth as Integer = totalWidth
for col as Integer = 0 to mCols.Ubound
dim c as ListColumnTV = mCols(col)
dim w as Integer = Round (c.width)
dim expr as String = mCols(col).WidthExpression
dim v as Double
dim t as columnWidthTypes = determineColumnWidthType (columnWidthCalcTypes.actual, expr, v)
if t = columnWidthTypes.absolute or (t = columnWidthTypes.remain and w = 0) then
c.WidthExpression = Str(w,"-#")
asteriskValues.Append 0
if w > 0 then w = w + colSpacing
remainWidth = remainWidth - w
elseif t = columnWidthTypes.percentage then
c.WidthExpression = ListBoxTV_Support.trimPeriod (Str (w / totalWidth * 100,"-#.#####")) + "%"
asteriskValues.Append 0
if w > 0 then w = w + colSpacing
remainWidth = remainWidth - w
else
asteriskValues.Append w
remainWidth = remainWidth - colSpacing
if w < lowestAsteriskWidth then lowestAsteriskWidth = w
end
next
// Finally, update the asterisk width expressions
dim scale as Double = remainWidth / lowestAsteriskWidth // so that we don't have any asterisk values below 1
for col as Integer = 0 to mCols.Ubound
dim w as Integer = asteriskValues(col)
if w <> 0 then
dim c as ListColumnTV = mCols(col)
c.WidthExpression = ListBoxTV_Support.trimPeriod (Str (Max (1, w / remainWidth * scale),"-#.########")) + "*"
end if
next
finally
_suppressReload = false
End Sub
Sub _DebugCheck()
// For debugging only.
// Checks the integrity of the data
#if DebugBuild
if mDataSource isA ListBoxTV_Support.ListBoxTVStaticOutlineDataSource then
ListBoxTV_Support.ListBoxTVStaticOutlineDataSource(mDataSource)._DebugCheck
end if
#endif
End Sub
Function _cell(row as Integer, column as Integer, ignoreCache as Boolean = false) As ListCellTV
return _cell (_rowItemForRow (row), column, ignoreCache, row)
End Function
Function _cell(rowItem as NSOutlineViewItemMBS, column as Integer, ignoreCache as Boolean = false, row as Integer = -1) As ListCellTV
if rowItem = nil then
return nil
end if
if not ignoreCache then
if mPreviousCell <> nil and mPreviousItem = rowItem and mPreviousCell._internalUse.Right = column then
// Usually, we get three calls for the same cell in sequence - 1st from objectValue, 2nd from dataCellForTableColumn, 3rd from willDisplayCell
// We cache the cell here for performance reasons
return mPreviousCell
end if
end if
if row < 0 then
if mPreviousItem = rowItem then
row = mPreviousItemRow
else
row = mTableView.rowForItem (rowItem)
end if
if row < 0 then break
end if
dim cell as ListCellTV = mDataSource.DataSource_Cell (ListRowTV(rowItem), column)
if cell = nil then
// create an empty one
cell = new ListCellTV ()
end if
cell._internalUse = row : column
mPreviousCell = cell
mPreviousItem = rowItem
mPreviousItemRow = row
return cell
End Function
Function _cellBackgroundPaint(g As Graphics, row As Integer, column As Integer, textFrame as NSRectMBS, controlView as NSViewMBS) As Boolean
return RaiseEvent CellBackgroundPaint (g, row, column, textFrame, controlView)
End Function
Function _cellTextPaint(g As Graphics, row As Integer, column As Integer, x as Integer, y as Integer, textFrame as NSRectMBS, controlView as NSViewMBS) As Boolean
return RaiseEvent CellTextPaint (g, row, column, x, y, textFrame, controlView)
End Function
Function _clickedColumn() As Integer
return mTableView.clickedColumn
End Function
Function _clickedRow() As Integer
return mTableView.clickedRow
End Function
Function _compareRows(c1 as ListCellTV, c2 as ListCellTV, ByRef result as Integer) As Boolean
return RaiseEvent CompareRows (c1, c2, c1._internalUse.Left, c2._internalUse.Left, c1._internalUse.Right, result)
End Function
Function _constructContextualMenu(base as MenuItem, x as Integer, y as Integer, row as Integer, column as Integer, menu as NSMenuMBS) As Boolean
dim res as Boolean
mSelectionCache = mTableView.selectedRowIndexes()
if row < 0 then
// Header click
res = RaiseEvent ConstructContextualMenuForHeader (base, x, y, column, menu)
else
// Cell click
res = RaiseEvent ConstructContextualMenu (base, x, y, row, column, menu)
end if
return res
End Function
Sub _contextualMenuAction(hitItem as MenuItem, row as Integer, column as Integer)
call RaiseEvent ContextualMenuAction (hitItem, row, column)
End Sub
Private Function _hasFocus() As Boolean
return me.Window.Focus = me
End Function
Sub _needsReload(needsRecalcColumnWidths as Boolean = false, enableXojoColumnWidths as Boolean = false)
if _suppressReload then
return
end if
clearRowCache
if needsRecalcColumnWidths then
// A column's WidthActual or WidthExpression was set
mColumnWidthsDirty = true
end if
if enableXojoColumnWidths then
// This means a WidthExpression (or ColumnWidths) property was set. This means the user
// may want to use the dynamic width modifiers ("%" and "*") from Xojo instead of the default
// NSTableView's columnAutoresizingStyle modes.
mHasDynamicColumnWidths = true
mTableView.columnAutoresizingStyle = NSTableViewMBS.NSTableViewLastColumnOnlyAutoresizingStyle
end if
if mReloadTimer = nil then
mReloadTimer = new Timer
AddHandler mReloadTimer.Action, AddressOf _reloadNow
mReloadTimer.Period = 0
end if
mReloadTimer.Mode = Timer.ModeSingle
mReloadTimer.Reset
End Sub
Private Sub _recalcColumnWidths()
#pragma DisableBackgroundTasks
mColumnWidthsDirty = false
mTableView.columnAutoresizingStyle = NSTableViewMBS.NSTableViewLastColumnOnlyAutoresizingStyle
dim totalWidth as Integer = me.ScrollView.documentVisibleRect.Width
dim remainingWidth as Integer = totalWidth
dim remainingMinWidth as Integer = totalWidth
dim remainingMaxWidth as Integer = totalWidth
dim asteriskCount, asteriskCountMin, asteriskCountMax as Double, asteriskColumnCount as Integer
dim actualWidth(), minWidth(), maxWidth() as Integer
dim colSpacing as Integer = mTableView.intercellSpacing.Width // extra space that every visible column occupies
// first pass - determine the space occupied by non-asterisk columns
for col as Integer = 0 to mCols.Ubound
dim minW, maxW, actW as Integer, type as columnWidthTypes
type = calcColumnWidth (columnWidthCalcTypes.actual, mCols(col).WidthExpression, totalWidth, mCols.Ubound-col, colSpacing, asteriskCount, remainingWidth, actW)
call calcColumnWidth (columnWidthCalcTypes.minimum, mCols(col).MinWidthExpression, totalWidth, mCols.Ubound-col, colSpacing, asteriskCountMin, remainingMinWidth, minW)
call calcColumnWidth (columnWidthCalcTypes.maximum, mCols(col).MaxWidthExpression, totalWidth, mCols.Ubound-col, colSpacing, asteriskCountMax, remainingMaxWidth, maxW)
actualWidth.Append actW
minWidth.Append minW
maxWidth.Append maxW
if type = columnWidthTypes.remain then
asteriskColumnCount = asteriskColumnCount + 1
end
if type = columnWidthTypes.absolute and actW <= 0 and minW <= 0 then
// hide this column
if not mCols(col).Hidden then
mCols(col).Hidden = true
end if
else
// unhide this column
if mCols(col).Hidden then
mCols(col).Hidden = false
end if
end if
next
// second pass - determine the space occupied by the remaining asterisk columns
if asteriskCount > 0 then
remainingWidth = Round (Max (0, remainingWidth - asteriskColumnCount * colSpacing))
dim asteriskWidth as Double = remainingWidth / asteriskCount
dim lastAsteriskCol as Integer = -1
for col as Integer = 0 to mCols.Ubound
if remainingWidth < 0 then remainingWidth = 0
dim value as Double
dim type as columnWidthTypes = determineColumnWidthType (columnWidthCalcTypes.actual, mCols(col).WidthExpression, value)
if type = columnWidthTypes.remain then
dim columnWidth as Double = Round (value * asteriskWidth)
actualWidth(col) = columnWidth
lastAsteriskCol = col
remainingWidth = remainingWidth - columnWidth
asteriskCount = asteriskCount - value
if asteriskCount <= 0 then exit
end if
next
if lastAsteriskCol >= 0 then
actualWidth(lastAsteriskCol) = actualWidth(lastAsteriskCol) + remainingWidth
end
end if
// finally, set the actual widths in the table view
mIgnoreTileEvents = true
for col as Integer = 0 to mCols.Ubound
dim c as ListColumnTV = mCols(col)
c.minWidth = minWidth(col)
c.maxWidth = maxWidth(col)
c.width = actualWidth(col)
next
RaiseEvent DidResizeCells()
finally
mIgnoreTileEvents = false
End Sub
Private Sub _reloadNow(t as Timer = nil)
if mReloadTimer <> nil then mReloadTimer.Mode = Timer.ModeOff
mIgnoreTileEvents = false
mInsideHeaderDrag = false
if mColumnWidthsDirty then
mColumnWidthsDirty = false
if mHasDynamicColumnWidths then
// A column's WidthExpression had been set, so we want to re-adjust the columns accordingly, once
_recalcColumnWidths()
end if
end if
mReloadCount = mReloadCount + 1
clearRowCache
mTableView.reloadData
if not mMaintainColumnWidths then
mTableView.columnAutoresizingStyle = mDefaultColumnAutoresizingStyle
end
End Sub
Private Function _rowItemForRow(row as Integer) As ListRowTV
dim rowItem as ListRowTV = mDataSource.DataSource_RowItem (row)
return rowItem
End Function
Function _selectedRow() As Integer
return mTableView.selectedRow
End Function
Sub _sort()
dim sorters() as NSSortDescriptorMBS = mTableView.sortDescriptors
if sorters.Ubound < 0 then return
dim firstSorter as NSSortDescriptorMBS = sorters(0)
dim col as Integer = mTableView.columnWithIdentifier(firstSorter.key)
if RaiseEvent SortColumn (col) then
// user's event code handled the sorting
return
end if
dim c as Integer = mReloadCount
mDataSource.DataSource_SortRows // shall use me.TableView.sortDescriptors
if c = mReloadCount then _reloadNow
End Sub
Private Sub addItem(txt as String, asFolder as Boolean)
if mDataSource isA ListBoxTV_Support.ListBoxTVStaticOutlineDataSource then
dim idx as Integer
if asFolder then
idx = ListBoxTV_Support.ListBoxTVStaticOutlineDataSource(mDataSource).AddFolder (mCurrentRootItem, txt)
else
idx = ListBoxTV_Support.ListBoxTVStaticOutlineDataSource(mDataSource).AddRow (mCurrentRootItem, txt)
end if
mLastAddedIndex = idx
else
break // If you have assigned your own DataSource, then you cannot use AddRow() as that makes little sense
end if
_needsReload ' we reload lazily so that multiple calls to AddRow won't cause many reloads
End Sub
Private Sub addItems(items() as String)
dim col0 as String
if items.Ubound >= 0 then
col0 = items(0)
end if
addItem col0, false
if mDataSource isA ListBoxTV_Support.ListBoxTVStaticOutlineDataSource then
for column as Integer = 1 to items.Ubound
_cell(mLastAddedIndex,column).text = items(column)
next
end if
End Sub
Private Function calcColumnWidth(calcType as columnWidthCalcTypes, expr as String, totalWidth as Integer, colsLeft as Integer, extraSpace as Integer, ByRef asterisksInOut as Double, ByRef remainWidthInOut as Integer, ByRef widthOut as Integer) As columnWidthTypes
dim space as Double
dim value as Double
dim type as columnWidthTypes = determineColumnWidthType (calcType, expr, value)
if type = columnWidthTypes.absolute then
space = Round (value)
elseif type = columnWidthTypes.percentage then
space = Round (totalWidth * value / 100)
else
asterisksInOut = asterisksInOut + Max (1, value)
end if
remainWidthInOut = remainWidthInOut - space
if space > 0 then
// if this column occupies some space, the Cell will actually add some more pixels, which we need to remove from the remaining space
remainWidthInOut = remainWidthInOut - extraSpace
if remainWidthInOut < 0 and space > -remainWidthInOut then
space = space + remainWidthInOut
remainWidthInOut = 0
end if
end if
widthOut = space
return type
End Function
Private Sub clearRowCache()
mPreviousCell = nil
mPreviousItem = nil
End Sub
Private Function determineColumnWidthType(calcType as columnWidthCalcTypes, expr as String, ByRef value as Double) As columnWidthTypes
#pragma DisableBackgroundTasks
expr = expr.Trim
if expr = "" then
if calcType = columnWidthCalcTypes.actual then
value = 1
return columnWidthTypes.remain
elseif calcType = columnWidthCalcTypes.maximum then
value = 100
return columnWidthTypes.percentage
else
value = 0
return columnWidthTypes.absolute
end if
end if
dim number as Double, pos as Integer
for pos = 1 to expr.Len
dim ch as Integer = expr.Mid(pos,1).Asc
if (ch < 48 or ch > 57) and ch <> 46 then
' not a digit nor a period (.)
exit
end if
next
number = expr.Left(pos-1).Val
expr = expr.Mid(pos).Trim
if expr = "" then
' absolute value
value = number
return columnWidthTypes.absolute
elseif expr = "%" then
' percentage
value = number
return columnWidthTypes.percentage
else
' remaining space
value = Max (1, number)
return columnWidthTypes.remain
end if
End Function
Private Sub didCollapse(rowItem as ListRowTV)
RaiseEvent CollapseRow (mTableView.rowForItem (rowItem))
mDataSource.DataSource_Collapse (rowItem)
End Sub
Private Sub expand(rowItem as ListRowTV)
if mTableView.isItemExpanded(rowItem) then
return
end if
dim c as Integer = mExpandCounter
mTableView.expandItem rowItem, false
if c = mExpandCounter then
// Did not work - we need to reload the list first
if mExpanding then
// We have to postpone the Expand because we reloading the list during an expansion messes up the TableView' state
mPendingExpands.Append rowItem
else
_reloadNow
mTableView.expandItem rowItem, false
if c = mExpandCounter then
break ' something went wrong
end if
end if
end if
End Sub
Private Sub expandBegin(rowItem as ListRowTV)
mExpanding = true
mExpandCounter = mExpandCounter + 1
mExpandLevel.Append mCurrentRootItem
mCurrentRootItem = rowItem
dim currentRow as Integer = mTableView.rowForItem (rowItem)
mLastAddedIndex = currentRow
mDataSource.DataSource_Expand (rowItem)
RaiseEvent ExpandRow (currentRow)
End Sub
Private Sub expandEnd(rowItem as ListRowTV)
if rowItem <> mCurrentRootItem then
break ' huh?!
end if
mCurrentRootItem = mExpandLevel.Pop
mExpanding = false
while mPendingExpands.Ubound >= 0
dim item as ListRowTV = mPendingExpands(0)
mPendingExpands.Remove 0
me.expand item
wend
End Sub
Private Function handleMouseDown(x as Integer, y as Integer, modifiers as Integer) As Boolean
if RaiseEvent MouseDown (x, y) then
return true
end
#if Target64Bit
declare function frameOfCellAtColumn_Row lib "Cocoa" selector "frameOfCellAtColumn:row:" (o as Integer, col as Integer, row as Integer) as NSRect64
#else
declare function frameOfCellAtColumn_Row lib "Cocoa" selector "frameOfCellAtColumn:row:" (o as Integer, col as Integer, row as Integer) as NSRect32
#endif
dim loc0 as new NSPointMBS (x, y)
dim loc as NSPointMBS = mTableView.convertPointFromView (loc0, me.ScrollView)
dim row as Integer = mTableView.rowAtPoint (loc)
dim col as Integer = mTableView.columnAtPoint (loc)
dim cellFrame as NSRectMBS = mTableView.frameOfCellAtColumnRow (col, row)
return RaiseEvent CellClick (row, col, loc.x - cellFrame.x, loc.y - cellFrame.y)
End Function
Private Function itemIsSameOrParentOf(item as NSOutlineViewItemMBS, child as NSOutlineViewItemMBS) As Boolean
while child <> nil
if item = child then
return true
end if
child = mTableView.parentForItem (child)
wend
End Function
Private Function rowsFromItems(items() as NSOutlineViewItemMBS) As Integer()
dim res() as Integer
for each item as NSOutlineViewItemMBS in items
dim row as Integer = mTableView.rowForItem (item)
res.Append row
next
return res
End Function
Private Sub setDelayedProperties()
// Handle some delayed settings (appears to be necessary with Real Studio 2012 but not with Xojo 2016)
mHadOpenEvent = true
me.ColumnCount = mDelayedColumnCount
me.AutoHideScrollBars = mDelayedHideScrollers
me.ColumnsResizable = mDelayedColumnResizing
me.RequiresSelection = mDelayedRequiresSelection
me.ScrollbarHorizontal = mDelayedHorScroller
me.ScrollbarVertical = mDelayedVerScroller
me.SelectionType = mDelayedSelectionType
me.ColumnWidths = mDelayedColumnWidths
End Sub
Private Sub setupFromInitialValue()
dim rows() as String = ReplaceLineEndings(me.InitialValue, EndOfLine).Split(EndOfLine)
if me.HasHeading and rows.Ubound >= 0 then
dim cols() as String = rows(0).Split(Chr(9))
for i as Integer = 0 to Min (cols.Ubound, me.ColumnCount-1)
me.Heading(i) = cols(i)
next
rows.Remove 0
end if
for each row as String in rows
dim cols() as String = row.Split(Chr(9))
me.AddRow cols(0)
for i as Integer = 1 to Min (cols.Ubound, me.ColumnCount-1)
me.Cell(mLastAddedIndex, i) = cols(i)
next
next
End Sub
Note "About"
This is a (nearly complete) replacement for Xojo's ListBox control, for Mac OS X.
It requires MBS Plugins 17.1 (March 9, 2017 or later)
Advantages over Xojo's ListBox:
• Uses Apple's native NSTableView control, with its proper appearance, hence...
• Looks better in a Mac app.
• Column resizing works better (observing minimum widths and showing a horizontal scrollbar when necessary).
• Support column reordering.
• Supports right-clicks (CMM clicks) on the headers.
• Correct handling of multiple selected rows, including right-clicks.
• Offers more customization options (e.g., you get extra Events from the NSTableView).
• ConstructContextualMenu works as it should (i.e. for right clicks and ctrl-clicks).
• Faster.
• Open Source.
Disadvantages:
• Mac-only.
• No custom drawing in cells via ...Paint events (but you can instead use NSGraphicsMBS functions).
• No support for Database Binding.
• Limited grid line options.
• A few functions where a -1 parameter designates ALL rows or columns do not work (leading to an OutOfRangeException).
To use it, copy the entire "ListBox_NSOutlineView" folder into your project, then change the Super class of your ListBox controls
to ListBoxTV, and see if your project still compiles and runs.
If you're using the CellBackgroundPaint or CellTextPaint events, see if you're only using it to change the color or
font of the cells and text. See the note "Handling CellBackgroundPaint and CellTextPaint" for a solution.
Note "Attn sortDescriptorsDidChange"
The sortDescriptorsDidChange event is not implemented here because we handle
sorting in the tableView's didClickTableColumn event, which is called after this event.
Note "Authors and Copyright"
Initial author (Feb 2017): Thomas Tempelmann, tempelmann@gmail.com
This code is free, i.e. no contributing author may claim rights (or copyright) on it.
If you add new features, please send them back to Thomas for redistribution.
Adding a test case for the new feature would be nice, too.
Note "Handling CellBackgroundPaint and CellTextPaint"
If you implement either of these events to draw custom text or images, they won't work, because the "g as Graphics" parameter will be nil
(that's a limitation caused by Xojo Inc. because their Plugin API does not supply the needed code for making use of the Graphics class).
If you only want to have a particular background or text color, you can instead preset them like this:
myListbox.CellStyle (row, col).TextColor = ...
myListbox.CellStyle (row, col).BackgroundColor = ...
Note "Handling contextual clicks (right click; ctrl-click)"
Since the Xojo ListBox's ConstructContextualMenu event does not handle right clicks (or ctrl-clicks)
correctly with multiple selections, you may have tried to ignore the event and instead handle such
clicks in the MouseDown event by checking "IsContextualClick". You may still do that.
But if you plan to use the ConstructContextualMenu, you need to set the ListBoxTV's control's
UseContextualClickEvent property to True, e.g. right in the Window layout editor for the control,
so that ConstructContextualMenu gets called as desired.
You should do that (i.e. set UseContextualClickEvent=true and use ConstructContextualMenu)
over checking for IsContextualClick in the MouseDown event in order to have proper behavior,
especially when you allow multi-selection in the Listbox.
Note "NSView vs. NSCell based TableView cells"
By default, the TableView is in NSCell mode, which works with a limited set of predefined layout choices.
The code handling this layout is found in the ListBoxTV.willDisplayCell() event.
Alternatively, you can supply your own layout by returning a custom NSViewMBS from the ListBoxTV.view()
event. If the view() event is added, the willDisplayCell() event becomes unused automatically. So, if you
experiment with returning an NSView and are not happy with it, you need to remove the view() event
entirely again to revert to using the NSCell mode.
As a simple example, here's how to put a button into every cell. Place the following code into the view() event:
dim button as new NSButtonMBS
button.Title = "Button"
return button
To add multiple views, see the forum: https://forum.xojo.com/49354-listboxtv-mbs-plugins
Note "On-demand Cell values (custom DataSource)"
If you have a lot (thousands) of rows, the performance of Xojo's ListBox will notably decrease.
With this ListBoxTV, you can prevent this.
The solution is to not add your rows and cells beforehand, by using AddRow(), but instead just
tell the Listbox how many rows you have. The TableView class will then show an adequate scrollbar
for the entire number of rows, but will only ask you to provide the data for the rows and cells
it currently shows in the window, which will be usually only a few dozen values. If the user
scrolls the list, your code will then be asked again for only those new cells, and so on.
To accomplish that, implement the ListBoxTVDataSource interface. Either implement them in a new
class, or add them to your window or other class that you already maintain for your data.
You do not need to add code for all its functions, which all begin with "DataSource_". Most of
them you may leave empty, unless you want to support sorting, row dragging, column dragging or
editing.
Then set the listbox's DataSource property to your class that implements the ListBoxTVDataSource interface.
The minimum you have to add code to are:
• DataSource_ChildCount (rowItem as ListRowTV) as Integer
Here you need to return the total number of rows you want to be able to show. If the list is
hierarchical, then check the rowItem parameter - if it's nil, it means the root (top) level, so you'd
return the number of rows at that level, otherwise the child count of the given ListRowTV node.
• DataSource_ChildItem (rowItem as ListRowTV, index as Integer) as ListRowTV
Here you need to return a ListRowTV object (or a subclass of it).
Again, if rowItem is nil, you're at the root level. Otherwise, it's the node for one of your previously
returned ListRowTV objects. This lets you build a tree of your hierarchical list.
• DataSource_Cell (row as Integer, column as Integer) as ListCellTV
This returns an object of type ListCellTV, which contains the Text property for the cell's display,
as well as other properties such as Indentation, Alignment etc.
If you want to keep it simple, return this:
return new ListCellTV ("here goes the cell's text")
• DataSource_RowItem (row as Integer) as ListBoxTV
This has to return the row item for the currently visible listbox row. This means you have to
be able to tell which ListRowTV appears at which row. If you have a flat list, this is equal to
calling DataSource_ChildItem(nil, row). For a hierarchical list, you need to iterate over all shown
rows, counting the visible children, until you reach the row in question.
Now, whenever you know the listbox needs to be refreshed, i.e. it should call your DataSource_ChildCount(),
DataSource_ChildItem() and DataSource_Cell() functions again, call the listbox's Reload() function.
Note "To Do"
Not working, yet:
• EnableDragReorder does not work right, yet:
DataSource_MoveRows can't distinguish whether one wants to insert into the last
row of a folder or into the row following the folder's end. To fix that, not the dest
row but the parent node and child index needs to be passed to it.
• Row Insertion.
• Font attributes (bold, italic) for entire list defaults and for individual cells.
Anything else? Let me (TT) know!
Note "Version History"
Released Versions (oldest first)
1 - (16 Mar 2017)
• Fixes Sort operation.
• Makes all DataSource functions atomic, by using "#pragma DisableBackgroundTasks".
2 - (14 Aug 2018)
• Fixes a bug in ListBoxTV.ListCount(), avoiding potentially outdated values.
• Improvements to "row drag reorder" handling.
• New methods BeginUpdates() and EndUpdates() for more efficient updating.
• New: ListBoxTV.RemoveRow().
• Custom sorting implemented.
• Various bug fixes.
• New Note in ListBoxTV about NSView vs NSCell based TableView cells.
3 - (16 Aug 2018)
• Should now work in 64 bit builds (by fixing NSRect struct usage)
4 - (18 Aug 2018)
• Fixes the vertical scrollbar gap no matter what the System Preferences "General" / "Show scroll bars" is set to.
5 - (20 Aug 2018)
• Fixes placement of the ContainerControl (in some circumstance, its original would get moved to negative offsets).
• Can now add multiple columns in one AddRow() call.
• Renames interface function DataSource_ItemCount() to DataSource_ChildCount().
Property DefaultRowHeight As Integer
Property InitialValue As String
Property _suppressReload As Boolean
Property Private mCols() As ListColumnTV
Property Private mColumnWidthsDirty As Boolean
Property Private mContextMenu As ListBoxTV_Support.ListBoxTVContextMenu
Property Private mCurrentRootItem As ListRowTV
Property Private mDataSource As ListBoxTVDataSource
Property Private mDebugState As Integer
Property Private mDefaultColumnAutoresizingStyle As Integer
Property Private mDelayedColumnCount As Integer
Property Private mDelayedColumnResizing As Boolean
Property Private mDelayedColumnWidths As String
Property Private mDelayedHideScrollers As Boolean
Property Private mDelayedHorScroller As Boolean
Property Private mDelayedRequiresSelection As Boolean
Property Private mDelayedSelectionType As Integer
Property Private mDelayedVerScroller As Boolean
Property Private mEnableDrag As Boolean
Property Private mEnableDragReorder As Boolean
Property Private mExpandCounter As Integer
Property Private mExpandLevel() As ListRowTV
Property Private mExpanding As Boolean
Property Private mHadOpenEvent As Boolean
Property Private mHasDynamicColumnWidths As Boolean
Property Private mHeaderView As NSTableHeaderViewMBS
Property Private mIgnoreTileEvents As Boolean
Property Private mInsideHeaderDrag As Boolean
Property Private mLastAddedIndex As Integer
Property Private mMaintainColumnWidths As Boolean
Property Private mOrigIndentationPerLevel As Integer
Property Private mPendingExpands() As ListRowTV
Property Private mPreviousCell As ListCellTV
Property Private mPreviousItem As Object
Property Private mPreviousItemRow As Integer
Property Private mReloadCount As Integer
Property Private mReloadTimer As Timer
Property Private mSelectionCache As NSIndexSetMBS
Property Private mSelectionType As Integer
Property Private mSelfRef As WeakRef
Points to itself. This is an optimization so that we don't have to create multiple WeakRefs in each child object
Property Private mTableView As NSOutlineViewMBS
Property Private mTextFont As String
Property Private mTextSize As Integer
Property Private mUseContextualClickEvent As Boolean
Structure NSRect32
x as Single
y as Single
w as Single
h as Single
End Structure
Structure NSRect64
x as Double
y as Double
w as Double
h as Double
End Structure
End Class
Module ListBoxTV_Support
Protected Function convertOSType(t as String) As String
if t.Len = 4 then
// convert OSType to UTI
t = UTTypeMBS.CreatePreferredIdentifierForTag (UTTypeMBS.kUTTagClassOSType, t, UTTypeMBS.kUTTypeData)
end if
return t
End Function
Protected Function trimPeriod(s as String) As String
if s.Right(1) = "." then
return s.Left (s.Len-1)
else
return s
end if
End Function
End Module
Class ListColumnTV Inherits NSTableColumnMBS
ComputedProperty Alignment As Integer
Sub Set()
if mAlignment <> value then
mAlignment = value
owner._needsReload()
end if
End Set
Sub Get()
return mAlignment
End Get
End ComputedProperty
ComputedProperty AlignmentOffset As Integer
Sub Set()
if mAlignmentOffset <> value then
mAlignmentOffset = value
owner._needsReload()
end if
End Set
Sub Get()
return mAlignmentOffset
End Get
End ComputedProperty
ComputedProperty ColumnIndex As Integer
Sub Get()
return mColumnIndex
End Get
End ComputedProperty
ComputedProperty MaxWidthActual As Integer
Sub Set()
mMaxWidthExpr = Str(value,"#")
owner._needsReload(true)
me.maxWidth = value
End Set
Sub Get()
return Round (me.maxWidth)
End Get
End ComputedProperty
ComputedProperty MaxWidthExpression As String
Sub Set()
mMaxWidthExpr = value
owner._needsReload(true, true)
End Set
Sub Get()
return mMaxWidthExpr
End Get
End ComputedProperty
ComputedProperty MinWidthActual As Integer
Sub Set()
mMinWidthExpr = Str(value,"#")
owner._needsReload(true)
me.minWidth = value
End Set
Sub Get()
return Round (me.minWidth)
End Get
End ComputedProperty
ComputedProperty MinWidthExpression As String
Sub Set()
mMinWidthExpr = value
owner._needsReload(true, true)
End Set
Sub Get()
return mMinWidthExpr
End Get
End ComputedProperty
ComputedProperty SortDirection As Integer
Sub Set()
if mSortDirection <> value then
mSortDirection = value
owner._needsReload()
end if
End Set
Sub Get()
return mSortDirection
End Get
End ComputedProperty
ComputedProperty UserResizable As Boolean
Sub Set()
me.Resizable = value
End Set
Sub Get()
return me.Resizable
End Get
End ComputedProperty
ComputedProperty WidthActual As Integer
Sub Set()
mWidthExpr = Str(value,"#")
owner._needsReload(true)
me.width = value
End Set
Sub Get()
return Round (me.width)
End Get
End ComputedProperty
ComputedProperty WidthExpression As String
Sub Set()
mWidthExpr = value
owner._needsReload(true, true)
End Set
Sub Get()
return mWidthExpr
End Get
End ComputedProperty
Sub Constructor(ownerRef as WeakRef, colNum as Integer)
mOwner = ownerRef
mColumnIndex = colNum
static lastID as Integer
lastID = lastID + 1
dim id as String = Str(lastID,"#") ' Since columns can be reordered, id can't identify the column index, but only ColumnIndex can.
super.Constructor (id)
const NSCaseInsensitiveSearch = 1
const NSLiteralSearch = 2
const NSBackwardsSearch = 4
const NSAnchoredSearch = 8
const NSNumericSearch = 64 ' -> smart sort where numbers get ordered properly
const NSDiacriticInsensitiveSearch = 128
const NSWidthInsensitiveSearch = 256
const NSForcedOrderingSearch = 512
// Set up default sort handling, for when the ListboxTV.SortColumn and ListboxTV.CompareRows events are not used
dim opts as Integer = NSCaseInsensitiveSearch + NSDiacriticInsensitiveSearch + NSNumericSearch
// Install the primary and secondary sort descriptors
dim sorter as NSSortDescriptorMBS = NSSortDescriptorMBS.sortDescriptorWithKeyWithCompare ("", true, opts) ' this sorter is used when the CompareRows() event returns false
mSortHandler = new ListRowComparator (mOwner, me, sorter) ' this is our internal sorter that invokes CompareRows() and falls back to the above sorter
me.sortDescriptorPrototype = mSortHandler
// Set some UI options
dim cell as NSCellMBS = me.headerCell
cell.wraps = false
cell.lineBreakMode = NSParagraphStyleMBS.NSLineBreakByTruncatingTail
End Sub
Function EffectiveType(rowItem as NSOutlineViewItemMBS) As Integer
dim myCell as ListCellTV = owner._cell(rowItem, ColumnIndex)
dim t as Integer = myCell.Type
if t = ListBox.TypeDefault then
return me.Type
else
return t
end
End Function
Sub SetFontAndSize(fontName as String, size as Double = 0)
if fontName = "" or fontName = "System" or fontName = "SmallSystem" then
me.headerCell.font = NSFontMBS.systemFontOfSize (size)
else
me.headerCell.font = NSFontMBS.fontWithName (fontName, size)
end if
End Sub
Sub _updateOwnIndex()
mColumnIndex = me.tableView.columnWithIdentifier (me.identifier)
End Sub
Private Function owner() As ListBoxTV
return ListBoxTV (mOwner.Value)
End Function
Property Tag As Variant
You may set this to anything you like - if the user drags the column to another position, this Tag will move along.
Property Type As Integer
Property Private mAlignment As Integer
Property Private mAlignmentOffset As Integer
Property Private mColumnIndex As Integer
Property Private mMaxWidthExpr As String
Property Private mMinWidthExpr As String
Property Private mOwner As WeakRef
ListBoxTV
Property Private mSortDirection As Integer
Property Private mSortHandler As NSSortDescriptorMBS
Property Private mWidthExpr As String
End Class
Class ListCellTV
ComputedProperty Picture As Picture
Sub Set()
mPicture = value
me.nsImage = new NSImageMBS (value, value.Mask(false))
End Set
Sub Get()
return mPicture
End Get
End ComputedProperty
Sub Constructor(text as String = "", checked as Boolean = false)
me.Text = text
me.Checked = checked
End Sub
Property Alignment As Integer
Use ListBox.Align... constants here
Property BackgroundColor As Color = &cFFFFFFFF
Property Bold As Boolean
Property Checked As Boolean
Property Indentation As Integer
Property Italic As Boolean
Property Tag As Variant
Property Text As String
Property TextColor As Color = &c00000000
Property ToolTip As String
Property Type As Integer
Use ListBox.Type... constants here
Property _internalUse As Pair
This will be modified internally during Sort and Draw operations
Property Private mPicture As Picture
Property nsImage As NSImageMBS
End Class
Class ListRowTV Inherits NSOutlineViewItemMBS
Property tag As Variant
End Class
Class DragItemTV
Sub AddItem(x as Integer, y as Integer, w as Integer, h as Integer)
mItems.Append new NSPasteboardItemMBS
mCurrentItemIdx = mItems.Ubound
End Sub
Private Function BestType(paramarray t() as String) As String
return mItems(mCurrentItemIdx).availableTypeFromArray(t)
End Function
Sub Constructor(pb as NSPasteboardMBS)
mPboard = pb
mItems = pb.pasteboardItems
if mItems.Ubound < 0 then
AddItem(0,0,0,0)
end if
Reset()
End Sub
Sub FinishAddedItems()
call mPboard.SetPasteboardItems mItems
End Sub
Function FolderItem() As FolderItem
dim f as FolderItem
dim item as NSPasteboardItemMBS = mItems(mCurrentItemIdx)
dim s as String = item.stringForType(NSPasteboardMBS.NSFilenamesPboardType).DefineEncoding(Encodings.UTF8)
if s <> "" then
f = PathToFolderItemMBS (s)
else
s = item.stringForType("public.file-url").DefineEncoding(Encodings.UTF8)
f = new FolderItem (s, FolderItem.PathTypeURL)
end if
return f
End Function
Sub FolderItem(assigns f as FolderItem)
mItems(mCurrentItemIdx).stringForType("public.file-url") = f.URLPath.ConvertEncoding(Encodings.UTF8)
End Sub
Function FolderItemAvailable() As Boolean
return HasType(NSPasteboardMBS.NSFilenamesPboardType, "public.file-url")
End Function
Function HasType(paramarray t() as String) As Boolean
return mItems(mCurrentItemIdx).availableTypeFromArray(t) <> ""
End Function
Function NextItem() As Boolean
dim res as Boolean = true
mCurrentItemIdx = mCurrentItemIdx + 1
if mCurrentItemIdx > mItems.Ubound then
mCurrentItemIdx = 0
res = false
end
mTypes = mItems(mCurrentItemIdx).types
return res
End Function
Function Pboard() As NSPasteboardMBS
return mPboard
End Function
Function RawData(type as String) As String
type = BestType (ListBoxTV_Support.convertOSType(type))
dim s as String = mItems(mCurrentItemIdx).stringForType(type)
return s
End Function
Sub RawData(type as String, assigns s as String)
mItems(mCurrentItemIdx).stringForType(ListBoxTV_Support.convertOSType(type)) = s
End Sub
Function RawDataAvailable(type as String) As Boolean
return HasType (ListBoxTV_Support.convertOSType(type))
End Function
Private Sub Reset()
mCurrentItemIdx = -1
call NextItem
End Sub
Function Text() As String
dim t as String = BestType (NSPasteboardMBS.NSPasteboardTypeString, NSPasteboardMBS.NSStringPboardType)
dim s as String = mItems(mCurrentItemIdx).stringForType(t).DefineEncoding(Encodings.UTF8)
return s
End Function
Sub Text(assigns s as String)
mItems(mCurrentItemIdx).stringForType(NSPasteboardMBS.NSPasteboardTypeString) = s.ConvertEncoding(Encodings.UTF8)
End Sub
Function TextAvailable() As Boolean
return HasType(NSPasteboardMBS.NSPasteboardTypeString, NSPasteboardMBS.NSStringPboardType)
End Function
Property Private mCurrentItemIdx As Integer
Property Private mItems() As NSPasteboardItemMBS
Property Private mPboard As NSPasteboardMBS
Property Private mTypes() As String
End Class
Interface ListBoxTVDataSource
Function DataSource_Cell(rowItem as ListRowTV, column as Integer) As ListCellTV
Sub DataSource_CellDidUpdate(cell as ListCellTV, rowItem as ListRowTV, column as Integer, updatesText as Boolean, updatesChecked as Boolean)
Sub DataSource_CellWillUpdate(cell as ListCellTV, rowItem as ListRowTV, column as Integer, updatesText as Boolean, updatesChecked as Boolean)
Function DataSource_ChildCount(rowItem as ListRowTV) As Integer
Function DataSource_ChildItem(rowItem as ListRowTV, index as Integer) As ListRowTV
Sub DataSource_Collapse(rowItem as ListRowTV)
Function DataSource_CurrentRowCount() As Integer
Sub DataSource_DeleteAllRows()
Sub DataSource_Expand(rowItem as ListRowTV)
Function DataSource_IsExpandable(rowItem as ListRowTV) As Boolean
Sub DataSource_MoveColumn(fromIdx as Integer, toIdx as Integer)
Sub DataSource_MoveRows(fromRows() as Integer, toAboveRow as Integer)
Function DataSource_RowItem(row as Integer) As ListRowTV
Sub DataSource_SetColumnCount(colCount as Integer)
Sub DataSource_SortRows()
End Interface
Class ListDrawCellHandler Inherits CustomNSTextFieldCellMBS
Const ImageInset = 3
EventHandler Function drawWithFrame(cellFrame as NSRectMBS, controlView as NSViewMBS) As boolean
dim textFrame as NSRectMBS = titleRectForBounds (cellFrame)
dim lb as ListBoxTV = ListBoxTV(mOwner.Value)
lb._suppressReload = true
dim row as Integer = me.cell._internalUse.Left
dim col as Integer = me.cell._internalUse.Right
if not lb._cellBackgroundPaint (nil, row, col, textFrame, controlView) then
// draw background for entire cell, even if the text is indented
if me.backgroundColor <> nil then
dim g as new NSGraphicsMBS
g.setFillColor me.backgroundColor
g.fillRect cellFrame
end if
end if
if cell.nsImage <> nil then
#if Target64Bit
declare sub drawInRect lib "Cocoa" selector "drawInRect:fromRect:operation:fraction:respectFlipped:hints:" ( _
h as Integer, dst as NSRect64, src as NSRect64, operation as Integer, fraction as Double, respectFlipped as Boolean, hints as Ptr)
dim dr, zeroRect as NSRect64
#else
declare sub drawInRect lib "Cocoa" selector "drawInRect:fromRect:operation:fraction:respectFlipped:hints:" ( _
h as Integer, dst as NSRect32, src as NSRect32, operation as Integer, fraction as Single, respectFlipped as Boolean, hints as Ptr)
dim dr, zeroRect as NSRect32
#endif
dim img as NSImageMBS = cell.nsImage
dr.w = img.width
dr.h = img.height
dr.x = cellFrame.x + ImageInset
dr.y = cellFrame.y + (cellFrame.Height - dr.h) \ 2
drawInRect (img.Handle, dr, zeroRect, NSGraphicsMBS.NSCompositeSourceOver, 1, controlView.isFlipped, nil)
end if
if not lb._cellTextPaint (nil, row, col, 0, 0, textFrame, controlView) then
superDrawWithFrame textFrame, controlView
end if
lb._suppressReload = false
return true
End EventHandler
EventHandler Function titleRectForBounds(rect as NSRectMBS) As NSRectMBS
// Let's indent the text (based on the cell's or column's AlignmentOffset, set in ListBoxTV.dataCellForTableColumn)
dim ofs as Integer = me.cell.Indentation
if me.cell.nsImage <> nil then
ofs = ofs + me.cell.nsImage.width + (2 * ImageInset)
end if
rect.X = rect.X + ofs
rect.Width = rect.Width - ofs
return rect
End EventHandler
Sub Constructor(owner as WeakRef)
mOwner = owner
super.Constructor
End Sub
Note "About"
This handles custom drawing for cells.
Note: This only works for "normal" cells, not for Checkbox cells - they would need a different NSCell subclass implementation from MBS, which isn't available (yet).
Property cell As ListCellTV
Property Private mOwner As WeakRef
ListBoxTV
Structure NSRect32
x as Single
y as Single
w as Single
h as Single
End Structure
Structure NSRect64
x as Double
y as Double
w as Double
h as Double
End Structure
End Class
Class ListRowComparator Inherits NSSortDescriptorMBS
EventHandler Function Comparator(obj1 as variant, obj2 as variant) As Integer
dim c1 as ListCellTV = obj1
dim c2 as ListCellTV = obj2
#if DebugBuild
if c1._internalUse.Right <> column.ColumnIndex or c1._internalUse.Right <> c2._internalUse.Right then raise new RuntimeException ' is this happens, we forgot up update the Column somewhere
#endif
dim res as integer
if owner._compareRows (c1, c2, res) then
return res
end
declare function compareObjectTo lib "Cocoa" selector "compareObject:toObject:" (h as Integer, o1 as CFStringRef, o2 as CFStringRef) as Integer
try
res = compareObjectTo (mSorter.Handle, c1.Text, c2.Text)
catch exc as ObjCException
break
end try
return res
End EventHandler
Sub Constructor(ownerRef as WeakRef, column as ListColumnTV, sorter as NSSortDescriptorMBS)
super.Constructor (column.identifier, true)
mOwner = ownerRef
mColumn = new WeakRef(column)
mSorter = sorter
End Sub
Function column() As ListColumnTV
return ListColumnTV (mColumn.Value)
End Function
Private Function owner() As ListBoxTV
return ListBoxTV (mOwner.Value)
End Function
Property Private mColumn As WeakRef
ListColumnTV
Property Private mOwner As WeakRef
ListBoxTV
Property Private mSorter As NSSortDescriptorMBS
End Class
Class ListBoxTVContextMenuItem Inherits NSMenuItemMBS
EventHandler Sub Action()
owner.MenuItemAction (me)
End EventHandler
Sub Constructor(menu as ListBoxTVContextMenu, item as MenuItem)
super.Constructor (item.Text, "")
mOwner = new WeakRef (menu)
me.Item = item
End Sub
Private Function owner() As ListBoxTVContextMenu
return ListBoxTVContextMenu (mOwner.Value)
End Function
Property Item As MenuItem
Property Private mOwner As WeakRef
ListBoxTVContextMenu
End Class
Class ListBoxTVContextMenu Inherits NSMenuMBS
EventHandler Sub EnableMenuItems()
me.Clear
dim row as Integer = owner._clickedRow
dim col as Integer = owner._clickedColumn
if row < 0 then
// Click in Header
// clickedColumn is always -1 in this case, so we need to calculate it manually
dim ev as NSEventMBS = NSApplicationMBS.sharedApplication.currentEvent
dim globalLocation as NSPointMBS = ev.locationInWindow
dim localLocation as NSPointMBS = owner.TableView.convertPointFromView (globalLocation, nil)
localLocation.Y = 0
col = owner.TableView.columnAtPoint (localLocation)
else
// Click in row / cell
if not owner.Selected (row) then
// deselect all others if the right-click was not inside the current selection - this is to avoid confusion for the user
owner.ListIndex = row
end if
end if
dim base as new MenuItem
if owner._constructContextualMenu (base, System.MouseX, System.MouseY, row, col, me) then
for i as Integer = 1 to base.Count
dim item as ListBoxTVContextMenuItem = new ListBoxTVContextMenuItem (me, base.Item(i-1))
mItems.Append item
super.addItem item
next
end if
mMenu = base
mRow = row
mCol = col
End EventHandler
Sub Clear()
super.removeAllItems
redim mItems(-1)
End Sub
Sub Constructor(ownerRef as WeakRef)
super.Constructor
mOwner = ownerRef
End Sub
Sub MenuItemAction(sender as ListBoxTVContextMenuItem)
dim item as MenuItem = sender.Item
owner._contextualMenuAction item, mRow, mCol
me.Clear
End Sub
Private Function owner() As ListBoxTV
return ListBoxTV (mOwner.Value)
End Function
Property Private mCol As Integer
Property Private mItems() As ListBoxTVContextMenuItem
Property Private mMenu As MenuItem
Property Private mOwner As WeakRef
ListBoxTV
Property Private mRow As Integer
End Class
Class ListBoxTVStaticOutlineDataSource
Function AddFolder(rowItem as ListRowTV, txt as String) As Integer
return insertItem (StaticListFolderTV(rowItem), -1, new StaticListFolderTV (mOwner, mColumnCount), txt)
End Function
Function AddRow(rowItem as ListRowTV, txt as String) As Integer
return insertItem (StaticListFolderTV(rowItem), -1, new StaticListRowTV (mOwner, mColumnCount), txt)
End Function
Sub Constructor(ownerRef as WeakRef)
mOwner = ownerRef
mDataRoot = new StaticListFolderTV (mOwner, 0)
End Sub
Function DataSource_Cell(rowItem as ListRowTV, column as Integer) As ListCellTV
dim res as ListCellTV = StaticListRowTV(rowItem).cells(column)
if res is nil then
// This cell has not been accessed before - create it now
res = new ListCellTV
StaticListRowTV(rowItem).cells(column) = res
end if
return res
End Function
Sub DataSource_CellDidUpdate(cell as ListCellTV, rowItem as ListRowTV, column as Integer, updatesText as Boolean, updatesChecked as Boolean)
// Nothing to do here - it exists for use with custom implementations of the ListBoxTVDataSource interface
End Sub
Sub DataSource_CellWillUpdate(cell as ListCellTV, rowItem as ListRowTV, column as Integer, updatesText as Boolean, updatesChecked as Boolean)
// Nothing to do here - it exists for use with custom implementations of the ListBoxTVDataSource interface
End Sub
Function DataSource_ChildCount(rowItem as ListRowTV) As Integer
if rowItem = nil then rowItem = mDataRoot
return StaticListFolderTV(rowItem).children.Ubound+1
End Function
Function DataSource_ChildItem(rowItem as ListRowTV, index as Integer) As ListRowTV
if rowItem = nil then rowItem = mDataRoot
if index <= StaticListFolderTV(rowItem).children.Ubound then
return StaticListFolderTV(rowItem).children(index)
end if
End Function
Sub DataSource_Collapse(rowItem as ListRowTV)
#pragma DisableBackgroundTasks // so that this code remains atomic (otherwise, we'd have to use a Semaphore)
invalidateRowsPast (rowItem)
redim StaticListFolderTV(rowItem).children(-1)
End Sub
Function DataSource_CurrentRowCount() As Integer
enumerateRowsUpTo -1, nil
return mRowItems.Ubound + 1
End Function
Sub DataSource_DeleteAllRows()
#pragma DisableBackgroundTasks // so that this code remains atomic (otherwise, we'd have to use a Semaphore)
invalidateRowsStartingAt 0
redim mDataRoot.children(-1)
End Sub
Sub DataSource_Expand(rowItem as ListRowTV)
invalidateRowsPast (rowItem)
End Sub
Function DataSource_IsExpandable(rowItem as ListRowTV) As Boolean
return rowItem isA StaticListFolderTV
End Function
Sub DataSource_MoveColumn(fromIdx as Integer, toIdx as Integer)
mDataRoot.MoveColumn fromIdx, toIdx
End Sub
Sub DataSource_MoveRows(fromRows() as Integer, toAboveRow as Integer)
#pragma DisableBackgroundTasks // so that this code remains atomic (otherwise, we'd have to use a Semaphore)
if fromRows.Ubound < 0 then return
fromRows.Sort
dim firstAffectedRow as Integer = Min (fromRows(0), toAboveRow)
dim items() as StaticListRowTV
for each row as Integer in fromRows
items.Append mRowItems(row).Right
next
dim destParent as StaticListFolderTV = mRowItems(toAboveRow).Left
dim destItem as StaticListRowTV = mRowItems(toAboveRow).Right
dim destIndex as Integer = destParent.children.IndexOf (destItem)
for each item as StaticListRowTV in items
dim itemParent as StaticListFolderTV = parentItem (item)
dim itemIndex as Integer = itemParent.children.IndexOf (item)
if itemParent = destParent then
if itemIndex < destIndex then
destIndex = destIndex - 1
end
end if
itemParent.children.Remove itemIndex
destParent.children.Insert destIndex, item
destIndex = destIndex + 1
next
invalidateRowsStartingAt firstAffectedRow
// Select the moved rows
dim firstItem as StaticListRowTV = items(0)
enumerateRowsUpTo -1, firstItem
dim destStartRow as Integer = firstItem.row
dim newRowNumbers() as Integer
for i as Integer = 0 to fromRows.Ubound
newRowNumbers.Append destStartRow + i
next
owner.SelectRows newRowNumbers
#if DebugBuild
self._DebugCheck
#endif
End Sub
Function DataSource_RowItem(row as Integer) As ListRowTV
enumerateRowsUpTo (row, nil)
if row < 0 or row > mRowItems.Ubound then
return nil
end if
dim item as StaticListRowTV = mRowItems(row).Right
if item.row < 0 then
// Something went wrong internally.
// Let's check this in the debugger by repeating the operation:
break
enumerateRowsUpTo (row, nil)
end if
return item
End Function
Sub DataSource_SetColumnCount(colCount as Integer)
mColumnCount = colCount
dim newUbound as Integer = colCount-1
mDataRoot.SetColumnUbound newUbound
End Sub
Sub DataSource_SortRows()
#pragma DisableBackgroundTasks // so that the loop and redim remain atomic
enumerateRowsUpTo -1, nil // enumerates all rows
mDataRoot.Sort
invalidateRowsStartingAt 0 // invalidates all rows
End Sub
Sub RemoveRow(row as Integer, animOptions as Integer)
#pragma DisableBackgroundTasks // so that this code remains atomic (otherwise, we'd have to use a Semaphore)
// update mRowItems to include the row
enumerateRowsUpTo row, nil
// get the item we want to delete
dim item as StaticListRowTV = mRowItems (row).Right
dim parent as StaticListFolderTV = mRowItems (row).Left
dim indexInParent as Integer = parent.children.IndexOf (item)
// remove it from the TableView
dim tv as NSOutlineViewMBS = owner.TableView
dim tvParent as NSOutlineViewItemMBS = parent
if tvParent = mDataRoot then tvParent = nil
tv.removeItemsAtIndexes NSIndexSetMBS.indexSetWithIndex (indexInParent), tvParent, animOptions
// Invalidate mRowItems past the deleted item
invalidateRowsPast item
dim p as Pair = mRowItems.Pop
item = p.Right
item.row = -1
// remove the item from the parent container
parent.children.Remove indexInParent
End Sub
Sub _DebugCheck()
// For debugging only.
// Checks the integrity of the data
#if DebugBuild
'dim row as Integer
'for each p as Pair in mRowItems
'dim item as StaticListRowTV = p.Right
'if item.row <> row then
'break
'return
'end if
'row = row + 1
'next
#endif
End Sub
Private Sub enumerateRowsUpTo(untilRow as Integer, untilItem as StaticListRowTV)
#pragma DisableBackgroundTasks // so that this code remains atomic (otherwise, we'd have to use a Semaphore)
if untilRow >= 0 and mRowItems.Ubound >= untilRow then
return
end if
dim parent as StaticListFolderTV
dim item as StaticListRowTV
if mRowItems.Ubound < 0 then
parent = mDataRoot
else
dim p as Pair = mRowItems(mRowItems.Ubound)
parent = p.Left
item = p.Right
#if DebugBuild
if mRowItems.Ubound <> item.row then break ' this would mean we have a bug in the algorithm
#endif
end if
do
dim startIndex as Integer
if item <> nil then
startIndex = parent.children.IndexOf (item) + 1
end if
if parent.EnumerateRows (mRowItems, startIndex, untilRow, untilItem) then
return
end if
// continue with parent of last item in mRowItems
if parent = mDataRoot then
// we're through
exit
end if
dim row as Integer = parent.row
if row < 0 then break ' that would indicate a bug
item = parent
parent = mRowItems(row).Left
loop
End Sub
Private Sub fillUpToExactly(parent as StaticListFolderTV, prevItem as StaticListRowTV)
#pragma DisableBackgroundTasks // so that this code remains atomic (otherwise, we'd have to use a Semaphore)
dim lastRow as Integer = mRowItems.Ubound
dim lastItem as StaticListRowTV
if lastRow >= 0 then
lastItem = mRowItems(lastRow).Right
end if
if prevItem = mDataRoot then
// We're adding the first item at the root
invalidateRowsStartingAt 0
return
end
if lastItem = prevItem then
// The perviously added item is the last child of this parent
return
end
// The last item in mRowItems may still be the last item of the parent, but of a child of the parent,
// so let's check that, too
while lastRow >= 0
dim lastItemParent as StaticListFolderTV = mRowItems(lastRow).Left
if lastItemParent = parent then
return
end if
lastRow = lastItemParent.row
wend
// invalidate items past the one we're going to add to mRowItems
if prevItem.row >= 0 then
invalidateRowsPast prevItem
end if
// mRowItems needs to be filled up to this item
enumerateRowsUpTo (-1, prevItem)
End Sub
Private Function insertItem(parent as StaticListFolderTV, atIndex as Integer, newItem as StaticListRowTV, txt as String) As Integer
#pragma DisableBackgroundTasks // so that this code remains atomic (otherwise, we'd have to use a Semaphore)
dim cell as ListCellTV = me.DataSource_Cell (newItem, 0) ' -> allocates the cell in the row object
cell.Text = txt
if parent = nil then parent = mDataRoot
// To keep mRowItems, our array of known row-to-item mappings, intact,
// we need to cut it down to the item/row that comes before the item we're
// adding now. That means we'll locate the previous item of this parent, and
// invalidate the items past it. We also need to advance mRowItems to that
// previous item in case it's not in the array, yet.
// Then we can add this new item to the mRowItems savely.
dim prevItem as StaticListRowTV = parent.ChildAt (atIndex)
if prevItem = nil then
// The parent has no children yet -> the parent will thus be the previous item
prevItem = parent
end
fillUpToExactly parent, prevItem
mRowItems.Append parent : newItem
dim row as Integer = mRowItems.Ubound
parent.children.Append newItem
newItem.row = row
return row
End Function
Private Sub invalidateRowsPast(rowItem as ListRowTV)
#pragma DisableBackgroundTasks // so that the loop and redim remain atomic
dim newUbound as Integer = StaticListRowTV(rowItem).row
if rowItem <> mDataRoot then
if newUbound < 0 then
' already invalidated
return
elseif newUbound > mRowItems.Ubound then
break ' should never happen
return
elseif newUbound = mRowItems.Ubound then
' okay, no change
return
end if
end if
' invalidate all the items past newUbound
for i as Integer = newUbound+1 to mRowItems.Ubound
dim item as StaticListRowTV = mRowItems(i).Right
item.row = -1
next
redim mRowItems (newUbound)
End Sub
Private Sub invalidateRowsStartingAt(row as Integer)
#pragma DisableBackgroundTasks // so that the loop and redim remain atomic
dim newUbound as Integer = row - 1
if newUbound < mRowItems.Ubound then
' invalidate all the items past newUbound
for i as Integer = row to mRowItems.Ubound
dim item as StaticListRowTV = mRowItems(i).Right
item.row = -1
next
redim mRowItems (newUbound)
end if
End Sub
Private Function owner() As ListBoxTV
return ListBoxTV (mOwner.Value)
End Function
Private Function parentItem(rowItem as StaticListRowTV) As StaticListFolderTV
return mRowItems (rowItem.row).Left
End Function
Property Private mColumnCount As Integer
Property Private mDataRoot As StaticListFolderTV
Property Private mOwner As WeakRef
ListBoxTV
Property Private mRowItems() As Pair
Pair (parent StaticListFolderTV : item as StaticListRowTV)
This is our cache of known Rows and their Items. We need this to determine the next
Row number and also to look up a RowItem's row number and parent.
Only invalidateRowsPast() may redim this array to make sure the invalidated rows get their row set to -1!
End Class
Class StaticListRowTV Inherits ListRowTV
EventHandler Function Description() As String
'+++ temporarily disabled because it interferes with debugging during setup
'// Lets return all cells' values, comma-separated
'dim res() as String
'for each c as ListCellTV in cells
'dim s as String = c.text
'if s.Len > 20 then
's = s.Left(20)+"…"
'end
'res.Append s
'next
'return "#"+Str(row)+" ["+Join(res, ", ")+"]"
End EventHandler
EventHandler Function valueForKey(key as string) As Variant
// This is only called when performing a sort using sort descriptors
// (see ListBoxTVDataSource.sortDescriptorsDidChange)
if me isA StaticListFolderTV then
// we need to sort the children as well
StaticListFolderTV (me).Sort
end if
dim col as Integer = owner.TableView.columnWithIdentifier(key)
dim cell as ListCellTV = cells(col)
cell._internalUse = me.row : col
return cell
End EventHandler
Sub Constructor(ownerRef as WeakRef, columnCount as Integer)
mOwner = ownerRef
me.row = -1 // -> unknown
redim me.cells(columnCount-1)
super.Constructor
End Sub
Sub MoveColumn(fromIdx as Integer, toIdx as Integer)
dim c as ListCellTV = me.cells(fromIdx)
me.cells.Remove fromIdx
me.cells.Insert toIdx, c
End Sub
Sub SetColumnUbound(ub as Integer)
redim me.cells(ub)
End Sub
Protected Function owner() As ListBoxTV
return ListBoxTV (mOwner.Value)
End Function
Property cells() As ListCellTV
Property Private mOwner As WeakRef
ListBoxTV
Property row As Integer
End Class
Class StaticListFolderTV Inherits StaticListRowTV
Function ChildAt(idx as integer) As StaticListRowTV
if children.Ubound >= 0 then
if idx < 0 or idx > children.Ubound then
' last child
return children (children.Ubound)
else
return children (idx)
end if
end if
End Function
Function EnumerateRows(rowItems() as Pair, startIndex as Integer, stopAtRow as Integer, stopAfterItem as StaticListRowTV) As Boolean
#pragma DisableBackgroundTasks // so that this code remains atomic (otherwise, we'd have to use a Semaphore)
// Returns true if specified row or item has been reached
for idx as Integer = startIndex to me.children.Ubound
dim child as StaticListRowTV = me.children(idx)
rowItems.Append me : child
child.row = rowItems.Ubound
if child isA StaticListFolderTV then
if StaticListFolderTV(child).EnumerateRows (rowItems, 0, stopAtRow, stopAfterItem) then
return true
end if
end if
if rowItems.Ubound = stopAtRow then
// Do not move this before the recursion call, or it'll cause errors.
// That's because enumerateRowsUpTo() starts with the next child, and therefore all this child's children need to have been enumerated already
return true
end if
if child = stopAfterItem then
return true
end if
next
End Function
Function LastChild() As StaticListRowTV
if children.Ubound >= 0 then
return children (children.Ubound)
end if
End Function
Sub MoveColumn(fromIdx as Integer, toIdx as Integer)
#pragma DisableBackgroundTasks // so that this code remains atomic (otherwise, we'd have to use a Semaphore)
super.MoveColumn fromIdx, toIdx
for each child as StaticListRowTV in me.children
child.MoveColumn fromIdx, toIdx
next
End Sub
Sub SetColumnUbound(ub as Integer)
#pragma DisableBackgroundTasks // so that this code remains atomic (otherwise, we'd have to use a Semaphore)
super.SetColumnUbound ub
for each child as StaticListRowTV in me.children
child.SetColumnUbound ub
next
End Sub
Sub Sort()
#pragma DisableBackgroundTasks // so that this code remains atomic (otherwise, we'd have to use a Semaphore)
dim newRows() as NSOutlineViewItemMBS = NSOutlineViewItemMBS.sortedArrayUsingDescriptors (me.children, owner.TableView.sortDescriptors)
for i as Integer = 0 to newRows.Ubound
dim row as StaticListRowTV = StaticListRowTV(newRows(i))
me.children(i) = row
next
End Sub
Property children() As StaticListRowTV
End Class
End Project
See also:
- /MacControls/Listbox and TableView Demos/ListboxTV drop-in/Flat Only/ListBoxTV Database with DataSource
- /MacControls/Listbox and TableView Demos/ListboxTV drop-in/Flat Only/ListBoxTV Simple Demo with DataSource
- /MacControls/Listbox and TableView Demos/ListboxTV drop-in/Flat Only/ListBoxTV TableView
- /MacControls/Listbox and TableView Demos/ListboxTV drop-in/Flat Only/ListboxTV with ContainerControl Cells
- /MacControls/Listbox and TableView Demos/NSOutlineView/Disk Browser
- /MacControls/Listbox and TableView Demos/NSOutlineView/OutlineControl
- /MacControls/Listbox Row Colors
The items on this page are in the following plugins: MBS MacControls Plugin.