Highlight component panels on hover and selection.
[sonitus.git] / src / main / java / net / pterodactylus / sonitus / gui / PipelinePanel.java
1 /*
2  * Sonitus - PipelinePanel.java - Copyright © 2013 David Roden
3  *
4  * This program is free software: you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
16  */
17
18 package net.pterodactylus.sonitus.gui;
19
20 import static javax.swing.BorderFactory.createCompoundBorder;
21 import static javax.swing.BorderFactory.createEmptyBorder;
22 import static javax.swing.BorderFactory.createEtchedBorder;
23
24 import java.awt.BorderLayout;
25 import java.awt.Color;
26 import java.awt.Component;
27 import java.awt.GridBagConstraints;
28 import java.awt.GridBagLayout;
29 import java.awt.Insets;
30 import java.awt.event.MouseAdapter;
31 import java.awt.event.MouseEvent;
32 import java.util.Collection;
33 import java.util.EventListener;
34 import java.util.logging.Logger;
35 import javax.swing.JComponent;
36 import javax.swing.JLabel;
37 import javax.swing.JPanel;
38 import javax.swing.UIManager;
39 import javax.swing.event.EventListenerList;
40
41 import net.pterodactylus.sonitus.data.ControlledComponent;
42 import net.pterodactylus.sonitus.data.Metadata;
43 import net.pterodactylus.sonitus.data.MetadataListener;
44 import net.pterodactylus.sonitus.data.Pipeline;
45 import net.pterodactylus.sonitus.data.Sink;
46 import net.pterodactylus.sonitus.data.Source;
47
48 /**
49  * {@link JPanel} that displays all components of a {@link Pipeline}.
50  *
51  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
52  */
53 public class PipelinePanel extends JPanel {
54
55         /** The logger. */
56         private static final Logger logger = Logger.getLogger(PipelinePanel.class.getName());
57
58         /** The pipeline being displayed. */
59         private final Pipeline pipeline;
60
61         /** The component hover listeners. */
62         private final EventListenerList componentSelectionListeners = new EventListenerList();
63
64         /** The currently selected component. */
65         private JComponent selectedComponent;
66
67         /**
68          * Creates a new pipeline panel displaying the given pipeline.
69          *
70          * @param pipeline
71          *              The pipeline to display
72          */
73         public PipelinePanel(Pipeline pipeline) {
74                 super(new GridBagLayout());
75                 this.pipeline = pipeline;
76                 updatePanel();
77         }
78
79         //
80         // LISTENER MANAGEMENT
81         //
82
83         /**
84          * Adds the given component selection listener to this panel.
85          *
86          * @param componentSelectionListener
87          *              The component selection listener to add
88          */
89         public void addComponentHoverListener(ComponentSelectionListener componentSelectionListener) {
90                 componentSelectionListeners.add(ComponentSelectionListener.class, componentSelectionListener);
91         }
92
93         //
94         // PRIVATE METHODS
95         //
96
97         /** Update the panel. Needs to be called when the pipeline is changed. */
98         private void updatePanel() {
99                 /* clear everything. */
100                 removeAll();
101
102                 /* count all sinks. */
103                 int sinkCount = 0;
104                 for (ControlledComponent component : pipeline.components()) {
105                         if (!(component instanceof Source)) {
106                                 logger.finest(String.format("%s is not a Source, skipping.", component.name()));
107                                 sinkCount++;
108                                 continue;
109                         }
110                         Collection<Sink> sinks = pipeline.sinks((Source) component);
111                         logger.finest(String.format("%s has %d sinks: %s", component.name(), sinks.size(), sinks));
112                         if (sinks.isEmpty()) {
113                                 sinkCount++;
114                         }
115                 }
116
117                 /* get number of maximum horizontal grid cells. */
118                 int gridCellCount = 1;
119                 for (int n = 2; n <= sinkCount; ++n) {
120                         gridCellCount *= n;
121                 }
122
123                 /* paint all components recursively. */
124                 addControlled(pipeline.source(), 0, 0, gridCellCount, null);
125         }
126
127         /**
128          * Displays the given component.
129          *
130          * @param controlledComponent
131          *              The component to add this panel.
132          * @param level
133          *              The level at which to show the component (the source is level {@code 0})
134          * @param position
135          *              The position at which to display the component
136          * @param width
137          *              The width of the component in grid cells
138          */
139         private void addControlled(final ControlledComponent controlledComponent, int level, int position, int width, ControlledComponent parentComponent) {
140                 /* create a GUI component that displays the component. */
141                 final JPanel componentPanel = createComponentPanel(controlledComponent, parentComponent);
142                 componentPanel.addMouseListener(new MouseAdapter() {
143
144                         @Override
145                         public void mouseClicked(MouseEvent e) {
146                                 for (Component component : getComponents()) {
147                                         component.setBackground(UIManager.getColor("Panel.background"));
148                                 }
149                                 for (ComponentSelectionListener componentSelectionListener : componentSelectionListeners.getListeners(ComponentSelectionListener.class)) {
150                                         componentPanel.setBackground(Color.LIGHT_GRAY);
151                                         componentSelectionListener.componentSelected(controlledComponent);
152                                 }
153                                 selectedComponent = componentPanel;
154                         }
155
156                         @Override
157                         public void mouseEntered(MouseEvent mouseEvent) {
158                                 if (componentPanel != selectedComponent) {
159                                         componentPanel.setBackground(Color.white);
160                                 }
161                         }
162
163                         @Override
164                         public void mouseExited(MouseEvent mouseEvent) {
165                                 if (componentPanel != selectedComponent) {
166                                         componentPanel.setBackground(UIManager.getColor("Panel.background"));
167                                 }
168                         }
169                 });
170
171                 /* show component. */
172                 add(componentPanel, new GridBagConstraints(position, level, width, 1, 1.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
173
174                 /* if the component does not have connected sinks, exit here. */
175                 if (!(controlledComponent instanceof Source)) {
176                         add(new JPanel(), new GridBagConstraints(position, 999, width, 1, 1.0, 1.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
177                         return;
178                 }
179
180                 /* iterate over the component’s sinks. */
181                 Collection<Sink> sinks = pipeline.sinks((Source) controlledComponent);
182                 if (!sinks.isEmpty()) {
183                         int sinkWidth = width / sinks.size();
184                         int sinkIndex = 0;
185                         for (Sink connectedSink : sinks) {
186                                 /* distribute all sinks evenly below this source. */
187                                 addControlled(connectedSink, level + 1, position + sinkIndex * sinkWidth, sinkWidth, controlledComponent);
188                                 sinkIndex++;
189                         }
190                 }
191         }
192
193         /**
194          * Creates a panel displaying a single component.
195          *
196          * @param controlledComponent
197          *              The component to display
198          * @return The created panel
199          */
200         private static JPanel createComponentPanel(final ControlledComponent controlledComponent, final ControlledComponent parentComponent) {
201                 JPanel componentPanel = new JPanel(new BorderLayout(12, 12));
202                 componentPanel.setBorder(createCompoundBorder(createEtchedBorder(), createEmptyBorder(0, 4, 0, 3)));
203                 componentPanel.add(new JLabel(controlledComponent.name()), BorderLayout.WEST);
204                 final JLabel titleLabel = new JLabel(controlledComponent.metadata().fullTitle());
205                 titleLabel.setFont(titleLabel.getFont().deriveFont(titleLabel.getFont().getSize2D() * 0.8f));
206                 componentPanel.add(titleLabel, BorderLayout.EAST);
207                 if (parentComponent != null) {
208                         titleLabel.setVisible(!parentComponent.metadata().fullTitle().equals(controlledComponent.metadata().fullTitle()));
209                 }
210                 controlledComponent.addMetadataListener(new MetadataListener() {
211
212                         @Override
213                         public void metadataUpdated(ControlledComponent component, Metadata metadata) {
214                                 titleLabel.setText(metadata.fullTitle());
215                                 titleLabel.setVisible((parentComponent == null) || !parentComponent.metadata().fullTitle().equals(metadata.fullTitle()));
216                         }
217                 });
218                 return componentPanel;
219         }
220
221         /**
222          * Interface for objects that want to be notified if the user moves the mouse
223          * cursor over a controlled component.
224          *
225          * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
226          */
227         public static interface ComponentSelectionListener extends EventListener {
228
229                 /**
230                  * Notifies the listener that the mouse is now over the given controlled
231                  * component.
232                  *
233                  * @param controlledComponent
234                  *              The controlled component now under the mouse
235                  */
236                 void componentSelected(ControlledComponent controlledComponent);
237
238         }
239
240 }