87f0dd39fefedae9976b0b54e46ad2e7822e9fa9
[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.MouseInteractionController;
13
14 import javax.swing.*;
15 import java.awt.*;
16 import java.awt.event.ComponentEvent;
17 import java.awt.event.ComponentListener;
18 import java.util.ArrayList;
19 import java.util.List;
20 import java.util.Timer;
21
22 public class ViewPanel extends JPanel implements ComponentListener {
23
24     private static final long serialVersionUID = 1683277888885045387L;
25     private final List<ViewRenderListener> viewRenderListeners = new ArrayList<>();
26     private final ViewContext context = new ViewContext(this);
27     /**
28      * Last time this view was updated.
29      */
30     private long lastUpdateMillis = 0;
31     private Timer canvasUpdateTimer;
32     private ViewUpdateTimerTask canvasUpdateTimerTask;
33     private RenderingContext renderingContext = null;
34     private MouseInteractionController currentMouseOverComponent;
35     /**
36      * Currently target FPS for this view. It might change at runtime.
37      */
38     private int targetFramerate = 30;
39     private boolean repaintDuringNextViewUpdate = true;
40
41     public ViewPanel() {
42         viewRenderListeners.add(context.getAvatar());
43
44         // initialize input tracker
45         context.getUserInputTracker().bind(this);
46         viewRenderListeners.add(context.getUserInputTracker());
47
48         initializePanelLayout();
49
50         setFrameRate(targetFramerate);
51
52         addComponentListener(this);
53     }
54
55     public void addViewUpdateListener(final ViewRenderListener listener) {
56         viewRenderListeners.add(listener);
57     }
58
59     @Override
60     public void componentHidden(final ComponentEvent e) {
61
62     }
63
64     @Override
65     public void componentMoved(final ComponentEvent e) {
66
67     }
68
69     @Override
70     public void componentResized(final ComponentEvent e) {
71         repaintDuringNextViewUpdate = true;
72     }
73
74     @Override
75     public void componentShown(final ComponentEvent e) {
76         repaintDuringNextViewUpdate = true;
77     }
78
79     public ViewContext getContext() {
80         return context;
81     }
82
83     @Override
84     public Dimension getMaximumSize() {
85         return getPreferredSize();
86     }
87
88     @Override
89     public Dimension getMinimumSize() {
90         return getPreferredSize();
91     }
92
93     @Override
94     public java.awt.Dimension getPreferredSize() {
95         return new java.awt.Dimension(640, 480);
96     }
97
98     public RenderingContext getRenderBuffer() {
99         return renderingContext;
100     }
101
102     public RenderingContext getRenderingContext() {
103         return renderingContext;
104     }
105
106     private void handleDetectedComponentMouseEvents() {
107         if (renderingContext.clickedItem != null) {
108             if (renderingContext.mouseClick.button == 0) {
109                 // mouse over
110                 if (currentMouseOverComponent == null) {
111                     currentMouseOverComponent = renderingContext.clickedItem;
112                     currentMouseOverComponent.mouseEntered();
113                     repaintDuringNextViewUpdate = true;
114                 } else if (currentMouseOverComponent != renderingContext.clickedItem) {
115                     currentMouseOverComponent.mouseExited();
116                     currentMouseOverComponent = renderingContext.clickedItem;
117                     currentMouseOverComponent.mouseEntered();
118                     repaintDuringNextViewUpdate = true;
119                 }
120             } else {
121                 // mouse click
122                 renderingContext.clickedItem.mouseClicked();
123                 repaintDuringNextViewUpdate = true;
124             }
125         } else if (currentMouseOverComponent != null) {
126             currentMouseOverComponent.mouseExited();
127             repaintDuringNextViewUpdate = true;
128             currentMouseOverComponent = null;
129         }
130     }
131
132     private void initializePanelLayout() {
133         setFocusCycleRoot(true);
134         setOpaque(true);
135         setFocusable(true);
136         setDoubleBuffered(false);
137         setVisible(true);
138         requestFocusInWindow();
139     }
140
141     public void renderFrame() {
142         // build new render buffer if needed, this happens when window was just
143         // created or resized
144         if ((renderingContext == null)
145                 || (renderingContext.width != getWidth())
146                 || (renderingContext.height != getHeight()))
147             renderingContext = new RenderingContext(getWidth(), getHeight());
148
149         // clear drawing area
150         {
151             renderingContext.graphics.setColor(Color.BLACK);
152             renderingContext.graphics.fillRect(0, 0, getWidth(), getHeight());
153         }
154
155         // paint root geometry collection to the offscreen render buffer
156         context.getRootShapeCollection().paint(context, renderingContext);
157
158         // draw rendered offscreen image to visible screen
159         final Graphics graphics = getGraphics();
160         if (graphics != null)
161             graphics.drawImage(renderingContext.bufferedImage, 0, 0, null);
162     }
163
164     /**
165      * Calling this methods tells 3D engine that current 3D view needs to be
166      * repainted on first opportunity.
167      */
168     public void repaintDuringNextViewUpdate() {
169         repaintDuringNextViewUpdate = true;
170     }
171
172     public void setFrameRate(final int frameRate) {
173         if (canvasUpdateTimerTask != null) {
174             canvasUpdateTimerTask.cancel();
175             canvasUpdateTimerTask = null;
176
177             canvasUpdateTimer.cancel();
178             canvasUpdateTimer = null;
179         }
180
181         targetFramerate = frameRate;
182
183         if (frameRate > 0) {
184             canvasUpdateTimer = new Timer();
185             canvasUpdateTimerTask = new ViewUpdateTimerTask(this);
186
187             canvasUpdateTimer.schedule(canvasUpdateTimerTask, 0,
188                     1000 / frameRate);
189         }
190     }
191
192     public void stop() {
193         if (canvasUpdateTimerTask != null) {
194             canvasUpdateTimerTask.cancel();
195             canvasUpdateTimerTask = null;
196         }
197
198         if (canvasUpdateTimer != null) {
199             canvasUpdateTimer.cancel();
200             canvasUpdateTimer = null;
201         }
202     }
203
204     /**
205      * This method is executed by periodic timer task, in frequency according to
206      * defined frame rate.
207      * <p>
208      * It tells view to update itself. View can decide if actual re-rendering of
209      * graphics is needed.
210      */
211     public void updateView() {
212         if (renderingContext != null){
213             renderingContext.mouseClick = null;
214             renderingContext.clickedItem = null;
215         }
216
217         // compute time passed since last view update
218         final long currentTime = System.currentTimeMillis();
219
220         if (lastUpdateMillis == 0) {
221             lastUpdateMillis = currentTime;
222             return;
223         }
224
225         final int millisecondsPassedSinceLastUpdate = (int) (currentTime - lastUpdateMillis);
226         lastUpdateMillis = currentTime;
227
228         // notify update listeners
229         boolean reRenderFrame = false;
230
231         for (final ViewRenderListener listener : viewRenderListeners)
232             if (listener.beforeRender(context,
233                     millisecondsPassedSinceLastUpdate))
234                 reRenderFrame = true;
235
236         // abort rendering if window size is invalid
237         if ((getWidth() <= 0) || (getHeight() <= 0))
238             return;
239
240         if (repaintDuringNextViewUpdate) {
241             repaintDuringNextViewUpdate = false;
242             reRenderFrame = true;
243         }
244
245         if (reRenderFrame) {
246             renderFrame();
247             handleDetectedComponentMouseEvents();
248         }
249     }
250 }