1from Cocoa import * 2 3import math 4 5searchIndex = 0 6 7class MyWindowController (NSWindowController): 8 query = objc.ivar() 9 previousRowCount = objc.ivar(objc._C_INT) 10 11 myTableView = objc.IBOutlet() 12 mySearchResults = objc.IBOutlet() 13 predicateEditor = objc.IBOutlet() 14 progressView = objc.IBOutlet() # the progress search view 15 progressSearch = objc.IBOutlet() # spinning gear 16 progressSearchLabel = objc.IBOutlet() # search result # 17 18 19 def dealloc(self): 20 NSNotificationCenter.defaultCenter().removeObserver_(self) 21 22 def awakeFromNib(self): 23 # no vertical scrolling, we always want to show all rows 24 self.predicateEditor.enclosingScrollView().setHasVerticalScroller_(False) 25 26 self.previousRowCount = 3 27 self.predicateEditor.addRow_(self) 28 29 # put the focus in the first text field 30 displayValue = self.predicateEditor.displayValuesForRow_(1).lastObject() 31 if isinstance(displayValue, NSControl): 32 self.window().makeFirstResponder_(displayValue) 33 34 # create and initalize our query 35 self.query = NSMetadataQuery.alloc().init() 36 37 # setup our Spotlight notifications 38 nf = NSNotificationCenter.defaultCenter() 39 nf.addObserver_selector_name_object_(self, 'queryNotification:', None, self.query) 40 41 # initialize our Spotlight query, sort by contact name 42 43 # XXX: this framework isn't wrapped yet! 44 self.query.setSortDescriptors_([NSSortDescriptor.alloc().initWithKey_ascending_( 45 'kMDItemContactKeywords', True)]) 46 self.query.setDelegate_(self) 47 48 # start with our progress search label empty 49 self.progressSearchLabel.setStringValue_("") 50 51 return 52 53 def applicationShouldTerminateAfterLastWindowClosed_(self, sender): 54 return True 55 56 def loadResultsFromQuery_(self, notif): 57 results = notif.object().results() 58 59 NSLog("search count = %d", len(results)) 60 foundResultsStr = "Results found: %d"%(len(results),) 61 self.progressSearchLabel.setStringValue_(foundResultsStr) 62 63 # iterate through the array of results, and match to the existing stores 64 for item in results: 65 cityStr = item.valueForAttribute_('kMDItemCity') 66 nameStr = item.valueForAttribute_('kMDItemDisplayName') 67 stateStr = item.valueForAttribute_('kMDItemStateOrProvince') 68 phoneNumbers = item.valueForAttribute_('kMDItemPhoneNumbers') 69 phoneStr = None 70 if phoneNumbers: 71 phoneStr = phoneNumbers[0] 72 73 storePath = item.valueForAttribute_('kMDItemPath').stringByResolvingSymlinksInPath() 74 75 # create a dictionary entry to be added to our search results array 76 emptyStr = "" 77 dict = { 78 'name': nameStr or "", 79 'phone': phoneStr or "", 80 'city': cityStr or "", 81 'state': stateStr or "", 82 'url': NSURL.fileURLWithPath_(storePath), 83 } 84 self.mySearchResults.append(dict) 85 86 def queryNotification_(self, note): 87 # the NSMetadataQuery will send back a note when updates are happening. 88 # By looking at the [note name], we can tell what is happening 89 if note.name() == NSMetadataQueryDidStartGatheringNotification: 90 # the query has just started 91 NSLog("search: started gathering") 92 93 self.progressSearch.setHidden_(False) 94 self.progressSearch.startAnimation_(self) 95 self.progressSearch.animate_(self) 96 self.progressSearchLabel.setStringValue_("Searching...") 97 98 elif note.name() == NSMetadataQueryDidFinishGatheringNotification: 99 # at this point, the query will be done. You may recieve an update 100 # later on. 101 NSLog("search: finished gathering"); 102 103 self.progressSearch.setHidden_(True) 104 self.progressSearch.stopAnimation_(self) 105 106 self.loadResultsFromQuery_(note) 107 108 elif note.name() == NSMetadataQueryGatheringProgressNotification: 109 # the query is still gathering results... 110 NSLog("search: progressing...") 111 112 self.progressSearch.animate_(self) 113 114 elif note.name() == NSMetadataQueryDidUpdateNotification: 115 # an update will happen when Spotlight notices that a file as 116 # added, removed, or modified that affected the search results. 117 NSLog("search: an update happened.") 118 119 # ------------------------------------------------------------------------- 120 # inspect:selectedObjects 121 # 122 # This method obtains the selected object (in our case for single selection, 123 # it's the first item), and opens its URL. 124 # ------------------------------------------------------------------------- 125 def inspect_(self, selectedObjects): 126 objectDict = selectedObjects[0] 127 if objectDict is not None: 128 url = objectDict['url'] 129 NSWorkspace.sharedWorkspace().openURL_(url) 130 131 # ------------------------------------------------------------------------ 132 # spotlightFriendlyPredicate:predicate 133 # 134 # This method will "clean up" an NSPredicate to make it ready for Spotlight, or return nil if the predicate can't be cleaned. 135 # 136 # Foundation's Spotlight support in NSMetdataQuery places the following requirements on an NSPredicate: 137 # - Value-type (always YES or NO) predicates are not allowed 138 # - Any compound predicate (other than NOT) must have at least two subpredicates 139 # ------------------------------------------------------------------------- 140 def spotlightFriendlyPredicate_(self, predicate): 141 if predicate == NSPredicate.predicateWithValue_(True) or predicate == NSPredicate.predicateWithValue_(False): 142 return False 143 144 elif isinstance(predicate, NSCompoundPredicate): 145 146 type = predicate.compoundPredicateType() 147 cleanSubpredicates = [] 148 for dirtySubpredicate in predicate.subpredicates(): 149 cleanSubpredicate = self.spotlightFriendlyPredicate_( 150 dirtySubpredicate) 151 if cleanSubpredicate: 152 cleanSubpredicates.append(cleanSubpredicate) 153 154 if len(cleanSubpredicates) == 0: 155 return None 156 157 else: 158 if len(cleanSubpredicates) == 1 and type != NSNotPredicateType: 159 return cleanSubpredicates[0] 160 161 else: 162 return NSCompoundPredicate.alloc().initWithType_subpredicates_(type, cleanSubpredicates) 163 164 else: 165 return predicate 166 167 # ------------------------------------------------------------------------- 168 # createNewSearchForPredicate:predicate:withTitle 169 # 170 # ------------------------------------------------------------------------- 171 def createNewSearchForPredicate_withTitle_(self, predicate, title): 172 if predicate is not None: 173 self.mySearchResults.removeObjects_( 174 self.mySearchResults.arrangedObjects()); # remove the old search results 175 176 # always search for items in the Address Book 177 addrBookPredicate = NSPredicate.predicateWithFormat_( 178 "(kMDItemKind = 'Address Book Person Data')") 179 predicate = NSCompoundPredicate.andPredicateWithSubpredicates_( 180 [addrBookPredicate, predicate]) 181 182 self.query.setPredicate_(predicate) 183 self.query.startQuery() 184 185 # -------------------------------------------------------------------------- 186 # predicateEditorChanged:sender 187 # 188 # This method gets called whenever the predicate editor changes. 189 # It is the action of our predicate editor and the single plate for all our updates. 190 # 191 # We need to do potentially three things: 192 # 1) Fire off a search if the user hits enter. 193 # 2) Add some rows if the user deleted all of them, so the user isn't left without any rows. 194 # 3) Resize the window if the number of rows changed (the user hit + or -). 195 # -------------------------------------------------------------------------- 196 @objc.IBAction 197 def predicateEditorChanged_(self, sender): 198 # check NSApp currentEvent for the return key 199 event = NSApp.currentEvent() 200 if event is None: 201 return 202 203 if event.type() == NSKeyDown: 204 characters = event.characters() 205 if len(characters) > 0 and characters[0] == u'\r': 206 # get the predicat, which is the object value of our view 207 predicate = self.predicateEditor.objectValue() 208 209 # make it Spotlight friendly 210 predicate = self.spotlightFriendlyPredicate_(predicate) 211 if predicate is not None: 212 global searchIndex 213 title = NSLocalizedString("Search #%ld", "Search title"); 214 self.createNewSearchForPredicate_withTitle_( 215 predicate, title % searchIndex) 216 searchIndex += 1 217 218 # if the user deleted the first row, then add it again - no sense leaving the user with no rows 219 if self.predicateEditor.numberOfRows() == 0: 220 self.predicateEditor.addRow_(self) 221 222 # resize the window vertically to accomodate our views: 223 224 # get the new number of rows, which tells us the needed change in height, 225 # note that we can't just get the view frame, because it's currently animating - this method is called before the animation is finished. 226 newRowCount = self.predicateEditor.numberOfRows() 227 228 # if there's no change in row count, there's no need to resize anything 229 if newRowCount == self.previousRowCount: 230 return 231 232 # The autoresizing masks, by default, allows the NSTableView to grow and keeps the predicate editor fixed. 233 # We need to temporarily grow the predicate editor, and keep the NSTableView fixed, so we have to change the autoresizing masks. 234 # Save off the old ones; we'll restore them after changing the window frame. 235 tableScrollView = self.myTableView.enclosingScrollView() 236 oldOutlineViewMask = tableScrollView.autoresizingMask() 237 238 predicateEditorScrollView = self.predicateEditor.enclosingScrollView() 239 oldPredicateEditorViewMask = predicateEditorScrollView.autoresizingMask() 240 241 tableScrollView.setAutoresizingMask_( 242 NSViewWidthSizable | NSViewMaxYMargin) 243 predicateEditorScrollView.setAutoresizingMask_( 244 NSViewWidthSizable | NSViewHeightSizable) 245 246 # determine if we need to grow or shrink the window 247 growing = (newRowCount > self.previousRowCount) 248 249 # if growing, figure out by how much. Sizes must contain nonnegative values, which is why we avoid negative floats here. 250 heightDifference = abs(self.predicateEditor.rowHeight() * (newRowCount - self.previousRowCount)) 251 252 # convert the size to window coordinates - 253 # if we didn't do this, we would break under scale factors other than 1. 254 # We don't care about the horizontal dimension, so leave that as 0. 255 # 256 sizeChange = self.predicateEditor.convertSize_toView_( 257 NSMakeSize(0, heightDifference), None) 258 259 # offset our status view 260 frame = self.progressView.frame() 261 self.progressView.setFrameOrigin_(NSMakePoint( 262 frame.origin.x, 263 frame.origin.y - self.predicateEditor.rowHeight() * (newRowCount - self.previousRowCount))) 264 265 # change the window frame size: 266 # - if we're growing, the height goes up and the origin goes down (corresponding to growing down). 267 # - if we're shrinking, the height goes down and the origin goes up. 268 windowFrame = self.window().frame() 269 if growing: 270 windowFrame.size.height += sizeChange.height 271 windowFrame.origin.y -= sizeChange.height 272 else: 273 windowFrame.size.height -= sizeChange.height 274 windowFrame.origin.y += sizeChange.height 275 276 self.window().setFrame_display_animate_(windowFrame, True, True) 277 278 # restore the autoresizing mask 279 tableScrollView.setAutoresizingMask_(oldOutlineViewMask) 280 predicateEditorScrollView.setAutoresizingMask_(oldPredicateEditorViewMask) 281 282 self.previousRowCount = newRowCount # save our new row count 283