]> git.saurik.com Git - wxWidgets.git/blame - wxPython/wx/lib/expando.py
Added wx.lib.expando, a multi-line textctrl that exands as more lines
[wxWidgets.git] / wxPython / wx / lib / expando.py
CommitLineData
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"""
15This module contains the `ExpandoTextCtrl` which is a multi-line
16text control that will expand its height on the fly to be able to show
17all the lines of the content of the control.
18"""
19
20import wx
21import 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.
27wxEVT_ETC_LAYOUT_NEEDED = wx.NewEventType()
28EVT_ETC_LAYOUT_NEEDED = wx.PyEventBinder( wxEVT_ETC_LAYOUT_NEEDED, 1 )
29
30
31#---------------------------------------------------------------------------
32
33class 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
195if __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