5 # - test variable-length list
7 # This module provides the "Select" widget which can be used to
8 # selected one item from a list, such as a command, and file, or
9 # anything else. Selecting an object should not have any significant
10 # effect (though the control of that is outside this module). That is
11 # because this widget is intended for a finger-touch display and
12 # precision might not be very good - a wrong selection should be
15 # A scale factor is available (though external controls must be used
16 # to change it). With small scale factors, the display might use
17 # multiple columns to get more entries on the display.
19 # There is no direct control of scolling. Rather the list is
20 # automatically scrolled to ensure that the selected item is displayed
21 # and is not to close to either end. If the selected item would be
22 # near the end, it is scrolled to be near the beginning, and similarly
23 # if it is near the beginning, the list is scrolled so that the
24 # selected item is near the end of the display
26 # However we never display blank space before the list and try to
27 # avoid displaying more than one blank space after the list.
29 # Each entry is a short text. It can have a number of highlights
36 # The text is either centered in the column or left justified
37 # (possibly leaving space for a bullet).
39 # This widget only provides display of the list and selection.
40 # It does not process input events directly. Rather some other
41 # module must take events from this window (or elsewhere) and send
42 # 'tap' events to this widget as appropriate. They are converted
43 # to selections. This allows e.g. a writing-recognition widget
44 # to process all input and keep strokes to itself, only sending
47 # The list of elements is passed as an array. However it could be
48 # changed at any time. This widget assumes that it will be told
49 # whenever the list changes so it doesn't have to poll the list
52 # When the list does change, we try to preserve the currently selected
53 # position based on the text of the entry.
55 # We support arrays with an apparent size of 0. In this case we
56 # don't try to preserve location on a change, and might display
57 # more white space at the end of the array (which should appear
58 # as containing None).
60 # It is possible to ask the "Select" to move the the "next" or
61 # "previous" item. This can have a function which tests candidates
64 # An entry in the list has two parts: the string and the highlight
65 # It must look like a tuple: e[0] is the string. e[1] is the highlight
66 # The highlight can be just a string, in which case it is a colour name,
67 # or a tuple of (colour underline bullet background selected-background)
68 # missing fields default to (black False False grey white)
69 # Also a mapping from string to type can be created.
71 import gtk, pango, gobject
74 class ListSelect(gtk.DrawingArea):
76 'selected' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
79 def __init__(self, center = False):
80 gtk.DrawingArea.__init__(self)
82 # Index of first entry displayed
84 # Index of currently selected entry
86 # string value of current selection
87 self.selected_str = None
93 self.fd = self.get_pango_context().get_font_description()
100 self.bullet_space = True
102 self.format_list = {}
106 self.connect("expose-event", self.draw)
107 self.connect("configure-event", self.reconfig)
109 self.connect_after("button_press_event", self.press)
110 self.add_events(gtk.gdk.BUTTON_PRESS_MASK)
113 def press(self, c, ev):
117 def draw(self, w, ev):
118 # draw any field that is in the area
120 for c in range(self.cols):
121 if (c+1) * self.colwidth < x:
123 if x + w < c * self.colwidth:
125 for r in range(self.rows):
126 if (r+1) * self.lineheight < y:
128 if y + h < r * self.lineheight:
130 if (r,c) not in self.to_draw:
131 self.to_draw.append((r,c))
133 for r,c in self.to_draw:
137 def draw_one(self, r, c, task = None):
138 ind = r + c * self.rows + self.top
139 if len(self.list) >= 0 and ind >= len(self.list):
143 if task != None and task != val:
146 strng,fmt = "", "blank"
150 val.on_change(self, ind)
155 fmt = self.get_format(fmt)
158 (col, under, bullet, back, sel) = fmt
161 (col, under, bullet, back, sel, bold) = fmt
162 # draw background rectangle
163 if ind == self.selected:
164 self.window.draw_rectangle(self.get_colour(sel), True,
165 c*self.colwidth, r*self.lineheight,
166 self.colwidth, self.lineheight)
168 self.window.draw_rectangle(self.get_colour(back), True,
169 c*self.colwidth, r*self.lineheight,
170 self.colwidth, self.lineheight)
172 w = int(self.lineheight * 0.4)
173 vo = (self.lineheight - w)/2
175 self.window.draw_rectangle(self.get_colour(col), True,
176 c*self.colwidth+ho, r*self.lineheight + vo,
180 layout = self.create_pango_layout(strng)
183 a.insert(pango.AttrUnderline(pango.UNDERLINE_SINGLE, 0, len(strng)))
184 if bold[0] < bold[1]:
185 a.insert(pango.AttrWeight(pango.WEIGHT_BOLD,bold[0], bold[1]))
186 layout.set_attributes(a)
190 ink, (ex,ey,ew,eh) = layout.get_pixel_extents()
191 offset = int((self.colwidth - ew) / 2)
196 self.window.draw_layout(self.get_colour(col),
197 c*self.colwidth + offset,
202 def set_colour(self, name, col):
203 self.colours[name] = col
204 def get_colour(self, col):
205 # col is either a colour name, or a pre-set colour.
206 # so if it isn't in the list, add it
208 return self.get_style().bg_gc[gtk.STATE_NORMAL]
209 if col not in self.colours:
210 self.set_colour(col, col)
211 if type(self.colours[col]) == str:
212 gc = self.window.new_gc()
213 gc.set_foreground(self.get_colormap().
214 alloc_color(gtk.gdk.color_parse(self.colours[col])))
215 self.colours[col] = gc;
216 return self.colours[col]
219 def set_format(self, name, colour, underline=False, bullet=False,
220 background=None, selected="white"):
221 self.format_list[name] = (colour, underline, bullet, background, selected)
223 def get_format(self, name):
224 if name in self.format_list:
225 return self.format_list[name]
227 return (None, False, False, None, None)
228 return (name, False, False, None, "white")
230 def calc_layout(self):
231 # The zoom or size or list has changed.
232 # We need to calculate lineheight and colwidth
233 # and from those, rows and cols.
234 # If the list is of indefinite length we cannot check the
235 # width of every entry so we just check until we have enough
242 while n < 0 or i < n:
247 layout = self.create_pango_layout(strng)
248 ink, (ex,ey,ew,eh) = layout.get_pixel_extents()
249 if ew > maxw: maxw = ew
250 if eh > maxh: maxh = eh
253 rs = int(self.height / maxh)
254 cs = int(self.width / maxw)
257 n = self.top + rs * cs
261 if self.bullet_space:
263 self.rows = int(self.height / maxh)
264 self.cols = int(self.width / maxw)
265 if i == 0 or self.rows == 0:
267 if self.cols > int((i + self.rows-1) / self.rows):
268 self.cols = int((i + self.rows-1) / self.rows)
271 self.lineheight = maxh
272 self.colwidth = int(self.width / self.cols)
273 self.offset = (self.colwidth - real_maxw) / 2
275 def check_scroll(self):
276 # the top and/or selected have changed, or maybe the layout has
278 # We need to make sure 'top' is still appropriate.
280 if self.selected == None:
283 margin = self.rows / 3
286 remainder = self.rows * self.cols - margin
287 if self.selected < self.top + margin:
288 self.top = self.selected - (remainder - 1)
291 if self.selected >= self.top + remainder:
292 self.top = self.selected - margin
294 if l >= 0 and self.top + self.rows * self.cols > l:
295 self.top = l - self.rows * self.cols + 1
297 return self.top != oldtop
299 def reconfig(self, w, ev):
300 alloc = w.get_allocation()
301 if alloc.width != self.width or alloc.height != self.height:
302 self.width, self.height = alloc.width, alloc.height
307 def set_zoom(self, zoom):
312 if zoom == self.zoom:
316 for i in range(zoom):
318 self.fd.set_absolute_size(s)
319 self.modify_font(self.fd)
325 def list_changed(self):
329 if self.list[i][0] == self.selected_str:
332 if self.selected >= l:
334 elif self.selected != None:
335 self.selected_str = self.list[self.selected][0]
340 def item_changed(self, ind, task = None):
341 # only changed if it is still 'task'
342 col = (ind - self.top) / self.rows
343 row = (ind - self.top) - (col * self.rows)
344 self.draw_one(row, col, task)
347 def map_pos(self, x, y):
348 row = int(y / self.lineheight)
349 col = int(x / self.colwidth)
350 ind = row + col * self.rows + self.top
352 if l >= 0 and ind >= l:
354 if l < 0 and self.list[ind] == None:
359 ind = self.map_pos(x,y)
363 def select(self, ind):
364 if self.selected == ind:
368 self.selected_str = self.list[ind][0]
369 if self.window == None or self.check_scroll():
372 col = (ind - self.top) / self.rows
373 row = (ind - self.top) - (col * self.rows)
374 self.draw_one(row, col)
376 col = (old - self.top) / self.rows
377 row = (old - self.top) - (col * self.rows)
378 self.draw_one(row, col)
380 self.callout(ind, self.list[ind])
381 self.emit('selected', ind)
383 if __name__ == "__main__":
385 # demo app using this widget
386 w = gtk.Window(gtk.WINDOW_TOPLEVEL)
387 w.connect("destroy", lambda w: gtk.main_quit())
388 w.set_title("ListSelect Test")
391 list = [ "zero", "one", "two", "three", "four", "five", "six", "seven", "eight",
392 "nine", "ten", "eleven", "twelve", "thirteen", "forteen"]
395 el.append((a, "blue"))
396 el[9] = (el[9][0], ("red",True,True,"black","white"))
397 el[13] = (el[13][0], ("black",False,False,"yellow","white",(4,8)))
400 print n, s, "selected"
415 w.connect("key_press_event", key)
416 w.add_events(gtk.gdk.KEY_PRESS_MASK)
417 #w.set_property('can-focus', True)
422 print "tap", ev.send_event
426 c.handler_unblock(han)
427 c.stop_emission("button_press_event")
429 han = s.connect("button_press_event", tap)