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