| /*=============================================================================# |
| # Copyright (c) 2012, 2020 Stephan Wahlbrink and others. |
| # |
| # This program and the accompanying materials are made available under the |
| # terms of the Eclipse Public License 2.0 which is available at |
| # https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 |
| # which is available at https://www.apache.org/licenses/LICENSE-2.0. |
| # |
| # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 |
| # |
| # Contributors: |
| # Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation |
| #=============================================================================*/ |
| |
| package org.eclipse.statet.rtm.base.ui.rexpr; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| import org.eclipse.jface.util.LocalSelectionTransfer; |
| import org.eclipse.osgi.util.NLS; |
| import org.eclipse.swt.SWT; |
| import org.eclipse.swt.dnd.DND; |
| import org.eclipse.swt.dnd.DropTarget; |
| import org.eclipse.swt.dnd.TextTransfer; |
| import org.eclipse.swt.dnd.Transfer; |
| import org.eclipse.swt.events.SelectionAdapter; |
| import org.eclipse.swt.events.SelectionEvent; |
| import org.eclipse.swt.events.SelectionListener; |
| import org.eclipse.swt.graphics.Color; |
| import org.eclipse.swt.graphics.Font; |
| import org.eclipse.swt.graphics.GC; |
| import org.eclipse.swt.graphics.Point; |
| import org.eclipse.swt.graphics.Rectangle; |
| import org.eclipse.swt.widgets.Composite; |
| import org.eclipse.swt.widgets.Control; |
| import org.eclipse.swt.widgets.Event; |
| import org.eclipse.swt.widgets.Label; |
| import org.eclipse.swt.widgets.Listener; |
| import org.eclipse.swt.widgets.Menu; |
| import org.eclipse.swt.widgets.MenuItem; |
| import org.eclipse.swt.widgets.Text; |
| |
| import org.eclipse.statet.jcommons.collections.CopyOnWriteIdentityListSet; |
| |
| import org.eclipse.statet.ecommons.emf.core.IContext; |
| import org.eclipse.statet.ecommons.ui.components.IObjValueListener; |
| import org.eclipse.statet.ecommons.ui.components.IObjValueWidget; |
| import org.eclipse.statet.ecommons.ui.components.ObjValueEvent; |
| import org.eclipse.statet.ecommons.ui.util.LayoutUtils; |
| import org.eclipse.statet.ecommons.ui.util.MenuUtils; |
| |
| import org.eclipse.statet.rtm.base.ui.RtModelUIPlugin; |
| import org.eclipse.statet.rtm.base.util.RExprType; |
| import org.eclipse.statet.rtm.base.util.RExprTypes; |
| import org.eclipse.statet.rtm.rtdata.types.RTypedExpr; |
| |
| |
| public class RExprWidget extends Composite implements IObjValueWidget<RTypedExpr> { |
| |
| |
| public static class TypeDef implements IObjValueListener<RTypedExpr> { |
| |
| |
| private final RExprTypeUIAdapter adapter; |
| |
| private Control control; |
| |
| private RExprWidget widget; |
| private String lastValue; |
| |
| |
| public TypeDef(final RExprTypeUIAdapter type) { |
| this.adapter= type; |
| } |
| |
| |
| public String getTypeKey() { |
| return this.adapter.getType().getTypeKey(); |
| } |
| |
| public RExprTypeUIAdapter getUIAdapter() { |
| return this.adapter; |
| } |
| |
| private Control getDetailControl(final RExprWidget widget) { |
| if (this.widget != null && this.widget != widget) { |
| throw new IllegalStateException(); |
| } |
| if (this.control == null) { |
| this.widget= widget; |
| this.control= createDetailControl(widget); |
| } |
| return this.control; |
| } |
| |
| public boolean hasDetail() { |
| return false; |
| } |
| |
| protected Control createDetailControl(final Composite parent) { |
| return null; |
| } |
| |
| protected void activate(final Text text) { |
| } |
| |
| protected void deactivate(final Text text) { |
| } |
| |
| @Override |
| public void valueAboutToChange(final ObjValueEvent<RTypedExpr> event) { |
| } |
| |
| @Override |
| public void valueChanged(final ObjValueEvent<RTypedExpr> event) { |
| } |
| |
| protected IContext getContext() { |
| return this.widget.context; |
| } |
| |
| protected void setExpr(final String expr) { |
| this.widget.doSetValue(this.widget.currentTypeDef, expr, 0); |
| } |
| |
| } |
| |
| private static class UndefinedAdapter extends RExprTypeUIAdapter { |
| |
| |
| public UndefinedAdapter(final String type) { |
| super(new RExprType(type, -1, NLS.bind("Unknown data type (''{0}'')", type)), |
| RtModelUIPlugin.getInstance().getImageRegistry().get( |
| RtModelUIPlugin.OBJ_UNKOWN_TYPE_IMAGE_ID) ); |
| } |
| |
| } |
| |
| |
| public static final int MIN_SIZE= 1 << 8; |
| |
| |
| private static int DELAY_MS= 333; |
| |
| private class SWTListener implements Listener { |
| |
| private boolean typeHover; |
| |
| private int ignoreMouse; |
| |
| @Override |
| public void handleEvent(final Event event) { |
| switch (event.type) { |
| case SWT.Resize: |
| updateLayout(); |
| return; |
| case SWT.MouseEnter: |
| this.typeHover= true; |
| RExprWidget.this.typeControl.redraw(); |
| return; |
| case SWT.MouseExit: |
| this.typeHover= false; |
| RExprWidget.this.typeControl.redraw(); |
| return; |
| case SWT.FocusIn: |
| RExprWidget.this.typeControl.redraw(); |
| return; |
| case SWT.FocusOut: |
| RExprWidget.this.typeControl.redraw(); |
| fireValueChanged(); |
| return; |
| case SWT.MouseDown: |
| doFocus(); |
| if (event.button == 1 && event.time != this.ignoreMouse) { |
| showTypeMenu(); |
| return; |
| } |
| return; |
| case SWT.KeyDown: |
| if (event.character == SWT.CR |
| || (event.stateMask & (SWT.MOD1 | SWT.MOD3 | SWT.MOD4)) != 0) { |
| fireValueChanged(); |
| return; |
| } |
| if (event.character == '+' && event.stateMask == SWT.MOD3) { |
| showTypeMenu(); |
| return; |
| } |
| return; |
| case SWT.Paint: |
| decorateType(event, this.typeHover); |
| return; |
| case SWT.Modify: |
| checkValueChanged(event.time, true); |
| return; |
| case SWT.Dispose: |
| onDispose(); |
| return; |
| } |
| } |
| |
| } |
| |
| |
| private final int options; |
| |
| private final RExprTypes types; |
| private final List<TypeDef> typeDefs; |
| |
| private IContext context; |
| |
| private boolean withDetail; |
| |
| private TypeDef currentTypeDef; |
| |
| private SWTListener mainListener; |
| private Label typeControl; |
| private Menu typeMenu; |
| |
| private Text textControl; |
| |
| private Control detailControl; |
| |
| private Color typeBorder2Color; |
| private Color typeBorderColor; |
| private Color typeHoverColor; |
| |
| private int textWidthHint; |
| |
| private RTypedExpr currentValue; |
| |
| private ObjValueEvent<RTypedExpr> nextEvent; |
| private long nextEventTime; |
| private final Runnable nextEventRunnable= new Runnable() { |
| @Override |
| public void run() { |
| if (RExprWidget.this.nextEventTime == 0) { |
| return; |
| } |
| final long diff= (System.nanoTime() - DELAY_MS * 1000000L - RExprWidget.this.nextEventTime) / 1000000L; |
| if (diff < -(DELAY_MS / 10)) { |
| getDisplay().timerExec(-(int) diff, RExprWidget.this.nextEventRunnable); |
| return; |
| } |
| fireValueChanged(); |
| } |
| }; |
| private final CopyOnWriteIdentityListSet<IObjValueListener<RTypedExpr>> listeners= new CopyOnWriteIdentityListSet<>(); |
| private int ignoreChanges; |
| |
| |
| public RExprWidget(final Composite parent, final int options, |
| final RExprTypes types, final List<RExprTypeUIAdapter> uiAdapters) { |
| super(parent, SWT.NONE); |
| if (uiAdapters == null) { |
| throw new NullPointerException("uiAdapters"); //$NON-NLS-1$ |
| } |
| |
| this.options= options; |
| this.types= types; |
| |
| final List<RExprWidget.TypeDef> typeDefs= new ArrayList<>(uiAdapters.size()); |
| for (final RExprTypeUIAdapter adapter : uiAdapters) { |
| final TypeDef typeDef= adapter.createWidgetDef(); |
| typeDefs.add(typeDef); |
| if (typeDef.hasDetail()) { |
| this.withDetail= true; |
| } |
| } |
| this.typeDefs= typeDefs; |
| |
| createContent(this, uiAdapters); |
| } |
| |
| |
| @Override |
| public Control getControl() { |
| return this; |
| } |
| |
| public Text getText() { |
| return this.textControl; |
| } |
| |
| @Override |
| public Class<RTypedExpr> getValueType() { |
| return RTypedExpr.class; |
| } |
| |
| protected void createContent(final Composite composite, final List<RExprTypeUIAdapter> uiAdapters) { |
| final SWTListener listener= new SWTListener(); |
| this.mainListener= listener; |
| addListener(SWT.Resize, listener); |
| addListener(SWT.Dispose, listener); |
| |
| this.typeControl= new Label(composite, SWT.CENTER); |
| this.typeControl.addListener(SWT.MouseEnter, listener); |
| this.typeControl.addListener(SWT.MouseExit, listener); |
| this.typeControl.addListener(SWT.MouseDown, listener); |
| this.typeControl.addListener(SWT.KeyDown, listener); |
| this.typeControl.addListener(SWT.Paint, listener); |
| this.typeControl.setSize(this.typeControl.computeSize(16 + 4, 16)); |
| |
| this.textControl= new Text(composite, SWT.BORDER | SWT.LEFT_TO_RIGHT); |
| this.textControl.addListener(SWT.KeyDown, listener); |
| this.textControl.addListener(SWT.FocusIn, listener); |
| this.textControl.addListener(SWT.FocusOut, listener); |
| this.textControl.addListener(SWT.Modify, listener); |
| this.textWidthHint= LayoutUtils.hintWidth(this.textControl, null, 25); |
| |
| final DropTarget dropTarget= new DropTarget(this.textControl, DND.DROP_COPY); |
| dropTarget.setTransfer(new Transfer[] { LocalSelectionTransfer.getTransfer(), TextTransfer.getInstance() } ); |
| dropTarget.addDropListener(new RExprDropAdapter(uiAdapters) { |
| @Override |
| protected IContext getContext() { |
| return RExprWidget.this.context; |
| } |
| @Override |
| protected String getCurrentTypeKey() { |
| return RExprWidget.this.currentTypeDef.getTypeKey(); |
| } |
| @Override |
| protected boolean insertText(final String text) { |
| RExprWidget.this.textControl.insert(text); |
| return true; |
| } |
| @Override |
| protected boolean setExpr(final String typeKey, final String expr, final int time) { |
| final TypeDef typeDef= getTypeDef(typeKey); |
| if (typeDef != null) { |
| doSetValue(typeDef, expr, time); |
| return true; |
| } |
| return false; |
| } |
| }); |
| } |
| |
| public void setContext(final IContext context) { |
| this.context= context; |
| } |
| |
| @Override |
| public void setFont(final Font font) { |
| super.setFont(font); |
| this.textControl.setFont(font); |
| this.textWidthHint= LayoutUtils.hintWidth(this.textControl, null, 25); |
| } |
| |
| public void setTypeBackgroundColor(final Color color) { |
| this.typeControl.setBackground(color); |
| } |
| |
| public void setTypeBorderColor(final Color color) { |
| this.typeBorderColor= color; |
| } |
| |
| public void setTypeBorder2Color(final Color color) { |
| this.typeBorder2Color= color; |
| } |
| |
| public void setTypeHoverColor(final Color color) { |
| this.typeHoverColor= color; |
| } |
| |
| public boolean getShowDetail() { |
| return this.withDetail; |
| } |
| |
| public void setShowDetail(final boolean enabled) { |
| this.withDetail= enabled; |
| updateLayout(); |
| } |
| |
| @Override |
| public void setEnabled(final boolean enabled) { |
| super.setEnabled(enabled); |
| this.textControl.setEnabled(enabled); |
| if (this.detailControl != null) { |
| this.detailControl.setEnabled(enabled); |
| } |
| } |
| |
| @Override |
| public Point computeSize(final int wHint, final int hHint, final boolean changed) { |
| final int detailWidth= this.textWidthHint / 2; |
| final int minTextWidth= this.textWidthHint / 3; |
| final int spacing= LayoutUtils.defaultHSpacing(); |
| final Point typeSize= this.typeControl.getSize(); |
| final int width= (wHint == SWT.DEFAULT) ? |
| (((this.options & MIN_SIZE) != 0) ? minTextWidth : this.textWidthHint) : |
| Math.max((this.textWidthHint / 3), wHint - typeSize.x - detailWidth); |
| final Rectangle trim= computeTrim(0, 0, |
| width + typeSize.x + spacing + detailWidth, |
| Math.max(this.textControl.computeSize(width, hHint).y, typeSize.y) ); |
| return new Point(trim.width, trim.height); |
| } |
| |
| protected void updateLayout() { |
| final int detailWidth= this.textWidthHint / 2; |
| final int minTextSize= this.textWidthHint / 3; |
| final int spacing= LayoutUtils.defaultHSpacing(); |
| final Rectangle clientArea= getClientArea(); |
| final Point typeSize= this.typeControl.getSize(); |
| int x= clientArea.x; |
| final int indent= 0; |
| // int indent= this.text.getBorderWidth(); |
| this.typeControl.setBounds(x + indent, clientArea.y, typeSize.x, clientArea.height); |
| x+= (++typeSize.x); |
| // indent+= spacing; |
| if (!this.withDetail) { |
| this.textControl.setBounds(x, clientArea.y, |
| Math.max(minTextSize, clientArea.width - typeSize.x), clientArea.height ); |
| return; |
| } |
| final int textWidth= Math.max(minTextSize, clientArea.width - typeSize.x - detailWidth - spacing); |
| this.textControl.setBounds(x, clientArea.y, textWidth, clientArea.height ); |
| x+= textWidth + spacing; |
| if (this.detailControl != null) { |
| this.detailControl.setBounds(x, clientArea.y, |
| Math.max(clientArea.width - x, 0), clientArea.height ); |
| } |
| } |
| |
| public void decorateType(final Event event, boolean hover) { |
| if (this.typeBorderColor == null || !isEnabled()) { |
| return; |
| } |
| if (!hover && this.typeMenu != null && this.typeMenu.isVisible()) { |
| hover= true; |
| } |
| final GC gc= event.gc; |
| final Point size= this.typeControl.getSize(); |
| |
| Color border; |
| Color blend; |
| if (hover) { |
| border= this.typeHoverColor; |
| blend= this.typeBorder2Color; |
| if (border == null) { |
| border= this.typeBorderColor; |
| } |
| } |
| else if (this.textControl.isFocusControl()) { |
| border= this.typeBorderColor; |
| blend= this.typeBorder2Color; |
| } |
| else { |
| border= getDisplay().getSystemColor(SWT.COLOR_WIDGET_LIGHT_SHADOW); |
| blend= null; |
| } |
| |
| if (this.textControl.getBorderWidth() > 2 && blend != null) { |
| gc.setForeground(blend); |
| gc.drawRectangle(1, 1, size.x - 3, size.y - 3); |
| } |
| |
| gc.setForeground(border); |
| gc.drawRectangle(0, 0, size.x - 1, size.y - 1); |
| gc.setAlpha(127); |
| gc.drawPoint(1, 1); |
| gc.drawPoint(1, size.y - 2); |
| gc.setAlpha(63); |
| gc.drawPoint(size.x - 2, 1); |
| gc.drawPoint(size.x - 2, size.y - 2); |
| |
| gc.setForeground(getBackground()); |
| gc.setAlpha(63); |
| gc.drawPoint(size.x - 1, 0); |
| gc.drawPoint(size.x - 1, size.y - 1); |
| gc.drawPoint(1, 0); |
| gc.drawPoint(0, 1); |
| gc.drawPoint(0, size.y - 2); |
| gc.drawPoint(1, size.y - 1); |
| |
| gc.setAlpha(127); |
| gc.drawPoint(0, 0); |
| gc.drawPoint(0, size.y - 1); |
| } |
| |
| protected void showTypeMenu() { |
| if (!isEnabled()) { |
| return; |
| } |
| |
| if (this.typeMenu == null) { |
| this.typeMenu= new Menu(this.typeControl); |
| |
| final Listener listener= new Listener() { |
| @Override |
| public void handleEvent(final Event event) { |
| switch (event.type) { |
| case SWT.Show: |
| if (!RExprWidget.this.mainListener.typeHover) { |
| RExprWidget.this.typeControl.redraw(); |
| } |
| break; |
| case SWT.Hide: |
| RExprWidget.this.mainListener.ignoreMouse= event.time; |
| RExprWidget.this.typeControl.redraw(); |
| break; |
| default: |
| break; |
| } |
| } |
| }; |
| this.typeMenu.addListener(SWT.Show, listener); |
| this.typeMenu.addListener(SWT.Hide, listener); |
| fillTypeMenu(this.typeMenu); |
| } |
| |
| MenuUtils.setPullDownPosition(this.typeMenu, this.typeControl); |
| this.typeMenu.setVisible(true); |
| } |
| |
| protected void fillTypeMenu(final Menu menu) { |
| final SelectionListener listener= new SelectionAdapter() { |
| @Override |
| public void widgetSelected(final SelectionEvent event) { |
| doSetValue((TypeDef) event.widget.getData(), null, event.time); |
| doFocus(); |
| } |
| }; |
| for (final TypeDef typeDef : this.typeDefs) { |
| final MenuItem menuItem= new MenuItem(menu, SWT.RADIO); |
| final RExprTypeUIAdapter uiAdapter= typeDef.getUIAdapter(); |
| menuItem.setImage(uiAdapter.getImage()); |
| menuItem.setText(uiAdapter.getLabel()); |
| menuItem.setData(typeDef); |
| menuItem.addSelectionListener(listener); |
| |
| menuItem.setSelection(typeDef == this.currentTypeDef); |
| } |
| } |
| |
| protected void doSetValue(TypeDef typeDef, String expr, final int time) { |
| if (typeDef == null) { |
| typeDef= getTypeDef(this.types.getDefaultTypeKey()); |
| } |
| this.ignoreChanges++; |
| try { |
| if (this.currentTypeDef == typeDef) { |
| if (expr != null) { |
| this.textControl.setText(expr); |
| } |
| } |
| else { |
| if (this.currentTypeDef != null) { |
| fireValueChanged(); |
| this.currentTypeDef.lastValue= (this.currentValue != null) ? this.currentValue.getExpr() : null; |
| } |
| this.currentTypeDef= typeDef; |
| if (expr == null) { |
| if (typeDef != null) { |
| if (typeDef.lastValue != null) { |
| expr= typeDef.lastValue; |
| } |
| else if (this.currentValue != null) { |
| expr= typeDef.getUIAdapter().adopt(this.currentValue.getTypeKey(), this.currentValue.getExpr()); |
| } |
| } |
| if (expr == null) { |
| expr= ""; //$NON-NLS-1$ |
| } |
| } |
| if (this.detailControl != null) { |
| this.detailControl.setVisible(false); |
| } |
| if (typeDef != null) { |
| final RExprTypeUIAdapter uiAdapter= typeDef.getUIAdapter(); |
| this.typeControl.setImage(uiAdapter.getImage()); |
| this.typeControl.setToolTipText(uiAdapter.getLabel()); |
| this.textControl.setText(expr); |
| if (this.withDetail) { |
| this.detailControl= typeDef.getDetailControl(this); |
| updateLayout(); |
| if (this.detailControl != null) { |
| this.detailControl.setEnabled(getEnabled()); |
| this.detailControl.setVisible(true); |
| } |
| } |
| } |
| else { |
| this.typeControl.setImage(null); |
| this.typeControl.setToolTipText(null); |
| this.detailControl= null; |
| } |
| } |
| } |
| finally { |
| this.ignoreChanges--; |
| checkValueChanged(time, false); |
| } |
| } |
| |
| protected void doFocus() { |
| this.textControl.selectAll(); |
| this.textControl.setFocus(); |
| } |
| |
| protected void onDispose() { |
| if (this.typeMenu != null) { |
| this.typeMenu.dispose(); |
| } |
| } |
| |
| |
| protected TypeDef getTypeDef(final String key) { |
| for (final TypeDef def : this.typeDefs) { |
| if (def.getTypeKey() == key) { |
| return def; |
| } |
| } |
| return null; |
| } |
| |
| @Override |
| public void addValueListener(final IObjValueListener<RTypedExpr> listener) { |
| this.listeners.add(listener); |
| } |
| |
| @Override |
| public void removeValueListener(final IObjValueListener<RTypedExpr> listener) { |
| this.listeners.remove(listener); |
| } |
| |
| protected void checkValueChanged(final int time, final boolean delay) { |
| if (this.ignoreChanges > 0) { |
| return; |
| } |
| |
| this.nextEvent= new ObjValueEvent<>(this, time, 0, this.currentValue, doGetValue(), SWT.NONE); |
| if (this.currentTypeDef != null) { |
| this.currentTypeDef.valueAboutToChange(this.nextEvent); |
| } |
| |
| if ((this.currentValue != null) ? this.currentValue.equals(this.nextEvent.newValue) : |
| null == this.nextEvent.newValue) { |
| return; |
| } |
| |
| if (delay) { |
| final boolean schedule= (this.nextEventTime == 0); |
| this.nextEventTime= System.nanoTime(); |
| if (schedule) { |
| getDisplay().timerExec(DELAY_MS, this.nextEventRunnable); |
| } |
| } |
| else { |
| fireValueChanged(); |
| } |
| } |
| |
| protected void fireValueChanged() { |
| this.nextEventTime= 0; |
| final ObjValueEvent<RTypedExpr> event= this.nextEvent; |
| if (event == null) { |
| return; |
| } |
| this.nextEvent= null; |
| this.currentValue= event.newValue; |
| for (final IObjValueListener<RTypedExpr> listener : this.listeners.toList()) { |
| listener.valueChanged(event); |
| } |
| } |
| |
| protected RTypedExpr doGetValue() { |
| final String text= this.textControl.getText(); |
| if (this.currentTypeDef == null || text.isEmpty()) { |
| return null; |
| } |
| return new RTypedExpr(this.currentTypeDef.getTypeKey(), text); |
| } |
| |
| @Override |
| public RTypedExpr getValue(final int idx) { |
| if (idx != 0) { |
| throw new IllegalArgumentException("idx: " + idx); //$NON-NLS-1$ |
| } |
| fireValueChanged(); |
| return this.currentValue; |
| } |
| |
| @Override |
| public void setValue(final int idx, final RTypedExpr value) { |
| if (idx != 0) { |
| throw new IllegalArgumentException("idx: " + idx); //$NON-NLS-1$ |
| } |
| if (value == null) { |
| doSetValue(null, "", 0); //$NON-NLS-1$ |
| return; |
| } |
| TypeDef def= getTypeDef(value.getTypeKey()); |
| if (def == null) { |
| def= new TypeDef(new UndefinedAdapter(value.getTypeKey())); |
| } |
| doSetValue(def, value.getExpr(), 0); |
| } |
| |
| } |