]>
Commit | Line | Data |
---|---|---|
ac9d5e95 RD |
1 | #--------------------------------------------------------------------------- |
2 | # Name: expando.py | |
3 | # Purpose: A multi-line text control that expands and collapses as more | |
4 | # or less lines are needed to display its content. | |
5 | # | |
6 | # Author: Robin Dunn | |
7 | # | |
8 | # Created: 18-Sept-2006 | |
9 | # RCS-ID: $Id$ | |
10 | # Copyright: (c) 2006 by Total Control Software | |
11 | # Licence: wxWindows license | |
12 | # | |
13 | #--------------------------------------------------------------------------- | |
14 | """ | |
15 | This module contains the `ExpandoTextCtrl` which is a multi-line | |
16 | text control that will expand its height on the fly to be able to show | |
17 | all the lines of the content of the control. | |
18 | """ | |
19 | ||
20 | import wx | |
21 | import wx.lib.newevent | |
22 | ||
23 | ||
24 | # This event class and binder object can be used to catch | |
25 | # notifications that the ExpandoTextCtrl has resized itself and | |
26 | # that layout adjustments may need to be made. | |
27 | wxEVT_ETC_LAYOUT_NEEDED = wx.NewEventType() | |
28 | EVT_ETC_LAYOUT_NEEDED = wx.PyEventBinder( wxEVT_ETC_LAYOUT_NEEDED, 1 ) | |
29 | ||
30 | ||
31 | #--------------------------------------------------------------------------- | |
32 | ||
33 | class ExpandoTextCtrl(wx.TextCtrl): | |
34 | """ | |
35 | The ExpandoTextCtrl is a multi-line wx.TextCtrl that will | |
36 | adjust its height on the fly as needed to accomodate the number of | |
37 | lines needed to display the current content of the control. It is | |
38 | assumed that the width of the control will be a fixed value and | |
39 | that only the height will be adjusted automatically. If the | |
40 | control is used in a sizer then the width should be set as part of | |
41 | the initial or min size of the control. | |
42 | ||
43 | When the control resizes itself it will attempt to also make | |
44 | necessary adjustments in the sizer hierarchy it is a member of (if | |
45 | any) but if that is not suffiecient then the programmer can catch | |
46 | the EVT_ETC_LAYOUT_NEEDED event in the container and make any | |
47 | other layout adjustments that may be needed. | |
48 | """ | |
49 | _defaultHeight = -1 | |
50 | ||
51 | def __init__(self, parent, id=-1, value="", | |
52 | pos=wx.DefaultPosition, size=wx.DefaultSize, | |
53 | style=0, validator=wx.DefaultValidator, name="expando"): | |
54 | # find the default height of a single line control | |
55 | self.defaultHeight = self._getDefaultHeight(parent) | |
56 | # make sure we default to that height if none was given | |
57 | w, h = size | |
58 | if h == -1: | |
59 | h = self.defaultHeight | |
60 | # always use the multi-line style | |
61 | style = style | wx.TE_MULTILINE | wx.TE_NO_VSCROLL | wx.TE_RICH2 | |
62 | # init the base class | |
63 | wx.TextCtrl.__init__(self, parent, id, value, pos, (w, h), | |
64 | style, validator, name) | |
65 | # save some basic metrics | |
66 | self.extraHeight = self.defaultHeight - self.GetCharHeight() | |
67 | self.numLines = self.GetNumberOfLines() | |
68 | self.maxHeight = -1 | |
69 | ||
70 | self.Bind(wx.EVT_TEXT, self.OnTextChanged) | |
71 | ||
72 | ||
73 | def SetMaxHeight(self, h): | |
74 | """ | |
75 | Sets the max height that the control will expand to on its | |
76 | own, and adjusts it down if needed. | |
77 | """ | |
78 | self.maxHeight = h | |
79 | if h != -1 and self.GetSize().height > h: | |
80 | self.SetSize((-1, h)) | |
81 | ||
82 | def GetMaxHeight(self): | |
83 | """Sets the max height that the control will expand to on its own""" | |
84 | return self.maxHeight | |
85 | ||
86 | ||
87 | def SetFont(self, font): | |
88 | wx.TextCtrl.SetFont(self, font) | |
89 | self.numLines = -1 | |
90 | self._adjustCtrl() | |
91 | ||
92 | ||
93 | def OnTextChanged(self, evt): | |
94 | # check if any adjustments are needed on every text update | |
95 | self._adjustCtrl() | |
96 | evt.Skip() | |
97 | ||
98 | ||
99 | def _adjustCtrl(self): | |
100 | # if the current number of lines is different than before | |
101 | # then recalculate the size needed and readjust | |
102 | numLines = self.GetNumberOfLines() | |
103 | if numLines != self.numLines: | |
104 | self.numLines = numLines | |
105 | charHeight = self.GetCharHeight() | |
106 | height = numLines * charHeight + self.extraHeight | |
107 | if not (self.maxHeight != -1 and height > self.maxHeight): | |
108 | # The size is changing... if the control is not in a | |
109 | # sizer then we just want to change the size and | |
110 | # that's it, the programmer will need to deal with | |
111 | # potential layout issues. If it is being managed by | |
112 | # a sizer then we'll change the min size setting and | |
113 | # then try to do a layout. In either case we'll also | |
114 | # send an event so the parent can handle any special | |
115 | # layout issues that it wants to deal with. | |
116 | if self.GetContainingSizer() is not None: | |
117 | mw, mh = self.GetMinSize() | |
118 | self.SetMinSize((mw, height)) | |
119 | if self.GetParent().GetSizer() is not None: | |
120 | self.GetParent().Layout() | |
121 | else: | |
122 | self.GetContainingSizer().Layout() | |
123 | else: | |
124 | self.SetSize((-1, height)) | |
125 | # send notification that layout is needed | |
126 | evt = wx.PyCommandEvent(wxEVT_ETC_LAYOUT_NEEDED, self.GetId()) | |
127 | evt.SetEventObject(self) | |
128 | evt.height = height | |
129 | evt.numLines = numLines | |
130 | self.GetEventHandler().ProcessEvent(evt) | |
131 | ||
132 | ||
133 | def _getDefaultHeight(self, parent): | |
134 | # checked for cached value | |
135 | if self.__class__._defaultHeight != -1: | |
136 | return self.__class__._defaultHeight | |
137 | # otherwise make a single line textctrl and find out its default height | |
138 | tc = wx.TextCtrl(parent) | |
139 | sz = tc.GetSize() | |
140 | tc.Destroy() | |
141 | self.__class__._defaultHeight = sz.height | |
142 | return sz.height | |
143 | ||
144 | ||
145 | if wx.VERSION < (2,7) and 'wxGTK' in wx.PlatformInfo: | |
146 | # the wxGTK version of GetNumberOfLines in 2.6 doesn't count | |
147 | # wrapped lines, so we need to implement our own. This is | |
148 | # fixed in 2.7. | |
149 | def GetNumberOfLines(self): | |
150 | text = self.GetValue() | |
151 | width = self.GetSize().width | |
152 | dc = wx.ClientDC(self) | |
153 | dc.SetFont(self.GetFont()) | |
154 | count = 0 | |
155 | for line in text.split('\n'): | |
156 | count += 1 | |
157 | w, h = dc.GetTextExtent(line) | |
158 | if w > width: | |
159 | # the width of the text is wider than the control, | |
160 | # calc how many lines it will be wrapped to | |
161 | count += self._wrapLine(line, dc, width) | |
162 | ||
163 | if not count: | |
164 | count = 1 | |
165 | return count | |
166 | ||
167 | ||
168 | def _wrapLine(self, line, dc, width): | |
169 | # Estimate where the control will wrap the lines and | |
170 | # return the count of extra lines needed. | |
171 | pte = dc.GetPartialTextExtents(line) | |
172 | width -= wx.SystemSettings.GetMetric(wx.SYS_VSCROLL_X) | |
173 | idx = 0 | |
174 | start = 0 | |
175 | count = 0 | |
176 | spc = -1 | |
177 | while idx < len(pte): | |
178 | if line[idx] == ' ': | |
179 | spc = idx | |
180 | if pte[idx] - start > width - _sbw: | |
181 | # we've reached the max width, add a new line | |
182 | count += 1 | |
183 | # did we see a space? if so restart the count at that pos | |
184 | if spc != -1: | |
185 | idx = spc + 1 | |
186 | spc = -1 | |
187 | start = pte[idx] | |
188 | else: | |
189 | idx += 1 | |
190 | return count | |
191 | ||
192 | #--------------------------------------------------------------------------- | |
193 | ||
194 | ||
195 | if __name__ == '__main__': | |
196 | print wx.VERSION | |
197 | ||
198 | class TestFrame(wx.Frame): | |
199 | def __init__(self): | |
200 | wx.Frame.__init__(self, None, title="Testing...") | |
201 | self.pnl = p = wx.Panel(self) | |
202 | self.eom = ExpandoTextCtrl(p, pos=(25,25), size=(250,-1)) | |
203 | self.Bind(EVT_ETC_LAYOUT_NEEDED, self.OnRefit, self.eom) | |
204 | ||
205 | # create some buttons and sizers to use in testing some | |
206 | # features and also the layout | |
207 | vBtnSizer = wx.BoxSizer(wx.VERTICAL) | |
208 | ||
209 | btn = wx.Button(p, -1, "Set MaxHeight") | |
210 | self.Bind(wx.EVT_BUTTON, self.OnSetMaxHeight, btn) | |
211 | vBtnSizer.Add(btn, 0, wx.ALL|wx.EXPAND, 5) | |
212 | ||
213 | btn = wx.Button(p, -1, "Set Font") | |
214 | self.Bind(wx.EVT_BUTTON, self.OnSetFont, btn) | |
215 | vBtnSizer.Add(btn, 0, wx.ALL|wx.EXPAND, 5) | |
216 | ||
217 | btn = wx.Button(p, -1, "Set Value") | |
218 | self.Bind(wx.EVT_BUTTON, self.OnSetValue, btn) | |
219 | vBtnSizer.Add(btn, 0, wx.ALL|wx.EXPAND, 5) | |
220 | ||
221 | for x in range(5): | |
222 | btn = wx.Button(p, -1, " ") | |
223 | vBtnSizer.Add(btn, 0, wx.ALL|wx.EXPAND, 5) | |
224 | ||
225 | hBtnSizer = wx.BoxSizer(wx.HORIZONTAL) | |
226 | for x in range(3): | |
227 | btn = wx.Button(p, -1, " ") | |
228 | hBtnSizer.Add(btn, 0, wx.ALL, 5) | |
229 | ||
230 | sizer = wx.BoxSizer(wx.HORIZONTAL) | |
231 | col1 = wx.BoxSizer(wx.VERTICAL) | |
232 | col1.Add(self.eom, 0, wx.ALL, 10) | |
233 | col1.Add(hBtnSizer) | |
234 | sizer.Add(col1) | |
235 | sizer.Add(vBtnSizer) | |
236 | p.SetSizer(sizer) | |
237 | ||
238 | # Put the panel in a sizer for the frame so we can use self.Fit() | |
239 | frameSizer = wx.BoxSizer() | |
240 | frameSizer.Add(p, 1, wx.EXPAND) | |
241 | self.SetSizer(frameSizer) | |
242 | ||
243 | self.Fit() | |
244 | ||
245 | ||
246 | def OnRefit(self, evt): | |
247 | # The Expando control will redo the layout of the | |
248 | # sizer it belongs to, but sometimes this may not be | |
249 | # enough, so it will send us this event so we can do any | |
250 | # other layout adjustments needed. In this case we'll | |
251 | # just resize the frame to fit the new needs of the sizer. | |
252 | self.Fit() | |
253 | ||
254 | def OnSetMaxHeight(self, evt): | |
255 | mh = self.eom.GetMaxHeight() | |
256 | dlg = wx.NumberEntryDialog(self, "", "Enter new max height:", | |
257 | "MaxHeight", mh, -1, 1000) | |
258 | if dlg.ShowModal() == wx.ID_OK: | |
259 | self.eom.SetMaxHeight(dlg.GetValue()) | |
260 | dlg.Destroy() | |
261 | ||
262 | ||
263 | def OnSetFont(self, evt): | |
264 | dlg = wx.FontDialog(self, wx.FontData()) | |
265 | dlg.GetFontData().SetInitialFont(self.eom.GetFont()) | |
266 | if dlg.ShowModal() == wx.ID_OK: | |
267 | self.eom.SetFont(dlg.GetFontData().GetChosenFont()) | |
268 | dlg.Destroy() | |
269 | ||
270 | ||
271 | def OnSetValue(self, evt): | |
272 | self.eom.SetValue("This is a test... Only a test. If this had " | |
273 | "been a real emergency you would have seen the " | |
274 | "quick brown fox jump over the lazy dog.") | |
275 | ||
276 | ||
277 | app = wx.App(False) | |
278 | frm = TestFrame() | |
279 | frm.Show() | |
280 | #import wx.py | |
281 | #shell = wx.py.shell.ShellFrame(frm, size=(500,300), | |
282 | # locals={'wx':wx, 'frm':frm}) | |
283 | #shell.Show() | |
284 | frm.Raise() | |
285 | app.MainLoop() | |
286 | ||
287 | ||
288 |