PredicateEditorSample¶
PredicateEditorSample¶
“PredicateEditorSample” is a Cocoa application that demonstrates how to use
the NSPredicateEditor
class. The NSPredicateEditor
class is a
subclass of NSRuleEditor
that is specialized for editing NSPredicate
objects. This sample is intended to show how to use the many different
features and aspects of this control and leverages Spotlight to search your
Address Book.
It shows how to manage this control inside your window along with an
NSTableView
, build Spotlight friendly queries based on NSPredicate
and
NSCompountPredicate
, build search results based on NSMetadataQuery
object.
Using the Sample¶
Simply build the example using the supplied setup.py
file. Enter
query information pertaining to your Address Book. The application will
display matches in its table view.
Note that this sample uses Interface Builder 3.0 to build the
NSPredicateEditorRowTemplates
that make up the control’s interface.
AddressBook searches are achieved by specifically requesting the “kind” of data to search via the kMDItemKind
key constant. This is the metadata attribute key that tells Spotlight to search for address book data only. Together along with the other predicates from the NSPredicateEditor
class we form a “compound predicate” and start the query. The code snippet below found in this sample shows how this is done:
# always search for items in the Address Book
addrBookPredicate = NSPredicate.predicateWithFormat_("(kMDItemKind = 'Address Book Person Data')")
predicate = NSCompoundPredicate.andPredicateWithSubpredicates_([addrBookPredicate, predicate])
query.setPredicate_(predicate)
query.startQuery()
Sources¶
CaseInsensitivePredicateTemplate.py¶
import Cocoa
from objc import super # noqa: A004
class CaseInsensitivePredicateTemplate(Cocoa.NSPredicateEditorRowTemplate):
def predicateWithSubpredicates_(self, subpredicates):
# we only make NSComparisonPredicates
predicate = super().predicateWithSubpredicates_(subpredicates)
# construct an identical predicate, but add the
# NSCaseInsensitivePredicateOption flag
return Cocoa.NSComparisonPredicate.predicateWithLeftExpression_rightExpression_modifier_type_options_( # noqa: B950
predicate.leftExpression(),
predicate.rightExpression(),
predicate.comparisonPredicateModifier(),
predicate.predicateOperatorType(),
predicate.options() | Cocoa.NSCaseInsensitivePredicateOption,
)
MyWindowController.py¶
import Cocoa
import objc
searchIndex = 0
class MyWindowController(Cocoa.NSWindowController):
query = objc.ivar()
previousRowCount = objc.ivar(type=objc._C_INT)
myTableView = objc.IBOutlet()
mySearchResults = objc.IBOutlet()
predicateEditor = objc.IBOutlet()
progressView = objc.IBOutlet() # the progress search view
progressSearch = objc.IBOutlet() # spinning gear
progressSearchLabel = objc.IBOutlet() # search result #
def dealloc(self):
Cocoa.NSNotificationCenter.defaultCenter().removeObserver_(self)
def awakeFromNib(self):
# no vertical scrolling, we always want to show all rows
self.predicateEditor.enclosingScrollView().setHasVerticalScroller_(False)
self.previousRowCount = 3
self.predicateEditor.addRow_(self)
# put the focus in the first text field
displayValue = self.predicateEditor.displayValuesForRow_(1).lastObject()
if isinstance(displayValue, Cocoa.NSControl):
self.window().makeFirstResponder_(displayValue)
# create and initialize our query
self.query = Cocoa.NSMetadataQuery.alloc().init()
# setup our Spotlight notifications
nf = Cocoa.NSNotificationCenter.defaultCenter()
nf.addObserver_selector_name_object_(
self, "queryNotification:", None, self.query
)
# initialize our Spotlight query, sort by contact name
self.query.setSortDescriptors_(
[
Cocoa.NSSortDescriptor.alloc().initWithKey_ascending_(
"kMDItemContactKeywords", True
)
]
)
self.query.setDelegate_(self)
# start with our progress search label empty
self.progressSearchLabel.setStringValue_("")
return
def applicationShouldTerminateAfterLastWindowClosed_(self, sender):
return True
def loadResultsFromQuery_(self, notif):
results = notif.object().results()
Cocoa.NSLog("search count = %d", len(results))
foundResultsStr = "Results found: %d" % (len(results),)
self.progressSearchLabel.setStringValue_(foundResultsStr)
if len(results) == 0:
return
# iterate through the array of results, and match to the existing stores
for item in results:
cityStr = item.valueForAttribute_("kMDItemCity")
nameStr = item.valueForAttribute_("kMDItemDisplayName")
stateStr = item.valueForAttribute_("kMDItemStateOrProvince")
phoneNumbers = item.valueForAttribute_("kMDItemPhoneNumbers")
phoneStr = None
if phoneNumbers:
phoneStr = phoneNumbers[0]
storePath = item.valueForAttribute_(
"kMDItemPath"
).stringByResolvingSymlinksInPath()
# create a dictionary entry to be added to our search results array
aDict = {
"name": nameStr or "",
"phone": phoneStr or "",
"city": cityStr or "",
"state": stateStr or "",
"url": Cocoa.NSURL.fileURLWithPath_(storePath),
}
self.mySearchResults.append(aDict)
def queryNotification_(self, note):
# the NSMetadataQuery will send back a note when updates are happening.
# By looking at the [note name], we can tell what is happening
if note.name() == Cocoa.NSMetadataQueryDidStartGatheringNotification:
# the query has just started
Cocoa.NSLog("search: started gathering")
self.progressSearch.setHidden_(False)
self.progressSearch.startAnimation_(self)
self.progressSearch.animate_(self)
self.progressSearchLabel.setStringValue_("Searching...")
elif note.name() == Cocoa.NSMetadataQueryDidFinishGatheringNotification:
# at this point, the query will be done. You may receive an update
# later on.
Cocoa.NSLog("search: finished gathering")
self.progressSearch.setHidden_(True)
self.progressSearch.stopAnimation_(self)
self.loadResultsFromQuery_(note)
elif note.name() == Cocoa.NSMetadataQueryGatheringProgressNotification:
# the query is still gathering results...
Cocoa.NSLog("search: progressing...")
self.progressSearch.animate_(self)
elif note.name() == Cocoa.NSMetadataQueryDidUpdateNotification:
# an update will happen when Spotlight notices that a file as
# added, removed, or modified that affected the search results.
Cocoa.NSLog("search: an update happened.")
# -------------------------------------------------------------------------
# inspect:selectedObjects
#
# This method obtains the selected object (in our case for single selection,
# it's the first item), and opens its URL.
# -------------------------------------------------------------------------
def inspect_(self, selectedObjects):
objectDict = selectedObjects[0]
if objectDict is not None:
url = objectDict["url"]
Cocoa.NSWorkspace.sharedWorkspace().openURL_(url)
# ------------------------------------------------------------------------
# spotlightFriendlyPredicate:predicate
#
# This method will "clean up" an NSPredicate to make it ready for Spotlight, or return nil
# if the predicate can't be cleaned.
#
# Foundation's Spotlight support in NSMetdataQuery places the following requirements on
# an NSPredicate:
# - Value-type (always YES or NO) predicates are not allowed
# - Any compound predicate (other than NOT) must have at least two subpredicates
# -------------------------------------------------------------------------
def spotlightFriendlyPredicate_(self, predicate):
if predicate == Cocoa.NSPredicate.predicateWithValue_(
True
) or predicate == Cocoa.NSPredicate.predicateWithValue_(False):
return False
elif isinstance(predicate, Cocoa.NSCompoundPredicate):
predicate_type = predicate.compoundPredicateType()
cleanSubpredicates = []
for dirtySubpredicate in predicate.subpredicates():
cleanSubpredicate = self.spotlightFriendlyPredicate_(dirtySubpredicate)
if cleanSubpredicate:
cleanSubpredicates.append(cleanSubpredicate)
if len(cleanSubpredicates) == 0:
return None
else:
if (
len(cleanSubpredicates) == 1
and predicate_type != Cocoa.NSNotPredicateType
):
return cleanSubpredicates[0]
else:
return (
Cocoa.NSCompoundPredicate.alloc().initWithType_subpredicates_(
predicate_type, cleanSubpredicates
)
)
else:
return predicate
# -------------------------------------------------------------------------
# createNewSearchForPredicate:predicate:withTitle
#
# -------------------------------------------------------------------------
def createNewSearchForPredicate_withTitle_(self, predicate, title):
if predicate is not None:
self.mySearchResults.removeObjects_(self.mySearchResults.arrangedObjects())
# remove the old search results
# always search for items in the Address Book
addrBookPredicate = Cocoa.NSPredicate.predicateWithFormat_(
"(kMDItemKind = 'Address Book Person Data')"
)
predicate = Cocoa.NSCompoundPredicate.andPredicateWithSubpredicates_(
[addrBookPredicate, predicate]
)
self.query.setPredicate_(predicate)
self.query.startQuery()
# --------------------------------------------------------------------------
# predicateEditorChanged:sender
#
# This method gets called whenever the predicate editor changes.
# It is the action of our predicate editor and the single plate for all our updates.
#
# We need to do potentially three things:
# 1) Fire off a search if the user hits enter.
# 2) Add some rows if the user deleted all of them, so the user isn't left
# without any rows.
# 3) Resize the window if the number of rows changed (the user hit + or -).
# --------------------------------------------------------------------------
@objc.IBAction
def predicateEditorChanged_(self, sender):
# check NSApp currentEvent for the return key
event = Cocoa.NSApp.currentEvent()
if event is None:
return
if event.type() == Cocoa.NSKeyDown:
characters = event.characters()
if len(characters) > 0 and characters[0] == "\r":
# get the predicate, which is the object value of our view
predicate = self.predicateEditor.objectValue()
# make it Spotlight friendly
predicate = self.spotlightFriendlyPredicate_(predicate)
if predicate is not None:
global searchIndex
title = Cocoa.NSLocalizedString("Search #%ld", "Search title")
self.createNewSearchForPredicate_withTitle_(
predicate, title % searchIndex
)
searchIndex += 1
# if the user deleted the first row, then add it again - no sense
# leaving the user with no rows
if self.predicateEditor.numberOfRows() == 0:
self.predicateEditor.addRow_(self)
# resize the window vertically to accommodate our views:
# get the new number of rows, which tells us the needed change in height,
# note that we can't just get the view frame, because it's currently
# animating - this method is called before the animation is finished.
newRowCount = self.predicateEditor.numberOfRows()
# if there's no change in row count, there's no need to resize anything
if newRowCount == self.previousRowCount:
return
# The autoresizing masks, by default, allows the NSTableView to grow
# and keeps the predicate editor fixed. We need to temporarily grow the
# predicate editor, and keep the NSTableView fixed, so we have to change
# the autoresizing masks.
# Save off the old ones; we'll restore them after changing the window frame.
tableScrollView = self.myTableView.enclosingScrollView()
oldOutlineViewMask = tableScrollView.autoresizingMask()
predicateEditorScrollView = self.predicateEditor.enclosingScrollView()
oldPredicateEditorViewMask = predicateEditorScrollView.autoresizingMask()
tableScrollView.setAutoresizingMask_(
Cocoa.NSViewWidthSizable | Cocoa.NSViewMaxYMargin
)
predicateEditorScrollView.setAutoresizingMask_(
Cocoa.NSViewWidthSizable | Cocoa.NSViewHeightSizable
)
# determine if we need to grow or shrink the window
growing = newRowCount > self.previousRowCount
# if growing, figure out by how much. Sizes must contain nonnegative
# values, which is why we avoid negative floats here.
heightDifference = abs(
self.predicateEditor.rowHeight() * (newRowCount - self.previousRowCount)
)
# convert the size to window coordinates -
# if we didn't do this, we would break under scale factors other than 1.
# We don't care about the horizontal dimension, so leave that as 0.
#
sizeChange = self.predicateEditor.convertSize_toView_(
Cocoa.NSMakeSize(0, heightDifference), None
)
# offset our status view
frame = self.progressView.frame()
self.progressView.setFrameOrigin_(
Cocoa.NSMakePoint(
frame.origin.x,
frame.origin.y
- self.predicateEditor.rowHeight()
* (newRowCount - self.previousRowCount),
)
)
# change the window frame size:
# - if we're growing, the height goes up and the origin goes down
# (corresponding to growing down).
# - if we're shrinking, the height goes down and the origin goes up.
windowFrame = self.window().frame()
if growing:
windowFrame.size.height += sizeChange.height
windowFrame.origin.y -= sizeChange.height
else:
windowFrame.size.height -= sizeChange.height
windowFrame.origin.y += sizeChange.height
self.window().setFrame_display_animate_(windowFrame, True, True)
# restore the autoresizing mask
tableScrollView.setAutoresizingMask_(oldOutlineViewMask)
predicateEditorScrollView.setAutoresizingMask_(oldPredicateEditorViewMask)
self.previousRowCount = newRowCount # save our new row count
main.py¶
import CaseInsensitivePredicateTemplate # noqa: F401
import MyWindowController # noqa: F401
from PyObjCTools import AppHelper
AppHelper.runEventLoop()
setup.py¶
"""
Script for building the example.
Usage:
python3 setup.py py2app
"""
from setuptools import setup
setup(
name="PredicateEditorSample",
app=["main.py"],
data_files=["English.lproj"],
setup_requires=["py2app", "pyobjc-framework-Cocoa"],
)