Platforms to show: All Mac Windows Linux Cross-Platform
/MacControls/Listbox and TableView Demos/NSOutlineView/Disk Browser
Required plugins for this example: MBS MacCocoa Plugin, MBS MacBase Plugin, MBS MacOSX Plugin, MBS Main Plugin, MBS MacControls Plugin, MBS MacFrameworks Plugin
You find this example project in your Plugins Download as a Xojo project file within the examples folder: /MacControls/Listbox and TableView Demos/NSOutlineView/Disk Browser
This example is the version from Sun, 5th Nov 2022.
Project "Disk Browser.xojo_binary_project"
Class App Inherits Application
Const kEditClear = "&Delete"
Const kFileQuit = "&Quit"
Const kFileQuitShortcut = ""
End Class
Class MainWindow Inherits Window
Const moveScriptCode = "on moveFiles (src, dest)\r tell application ""Finder""\r with timeout of 5 seconds\r move src to dest\r end timeout\r end tell\rend moveFiles"
Control folderBrowser Inherits HierarchicalMacListBox
ControlInstance folderBrowser Inherits HierarchicalMacListBox
EventHandler Sub DoubleClick()
// Let's open the selected items
self.openSelectedItems nil
End EventHandler
EventHandler Sub ItemDidCollapse(notification as NSNotificationMBS, item as NSOutlineViewItemMBS)
// At this point, we may dispose of the children, in order to release the memory used for them.
// However, this will also make the OutlineView forget the "expanded" state of inner folders.
'dim node as DiskItem = nodeForItem (item)
'node.ClearChildren
End EventHandler
EventHandler Sub Open()
// Add two columns to the listbox
dim col as new NSTableColumnMBS("name")
col.width = 200
col.minWidth = 100
col.Title = "Name"
col.Editable = true
me.AddColumn col
col = new NSTableColumnMBS("size")
col.width = 120
col.minWidth = 60
col.Title = "Size"
col.Editable = false
me.AddColumn col
// Set some options to make the list "prettier"
me.View.usesAlternatingRowBackgroundColors = true
me.View.gridStyleMask = NSTableViewMBS.NSTableViewSolidHorizontalGridLineMask+NSTableViewMBS.NSTableViewSolidVerticalGridLineMask
// Specify the column that contains the "expand" icons. If we don't set this, we effectively get a flat list instead
dim cols() as NSTableColumnMBS = me.View.tableColumns()
me.View.outlineTableColumn = cols(0) ' use the first column
// If we want to allow column resizing, this option should be set to avoid ugly visual effect while resizing
// (i.e. the vertical grid lines do not move nicely, then), or the gridlines should be not used.
me.View.autoresizesOutlineColumn = false
// And if the window is resized, we want only the first column to resize
me.View.columnAutoresizingStyle = NSTableViewMBS.NSTableViewFirstColumnOnlyAutoresizingStyle
// Set up our hierarchical data, which is a tree structure
mRootItem = new DiskItem (me.View)
// Let's add all currently available volumes
for i as Integer = 0 to VolumeCount-1
mRootItem.AddChild Volume(i)
next
me.Reload ' this forces the listbox to update (by calling Events such as childOfItem etc.)
// Let's save and re-open the expanded folder on restart of this app (needs to happen after loading the data, so that the list root has been loaded already)
me.View.autosaveName = "Open Folders"
me.View.autosaveExpandedItems = true
// And we allow dragging of out items as well
me.View.registerForDraggedTypes Array (NSPasteboardMBS.NSFilenamesPboardType, "public.file-url")
// Set up a contextual menu
mMenuItems.Append new MyMenuItem ("Open", 1, AddressOf openSelectedItems)
mMenuItems.Append new MyMenuItem ("Rename", 2, AddressOf renameClickedItem)
mMenuItems.Append new MyMenuItem ("Refresh", 3, AddressOf refreshSelectedItems)
mMenu = new NSMenuMBS
for each item as NSMenuItemMBS in mMenuItems
mMenu.addItem item
next
me.View.menu = mMenu
End EventHandler
EventHandler Function acceptDrop(info as NSDraggingInfoMBS, item as NSOutlineViewItemMBS, index as Integer) As Boolean
// This is called when data got dropped onto a specific row. We return whether we want to accept the data.
// If index is -1, it means a drop onto the item, otherwise it means a drop to insert at the child index of the given parent item.
redim mDraggedFiles(-1)
dim draggedFiles() as FolderItem = fetchFilesFromPboard (info.draggingPasteboard)
dim targetItem as DiskItem = determineDropTarget (item, index)
dim targetFolder as FolderItem = targetItem.FolderItem
if targetFolder = nil then
// Can't drag into the root, which is a list of volumes
return false
end if
// Now we can move the selected files to the target folder
// However, we need to delay that operation, because the AppleScript we'll use won't run as long as this drag isn't finished,
// which would be leading to a timeout. Instead, the move will be handled by the draggingExited event.
mDropSourceFiles = draggedFiles
mDropDestFolder = targetFolder
return true
End EventHandler
EventHandler Function childOfItem(index as Integer, item as NSOutlineViewItemMBS) As NSOutlineViewItemMBS
// Returns the child object for a row. If item is nil, it means the root of the hierarchy
dim node as DiskItem = nodeForItem (item)
if node <> nil then
return node.ChildAtIndex(index)
end if
End EventHandler
EventHandler Sub concludeDragOperation(info as NSDraggingInfoMBS)
redim mDraggedFiles(-1)
if mDropSourceFiles.Ubound >= 0 and mDropDestFolder <> nil then
// We have to use a Timer or we'll get a Timeout if the drag came from the Finder
mDropFinishTimer = new Timer
AddHandler mDropFinishTimer.Action, AddressOf moveFilesInTimer
mDropFinishTimer.Mode = Timer.ModeSingle
else
redim mDropSourceFiles(-1)
mDropDestFolder = nil
end if
End EventHandler
EventHandler Sub draggingEnded(info as NSDraggingInfoMBS)
redim mDraggedFiles(-1)
End EventHandler
EventHandler Sub draggingExited(info as NSDraggingInfoMBS)
redim mDraggedFiles(-1)
End EventHandler
EventHandler Function isItemExpandable(item as NSOutlineViewItemMBS) As Boolean
// Returns whether the row is expandable (then showing the "triangle" expand control)
dim node as DiskItem = nodeForItem (item)
if node <> nil then
return node.IsFolder
end if
End EventHandler
EventHandler Function itemForPersistentObject(persistentObject as Variant) As NSOutlineViewItemMBS
// This is called to identify an item, for restoring its expand state from the preferences.
// It goes along with persistentObjectForItem(), which is the inverse operation.
dim node as DiskItem = mRootItem.FindByPath (persistentObject)
return node
End EventHandler
EventHandler Function numberOfChildrenOfItem(item as NSOutlineViewItemMBS) As Integer
// Returns the number of child objects for a row. If item is nil, it means the root of the hierarchy
dim node as DiskItem = nodeForItem (item)
if node <> nil then
return node.ChildCount
end if
End EventHandler
EventHandler Function objectValue(tableColumn as NSTableColumnMBS, item as NSOutlineViewItemMBS) As Variant
// Returns the text for a cell
dim node as DiskItem = nodeForItem (item)
if node = nil or node.FolderItem = nil then return ""
if tableColumn.identifier = "Name" then
return node.FolderItem.Name
elseif node.IsFolder then
return "-"
else
return Format (node.FolderItem.Length, ",###")
end
End EventHandler
EventHandler Function persistentObjectForItem(item as NSOutlineViewItemMBS) As Variant
// This is called to identify a folder, for preserving its expand state in the preferences.
// It goes along with itemForPersistentObject(), which is the inverse operation.
dim node as DiskItem = nodeForItem (item)
return node.Path
End EventHandler
EventHandler Sub setObjectValue(tableColumn as NSTableColumnMBS, item as NSOutlineViewItemMBS, value as Variant)
// This is called when the user has edited a cell and is leaving it
dim node as DiskItem = nodeForItem (item)
dim f as FolderItem = node.FolderItem
if f <> nil then
f.Name = value
if f.LastErrorCode <> 0 then
beep ' rename failed
end if
end if
End EventHandler
EventHandler Function shouldEdit(tableColumn as NSTableColumnMBS, item as NSOutlineViewItemMBS) As Boolean
// This is called when the user click into an editable cell - we can still deny that here for individual cells
dim node as DiskItem = nodeForItem (item)
dim f as FolderItem = node.FolderItem
if f <> nil and not f.Locked then
return true
end if
End EventHandler
EventHandler Function validateDrop(info as NSDraggingInfoMBS, proposedItem as NSOutlineViewItemMBS, proposedChildIndex as Integer) As Integer
// This is called when data gets dragged over a specific row. We return whether that would get accepted as a drop.
// We may also retarget a different row, e.g. we'd target the enclosing folder, by calling .
// If index is -1, it means a drop onto the item, otherwise it means a drop to insert at the child index of the given parent item.
// Determine the dragged items (files, folders)
if mDraggedFiles.Ubound < 0 or info.draggingSequenceNumber <> mLastDragSequence then
// It's a new drag operation - let's cache the dragged files (for performance reasons)
mLastDragSequence = info.draggingSequenceNumber
mDraggedFiles = fetchFilesFromPboard (info.draggingPasteboard)
end if
// We retarget to the enclosing folder
dim targetItem as DiskItem = determineDropTarget (proposedItem, proposedChildIndex)
dim targetFolder as FolderItem = targetItem.FolderItem
if targetFolder = nil then
// Can't drag into the root, which is a list of volumes
me.View.setDropItem nil, me.View.NSOutlineViewDropOnItemIndex
return NSDraggingInfoMBS.NSDragOperationNone
end if
// We must not allow dragging a folder into one of its own sub folders, as such a move is not permited by the file system
dim targetPath as String = targetFolder.UnixpathMBS
for each f as FolderItem in mDraggedFiles
if f.Directory then
dim sourcePath as String = f.UnixpathMBS
if StrComp (sourcePath, targetPath.Left(sourcePath.Len), 0) = 0 then
// The dragged folder is a parent of the target folder
me.View.setDropItem nil, me.View.NSOutlineViewDropOnItemIndex
return NSDraggingInfoMBS.NSDragOperationNone // we disallow this drag to the current target
end
end if
next
// Set the designated drop folder
me.View.setDropItem targetItem, me.View.NSOutlineViewDropOnItemIndex
return NSDraggingInfoMBS.NSDragOperationMove // We could also support Copy, Link (generating an Alias) etc., by returning those options as well, but would have to handle them in acceptDrop then.
End EventHandler
EventHandler Function writeItems(items() as NSOutlineViewItemMBS, pasteboard as NSPasteboardMBS) As Boolean
// This is called when rows get dragged.
call pasteboard.clearContents
dim pbItems() as NSPasteboardItemMBS
// Let's drag the selected file(s)
for each item as NSOutlineViewItemMBS in items
dim pbItem as new NSPasteboardItemMBS
dim url as String = nodeForItem(item).FolderItem.URLPath
dim name as String = nodeForItem(item).FolderItem.Name
pbItem.stringForType("public.file-url") = url.ConvertEncoding(Encodings.UTF8)
pbItem.stringForType(NSPasteboardMBS.NSPasteboardTypeString) = name.ConvertEncoding(Encodings.UTF8)
pbItems.Append pbItem
next
call pasteboard.SetPasteboardItems pbItems
me.View.setDraggingSourceOperationMask (NSDraggingInfoMBS.NSDragOperationEvery, false)
return true
End EventHandler
End Control
Control showHiddenChk Inherits CheckBox
ControlInstance showHiddenChk Inherits CheckBox
EventHandler Sub Action()
mRootItem.ShowHidden = me.Value
End EventHandler
End Control
Control Label1 Inherits Label
ControlInstance Label1 Inherits Label
End Control
Private Function determineDropTarget(item as NSOutlineViewItemMBS, index as Integer) As DiskItem
// We retarget a drop to their enclosing folder
dim targetItem as DiskItem = nodeForItem (item)
if index >= 0 then
' It's an insert - we already have the parent in 'node'
elseif not targetItem.IsFolder then
targetItem = targetItem.Parent
end if
return targetItem
End Function
Private Function fetchFilesFromPboard(pb as NSPasteboardMBS) As FolderItem()
dim res() as FolderItem
dim items() as NSPasteboardItemMBS = pb.pasteboardItems
for each item as NSPasteboardItemMBS in items
dim f as FolderItem
dim s as String = item.stringForType(NSPasteboardMBS.NSFilenamesPboardType).DefineEncoding(Encodings.UTF8)
if s <> "" then
f = PathToFolderItemMBS (s)
end
if f = nil then
s = item.stringForType("public.file-url").DefineEncoding(Encodings.UTF8)
f = GetTrueFolderItem (s, FolderItem.PathTypeURL)
end if
if f <> nil then
res.Append f
end if
next
return res
End Function
Private Function moveFiles(sourceFiles() as FolderItem, destFolder as FolderItem) As Boolean
// We're not using Xojo's MoveFileTo because (a) that would also move files across volumes, which is unexpected,
// it may not copy all metadata, and it provides no Undo.
// Instead, we ask the Finder via AppleScript, which takes care of all this, including error reporting.
#if true
// Using the Finder
dim script as new NSAppleScriptMBS (moveScriptCode)
dim error as Dictionary
if not script.Compile (error) then
break
return false
end
dim srcList as NSAppleEventDescriptorMBS = NSAppleEventDescriptorMBS.listDescriptor()
for each f as FolderItem in sourceFiles
srcList.insertDescriptor NSAppleEventDescriptorMBS.descriptorWithFileURL(f), 0
next
dim args() As NSAppleEventDescriptorMBS
args.Append srcList
args.Append NSAppleEventDescriptorMBS.descriptorWithFileURL(destFolder)
dim p as NSAppleEventDescriptorMBS = script.executeSubroutine ("moveFiles", args, error)
#pragma unused p
if error <> nil then
break ' check the "error" dictionary
beep
end
return true
#else
// Lame version that moves files across volumes, even, unless we check that first and use CopyFileTo instead
dim hadError as Boolean
for each f as FolderItem in draggedFiles
f.MoveFileTo targetFolder
if f.LastErrorCode <> 0 then
hadError = true
end if
next
if hadError then
beep
end if
return true
#endif
End Function
Private Sub moveFilesInTimer(t as Timer)
t.Mode = Timer.ModeOff
mDropFinishTimer = nil
if mDropSourceFiles.Ubound >= 0 and mDropDestFolder <> nil then
call moveFiles (mDropSourceFiles, mDropDestFolder)
dim destPath as String = mDropDestFolder.UnixpathMBS
dim item as DiskItem = mRootItem.FindByPath (destPath)
if item <> nil then
item.UpdateChildrenFromDisk
end if
end if
redim mDropSourceFiles(-1)
mDropDestFolder = nil
End Sub
Private Function nodeForItem(item as NSOutlineViewItemMBS) As DiskItem
dim node as DiskItem
if item = nil then
node = mRootItem
else
node = DiskItem(item)
end if
return node
End Function
Private Sub openSelectedItems(menuItem as MyMenuItem)
dim selected as NSIndexSetMBS = selectedItems (menuItem, folderBrowser)
for each row as Integer in selected.Values
dim item as NSOutlineViewItemMBS = folderBrowser.View.itemAtRow (row)
if item = nil then
break ' huh?
else
dim node as DiskItem = nodeForItem (item)
dim f as FolderItem = node.FolderItem
if f <> nil then
f.Launch (false)
end if
end if
next
End Sub
Private Sub refreshSelectedItems(menuItem as MyMenuItem)
dim selected as NSIndexSetMBS = selectedItems (menuItem, folderBrowser)
for each row as Integer in selected.Values
dim item as NSOutlineViewItemMBS = folderBrowser.View.itemAtRow (row)
if item = nil then
break ' huh?
else
dim node as DiskItem = nodeForItem (item)
node.UpdateChildrenFromDisk
end if
next
End Sub
Private Sub renameClickedItem(menuItem as MyMenuItem)
dim row as Integer = folderBrowser.View.clickedRow
folderBrowser.View.edit (0, row, true)
End Sub
Private Function selectedItems(menuItem as NSMenuItemMBS, lb as NSOutlineControlMBS) As NSIndexSetMBS
dim selected as NSIndexSetMBS = lb.View.selectedRowIndexes
// We also need to check the clicked row if it's from a contextual menu
if menuItem <> nil then
dim row as Integer = lb.View.clickedRow
if selected.containsIndex (row) then
// The right-click happened inside the selection -> we use the entire selection
else
// The right-click happened outside the selection -> we use only the clicked row
selected = NSIndexSetMBS.indexSetWithIndex (row)
end if
end if
return selected
End Function
Note "About"
Written 8 Mar 2017 by Thomas Tempelmann, tempelmann@gmail.com
Consider this sample code - you made use it as you like.
Property Private mDraggedFiles() As FolderItem
We cache the dragged files here during a drop operation
Property Private mDropDestFolder As FolderItem
Property Private mDropFinishTimer As Timer
Property Private mDropSourceFiles() As FolderItem
Property Private mLastDragSequence As Integer
Property Private mMenu As NSMenuMBS
Property Private mMenuItems() As NSMenuItemMBS
Property Private mRootItem As DiskItem
End Class
FileTypesFolder
Filetype special/disk
Filetype special/folder
End FileTypesFolder
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 HierarchicalMacListBox Inherits NSOutlineControlMBS
EventHandler Function ConstructContextualMenu(base as MenuItem, x as Integer, y as Integer) As Boolean
' let's ignore these here - we'll use the tableView's menu property instead
End EventHandler
EventHandler Function ContextualMenuAction(hitItem as MenuItem) As Boolean
' let's ignore these here - we'll use the tableView's menu property instead
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)
'
End EventHandler
Sub AddColumn(col as NSTableColumnMBS)
mRetainedCols.Append col
me.View.addTableColumn col
End Sub
Sub Reload()
me.View.reloadData
End Sub
Property Private mRetainedCols() As NSTableColumnMBS
End Class
Class DiskItem Inherits NSOutlineViewItemMBS
ComputedProperty Shared ShowHidden As Boolean
Sub Set()
gShowHidden = value
End Set
Sub Get()
return gShowHidden
End Get
End ComputedProperty
Sub AddChild(f as FolderItem)
children.Append new DiskItem (mTableView, f, me)
mChildrenByPath = nil
End Sub
Function ChildAtIndex(index as Integer) As DiskItem
fetchChildren()
return children (index)
End Function
Function ChildCount() As Integer
fetchChildren()
return children.Ubound + 1
End Function
Sub ClearChildren()
redim children(-1)
mLoaded = false
mMonitor = nil
End Sub
Private Sub Constructor()
End Sub
Sub Constructor(lb as NSOutlineViewMBS)
me.Constructor (lb, nil, nil)
End Sub
Private Sub Constructor(lb as NSOutlineViewMBS, f as FolderItem, parent as DiskItem)
if parent <> nil then
if parent.mSelfRef = nil then
// We cache the parent's WeakRef to avoid creating many identical WeakRef instances
parent.mSelfRef = new WeakRef (parent)
end if
mParent = parent.mSelfRef
end if
mTableView = lb
me.f = f
if f <> nil then
mPath = f.UnixpathMBS+"/"
end if
super.Constructor
End Sub
Function FindByPath(path as String) As DiskItem
// Recurses into its children
if path = "" then
return nil
end if
if path.Right(1) <> "/" then
path = path + "/"
end if
dim node as DiskItem = findChild (path)
if node <> nil then
return node
end if
// Try the parents
dim dirs() as String = path.Split("/")
do
call dirs.Pop
if dirs.Ubound <= 0 then
exit
end if
dirs(dirs.Ubound) = ""
node = findChild (Join (dirs, "/"))
if node <> nil then
node.fetchChildren()
return node.FindByPath (path)
end if
loop
End Function
Function FolderItem() As FolderItem
return me.f
End Function
Function IsFolder() As Boolean
return f <> nil and f.Directory
End Function
Function Parent() As DiskItem
if mParent <> nil then
return DiskItem(mParent.Value)
end if
End Function
Function Path() As String
return mPath
End Function
Sub UpdateChildrenFromDisk()
mUpdateChildren = true
mTableView.reloadItem (me, true)
End Sub
Sub UpdateFromDisk()
me.f = PathToFolderItemMBS (mPath)
mTableView.reloadItem (me, false)
End Sub
Private Sub fetchChildren()
if not mLoaded or mUpdateChildren then
// We're going to fetch the folder contents now.
// Let's also install a watcher for changes to the folder so that we can automatically update the list
if me.f <> nil and mMonitor = nil then
mMonitor = new FolderMonitor (mPath, AddressOf folderHasChanged)
end if
if me.f <> nil then
dim newChildren() as DiskItem
me.f = GetFolderItem(f.NativePath) ' recreates the FolderItem so that it doesn't use cached information
dim n as Integer = me.f.Count
for i as Integer = 1 to n
dim f2 as FolderItem = f.TrueItem (i)
dim item as DiskItem
if gShowHidden or f2 <> nil and f2.Visible then
if mUpdateChildren and f2 <> nil then
// This means we'll have to keep existing children (this assures that any expanded folders in the list remain that way)
item = findChild (f2)
end if
if item = nil then
item = new DiskItem (mTableView, f2, me)
end if
newChildren.Append item
mChildrenByPath = nil
end if
next
me.children = newChildren
end if
mUpdateChildren = false
mLoaded = true
end if
End Sub
Private Function findChild(f as FolderItem) As DiskItem
// Looks only at immediate children
if f <> nil then
dim path as String = f.UnixpathMBS+"/"
return findChild (path)
end
End Function
Private Function findChild(path as String) As DiskItem
// Looks only at immediate children
if mChildrenByPath = nil then
// rebuild the cache
mChildrenByPath = new Dictionary
for each child as DiskItem in me.children
mChildrenByPath.Value (child.mPath) = child
next
end if
return mChildrenByPath.Lookup (path, nil)
End Function
Private Sub folderHasChanged(path as String)
me.UpdateChildrenFromDisk
End Sub
Note "About"
Written 8 Mar 2017 by Thomas Tempelmann, tempelmann@gmail.com
Consider this sample code - you made use it as you like.
Property Private children() As DiskItem
Property Private f As FolderItem
Property Private Shared gShowHidden As Boolean
Property Private mChildrenByPath As Dictionary
Property Private mLoaded As Boolean
Property Private mMonitor As FolderMonitor
Property Private mParent As WeakRef
Property Private mPath As String
Property Private mSelfRef As WeakRef
Property Private mTableView As NSOutlineViewMBS
Property Private mUpdateChildren As Boolean
End Class
Class FolderMonitor
Const Logging = true
Delegate Sub FolderChangedProc(path as String)
Sub Constructor(path as String, callee as FolderChangedProc)
if path.Left(1) <> "/" then
raise new InvalidParentException ' Paths may not be relative
end if
if path.Right(1) <> "/" then
// Paths need to end in a "/" for matching in handleFSEvent()
path = path + "/"
end if
// Determine if this path requires us to watch a new FSEvents root path
dim needsNewRoot as Boolean, commonPath as String
if gWatcher = nil or gCurrentFSEventsPath = "" then
needsNewRoot = true
commonPath = path
if DefaultLatencyInSeconds <= 0 then DefaultLatencyInSeconds = 1 ' default: 1 second
else
commonPath = determineCommonPath (path, gCurrentFSEventsPath)
if commonPath.Len < gCurrentFSEventsPath.Len then
// This means that the new path is a parent of the currently used FSEvents path
needsNewRoot = true
end if
end if
if needsNewRoot then
// By monitoring the root dir, we learn about changes even on other volumes, including network volumes
dim since as UInt64
if gWatcher = nil then
// Let's start watching now
since = FSEventsMBS.kFSEventStreamEventIdSinceNow
else
// Let's start watching from the currently last reported event
gWatcher.Stop
since = gWatcher.GetLatestEventId
gWatcher = nil
end if
gCurrentFSEventsPath = commonPath
gWatcher = new FSEventsMBS (gCurrentFSEventsPath, since, DefaultLatencyInSeconds, 0)
AddHandler gWatcher.Callback, AddressOf handleFSEvent
if not gWatcher.Start then
break ' huh?
raise new UnsupportedOperationException
end if
end if
if gClientPaths = nil then
gClientPaths = new Dictionary
end
dim v as Variant = gClientPaths.Lookup (path, nil)
if v is nil then
gClientPaths.Value (path) = callee
else
// we already watch this path - the value may be a single entry or type FolderChangedProc or an array of them
if v isA FolderChangedProc then
// turn the Dictionary's value into an array
if v = callee then
break // we've already registered this one
return ' we do not want to set mPath and mCallee so that the Destructor won't release this duplicate
end
dim callees() as FolderChangedProc
callees.Append v
callees.Append callee
gClientPaths.Value (path) = callees
else
// it's already an array - add to it
dim callees() as FolderChangedProc = v
if callees.IndexOf (callee) >= 0 then
break // we've already registered this one
return ' we do not want to set mPath and mCallee so that the Destructor won't release this duplicate
end if
callees.Append callee
end if
end if
mPath = path
mCallee = callee
End Sub
Private Sub Destructor()
if mCallee = nil then
// this was a duplicate
return
end if
dim v as Variant = gClientPaths.Lookup (mPath, nil)
if v is nil then
break ' huh?
elseif v isA FolderChangedProc then
if v <> mCallee then
break ' huh?
else
gClientPaths.Remove (mPath)
end if
else
// it's an array - remove our callee from it
dim callees() as FolderChangedProc = v
dim idx as Integer = callees.IndexOf (mCallee)
if idx < 0 then
break ' huh?
else
callees.Remove idx
if callees.Ubound < 0 then
gClientPaths.Remove (mPath)
end if
end if
end if
if gClientPaths.Count = 0 then
// No more clients -> dispose of the FSEvents watcher
gWatcher = nil
gClientPaths = nil
gCurrentFSEventsPath = ""
end if
End Sub
Private Function determineCommonPath(path1 as String, path2 as String) As String
dim dirs1() as String = path1.Split("/")
dim dirs2() as String = path2.Split("/")
dim n as Integer = Min (dirs1.Ubound-1, dirs2.Ubound-1)
dim i as Integer
for i = 1 to n
if dirs1(i) <> dirs2(i) then
exit
end if
next
redim dirs1(i)
dirs1(i) = ""
dim res as String = Join (dirs1, "/")
return res
End Function
Private Shared Sub handleFSEvent(sender as FSEventsMBS, index as Integer, count as Integer, path as string, flags as Integer, eventID as UInt64)
#if DebugBuild and Logging
System.DebugLog "handleFSEvent: "+path
#endif
if gClientPaths <> nil then
dim v as Variant = gClientPaths.Lookup (path, nil)
if v = nil then
' we're not monitoring this path
else
#if DebugBuild and Logging
System.DebugLog " -> notifiying"
#endif
if v isA FolderChangedProc then
dim callee as FolderChangedProc = v
callee.Invoke (path)
else
// it's an array - call all callees in it
dim callees() as FolderChangedProc = v
for each callee as FolderChangedProc in callees
callee.Invoke (path)
next
end if
end if
end if
End Sub
Note "About"
This class was written on 8 Mar 2017 by Thomas Tempelmann, tempelmann@gmail.com.
You may use this code freely without restrictions.
It provides an easy way to monitor changes to particular folders. It uses the MBS plugin's
FSEventsMBS class. While the FSEventsMBS is meant to monitor not only just one folder
but also all its contained folders, this class makes it possible to instead just pick single
folders (by their paths, which you can obtain with FolderItem.UnixpathMBS) and get a
callback when one of them changes.
This class takes care of all the management requires for this, such as having multiple
watchers interested in the same path, and disposing of the FSEventsMBS watcher once
there's no monitoring requested any more.
To use it, write something like
dim monitor as new FolderMonitor (file.UnixpathMBS, AddressOf handleFolderChange)
and then store 'monitor' in a property for as long as you want the given folder monitored.
You also have to implement a method like this:
sub handleFolderChange (path as String)
It will be called once the given folder has changed, with up to about a second delay, usually.
See FolderMonitorTestWin for a demo.
Property Shared DefaultLatencyInSeconds As Double
Property Private Shared gClientPaths As Dictionary
Key: path as String, Value: callee as FolderChangedProc or an array of them.
Property Private Shared gCurrentFSEventsPath As String
Property Private Shared gWatcher As FSEventsMBS
Property Private mCallee As FolderChangedProc
Property Private mPath As String
End Class
Class MyMenuItem Inherits NSMenuItemMBS
Delegate Sub ActionProc(menuItem as MyMenuItem)
EventHandler Sub Action()
mAction.Invoke (me)
End EventHandler
Sub Constructor(title as String, ttag as Variant, actionHandler as ActionProc)
me.myTag = ttag
mAction = actionHandler
super.Constructor (title)
End Sub
Property Private mAction As ActionProc
Property myTag As Variant
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/ListboxTV drop-in/Hierarchical & Flat/ListBoxTV OutlineView
- /MacControls/Listbox and TableView Demos/NSOutlineView/OutlineControl
- /MacControls/Listbox Row Colors
The items on this page are in the following plugins: MBS MacControls Plugin.