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 */ 35WebInspector.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 58WebInspector.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.ObjectPropertiesSection} 104 */ 105WebInspector.WatchExpressionsSection = function() 106{ 107 this._watchObjectGroupId = "watch-group"; 108 109 WebInspector.ObjectPropertiesSection.call(this, WebInspector.runtimeModel.createRemoteObjectFromPrimitiveValue("")); 110 111 this.treeElementConstructor = WebInspector.WatchedPropertyTreeElement; 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 132WebInspector.WatchExpressionsSection.NewWatchExpression = "\xA0"; 133 134WebInspector.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, [], WebInspector.WatchExpressionTreeElement, WebInspector.WatchExpressionsSection.CompareProperties); 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 addExpression: function(expression) 228 { 229 this.watchExpressions.push(expression); 230 this.saveExpressions(); 231 this.update(); 232 }, 233 234 addNewExpressionAndEdit: function() 235 { 236 this._newExpressionAdded = true; 237 this.watchExpressions.push(WebInspector.WatchExpressionsSection.NewWatchExpression); 238 this.update(); 239 }, 240 241 _sectionDoubleClick: function(event) 242 { 243 if (event.target !== this.element && event.target !== this.propertiesElement && event.target !== this.emptyElement) 244 return; 245 event.consume(); 246 this.addNewExpressionAndEdit(); 247 }, 248 249 updateExpression: function(element, value) 250 { 251 if (value === null) { 252 var index = element.property.watchIndex; 253 this.watchExpressions.splice(index, 1); 254 } 255 else 256 this.watchExpressions[element.property.watchIndex] = value; 257 this.saveExpressions(); 258 this.update(); 259 }, 260 261 _deleteAllExpressions: function() 262 { 263 this.watchExpressions = []; 264 this.saveExpressions(); 265 this.update(); 266 }, 267 268 /** 269 * @return {?TreeElement} 270 */ 271 findAddedTreeElement: function() 272 { 273 var children = this.propertiesTreeOutline.children; 274 for (var i = 0; i < children.length; ++i) { 275 if (children[i].property.name === WebInspector.WatchExpressionsSection.NewWatchExpression) 276 return children[i]; 277 } 278 return null; 279 }, 280 281 /** 282 * @return {number} 283 */ 284 saveExpressions: function() 285 { 286 var toSave = []; 287 for (var i = 0; i < this.watchExpressions.length; i++) 288 if (this.watchExpressions[i]) 289 toSave.push(this.watchExpressions[i]); 290 291 WebInspector.settings.watchExpressions.set(toSave); 292 return toSave.length; 293 }, 294 295 _mouseMove: function(e) 296 { 297 if (this.propertiesElement.firstChild) 298 this._updateHoveredElement(e.pageY); 299 }, 300 301 _mouseOut: function() 302 { 303 if (this._hoveredElement) { 304 this._hoveredElement.classList.remove("hovered"); 305 delete this._hoveredElement; 306 } 307 delete this._lastMouseMovePageY; 308 }, 309 310 _updateHoveredElement: function(pageY) 311 { 312 var candidateElement = this.propertiesElement.firstChild; 313 while (true) { 314 var next = candidateElement.nextSibling; 315 while (next && !next.clientHeight) 316 next = next.nextSibling; 317 if (!next || next.totalOffsetTop() > pageY) 318 break; 319 candidateElement = next; 320 } 321 322 if (this._hoveredElement !== candidateElement) { 323 if (this._hoveredElement) 324 this._hoveredElement.classList.remove("hovered"); 325 if (candidateElement) 326 candidateElement.classList.add("hovered"); 327 this._hoveredElement = candidateElement; 328 } 329 330 this._lastMouseMovePageY = pageY; 331 }, 332 333 _emptyElementContextMenu: function(event) 334 { 335 var contextMenu = new WebInspector.ContextMenu(event); 336 contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Add watch expression" : "Add Watch Expression"), this.addNewExpressionAndEdit.bind(this)); 337 contextMenu.show(); 338 }, 339 340 __proto__: WebInspector.ObjectPropertiesSection.prototype 341} 342 343/** 344 * @param {!WebInspector.RemoteObjectProperty} propertyA 345 * @param {!WebInspector.RemoteObjectProperty} propertyB 346 * @return {number} 347 */ 348WebInspector.WatchExpressionsSection.CompareProperties = function(propertyA, propertyB) 349{ 350 if (propertyA.watchIndex == propertyB.watchIndex) 351 return 0; 352 else if (propertyA.watchIndex < propertyB.watchIndex) 353 return -1; 354 else 355 return 1; 356} 357 358/** 359 * @constructor 360 * @extends {WebInspector.ObjectPropertyTreeElement} 361 * @param {!WebInspector.RemoteObjectProperty} property 362 */ 363WebInspector.WatchExpressionTreeElement = function(property) 364{ 365 WebInspector.ObjectPropertyTreeElement.call(this, property); 366} 367 368WebInspector.WatchExpressionTreeElement.prototype = { 369 onexpand: function() 370 { 371 WebInspector.ObjectPropertyTreeElement.prototype.onexpand.call(this); 372 this.treeOutline.section._expandedExpressions[this._expression()] = true; 373 }, 374 375 oncollapse: function() 376 { 377 WebInspector.ObjectPropertyTreeElement.prototype.oncollapse.call(this); 378 delete this.treeOutline.section._expandedExpressions[this._expression()]; 379 }, 380 381 onattach: function() 382 { 383 WebInspector.ObjectPropertyTreeElement.prototype.onattach.call(this); 384 if (this.treeOutline.section._expandedExpressions[this._expression()]) 385 this.expanded = true; 386 }, 387 388 _expression: function() 389 { 390 return this.property.name; 391 }, 392 393 update: function() 394 { 395 WebInspector.ObjectPropertyTreeElement.prototype.update.call(this); 396 397 if (this.property.wasThrown) { 398 this.valueElement.textContent = WebInspector.UIString("<not available>"); 399 this.listItemElement.classList.add("dimmed"); 400 } else 401 this.listItemElement.classList.remove("dimmed"); 402 403 var deleteButton = document.createElement("input"); 404 deleteButton.type = "button"; 405 deleteButton.title = WebInspector.UIString("Delete watch expression."); 406 deleteButton.classList.add("enabled-button"); 407 deleteButton.classList.add("delete-button"); 408 deleteButton.addEventListener("click", this._deleteButtonClicked.bind(this), false); 409 this.listItemElement.addEventListener("contextmenu", this._contextMenu.bind(this), false); 410 this.listItemElement.insertBefore(deleteButton, this.listItemElement.firstChild); 411 }, 412 413 /** 414 * @param {!WebInspector.ContextMenu} contextMenu 415 * @override 416 */ 417 populateContextMenu: function(contextMenu) 418 { 419 if (!this.isEditing()) { 420 contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Add watch expression" : "Add Watch Expression"), this.treeOutline.section.addNewExpressionAndEdit.bind(this.treeOutline.section)); 421 contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Delete watch expression" : "Delete Watch Expression"), this._deleteButtonClicked.bind(this)); 422 } 423 if (this.treeOutline.section.watchExpressions.length > 1) 424 contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Delete all watch expressions" : "Delete All Watch Expressions"), this._deleteAllButtonClicked.bind(this)); 425 }, 426 427 _contextMenu: function(event) 428 { 429 var contextMenu = new WebInspector.ContextMenu(event); 430 this.populateContextMenu(contextMenu); 431 contextMenu.show(); 432 }, 433 434 _deleteAllButtonClicked: function() 435 { 436 this.treeOutline.section._deleteAllExpressions(); 437 }, 438 439 _deleteButtonClicked: function() 440 { 441 this.treeOutline.section.updateExpression(this, null); 442 }, 443 444 /** 445 * @return {boolean} 446 */ 447 renderPromptAsBlock: function() 448 { 449 return true; 450 }, 451 452 /** 453 * @override 454 * @return {{element: !Element, value: (string|undefined)}} 455 */ 456 elementAndValueToEdit: function() 457 { 458 return { element: this.nameElement, value: this.property.name.trim() }; 459 }, 460 461 /** 462 * @override 463 */ 464 editingCancelled: function(element, context) 465 { 466 if (!context.elementToEdit.textContent) 467 this.treeOutline.section.updateExpression(this, null); 468 469 WebInspector.ObjectPropertyTreeElement.prototype.editingCancelled.call(this, element, context); 470 }, 471 472 /** 473 * @override 474 * @param {string} expression 475 */ 476 applyExpression: function(expression) 477 { 478 expression = expression.trim(); 479 this.property.name = expression || null; 480 this.treeOutline.section.updateExpression(this, expression); 481 }, 482 483 __proto__: WebInspector.ObjectPropertyTreeElement.prototype 484} 485 486 487/** 488 * @constructor 489 * @extends {WebInspector.ObjectPropertyTreeElement} 490 * @param {!WebInspector.RemoteObjectProperty} property 491 */ 492WebInspector.WatchedPropertyTreeElement = function(property) 493{ 494 WebInspector.ObjectPropertyTreeElement.call(this, property); 495} 496 497WebInspector.WatchedPropertyTreeElement.prototype = { 498 onattach: function() 499 { 500 WebInspector.ObjectPropertyTreeElement.prototype.onattach.call(this); 501 if (this.hasChildren && this.propertyPath() in this.treeOutline.section._expandedProperties) 502 this.expand(); 503 }, 504 505 onexpand: function() 506 { 507 WebInspector.ObjectPropertyTreeElement.prototype.onexpand.call(this); 508 this.treeOutline.section._expandedProperties[this.propertyPath()] = true; 509 }, 510 511 oncollapse: function() 512 { 513 WebInspector.ObjectPropertyTreeElement.prototype.oncollapse.call(this); 514 delete this.treeOutline.section._expandedProperties[this.propertyPath()]; 515 }, 516 517 __proto__: WebInspector.ObjectPropertyTreeElement.prototype 518} 519