Exemple #1
0
def test_tabs():
    a = Tk()
    n = Notebook( a, TOP )

    # uses the Notebook's frame
    f1 = Frame( n() )
    b1 = Button( f1, text="Ignore me" )
    e1 = Entry( f1 )
    # pack your widgets before adding the frame 
    # to the Notebook (but not the frame itself)!
    b1.pack( fill=BOTH, expand=1 )
    e1.pack( fill=BOTH, expand=1 )

    # keeps the reference to the radiobutton (optional)
    x1 = n.add_screen( f1, "Tab 1" )

    f2 = Frame( n() )
    # this button destroys the 1st screen radiobutton
    b2 = Button( f2, text='Remove Tab 1', command=lambda:x1.destroy() )
    b3 = Button( f2, text='Beep...', command=lambda:Tk.bell( a ) )
    b2.pack( fill=BOTH, expand=1 )
    b3.pack( fill=BOTH, expand=1 )

    f3 = Frame( n() )


    n.add_screen( f2, "Tab 2" )
    n.add_screen( f3, "Minimize" )
    a.mainloop()
Exemple #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 Cap(Frame):
	def __init__(self):
		' set defaults, create widgets, bind callbacks, start live view '
		self.root = Tk()
		# menu:
		self.menu = Menu(self.root)
		self.root.config(menu=self.menu)
		# bind global keypresses:
		self.root.bind('q', lambda e: self.root.quit())
		self.root.bind('x', lambda e: self.root.quit())
		self.root.bind('<Destroy>', self.do_stop_video)
		self.root.bind('<space>', self.do_single_shot)
		self.root.bind('<Return>', self.do_single_shot)
		self.root.bind('<Button-3>', self.do_single_shot)
		self.root.bind('<Left>', lambda e: self.degree.set(-90))
		self.root.bind('<Right>', lambda e: self.degree.set(90))
		self.root.bind('<Up>', lambda e: self.degree.set(0))
		self.root.bind('a', lambda e: self.autocontrast.set(not self.autocontrast.get()))
		self.root.bind('e', lambda e: self.equalize.set(not self.equalize.get()))
		self.root.bind('g', lambda e: self.grayscale.set(not self.grayscale.get()))
		self.root.bind('i', lambda e: self.invert.set(not self.invert.get()))
		self.root.bind('s', lambda e: self.solarize.set(not self.solarize.get()))
		# config:
		self.config = RawConfigParser()
		self.config.read('filmroller.conf')
		if not self.config.has_section('global'):
			self.config.add_section('global')
		self.video = None
		self.invert = BooleanVar(name='invert')
		self.invert.set(self.config_get('invert', True))
		self.invert.trace('w', self.do_configure)
		self.grayscale = BooleanVar(name='grayscale')
		self.grayscale.set(self.config_get('grayscale', False))
		self.grayscale.trace('w', self.do_configure)
		self.autocontrast = BooleanVar(name='autocontrast')
		self.autocontrast.set(self.config_get('autocontrast', True))
		self.autocontrast.trace('w', self.do_configure)
		self.equalize = BooleanVar(name='equalize')
		self.equalize.set(self.config_get('equalize', False))
		self.equalize.trace('w', self.do_configure)
		self.solarize = BooleanVar(name='solarize')
		self.solarize.set(self.config_get('solarize', False))
		self.solarize.trace('w', self.do_configure)
		self.degree = IntVar(name='degree')
		self.degree.set(0)
		self.filename = StringVar(name='filename')
		self.videodevice = StringVar(name='videodevice')
		dev_names = sorted(['/dev/{}'.format(x) for x in listdir('/dev') if x.startswith('video')])
		d = self.config_get('videodevice', dev_names[-1])
		if not d in dev_names:
			d = dev_names[-1]
		self.videodevice.set(d)
		self.videodevice.trace('w', self.do_configure)
		#
		self.path = 'filmroller'
		if not exists(self.path):
			makedirs(self.path)
		# create gui:
		Frame.__init__(self, self.root)
		self.grid()
		self.x_canvas = Canvas(self, width=640, height=640, )
		self.x_canvas.pack(side='top')
		self.x_canvas.bind('<Button-1>', self.do_change_rotation)
		Checkbutton(self, text='Invert', variable=self.invert).pack(side='left')
		Checkbutton(self, text='Gray', variable=self.grayscale).pack(side='left')
		Checkbutton(self, text='Auto', variable=self.autocontrast).pack(side='left')
		OptionMenu(self, self.videodevice, *dev_names, command=self.restart_video).pack(side='left')
		Button(self, text='First role', command=self.do_first_role).pack(side='left')
		Label(self, textvariable=self.filename).pack(side='left')
		Button(self, text='Next role', command=self.do_inc_role).pack(side='left')
		Button(self, text='Take!', command=self.do_single_shot).pack(side='right')
		#filemenu = Menu(self.menu)
		#self.menu.add_cascade(label=self.videodevice.get(), menu=filemenu, )
		#for n in dev_names:
		#	filemenu.add_command(label=n, )
		#filemenu.add_separator()
		# start operation:
		self.do_first_role()
		self.do_start_video()

	def do_change_rotation(self, event):
		' determine where the image was clicked and turn that to the top '
		if event.x < 200:
			self.degree.set(-90)
		elif event.x > 640 - 200:
			self.degree.set(90)
		else:
			self.degree.set(0)

	def config_get(self, name, default):
		' read a configuration entry, fallback to default if not already stored '
		if not self.config.has_option('global', name):
			return default
		if isinstance(default, bool):
			return self.config.getboolean('global', name)
		else:
			return self.config.get('global', name)

	def do_configure(self, name, mode, cbname):
		' change a configuration entry '
		if cbname == 'w':
			value = getattr(self, name).get()
			self.config.set('global', name, str(value))
			self.config.write(open('filmroller.conf', 'w'))

	def do_first_role(self, *args):
		' jump back to first role '
		self.role = 'aa'
		self.serial = 0
		self.inc_picture()

	def inc_picture(self):
		' increment the picture number, jump over existing files '
		self.filename.set('{}/scanned.{}-{:04}.jpg'.format(self.path, self.role, self.serial, ))
		while exists(self.filename.get()):
			self.serial += 1
			self.filename.set('{}/scanned.{}-{:04}.jpg'.format(self.path, self.role, self.serial, ))
		self.root.title('filmroller - ' + self.filename.get())

	def do_inc_role(self, *args):
		' increment to next role '
		self.serial = 0
		self.role = ascii_increment(self.role)
		self.inc_picture()

	def set_pauseimage(self):
		' show pause image (during shot) '
		self.image = fromfile('filmroller.pause.png')
		self.image.thumbnail((self.previewsize['size_x'], self.previewsize['size_y'], ), )
		self.photo = PhotoImage(self.image)
		self.x_canvas.create_image(640/2, 640/2, image=self.photo)

	def do_stop_video(self, *args):
		' stop video and release device '
		if self.video is not None:
			self.video.stop()
			self.video.close()
			self.video = None

	def restart_video(self, *args):
		' restart video (if device changes or hangs) '
		self.do_stop_video()
		self.root.after(1, self.do_start_video)

	def do_start_video(self, *args):
		' init video and start live view '
		if self.video is None:
			self.video = Video_device(self.videodevice.get())
			_, _, self.fourcc = self.video.get_format()
			caps = sorted(self.video.get_framesizes(self.fourcc), cmp=lambda a, b: cmp(a['size_x']*a['size_y'], b['size_x']*b['size_y']))
			self.previewsize, self.highressize = caps[0], caps[-1]
			self.previewsize['size_x'], self.previewsize['size_y'] = self.video.set_format(
				self.previewsize['size_x'], self.previewsize['size_y'], 0, 'MJPEG')
			try: self.video.set_auto_white_balance(True)
			except: pass
			try: self.video.set_exposure_auto(True)
			except: pass
			try: self.video.set_focus_auto(True)
			except: pass
			self.video.create_buffers(30)
			self.video.queue_all_buffers()
			self.video.start()
			self.root.after(1, self.do_live_view)
			#self.x_canvas.config(width=640, height=480)
			#self.x_canvas.pack(side='top')
			self.degree.set(0)

	def do_live_view(self, *args):
		' show single pic live view and ask tk to call us again later '
		if self.video is not None:
			select((self.video, ), (), ())
			data = self.video.read_and_queue()
			self.image = frombytes('RGB', (self.previewsize['size_x'], self.previewsize['size_y']), data)
			if self.invert.get():
				self.image = invert(self.image)
			if self.grayscale.get():
				self.image = grayscale(self.image)
			if self.autocontrast.get():
				self.image = autocontrast(self.image)
			if self.equalize.get():
				self.image = equalize(self.image)
			if self.solarize.get():
				self.image = solarize(self.image)
			if self.degree.get():
				self.image = self.image.rotate(self.degree.get())
			self.photo = PhotoImage(self.image)
			self.x_canvas.create_image(640/2, 640/2, image=self.photo)
			self.root.after(3, self.do_live_view)

	def do_single_shot(self, *args):
		' do a high res single shot and store it '
		def _go():
			self.video = Video_device(self.videodevice.get())
			try:
				self.highressize['size_x'], self.highressize['size_y'] = self.video.set_format(
					self.highressize['size_x'], self.highressize['size_y'], 0, 'MJPEG')
				try: self.video.set_auto_white_balance(True)
				except: pass
				try: self.video.set_exposure_auto(True)
				except: pass
				try: self.video.set_focus_auto(True)
				except: pass
				self.video.create_buffers(7)
				self.video.queue_all_buffers()
				self.video.start()
				stop_time = time() + 3.0
				# wait for auto
				while stop_time >= time():
					select((self.video, ), (), ())
					self.update_idletasks()
					data = self.video.read_and_queue()
				image = frombytes('RGB', (self.highressize['size_x'], self.highressize['size_y'], ), data)
				if self.invert.get():
					image = invert(image)
				if self.grayscale.get():
					image = grayscale(image)
				if self.autocontrast.get():
					image = autocontrast(image)
				if self.equalize.get():
					self.image = equalize(self.image)
				if self.solarize.get():
					self.image = solarize(self.image)
				if self.degree.get():
					image = image.rotate(self.degree.get())
				image.save(self.filename.get())
				self.inc_picture()
				self.root.bell()
				self.video.stop()
			finally:
				self.video.close()
				self.video = None
			self.root.after(1, self.do_start_video)
		self.do_stop_video()
		self.set_pauseimage()
		self.update_idletasks()
		self.root.after(1, _go)
Exemple #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')