Jogamp
JOGL: Unify library loading (impl and binding), incl. lookup ; GLAutoDrawable: dispos...
[jogl.git] / src / newt / classes / com / jogamp / newt / opengl / GLWindow.java
CommitLineData
a959c53b
KR
1/*
2 * Copyright (c) 2008 Sun Microsystems, Inc. All Rights Reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are
6 * met:
7 *
8 * - Redistribution of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 *
11 * - Redistribution in binary form must reproduce the above copyright
12 * notice, this list of conditions and the following disclaimer in the
13 * documentation and/or other materials provided with the distribution.
14 *
15 * Neither the name of Sun Microsystems, Inc. or the names of
16 * contributors may be used to endorse or promote products derived from
17 * this software without specific prior written permission.
18 *
19 * This software is provided "AS IS," without a warranty of any kind. ALL
20 * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES,
21 * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A
22 * PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN
23 * MICROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL NOT BE LIABLE FOR
24 * ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR
25 * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR
26 * ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR
27 * DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE
28 * DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY,
29 * ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, EVEN IF
30 * SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
31 *
32 */
33
2d57c252 34package com.jogamp.newt.opengl;
a959c53b 35
2d57c252 36import com.jogamp.newt.*;
1ad8c39d 37import com.jogamp.newt.event.*;
811bd23e 38import com.jogamp.nativewindow.impl.RecursiveToolkitLock;
a959c53b
KR
39import javax.media.nativewindow.*;
40import javax.media.opengl.*;
f3bc93b0 41import com.jogamp.opengl.impl.GLDrawableHelper;
a92906bc 42import java.util.*;
a959c53b
KR
43
44/**
45 * An implementation of {@link Window} which is customized for OpenGL
2268a6ce
SG
46 * use, and which implements the {@link javax.media.opengl.GLAutoDrawable} interface.
47 * <P>
48 * This implementation does not make the OpenGL context current<br>
49 * before calling the various input EventListener callbacks (MouseListener, KeyListener,
50 * etc.).<br>
51 * This design decision is made to favor a more performant and simplified
52 * implementation, as well as the event dispatcher shall be allowed
53 * not having a notion about OpenGL.
54 * <p>
a959c53b
KR
55 */
56public class GLWindow extends Window implements GLAutoDrawable {
a959c53b 57 private Window window;
2268a6ce 58 private boolean runPumpMessages;
a959c53b 59
476d1d75
MB
60 /**
61 * Constructor. Do not call this directly -- use {@link #create()} instead.
62 */
811bd23e 63 protected GLWindow(Window window) {
a959c53b 64 this.window = window;
811bd23e 65 this.window.setHandleDestroyNotify(false);
1c1053c6 66 this.runPumpMessages = ( null == getScreen().getDisplay().getEDTUtil() ) ;
1ad8c39d 67 window.addWindowListener(new WindowAdapter() {
a959c53b
KR
68 public void windowResized(WindowEvent e) {
69 sendReshape = true;
70 }
71
a959c53b
KR
72 public void windowDestroyNotify(WindowEvent e) {
73 sendDestroy = true;
74 }
75 });
76 }
77
6e599a26
SG
78 /** Creates a new GLWindow attaching the given window - not owning the Window. */
79 public static GLWindow create(Window window) {
80 return create(null, window, null, false);
a959c53b
KR
81 }
82
811bd23e 83 /** Creates a new GLWindow attaching a new native child Window of the given <code>parentNativeWindow</code>
6e599a26 84 with the given GLCapabilities - owning the Window */
811bd23e
SG
85 public static GLWindow create(NativeWindow parentNativeWindow, GLCapabilities caps) {
86 return create(parentNativeWindow, null, caps, false);
a959c53b
KR
87 }
88
6e599a26
SG
89 /** Creates a new GLWindow attaching a new decorated Window on the local display, screen 0, with a
90 dummy visual ID and given GLCapabilities - owning the window */
a959c53b 91 public static GLWindow create(GLCapabilities caps) {
6e599a26 92 return create(null, null, caps, false);
a959c53b
KR
93 }
94
6e599a26
SG
95 /** Creates a new GLWindow attaching a new Window on the local display, screen 0, with a
96 dummy visual ID and given GLCapabilities - owning the window */
a959c53b 97 public static GLWindow create(GLCapabilities caps, boolean undecorated) {
6e599a26 98 return create(null, null, caps, undecorated);
a959c53b
KR
99 }
100
3cc7335e 101 /** Either or: window (prio), or caps and undecorated (2nd choice) */
811bd23e 102 private static GLWindow create(NativeWindow parentNativeWindow, Window window,
3cc7335e
SG
103 GLCapabilities caps,
104 boolean undecorated) {
a959c53b 105 if (window == null) {
3cc7335e
SG
106 if (caps == null) {
107 caps = new GLCapabilities(null); // default ..
108 }
811bd23e 109 window = NewtFactory.createWindow(parentNativeWindow, caps, undecorated);
a959c53b
KR
110 }
111
811bd23e 112 return new GLWindow(window);
a959c53b
KR
113 }
114
811bd23e
SG
115 public boolean isNativeWindowValid() {
116 return (null!=window)?window.isNativeWindowValid():false;
117 }
118
6e599a26 119 public boolean isDestroyed() {
811bd23e 120 return (null!=window)?window.isDestroyed():true;
6e599a26
SG
121 }
122
d94115b3 123 public final Window getInnerWindow() {
811bd23e 124 return window.getInnerWindow();
6e599a26
SG
125 }
126
d94115b3
SG
127 public final Object getWrappedWindow() {
128 return window.getWrappedWindow();
129 }
130
a92906bc
SG
131 /**
132 * EXPERIMENTAL<br>
133 * Enable or disables running the {@link Display#pumpMessages} in the {@link #display()} call.<br>
134 * The default behavior is to run {@link Display#pumpMessages}.<P>
135 *
136 * The idea was that in a single threaded environment with one {@link Display} and many {@link Window}'s,
137 * a performance benefit was expected while disabling the implicit {@link Display#pumpMessages} and
138 * do it once via {@link GLWindow#runCurrentThreadPumpMessage()} <br>
139 * This could not have been verified. No measurable difference could have been recognized.<P>
140 *
141 * Best performance has been achieved with one GLWindow per thread.<br>
142 *
2268a6ce 143 * Enabling local pump messages while using the EDT,
2d57c252 144 * {@link com.jogamp.newt.NewtFactory#setUseEDT(boolean)},
2268a6ce
SG
145 * will result in an exception.
146 *
a92906bc
SG
147 * @deprecated EXPERIMENTAL, semantic is about to be removed after further verification.
148 */
149 public void setRunPumpMessages(boolean onoff) {
1c1053c6 150 if( onoff && null!=getScreen().getDisplay().getEDTUtil() ) {
2268a6ce
SG
151 throw new GLException("GLWindow.setRunPumpMessages(true) - Can't do with EDT on");
152 }
a92906bc
SG
153 runPumpMessages = onoff;
154 }
155
6e599a26 156 protected void createNativeImpl() {
a959c53b
KR
157 shouldNotCallThis();
158 }
159
160 protected void closeNative() {
161 shouldNotCallThis();
162 }
163
811bd23e 164 protected void dispose() {
a959c53b 165 if(Window.DEBUG_WINDOW_EVENT || window.DEBUG_IMPLEMENTATION) {
811bd23e 166 Exception e1 = new Exception("GLWindow.dispose() "+Thread.currentThread()+", start: "+this);
a959c53b
KR
167 e1.printStackTrace();
168 }
169
4512900d 170 if ( null != context && context.isCreated() && null != drawable && drawable.isRealized() ) {
811bd23e 171 helper.invokeGL(drawable, context, disposeAction, null);
eab82899 172 }
a959c53b
KR
173
174 if (context != null) {
175 context.destroy();
811bd23e 176 context = null;
a959c53b
KR
177 }
178 if (drawable != null) {
179 drawable.setRealized(false);
811bd23e 180 drawable = null;
a959c53b
KR
181 }
182
183 if(Window.DEBUG_WINDOW_EVENT || window.DEBUG_IMPLEMENTATION) {
811bd23e 184 System.out.println("GLWindow.dispose() "+Thread.currentThread()+", fin: "+this);
a959c53b
KR
185 }
186 }
187
811bd23e
SG
188 class DestroyAction implements Runnable {
189 boolean deep;
190 public DestroyAction(boolean deep) {
191 this.deep = deep;
192 }
193 public void run() {
194 windowLock();
195 try {
196 if(null==window || window.isDestroyed()) {
197 return; // nop
198 }
199 dispose();
a92906bc 200
811bd23e
SG
201 if(null!=window) {
202 window.destroy(deep);
203 }
a959c53b 204
811bd23e
SG
205 if(deep) {
206 helper=null;
207 }
208 } finally {
209 windowUnlock();
a959c53b 210 }
a959c53b 211 }
811bd23e 212 }
a959c53b 213
811bd23e
SG
214 /**
215 * @param deep If true, all resources, ie listeners, parent handles, size, position
216 * and the referenced NEWT screen and display, will be destroyed as well. Be aware that if you call
217 * this method with deep = true, you will not be able to regenerate the Window.
218 * @see #destroy()
219 */
220 public void destroy(boolean deep) {
221 if(!isDestroyed()) {
222 runOnEDTIfAvail(true, new DestroyAction(deep));
223 }
a959c53b
KR
224 }
225
226 public boolean getPerfLogEnabled() { return perfLog; }
227
228 public void enablePerfLog(boolean v) {
229 perfLog = v;
230 }
231
811bd23e 232 protected void setVisibleImpl(boolean visible) {
6e599a26
SG
233 shouldNotCallThis();
234 }
235
811bd23e
SG
236 public void reparentWindow(NativeWindow newParent, Screen newScreen) {
237 window.reparentWindow(newParent, newScreen);
238 }
239
240 class VisibleAction implements Runnable {
241 boolean visible;
242 public VisibleAction(boolean visible) {
243 this.visible = visible;
244 }
245 public void run() {
246 windowLock();
247 try{
248 window.setVisible(visible);
249 if (null == context && visible && 0 != window.getWindowHandle() && 0<getWidth()*getHeight()) {
250 NativeWindow nw;
d94115b3
SG
251 if (getWrappedWindow() != null) {
252 nw = NativeWindowFactory.getNativeWindow(getWrappedWindow(), window.getGraphicsConfiguration());
811bd23e
SG
253 } else {
254 nw = window;
255 }
256 GLCapabilities glCaps = (GLCapabilities) nw.getGraphicsConfiguration().getNativeGraphicsConfiguration().getChosenCapabilities();
257 if(null==factory) {
258 factory = GLDrawableFactory.getFactory(glCaps.getGLProfile());
259 }
260 if(null==drawable) {
261 drawable = factory.createGLDrawable(nw);
262 }
263 drawable.setRealized(true);
264 context = drawable.createContext(null);
265 sendReshape = true; // ensure a reshape event is send ..
266 }
267 } finally {
268 windowUnlock();
a959c53b 269 }
a959c53b
KR
270 }
271 }
272
811bd23e
SG
273 public void setVisible(boolean visible) {
274 if(!isDestroyed()) {
275 runOnEDTIfAvail(true, new VisibleAction(visible));
276 }
277 }
278
279 public Capabilities getRequestedCapabilities() {
280 return window.getRequestedCapabilities();
281 }
282
283 public NativeWindow getParentNativeWindow() {
284 return window.getParentNativeWindow();
285 }
286
a959c53b
KR
287 public Screen getScreen() {
288 return window.getScreen();
289 }
290
291 public void setTitle(String title) {
292 window.setTitle(title);
293 }
294
295 public String getTitle() {
296 return window.getTitle();
297 }
298
299 public void setUndecorated(boolean value) {
300 window.setUndecorated(value);
301 }
302
303 public boolean isUndecorated() {
304 return window.isUndecorated();
305 }
306
ccc30b05
SG
307 public void requestFocus() {
308 window.requestFocus();
309 }
310
811bd23e
SG
311 public Insets getInsets() {
312 return window.getInsets();
313 }
314
a959c53b
KR
315 public void setSize(int width, int height) {
316 window.setSize(width, height);
317 }
811bd23e
SG
318 protected void setSizeImpl(int width, int height) {
319 shouldNotCallThis();
320 }
a959c53b
KR
321
322 public void setPosition(int x, int y) {
323 window.setPosition(x, y);
324 }
811bd23e
SG
325 protected void setPositionImpl(int x, int y) {
326 shouldNotCallThis();
7bed517f 327 }
328
a959c53b
KR
329 public boolean setFullscreen(boolean fullscreen) {
330 return window.setFullscreen(fullscreen);
331 }
811bd23e
SG
332 protected boolean setFullscreenImpl(boolean fullscreen, int x, int y, int w, int h) {
333 shouldNotCallThis();
334 return false;
335 }
a959c53b
KR
336
337 public boolean isVisible() {
338 return window.isVisible();
339 }
340
341 public int getX() {
342 return window.getX();
343 }
344
345 public int getY() {
346 return window.getY();
347 }
348
349 public int getWidth() {
350 return window.getWidth();
351 }
352
353 public int getHeight() {
354 return window.getHeight();
355 }
356
357 public boolean isFullscreen() {
358 return window.isFullscreen();
359 }
360
811bd23e
SG
361 public void sendEvent(NEWTEvent e) {
362 window.sendEvent(e);
363 }
364
bf584fba
SG
365 public void addSurfaceUpdatedListener(SurfaceUpdatedListener l) {
366 window.addSurfaceUpdatedListener(l);
367 }
368 public void removeSurfaceUpdatedListener(SurfaceUpdatedListener l) {
369 window.removeSurfaceUpdatedListener(l);
370 }
e8f4dc96
SG
371 public void removeAllSurfaceUpdatedListener() {
372 window.removeAllSurfaceUpdatedListener();
373 }
bf584fba
SG
374 public SurfaceUpdatedListener[] getSurfaceUpdatedListener() {
375 return window.getSurfaceUpdatedListener();
376 }
377 public void surfaceUpdated(Object updater, NativeWindow window0, long when) {
378 window.surfaceUpdated(updater, window, when);
379 }
380
a959c53b
KR
381 public void addMouseListener(MouseListener l) {
382 window.addMouseListener(l);
383 }
384
385 public void removeMouseListener(MouseListener l) {
386 window.removeMouseListener(l);
387 }
388
389 public MouseListener[] getMouseListeners() {
390 return window.getMouseListeners();
391 }
392
393 public void addKeyListener(KeyListener l) {
394 window.addKeyListener(l);
395 }
396
397 public void removeKeyListener(KeyListener l) {
398 window.removeKeyListener(l);
399 }
400
401 public KeyListener[] getKeyListeners() {
402 return window.getKeyListeners();
403 }
404
405 public void addWindowListener(WindowListener l) {
406 window.addWindowListener(l);
407 }
408
409 public void removeWindowListener(WindowListener l) {
410 window.removeWindowListener(l);
411 }
412
413 public WindowListener[] getWindowListeners() {
414 return window.getWindowListeners();
415 }
416
417 public String toString() {
1c1053c6 418 return "NEWT-GLWindow[ \n\tHelper: "+helper+", \n\tDrawable: "+drawable + /** ", \n\tWindow: "+window+", \n\tFactory: "+factory+ */ "]";
a959c53b
KR
419 }
420
421 //----------------------------------------------------------------------
422 // OpenGL-related methods and state
423 //
424
a959c53b
KR
425 private GLDrawableFactory factory;
426 private GLDrawable drawable;
427 private GLContext context;
428 private GLDrawableHelper helper = new GLDrawableHelper();
429 // To make reshape events be sent immediately before a display event
430 private boolean sendReshape=false;
431 private boolean sendDestroy=false;
432 private boolean perfLog = false;
433
434 public GLDrawableFactory getFactory() {
435 return factory;
436 }
437
438 public void setContext(GLContext newCtx) {
439 context = newCtx;
440 }
441
442 public GLContext getContext() {
443 return context;
444 }
445
446 public GL getGL() {
447 if (context == null) {
448 return null;
449 }
450 return context.getGL();
451 }
452
4e0a5af0 453 public GL setGL(GL gl) {
a959c53b
KR
454 if (context != null) {
455 context.setGL(gl);
4e0a5af0 456 return gl;
a959c53b 457 }
4e0a5af0 458 return null;
a959c53b
KR
459 }
460
461 public void addGLEventListener(GLEventListener listener) {
462 helper.addGLEventListener(listener);
463 }
464
465 public void removeGLEventListener(GLEventListener listener) {
466 helper.removeGLEventListener(listener);
467 }
468
469 public void display() {
cf4c4037
SG
470 display(false);
471 }
472
473 public void display(boolean forceReshape) {
6e599a26
SG
474 if( null == window ) { return; }
475
476 if( null == context && window.isVisible() ) {
477 // retry native window and drawable/context creation
478 setVisible(true);
479 }
480
811bd23e 481 if( window.isNativeWindowValid() && null != context ) {
a92906bc 482 if(runPumpMessages) {
2268a6ce 483 window.getScreen().getDisplay().pumpMessages();
a92906bc 484 }
811bd23e 485 if(sendDestroy || window.hasDeviceChanged() && GLAutoDrawable.SCREEN_CHANGE_ACTION_ENABLED) {
a959c53b
KR
486 destroy();
487 sendDestroy=false;
6e599a26 488 } else if ( window.isVisible() ) {
cf4c4037
SG
489 if(forceReshape) {
490 sendReshape = true;
491 }
a959c53b
KR
492 helper.invokeGL(drawable, context, displayAction, initAction);
493 }
494 }
495 }
496
a92906bc 497 /** This implementation uses a static value */
a959c53b 498 public void setAutoSwapBufferMode(boolean onOrOff) {
2268a6ce 499 helper.setAutoSwapBufferMode(onOrOff);
a959c53b
KR
500 }
501
a92906bc 502 /** This implementation uses a static value */
a959c53b 503 public boolean getAutoSwapBufferMode() {
2268a6ce 504 return helper.getAutoSwapBufferMode();
a959c53b
KR
505 }
506
507 public void swapBuffers() {
2268a6ce
SG
508 if(drawable!=null && context != null) {
509 if (context != GLContext.getCurrent()) {
a959c53b
KR
510 // Assume we should try to make the context current before swapping the buffers
511 helper.invokeGL(drawable, context, swapBuffersAction, initAction);
512 } else {
513 drawable.swapBuffers();
514 }
515 }
516 }
517
518 class InitAction implements Runnable {
519 public void run() {
520 helper.init(GLWindow.this);
521 startTime = System.currentTimeMillis();
522 curTime = startTime;
523 if(perfLog) {
524 lastCheck = startTime;
525 totalFrames = 0; lastFrames = 0;
526 }
527 }
528 }
529 private InitAction initAction = new InitAction();
530
531 class DisposeAction implements Runnable {
532 public void run() {
533 helper.dispose(GLWindow.this);
534 }
535 }
536 private DisposeAction disposeAction = new DisposeAction();
537
538 class DisplayAction implements Runnable {
539 public void run() {
540 if (sendReshape) {
541 int width = getWidth();
542 int height = getHeight();
543 getGL().glViewport(0, 0, width, height);
544 helper.reshape(GLWindow.this, 0, 0, width, height);
545 sendReshape = false;
546 }
547
548 helper.display(GLWindow.this);
549
550 curTime = System.currentTimeMillis();
551 totalFrames++;
552
553 if(perfLog) {
554 long dt0, dt1;
555 lastFrames++;
556 dt0 = curTime-lastCheck;
557 if ( dt0 > 5000 ) {
558 dt1 = curTime-startTime;
559 System.out.println(dt0/1000 +"s: "+ lastFrames + "f, " + (lastFrames*1000)/dt0 + " fps, "+dt0/lastFrames+" ms/f; "+
560 "total: "+ dt1/1000+"s, "+(totalFrames*1000)/dt1 + " fps, "+dt1/totalFrames+" ms/f");
561 lastCheck=curTime;
562 lastFrames=0;
563 }
564 }
565 }
566 }
a92906bc 567 private DisplayAction displayAction = new DisplayAction();
a959c53b
KR
568
569 public long getStartTime() { return startTime; }
570 public long getCurrentTime() { return curTime; }
571 public long getDuration() { return curTime-startTime; }
572 public int getTotalFrames() { return totalFrames; }
573
574 private long startTime = 0;
575 private long curTime = 0;
576 private long lastCheck = 0;
577 private int totalFrames = 0, lastFrames = 0;
a959c53b 578
a959c53b
KR
579 class SwapBuffersAction implements Runnable {
580 public void run() {
581 drawable.swapBuffers();
582 }
583 }
a959c53b
KR
584 private SwapBuffersAction swapBuffersAction = new SwapBuffersAction();
585
586 //----------------------------------------------------------------------
6e599a26 587 // NativeWindow/Window methods
a959c53b
KR
588 //
589
811bd23e 590 public int lockSurface() throws NativeWindowException {
a959c53b 591 if(null!=drawable) return drawable.getNativeWindow().lockSurface();
811bd23e 592 return window.lockSurface();
a959c53b
KR
593 }
594
811bd23e 595 public void unlockSurface() {
a959c53b 596 if(null!=drawable) drawable.getNativeWindow().unlockSurface();
811bd23e 597 else window.unlockSurface();
a959c53b
KR
598 }
599
811bd23e 600 public boolean isSurfaceLocked() {
a959c53b 601 if(null!=drawable) return drawable.getNativeWindow().isSurfaceLocked();
811bd23e 602 return window.isSurfaceLocked();
a959c53b
KR
603 }
604
811bd23e 605 public Exception getLockedStack() {
a959c53b 606 if(null!=drawable) return drawable.getNativeWindow().getLockedStack();
811bd23e 607 return window.getLockedStack();
a959c53b
KR
608 }
609
8883fa88 610 public boolean surfaceSwap() {
611 if(null!=drawable) return drawable.getNativeWindow().surfaceSwap();
612 return super.surfaceSwap();
613 }
614
a959c53b
KR
615 public long getWindowHandle() {
616 if(null!=drawable) return drawable.getNativeWindow().getWindowHandle();
6e599a26 617 return window.getWindowHandle();
a959c53b
KR
618 }
619
620 public long getSurfaceHandle() {
621 if(null!=drawable) return drawable.getNativeWindow().getSurfaceHandle();
6e599a26
SG
622 return window.getSurfaceHandle();
623 }
624
625 public AbstractGraphicsConfiguration getGraphicsConfiguration() {
626 if(null!=drawable) return drawable.getNativeWindow().getGraphicsConfiguration();
627 return window.getGraphicsConfiguration();
628 }
629
630 //----------------------------------------------------------------------
631 // GLDrawable methods
632 //
633
634 public NativeWindow getNativeWindow() {
635 return null!=drawable ? drawable.getNativeWindow() : null;
a959c53b
KR
636 }
637
638 //----------------------------------------------------------------------
639 // GLDrawable methods that are not really needed
640 //
641
642 public GLContext createContext(GLContext shareWith) {
643 return drawable.createContext(shareWith);
644 }
645
646 public void setRealized(boolean realized) {
647 }
648
811bd23e
SG
649 public boolean isRealized() {
650 return ( null != drawable ) ? drawable.isRealized() : false;
651 }
652
a959c53b
KR
653 public GLCapabilities getChosenGLCapabilities() {
654 if (drawable == null) {
655 throw new GLException("No drawable yet");
656 }
657
658 return drawable.getChosenGLCapabilities();
659 }
660
661 public GLProfile getGLProfile() {
662 if (drawable == null) {
663 throw new GLException("No drawable yet");
664 }
665
666 return drawable.getGLProfile();
667 }
a959c53b 668}
http://JogAmp.org git info: FAQ, tutorial and man pages.