Mercurial > projects > dwt-addons
diff dwtx/jface/fieldassist/DecoratedField.d @ 17:f459f9147650
ImageAndMessgeDialog
author | Frank Benoit <benoit@tionex.de> |
---|---|
date | Tue, 01 Apr 2008 08:24:51 +0200 |
parents | |
children | ef4534de0cf9 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/dwtx/jface/fieldassist/DecoratedField.d Tue Apr 01 08:24:51 2008 +0200 @@ -0,0 +1,886 @@ +/******************************************************************************* + * Copyright (c) 2005, 2007 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * IBM Corporation - initial API and implementation + * Port to the D programming language: + * Frank Benoit <benoit@tionex.de> + *******************************************************************************/ +module dwtx.jface.fieldassist.DecoratedField; + +import dwtx.jface.fieldassist.FieldDecoration; +import dwtx.jface.fieldassist.FieldDecorationRegistry; +import dwtx.jface.fieldassist.IControlCreator; + +import dwt.DWT; +import dwt.events.DisposeEvent; +import dwt.events.DisposeListener; +import dwt.events.FocusEvent; +import dwt.events.FocusListener; +import dwt.events.MouseAdapter; +import dwt.events.MouseEvent; +import dwt.events.MouseTrackListener; +import dwt.events.PaintEvent; +import dwt.events.PaintListener; +import dwt.graphics.GC; +import dwt.graphics.Image; +import dwt.graphics.Point; +import dwt.graphics.Region; +import dwt.layout.FormAttachment; +import dwt.layout.FormData; +import dwt.layout.FormLayout; +import dwt.widgets.Composite; +import dwt.widgets.Control; +import dwt.widgets.Display; +import dwt.widgets.Label; +import dwt.widgets.Shell; +import dwtx.core.runtime.Assert; + +import dwt.dwthelper.utils; + +/** + * DecoratedField manages image decorations around a control. It allows clients + * to specify an image decoration and a position for the decoration relative to + * the field. Decorations may be assigned descriptions, which are shown when the + * user hovers over the decoration. Clients can decorate any kind of control by + * supplying a {@link IControlCreator} to create the control that is decorated. + * <p> + * Decorations always appear on either horizontal side of the field, never above + * or below it. Decorations can be positioned at the top or bottom of either + * side. Future implementations may provide additional positioning options for + * decorations. + * <p> + * By default, DecoratedField will consult the {@link FieldDecorationRegistry} + * to determine how much space should be reserved for each decoration. This + * allows fields with decorations from different sources to align properly on + * the same dialog, since the registry tracks the size of all decorations + * registered. Therefore, it is recommended, but not required, that clients of + * DecoratedField register the decorations used. In cases where alignment + * between different fields is not a concern, clients can use + * <code>setUseMaximumDecorationWidth(false)</code> and need not register + * their decorations. + * <p> + * This class is not intended to be subclassed. + * + * @since 3.2 + * @deprecated As of 3.3, clients should use {@link ControlDecoration} instead. + */ +public class DecoratedField { + + /** + * Cached platform flags for dealing with platform-specific issues. + */ + private static bool CARBON = "carbon".equals(DWT.getPlatform()); //$NON-NLS-1$ + + /** + * Constants describing the array indices used to hold the decorations in + * array slots. + */ + + private static const int LEFT_TOP = 0; + + private static const int LEFT_BOTTOM = 1; + + private static const int RIGHT_TOP = 2; + + private static const int RIGHT_BOTTOM = 3; + + private static const int DECORATION_SLOTS = 4; + + /** + * Simple data structure class for specifying the internals for a field + * decoration. This class contains data specific to the implementation of + * field decorations as labels attached to the field. Clients should use + * <code>FieldDecoration</code> for specifying a decoration. + */ + private class FieldDecorationData { + + /* Package */FieldDecoration decoration; + + /* Package */Label label; + + /* Package */FormData data; + + /* Package */bool showOnFocus; + + /* Package */bool visible = true; + + /** + * Create a decoration data representing the specified decoration, using + * the specified label and form data for its representation. + * + * @param decoration + * the decoration whose data is kept. + * @param label + * the label used to represent the decoration. + * @param formData + * the form data used to attach the decoration to its field. + * @param showOnFocus + * a bool specifying whether the decoration should only be + * shown when the field has focus. + */ + this(FieldDecoration decoration, Label label, + FormData formData, bool showOnFocus) { + this.decoration = decoration; + this.label = label; + this.data = formData; + this.showOnFocus = showOnFocus; + } + } + + /** + * Decorations keyed by position. + */ + private FieldDecorationData[] decDatas; + + /** + * The associated control + */ + private Control control; + + /** + * The composite with form layout used to manage decorations. + */ + private Composite form; + + /** + * The bool that indicates whether the maximum decoration width is used + * when allocating space for decorations. + */ + private bool useMaxDecorationWidth = true; + + /** + * The hover used for showing description text + */ + private Hover hover; + + /** + * The hover used to show a decoration image's description. + */ + class Hover { + private static const String EMPTY = ""; //$NON-NLS-1$ + + /** + * Offset of info hover arrow from the left or right side. + */ + private int hao = 10; + + /** + * Width of info hover arrow. + */ + private int haw = 8; + + /** + * Height of info hover arrow. + */ + private int hah = 10; + + /** + * Margin around info hover text. + */ + private int hm = 2; + + /** + * This info hover's shell. + */ + Shell hoverShell; + + /** + * The info hover text. + */ + String text = EMPTY; + + /** + * The region used to manage the shell shape + */ + Region region; + + /** + * bool indicating whether the last computed polygon location had an + * arrow on left. (true if left, false if right). + */ + bool arrowOnLeft = true; + + /* + * Create a hover parented by the specified shell. + */ + this(Shell parent) { + final Display display = parent.getDisplay(); + hoverShell = new Shell(parent, DWT.NO_TRIM | DWT.ON_TOP + | DWT.NO_FOCUS); + hoverShell.setBackground(display + .getSystemColor(DWT.COLOR_INFO_BACKGROUND)); + hoverShell.setForeground(display + .getSystemColor(DWT.COLOR_INFO_FOREGROUND)); + hoverShell.addPaintListener(new class PaintListener { + public void paintControl(PaintEvent pe) { + pe.gc.drawString(text, hm, hm); + if (!CARBON) { + pe.gc.drawPolygon(getPolygon(true)); + } + } + }); + hoverShell.addMouseListener(new class MouseAdapter { + public void mouseDown(MouseEvent e) { + hideHover(); + } + }); + } + + /* + * Compute a polygon that represents a hover with an arrow pointer. If + * border is true, compute the polygon inset by 1-pixel border. Consult + * the arrowOnLeft flag to determine which side the arrow is on. + */ + int[] getPolygon(bool border) { + Point e = getExtent(); + int b = border ? 1 : 0; + if (arrowOnLeft) { + return [ 0, 0, e.x - b, 0, e.x - b, e.y - b, + hao + haw, e.y - b, hao + haw / 2, e.y + hah - b, hao, + e.y - b, 0, e.y - b, 0, 0 ]; + } + return [ 0, 0, e.x - b, 0, e.x - b, e.y - b, + e.x - hao - b, e.y - b, e.x - hao - haw / 2, e.y + hah - b, + e.x - hao - haw, e.y - b, 0, e.y - b, 0, 0 ]; + } + + /* + * Dispose the hover, it is no longer needed. Dispose any resources + * allocated by the hover. + */ + void dispose() { + if (!hoverShell.isDisposed()) { + hoverShell.dispose(); + } + if (region !is null) { + region.dispose(); + } + } + + /* + * Set the visibility of the hover. + */ + void setVisible(bool visible) { + if (visible) { + if (!hoverShell.isVisible()) { + hoverShell.setVisible(true); + } + } else { + if (hoverShell.isVisible()) { + hoverShell.setVisible(false); + } + } + } + + /* + * Set the text of the hover to the specified text. Recompute the size + * and location of the hover to hover near the specified control, + * pointing the arrow toward the target control. + */ + void setText(String t, Control hoverNear, Control targetControl) { + if (t is null) { + t = EMPTY; + } + if (!t.equals(text)) { + Point oldSize = getExtent(); + text = t; + hoverShell.redraw(); + Point newSize = getExtent(); + if (!oldSize.opEquals(newSize)) { + // set a flag that indicates the direction of arrow + arrowOnLeft = hoverNear.getLocation().x <= targetControl + .getLocation().x; + setNewShape(); + } + } + + if (hoverNear !is null) { + Point extent = getExtent(); + int y = -extent.y - hah + 1; + int x = arrowOnLeft ? -hao + haw / 2 : -extent.x + hao + haw + / 2; + + hoverShell.setLocation(hoverNear.toDisplay(x, y)); + } + + } + + /* + * Return whether or not the hover (shell) is visible. + */ + bool isVisible() { + return hoverShell.isVisible(); + } + + /* + * Compute the extent of the hover for the current text. + */ + Point getExtent() { + GC gc = new GC(hoverShell); + Point e = gc.textExtent(text); + gc.dispose(); + e.x += hm * 2; + e.y += hm * 2; + return e; + } + + /* + * Compute a new shape for the hover shell. + */ + void setNewShape() { + Region oldRegion = region; + region = new Region(); + region.add(getPolygon(false)); + hoverShell.setRegion(region); + if (oldRegion !is null) { + oldRegion.dispose(); + } + + } + } + + /** + * Construct a decorated field which is parented by the specified composite + * and has the given style bits. Use the controlCreator to create the + * specific kind of control that is decorated inside the field. + * + * @param parent + * the parent of the decorated field. + * @param style + * the desired style bits for the field. + * @param controlCreator + * the IControlCreator used to specify the specific kind of + * control that is to be decorated. + * + * @see IControlCreator + */ + public this(Composite parent, int style, + IControlCreator controlCreator) { + decDatas = new FieldDecorationData[DECORATION_SLOTS]; + this.form = createForm(parent); + this.control = controlCreator.createControl(form, style); + + addControlListeners(); + form.setTabList([ control ]); + + // Set up the initial layout data. + FormData data = new FormData(); + data.left = new FormAttachment(0, 0); + data.top = new FormAttachment(0, 0); + data.right = new FormAttachment(100, 0); + data.bottom = new FormAttachment(100, 0); + control.setLayoutData(data); + + } + + /** + * Adds an image decoration to the field. + * + * @param decoration + * A FieldDecoration describing the image and description for the + * decoration + * + * @param position + * The DWT constant indicating the position of the decoration + * relative to the field's control. The position should include + * style bits describing both the vertical and horizontal + * orientation. <code>DWT.LEFT</code> and + * <code>DWT.RIGHT</code> describe the horizontal placement of + * the decoration relative to the field, and the constants + * <code>DWT.TOP</code> and <code>DWT.BOTTOM</code> describe + * the vertical alignment of the decoration relative to the + * field. Decorations always appear on either horizontal side of + * the field, never above or below it. For example, a decoration + * appearing on the left side of the field, at the top, is + * specified as DWT.LEFT | DWT.TOP. If an image decoration + * already exists in the specified position, it will be replaced + * by the one specified. + * @param showOnFocus + * <code>true</code> if the decoration should only be shown + * when the associated control has focus, <code>false</code> if + * it should always be shown. + * + */ + public void addFieldDecoration(FieldDecoration decoration, int position, + bool showOnFocus) { + Label label; + FormData formData; + int i = indexForPosition(position); + if (decDatas[i] is null) { + formData = createFormDataForIndex(i, decoration.getImage()); + label = new Label(form, DWT.HORIZONTAL | DWT.VERTICAL | DWT.CENTER); + label.addMouseTrackListener(new class MouseTrackListener { + Label label_; + this(){ + label_=label; + } + public void mouseHover(MouseEvent event) { + FieldDecorationData decData = cast(FieldDecorationData) event.widget + .getData(); + String desc = decData.decoration.getDescription(); + if (desc !is null) { + showHoverText(desc, label_); + } + } + + public void mouseEnter(MouseEvent event) { + } + + public void mouseExit(MouseEvent event) { + hideHover(); + } + }); + decDatas[i] = new FieldDecorationData(decoration, label, formData, + showOnFocus); + } else { + label = decDatas[i].label; + formData = decDatas[i].data; + decDatas[i].decoration = decoration; + decDatas[i].showOnFocus = showOnFocus; + } + label.setImage(decDatas[i].decoration.getImage()); + label.setData(decDatas[i]); + label.setLayoutData(formData); + label.setVisible(!showOnFocus); + + // Since sizes may have changed or there could be a new position + // defined, we need to update layout data on the control. + updateControlAttachments(i, decDatas[i]); + } + + /* + * A decoration at the specified index has been added. Update the control's + * attachments if it has not previously been attached on that side or if it + * was attached to a decoration with a lesser width. + */ + private void updateControlAttachments(int index, FieldDecorationData decData) { + FormData formData = cast(FormData) control.getLayoutData(); + int newWidth = widthOf(decData.decoration.getImage()); + // opposing represents the location of the decoration above or below + // the one in question. + int opposing; + + switch (index) { + case LEFT_TOP: + case LEFT_BOTTOM: + if (index is LEFT_TOP) { + opposing = LEFT_BOTTOM; + } else { + opposing = LEFT_TOP; + } + if (decDatas[opposing] is null) { + // No decorator on the opposing side. + // Attach the control to this decorator + formData.left = new FormAttachment(decData.label); + } else if (decDatas[opposing].data.width < newWidth) { + // Decorator on opposing side is the smaller one. Attach + // control to the new one. + formData.left = new FormAttachment(decData.label); + // Center align the smaller one relative to the larger one. + decDatas[opposing].data.left.alignment = DWT.CENTER; + decDatas[opposing].data.left.control = decData.label; + } else { + // The new decorator is the smaller one. Keep the + // control attached to the opposing one. + formData = null; + // Horizontally center the smaller one relative to the larger + // one. + decData.data.left.alignment = DWT.CENTER; + decData.data.left.control = decDatas[opposing].label; + } + break; + /* + * The only real difference in right side cases is that we are attaching + * the right side of the control to the wider decoration rather than the + * left side of the control. Other concerns (horizontally aligning the + * smaller decoration relative to the larger one) are the same. + */ + case RIGHT_TOP: + case RIGHT_BOTTOM: + if (index is RIGHT_TOP) { + opposing = RIGHT_BOTTOM; + } else { + opposing = RIGHT_TOP; + } + if (decDatas[opposing] is null) { + // No decorator on the opposing side. + // Attach the control to this decorator. + formData.right = new FormAttachment(decData.label); + } else if (decDatas[opposing].data.width < newWidth) { + // Decorator on opposing side is the smaller one. Attach + // control to the new one. + formData.right = new FormAttachment(decData.label); + // Center align the smaller one to the larger one. + // Note that this could be done using the left or right + // attachment, we use the right since it is already + // created for all right-side decorations. + decDatas[opposing].data.right.alignment = DWT.CENTER; + decDatas[opposing].data.right.control = decData.label; + } else { + // The new decorator is the smaller one. Keep the + // control attached to the opposing one. + formData = null; + // Horizontally center align the smaller one to the + // larger one. + decData.data.right.alignment = DWT.CENTER; + decData.data.right.control = decDatas[opposing].label; + } + break; + default: + return; + } + if (formData !is null) { + // Form data was updated. + control.setLayoutData(formData); + form.layout(); + } + } + + /** + * Get the control that is decorated by the receiver. + * + * @return the Control decorated by the receiver, or <code>null</code> if + * none has been created yet. + */ + public Control getControl() { + return control; + } + + /** + * Get the control that represents the decorated field. This composite + * should be used to lay out the field within its parent. + * + * @return the Control that should be layed out in the field's parent's + * layout. This is typically not the control itself, since + * additional controls are used to represent the decorations. + */ + public Control getLayoutControl() { + return form; + } + + /** + * Create the parent composite and a form layout that will be used to manage + * decorations. + */ + private Composite createForm(Composite parent) { + Composite composite = new Composite(parent, DWT.NO_FOCUS); + // see https://bugs.eclipse.org/bugs/show_bug.cgi?id=126553 + composite.setBackgroundMode(DWT.INHERIT_DEFAULT); + composite.setLayout(new FormLayout()); + return composite; + } + + /** + * Add any listeners needed on the target control. + */ + private void addControlListeners() { + control.addDisposeListener(new class DisposeListener { + public void widgetDisposed(DisposeEvent event) { + if (hover !is null) { + hover.dispose(); + } + } + }); + control.addFocusListener(new class FocusListener { + public void focusGained(FocusEvent event) { + controlFocusGained(); + } + + public void focusLost(FocusEvent event) { + controlFocusLost(); + } + + }); + } + + /* + * Return the index in the array of decoration datas that represents the + * specified DWT position. + * + * @param position The DWT constant indicating the position of the + * decoration relative to the field's control. The position should include + * style bits describing both the vertical and horizontal orientation. + * <code>DWT.LEFT</code> and <code>DWT.RIGHT</code> describe the + * horizontal placement of the decoration relative to the field, and the + * constants <code>DWT.TOP</code> and <code>DWT.BOTTOM</code> describe + * the vertical alignment of the decoration relative to the field. + * Decorations always appear on either horizontal side of the field, never + * above or below it. For example, a decoration appearing on the left side + * of the field, at the top, is specified as DWT.LEFT | DWT.TOP. + * + * @return index the index in the array of decorations that represents the + * specified DWT position. If the position is not an expected position, the + * index representing the top left position will be returned. + * + */ + private int indexForPosition(int position) { + switch (position) { + case DWT.LEFT | DWT.BOTTOM: + return LEFT_BOTTOM; + case DWT.RIGHT | DWT.TOP: + return RIGHT_TOP; + case DWT.RIGHT | DWT.BOTTOM: + return RIGHT_BOTTOM; + default: + return LEFT_TOP; + } + } + + /* + * Create a form data that will place the decoration at the specified + * position. + * + * @param index the index in the decDatas describing the position of the + * decoration. + * + * @param image the image shown in the decoration. + * + */ + private FormData createFormDataForIndex(int index, Image image) { + Assert.isTrue(index >= 0 && index < DECORATION_SLOTS, + "Index out of range"); //$NON-NLS-1$ + + FormData data = new FormData(); + switch (index) { + case LEFT_TOP: + data.left = new FormAttachment(0, 0); + data.top = new FormAttachment(0, 0); + break; + case LEFT_BOTTOM: + data.left = new FormAttachment(0, 0); + data.bottom = new FormAttachment(100, 0); + break; + case RIGHT_TOP: + data.right = new FormAttachment(100, 0); + data.top = new FormAttachment(0, 0); + break; + case RIGHT_BOTTOM: + data.right = new FormAttachment(100, 0); + data.bottom = new FormAttachment(100, 0); + break; + } + data.width = widthOf(image); + data.height = DWT.DEFAULT; + + return data; + } + + /** + * Show the specified text using the same hover dialog as is used to show + * decorator descriptions. Normally, a decoration's description text will be + * shown in an info hover over the field's control whenever the mouse hovers + * over the decoration. This method can be used to show a decoration's + * description text at other times (such as when the control receives + * focus), or to show other text associated with the field. + * + * <p> + * If there is currently a hover visible, the hover's text will be replaced + * with the specified text. + * + * @param text + * the text to be shown in the info hover, or <code>null</code> + * if no text should be shown. + */ + public void showHoverText(String text) { + showHoverText(text, control); + } + + /** + * Hide any hover popups that are currently showing on the control. + * Normally, a decoration's description text will be shown in an info hover + * over the field's control as long as the mouse hovers over the decoration, + * and will be hidden when the mouse exits the control. This method can be + * used to hide a hover that was shown using <code>showHoverText</code>, + * or to programatically hide the current decoration hover. + * + * <p> + * This message has no effect if there is no current hover. + * + */ + public void hideHover() { + if (hover !is null) { + hover.setVisible(false); + } + } + + /* + * The target control gained focus. Any decorations that should show only + * when they have the focus should be shown here. + */ + private void controlFocusGained() { + for (int i = 0; i < DECORATION_SLOTS; i++) { + if (decDatas[i] !is null && decDatas[i].showOnFocus) { + setVisible(decDatas[i], true); + } + } + } + + /* + * The target control lost focus. Any decorations that should show only when + * they have the focus should be hidden here. + */ + private void controlFocusLost() { + for (int i = 0; i < DECORATION_SLOTS; i++) { + if (decDatas[i] !is null && decDatas[i].showOnFocus) { + setVisible(decDatas[i], false); + } + } + } + + /** + * Show the specified decoration. This message has no effect if the + * decoration is already showing, or was not already added to the field + * using <code>addFieldDecoration</code>. + * + * @param decoration + * the decoration to be shown. + */ + public void showDecoration(FieldDecoration decoration) { + FieldDecorationData data = getDecorationData(decoration); + if (data is null) { + return; + } + // record the fact that client would like it to be visible + data.visible = true; + // even if it is supposed to be shown, if the field does not have focus, + // do not show it (yet) + if (!data.showOnFocus || control.isFocusControl()) { + setVisible(data, true); + } + } + + /** + * Hide the specified decoration. This message has no effect if the + * decoration is already hidden, or was not already added to the field using + * <code>addFieldDecoration</code>. + * + * @param decoration + * the decoration to be hidden. + */ + public void hideDecoration(FieldDecoration decoration) { + FieldDecorationData data = getDecorationData(decoration); + if (data is null) { + return; + } + // Store the desired visibility in the decData. We remember the + // client's instructions so that changes in visibility caused by + // field focus changes won't violate the client's visibility setting. + data.visible = false; + setVisible(data, false); + } + + /** + * Update the specified decoration. This message should be used if the image + * or description in the decoration have changed. This message has no + * immediate effect if the decoration is not visible, and no effect at all + * if the decoration was not previously added to the field. + * + * @param decoration + * the decoration to be hidden. + */ + public void updateDecoration(FieldDecoration decoration) { + FieldDecorationData data = getDecorationData(decoration); + if (data is null) { + return; + } + if (data.label !is null) { + data.label.setImage(decoration.getImage()); + // If the decoration is being shown, and a hover is active, + // update the hover text to display the new description. + if (data.label.getVisible() is true && hover !is null) { + showHoverText(decoration.getDescription(), data.label); + } + } + } + + /* + * Set the visibility of the specified decoration data. This method does not + * change the visibility value stored in the decData, but instead consults + * it to determine how the visibility should be changed. This method is + * called any time visibility of a decoration might change, whether by + * client API or focus changes. + */ + private void setVisible(FieldDecorationData decData, bool visible) { + // Check the decData visibility flag, since it contains the client's + // instructions for visibility. + if (visible && decData.visible) { + decData.label.setVisible(true); + } else { + decData.label.setVisible(false); + } + } + + /* + * Get the FieldDecorationData that corresponds to the given decoration. + */ + private FieldDecorationData getDecorationData(FieldDecoration dec) { + for (int i = 0; i < DECORATION_SLOTS; i++) { + if (decDatas[i] !is null && dec is decDatas[i].decoration + && decDatas[i].label !is null + && !decDatas[i].label.isDisposed()) { + return decDatas[i]; + } + } + return null; + } + + /* + * Show the specified text in the hover, positioning the hover near the + * specified control. + */ + private void showHoverText(String text, Control hoverNear) { + if (text is null) { + hideHover(); + return; + } + + if (hover is null) { + hover = new Hover(hoverNear.getShell()); + } + hover.setText(text, hoverNear, control); + hover.setVisible(true); + } + + /** + * Set a bool that indicates whether the receiver should use the + * decoration registry's maximum decoration width when allocating space for + * decorations. The default value is <code>true</code>. Using the maximum + * decoration width is useful so that decorated fields on the same dialog + * that have different decoration widths will all align. This also allows + * client dialogs to align non-decorated fields with decorated fields by + * consulting the maximum decoration width. + * </p> + * <p> + * Clients may wish to set this value to <code>false</code> in cases where + * space usage is more important than alignment of fields. This value must + * be set before the decorations are added in order to ensure proper + * alignment. + * </p> + * + * @param useMaximumWidth + * <code>true</code> if the maximum decoration width should be + * used as the size for all decorations, <code>false</code> if + * only the decoration size should be used. + * + * @see FieldDecorationRegistry#getMaximumDecorationWidth() + */ + public void setUseMaximumDecorationWidth(bool useMaximumWidth) { + useMaxDecorationWidth = useMaximumWidth; + } + + /* + * Return the width appropriate for the specified decoration image. + */ + private int widthOf(Image image) { + if (image is null) { + return 0; + } + return useMaxDecorationWidth ? FieldDecorationRegistry.getDefault() + .getMaximumDecorationWidth() : image.getBounds().width; + } +}