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:

The items on this page are in the following plugins: MBS MacControls Plugin.


The biggest plugin in space...