class MultiChoice(PaletteView, Control): highlight_color = ThemeProperty('highlight_color') cell_margin = ThemeProperty('cell_margin') align = 'c' tab_stop = True def __init__(self, cell_size, values, **kwds): PaletteView.__init__(self, cell_size, 1, len(values), **kwds) self.values = values def num_items(self): return len(self.values) def item_is_selected(self, n): return self.get_value() == self.values[n] def click_item(self, n, e): if self.tab_stop: self.focus() self.set_value(self.values[n]) def draw(self, surf): if self.has_focus(): surf.fill(self.highlight_color) PaletteView.draw(self, surf) def key_down(self, e): k = e.key if k == K_LEFT: self.change_value(-1) elif k == K_RIGHT: self.change_value(1) else: PaletteView.key_down(self, e) def change_value(self, d): values = self.values if values: n = len(values) value = self.get_value() try: i = values.index(value) except ValueError: if d < 0: i = 0 else: i = n - 1 else: i = max(0, min(n - 1, i + d)) self.set_value(values[i])
class DummyControl(Control): dummyThemeAttribute = ThemeProperty('dummyThemeAttribute') """" A dummy control for unit testing """ def __init__(self, **attrs): self.logger = logging.getLevelName(__name__) self.set(**attrs) self._dummyThemeAttribute = None def getDummyThemeAttribute(self): return self._dummyThemeAttribute def setDummyThemeAttribute(self, theNewValue): self._dummyThemeAttribute = theNewValue def set(self, **kwds): for name, value in kwds.items(): if not hasattr(self, name): raise TypeError("Unexpected keyword argument '%s'" % name) setattr(self, name, value) def __repr__(self): formattedMe: str = \ f"DummyControl(value: '{self._value}' enabled: '{self._enabled}' highlighted: '{self._highlighted})'" return formattedMe
class CheckWidget(Widget): default_size = (16, 16) """ The default size of the checkbox; Default is 16x16 """ margin = 4 """ The margin around the check mark; Default is 4 """ border_width = 1 """ This border width of the rectangle around the checkmark; Default is 1 """ smooth = ThemeProperty('smooth') """ Set to True if you want the checkmark anti-aliased; Default is True """ def __init__(self, **kwds): super().__init__(Rect((0, 0), self.default_size), **kwds) def draw(self, theSurface: Surface): """ Args: theSurface: The surface to draw on """ if self.highlighted: r = self.get_margin_rect() fg = self.fg_color d = CHECK_MARK_TWEAK p1 = (r.left, r.centery - d) p2 = (r.centerx - d, r.bottom) p3 = (r.right, r.top - d) if self.smooth: draw.aalines(theSurface, fg, False, [p1, p2, p3]) else: draw.lines(theSurface, fg, False, [p1, p2, p3])
class PaletteView(GridView): """ The PaletteView class is an abstract base class for implementing tool palettes and similar things. A PaletteView displays an array of items which can be selected by clicking, with the selected item being highlighted. There is provision for scrolling, so that the palette can contain more items than are displayed at one time. The PaletteView does not maintain the items themselves or keep track of which one is selected; these things are responsibilities of the subclass. """ """ nrows int No. of displayed rows ncols int No. of displayed columns Abstract methods: num_items() --> no. of items draw_item(surface, item_no, rect) """ sel_width = ThemeProperty('sel_width') """ Width of the border drawn around the selected item when the highlight_style is 'frame'. """ scroll_button_size = ThemeProperty('scroll_button_size') """ Size of the scrolling buttons. (This is a number, not a tuple -- the scroll buttons are square.) """ scroll_button_color = ThemeProperty('scroll_button_color') """ Color in which to draw the scrolling buttons. """ highlight_style = ThemeProperty('highlight_style') """ Determines the way in which a selected cell is highlighted. Values are: - _frame_ to draw a frame around the cell - _fill_ to fill its background with the sel_color - _reverse_ to swap the foreground and background colours - _arrows_ """ def __init__(self, cell_size, nrows, ncols, scrolling=False, **kwds): """ Initializes the palette view with the specified cell_size, and a rect sized for displaying nrows rows and ncols columns of items. If scrolling is true, controls will be displayed for scrolling the view. Args: cell_size: A tuple that specifies the cell size (width, height) nrows: The # of rows ncols: The # of columns scrolling: True to display scroll bars, else false **kwds: """ self.logger = logging.getLogger(__name__) # # Python 3 update # # GridView.__init__(self, cell_size, nrows, ncols, **kwds) super().__init__(cell_size, nrows, ncols, **kwds) self.scrolling = scrolling if scrolling: d = self.scroll_button_size #l = self.width #b = self.height self.width += d # self.scroll_up_rect = Rect(l, 0, d, d).inflate(-4, -4) # self.scroll_down_rect = Rect(l, b - d, d, d).inflate(-4, -4) self.scroll = 0 def scroll_up_rect(self): d = self.scroll_button_size r = Rect(0, 0, d, d) m = self.margin r.top = m r.right = self.width - m r.inflate_ip(-4, -4) return r def scroll_down_rect(self): d = self.scroll_button_size r = Rect(0, 0, d, d) m = self.margin r.bottom = self.height - m r.right = self.width - m r.inflate_ip(-4, -4) return r def draw(self, surface): GridView.draw(self, surface) if self.can_scroll_up(): self.draw_scroll_up_button(surface) if self.can_scroll_down(): self.draw_scroll_down_button(surface) def draw_scroll_up_button(self, surface): r = self.scroll_up_rect() c = self.scroll_button_color draw.polygon(surface, c, [r.bottomleft, r.midtop, r.bottomright]) def draw_scroll_down_button(self, surface): r = self.scroll_down_rect() c = self.scroll_button_color draw.polygon(surface, c, [r.topleft, r.midbottom, r.topright]) def draw_cell(self, surface, row, col, rect): i = self.cell_to_item_no(row, col) if i is not None: highlight = self.item_is_selected(i) self.draw_item_and_highlight(surface, i, rect, highlight) def draw_item_with(self, surface, i, rect, fg): old_fg = self.fg_color self.fg_color = fg try: self.draw_item(surface, i, rect) finally: self.fg_color = old_fg def draw_prehighlight_with(self, theSurface: Surface, theItemNumber: int, theRect: Rect, color): style = self.highlight_style if style == 'frame': frame_rect(theSurface, color, theRect, self.sel_width) elif style == 'fill' or style == 'reverse': theSurface.fill(color, theRect) def mouse_down(self, event): if self.scrolling: p = event.local # # Python 3 method signature update for tuples # break up to make easier to debug # scrollDownRect: Rect = self.scroll_down_rect() scrollUpRect: Rect = self.scroll_up_rect() canScrollDown: bool = scrollDownRect.collidepoint(p[0], p[1]) canScrollUp: bool = scrollUpRect.collidepoint(p[0], p[1]) self.logger.debug( f"p: {p}, downRect.centerx {scrollDownRect.centerx}, downRect.centery {scrollDownRect.centery}" ) # if self.scroll_up_rect().collidepoint(p): if canScrollUp: self.scroll_up() return # elif self.scroll_down_rect().collidepoint(p): elif canScrollDown: self.scroll_down() return GridView.mouse_down(self, event) def scroll_up(self): if self.can_scroll_up(): self.scroll -= self.items_per_page() def scroll_down(self): if self.can_scroll_down(): self.scroll += self.items_per_page() def scroll_to_item(self, n): i = max(0, min(n, self.num_items() - 1)) p = self.items_per_page() self.scroll = p * (i // p) def can_scroll_up(self): return self.scrolling and self.scroll > 0 def can_scroll_down(self): return self.scrolling and self.scroll + self.items_per_page( ) < self.num_items() def items_per_page(self): return self.num_rows() * self.num_cols() def click_cell(self, row, col, event): i = self.cell_to_item_no(row, col) if i is not None: self.click_item(i, event) def cell_to_item_no(self, row, col): i = self.scroll + row * self.num_cols() + col if 0 <= i < self.num_items(): return i else: return None def num_rows(self): ch = self.cell_size[1] if ch: return self.height // ch else: return 0 def num_cols(self): width = self.width if self.scrolling: width -= self.scroll_button_size cw = self.cell_size[0] if cw: return width // cw else: return 0 # ======================================================================== # # Abstract methods follow; Some implemented with default behavior # # ======================================================================== def draw_item_and_highlight(self, theSurface: Surface, theItemNumber: int, theRect: Rect, highlight: bool): """ Draws the cell for item theItemNumber, together with highlighting if highlight is true. The default implementation calls draw_prehighlight, draw_item and draw_posthighlight. Args: theSurface: The surface to drawn on theItemNumber: The item # of highlight theRect: The pygame rect to use highlight: If True highlight Returns: """ if highlight: self.draw_prehighlight(theSurface, theItemNumber, theRect) if highlight and self.highlight_style == 'reverse': fg = self.inherited('bg_color') or self.sel_color else: fg = self.fg_color self.draw_item_with(theSurface, theItemNumber, theRect, fg) if highlight: self.draw_posthighlight(theSurface, theItemNumber, theRect) def draw_prehighlight(self, theSurface: Surface, theItemNumber: int, theRect: Rect): """ Called for highlighted cells before draw_item, to draw highlighting that is to appear underneath the cell's contents. Args: theSurface: theItemNumber: theRect: """ if self.highlight_style == 'reverse': color = self.fg_color else: color = self.sel_color self.draw_prehighlight_with(theSurface, theItemNumber, theRect, color) def draw_posthighlight(self, theSurface: Surface, theItemNumber: int, theRect: Rect): """ Called for highlighted cells after draw_item, to draw highlighting that is to appear on top of the cell's contents. Args: theSurface: theItemNumber: theRect: """ pass def item_is_selected(self, theItemNumber: int) -> bool: """ Should return a boolean indicating whether item number item_no is currently to be considered selected. Args: theItemNumber: Returns True if it is, False """ return False def click_item(self, theItemNumber: int, theEvent: Event): """ Called when a mouse-down event occurs in item theItemNumber. Typically the subclass will record the fact that the item is selected so that this can be reported later via item_is_selected(). Args: theItemNumber: theEvent: Returns: """ pass
class Widget(AlbowRect): """ The Widget class is the base class for all widgets. A widget occupies a rectangular area of the PyGame screen to which all drawing in it is clipped, and it may receive mouse and keyboard events. A widget may also contain subwidgets. .. Note:: Due to a limitation of PyGame sub-surfaces, a widget's rectangle must be entirely contained within that of its parent widget. An exception will occur if this is violated. """ lastDebugRectTime = datetime.now() + timedelta(seconds=4) debug_rect = False current_cursor = None root_widget = None font = FontProperty('font') """ Font to use for drawing text in the widget. How this property is used depends on the widget. Some widgets have additional font properties for specific parts of the widget. """ fg_color = ThemeProperty('fg_color') """ Foreground colour for the contents of the widget. How this property is used depends on the widget. Some widgets have additional colour properties for specific parts of the widget. """ bg_color = ThemeProperty('bg_color') """ Background colour of the widget. If specified, the widget's rect is filled with this colour before drawing its contents. If no background colour is specified or it is set to None, the widget has no background and is drawn transparently over its parent. For most widgets, it defaults to None. """ bg_image = ThemeProperty('bg_image') """ An image to be displayed in the background. If specified, this overrides any bg_color. """ scale_bg = ThemeProperty('scale_bg') """ If true, and the background image is smaller than the widget in either direction, the background image is scaled to fill the widget, otherwise it is centered. Note: Due to a limitation of the pygame rotozoom function, scaling is currently uniform in both directions, with the scale factor being that required to ensure that the whole widget is covered. """ border_width = ThemeProperty('border_width') """ Width of a border to be drawn inside the outer edge of the widget. If this is unspecified or set to zero, no border is drawn. """ border_color = ThemeProperty('border_color') """ Color in which to draw the border specified by border_width. """ sel_color = ThemeProperty('sel_color') margin = ThemeProperty('margin') """ The amount of space to leave between the edge of the widget and its contents. Note that this distance includes the border_width, e.g. if border_width == 1 and margin == 3, then there is 2 pixels of space between the inside of the border and the contents. Most of the predefined Albow widgets honour the margin property, but this is not automatic for your own widget subclasses. You may find the get_margin_rect() method helpful in implementing support for the margin property in your widget classes. """ menu_bar = overridable_property('menu_bar') """ A MenuBar to be attached to and managed by this widget. Assigning to the menu_bar property automatically adds the menu bar as a child widget. Also, if the width of the menu bar has not already been set, it is set to be the same width as this widget and to stretch horizontally with it. When a key down event with the platform's standard menu command modifier (Command on Mac, Control on other platforms) is dispatched through this widget, the menu bar is first given a chance to handle the event. If the menu bar does not handle it, dispatching continues as normal. """ is_gl_container: bool = overridable_property('is_gl_container') """ Controls the drawing behaviour of the widget when used in an OpenGL window. When true, - no 2D drawing is performed for the widget itself - its background colour and border properties are ignored - its draw() and draw_over() methods are never called. If it has 3D subwidgets, 3D drawing is performed for them. When false, the widget and its subwidgets are rendered to a temporary surface which is then drawn to the window using glDrawPixels() with blending. No 3D drawing is performed for any of its subwidgets. In either case, input events are handled in the usual way. This property has no effect on widgets in a non-OpenGL window. """ tab_stop: bool = False """ True if this widget should receive the keyboard focus when the user presses the Tab key. Defaults to false. """ enter_response = None cancel_response = None _menubar = None _visible = True _is_gl_container = False redraw_every_event = True resizing_axes = {'h': 'lr', 'v': 'tb'} resizing_values = {'': [0], 'm': [1], 's': [0, 1]} visible = overridable_property('visible') """ When true, the widget is visible and active. When false, the widget is invisible and will not receive events. Defaults to true. The behaviour of this property can be customized by overriding the get_visible method. """ parent = None """ Read-only. The widget having this widget as a subwidget, or None if the widget is not contained in another widget. A widget must ultimately be contained in the root widget in order to be drawn and to receive events. """ focus_switch: "Widget" = None """ subwidget to receive key events """ def __init__(self, rect: Rect = None, **kwds): """ Creates a new widget, initially without any parent. If a rect is given, it specifies the new widget's initial size and position relative to its parent. Args: rect: A PyGame rectangle defining the portion of the parent widget's coordinate system occupied by the widget. Modifying this rectangle changes the widget's size and position. **kwds: Additional attributes specified as key-value pairs """ super().__init__(rect) self.logger = logging.getLogger(__name__) # self.predictor = albow.core.ui.Predictor.Predictor(self) # """Helps the widget look up attributes""" self.is_modal = False self.modal_result = None self.set(**kwds) def set(self, **kwds): for name, value in kwds.items(): if not hasattr(self, name): raise TypeError(f"Unexpected keyword argument {name}") setattr(self, name, value) def add_anchor(self, mode: str): """ Adds the options specified by mode to the anchor property. Args: mode: The new anchor mode to add Returns: """ self.anchor = "".join(set(self.anchor) | set(mode)) def remove_anchor(self, mode: str): """ Remove the options specified by mode from anchor property. Args: mode: The anchor mode to remove Returns: """ self.anchor = "".join(set(self.anchor) - set(mode)) def set_resizing(self, axis, value): chars = self.resizing_axes[axis] anchor = self.anchor for c in chars: anchor = anchor.replace(c, '') for i in self.resizing_values[value]: anchor += chars[i] self.anchor = anchor + value def add(self, arg: 'Widget'): # Python 3 forward reference; """ Adds the given widget or sequence of widgets as a subwidget of this widget. Args: arg: May be a single widget or multiple """ if arg: self.logger.debug( f"arg: '{arg.__str__()}' is Widget {isinstance(arg, Widget)}") # # Python 3 hack because 'Label' is sometimes reported as not a 'Widget' # if isinstance(arg, Widget) or not hasattr(arg, '__iter__'): arg.set_parent(self) else: self.logger.debug(f"arg is container: {arg.__str__}") for item in arg: self.add(item) def add_centered(self, widget): """ Adds the given widget and positions it in the center of this widget. Args: widget: The widget to center """ w, h = self.size widget.center = w // 2, h // 2 self.add(widget) def remove(self, widget): """ If the given widget is a subwidget of this widget, it is removed and its parent attribute is set to None. Args: widget: The widget to act on """ if widget in self.subwidgets: widget.set_parent(None) def set_parent(self, parent): """ Changes the parent of this widget to the given widget. This is an alternative to using the add and remove methods of the parent widget. Setting the parent to None removes the widget from any parent. Args: parent: """ if parent is not self.parent: if self.parent: self.parent._remove(self) self.parent = parent if parent: parent._add(self) def _add(self, widget): self.subwidgets.append(widget) def _remove(self, widget): self.subwidgets.remove(widget) if self.focus_switch is widget: self.focus_switch = cast(Widget, None) def draw_all(self, surface): if self.visible: surf_rect = surface.get_rect() bg_image = self.bg_image if bg_image: if self.scale_bg: bg_width, bg_height = bg_image.get_size() width, height = self.size if width > bg_width or height > bg_height: hscale = width / bg_width vscale = height / bg_height bg_image = rotozoom(bg_image, 0.0, max(hscale, vscale)) r = bg_image.get_rect() r.center = surf_rect.center surface.blit(bg_image, r) else: bg = self.bg_color if bg: surface.fill(bg) self.draw(surface) bw = self.border_width if bw: bc = self.border_color or self.fg_color frame_rect(surface, bc, surf_rect, bw) for widget in self.subwidgets: sub_rect = widget.rect sub_rect = surf_rect.clip(sub_rect) self.debugSubWidgetDraws(sub_rect, widget) if sub_rect.width > 0 and sub_rect.height > 0: try: sub = surface.subsurface(sub_rect) except ValueError as e: if str( e ) == "subsurface rectangle outside surface area": self.diagnose_subsurface_problem(surface, widget) else: raise else: widget.draw_all(sub) self.draw_over(surface) def debugSubWidgetDraws(self, sub_rect, widget): if Widget.debug_rect is True: currentTime = datetime.now() if currentTime >= Widget.lastDebugRectTime: self.logger.info( f"Drawing subwidget '{widget}{sub_rect}' of '{self}'") Widget.lastDebugRectTime = currentTime + timedelta(seconds=3) def diagnose_subsurface_problem(self, surface, widget): mess = "Widget %s %s outside parent surface %s %s" % ( widget, widget.rect, self, surface.get_rect()) sys.stderr.write("%s\n" % mess) surface.fill((255, 0, 0), widget.rect) def find_widget(self, pos: tuple): for widget in self.subwidgets[::-1]: if widget.visible: r = widget.rect if isinstance(pos, map): pos = list(pos) if r.collidepoint(pos[0], pos[1]): return widget.find_widget(subtract(pos, r.topleft)) return self def handle_mouse(self, name, event): self.augment_mouse_event(event) self.call_handler(name, event) self.setup_cursor(event) def augment_mouse_event(self, event): """ Args: event: The event to augment """ posMap = self.global_to_local(event.pos) event.dict['local'] = list(posMap) def setup_cursor(self, event): cursor = self.get_cursor(event) or arrow_cursor if cursor is not Widget.current_cursor: set_cursor(*cursor) Widget.current_cursor = cursor def dispatch_key(self, name, event): if self.visible: if event.cmd and event.type == KEYDOWN: menubar = self._menubar if menubar and menubar.handle_command_key(event): return widget = self.focus_switch if widget: widget.dispatch_key(name, event) else: self.call_handler(name, event) else: self.call_parent_handler(name, event) def handle_event(self, name, event): handler = getattr(self, name, None) if handler: return handler(event) else: parent = self.next_handler() if parent: return parent.handle_event(name, event) def get_focus(self): """ If this widget or one of its subwidgets has the keyboard focus, returns that widget. Otherwise it returns the widget that would have the keyboard focus if this widget were on the focus path. Returns: A widget with the focus """ widget = self while 1: focus = widget.focus_switch if not focus: break widget = focus return widget def notify_attention_loss(self): widget = self while 1: if widget.is_modal: break parent = widget.parent if not parent: break focus = parent.focus_switch if focus and focus is not widget: focus.dispatch_attention_loss() widget = parent def dispatch_attention_loss(self): widget = self while widget: widget.attention_lost() widget = widget.focus_switch def handle_command(self, name, *args): method = getattr(self, name, None) if method: return method(*args) else: parent = self.next_handler() if parent: return parent.handle_command(name, *args) def next_handler(self): if not self.is_modal: return self.parent def call_handler(self, name, *args): """ If the widget has a method with the given name, it is called with the given arguments, and its return value is is returned. Otherwise, nothing is done and 'pass' is returned. Args: name: The method name *args: The arguments to use Returns: The value of the 'called' method """ method = getattr(self, name, None) if method: return method(*args) else: return 'pass' def call_parent_handler(self, name, *args): """ Invokes call_handler on the parent of this widget, if any. This can be used to pass an event on to a parent widget if you don't want to handle it. Args: name: The method name *args: Its arguments Returns: The value of the 'called' methood """ parent = self.next_handler() if parent: parent.call_handler(name, *args) def is_inside(self, container): widget = self while widget: if widget is container: return True widget = widget.parent return False def present(self, centered: bool = True): """ Presents the widget as a modal dialog. The widget is added as a subwidget of the root widget, centered within it if centered is true. A nested event loop is entered in which any events for widgets other than this widget and its subwidgets are ignored. Control is retained until this widget's dismiss method is called. The argument to dismiss is returned from the present call. Args: centered: Indicates whether or not to center; default is True Returns: The value returned from the modal widget """ root = self.get_root() if centered: self.center = root.center root.add(self) root.run_modal(self) self.dispatch_attention_loss() root.remove(self) self.logger.debug("Widget.present: returning. Result: %s", self.modal_result) return self.modal_result def dismiss(self, value=True): """ When the presented widget presented is modal using present() causes the modal event loop to exit and the present() call to return with the given result. Args: value: The value to set in modal_result Returns: """ self.modal_result = value def get_root(self): """ Returns the root widget (whether this widget is contained within it or not). Deprecated, use RootWidget.getRoot() Returns: The root widget """ return Widget.root_widget def get_top_widget(self) -> "Widget": """ Returns the highest widget in the containment hierarchy currently receiving input events. If a modal dialog is in progress, the modal dialog widget is the top widget, otherwise it is the root widget. Returns: The top level widget in a containment hierarchy """ top = self while top.parent and not top.is_modal: top = top.parent return top def focus(self): """ Gives this widget the keyboard focus. The widget must be visible (i.e. contained within the root widget) for this to have any affect. """ parent = self.next_handler() if parent: parent.focus_on(self) def focus_on(self, subwidget): old_focus = self.focus_switch if old_focus is not subwidget: if old_focus: old_focus.dispatch_attention_loss() self.focus_switch = subwidget self.focus() def has_focus(self): """ Returns: True if the widget is on the focus path, i.e. this widget or one of its subwidgets currently\ has the keyboard focus. """ return self.is_modal or (self.parent and self.parent.focused_on(self)) def focused_on(self, widget): return self.focus_switch is widget and self.has_focus() def focus_chain(self): result = [] widget = self while widget: result.append(widget) widget = widget.focus_switch return result def shrink_wrap(self): contents = self.subwidgets if contents: rects = [widget.rect for widget in contents] # rmax = Rect.unionall(rects) # broken in PyGame 1.7.1 rmax = rects.pop() for r in rects: rmax = rmax.union(r) self._rect.size = list(add(rmax.topleft, rmax.bottomright)) def invalidate(self): """ Marks the widget as needing to be redrawn. You will need to call this from the begin_frame() method of your Shell or Screen if you have the redraw_every_frame attribute of the root widget set to False. NOTE: Currently, calling this method on any widget will cause all widgets to be redrawn on the next return to the event loop. Future versions may be more selective. """ root = self.get_root() if root: root.do_draw = True def predict(self, kwds, name): try: return kwds[name] except KeyError: return Theme.getThemeRoot().get(self.__class__, name) def predict_attr(self, kwds, name): try: return kwds[name] except KeyError: return getattr(self, name) def init_attr(self, kwds, name): try: return kwds.pop(name) except KeyError: return getattr(self, name) def predict_font(self, kwds, name='font'): return kwds.get(name) or Theme.getThemeRoot().get_font( self.__class__, name) def get_margin_rect(self) -> Rect: """ Returns a Rect in local coordinates representing the content area of the widget, as determined by its margin property. Returns: The rect of the content area """ r = Rect((0, 0), self.size) d = -2 * self.margin r.inflate_ip(d, d) return r def set_size_for_text(self, width, nLines=1): """ Sets the widget's Rect to a suitable size for displaying text of the specified width and number of lines in its current font, as determined by the font property. The width can be either a number of pixels or a piece of sample text. Args: width: The number of pixels or some sample text nLines: The number of lines in the text; Defaults to 1 """ if width is not None: font = self.font d = 2 * self.margin if isinstance(width, str): width, height = font.size(width) width += d + 2 else: height = font.size("X")[1] self.size = (width, height * nLines + d) def tab_to_first(self): chain = self.get_tab_order() if chain: chain[0].focus() def tab_to_next(self): top = self.get_top_widget() chain = top.get_tab_order() try: i = chain.index(self) except ValueError: return target = chain[(i + 1) % len(chain)] target.focus() def get_tab_order(self): result = [] self.collect_tab_order(result) return result def collect_tab_order(self, result): if self.visible: if self.tab_stop: result.append(self) for child in self.subwidgets: child.collect_tab_order(result) def inherited(self, attributeName: str): """ Looks up the parent hierarchy to find the first widget that has an attribute with the given name, and returns its value. If not found, returns None. Args: attributeName: The name of the attribute Returns: The attribute's value or None if not found """ value = getattr(self, attributeName) if value is not None: return value else: parent = self.next_handler() if parent: return parent.inherited(attributeName) def get_mouse(self): root = self.get_root() return root.get_mouse_for(self) def get_menu_bar(self): return self._menubar def set_menu_bar(self, menubar): if menubar is not self._menubar: if self._menubar: self.remove(self._menubar) self._menubar = menubar if menubar: if menubar.width == 0: menubar.width = self.width menubar.anchor = 'lr' self.add(menubar) def get_is_gl_container(self): return self._is_gl_container def set_is_gl_container(self, x): self._is_gl_container = x def gl_draw_all(self, gl_surface): if self.visible: if self.is_gl_container: self.gl_draw_self(gl_surface) for subwidget in self.subwidgets: gl_subsurface = gl_surface.subsurface(subwidget.rect) subwidget.gl_draw_all(gl_subsurface) else: surface = Surface(self.size, SRCALPHA) self.draw_all(surface) gl_surface.gl_enter() gl_surface.blit(surface) gl_surface.gl_exit() def gl_draw_self(self, gl_surface): gl_surface.gl_enter() # TODO: draw background and border here self.draw(gl_surface) gl_surface.gl_exit() def defer_drawing(self): """ Called every time around the event loop on the root widget or a widget that is modal. If it returns true, the frame timer runs, scheduled calls are made, and screen updates are performed once per frame. Otherwise the screen is updated after each mouse down, mouser up or keyboard event and scheduled calls are not made. """ return False def relative_mode(self): """ Return true if relative input mode should be used. Called each time around the event loop on the root widget or a widget that is modal. In relative input mode, the mouse cursor is hidden and mouse movements are not constrained to the edges of the window. In this mode, mouse movement events are delivered to the widget having the keyboard focus by calling the 'mouse_delta' method. The 'rel' attribute of the event should be used to obtain the movement since the last mouse event. Mouse down and mouse up events are also delivered to the focus widget, using the usual methods. The user can always escape from relative mode temporarily by pressing Ctrl-Shift-Escape. Normal mouse functionality is restored and further input events are ignored until a mouse click or key press occurs. """ return False def __contains__(self, event: Event): r = Rect(self._rect) r.left = 0 r.top = 0 answer: bool = False try: p = self.global_to_local(event.pos) pList = list(p) answer = r.collidepoint(pList[0], pList[1]) except AttributeError as ae: self.logger.error(f"{ae.__repr__()}") return answer # # Abstract methods follow # def draw(self, surface: Surface): """ Called whenever the widget's contents need to be drawn. The surface is a subsurface the same size as the widget's rect with the drawing origin at its top left corner. The widget is filled with its background colour, if any, before this method is called. The border and subwidgets, if any, are drawn after this method returns. Args: surface: The pygame surface to draw on """ pass def draw_over(self, surface: Surface): """ Called after drawing all the subwidgets of this widget. This method can be used to draw content that is to appear on top of any subwidgets. Args: surface: The pygame surface to draw on """ pass def key_down(self, theKeyEvent: Event): """ Called when a key press event occurs and this widget has the keyboard focus, or a subwidget has the focus but did not handle the event. NOTE: If you override this method and don't want to handle a key_down event, be sure to call the inherited key_down() method to pass the event to the parent widget. Args: theKeyEvent: The key event """ k = theKeyEvent.key self.logger.debug("Widget.key_down: %s", k) if k == K_RETURN or k == K_KP_ENTER: if self.enter_response is not None: self.dismiss(self.enter_response) return elif k == K_ESCAPE: if self.cancel_response is not None: self.dismiss(self.cancel_response) return elif k == K_TAB: self.tab_to_next() return self.call_parent_handler('key_down', theKeyEvent) def key_up(self, theKeyEvent: Event): """ Called when a key release event occurs and this widget has the keyboard focus. NOTE: - If you override this method and don't want to handle a key_up event - be sure to call the inherited key_up() method to pass the event to the parent widget. Args: theKeyEvent: The key event """ self.call_parent_handler('key_up', theKeyEvent) def get_cursor(self, event): """ Called to determine the appropriate cursor to display over the widget. The ResourceUtility.get_cursor() function returns a suitable tuple. Args: event: An event object containing the mouse coordinates to be used in determining the cursor. Returns: A cursor in the form of a tuple of arguments to the PyGame set_cursor() function """ self.logger.debug(f"event {event}") return arrow_cursor def attention_lost(self): """ Called when the widget is on the focus path, and a mouse-down event occurs in any widget which is not on the focus path. The focus path is defined as the widget having the keyboard focus, plus any widgets on the path from there up the parent hierarchy to the root widget. This method can be useful to ensure that changes to a data structure being edited are committed before performing some other action. """ pass def get_visible(self): """ Called to determine the value of the visible property. By overriding this, you can make the visibility of the widget dependent on some external condition. Returns: The widget visibility state """ return self._visible def set_visible(self, x): self._visible = x
class TitledDialog(Dialog): TD_SIZE: int = 300 wrap_width = ThemeProperty('wrap_width') """ The number of pixels at which we wrap the input text message """ def __init__(self, title: str = 'Default Title', message: str = '', okTxt: str = 'Ok', cancelTxt: str = 'Cancel', thirdButtTxt: str = None, client=None, wrapWidth: int = 100, **kwds): """ The dialog reports which button was pressed with the text of the button. TODO: This constructor has way too many parameters; Dude, simplify the parameter list; Perhaps, use the kwds dictionary Args: title: The title of the titled dialog message: The message to display okTxt: The text to display in the first button, The default is 'Ok' cancelTxt: The text to display in the second button, The default is 'Cancel thirdButtTxt: The text to display in the third button, The default is None which means the button will NOT be displayed client: Where to center the window. The default is the entire window wrapWidth: When to start wrapping the message text **kwds: Additional attributes to pass to the basic dialog """ super().__init__(client=client, width=TitledDialog.TD_SIZE, **kwds) self.logger: Logger = getLogger(__name__) self.title: str = title self.wrap_width = wrapWidth self.logger.info(f'margin: {self.margin}') dlgTitleBar: DialogTitleBar = DialogTitleBar( theTitle=title, width=TitledDialog.TD_SIZE) mainContainer: Column = self._makeMainPartOfDialog( dlgTitleBar, message) buttRow = self._makeButtons(cancelTxt, okTxt, thirdButtTxt) dlgColAttrs: AttrDict = { 'expand': 1, 'margin': 8, 'border_width': 2, 'border_color': Theme.CITY_LIGHTS, 'align': 'r', } dlgColumn: Column = Column([mainContainer, buttRow], **dlgColAttrs) dlgColumn.topleft = (self.margin, self.margin) self.add(dlgColumn) self.shrink_wrap() def _makeMainPartOfDialog(self, dlgTitleBar, message): lblMsg: Label = wrapped_label(message, self.wrap_width, margin=3) mainColAttrs: AttrDict = { 'spacing': self.margin, 'margin': 4, 'align': 'l', 'equalize': 'w', } mainColumn: Column = Column([dlgTitleBar, lblMsg], **mainColAttrs) return mainColumn def _makeButtons(self, cancelTxt, okTxt, thirdButtTxt): """ Args: okTxt: The text to display in the first button cancelTxt: The text to display in the second button thirdButtTxt: The text to display in the third button Returns: A row container with the appropriate buttons """ butOk: Button = Button(okTxt, action=lambda x=okTxt: self.dismiss(x)) butCancel: Button = Button(cancelTxt, action=lambda x=cancelTxt: self.dismiss(x)) butThree: Button = cast(Button, None) if thirdButtTxt is not None: butThree = Button(thirdButtTxt, action=lambda x=thirdButtTxt: self.dismiss(x)) buttRowAttrs: AttrDict = { 'spacing': self.margin, 'margin': 4, 'equalize': 'w', } if butThree is None: buttRow: Row = Row([butOk, butCancel], **buttRowAttrs) else: buttRow: Row = Row([butOk, butCancel, butThree], **buttRowAttrs) return buttRow
class Image(Widget): """ An Image is a widget that displays an image. """ highlight_color = ThemeProperty('highlight_color') """ The image highlight color """ image = overridable_property('image') """ The image to display. The behaviour of this property can be customised by overriding the `get_image()` method. """ highlighted = False """ Indicates whether or not to highlight the image; Default is _False_ """ def __init__(self, theImage = None, theRect: Rect = None, **kwds): """ TODO Do a unit test on this class Initializes the widget to display the given image. The initial size is determined by the image. Args: theImage: Either a string that is the image name and can be found via the resource lookup methods or an actual image. (TBD) - I don't like this and will change the API to only accept image names theRect: The pygame rectangle to draw in **kwds: """ super().__init__(theRect, **kwds) if theImage: if isinstance(theImage, str): theImage = ResourceUtility.get_image(theImage) w, h = theImage.get_size() d = 2 * self.margin self.size = w + d, h + d self._image = theImage else: self._image = None def get_image(self): """ Called to get the value of the image property. By overriding this method, you can make the widget display an image from an outside source. Returns: The pygame image """ return self._image def set_image(self, x): self._image = x def draw(self, surf): if self.highlighted: surf.fill(self.highlight_color) self.draw_image(surf, self.image) def draw_image(self, surf, image): frame = surf.get_rect() r = image.get_rect() r.center = frame.center surf.blit(image, r)
class TextBox(Widget): """ This is a basic _read-only_ multi-line display widget with support for scrolling. Currently, the API consumer breaks up lines via the `LINE_SEPARATOR` character. The widget automatically displays scroll buttons when the number of lines in `text` exceeds the `numberOfRows` """ LINE_SEPARATOR = "\n" """ The character to use to break up lines in the text widget """ CANONICAL_WIDEST_TALLEST_CHARACTER = "W" NO_TEXT = '' disabled_color = ThemeProperty('disabled_color') """ The color to use when the text box is disabled """ enabled_bg_color = ThemeProperty('enabled_bg_color') """ The enabled background color """ disabled_bg_color = ThemeProperty('disabled_bg_color') """ The disabled background color """ scroll_button_size = ThemeProperty('scroll_button_size') """ Size of the scrolling buttons. This is a number, not a tuple -- the scroll buttons are square. """ scroll_button_color = ThemeProperty('scroll_button_color') """ Color in which to draw the scrolling buttons. """ def __init__(self, theText: str = "", theNumberOfColumns: int = 28, theNumberOfRows: int = 6, **kwds): """ Args: theText: The text to display in the multi-line widget theNumberOfColumns: The number of columns to display. One column is one character theNumberOfRows: The number of rows to display. One text line is one row **kwds: Additional key value pairs that affect the text widget """ self.logger = logging.getLogger(__name__) super().__init__(**kwds) self.margin = 4 lines: List = [] if theText is not None: lines = theText.strip().split(TextBox.LINE_SEPARATOR) self.lines = lines """ Saves the broken up lines""" self.numberOfColumns = theNumberOfColumns """The number of columns in the widget""" self.numberOfRows = theNumberOfRows """The number of rows in the widget""" self.firstIdx = 0 """"The index into `lines` as the first line to display""" self.lastIdx = 0 """The index into `lines` as the last line to display""" self.size = self.computeBoxSize(theNumberOfColumns, theNumberOfRows) self.logger.debug(f"size: {self.size}") self.lastInsertedVisible = True """If `True` whenever the developer inserts a new line then the widget scrolls, if necessary to keep it visible""" self.firstRow = 0 """What scroll bars think the first row index is""" self._text = theText self._lastInsertedVisible = True self.debugJustInserted = False def getText(self): return self._text def setText(self, theNewText: str): """ Replace the contents with this new text Args: theNewText: The text that replaces what was in the widget """ lines = theNewText.strip().split(TextBox.LINE_SEPARATOR) self.lines = lines self.debugJustInserted = True self.logger.debug(f"# of lines: {len(self.lines)}") self._text = theNewText self._recomputeTextToDisplayIndices() text = property(getText, setText) """ The text to be displayed. This can be changed dynamically """ def getLastInsertedVisible(self): return self._lastInsertedVisible def setLastInsertedVisible(self, theNewValue: bool): self._lastInsertedVisible = theNewValue lastInsertedVisible = property(getLastInsertedVisible, setLastInsertedVisible) def addText(self, newText: str): """ Different than setText. This appends the new text to the contents of the widget Args: newText: The new text to append to the text widget """ oldLines: str = self.getText() oldLines += f"{newText}{TextBox.LINE_SEPARATOR}" self.setText(oldLines) def insertText(self, theNewLine): """ Args: theNewLine: """ oldLines: str = self.getText() oldLines = f"{theNewLine}{TextBox.LINE_SEPARATOR}" + oldLines self.setText(oldLines) def deleteText(self, theLineNumber: int = 0): """ Lines are defined as strings of text separated by `TextBox.LINE_SEPARATOR` Can't delete any lines if widget is empty (operation is ignored) Can't delete a line that does not exist (operation is ignored) Args: theLineNumber: The line number to delete; Defaults to the first line (numbered 0) """ if len(self.getText()) > 0: oldLines: str = self.getText() splits: List[str] = oldLines.splitlines(True) self.logger.info(f'splits: {splits}') if len(splits) > theLineNumber: del splits[theLineNumber] newLines: str = ''.join(splits) self.logger.info(f'newLines: {newLines}') self.setText(newLines) def clearText(self): """ Empties the text widget """ self.firstIdx = 0 self.lastIdx = 0 self.setText(TextBox.NO_TEXT) def draw(self, theSurface: Surface): """ Args: theSurface: The surface onto which to draw """ r = theSurface.get_rect() b = self.border_width if b: e = -2 * b r.inflate_ip(e, e) theSurface.fill(self.bg_color, r) x = self.margin y = self.margin if self.logger.level == logging.DEBUG: if self.debugJustInserted is True: self.debugJustInserted = False self.logger.debug( f"firstIdx: {self.firstIdx} lastIdx: {self.lastIdx}") for idx in range(self.firstIdx, self.lastIdx): buf = self.font.render(self.lines[idx], True, self.fg_color) theSurface.blit(buf, (x, y)) y += buf.get_rect().height if len(self.lines) > self.numberOfRows: self.draw_scroll_up_button(theSurface) self.draw_scroll_down_button(theSurface) def mouse_down(self, theEvent: Event): localPosition = theEvent.local scrollDownRect: Rect = self.scroll_down_rect() scrollUpRect: Rect = self.scroll_up_rect() scrolledDown: bool = scrollDownRect.collidepoint( localPosition[0], localPosition[1]) scrolledUp: bool = scrollUpRect.collidepoint(localPosition[0], localPosition[1]) if scrolledDown: self.firstIdx += 1 elif scrolledUp: if self.firstIdx != 0: self.firstIdx -= 1 if self.firstRow < 0: self.firstIdx = 0 if self.firstIdx >= len(self.lines) - 1: self.firstIdx = len(self.lines) - 1 # self.firstRow = self.firstIdx # self._recomputeTextToDisplayIndices() self.logger.debug( f"firstRow: {self.firstRow} -- len(self.lines) {len(self.lines)}") def computeBoxSize(self, theNumberOfColumns: int, theNumberOfRows: int) -> tuple: width, height = self.font.size( TextBox.CANONICAL_WIDEST_TALLEST_CHARACTER) self.logger.debug(f"width: {width}, height: {height}") size = (width * theNumberOfColumns, (height * theNumberOfRows) + self.margin) self.logger.debug(f"size {size}") return size def draw_scroll_up_button(self, theSurface: Surface): r = self.scroll_up_rect() c = self.scroll_button_color draw.polygon(theSurface, c, [r.bottomleft, r.midtop, r.bottomright]) def draw_scroll_down_button(self, theSurface: Surface): r = self.scroll_down_rect() c = self.scroll_button_color draw.polygon(theSurface, c, [r.topleft, r.midbottom, r.topright]) def scroll_up_rect(self): d = self.scroll_button_size r = Rect(0, 0, d, d) m = self.margin r.top = m r.right = self.width - m r.inflate_ip(-4, -4) return r def scroll_down_rect(self): d = self.scroll_button_size r = Rect(0, 0, d, d) m = self.margin r.bottom = self.height - m r.right = self.width - m r.inflate_ip(-4, -4) return r def _recomputeTextToDisplayIndices(self): # self.firstIdx = self.firstRow if len(self.lines) < self.numberOfRows: self.lastIdx = len(self.lines) else: if self.lastInsertedVisible is True: self.lastIdx = len(self.lines) self.firstIdx = self.lastIdx - self.numberOfRows self.firstRow = self.firstIdx else: self.lastIdx = self.firstIdx + self.numberOfRows if self.lastIdx >= len(self.lines): self.lastIdx = len(self.lines) self.logger.debug( f"firstIdx: {self.firstIdx} lastIdx: {self.lastIdx}")
class TabPanel(Widget): """ A `TabPanel` is a widget that manages a collection of pages and displays one of them at a time. Switching between pages is accomplished by means of a row of 'tabs' at the top of the TabPanel, one for each page. Clicking on a tab brings its corresponding page to the front. """ tab_font = FontProperty('tab_font') """ Font in which to display the page titles in the tabs. """ tab_height = ThemeProperty('tab_height') """ Height of the tabs. """ tab_border_width = ThemeProperty('tab_border_width') """ Width of the border, if any, to draw around each tab. """ tab_spacing = ThemeProperty('tab_spacing') """ Width of space to leave between adjacent tabs. """ tab_margin = ThemeProperty('tab_margin') """ Width of space to leave before the first tab and after the last tab. """ tab_fg_color = ThemeProperty('tab_fg_color') """ Color in which to display the page titles. """ default_tab_bg_color = ThemeProperty('default_tab_bg_color') """ Color with which to fill the background of any tab whose page does not specify a tab color. """ tab_area_bg_color = ThemeProperty('tab_area_bg_color') """ Color with which to fill any background areas of the tab region not covered by a tab. """ tab_dimming = ThemeProperty('tab_dimming') """ Factor by which to dim the background colour of tabs other than the currently selected tab. The range is 0.0 to 1.0. """ def __init__(self, pages=None, enterTabAction: Callable = None, exitTabAction: Callable = None, **kwds): """ Args: pages: The pages, if provided should be a sequence of (title, widget) pairs, which will be added by the add_page() method. **kwds: """ super().__init__(**kwds) self.logger = logging.getLogger(__name__) self.enterTabAction = enterTabAction """ The method to call when a tab is switched to. The method is called with an augmented event that has the tab index that is getting the focus. The tab index is 0 based """ self.exitTabAction = exitTabAction """ The method to call when a tab is switched from. The method is called with an augmented event that has the tab index of the tab losing the focus. The tab index is 0 based """ self.pages = [] self.current_page = None if pages: w = h = 0 for title, page in pages: w = max(w, page.width) h = max(h, page.height) self._add_page(title, page) self.size = (w, h) self.show_page(pages[0][1]) def content_size(self) -> tuple: """ The area that a page will take up when it is displayed. Returns: A tuple `(width, height)` of the size of the content area """ return self.width, self.height - self.tab_height def content_rect(self) -> Rect: """ Returns: A `Rect` representing the content area in the local coordinate system of the `TabPanel`. """ return Rect((0, self.tab_height), self.content_size()) def page_height(self) -> int: """ Returns: The height of a page (equal to the height of the content_rect()). """ return self.height - self.tab_height def add_page(self, theTitle: str, thePage: Widget): """ Adds the given widget as a new page, with the specified title to be displayed in its tab. .. Note:: The widget should **NOT** also be added using add(); the TabPanel will do that itself when appropriate. Args: theTitle: The page title thePage: The widget that is the page """ self._add_page(theTitle, thePage) if not self.current_page: self.show_page(thePage) def _add_page(self, title, page): page.tab_title = title page.anchor = 'ltrb' self.pages.append(page) def remove_page(self, thePage: Widget): """ Removes the specified page, if present. Args: thePage: The page to remove """ try: i = self.pages.index(thePage) del self.pages[i] except IndexError: pass if thePage is self.current_page: self.show_page(None) def show_page(self, newPage: Widget, theEvent: Event = None): if self.current_page: self._doExitTabAction(theEvent) self.remove(self.current_page) self.current_page = newPage if newPage: th = self.tab_height newPage.rect = Rect(0, th, self.width, self.height - th) self._doEnterTabAction(theEvent=theEvent, newPage=newPage) self.add(newPage) newPage.focus() def draw(self, surf): self.draw_tab_area_bg(surf) self.draw_tabs(surf) def draw_tab_area_bg(self, surf): bg = self.tab_area_bg_color if bg: surf.fill(bg, (0, 0, self.width, self.tab_height)) def draw_tabs(self, surf): font = self.tab_font fg = self.tab_fg_color b = self.tab_border_width if b: surf.fill(fg, (0, self.tab_height - b, self.width, b)) for i, title, page, selected, rect in self.iter_tabs(): x0 = rect.left w = rect.width h = rect.height r = rect if not selected: r = Rect(r) r.bottom -= b self.draw_tab_bg(surf, page, selected, r) if b: surf.fill(fg, (x0, 0, b, h)) surf.fill(fg, (x0 + b, 0, w - 2 * b, b)) surf.fill(fg, (x0 + w - b, 0, b, h)) buf = font.render(title, True, page.fg_color or fg) r = buf.get_rect() r.center = (x0 + w // 2, h // 2) surf.blit(buf, r) def iter_tabs(self): pages = self.pages current_page = self.current_page n = len(pages) b = self.tab_border_width s = self.tab_spacing h = self.tab_height m = self.tab_margin width = self.width - 2 * m + s - b x0 = m for i, page in enumerate(pages): x1 = m + (i + 1) * width // n selected = page is current_page yield i, page.tab_title, page, selected, Rect( x0, 0, x1 - x0 - s + b, h) x0 = x1 def draw_tab_bg(self, surf, page, selected, rect): bg = self.tab_bg_color_for_page(page) if not selected: bg = brighten(bg, self.tab_dimming) surf.fill(bg, rect) def tab_bg_color_for_page(self, page): return getattr(page, 'tab_bg_color', None) \ or page.bg_color \ or self.default_tab_bg_color def mouse_down(self, theEvent: Event): x, y = theEvent.local if y < self.tab_height: i = self.tab_number_containing_x(x) if i is not None: self.show_page(self.pages[i], theEvent) def tab_number_containing_x(self, x): n = len(self.pages) m = self.tab_margin width = self.width - 2 * m + self.tab_spacing - self.tab_border_width i = (x - m) * n // width if 0 <= i < n: return i def _doExitTabAction(self, theEvent: Event): leavingIndex: int = self.pages.index(self.current_page) self.logger.debug(f"leavingIndex: {leavingIndex}") if self.exitTabAction is not None: augmentedEvent: Event = self._augmentEvent(theEvent=theEvent, theIndex=leavingIndex) self.exitTabAction(augmentedEvent) def _doEnterTabAction(self, theEvent: Event, newPage): if newPage is not None: enterIndex: int = self.pages.index(newPage) self.logger.debug(f"enterIndex: {enterIndex}") if self.exitTabAction is not None: augmentedEvent: Event = self._augmentEvent(theEvent=theEvent, theIndex=enterIndex) self.enterTabAction(augmentedEvent) def _augmentEvent(self, theEvent: Event, theIndex: int) -> Event: if theEvent is None: theEvent = Event(USEREVENT, {'index': theIndex}) else: theEvent.dict['index'] = theIndex return theEvent
class Label(Widget): """ Initializes the label with the given text and font. If a width is specified, it is used, otherwise the label is initially made just wide enough to contain the text. The text may consist of more than one line, separated by '\\n'. The initial height is determined by the number of lines and the font specified at construction time or the default font from the theme. """ """ Properties """ text = overridable_property('text') """ The text to be displayed. This can be changed dynamically """ align = overridable_property('align') """ Specifies the alignment of the text within the widget's rect. One of 'l', 'c' or 'r' for left, center or right. """ highlight_color = ThemeProperty('highlight_color') """The color to use for highlighting the label""" disabled_color = ThemeProperty('disabled_color') """The color to use when the label is disabled""" highlight_bg_color = ThemeProperty('highlight_bg_color') """The highlight background color""" enabled_bg_color = ThemeProperty('enabled_bg_color') """The enabled background color""" disabled_bg_color = ThemeProperty('disabled_bg_color') """The disabled background color""" enabled = True """Indicates if label should be enabled. Defaults to True""" highlighted = False """ Indicates whether the label should be highlighted. Defaults to False. If set to true you MUST define highlight_color """ _align = 'l' def __init__(self, text, width=None, **kwds): """ Args: text: The label text width: The width of the label **kwds: Additional key value pairs that affect the label """ self.logger = logging.getLogger(__name__) super().__init__(**kwds) self.size = self.computeSize(text, width) self._text = text self.logger.debug("Control size %s", self.size) def get_text(self): return self._text def set_text(self, theNewText): self._text = theNewText self.size = self.computeSize(theNewText) def get_align(self): return self._align def set_align(self, x): self._align = x def draw(self, surface: Surface): """ Args: surface: The surface onto which to draw """ if not self.enabled: fg = self.disabled_color bg = self.disabled_bg_color elif self.highlighted: fg = self.highlight_color bg = self.highlight_bg_color else: fg = self.fg_color bg = self.enabled_bg_color self.draw_with(surface, fg, bg) def draw_with(self, surface: Surface, fg: tuple, bg: tuple = None): """ Args: surface: The surface to drawn on fg: The foreground color bg: The background color Returns: """ if bg: r = surface.get_rect() b = self.border_width if b: e = -2 * b r.inflate_ip(e, e) surface.fill(bg, r) m = self.margin align = self.align width = surface.get_width() y = m lines = self.text.split("\n") font = self.font dy = font.get_linesize() for line in lines: image = font.render(line, True, fg) r = image.get_rect() r.top = y if align == 'l': r.left = m elif align == 'r': r.right = width - m else: r.centerx = width // 2 surface.blit(image, r) y += dy def computeSize(self, theText: str, theWidth: int = None) -> tuple: """ Args: theText: The text from which to compute the label size theWidth: A *must* have minimum width Returns: a tuple of the from (width, height) """ font = self.font lines = theText.split("\n") tw, th = 0, 0 for line in lines: w, h = font.size(line) tw = max(tw, w) th += h if theWidth is not None: tw = theWidth else: tw = max(1, tw) d = 2 * self.margin adjustedWidth = tw + d adjustedHeight = th + d # self.size = (tw + d, th + d) # Python 3 update size = (adjustedWidth, adjustedHeight) return size
class FileDialog(Dialog): box_width = 250 default_prompt = None up_button_text = ThemeProperty("up_button_text") def __init__(self, prompt=None, suffixes=None, **kwds): super().__init__(**kwds) label = None d = self.margin self.suffixes = suffixes or () if self.up_button_text is None: self.up_button_text = '' up_button = Button(self.up_button_text, action=self.go_up) dir_box = DirectoryPathView(self.box_width - up_button.width - 10, self) self.dir_box = dir_box top_row = Row([dir_box, up_button]) list_box = FileListView(self.box_width - 16, self) self.list_box = list_box ctrls = [top_row, list_box] prompt = prompt or self.default_prompt if prompt: label = Label(prompt) if self.saving: filename_box = TextField(self.box_width) filename_box.change_action = self.update self.filename_box = filename_box ctrls.append(Column([label, filename_box], align='l', spacing=0)) else: if label: ctrls.insert(0, label) ok_button = Button(self.ok_label, action=self.ok, enable=self.ok_enable) self.ok_button = ok_button cancel_button = Button("Cancel", action=self.cancel) vbox = Column(ctrls, align='l', spacing=d) vbox.topleft = (d, d) y = vbox.bottom + d ok_button.topleft = (vbox.left, y) cancel_button.topright = (vbox.right, y) self.add(vbox) self.add(ok_button) self.add(cancel_button) self.shrink_wrap() self._directory = None self.directory = os.getcwd() # print "FileDialog: cwd =", repr(self.directory) ### if self.saving: filename_box.focus() def get_directory(self): return self._directory def set_directory(self, x): x = os.path.abspath(x) while not os.path.exists(x): y = os.path.dirname(x) if y == x: x = os.getcwd() break x = y # if self._directory <> x: if self._directory != x: self._directory = x self.list_box.update() self.update() directory = property(get_directory, set_directory) def filter(self, path): suffixes = self.suffixes if not suffixes: return os.path.isfile(path) for suffix in suffixes: if path.endswith(suffix): return True def update(self): pass def go_up(self): self.directory = os.path.dirname(self.directory) def dir_box_click(self, double): if double: name = self.list_box.get_selected_name() path = os.path.join(self.directory, name) suffix = os.path.splitext(name)[1] if suffix not in self.suffixes and os.path.isdir(path): self.directory = path else: self.double_click_file(name) self.update() def ok(self): self.dismiss(True) def cancel(self): self.dismiss(False)