001: //** Copyright Statement ***************************************************
002: //The Salmon Open Framework for Internet Applications (SOFIA)
003: // Copyright (C) 1999 - 2002, Salmon LLC
004: //
005: // This program is free software; you can redistribute it and/or
006: // modify it under the terms of the GNU General Public License version 2
007: // as published by the Free Software Foundation;
008: //
009: // This program is distributed in the hope that it will be useful,
010: // but WITHOUT ANY WARRANTY; without even the implied warranty of
011: // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
012: // GNU General Public License for more details.
013: //
014: // You should have received a copy of the GNU General Public License
015: // along with this program; if not, write to the Free Software
016: // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
017: //
018: // For more information please visit http://www.salmonllc.com
019: //** End Copyright Statement ***************************************************
020: package com.salmonllc.swing;
021:
022: import com.salmonllc.swing.events.ValueChangedListener;
023: import com.salmonllc.swing.events.ValueChangedEvent;
024: import com.salmonllc.swing.table.STableColumn;
025: import com.salmonllc.personalization.Skin;
026: import com.salmonllc.sql.DataStoreException;
027: import com.salmonllc.sql.DataStoreBuffer;
028: import com.salmonllc.sql.ModelChangedEvent;
029:
030: import javax.swing.*;
031: import javax.swing.table.JTableHeader;
032: import javax.swing.plaf.basic.BasicArrowButton;
033: import javax.swing.plaf.metal.MetalLookAndFeel;
034: import javax.swing.plaf.metal.MetalTheme;
035: import javax.swing.text.*;
036: import javax.swing.event.*;
037: import java.awt.event.*;
038: import java.awt.*;
039: import java.util.HashSet;
040: import java.util.Set;
041: import java.util.Vector;
042:
043: /**
044: * Because the SOFIA Swing components extend from standard Swing components, they cannot have a common ancestor. Instead they implement the SComponent interface. This class serves as a base class for all the SComponents in the absence of a real one. It contains helper functions that the various Scomponents need.
045: */
046: public class SComponentHelper implements FocusListener, KeyListener,
047: MouseListener, DocumentListener, ActionListener, ItemListener,
048: ListSelectionListener, ChangeListener {
049: private Object _comp;
050: private Vector _valueChangedListeners;
051: private boolean _dataDirty;
052: private JPopupMenu _menu;
053: private int _maxLength = 9999999;
054:
055: public class MaxLengthInput extends PlainDocument {
056: public void insertString(int offs, String str, AttributeSet a)
057: throws BadLocationException {
058: if (str == null || getLength() + str.length() <= _maxLength)
059: super .insertString(offs, str, a);
060: else {
061: int remainder = _maxLength - getLength();
062: if (remainder > 0)
063: super .insertString(offs, str
064: .substring(0, remainder), a);
065: }
066: }
067: }
068:
069: private class SelectAllAction extends AbstractAction {
070: private JTextComponent _compToChange;
071:
072: public SelectAllAction(JTextComponent compToChange) {
073: _compToChange = compToChange;
074: }
075:
076: public void actionPerformed(ActionEvent e) {
077: _compToChange.selectAll();
078: }
079: }
080:
081: private class ClearAction extends AbstractAction {
082: private JTextComponent _compToChange;
083:
084: public ClearAction(JTextComponent compToChange) {
085: _compToChange = compToChange;
086: }
087:
088: public void actionPerformed(ActionEvent e) {
089: _compToChange.setText("");
090: }
091: }
092:
093: /**
094: * Creates a new SComponentHelper for a button group
095: */
096: public SComponentHelper(SButtonGroup comp) {
097: _comp = comp;
098: }
099:
100: /**
101: * Creates a new SComponentHelper for a standard JComponent
102: */
103: public SComponentHelper(JComponent comp) {
104: _comp = comp;
105: comp.addFocusListener(this );
106: comp.addKeyListener(this );
107: comp.addMouseListener(this );
108: if (comp instanceof JToggleButton) {
109: ((JToggleButton) comp).addItemListener(this );
110: } else if (comp instanceof JComboBox) {
111: ((JComboBox) comp).addActionListener(this );
112: } else if (comp instanceof JList) {
113: ((JList) comp).addListSelectionListener(this );
114: } else if (comp instanceof JTextComponent) {
115: ((JTextComponent) comp).setDocument(new MaxLengthInput());
116: ((JTextComponent) comp).getDocument().addDocumentListener(
117: this );
118: addKeyboardMappings(comp);
119: addPopupMenu(comp);
120: if (comp instanceof JTextArea)
121: ((JTextArea) comp).setLineWrap(true);
122: } else if (comp instanceof JSpinner) {
123: ((JSpinner) comp).addChangeListener(this );
124: } else if (comp instanceof JSlider) {
125: ((JSlider) comp).addChangeListener(this );
126: }
127: }
128:
129: /**
130: * Invoked when a key has been typed.
131: * See the class description for {@link KeyEvent} for a definition of
132: * a key typed event.
133: */
134: public void keyTyped(KeyEvent e) {
135: }
136:
137: /**
138: * Invoked when a key has been pressed.
139: * See the class description for {@link KeyEvent} for a definition of
140: * a key pressed event.
141: */
142: public void keyPressed(KeyEvent e) {
143: PopupManager.getPopupManager().hideWindow();
144: }
145:
146: /**
147: * Invoked when a key has been released.
148: * See the class description for {@link KeyEvent} for a definition of
149: * a key released event.
150: */
151: public void keyReleased(KeyEvent e) {
152: }
153:
154: /**
155: * Invoked when an item has been selected or deselected by the user.
156: * The code written for this method performs the operations
157: * that need to occur when an item is selected (or deselected).
158: */
159: public void itemStateChanged(ItemEvent e) {
160: if (!_dataDirty) {
161: ValueChangedEvent evt = ((SComponent) _comp)
162: .generateValueChangedEvent();
163: if (evt != null)
164: notifyValueChangedListeners(evt);
165: }
166: }
167:
168: /**
169: * @see FocusListener#focusGained(FocusEvent)
170: */
171: public void focusGained(FocusEvent e) {
172: Object o = e.getSource();
173: if (o instanceof STextField) {
174: ((STextField) o).selectAll();
175: }
176: }
177:
178: private void acceptValue(Component comp) {
179: Component parent = comp.getParent();
180: if (parent instanceof JTable) {
181: if (((JTable) parent).isEditing() && !isMenuVisible()) {
182: DefaultCellEditor ed = (DefaultCellEditor) ((JTable) parent)
183: .getCellEditor();
184: ed.stopCellEditing();
185: }
186: } else if (_comp instanceof SComponent) {
187: if (_dataDirty) {
188: ValueChangedEvent evt = ((SComponent) _comp)
189: .generateValueChangedEvent();
190: if (evt != null)
191: notifyValueChangedListeners(evt);
192: }
193: }
194: }
195:
196: /**
197: * Components update the model when they lose focus. Just in case you want to update the model on a component with focus programatically like if the user presses a hot key, call accept value on a component to get it to move the value to the model.
198: */
199: public void acceptValue() {
200: acceptValue((JComponent) _comp);
201: }
202:
203: /**
204: * Call acceptValue on the item that currently has focus.
205: * @see #acceptValue()
206: */
207: public static void acceptCurrentValue() {
208: Object focusOwner = KeyboardFocusManager
209: .getCurrentKeyboardFocusManager().getFocusOwner();
210: if (focusOwner instanceof SComponent)
211: ((SComponent) focusOwner).getHelper().acceptValue();
212: }
213:
214: /**
215: * @see FocusListener#focusLost(FocusEvent)
216: */
217: public void focusLost(FocusEvent e) {
218: Component comp = (Component) e.getSource();
219: Component prev = e.getOppositeComponent();
220:
221: if (comp instanceof STable) {
222: STable tab = (STable) comp;
223: if (prev == null || !(prev.getParent() == comp)) {
224: if (tab.isEditing()) {
225: Component editingComp = tab.getEditorComponent();
226: if (editingComp instanceof SComponent) {
227: if (((SComponent) editingComp).getHelper()
228: .isMenuVisible())
229: return;
230: }
231: DefaultCellEditor ed = (DefaultCellEditor) ((JTable) comp)
232: .getCellEditor();
233: ed.stopCellEditing();
234: return;
235: }
236: }
237: } else if (comp instanceof JComboBox) {
238: if (comp.getParent() instanceof JTable)
239: return;
240: }
241: if (comp != null)
242: acceptValue(comp);
243: }
244:
245: /**
246: * Invoked when the mouse button has been clicked (pressed
247: * and released) on a component.
248: */
249: public void mouseClicked(MouseEvent e) {
250: }
251:
252: /**
253: * Invoked when a mouse button has been pressed on a component.
254: */
255: public void mousePressed(MouseEvent e) {
256: PopupManager.getPopupManager().hideWindow();
257: if (e.getModifiers() == MouseEvent.META_MASK) {
258: if (_comp instanceof STable) {
259: STable tab = (STable) _comp;
260: int row = tab.rowAtPoint(e.getPoint());
261: int col = tab.columnAtPoint(e.getPoint());
262: if (tab.isEditing())
263: tab.getCellEditor().stopCellEditing();
264: tab.changeSelection(row, col, false, false);
265: CellEditor edit = tab.getCellEditor(row, col);
266: if (edit != null && edit instanceof DefaultCellEditor) {
267: Component comp = ((DefaultCellEditor) edit)
268: .getComponent();
269: if (comp instanceof SComponent) {
270: SComponentHelper helper = ((SComponent) comp)
271: .getHelper();
272: JPopupMenu menu = helper.getMenu();
273: if (menu != null) {
274: if (comp instanceof JComponent) {
275: PopupManager.getPopupManager()
276: .showPopupMenu(menu,
277: (JComponent) _comp,
278: e.getX(), e.getY());
279: tab.editCellAt(row, col);
280: ((JComponent) tab.getEditorComponent())
281: .grabFocus();
282:
283: }
284: }
285: }
286:
287: }
288: } else if (_menu != null) {
289: if (_comp instanceof JComponent) {
290: ((JComponent) _comp).grabFocus();
291: _menu
292: .show(((JComponent) _comp), e.getX(), e
293: .getY());
294: }
295: }
296: }
297: }
298:
299: /**
300: * Invoked when a mouse button has been released on a component.
301: */
302: public void mouseReleased(MouseEvent e) {
303: }
304:
305: /**
306: * Invoked when the mouse enters a component.
307: */
308: public void mouseEntered(MouseEvent e) {
309: }
310:
311: /**
312: * Invoked when the mouse exits a component.
313: */
314: public void mouseExited(MouseEvent e) {
315: }
316:
317: /**
318: * @see DocumentListener#changedUpdate(DocumentEvent)
319: */
320: public void changedUpdate(DocumentEvent e) {
321: _dataDirty = true;
322: }
323:
324: /**
325: * @see DocumentListener#insertUpdate(DocumentEvent)
326: */
327: public void insertUpdate(DocumentEvent e) {
328: _dataDirty = true;
329: }
330:
331: /**
332: * @see DocumentListener#removeUpdate(DocumentEvent)
333: */
334: public void removeUpdate(DocumentEvent e) {
335: _dataDirty = true;
336: }
337:
338: /**
339: * This method adds a listener the will be notified when the value in this component changes.
340: * @param l The listener to add.
341: */
342: public void addValueChangedListener(ValueChangedListener l) {
343: if (_valueChangedListeners == null)
344: _valueChangedListeners = new Vector();
345: //
346: int listenersSize = _valueChangedListeners.size();
347: for (int i = 0; i < listenersSize; i++) {
348: if (((ValueChangedListener) _valueChangedListeners
349: .elementAt(i)) == l)
350: return;
351: }
352: _valueChangedListeners.addElement(l);
353: }
354:
355: /**
356: * This method removes a listener from the list that will be notified if the text in the component changes.
357: * @param l The listener to remove.
358: */
359: public void removeValueChangedListener(ValueChangedListener l) {
360: if (_valueChangedListeners == null)
361: return;
362:
363: for (int i = 0; i < _valueChangedListeners.size(); i++) {
364: if (((com.salmonllc.html.events.ValueChangedListener) _valueChangedListeners
365: .elementAt(i)) == l) {
366: _valueChangedListeners.removeElementAt(i);
367: return;
368: }
369: }
370: }
371:
372: /**
373: * Called from the framework when value changed listeners for the component need to be fired
374: * @param evt
375: */
376: public void notifyValueChangedListeners(ValueChangedEvent evt) {
377: if (evt.getNewValue() != null
378: && evt.getNewValue().length() == 0)
379: evt.setNewValue(null);
380:
381: if (_valueChangedListeners != null)
382: for (int i = 0; i < _valueChangedListeners.size(); i++) {
383: ValueChangedListener l = (ValueChangedListener) _valueChangedListeners
384: .elementAt(i);
385: if (!l.valueChanged(evt))
386: break;
387: }
388:
389: DataStoreBuffer ds = evt.getDataStore();
390: int row = evt.getRow();
391: int col = evt.getColumn();
392:
393: try {
394: if (evt.getAcceptValueInt() == ValueChangedEvent.PROCESSING_MOVE_CHANGE_TO_TEMP) {
395: _dataDirty = false;
396: ds.setTempValue(row, col, evt.getNewValue());
397: } else if (evt.getAcceptValueInt() == ValueChangedEvent.PROCESSING_COMMIT_CHANGE) {
398: _dataDirty = false;
399: ds.setFormattedString(row, col, evt.getNewValue());
400: } else if (evt.getAcceptValueInt() == ValueChangedEvent.PROCESSING_DISCARD_CHANGE) {
401: ModelChangedEvent e = new ModelChangedEvent(evt
402: .getDataStore(), evt.getColumn(), evt
403: .getOldValue(), evt.getNewValue());
404: e.getDataStore().notifyListeners(e);
405: }
406:
407: } catch (DataStoreException e) {
408: e.printStackTrace();
409: }
410:
411: }
412:
413: /**
414: * Test to see if the data in the component is dirty (changed, but not moved to the model)
415: */
416: public boolean isDataDirty() {
417: return _dataDirty;
418: }
419:
420: /**
421: * Sets the data dirty flag for the component (data is changed, but not moved to the model)
422: */
423: public void setDataDirty(boolean dataDirty) {
424: _dataDirty = dataDirty;
425: }
426:
427: /**
428: * Invoked when an action occurs.
429: */
430: public void actionPerformed(ActionEvent e) {
431: if (!_dataDirty) {
432: ValueChangedEvent evt = ((SComponent) _comp)
433: .generateValueChangedEvent();
434: if (evt != null)
435: notifyValueChangedListeners(evt);
436: }
437: }
438:
439: /**
440: * Adds keyboard mappings to the component. Sets shift+delete, shift+insert, control+insert to cut/paste/copy and sets the tab to a traversal key for a text area.
441: * @param comp
442: */
443: public void addKeyboardMappings(JComponent comp) {
444: InputMap m = comp.getInputMap();
445: m.put(KeyStroke.getKeyStroke("shift DELETE"),
446: DefaultEditorKit.cutAction);
447: m.put(KeyStroke.getKeyStroke("shift INSERT"),
448: DefaultEditorKit.pasteAction);
449: m.put(KeyStroke.getKeyStroke("control INSERT"),
450: DefaultEditorKit.pasteAction);
451: if (comp instanceof STextArea) {
452: Set forward = new HashSet();
453: Set backward = new HashSet();
454: forward.add(KeyStroke.getKeyStroke("TAB"));
455: backward.add(KeyStroke.getKeyStroke("shift TAB"));
456: comp.setFocusTraversalKeys(
457: KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,
458: forward);
459: comp.setFocusTraversalKeys(
460: KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS,
461: backward);
462: }
463: }
464:
465: /**
466: * Removes the keyboard mappings from a component added with addKeyboardMappings
467: */
468: public void removeKeyboardMappings(JComponent comp) {
469: InputMap m = comp.getInputMap();
470: m.remove(KeyStroke.getKeyStroke("shift DELETE"));
471: m.remove(KeyStroke.getKeyStroke("shift INSERT"));
472: m.remove(KeyStroke.getKeyStroke("control INSERT"));
473: }
474:
475: /**
476: * Adds a right click cut/copy/paste menu to the component
477: */
478: public void addPopupMenu(JComponent comp) {
479: if (comp instanceof STextField || comp instanceof STextArea) {
480: ActionMap am = comp.getActionMap();
481: _menu = new JPopupMenu();
482: JMenuItem cut = new JMenuItem("Cut");
483: cut.addActionListener(am.get(DefaultEditorKit.cutAction));
484: JMenuItem copy = new JMenuItem("Copy");
485: copy.addActionListener(am.get(DefaultEditorKit.copyAction));
486: JMenuItem paste = new JMenuItem("Paste");
487: paste.addActionListener(am
488: .get(DefaultEditorKit.pasteAction));
489: _menu.add(cut);
490: _menu.add(copy);
491: _menu.add(paste);
492:
493: JMenuItem select = new JMenuItem("Select All");
494: select.addActionListener(new SelectAllAction(
495: (JTextComponent) comp));
496: _menu.add(select);
497:
498: JMenuItem clear = new JMenuItem("Clear");
499: clear.addActionListener(new ClearAction(
500: (JTextComponent) comp));
501: _menu.add(clear);
502: }
503:
504: }
505:
506: /**
507: * Removes a right click menu from the component
508: */
509: public void removePopupMenu() {
510: _menu = null;
511: }
512:
513: /**
514: * Utility method that compares two values to see if they are equal
515: */
516: public static boolean valuesEqual(Object newValue, Object oldValue) {
517: if (newValue == null && oldValue != null)
518: return false;
519: else if (newValue != null && oldValue == null)
520: return false;
521: else if (newValue != null && oldValue != null)
522: if (!newValue.toString().trim().equals(
523: oldValue.toString().trim()))
524: return false;
525:
526: return true;
527: }
528:
529: /**
530: * Components may have a right click cut/copy/paste menu. This method indicates that whether or not it is currently visible
531: */
532: public boolean isMenuVisible() {
533: if (_menu == null)
534: return false;
535: return _menu.isVisible();
536: }
537:
538: /**
539: * Called whenever the value of the selection changes.
540: * @param e the event that characterizes the change.
541: */
542: public void valueChanged(ListSelectionEvent e) {
543: if (!_dataDirty) {
544: ValueChangedEvent evt = ((SComponent) _comp)
545: .generateValueChangedEvent();
546: if (evt != null)
547: notifyValueChangedListeners(evt);
548: }
549: }
550:
551: /**
552: * Returns the right click popup menu for this component
553: */
554: public JPopupMenu getMenu() {
555: return _menu;
556: }
557:
558: /**
559: * Enables or disables all components in the container
560: */
561: public static void setAllComponentsEnabled(Container cont,
562: boolean enabled) {
563: cont.setEnabled(enabled);
564: Component[] comp = cont.getComponents();
565: for (int i = 0; i < comp.length; i++) {
566: if (comp[i] instanceof Container)
567: setAllComponentsEnabled((Container) comp[i], enabled);
568: else
569: comp[i].setEnabled(enabled);
570: }
571: }
572:
573: /**
574: * Sets all components in the container to focusable, skipping ones that are currently set to false. This method is mainly used as a work around for a bug in JDK 1.4 with respect to applets. Without explicitly calling setFocusable(true) on each component, they do not respond to the tab key.
575: */
576: public static void setAllComponentsToFocusable(Container cont) {
577: setAllComponentsToFocusable(cont, true);
578: }
579:
580: /**
581: * Sets all components in the container to focusable, and will skip ones that are currently set to false depending on the argument passed. This method is mainly used as a work around for a bug in JDK 1.4 with respect to applets. Without explicitly calling setFocusable(true) on each component, they do not respond to the tab key.
582: */
583: public static void setAllComponentsToFocusable(Container cont,
584: boolean skipFalse) {
585: setComponentToFocusable(cont, skipFalse);
586: Component[] comp = cont.getComponents();
587: for (int i = 0; i < comp.length; i++) {
588: if (comp[i] instanceof Container)
589: if (!(comp[i] instanceof JTable))
590: setAllComponentsToFocusable((Container) comp[i],
591: skipFalse);
592: else
593: setComponentToFocusable(comp[i], skipFalse);
594:
595: }
596: }
597:
598: private static void setComponentToFocusable(Component comp,
599: boolean skipFalse) {
600: if (skipFalse && !comp.isFocusable())
601: return;
602: if (comp instanceof JLabel)
603: return;
604: else if (comp instanceof JPanel)
605: return;
606: else if (comp instanceof JScrollBar)
607: return;
608: else if (comp instanceof JScrollPane)
609: return;
610: else if (comp instanceof JWindow)
611: return;
612: else if (comp instanceof JViewport)
613: return;
614: else if (comp instanceof CellRendererPane)
615: return;
616: else if (comp instanceof BasicArrowButton)
617: return;
618: else if (comp instanceof SComboBox)
619: return;
620: else if (comp instanceof javax.swing.Box)
621: return;
622: comp.setFocusable(true);
623: }
624:
625: /**
626: * Sets the wait mouse cursor for this component
627: */
628: public static void setWaitCursor(JComponent comp) {
629: setCursor(comp, new Cursor(Cursor.WAIT_CURSOR));
630: }
631:
632: /**
633: * Sets the default mouse cursor for this component
634: */
635: public static void setDefaultCursor(JComponent comp) {
636: setCursor(comp, new Cursor(Cursor.DEFAULT_CURSOR));
637: }
638:
639: /**
640: * Sets the mouse cursor for a component
641: */
642: public static void setCursor(JComponent comp, Cursor cur) {
643: comp.setCursor(cur);
644: Component parent = comp.getParent();
645: while (parent != null) {
646: parent = parent.getParent();
647: if (parent != null)
648: parent.setCursor(cur);
649: }
650: }
651:
652: /**
653: * Finds the parent frame of the component or null if there isn't one
654: */
655: public static JFrame getParentFrame(JComponent comp) {
656: Component p = comp.getParent();
657: while (p != null) {
658: if (p instanceof JFrame)
659: return (JFrame) p;
660: else
661: p = p.getParent();
662: }
663: return null;
664: }
665:
666: /**
667: * Invoked when the target of the listener has changed its state.
668: *
669: * @param e a ChangeEvent object
670: */
671: public void stateChanged(ChangeEvent e) {
672: if (!_dataDirty) {
673: ValueChangedEvent evt = ((SComponent) _comp)
674: .generateValueChangedEvent();
675: if (evt != null)
676: notifyValueChangedListeners(evt);
677: }
678: }
679:
680: int getMaxLength() {
681: return _maxLength;
682: }
683:
684: void setMaxLength(int maxLength) {
685: _maxLength = maxLength;
686: }
687:
688: /**
689: * Applies a SOFIA skin to the look and feel defaults for the application
690: */
691: public static void applySkin(Skin sk, Container p) {
692: try {
693: String metalTheme = sk.getSwingMetalTheme();
694: if (metalTheme != null) {
695: Class c = Class.forName(metalTheme);
696: MetalTheme theme = (MetalTheme) c.newInstance();
697: if (theme instanceof com.salmonllc.personalization.SMetalTheme)
698: sk.applyAttributes(theme);
699: MetalLookAndFeel.setCurrentTheme(theme);
700: }
701: String lfd = sk.getSwingLookAndFeel();
702: if (lfd == null)
703: lfd = "default";
704: UIManager.LookAndFeelInfo info[] = UIManager
705: .getInstalledLookAndFeels();
706: boolean found = false;
707: for (int i = 0; i < info.length; i++) {
708: if (lfd.equals(info[i].getClassName())) {
709: found = true;
710: break;
711: } else if (lfd.equals(info[i].getName())) {
712: lfd = info[i].getClassName();
713: found = true;
714: break;
715: }
716: }
717:
718: if (!found)
719: UIManager.setLookAndFeel(UIManager
720: .getSystemLookAndFeelClassName());
721: else
722: UIManager.setLookAndFeel(lfd);
723:
724: applyAttributes(p, sk);
725:
726: } catch (Exception e) {
727: }
728:
729: }
730:
731: private static void applyAttributes(Object comp, Skin sk) {
732: sk.applyAttributes(comp);
733: if (comp instanceof JComponent)
734: ((JComponent) comp).updateUI();
735: if (comp instanceof STable) {
736: JTableHeader header = ((STable) comp).getTableHeader();
737: header.updateUI();
738: STableColumn col[] = ((STable) comp).getColumns();
739: for (int i = 0; i < col.length; i++) {
740: applyAttributes(col[i], sk);
741: }
742: }
743: if (comp instanceof Container) {
744: Component[] comps = ((Container) comp).getComponents();
745: for (int i = 0; i < comps.length; i++)
746: applyAttributes(comps[i], sk);
747: }
748: }
749: }
|