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