Home | History | Annotate | Download | only in front_end
      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.SourceMapping}
     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.ProjectWillReset, this._projectWillReset, 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     WebInspector.resourceTreeModel.addEventListener(WebInspector.ResourceTreeModel.EventTypes.MainFrameCreatedOrNavigated, this._mainFrameCreatedOrNavigated, 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.RawLocation} 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 new WebInspector.UILocation(uiSourceCode, location.lineNumber, location.columnNumber);
     65     },
     66 
     67     /**
     68      * @param {!WebInspector.UISourceCode} uiSourceCode
     69      * @param {number} lineNumber
     70      * @param {number} columnNumber
     71      * @return {!WebInspector.RawLocation}
     72      */
     73     uiLocationToRawLocation: function(uiSourceCode, lineNumber, columnNumber)
     74     {
     75         return new WebInspector.CSSLocation(uiSourceCode.url || "", lineNumber, columnNumber);
     76     },
     77 
     78     /**
     79      * @param {!WebInspector.CSSStyleSheetHeader} header
     80      */
     81     addHeader: function(header)
     82     {
     83         var url = header.resourceURL();
     84         if (!url)
     85             return;
     86 
     87         header.pushSourceMapping(this);
     88         var map = this._urlToHeadersByFrameId[url];
     89         if (!map) {
     90             map = /** @type {!StringMap.<!StringMap.<!WebInspector.CSSStyleSheetHeader>>} */ (new StringMap());
     91             this._urlToHeadersByFrameId[url] = map;
     92         }
     93         var headersById = map.get(header.frameId);
     94         if (!headersById) {
     95             headersById = /** @type {!StringMap.<!WebInspector.CSSStyleSheetHeader>} */ (new StringMap());
     96             map.put(header.frameId, headersById);
     97         }
     98         headersById.put(header.id, header);
     99         var uiSourceCode = this._workspace.uiSourceCodeForURL(url);
    100         if (uiSourceCode)
    101             this._bindUISourceCode(uiSourceCode, header);
    102     },
    103 
    104     /**
    105      * @param {!WebInspector.CSSStyleSheetHeader} header
    106      */
    107     removeHeader: function(header)
    108     {
    109         var url = header.resourceURL();
    110         if (!url)
    111             return;
    112 
    113         var map = this._urlToHeadersByFrameId[url];
    114         console.assert(map);
    115         var headersById = map.get(header.frameId);
    116         console.assert(headersById);
    117         headersById.remove(header.id);
    118 
    119         if (!headersById.size()) {
    120             map.remove(header.frameId);
    121             if (!map.size()) {
    122                 delete this._urlToHeadersByFrameId[url];
    123                 var uiSourceCode = this._workspace.uiSourceCodeForURL(url);
    124                 if (uiSourceCode)
    125                     this._unbindUISourceCode(uiSourceCode);
    126             }
    127         }
    128     },
    129 
    130     /**
    131      * @param {!WebInspector.UISourceCode} uiSourceCode
    132      */
    133     _unbindUISourceCode: function(uiSourceCode)
    134     {
    135         var styleFile = this._styleFiles.get(uiSourceCode);
    136         if (!styleFile)
    137             return;
    138         styleFile.dispose();
    139         this._styleFiles.remove(uiSourceCode);
    140     },
    141 
    142     /**
    143      * @param {!WebInspector.Event} event
    144      */
    145     _uiSourceCodeAddedToWorkspace: function(event)
    146     {
    147         var uiSourceCode = /** @type {!WebInspector.UISourceCode} */ (event.data);
    148         var url = uiSourceCode.url;
    149         if (!url || !this._urlToHeadersByFrameId[url])
    150             return;
    151         this._bindUISourceCode(uiSourceCode, this._urlToHeadersByFrameId[url].values()[0].values()[0]);
    152     },
    153 
    154     /**
    155      * @param {!WebInspector.UISourceCode} uiSourceCode
    156      * @param {!WebInspector.CSSStyleSheetHeader} header
    157      */
    158     _bindUISourceCode: function(uiSourceCode, header)
    159     {
    160         if (this._styleFiles.get(uiSourceCode) || header.isInline)
    161             return;
    162         var url = uiSourceCode.url;
    163         this._styleFiles.put(uiSourceCode, new WebInspector.StyleFile(uiSourceCode, this));
    164         header.updateLocations();
    165     },
    166 
    167     /**
    168      * @param {!WebInspector.Event} event
    169      */
    170     _projectWillReset: function(event)
    171     {
    172         var project = /** @type {!WebInspector.Project} */ (event.data);
    173         var uiSourceCodes = project.uiSourceCodes();
    174         for (var i = 0; i < uiSourceCodes.length; ++i)
    175             this._unbindUISourceCode(uiSourceCodes[i]);
    176     },
    177 
    178     /**
    179      * @param {!WebInspector.Event} event
    180      */
    181     _uiSourceCodeRemoved: function(event)
    182     {
    183         var uiSourceCode = /** @type {!WebInspector.UISourceCode} */ (event.data);
    184         this._unbindUISourceCode(uiSourceCode);
    185     },
    186 
    187     _initialize: function()
    188     {
    189         /** @type {!Object.<string, !StringMap.<!StringMap.<!WebInspector.CSSStyleSheetHeader>>>} */
    190         this._urlToHeadersByFrameId = {};
    191         /** @type {!Map.<!WebInspector.UISourceCode, !WebInspector.StyleFile>} */
    192         this._styleFiles = new Map();
    193     },
    194 
    195     /**
    196      * @param {!WebInspector.Event} event
    197      */
    198     _mainFrameCreatedOrNavigated: function(event)
    199     {
    200         for (var url in this._urlToHeadersByFrameId) {
    201             var uiSourceCode = this._workspace.uiSourceCodeForURL(url);
    202             if (!uiSourceCode)
    203                 continue;
    204             this._unbindUISourceCode(uiSourceCode);
    205         }
    206         this._initialize();
    207     },
    208 
    209     /**
    210      * @param {!WebInspector.UISourceCode} uiSourceCode
    211      * @param {string} content
    212      * @param {boolean} majorChange
    213      * @param {function(?string)} userCallback
    214      */
    215     _setStyleContent: function(uiSourceCode, content, majorChange, userCallback)
    216     {
    217         var styleSheetIds = this._cssModel.styleSheetIdsForURL(uiSourceCode.url);
    218         if (!styleSheetIds.length) {
    219             userCallback("No stylesheet found: " + uiSourceCode.url);
    220             return;
    221         }
    222 
    223         this._isSettingContent = true;
    224 
    225         /**
    226          * @param {?Protocol.Error} error
    227          * @this {WebInspector.StylesSourceMapping}
    228          */
    229         function callback(error)
    230         {
    231             userCallback(error);
    232             delete this._isSettingContent;
    233         }
    234         this._cssModel.setStyleSheetText(styleSheetIds[0], content, majorChange, callback.bind(this));
    235     },
    236 
    237     /**
    238      * @param {!WebInspector.Event} event
    239      */
    240     _styleSheetChanged: function(event)
    241     {
    242         if (this._isSettingContent)
    243             return;
    244 
    245         if (event.data.majorChange) {
    246             this._updateStyleSheetText(event.data.styleSheetId);
    247             return;
    248         }
    249 
    250         this._updateStyleSheetTextSoon(event.data.styleSheetId);
    251     },
    252 
    253     /**
    254      * @param {!CSSAgent.StyleSheetId} styleSheetId
    255      */
    256     _updateStyleSheetTextSoon: function(styleSheetId)
    257     {
    258         if (this._updateStyleSheetTextTimer)
    259             clearTimeout(this._updateStyleSheetTextTimer);
    260 
    261         this._updateStyleSheetTextTimer = setTimeout(this._updateStyleSheetText.bind(this, styleSheetId), WebInspector.StylesSourceMapping.MinorChangeUpdateTimeoutMs);
    262     },
    263 
    264     /**
    265      * @param {!CSSAgent.StyleSheetId} styleSheetId
    266      */
    267     _updateStyleSheetText: function(styleSheetId)
    268     {
    269         if (this._updateStyleSheetTextTimer) {
    270             clearTimeout(this._updateStyleSheetTextTimer);
    271             delete this._updateStyleSheetTextTimer;
    272         }
    273 
    274         CSSAgent.getStyleSheetText(styleSheetId, callback.bind(this));
    275 
    276         /**
    277          * @param {?string} error
    278          * @param {string} content
    279          * @this {WebInspector.StylesSourceMapping}
    280          */
    281         function callback(error, content)
    282         {
    283             if (!error)
    284                 this._innerStyleSheetChanged(styleSheetId, content);
    285         }
    286     },
    287 
    288     /**
    289      * @param {!CSSAgent.StyleSheetId} styleSheetId
    290      * @param {string} content
    291      */
    292     _innerStyleSheetChanged: function(styleSheetId, content)
    293     {
    294         var header = this._cssModel.styleSheetHeaderForId(styleSheetId);
    295         if (!header)
    296             return;
    297         var styleSheetURL = header.resourceURL();
    298         if (!styleSheetURL)
    299             return;
    300 
    301         var uiSourceCode = this._workspace.uiSourceCodeForURL(styleSheetURL)
    302         if (!uiSourceCode)
    303             return;
    304 
    305         var styleFile = this._styleFiles.get(uiSourceCode);
    306         if (styleFile)
    307             styleFile.addRevision(content);
    308     }
    309 }
    310 
    311 /**
    312  * @constructor
    313  * @param {!WebInspector.UISourceCode} uiSourceCode
    314  * @param {!WebInspector.StylesSourceMapping} mapping
    315  */
    316 WebInspector.StyleFile = function(uiSourceCode, mapping)
    317 {
    318     this._uiSourceCode = uiSourceCode;
    319     this._mapping = mapping;
    320     this._uiSourceCode.addEventListener(WebInspector.UISourceCode.Events.WorkingCopyChanged, this._workingCopyChanged, this);
    321     this._uiSourceCode.addEventListener(WebInspector.UISourceCode.Events.WorkingCopyCommitted, this._workingCopyCommitted, this);
    322 }
    323 
    324 WebInspector.StyleFile.updateTimeout = 200;
    325 
    326 WebInspector.StyleFile.sourceURLRegex = /\n[\040\t]*\/\*#[\040\t]sourceURL=[\040\t]*([^\s]*)[\040\t]*\*\/[\040\t]*$/m;
    327 
    328 WebInspector.StyleFile.prototype = {
    329     _workingCopyCommitted: function(event)
    330     {
    331         if (this._isAddingRevision)
    332             return;
    333 
    334         this._commitIncrementalEdit(true);
    335     },
    336 
    337     _workingCopyChanged: function(event)
    338     {
    339         if (this._isAddingRevision)
    340             return;
    341 
    342         // FIXME: Extensions tests override updateTimeout because extensions don't have any control over applying changes to domain specific bindings.
    343         if (WebInspector.StyleFile.updateTimeout >= 0) {
    344             this._incrementalUpdateTimer = setTimeout(this._commitIncrementalEdit.bind(this, false), WebInspector.StyleFile.updateTimeout)
    345         } else
    346             this._commitIncrementalEdit(false);
    347     },
    348 
    349     /**
    350      * @param {boolean} majorChange
    351      */
    352     _commitIncrementalEdit: function(majorChange)
    353     {
    354         this._clearIncrementalUpdateTimer();
    355         this._mapping._setStyleContent(this._uiSourceCode, this._uiSourceCode.workingCopy(), majorChange, this._styleContentSet.bind(this));
    356     },
    357 
    358     /**
    359      * @param {?string} error
    360      */
    361     _styleContentSet: function(error)
    362     {
    363         if (error)
    364             WebInspector.showErrorMessage(error);
    365     },
    366 
    367     _clearIncrementalUpdateTimer: function()
    368     {
    369         if (!this._incrementalUpdateTimer)
    370             return;
    371         clearTimeout(this._incrementalUpdateTimer);
    372         delete this._incrementalUpdateTimer;
    373     },
    374 
    375     /**
    376      * @param {string} content
    377      */
    378     addRevision: function(content)
    379     {
    380         this._isAddingRevision = true;
    381         if (this._uiSourceCode.project().type() === WebInspector.projectTypes.FileSystem)
    382             content = content.replace(WebInspector.StyleFile.sourceURLRegex, "");
    383         this._uiSourceCode.addRevision(content);
    384         delete this._isAddingRevision;
    385     },
    386 
    387     dispose: function()
    388     {
    389         this._uiSourceCode.removeEventListener(WebInspector.UISourceCode.Events.WorkingCopyCommitted, this._workingCopyCommitted, this);
    390         this._uiSourceCode.removeEventListener(WebInspector.UISourceCode.Events.WorkingCopyChanged, this._workingCopyChanged, this);
    391     }
    392 }
    393