Example #1
0
class SimulationCanvas( object ):
    """A canvas where blocks can be dragged around and connected up"""

    size = ( width, height ) = ( 550, 300 )

    def __init__( self, frame ):
        # Create the canvas
        self.canvas = Canvas( frame,
                             width=self.width, height=self.height,
                             relief=RIDGE,
                             background=colours["background"],
                             borderwidth=1 )
        # Add event handlers for dragable items
        self.canvas.tag_bind ( "DRAG", "<ButtonPress-1>", self.mouse_down )
        #self.canvas.tag_bind ("DRAG", "<ButtonRelease-1>", self.mouse_release)
        self.canvas.tag_bind ( "DRAG", "<Enter>", self.enter )
        self.canvas.tag_bind ( "DRAG", "<Leave>", self.leave )
        self.canvas.pack( side=TOP )

        # Some default locations
        self.PREVIEW_WIDTH = 80
        self.PREVIEW_LOCATION = ( self.PREVIEW_X, self.PREVIEW_Y ) = ( 15, 30 )


        # Draw a "preview" area
        self.canvas.create_line( self.PREVIEW_WIDTH, 0, self.PREVIEW_WIDTH, self.height, dash=True )

        # A dict indexed by unique ID of elements in the canvas.
        self.blocks = {}

    def preview_actor( self, codefile ):
        """
        Display a preview of an actor or compisite actor in the canvas,
        it will be dragable into desired position
        """
        logging.debug( "Creating a preview of %(name)s on simulation canvas." % {'name':codefile.name} )

        logging.debug( "Deleting any existing items still tagged 'preview'" )
        self.canvas.delete( "preview" )

        block = CanvasBlock( self.canvas, codefile, *self.PREVIEW_LOCATION )
        self.blocks[block.id] = block



    def mouse_down( self, event ):
        logging.debug( "The mouse was pressed at (%d, %d)" % ( event.x, event.y ) )
        logging.debug( "The mouse went down on a block. Binding mouse release..." )
        selected = self.canvas.gettags( "current" )
        logging.debug( "Currently selected items tags are %s" % selected.__repr__() )
        self.selected_name = [tag for tag in selected if tag.startswith( "name:" ) ][0][5:]
        self.selected_id = [tag for tag in selected if tag.startswith( "id:" ) ][0][3:]
        self.selected_type = [tag for tag in selected if tag.startswith( "type:" ) ][0][5:]
        logging.debug( "Block selected was %s with id:%s" % ( self.selected_name, self.selected_id ) )

        #self.canvas.addtag( 'Selected', 'withtag', self.selected_id )
        logging.debug( "Current blocks are: %s" % self.blocks )
        #self.blocks[block_id].set_colour( colours['selected'] )

        if self.selected_type == "block" or self.selected_type == "text":
            self.blocks[self.selected_id].select(event.x, event.y)
            self.canvas.bind( "<ButtonRelease-1>", self.block_move_mouse_release )
        elif self.selected_type.startswith("input") or self.selected_type.startswith("output"):
            self.blocks[self.selected_id].select_port(self.selected_type)
            self.canvas.bind( "<ButtonRelease-1>", self.port_connect_mouse_release )
        else:
            logging.info("Tried to select %s" % self.selected_type)

    
    def block_move_mouse_release( self, event ):
        logging.debug( "The mouse was released at (%d, %d)" % ( event.x, event.y ) )
        self.canvas.bind( "<ButtonRelease-1>", lambda e: None )
        if event.x >= 0 and event.x <= self.canvas.winfo_width() \
            and event.y >= 0 and event.y <= self.canvas.winfo_height():
                logging.debug( "Valid move inside canvas. Relocating block." )
                self.blocks[self.selected_id].move_to(event.x, event.y)
                if event.x >= self.PREVIEW_WIDTH:
                    if self.blocks[self.selected_id].is_preview():
                        logging.info( "Moved out of preview zone, adding new component to model" )
                        #TODO HERE - add to model compiler or what ever...
                    self.blocks[self.selected_id].unselect()
                else:
                    self.blocks[self.selected_id].preview()
        else:
            logging.info( "Invalid move." )

    def port_connect_mouse_release( self, event ):
        logging.debug( "The mouse was released at (%d, %d)" % ( event.x, event.y ) )
        self.canvas.bind( "<ButtonRelease-1>", lambda e: None )
        if event.x >= 0 and event.x <= self.canvas.winfo_width() \
            and event.y >= 0 and event.y <= self.canvas.winfo_height():
                logging.debug( "Valid location inside canvas." )

                event.widget.itemconfigure( "Selected", fill="#000000" )
                event.widget.itemconfigure( "type:text", fill="#000000" )

                #block = self.canvas.gettags("Selected")
                #logging.debug("Block moved was made up of these components: %s" % block.__repr__())
                self.canvas.dtag( "Selected", "Selected" )

        else:
            logging.info( "Invalid wiring." )

    def enter( self, event ):
        logging.debug( "Enter" )

    def leave( self, event ):
        logging.debug( "Leaving" )
Example #2
0
class NvimTk(object):
    """Wraps all nvim/tk event handling."""
    def __init__(self, nvim):
        """Initialize with a Nvim instance."""
        self._nvim = nvim
        self._attrs = {}
        self._nvim_updates = deque()
        self._canvas = None
        self._fg = '#000000'
        self._bg = '#ffffff'

    def run(self):
        """Start the UI."""
        self._tk_setup()
        t = Thread(target=self._nvim_event_loop)
        t.daemon = True
        t.start()
        self._root.mainloop()

    def _tk_setup(self):
        self._root = Tk()
        self._root.bind('<<nvim_redraw>>', self._tk_nvim_redraw)
        self._root.bind('<<nvim_detach>>', self._tk_nvim_detach)
        self._root.bind('<Key>', self._tk_key)

    def _tk_nvim_redraw(self, *args):
        update = self._nvim_updates.popleft()
        for update in update:
            handler = getattr(self, '_tk_nvim_' + update[0])
            for args in update[1:]:
                handler(*args)

    def _tk_nvim_detach(self, *args):
        self._root.destroy()

    def _tk_nvim_resize(self, width, height):
        self._tk_redraw_canvas(width, height)

    def _tk_nvim_clear(self):
        self._tk_clear_region(0, self._height - 1, 0, self._width - 1)

    def _tk_nvim_eol_clear(self):
        row, col = (
            self._cursor_row,
            self._cursor_col,
        )
        self._tk_clear_region(row, row, col, self._scroll_right)

    def _tk_nvim_cursor_goto(self, row, col):
        self._cursor_row = row
        self._cursor_col = col

    def _tk_nvim_cursor_on(self):
        pass

    def _tk_nvim_cursor_off(self):
        pass

    def _tk_nvim_mouse_on(self):
        pass

    def _tk_nvim_mouse_off(self):
        pass

    def _tk_nvim_insert_mode(self):
        pass

    def _tk_nvim_normal_mode(self):
        pass

    def _tk_nvim_set_scroll_region(self, top, bot, left, right):
        self._scroll_top = top
        self._scroll_bot = bot
        self._scroll_left = left
        self._scroll_right = right

    def _tk_nvim_scroll(self, count):
        top, bot = (
            self._scroll_top,
            self._scroll_bot,
        )
        left, right = (
            self._scroll_left,
            self._scroll_right,
        )

        if count > 0:
            destroy_top = top
            destroy_bot = top + count - 1
            move_top = destroy_bot + 1
            move_bot = bot
            fill_top = move_bot + 1
            fill_bot = fill_top + count - 1
        else:
            destroy_top = bot + count + 1
            destroy_bot = bot
            move_top = top
            move_bot = destroy_top - 1
            fill_bot = move_top - 1
            fill_top = fill_bot + count + 1

        # destroy items that would be moved outside the scroll region after
        # scrolling
        # self._tk_clear_region(destroy_top, destroy_bot, left, right)
        # self._tk_clear_region(move_top, move_bot, left, right)
        self._tk_destroy_region(destroy_top, destroy_bot, left, right)
        self._tk_tag_region('move', move_top, move_bot, left, right)
        self._canvas.move('move', 0, -count * self._rowsize)
        self._canvas.dtag('move', 'move')
        # self._tk_fill_region(fill_top, fill_bot, left, right)

    def _tk_nvim_highlight_set(self, attrs):
        self._attrs = attrs

    def _tk_nvim_put(self, data):
        # choose a Font instance
        font = self._fnormal
        if self._attrs.get('bold', False):
            font = self._fbold
        if self._attrs.get('italic', False):
            font = self._fbolditalic if font == self._fbold else self._fitalic
        # colors
        fg = "#{0:0{1}x}".format(self._attrs.get('foreground', self._fg), 6)
        bg = "#{0:0{1}x}".format(self._attrs.get('background', self._bg), 6)
        # get the "text" and "rect" which correspond to the current cell
        x, y = self._tk_get_coords(self._cursor_row, self._cursor_col)
        items = self._canvas.find_overlapping(x, y, x + 1, y + 1)
        if len(items) != 2:
            # caught part the double-width character in the cell to the left,
            # filter items which dont have the same horizontal coordinate as
            # "x"
            predicate = lambda item: self._canvas.coords(item)[0] == x
            items = filter(predicate, items)
        # rect has lower id than text, sort to unpack correctly
        rect, text = sorted(items)
        self._canvas.itemconfig(text, fill=fg, font=font, text=data or ' ')
        self._canvas.itemconfig(rect, fill=bg)
        self._tk_nvim_cursor_goto(self._cursor_row, self._cursor_col + 1)

    def _tk_nvim_bell(self):
        self._root.bell()

    def _tk_nvim_update_fg(self, fg):
        self._fg = "#{0:0{1}x}".format(fg, 6)

    def _tk_nvim_update_bg(self, bg):
        self._bg = "#{0:0{1}x}".format(bg, 6)

    def _tk_redraw_canvas(self, width, height):
        if self._canvas:
            self._canvas.destroy()
        self._fnormal = Font(family='Monospace', size=13)
        self._fbold = Font(family='Monospace', weight='bold', size=13)
        self._fitalic = Font(family='Monospace', slant='italic', size=13)
        self._fbolditalic = Font(family='Monospace',
                                 weight='bold',
                                 slant='italic',
                                 size=13)
        self._colsize = self._fnormal.measure('A')
        self._rowsize = self._fnormal.metrics('linespace')
        self._canvas = Canvas(self._root,
                              width=self._colsize * width,
                              height=self._rowsize * height)
        self._tk_fill_region(0, height - 1, 0, width - 1)
        self._cursor_row = 0
        self._cursor_col = 0
        self._scroll_top = 0
        self._scroll_bot = height - 1
        self._scroll_left = 0
        self._scroll_right = width - 1
        self._width, self._height = (
            width,
            height,
        )
        self._canvas.pack()

    def _tk_fill_region(self, top, bot, left, right):
        # create columns from right to left so the left columns have a
        # higher z-index than the right columns. This is required to
        # properly display characters that cross cell boundary
        for rownum in range(bot, top - 1, -1):
            for colnum in range(right, left - 1, -1):
                x1 = colnum * self._colsize
                y1 = rownum * self._rowsize
                x2 = (colnum + 1) * self._colsize
                y2 = (rownum + 1) * self._rowsize
                # for each cell, create two items: The rectangle is used for
                # filling background and the text is for cell contents.
                self._canvas.create_rectangle(x1,
                                              y1,
                                              x2,
                                              y2,
                                              fill=self._bg,
                                              width=0)
                self._canvas.create_text(x1,
                                         y1,
                                         anchor='nw',
                                         font=self._fnormal,
                                         width=1,
                                         fill=self._fg,
                                         text=' ')

    def _tk_clear_region(self, top, bot, left, right):
        self._tk_tag_region('clear', top, bot, left, right)
        self._canvas.itemconfig('clear', fill=self._bg)
        self._canvas.dtag('clear', 'clear')

    def _tk_destroy_region(self, top, bot, left, right):
        self._tk_tag_region('destroy', top, bot, left, right)
        self._canvas.delete('destroy')
        self._canvas.dtag('destroy', 'destroy')

    def _tk_tag_region(self, tag, top, bot, left, right):
        x1, y1 = self._tk_get_coords(top, left)
        x2, y2 = self._tk_get_coords(bot, right)
        self._canvas.addtag_overlapping(tag, x1, y1, x2 + 1, y2 + 1)

    def _tk_get_coords(self, row, col):
        x = col * self._colsize
        y = row * self._rowsize
        return x, y

    def _tk_key(self, event):
        if 0xffe1 <= event.keysym_num <= 0xffee:
            # this is a modifier key, ignore. Source:
            # https://www.tcl.tk/man/tcl8.4/TkCmd/keysyms.htm
            return
        # Translate to Nvim representation of keys
        send = []
        if event.state & 0x1:
            send.append('S')
        if event.state & 0x4:
            send.append('C')
        if event.state & (0x8 | 0x80):
            send.append('A')
        special = len(send) > 0
        key = event.char
        if _is_invalid_key(key):
            special = True
            key = event.keysym
        send.append(SPECIAL_KEYS.get(key, key))
        send = '-'.join(send)
        if special:
            send = '<' + send + '>'
        nvim = self._nvim
        nvim.session.threadsafe_call(lambda: nvim.input(send))

    def _nvim_event_loop(self):
        self._nvim.session.run(self._nvim_request, self._nvim_notification,
                               lambda: self._nvim.attach_ui(80, 24))
        self._root.event_generate('<<nvim_detach>>', when='tail')

    def _nvim_request(self, method, args):
        raise Exception('This UI does not implement any methods')

    def _nvim_notification(self, method, args):
        if method == 'redraw':
            self._nvim_updates.append(args)
            self._root.event_generate('<<nvim_redraw>>', when='tail')
class SimulationCanvas(object):
    """A canvas where blocks can be dragged around and connected up"""

    size = (width, height) = (550, 300)

    def __init__(self, frame):
        # Create the canvas
        self.canvas = Canvas(frame,
                             width=self.width,
                             height=self.height,
                             relief=RIDGE,
                             background=colours["background"],
                             borderwidth=1)
        # Add event handlers for dragable items
        self.canvas.tag_bind("DRAG", "<ButtonPress-1>", self.mouse_down)
        #self.canvas.tag_bind ("DRAG", "<ButtonRelease-1>", self.mouse_release)
        self.canvas.tag_bind("DRAG", "<Enter>", self.enter)
        self.canvas.tag_bind("DRAG", "<Leave>", self.leave)
        self.canvas.pack(side=TOP)

        # Some default locations
        self.PREVIEW_WIDTH = 80
        self.PREVIEW_LOCATION = (self.PREVIEW_X, self.PREVIEW_Y) = (15, 30)

        # Draw a "preview" area
        self.canvas.create_line(self.PREVIEW_WIDTH,
                                0,
                                self.PREVIEW_WIDTH,
                                self.height,
                                dash=True)

        # A dict indexed by unique ID of elements in the canvas.
        self.blocks = {}

    def preview_actor(self, codefile):
        """
        Display a preview of an actor or compisite actor in the canvas,
        it will be dragable into desired position
        """
        logging.debug("Creating a preview of %(name)s on simulation canvas." %
                      {'name': codefile.name})

        logging.debug("Deleting any existing items still tagged 'preview'")
        self.canvas.delete("preview")

        block = CanvasBlock(self.canvas, codefile, *self.PREVIEW_LOCATION)
        self.blocks[block.id] = block

    def mouse_down(self, event):
        logging.debug("The mouse was pressed at (%d, %d)" % (event.x, event.y))
        logging.debug(
            "The mouse went down on a block. Binding mouse release...")
        selected = self.canvas.gettags("current")
        logging.debug("Currently selected items tags are %s" %
                      selected.__repr__())
        self.selected_name = [
            tag for tag in selected if tag.startswith("name:")
        ][0][5:]
        self.selected_id = [tag for tag in selected
                            if tag.startswith("id:")][0][3:]
        self.selected_type = [
            tag for tag in selected if tag.startswith("type:")
        ][0][5:]
        logging.debug("Block selected was %s with id:%s" %
                      (self.selected_name, self.selected_id))

        #self.canvas.addtag( 'Selected', 'withtag', self.selected_id )
        logging.debug("Current blocks are: %s" % self.blocks)
        #self.blocks[block_id].set_colour( colours['selected'] )

        if self.selected_type == "block" or self.selected_type == "text":
            self.blocks[self.selected_id].select(event.x, event.y)
            self.canvas.bind("<ButtonRelease-1>",
                             self.block_move_mouse_release)
        elif self.selected_type.startswith(
                "input") or self.selected_type.startswith("output"):
            self.blocks[self.selected_id].select_port(self.selected_type)
            self.canvas.bind("<ButtonRelease-1>",
                             self.port_connect_mouse_release)
        else:
            logging.info("Tried to select %s" % self.selected_type)

    def block_move_mouse_release(self, event):
        logging.debug("The mouse was released at (%d, %d)" %
                      (event.x, event.y))
        self.canvas.bind("<ButtonRelease-1>", lambda e: None)
        if event.x >= 0 and event.x <= self.canvas.winfo_width() \
            and event.y >= 0 and event.y <= self.canvas.winfo_height():
            logging.debug("Valid move inside canvas. Relocating block.")
            self.blocks[self.selected_id].move_to(event.x, event.y)
            if event.x >= self.PREVIEW_WIDTH:
                if self.blocks[self.selected_id].is_preview():
                    logging.info(
                        "Moved out of preview zone, adding new component to model"
                    )
                    #TODO HERE - add to model compiler or what ever...
                self.blocks[self.selected_id].unselect()
            else:
                self.blocks[self.selected_id].preview()
        else:
            logging.info("Invalid move.")

    def port_connect_mouse_release(self, event):
        logging.debug("The mouse was released at (%d, %d)" %
                      (event.x, event.y))
        self.canvas.bind("<ButtonRelease-1>", lambda e: None)
        if event.x >= 0 and event.x <= self.canvas.winfo_width() \
            and event.y >= 0 and event.y <= self.canvas.winfo_height():
            logging.debug("Valid location inside canvas.")

            event.widget.itemconfigure("Selected", fill="#000000")
            event.widget.itemconfigure("type:text", fill="#000000")

            #block = self.canvas.gettags("Selected")
            #logging.debug("Block moved was made up of these components: %s" % block.__repr__())
            self.canvas.dtag("Selected", "Selected")

        else:
            logging.info("Invalid wiring.")

    def enter(self, event):
        logging.debug("Enter")

    def leave(self, event):
        logging.debug("Leaving")
Example #4
-2
class NvimTk(object):

    """Wraps all nvim/tk event handling."""

    def __init__(self, nvim):
        """Initialize with a Nvim instance."""
        self._nvim = nvim
        self._attrs = {}
        self._nvim_updates = deque()
        self._canvas = None
        self._fg = '#000000'
        self._bg = '#ffffff'

    def run(self):
        """Start the UI."""
        self._tk_setup()
        t = Thread(target=self._nvim_event_loop)
        t.daemon = True
        t.start()
        self._root.mainloop()

    def _tk_setup(self):
        self._root = Tk()
        self._root.bind('<<nvim_redraw>>', self._tk_nvim_redraw)
        self._root.bind('<<nvim_detach>>', self._tk_nvim_detach)
        self._root.bind('<Key>', self._tk_key)

    def _tk_nvim_redraw(self, *args):
        update = self._nvim_updates.popleft()
        for update in update:
            handler = getattr(self, '_tk_nvim_' + update[0])
            for args in update[1:]:
                handler(*args)

    def _tk_nvim_detach(self, *args):
        self._root.destroy()

    def _tk_nvim_resize(self, width, height):
        self._tk_redraw_canvas(width, height)

    def _tk_nvim_clear(self):
        self._tk_clear_region(0, self._height - 1, 0, self._width - 1)

    def _tk_nvim_eol_clear(self):
        row, col = (self._cursor_row, self._cursor_col,)
        self._tk_clear_region(row, row, col, self._scroll_right)

    def _tk_nvim_cursor_goto(self, row, col):
        self._cursor_row = row
        self._cursor_col = col

    def _tk_nvim_cursor_on(self):
        pass

    def _tk_nvim_cursor_off(self):
        pass

    def _tk_nvim_mouse_on(self):
        pass

    def _tk_nvim_mouse_off(self):
        pass

    def _tk_nvim_insert_mode(self):
        pass

    def _tk_nvim_normal_mode(self):
        pass

    def _tk_nvim_set_scroll_region(self, top, bot, left, right):
        self._scroll_top = top
        self._scroll_bot = bot
        self._scroll_left = left
        self._scroll_right = right

    def _tk_nvim_scroll(self, count):
        top, bot = (self._scroll_top, self._scroll_bot,)
        left, right = (self._scroll_left, self._scroll_right,)

        if count > 0:
            destroy_top = top
            destroy_bot = top + count - 1
            move_top = destroy_bot + 1
            move_bot = bot
            fill_top = move_bot + 1
            fill_bot = fill_top + count - 1
        else:
            destroy_top = bot + count + 1
            destroy_bot = bot
            move_top = top
            move_bot = destroy_top - 1
            fill_bot = move_top - 1
            fill_top = fill_bot + count + 1

        # destroy items that would be moved outside the scroll region after
        # scrolling
        # self._tk_clear_region(destroy_top, destroy_bot, left, right)
        # self._tk_clear_region(move_top, move_bot, left, right)
        self._tk_destroy_region(destroy_top, destroy_bot, left, right)
        self._tk_tag_region('move', move_top, move_bot, left, right)
        self._canvas.move('move', 0, -count * self._rowsize)
        self._canvas.dtag('move', 'move')
        # self._tk_fill_region(fill_top, fill_bot, left, right)

    def _tk_nvim_highlight_set(self, attrs):
        self._attrs = attrs

    def _tk_nvim_put(self, data):
        # choose a Font instance
        font = self._fnormal
        if self._attrs.get('bold', False):
            font = self._fbold
        if self._attrs.get('italic', False):
            font = self._fbolditalic if font == self._fbold else self._fitalic
        # colors
        fg = "#{0:0{1}x}".format(self._attrs.get('foreground', self._fg), 6)
        bg = "#{0:0{1}x}".format(self._attrs.get('background', self._bg), 6)
        # get the "text" and "rect" which correspond to the current cell
        x, y = self._tk_get_coords(self._cursor_row, self._cursor_col)
        items = self._canvas.find_overlapping(x, y, x + 1, y + 1)
        if len(items) != 2:
            # caught part the double-width character in the cell to the left,
            # filter items which dont have the same horizontal coordinate as
            # "x"
            predicate = lambda item: self._canvas.coords(item)[0] == x
            items = filter(predicate, items)
        # rect has lower id than text, sort to unpack correctly
        rect, text = sorted(items)
        self._canvas.itemconfig(text, fill=fg, font=font, text=data or ' ')
        self._canvas.itemconfig(rect, fill=bg)
        self._tk_nvim_cursor_goto(self._cursor_row, self._cursor_col + 1)

    def _tk_nvim_bell(self):
        self._root.bell()

    def _tk_nvim_update_fg(self, fg):
        self._fg = "#{0:0{1}x}".format(fg, 6)

    def _tk_nvim_update_bg(self, bg):
        self._bg = "#{0:0{1}x}".format(bg, 6)

    def _tk_redraw_canvas(self, width, height):
        if self._canvas:
            self._canvas.destroy()
        self._fnormal = Font(family='Monospace', size=13)
        self._fbold = Font(family='Monospace', weight='bold', size=13)
        self._fitalic = Font(family='Monospace', slant='italic', size=13)
        self._fbolditalic = Font(family='Monospace', weight='bold',
                                 slant='italic', size=13)
        self._colsize = self._fnormal.measure('A')
        self._rowsize = self._fnormal.metrics('linespace')
        self._canvas = Canvas(self._root, width=self._colsize * width,
                              height=self._rowsize * height)
        self._tk_fill_region(0, height - 1, 0, width - 1)
        self._cursor_row = 0
        self._cursor_col = 0
        self._scroll_top = 0
        self._scroll_bot = height - 1
        self._scroll_left = 0
        self._scroll_right = width - 1
        self._width, self._height = (width, height,)
        self._canvas.pack()

    def _tk_fill_region(self, top, bot, left, right):
        # create columns from right to left so the left columns have a
        # higher z-index than the right columns. This is required to
        # properly display characters that cross cell boundary
        for rownum in range(bot, top - 1, -1):
            for colnum in range(right, left - 1, -1):
                x1 = colnum * self._colsize
                y1 = rownum * self._rowsize
                x2 = (colnum + 1) * self._colsize
                y2 = (rownum + 1) * self._rowsize
                # for each cell, create two items: The rectangle is used for
                # filling background and the text is for cell contents.
                self._canvas.create_rectangle(x1, y1, x2, y2,
                                              fill=self._background, width=0)
                self._canvas.create_text(x1, y1, anchor='nw',
                                         font=self._fnormal, width=1,
                                         fill=self._foreground, text=' ')

    def _tk_clear_region(self, top, bot, left, right):
        self._tk_tag_region('clear', top, bot, left, right)
        self._canvas.itemconfig('clear', fill=self._bg)
        self._canvas.dtag('clear', 'clear')

    def _tk_destroy_region(self, top, bot, left, right):
        self._tk_tag_region('destroy', top, bot, left, right)
        self._canvas.delete('destroy')
        self._canvas.dtag('destroy', 'destroy')

    def _tk_tag_region(self, tag, top, bot, left, right):
        x1, y1 = self._tk_get_coords(top, left)
        x2, y2 = self._tk_get_coords(bot, right)
        self._canvas.addtag_overlapping(tag, x1, y1, x2 + 1, y2 + 1)

    def _tk_get_coords(self, row, col):
        x = col * self._colsize
        y = row * self._rowsize
        return x, y

    def _tk_key(self, event):
        if 0xffe1 <= event.keysym_num <= 0xffee:
            # this is a modifier key, ignore. Source:
            # https://www.tcl.tk/man/tcl8.4/TkCmd/keysyms.htm
            return
        # Translate to Nvim representation of keys
        send = []
        if event.state & 0x1:
            send.append('S')
        if event.state & 0x4:
            send.append('C')
        if event.state & (0x8 | 0x80):
            send.append('A')
        special = len(send) > 0
        key = event.char
        if _is_invalid_key(key):
            special = True
            key = event.keysym
        send.append(SPECIAL_KEYS.get(key, key))
        send = '-'.join(send)
        if special:
            send = '<' + send + '>'
        nvim = self._nvim
        nvim.session.threadsafe_call(lambda: nvim.input(send))

    def _nvim_event_loop(self):
        self._nvim.session.run(self._nvim_request,
                               self._nvim_notification,
                               lambda: self._nvim.attach_ui(80, 24))
        self._root.event_generate('<<nvim_detach>>', when='tail')

    def _nvim_request(self, method, args):
        raise Exception('This UI does not implement any methods')

    def _nvim_notification(self, method, args):
        if method == 'redraw':
            self._nvim_updates.append(args)
            self._root.event_generate('<<nvim_redraw>>', when='tail')