Skip to content

Instantly share code, notes, and snippets.

@acidnine
Last active August 24, 2026 03:18
Show Gist options
  • Select an option

  • Save acidnine/bae991ad9daa714c2ef18cbba7572f02 to your computer and use it in GitHub Desktop.

Select an option

Save acidnine/bae991ad9daa714c2ef18cbba7572f02 to your computer and use it in GitHub Desktop.
XED Plugin Column Edit
[Plugin]
Loader=python3
Module=column_edit
IAge=2
Name=Column Edit
Description=Ctrl+Shift+drag box/column selection with column insert and column delete modes (Sublime/VSCode-style). Works with Selection Stats plugin.
Authors=Acidnine; Gemini / ChatGPT / Claude
Copyright=Copyright © 2026 Acidnine
Website=https://vault21.net/
Version=1.0.0
Hidden=false
"""
Column Edit plugin for Xed.
Adds Sublime/VSCode-style box (column/vertical) selection:
Ctrl+Shift + click, then drag (up/down and optionally left/right)
While a box selection is active:
- typing a character -> inserts that character at the selection's
column on every line in the selection
(Column Insert Mode)
- Backspace / Delete -> deletes a character (or the selected column
range) on every line in the selection
(Column Delete Mode)
- Escape / click elsewhere / focus-out -> exits box mode
Column math:
The column is derived from the pixel position of your click, using the
width of a single space glyph in the view's current font as the
per-character width. Where the mouse position lands on top of real
text, the real character grid is used instead (so it stays accurate
even in a proportional font, or with tabs). Only *past* the end of a
line does it fall back to the fixed-width assumption, since that's a
"virtual" column that doesn't exist yet.
Padding rule:
A line that is shorter than the target column is left completely
alone until you actually type or delete something that would land on
it. Only then is it padded with spaces up to that column. Lines you
never touch are never padded.
"""
import gi
import math
import os
import warnings
gi.require_version("Gtk", "3.0")
gi.require_version("Xed", "1.0")
gi.require_version("PangoCairo", "1.0")
from gi.repository import GObject, GLib, Gtk, Gdk, Xed, PangoCairo # noqa: E402
DEBUG = bool(os.environ.get("XED_DEBUG_COLUMN_EDIT"))
def _dbg(*args):
if DEBUG:
print("[column-edit]", *args)
# PyGObject keeps its own bookkeeping of Python-side signal closures and
# tries to tidy them up when the underlying GObject is garbage collected.
# Since we already disconnect our handlers ourselves in do_deactivate(),
# that later automatic cleanup can find a handler id we already removed
# and warn about it - harmless (GObject drops all handlers on finalize
# regardless), but noisy. It fires well after our own disconnect call has
# returned, so a local try/except or context manager can't catch it; a
# standing filter on this specific message is what actually works.
warnings.filterwarnings(
"ignore",
message=r".*has no handler with id.*",
category=Warning,
module=r"gi\.overrides\.GObject",
)
# ============================================================================
# Appearance / behavior config - edit these to taste, then re-enable the
# plugin (or restart xed) to pick up changes.
# ============================================================================
# Shown when the box has real width (col_max > col_min) - i.e. typing will
# delete/replace this range on every line. Reddish by default so it reads
# as "this text is about to go away."
REPLACE_HIGHLIGHT_RGBA = "rgba(230, 70, 70, 0.35)"
# Shown when the box is zero-width (pure vertical insert points) - only
# used if SHOW_INSERT_HIGHLIGHT is True below.
INSERT_HIGHLIGHT_RGBA = "rgba(90, 150, 255, 0.30)"
# Zero-width column selections (just insert carets, nothing will be
# deleted) are invisible by default. Set True to show a faint highlight
# for those too.
SHOW_INSERT_HIGHLIGHT = False
# Small in-editor badge (top-right corner of the view) showing the current
# column and whether text will be replaced. Xed's real statusbar isn't
# reachable from a ViewActivatable (it's private to XedWindow), so this
# draws directly on the view instead.
SHOW_STATUS_OVERLAY = True
STATUS_OVERLAY_BG_RGBA = (0.10, 0.10, 0.12, 0.85)
STATUS_OVERLAY_FG_RGBA = (1.0, 1.0, 1.0, 0.95)
# Draws a thin caret on every selected row at the column where typing would
# actually land (the left edge of the range - col_min - since that's where
# a replace/insert starts). This is the only indicator for a zero-width
# (pure insert) column selection, since that case has no highlight by
# default; for a wide selection it sits at the highlight's left edge.
SHOW_MULTI_CURSOR = True
MULTI_CURSOR_RGBA = (1.0, 1.0, 1.0, 0.95)
MULTI_CURSOR_WIDTH_PX = 2
# Blink the carets like a normal text cursor. Uses GTK's own blink interval
# when available, falling back to a fixed 530ms.
MULTI_CURSOR_BLINK = True
# ============================================================================
class ColumnEditPlugin(GObject.Object, Xed.ViewActivatable):
"""
Xed instantiates one of these per open XedView automatically (via
XED_TYPE_VIEW_ACTIVATABLE in xed-view.c) - no manual window/tab
tracking required.
"""
__gtype_name__ = "ColumnEditPlugin"
view = GObject.Property(type=Xed.View)
def do_activate(self):
_dbg("activating for view", self.view)
self.helper = ColumnEditHelper(self.view)
# Expose helper dynamically so selection_stats can safely interface with it
self.view.plugin_column_helper = self.helper
def do_deactivate(self):
_dbg("deactivating for view", self.view)
if getattr(self, "helper", None):
self.helper.disconnect_all()
self.helper = None
if hasattr(self.view, 'plugin_column_helper'):
del self.view.plugin_column_helper
class ColumnEditHelper(GObject.Object):
"""Owns box-selection state + event handling for a single GtkTextView."""
__gtype_name__ = "ColumnEditHelper"
__gsignals__ = {
'column-selection-changed': (GObject.SignalFlags.RUN_FIRST, None, (int, int)),
}
TAG_REPLACE = "column-edit-highlight-replace"
TAG_INSERT = "column-edit-highlight-insert"
def __init__(self, view):
GObject.Object.__init__(self)
self.view = view
self.buffer = view.get_buffer()
self.active = False
self.dragging = False
self.start_line = 0
self.start_col = 0
self.end_line = 0
self.end_col = 0
self._char_width_cache = None
self._blink_timer_id = None
self._blink_visible = True
self._ids = [
view.connect("button-press-event", self.on_button_press),
view.connect("motion-notify-event", self.on_motion_notify),
view.connect("button-release-event", self.on_button_release),
view.connect("key-press-event", self.on_key_press),
view.connect("focus-out-event", self.on_focus_out),
]
if SHOW_STATUS_OVERLAY or SHOW_MULTI_CURSOR:
self._ids.append(view.connect_after("draw", self.on_draw))
def disconnect_all(self):
with warnings.catch_warnings():
warnings.simplefilter("ignore")
for hid in self._ids:
try:
if GObject.signal_handler_is_connected(self.view, hid):
self.view.disconnect(hid)
except Exception:
pass
self._end_column_mode()
# --- mouse handling ----------------------------------------------------
def on_button_press(self, view, event):
if event.button != 1:
return False
ctrl = bool(event.state & Gdk.ModifierType.CONTROL_MASK)
shift = bool(event.state & Gdk.ModifierType.SHIFT_MASK)
if not (ctrl and shift):
if self.active:
self._end_column_mode()
return False # let xed handle the click normally
bx, by = view.window_to_buffer_coords(
Gtk.TextWindowType.TEXT, int(event.x), int(event.y)
)
line_num, col = self._line_col_at_buffer_xy(bx, by)
self.active = True
self.dragging = True
self.start_line = self.end_line = line_num
self.start_col = self.end_col = col
self._char_width_cache = None
self._update_highlight()
self._start_blink()
_dbg("box start", line_num, col)
return True # swallow: don't move the real cursor
def on_motion_notify(self, view, event):
if not (self.active and self.dragging):
return False
bx, by = view.window_to_buffer_coords(
Gtk.TextWindowType.TEXT, int(event.x), int(event.y)
)
line_num, col = self._line_col_at_buffer_xy(bx, by)
self.end_line = line_num
self.end_col = col
self._update_highlight()
return True
def on_button_release(self, view, event):
if self.dragging:
self.dragging = False
return True
return False
def on_focus_out(self, view, event):
if self.active:
self._end_column_mode()
return False
# --- keyboard handling ---------------------------------------------------
def on_key_press(self, view, event):
if not self.active:
return False
keyval = event.keyval
state = event.state
# 1. Ignore pure modifier keys so pressing Shift doesn't instantly exit mode
modifier_keys = (
Gdk.KEY_Shift_L, Gdk.KEY_Shift_R,
Gdk.KEY_Control_L, Gdk.KEY_Control_R,
Gdk.KEY_Alt_L, Gdk.KEY_Alt_R,
Gdk.KEY_Meta_L, Gdk.KEY_Meta_R,
Gdk.KEY_Super_L, Gdk.KEY_Super_R,
Gdk.KEY_Caps_Lock, Gdk.KEY_Num_Lock
)
if keyval in modifier_keys:
return False
if keyval == Gdk.KEY_Escape:
self._end_column_mode()
return True
ctrl = bool(state & Gdk.ModifierType.CONTROL_MASK)
shift = bool(state & Gdk.ModifierType.SHIFT_MASK)
alt = bool(state & Gdk.ModifierType.MOD1_MASK)
# 2. Column-aware copy, cut, paste bindings
is_copy = (ctrl and keyval in (Gdk.KEY_c, Gdk.KEY_C, Gdk.KEY_Insert))
is_cut = (ctrl and keyval in (Gdk.KEY_x, Gdk.KEY_X)) or (shift and keyval == Gdk.KEY_Delete)
is_paste = (ctrl and keyval in (Gdk.KEY_v, Gdk.KEY_V)) or (shift and keyval == Gdk.KEY_Insert)
if is_copy:
self._apply_copy()
return True
if is_cut:
self._apply_copy()
self._apply_delete_forward()
return True
if is_paste:
self._apply_paste()
return True
# 3. Handle Deletions
if keyval == Gdk.KEY_BackSpace:
self._apply_backspace()
return True
if keyval == Gdk.KEY_Delete:
self._apply_delete_forward()
return True
if keyval in (Gdk.KEY_Return, Gdk.KEY_KP_Enter):
self._end_column_mode()
return True
# Tab has no printable unicode representation (chr(9) is not
# "printable"), so without this it fell through to the catch-all
# below and exited column mode, inserting a single tab at the real
# cursor instead of one per selected row. Respect the editor's own
# "insert spaces instead of tabs" setting rather than always
# inserting a literal tab char.
if keyval in (Gdk.KEY_Tab, Gdk.KEY_ISO_Left_Tab, Gdk.KEY_KP_Tab) and not (ctrl or alt):
col_min, _col_max = sorted((self.start_col, self.end_col))
self._apply_insert(self._tab_text(col_min))
return True
# 4. Explicit navigation keys bail out of column mode
nav_keys = (
Gdk.KEY_Up, Gdk.KEY_Down, Gdk.KEY_Left, Gdk.KEY_Right,
Gdk.KEY_Home, Gdk.KEY_End, Gdk.KEY_Page_Up, Gdk.KEY_Page_Down
)
if keyval in nav_keys:
self._end_column_mode()
return False
# 5. Process printable characters when standard command modifiers aren't held
if not (ctrl or alt):
unicode_val = Gdk.keyval_to_unicode(keyval)
if unicode_val:
ch = chr(unicode_val)
if ch.isprintable():
self._apply_insert(ch)
return True
if ctrl or alt:
return False
self._end_column_mode()
return False
# --- column math ---------------------------------------------------------
def _char_width_px(self):
if self._char_width_cache is None:
layout = self.view.create_pango_layout(" ")
width, _height = layout.get_pixel_size()
self._char_width_cache = max(width, 1)
return self._char_width_cache
def _line_bounds_iters(self, line_num):
n_lines = self.buffer.get_line_count()
line_num = max(0, min(line_num, n_lines - 1))
start_it = self.buffer.get_iter_at_line(line_num)
end_it = start_it.copy()
if not end_it.ends_line():
end_it.forward_to_line_end()
return start_it, end_it
def _line_col_at_buffer_xy(self, bx, by):
line_result = self.view.get_line_at_y(by)
if len(line_result) == 3:
_found, line_it, _line_top = line_result
else:
line_it, _line_top = line_result
line_num = line_it.get_line()
_start_it, end_it = self._line_bounds_iters(line_num)
line_len = end_it.get_line_offset()
end_rect = self.view.get_iter_location(end_it)
end_x = end_rect.x + end_rect.width
if bx <= end_x:
result = self.view.get_iter_at_position(int(bx), int(by))
if len(result) == 3:
_found, it, trailing = result
else:
it, trailing = result
col = it.get_line_offset()
if trailing:
col += 1
return line_num, min(col, line_len)
else:
char_w = self._char_width_px()
extra_px = bx - end_x
extra_chars = int(round(extra_px / char_w))
return line_num, line_len + max(extra_chars, 0)
# --- editing ops -----------------------------------------------------
def _line_len(self, line_num):
_s, e = self._line_bounds_iters(line_num)
return e.get_line_offset()
def _insert_on_line(self, line_num, col_min, col_max, ch):
if line_num >= self.buffer.get_line_count():
return
line_len = self._line_len(line_num)
if col_max > col_min and line_len > col_min:
del_end_col = min(col_max, line_len)
d_start = self.buffer.get_iter_at_line_offset(line_num, col_min)
d_end = self.buffer.get_iter_at_line_offset(line_num, del_end_col)
self.buffer.delete(d_start, d_end)
line_len = self._line_len(line_num)
if line_len < col_min:
pad_it = self.buffer.get_iter_at_line_offset(line_num, line_len)
self.buffer.insert(pad_it, " " * (col_min - line_len))
ins_it = self.buffer.get_iter_at_line_offset(line_num, col_min)
self.buffer.insert(ins_it, ch)
def _delete_range_on_line(self, line_num, col_from, col_to):
if line_num >= self.buffer.get_line_count():
return
line_len = self._line_len(line_num)
if line_len <= col_from:
return
actual_to = min(col_to, line_len)
s_it = self.buffer.get_iter_at_line_offset(line_num, col_from)
e_it = self.buffer.get_iter_at_line_offset(line_num, actual_to)
self.buffer.delete(s_it, e_it)
def _tab_text(self, col):
"""What a Tab press should insert at buffer column `col`, matching
the editor's own settings: a literal tab, or - if the editor is
configured to insert spaces instead of tabs - enough spaces to
reach the next indent stop. Since every selected row is inserted
at the same numeric column in column mode, this is computed once
and reused for all rows (each row lands on the same stop)."""
try:
use_spaces = self.view.get_insert_spaces_instead_of_tabs()
except Exception:
use_spaces = False
if not use_spaces:
return "\t"
width = None
get_indent_width = getattr(self.view, "get_indent_width", None)
if get_indent_width:
try:
iw = get_indent_width()
if iw and iw > 0:
width = iw
except Exception:
pass
if not width:
try:
width = self.view.get_tab_width()
except Exception:
width = 8
if not width or width <= 0:
width = 8
n = width - (col % width)
if n <= 0:
n = width
return " " * n
def _apply_insert(self, ch):
lo, hi = sorted((self.start_line, self.end_line))
col_min, col_max = sorted((self.start_col, self.end_col))
self.buffer.begin_user_action()
for line in range(lo, hi + 1):
self._insert_on_line(line, col_min, col_max, ch)
self.buffer.end_user_action()
new_col = col_min + len(ch)
self.start_col = new_col
self.end_col = new_col
self._update_highlight()
def _apply_backspace(self):
lo, hi = sorted((self.start_line, self.end_line))
col_min, col_max = sorted((self.start_col, self.end_col))
self.buffer.begin_user_action()
if col_max > col_min:
for line in range(lo, hi + 1):
self._delete_range_on_line(line, col_min, col_max)
self.start_col = self.end_col = col_min
elif col_min > 0:
for line in range(lo, hi + 1):
self._delete_range_on_line(line, col_min - 1, col_min)
self.start_col = self.end_col = col_min - 1
self.buffer.end_user_action()
self._update_highlight()
def _apply_delete_forward(self):
lo, hi = sorted((self.start_line, self.end_line))
col_min, col_max = sorted((self.start_col, self.end_col))
self.buffer.begin_user_action()
if col_max > col_min:
for line in range(lo, hi + 1):
self._delete_range_on_line(line, col_min, col_max)
self.start_col = self.end_col = col_min
else:
for line in range(lo, hi + 1):
self._delete_range_on_line(line, col_min, col_min + 1)
self.buffer.end_user_action()
self._update_highlight()
def _apply_copy(self):
lo, hi = sorted((self.start_line, self.end_line))
col_min, col_max = sorted((self.start_col, self.end_col))
if col_max <= col_min:
return
lines_text = []
for line in range(lo, hi + 1):
line_len = self._line_len(line)
actual_min = min(col_min, line_len)
actual_max = min(col_max, line_len)
if actual_max > actual_min:
s_it = self.buffer.get_iter_at_line_offset(line, actual_min)
e_it = self.buffer.get_iter_at_line_offset(line, actual_max)
text = self.buffer.get_text(s_it, e_it, False)
lines_text.append(text)
else:
lines_text.append("")
text = "\n".join(lines_text)
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
clipboard.set_text(text, -1)
def _apply_paste(self):
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
text = clipboard.wait_for_text()
if not text:
return
# Deliberately NOT stripping a trailing empty element here. Our own
# _apply_copy never appends a trailing newline, so a trailing ""
# after split means the last copied row genuinely had no characters
# at that column (line was too short) - it's a real row, not an
# artifact, and needs to land on the matching destination row so
# that row gets cleared rather than skipped.
lines = text.split("\n")
lo, hi = sorted((self.start_line, self.end_line))
col_min, col_max = sorted((self.start_col, self.end_col))
# A single-line clipboard (no embedded newlines) is broadcast to
# every selected row, same as typing a character in column mode.
# Anything with multiple lines is mapped row-for-row: destination
# row i gets clipboard line i. If the destination selection has
# more rows than were copied, the extra rows just get their
# existing column range cleared (nothing to merge in). If the
# destination has fewer rows than were copied, the extra clipboard
# lines are simply not used.
broadcast = len(lines) == 1
self.buffer.begin_user_action()
for i, line_num in reversed(list(enumerate(range(lo, hi + 1)))):
if broadcast:
paste_str = lines[0]
elif i < len(lines):
paste_str = lines[i]
else:
paste_str = ""
self._insert_on_line(line_num, col_min, col_max, paste_str)
self.buffer.end_user_action()
if broadcast:
new_col = col_min + len(lines[0])
self.start_col = self.end_col = new_col
self._update_highlight()
else:
# Row-for-row pastes don't collapse to a single meaningful
# column (rows can differ in length), so just drop out of
# box mode, same as a normal multi-line paste would.
self._end_column_mode()
# --- visuals: box highlight ---------------------------------------------
def _get_tag(self, name, rgba_str):
table = self.buffer.get_tag_table()
tag = table.lookup(name)
if tag is None:
tag = self.buffer.create_tag(name)
rgba = Gdk.RGBA()
rgba.parse(rgba_str)
tag.set_property("background-rgba", rgba)
return tag
def _clear_highlight(self):
for name, color in (
(self.TAG_REPLACE, REPLACE_HIGHLIGHT_RGBA),
(self.TAG_INSERT, INSERT_HIGHLIGHT_RGBA),
):
tag = self._get_tag(name, color)
self.buffer.remove_tag(tag, self.buffer.get_start_iter(), self.buffer.get_end_iter())
def _update_highlight(self):
self._blink_visible = True
self._clear_highlight()
lo, hi = sorted((self.start_line, self.end_line))
col_min, col_max = sorted((self.start_col, self.end_col))
n_lines = self.buffer.get_line_count()
will_replace = col_max > col_min
if not will_replace and not SHOW_INSERT_HIGHLIGHT:
self.view.queue_draw()
self.emit('column-selection-changed', 0, 0)
return
tag = (
self._get_tag(self.TAG_REPLACE, REPLACE_HIGHLIGHT_RGBA)
if will_replace
else self._get_tag(self.TAG_INSERT, INSERT_HIGHLIGHT_RGBA)
)
total_chars = 0
for line in range(lo, min(hi, n_lines - 1) + 1):
line_len = self._line_len(line)
start_col = min(col_min, line_len)
end_col = min(col_max, line_len) if will_replace else min(col_min + 1, line_len)
if end_col <= start_col:
continue
s_it = self.buffer.get_iter_at_line_offset(line, start_col)
e_it = self.buffer.get_iter_at_line_offset(line, end_col)
self.buffer.apply_tag(tag, s_it, e_it)
if will_replace:
total_chars += (end_col - start_col)
rows_count = (hi - lo) + 1
self.emit('column-selection-changed', rows_count, total_chars)
self.view.queue_draw()
# --- visuals: multi-cursor carets ---------------------------------------
def _blink_interval_ms(self):
try:
settings = Gtk.Settings.get_default()
if settings and settings.get_property("gtk-cursor-blink"):
return max(settings.get_property("gtk-cursor-blink-time") // 2, 100)
except Exception:
pass
return 530
def _start_blink(self):
if not (SHOW_MULTI_CURSOR and MULTI_CURSOR_BLINK):
return
if self._blink_timer_id is None:
self._blink_visible = True
self._blink_timer_id = GLib.timeout_add(
self._blink_interval_ms(), self._on_blink_tick
)
def _stop_blink(self):
if self._blink_timer_id is not None:
try:
GLib.source_remove(self._blink_timer_id)
except Exception:
pass
self._blink_timer_id = None
self._blink_visible = True
def _on_blink_tick(self):
if not self.active:
self._blink_timer_id = None
return False
self._blink_visible = not self._blink_visible
try:
self.view.queue_draw()
except Exception:
pass
return True
def _column_screen_pos(self, line_num, col):
"""(x, y, height) in buffer coords for `col` on `line_num`, using the
same real-grid-then-fixed-width-fallback logic as the mouse->column
math, so the caret lines up with text even past the end of a
(not-yet-padded) short line."""
n_lines = self.buffer.get_line_count()
if line_num >= n_lines:
return None
line_len = self._line_len(line_num)
if col <= line_len:
it = self.buffer.get_iter_at_line_offset(line_num, col)
rect = self.view.get_iter_location(it)
return rect.x, rect.y, rect.height
end_it = self.buffer.get_iter_at_line_offset(line_num, line_len)
end_rect = self.view.get_iter_location(end_it)
char_w = self._char_width_px()
extra = col - line_len
return end_rect.x + extra * char_w, end_rect.y, end_rect.height
def _draw_multi_cursors(self, view, cr):
if not (SHOW_MULTI_CURSOR and self.active):
return
if MULTI_CURSOR_BLINK and not self._blink_visible:
return
lo, hi = sorted((self.start_line, self.end_line))
col_min, _col_max = sorted((self.start_col, self.end_col))
n_lines = self.buffer.get_line_count()
cr.save()
cr.set_source_rgba(*MULTI_CURSOR_RGBA)
for line in range(lo, min(hi, n_lines - 1) + 1):
pos = self._column_screen_pos(line, col_min)
if pos is None:
continue
bx, by, bh = pos
wx, wy = view.buffer_to_window_coords(
Gtk.TextWindowType.WIDGET, int(bx), int(by)
)
cr.rectangle(wx, wy, MULTI_CURSOR_WIDTH_PX, bh)
cr.fill()
cr.restore()
# --- visuals: status overlay --------------------------------------------
def _status_text(self):
lo, hi = sorted((self.start_line, self.end_line))
col_min, col_max = sorted((self.start_col, self.end_col))
n_lines = hi - lo + 1
display_col = col_min + 1
if col_max > col_min:
width = col_max - col_min
return "Col {}\N{EN DASH}{} \N{BULLET} {} char{} \N{RIGHTWARDS ARROW} replaced \N{BULLET} {} line{}".format(
display_col,
col_max + 1,
width,
"" if width == 1 else "s",
n_lines,
"" if n_lines == 1 else "s",
)
else:
return "Col {} \N{BULLET} insert \N{BULLET} {} line{}".format(
display_col, n_lines, "" if n_lines == 1 else "s"
)
def _rounded_rect(self, cr, x, y, w, h, r):
cr.new_sub_path()
cr.arc(x + w - r, y + r, r, -math.pi / 2, 0)
cr.arc(x + w - r, y + h - r, r, 0, math.pi / 2)
cr.arc(x + r, y + h - r, r, math.pi / 2, math.pi)
cr.arc(x + r, y + r, r, math.pi, 3 * math.pi / 2)
cr.close_path()
def on_draw(self, view, cr):
if not self.active:
return False
self._draw_multi_cursors(view, cr)
if not SHOW_STATUS_OVERLAY:
return False
text = self._status_text()
layout = view.create_pango_layout(text)
_ink, logical = layout.get_pixel_extents()
pad_x, pad_y = 10, 5
w = logical.width + pad_x * 2
h = logical.height + pad_y * 2
alloc = view.get_allocation()
x = max(alloc.width - w - 12, 4)
y = 8
cr.save()
self._rounded_rect(cr, x, y, w, h, 6)
cr.set_source_rgba(*STATUS_OVERLAY_BG_RGBA)
cr.fill()
cr.set_source_rgba(*STATUS_OVERLAY_FG_RGBA)
cr.move_to(x + pad_x, y + pad_y)
PangoCairo.show_layout(cr, layout)
cr.restore()
return False
def _end_column_mode(self):
self.active = False
self.dragging = False
self._stop_blink()
self._clear_highlight()
self.emit('column-selection-changed', 0, 0)
try:
self.view.queue_draw()
except Exception:
pass
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment