-
Notifications
You must be signed in to change notification settings - Fork 0
/
layout.py
506 lines (472 loc) · 19.6 KB
/
layout.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
'''
.. mindscape -- Mindscape Engine
layout -- Layout Manager
========================
This module provides an interface to a simple layout manager which is entirely
compatible with the scenegraph (provided that it is rendered after 3D geometry
and with the depth test turned off). The layout manager is a *tiling* manager,
which means that it deals with :class:`Grid`\ s.
'''
import pygame
from pygame.locals import *
from OpenGL.GL import *
from vmath import Vector
from scenegraph import Renderable, Texture
from event import EVENT, KBD, MOUSE
from log import main, DV1, DV2, DV3, obCode
logger=main.getChild('layout')
class LayoutCell(object):
'''A :class:`LayoutCell` represents a single cell in a layout facility. In
particular, once computations have been undergone by the manager holding (a set)
of these cells, the :attr:`offset` and :attr:`size` attributes should represent
the (window coordinate) space that this cell takes up, on one dimension. (Two
cells are needed for two dimensions.)'''
def __init__(self, weight=1.0, fixed=0):
#: A floating point number indicating how much space this cell should be given relative to other cells in the same layout.
self.weight=weight
#: A numeric amount of fixed space this cell must contain (measured in the window coordinate system).
self.fixed=fixed
#: The offset from the origin on the dimension specifying this cell (-1 if not computed or not in a layout).
self.offset=-1
#: The size of this cell on this dimension (-1 if not computed or not in a layout).
self.size=-1
class LayoutVector(object):
'''A :class:`LayoutVector` contains a vector (well, a list) of cells arranged
along one dimension. The layout may be computed against a dimensional measure
(in any coordinate system, though typically the window coordinate system is used
for compatibility with the rest of this module), and the :attr:`LayoutCell.offset`
and :attr:`LayoutCell.size` attributes will be set appropriately.
The constructor may be given either one parameter (the number of :class:`LayoutCell`\ s
to construct), or an arbitrary number of positional parameters containing
:class:`LayoutCell`\ s or derivatives thereof (or, really, any object implementing
that interface).
Furthermore, ``__getitem__``, ``__len__``, and ``__iter__`` are implemented,
and defer calls to the underlying cell collection.'''
def __init__(self, *cells):
if len(cells)==1 and isinstance(cells[0], (int, long)):
self.cells=[LayoutCell() for i in xrange(cells[0])]
else:
self.cells=list(cells)
def Compute(self, dim):
'''Compute the layout of the cells along this dimension, assuming a size
along this dimensions of the argument given.'''
dim-=sum([i.fixed for i in self.cells]) #Remove fixed allocations from weighting
wtotal=sum([i.weight for i in self.cells])
offset=0
for cell in self.cells:
cell.offset=offset
cell.size=cell.fixed+(dim*cell.weight/wtotal)
offset+=cell.size
def __getitem__(self, idx):
return self.cells[idx]
def __len__(self):
return len(self.cells)
def __iter__(self):
return iter(self.cells)
class Grid(object):
'''A :class:`Grid` is basically just two :class:`LayoutVector`\ s on
perpendicular axes, with a convenience function (:func:`CellPair`) to return
the two cells (as a tuple) at the specified indices.
The constructor expects two parameters, which may either be integers (specifying
the number of cells to construct on that axis) or a :class:`LayoutVector`, subclass,
or implementor of that interface.'''
def __init__(self, rows, cols):
#: A :class:`LayoutVector` specifying layout along the Y axis.
self.rows=(LayoutVector(rows) if isinstance(rows, (int, long)) else rows)
#: A :class:`LayoutVector` specifyinh layout along the X axis.
self.cols=(LayoutVector(cols) if isinstance(cols, (int, long)) else cols)
def Compute(self, dims):
'''Compute all :class:`LayoutVector`\ s from the dimension :class:`vmath.Vector`
given.'''
self.rows.Compute(dims.y)
self.cols.Compute(dims.x)
def CellPair(self, x, y):
'''Returns a tuple ``(:class:`LayoutCell`, :class:`LayoutCell`)`` as
specified by the ``x`` and ``y`` parameters.'''
return self.cols[x], self.rows[y]
def CellsAt(self, pos):
'''Returns a tuple ``(:class:`LayoutCell`, :class:`LayoutCell`)`` that
represents the cell pair at the given position in the layout held by this
:class:`Grid`. This may be used for various sorts of hit-testing.'''
xcell=None
ycell=None
for cell in self.cols:
if pos.x>=cell.offset and pos.x<cell.offset+cell.size:
xcell=cell
for cell in self.rows:
if pos.y>=cell.offset and pos.y<cell.offset+cell.size:
ycell=cell
return xcell, ycell
class Widget(Renderable):
'''A :class:`Widget` is a special type of :class:`scenegraph.Renderable`
that expects to be in an environment where:
* The projection and modelview matrices are identity, giving a precise mapping
between input (vertex) coordinates and normalized device coordinates, *but*
* the viewport is set so that the rendering bounds are only a small portion of
the window--specified by the cell pair given.
As such, the :func:`scenegraph.Renderable.PushState` and :func:`scenegraph.Renderable.PopState`
functions are overriden (such that the :attr:`scenegraph.Renderable.enable`,
:attr:`scenegraph.Renderable.disable`, and :attr:`scenegraph.Renderable.modifications`
will *not* work). Their primary job is to set up the viewport, as specified
above. (The matrices are assumed to be identity beforehand, an assumption usually
guaranteed by an enclosing :class:`Container`).
The ``xcell`` and ``ycell`` parameters should be set to a cell on corresponding
layout axes.'''
def __init__(self, xcell, ycell, fcol=None, bcol=None, **kwargs):
super(Widget, self).__init__(**kwargs)
#: A :class:`LayoutCell` along the x axis.
self.xcell=xcell
#: A :class:`LayoutCell` along the y axis.
self.ycell=ycell
#: A :class:`vmath.Vector` cotaining the foreground color, or ``None`` (whose application differs per widget).
self.fcol=fcol
#: A :class:`vmath.Vector` containing the background color, or ``None`` (whose application differs per widget).
self.bcol=bcol
@property
def pos(self):
'''A 2D :class:`vmath.Vector` containing the cell positions.'''
return Vector(self.xcell.offset, self.ycell.offset)
@property
def size(self):
'''A 2D :class:`vmath.Vector` containing the cell sizes.'''
return Vector(self.xcell.size, self.ycell.size)
def PushState(self):
'''Initialize the state (basically, push and set the viewport).
.. note::
This does not respect any of the other :class:`scenegraph.Renderable`
attributes, including :attr:`scenegraph.Renderable.enable`,
:attr:`scenegraph.Renderable.disable`, :attr:`scenegraph.Renderable.modifications`,
and so on.'''
glPushAttrib(GL_VIEWPORT_BIT)
glViewport(*([int(i) for i in self.pos]+[int(i) for i in self.size]))
def PopState(self):
'''Reset the state (basically, pop the viewport).'''
glPopAttrib()
class Container(Widget):
'''A :class:`Container` is a :class:`Widget` that contains other :class:`Widget`\ s.
In particular, a :class:`Container` is special in that it can accept an
:attr:`Widget.xcell` and :attr:`Widget.ycell` value of ``None``, implying that
this container is the *top-level container*, which causes it to do useful duties
(like set up the identity matrices; see :class:`Widget`).
Containers always have a layout system--presently, :attr:`grid` (though the name
is subject to change). It is logical (but not required) to put :class:`Widget`\ s
that are in this layout system as children of the :class:`Container`. Other
:class:`scenegraph.Renderable`\ s should not be made children of :class:`Container`\ s
due to the odd circumstances under which :class:`Widget`\ s are rendered.'''
def __init__(self, grid, xcell=None, ycell=None, **kwargs):
super(Container, self).__init__(xcell, ycell, **kwargs)
#: The :class:`Grid` representing the layout.
self.grid=grid
#: A :class:`Widget` that will receive all :attr:`event.EVENT.KBD` events, or ``None`` if they are to propagate normally (to all children).
self.focus=None
#: A :class:`Widget` that will receive all :attr:`event.EVENT.MOUSE` events, or ``None`` if they are to propagate normally (by position).
#:
#: .. note::
#:
#: Events thusly grabbed are still in the :class:`Widget`'s local coordinate space.
#: This means you may see negative values.
self.grab=None
def PushState(self):
'''Initialize the state. Depending on whether this is a top-level
container, this may initialize the matrices (without affecting the viewport),
or it may just set a viewport as with the usual :func:`Widget.PushState`.'''
if self.xcell is None or self.ycell is None:
#Initialize this as if we are a master layout (we probably are)
glMatrixMode(GL_PROJECTION)
glPushMatrix()
glLoadIdentity()
glMatrixMode(GL_MODELVIEW)
glPushMatrix()
glLoadIdentity()
self.grid.Compute(Vector(*(glGetIntegerv(GL_VIEWPORT)[2:])))
else:
self.grid.Compute(self.size)
super(Container, self).PushState() #Just do what every other widget does
def PopState(self):
'''Reverts the state, undoing the actions done during :func:`PushState`.'''
if self.xcell is None or self.ycell is None:
glMatrixMode(GL_PROJECTION)
glPopMatrix()
glMatrixMode(GL_MODELVIEW)
glPopMatrix()
else:
super(Container, self).PopState()
def Render(self):
'''Render the :class:`Container` (which actually does nothing but
renders its children via :func:`scenegraph.Renderable.RenderChildren`.'''
self.RenderChildren()
def ChildAt(self, pos):
'''Returns a :class:`Widget` at the position specified, if one exists
there; otherwise, returns ``None``.'''
xc, yc=self.grid.CellsAt(pos)
if xc is None or yc is None:
return None
for child in self.children:
if child.xcell is xc and child.ycell is yc:
return child
return None #Explicit is better than implicit.
def TriggerChildren(self, ev):
'''Overrides the default propagation behavior by ensuring that
:class:`Event` objects with a ``pos`` attribute are dispatched only to
:class:`Widget` objects in that position, as well as obeying the :attr:`focus`
and :attr:`grab` attributes, if they are set.
.. note::
This special naming convention is all-inclusive; *any* :class:`Event` with
a ``pos`` attribute will be propagated thusly, and any without a ``pos``
attribute will simply propagate to all children, as usual. This means you
can use the same behavior in your position-sensitive events, as well.'''
if ev.type==EVENT.MOUSE and self.grab is not None:
## print 'Grabbed event sent to', self.grab
self.grab.Trigger(ev)
return
if ev.type==EVENT.KBD and self.focus is not None:
self.focus.Trigger(ev)
return
if hasattr(ev, 'pos'):
child=self.ChildAt(ev.pos)
if child is not None:
## print 'Pre event prop:', ev.pos
ev.pos-=child.pos #Relative coordinate
## print 'Child pos:', child.pos
## print 'Post event prop:', ev.pos
child.Trigger(ev)
else:
super(Container, self).TriggerChildren(ev)
def SetFocus(self, focus=None):
'''Sets the :attr:`focus` attribute.
.. note::
You should call this instead of setting the attribute directly, as doing that
will silently fail if the :class:`Widget` is not a :class:`Container`--which
can lead to unexpected behavior. Using this invalidly will instead raise an
AttributeError--which will help with debugging!'''
self.focus=focus
def SetGrab(self, grab=None):
'''Sets the :attr:`grab` attribute.
.. note::
See :func:`SetFocus`.'''
self.grab=grab
## print 'Grab set to', grab
class ALIGN:
'''An enumeration class of legal values for :attr:`Label.align` and similar
attributes in derived classes.'''
#: Align the left edge with the left edge of the cell.
LEFT=0x01
#: Center the object in the cell horizontally.
#:
#: .. note::
#:
#: This is the default, so its value is 0, and it is not effective with the other attributes.
CENTER=0x00
#: Align the right edge with the right edge of the cell.
RIGHT=0x02
#: Align the top edge with the top edge of the cell.
TOP=0x04
#: Center the object in the cell vertically.
#:
#: .. note::
#:
#: This is the default, so its value is 0, and it is not effective with the other attributes.
MIDDLE=0x00
#: Align the bottom edge with the bottom edge of the cell.
BOTTOM=0x08
#: Fill the entire horizontal expanse, stretching the object if necessary. (Equivalent to LEFT|RIGHT.)
FILLX=LEFT|RIGHT
#: Fill the entire vertical expanse, stretching the object if necessary. (Equivalent to TOP|BOTTOM.)
FILLY=TOP|BOTTOM
class Label(Widget):
'''The :class:`Label` is a :class:`Widget` designed to display text.
Since this function is useful to some other :class:`Widget`\ s, it's also the
base class for a few.'''
def __init__(self, xcell, ycell, text='', align=0, font=None, **kwargs):
super(Label, self).__init__(xcell, ycell, **kwargs)
#: A string containing the text to display.
self.text=text
self._oldtext=None
#: The alignment mode (a :class:`ALIGN` bit mask).
self.align=align
#: The ``pygame.Font`` object to use for rendering.
self.font=(pygame.font.SysFont(pygame.font.get_default_font(), 30) if font is None else font)
#: The :class:`scenegraph.Texture` used to store the font texture.
self.tex=None
if self.text:
self.Update()
def Update(self, text=None):
'''Updates the Label, drawing the new :attr:`text` to the :attr:`tex`
:class:`scenegraph.Texture`.
.. note::
This is done automatically whenever :attr:`text` is no longer determined to
be the same object as before. Since strings in Python are immutable, this
must be the case if the value is no longer the same as well. However, this
process is *not* performed if the :attr:`Widget.fcol` attribute is changed
(which determines the font color), and so it must be either called manually
from code that does this, or the :attr:`text` attribute must be changed to
a different object. A trivial way of performing the latter is assigning
``label.text=str(label.text)``.
This behavior may change in later versions to update when the color is changed
as well.'''
if text is None:
text=self.text
fcol=self.fcol
if fcol is None:
fcol=Vector(1, 1, 1)
tsurf=self.font.render(text, True, tuple(255*fcol.FastTo3()))
if self.tex is None:
self.tex=Texture()
self.tex.surf=tsurf
self.tex.Reload()
def Render(self):
'''Renders the label using the current viewport.'''
glPushAttrib(GL_ENABLE_BIT)
glDisable(GL_TEXTURE_2D)
glDisable(GL_DEPTH_TEST)
if self.bcol is not None:
glColor4d(*self.bcol.FastTo4())
glRectdv((-1, -1), (1, 1))
if self.text:
if self.text is not self._oldtext:
self.Update()
self._oldtext=self.text
self.RenderText()
glPopAttrib()
def RenderText(self):
'''Renders the text--a process which is usable by subclasses as needed.'''
glPushAttrib(GL_ENABLE_BIT)
tsz=Vector(*self.tex.surf.get_size())
vsz=Vector(*(glGetIntegerv(GL_VIEWPORT)[2:]))
csz=tsz/vsz
minima=-csz
maxima=csz.copy()
if self.align&ALIGN.LEFT:
minima.x=-1
maxima.x-=1-csz.x
if self.align&ALIGN.RIGHT:
maxima.x=1
if not self.align&ALIGN.LEFT:
minima.x+=1-csz.x
if self.align&ALIGN.BOTTOM:
minima.y=-1
maxima.y-=1-csz.y
if self.align&ALIGN.TOP:
maxima.y=1
if not self.align&ALIGN.BOTTOM:
minima.y+=1-csz.y
with self.tex:
glColor4d(1, 1, 1, 1)
glBegin(GL_QUADS)
glTexCoord2d(0, 0)
glVertex2d(minima.x, minima.y)
glTexCoord2d(1, 0)
glVertex2d(maxima.x, minima.y)
glTexCoord2d(1, 1)
glVertex2d(maxima.x, maxima.y)
glTexCoord2d(0, 1)
glVertex2d(minima.x, maxima.y)
glEnd()
glPopAttrib()
class ORIENT:
'''An enumeration of legal values for the :attr:`Slider.orient` attribute.'''
#: Orient horizontally, along the X axis.
HORIZONTAL=0
#: Orient vertically, along the Y axis.
VERTICAL=1
class Slider(Label):
'''A :class:`Slider` is a type of widget that allows a user to manipulate a
value by providing an axis between two extreme values; the user is expected to
use the mouse to select the value as a point within this range; the value is
readable as the :attr:`value` attribute.'''
def __init__(self, xcell, ycell, value=0, min=0, max=1, mapfunc=None, showval=True, orient=ORIENT.HORIZONTAL, hwidth=0.05, hcol=None, kmove=0.05, **kwargs):
super(Slider, self).__init__(xcell, ycell, **kwargs)
#: The actual value of this :class:`Slider`, as modified by the user (and possibly mapped by :attr:`mapfunc`).
self.value=value
self._oldvalue=None
#: The smallest value this should contain.
self.min=min
#: The largest value this should contain.
self.max=max
#: A function called with the raw, floating-point value from the user input, whose return is mapped to :attr:`value`.
self.mapfunc=mapfunc
#: True to display the value as a text object (which obeys :class:`Label` attributes like :attr:`Label.align`).
self.showval=showval
#: The orientation (a :class:`ORIENT` constant).
self.orient=orient
#: The floating-point width of the slide handle in normalized device coordinates (where 1 would be half the viewport).
self.hwidth=hwidth
#: A 4D :class:`vmath.Vector` color of the slide handle (or ``None`` for the default transparent gray).
self.hcol=hcol
#: A floating-point in the range [0, 1] specifying how much a keypress should change the value (as a fraction of the entire range).
self.kmove=kmove
@property
def range(self):
'''The computed difference between :attr:`max` and :attr:`min`.'''
return self.max-self.min
@property
def ratio(self):
'''The position of :attr:`value` normalized such that the minimal point
is 0 and the maximal point is 1.'''
return (self.value-self.min)/self.range
def Map(self, n):
'''Maps a value ``n`` in the range [0, 1] to a value between [min, max].'''
return (n*self.range)+self.min
def ClampValue(self):
'''Clamps :attr:`value` to the range [:attr:`min`, :attr:`max`].
.. note::
If [min, max] is an invalid interval, the value is always set to :attr:`max`.'''
if self.value<self.min:
self.value=self.min
if self.value>self.max:
self.value=self.max
@staticmethod
def Step(n):
'''Produces a lambda function that may be used as a value for :attr:`mapfunc`.
The return function will produce values ``x`` such that ``x*n`` is an integer
(barring round-off error), or, alternatively, will clamp values to ``1/n`` increments.
Calling this with ``n==1`` will result in an analogue to the ``int`` function, which
should probably be used instead.'''
return lambda x, n=n: int(x*n)/float(n)
def Render(self):
'''Renders the slider.'''
glPushAttrib(GL_ENABLE_BIT)
glDisable(GL_DEPTH_TEST)
glDisable(GL_TEXTURE_2D)
if self.bcol is not None:
glColor4d(*self.bcol.FastTo4())
glRectdv((-1, -1), (1, 1))
if self.showval and self.value!=self._oldvalue:
self.text=str(self.value)
self.Update()
self.RenderText()
hcol=self.hcol
if hcol is None:
hcol=Vector(0.5, 0.5, 0.5, 0.5)
glColor4d(*hcol.FastTo4())
pos=self.ratio*2-1
if self.orient==ORIENT.HORIZONTAL:
glRectdv((pos-self.hwidth, -1), (pos+self.hwidth, 1))
else:
glRectdv((-1, pos-self.hwidth), (1, pos+self.hwidth))
glPopAttrib()
def Handle(self, ev):
if ev.type==EVENT.MOUSE:
## print 'Mouse event:', ev
if ev.subtype==MOUSE.BUTTONDOWN and ev.button==0:
self.parent.SetGrab(self)
self.parent.SetFocus(self)
elif ev.subtype==MOUSE.MOVE and ev.buttons[0]:
if self.orient==ORIENT.HORIZONTAL:
ratio=ev.pos.x/self.size.x
else:
ratio=ev.pos.y/self.size.y
self.value=self.Map(ratio)
self.ClampValue()
elif ev.subtype==MOUSE.BUTTONUP and ev.button==0:
self.parent.SetGrab()
elif ev.type==EVENT.KBD:
if ev.subtype==KBD.KEYDOWN:
delta=0
if ev.key in (K_RIGHT, K_UP):
delta=self.range*self.kmove
elif ev.key in (K_LEFT, K_DOWN):
delta=self.range*-self.kmove
if delta:
self.value+=delta
self.ClampValue()