blob: ac1c481b7b84d907f15a211790ec4eb7c6efb08b [file] [log] [blame]
/*=============================================================================#
# 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);
}
}