0

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()
user0
  • 97
  • 9
  • Please create a [mcve]. – Bryan Oakley Mar 22 '18 at 11:37
  • Here is the link to a minimal example: [link] (https://www.dropbox.com/s/42m9c6v9plgfktu/example.py) – user0 Mar 22 '18 at 16:52
  • Please do not link to code on another site. [edit] your question to include the code. Also, that code is definitely not minimal. All you need is a properly configured text widget and enough code to make it appear, plus whatever bindings you are using. I don't think you need over 100 lines of data to illustrate this problem. – Bryan Oakley Mar 22 '18 at 16:58
  • Tried best to minimize code (added inline). There's some data in the beginning that has to be parsed into a dict and that is a necessary part mouse event handler. – user0 Mar 23 '18 at 04:23
  • This seems to be a problem with the tkinter (and Tk) Text widget. I see this in Tcl/Tk as well but not in a wxPython TextCtrl. – user0 Mar 26 '18 at 13:57

0 Answers0