|
21 | 21 |
|
22 | 22 | exports.PackageController = PackageController |
23 | 23 |
|
| 24 | + /* |
| 25 | + * Utility class used by the PackageController. |
| 26 | + * |
| 27 | + * A SetProp instance maintains a `d3.set` of ids. By adding a |
| 28 | + * `widget` (e.g. a PackageWidget instance) with a given `id`, |
| 29 | + * it will use the `getCurrent` and `getChanges` to make sure |
| 30 | + * the "state" of that widget is kept track of in thet set. |
| 31 | + * |
| 32 | + * This is used as an alternative to `Bacon.combineTemplate`, |
| 33 | + * which seems to perform poorly when there are thousands of |
| 34 | + * items in the template. |
| 35 | + * |
| 36 | + * The intended use case for this class is to create a property |
| 37 | + * representing the set of all "selected" packages, and another |
| 38 | + * property representing the set of all "instrumented" packages. |
| 39 | + * |
| 40 | + * @param getCurrent - a function(widget) that returns the current |
| 41 | + * "state" of the widget, as a boolean |
| 42 | + * @param getChanges - a function(widget) that returns a |
| 43 | + * Bacon.Observable that pushes boolean events that represent |
| 44 | + * the widget's "state" as it changes. |
| 45 | + */ |
| 46 | + function SetProp(getCurrent, getChanges){ |
| 47 | + |
| 48 | + var set = d3.set() |
| 49 | + var bus = new Bacon.Bus() |
| 50 | + |
| 51 | + this.prop = bus.toProperty(set) |
| 52 | + |
| 53 | + this.add = function(id, widget){ |
| 54 | + var current = getCurrent(widget) |
| 55 | + if(current) set.add(id) |
| 56 | + |
| 57 | + getChanges(widget).onValue(function(bool){ |
| 58 | + if(bool & !set.has(id)) { |
| 59 | + set.add(id) |
| 60 | + bus.push(set) |
| 61 | + } else if(!bool && set.has(id)){ |
| 62 | + set.remove(id) |
| 63 | + bus.push(set) |
| 64 | + } |
| 65 | + }) |
| 66 | + } |
| 67 | + } |
| 68 | + |
24 | 69 | function PackageController(treeData, $container, $totalsContainer, $clearSelectionButton){ |
25 | 70 |
|
26 | 71 | // build this into a Map[packageId -> packageWidget.selectedProp] |
27 | | - var stateTemplate = {} |
| 72 | + var selectedWidgetsSP = new SetProp( |
| 73 | + /*getCurrent*/ function(pw){ return pw.selected() }, |
| 74 | + /*getChanges*/ function(pw){ return pw.selectedProp.changes() } |
| 75 | + ) |
28 | 76 |
|
29 | 77 | // build this into a Map[packageId -> packageWidget.instrumentationSelectedProp] |
30 | | - var instrumentedTemplate = {} |
| 78 | + var instrumentedWidgetsSP = new SetProp( |
| 79 | + /*getCurrent*/ function(pw){ return pw.instrumentationSelected() }, |
| 80 | + /*getChanges*/ function(pw){ return pw.instrumentationSelectedProp.changes() } |
| 81 | + ) |
31 | 82 |
|
32 | 83 | var widgets = {} |
33 | 84 |
|
|
76 | 127 |
|
77 | 128 | })(treeData.root, undefined) |
78 | 129 |
|
| 130 | + var widgetCount = 0 |
| 131 | + |
79 | 132 | // initialize the `stateTemplate` and `widgets` maps |
80 | 133 | // based on the package nodes in `treeData` |
81 | 134 | ;(function setupTreeHierarchy(packageParentNode, node){ |
82 | 135 |
|
83 | 136 | var pw = new PackageWidget() |
84 | 137 | widgets[node.id] = pw |
| 138 | + pw.associatedNode = node |
| 139 | + // pw.parentNode = packageParentNode |
| 140 | + widgetCount++ |
85 | 141 |
|
86 | 142 | pw.instrumentationSelected(node.partialTraceSelection) |
87 | 143 |
|
88 | 144 | pw.collapseChildren(/* collapsed = */true, /* animate = */false) |
89 | 145 |
|
90 | | - stateTemplate[node.id] = pw.selectedProp |
91 | | - pw.selectionClicks.onValue(function(){ |
92 | | - handleSelectionClick(node, pw) |
93 | | - }) |
94 | | - |
95 | | - instrumentedTemplate[node.id] = pw.instrumentationSelectedProp |
96 | | - pw.instrumentationSelectedClicks.onValue(function(){ |
97 | | - handleInstrumentationSelectionClick(node, pw) |
98 | | - }) |
99 | | - |
100 | 146 | pw.uiParts.collapser.click(function(event){ |
101 | 147 | pw.collapseChildren('toggle', true) |
102 | 148 | event.stopPropagation() |
103 | 149 | }) |
104 | 150 |
|
| 151 | + node.children |
| 152 | + .sort(function(a,b){ |
| 153 | + // alphabetic sort by node.label |
| 154 | + var an = a.label.toUpperCase(), |
| 155 | + bn = b.label.toUpperCase() |
| 156 | + |
| 157 | + if(an < bn) return -1 |
| 158 | + if(an > bn) return 1 |
| 159 | + return 0 |
| 160 | + }) |
| 161 | + .forEach(function(kid){ |
| 162 | + var nextParent = (node.kind == 'group' || node.kind == 'package')? node : packageParentNode |
| 163 | + setupTreeHierarchy(nextParent, kid) |
| 164 | + }) |
105 | 165 |
|
| 166 | + // Figure out the full+abbreviated labels for the widget |
106 | 167 | if(node.kind == 'root'){ |
107 | | - pw.uiParts.main.appendTo($totalsContainer) |
108 | 168 | pw.abbreviatedLabel('Overall Coverage') |
| 169 | + } else if(node.isSelfNode){ |
| 170 | + pw.fullLabel(node.label).abbreviatedLabel(node.label) |
| 171 | + } else { |
| 172 | + var abbrevName |
| 173 | + |
| 174 | + if (node.kind != 'group' && packageParentNode && packageParentNode.kind == 'group') |
| 175 | + abbrevName = node.label |
| 176 | + else { |
| 177 | + var parentName = packageParentNode ? packageParentNode.label : '', |
| 178 | + abbrevName = node.label.substr(parentName.length) |
| 179 | + } |
| 180 | + |
| 181 | + pw.fullLabel(node.label).abbreviatedLabel(abbrevName) |
| 182 | + } |
| 183 | + |
| 184 | + if(node.kind == 'root'){ |
| 185 | + pw.uiParts.main.appendTo($totalsContainer) |
| 186 | + // pw.abbreviatedLabel('Overall Coverage') |
109 | 187 | pw.selectable(false) |
110 | 188 | pw.instrumentationSelectable(false) |
111 | 189 |
|
|
118 | 196 |
|
119 | 197 | pw.methodCount(node.methodCount) |
120 | 198 |
|
| 199 | + |
| 200 | + |
121 | 201 | if(packageParentNode){ |
122 | 202 | widgets[packageParentNode.id].children.add(pw) |
123 | 203 | } else { |
124 | 204 | pw.uiParts.main.appendTo($container) |
125 | 205 | } |
126 | 206 |
|
127 | | - if(node.isSelfNode){ |
128 | | - pw.fullLabel(node.label).abbreviatedLabel(node.label) |
129 | | - } else { |
130 | | - var abbrevName |
131 | | - |
132 | | - if (node.kind != 'group' && packageParentNode && packageParentNode.kind == 'group') |
133 | | - abbrevName = node.label |
134 | | - else { |
135 | | - var parentName = packageParentNode ? packageParentNode.label : '', |
136 | | - abbrevName = node.label.substr(parentName.length) |
137 | | - } |
138 | | - |
139 | | - pw.fullLabel(node.label).abbreviatedLabel(abbrevName) |
140 | | - } |
141 | 207 | } |
142 | 208 |
|
143 | | - node.children |
144 | | - .sort(function(a,b){ |
145 | | - // alphabetic sort by node.label |
146 | | - var an = a.label.toUpperCase(), |
147 | | - bn = b.label.toUpperCase() |
| 209 | + })(undefined, treeData.root) |
148 | 210 |
|
149 | | - if(an < bn) return -1 |
150 | | - if(an > bn) return 1 |
151 | | - return 0 |
152 | | - }) |
153 | | - .forEach(function(kid){ |
154 | | - var nextParent = (node.kind == 'group' || node.kind == 'package')? node : packageParentNode |
155 | | - setupTreeHierarchy(nextParent, kid) |
156 | | - }) |
| 211 | + console.log('created', widgetCount, 'PackageWidgets') |
157 | 212 |
|
158 | | - })(undefined, treeData.root) |
| 213 | + function forEachWidget(f){ |
| 214 | + for(var id in widgets){ |
| 215 | + var w = widgets[id], n = w.associatedNode |
| 216 | + f(w,n,id) |
| 217 | + } |
| 218 | + } |
| 219 | + |
| 220 | + forEachWidget(function(pw, node, id){ |
| 221 | + selectedWidgetsSP.add(id, pw) |
| 222 | + instrumentedWidgetsSP.add(id, pw) |
| 223 | + pw.selectionClicks.onValue(function(){ |
| 224 | + handleSelectionClick(node, pw) |
| 225 | + }) |
| 226 | + pw.instrumentationSelectedClicks.onValue(function(){ |
| 227 | + handleInstrumentationSelectionClick(node, pw) |
| 228 | + }) |
| 229 | + }) |
159 | 230 |
|
160 | 231 | // Disable all of the widgets while the trace is running |
161 | 232 | Trace.running.onValue(function(isRunning){ |
162 | | - for(var id in widgets){ |
163 | | - var pw = widgets[id], |
164 | | - node = treeData.getNode(id) |
165 | | - |
| 233 | + forEachWidget(function(pw, node){ |
166 | 234 | if(node.kind != 'root'){ |
167 | 235 | pw.instrumentationSelectable(!isRunning) |
168 | 236 | } |
169 | | - } |
| 237 | + }) |
170 | 238 | }) |
171 | 239 |
|
172 | 240 | // checkSelected = function(widget){ return <is widget selected> } |
|
230 | 298 |
|
231 | 299 | /** |
232 | 300 | * Exposes the selection state of each of the widgets, as a |
233 | | - * Map[package.id -> isSelected] |
| 301 | + * Set containing the IDs of selected widgets. |
234 | 302 | */ |
235 | | - this.selectedWidgets = Bacon.combineTemplate(stateTemplate).debounce(10) |
| 303 | + this.selectedWidgets = selectedWidgetsSP.prop.debounce(10).noLazy() |
236 | 304 |
|
237 | 305 | /** |
238 | 306 | * Exposes the 'instrumented' state of each of the widgets, as |
239 | | - * a Map[package.id -> isInstrumented] |
| 307 | + * a Set containing the IDs of widgets that are marked as instrumented. |
240 | 308 | */ |
241 | | - this.instrumentedWidgets = Bacon.combineTemplate(instrumentedTemplate).debounce(10) |
| 309 | + this.instrumentedWidgets = instrumentedWidgetsSP.prop.debounce(10).noLazy() |
242 | 310 |
|
243 | 311 | // Decide whether or not to show the "clear all selections" button |
244 | 312 | // depending on whether or not there are selected widgets |
245 | 313 | this.selectedWidgets |
246 | | - .map(function(selectionMap){ |
247 | | - for(var k in selectionMap) if(selectionMap[k]) return true |
| 314 | + .map(function(selectedIds){ |
| 315 | + return !selectedIds.size() |
248 | 316 | }) |
249 | | - .not() |
250 | 317 | .assign($clearSelectionButton, 'toggleClass', 'hidden') |
251 | 318 |
|
252 | 319 | // Set all (selectable) widgets to not be selected when the clear selection button is clicked |
|
282 | 349 | this.applyMethodCoverage = function(coverageRecords, activeRecordings){ |
283 | 350 | applyMethodCoverage(treeData, widgets, coverageRecords, activeRecordings, nodePackageParents) |
284 | 351 | } |
| 352 | + |
285 | 353 | } |
286 | 354 |
|
287 | 355 | // Trigger a `flashHighlight` on the appropriate package widgets |
|
0 commit comments