1 /* 2 * Copyright (C) 2012 Google Inc. 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 Google Inc. 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 * @implements {WebInspector.CSSSourceMapping} 34 * @param {!WebInspector.CSSStyleModel} cssModel 35 * @param {!WebInspector.Workspace} workspace 36 */ 37 WebInspector.StylesSourceMapping = function(cssModel, workspace) 38 { 39 this._cssModel = cssModel; 40 this._workspace = workspace; 41 this._workspace.addEventListener(WebInspector.Workspace.Events.ProjectRemoved, this._projectRemoved, this); 42 this._workspace.addEventListener(WebInspector.Workspace.Events.UISourceCodeAdded, this._uiSourceCodeAddedToWorkspace, this); 43 this._workspace.addEventListener(WebInspector.Workspace.Events.UISourceCodeRemoved, this._uiSourceCodeRemoved, this); 44 45 cssModel.target().resourceTreeModel.addEventListener(WebInspector.ResourceTreeModel.EventTypes.MainFrameNavigated, this._mainFrameNavigated, this); 46 47 this._cssModel.addEventListener(WebInspector.CSSStyleModel.Events.StyleSheetChanged, this._styleSheetChanged, this); 48 this._initialize(); 49 } 50 51 WebInspector.StylesSourceMapping.MinorChangeUpdateTimeoutMs = 1000; 52 53 WebInspector.StylesSourceMapping.prototype = { 54 /** 55 * @param {!WebInspector.CSSLocation} rawLocation 56 * @return {?WebInspector.UILocation} 57 */ 58 rawLocationToUILocation: function(rawLocation) 59 { 60 var location = /** @type WebInspector.CSSLocation */ (rawLocation); 61 var uiSourceCode = this._workspace.uiSourceCodeForURL(location.url); 62 if (!uiSourceCode) 63 return null; 64 return uiSourceCode.uiLocation(location.lineNumber, location.columnNumber); 65 }, 66 67 /** 68 * @param {!WebInspector.UISourceCode} uiSourceCode 69 * @param {number} lineNumber 70 * @param {number} columnNumber 71 * @return {!WebInspector.CSSLocation} 72 */ 73 uiLocationToRawLocation: function(uiSourceCode, lineNumber, columnNumber) 74 { 75 return new WebInspector.CSSLocation(this._cssModel.target(), null, uiSourceCode.url || "", lineNumber, columnNumber); 76 }, 77 78 /** 79 * @return {boolean} 80 */ 81 isIdentity: function() 82 { 83 return true; 84 }, 85 86 /** 87 * @param {!WebInspector.UISourceCode} uiSourceCode 88 * @param {number} lineNumber 89 * @return {boolean} 90 */ 91 uiLineHasMapping: function(uiSourceCode, lineNumber) 92 { 93 return true; 94 }, 95 96 /** 97 * @return {!WebInspector.Target} 98 */ 99 target: function() 100 { 101 return this._cssModel.target(); 102 }, 103 104 /** 105 * @param {!WebInspector.CSSStyleSheetHeader} header 106 */ 107 addHeader: function(header) 108 { 109 var url = header.resourceURL(); 110 if (!url) 111 return; 112 113 WebInspector.cssWorkspaceBinding.pushSourceMapping(header, this); 114 var map = this._urlToHeadersByFrameId[url]; 115 if (!map) { 116 map = /** @type {!StringMap.<!StringMap.<!WebInspector.CSSStyleSheetHeader>>} */ (new StringMap()); 117 this._urlToHeadersByFrameId[url] = map; 118 } 119 var headersById = map.get(header.frameId); 120 if (!headersById) { 121 headersById = /** @type {!StringMap.<!WebInspector.CSSStyleSheetHeader>} */ (new StringMap()); 122 map.set(header.frameId, headersById); 123 } 124 headersById.set(header.id, header); 125 var uiSourceCode = this._workspace.uiSourceCodeForURL(url); 126 if (uiSourceCode) 127 this._bindUISourceCode(uiSourceCode, header); 128 }, 129 130 /** 131 * @param {!WebInspector.CSSStyleSheetHeader} header 132 */ 133 removeHeader: function(header) 134 { 135 var url = header.resourceURL(); 136 if (!url) 137 return; 138 139 var map = this._urlToHeadersByFrameId[url]; 140 console.assert(map); 141 var headersById = map.get(header.frameId); 142 console.assert(headersById); 143 headersById.remove(header.id); 144 145 if (!headersById.size) { 146 map.remove(header.frameId); 147 if (!map.size) { 148 delete this._urlToHeadersByFrameId[url]; 149 var uiSourceCode = this._workspace.uiSourceCodeForURL(url); 150 if (uiSourceCode) 151 this._unbindUISourceCode(uiSourceCode); 152 } 153 } 154 }, 155 156 /** 157 * @param {!WebInspector.UISourceCode} uiSourceCode 158 */ 159 _unbindUISourceCode: function(uiSourceCode) 160 { 161 var styleFile = this._styleFiles.get(uiSourceCode); 162 if (!styleFile) 163 return; 164 styleFile.dispose(); 165 this._styleFiles.remove(uiSourceCode); 166 }, 167 168 /** 169 * @param {!WebInspector.Event} event 170 */ 171 _uiSourceCodeAddedToWorkspace: function(event) 172 { 173 var uiSourceCode = /** @type {!WebInspector.UISourceCode} */ (event.data); 174 var url = uiSourceCode.url; 175 if (!url || !this._urlToHeadersByFrameId[url]) 176 return; 177 this._bindUISourceCode(uiSourceCode, this._urlToHeadersByFrameId[url].values()[0].values()[0]); 178 }, 179 180 /** 181 * @param {!WebInspector.UISourceCode} uiSourceCode 182 * @param {!WebInspector.CSSStyleSheetHeader} header 183 */ 184 _bindUISourceCode: function(uiSourceCode, header) 185 { 186 if (this._styleFiles.get(uiSourceCode) || header.isInline) 187 return; 188 var url = uiSourceCode.url; 189 this._styleFiles.set(uiSourceCode, new WebInspector.StyleFile(uiSourceCode, this)); 190 WebInspector.cssWorkspaceBinding.updateLocations(header); 191 }, 192 193 /** 194 * @param {!WebInspector.Event} event 195 */ 196 _projectRemoved: function(event) 197 { 198 var project = /** @type {!WebInspector.Project} */ (event.data); 199 var uiSourceCodes = project.uiSourceCodes(); 200 for (var i = 0; i < uiSourceCodes.length; ++i) 201 this._unbindUISourceCode(uiSourceCodes[i]); 202 }, 203 204 /** 205 * @param {!WebInspector.Event} event 206 */ 207 _uiSourceCodeRemoved: function(event) 208 { 209 var uiSourceCode = /** @type {!WebInspector.UISourceCode} */ (event.data); 210 this._unbindUISourceCode(uiSourceCode); 211 }, 212 213 _initialize: function() 214 { 215 /** @type {!Object.<string, !StringMap.<!StringMap.<!WebInspector.CSSStyleSheetHeader>>>} */ 216 this._urlToHeadersByFrameId = {}; 217 /** @type {!Map.<!WebInspector.UISourceCode, !WebInspector.StyleFile>} */ 218 this._styleFiles = new Map(); 219 }, 220 221 /** 222 * @param {!WebInspector.Event} event 223 */ 224 _mainFrameNavigated: function(event) 225 { 226 for (var url in this._urlToHeadersByFrameId) { 227 var uiSourceCode = this._workspace.uiSourceCodeForURL(url); 228 if (!uiSourceCode) 229 continue; 230 this._unbindUISourceCode(uiSourceCode); 231 } 232 this._initialize(); 233 }, 234 235 /** 236 * @param {!WebInspector.UISourceCode} uiSourceCode 237 * @param {string} content 238 * @param {boolean} majorChange 239 * @param {function(?string)} userCallback 240 */ 241 _setStyleContent: function(uiSourceCode, content, majorChange, userCallback) 242 { 243 var styleSheetIds = this._cssModel.styleSheetIdsForURL(uiSourceCode.url); 244 if (!styleSheetIds.length) { 245 userCallback("No stylesheet found: " + uiSourceCode.url); 246 return; 247 } 248 249 this._isSettingContent = true; 250 251 /** 252 * @param {?Protocol.Error} error 253 * @this {WebInspector.StylesSourceMapping} 254 */ 255 function callback(error) 256 { 257 userCallback(error); 258 delete this._isSettingContent; 259 } 260 this._cssModel.setStyleSheetText(styleSheetIds[0], content, majorChange, callback.bind(this)); 261 }, 262 263 /** 264 * @param {!WebInspector.Event} event 265 */ 266 _styleSheetChanged: function(event) 267 { 268 if (this._isSettingContent) 269 return; 270 271 if (event.data.majorChange) { 272 this._updateStyleSheetText(event.data.styleSheetId); 273 return; 274 } 275 276 this._updateStyleSheetTextSoon(event.data.styleSheetId); 277 }, 278 279 /** 280 * @param {!CSSAgent.StyleSheetId} styleSheetId 281 */ 282 _updateStyleSheetTextSoon: function(styleSheetId) 283 { 284 if (this._updateStyleSheetTextTimer) 285 clearTimeout(this._updateStyleSheetTextTimer); 286 287 this._updateStyleSheetTextTimer = setTimeout(this._updateStyleSheetText.bind(this, styleSheetId), WebInspector.StylesSourceMapping.MinorChangeUpdateTimeoutMs); 288 }, 289 290 /** 291 * @param {!CSSAgent.StyleSheetId} styleSheetId 292 */ 293 _updateStyleSheetText: function(styleSheetId) 294 { 295 if (this._updateStyleSheetTextTimer) { 296 clearTimeout(this._updateStyleSheetTextTimer); 297 delete this._updateStyleSheetTextTimer; 298 } 299 300 var header = this._cssModel.styleSheetHeaderForId(styleSheetId); 301 if (!header) 302 return; 303 var styleSheetURL = header.resourceURL(); 304 if (!styleSheetURL) 305 return; 306 var uiSourceCode = this._workspace.uiSourceCodeForURL(styleSheetURL) 307 if (!uiSourceCode) 308 return; 309 header.requestContent(callback.bind(this, uiSourceCode)); 310 311 /** 312 * @param {!WebInspector.UISourceCode} uiSourceCode 313 * @param {?string} content 314 * @this {WebInspector.StylesSourceMapping} 315 */ 316 function callback(uiSourceCode, content) 317 { 318 var styleFile = this._styleFiles.get(uiSourceCode); 319 if (styleFile) 320 styleFile.addRevision(content || ""); 321 } 322 } 323 } 324 325 /** 326 * @constructor 327 * @param {!WebInspector.UISourceCode} uiSourceCode 328 * @param {!WebInspector.StylesSourceMapping} mapping 329 */ 330 WebInspector.StyleFile = function(uiSourceCode, mapping) 331 { 332 this._uiSourceCode = uiSourceCode; 333 this._mapping = mapping; 334 this._uiSourceCode.addEventListener(WebInspector.UISourceCode.Events.WorkingCopyChanged, this._workingCopyChanged, this); 335 this._uiSourceCode.addEventListener(WebInspector.UISourceCode.Events.WorkingCopyCommitted, this._workingCopyCommitted, this); 336 this._commitThrottler = new WebInspector.Throttler(WebInspector.StyleFile.updateTimeout); 337 } 338 339 WebInspector.StyleFile.updateTimeout = 200; 340 341 WebInspector.StyleFile.prototype = { 342 /** 343 * @param {!WebInspector.Event} event 344 */ 345 _workingCopyCommitted: function(event) 346 { 347 if (this._isAddingRevision) 348 return; 349 350 this._isMajorChangePending = true; 351 this._commitThrottler.schedule(this._commitIncrementalEdit.bind(this), true); 352 }, 353 354 /** 355 * @param {!WebInspector.Event} event 356 */ 357 _workingCopyChanged: function(event) 358 { 359 if (this._isAddingRevision) 360 return; 361 362 this._commitThrottler.schedule(this._commitIncrementalEdit.bind(this), false); 363 }, 364 365 /** 366 * @param {!WebInspector.Throttler.FinishCallback} finishCallback 367 */ 368 _commitIncrementalEdit: function(finishCallback) 369 { 370 this._mapping._setStyleContent(this._uiSourceCode, this._uiSourceCode.workingCopy(), this._isMajorChangePending, this._styleContentSet.bind(this, finishCallback)); 371 this._isMajorChangePending = false; 372 }, 373 374 /** 375 * @param {!WebInspector.Throttler.FinishCallback} finishCallback 376 * @param {?string} error 377 */ 378 _styleContentSet: function(finishCallback, error) 379 { 380 if (error) 381 WebInspector.console.error(error); 382 finishCallback(); 383 }, 384 385 /** 386 * @param {string} content 387 */ 388 addRevision: function(content) 389 { 390 this._isAddingRevision = true; 391 this._uiSourceCode.addRevision(content); 392 delete this._isAddingRevision; 393 }, 394 395 dispose: function() 396 { 397 this._uiSourceCode.removeEventListener(WebInspector.UISourceCode.Events.WorkingCopyCommitted, this._workingCopyCommitted, this); 398 this._uiSourceCode.removeEventListener(WebInspector.UISourceCode.Events.WorkingCopyChanged, this._workingCopyChanged, this); 399 } 400 } 401