Better exception handling. Formatting. Copyright update.
[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 = new RenderingContext(1, 1, 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                     renderingContext);
158
159         // clear drawing area
160         {
161             renderingContext.graphics.setColor(Color.BLACK);
162             renderingContext.graphics.fillRect(0, 0, getWidth(), getHeight());
163         }
164
165         // paint root geometry collection to the offscreen render buffer
166         context.getRootShapeCollection().paint(context, renderingContext);
167
168         // draw rendered offscreen image to visible screen
169         final Graphics graphics = getGraphics();
170         if (graphics != null)
171             graphics.drawImage(renderingContext.bufferedImage, 0, 0, null);
172     }
173
174     /**
175      * Calling this methods tells 3D engine that current 3D view needs to be
176      * repainted on first opportunity.
177      */
178     public void repaintDuringNextViewUpdate() {
179         repaintDuringNextViewUpdate = true;
180     }
181
182     public void setFrameRate(final int frameRate) {
183         if (canvasUpdateTimerTask != null) {
184             canvasUpdateTimerTask.cancel();
185             canvasUpdateTimerTask = null;
186
187             canvasUpdateTimer.cancel();
188             canvasUpdateTimer = null;
189         }
190
191         targetFramerate = frameRate;
192
193         if (frameRate > 0) {
194             canvasUpdateTimer = new Timer();
195             canvasUpdateTimerTask = new ViewUpdateTimerTask(this);
196
197             canvasUpdateTimer.schedule(canvasUpdateTimerTask, 0,
198                     1000 / frameRate);
199         }
200     }
201
202     public void stop() {
203         if (canvasUpdateTimerTask != null) {
204             canvasUpdateTimerTask.cancel();
205             canvasUpdateTimerTask = null;
206         }
207
208         if (canvasUpdateTimer != null) {
209             canvasUpdateTimer.cancel();
210             canvasUpdateTimer = null;
211         }
212     }
213
214     /**
215      * This method is executed by periodic timer task, in frequency according to
216      * defined frame rate.
217      * <p>
218      * It tells view to update itself. View can decide if actual re-rendering of
219      * graphics is needed.
220      */
221     public void updateView() {
222         renderingContext.mouseClick = null;
223         renderingContext.clickedItem = null;
224
225         // compute time passed since last view update
226         final long currentTime = System.currentTimeMillis();
227
228         if (lastUpdateMillis == 0) {
229             lastUpdateMillis = currentTime;
230             return;
231         }
232
233         final int millisecondsPassedSinceLastUpdate = (int) (currentTime - lastUpdateMillis);
234         lastUpdateMillis = currentTime;
235
236         // notify update listeners
237         boolean rerenderView = false;
238
239         for (final ViewUpdateListener listener : viewUpdateListeners)
240             if (listener.beforeViewUpdate(context,
241                     millisecondsPassedSinceLastUpdate))
242                 rerenderView = true;
243
244         // abort rendering if window size is invalid
245         if ((getWidth() <= 0) || (getHeight() <= 0))
246             return;
247
248         if (repaintDuringNextViewUpdate) {
249             repaintDuringNextViewUpdate = false;
250             rerenderView = true;
251         }
252
253         if (rerenderView) {
254             renderFrame();
255             handleDetectedComponentMouseEvents();
256         }
257     }
258 }