/*! ****************************************************************************** * * Pentaho Data Integration * * Copyright (C) 2002-2016 by Pentaho : http://www.pentaho.com * ******************************************************************************* * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ******************************************************************************/ package org.pentaho.di.ui.spoon; import org.eclipse.swt.SWT; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Menu; import org.eclipse.swt.widgets.MenuItem; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.ToolBar; import org.eclipse.swt.widgets.ToolItem; import org.pentaho.di.core.exception.KettleException; import org.pentaho.di.core.logging.LogChannel; import org.pentaho.di.core.logging.LogChannelInterface; import org.pentaho.di.i18n.BaseMessages; import org.pentaho.di.i18n.LanguageChoice; import org.pentaho.di.ui.core.gui.GUIResource; import org.pentaho.ui.xul.XulComponent; import org.pentaho.ui.xul.XulDomContainer; import org.pentaho.ui.xul.XulException; import org.pentaho.ui.xul.XulOverlay; import org.pentaho.ui.xul.containers.XulDeck; import org.pentaho.ui.xul.containers.XulToolbar; import org.pentaho.ui.xul.containers.XulVbox; import org.pentaho.ui.xul.dom.Document; import org.pentaho.ui.xul.impl.XulEventHandler; import org.pentaho.ui.xul.swt.tags.SwtDeck; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.MissingResourceException; import java.util.ResourceBundle; /** * Singleton Object controlling SpoonPerspectives. * * A Perspective is an optional Spoon mode that can be added by a SpoonPlugin. Perspectives take over the look of the * entire application by replacing the main UI area. * * @author nbaker * */ public class SpoonPerspectiveManager { private static Class<?> PKG = SpoonPerspectiveManager.class; private static SpoonPerspectiveManager instance = new SpoonPerspectiveManager(); private final Map<Class<? extends SpoonPerspective>, SpoonPerspective> perspectives; private final Map<SpoonPerspective, PerspectiveManager> perspectiveManagerMap; private final LinkedHashSet<SpoonPerspective> orderedPerspectives; private XulDeck deck; private SpoonPerspective activePerspective; private XulDomContainer domContainer; private boolean forcePerspective = false; private String startupPerspective = null; private final LogChannelInterface log = new LogChannel( this ); private String[] defaultDisabled = new String[] { "schedulerPerspective" }; public String getStartupPerspective() { return startupPerspective; } public void setStartupPerspective( String startupPerspective ) { this.startupPerspective = startupPerspective; } Map<SpoonPerspective, PerspectiveManager> getPerspectiveManagerMap() { return Collections.unmodifiableMap( perspectiveManagerMap ); } protected static class SpoonPerspectiveComparator implements Comparator<SpoonPerspective> { public int compare( SpoonPerspective o1, SpoonPerspective o2 ) { return o1.getId().compareTo( o2.getId() ); } } static class PerspectiveManager { private final SpoonPerspective per; private final XulVbox box; private final XulToolbar mainToolbar; private final List<PerspectiveData> perspectiveList; private final String name; private boolean initialized; public PerspectiveManager( SpoonPerspective per, XulVbox box, XulToolbar mainToolbar, List<PerspectiveData> perspectiveList, String name ) { super(); this.per = per; this.box = box; this.mainToolbar = mainToolbar; this.perspectiveList = perspectiveList; this.name = name; initialized = false; } public void initializeIfNeeded() { if ( !initialized ) { performInit(); initialized = true; } } void performInit() { per.getUI().setParent( (Composite) box.getManagedObject() ); per.getUI().layout(); ( (Composite) mainToolbar.getManagedObject() ).layout( true, true ); } /** * Sets hidden to true for {@code perspectiveName} from {@code perspectiveList} */ void setPerspectiveHidden( final String perspectiveName, boolean hidden ) { for ( PerspectiveData perspectiveData : perspectiveList ) { if ( perspectiveData.getName().equals( perspectiveName ) ) { perspectiveData.setHidden( hidden ); } } } } @SuppressWarnings( "rawtypes" ) private SpoonPerspectiveManager() { perspectives = new LinkedHashMap<Class<? extends SpoonPerspective>, SpoonPerspective>(); perspectiveManagerMap = new HashMap<SpoonPerspective, PerspectiveManager>(); orderedPerspectives = new LinkedHashSet<SpoonPerspective>(); } /** * Returns the single instance of this class. * * @return SpoonPerspectiveManager instance. */ public static SpoonPerspectiveManager getInstance() { return instance; } /** * Sets the deck used by the Perspective Manager to display Perspectives in. * * @param deck */ public void setDeck( XulDeck deck ) { this.deck = deck; } /** * Receives the main XUL document comprising the menuing system and main layout of Spoon. Perspectives are able to * modify these areas when activated. Any other areas need to be modified via a SpoonPlugin. * * @param doc */ public void setXulDoc( XulDomContainer doc ) { this.domContainer = doc; } /** * Adds a SpoonPerspective making it available to be activated later. * * @param perspective */ public void addPerspective( SpoonPerspective perspective ) { if ( activePerspective == null ) { activePerspective = perspective; } perspectives.put( perspective.getClass(), perspective ); orderedPerspectives.add( perspective ); if ( domContainer != null ) { initialize(); } } /** * Changes perspective visibility due to {@code hidePerspective} value. * If perspective exists already, and we want to make it visible, no new perspective will be added. * */ private void changePerspectiveVisibility( final String perspectiveId, boolean hidePerspective ) { PerspectiveManager perspectiveManager; for ( SpoonPerspective sp : getPerspectiveManagerMap().keySet() ) { if ( sp.getId().equals( perspectiveId ) ) { perspectiveManager = getPerspectiveManagerMap().get( sp ); perspectiveManager.setPerspectiveHidden( sp.getDisplayName( Locale.getDefault() ), hidePerspective ); return; } } getLogger().logError( "Perspective with " + perspectiveId + " is not found." ); } /** * Shows perspective with {@code perspectiveId} if it is not shown yet. */ public void showPerspective( final String perspectiveId ) { changePerspectiveVisibility( perspectiveId, false ); } /** * Hides perspective with {@code perspectiveId}. */ public void hidePerspective( final String perspectiveId ) { changePerspectiveVisibility( perspectiveId, true ); } /** * Returns an unmodifiable List of perspectives in no set order. * * @return */ @SuppressWarnings( "unchecked" ) public List<SpoonPerspective> getPerspectives() { return Collections.unmodifiableList( new ArrayList<SpoonPerspective>( orderedPerspectives ) ); } private void unloadPerspective( SpoonPerspective per ) { per.setActive( false ); List<XulOverlay> overlays = per.getOverlays(); if ( overlays != null ) { for ( XulOverlay overlay : overlays ) { try { domContainer.removeOverlay( overlay.getOverlayUri() ); } catch ( XulException e ) { log.logError( "Error unload perspective", e ); } } } getSpoon().enableMenus(); } /** * Activates the given instance of the class literal passed in. Activating a perspective first deactivates the current * perspective removing any overlays its applied to the UI. It then switches the main deck to display the perspective * UI and applies the optional overlays to the main Spoon XUL container. * * @param clazz * SpoonPerspective class literal * @throws KettleException * throws a KettleException if no perspective is found for the given parameter */ public void activatePerspective( Class<? extends SpoonPerspective> clazz ) throws KettleException { if ( this.forcePerspective ) { // we are currently prevented from switching perspectives return; } SpoonPerspective sp = perspectives.get( clazz ); if ( sp == null ) { throw new KettleException( "Could not locate perspective by class: " + clazz ); } PerspectiveManager perspectiveManager = getPerspectiveManagerMap().get( sp ); if ( perspectiveManager != null ) { perspectiveManager.initializeIfNeeded(); } unloadPerspective( activePerspective ); activePerspective = sp; List<XulOverlay> overlays = sp.getOverlays(); if ( overlays != null ) { for ( XulOverlay overlay : overlays ) { try { ResourceBundle res = null; if ( overlay.getResourceBundleUri() != null ) { try { res = ResourceBundle.getBundle( overlay.getResourceBundleUri() ); } catch ( MissingResourceException ignored ) { // Ignore errors } } else { try { res = ResourceBundle.getBundle( overlay.getOverlayUri().replace( ".xul", ".properties" ) ); } catch ( MissingResourceException ignored ) { // Ignore errors } } if ( res == null ) { res = new XulSpoonResourceBundle( sp.getClass() ); } domContainer.loadOverlay( overlay.getOverlayUri(), res ); } catch ( XulException e ) { log.logError( "Error activate perspective", e ); } } } List<XulEventHandler> theXulEventHandlers = sp.getEventHandlers(); if ( theXulEventHandlers != null ) { for ( XulEventHandler handler : theXulEventHandlers ) { domContainer.addEventHandler( handler ); } } sp.setActive( true ); deck.setSelectedIndex( deck.getChildNodes().indexOf( deck.getElementById( "perspective-" + sp.getId() ) ) ); getSpoon().enableMenus(); } /** * Returns the current active perspective. * * @return active SpoonPerspective */ public SpoonPerspective getActivePerspective() { return activePerspective; } /** * Returns whether this perspective manager is prevented from switching perspectives */ public boolean isForcePerspective() { return forcePerspective; } /** * Sets whether this perspective manager is prevented from switching perspectives. This is used when a startup * perspective is requested on the command line parameter to prevent other perpsectives from openeing. */ public void setForcePerspective( boolean forcePerspective ) { this.forcePerspective = forcePerspective; } public void removePerspective( SpoonPerspective per ) { perspectives.remove( per ); orderedPerspectives.remove( per ); Document document = domContainer.getDocumentRoot(); XulComponent comp = document.getElementById( "perspective-" + per.getId() ); comp.getParent().removeChild( comp ); comp = document.getElementById( "perspective-btn-" + per.getId() ); comp.getParent().removeChild( comp ); XulToolbar mainToolbar = (XulToolbar) domContainer.getDocumentRoot().getElementById( "main-toolbar" ); ( (Composite) mainToolbar.getManagedObject() ).layout( true, true ); deck.setSelectedIndex( 0 ); } private List<SpoonPerspective> installedPerspectives = new ArrayList<SpoonPerspective>(); public void initialize() { XulToolbar mainToolbar = (XulToolbar) domContainer.getDocumentRoot().getElementById( "main-toolbar" ); SwtDeck deck = (SwtDeck) domContainer.getDocumentRoot().getElementById( "canvas-deck" ); int y = 0; int perspectiveIdx = 0; Class<? extends SpoonPerspective> perClass = null; List<SpoonPerspective> perspectives = getPerspectives(); if ( this.startupPerspective != null ) { for ( int i = 0; i < perspectives.size(); i++ ) { if ( perspectives.get( i ).getId().equals( this.startupPerspective ) ) { perspectiveIdx = i; break; } } } final List<PerspectiveData> perspectiveList = new ArrayList<>(); final ToolBar swtToolbar = (ToolBar) mainToolbar.getManagedObject(); final Shell shell = swtToolbar.getShell(); final ToolItem perspectiveButton = new ToolItem( swtToolbar, SWT.DROP_DOWN, 7 ); perspectiveButton.setImage( GUIResource.getInstance().getImage( "ui/images/perspective_changer.svg" ) ); perspectiveButton.setToolTipText( BaseMessages.getString( PKG, "Spoon.Menu.View.Perspectives" ) ); perspectiveButton.addSelectionListener( new SelectionAdapter() { @Override public void widgetSelected( SelectionEvent e ) { Menu menu = new Menu( shell ); for ( final PerspectiveData perspectiveData : perspectiveList ) { MenuItem item = new MenuItem( menu, SWT.CHECK ); if ( perspectiveData.isHidden() ) { item.setEnabled( false ); } if ( activePerspective.getId().equals( perspectiveData.getId() ) ) { item.setSelection( true ); } item.setText( perspectiveData.getName() ); item.addSelectionListener( new SelectionAdapter() { @Override public void widgetSelected( SelectionEvent selectionEvent ) { Spoon.getInstance().loadPerspective( perspectiveData.getId() ); swtToolbar.forceFocus(); } } ); } ToolItem item = (ToolItem) e.widget; Rectangle rect = item.getBounds(); Point pt = item.getParent().toDisplay( new Point( rect.x, rect.y + rect.height ) ); menu.setLocation( pt.x, pt.y ); menu.setVisible( true ); } } ); for ( final SpoonPerspective per : getPerspectives() ) { if ( installedPerspectives.contains( per ) ) { y++; continue; } String name = per.getDisplayName( LanguageChoice.getInstance().getDefaultLocale() ); PerspectiveData perspectiveData = new PerspectiveData( name, per.getId() ); if ( Arrays.asList( defaultDisabled ).contains( per.getId() ) ) { perspectiveData.setHidden( true ); } perspectiveList.add( perspectiveData ); XulVbox box = deck.createVBoxCard(); box.setId( "perspective-" + per.getId() ); box.setFlex( 1 ); deck.addChild( box ); PerspectiveManager perspectiveManager = new PerspectiveManager( per, box, mainToolbar, perspectiveList, name ); perspectiveManagerMap.put( per, perspectiveManager ); // Need to force init for main perspective even if it won't be shown if ( perspectiveIdx == y || y == 0 ) { if ( perspectiveIdx == y ) { // we have a startup perspective. Hold onto the class perClass = per.getClass(); } // force init perspectiveManager.initializeIfNeeded(); } y++; installedPerspectives.add( per ); } deck.setSelectedIndex( perspectiveIdx ); if ( perClass != null ) { // activate the startup perspective try { activatePerspective( perClass ); // stop other perspectives from opening SpoonPerspectiveManager.getInstance().setForcePerspective( true ); } catch ( KettleException e ) { // TODO Auto-generated catch block } } } static class PerspectiveData { private String name; private String id; private boolean hidden = false; public PerspectiveData( String name, String id ) { this.name = name; this.id = id; } public String getName() { return name; } public String getId() { return id; } public boolean isHidden() { return hidden; } public void setHidden( boolean hidden ) { this.hidden = hidden; } } /** * For testing */ Spoon getSpoon() { return Spoon.getInstance(); } /** * For testing */ LogChannelInterface getLogger() { return log; } }