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