replaced <code></code> with more concise {@code}
[idea/community.git] / plugins / ui-designer / src / com / intellij / uiDesigner / radComponents / RadContainer.java
1 /*
2  * Copyright 2000-2009 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.uiDesigner.radComponents;
17
18 import com.intellij.openapi.diagnostic.Logger;
19 import com.intellij.openapi.util.Comparing;
20 import com.intellij.uiDesigner.*;
21 import com.intellij.uiDesigner.core.AbstractLayout;
22 import com.intellij.uiDesigner.designSurface.ComponentDropLocation;
23 import com.intellij.uiDesigner.lw.*;
24 import com.intellij.uiDesigner.palette.Palette;
25 import com.intellij.uiDesigner.propertyInspector.Property;
26 import com.intellij.uiDesigner.propertyInspector.PropertyEditor;
27 import com.intellij.uiDesigner.propertyInspector.PropertyRenderer;
28 import com.intellij.uiDesigner.propertyInspector.editors.string.StringEditor;
29 import com.intellij.uiDesigner.shared.BorderType;
30 import com.intellij.uiDesigner.shared.XYLayoutManager;
31 import com.intellij.uiDesigner.snapShooter.SnapshotContext;
32 import org.jetbrains.annotations.NonNls;
33 import org.jetbrains.annotations.NotNull;
34 import org.jetbrains.annotations.Nullable;
35
36 import javax.swing.*;
37 import javax.swing.border.*;
38 import java.awt.*;
39 import java.util.ArrayList;
40
41 /**
42  * @author Anton Katilin
43  * @author Vladimir Kondratyev
44  */
45 public class RadContainer extends RadComponent implements IContainer {
46   private static final Logger LOG = Logger.getInstance("#com.intellij.uiDesigner.radComponents.RadContainer");
47
48   public static class Factory extends RadComponentFactory {
49     public RadComponent newInstance(ModuleProvider module, Class aClass, String id) {
50       return new RadContainer(module, aClass, id);
51     }
52
53     public RadComponent newInstance(final Class componentClass, final String id, final Palette palette) {
54       return new RadContainer(componentClass, id, palette);
55     }
56   }
57
58   /**
59    * value: RadComponent[]
60    */
61   @NonNls
62   public static final String PROP_CHILDREN = "children";
63   /**
64    * Children components
65    */
66   private final ArrayList<RadComponent> myComponents;
67   /**
68    * Describes border's type.
69    */
70   @NotNull private BorderType myBorderType;
71   /**
72    * Border's title. If border doesn't have any title then
73    * this member is {@code null}.
74    */
75   @Nullable private StringDescriptor myBorderTitle;
76   private int myBorderTitleJustification;
77   private int myBorderTitlePosition;
78   private FontDescriptor myBorderTitleFont;
79   private ColorDescriptor myBorderTitleColor;
80   private Insets myBorderSize;
81   private ColorDescriptor myBorderColor;
82
83   protected RadLayoutManager myLayoutManager;
84   private LayoutManager myDelegeeLayout;
85
86   public RadContainer(final ModuleProvider module, final String id) {
87     this(module, JPanel.class, id);
88   }
89
90   public RadContainer(final ModuleProvider module, final Class aClass, final String id) {
91     super(module, aClass, id);
92
93     myComponents = new ArrayList<>();
94
95     // By default container doesn't have any special border
96     setBorderType(BorderType.NONE);
97
98     myLayoutManager = createInitialLayoutManager();
99     if (myLayoutManager != null) {
100       final LayoutManager layoutManager = myLayoutManager.createLayout();
101       if (layoutManager != null) {
102         setLayout(layoutManager);
103       }
104     }
105   }
106
107   public RadContainer(@NotNull final Class aClass, @NotNull final String id, final Palette palette) {
108     this(null, aClass, id);
109     setPalette(palette);
110   }
111
112   @Nullable
113   protected RadLayoutManager createInitialLayoutManager() {
114     String defaultLayoutManager = UIFormXmlConstants.LAYOUT_INTELLIJ;
115     if (getModule() != null) {
116       final GuiDesignerConfiguration configuration = GuiDesignerConfiguration.getInstance(getProject());
117       defaultLayoutManager = configuration.DEFAULT_LAYOUT_MANAGER;
118     }
119
120     try {
121       return LayoutManagerRegistry.createLayoutManager(defaultLayoutManager);
122     }
123     catch (Exception e) {
124       LOG.error(e);
125       return new RadGridLayoutManager();
126     }
127   }
128
129   public Property getInplaceProperty(final int x, final int y) {
130     // 1. We have to check whether user clicked inside border (if any) or not.
131     // In this case we have return inplace editor for border text
132     final Insets insets = getDelegee().getInsets(); // border insets
133     if (
134       x < insets.left || x > getWidth() - insets.right ||
135       y < 0 || y > insets.top
136       ) {
137       return super.getInplaceProperty(x, y);
138     }
139
140     // 2. Now we are sure that user clicked inside  title area
141     return new MyBorderTitleProperty();
142   }
143
144   @Override
145   @Nullable
146   public Property getDefaultInplaceProperty() {
147     return new MyBorderTitleProperty();
148   }
149
150   @Override
151   @Nullable
152   public Rectangle getDefaultInplaceEditorBounds() {
153     return getBorderInPlaceEditorBounds(new MyBorderTitleProperty());
154   }
155
156   public Rectangle getInplaceEditorBounds(final Property property, final int x, final int y) {
157     if (property instanceof MyBorderTitleProperty) { // If this is our property
158       return getBorderInPlaceEditorBounds(property);
159     }
160     return super.getInplaceEditorBounds(property, x, y);
161   }
162
163   private Rectangle getBorderInPlaceEditorBounds(final Property property) {
164     final MyBorderTitleProperty _property = (MyBorderTitleProperty)property;
165     final Insets insets = getDelegee().getInsets();
166     return new Rectangle(
167       insets.left,
168       0,
169       getWidth() - insets.left - insets.right,
170       _property.getPreferredSize().height
171     );
172   }
173
174   public final LayoutManager getLayout() {
175     if (myDelegeeLayout != null) {
176       return myDelegeeLayout;
177     }
178     return getDelegee().getLayout();
179   }
180
181   public final void setLayout(final LayoutManager layout) {
182     // some components (for example, JXCollapsiblePanel from SwingX) have asymmetrical getLayout/setLayout - a different
183     // layout is returned compared to what was passed to setLayout(). to avoid crashes, we store the layout we passed to
184     // the component.
185     myDelegeeLayout = layout;
186     getDelegee().setLayout(layout);
187
188     if (layout instanceof AbstractLayout) {
189       AbstractLayout aLayout = (AbstractLayout)layout;
190       for (int i = 0; i < getComponentCount(); i++) {
191         final RadComponent c = getComponent(i);
192         aLayout.addLayoutComponent(c.getDelegee(), c.getConstraints());
193       }
194     }
195   }
196
197   public final boolean isXY() {
198     return getLayout() instanceof XYLayoutManager;
199   }
200
201   /**
202    * @param component component to be added.
203    * @throws java.lang.IllegalArgumentException
204    *          if {@code component} is {@code null}
205    * @throws java.lang.IllegalArgumentException
206    *          if {@code component} already exist in the
207    *          container
208    */
209   public final void addComponent(@NotNull final RadComponent component, int index) {
210     if (myComponents.contains(component)) {
211       //noinspection HardCodedStringLiteral
212       throw new IllegalArgumentException("component is already added: " + component);
213     }
214
215     final RadComponent[] oldChildren = myComponents.toArray(new RadComponent[myComponents.size()]);
216
217     // Remove from old parent
218     final RadContainer oldParent = component.getParent();
219     if (oldParent != null) {
220       oldParent.removeComponent(component);
221     }
222
223     // Attach to new parent
224     myComponents.add(index, component);
225     component.setParent(this);
226     myLayoutManager.addComponentToContainer(this, component, index);
227
228     final RadComponent[] newChildren = myComponents.toArray(new RadComponent[myComponents.size()]);
229     firePropertyChanged(PROP_CHILDREN, oldChildren, newChildren);
230   }
231
232   public final void addComponent(@NotNull final RadComponent component) {
233     addComponent(component, myComponents.size());
234   }
235
236   /**
237    * Removes specified {@code component} from the container.
238    * This method also removes component's delegee from the
239    * container's delegee. Client code is responsible for revalidation
240    * of invalid Swing hierarchy.
241    *
242    * @param component component to be removed.
243    * @throws java.lang.IllegalArgumentException
244    *          if {@code component}
245    *          is {@code null}
246    * @throws java.lang.IllegalArgumentException
247    *          if {@code component}
248    *          doesn't exist in the container
249    */
250   public final void removeComponent(@NotNull final RadComponent component) {
251     if (!myComponents.contains(component)) {
252       //noinspection HardCodedStringLiteral
253       throw new IllegalArgumentException("component is not added: " + component);
254     }
255
256     final RadComponent[] oldChildren = myComponents.toArray(new RadComponent[myComponents.size()]);
257
258     // Remove child
259     component.setParent(null);
260     myComponents.remove(component);
261     myLayoutManager.removeComponentFromContainer(this, component);
262
263     final RadComponent[] newChildren = myComponents.toArray(new RadComponent[myComponents.size()]);
264     firePropertyChanged(PROP_CHILDREN, oldChildren, newChildren);
265   }
266
267   public final RadComponent getComponent(final int index) {
268     return myComponents.get(index);
269   }
270
271   public final int getComponentCount() {
272     return myComponents.size();
273   }
274
275   public int indexOfComponent(IComponent component) {
276     return myComponents.indexOf(component);
277   }
278
279   /**
280    * @return new array with all children
281    */
282   public final RadComponent[] getComponents() {
283     return myComponents.toArray(new RadComponent[myComponents.size()]);
284   }
285
286   @NotNull
287   public ComponentDropLocation getDropLocation(@Nullable Point location) {
288     return getLayoutManager().getDropLocation(this, location);
289   }
290
291   public RadComponent findComponentInRect(final int startRow, final int startCol, final int rowSpan, final int colSpan) {
292     for (int r = startRow; r < startRow + rowSpan; r++) {
293       for (int c = startCol; c < startCol + colSpan; c++) {
294         final RadComponent result = getComponentAtGrid(r, c);
295         if (result != null) {
296           return result;
297         }
298       }
299     }
300     return null;
301   }
302
303   @Nullable
304   public RadComponent getComponentAtGrid(boolean rowFirst, int coord1, int coord2) {
305     return rowFirst ? getComponentAtGrid(coord1, coord2) : getComponentAtGrid(coord2, coord1);
306   }
307
308   @Nullable
309   public RadComponent getComponentAtGrid(int row, int col) {
310     return getGridLayoutManager().getComponentAtGrid(this, row, col);
311   }
312
313   public int getGridRowCount() {
314     return getGridLayoutManager().getGridRowCount(this);
315   }
316
317   public int getGridColumnCount() {
318     return getGridLayoutManager().getGridColumnCount(this);
319   }
320
321   public int getGridCellCount(boolean isRow) {
322     return isRow ? getGridRowCount() : getGridColumnCount();
323   }
324
325   public int getGridRowAt(int y) {
326     return getGridLayoutManager().getGridRowAt(this, y);
327   }
328
329   public int getGridColumnAt(int x) {
330     return getGridLayoutManager().getGridColumnAt(this, x);
331   }
332
333   /**
334    * @return border's type.
335    * @see com.intellij.uiDesigner.shared.BorderType
336    */
337   @NotNull
338   public final BorderType getBorderType() {
339     return myBorderType;
340   }
341
342   /**
343    * @throws java.lang.IllegalArgumentException
344    *          if {@code type}
345    *          is {@code null}
346    * @see com.intellij.uiDesigner.shared.BorderType
347    */
348   public final void setBorderType(@NotNull final BorderType type) {
349     if (myBorderType == type) {
350       return;
351     }
352     myBorderType = type;
353     updateBorder();
354   }
355
356   /**
357    * @return border's title. If the container doesn't have any title then the
358    *         method returns {@code null}.
359    */
360   @Nullable
361   public final StringDescriptor getBorderTitle() {
362     return myBorderTitle;
363   }
364
365   /**
366    * @param title new border's title. {@code null} means that
367    *              the containr doesn't have have titled border.
368    */
369   public final void setBorderTitle(final StringDescriptor title) {
370     if (Comparing.equal(title, myBorderTitle)) {
371       return;
372     }
373     myBorderTitle = title;
374     updateBorder();
375   }
376
377   public int getBorderTitleJustification() {
378     return myBorderTitleJustification;
379   }
380
381   public void setBorderTitleJustification(final int borderTitleJustification) {
382     if (myBorderTitleJustification != borderTitleJustification) {
383       myBorderTitleJustification = borderTitleJustification;
384       updateBorder();
385     }
386   }
387
388   public int getBorderTitlePosition() {
389     return myBorderTitlePosition;
390   }
391
392   public void setBorderTitlePosition(final int borderTitlePosition) {
393     if (myBorderTitlePosition != borderTitlePosition) {
394       myBorderTitlePosition = borderTitlePosition;
395       updateBorder();
396     }
397   }
398
399   public FontDescriptor getBorderTitleFont() {
400     return myBorderTitleFont;
401   }
402
403   public void setBorderTitleFont(final FontDescriptor borderTitleFont) {
404     if (!Comparing.equal(myBorderTitleFont, borderTitleFont)) {
405       myBorderTitleFont = borderTitleFont;
406       updateBorder();
407     }
408   }
409
410   public ColorDescriptor getBorderTitleColor() {
411     return myBorderTitleColor;
412   }
413
414   public void setBorderTitleColor(final ColorDescriptor borderTitleColor) {
415     if (!Comparing.equal(myBorderTitleColor, borderTitleColor)) {
416       myBorderTitleColor = borderTitleColor;
417       updateBorder();
418     }
419   }
420
421   public Insets getBorderSize() {
422     return myBorderSize;
423   }
424
425   public void setBorderSize(final Insets borderSize) {
426     if (!Comparing.equal(myBorderSize, borderSize)) {
427       myBorderSize = borderSize;
428       updateBorder();
429     }
430   }
431
432   public ColorDescriptor getBorderColor() {
433     return myBorderColor;
434   }
435
436   public void setBorderColor(final ColorDescriptor borderColor) {
437     if (!Comparing.equal(myBorderColor, borderColor)) {
438       myBorderColor = borderColor;
439       updateBorder();
440     }
441   }
442
443   /**
444    * Updates delegee's border
445    */
446   public boolean updateBorder() {
447     String title = null;
448     String oldTitle = null;
449     if (myBorderTitle != null) {
450       oldTitle = myBorderTitle.getResolvedValue();
451       myBorderTitle.setResolvedValue(null);
452       // NOTE: the explicit getValue() check is required for SnapShooter operation
453       if (myBorderTitle.getValue() != null) {
454         title = myBorderTitle.getValue();
455       }
456       else {
457         title = StringDescriptorManager.getInstance(getModule()).resolve(this, myBorderTitle);
458       }
459     }
460     Font font = (myBorderTitleFont != null) ? myBorderTitleFont.getResolvedFont(getDelegee().getFont()) : null;
461     Color titleColor = (myBorderTitleColor != null) ? myBorderTitleColor.getResolvedColor() : null;
462     Color borderColor = (myBorderColor != null) ? myBorderColor.getResolvedColor() : null;
463     getDelegee().setBorder(myBorderType.createBorder(title, myBorderTitleJustification, myBorderTitlePosition,
464                                                      font, titleColor, myBorderSize, borderColor));
465     return myBorderTitle != null && !Comparing.equal(oldTitle, myBorderTitle.getResolvedValue());
466   }
467
468   public RadLayoutManager getLayoutManager() {
469     RadContainer parent = this;
470     while (parent != null) {
471       if (parent.myLayoutManager != null) {
472         return parent.myLayoutManager;
473       }
474       parent = parent.getParent();
475     }
476     return null;
477   }
478
479   public void setLayoutManager(final RadLayoutManager layoutManager) {
480     myLayoutManager = layoutManager;
481     setLayout(myLayoutManager.createLayout());
482   }
483
484   public void setLayoutManager(RadLayoutManager layoutManager, LayoutManager layout) {
485     myLayoutManager = layoutManager;
486     setLayout(layout);
487   }
488
489   public RadComponent getActionTargetComponent(RadComponent child) {
490     return child;
491   }
492
493   @Override
494   public boolean areChildrenExclusive() {
495     return myLayoutManager.areChildrenExclusive();
496   }
497
498   @Override
499   public void refresh() {
500     for (int i = 0; i < getComponentCount(); i++) {
501       getComponent(i).refresh();
502     }
503     myLayoutManager.refresh(this);
504   }
505
506   /**
507    * Serializes container's border
508    */
509   protected final void writeBorder(final XmlWriter writer) {
510     writer.startElement(UIFormXmlConstants.ELEMENT_BORDER);
511     try {
512       writer.addAttribute(UIFormXmlConstants.ATTRIBUTE_TYPE, getBorderType().getId());
513       if (getBorderTitle() != null) {
514         final StringDescriptor descriptor = getBorderTitle();
515         writer.writeStringDescriptor(descriptor, UIFormXmlConstants.ATTRIBUTE_TITLE,
516                                      UIFormXmlConstants.ATTRIBUTE_TITLE_RESOURCE_BUNDLE,
517                                      UIFormXmlConstants.ATTRIBUTE_TITLE_KEY);
518       }
519       if (myBorderTitleJustification != 0) {
520         writer.addAttribute(UIFormXmlConstants.ATTRIBUTE_TITLE_JUSTIFICATION, myBorderTitleJustification);
521       }
522       if (myBorderTitlePosition != 0) {
523         writer.addAttribute(UIFormXmlConstants.ATTRIBUTE_TITLE_POSITION, myBorderTitlePosition);
524       }
525       if (myBorderTitleFont != null) {
526         writer.startElement(UIFormXmlConstants.ELEMENT_FONT);
527         writer.writeFontDescriptor(myBorderTitleFont);
528         writer.endElement();
529       }
530       if (myBorderTitleColor != null) {
531         writer.startElement(UIFormXmlConstants.ELEMENT_TITLE_COLOR);
532         writer.writeColorDescriptor(myBorderTitleColor);
533         writer.endElement();
534       }
535       if (myBorderSize != null) {
536         writer.startElement(UIFormXmlConstants.ELEMENT_SIZE);
537         writer.writeInsets(myBorderSize);
538         writer.endElement();
539       }
540       if (myBorderColor != null) {
541         writer.startElement(UIFormXmlConstants.ELEMENT_COLOR);
542         writer.writeColorDescriptor(myBorderColor);
543         writer.endElement();
544       }
545     }
546     finally {
547       writer.endElement(); // border
548     }
549   }
550
551   /**
552    * Serializes container's children
553    */
554   protected final void writeChildren(final XmlWriter writer) {
555     // Children
556     writer.startElement("children");
557     try {
558       writeChildrenImpl(writer);
559     }
560     finally {
561       writer.endElement(); // children
562     }
563   }
564
565   protected final void writeChildrenImpl(final XmlWriter writer) {
566     for (int i = 0; i < getComponentCount(); i++) {
567       getComponent(i).write(writer);
568     }
569   }
570
571   public void write(final XmlWriter writer) {
572     if (isXY()) {
573       writer.startElement("xy");
574     }
575     else {
576       writer.startElement("grid");
577     }
578     try {
579       writeId(writer);
580       writeClassIfDifferent(writer, JPanel.class.getName());
581       writeBinding(writer);
582
583       if (myLayoutManager != null) {
584         writer.addAttribute(UIFormXmlConstants.ATTRIBUTE_LAYOUT_MANAGER, myLayoutManager.getName());
585       }
586
587       getLayoutManager().writeLayout(writer, this);
588
589       // Constraints and properties
590       writeConstraints(writer);
591       writeProperties(writer);
592
593       // Border
594       writeBorder(writer);
595
596       // Children
597       writeChildren(writer);
598     }
599     finally {
600       writer.endElement(); // xy/grid
601     }
602   }
603
604   public boolean accept(ComponentVisitor visitor) {
605     if (!super.accept(visitor)) {
606       return false;
607     }
608
609     for (int i = 0; i < getComponentCount(); i++) {
610       final IComponent c = getComponent(i);
611       if (!c.accept(visitor)) {
612         return false;
613       }
614     }
615
616     return true;
617   }
618
619   protected void writeNoLayout(final XmlWriter writer, final String defaultClassName) {
620     writeId(writer);
621     writeClassIfDifferent(writer, defaultClassName);
622     writeBinding(writer);
623
624     // Constraints and properties
625     writeConstraints(writer);
626     writeProperties(writer);
627
628     // Margin and border
629     writeBorder(writer);
630     writeChildren(writer);
631   }
632
633   @Override
634   protected void importSnapshotComponent(final SnapshotContext context, final JComponent component) {
635     getLayoutManager().createSnapshotLayout(context, component, this, component.getLayout());
636     importSnapshotBorder(component);
637     for (Component child : component.getComponents()) {
638       if (child instanceof JComponent) {
639         RadComponent childComponent = createSnapshotComponent(context, (JComponent)child);
640         if (childComponent != null) {
641           getLayoutManager().addSnapshotComponent(component, (JComponent)child, this, childComponent);
642         }
643       }
644     }
645   }
646
647   private void importSnapshotBorder(final JComponent component) {
648     Border border = component.getBorder();
649     if (border != null) {
650       if (border instanceof TitledBorder) {
651         TitledBorder titledBorder = (TitledBorder)border;
652         setBorderTitle(StringDescriptor.create(titledBorder.getTitle()));
653         setBorderTitleJustification(titledBorder.getTitleJustification());
654         setBorderTitlePosition(titledBorder.getTitlePosition());
655         final Font titleFont = titledBorder.getTitleFont();
656         if (titleFont != null) {
657           setBorderTitleFont(new FontDescriptor(titleFont.getName(), titleFont.getStyle(), titleFont.getSize()));
658         }
659         setBorderTitleColor(new ColorDescriptor(titledBorder.getTitleColor()));
660         border = titledBorder.getBorder();
661       }
662
663       if (border instanceof EtchedBorder) {
664         setBorderType(BorderType.ETCHED);
665       }
666       else if (border instanceof BevelBorder) {
667         BevelBorder bevelBorder = (BevelBorder)border;
668         setBorderType(bevelBorder.getBevelType() == BevelBorder.RAISED ? BorderType.BEVEL_RAISED : BorderType.BEVEL_LOWERED);
669       }
670       else if (border instanceof EmptyBorder) {
671         EmptyBorder emptyBorder = (EmptyBorder)border;
672         setBorderType(BorderType.EMPTY);
673         setBorderSize(emptyBorder.getBorderInsets());
674       }
675       else if (border instanceof LineBorder) {
676         LineBorder lineBorder = (LineBorder)border;
677         setBorderType(BorderType.LINE);
678         setBorderColor(new ColorDescriptor(lineBorder.getLineColor()));
679       }
680     }
681   }
682
683   public RadAbstractGridLayoutManager getGridLayoutManager() {
684     if (!(myLayoutManager instanceof RadAbstractGridLayoutManager)) {
685       throw new RuntimeException("Not a grid container: " + myLayoutManager);
686     }
687     return (RadAbstractGridLayoutManager)myLayoutManager;
688   }
689
690   @Nullable
691   public RadComponent findComponentWithConstraints(final Object constraints) {
692     for (RadComponent component : getComponents()) {
693       if (constraints.equals(component.getCustomLayoutConstraints())) {
694         return component;
695       }
696     }
697     return null;
698   }
699
700   private final class MyBorderTitleProperty extends Property<RadContainer, StringDescriptor> {
701     private final StringEditor myEditor;
702
703     public MyBorderTitleProperty() {
704       super(null, "Title");
705       myEditor = new StringEditor(getProject());
706     }
707
708     public Dimension getPreferredSize() {
709       return myEditor.getPreferredSize();
710     }
711
712     public StringDescriptor getValue(final RadContainer component) {
713       return myBorderTitle;
714     }
715
716     protected void setValueImpl(final RadContainer container, final StringDescriptor value) throws Exception {
717       setBorderTitle(value);
718     }
719
720     @NotNull
721     public PropertyRenderer<StringDescriptor> getRenderer() {
722       return null;
723     }
724
725     public PropertyEditor<StringDescriptor> getEditor() {
726       return myEditor;
727     }
728   }
729 }