Platforms to show: All Mac Windows Linux Cross-Platform
/MacFrameworks/FSEvents/FSEventsMBS Demo
Required plugins for this example: MBS MacOSX Plugin, MBS MacFrameworks 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.xojo_binary_project"
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
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
End Project
The items on this page are in the following plugins: MBS MacFrameworks Plugin.