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 WebInspector.context.addFlavorChangeListener(WebInspector.ExecutionContext ,this.refreshExpressions, this); 56 } 57 58 WebInspector.WatchExpressionsSidebarPane.prototype = { 59 wasShown: function() 60 { 61 this._refreshExpressionsIfNeeded(); 62 }, 63 64 refreshExpressions: function() 65 { 66 this._requiresUpdate = true; 67 this._refreshExpressionsIfNeeded(); 68 }, 69 70 addExpression: function(expression) 71 { 72 this.section.addExpression(expression); 73 this.expand(); 74 }, 75 76 _refreshExpressionsIfNeeded: function() 77 { 78 if (this._requiresUpdate && this.isShowing()) { 79 this.section.update(); 80 delete this._requiresUpdate; 81 } else 82 this._requiresUpdate = true; 83 }, 84 85 _addButtonClicked: function(event) 86 { 87 event.consume(); 88 this.expand(); 89 this.section.addNewExpressionAndEdit(); 90 }, 91 92 _refreshButtonClicked: function(event) 93 { 94 event.consume(); 95 this.refreshExpressions(); 96 }, 97 98 __proto__: WebInspector.SidebarPane.prototype 99 } 100 101 /** 102 * @constructor 103 * @extends {WebInspector.PropertiesSection} 104 */ 105 WebInspector.WatchExpressionsSection = function() 106 { 107 this._watchObjectGroupId = "watch-group"; 108 109 WebInspector.PropertiesSection.call(this, ""); 110 this.treeElementConstructor = WebInspector.WatchedPropertyTreeElement; 111 this.skipProto = false; 112 this._expandedExpressions = {}; 113 this._expandedProperties = {}; 114 115 this.emptyElement = document.createElement("div"); 116 this.emptyElement.className = "info"; 117 this.emptyElement.textContent = WebInspector.UIString("No Watch Expressions"); 118 119 this.watchExpressions = WebInspector.settings.watchExpressions.get(); 120 121 this.headerElement.className = "hidden"; 122 this.editable = true; 123 this.expanded = true; 124 this.propertiesElement.classList.add("watch-expressions"); 125 126 this.element.addEventListener("mousemove", this._mouseMove.bind(this), true); 127 this.element.addEventListener("mouseout", this._mouseOut.bind(this), true); 128 this.element.addEventListener("dblclick", this._sectionDoubleClick.bind(this), false); 129 this.emptyElement.addEventListener("contextmenu", this._emptyElementContextMenu.bind(this), false); 130 } 131 132 WebInspector.WatchExpressionsSection.NewWatchExpression = "\xA0"; 133 134 WebInspector.WatchExpressionsSection.prototype = { 135 /** 136 * @param {!Event=} e 137 */ 138 update: function(e) 139 { 140 if (e) 141 e.consume(); 142 143 /*** 144 * @param {string} expression 145 * @param {number} watchIndex 146 * @param {?WebInspector.RemoteObject} result 147 * @param {boolean} wasThrown 148 * @this {WebInspector.WatchExpressionsSection} 149 */ 150 function appendResult(expression, watchIndex, result, wasThrown) 151 { 152 if (!result) 153 return; 154 155 var property = new WebInspector.RemoteObjectProperty(expression, result); 156 property.watchIndex = watchIndex; 157 property.wasThrown = wasThrown; 158 159 // To clarify what's going on here: 160 // In the outer function, we calculate the number of properties 161 // that we're going to be updating, and set that in the 162 // propertyCount variable. 163 // In this function, we test to see when we are processing the 164 // last property, and then call the superclass's updateProperties() 165 // method to get all the properties refreshed at once. 166 properties.push(property); 167 168 if (properties.length == propertyCount) { 169 this.updateProperties(properties); 170 171 // check to see if we just added a new watch expression, 172 // which will always be the last property 173 if (this._newExpressionAdded) { 174 delete this._newExpressionAdded; 175 176 var treeElement = this.findAddedTreeElement(); 177 if (treeElement) 178 treeElement.startEditing(); 179 } 180 181 // Force displaying delete button for hovered element. 182 if (this._lastMouseMovePageY) 183 this._updateHoveredElement(this._lastMouseMovePageY); 184 } 185 } 186 187 // TODO: pass exact injected script id. 188 WebInspector.targetManager.targets().forEach(function(target) {target.runtimeAgent().releaseObjectGroup(this._watchObjectGroupId)}, this); 189 var properties = []; 190 191 // Count the properties, so we known when to call this.updateProperties() 192 // in appendResult() 193 var propertyCount = 0; 194 for (var i = 0; i < this.watchExpressions.length; ++i) { 195 if (!this.watchExpressions[i]) 196 continue; 197 ++propertyCount; 198 } 199 200 // Now process all the expressions, since we have the actual count, 201 // which is checked in the appendResult inner function. 202 var currentExecutionContext = WebInspector.context.flavor(WebInspector.ExecutionContext); 203 if (currentExecutionContext) { 204 for (var i = 0; i < this.watchExpressions.length; ++i) { 205 var expression = this.watchExpressions[i]; 206 if (!expression) 207 continue; 208 209 currentExecutionContext.evaluate(expression, this._watchObjectGroupId, false, true, false, false, appendResult.bind(this, expression, i)); 210 } 211 } 212 213 if (!propertyCount) { 214 if (!this.emptyElement.parentNode) 215 this.element.appendChild(this.emptyElement); 216 } else { 217 if (this.emptyElement.parentNode) 218 this.element.removeChild(this.emptyElement); 219 } 220 221 // note this is setting the expansion of the tree, not the section; 222 // with no expressions, and expanded tree, we get some extra vertical 223 // white space 224 this.expanded = (propertyCount != 0); 225 }, 226 227 228 /** 229 * @param {!Array.<!WebInspector.RemoteObjectProperty>} properties 230 */ 231 updateProperties: function(properties) 232 { 233 this.propertiesTreeOutline.removeChildren(); 234 WebInspector.ObjectPropertyTreeElement.populateWithProperties(this.propertiesTreeOutline, properties, [], 235 WebInspector.WatchExpressionTreeElement, WebInspector.WatchExpressionsSection.CompareProperties, false, null); 236 237 this.propertiesForTest = properties; 238 }, 239 240 addExpression: function(expression) 241 { 242 this.watchExpressions.push(expression); 243 this.saveExpressions(); 244 this.update(); 245 }, 246 247 addNewExpressionAndEdit: function() 248 { 249 this._newExpressionAdded = true; 250 this.watchExpressions.push(WebInspector.WatchExpressionsSection.NewWatchExpression); 251 this.update(); 252 }, 253 254 _sectionDoubleClick: function(event) 255 { 256 if (event.target !== this.element && event.target !== this.propertiesElement && event.target !== this.emptyElement) 257 return; 258 event.consume(); 259 this.addNewExpressionAndEdit(); 260 }, 261 262 updateExpression: function(element, value) 263 { 264 if (value === null) { 265 var index = element.property.watchIndex; 266 this.watchExpressions.splice(index, 1); 267 } 268 else 269 this.watchExpressions[element.property.watchIndex] = value; 270 this.saveExpressions(); 271 this.update(); 272 }, 273 274 _deleteAllExpressions: function() 275 { 276 this.watchExpressions = []; 277 this.saveExpressions(); 278 this.update(); 279 }, 280 281 /** 282 * @return {?TreeElement} 283 */ 284 findAddedTreeElement: function() 285 { 286 var children = this.propertiesTreeOutline.children; 287 for (var i = 0; i < children.length; ++i) { 288 if (children[i].property.name === WebInspector.WatchExpressionsSection.NewWatchExpression) 289 return children[i]; 290 } 291 return null; 292 }, 293 294 /** 295 * @return {number} 296 */ 297 saveExpressions: function() 298 { 299 var toSave = []; 300 for (var i = 0; i < this.watchExpressions.length; i++) 301 if (this.watchExpressions[i]) 302 toSave.push(this.watchExpressions[i]); 303 304 WebInspector.settings.watchExpressions.set(toSave); 305 return toSave.length; 306 }, 307 308 _mouseMove: function(e) 309 { 310 if (this.propertiesElement.firstChild) 311 this._updateHoveredElement(e.pageY); 312 }, 313 314 _mouseOut: function() 315 { 316 if (this._hoveredElement) { 317 this._hoveredElement.classList.remove("hovered"); 318 delete this._hoveredElement; 319 } 320 delete this._lastMouseMovePageY; 321 }, 322 323 _updateHoveredElement: function(pageY) 324 { 325 var candidateElement = this.propertiesElement.firstChild; 326 while (true) { 327 var next = candidateElement.nextSibling; 328 while (next && !next.clientHeight) 329 next = next.nextSibling; 330 if (!next || next.totalOffsetTop() > pageY) 331 break; 332 candidateElement = next; 333 } 334 335 if (this._hoveredElement !== candidateElement) { 336 if (this._hoveredElement) 337 this._hoveredElement.classList.remove("hovered"); 338 if (candidateElement) 339 candidateElement.classList.add("hovered"); 340 this._hoveredElement = candidateElement; 341 } 342 343 this._lastMouseMovePageY = pageY; 344 }, 345 346 _emptyElementContextMenu: function(event) 347 { 348 var contextMenu = new WebInspector.ContextMenu(event); 349 contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Add watch expression" : "Add Watch Expression"), this.addNewExpressionAndEdit.bind(this)); 350 contextMenu.show(); 351 }, 352 353 __proto__: WebInspector.PropertiesSection.prototype 354 } 355 356 /** 357 * @param {!WebInspector.RemoteObjectProperty} propertyA 358 * @param {!WebInspector.RemoteObjectProperty} propertyB 359 * @return {number} 360 */ 361 WebInspector.WatchExpressionsSection.CompareProperties = function(propertyA, propertyB) 362 { 363 if (propertyA.watchIndex == propertyB.watchIndex) 364 return 0; 365 else if (propertyA.watchIndex < propertyB.watchIndex) 366 return -1; 367 else 368 return 1; 369 } 370 371 /** 372 * @constructor 373 * @extends {WebInspector.ObjectPropertyTreeElement} 374 * @param {!WebInspector.RemoteObjectProperty} property 375 */ 376 WebInspector.WatchExpressionTreeElement = function(property) 377 { 378 WebInspector.ObjectPropertyTreeElement.call(this, property); 379 } 380 381 WebInspector.WatchExpressionTreeElement.prototype = { 382 onexpand: function() 383 { 384 WebInspector.ObjectPropertyTreeElement.prototype.onexpand.call(this); 385 this.treeOutline.section._expandedExpressions[this._expression()] = true; 386 }, 387 388 oncollapse: function() 389 { 390 WebInspector.ObjectPropertyTreeElement.prototype.oncollapse.call(this); 391 delete this.treeOutline.section._expandedExpressions[this._expression()]; 392 }, 393 394 onattach: function() 395 { 396 WebInspector.ObjectPropertyTreeElement.prototype.onattach.call(this); 397 if (this.treeOutline.section._expandedExpressions[this._expression()]) 398 this.expanded = true; 399 }, 400 401 _expression: function() 402 { 403 return this.property.name; 404 }, 405 406 update: function() 407 { 408 WebInspector.ObjectPropertyTreeElement.prototype.update.call(this); 409 410 if (this.property.wasThrown) { 411 this.valueElement.textContent = WebInspector.UIString("<not available>"); 412 this.listItemElement.classList.add("dimmed"); 413 } else 414 this.listItemElement.classList.remove("dimmed"); 415 416 var deleteButton = document.createElement("input"); 417 deleteButton.type = "button"; 418 deleteButton.title = WebInspector.UIString("Delete watch expression."); 419 deleteButton.classList.add("enabled-button"); 420 deleteButton.classList.add("delete-button"); 421 deleteButton.addEventListener("click", this._deleteButtonClicked.bind(this), false); 422 this.listItemElement.addEventListener("contextmenu", this._contextMenu.bind(this), false); 423 this.listItemElement.insertBefore(deleteButton, this.listItemElement.firstChild); 424 }, 425 426 /** 427 * @param {!WebInspector.ContextMenu} contextMenu 428 * @override 429 */ 430 populateContextMenu: function(contextMenu) 431 { 432 if (!this.isEditing()) { 433 contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Add watch expression" : "Add Watch Expression"), this.treeOutline.section.addNewExpressionAndEdit.bind(this.treeOutline.section)); 434 contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Delete watch expression" : "Delete Watch Expression"), this._deleteButtonClicked.bind(this)); 435 } 436 if (this.treeOutline.section.watchExpressions.length > 1) 437 contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Delete all watch expressions" : "Delete All Watch Expressions"), this._deleteAllButtonClicked.bind(this)); 438 }, 439 440 _contextMenu: function(event) 441 { 442 var contextMenu = new WebInspector.ContextMenu(event); 443 this.populateContextMenu(contextMenu); 444 contextMenu.show(); 445 }, 446 447 _deleteAllButtonClicked: function() 448 { 449 this.treeOutline.section._deleteAllExpressions(); 450 }, 451 452 _deleteButtonClicked: function() 453 { 454 this.treeOutline.section.updateExpression(this, null); 455 }, 456 457 /** 458 * @return {boolean} 459 */ 460 renderPromptAsBlock: function() 461 { 462 return true; 463 }, 464 465 /** 466 * @override 467 * @return {{element: !Element, value: (string|undefined)}} 468 */ 469 elementAndValueToEdit: function() 470 { 471 return { element: this.nameElement, value: this.property.name.trim() }; 472 }, 473 474 /** 475 * @override 476 */ 477 editingCancelled: function(element, context) 478 { 479 if (!context.elementToEdit.textContent) 480 this.treeOutline.section.updateExpression(this, null); 481 482 WebInspector.ObjectPropertyTreeElement.prototype.editingCancelled.call(this, element, context); 483 }, 484 485 /** 486 * @override 487 * @param {string} expression 488 */ 489 applyExpression: function(expression) 490 { 491 expression = expression.trim(); 492 this.property.name = expression || null; 493 this.treeOutline.section.updateExpression(this, expression); 494 }, 495 496 __proto__: WebInspector.ObjectPropertyTreeElement.prototype 497 } 498 499 500 /** 501 * @constructor 502 * @extends {WebInspector.ObjectPropertyTreeElement} 503 * @param {!WebInspector.RemoteObjectProperty} property 504 */ 505 WebInspector.WatchedPropertyTreeElement = function(property) 506 { 507 WebInspector.ObjectPropertyTreeElement.call(this, property); 508 } 509 510 WebInspector.WatchedPropertyTreeElement.prototype = { 511 onattach: function() 512 { 513 WebInspector.ObjectPropertyTreeElement.prototype.onattach.call(this); 514 if (this.hasChildren && this.propertyPath() in this.treeOutline.section._expandedProperties) 515 this.expand(); 516 }, 517 518 onexpand: function() 519 { 520 WebInspector.ObjectPropertyTreeElement.prototype.onexpand.call(this); 521 this.treeOutline.section._expandedProperties[this.propertyPath()] = true; 522 }, 523 524 oncollapse: function() 525 { 526 WebInspector.ObjectPropertyTreeElement.prototype.oncollapse.call(this); 527 delete this.treeOutline.section._expandedProperties[this.propertyPath()]; 528 }, 529 530 __proto__: WebInspector.ObjectPropertyTreeElement.prototype 531 } 532