I am working on a Devanagari/Indic editor using Tkinter/Python3 and have a question on the Text widget font rendering.
Devanagari (and all Indic language scripts) is an alphasyllabary and groups of consonants ('clusters') and vowel signs form a syllable. Consonants are joined using the Unicode ZWJ character and the Text widget is responsible for the rendering (vowel diacritics might be reordered). Vowel characters occurring after consonant use the diacritic form and not their full forms. See the following description from Microsoft.
Developing OpenType Fonts for Devanagari Script
Devanagari syllable - Effective orthographic "unit" of Devanagari >writing systems. Syllables are composed of consonant letters, independent >vowels and dependant vowels. In a text sequence, these characters are stored >in phonetic order (although they may not be represented in phonetic order >when displayed). Once a syllable is shaped, it is indivisible. The cursor cannot be positioned within the syllable. Transformations >discussed in this document do not cross syllable boundaries.
When not selecting text using the mouse drag, I take care in Python event handlers so that the INSERT and CURRENT cursors are never on character positions that fall inside a syllable, so the vowel diactric and consonant half-forms don't show.
However, I am stumped when it comes to handle the mouse drag (for text selection). I can't seem to figure out how to disallow the mouse cursor during a drag (B1-Motion) event from moving to character positions between a syllable.
I use
self.editorText.mark_set ("my_index", "@%d,%d" % (event.x, event.y))
to get the drag index, then use my routine to seek the first character position to the left of and outside the syllable. But this just does not work.
See images below on the font rendering problems...
Devanagari text displaying correctly
Mouse drag cursor gets inside a syllable and messes up the display
Any help is appreciated!
-PP
import tkinter as tk
import tkinter.ttk as ttk
from tkinter import font
reverse = {}
ucode = {}
varna = {}
enc1 = {}
enc2 = {}
first_vowel = ''
allLines = """VIRAMA Special 2381 -
p Consonant 2346 -
r Consonant 2352 -
A Vowel 2310 2366
aa Vowel 2310 2366
u Vowel 2313 2369
D Consonant 2337 -
ai Vowel 2320 2376
g Consonant 2327 -
n Consonant 2344 -
ZWNJ JoinerSpecial 8204 -
ZWJ JoinerSpecial 8205 -
"""
#default EditorText height in pixels
editorTextWinfoHeightPixels = 1
editorTextFontHeightPixels = 1
class klasse(object):
def __init__(self, master):
self.top_line_number = 1
self.bottom_line_number = 20
self.last_line_number = 1
self.current_line_number = 1
self.current_char_position = 0
self.previous_linelength = 0
self.current_linelength = 0
self.ieSelection = ''
self.ieSelectionLength = 0
self.dragLineStart = 0
self.dragLineEnd = 0
self.dragCharStart = 0
self.dragCharEnd = 0
self.numCopiedLines = 0
self.fonts=list(font.families())
self.fonts.sort()
maxFontNameLength = len(max(self.fonts))
self.frame = tk.Frame(root)
self.editorFrame = tk.Frame(self.frame)
self.editorFrame.pack(side="left", fill="both", expand="yes")
self.editorText = tk.Text(self.editorFrame, exportselection="true")
self.editorText.pack(side="top", fill="both", expand="yes", padx=2)
self.editorText.config( wrap="word", # use word wrapping
undo=True,
font=('Arial', 20))
self.editorText.see(tk.INSERT)
self.editorText.focus()
self.editorText.bindtags((self.editorText))
self.editorText.bind('<B1-Motion>', self.handleMouseDrag)
self.editorText.bind('<Button-1>', self.mouseButtonCallback)
self.frame.pack(fill="both", expand="yes")
self.bottom_line_number = int(301/editorTextFontHeightPixels)
self.editorText.insert("insert", chr(2346))
self.editorText.insert("insert", chr(2381))
self.editorText.insert("insert", chr(2352))
self.editorText.insert("insert", chr(2366))
self.editorText.insert("insert", chr(2313))
self.editorText.insert("insert", chr(2337))
self.editorText.insert("insert", chr(2346))
self.editorText.insert("insert", chr(2366))
#self.editorText.insert("insert", chr(2376))
self.editorText.insert("insert", chr(2327))
self.editorText.insert("insert", chr(2344))
def handleMouseDrag (self, event):
[start_line_number, start_char_position] = self.editorText.index('current').split('.')
start_line_number = int(start_line_number)
start_char_position = int(start_char_position)
self.editorText.mark_set ("anirb_index", "@%d,%d" % (event.x, event.y))
[temp_current_line_number, temp_current_char_position] = self.editorText.index('anirb_index').split('.')
temp_current_line_number = int(temp_current_line_number)
temp_current_char_position = int(temp_current_char_position)
if temp_current_line_number > start_line_number or\
(temp_current_line_number == start_line_number and temp_current_char_position > start_char_position):
#forward selection
self.editorText.mark_set ("sel.first", str(start_line_number)+'.'+str(start_char_position))
self.editorText.mark_set ("sel.last", str(temp_current_line_number)+'.'+str(temp_current_char_position))
else:
#backward selection
self.editorText.mark_set ("sel.first", str(temp_current_line_number)+'.'+str(temp_current_char_position))
self.editorText.mark_set ("sel.last", str(start_line_number)+'.'+str(start_char_position))
if self.editorText.tag_ranges ("sel") != []:
self.editorText.tag_remove ("sel", "1.0", "end")
self.current_line_number = temp_current_line_number
self.current_char_position = temp_current_char_position
self.editorText.tag_add ("sel", "sel.first", "sel.last")
self.editorText.tag_config ("sel", background="darkblue", foreground="white")
self.editorText.mark_set ("insert", "sel.last")
def mouseSettle (self, callingWidget, mouseCursorIndex):
global ucode
current_char = ''
[temp_current_line_number, temp_current_char_position] = mouseCursorIndex
while (1):
if temp_current_char_position == 0:
break
curr_char_index = str(temp_current_line_number) + '.' + str(temp_current_char_position)
prev_char_index = str(temp_current_line_number) + '.' + str(temp_current_char_position - 1)
char_to_left = self.editorText.get (prev_char_index, curr_char_index)
if char_to_left != '':
char_to_left = ord(char_to_left)
next_char_index = str(temp_current_line_number) + '.' + str(temp_current_char_position + 1)
current_char = self.editorText.get (curr_char_index, next_char_index)
if current_char != '':
current_char = ord(current_char)
if current_char not in ucode.keys() or current_char == '':
ucode[current_char] = "null"
if char_to_left not in ucode.keys() or char_to_left == '':
ucode[char_to_left] = "null"
if ("consonant" in ucode[current_char] and "special" not in ucode[char_to_left]) or \
(ucode[current_char] == "null" and "special" not in ucode[char_to_left]) or \
ucode[char_to_left] == "vowel" or \
ucode[char_to_left] == "standalone" or \
ucode[char_to_left] == "null":
break
temp_current_char_position -= 1
#endwhile
return temp_current_char_position
#enddef
#left mouse button click in editorText widget
def mouseButtonCallback (self, event=None):
global ucode
[self.current_line_number, temp_current_char_position] = self.editorText.index('current').split('.')
self.current_line_number = int(self.current_line_number)
temp_current_char_position = int(temp_current_char_position)
self.current_linelength = len(self.editorText.get (str(self.current_line_number)+'.0', str(self.current_line_number)+'.end'))
if temp_current_char_position >= self.current_linelength:
self.current_char_position = self.current_linelength
else:
#mouseSettle will return the new character position for INSERT cursor
self.current_char_position = self.mouseSettle (0, [self.current_line_number, temp_current_char_position])
self.editorText.mark_set ("insert", str(self.current_line_number)+'.'+str(self.current_char_position))
self.editorText.mark_set ("current", str(self.current_line_number)+'.'+str(self.current_char_position))
#remove all selection
if self.editorText.tag_ranges ("sel") != []:
self.editorText.tag_remove ("sel", "1.0", "end")
difference = self.bottom_line_number - self.top_line_number
if self.bottom_line_number < self.current_line_number:
self.bottom_line_number = self.current_line_number
self.top_line_number = self.bottom_line_number - difference
if self.top_line_number > self.current_line_number:
top_line_number = self.current_line_number
self.bottom_line_number = self.top_line_number + difference
#in case user clicked on a partially visible line at the top or bottom adjust those
self.editorText.see(tk.INSERT)
self.editorText.focus()
def parse ():
global reverse
global ucode
global enc1
global enc2
global varna
global first_vowel
for line in allLines.split('\n'):
#split on the comment character
lineList = line.split('#')
line = lineList[0] #whatever came before the comment character
if line.lstrip() == '':
continue
lineList = line.split()
#discard comments
if len(lineList) == 4:
(key, v, e1, e2) = lineList
print ("key is ---", key)
if e1 == "-":
e1 = ''
if e2 == "-":
e2 = ''
elif len(lineList) > 0 and len(lineList) < 4:
print('Error: Illegal Map ')
varna[key] = v.lower()
enc1[key] = e1.lower()
enc2[key] = e2.lower()
if varna[key] == "vowel" and enc2[key] == '':
first_vowel = key
#now create the hashes that characterize each 16-bit unicode character
if "+" not in e1 and e1 != '':
ucode[int(e1)] = v.lower()
#this is the reverse map of enc1 to the latin input character (key)
reverse[int(e1)] = key
if "+" not in e2 and e2 != '':
ucode[int(e2)] = v.lower()
#this is the reverse map of enc2 to the latin input character (key)
reverse[int(e2)] = key
parse()
counter = 0
root = tk.Tk()
app = klasse(root)
root.minsize (900, 350)
root.mainloop()