/*******************************************************************************
* Copyright (c) 2017 itemis AG 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:
* Matthias Wienand (itemis AG) - initial API and implementation
*
*******************************************************************************/
package org.eclipse.gef.mvc.fx.ui.actions;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.gef.fx.nodes.InfiniteCanvas;
import org.eclipse.gef.mvc.fx.viewer.IViewer;
import org.eclipse.jface.action.IAction;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.CoolBar;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.ToolBar;
import org.eclipse.swt.widgets.ToolItem;
import javafx.beans.value.ChangeListener;
import javafx.scene.Parent;
/**
* The {@link ZoomComboContributionItem} is an
* {@link AbstractViewerContributionItem} that contributes a zoom {@link Combo}
* to the tool bar. The zoom combo displays the current zoom level/factor in
* percent and provides a drop down menu that contains a number of predefined
* zoom factors (see {@link #getZoomFactors()}). Additionally, you can specify
* additional actions for which items should be added to the drop down menu when
* constructing a {@link ZoomComboContributionItem} (see
* {@link #ZoomComboContributionItem(IAction...)}).
* <p>
* If the user enters a custom value in the {@link Combo} and presses the return
* key, the string is converted to a zoom factor, which is then restricted to
* the range specified by {@link #getMinimumPermissibleZoomFactor()} and
* {@link #getMaximumPermissibleZoomFactor()} before it is applied.
*
* @author mwienand
*
*/
public class ZoomComboContributionItem extends AbstractViewerContributionItem {
/**
* The ID (see {@link #setId(String)}) for this
* {@link ZoomComboContributionItem}.
*/
public static final String ZOOM_COMBO_CONTRIBUTION_ITEM_ID = "ZoomComboContributionItem";
private static final Pattern NUMBER_PATTERN = Pattern
.compile("(\\d+\\.\\d+|\\.\\d+|\\d+)");
private static final NumberFormat PERCENT_FORMAT = NumberFormat
.getPercentInstance();
static {
PERCENT_FORMAT.setGroupingUsed(false);
PERCENT_FORMAT.setMinimumFractionDigits(0);
PERCENT_FORMAT.setMaximumFractionDigits(0);
}
// controls
private ToolItem toolItem;
private Combo zoomCombo;
private ChangeListener<? super Number> zoomListener;
// actions
private AbstractZoomAction zoomAction = createZoomAction();
private List<IAction> additionalActions = new ArrayList<>();
/**
* Constructs a new {@link ZoomComboContributionItem}.
*
* @param additionalActions
* Additional {@link IAction}s that should be shown in the
* {@link Combo}, e.g. {@link FitToViewportAction}.
*/
public ZoomComboContributionItem(IAction... additionalActions) {
setId(ZOOM_COMBO_CONTRIBUTION_ITEM_ID);
this.additionalActions.addAll(Arrays.asList(additionalActions));
}
/**
* Returns an {@link AbstractZoomAction} that is used to carry out zooming
* when one of the predefined zoom factors is selected by the user. The zoom
* factor is passed on to the action using the {@link Event#data} field of
* the {@link Event} that is given to
* {@link AbstractZoomAction#determineZoomFactor(double, Event)}.
*
* @return The {@link AbstractZoomAction} that is used to apply predefined
* zoom factors.
*/
protected AbstractZoomAction createZoomAction() {
return new AbstractZoomAction("Zoom") {
@Override
protected double determineZoomFactor(double currentZoomFactor,
Event event) {
return ((double) event.data) * 1d / currentZoomFactor;
}
};
}
@Override
public void dispose() {
if (getViewer() != null) {
init(null);
}
if (toolItem != null && !toolItem.isDisposed()) {
toolItem.dispose();
}
}
@Override
public void fill(Composite parent) {
throw new UnsupportedOperationException();
}
@Override
public void fill(CoolBar parent, int index) {
throw new UnsupportedOperationException();
}
@Override
public void fill(Menu menu, int index) {
throw new UnsupportedOperationException();
}
@Override
public void fill(ToolBar tb, int index) {
toolItem = new ToolItem(tb, SWT.SEPARATOR, index);
zoomCombo = new Combo(tb, SWT.DROP_DOWN);
zoomCombo.setItems(getItems().toArray(new String[0]));
toolItem.setWidth(
zoomCombo.computeSize(SWT.DEFAULT, SWT.DEFAULT, true).x);
toolItem.setControl(zoomCombo);
zoomCombo.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
if (e.keyCode == '\n' || e.keyCode == '\r') {
double zoom = toZoomFactor(zoomCombo.getText());
if (zoom > 0) {
Event event = new Event();
event.data = zoom;
zoomAction.runWithEvent(event);
} else {
updateComboText();
}
}
}
});
zoomCombo.addSelectionListener(new SelectionListener() {
@Override
public void widgetDefaultSelected(SelectionEvent e) {
widgetSelected(e);
}
@Override
public void widgetSelected(SelectionEvent e) {
int selectionIndex = zoomCombo.getSelectionIndex();
if (selectionIndex < 0) {
// no selection => nothing to do
return;
}
List<Double> zoomFactors = getZoomFactors();
if (selectionIndex >= zoomFactors.size()) {
// additional action selected
additionalActions.get(selectionIndex - zoomFactors.size())
.runWithEvent(null);
} else {
// zoom factor selected
Event event = new Event();
event.data = zoomFactors.get(selectionIndex);
zoomAction.runWithEvent(event);
}
}
});
updateComboText();
}
/**
* Returns a list of all strings that should be selectable in the
* {@link Combo} drop down menu.
*
* @return A list of all {@link Combo} items.
*/
protected List<String> getItems() {
List<String> items = new ArrayList<>();
// insert zoom factor items
for (Double zoomFactor : getZoomFactors()) {
items.add(toPercentText(zoomFactor));
}
// insert additional action items
for (IAction a : additionalActions) {
items.add(a.getText());
}
return items;
}
/**
* Returns the maximum zoom factor that is permissible when the user does
* not select a predefined value, but enters a custom zoom factor instead.
*
* @return The maximum zoom factor for custom input.
*/
protected double getMaximumPermissibleZoomFactor() {
return 64d;
}
/**
* Returns the minimum zoom factor that is permissible when the user does
* not select a predefined value, but enters a custom zoom factor instead.
*
* @return The minimum zoom factor for custom input.
*/
protected double getMinimumPermissibleZoomFactor() {
return 0.001d;
}
/**
* Returns a list containing all zoom factors in the order in which items
* should be created for them in the {@link Combo}.
*
* @return A list containing the predefined zoom factors.
*/
protected List<Double> getZoomFactors() {
return Arrays.asList(0.125d, 0.25d, 1d / 3d, 0.5d, 2d / 3d, 0.75d, 1d,
1.25d, 1.5d, 2d, 3d, 4d, 8d);
}
@Override
public void init(IViewer viewer) {
super.init(viewer);
// initialize delegate actions
if (zoomAction instanceof IViewerAction) {
((IViewerAction) zoomAction).init(viewer);
}
for (IAction a : additionalActions) {
if (a instanceof IViewerAction) {
((IViewerAction) a).init(viewer);
}
}
}
@Override
protected void register() {
if (zoomListener != null) {
throw new IllegalStateException(
"Zoom listener is already registered.");
}
Parent canvas = getViewer().getCanvas();
if (canvas instanceof InfiniteCanvas) {
InfiniteCanvas infiniteCanvas = (InfiniteCanvas) canvas;
zoomListener = (a, o, n) -> {
showZoomFactor(n);
};
infiniteCanvas.getContentTransform().mxxProperty()
.addListener(zoomListener);
showZoomFactor(infiniteCanvas.getContentTransform().getMxx());
}
}
/**
* Updates the zoom factor that is displayed by the {@link Combo}. Converts
* the given number to a percent text using {@link #toPercentText(double)}
* and sets it as the combo's display text.
*
* @param zoomFactor
* The number to display in the {@link Combo}.
*/
protected void showZoomFactor(Number zoomFactor) {
if (zoomCombo != null) {
String text = toPercentText(zoomFactor.doubleValue());
zoomCombo.setText(text);
}
}
/**
* Converts the given zoom factor to a corresponding percent text which can
* be displayed in the {@link Combo}. The resulting percent text does
* neither have fraction digits nor grouping delimiters and contains a
* trailing <code>"%"</code>, e.g. the zoom factor <code>2.125</code> is
* converted to the text <code>213%</code>.
*
* @param zoomFactor
* The zoom factor for which to determine the corresponding
* percent text to display in the {@link Combo}.
* @return The corresponding percent text.
*/
protected String toPercentText(double zoomFactor) {
return PERCENT_FORMAT.format(zoomFactor);
}
/**
* Converts the given percent text to a corresponding zoom factor,
* restricted to the range of permissible zoom factors that is specified by
* {@link #getMinimumPermissibleZoomFactor()} and
* {@link #getMaximumPermissibleZoomFactor()}.
* <p>
* This method is called to determine the zoom factor when the user does not
* select a predefined item in the {@link Combo}, but enters custom input
* instead.
*
* @param percentText
* The percent text for which to determine the corresponding zoom
* factor.
* @return The zoom factor corresponding to the given percent text,
* restricted to the permissible range.
*/
protected double toZoomFactor(String percentText) {
Matcher matcher = NUMBER_PATTERN.matcher(percentText);
if (matcher.find()) {
// user input is valid
try {
double zoom = PERCENT_FORMAT.parse(matcher.group(1))
.doubleValue() / 100;
// return restricted zoom level
return Math.min(getMaximumPermissibleZoomFactor(),
Math.max(getMinimumPermissibleZoomFactor(), zoom));
} catch (ParseException e) {
throw new IllegalStateException(e);
}
}
// user input is invalid
return -1;
}
@Override
protected void unregister() {
if (zoomListener == null) {
throw new IllegalStateException(
"Zoom listener not yet registered.");
}
Parent canvas = getViewer().getCanvas();
if (canvas instanceof InfiniteCanvas) {
((InfiniteCanvas) canvas).getContentTransform().mxxProperty()
.removeListener(zoomListener);
zoomListener = null;
}
}
/**
* Updates the zoom factor that is displayed in the {@link Combo}. The
* current zoom factor is queried from the {@link #getViewer() viewer} and
* set as the display text for the {@link Combo} using
* {@link #showZoomFactor(Number)}.
*/
protected void updateComboText() {
if (isEnabled()) {
Parent canvas = getViewer().getCanvas();
if (canvas instanceof InfiniteCanvas) {
InfiniteCanvas infiniteCanvas = (InfiniteCanvas) canvas;
showZoomFactor(infiniteCanvas.getContentTransform().getMxx());
}
}
}
}