Panda3D
subprocessWindow.cxx
Go to the documentation of this file.
1 /**
2  * PANDA 3D SOFTWARE
3  * Copyright (c) Carnegie Mellon University. All rights reserved.
4  *
5  * All use of this software is subject to the terms of the revised BSD
6  * license. You should have received a copy of this license along
7  * with this source code in a file named "LICENSE."
8  *
9  * @file subprocessWindow.cxx
10  * @author drose
11  * @date 2009-07-11
12  */
13 
14 #include "subprocessWindow.h"
15 
16 #ifdef SUPPORT_SUBPROCESS_WINDOW
17 
18 #include "graphicsEngine.h"
19 #include "config_display.h"
20 #include "nativeWindowHandle.h"
21 #include "mouseButton.h"
22 #include "throw_event.h"
23 
24 using std::string;
25 
26 TypeHandle SubprocessWindow::_type_handle;
27 
28 /**
29  * Normally, the SubprocessWindow constructor is not called directly; these
30  * are created instead via the GraphicsEngine::make_output() function.
31  */
32 SubprocessWindow::
33 SubprocessWindow(GraphicsEngine *engine, GraphicsPipe *pipe,
34  const string &name,
35  const FrameBufferProperties &fb_prop,
36  const WindowProperties &win_prop,
37  int flags,
39  GraphicsOutput *host) :
40  GraphicsWindow(engine, pipe, name, fb_prop, win_prop, flags, gsg, host)
41 {
42  _input = GraphicsWindowInputDevice::pointer_and_keyboard(this, "keyboard/mouse");
43  _input_devices.push_back(_input.p());
44 
45  // This will be an offscreen buffer that we use to render the actual
46  // contents.
47  _buffer = nullptr;
48 
49  // Create a texture to receive the contents of the framebuffer from the
50  // offscreen buffer.
51  _texture = new Texture(name);
52 
53  _fd = -1;
54  _mmap_size = 0;
55  _filename = string();
56  _swbuffer = nullptr;
57  _last_event_flags = 0;
58 }
59 
60 /**
61  *
62  */
63 SubprocessWindow::
64 ~SubprocessWindow() {
65  nassertv(_buffer == nullptr);
66  nassertv(_swbuffer == nullptr);
67 }
68 
69 /**
70  * Do whatever processing is necessary to ensure that the window responds to
71  * user events. Also, honor any requests recently made via
72  * request_properties().
73  *
74  * This function is called only within the window thread.
75  */
76 void SubprocessWindow::
77 process_events() {
79 
80  if (_swbuffer != nullptr) {
82  while (_swbuffer->get_event(swb_event)) {
83  // Deal with this event.
84  if (swb_event._flags & SubprocessWindowBuffer::EF_mouse_position) {
85  _input->set_pointer_in_window(swb_event._x, swb_event._y);
86  } else if ((swb_event._flags & SubprocessWindowBuffer::EF_has_mouse) == 0) {
87  _input->set_pointer_out_of_window();
88  }
89 
90  unsigned int diff = swb_event._flags ^ _last_event_flags;
91  _last_event_flags = swb_event._flags;
92 
93  if (diff & SubprocessWindowBuffer::EF_shift_held) {
94  transition_button(swb_event._flags & SubprocessWindowBuffer::EF_shift_held, KeyboardButton::shift());
95  }
96  if (diff & SubprocessWindowBuffer::EF_control_held) {
97  transition_button(swb_event._flags & SubprocessWindowBuffer::EF_control_held, KeyboardButton::control());
98  }
99  if (diff & SubprocessWindowBuffer::EF_alt_held) {
100  transition_button(swb_event._flags & SubprocessWindowBuffer::EF_alt_held, KeyboardButton::alt());
101  }
102  if (diff & SubprocessWindowBuffer::EF_meta_held) {
103  transition_button(swb_event._flags & SubprocessWindowBuffer::EF_meta_held, KeyboardButton::meta());
104  }
105 
106  ButtonHandle button = ButtonHandle::none();
107  if (swb_event._source == SubprocessWindowBuffer::ES_mouse) {
108  button = MouseButton::button(swb_event._code);
109 
110  } else if (swb_event._source == SubprocessWindowBuffer::ES_keyboard) {
111  int keycode;
112  button = translate_key(keycode, swb_event._code, swb_event._flags);
113  if (keycode != 0 && swb_event._type != SubprocessWindowBuffer::ET_button_up) {
114  _input->keystroke(keycode);
115  }
116  }
117 
118  if (swb_event._type == SubprocessWindowBuffer::ET_button_up) {
119  _input->button_up(button);
120  } else if (swb_event._type == SubprocessWindowBuffer::ET_button_down) {
121  _input->button_down(button);
122  }
123  }
124  }
125 }
126 
127 /**
128  * This function will be called within the draw thread before beginning
129  * rendering for a given frame. It should do whatever setup is required, and
130  * return true if the frame should be rendered, or false if it should be
131  * skipped.
132  */
133 bool SubprocessWindow::
134 begin_frame(FrameMode mode, Thread *current_thread) {
135  if (_swbuffer == nullptr || _buffer == nullptr) {
136  return false;
137  }
138 
139  bool result = _buffer->begin_frame(mode, current_thread);
140  return result;
141 }
142 
143 /**
144  * This function will be called within the draw thread after rendering is
145  * completed for a given frame. It should do whatever finalization is
146  * required.
147  */
148 void SubprocessWindow::
149 end_frame(FrameMode mode, Thread *current_thread) {
150  _buffer->end_frame(mode, current_thread);
151 
152  if (mode == FM_render) {
153  _flip_ready = true;
154  }
155 }
156 
157 /**
158  * This function will be called within the draw thread after end_frame() has
159  * been called on all windows, to initiate the exchange of the front and back
160  * buffers.
161  *
162  * This should instruct the window to prepare for the flip at the next video
163  * sync, but it should not wait.
164  *
165  * We have the two separate functions, begin_flip() and end_flip(), to make it
166  * easier to flip all of the windows at the same time.
167  */
168 void SubprocessWindow::
169 begin_flip() {
170  nassertv(_buffer != nullptr);
171  if (_swbuffer == nullptr) {
172  return;
173  }
174 
175  RenderBuffer buffer(_gsg, DrawableRegion::get_renderbuffer_type(RTP_color));
176  buffer = _gsg->get_render_buffer(_buffer->get_draw_buffer_type(),
177  _buffer->get_fb_properties());
178 
179  bool copied =
180  _gsg->framebuffer_copy_to_ram(_texture, 0, -1,
181  _overlay_display_region, buffer);
182 
183  if (copied) {
184  CPTA_uchar image = _texture->get_ram_image();
185  size_t framebuffer_size = _swbuffer->get_framebuffer_size();
186  nassertv(image.size() == framebuffer_size);
187 
188  if (!_swbuffer->ready_for_write()) {
189  // We have to wait for the other end to remove the last frame we
190  // rendered. We only wait so long before we give up, so we don't
191  // completely starve the Python process just because the render window
192  // is offscreen or something.
193 
195  double start = clock->get_real_time();
196  while (!_swbuffer->ready_for_write()) {
198  double now = clock->get_real_time();
199  if (now - start > subprocess_window_max_wait) {
200  // Never mind.
201  return;
202  }
203  }
204  }
205 
206  // We're ready to go. Copy the image to our shared framebuffer.
207  void *target = _swbuffer->open_write_framebuffer();
208  memcpy(target, image.p(), framebuffer_size);
209  _swbuffer->close_write_framebuffer();
210  }
211 }
212 
213 /**
214  * Applies the requested set of properties to the window, if possible, for
215  * instance to request a change in size or minimization status.
216  *
217  * The window properties are applied immediately, rather than waiting until
218  * the next frame. This implies that this method may *only* be called from
219  * within the window thread.
220  *
221  * The properties that have been applied are cleared from the structure by
222  * this function; so on return, whatever remains in the properties structure
223  * are those that were unchanged for some reason (probably because the
224  * underlying interface does not support changing that property on an open
225  * window).
226  */
227 void SubprocessWindow::
228 set_properties_now(WindowProperties &properties) {
229  Filename filename;
230  WindowHandle *window_handle = properties.get_parent_window();
231  if (window_handle != nullptr) {
232  WindowHandle::OSHandle *os_handle = window_handle->get_os_handle();
233  if (os_handle != nullptr) {
234  if (os_handle->is_of_type(NativeWindowHandle::SubprocessHandle::get_class_type())) {
235  NativeWindowHandle::SubprocessHandle *subprocess_handle = DCAST(NativeWindowHandle::SubprocessHandle, os_handle);
236  filename = subprocess_handle->get_filename();
237  }
238  }
239  }
240 
241  if (!filename.empty() && filename != _filename) {
242  // We're changing the subprocess buffer filename; that means we might as
243  // well completely close and re-open the window.
244  display_cat.info() << "Re-opening SubprocessWindow\n";
245  internal_close_window();
246 
247  _properties.add_properties(properties);
248  properties.clear();
249 
250  internal_open_window();
251  set_size_and_recalc(_properties.get_x_size(), _properties.get_y_size());
252  throw_event(get_window_event(), this);
253  return;
254  }
255 
257  if (!properties.is_any_specified()) {
258  // The base class has already handled this case.
259  return;
260  }
261 
262  if (properties.has_parent_window()) {
263  // Redundant parent-window specification.
264  properties.clear_parent_window();
265  }
266 }
267 
268 /**
269  * Closes the window right now. Called from the window thread.
270  */
271 void SubprocessWindow::
272 close_window() {
273  internal_close_window();
274 
275  WindowProperties properties;
276  properties.set_open(false);
277  properties.set_foreground(false);
278  system_changed_properties(properties);
279 }
280 
281 /**
282  * Opens the window right now. Called from the window thread. Returns true
283  * if the window is successfully opened, or false if there was a problem.
284  */
285 bool SubprocessWindow::
286 open_window() {
287  if (!internal_open_window()) {
288  return false;
289  }
290 
291  WindowProperties properties;
292  properties.set_open(true);
293  properties.set_foreground(true);
294  system_changed_properties(properties);
295 
296  return true;
297 }
298 
299 /**
300  * Closes the "window" and resets the buffer, without changing the
301  * WindowProperties.
302  */
303 void SubprocessWindow::
304 internal_close_window() {
305  if (_swbuffer != nullptr) {
307  (_fd, _mmap_size, _filename.to_os_specific(), _swbuffer);
308  _fd = -1;
309  _filename = string();
310 
311  _swbuffer = nullptr;
312  }
313 
314  if (_buffer != nullptr) {
315  _buffer->request_close();
316  _buffer->process_events();
317  _engine->remove_window(_buffer);
318  _buffer = nullptr;
319  }
320 
321  // Tell our parent window (if any) that we're no longer its child.
322  if (_window_handle != nullptr &&
323  _parent_window_handle != nullptr) {
324  _parent_window_handle->detach_child(_window_handle);
325  }
326 
327  _window_handle = nullptr;
328  _parent_window_handle = nullptr;
329  _is_valid = false;
330 }
331 
332 /**
333  * Opens the "window" and the associated offscreen buffer, without changing
334  * the WindowProperties.
335  */
336 bool SubprocessWindow::
337 internal_open_window() {
338  nassertr(_buffer == nullptr, false);
339 
340  // Create a buffer with the same properties as the window.
341  int flags = _creation_flags;
342  flags = ((flags & ~GraphicsPipe::BF_require_window) | GraphicsPipe::BF_refuse_window);
343  WindowProperties win_props = WindowProperties::size(_properties.get_x_size(), _properties.get_y_size());
344 
345  GraphicsOutput *buffer =
346  _engine->make_output(_pipe, _name, 0, _fb_properties, win_props,
347  flags, _gsg, _host);
348  if (buffer != nullptr) {
349  _buffer = DCAST(GraphicsBuffer, buffer);
350  // However, the buffer is not itself intended to be rendered. We only
351  // render it indirectly, via callbacks in here.
352  _buffer->set_active(false);
353 
354  _buffer->request_open();
355  _buffer->process_events();
356 
357  _is_valid = _buffer->is_valid();
358  }
359 
360  if (!_is_valid) {
361  display_cat.error()
362  << "Failed to open SubprocessWindowBuffer's internal offscreen buffer.\n";
363  return false;
364  }
365 
366  _gsg = _buffer->get_gsg();
367 
368  WindowHandle *window_handle = _properties.get_parent_window();
369  if (window_handle != nullptr) {
370  WindowHandle::OSHandle *os_handle = window_handle->get_os_handle();
371  if (os_handle != nullptr) {
372  if (os_handle->is_of_type(NativeWindowHandle::SubprocessHandle::get_class_type())) {
373  NativeWindowHandle::SubprocessHandle *subprocess_handle = DCAST(NativeWindowHandle::SubprocessHandle, os_handle);
374  _filename = subprocess_handle->get_filename();
375  }
376  }
377  }
378  _parent_window_handle = window_handle;
379 
380  if (_filename.empty()) {
381  _is_valid = false;
382  display_cat.error()
383  << "No filename given to SubprocessWindow.\n";
384  return false;
385  }
386 
388  (_fd, _mmap_size, _filename.to_os_specific());
389 
390  if (_swbuffer == nullptr) {
391  close(_fd);
392  _fd = -1;
393  _filename = string();
394  _is_valid = false;
395  display_cat.error()
396  << "Failed to open SubprocessWindowBuffer's shared-memory buffer "
397  << _filename << "\n";
398  return false;
399  }
400 
401  if (display_cat.is_debug()) {
402  display_cat.debug()
403  << "SubprocessWindow reading " << _filename << "\n";
404  }
405 
406  // Create a WindowHandle for ourselves
407  _window_handle = NativeWindowHandle::make_subprocess(_filename);
408 
409  // And tell our parent window that we're now its child.
410  if (_parent_window_handle != nullptr) {
411  _parent_window_handle->attach_child(_window_handle);
412  }
413 
414  return true;
415 }
416 
417 /**
418  * Converts the os-specific keycode into the appropriate ButtonHandle object.
419  * Also stores the corresponding Unicode keycode in keycode, if any; or 0
420  * otherwise.
421  */
422 ButtonHandle SubprocessWindow::
423 translate_key(int &keycode, int os_code, unsigned int flags) const {
424  keycode = 0;
425  ButtonHandle nk = ButtonHandle::none();
426 
427 #ifdef __APPLE__
428  switch ((os_code >> 8) & 0xff) {
429  case 0: nk = KeyboardButton::ascii_key('a'); break;
430  case 11: nk = KeyboardButton::ascii_key('b'); break;
431  case 8: nk = KeyboardButton::ascii_key('c'); break;
432  case 2: nk = KeyboardButton::ascii_key('d'); break;
433  case 14: nk = KeyboardButton::ascii_key('e'); break;
434  case 3: nk = KeyboardButton::ascii_key('f'); break;
435  case 5: nk = KeyboardButton::ascii_key('g'); break;
436  case 4: nk = KeyboardButton::ascii_key('h'); break;
437  case 34: nk = KeyboardButton::ascii_key('i'); break;
438  case 38: nk = KeyboardButton::ascii_key('j'); break;
439  case 40: nk = KeyboardButton::ascii_key('k'); break;
440  case 37: nk = KeyboardButton::ascii_key('l'); break;
441  case 46: nk = KeyboardButton::ascii_key('m'); break;
442  case 45: nk = KeyboardButton::ascii_key('n'); break;
443  case 31: nk = KeyboardButton::ascii_key('o'); break;
444  case 35: nk = KeyboardButton::ascii_key('p'); break;
445  case 12: nk = KeyboardButton::ascii_key('q'); break;
446  case 15: nk = KeyboardButton::ascii_key('r'); break;
447  case 1: nk = KeyboardButton::ascii_key('s'); break;
448  case 17: nk = KeyboardButton::ascii_key('t'); break;
449  case 32: nk = KeyboardButton::ascii_key('u'); break;
450  case 9: nk = KeyboardButton::ascii_key('v'); break;
451  case 13: nk = KeyboardButton::ascii_key('w'); break;
452  case 7: nk = KeyboardButton::ascii_key('x'); break;
453  case 16: nk = KeyboardButton::ascii_key('y'); break;
454  case 6: nk = KeyboardButton::ascii_key('z'); break;
455 
456  // top row numbers
457  case 29: nk = KeyboardButton::ascii_key('0'); break;
458  case 18: nk = KeyboardButton::ascii_key('1'); break;
459  case 19: nk = KeyboardButton::ascii_key('2'); break;
460  case 20: nk = KeyboardButton::ascii_key('3'); break;
461  case 21: nk = KeyboardButton::ascii_key('4'); break;
462  case 23: nk = KeyboardButton::ascii_key('5'); break;
463  case 22: nk = KeyboardButton::ascii_key('6'); break;
464  case 26: nk = KeyboardButton::ascii_key('7'); break;
465  case 28: nk = KeyboardButton::ascii_key('8'); break;
466  case 25: nk = KeyboardButton::ascii_key('9'); break;
467 
468  // key pad ... do they really map to the top number in panda ?
469  case 82: nk = KeyboardButton::ascii_key('0'); break;
470  case 83: nk = KeyboardButton::ascii_key('1'); break;
471  case 84: nk = KeyboardButton::ascii_key('2'); break;
472  case 85: nk = KeyboardButton::ascii_key('3'); break;
473  case 86: nk = KeyboardButton::ascii_key('4'); break;
474  case 87: nk = KeyboardButton::ascii_key('5'); break;
475  case 88: nk = KeyboardButton::ascii_key('6'); break;
476  case 89: nk = KeyboardButton::ascii_key('7'); break;
477  case 91: nk = KeyboardButton::ascii_key('8'); break;
478  case 92: nk = KeyboardButton::ascii_key('9'); break;
479 
480  // case 36: nk = KeyboardButton::ret(); break; no return in panda ???
481  case 49: nk = KeyboardButton::space(); break;
482  case 51: nk = KeyboardButton::backspace(); break;
483  case 48: nk = KeyboardButton::tab(); break;
484  case 53: nk = KeyboardButton::escape(); break;
485  case 76: nk = KeyboardButton::enter(); break;
486  case 36: nk = KeyboardButton::enter(); break;
487 
488  case 123: nk = KeyboardButton::left(); break;
489  case 124: nk = KeyboardButton::right(); break;
490  case 125: nk = KeyboardButton::down(); break;
491  case 126: nk = KeyboardButton::up(); break;
492  case 116: nk = KeyboardButton::page_up(); break;
493  case 121: nk = KeyboardButton::page_down(); break;
494  case 115: nk = KeyboardButton::home(); break;
495  case 119: nk = KeyboardButton::end(); break;
496  case 114: nk = KeyboardButton::help(); break;
497  case 117: nk = KeyboardButton::del(); break;
498 
499  // case 71: nk = KeyboardButton::num_lock() break;
500 
501  case 122: nk = KeyboardButton::f1(); break;
502  case 120: nk = KeyboardButton::f2(); break;
503  case 99: nk = KeyboardButton::f3(); break;
504  case 118: nk = KeyboardButton::f4(); break;
505  case 96: nk = KeyboardButton::f5(); break;
506  case 97: nk = KeyboardButton::f6(); break;
507  case 98: nk = KeyboardButton::f7(); break;
508  case 100: nk = KeyboardButton::f8(); break;
509  case 101: nk = KeyboardButton::f9(); break;
510  case 109: nk = KeyboardButton::f10(); break;
511  case 103: nk = KeyboardButton::f11(); break;
512  case 111: nk = KeyboardButton::f12(); break;
513 
514  case 105: nk = KeyboardButton::f13(); break;
515  case 107: nk = KeyboardButton::f14(); break;
516  case 113: nk = KeyboardButton::f15(); break;
517  case 106: nk = KeyboardButton::f16(); break;
518 
519  // shiftable chartablet
520  case 50: nk = KeyboardButton::ascii_key('`'); break;
521  case 27: nk = KeyboardButton::ascii_key('-'); break;
522  case 24: nk = KeyboardButton::ascii_key('='); break;
523  case 33: nk = KeyboardButton::ascii_key('['); break;
524  case 30: nk = KeyboardButton::ascii_key(']'); break;
525  case 42: nk = KeyboardButton::ascii_key('\\'); break;
526  case 41: nk = KeyboardButton::ascii_key(';'); break;
527  case 39: nk = KeyboardButton::ascii_key('\''); break;
528  case 43: nk = KeyboardButton::ascii_key(','); break;
529  case 47: nk = KeyboardButton::ascii_key('.'); break;
530  case 44: nk = KeyboardButton::ascii_key('/'); break;
531 
532  default:
533  // Punt.
534  nk = KeyboardButton::ascii_key(os_code & 0xff);
535  }
536 
537  if (nk.has_ascii_equivalent()) {
538  // If we assigned an ASCII button, then get the original ASCII code from
539  // the event (it will include shift et al).
540 
541  // TODO: is it possible to get any international characters via this old
542  // EventRecord interface?
543  keycode = os_code & 0xff;
544  }
545 
546 #endif // __APPLE__
547 
548  return nk;
549 }
550 
551 /**
552  * Sends the appropriate up/down transition for the indicated modifier key, as
553  * determined implicitly from the flags.
554  */
555 void SubprocessWindow::
556 transition_button(unsigned int flags, ButtonHandle button) {
557  if (flags) {
558  _input->button_down(button);
559  } else {
560  _input->button_up(button);
561  }
562 }
563 
564 
565 #endif // SUPPORT_SUBPROCESS_WINDOW
A ButtonHandle represents a single button from any device, including keyboard buttons and mouse butto...
Definition: buttonHandle.h:26
has_ascii_equivalent
Returns true if the button was created with an ASCII equivalent code (e.g.
Definition: buttonHandle.h:63
A ClockObject keeps track of elapsed real time and discrete time.
Definition: clockObject.h:58
get_real_time
Returns the actual number of seconds elapsed since the ClockObject was created, or since it was last ...
Definition: clockObject.h:92
static ClockObject * get_global_clock()
Returns a pointer to the global ClockObject.
Definition: clockObject.I:215
const Element * p() const
Function p() is similar to the function from ConstPointerTo.
static int get_renderbuffer_type(int plane)
Returns the RenderBuffer::Type that corresponds to a RenderTexturePlane.
The name of a file, such as a texture file or an Egg file.
Definition: filename.h:39
A container for the various kinds of properties we might ask to have on a graphics frameBuffer before...
An offscreen buffer for rendering into.
This class is the main interface to controlling the render process.
This is a base class for the various different classes that represent the result of a frame of render...
An object to create GraphicsOutputs that share a particular 3-D API.
Definition: graphicsPipe.h:52
Encapsulates all the communication with a particular instance of a given rendering backend.
A window, fullscreen or on a desktop, into which a graphics device sends its output for interactive d...
virtual void process_events()
Do whatever processing is necessary to ensure that the window responds to user events.
virtual void set_properties_now(WindowProperties &properties)
Applies the requested set of properties to the window, if possible, for instance to request a change ...
static ButtonHandle ascii_key(char ascii_equivalent)
Returns the ButtonHandle associated with the particular ASCII character, if there is one,...
static ButtonHandle button(int button_number)
Returns the ButtonHandle associated with the particular numbered mouse button (zero-based),...
Definition: mouseButton.cxx:32
A RenderBuffer is an arbitrary subset of the various layers (depth buffer, color buffer,...
Definition: renderBuffer.h:27
static void close_buffer(int fd, size_t mmap_size, const std::string &filename, SubprocessWindowBuffer *buffer)
Closes a buffer object created via a previous call to open_buffer().
static SubprocessWindowBuffer * open_buffer(int &fd, size_t &mmap_size, const std::string &filename)
Call this method to open a reference to an existing buffer in shared memory space.
Represents a texture object, which is typically a single 2-d image but may also represent a 1-d or 3-...
Definition: texture.h:71
A thread; that is, a lightweight process.
Definition: thread.h:46
static void force_yield()
Suspends the current thread for the rest of the current epoch.
Definition: thread.I:201
TypeHandle is the identifier used to differentiate C++ class types.
Definition: typeHandle.h:81
bool is_of_type(TypeHandle handle) const
Returns true if the current object is or derives from the indicated type.
Definition: typedObject.I:28
This object represents a window on the desktop, not necessarily a Panda window.
Definition: windowHandle.h:34
get_os_handle
Returns the OS-specific handle stored internally to the WindowHandle wrapper.
Definition: windowHandle.h:44
A container for the various kinds of properties we might ask to have on a graphics window before we o...
static WindowProperties size(const LVecBase2i &size)
Returns a WindowProperties structure with only the size specified.
has_parent_window
Checks the S_parent_window specification from the properties.
clear_parent_window
Removes the S_parent_window specification from the properties.
void clear()
Unsets all properties that have been specified so far, and resets the WindowProperties structure to i...
bool is_any_specified() const
Returns true if any properties have been specified, false otherwise.
set_foreground
Specifies whether the window should be opened in the foreground (true), or left in the background (fa...
set_open
Specifies whether the window should be open.
get_parent_window
Returns the parent window specification, or NULL if there is no parent window specified.
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.