Xojo Conferences

Platforms to show: All Mac Windows Linux Cross-Platform

/MacFrameworks/FSEvents/FSEventsMBS Demo
Required plugins for this example: MBS MacFrameworks Plugin, MBS MacOSX Plugin
You find this example project in your Plugins Download as a Xojo project file within the examples folder: /MacFrameworks/FSEvents/FSEventsMBS Demo
This example is the version from Thu, 8th Mar 2017.
Project "FSEventsMBS Demo.rbp"
Class App Inherits Application
Const kEditClear = "&Delete"
Const kFileQuit = "&Quit"
Const kFileQuitShortcut = ""
End Class
Class FolderMonitor
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 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 elseif 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 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 FolderMonitorTestWin Inherits Window
Control monitorList Inherits Listbox
ControlInstance monitorList Inherits Listbox
EventHandler Function KeyDown(Key As String) As Boolean if key.Asc = 127 or key.Asc = 8 then ' Delete or Backspace? // Let's remove all selected rows for row as Integer = me.ListCount-1 downTo 0 if me.Selected(row) then me.RemoveRow row ' as this clears its Tag, the assigned FolderMonitor object will be freed as well end if next return true end if End EventHandler
End Control
Control Label1 Inherits Label
ControlInstance Label1 Inherits Label
End Control
Control changesList Inherits Listbox
ControlInstance changesList Inherits Listbox
End Control
Control Label2 Inherits Label
ControlInstance Label2 Inherits Label
End Control
EventHandler Sub DropObject(obj As DragItem, action As Integer) do if obj.FolderItemAvailable then dim f as FolderItem = obj.FolderItem if f <> nil then if not f.Directory then f = f.Parent addFolder f end if end if loop until not obj.NextItem End EventHandler
EventHandler Sub Open() self.AcceptRawDataDrop "public.file-url" ' same as: self.AcceptFileDrop FileTypesFolder.All End EventHandler
Private Sub addFolder(dir as FolderItem) dim path as String = dir.UnixpathMBS monitorList.AddRow path dim row as Integer = monitorList.LastIndex monitorList.RowTag(row) = new FolderMonitor (path, AddressOf folderHasChanged) End Sub
Private Sub folderHasChanged(path as String) dim now as new Date changesList.AddRow now.LongTime dim row as Integer = changesList.LastIndex changesList.Cell (row, 1) = path changesList.ScrollPosition = row ' scrolls to the bottom End Sub
End Class
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
End Project

Feedback, Comments & Corrections

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

MBS Xojo blog