Fixed mouse hovering detection
[sixth-3d.git] / src / main / java / eu / svjatoslav / sixth / e3d / gui / ViewPanel.java
1 /*
2  * Sixth 3D engine. Copyright ©2012-2018, Svjatoslav Agejenko, svjatoslav@svjatoslav.eu
3  *
4  * This program is free software; you can redistribute it and/or
5  * modify it under the terms of version 3 of the GNU Lesser General Public License
6  * or later as published by the Free Software Foundation.
7  *
8  */
9
10 package eu.svjatoslav.sixth.e3d.gui;
11
12 import eu.svjatoslav.sixth.e3d.gui.humaninput.HIDInputTracker;
13 import eu.svjatoslav.sixth.e3d.gui.humaninput.KeyboardFocusStack;
14 import eu.svjatoslav.sixth.e3d.renderer.raster.ShapeCollection;
15
16 import javax.swing.*;
17 import java.awt.*;
18 import java.awt.event.ComponentEvent;
19 import java.awt.event.ComponentListener;
20 import java.util.Set;
21 import java.util.Timer;
22 import java.util.concurrent.ConcurrentHashMap;
23
24 /**
25  * Java Swing GUI panel that contains canvas for 3D rendering.
26  */
27 public class ViewPanel extends JPanel implements ComponentListener {
28     private static final long serialVersionUID = 1683277888885045387L;
29     public Color backgroundColor = Color.BLACK;
30     private final HIDInputTracker HIDInputTracker = new HIDInputTracker(this);
31     private final KeyboardFocusStack keyboardFocusStack;
32     private final Avatar avatar = new Avatar();
33     private final ShapeCollection rootShapeCollection = new ShapeCollection();
34     private final Set<ViewRenderListener> viewRenderListeners = ConcurrentHashMap.newKeySet();
35     /**
36      * Last time this view was updated.
37      */
38     private long lastUpdateMillis = 0;
39     private Timer canvasUpdateTimer;
40     private ViewUpdateTimerTask canvasUpdateTimerTask;
41     private RenderingContext renderingContext = null;
42     /**
43      * Currently target FPS for this view. It can be changed at runtime. Also when nothing
44      * changes in the view, then frames are not really repainted.
45      */
46     private int targetFPS = 30;
47     /**
48      * Set to true if it is known than next frame reeds to be painted. Flag is cleared
49      * immediately after frame got updated.
50      */
51     private boolean viewRepaintNeeded = true;
52
53     public ViewPanel() {
54         viewRenderListeners.add(avatar);
55         viewRenderListeners.add(HIDInputTracker);
56
57         keyboardFocusStack = new KeyboardFocusStack(this);
58
59         initializePanelLayout();
60
61         setFrameRate(targetFPS);
62
63         addComponentListener(this);
64     }
65
66     public Avatar getAvatar() {
67         return avatar;
68     }
69
70     public KeyboardFocusStack getKeyboardFocusStack() {
71         return keyboardFocusStack;
72     }
73
74     public ShapeCollection getRootShapeCollection() {
75         return rootShapeCollection;
76     }
77
78     public HIDInputTracker getHIDInputTracker() {
79         return HIDInputTracker;
80     }
81
82     public void addViewUpdateListener(final ViewRenderListener listener) {
83         viewRenderListeners.add(listener);
84     }
85
86     @Override
87     public void componentHidden(final ComponentEvent e) {
88
89     }
90
91     @Override
92     public void componentMoved(final ComponentEvent e) {
93
94     }
95
96     @Override
97     public void componentResized(final ComponentEvent e) {
98         viewRepaintNeeded = true;
99     }
100
101     @Override
102     public void componentShown(final ComponentEvent e) {
103         viewRepaintNeeded = true;
104     }
105
106     @Override
107     public Dimension getMaximumSize() {
108         return getPreferredSize();
109     }
110
111     @Override
112     public Dimension getMinimumSize() {
113         return getPreferredSize();
114     }
115
116     @Override
117     public java.awt.Dimension getPreferredSize() {
118         return new java.awt.Dimension(640, 480);
119     }
120
121     public RenderingContext getRenderingContext() {
122         return renderingContext;
123     }
124
125     private void initializePanelLayout() {
126         setFocusCycleRoot(true);
127         setOpaque(true);
128         setFocusable(true);
129         setDoubleBuffered(false);
130         setVisible(true);
131         requestFocusInWindow();
132     }
133
134     private void renderFrame() {
135         // paint root geometry collection to the offscreen render buffer
136         clearCanvas();
137         rootShapeCollection.paint(this, renderingContext);
138
139         // draw rendered offscreen buffer to visible screen
140         final Graphics graphics = getGraphics();
141         if (graphics != null)
142             graphics.drawImage(renderingContext.bufferedImage, 0, 0, null);
143     }
144
145     private void clearCanvas() {
146         renderingContext.graphics.setColor(backgroundColor);
147         renderingContext.graphics.fillRect(0, 0, getWidth(), getHeight());
148     }
149
150     /**
151      * Calling this methods tells 3D engine that current 3D view needs to be
152      * repainted on first opportunity.
153      */
154     public void repaintDuringNextViewUpdate() {
155         viewRepaintNeeded = true;
156     }
157
158     public void setFrameRate(final int frameRate) {
159         if (canvasUpdateTimerTask != null) {
160             canvasUpdateTimerTask.cancel();
161             canvasUpdateTimerTask = null;
162
163             canvasUpdateTimer.cancel();
164             canvasUpdateTimer = null;
165         }
166
167         targetFPS = frameRate;
168
169         if (frameRate > 0) {
170             canvasUpdateTimer = new Timer();
171             canvasUpdateTimerTask = new ViewUpdateTimerTask(this);
172
173             canvasUpdateTimer.schedule(canvasUpdateTimerTask, 0,
174                     1000 / frameRate);
175         }
176     }
177
178     public void stop() {
179         if (canvasUpdateTimerTask != null) {
180             canvasUpdateTimerTask.cancel();
181             canvasUpdateTimerTask = null;
182         }
183
184         if (canvasUpdateTimer != null) {
185             canvasUpdateTimer.cancel();
186             canvasUpdateTimer = null;
187         }
188     }
189
190     /**
191      * This method is executed by periodic timer task, in frequency according to
192      * defined frame rate.
193      * <p>
194      * It tells view to update itself. View can decide if actual re-rendering of
195      * graphics is needed.
196      */
197     void updateView() {
198         maintainRenderingContext();
199
200         final int millisecondsPassedSinceLastUpdate = getMillisecondsPassedSinceLastUpdate();
201
202         boolean renderFrame = notifyViewRenderListeners(millisecondsPassedSinceLastUpdate);
203
204         if (viewRepaintNeeded) {
205             viewRepaintNeeded = false;
206             renderFrame = true;
207         }
208
209         // abort rendering if window size is invalid
210         if ((getWidth() > 0) && (getHeight() > 0) && renderFrame) {
211             renderFrame();
212             viewRepaintNeeded = renderingContext.handleDetectedComponentMouseEvents();
213         }
214
215     }
216
217     private void maintainRenderingContext() {
218         int panelWidth = getWidth();
219         int panelHeight = getHeight();
220
221         if (panelWidth <= 0 || panelHeight <=0){
222             renderingContext = null;
223             return;
224         }
225
226         if ((renderingContext == null)
227                 || (renderingContext.width != panelWidth)
228                 || (renderingContext.height != panelHeight)) {
229             renderingContext = new RenderingContext(panelWidth, panelHeight);
230         }
231
232         renderingContext.prepareForNewFrameRendering();
233     }
234
235     private boolean notifyViewRenderListeners(int millisecondsPassedSinceLastUpdate) {
236         boolean reRenderFrame = false;
237         for (final ViewRenderListener listener : viewRenderListeners)
238             if (listener.beforeRender(this, millisecondsPassedSinceLastUpdate))
239                 reRenderFrame = true;
240         return reRenderFrame;
241     }
242
243     private int getMillisecondsPassedSinceLastUpdate() {
244         final long currentTime = System.currentTimeMillis();
245
246         if (lastUpdateMillis == 0)
247             lastUpdateMillis = currentTime;
248
249         final int millisecondsPassedSinceLastUpdate = (int) (currentTime - lastUpdateMillis);
250         lastUpdateMillis = currentTime;
251         return millisecondsPassedSinceLastUpdate;
252     }
253
254     public void addViewRenderListener(ViewRenderListener viewRenderListener) {
255         viewRenderListeners.add(viewRenderListener);
256     }
257
258     public void removeViewRenderListener(ViewRenderListener viewRenderListener) {
259         viewRenderListeners.remove(viewRenderListener);
260     }
261
262 }