1 /* 2 * Copyright (C) IBM Corp. 2009 All rights reserved. 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions are 6 * met: 7 * 8 * * Redistributions of source code must retain the above copyright 9 * notice, this list of conditions and the following disclaimer. 10 * * Redistributions in binary form must reproduce the above 11 * copyright notice, this list of conditions and the following disclaimer 12 * in the documentation and/or other materials provided with the 13 * distribution. 14 * * Neither the name of IBM Corp. nor the names of its 15 * contributors may be used to endorse or promote products derived from 16 * this software without specific prior written permission. 17 * 18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 */ 30 31 /** 32 * @constructor 33 * @extends {WebInspector.SidebarPane} 34 */ 35 WebInspector.WatchExpressionsSidebarPane = function() 36 { 37 WebInspector.SidebarPane.call(this, WebInspector.UIString("Watch Expressions")); 38 39 this.section = new WebInspector.WatchExpressionsSection(); 40 this.bodyElement.appendChild(this.section.element); 41 42 var refreshButton = document.createElement("button"); 43 refreshButton.className = "pane-title-button refresh"; 44 refreshButton.addEventListener("click", this._refreshButtonClicked.bind(this), false); 45 refreshButton.title = WebInspector.UIString("Refresh"); 46 this.titleElement.appendChild(refreshButton); 47 48 var addButton = document.createElement("button"); 49 addButton.className = "pane-title-button add"; 50 addButton.addEventListener("click", this._addButtonClicked.bind(this), false); 51 this.titleElement.appendChild(addButton); 52 addButton.title = WebInspector.UIString("Add watch expression"); 53 54 this._requiresUpdate = true; 55 } 56 57 WebInspector.WatchExpressionsSidebarPane.prototype = { 58 wasShown: function() 59 { 60 this._refreshExpressionsIfNeeded(); 61 }, 62 63 reset: function() 64 { 65 this.refreshExpressions(); 66 }, 67 68 refreshExpressions: function() 69 { 70 this._requiresUpdate = true; 71 this._refreshExpressionsIfNeeded(); 72 }, 73 74 addExpression: function(expression) 75 { 76 this.section.addExpression(expression); 77 this.expand(); 78 }, 79 80 _refreshExpressionsIfNeeded: function() 81 { 82 if (this._requiresUpdate && this.isShowing()) { 83 this.section.update(); 84 delete this._requiresUpdate; 85 } else 86 this._requiresUpdate = true; 87 }, 88 89 _addButtonClicked: function(event) 90 { 91 event.consume(); 92 this.expand(); 93 this.section.addNewExpressionAndEdit(); 94 }, 95 96 _refreshButtonClicked: function(event) 97 { 98 event.consume(); 99 this.refreshExpressions(); 100 }, 101 102 __proto__: WebInspector.SidebarPane.prototype 103 } 104 105 /** 106 * @constructor 107 * @extends {WebInspector.ObjectPropertiesSection} 108 */ 109 WebInspector.WatchExpressionsSection = function() 110 { 111 this._watchObjectGroupId = "watch-group"; 112 113 WebInspector.ObjectPropertiesSection.call(this, WebInspector.RemoteObject.fromPrimitiveValue("")); 114 115 this.treeElementConstructor = WebInspector.WatchedPropertyTreeElement; 116 this._expandedExpressions = {}; 117 this._expandedProperties = {}; 118 119 this.emptyElement = document.createElement("div"); 120 this.emptyElement.className = "info"; 121 this.emptyElement.textContent = WebInspector.UIString("No Watch Expressions"); 122 123 this.watchExpressions = WebInspector.settings.watchExpressions.get(); 124 125 this.headerElement.className = "hidden"; 126 this.editable = true; 127 this.expanded = true; 128 this.propertiesElement.addStyleClass("watch-expressions"); 129 130 this.element.addEventListener("mousemove", this._mouseMove.bind(this), true); 131 this.element.addEventListener("mouseout", this._mouseOut.bind(this), true); 132 this.element.addEventListener("dblclick", this._sectionDoubleClick.bind(this), false); 133 this.emptyElement.addEventListener("contextmenu", this._emptyElementContextMenu.bind(this), false); 134 } 135 136 WebInspector.WatchExpressionsSection.NewWatchExpression = "\xA0"; 137 138 WebInspector.WatchExpressionsSection.prototype = { 139 update: function(e) 140 { 141 if (e) 142 e.consume(); 143 144 function appendResult(expression, watchIndex, result, wasThrown) 145 { 146 if (!result) 147 return; 148 149 var property = new WebInspector.RemoteObjectProperty(expression, result); 150 property.watchIndex = watchIndex; 151 property.wasThrown = wasThrown; 152 153 // To clarify what's going on here: 154 // In the outer function, we calculate the number of properties 155 // that we're going to be updating, and set that in the 156 // propertyCount variable. 157 // In this function, we test to see when we are processing the 158 // last property, and then call the superclass's updateProperties() 159 // method to get all the properties refreshed at once. 160 properties.push(property); 161 162 if (properties.length == propertyCount) { 163 this.updateProperties(properties, [], WebInspector.WatchExpressionTreeElement, WebInspector.WatchExpressionsSection.CompareProperties); 164 165 // check to see if we just added a new watch expression, 166 // which will always be the last property 167 if (this._newExpressionAdded) { 168 delete this._newExpressionAdded; 169 170 var treeElement = this.findAddedTreeElement(); 171 if (treeElement) 172 treeElement.startEditing(); 173 } 174 175 // Force displaying delete button for hovered element. 176 if (this._lastMouseMovePageY) 177 this._updateHoveredElement(this._lastMouseMovePageY); 178 } 179 } 180 181 // TODO: pass exact injected script id. 182 RuntimeAgent.releaseObjectGroup(this._watchObjectGroupId) 183 var properties = []; 184 185 // Count the properties, so we known when to call this.updateProperties() 186 // in appendResult() 187 var propertyCount = 0; 188 for (var i = 0; i < this.watchExpressions.length; ++i) { 189 if (!this.watchExpressions[i]) 190 continue; 191 ++propertyCount; 192 } 193 194 // Now process all the expressions, since we have the actual count, 195 // which is checked in the appendResult inner function. 196 for (var i = 0; i < this.watchExpressions.length; ++i) { 197 var expression = this.watchExpressions[i]; 198 if (!expression) 199 continue; 200 201 WebInspector.runtimeModel.evaluate(expression, this._watchObjectGroupId, false, true, false, false, appendResult.bind(this, expression, i)); 202 } 203 204 if (!propertyCount) { 205 if (!this.emptyElement.parentNode) 206 this.element.appendChild(this.emptyElement); 207 } else { 208 if (this.emptyElement.parentNode) 209 this.element.removeChild(this.emptyElement); 210 } 211 212 // note this is setting the expansion of the tree, not the section; 213 // with no expressions, and expanded tree, we get some extra vertical 214 // white space 215 this.expanded = (propertyCount != 0); 216 }, 217 218 addExpression: function(expression) 219 { 220 this.watchExpressions.push(expression); 221 this.saveExpressions(); 222 this.update(); 223 }, 224 225 addNewExpressionAndEdit: function() 226 { 227 this._newExpressionAdded = true; 228 this.watchExpressions.push(WebInspector.WatchExpressionsSection.NewWatchExpression); 229 this.update(); 230 }, 231 232 _sectionDoubleClick: function(event) 233 { 234 if (event.target !== this.element && event.target !== this.propertiesElement && event.target !== this.emptyElement) 235 return; 236 event.consume(); 237 this.addNewExpressionAndEdit(); 238 }, 239 240 updateExpression: function(element, value) 241 { 242 if (value === null) { 243 var index = element.property.watchIndex; 244 this.watchExpressions.splice(index, 1); 245 } 246 else 247 this.watchExpressions[element.property.watchIndex] = value; 248 this.saveExpressions(); 249 this.update(); 250 }, 251 252 _deleteAllExpressions: function() 253 { 254 this.watchExpressions = []; 255 this.saveExpressions(); 256 this.update(); 257 }, 258 259 findAddedTreeElement: function() 260 { 261 var children = this.propertiesTreeOutline.children; 262 for (var i = 0; i < children.length; ++i) { 263 if (children[i].property.name === WebInspector.WatchExpressionsSection.NewWatchExpression) 264 return children[i]; 265 } 266 }, 267 268 saveExpressions: function() 269 { 270 var toSave = []; 271 for (var i = 0; i < this.watchExpressions.length; i++) 272 if (this.watchExpressions[i]) 273 toSave.push(this.watchExpressions[i]); 274 275 WebInspector.settings.watchExpressions.set(toSave); 276 return toSave.length; 277 }, 278 279 _mouseMove: function(e) 280 { 281 if (this.propertiesElement.firstChild) 282 this._updateHoveredElement(e.pageY); 283 }, 284 285 _mouseOut: function() 286 { 287 if (this._hoveredElement) { 288 this._hoveredElement.removeStyleClass("hovered"); 289 delete this._hoveredElement; 290 } 291 delete this._lastMouseMovePageY; 292 }, 293 294 _updateHoveredElement: function(pageY) 295 { 296 var candidateElement = this.propertiesElement.firstChild; 297 while (true) { 298 var next = candidateElement.nextSibling; 299 while (next && !next.clientHeight) 300 next = next.nextSibling; 301 if (!next || next.totalOffsetTop() > pageY) 302 break; 303 candidateElement = next; 304 } 305 306 if (this._hoveredElement !== candidateElement) { 307 if (this._hoveredElement) 308 this._hoveredElement.removeStyleClass("hovered"); 309 if (candidateElement) 310 candidateElement.addStyleClass("hovered"); 311 this._hoveredElement = candidateElement; 312 } 313 314 this._lastMouseMovePageY = pageY; 315 }, 316 317 _emptyElementContextMenu: function(event) 318 { 319 var contextMenu = new WebInspector.ContextMenu(event); 320 contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Add watch expression" : "Add Watch Expression"), this.addNewExpressionAndEdit.bind(this)); 321 contextMenu.show(); 322 }, 323 324 __proto__: WebInspector.ObjectPropertiesSection.prototype 325 } 326 327 WebInspector.WatchExpressionsSection.CompareProperties = function(propertyA, propertyB) 328 { 329 if (propertyA.watchIndex == propertyB.watchIndex) 330 return 0; 331 else if (propertyA.watchIndex < propertyB.watchIndex) 332 return -1; 333 else 334 return 1; 335 } 336 337 /** 338 * @constructor 339 * @extends {WebInspector.ObjectPropertyTreeElement} 340 * @param {WebInspector.RemoteObjectProperty} property 341 */ 342 WebInspector.WatchExpressionTreeElement = function(property) 343 { 344 WebInspector.ObjectPropertyTreeElement.call(this, property); 345 } 346 347 WebInspector.WatchExpressionTreeElement.prototype = { 348 onexpand: function() 349 { 350 WebInspector.ObjectPropertyTreeElement.prototype.onexpand.call(this); 351 this.treeOutline.section._expandedExpressions[this._expression()] = true; 352 }, 353 354 oncollapse: function() 355 { 356 WebInspector.ObjectPropertyTreeElement.prototype.oncollapse.call(this); 357 delete this.treeOutline.section._expandedExpressions[this._expression()]; 358 }, 359 360 onattach: function() 361 { 362 WebInspector.ObjectPropertyTreeElement.prototype.onattach.call(this); 363 if (this.treeOutline.section._expandedExpressions[this._expression()]) 364 this.expanded = true; 365 }, 366 367 _expression: function() 368 { 369 return this.property.name; 370 }, 371 372 update: function() 373 { 374 WebInspector.ObjectPropertyTreeElement.prototype.update.call(this); 375 376 if (this.property.wasThrown) { 377 this.valueElement.textContent = WebInspector.UIString("<not available>"); 378 this.listItemElement.addStyleClass("dimmed"); 379 } else 380 this.listItemElement.removeStyleClass("dimmed"); 381 382 var deleteButton = document.createElement("input"); 383 deleteButton.type = "button"; 384 deleteButton.title = WebInspector.UIString("Delete watch expression."); 385 deleteButton.addStyleClass("enabled-button"); 386 deleteButton.addStyleClass("delete-button"); 387 deleteButton.addEventListener("click", this._deleteButtonClicked.bind(this), false); 388 this.listItemElement.addEventListener("contextmenu", this._contextMenu.bind(this), false); 389 this.listItemElement.insertBefore(deleteButton, this.listItemElement.firstChild); 390 }, 391 392 /** 393 * @param {WebInspector.ContextMenu} contextMenu 394 * @override 395 */ 396 populateContextMenu: function(contextMenu) 397 { 398 if (!this.isEditing()) { 399 contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Add watch expression" : "Add Watch Expression"), this.treeOutline.section.addNewExpressionAndEdit.bind(this.treeOutline.section)); 400 contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Delete watch expression" : "Delete Watch Expression"), this._deleteButtonClicked.bind(this)); 401 } 402 if (this.treeOutline.section.watchExpressions.length > 1) 403 contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Delete all watch expressions" : "Delete All Watch Expressions"), this._deleteAllButtonClicked.bind(this)); 404 }, 405 406 _contextMenu: function(event) 407 { 408 var contextMenu = new WebInspector.ContextMenu(event); 409 this.populateContextMenu(contextMenu); 410 contextMenu.show(); 411 }, 412 413 _deleteAllButtonClicked: function() 414 { 415 this.treeOutline.section._deleteAllExpressions(); 416 }, 417 418 _deleteButtonClicked: function() 419 { 420 this.treeOutline.section.updateExpression(this, null); 421 }, 422 423 renderPromptAsBlock: function() 424 { 425 return true; 426 }, 427 428 /** 429 * @param {Event=} event 430 */ 431 elementAndValueToEdit: function(event) 432 { 433 return [this.nameElement, this.property.name.trim()]; 434 }, 435 436 editingCancelled: function(element, context) 437 { 438 if (!context.elementToEdit.textContent) 439 this.treeOutline.section.updateExpression(this, null); 440 441 WebInspector.ObjectPropertyTreeElement.prototype.editingCancelled.call(this, element, context); 442 }, 443 444 applyExpression: function(expression, updateInterface) 445 { 446 expression = expression.trim(); 447 448 if (!expression) 449 expression = null; 450 451 this.property.name = expression; 452 this.treeOutline.section.updateExpression(this, expression); 453 }, 454 455 __proto__: WebInspector.ObjectPropertyTreeElement.prototype 456 } 457 458 459 /** 460 * @constructor 461 * @extends {WebInspector.ObjectPropertyTreeElement} 462 * @param {WebInspector.RemoteObjectProperty} property 463 */ 464 WebInspector.WatchedPropertyTreeElement = function(property) 465 { 466 WebInspector.ObjectPropertyTreeElement.call(this, property); 467 } 468 469 WebInspector.WatchedPropertyTreeElement.prototype = { 470 onattach: function() 471 { 472 WebInspector.ObjectPropertyTreeElement.prototype.onattach.call(this); 473 if (this.hasChildren && this.propertyPath() in this.treeOutline.section._expandedProperties) 474 this.expand(); 475 }, 476 477 onexpand: function() 478 { 479 WebInspector.ObjectPropertyTreeElement.prototype.onexpand.call(this); 480 this.treeOutline.section._expandedProperties[this.propertyPath()] = true; 481 }, 482 483 oncollapse: function() 484 { 485 WebInspector.ObjectPropertyTreeElement.prototype.oncollapse.call(this); 486 delete this.treeOutline.section._expandedProperties[this.propertyPath()]; 487 }, 488 489 __proto__: WebInspector.ObjectPropertyTreeElement.prototype 490 } 491