]>
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() | |
c7c45995 | 67 | self.numLines = 1 |
ac9d5e95 | 68 | self.maxHeight = -1 |
c7c45995 RD |
69 | if value: |
70 | wx.CallAfter(self._adjustCtrl) | |
71 | ||
ac9d5e95 RD |
72 | self.Bind(wx.EVT_TEXT, self.OnTextChanged) |
73 | ||
74 | ||
75 | def SetMaxHeight(self, h): | |
76 | """ | |
77 | Sets the max height that the control will expand to on its | |
78 | own, and adjusts it down if needed. | |
79 | """ | |
80 | self.maxHeight = h | |
81 | if h != -1 and self.GetSize().height > h: | |
82 | self.SetSize((-1, h)) | |
83 | ||
84 | def GetMaxHeight(self): | |
85 | """Sets the max height that the control will expand to on its own""" | |
86 | return self.maxHeight | |
87 | ||
88 | ||
89 | def SetFont(self, font): | |
90 | wx.TextCtrl.SetFont(self, font) | |
91 | self.numLines = -1 | |
92 | self._adjustCtrl() | |
4123a7a9 RD |
93 | |
94 | def WriteText(self, text): | |
95 | # work around a bug of a lack of a EVT_TEXT when calling | |
96 | # WriteText on wxMac | |
97 | wx.TextCtrl.WriteText(self, text) | |
98 | self._adjustCtrl() | |
99 | ||
100 | def AppendText(self, text): | |
101 | # Instead of using wx.TextCtrl.AppendText append and set the | |
102 | # insertion point ourselves. This works around a bug on wxMSW | |
103 | # where it scrolls the old text out of view, and since there | |
104 | # is no scrollbar there is no way to get back to it. | |
105 | self.SetValue(self.GetValue() + text) | |
106 | self.SetInsertionPointEnd() | |
107 | ||
ac9d5e95 RD |
108 | |
109 | def OnTextChanged(self, evt): | |
110 | # check if any adjustments are needed on every text update | |
111 | self._adjustCtrl() | |
112 | evt.Skip() | |
113 | ||
114 | ||
115 | def _adjustCtrl(self): | |
116 | # if the current number of lines is different than before | |
117 | # then recalculate the size needed and readjust | |
118 | numLines = self.GetNumberOfLines() | |
119 | if numLines != self.numLines: | |
120 | self.numLines = numLines | |
121 | charHeight = self.GetCharHeight() | |
122 | height = numLines * charHeight + self.extraHeight | |
123 | if not (self.maxHeight != -1 and height > self.maxHeight): | |
124 | # The size is changing... if the control is not in a | |
125 | # sizer then we just want to change the size and | |
126 | # that's it, the programmer will need to deal with | |
127 | # potential layout issues. If it is being managed by | |
128 | # a sizer then we'll change the min size setting and | |
129 | # then try to do a layout. In either case we'll also | |
130 | # send an event so the parent can handle any special | |
131 | # layout issues that it wants to deal with. | |
132 | if self.GetContainingSizer() is not None: | |
133 | mw, mh = self.GetMinSize() | |
134 | self.SetMinSize((mw, height)) | |
135 | if self.GetParent().GetSizer() is not None: | |
136 | self.GetParent().Layout() | |
137 | else: | |
138 | self.GetContainingSizer().Layout() | |
139 | else: | |
140 | self.SetSize((-1, height)) | |
141 | # send notification that layout is needed | |
142 | evt = wx.PyCommandEvent(wxEVT_ETC_LAYOUT_NEEDED, self.GetId()) | |
143 | evt.SetEventObject(self) | |
144 | evt.height = height | |
145 | evt.numLines = numLines | |
146 | self.GetEventHandler().ProcessEvent(evt) | |
147 | ||
148 | ||
149 | def _getDefaultHeight(self, parent): | |
150 | # checked for cached value | |
151 | if self.__class__._defaultHeight != -1: | |
152 | return self.__class__._defaultHeight | |
153 | # otherwise make a single line textctrl and find out its default height | |
154 | tc = wx.TextCtrl(parent) | |
155 | sz = tc.GetSize() | |
156 | tc.Destroy() | |
157 | self.__class__._defaultHeight = sz.height | |
158 | return sz.height | |
159 | ||
160 | ||
855dd2ec | 161 | if 'wxGTK' in wx.PlatformInfo: ## and wx.VERSION < (2,7): it's broke again in 2.7.2... |
ac9d5e95 RD |
162 | # the wxGTK version of GetNumberOfLines in 2.6 doesn't count |
163 | # wrapped lines, so we need to implement our own. This is | |
164 | # fixed in 2.7. | |
165 | def GetNumberOfLines(self): | |
166 | text = self.GetValue() | |
167 | width = self.GetSize().width | |
168 | dc = wx.ClientDC(self) | |
169 | dc.SetFont(self.GetFont()) | |
170 | count = 0 | |
171 | for line in text.split('\n'): | |
172 | count += 1 | |
173 | w, h = dc.GetTextExtent(line) | |
174 | if w > width: | |
175 | # the width of the text is wider than the control, | |
176 | # calc how many lines it will be wrapped to | |
177 | count += self._wrapLine(line, dc, width) | |
178 | ||
179 | if not count: | |
180 | count = 1 | |
181 | return count | |
182 | ||
183 | ||
184 | def _wrapLine(self, line, dc, width): | |
185 | # Estimate where the control will wrap the lines and | |
186 | # return the count of extra lines needed. | |
187 | pte = dc.GetPartialTextExtents(line) | |
188 | width -= wx.SystemSettings.GetMetric(wx.SYS_VSCROLL_X) | |
189 | idx = 0 | |
190 | start = 0 | |
191 | count = 0 | |
192 | spc = -1 | |
193 | while idx < len(pte): | |
194 | if line[idx] == ' ': | |
195 | spc = idx | |
c7c45995 | 196 | if pte[idx] - start > width: |
ac9d5e95 RD |
197 | # we've reached the max width, add a new line |
198 | count += 1 | |
199 | # did we see a space? if so restart the count at that pos | |
200 | if spc != -1: | |
201 | idx = spc + 1 | |
202 | spc = -1 | |
203 | start = pte[idx] | |
204 | else: | |
205 | idx += 1 | |
206 | return count | |
207 | ||
208 | #--------------------------------------------------------------------------- |