1/*
2 * Copyright (C) Research In Motion Limited 2010. All rights reserved.
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Library General Public
6 * License as published by the Free Software Foundation; either
7 * version 2 of the License, or (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12 * Library General Public License for more details.
13 *
14 * You should have received a copy of the GNU Library General Public License
15 * along with this library; see the file COPYING.LIB.  If not, write to
16 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
17 * Boston, MA 02110-1301, USA.
18 */
19
20#ifndef SVGListProperty_h
21#define SVGListProperty_h
22
23#if ENABLE(SVG)
24#include "SVGAnimatedProperty.h"
25#include "SVGException.h"
26#include "SVGPropertyTearOff.h"
27#include "SVGPropertyTraits.h"
28
29namespace WebCore {
30
31enum ListModification {
32    ListModificationUnknown = 0,
33    ListModificationInsert = 1,
34    ListModificationReplace = 2,
35    ListModificationRemove = 3,
36    ListModificationAppend = 4
37};
38
39template<typename PropertyType>
40class SVGAnimatedListPropertyTearOff;
41
42template<typename PropertyType>
43class SVGListProperty : public SVGProperty {
44public:
45    typedef SVGListProperty<PropertyType> Self;
46
47    typedef typename SVGPropertyTraits<PropertyType>::ListItemType ListItemType;
48    typedef SVGPropertyTearOff<ListItemType> ListItemTearOff;
49    typedef PassRefPtr<ListItemTearOff> PassListItemTearOff;
50    typedef SVGAnimatedListPropertyTearOff<PropertyType> AnimatedListPropertyTearOff;
51    typedef typename SVGAnimatedListPropertyTearOff<PropertyType>::ListWrapperCache ListWrapperCache;
52
53    bool canAlterList(ExceptionCode& ec) const
54    {
55        if (m_role == AnimValRole) {
56            ec = NO_MODIFICATION_ALLOWED_ERR;
57            return false;
58        }
59
60        return true;
61    }
62
63    static void detachListWrappersAndResize(ListWrapperCache* wrappers, unsigned newListSize = 0)
64    {
65        // See SVGPropertyTearOff::detachWrapper() for an explanation about what's happening here.
66        ASSERT(wrappers);
67        unsigned size = wrappers->size();
68        for (unsigned i = 0; i < size; ++i) {
69            if (ListItemTearOff* item = wrappers->at(i).get())
70                item->detachWrapper();
71        }
72
73        // Reinitialize the wrapper cache to be equal to the new values size, after the XML DOM changed the list.
74        if (newListSize)
75            wrappers->fill(0, newListSize);
76        else
77            wrappers->clear();
78    }
79
80    void detachListWrappers(unsigned newListSize)
81    {
82        detachListWrappersAndResize(m_wrappers, newListSize);
83    }
84
85    void setValuesAndWrappers(PropertyType* values, ListWrapperCache* wrappers, bool shouldOwnValues)
86    {
87        // This is only used for animVal support, to switch the underlying values & wrappers
88        // to the current animated values, once animation for a list starts.
89        ASSERT(m_values);
90        ASSERT(m_wrappers);
91        ASSERT(m_role == AnimValRole);
92        if (m_ownsValues)
93            delete m_values;
94        m_values = values;
95        m_ownsValues = shouldOwnValues;
96        m_wrappers = wrappers;
97        ASSERT(m_values->size() == m_wrappers->size());
98    }
99
100    // SVGList::clear()
101    void clearValues(ExceptionCode& ec)
102    {
103        if (!canAlterList(ec))
104            return;
105
106        m_values->clear();
107        commitChange();
108    }
109
110    void clearValuesAndWrappers(ExceptionCode& ec)
111    {
112        if (!canAlterList(ec))
113            return;
114
115        detachListWrappers(0);
116        m_values->clear();
117        commitChange();
118    }
119
120    // SVGList::numberOfItems()
121    unsigned numberOfItems() const
122    {
123        return m_values->size();
124    }
125
126    // SVGList::initialize()
127    ListItemType initializeValues(const ListItemType& newItem, ExceptionCode& ec)
128    {
129        if (!canAlterList(ec))
130            return ListItemType();
131
132        // Spec: If the inserted item is already in a list, it is removed from its previous list before it is inserted into this list.
133        processIncomingListItemValue(newItem, 0);
134
135        // Spec: Clears all existing current items from the list and re-initializes the list to hold the single item specified by the parameter.
136        m_values->clear();
137        m_values->append(newItem);
138
139        commitChange();
140        return newItem;
141    }
142
143    PassListItemTearOff initializeValuesAndWrappers(PassListItemTearOff passNewItem, ExceptionCode& ec)
144    {
145        ASSERT(m_wrappers);
146        if (!canAlterList(ec))
147            return 0;
148
149        // Not specified, but FF/Opera do it this way, and it's just sane.
150        if (!passNewItem) {
151            ec = SVGException::SVG_WRONG_TYPE_ERR;
152            return 0;
153        }
154
155        RefPtr<ListItemTearOff> newItem = passNewItem;
156        ASSERT(m_values->size() == m_wrappers->size());
157
158        // Spec: If the inserted item is already in a list, it is removed from its previous list before it is inserted into this list.
159        processIncomingListItemWrapper(newItem, 0);
160
161        // Spec: Clears all existing current items from the list and re-initializes the list to hold the single item specified by the parameter.
162        detachListWrappers(0);
163        m_values->clear();
164
165        m_values->append(newItem->propertyReference());
166        m_wrappers->append(newItem);
167
168        commitChange();
169        return newItem.release();
170    }
171
172    // SVGList::getItem()
173    bool canGetItem(unsigned index, ExceptionCode& ec)
174    {
175        if (index >= m_values->size()) {
176            ec = INDEX_SIZE_ERR;
177            return false;
178        }
179
180        return true;
181    }
182
183    ListItemType getItemValues(unsigned index, ExceptionCode& ec)
184    {
185        if (!canGetItem(index, ec))
186            return ListItemType();
187
188        // Spec: Returns the specified item from the list. The returned item is the item itself and not a copy.
189        return m_values->at(index);
190    }
191
192    PassListItemTearOff getItemValuesAndWrappers(AnimatedListPropertyTearOff* animatedList, unsigned index, ExceptionCode& ec)
193    {
194        ASSERT(m_wrappers);
195        if (!canGetItem(index, ec))
196            return 0;
197
198        // Spec: Returns the specified item from the list. The returned item is the item itself and not a copy.
199        // Any changes made to the item are immediately reflected in the list.
200        ASSERT(m_values->size() == m_wrappers->size());
201        RefPtr<ListItemTearOff> wrapper = m_wrappers->at(index);
202        if (!wrapper) {
203            // Create new wrapper, which is allowed to directly modify the item in the list, w/o copying and cache the wrapper in our map.
204            // It is also associated with our animated property, so it can notify the SVG Element which holds the SVGAnimated*List
205            // that it has been modified (and thus can call svgAttributeChanged(associatedAttributeName)).
206            wrapper = ListItemTearOff::create(animatedList, UndefinedRole, m_values->at(index));
207            m_wrappers->at(index) = wrapper;
208        }
209
210        return wrapper.release();
211    }
212
213    // SVGList::insertItemBefore()
214    ListItemType insertItemBeforeValues(const ListItemType& newItem, unsigned index, ExceptionCode& ec)
215    {
216        if (!canAlterList(ec))
217            return ListItemType();
218
219        // Spec: If the index is greater than or equal to numberOfItems, then the new item is appended to the end of the list.
220        if (index > m_values->size())
221            index = m_values->size();
222
223        // Spec: If newItem is already in a list, it is removed from its previous list before it is inserted into this list.
224        if (!processIncomingListItemValue(newItem, &index)) {
225            // Inserting the item before itself is a no-op.
226            return newItem;
227        }
228
229        // Spec: Inserts a new item into the list at the specified position. The index of the item before which the new item is to be
230        // inserted. The first item is number 0. If the index is equal to 0, then the new item is inserted at the front of the list.
231        m_values->insert(index, newItem);
232
233        commitChange();
234        return newItem;
235    }
236
237    PassListItemTearOff insertItemBeforeValuesAndWrappers(PassListItemTearOff passNewItem, unsigned index, ExceptionCode& ec)
238    {
239        ASSERT(m_wrappers);
240        if (!canAlterList(ec))
241            return 0;
242
243        // Not specified, but FF/Opera do it this way, and it's just sane.
244        if (!passNewItem) {
245            ec = SVGException::SVG_WRONG_TYPE_ERR;
246            return 0;
247        }
248
249        // Spec: If the index is greater than or equal to numberOfItems, then the new item is appended to the end of the list.
250        if (index > m_values->size())
251             index = m_values->size();
252
253        RefPtr<ListItemTearOff> newItem = passNewItem;
254        ASSERT(m_values->size() == m_wrappers->size());
255
256        // Spec: If newItem is already in a list, it is removed from its previous list before it is inserted into this list.
257        if (!processIncomingListItemWrapper(newItem, &index))
258            return newItem.release();
259
260        // Spec: Inserts a new item into the list at the specified position. The index of the item before which the new item is to be
261        // inserted. The first item is number 0. If the index is equal to 0, then the new item is inserted at the front of the list.
262        m_values->insert(index, newItem->propertyReference());
263
264        // Store new wrapper at position 'index', change its underlying value, so mutations of newItem, directly affect the item in the list.
265        m_wrappers->insert(index, newItem);
266
267        commitChange();
268        return newItem.release();
269    }
270
271    // SVGList::replaceItem()
272    bool canReplaceItem(unsigned index, ExceptionCode& ec)
273    {
274        if (!canAlterList(ec))
275            return false;
276
277        if (index >= m_values->size()) {
278            ec = INDEX_SIZE_ERR;
279            return false;
280        }
281
282        return true;
283    }
284
285    ListItemType replaceItemValues(const ListItemType& newItem, unsigned index, ExceptionCode& ec)
286    {
287        if (!canReplaceItem(index, ec))
288            return ListItemType();
289
290        // Spec: If newItem is already in a list, it is removed from its previous list before it is inserted into this list.
291        // Spec: If the item is already in this list, note that the index of the item to replace is before the removal of the item.
292        if (!processIncomingListItemValue(newItem, &index)) {
293            // Replacing the item with itself is a no-op.
294            return newItem;
295        }
296
297        if (m_values->isEmpty()) {
298            // 'newItem' already lived in our list, we removed it, and now we're empty, which means there's nothing to replace.
299            ec = INDEX_SIZE_ERR;
300            return ListItemType();
301        }
302
303        // Update the value at the desired position 'index'.
304        m_values->at(index) = newItem;
305
306        commitChange();
307        return newItem;
308    }
309
310    PassListItemTearOff replaceItemValuesAndWrappers(PassListItemTearOff passNewItem, unsigned index, ExceptionCode& ec)
311    {
312        ASSERT(m_wrappers);
313        if (!canReplaceItem(index, ec))
314            return 0;
315
316        // Not specified, but FF/Opera do it this way, and it's just sane.
317        if (!passNewItem) {
318            ec = SVGException::SVG_WRONG_TYPE_ERR;
319            return 0;
320        }
321
322        ASSERT(m_values->size() == m_wrappers->size());
323        RefPtr<ListItemTearOff> newItem = passNewItem;
324
325        // Spec: If newItem is already in a list, it is removed from its previous list before it is inserted into this list.
326        // Spec: If the item is already in this list, note that the index of the item to replace is before the removal of the item.
327        if (!processIncomingListItemWrapper(newItem, &index))
328            return newItem.release();
329
330        if (m_values->isEmpty()) {
331            ASSERT(m_wrappers->isEmpty());
332            // 'passNewItem' already lived in our list, we removed it, and now we're empty, which means there's nothing to replace.
333            ec = INDEX_SIZE_ERR;
334            return 0;
335        }
336
337        // Detach the existing wrapper.
338        RefPtr<ListItemTearOff> oldItem = m_wrappers->at(index);
339        if (oldItem)
340            oldItem->detachWrapper();
341
342        // Update the value and the wrapper at the desired position 'index'.
343        m_values->at(index) = newItem->propertyReference();
344        m_wrappers->at(index) = newItem;
345
346        commitChange();
347        return newItem.release();
348    }
349
350    // SVGList::removeItem()
351    bool canRemoveItem(unsigned index, ExceptionCode& ec)
352    {
353        if (!canAlterList(ec))
354            return false;
355
356        if (index >= m_values->size()) {
357            ec = INDEX_SIZE_ERR;
358            return false;
359        }
360
361        return true;
362    }
363
364    ListItemType removeItemValues(unsigned index, ExceptionCode& ec)
365    {
366        if (!canRemoveItem(index, ec))
367            return ListItemType();
368
369        ListItemType oldItem = m_values->at(index);
370        m_values->remove(index);
371
372        commitChange();
373        return oldItem;
374    }
375
376    PassListItemTearOff removeItemValuesAndWrappers(AnimatedListPropertyTearOff* animatedList, unsigned index, ExceptionCode& ec)
377    {
378        ASSERT(m_wrappers);
379        if (!canRemoveItem(index, ec))
380            return 0;
381
382        ASSERT(m_values->size() == m_wrappers->size());
383
384        // Detach the existing wrapper.
385        RefPtr<ListItemTearOff> oldItem = m_wrappers->at(index);
386        if (!oldItem)
387            oldItem = ListItemTearOff::create(animatedList, UndefinedRole, m_values->at(index));
388
389        oldItem->detachWrapper();
390        m_wrappers->remove(index);
391        m_values->remove(index);
392
393        commitChange();
394        return oldItem.release();
395    }
396
397    // SVGList::appendItem()
398    ListItemType appendItemValues(const ListItemType& newItem, ExceptionCode& ec)
399    {
400        if (!canAlterList(ec))
401            return ListItemType();
402
403        // Spec: If newItem is already in a list, it is removed from its previous list before it is inserted into this list.
404        processIncomingListItemValue(newItem, 0);
405
406        // Append the value at the end of the list.
407        m_values->append(newItem);
408
409        commitChange(ListModificationAppend);
410        return newItem;
411    }
412
413    PassListItemTearOff appendItemValuesAndWrappers(PassListItemTearOff passNewItem, ExceptionCode& ec)
414    {
415        ASSERT(m_wrappers);
416        if (!canAlterList(ec))
417            return 0;
418
419        // Not specified, but FF/Opera do it this way, and it's just sane.
420        if (!passNewItem) {
421            ec = SVGException::SVG_WRONG_TYPE_ERR;
422            return 0;
423        }
424
425        RefPtr<ListItemTearOff> newItem = passNewItem;
426        ASSERT(m_values->size() == m_wrappers->size());
427
428        // Spec: If newItem is already in a list, it is removed from its previous list before it is inserted into this list.
429        processIncomingListItemWrapper(newItem, 0);
430
431        // Append the value and wrapper at the end of the list.
432        m_values->append(newItem->propertyReference());
433        m_wrappers->append(newItem);
434
435        commitChange(ListModificationAppend);
436        return newItem.release();
437    }
438
439    PropertyType& values()
440    {
441        ASSERT(m_values);
442        return *m_values;
443    }
444
445    ListWrapperCache& wrappers() const
446    {
447        ASSERT(m_wrappers);
448        return *m_wrappers;
449    }
450
451protected:
452    SVGListProperty(SVGPropertyRole role, PropertyType& values, ListWrapperCache* wrappers)
453        : m_role(role)
454        , m_ownsValues(false)
455        , m_values(&values)
456        , m_wrappers(wrappers)
457    {
458    }
459
460    virtual ~SVGListProperty()
461    {
462        if (m_ownsValues)
463            delete m_values;
464    }
465
466    virtual void commitChange() = 0;
467    virtual void commitChange(ListModification)
468    {
469        commitChange();
470    }
471
472    virtual bool processIncomingListItemValue(const ListItemType& newItem, unsigned* indexToModify) = 0;
473    virtual bool processIncomingListItemWrapper(RefPtr<ListItemTearOff>& newItem, unsigned* indexToModify) = 0;
474
475    SVGPropertyRole m_role;
476    bool m_ownsValues;
477    PropertyType* m_values;
478    ListWrapperCache* m_wrappers;
479};
480
481}
482
483#endif // ENABLE(SVG)
484#endif // SVGListProperty_h
485