]>
Commit | Line | Data |
---|---|---|
1 | #---------------------------------------------------------------------- | |
2 | # Name: sized_controls.py | |
3 | # Purpose: Implements default, HIG-compliant sizers under the hood | |
4 | # and provides a simple interface for customizing those sizers. | |
5 | # | |
6 | # Author: Kevin Ollivier | |
7 | # | |
8 | # Created: 26-May-2006 | |
9 | # Copyright: (c) 2006 Kevin Ollivier | |
10 | # Licence: wxWindows license | |
11 | #---------------------------------------------------------------------- | |
12 | ||
13 | import wx | |
14 | ||
15 | # For HIG info: links to all the HIGs can be found here: | |
16 | # http://en.wikipedia.org/wiki/Human_Interface_Guidelines | |
17 | ||
18 | ||
19 | # useful defines for sizer prop values | |
20 | ||
21 | halign = { "left": wx.ALIGN_LEFT, | |
22 | "center": wx.ALIGN_CENTER_HORIZONTAL, | |
23 | "centre": wx.ALIGN_CENTRE_HORIZONTAL, | |
24 | "right": wx.ALIGN_RIGHT, | |
25 | } | |
26 | ||
27 | valign = { "top": wx.ALIGN_TOP, | |
28 | "bottom": wx.ALIGN_BOTTOM, | |
29 | "center": wx.ALIGN_CENTER_VERTICAL, | |
30 | "centre": wx.ALIGN_CENTRE_VERTICAL, | |
31 | } | |
32 | ||
33 | align = { "center": wx.ALIGN_CENTER, | |
34 | "centre": wx.ALIGN_CENTRE, | |
35 | } | |
36 | ||
37 | border = { "left": wx.LEFT, | |
38 | "right": wx.RIGHT, | |
39 | "top": wx.TOP, | |
40 | "bottom": wx.BOTTOM, | |
41 | "all": wx.ALL, | |
42 | } | |
43 | ||
44 | minsize = { "fixed": wx.FIXED_MINSIZE, | |
45 | "adjust": wx.ADJUST_MINSIZE, | |
46 | } | |
47 | ||
48 | misc_flags = { "expand": wx.EXPAND, } | |
49 | ||
50 | ||
51 | # My attempt at creating a more intuitive replacement for nesting box sizers | |
52 | class TableSizer(wx.PySizer): | |
53 | def __init__(self, rows=0, cols=0): | |
54 | wx.PySizer.__init__(self) | |
55 | self.rows = rows | |
56 | self.cols = cols | |
57 | self.fixed_width = 0 | |
58 | self.fixed_height = 0 | |
59 | self.hgrow = 0 | |
60 | self.vgrow = 0 | |
61 | ||
62 | self.row_widths = [] | |
63 | self.col_heights = [] | |
64 | ||
65 | # allow us to use 'old-style' proportions when emulating box sizers | |
66 | self.isHorizontal = (self.rows == 1 and self.cols == 0) | |
67 | self.isVertical = (self.cols == 1 and self.rows == 0) | |
68 | ||
69 | def CalcNumRowsCols(self): | |
70 | numrows = self.rows | |
71 | numcols = self.cols | |
72 | numchild = len(self.GetChildren()) | |
73 | ||
74 | if numrows == 0 and numcols == 0: | |
75 | return 0, 0 | |
76 | ||
77 | if numrows == 0: | |
78 | rows, mod = divmod(numchild, self.cols) | |
79 | if mod > 0: | |
80 | rows += 1 | |
81 | numrows = rows | |
82 | ||
83 | if numcols == 0: | |
84 | cols, mod = divmod(numchild, self.rows) | |
85 | if mod > 0: | |
86 | cols += 1 | |
87 | numcols = cols | |
88 | ||
89 | return numrows, numcols | |
90 | ||
91 | def CalcMin(self): | |
92 | numrows, numcols = self.CalcNumRowsCols() | |
93 | numchild = len(self.GetChildren()) | |
94 | ||
95 | if numchild == 0: | |
96 | return wx.Size(10, 10) | |
97 | ||
98 | if numrows == 0 and numcols == 0: | |
99 | print "TableSizer must have the number of rows or columns set. Cannot continue." | |
100 | return wx.Size(10, 10) | |
101 | ||
102 | self.row_widths = [0 for x in range(0, numrows)] | |
103 | self.col_heights = [0 for x in range(0, numcols)] | |
104 | currentRow = 0 | |
105 | currentCol = 0 | |
106 | counter = 0 | |
107 | self.hgrow = 0 | |
108 | self.vgrow = 0 | |
109 | ||
110 | # get the max row width and max column height | |
111 | for item in self.GetChildren(): | |
112 | if self.cols != 0: | |
113 | currentRow, currentCol = divmod(counter, numcols) | |
114 | else: | |
115 | currentCol, currentRow = divmod(counter, numrows) | |
116 | ||
117 | if item.IsShown(): | |
118 | width, height = item.CalcMin() | |
119 | ||
120 | if self.isVertical and item.GetProportion() > 0: | |
121 | self.hgrow += item.GetProportion() | |
122 | elif self.isHorizontal and item.GetProportion() > 0: | |
123 | self.vgrow += item.GetProportion() | |
124 | ||
125 | if width > self.row_widths[currentRow]: | |
126 | self.row_widths[currentRow] = width | |
127 | ||
128 | if height > self.col_heights[currentCol]: | |
129 | self.col_heights[currentCol] = height | |
130 | ||
131 | counter += 1 | |
132 | ||
133 | minwidth = 0 | |
134 | for row_width in self.row_widths: | |
135 | minwidth += row_width | |
136 | ||
137 | minheight = 0 | |
138 | for col_height in self.col_heights: | |
139 | minheight += col_height | |
140 | ||
141 | self.fixed_width = minwidth | |
142 | self.fixed_height = minheight | |
143 | ||
144 | return wx.Size(minwidth, minheight) | |
145 | ||
146 | def RecalcSizes(self): | |
147 | numrows, numcols = self.CalcNumRowsCols() | |
148 | numchild = len(self.GetChildren()) | |
149 | ||
150 | if numchild == 0: | |
151 | return | |
152 | currentRow = 0 | |
153 | currentCol = 0 | |
154 | counter = 0 | |
155 | ||
156 | print "cols %d, rows %d" % (self.cols, self.rows) | |
157 | print "fixed_height %d, fixed_width %d" % (self.fixed_height, self.fixed_width) | |
158 | #print "self.GetSize() = " + `self.GetSize()` | |
159 | ||
160 | row_widths = [0 for x in range(0, numrows)] | |
161 | col_heights = [0 for x in range(0, numcols)] | |
162 | item_sizes = [0 for x in range(0, len(self.GetChildren()))] | |
163 | grow_sizes = [0 for x in range(0, len(self.GetChildren()))] | |
164 | ||
165 | curHPos = 0 | |
166 | curVPos = 0 | |
167 | curCol = 0 | |
168 | curRow = 0 | |
169 | # first, we set sizes for all children, and while doing so, calc | |
170 | # the maximum row heights and col widths. Then, afterwards we handle | |
171 | # the positioning of the controls | |
172 | ||
173 | for item in self.GetChildren(): | |
174 | if self.cols != 0: | |
175 | currentRow, currentCol = divmod(counter, numcols) | |
176 | else: | |
177 | currentCol, currentRow = divmod(counter, numrows) | |
178 | if item.IsShown(): | |
179 | item_minsize = item.GetMinSizeWithBorder() | |
180 | width = item_minsize[0] | |
181 | height = item_minsize[1] | |
182 | ||
183 | print "row_height %d, row_width %d" % (self.col_heights[currentCol], self.row_widths[currentRow]) | |
184 | growable_width = (self.GetSize()[0]) - width | |
185 | growable_height = (self.GetSize()[1]) - height | |
186 | ||
187 | #if not self.isVertical and not self.isHorizontal: | |
188 | # growable_width = self.GetSize()[0] - self.row_widths[currentRow] | |
189 | # growable_height = self.GetSize()[1] - self.col_heights[currentCol] | |
190 | ||
191 | #print "grow_height %d, grow_width %d" % (growable_height, growable_width) | |
192 | ||
193 | item_vgrow = 0 | |
194 | item_hgrow = 0 | |
195 | # support wx.EXPAND for box sizers to be compatible | |
196 | if item.GetFlag() & wx.EXPAND: | |
197 | if self.isVertical: | |
198 | if self.hgrow > 0 and item.GetProportion() > 0: | |
199 | item_hgrow = (growable_width * item.GetProportion()) / self.hgrow | |
200 | item_vgrow = growable_height | |
201 | ||
202 | elif self.isHorizontal: | |
203 | if self.vgrow > 0 and item.GetProportion() > 0: | |
204 | item_vgrow = (growable_height * item.GetProportion()) / self.vgrow | |
205 | item_hgrow = growable_width | |
206 | ||
207 | if growable_width > 0 and item.GetHGrow() > 0: | |
208 | item_hgrow = (growable_width * item.GetHGrow()) / 100 | |
209 | print "hgrow = %d" % (item_hgrow) | |
210 | ||
211 | if growable_height > 0 and item.GetVGrow() > 0: | |
212 | item_vgrow = (growable_height * item.GetVGrow()) / 100 | |
213 | print "vgrow = %d" % (item_vgrow) | |
214 | ||
215 | grow_size = wx.Size(item_hgrow, item_vgrow) | |
216 | size = item_minsize #wx.Size(item_minsize[0] + item_hgrow, item_minsize[1] + item_vgrow) | |
217 | if size[0] + grow_size[0] > row_widths[currentRow]: | |
218 | row_widths[currentRow] = size[0] + grow_size[0] | |
219 | if size[1] + grow_size[1] > col_heights[currentCol]: | |
220 | col_heights[currentCol] = size[1] + grow_size[1] | |
221 | ||
222 | grow_sizes[counter] = grow_size | |
223 | item_sizes[counter] = size | |
224 | ||
225 | counter += 1 | |
226 | ||
227 | counter = 0 | |
228 | for item in self.GetChildren(): | |
229 | if self.cols != 0: | |
230 | currentRow, currentCol = divmod(counter, numcols) | |
231 | else: | |
232 | currentCol, currentRow = divmod(counter, numrows) | |
233 | ||
234 | itempos = self.GetPosition() | |
235 | if item.IsShown(): | |
236 | rowstart = itempos[0] | |
237 | for row in range(0, currentRow): | |
238 | rowstart += row_widths[row] | |
239 | ||
240 | colstart = itempos[1] | |
241 | for col in range(0, currentCol): | |
242 | #print "numcols = %d, currentCol = %d, col = %d" % (numcols, currentCol, col) | |
243 | colstart += col_heights[col] | |
244 | ||
245 | itempos[0] += rowstart | |
246 | itempos[1] += colstart | |
247 | ||
248 | if item.GetFlag() & wx.ALIGN_RIGHT: | |
249 | itempos[0] += (row_widths[currentRow] - item_sizes[counter][0]) | |
250 | elif item.GetFlag() & (wx.ALIGN_CENTER | wx.ALIGN_CENTER_HORIZONTAL): | |
251 | itempos[0] += (row_widths[currentRow] - item_sizes[counter][0]) / 2 | |
252 | ||
253 | if item.GetFlag() & wx.ALIGN_BOTTOM: | |
254 | itempos[1] += (col_heights[currentCol] - item_sizes[counter][1]) | |
255 | elif item.GetFlag() & (wx.ALIGN_CENTER | wx.ALIGN_CENTER_VERTICAL): | |
256 | itempos[1] += (col_heights[currentCol] - item_sizes[counter][1]) / 2 | |
257 | ||
258 | hgrowth = (grow_sizes[counter][0] - itempos[0]) | |
259 | if hgrowth > 0: | |
260 | item_sizes[counter][0] += hgrowth | |
261 | ||
262 | vgrowth = (grow_sizes[counter][1] - itempos[1]) | |
263 | if vgrowth > 0: | |
264 | item_sizes[counter][1] += vgrowth | |
265 | #item_sizes[counter][1] -= itempos[1] | |
266 | item.SetDimension(itempos, item_sizes[counter]) | |
267 | ||
268 | counter += 1 | |
269 | ||
270 | def GetDefaultBorder(self): | |
271 | border = 4 | |
272 | if wx.Platform == "__WXMAC__": | |
273 | border = 6 | |
274 | elif wx.Platform == "__WXMSW__": | |
275 | # MSW HIGs use dialog units, not pixels | |
276 | pnt = self.ConvertDialogPointToPixels(wx.Point(4, 4)) | |
277 | border = pnt[0] | |
278 | elif wx.Platform == "__WXGTK__": | |
279 | border = 3 | |
280 | ||
281 | return border | |
282 | ||
283 | def SetDefaultSizerProps(self): | |
284 | item = self.GetParent().GetSizer().GetItem(self) | |
285 | item.SetProportion(0) | |
286 | item.SetFlag(wx.ALL) | |
287 | item.SetBorder(self.GetDefaultBorder()) | |
288 | ||
289 | def GetSizerProps(self): | |
290 | """ | |
291 | Returns a dictionary of prop name + value | |
292 | """ | |
293 | props = {} | |
294 | item = self.GetParent().GetSizer().GetItem(self) | |
295 | ||
296 | props['proportion'] = item.GetProportion() | |
297 | flags = item.GetFlag() | |
298 | ||
299 | if flags & border['all'] == border['all']: | |
300 | props['border'] = (['all'], item.GetBorder()) | |
301 | else: | |
302 | borders = [] | |
303 | for key in border: | |
304 | if flags & border[key]: | |
305 | borders.append(key) | |
306 | ||
307 | props['border'] = (borders, item.GetBorder()) | |
308 | ||
309 | if flags & align['center'] == align['center']: | |
310 | props['align'] = 'center' | |
311 | else: | |
312 | for key in halign: | |
313 | if flags & halign[key]: | |
314 | props['halign'] = key | |
315 | ||
316 | for key in valign: | |
317 | if flags & valign[key]: | |
318 | props['valign'] = key | |
319 | ||
320 | for key in minsize: | |
321 | if flags & minsize[key]: | |
322 | props['minsize'] = key | |
323 | ||
324 | for key in misc_flags: | |
325 | if flags & misc_flags[key]: | |
326 | props[key] = "true" | |
327 | ||
328 | return props | |
329 | ||
330 | def SetSizerProp(self, prop, value): | |
331 | ||
332 | lprop = prop.lower() | |
333 | item = self.GetParent().GetSizer().GetItem(self) | |
334 | flag = item.GetFlag() | |
335 | if lprop == "proportion": | |
336 | item.SetProportion(int(value)) | |
337 | elif lprop == "hgrow": | |
338 | item.SetHGrow(int(value)) | |
339 | elif lprop == "vgrow": | |
340 | item.SetVGrow(int(value)) | |
341 | elif lprop == "align": | |
342 | flag = flag | align[value] | |
343 | elif lprop == "halign": | |
344 | flag = flag | halign[value] | |
345 | elif lprop == "valign": | |
346 | flag = flag | valign[value] | |
347 | elif lprop == "border": | |
348 | # this arg takes a tuple (dir, pixels) | |
349 | dirs, amount = value | |
350 | if dirs == "all": | |
351 | dirs = ["all"] | |
352 | for dir in dirs: | |
353 | flag = flag | border[dir] | |
354 | item.SetBorder(amount) | |
355 | elif lprop == "minsize": | |
356 | flag = flag | minsize[value] | |
357 | elif lprop in misc_flags: | |
358 | if not value or str(value) == "" or str(value).lower() == "false": | |
359 | flag = flag &~ misc_flags[lprop] | |
360 | else: | |
361 | flag = flag | misc_flags[lprop] | |
362 | ||
363 | item.SetFlag(flag) | |
364 | ||
365 | def SetSizerProps(self, props={}, **kwargs): | |
366 | allprops = {} | |
367 | allprops.update(props) | |
368 | allprops.update(kwargs) | |
369 | ||
370 | for prop in allprops: | |
371 | self.SetSizerProp(prop, allprops[prop]) | |
372 | ||
373 | def GetDialogBorder(self): | |
374 | border = 6 | |
375 | if wx.Platform == "__WXMAC__" or wx.Platform == "__WXGTK__": | |
376 | border = 12 | |
377 | elif wx.Platform == "__WXMSW__": | |
378 | pnt = self.ConvertDialogPointToPixels(wx.Point(7, 7)) | |
379 | border = pnt[0] | |
380 | ||
381 | return border | |
382 | ||
383 | def SetHGrow(self, proportion): | |
384 | data = self.GetUserData() | |
385 | if "HGrow" in data: | |
386 | data["HGrow"] = proportion | |
387 | self.SetUserData(data) | |
388 | ||
389 | def GetHGrow(self): | |
390 | if self.GetUserData() and "HGrow" in self.GetUserData(): | |
391 | return self.GetUserData()["HGrow"] | |
392 | else: | |
393 | return 0 | |
394 | ||
395 | def SetVGrow(self, proportion): | |
396 | data = self.GetUserData() | |
397 | if "VGrow" in data: | |
398 | data["VGrow"] = proportion | |
399 | self.SetUserData(data) | |
400 | ||
401 | ||
402 | def GetVGrow(self): | |
403 | if self.GetUserData() and "VGrow" in self.GetUserData(): | |
404 | return self.GetUserData()["VGrow"] | |
405 | else: | |
406 | return 0 | |
407 | ||
408 | def GetDefaultPanelBorder(self): | |
409 | # child controls will handle their borders, so don't pad the panel. | |
410 | return 0 | |
411 | ||
412 | # Why, Python?! Why do you make it so easy?! ;-) | |
413 | wx.Dialog.GetDialogBorder = GetDialogBorder | |
414 | wx.Panel.GetDefaultBorder = GetDefaultPanelBorder | |
415 | wx.Notebook.GetDefaultBorder = GetDefaultPanelBorder | |
416 | wx.SplitterWindow.GetDefaultBorder = GetDefaultPanelBorder | |
417 | ||
418 | wx.Window.GetDefaultBorder = GetDefaultBorder | |
419 | wx.Window.SetDefaultSizerProps = SetDefaultSizerProps | |
420 | wx.Window.SetSizerProp = SetSizerProp | |
421 | wx.Window.SetSizerProps = SetSizerProps | |
422 | wx.Window.GetSizerProps = GetSizerProps | |
423 | ||
424 | wx.SizerItem.SetHGrow = SetHGrow | |
425 | wx.SizerItem.GetHGrow = GetHGrow | |
426 | wx.SizerItem.SetVGrow = SetVGrow | |
427 | wx.SizerItem.GetVGrow = GetVGrow | |
428 | ||
429 | ||
430 | class SizedPanel(wx.PyPanel): | |
431 | def __init__(self, *args, **kwargs): | |
432 | wx.PyPanel.__init__(self, *args, **kwargs) | |
433 | sizer = wx.BoxSizer(wx.VERTICAL) #TableSizer(1, 0) | |
434 | self.SetSizer(sizer) | |
435 | self.sizerType = "vertical" | |
436 | ||
437 | def AddChild(self, child): | |
438 | wx.PyPanel.base_AddChild(self, child) | |
439 | ||
440 | sizer = self.GetSizer() | |
441 | item = sizer.Add(child) | |
442 | item.SetUserData({"HGrow":0, "VGrow":0}) | |
443 | ||
444 | # Note: One problem is that the child class given to AddChild | |
445 | # is the underlying wxWidgets control, not its Python subclass. So if | |
446 | # you derive your own class, and override that class' GetDefaultBorder(), | |
447 | # etc. methods, it will have no effect. | |
448 | child.SetDefaultSizerProps() | |
449 | ||
450 | def GetSizerType(self): | |
451 | return self.sizerType | |
452 | ||
453 | def SetSizerType(self, type, options={}): | |
454 | sizer = None | |
455 | self.sizerType = type | |
456 | if type == "horizontal": | |
457 | sizer = wx.BoxSizer(wx.HORIZONTAL) # TableSizer(0, 1) | |
458 | ||
459 | elif type == "vertical": | |
460 | sizer = wx.BoxSizer(wx.VERTICAL) # TableSizer(1, 0) | |
461 | ||
462 | elif type == "form": | |
463 | #sizer = TableSizer(2, 0) | |
464 | sizer = wx.FlexGridSizer(0, 2, 0, 0) | |
465 | sizer.AddGrowableCol(1) | |
466 | ||
467 | elif type == "table": | |
468 | rows = cols = 0 | |
469 | if options.has_key('rows'): | |
470 | rows = int(options['rows']) | |
471 | ||
472 | if options.has_key('cols'): | |
473 | cols = int(options['cols']) | |
474 | ||
475 | sizer = TableSizer(rows, cols) | |
476 | ||
477 | elif type == "grid": | |
478 | sizer = wx.FlexGridSizer(0, 0, 0, 0) | |
479 | if options.has_key('rows'): | |
480 | sizer.SetRows(int(options['rows'])) | |
481 | else: | |
482 | sizer.SetRows(0) | |
483 | if options.has_key('cols'): | |
484 | sizer.SetCols(int(options['cols'])) | |
485 | else: | |
486 | sizer.SetCols(0) | |
487 | ||
488 | if options.has_key('growable_row'): | |
489 | row, proportion = options['growable_row'] | |
490 | sizer.SetGrowableRow(row, proportion) | |
491 | ||
492 | if options.has_key('growable_col'): | |
493 | col, proportion = options['growable_col'] | |
494 | sizer.SetGrowableCol(col, proportion) | |
495 | ||
496 | if options.has_key('hgap'): | |
497 | sizer.SetHGap(options['hgap']) | |
498 | ||
499 | if options.has_key('vgap'): | |
500 | sizer.SetVGap(options['vgap']) | |
501 | if sizer: | |
502 | self._SetNewSizer(sizer) | |
503 | ||
504 | def _SetNewSizer(self, sizer): | |
505 | props = {} | |
506 | for child in self.GetChildren(): | |
507 | props[child.GetId()] = child.GetSizerProps() | |
508 | self.GetSizer().Detach(child) | |
509 | ||
510 | wx.PyPanel.SetSizer(self, sizer) | |
511 | ||
512 | for child in self.GetChildren(): | |
513 | self.GetSizer().Add(child) | |
514 | child.SetSizerProps(props[child.GetId()]) | |
515 | ||
516 | class SizedDialog(wx.Dialog): | |
517 | def __init__(self, *args, **kwargs): | |
518 | wx.Dialog.__init__(self, *args, **kwargs) | |
519 | ||
520 | self.borderLen = 12 | |
521 | self.mainPanel = SizedPanel(self, -1) | |
522 | ||
523 | mysizer = wx.BoxSizer(wx.VERTICAL) | |
524 | mysizer.Add(self.mainPanel, 1, wx.EXPAND | wx.ALL, self.GetDialogBorder()) | |
525 | self.SetSizer(mysizer) | |
526 | ||
527 | self.SetAutoLayout(True) | |
528 | ||
529 | def GetContentsPane(self): | |
530 | return self.mainPanel | |
531 | ||
532 | def SetButtonSizer(self, sizer): | |
533 | self.GetSizer().Add(sizer, 0, wx.EXPAND | wx.BOTTOM | wx.RIGHT, self.GetDialogBorder()) | |
534 | ||
535 | # Temporary hack to fix button ordering problems. | |
536 | cancel = self.FindWindowById(wx.ID_CANCEL) | |
537 | no = self.FindWindowById(wx.ID_NO) | |
538 | if no and cancel: | |
539 | cancel.MoveAfterInTabOrder(no) | |
540 | ||
541 | class SizedFrame(wx.Frame): | |
542 | def __init__(self, *args, **kwargs): | |
543 | wx.Frame.__init__(self, *args, **kwargs) | |
544 | ||
545 | self.borderLen = 12 | |
546 | # this probably isn't needed, but I thought it would help to make it consistent | |
547 | # with SizedDialog, and creating a panel to hold things is often good practice. | |
548 | self.mainPanel = SizedPanel(self, -1) | |
549 | ||
550 | mysizer = wx.BoxSizer(wx.VERTICAL) | |
551 | mysizer.Add(self.mainPanel, 1, wx.EXPAND) | |
552 | self.SetSizer(mysizer) | |
553 | ||
554 | self.SetAutoLayout(True) | |
555 | ||
556 | def GetContentsPane(self): | |
557 | return self.mainPanel |