/* * #! * Ontopia Vizigator * #- * Copyright (C) 2001 - 2013 The Ontopia Project * #- * 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 net.ontopia.topicmaps.viz; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Cursor; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.Point; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.IOException; import java.util.HashMap; import java.util.Iterator; import java.util.List; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JButton; import javax.swing.JCheckBoxMenuItem; import javax.swing.JLabel; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JScrollBar; import javax.swing.JTextField; import javax.swing.KeyStroke; import net.ontopia.topicmaps.core.TopicMapIF; import net.ontopia.utils.OntopiaRuntimeException; import com.touchgraph.graphlayout.TGLensSet; import com.touchgraph.graphlayout.TGPanel; import com.touchgraph.graphlayout.TGPoint2D; import com.touchgraph.graphlayout.interaction.HVScroll; import com.touchgraph.graphlayout.interaction.TGUIManager; import com.touchgraph.graphlayout.interaction.ZoomScroll; /** * EXPERIMENTAL: A panel in which topic map visualization can be shown. */ public class VizPanel extends JPanel { private static final String ITEM_ID_SHOW_SEARCH_BAR = "show.search.bar"; private static final String ITEM_ID_TOPIC_STYLES = "topic.styles"; private static final String ITEM_ID_ASSOCIATION_STYLES = "association.styles"; private static final String ITEM_ID_TOGGLE_CONTROLS = "toggle.controls"; private static final String ITEM_ID_SET_ALL_FIXED = "set.all.nodes.sticky"; private static final String ITEM_ID_SET_ALL_UNFIXED = "set.all.nodes.unsticky"; private static final String ITEM_ID_STOP_MOVING_NODES = "stop.moving.nodes"; private static final String ITEM_ID_ENABLE_MOTION_KILLER = "enable.motion.killer"; private static final String ITEM_ID_ENABLE_ANIMATION = "enable.animation"; private static final String ITEM_ID_ASSOCIATION_SCOPING = "association.scoping"; private static final String ITEM_FOCUS_NEXT = "redo"; private static final String ITEM_FOCUS_PREVIOUS = "undo"; private static final String ITEM_ID_ENABLE_NEIGH_CIRC = "enable.neighbour.circle"; public JPopupMenu glPopup; private HVScroll hvScroll; private ZoomScroll zoomScroll; private TGPanel tgPanel; private TGLensSet tgLensSet; private TGUIManager tgUIManager; private VizController controller; private OSpinner locSpinner; private JTextField searchTextField; private BlinkingThread blinkThread; private JButton clearButton; private JPanel searchPanel; private boolean searchPanelVisible = true; private boolean controlsVisible = true; private JCheckBoxMenuItem searchMenuItem; private JMenuItem topicStylesMenuItem; private JMenuItem associationStylesMenuItem; private JPanel topPanel; // Used for filtering out associations whose scope doesn't match a filter. private AssociationScopeFilterMenu associationScopeFilterMenu; private ParsedMenuFile enabledItemIds; private JMenuItem enableMotionKillerMenuItem; private String startMotionKiller; private String stopMotionKiller; private JMenuItem redoMenuItem; private JMenuItem undoMenuItem; private TypesConfigFrame topicFrame; private TypesConfigFrame assocFrame; private VizFrontEndIF vizFrontEnd; public VizPanel(VizFrontEndIF vizFrontEnd) throws IOException { this.vizFrontEnd = vizFrontEnd; this.controlsVisible = vizFrontEnd.getDefaultControlsVisible(); String wallpapSrc = vizFrontEnd.getWallpaper(); if(wallpapSrc != null) { tgPanel = new ExtendedTGPanel(wallpapSrc); } else { tgPanel = new TGPanel(); } init(); controller = new VizController(this, this.vizFrontEnd, tgPanel); enabledItemIds = controller.getEnabledItemIds(); buildPanel(); if (vizFrontEnd.mapPreLoaded()) { setLocality(controller.getDefaultLocality()); locSpinner.setMax(controller.getMaxLocality()); } addUIs(); if (vizFrontEnd.mapPreLoaded()) { controller.initializeMotionKillerEnabled(); controller.undoManager.reset(); } } public void init() { tgLensSet = new TGLensSet(); hvScroll = new HVScroll(tgPanel, tgLensSet); zoomScroll = new ZoomScroll(tgPanel); tgPanel.setBackColor(VizTopicMapConfigurationManager .DEFAULT_PANEL_BACKGROUND_COLOUR); buildLens(); tgPanel.setLensSet(tgLensSet); setVisible(true); } /** * Returns the controller for this panel. */ public VizController getController() { return controller; } /** * Return the TGPanel used with this GLPanel. */ public TGPanel getTGPanel() { return tgPanel; } // navigation ................. /** * Return the HVScroll used with this GLPanel. */ public HVScroll getHVScroll() { return hvScroll; } /** * Sets the horizontal offset to p.x, and the vertical offset to p.y * given a Point <tt>p<tt>. */ public void setOffset(Point p) { hvScroll.setOffset(p); } /** * Return the horizontal and vertical offset position as a Point. */ public Point getOffset() { return hvScroll.getOffset(); } // zoom ....................... /** * Return the ZoomScroll used with this GLPanel. */ public ZoomScroll getZoomScroll() { return zoomScroll; } /** * Set the zoom value of this GLPanel (allowable values between -100 to 100). */ public void setZoomValue(int zoomValue) { zoomScroll.setZoomValue(zoomValue); } /** Return the zoom value of this GLPanel. */ public int getZoomValue() { return zoomScroll.getZoomValue(); } public void buildLens() { tgLensSet.addLens(hvScroll.getLens()); tgLensSet.addLens(zoomScroll.getLens()); tgLensSet.addLens(tgPanel.getAdjustOriginLens()); } /** * Construct display panel, with H/V scroll bars, locality stepper * and zoom scrollbar. */ public void buildPanel() { final JScrollBar horizontalSB = hvScroll.getHorizontalSB(); final JScrollBar verticalSB = hvScroll.getVerticalSB(); final JScrollBar zoomSB = zoomScroll.getZoomSB(); zoomSB.setToolTipText(Messages.getString("Viz.ZoomBarHoverHelp")); // set the initial zoom value to 2/3 of the total value zoomSB.setValue(zoomSB.getMinimum() + (((zoomSB.getMaximum() - zoomSB.getMinimum()) / 3) * 2)); setLayout(new BorderLayout()); JPanel scrollPanel = new JPanel(); scrollPanel.setLayout(new GridBagLayout()); GridBagConstraints c = new GridBagConstraints(); JPanel modeSelectPanel = new JPanel(); modeSelectPanel.setLayout(new FlowLayout(FlowLayout.CENTER, 0, 0)); JPanel locPanel = new JPanel(); locPanel.setLayout(new GridBagLayout()); c.gridy = 0; c.fill = GridBagConstraints.HORIZONTAL; c.gridx = 0; c.weightx = 0; c.insets = new Insets(0, 10, 0, 10); locPanel.add(modeSelectPanel, c); c.insets = new Insets(0, 0, 0, 0); c.gridx = 1; c.weightx = 1; JPanel spinnerPanel = buildSpinnerPanel(); locPanel.add(spinnerPanel, c); if (shouldShowSearchPanel()) this.buildSearchPanel(); topPanel = new JPanel(new BorderLayout()); if (shouldShowSearchPanel()) topPanel.add(searchPanel, BorderLayout.NORTH); topPanel.add(locPanel, BorderLayout.SOUTH); add(topPanel, BorderLayout.NORTH); c.fill = GridBagConstraints.BOTH; c.gridwidth = 1; c.gridx = 0; c.gridy = 1; c.weightx = 1; c.weighty = 1; scrollPanel.add(tgPanel, c); c.gridx = 1; c.gridy = 1; c.weightx = 0; c.weighty = 0; scrollPanel.add(verticalSB, c); c.gridx = 0; c.gridy = 2; scrollPanel.add(horizontalSB, c); add(scrollPanel, BorderLayout.CENTER); glPopup = new JPopupMenu(); JMenuItem menuItem = new JMenuItem(Messages .getString("Viz.PopupToggleControls")); menuItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { controlsVisible = !controlsVisible; horizontalSB.setVisible(controlsVisible); verticalSB.setVisible(controlsVisible); topPanel.setVisible(controlsVisible); } }); add(glPopup, menuItem, ITEM_ID_TOGGLE_CONTROLS); // Create association scope filter menu createAssociationScopeFilterMenu(); // Create topic styles menu item createTopicStylesMenuItem(); // Create topic styles menu item createAssociationStylesMenuItem(); // Create search menu items. createSearchMenuItems(); // All-sticky implementation createStickyMenuItems(); // Create menu item that stops moving nodes. createStopMovingNodesMenuItem(); // Create menu item for turning motion reduction on/off. createMotionReductionMenuItem(); // Create menu items for undoing and redoing actions. if (UndoManager.ENABLE_UNDO_MANAGER) createUndoRedoMenuItems(); // If enabled, create neighbouring circle menu items. if (VizDebugUtils.isNeighbouringCircleEnabled()) createDisEnableNeighCircMenuItem(); // Create menu item for turing animation on/off. if (VizDebugUtils.isAnimatorEnabled()) addAnimationMenuItem(); horizontalSB.setVisible(controlsVisible); verticalSB.setVisible(controlsVisible); topPanel.setVisible(controlsVisible); } /** * Updates the text of the motion killer menu item. */ public void updateEnableMotionKillerMenuItem() { boolean enable = controller.isMotionKillerEnabled(); if (enableMotionKillerMenuItem != null) enableMotionKillerMenuItem.setText(enable ? stopMotionKiller : startMotionKiller); } /** * Enables/disables the motion killer menu item. * @param enabled true iff the motion killer menu item should be enabled. */ public void enableDisableMotionKillerMenuItem(boolean enabled) { enableMotionKillerMenuItem.setEnabled(enabled); } protected void addAnimationMenuItem() { // FIXME: IF ever releasing this, remember to use message files. final String startAnimation = "[Start Animation]"; final String stopAnimation = "[Stop Animation]"; final JMenuItem enableAnimationMenuItem = new JMenuItem(VizigatorUser.INITIALLY_ENABLED ? stopAnimation : startAnimation); enableAnimationMenuItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { boolean enable = !controller.isAnimationEnabled(); controller.enableAnimation(enable); enableAnimationMenuItem.setText(enable ? stopAnimation : startAnimation); } }); add(glPopup, enableAnimationMenuItem, ITEM_ID_ENABLE_ANIMATION); } /** * Creates four menu items to make/force all nodes (un)sticky. */ protected void createStickyMenuItems() { JMenuItem setAllFixedMenuItem = new JMenuItem( Messages.getString("Viz.SetAllNodesSticky")); setAllFixedMenuItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { controller.setAllNodesFixed(true); } }); add(glPopup, setAllFixedMenuItem, ITEM_ID_SET_ALL_FIXED); JMenuItem setAllUnfixedMenuItem = new JMenuItem( Messages.getString("Viz.SetAllNodesUnsticky")); setAllUnfixedMenuItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { controller.setAllNodesFixed(false); } }); add(glPopup, setAllUnfixedMenuItem, ITEM_ID_SET_ALL_UNFIXED); } /** * Creates search menu items. */ protected void createSearchMenuItems() { searchMenuItem = new JCheckBoxMenuItem(Messages .getString("Viz.ShowSearchBar"), searchPanelVisible); searchMenuItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent action) { switchSearchPanel(); searchMenuItem.setSelected(searchPanelVisible); } }); add(glPopup, searchMenuItem, ITEM_ID_SHOW_SEARCH_BAR); } /** * Creates topic styles menu items. */ protected void createTopicStylesMenuItem() { topicStylesMenuItem = new JMenuItem(Messages .getString("Viz.TopicTypeConfiguration")); topicStylesMenuItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent action) { menuOpenTopicConfig(); } }); add(glPopup, topicStylesMenuItem, ITEM_ID_TOPIC_STYLES); } /** * Creates association styles menu items. */ protected void createAssociationStylesMenuItem() { associationStylesMenuItem = new JMenuItem(Messages .getString("Viz.AssociationTypeConfiguration")); associationStylesMenuItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent action) { menuOpenAssociationConfig(); } }); add(glPopup, associationStylesMenuItem, ITEM_ID_ASSOCIATION_STYLES); } /** * Creates association scope filter menu. */ protected void createAssociationScopeFilterMenu() { associationScopeFilterMenu = new AssociationScopeFilterMenu(Messages .getString("Viz.AssociationScopingMenuTitle")); add(glPopup, associationScopeFilterMenu, ITEM_ID_ASSOCIATION_SCOPING); } /** * Creates four menu items to make/force all nodes (un)sticky. */ protected void createStopMovingNodesMenuItem() { JMenuItem stopMovingNodesMenuItem = new JMenuItem( Messages.getString("Viz.StopMovingNodes")); stopMovingNodesMenuItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { controller.stopMovingNodes(); } }); add(glPopup, stopMovingNodesMenuItem, ITEM_ID_STOP_MOVING_NODES); } public void setUndoEnabled(boolean enabled) { if (undoMenuItem != null) undoMenuItem.setEnabled(enabled); } public void setRedoEnabled(boolean enabled) { if (undoMenuItem != null) redoMenuItem.setEnabled(enabled); } /** * Creates menu items for retrieving the previous/next focus node. */ protected void createUndoRedoMenuItems() { undoMenuItem = new JMenuItem(Messages .getString("Viz.Undo")); undoMenuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Z, KeyInputManager.KEY_MASK)); undoMenuItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { controller.undo(); } }); add(glPopup, undoMenuItem, ITEM_FOCUS_PREVIOUS); redoMenuItem = new JMenuItem(Messages .getString("Viz.Redo")); redoMenuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Y, KeyInputManager.KEY_MASK)); redoMenuItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { controller.redo(); } }); add(glPopup, redoMenuItem, ITEM_FOCUS_NEXT); } protected void createMotionReductionMenuItem() { startMotionKiller = Messages .getString("Viz.StartMotionKiller"); stopMotionKiller = Messages.getString("Viz.StopMotionKiller"); enableMotionKillerMenuItem = new JMenuItem(startMotionKiller); enableMotionKillerMenuItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { boolean enable = !controller.isMotionKillerEnabled(); controller.enableMotionKiller(enable); updateEnableMotionKillerMenuItem(); } }); add(glPopup, enableMotionKillerMenuItem, ITEM_ID_ENABLE_MOTION_KILLER); } /** * Creates a menu item for hiding showing the neighbouring circle. */ protected void createDisEnableNeighCircMenuItem() { final String enableNeighbCirc = Messages .getString("Viz.EnableNeighbouringCircle"); final String disableNeighbCirc = Messages .getString("Viz.DisableNeighbouringCircle"); final JMenuItem disEnableNeighCircMenuItem = new JMenuItem(controller.showNeighbouringCircle ? disableNeighbCirc : enableNeighbCirc); disEnableNeighCircMenuItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { if (disEnableNeighCircMenuItem.getText().equals(enableNeighbCirc)) { disEnableNeighCircMenuItem.setText(disableNeighbCirc); controller.showNeighbouringCircle = true; controller.showNeighboursOnMouseover = true; } else { disEnableNeighCircMenuItem.setText(enableNeighbCirc); controller.showNeighbouringCircle = false; controller.showNeighboursOnMouseover = false; } } }); add(glPopup, disEnableNeighCircMenuItem, ITEM_ID_ENABLE_NEIGH_CIRC); } protected void switchSearchPanel() { if (searchPanelVisible) topPanel.remove(searchPanel); else topPanel.add(searchPanel, BorderLayout.NORTH); searchPanelVisible = !searchPanelVisible; this.revalidate(); this.setSearchFocus(); } private boolean shouldShowSearchPanel() { return enabled("search.bar"); } private JPanel buildSearchPanel() { searchPanel = new JPanel(); searchPanel.setLayout(new BoxLayout(searchPanel, BoxLayout.X_AXIS)); searchPanel.add(Box.createHorizontalStrut(10)); searchPanel .add(new JLabel(Messages.getString("Viz.EnterSearchExpression"))); searchPanel.add(Box.createHorizontalStrut(10)); searchTextField = new JTextField(); searchTextField.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent event) { performSearch(); } }); searchPanel.add(searchTextField); JButton searchButton = new JButton(Messages .getString("Viz.SearchButtonText")); searchButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent event) { performSearch(); } }); searchPanel.add(searchButton); clearButton = new JButton(Messages.getString("Viz.ClearButtonText")); clearButton.setEnabled(false); clearButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent event) { clearSearch(); } }); searchPanel.add(clearButton); return searchPanel; } /** * Clears the search results, the search string and sets the input * focus in the search field. */ public void clearSearch() { this.clearSearchResults(); searchTextField.setText(""); this.setSearchFocus(); } protected void performSearch() { this.clearSearchResults(); String searchString = searchTextField.getText().trim(); if (searchString.length() == 0) return; List results; this.setCursor(new Cursor(Cursor.WAIT_CURSOR)); try { results = controller.performSearch(searchString); } finally { this.setCursor(new Cursor(Cursor.DEFAULT_CURSOR)); } if (results.size() > 0) this.showSearchResults(results); else ErrorDialog.showError(this, Messages .getString("Viz.NoSearchResultsFound")); this.setSearchFocus(); clearButton.setEnabled(true); } private void showSearchResults(List results) { // Start them blinking blinkThread = new BlinkingThread(results); // Find the node with the max number of edges. // We assume this is the most important node. TMTopicNode maxElement = (TMTopicNode) results.get(0); for (int i = 1; i < results.size(); i++) { TMTopicNode element = (TMTopicNode) results.get(i); if (element.edgeCount() > maxElement.edgeCount()) maxElement = element; } // Center !!! this node. This code is not correct, but it is close // and I don't have any more time ! Point firstLocation = (maxElement).getLocation(); TGPoint2D target = new TGPoint2D(firstLocation.getX(), firstLocation.getY()); Point fromLocation = hvScroll.getOffset(); TGPoint2D from = new TGPoint2D(fromLocation.getX(), fromLocation.getY()); hvScroll.scrollAtoB(target, from); tgPanel.repaintAfterMove(); } /** * Constructs top level panel consisting of the LocalitySpinner and * the ZoomScroll Bar. */ protected JPanel buildSpinnerPanel() { locSpinner = new OSpinner(); locSpinner.setPreferredSize(new Dimension(40, 20)); locSpinner.addPropertyChangeListener("value", new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent evt) { controller.setLocality(((Integer) evt.getNewValue()).intValue()); } }); final JPanel sbp = new JPanel(new GridBagLayout()); GridBagConstraints c = new GridBagConstraints(); c.gridx = 0; c.gridy = 0; c.weightx = 0; sbp.add(locSpinner, c); c.gridx = 1; c.gridy = 0; c.weightx = 1; c.insets = new Insets(0, 10, 0, 17); c.fill = GridBagConstraints.HORIZONTAL; zoomScroll.getZoomSB().setVisible(true); zoomScroll.getZoomSB().setMinimumSize(new Dimension(200, 17)); sbp.add(zoomScroll.getZoomSB(), c); return sbp; } /** * Add and activate the Touch Graph 'Navigate GUI Manager' to the TG display. */ private void addUIs() { tgUIManager = new TGUIManager(); NavigateUI navigateUI = new NavigateUI(this, controller); tgUIManager.addUI(navigateUI, "Navigate"); tgUIManager.activate("Navigate"); } public void clearSearchResults() { if (blinkThread != null) { blinkThread.stopBlinking(); blinkThread = null; clearButton.setEnabled(false); } } public void setSearchFocus() { searchTextField.setSelectionStart(0); searchTextField.setSelectionEnd(searchTextField.getText().length()); searchTextField.requestFocus(); } public int getLocality() { return locSpinner.getValue(); } public void setLocality(int value) { locSpinner.setValue(value); } public AssociationScopeFilterMenu getAssociationScopeFilterMenu() { return associationScopeFilterMenu; } public void configureDynamicMenus(ActionListener parentListener) { TopicMapIF topicMap = controller.getTopicMap(); controller.configure(associationScopeFilterMenu, topicMap, parentListener); } public TypesConfigFrame getTopicFrame() { return topicFrame; } public TypesConfigFrame getAssocFrame() { return assocFrame; } protected boolean enabled(String itemId) { VizDebugUtils.debug("VizPanel.enabled(" + itemId + ") - " + enabledItemIds); return enabledItemIds.enabled(itemId); } public void add(JPopupMenu menu, JMenuItem item, String itemId) { if (enabled(itemId)) menu.add(item); } private void menuOpenAssociationConfig() { if (!controller.hasTopicMap()) return; if (assocFrame == null) { assocFrame = vizFrontEnd.getTypesConfigFrame(controller, false); assocFrame.show(); } else { assocFrame.setVisible(true); assocFrame.toFront(); } } private void menuOpenTopicConfig() { if (!controller.hasTopicMap()) return; if (topicFrame == null) { topicFrame = vizFrontEnd.getTypesConfigFrame(controller, true); topicFrame.show(); } else { topicFrame.setVisible(true); topicFrame.toFront(); } } // --- BlinkingThread private class BlinkingThread extends Thread { private boolean blinking = true; private boolean blink = true; private HashMap blinkMap; private final List results; private BlinkingThread(List list) { super("Blinking Thread"); this.results = list; this.initializeBlinkMap(); start(); } public void stopBlinking() { blinking = false; blink = false; paintResults(); } public void run() { while (blinking) { try { Thread.sleep(200); } catch (InterruptedException e) { // Do nothing } paintResults(); blink = !blink; } } private void paintResults() { for (Iterator iter = results.iterator(); iter.hasNext();) { TMTopicNode element = (TMTopicNode) iter.next(); if (element.isVisible()) { if (blink) element.repaint((Color) blinkMap.get(element .getPaintBackColor(tgPanel)), tgPanel, true); else element.repaint(element.getPaintBackColor(tgPanel), tgPanel, true); } } } private void initializeBlinkMap() { blinkMap = new HashMap(results.size()); for (Iterator iter = results.iterator(); iter.hasNext();) { Color colour = ((TMTopicNode) iter.next()).getPaintBackColor(tgPanel); blinkMap.put(colour, this.blinkColourFor(colour)); } } private Color blinkColourFor(Color aColour) { return new Color(0xFF - aColour.getRed(), 0xFF - aColour.getGreen(), 0xFF - aColour.getBlue()); } } }