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