| /******************************************************************************* |
| * Copyright (c) 2018 Remain Software |
| * 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: |
| * wim.jongman@remainsoftware.com - initial API and implementation |
| *******************************************************************************/ |
| package org.eclipse.tips.ui.internal; |
| |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Timer; |
| import java.util.TimerTask; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| import org.eclipse.core.runtime.FileLocator; |
| import org.eclipse.core.runtime.IProgressMonitor; |
| import org.eclipse.core.runtime.IStatus; |
| import org.eclipse.core.runtime.Platform; |
| import org.eclipse.core.runtime.Status; |
| import org.eclipse.core.runtime.jobs.Job; |
| import org.eclipse.swt.SWT; |
| import org.eclipse.swt.browser.Browser; |
| import org.eclipse.swt.custom.StackLayout; |
| import org.eclipse.swt.events.SelectionAdapter; |
| import org.eclipse.swt.events.SelectionEvent; |
| import org.eclipse.swt.graphics.Color; |
| import org.eclipse.swt.graphics.Image; |
| import org.eclipse.swt.graphics.Point; |
| import org.eclipse.swt.graphics.Rectangle; |
| import org.eclipse.swt.internal.DPIUtil; |
| import org.eclipse.swt.layout.FillLayout; |
| import org.eclipse.swt.layout.GridData; |
| import org.eclipse.swt.layout.GridLayout; |
| import org.eclipse.swt.widgets.Button; |
| import org.eclipse.swt.widgets.Composite; |
| import org.eclipse.swt.widgets.Control; |
| import org.eclipse.swt.widgets.Label; |
| import org.eclipse.swt.widgets.Menu; |
| import org.eclipse.swt.widgets.MenuItem; |
| import org.eclipse.tips.core.IHtmlTip; |
| import org.eclipse.tips.core.IUrlTip; |
| import org.eclipse.tips.core.Tip; |
| import org.eclipse.tips.core.TipAction; |
| import org.eclipse.tips.core.TipImage; |
| import org.eclipse.tips.core.TipManager; |
| import org.eclipse.tips.core.TipProvider; |
| import org.eclipse.tips.core.internal.LogUtil; |
| import org.eclipse.tips.ui.ISwtTip; |
| import org.eclipse.tips.ui.internal.util.ImageUtil; |
| import org.eclipse.tips.ui.internal.util.ResourceManager; |
| |
| @SuppressWarnings("restriction") |
| public class TipComposite extends Composite implements ProviderSelectionListener { |
| private static final int READ_TIMER = 2000; |
| private TipProvider fProvider; |
| private Browser fBrowser; |
| private Slider fSlider; |
| private TipManager fTipManager; |
| private Tip fCurrentTip; |
| private Button fShowAtStart; |
| private Button fUnreadOnly; |
| private Button fPreviousTipButton; |
| private Pattern fGtkHackPattern = Pattern.compile("(.*?)([0-9]+)(.*?)([0-9]+)(.*?)"); |
| private Composite fSWTComposite; |
| private Composite fBrowserComposite; |
| private StackLayout fContentStack; |
| private Button fMultiActionMenuButton; |
| private Composite fNavigationBar; |
| private StackLayout fActionStack; |
| private Composite fEmptyActionComposite; |
| private Composite fSingleActionComposite; |
| private Composite fMultiActionComposite; |
| private Button fSingleActionButton; |
| private Button fMultiActionButton; |
| private Composite fContentComposite; |
| private List<Image> fActionImages = new ArrayList<>(); |
| private Menu fActionMenu; |
| |
| /** |
| * Constructor. |
| * |
| * @param parent |
| * the parent |
| * @param style |
| * the style |
| */ |
| public TipComposite(Composite parent, int style) { |
| super(parent, style); |
| GridLayout gridLayout_1 = new GridLayout(1, false); |
| gridLayout_1.marginWidth = 2; |
| gridLayout_1.marginHeight = 2; |
| setLayout(gridLayout_1); |
| |
| fContentComposite = new Composite(this, SWT.NONE); |
| fContentStack = new StackLayout(); |
| fContentComposite.setLayout(fContentStack); |
| GridData gd_gridComposite = new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1); |
| gd_gridComposite.widthHint = 900; |
| gd_gridComposite.heightHint = 600; |
| // gd_gridComposite.minimumWidth = -1; |
| // gd_gridComposite.minimumHeight = -1; |
| fContentComposite.setLayoutData(gd_gridComposite); |
| |
| fBrowserComposite = new Composite(fContentComposite, SWT.NONE); |
| fBrowserComposite.setLayout(new FillLayout(SWT.HORIZONTAL)); |
| |
| fBrowser = new Browser(fBrowserComposite, SWT.NONE); |
| fBrowser.setJavascriptEnabled(true); |
| |
| fSWTComposite = new Composite(fContentComposite, SWT.NONE); |
| fSWTComposite.setLayout(new FillLayout(SWT.HORIZONTAL)); |
| |
| fNavigationBar = new Composite(this, SWT.NONE); |
| fNavigationBar.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); |
| GridLayout gl_NavigationBar = new GridLayout(2, false); |
| gl_NavigationBar.horizontalSpacing = 0; |
| gl_NavigationBar.marginHeight = 0; |
| gl_NavigationBar.verticalSpacing = 0; |
| gl_NavigationBar.marginWidth = 0; |
| fNavigationBar.setLayout(gl_NavigationBar); |
| |
| Composite preferenceBar = new Composite(fNavigationBar, SWT.NONE); |
| FillLayout fl_composite_3 = new FillLayout(SWT.HORIZONTAL); |
| fl_composite_3.marginWidth = 5; |
| fl_composite_3.spacing = 5; |
| preferenceBar.setLayout(fl_composite_3); |
| |
| fShowAtStart = new Button(preferenceBar, SWT.CHECK); |
| fShowAtStart.setText("Show tips at startup"); |
| fShowAtStart.addSelectionListener(new SelectionAdapter() { |
| @Override |
| public void widgetSelected(SelectionEvent e) { |
| fTipManager.setRunAtStartup(fShowAtStart.getSelection()); |
| } |
| }); |
| |
| fUnreadOnly = new Button(preferenceBar, SWT.CHECK); |
| fUnreadOnly.setText("Unread only"); |
| fUnreadOnly.addSelectionListener(new SelectionAdapter() { |
| @Override |
| public void widgetSelected(SelectionEvent e) { |
| fTipManager.setServeReadTips(!fUnreadOnly.getSelection()); |
| fPreviousTipButton.setEnabled(fTipManager.mustServeReadTips()); |
| fSlider.load(); |
| getNextTip(); |
| } |
| }); |
| |
| Composite buttonBar = new Composite(fNavigationBar, SWT.NONE); |
| buttonBar.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, true, false, 1, 1)); |
| GridLayout gl_buttonBar = new GridLayout(4, true); |
| gl_buttonBar.marginHeight = 0; |
| buttonBar.setLayout(gl_buttonBar); |
| |
| Composite actionComposite = new Composite(buttonBar, SWT.NONE); |
| fActionStack = new StackLayout(); |
| actionComposite.setLayout(fActionStack); |
| actionComposite.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); |
| |
| fSingleActionComposite = new Composite(actionComposite, SWT.NONE); |
| GridLayout gl_SingleActionComposite = new GridLayout(1, false); |
| gl_SingleActionComposite.marginWidth = 0; |
| fSingleActionComposite.setLayout(gl_SingleActionComposite); |
| |
| fSingleActionButton = new Button(fSingleActionComposite, SWT.NONE); |
| fSingleActionButton.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); |
| fSingleActionButton.setText("More..."); |
| fSingleActionButton.addSelectionListener(new SelectionAdapter() { |
| @Override |
| public void widgetSelected(SelectionEvent e) { |
| runTipAction(fCurrentTip.getActions().get(0)); |
| } |
| }); |
| |
| fMultiActionComposite = new Composite(actionComposite, SWT.NONE); |
| GridLayout gl_MultiActionComposite = new GridLayout(2, false); |
| gl_MultiActionComposite.marginWidth = 0; |
| gl_MultiActionComposite.verticalSpacing = 0; |
| gl_MultiActionComposite.horizontalSpacing = 0; |
| fMultiActionComposite.setLayout(gl_MultiActionComposite); |
| |
| fMultiActionButton = new Button(fMultiActionComposite, SWT.NONE); |
| fMultiActionButton.setText("New Button"); |
| fMultiActionButton.addSelectionListener(new SelectionAdapter() { |
| @Override |
| public void widgetSelected(SelectionEvent e) { |
| runTipAction(fCurrentTip.getActions().get(0)); |
| } |
| }); |
| |
| fMultiActionMenuButton = new Button(fMultiActionComposite, SWT.NONE); |
| fMultiActionMenuButton.setImage(ResourceManager.getPluginImage("org.eclipse.tips.ui", "icons/popup_menu.gif")); |
| fMultiActionMenuButton.addSelectionListener(new SelectionAdapter() { |
| @Override |
| public void widgetSelected(SelectionEvent e) { |
| showActionMenu(); |
| } |
| }); |
| |
| fEmptyActionComposite = new Composite(actionComposite, SWT.NONE); |
| fEmptyActionComposite.setLayout(new FillLayout(SWT.HORIZONTAL)); |
| |
| fPreviousTipButton = new Button(buttonBar, SWT.NONE); |
| fPreviousTipButton.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1)); |
| fPreviousTipButton.setText("Previous Tip"); |
| fPreviousTipButton.addSelectionListener(new SelectionAdapter() { |
| |
| @Override |
| public void widgetSelected(SelectionEvent e) { |
| getPreviousTip(); |
| } |
| }); |
| fPreviousTipButton.setEnabled(false); |
| |
| Button btnNextTip = new Button(buttonBar, SWT.NONE); |
| btnNextTip.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1)); |
| btnNextTip.setText("Next Tip"); |
| btnNextTip.addSelectionListener(new SelectionAdapter() { |
| @Override |
| public void widgetSelected(SelectionEvent e) { |
| getNextTip(); |
| } |
| }); |
| |
| Button btnClose = new Button(buttonBar, SWT.NONE); |
| btnClose.addSelectionListener(new SelectionAdapter() { |
| @Override |
| public void widgetSelected(SelectionEvent e) { |
| getParent().dispose(); |
| } |
| }); |
| btnClose.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1)); |
| btnClose.setText("Close"); |
| |
| Label label_1 = new Label(this, SWT.SEPARATOR | SWT.HORIZONTAL); |
| label_1.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1)); |
| |
| fSlider = new Slider(this, SWT.NONE); |
| GridLayout gridLayout = (GridLayout) fSlider.getLayout(); |
| gridLayout.verticalSpacing = 0; |
| gridLayout.marginWidth = 0; |
| gridLayout.horizontalSpacing = 0; |
| fSlider.setLayoutData(new GridData(SWT.FILL, SWT.BOTTOM, false, false, 1, 1)); |
| fContentStack.topControl = fBrowserComposite; |
| fSlider.addTipProviderListener(this); |
| } |
| |
| private void showActionMenu() { |
| Rectangle rect = fMultiActionButton.getBounds(); |
| Point pt = new Point(rect.x - 1, rect.y + rect.height); |
| pt = fMultiActionButton.toDisplay(pt); |
| fActionMenu.setLocation(pt.x, pt.y); |
| fActionMenu.setVisible(true); |
| } |
| |
| private void runTipAction(TipAction tipAction) { |
| Job job = new Job("Running " + tipAction.getTooltip()) { |
| @Override |
| protected IStatus run(IProgressMonitor monitor) { |
| try { |
| tipAction.getRunner().run(); |
| } catch (Exception e) { |
| IStatus status = LogUtil.error(getClass(), e); |
| fTipManager.log(status); |
| return status; |
| } |
| return Status.OK_STATUS; |
| } |
| }; |
| job.setUser(true); |
| job.schedule(); |
| } |
| |
| /** |
| * Sets the selected provider. |
| * |
| * @param provider |
| * the {@link TipProvider} |
| */ |
| public void setProvider(TipProvider provider) { |
| fProvider = provider; |
| fSlider.setTipProvider(provider); |
| getCurrentTip(); |
| } |
| |
| /** |
| * Schedules a TimerTask that is executed after {@value #READ_TIMER} |
| * milliseconds after which the tip is marked as read. |
| */ |
| private void hitTimer() { |
| Tip timerTip = fCurrentTip; |
| Timer timer = new Timer("Tip read timer"); |
| timer.schedule(new TimerTask() { |
| @Override |
| public void run() { |
| if (timerTip == fCurrentTip) { |
| fTipManager.setAsRead(timerTip); |
| fSlider.updateButtons(); |
| } |
| timer.cancel(); |
| } |
| }, READ_TIMER); |
| } |
| |
| private void getPreviousTip() { |
| processTip(fProvider.getPreviousTip()); |
| } |
| |
| private void getNextTip() { |
| if (fProvider.getTips(true).isEmpty() && !fTipManager.getProviders().isEmpty()) { |
| fProvider.getNextTip(); // advance current tip |
| for (TipProvider provider : fTipManager.getProviders()) { |
| if (!provider.getTips(true).isEmpty()) { |
| setProvider(provider); |
| break; |
| } |
| } |
| } |
| processTip(fProvider.getNextTip()); |
| } |
| |
| private void getCurrentTip() { |
| processTip(fProvider.getCurrentTip()); |
| } |
| |
| private void processTip(Tip tip) { |
| fCurrentTip = tip; |
| hitTimer(); |
| enableActionButtons(tip); |
| prepareForHTML(); |
| loadContent(tip); |
| } |
| |
| private void loadContent(Tip tip) { |
| if (tip instanceof ISwtTip) { |
| loadContentSWT(tip); |
| } else if (tip instanceof IHtmlTip) { |
| loadContentHtml((IHtmlTip) tip); |
| } else if (tip instanceof IUrlTip) { |
| loadContentUrl((IUrlTip) tip); |
| } else { |
| fTipManager.log(LogUtil.error(getClass(), "Unknown Tip implementation: " + tip)) ; |
| } |
| fContentComposite.requestLayout(); |
| } |
| |
| private void loadContentHtml(IHtmlTip tip) { |
| try { |
| fBrowser.setText(getScaling() + getHTML(tip).trim()); |
| } catch (IOException e) { |
| fTipManager.log(LogUtil.error(getClass(), e)); |
| } |
| } |
| |
| private void loadContentUrl(IUrlTip tip) { |
| try { |
| fBrowser.setUrl(FileLocator.resolve(tip.getURL()).toString()); |
| } catch (IOException e) { |
| fTipManager.log(LogUtil.error(getClass(), e)); |
| } |
| } |
| |
| private void loadContentSWT(Tip tip) { |
| for (Control control : fSWTComposite.getChildren()) { |
| control.dispose(); |
| } |
| fContentStack.topControl = fSWTComposite; |
| ((ISwtTip) tip).createControl(fSWTComposite); |
| fSWTComposite.requestLayout(); |
| } |
| |
| private void prepareForHTML() { |
| fContentStack.topControl = fBrowserComposite; |
| loadTimeOutScript(); |
| fBrowserComposite.requestLayout(); |
| } |
| |
| /** |
| * Sets content in the browser that displays a message after 500ms if the Tip |
| * could not load fast enough. |
| */ |
| private void loadTimeOutScript() { |
| fBrowser.setText(getScaling() + getLoadingScript(500)); |
| while (!getShell().isDisposed()) { |
| if (!getDisplay().readAndDispatch()) { |
| break; |
| } |
| } |
| } |
| |
| private void enableActionButtons(Tip tip) { |
| disposeActionImages(); |
| if (tip.getActions().isEmpty()) { |
| fActionStack.topControl = fEmptyActionComposite; |
| } else if (tip.getActions().size() == 1) { |
| TipAction action = tip.getActions().get(0); |
| fActionStack.topControl = fSingleActionComposite; |
| fSingleActionButton.setImage(getActionImage(action.getTipImage())); |
| fSingleActionButton.setText(action.getText()); |
| fSingleActionButton.setToolTipText(action.getTooltip()); |
| blinkActionComposite(fSingleActionComposite); |
| } else { |
| TipAction action = tip.getActions().get(0); |
| fActionStack.topControl = fMultiActionComposite; |
| fMultiActionButton.setImage(getActionImage(tip.getActions().get(0).getTipImage())); |
| fMultiActionButton.setText(action.getText()); |
| fMultiActionButton.setToolTipText(action.getTooltip()); |
| loadActionMenu(tip); |
| blinkActionComposite(fMultiActionComposite); |
| } |
| fEmptyActionComposite.getParent().requestLayout(); |
| fNavigationBar.requestLayout(); |
| } |
| |
| private void disposeActionImages() { |
| fActionImages.forEach(img -> img.dispose()); |
| } |
| |
| private void loadActionMenu(Tip pTip) { |
| if (fActionMenu != null) { |
| fActionMenu.dispose(); |
| } |
| fActionMenu = new Menu(fContentComposite.getShell(), SWT.POP_UP); |
| pTip.getActions().subList(1, pTip.getActions().size()).forEach(action -> { |
| MenuItem item = new MenuItem(fActionMenu, SWT.PUSH); |
| item.setText(action.getText()); |
| item.setToolTipText(action.getTooltip()); |
| item.setText(action.getText()); |
| item.setImage(getActionImage(action.getTipImage())); |
| item.addListener(SWT.Selection, e -> runTipAction(action)); |
| }); |
| } |
| |
| private void blinkActionComposite(Composite composite) { |
| Color bg = composite.getParent().getBackground(); |
| Color red = getDisplay().getSystemColor(SWT.COLOR_RED); |
| boolean flip = false; |
| for (int i = 1; i < 6; i++) { |
| boolean flop = flip; |
| if (!composite.isDisposed()) { |
| getDisplay().timerExec(i * 200, () -> { |
| if (!composite.isDisposed()) { |
| composite.getParent().setBackground(flop ? bg : red); |
| } |
| }); |
| } |
| flip = !flip; |
| } |
| } |
| |
| private Image getActionImage(TipImage tipImage) { |
| if (tipImage == null) { |
| return null; |
| } |
| try { |
| Image image = new Image(getDisplay(), ImageUtil.decodeToImage(tipImage.getBase64Image())); |
| if (image != null) { |
| fActionImages.add(image); |
| return image; |
| } |
| } catch (IOException e) { |
| fTipManager.log(LogUtil.error(getClass(), e)); |
| } |
| return null; |
| } |
| |
| /** |
| * Get the timeout script in case the tips takes too to load. |
| * |
| * @param timeout |
| * the timeout in milliseconds |
| * @return the script |
| */ |
| private static String getLoadingScript(int timeout) { |
| return "<style>div{position:fixed;top:50%;left:40%}</style>" + "<div id=\"txt\"></div>" |
| + "<script>var wss=function(){document.getElementById(\"txt\").innerHTML=\"Loading next Tip...\"};window.setTimeout(wss," |
| + timeout + ");</script>"; |
| } |
| |
| private String getHTML(IHtmlTip tip) throws IOException { |
| String encodedImage = encodeImage(tip); |
| return tip.getHTML() + encodedImage; |
| } |
| |
| private static String getScaling() { |
| if (Platform.isRunning() && Platform.getWS().startsWith("gtk")) { |
| int deviceZoom = DPIUtil.getDeviceZoom(); |
| int zoom = deviceZoom; |
| return "<style>" + "body {" + " zoom: " + zoom + "%;" + "}</style> "; |
| } |
| return ""; |
| } |
| |
| private String encodeImage(IHtmlTip tip) throws IOException { |
| TipImage image = tip.getImage(); |
| if (image == null) { |
| return ""; |
| } |
| return encodeImageFromBase64(image); |
| } |
| |
| private String encodeImageFromBase64(TipImage image) throws IOException { |
| int width = fBrowser.getClientArea().width; |
| int height = Math.min(fBrowser.getClientArea().height / 2, (2 * (width / 3))); |
| String attributes = gtkHack(image.getIMGAttributes(width, height).trim()); |
| String encoded = "" // |
| + "<center> <img " // |
| + attributes // |
| + " src=\"" // |
| + image.getBase64Image() // |
| + "\"></center><br/>"; |
| return encoded; |
| } |
| |
| private String gtkHack(String imageAttribute) { |
| if (!Platform.isRunning()) { |
| return imageAttribute; |
| } |
| if (!Platform.getWS().startsWith("gtk")) { |
| return imageAttribute; |
| } |
| Matcher m = fGtkHackPattern.matcher(imageAttribute); |
| if (!m.matches()) { |
| return imageAttribute; |
| } |
| return m.group(1) + (Integer.parseInt(m.group(2)) * 120 / 100) + m.group(3) |
| + (Integer.parseInt(m.group(4)) * 120 / 100) + m.group(5); |
| } |
| |
| @Override |
| protected void checkSubclass() { |
| } |
| |
| /** |
| * @return the {@link Browser} widget |
| */ |
| public Browser getBrowser() { |
| return fBrowser; |
| } |
| |
| /** |
| * @return the {@link Slider} widget |
| */ |
| public Slider getSlider() { |
| return fSlider; |
| } |
| |
| @Override |
| public void selected(TipProvider provider) { |
| setProvider(provider); |
| } |
| |
| /** |
| * Sets the {@link TipManager} |
| * |
| * @param tipManager |
| * the {@link TipManager} that opened the dialog. |
| */ |
| public void setTipManager(TipManager tipManager) { |
| fTipManager = tipManager; |
| |
| getDisplay().syncExec(() -> { |
| fSlider.setTipManager(fTipManager); |
| fShowAtStart.setSelection(fTipManager.isRunAtStartup()); |
| fUnreadOnly.setSelection(!fTipManager.mustServeReadTips()); |
| fPreviousTipButton.setEnabled(fTipManager.mustServeReadTips()); |
| }); |
| } |
| |
| @Override |
| public void dispose() { |
| disposeActionImages(); |
| super.dispose(); |
| } |
| } |