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
91 self.fd = self.get_pango_context().get_font_description()
98 self.bullet_space = True
100 self.format_list = {}
104 self.connect("expose-event", self.draw)
105 self.connect("configure-event", self.reconfig)
107 self.connect_after("button_press_event", self.press)
108 self.add_events(gtk.gdk.BUTTON_PRESS_MASK)
111 def press(self, c, ev):
115 def draw(self, w, ev):
116 # draw any field that is in the area
118 for c in range(self.cols):
119 if (c+1) * self.colwidth < x:
121 if x + w < c * self.colwidth:
123 for r in range(self.rows):
124 if (r+1) * self.lineheight < y:
126 if y + h < r * self.lineheight:
128 if (r,c) not in self.to_draw:
129 self.to_draw.append((r,c))
131 for r,c in self.to_draw:
135 def draw_one(self, r, c, task = None):
136 ind = r + c * self.rows + self.top
137 if len(self.list) >= 0 and ind >= len(self.list):
141 if task != None and task != val:
144 strng,fmt = "", "blank"
148 val.on_change(self, ind)
153 fmt = self.get_format(fmt)
156 (col, under, bullet, back, sel) = fmt
159 (col, under, bullet, back, sel, bold) = fmt
160 # draw background rectangle
161 if ind == self.selected:
162 self.window.draw_rectangle(self.get_colour(sel), True,
163 c*self.colwidth, r*self.lineheight,
164 self.colwidth, self.lineheight)
166 self.window.draw_rectangle(self.get_colour(back), True,
167 c*self.colwidth, r*self.lineheight,
168 self.colwidth, self.lineheight)
170 w = int(self.lineheight * 0.4)
171 vo = (self.lineheight - w)/2
173 self.window.draw_rectangle(self.get_colour(col), True,
174 c*self.colwidth+ho, r*self.lineheight + vo,
178 layout = self.create_pango_layout(strng)
181 a.insert(pango.AttrUnderline(pango.UNDERLINE_SINGLE, 0, len(strng)))
182 if bold[0] < bold[1]:
183 a.insert(pango.AttrWeight(pango.WEIGHT_BOLD,bold[0], bold[1]))
184 layout.set_attributes(a)
188 ink, (ex,ey,ew,eh) = layout.get_pixel_extents()
189 offset = int((self.colwidth - ew) / 2)
194 self.window.draw_layout(self.get_colour(col),
195 c*self.colwidth + offset,
200 def set_colour(self, name, col):
201 self.colours[name] = col
202 def get_colour(self, col):
203 # col is either a colour name, or a pre-set colour.
204 # so if it isn't in the list, add it
206 return self.get_style().bg_gc[gtk.STATE_NORMAL]
207 if col not in self.colours:
208 self.set_colour(col, col)
209 if type(self.colours[col]) == str:
210 gc = self.window.new_gc()
211 gc.set_foreground(self.get_colormap().
212 alloc_color(gtk.gdk.color_parse(self.colours[col])))
213 self.colours[col] = gc;
214 return self.colours[col]
217 def set_format(self, name, colour, underline=False, bullet=False,
218 background=None, selected="white"):
219 self.format_list[name] = (colour, underline, bullet, background, selected)
221 def get_format(self, name):
222 if name in self.format_list:
223 return self.format_list[name]
225 return (None, False, False, None, None)
226 return (name, False, False, None, "white")
228 def calc_layout(self):
229 # The zoom or size or list has changed.
230 # We need to calculate lineheight and colwidth
231 # and from those, rows and cols.
232 # If the list is of indefinite length we cannot check the
233 # width of every entry so we just check until we have enough
240 while n < 0 or i < n:
245 layout = self.create_pango_layout(strng)
246 ink, (ex,ey,ew,eh) = layout.get_pixel_extents()
247 if ew > maxw: maxw = ew
248 if eh > maxh: maxh = eh
251 rs = int(self.height / maxh)
252 cs = int(self.width / maxw)
255 n = self.top + rs * cs
259 if self.bullet_space:
261 self.rows = int(self.height / maxh)
262 self.cols = int(self.width / maxw)
265 if self.cols > int((i + self.rows-1) / self.rows):
266 self.cols = int((i + self.rows-1) / self.rows)
269 self.lineheight = maxh
270 self.colwidth = int(self.width / self.cols)
271 self.offset = (self.colwidth - real_maxw) / 2
273 def check_scroll(self):
274 # the top and/or selected have changed, or maybe the layout has
276 # We need to make sure 'top' is still appropriate.
278 if self.selected == None:
281 margin = self.rows / 3
284 remainder = self.rows * self.cols - margin
285 if self.selected < self.top + margin:
286 self.top = self.selected - (remainder - 1)
289 if self.selected >= self.top + remainder:
290 self.top = self.selected - margin
292 if l >= 0 and self.top + self.rows * self.cols > l:
293 self.top = l - self.rows * self.cols + 1
295 return self.top != oldtop
297 def reconfig(self, w, ev):
298 alloc = w.get_allocation()
299 if alloc.width != self.width or alloc.height != self.height:
300 self.width, self.height = alloc.width, alloc.height
305 def set_zoom(self, zoom):
310 if zoom == self.zoom:
314 for i in range(zoom):
316 self.fd.set_absolute_size(s)
317 self.modify_font(self.fd)
323 def list_changed(self):
327 if self.list[i][0] == self.selected_str:
330 if self.selected >= l:
332 elif self.selected != None:
333 self.selected_str = self.list[self.selected][0]
338 def item_changed(self, ind, task = None):
339 # only changed if it is still 'task'
340 col = (ind - self.top) / self.rows
341 row = (ind - self.top) - (col * self.rows)
342 self.draw_one(row, col, task)
345 def map_pos(self, x, y):
346 row = int(y / self.lineheight)
347 col = int(x / self.colwidth)
348 ind = row + col * self.rows + self.top
350 if l >= 0 and ind >= l:
352 if l < 0 and self.list[ind] == None:
357 ind = self.map_pos(x,y)
361 def select(self, ind):
362 if self.selected == ind:
363 self.emit('selected', -1 if ind == None else ind)
367 self.selected_str = None
369 self.emit('selected', -1)
373 self.selected_str = self.list[ind][0]
374 if self.window == None or self.check_scroll():
377 col = (ind - self.top) / self.rows
378 row = (ind - self.top) - (col * self.rows)
379 self.draw_one(row, col)
381 col = (old - self.top) / self.rows
382 row = (old - self.top) - (col * self.rows)
383 self.draw_one(row, col)
384 self.emit('selected', ind)
386 if __name__ == "__main__":
388 # demo app using this widget
389 w = gtk.Window(gtk.WINDOW_TOPLEVEL)
390 w.connect("destroy", lambda w: gtk.main_quit())
391 w.set_title("ListSelect Test")
394 list = [ "zero", "one", "two", "three", "four", "five", "six", "seven", "eight",
395 "nine", "ten", "eleven", "twelve", "thirteen", "forteen"]
398 el.append((a, "blue"))
399 el[9] = (el[9][0], ("red",True,True,"black","white"))
400 el[13] = (el[13][0], ("black",False,False,"yellow","white",(4,8)))
402 print n, s.list[n], "selected"
403 s.connect('selected', sel)
417 w.connect("key_press_event", key)
418 w.add_events(gtk.gdk.KEY_PRESS_MASK)
419 #w.set_property('can-focus', True)
424 print "tap", ev.send_event
428 c.handler_unblock(han)
429 c.stop_emission("button_press_event")
431 han = s.connect("button_press_event", tap)