Bug 476037: Increase/Decrease font size commands for text editors

Basic support for Zoom In/Out on text editors.
Note that the action, which relies on preferences, affects all text
editors in the workspace (which is probably what most users of this
feature would prefer).

Change-Id: I61c2fcc3a6535cb978f97ea7bbb6e9551203b279
Signed-off-by: Mickael Istria <mistria@redhat.com>
diff --git a/org.eclipse.ui.workbench.texteditor/icons/full/etool16/zoomIn.gif b/org.eclipse.ui.workbench.texteditor/icons/full/etool16/zoomIn.gif
new file mode 100644
index 0000000..466d756
--- /dev/null
+++ b/org.eclipse.ui.workbench.texteditor/icons/full/etool16/zoomIn.gif
Binary files differ
diff --git a/org.eclipse.ui.workbench.texteditor/icons/full/etool16/zoomOut.gif b/org.eclipse.ui.workbench.texteditor/icons/full/etool16/zoomOut.gif
new file mode 100644
index 0000000..dd2c92f
--- /dev/null
+++ b/org.eclipse.ui.workbench.texteditor/icons/full/etool16/zoomOut.gif
Binary files differ
diff --git a/org.eclipse.ui.workbench.texteditor/plugin.properties b/org.eclipse.ui.workbench.texteditor/plugin.properties
index f49ffdb..21c2766 100644
--- a/org.eclipse.ui.workbench.texteditor/plugin.properties
+++ b/org.eclipse.ui.workbench.texteditor/plugin.properties
@@ -9,6 +9,7 @@
 #     IBM Corporation - initial API and implementation
 #     Tom Eicher (Avaloq Evolution AG) - block selection mode
 #     Daesung Ha <nberserk@gmail.com> - update recenter command description
+#     Mickael Istria (Red Hat Inc.) - 469918 Zoom In/Out
 ###############################################################################
 pluginName= Text Editor Framework
 providerName= Eclipse.org
@@ -120,6 +121,10 @@
 command.scrollLineDown.name = Scroll Line Down
 command.scrollLineUp.description = Scroll up one line of text
 command.scrollLineUp.name = Scroll Line Up
+command.zoomIn.name = Zoom In
+command.zoomIm.description = Zoom in text, increase default font size for text editors
+command.zoomOut.name = Zoom Out
+command.zoomOut.description = Zoom out text, decrease default font size for text editors
 command.selectLineDown.description = Extend the selection to the next line of text
 command.selectLineDown.name = Select Line Down
 command.selectLineEnd.description = Select to the end of the line of text
diff --git a/org.eclipse.ui.workbench.texteditor/plugin.xml b/org.eclipse.ui.workbench.texteditor/plugin.xml
index f3a897f..b3815ac 100644
--- a/org.eclipse.ui.workbench.texteditor/plugin.xml
+++ b/org.eclipse.ui.workbench.texteditor/plugin.xml
@@ -208,6 +208,18 @@
 	        id="org.eclipse.ui.edit.text.scroll.lineDown">
 	  </command>
 	  <command
+	        name="%command.zoomIn.name"
+	        description="%command.zoomIn.description"
+	        categoryId="org.eclipse.ui.category.textEditor"
+	        id="org.eclipse.ui.edit.text.zoomIn">
+	  </command>
+	  <command
+	        name="%command.zoomOut.name"
+	        description="%command.zoomOut.description"
+	        categoryId="org.eclipse.ui.category.textEditor"
+	        id="org.eclipse.ui.edit.text.zoomOut">
+	  </command>
+	  <command
 	        name="%command.selectLineUp.name"
 	        description="%command.selectLineUp.description"
 	        categoryId="org.eclipse.ui.category.textEditor"
@@ -448,6 +460,17 @@
             id="org.eclipse.ui.edit.text.open.hyperlink">
       </command>
 	</extension>
+
+	<extension point="org.eclipse.ui.commandImages">
+		<image
+			commandId="org.eclipse.ui.edit.text.zoomIn"
+			icon="$nl$/icons/full/etool16/zoomIn.gif">
+		</image>
+		<image
+			commandId="org.eclipse.ui.edit.text.zoomOut"
+			icon="$nl$/icons/full/etool16/zoomOut.gif">
+		</image>
+	</extension>
    
 	<extension
 	     point="org.eclipse.ui.bindings">
@@ -514,6 +537,16 @@
 			schemeId="org.eclipse.ui.defaultAcceleratorConfiguration"
 	        sequence="CTRL+ARROW_DOWN"/>
 	  <key
+	        commandId="org.eclipse.ui.edit.text.zoomIn"
+	        contextId="org.eclipse.ui.textEditorScope"
+			schemeId="org.eclipse.ui.defaultAcceleratorConfiguration"
+	        sequence="M1++"/>
+	  <key
+	        commandId="org.eclipse.ui.edit.text.zoomOut"
+	        contextId="org.eclipse.ui.textEditorScope"
+			schemeId="org.eclipse.ui.defaultAcceleratorConfiguration"
+	        sequence="M1+-"/>
+	  <key
 	        commandId="org.eclipse.ui.edit.text.toggleOverwrite"
 	        contextId="org.eclipse.ui.textEditorScope"
 			schemeId="org.eclipse.ui.defaultAcceleratorConfiguration"
@@ -1237,4 +1270,16 @@
          </fontValue>
       </fontDefinition>
    </extension>
+   
+   <extension
+         point="org.eclipse.ui.handlers">
+      <handler
+            class="org.eclipse.ui.texteditor.TextZoomInHandler"
+            commandId="org.eclipse.ui.edit.text.zoomIn">
+      </handler>
+      <handler
+            class="org.eclipse.ui.texteditor.TextZoomOutHandler"
+            commandId="org.eclipse.ui.edit.text.zoomOut">
+      </handler>
+   </extension>
 </plugin>
diff --git a/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/AbstractTextEditor.java b/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/AbstractTextEditor.java
index 0ad6d35..3a45e4e 100644
--- a/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/AbstractTextEditor.java
+++ b/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/AbstractTextEditor.java
@@ -4508,7 +4508,7 @@
 	 *         none is defined
 	 * @since 2.1
 	 */
-	private String getSymbolicFontName() {
+	/*package*/ String getSymbolicFontName() {
 		if (getConfigurationElement() != null)
 			return getConfigurationElement().getAttribute("symbolicFontName"); //$NON-NLS-1$
 		return null;
diff --git a/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/AbstractTextZoomHandler.java b/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/AbstractTextZoomHandler.java
new file mode 100644
index 0000000..405525b
--- /dev/null
+++ b/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/AbstractTextZoomHandler.java
@@ -0,0 +1,181 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Red Hat Inc.
+ * 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:
+ *     Mickael Istria (Red Hat Inc.) - 469918 Zoom In/Out
+ *******************************************************************************/
+package org.eclipse.ui.texteditor;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Set;
+
+import org.eclipse.swt.graphics.FontData;
+
+import org.eclipse.core.commands.AbstractHandler;
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
+
+import org.eclipse.core.runtime.IConfigurationElement;
+import org.eclipse.core.runtime.Platform;
+import org.eclipse.core.runtime.Status;
+
+import org.eclipse.jface.resource.FontDescriptor;
+import org.eclipse.jface.resource.FontRegistry;
+import org.eclipse.jface.resource.JFaceResources;
+
+import org.eclipse.ui.IWorkbenchPart;
+import org.eclipse.ui.handlers.HandlerUtil;
+import org.eclipse.ui.part.AbstractMultiEditor;
+import org.eclipse.ui.part.MultiPageEditorPart;
+
+/**
+ * Abstract handler to change the default font size on Text editors.
+ */
+abstract class AbstractTextZoomHandler extends AbstractHandler {
+
+	private static Map<String, String> fgFontToDefault;
+
+	private static Map<String, Set<String>> fgDefaultToFonts;
+
+	private int fFontSizeOffset;
+
+	/**
+	 * Implementations of this class have to specify in the constructor how much the font would
+	 * change when the handler is executed.
+	 *
+	 * @param fontSizeOffset how many points the default font will change. The offset can be
+	 *            negative, to reduce font size (zoom out) or positive to increase font size (zoom
+	 *            in)
+	 */
+	protected AbstractTextZoomHandler(int fontSizeOffset) {
+		fFontSizeOffset= fontSizeOffset;
+	}
+
+	@Override
+	public Object execute(ExecutionEvent event) throws ExecutionException {
+		AbstractTextEditor textEditor= getActiveTextEditor(event);
+		if (textEditor == null) {
+			return null;
+		}
+		FontRegistry fontRegistry= textEditor.getSite().getWorkbenchWindow().getWorkbench().getThemeManager().getCurrentTheme().getFontRegistry();
+		String fontProperty= textEditor.getSymbolicFontName();
+		if (fontProperty == null) {
+			fontProperty= JFaceResources.TEXT_FONT;
+		}
+		Set<String> fontsToSet= getAffectedFontNames(fontProperty, fontRegistry);
+		FontData[] initialFontData= null;
+		String currentFontName= fontProperty;
+		while (currentFontName != null && (initialFontData= fontRegistry.getFontData(currentFontName)) == null) {
+			currentFontName= fgFontToDefault.get(currentFontName);
+		}
+
+		FontData[] newFontData= createFontDescriptor(initialFontData).getFontData();
+		if (newFontData != null) {
+			fontsToSet.stream().forEach(fontName -> fontRegistry.put(fontName, newFontData));
+		}
+		return Status.OK_STATUS;
+	}
+
+	private FontDescriptor createFontDescriptor(FontData[] initialFontData) {
+		int destFontSize= initialFontData[0].getHeight() + fFontSizeOffset;
+		if (destFontSize <= 0) {
+			return FontDescriptor.createFrom(initialFontData);
+		}
+		return FontDescriptor.createFrom(initialFontData).setHeight(destFontSize);
+	}
+
+	private AbstractTextEditor getActiveTextEditor(ExecutionEvent event) {
+		IWorkbenchPart part= HandlerUtil.getActiveEditor(event);
+		if (part instanceof AbstractTextEditor) {
+			return (AbstractTextEditor)part;
+		} else if ((part instanceof AbstractMultiEditor) && ((AbstractMultiEditor)part).getActiveEditor() instanceof AbstractTextEditor) {
+			return (AbstractTextEditor)((AbstractMultiEditor)part).getActiveEditor();
+		} else if ((part instanceof MultiPageEditorPart) && ((MultiPageEditorPart)part).getSelectedPage() instanceof AbstractTextEditor) {
+			return (AbstractTextEditor)((MultiPageEditorPart)part).getSelectedPage();
+		}
+		return null;
+	}
+
+	@Override
+	public boolean isEnabled() {
+		return true;
+	}
+
+	/**
+	 * 
+	 * @param referenceFontName the font name on which change is initially requested
+	 * @param fontRegistry the font registry
+	 * @return the names of the fonts that should be modified together with the referenceFontName.
+	 *         Those are parent fonts from which the reference font inherit, or children font that
+	 *         are set to default or inherit from reference font or a common parent that is affected
+	 *         too.
+	 */
+	private Set<String> getAffectedFontNames(String referenceFontName, FontRegistry fontRegistry) {
+		synchronized (AbstractTextZoomHandler.class) {
+			if (fgFontToDefault == null) {
+				// TODO: This should rely on ThemeRegistry and IThemeElementDefinition,
+				// but those aren't visible at that time. So we're recreating the font hierarchy
+				fgFontToDefault= new HashMap<>();
+				fgDefaultToFonts= new HashMap<>();
+				IConfigurationElement[] themeElements= Platform.getExtensionRegistry().getConfigurationElementsFor("org.eclipse.ui.themes"); //$NON-NLS-1$
+				for (int i= 0; i < themeElements.length; i++) {
+					IConfigurationElement extension= themeElements[i];
+					if ("fontDefinition".equals(extension.getName())) { //$NON-NLS-1$
+						String fontId= extension.getAttribute("id"); //$NON-NLS-1$
+						String defaultsTo= extension.getAttribute("defaultsTo"); //$NON-NLS-1$
+						if (defaultsTo != null) {
+							fgFontToDefault.put(fontId, defaultsTo);
+							if (!fgDefaultToFonts.containsKey(defaultsTo)) {
+								fgDefaultToFonts.put(defaultsTo, new HashSet<>());
+							}
+							fgDefaultToFonts.get(defaultsTo).add(fontId);
+						}
+
+					}
+				}
+			}
+		}
+		Set<String> res= new HashSet<>();
+		FontData[] referenceFontData= fontRegistry.getFontData(referenceFontName);
+		if (fontRegistry.hasValueFor(referenceFontName)) {
+			res.add(referenceFontName);
+		}
+		String currentFontName= referenceFontName;
+		// identify "root" font to change
+		while (fgFontToDefault.get(currentFontName) != null && Arrays.equals(referenceFontData, fontRegistry.getFontData(currentFontName))) {
+			currentFontName= fgFontToDefault.get(currentFontName);
+		}
+		LinkedList<String> fontsToProcess= new LinkedList<>();
+		fontsToProcess.add(currentFontName);
+		// propage to "children" fonts
+		Set<String> alreadyProcessed= new HashSet<>();
+		while (!fontsToProcess.isEmpty()) {
+			currentFontName= fontsToProcess.get(0);
+			fontsToProcess.remove(0);
+			// with recent Java, use currentFOntName = fontsToProcess.poll instead of the 2 lines above
+			if (!alreadyProcessed.contains(currentFontName)) { // avoid infinite loop
+				alreadyProcessed.add(currentFontName);
+				FontData[] currentFontData= fontRegistry.getFontData(currentFontName);
+				if (currentFontData == null || Arrays.equals(referenceFontData, currentFontData)) {
+					if (fontRegistry.hasValueFor(currentFontName)) {
+						res.add(currentFontName);
+					}
+					Set<String> children= fgDefaultToFonts.get(currentFontName);
+					if (children != null) {
+						fontsToProcess.addAll(children);
+					}
+				}
+			}
+		}
+		return res;
+	}
+
+}
diff --git a/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/TextZoomInHandler.java b/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/TextZoomInHandler.java
new file mode 100644
index 0000000..fccca0e
--- /dev/null
+++ b/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/TextZoomInHandler.java
@@ -0,0 +1,23 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Red Hat Inc.
+ * 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:
+ *     Mickael Istria (Red Hat Inc.) - 469918 Zoom In/Out
+ *******************************************************************************/
+
+package org.eclipse.ui.texteditor;
+
+/**
+ * @noreference This class is not intended to be referenced by clients.
+ */
+final public class TextZoomInHandler extends AbstractTextZoomHandler {
+
+	public TextZoomInHandler() {
+		super(+2);
+	}
+
+}
diff --git a/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/TextZoomOutHandler.java b/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/TextZoomOutHandler.java
new file mode 100644
index 0000000..c70ff69
--- /dev/null
+++ b/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/TextZoomOutHandler.java
@@ -0,0 +1,23 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Red Hat Inc.
+ * 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:
+ *     Mickael Istria (Red Hat Inc.) - 469918 Zoom In/Out
+ *******************************************************************************/
+
+package org.eclipse.ui.texteditor;
+
+/**
+ * @noreference This class is not intended to be referenced by clients.
+ */
+final public class TextZoomOutHandler extends AbstractTextZoomHandler {
+
+	public TextZoomOutHandler() {
+		super(-2);
+	}
+
+}