class SelectableText(Text): selected = widget.causes_redraw("_selected") selected_color = widget.auto_reconfig("_selected_color", g.resolve_color_alias) _selected_color = widget.causes_redraw("__selected_color") unselected_color = widget.auto_reconfig("_unselected_color", g.resolve_color_alias) unselected_color = widget.causes_redraw("__unselected_color") def __init__(self, parent, pos, size, border_color=None, unselected_color=None, selected_color=None, **kwargs): super(SelectableText, self).__init__(parent, pos, size, **kwargs) self.border_color = border_color or "text_border" self.selected_color = selected_color or "text_background_selected" self.unselected_color = unselected_color or "text_background_unselected" self.selected = False def redraw(self): if self.selected: self.background_color = self.selected_color else: self.background_color = self.unselected_color super(SelectableText, self).redraw()
class SelectableText(Text): selected = widget.causes_redraw("_selected") selected_color = widget.causes_redraw("_selected_color") unselected_color = widget.causes_redraw("_unselected_color") def __init__(self, parent, pos, size, border_color=None, unselected_color=None, selected_color=None, **kwargs): super(SelectableText, self).__init__(parent, pos, size, **kwargs) self.border_color = border_color or g.colors["white"] self.selected_color = selected_color or g.colors["light_blue"] self.unselected_color = unselected_color or g.colors["dark_blue"] self.selected = False def redraw(self): if self.selected: self.background_color = self.selected_color else: self.background_color = self.unselected_color super(SelectableText, self).redraw()
class ProgressText(SelectableText): progress = widget.causes_redraw("_progress") progress_color = widget.causes_redraw("_progress_color") def __init__(self, parent, *args, **kwargs): self.parent = parent self.progress = kwargs.pop("progress", 0) self.progress_color = kwargs.pop("progress", g.colors["blue"]) super(ProgressText, self).__init__(parent, pos, size, **kwargs) def redraw(self): super(ProgressText, self).redraw() width, height = self.real_size self.surface.fill(self.progress_color, (0, 0, width * self.progress, height)) self.draw_borders()
class StyledText(Text): def update_text(self): self.text = "".join(self.chunks) chunks = widget.call_on_change("_chunks", update_text) styles = widget.causes_redraw("_styles") def __init__(self, *args, **kwargs): chunks = kwargs.pop("chunks", ()) styles = kwargs.pop("styles", ()) super(StyledText, self).__init__(*args, **kwargs) self.chunks = chunks self.styles = styles def print_text(self): if self.styles: offset = 0 styles = [] for chunk, style in zip(self.chunks, self.styles): offset += len(chunk) styles.append(list(style) + [offset]) styles[-1][-1] = 0 print_string(self.surface, self.text, (3, 2), self.font, styles, self.align, self.valign, self.real_size, self.wrap) else: super(StyledText, self).print_text()
class ProgressText(SelectableText): progress = widget.causes_redraw("_progress") progress_color = widget.auto_reconfig("_progress_color", g.resolve_color_alias) _progress_color = widget.causes_redraw("__progress_color") def __init__(self, parent, pos, size, *args, **kwargs): self.parent = parent self.progress = kwargs.pop("progress", 0) self.progress_color = kwargs.pop("progress", "progress_background_progress") kwargs.setdefault("border_color", "progress_border") kwargs.setdefault("selected_color", "progress_background_selected") kwargs.setdefault("unselected_color", "progress_background_unselected") super(ProgressText, self).__init__(parent, pos, size, **kwargs) def redraw(self): super(ProgressText, self).redraw() width, height = self.real_size self.surface.fill(self.progress_color, (0, 0, width * self.progress, height)) self.draw_borders()
class FastText(Text): """Reduces font searches by assuming a monospace font, single-line text, and a fixed widget width.""" text = widget.set_on_change("_text", "maybe_needs_refont") _text = widget.causes_redraw("__text") old_text = "" maybe_needs_refont = False def redraw(self): self.pick_font(self.calc_text_size(self._real_size)) super(FastText, self).redraw() def pick_font(self, dimensions=None): if self.maybe_needs_refont and not self.needs_refont: if len(self.old_text) != len(self.text): self.old_text = self.text self.needs_refont = True self.maybe_needs_refont = False return super(FastText, self).pick_font(dimensions)
class Slider(button.Button): slider_color = widget.causes_redraw("_slider_color") slider_pos = widget.causes_rebuild("_slider_pos") slider_max = widget.causes_rebuild("_slider_max") slider_size = widget.causes_rebuild("_slider_size") horizontal = widget.causes_rebuild("_horizontal") def __init__(self, parent, pos = (-1,0), size = (-.1, -1), anchor = constants.TOP_RIGHT, borders = constants.ALL, border_color=None, background_color=None, slider_color=None, slider_pos=0, slider_max=10, slider_size=5, horizontal=False, **kwargs): kwargs.setdefault("priority", 80) super(Slider, self).__init__(parent, pos, size, anchor=anchor, **kwargs) self.borders = borders self.border_color = border_color or g.colors["white"] self.background_color = background_color or g.colors["dark_blue"] self.selected_color = self.background_color self.unselected_color = self.background_color self.slider_color = slider_color or g.colors["light_blue"] self.slider_pos = slider_pos self.slider_max = slider_max self.slider_size = slider_size self.horizontal = horizontal self.drag_state = None self.button = button.Button(self, pos = None, size = None, anchor = constants.TOP_LEFT, border_color = self.border_color, selected_color = self.slider_color, unselected_color = self.slider_color, priority = self.priority - 5) def redraw(self): super(Slider, self).redraw() self.button.selected_color = self.slider_color self.button.unselected_color = self.slider_color def add_hooks(self): super(Slider, self).add_hooks() self.parent.add_handler(constants.DRAG, self.handle_drag) self.parent.add_handler(constants.CLICK, self.handle_click, 50) def remove_hooks(self): super(Slider, self).remove_hooks() self.parent.remove_handler(constants.DRAG, self.handle_drag) self.parent.remove_handler(constants.CLICK, self.handle_click) def _calc_length(self, items): return items / float(self.slider_size + self.slider_max) def rebuild(self): super(Slider, self).rebuild() self.needs_resize = True def resize(self): super(Slider, self).resize() bar_start = self._calc_length(self.slider_pos) bar_length = self._calc_length(self.slider_size) if self.horizontal: self.button.pos = (-bar_start, 0) self.button.size = (-bar_length, -1) borders = [constants.TOP, constants.BOTTOM] self.button.resize() real_pos = self.button.real_pos[0] real_size = self.button.real_size[0] if real_pos == 0: borders.append(constants.LEFT) if real_pos + real_size == self.real_size[0]: borders.append(constants.RIGHT) self.button.borders = tuple(borders) else: self.button.pos = (0, -bar_start) self.button.size = (-1, -bar_length) borders = [constants.LEFT, constants.RIGHT] self.button.resize() real_pos = self.button.real_pos[1] real_size = self.button.real_size[1] if real_pos == 0: borders.append(constants.TOP) if real_pos + real_size == self.real_size[1]: borders.append(constants.BOTTOM) self.button.borders = tuple(borders) def handle_drag(self, event): if not self.visible: return if self.drag_state == None: self.start_pos = tuple(event.pos[i]-event.rel[i] for i in range(2)) self.start_slider_pos = self.slider_pos if self.button.is_over(self.start_pos): self.drag_state = True else: self.drag_state = False if self.drag_state == True: if self.horizontal: dir = 0 else: dir = 1 mouse_pos = pygame.mouse.get_pos() rel = mouse_pos[dir] - self.start_pos[dir] unit = self._calc_length(1) * self.real_size[dir] movement = int( ( rel + (unit / 2.) ) // unit ) new_pos = self.safe_pos(self.start_slider_pos + movement) self.slider_pos = new_pos raise constants.Handled def safe_pos(self, value): return max(0, min(self.slider_max, value)) def handle_click(self, event): if self.drag_state == True: self.drag_state = None if not self.is_over(pygame.mouse.get_pos()): raise constants.Handled else: self.drag_state = None def jump(self, go_lower, big_jump=False, tiny_jump=False): if big_jump: jump_dist = max(1, self.slider_max // 2) elif tiny_jump: jump_dist = max(1, self.slider_max // 100) else: jump_dist = max(1, self.slider_size - 1) if go_lower: self.slider_pos = self.safe_pos(self.slider_pos - jump_dist) else: self.slider_pos = self.safe_pos(self.slider_pos + jump_dist) def activated(self, event): assert event.type == pygame.MOUSEBUTTONUP if self.horizontal: self.jump(go_lower=(event.pos[0] < self.button.collision_rect[0])) else: self.jump(go_lower = event.pos[1] < self.button.collision_rect[1]) raise constants.Handled
class EditableText(widget.FocusWidget, Text): cursor_pos = widget.causes_redraw("_cursor_pos") def __init__(self, parent, *args, **kwargs): super(EditableText, self).__init__(parent, *args, **kwargs) if self.text is None: self.text = "" self.cursor_pos = len(self.text) def add_hooks(self): super(EditableText, self).add_hooks() self.parent.add_handler(constants.KEYDOWN, self.handle_key, 150) self.parent.add_handler(constants.CLICK, self.handle_click) def remove_hooks(self): super(EditableText, self).remove_hooks() self.parent.remove_handler(constants.KEYDOWN, self.handle_key) self.parent.remove_handler(constants.CLICK, self.handle_click) def handle_key(self, event): if not self.has_focus: return assert event.type == pygame.KEYDOWN if event.key == pygame.K_BACKSPACE: if self.cursor_pos > 0: self.text = self.text[:self.cursor_pos - 1] \ + self.text[self.cursor_pos:] self.cursor_pos -= 1 elif event.key == pygame.K_DELETE: if self.cursor_pos < len(self.text): self.text = self.text[:self.cursor_pos] \ + self.text[self.cursor_pos + 1:] elif event.key == pygame.K_LEFT: self.cursor_pos = max(0, self.cursor_pos - 1) elif event.key == pygame.K_RIGHT: self.cursor_pos = min(len(self.text), self.cursor_pos + 1) elif event.key == pygame.K_UP: self.cursor_pos = 0 elif event.key == pygame.K_DOWN: self.cursor_pos = len(self.text) elif event.unicode: char = event.unicode if char == "\r": char = "\n" self.text = self.text[:self.cursor_pos] + char \ + self.text[self.cursor_pos:] self.cursor_pos += len(char) else: return raise constants.Handled hitbox = [0, 0, 0, 0] def handle_click(self, event): if getattr(self, "collision_rect", None) is None: return elif not self.collision_rect.collidepoint(event.pos): return self.has_focus = True self.took_focus(self) self.font.set_bold(self.bold) click_x = event.pos[0] - self.collision_rect[0] click_y = event.pos[1] - self.collision_rect[1] if self.wrap: lines = split_wrap(self.text, self.font, self.real_size[0] - 4) else: lines = split_wrap(self.text, self.font, 0) line_size = self.font.get_linesize() self.hitbox[3] = line_size real_text_height = line_size * len(lines) line_y = 2 if self.valign != constants.TOP \ and real_text_height <= self.collision_rect.height - 4: excess_space = self.collision_rect.height - real_text_height if self.valign == constants.MID: line_y = excess_space // 2 else: # self.valign == constants.BOTTOM line_y = excess_space char_offset = 0 for line in lines: line_y += line_size char_offset += len(line) if line_y >= click_y: break char_offset -= len(line) self.hitbox[1] = line_y - line_size line_x = 3 if self.align != constants.LEFT: line_width = self.font.size(line)[0] excess_space = self.collision_rect.width - line_width if self.align == constants.CENTER: line_x = excess_space // 2 else: # self.align == constants.LEFT line_x = excess_space prev_width = 20000 widths = get_widths(self.font, line) for index, width in enumerate(widths): if line_x + (width // 2) < click_x: line_x += width prev_width = width else: break else: index += 1 width = 20000 self.hitbox[0] = line_x - prev_width // 2 self.hitbox[2] = prev_width - (prev_width // 2) + width // 2 self.cursor_pos = char_offset + index self.font.set_bold(False) def redraw(self): super(EditableText, self).redraw() if self.wrap: lines = split_wrap(self.text, self.font, self.real_size[0] - 4) else: lines = split_wrap(self.text, self.font, 0) if not self.has_focus: return line_size = self.font.get_linesize() real_text_height = line_size * len(lines) line_y = 2 if self.valign != constants.TOP \ and real_text_height <= self.real_size[1] - 4: excess_space = self.real_size[1] - real_text_height if self.valign == constants.MID: line_y = excess_space // 2 else: # self.valign == constants.BOTTOM line_y = excess_space char_offset = 0 for line in lines: if char_offset + len(line) < self.cursor_pos: char_offset += len(line) line_y += line_size else: break after_char = self.cursor_pos - char_offset line_x = 3 if self.align != constants.LEFT: line_width = self.font.size(line)[0] excess_space = self.real_size[0] - line_width if self.align == constants.CENTER: line_x = excess_space // 2 else: # self.align == constants.LEFT line_x = excess_space line_x += self.font.size(line[:after_char])[0] self.surface.fill(self.color, (line_x, line_y, 1, line_size)) if DEBUG: s = pygame.Surface(self.hitbox[2:]).convert_alpha() s.fill((255, 0, 255, 100)) self.surface.blit(s, self.hitbox)
class Text(widget.BorderedWidget): text = widget.call_on_change("_text", resize_redraw) base_font = widget.call_on_change("_base_font", resize_redraw) shrink_factor = widget.call_on_change("_shrink_factor", resize_redraw) underline = widget.call_on_change("_underline", resize_redraw) wrap = widget.call_on_change("_wrap", resize_redraw) bold = widget.call_on_change("_bold", resize_redraw) color = widget.causes_redraw("_color") align = widget.causes_redraw("_align") valign = widget.causes_redraw("_valign") def __init__(self, parent, pos, size=(0, .05), anchor=constants.TOP_LEFT, text=None, base_font=None, shrink_factor=1, color=None, align=constants.CENTER, valign=constants.MID, underline=-1, wrap=True, bold=False, text_size=36, **kwargs): super(Text, self).__init__(parent, pos, size, anchor, **kwargs) self.text = text self.base_font = base_font or g.font[0] self.color = color or g.colors["white"] self.shrink_factor = shrink_factor self.underline = underline self.align = align self.valign = valign self.wrap = wrap self.bold = bold self.text_size = text_size max_size = property(lambda self: convert_font_size(self.text_size)) font = property(lambda self: self._font) def pick_font(self, dimensions): nice_size = self.pick_font_size(dimensions, False) mean_size = self.pick_font_size(dimensions) if nice_size > mean_size - convert_font_size(5): size = nice_size else: size = mean_size return self.base_font[size] def font_bisect(self, test_font): left = 0 right = len(self.base_font) if self.max_size: right = min(right, self.max_size) def test_size(size): font = self.base_font[size] font.set_bold(self.bold) result = test_font(font) font.set_bold(False) return result return do_bisect(left, right, test_size) def pick_font_size(self, dimensions, break_words=True): if dimensions[0]: width = int((dimensions[0] - 4) * self.shrink_factor) else: width = None height = int((dimensions[1] - 4) * self.shrink_factor) basic_line_count = self.text.count("\n") + 1 def test_size(test_font): too_wide = False if width: if self.wrap: try: lines = split_wrap(self.text, test_font, width, break_words) except WrapError: lines = [] too_wide = True else: lines = split_wrap(self.text, test_font, 0) for line in lines: if test_font.size(line)[0] > width: too_wide = True break line_count = len(lines) else: line_count = basic_line_count too_tall = (test_font.get_linesize() * line_count) > height return not (too_tall or too_wide) return self.font_bisect(test_size) def size_using_font(self, font, width=0): #Calculate the size of the text block. raw_width, raw_height = size_of_block(self.text, font, width) #Adjust for shrink_factor and borders. width = int(raw_width / self.shrink_factor) + 4 height = int(raw_height / self.shrink_factor) + 4 return width, height def calc_text_size(self, initial_dimensions): if not (initial_dimensions[0] and initial_dimensions[1]): if not self.max_size: raise ValueError("No font size given, but a dimension is 0.") max_font = self.base_font[self.max_size] if initial_dimensions[0] == initial_dimensions[1] == 0: # No size specified, use the natural size of the max font. width, height = self.size_using_font(max_font) return (width, height), max_font elif not initial_dimensions[1]: # Width specified, use the size of the max font, word-wrapped. text_width = int( (initial_dimensions[0] - 4) * self.shrink_factor) width, height = self.size_using_font(max_font, width=text_width) return (initial_dimensions[0], height), max_font else: # Height specified. Try the natural size of the max font. width, height = self.size_using_font(max_font) if height <= initial_dimensions[1]: return (width, initial_dimensions[1]), max_font else: # Too tall. Run a binary search to find the largest font # size that fits. def test_size(font): width, height = self.size_using_font(font) width, raw_height = size_of_block(self.text, font) height = int(raw_height / self.shrink_factor) + 4 return height <= initial_dimensions[1] font_size = self.font_bisect(test_size) font = self.base_font[font_size] width, height = self.size_using_font(font) return (width, initial_dimensions[1]), font else: # Both sizes specified. Search for a usable font size. return initial_dimensions, self.pick_font(initial_dimensions) def _calc_size(self): base_size = list(super(Text, self)._calc_size()) if self.text is None: return tuple(base_size) else: # Determine the true size and font of the text area. text_size, font = self.calc_text_size(base_size) self._font = font return tuple(text_size) def redraw(self): super(Text, self).redraw() if self.text != None: self.print_text() def print_text(self): # Mark the character to be underlined (if any). no_underline = [self.color, None, False] underline = [self.color, None, True] styles = [no_underline + [0]] if 0 <= self.underline < len(self.text): styles.insert(0, underline + [self.underline + 1]) if self.underline != 0: styles.insert(0, no_underline + [self.underline]) self.font.set_bold(self.bold) # Print the string itself. print_string(self.surface, self.text, (3, 2), self.font, styles, self.align, self.valign, self.real_size, self.wrap) self.font.set_bold(False)
class Dialog(text.Text): """A Dialog is a Widget that has its own event loop and can be faded out.""" top = None # The top-level dialog. faded = widget.causes_redraw("_faded") # Used for detecting double-clicks. # (time, (x, y), button) last_click = (0, (0, 0), -1 ) def __init__(self, parent=None, pos=(.5, .1), size=(1, .9), anchor=constants.TOP_CENTER, **kwargs): kwargs.setdefault("background_color", (0, 0, 0, 0)) kwargs.setdefault("borders", ()) super(Dialog, self).__init__(parent, pos, size, anchor, **kwargs) self.visible = False self.faded = False self.is_above_mask = True self.self_mask = True self.needs_remask = True self.needs_timer = None self.handlers = {} self.key_handlers = {} self.add_handler(constants.CLICK, self.fake_escape, 200) def lost_focus(self): self.key_down = None self.faded = True self.stop_timer() def fake_escape(self, event): if event.button == 3: fake_key(pygame.K_ESCAPE) raise constants.Handled def regained_focus(self): self.faded = False self.start_timer() self.fake_mouse() def make_top(self): """Makes this dialog be the top-level dialog.""" if self.parent != None: raise ValueError, \ "Dialogs with parents cannot be the top-level dialog." else: Dialog.top = self def remake_surfaces(self): """Recreates the surfaces that this widget will draw on.""" super(Dialog, self).remake_surfaces() def start_timer(self, force = False): if self.needs_timer == None: self.needs_timer = bool(self.handlers.get(constants.TICK, False)) if self.needs_timer or force: pygame.time.set_timer(pygame.USEREVENT, 1000 / g.FPS) def stop_timer(self): pygame.time.set_timer(pygame.USEREVENT, 0) def reset_timer(self): self.stop_timer() self.start_timer() def show(self): """Shows the dialog and enters an event-handling loop.""" from code.g import play_music self.visible = True self.key_down = None self.start_timer() # Pretend to jiggle the mouse pointer, to force buttons to update their # selected state. Dialog.top.maybe_update() self.fake_mouse() # Force a timer tick at the start to make sure everything's initialized. if self.needs_timer: self.handle(pygame.event.Event(pygame.USEREVENT)) Dialog.top.maybe_update() pygame.display.flip() while True: # Update handles updates of all kinds to all widgets, as needed. Dialog.top.maybe_update() play_music() event = pygame.event.wait() result = self.handle(event) if result != constants.NO_RESULT: self.visible = False return result self.stop_timer() def add_handler(self, type, handler, priority = 100): """Adds a handler of the given type, with the given priority.""" bisect.insort( self.handlers.setdefault(type, []), (priority, handler) ) def remove_handler(self, type, handler): """Removes all instances of the given handler from the given type.""" self.handlers[type] = [h for h in self.handlers.get(type, []) if h[1] != handler] def add_key_handler(self, key, handler, priority = 100): """Adds a key handler to the given key, with the given priority.""" bisect.insort( self.key_handlers.setdefault(key, []), (priority, handler) ) def remove_key_handler(self, key, handler): """Removes all instances of the given handler from the given key.""" self.key_handlers[key] = [h for h in self.key_handlers.get(key, []) if h[1] != handler] def handle(self, event): """Sends an event through all the applicable handlers, returning constants.NO_RESULT if the event goes unhandled or is handled without requesting the dialog to exit. Otherwise, returns the value provided by the handler.""" # Get the applicable handlers. The handlers lists are all sorted. # If more than one handler type is applicable, we use [:] to make a # copy of the first type's list, then insort_all to insert the elements # of the other lists in proper sorted order. handlers = [] if event.type == pygame.MOUSEMOTION: # Compress multiple MOUSEMOTION events into one. # Note that the pos will be wrong, so pygame.mouse.get_pos() must # be used instead. time.sleep(1. / g.FPS) pygame.event.clear(pygame.MOUSEMOTION) # Generic mouse motion handlers. handlers = self.handlers.get(constants.MOUSEMOTION, [])[:] # Drag handlers. if event.buttons[0]: insort_all(handlers, self.handlers.get(constants.DRAG, [])) elif event.type == pygame.USEREVENT: # Clear excess timer ticks. pygame.event.clear(pygame.USEREVENT) # Timer tick handlers. handlers = self.handlers.get(constants.TICK, []) # Generate repeated keys. if self.key_down: self.repeat_counter += 1 if self.repeat_counter >= 5: self.repeat_counter = 0 self.handle(self.key_down) elif event.type in (pygame.KEYDOWN, pygame.KEYUP): # Generic key event handlers. handlers = self.handlers.get(constants.KEY, [])[:] if event.type == pygame.KEYDOWN: # Generic keydown handlers. insort_all(handlers, self.handlers.get(constants.KEYDOWN, [])) if event.unicode: # Unicode-based keydown handlers for this particular key. insort_all(handlers, self.key_handlers.get(event.unicode, [])) # Keycode-based handlers for this particular key. insort_all(handlers, self.key_handlers.get(event.key, [])) # Begin repeating keys. if self.key_down is not event: self.key_down = event self.repeat_counter = -10 self.start_timer(force = True) else: # event.type == pygame.KEYUP: # Stop repeating keys. self.key_down = None self.reset_timer() # Generic keyup handlers. insort_all(handlers, self.handlers.get(constants.KEYUP, [])) # Keycode-based handlers for this particular key. insort_all(handlers, self.key_handlers.get(event.key, [])) # OLPC XO-1 ebook mode. if g.ebook_mode and event.key in KEYPAD: handlers = [(0, handle_ebook)] elif event.type == pygame.MOUSEBUTTONUP: # Handle mouse scrolls by imitating PageUp/Dn if event.button in (4, 5): if event.button == 4: key = pygame.K_PAGEUP else: key = pygame.K_PAGEDOWN fake_key(key) return constants.NO_RESULT # Mouse click handlers. handlers = [] + self.handlers.get(constants.CLICK, []) when = time.time() where = event.pos what = event.button old_when, old_where, old_what = self.last_click self.last_click = when, where, what if what == old_what and when - old_when < .5: # Taxicab distance. dist = (abs(where[0] - old_where[0]) + abs(where[1] - old_where[1])) if dist < 10: # Add double-click handlers, but keep the click handlers. insort_all(handlers, self.handlers.get(constants.DOUBLECLICK, [])) elif event.type == pygame.QUIT: raise SystemExit return self.call_handlers(handlers, event) def fake_mouse(self): """Fakes a MOUSEMOTION event. MOUSEMOTION handlers must be able to handle a None event, in order to support this method.""" handlers = self.handlers.get(constants.MOUSEMOTION, [])[:] self.call_handlers(handlers, event=None) def call_handlers(self, handlers, event): # Feed the event to all the handlers, in priority order. for __, handler in handlers: try: handler(event) except constants.Handled: break # If it's been handled, we leave the rest alone. except constants.ExitDialog, e: # Exiting the dialog. if e.args: # If we're given a return value, we pass it on. return e.args[0] else: # Otherwise, exit with a return value of None. return # None of the handlers instructed the dialog to close, so we pass that # information back up to the event loop. return constants.NO_RESULT
class Listbox(widget.FocusWidget, text.SelectableText): list = widget.causes_rebuild("_list") align = widget.causes_redraw("_align") list_size = widget.causes_rebuild("_list_size") list_pos = widget.causes_rebuild("_list_pos") def __init__(self, parent, pos, size, anchor=constants.TOP_LEFT, list=None, list_pos=0, list_size=-20, borders=constants.ALL, align=constants.CENTER, **kwargs): super(Listbox, self).__init__(parent, pos, size, anchor=anchor, **kwargs) self.list = list or [] self.display_elements = [] self.borders = borders self.align = align self.list_size = list_size self.list_pos = list_pos self.auto_scroll = True self.scrollbar = scrollbar.UpdateScrollbar(self, update_func=self.on_scroll) def add_hooks(self): super(Listbox, self).add_hooks() self.parent.add_handler(constants.CLICK, self.on_click, 90) self.parent.add_key_handler(pygame.K_UP, self.got_key) self.parent.add_key_handler(pygame.K_DOWN, self.got_key) self.parent.add_key_handler(pygame.K_PAGEUP, self.got_key) self.parent.add_key_handler(pygame.K_PAGEDOWN, self.got_key) def remove_hooks(self): super(Listbox, self).remove_hooks() self.parent.remove_handler(constants.CLICK, self.on_click) self.parent.remove_key_handler(pygame.K_UP, self.got_key) self.parent.remove_key_handler(pygame.K_DOWN, self.got_key) self.parent.remove_key_handler(pygame.K_PAGEUP, self.got_key) self.parent.remove_key_handler(pygame.K_PAGEDOWN, self.got_key) def on_scroll(self, scroll_pos): self.needs_rebuild = True def on_click(self, event): if self.collision_rect.collidepoint(event.pos): self.has_focus = True self.took_focus(self) # Figure out which element was clicked... local_vert_abs = event.pos[1] - self.collision_rect[1] local_vert_pos = local_vert_abs / float(self.collision_rect.height) index = int(local_vert_pos * len(self.display_elements)) # ... and select it. self.list_pos = index + self.scrollbar.scroll_pos def safe_pos(self, raw_pos): return max(0, min(len(self.list) - 1, raw_pos)) def got_key(self, event): if not self.has_focus: return if event.type == pygame.KEYDOWN: if event.key == pygame.K_UP: new_pos = self.list_pos - 1 elif event.key == pygame.K_DOWN: new_pos = self.list_pos + 1 elif event.key == pygame.K_PAGEUP: new_pos = self.list_pos - (self.scrollbar.window - 1) elif event.key == pygame.K_PAGEDOWN: new_pos = self.list_pos + (self.scrollbar.window - 1) else: return self.list_pos = self.safe_pos(new_pos) self.scrollbar.scroll_to(self.list_pos) raise constants.Handled def num_elements(self): # If self.list_size is negative, we interpret it as a minimum height # for each element and calculate the number of elements to show. list_size = self.list_size if list_size < 0: min_height = -list_size list_size = max(1, self._make_collision_rect().height // min_height) return list_size def remake_elements(self): list_size = self.num_elements() current_size = len(self.display_elements) if current_size > list_size: # Remove the excess ones. for child in self.display_elements[list_size:]: child.remove_hooks() del self.display_elements[list_size:] elif current_size < list_size: if current_size > 0: self.display_elements[-1].borders = \ (constants.LEFT, constants.TOP) # Create the new ones. for i in range(list_size - current_size): self.display_elements.append(self.make_element()) self.display_elements[-1].borders = (constants.TOP, constants.LEFT, constants.BOTTOM) # Move the scrollbar to the end so that it gets drawn on top. self.children.remove(self.scrollbar) self.children.append(self.scrollbar) def make_element(self): return text.SelectableText(self, None, None, anchor=constants.TOP_LEFT, borders=(constants.TOP, constants.LEFT), border_color=self.border_color, selected_color=self.selected_color, unselected_color=self.unselected_color, align=self.align) def resize(self): super(Listbox, self).resize() if self.num_elements() != len(self.display_elements): self.remake_elements() self.scrollbar.resize() self.rebuild() def rebuild(self): self.list_pos = self.safe_pos(self.list_pos) if self.needs_resize: self.resize() return window_size = len(self.display_elements) list_size = len(self.list) self.scrollbar.window = len(self.display_elements) self.scrollbar.elements = list_size if self.auto_scroll: self.auto_scroll = False self.scrollbar.center(self.list_pos) self.scrollbar.rebuild() scrollbar_width = self.scrollbar.real_size[0] my_width = self.real_size[0] scrollbar_rel_width = scrollbar_width / float(my_width) offset = self.scrollbar.scroll_pos for index, element in enumerate(self.display_elements): list_index = index + offset # Position and size the element. element.pos = (0, -index / float(window_size)) element.size = (-1 + scrollbar_rel_width, -1 / float(window_size)) # Set up the element contents. element.selected = (list_index == self.list_pos) self.update_element(element, list_index) self.needs_redraw = True super(Listbox, self).rebuild() def update_element(self, element, list_index): if 0 <= list_index < len(self.list): element.text = self.list[list_index] else: element.text = ""
class Text(widget.BorderedWidget): text = causes_refont("_text") base_font = causes_refont("_base_font") color = widget.causes_redraw("_color") shrink_factor = causes_refont("_shrink_factor") underline = causes_refont("_underline") align = widget.causes_redraw("_align") valign = widget.causes_redraw("_valign") wrap = causes_refont("_wrap") bold = causes_refont("_bold") oversize = causes_refont("_oversize") needs_refont = widget.causes_resize("_needs_refont") lorem_ipsum = {} def __init__(self, parent, pos, size=(0, .05), anchor=constants.TOP_LEFT, text=None, base_font=None, shrink_factor=1, color=None, align=constants.CENTER, valign=constants.MID, underline=-1, wrap=True, bold=False, oversize=False, **kwargs): super(Text, self).__init__(parent, pos, size, anchor, **kwargs) self.needs_refont = True self.text = text self.base_font = base_font or g.font[0] self.color = color or g.colors["white"] self.shrink_factor = shrink_factor self.underline = underline self.align = align self.valign = valign self.wrap = wrap self.bold = bold self.oversize = oversize def pick_font(self, dimensions=None): if dimensions and self.needs_refont: nice_size = self.pick_font_size(dimensions, False) mean_size = self.pick_font_size(dimensions) if nice_size > mean_size - 5: size = nice_size else: size = mean_size self._font = self.base_font[size] self.needs_refont = False return self._font font = property(pick_font) def pick_font_size(self, dimensions, break_words=True): if dimensions[0]: width = dimensions[0] - 4 else: width = None height = dimensions[1] basic_line_count = self.text.count("\n") + 1 # Run a binary search for the best font size. # Thanks to bisect.bisect_left for the basic implementation. left = 8 if self.oversize or self.base_font[0] not in Text.lorem_ipsum: right = len(self.base_font) else: right = Text.lorem_ipsum[self.base_font[0]].font_size while left + 1 < right: test_index = (left + right) // 2 test_font = self.base_font[test_index] test_font.set_bold(self.bold) too_wide = False if width: if self.wrap: try: lines = split_wrap(self.text, test_font, width, break_words) except WrapError: lines = [] too_wide = True else: lines = split_wrap(self.text, test_font, 0) for line in lines: if test_font.size(line)[0] > width: too_wide = True break line_count = len(lines) else: line_count = basic_line_count too_tall = (test_font.get_linesize() * line_count) > height if too_tall or too_wide: right = test_index else: left = test_index test_font.set_bold(False) return left def calc_text_size(self, dimensions=None): if dimensions == None: dimensions = self.real_size # Calculate the text height. height = int( (dimensions[1] - 4) * self.shrink_factor ) width = dimensions[0] return width, height def _calc_size(self): base_size = list(super(Text, self)._calc_size()) if self.text != None: # Determine the true size of the text area. text_size = self.calc_text_size(base_size) # Pick a font based on that size. self.needs_refont = True font = self.pick_font(text_size) # If the width is unspecified, calculate it from the font and text. if base_size[0] == 0: base_size[0] = font.size(self.text)[0] + 16 return tuple(base_size) def redraw(self): super(Text, self).redraw() if self.text != None: self.print_text() def print_text(self): # Mark the character to be underlined (if any). no_underline = [self.color, None, False] underline = [self.color, None, True] styles = [no_underline + [0]] if 0 <= self.underline < len(self.text): styles.insert(0, underline + [self.underline + 1]) if self.underline != 0: styles.insert(0, no_underline + [self.underline]) self.font.set_bold(self.bold) # Print the string itself. print_string(self.surface, self.text, (3, 2), self.font, styles, self.align, self.valign, self.real_size, self.wrap) self.font.set_bold(False)