Skip to content

Instantly share code, notes, and snippets.

@muguu
Last active February 2, 2023 13:29
Show Gist options
  • Save muguu/6502aa7064dc0795a684adaac3d65212 to your computer and use it in GitHub Desktop.
Save muguu/6502aa7064dc0795a684adaac3d65212 to your computer and use it in GitHub Desktop.
Plugin for ZimWiki V0.66 or greater
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 2017 Murat Guven <[email protected]>
# This is a plugin for ZimWiki from Jaap Karssenberg <[email protected]>
#
# This plugin provides auto completion for tags similiar to code completion in code editors.
# When you press the @ key, a list of available tags are shown and can be selected.
# The {AutoCompletion} class can be used to provide auto completion on any given
# list within a given gtk.TextView widget
# The signal 'tag-selected' is emitted together with the tag as argument when a tag is selected
# V0.93
# Signal added
import logging
import gobject
import gtk
import gtk.gdk
from zim.plugins import PluginClass, WindowExtension, extends
from zim.actions import action
from zim.gui.widgets import Window, BrowserTreeView, ScrolledWindow
ACTKEY = 'at'
logger = logging.getLogger('zim.plugins.autocompletion')
class AutoCompletionPlugin(PluginClass):
plugin_info = {
'name': _('Tag Auto Completion'), # T: plugin name
'description': _('''\
This plugin provides auto completion for tags. When you press the @ key,
a list of available tags are shown and can be selected (via tab, space or enter, mouse or cursor).
See configuration for tab key handling.
(V0.91)
'''), # T: plugin description
'author': "Murat Güven",
'help': 'Plugins:Tag Auto Completion',
}
plugin_preferences = (
# key, type, label, default
('tab_behaviour', 'choice', _('Use tab key to'), 'select', ('select', 'cycle')),
('space_selection', 'bool', _('Use also SHIFT + Space key for selection'), False),
)
@extends('MainWindow')
class MainWindowExtension(WindowExtension):
uimanager_xml = '''
<ui>
<menubar name='menubar'>
<menu action='tools_menu'>
<placeholder name='plugin_items'>
<menuitem action='tag_auto_completion'/>
</placeholder>
</menu>
</menubar>
</ui>
'''
def __init__(self, plugin, window):
WindowExtension.__init__(self, plugin, window)
self.plugin = plugin
self.window = window
self.connectto(window.pageview.view, 'key-press-event')
@action(_('Auto_Completion'), ) # T: menu item
def tag_auto_completion(self):
text_view = self.window.pageview.view
all_tags = self.window.ui.notebook.tags.list_all_tags()
self.tag_list = []
activation_char = "@"
for tag in all_tags:
self.tag_list.append(tag.name)
tag_auto_completion = AutoCompletion(
self.plugin, text_view, self.window, activation_char, char_insert=False)
# tag_list as param for completion method as otherwise the list is added at each activation?
tag_auto_completion.completion(self.tag_list)
def on_key_press_event(self, widget, event):
if gtk.gdk.keyval_name(event.keyval) == ACTKEY:
self.tag_auto_completion()
VIS_COL = 0
DATA_COL = 1
WIN_WIDTH = 200
WIN_HEIGHT = 200
SHIFT = ('Shift_L', 'Shift_R')
KEYSTATES = gtk.gdk.CONTROL_MASK |gtk.gdk.META_MASK| gtk.gdk.MOD1_MASK | gtk.gdk.LOCK_MASK
IGNORE_KEYS = ['Up', 'Down', 'Page_Up', 'Page_Down', 'Left', 'Right', \
'Home', 'End', 'Menu', 'Scroll_Lock', 'Alt_L', 'Alt_R', \
'VoidSymbol', 'Meta_L', 'Meta_R', 'Num_Lock', 'Insert', \
'Delete', 'Pause', 'Control_L', 'Control_R', \
'ISO_Level3_Shift', 'Caps_Lock']
GREY = 65535
class AutoCompletionTreeView(object):
def __init__(self, model):
self.model = model
self.completion_win = Window()
self.completion_win.set_modal(True)
self.completion_win.set_keep_above(True)
self.completion_tree_view = BrowserTreeView(self.model)
self.completion_tree_view.set_enable_search(False)
self.completion_scrolled_win = ScrolledWindow(self.completion_tree_view)
self.completion_win.add(self.completion_scrolled_win)
self.column = gtk.TreeViewColumn()
self.completion_tree_view.append_column(self.column)
self.renderer_text = gtk.CellRendererText()
self.column.pack_start(self.renderer_text, False)
self.column.set_attributes(self.renderer_text, text=DATA_COL)
# display an undecorated window with a grey border
self.completion_scrolled_win.set_size_request(WIN_WIDTH, WIN_HEIGHT)
self.completion_scrolled_win.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
self.completion_win.set_decorated(False)
self.completion_scrolled_win.set_border_width(2)
self.completion_scrolled_win.modify_bg(gtk.STATE_NORMAL, gtk.gdk.Color(GREY))
self.column.set_min_width(50)
# hide column
self.completion_tree_view.set_headers_visible(False)
class AutoCompletion(gobject.GObject):
#todo: Make cursor visible
#todo: get theme color to use for frame around completion window
#todo: handling of modifier in Linux
# define signal (closure type, return type and arg types)
__gsignals__ = {
'tag-selected': (gobject.SIGNAL_RUN_LAST, None, (object,)),
}
def __init__(self, plugin, text_view, window, activation_char, char_insert=False):
'''
Parameters for using this class:
- gtk.TextView
- the gtk.Window in which the TextView is added
- list of unicode elements to be used for completion
- a character which shall activate the class and if that char shall be inserted
into the text buffer
'''
self.plugin = plugin
self.text_view = text_view
self.window = window
self.activation_char = activation_char
self.char_insert = char_insert
self.real_model = gtk.ListStore(bool, str)
self.model = self.real_model.filter_new()
self.model.set_visible_column(VIS_COL)
self.model = gtk.TreeModelSort(self.model)
self.model.set_sort_column_id(DATA_COL, gtk.SORT_ASCENDING)
self.selected_data = ""
# add F-Keys to ignore them later
for f_key in range(1, 13):
IGNORE_KEYS.append('F' + str(f_key))
gobject.GObject.__init__(self)
def completion(self, completion_list):
self.entered_text = ""
self.completion_list = completion_list
self.ac_tree_view = AutoCompletionTreeView(self.model)
self.tree_selection = self.ac_tree_view.completion_tree_view.get_selection()
self.fill_completion_list(self.completion_list)
buffer = self.text_view.get_buffer()
cursor = buffer.get_iter_at_mark(buffer.get_insert())
#insert activation char at cursor pos as it is not shown due to accelerator setting
if self.activation_char and self.char_insert:
buffer.insert(cursor, self.activation_char)
x, y = self.get_iter_pos(self.text_view, self.window)
self.ac_tree_view.completion_win.move(x, y)
self.ac_tree_view.completion_win.show_all()
self.ac_tree_view.completion_win.connect(
'key_press_event',
self.do_key_press,
self.ac_tree_view.completion_win)
self.ac_tree_view.completion_tree_view.connect(
'row-activated',
self.do_row_activated)
def update_completion_list(self):
tree_selection = self.ac_tree_view.completion_tree_view.get_selection()
entered_text = self.entered_text.decode('latin2')
# filter list against input (find any)
def filter(model, path, iter):
data = model[iter][DATA_COL].decode('latin2')
if entered_text.upper() in data.upper():
model[iter][VIS_COL] = True
else:
model[iter][VIS_COL] = False
self.real_model.foreach(filter)
self.select_match(tree_selection)
def select_match(self, tree_selection):
path = None
entered_text = self.entered_text.decode('latin2')
for index, element in enumerate(self.model):
# set path = 0 to select first row if there is no hit on below statement
path = 0
# select first match of filtered list
if element[DATA_COL].decode('latin2').upper().startswith(entered_text.upper()):
path = index
break
# if there is no match where elements in model
# starts with entered text, then select first row (=0)
if path is not None:
tree_selection.select_path(path)
self.ac_tree_view.completion_tree_view.scroll_to_cell(path)
def fill_completion_list(self, completion_list):
self.real_model.clear()
for element in completion_list:
self.real_model.append((True, element))
def do_row_activated(self, view, path, col):
self.insert_data()
self.ac_tree_view.completion_win.destroy()
def do_key_press(self, widget, event, completion_window):
modifier = event.state & KEYSTATES
shift_mod = event.state & gtk.gdk.SHIFT_MASK
buffer = self.text_view.get_buffer()
cursor = buffer.get_iter_at_mark(buffer.get_insert())
if gtk.gdk.keyval_name(event.keyval) == 'Escape':
completion_window.destroy()
return
# Ignore special keys
if gtk.gdk.keyval_name(event.keyval) in IGNORE_KEYS:
return
# delete text from buffer and close if activation_char is identified
if gtk.gdk.keyval_name(event.keyval) == 'BackSpace':
cursor.backward_chars(1)
start = buffer.get_iter_at_mark(buffer.get_insert())
char = buffer.get_text(start, cursor)
buffer.delete(start, cursor)
if char == self.activation_char:
completion_window.destroy()
return
self.entered_text = self.entered_text[:-1]
self.update_completion_list()
return
if event.get_state() & gtk.gdk.SHIFT_MASK and \
self.plugin.preferences['space_selection'] and \
gtk.gdk.keyval_name(event.keyval) == 'space':
self.insert_data(" ")
completion_window.destroy()
return
if gtk.gdk.keyval_name(event.keyval) == 'Return':
self.insert_data()
completion_window.destroy()
return
if gtk.gdk.keyval_name(event.keyval) == "space":
buffer.insert(cursor, " ")
completion_window.destroy()
return
if gtk.gdk.keyval_name(event.keyval) == "Tab":
if self.plugin.preferences['tab_behaviour'] == 'select':
self.insert_data()
completion_window.destroy()
return
# cycle: select next item in tree
(model, path) = self.tree_selection.get_selected_rows()
current_path = path[0][0]
next_path = current_path + 1
self.tree_selection.select_path(next_path)
return
if gtk.gdk.keyval_name(event.keyval) == "ISO_Left_Tab":
if self.plugin.preferences['tab_behaviour'] == 'cycle':
# select previous item in tree
(model, path) = self.tree_selection.get_selected_rows()
current_path = path[0][0]
next_path = current_path - 1
if next_path >= 0:
self.tree_selection.select_path(next_path)
# SHIFT Tab is not used for selection
return
entered_chr = unichr(event.keyval)
# for any upper case char
if shift_mod or gtk.gdk.keyval_name(event.keyval) in SHIFT:
# to prevent that SHIFT code is added to buffer.
# Don't know if there is another way to handle this
if gtk.gdk.keyval_name(event.keyval) in SHIFT:
return
buffer.insert(cursor, entered_chr)
self.entered_text += entered_chr
self.update_completion_list()
return
# for any other char without modifier
if not modifier:
buffer.insert(cursor, entered_chr)
self.entered_text += entered_chr
self.update_completion_list()
return
def insert_data(self, space=""):
tree_selection = self.ac_tree_view.completion_tree_view.get_selection()
(model, path) = tree_selection.get_selected()
try:
# is there any entry left or is the list empty?
selected_data = model[path][DATA_COL]
except:
# if nothing is selected (say: nothing found and nothing is shown in treeview)
return
buffer = self.text_view.get_buffer()
cursor = buffer.get_iter_at_mark(buffer.get_insert())
# delete entered text
n_entered_text = len(self.entered_text)
cursor.backward_chars(n_entered_text)
start = buffer.get_iter_at_mark(buffer.get_insert())
buffer.delete(start, cursor)
# insert selected text
buffer.insert(start, selected_data + space)
# now emit signal 'tag-selected' with tag in selected_data to
# hand over tag
self.emit('tag-selected', selected_data)
def get_iter_pos(self, textview, window):
ACTKEY_CORRECTION = 0
COLUMN_INVISIBLE_CORRECTION = 0
buffer = textview.get_buffer()
cursor = buffer.get_iter_at_mark(buffer.get_insert())
top_x, top_y = textview.get_toplevel().get_position()
iter_location = textview.get_iter_location(cursor)
mark_x, mark_y = iter_location.x, iter_location.y + iter_location.height
#calculate buffer-coordinates to coordinates within the window
win_location = textview.buffer_to_window_coords(gtk.TEXT_WINDOW_WIDGET,
int(mark_x), int(mark_y))
#now find the right window --> Editor Window and the right pos on screen
win = textview.get_window(gtk.TEXT_WINDOW_WIDGET)
view_pos = win.get_position()
xx = win_location[0] + view_pos[0]
yy = win_location[1] + view_pos[1] + iter_location.height
x = top_x + xx + ACTKEY_CORRECTION
y = top_y + yy - COLUMN_INVISIBLE_CORRECTION
x, y = self.calculate_with_monitors(x, y, iter_location, window)
return (x, y + iter_location.height)
def calculate_with_monitors(self, x, y, iter_location, window):
'''
Calculate correct x,y position if multiple monitors are used
'''
STATUS_BAR_CORRECTION = 30
screen = window.get_screen()
cursor_screen = screen.get_monitor_at_point(x, y)
cursor_monitor_geom = screen.get_monitor_geometry(cursor_screen)
if x + WIN_WIDTH >= (cursor_monitor_geom.width + cursor_monitor_geom.x):
diff = x - (cursor_monitor_geom.width + cursor_monitor_geom.x) + WIN_WIDTH
x = x - diff
if y + iter_location.height + WIN_HEIGHT >= (
cursor_monitor_geom.height + cursor_monitor_geom.y - STATUS_BAR_CORRECTION):
diff = WIN_HEIGHT + 2 * iter_location.height
y = y - diff
return x, y
@muguu
Copy link
Author

muguu commented Jun 15, 2017

Added SHIFT + SPACE as RETURN equivalent and did some pep8 cleaning

@shivams
Copy link

shivams commented Aug 27, 2019

@muguu This does not work with the latest release of Zim (v0.71.1).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment