1from Cocoa import * 2from Quartz import * 3 4# APPLICATION DATA STORAGE NOTES: 5# - This application uses a simple data storage as an array of entries, 6# each containing two attributes: a label and a value 7# - The array is represented as a NSMutableArray, the entries as 8# NSMutableDictionaries, the label as a NSString with the "label" key and 9# the value as a NSNumber with the "value" key 10# 11# 12# QUARTZ COMPOSER COMPOSITION NOTES: 13# - The enclosed Quartz Composer composition renders a 3D bars chart and is 14# loaded on the QCView in the application's window 15# - This composition has three input parameters: 16# * "Data": the data to display by the chart which must be formatted as a 17# NSArray of NSDictionaries, each NSDictionary containing "label" / NSString 18# and "value" / NSNumber value-key pairs 19# * "Scale": a NSNumber used to scale the chart bars 20# * "Spacing": a NSNumber indicating the extra spacing between the chart bars 21# - The "Data" and "Scale" input parameters are set programmatically while the 22# "Spacing" is set directly from the UI through Cocoa bindings 23# - Note that this composition is quite simple and has the following 24# limitations: 25# * it may have rendering artifacts when looking at the chart from some angles 26# * it does not support negative values 27# * labels are not truncated if too long 28# - Basically, the composition performs the following: 29# * renders a background gradient 30# * draws three planes on the X, Y and Z axes 31# * uses an Iterator patch to loop on the chart data, which is available as 32# a Structure, and for each member, retrieves the label and value, then 33# draws them 34# * the chart rendering is enclosed into a Camera macro patch used to center 35# it in the view 36# * the Camera macro patch is itself enclosed into a TrackBall macro patch 37# so that the user can rotate the chart with the mouse 38# * the TrackBall macro patch is itself enclosed into a Lighting macro patch 39# so that the chart is lighted 40# - This composition makes uses of transparency for a nicer effect, but 41# neither OpenGL nor Quartz Composer handle automatically proper rendering 42# of mixed opaque and transparent 3D objects 43# - A simple, but not fail-proof, algorithm to render opaque and transparent 44# 3D objects is to: 45# * render opaque objects first with depth testing set to "Read / Write" 46# * render transparent objects with depth testing set to "Read-Only" 47# 48# 49# NIB FILES NOTES: 50# - The QCView is configured to start rendering automatically and forward user 51# events (mouse events are required to rotate the chart) 52# - An AppController instance is connected as the data source for the 53# NSTableView 54# - The NSTableView is set up so that the identifiers of table columns match 55# the keys used in the data storage 56# - The "Value" column of the NSTableView has a NSNumberFormatter which 57# guarantees only positive or null numbers can be entered here 58# - The "Label" column of the NSTableView simply contains text 59# 60 61# Keys for the entries in the data storage 62kDataKey_Label = "label" # NSString 63kDataKey_Value = "value" # NSNumber 64 65# Keys for the composition input parameters 66kParameterKey_Data = "Data" # NSArray of NSDictionaries 67kParameterKey_Scale = "Scale" # NSNumber 68kParameterKey_Spacing = "Spacing" # NSNumber 69 70class AppController (NSObject): 71 tableView = objc.IBOutlet() 72 view = objc.IBOutlet() 73 74 _data = objc.ivar() 75 76 def init(self): 77 # Allocate our data storage 78 self = super(AppController, self).init() 79 if self is None: 80 return None 81 82 self._data = [] 83 84 return self 85 86 def awakeFromNib(self): 87 # Load the composition file into the QCView (because this 88 # QCView is bound to a QCPatchController in the nib file, this 89 # will actually update the QCPatchController along with all the 90 # bindings) 91 if not self.view.loadCompositionFromFile_(NSBundle.mainBundle().pathForResource_ofType_("Chart", "qtz")): 92 NSLog("Composition loading failed") 93 NSApp.terminate_(None) 94 95 # Populate data storage 96 self._data.extend([ 97 { 98 kDataKey_Label:"Palo Alto", 99 kDataKey_Value: 2, 100 }, 101 { 102 kDataKey_Label: "Cupertino", 103 kDataKey_Value: 1, 104 }, 105 { 106 kDataKey_Label: "Menlo Park", 107 kDataKey_Value: 4, 108 }, 109 { 110 kDataKey_Label: "Mountain View", 111 kDataKey_Value: 8, 112 }, 113 { 114 kDataKey_Label: "San Francisco", 115 kDataKey_Value: 7, 116 }, 117 { 118 kDataKey_Label: "Los Altos", 119 kDataKey_Value: 3, 120 }, 121 ]) 122 123 #Initialize the views 124 self.tableView.reloadData() 125 self.updateChart() 126 127 def updateChart(self): 128 #Update the data displayed by the chart - it will be converted to a 129 # Structure of Structures by Quartz Composer 130 self.view.setValue_forInputKey_(self._data, kParameterKey_Data) 131 132 #Compute the maximum value and set the chart scale accordingly 133 max = 0.0 134 for obj in self._data: 135 value = obj[kDataKey_Value] 136 if value > max: 137 max = value 138 139 if max == 0.0: 140 scale = 1.0 141 else: 142 scale = 1/max 143 self.view.setValue_forInputKey_(scale, kParameterKey_Scale) 144 145 @objc.IBAction 146 def addEntry_(self, sender): 147 #Add a new entry to the data storage 148 self._data.append({ 149 kDataKey_Label: "Untitled", 150 kDAtaKey_Value: 0, 151 }) 152 153 #Notify the NSTableView and update the chart 154 self.tableView.reloadData() 155 self.updateChart() 156 157 #Automatically select and edit the new entry 158 self.tableView.selectRow_byExtendingSelection(len(self._data)-1, False) 159 self.tableView.editColumn_row_withEvent_select_( 160 self.tableView.columnWithIdentifier_(kDataKey_Label), 161 len(self._data)-1, None, True) 162 163 @objc.IBAction 164 def removeEntry_(self, sender): 165 #Make sure we have a valid selected row 166 selectedRow = self.tableView.selectedRow() 167 if selectedRow < 0 or tableView.editedRow() == selectedRow: 168 return 169 170 #Remove the currently selected entry from the data storage 171 del self._data[selectedRow] 172 173 #Notify the NSTableView and update the chart 174 self.tableView.reloadData() 175 self.updateChart() 176 177 178 def numberOfRowsInTableView_(self, aTableView): 179 # Return the number of entries in the data storage 180 return len(self._data) 181 182 def tableView_objectValueForTableColumn_row_(self, aTableView, aTableColumn, rowIndex): 183 # Get the "label" or "value" attribute of the entry from the data 184 # storage at index "rowIndex" 185 return self._data[rowIndex][aTableColumn.identifier()] 186 187 def tableView_setObjectValue_forTableColumn_row_( 188 self, aTableView, anObject, aTableColumn, rowIndex): 189 190 # Set the "label" or "value" attribute of the entry from the data 191 # storage at index "rowIndex" 192 self._data[rowIndex][aTableColumn.identifier()] = anObject 193 194 # Update the chart 195 self.updateChart() 196