6fda15390ba653fd1b704a1ab7ba7ce30f4e8404
[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.KeyboardFocusTracker;
13 import eu.svjatoslav.sixth.e3d.gui.humaninput.MouseInteractionController;
14 import eu.svjatoslav.sixth.e3d.gui.humaninput.UserInputTracker;
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     private final UserInputTracker userInputTracker = new UserInputTracker(this);
31     private final KeyboardFocusTracker keyboardFocusTracker = new KeyboardFocusTracker(
32             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(userInputTracker);
60
61         initializePanelLayout();
62
63         setFrameRate(targetFPS);
64
65         addComponentListener(this);
66     }
67
68     public Avatar getAvatar() {
69         return avatar;
70     }
71
72     public KeyboardFocusTracker getKeyboardFocusTracker() {
73         return keyboardFocusTracker;
74     }
75
76     public ShapeCollection getRootShapeCollection() {
77         return rootShapeCollection;
78     }
79
80     public UserInputTracker getUserInputTracker() {
81         return userInputTracker;
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 getRenderBuffer() {
124         return renderingContext;
125     }
126
127     public RenderingContext getRenderingContext() {
128         return renderingContext;
129     }
130
131     private void handleDetectedComponentMouseEvents() {
132         if (renderingContext.clickedItem != null) {
133             if (renderingContext.mouseClick.button == 0) {
134                 // mouse over
135                 if (currentMouseOverComponent == null) {
136                     currentMouseOverComponent = renderingContext.clickedItem;
137                     currentMouseOverComponent.mouseEntered();
138                     viewRepaintNeeded = true;
139                 } else if (currentMouseOverComponent != renderingContext.clickedItem) {
140                     currentMouseOverComponent.mouseExited();
141                     currentMouseOverComponent = renderingContext.clickedItem;
142                     currentMouseOverComponent.mouseEntered();
143                     viewRepaintNeeded = true;
144                 }
145             } else {
146                 // mouse click
147                 renderingContext.clickedItem.mouseClicked();
148                 viewRepaintNeeded = true;
149             }
150         } else if (currentMouseOverComponent != null) {
151             currentMouseOverComponent.mouseExited();
152             viewRepaintNeeded = true;
153             currentMouseOverComponent = null;
154         }
155     }
156
157     private void initializePanelLayout() {
158         setFocusCycleRoot(true);
159         setOpaque(true);
160         setFocusable(true);
161         setDoubleBuffered(false);
162         setVisible(true);
163         requestFocusInWindow();
164     }
165
166     public void renderFrame() {
167         // build new render buffer if needed, this happens when window was just
168         // created or resized
169         if ((renderingContext == null)
170                 || (renderingContext.width != getWidth())
171                 || (renderingContext.height != getHeight()))
172             renderingContext = new RenderingContext(getWidth(), getHeight());
173
174         // clear drawing area
175         {
176             renderingContext.graphics.setColor(Color.BLACK);
177             renderingContext.graphics.fillRect(0, 0, getWidth(), getHeight());
178         }
179
180         // paint root geometry collection to the offscreen render buffer
181         rootShapeCollection.paint(this, renderingContext);
182
183         // draw rendered offscreen image to visible screen
184         final Graphics graphics = getGraphics();
185         if (graphics != null)
186             graphics.drawImage(renderingContext.bufferedImage, 0, 0, null);
187     }
188
189     /**
190      * Calling this methods tells 3D engine that current 3D view needs to be
191      * repainted on first opportunity.
192      */
193     public void repaintDuringNextViewUpdate() {
194         viewRepaintNeeded = true;
195     }
196
197     public void setFrameRate(final int frameRate) {
198         if (canvasUpdateTimerTask != null) {
199             canvasUpdateTimerTask.cancel();
200             canvasUpdateTimerTask = null;
201
202             canvasUpdateTimer.cancel();
203             canvasUpdateTimer = null;
204         }
205
206         targetFPS = frameRate;
207
208         if (frameRate > 0) {
209             canvasUpdateTimer = new Timer();
210             canvasUpdateTimerTask = new ViewUpdateTimerTask(this);
211
212             canvasUpdateTimer.schedule(canvasUpdateTimerTask, 0,
213                     1000 / frameRate);
214         }
215     }
216
217     public void stop() {
218         if (canvasUpdateTimerTask != null) {
219             canvasUpdateTimerTask.cancel();
220             canvasUpdateTimerTask = null;
221         }
222
223         if (canvasUpdateTimer != null) {
224             canvasUpdateTimer.cancel();
225             canvasUpdateTimer = null;
226         }
227     }
228
229     /**
230      * This method is executed by periodic timer task, in frequency according to
231      * defined frame rate.
232      * <p>
233      * It tells view to update itself. View can decide if actual re-rendering of
234      * graphics is needed.
235      */
236     public void updateView() {
237         if (renderingContext != null) {
238             renderingContext.mouseClick = null;
239             renderingContext.clickedItem = null;
240         }
241
242         // compute time passed since last view update
243         final long currentTime = System.currentTimeMillis();
244
245         if (lastUpdateMillis == 0) {
246             lastUpdateMillis = currentTime;
247             return;
248         }
249
250         final int millisecondsPassedSinceLastUpdate = (int) (currentTime - lastUpdateMillis);
251         lastUpdateMillis = currentTime;
252
253         // notify update listeners
254         boolean reRenderFrame = false;
255
256         for (final ViewRenderListener listener : viewRenderListeners)
257             if (listener.beforeRender(this, millisecondsPassedSinceLastUpdate))
258                 reRenderFrame = true;
259
260         // abort rendering if window size is invalid
261         if ((getWidth() <= 0) || (getHeight() <= 0))
262             return;
263
264         if (viewRepaintNeeded) {
265             viewRepaintNeeded = false;
266             reRenderFrame = true;
267         }
268
269         if (reRenderFrame) {
270             renderFrame();
271             handleDetectedComponentMouseEvents();
272         }
273     }
274 }