Source code for enable.text_grid

# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE.txt and may be redistributed only under
# the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!
"""
TextGrid is a text grid widget that is meant to be used with Numpy.
"""

# Major library imports
from numpy import arange, array, dstack, repeat, newaxis

# Enthought library imports
from traits.api import (
    Any, Array, Bool, Int, List, Property, Tuple, observe,
)
from enable.trait_defs.kiva_font_trait import KivaFont

# Relative imports
from .component import Component
from .colors import black_color_trait, ColorTrait
from .enable_traits import LineStyle
from .font_metrics_provider import font_metrics_provider


[docs]class TextGrid(Component): """ A 2D grid of string values """ # A 2D array of strings string_array = Array # The cell size can be set to a tuple (w,h) or to "auto". cell_size = Property # ------------------------------------------------------------------------ # Appereance traits # ------------------------------------------------------------------------ # The font to use for the text of the grid font = KivaFont("modern 14") # The color of the text text_color = black_color_trait # The padding around each cell cell_padding = Int(5) # The thickness of the border between cells cell_border_width = Int(1) # The color of the border between cells cell_border_color = black_color_trait # The dash style of the border between cells cell_border_style = LineStyle("solid") # Text color of highlighted items highlight_color = ColorTrait("red") # Cell background color of highlighted items highlight_bgcolor = ColorTrait("lightgray") # A list of tuples of the (i,j) of selected cells selected_cells = List # ------------------------------------------------------------------------ # Private traits # ------------------------------------------------------------------------ # Are our cached extent values still valid? _cache_valid = Bool(False) # The maximum width and height of all cells, as a tuple (w,h) _cached_cell_size = Tuple # The maximum (leading, descent) of all the text strings (positive value) _text_offset = Array # An array NxMx2 of the x,y positions of the lower-left coordinates of # each cell _cached_cell_coords = Array # "auto" or a tuple _cell_size = Any("auto") # ------------------------------------------------------------------------ # Public methods # ------------------------------------------------------------------------ def __init__(self, **kwtraits): super().__init__(**kwtraits) self.selected_cells = [] # ------------------------------------------------------------------------ # AbstractComponent interface # ------------------------------------------------------------------------ def _draw_mainlayer(self, gc, view_bounds=None, mode="default"): text_color = self.text_color_ highlight_color = self.highlight_color_ highlight_bgcolor = self.highlight_bgcolor_ padding = self.cell_padding border_width = self.cell_border_width with gc: gc.set_stroke_color(text_color) gc.set_fill_color(text_color) gc.set_font(self.font) gc.set_text_position(0, 0) width, height = self._get_actual_cell_size() numrows, numcols = self.string_array.shape # draw selected backgrounds # XXX should this be in the background layer? for j, row in enumerate(self.string_array): for i, text in enumerate(row): if (i, j) in self.selected_cells: gc.set_fill_color(highlight_bgcolor) ll_x, ll_y = self._cached_cell_coords[i, j + 1] # render this a bit big, but covered by border gc.rect( ll_x, ll_y, width + 2 * padding + border_width, height + 2 * padding + border_width, ) gc.fill_path() gc.set_fill_color(text_color) self._draw_grid_lines(gc) for j, row in enumerate(self.string_array): for i, text in enumerate(row): x, y = ( self._cached_cell_coords[i, j + 1] + self._text_offset + padding + border_width / 2.0 ) if (i, j) in self.selected_cells: gc.set_fill_color(highlight_color) gc.set_stroke_color(highlight_color) gc.set_text_position(x, y) gc.show_text(text) gc.set_stroke_color(text_color) gc.set_fill_color(text_color) else: gc.set_text_position(x, y) gc.show_text(text) # ------------------------------------------------------------------------ # Private methods # ------------------------------------------------------------------------ def _draw_grid_lines(self, gc): gc.set_stroke_color(self.cell_border_color_) gc.set_line_dash(self.cell_border_style_) gc.set_line_width(self.cell_border_width) # Skip the leftmost and bottommost cell coords (since Y axis is # reversed, the bottommost coord is the last one) x_points = self._cached_cell_coords[:, 0, 0] y_points = self._cached_cell_coords[0, :, 1] for x in x_points: gc.move_to(x, self.y) gc.line_to(x, self.y + self.height) gc.stroke_path() for y in y_points: gc.move_to(self.x, y) gc.line_to(self.x + self.width, y) gc.stroke_path() def _compute_cell_sizes(self): if not self._cache_valid: gc = font_metrics_provider() max_w = 0 max_h = 0 min_l = 0 min_d = 0 for text in self.string_array.ravel(): gc.set_font(self.font) l, d, w, h = gc.get_text_extent(text) if -l + w > max_w: max_w = -l + w if -d + h > max_h: max_h = -d + h if l < min_l: min_l = l if d < min_d: min_d = d self._cached_cell_size = (max_w, max_h) self._text_offset = array([-min_l, -min_d]) self._cache_valid = True def _compute_positions(self): if self.string_array is None or len(self.string_array.shape) != 2: return width, height = self._get_actual_cell_size() numrows, numcols = self.string_array.shape cell_width = width + 2 * self.cell_padding + self.cell_border_width cell_height = height + 2 * self.cell_padding + self.cell_border_width x_points = ( arange(numcols + 1) * cell_width + self.cell_border_width / 2.0 + self.x ) y_points = ( arange(numrows + 1) * cell_height + self.cell_border_width / 2.0 + self.y ) tmp = dstack( ( repeat(x_points[:, newaxis], numrows + 1, axis=1), repeat(y_points[:, newaxis].T, numcols + 1, axis=0), ) ) # We have to reverse the y-axis (e.g. the 0th row needs to be at the # highest y-position). self._cached_cell_coords = tmp[:, ::-1] def _update_bounds(self): if self.string_array is not None and len(self.string_array.shape) == 2: rows, cols = self.string_array.shape margin = 2 * self.cell_padding + self.cell_border_width width, height = self._get_actual_cell_size() self.bounds = [ cols * (width + margin) + self.cell_border_width, rows * (height + margin) + self.cell_border_width, ] else: self.bounds = [0, 0] def _get_actual_cell_size(self): if self._cell_size == "auto": if not self._cache_valid: self._compute_cell_sizes() return self._cached_cell_size else: if not self._cache_valid: # actually computing the text offset self._compute_cell_sizes() return self._cell_size # ------------------------------------------------------------------------ # Event handlers # ------------------------------------------------------------------------
[docs] def normal_left_down(self, event): self.selected_cells = [self._get_index_for_xy(event.x, event.y)] self.request_redraw()
def _get_index_for_xy(self, x, y): width, height = ( array(self._get_actual_cell_size()) + 2 * self.cell_padding + self.cell_border_width ) numrows, numcols = self.string_array.shape i = int((x - self.padding_left) / width) j = numrows - (int((y - self.padding_bottom) / height) + 1) shape = self.string_array.shape if 0 <= i < shape[1] and 0 <= j < shape[0]: return i, j else: return None # ------------------------------------------------------------------------ # Trait events, property setters and getters # ------------------------------------------------------------------------ def _string_array_changed(self, old, new): if self._cell_size == "auto": self._cache_valid = False self._compute_cell_sizes() self._compute_positions() self._update_bounds()
[docs] @observe("cell_border_width,cell_padding") def cell_properties_changed(self, event=None): self._compute_positions() self._update_bounds()
def _set_cell_size(self, newsize): self._cell_size = newsize if newsize == "auto": self._compute_cell_sizes() self._compute_positions() self._update_bounds() def _get_cell_size(self): return self._cell_size