text_manipulation_functions_modified

don't discriminate between dictation and commands

unlisted ⁨1⁩ ⁨file⁩ 2019-09-09 15:50:38 UTC

text_manipulation_functions_modified.py

Raw
import pyperclip
import re 
import copy
from dragonfly import Key, Pause, AppContext, Window
from castervoice.lib import context
from castervoice.lib.ccr.core.punctuation import text_punc_dict,  double_text_punc_dict
from castervoice.lib.alphanumeric import caster_alphabet


contexts = {
    "texstudio": AppContext(executable="texstudio"),
    "lyx": AppContext(executable="lyx"),
    "winword": AppContext(executable="winword")
}

def get_application():
    window = Window.get_foreground()
    # Check all contexts. Return the name of the first one that matches or
    # "standard" if none matched.
    for name, context in contexts.items():
        if context.matches(window.executable, window.title, window.handle):
            return name
    return "standard"
            
def get_start_end_position(text, phrase, direction, occurrence_number, dictation_versus_character):
# def get_start_end_position(text, phrase, direction):
    # print(dictation_versus_character)
    # if dictation_versus_character == "character":
    pattern = re.escape(phrase) 
    # if dictation_versus_character == "dictation":
        # avoid e.g. matching 'and' in 'land' but allow e.g. matching 'and' in 'hello.and'
        # for matching purposes use lowercase
        # PROBLEM: this will not match words in class names like "Class" in "ClassName"
        # PROBLEM: it's not matching the right one when you have two occurrences of the same word in a row
        # pattern = re.escape('(?:[^A-Za-z]|\A)({})(?:[^A-Za-z]|\Z)'.format(phrase.lower())) # must get group 1
        # pattern = '(?:[^A-Za-z]|\A)({})(?:[^A-Za-z]|\Z)'.format(phrase.lower()) # must get group 1
        
    if not re.search(pattern, text.lower()):
        # replaced phase not found
        print("'{}' not found".format(phrase))
        return
    match_iter = re.finditer(pattern, text.lower())
    # if dictation_versus_character == "character":
    match_index_list = [(m.start(), m.end()) for m in match_iter] 
    # if dictation_versus_character == "dictation":
        # match_index_list = [(m.start(1), m.end(1)) for m in match_iter] # first group
    
    if direction == "left":
        try:
            match = match_index_list[-1*occurrence_number] # count from the right
        except IndexError:
            print("There aren't that many occurrences of '{}'".format(phrase))
            return
    if direction == "right":
        try:
            match = match_index_list[occurrence_number - 1] # count from the left
        except IndexError:
            print("There aren't that many occurrences of '{}'".format(phrase))
            return 
    left_index, right_index = match
    return (left_index, right_index)
    
copy_pause_time_dict = {"standard": "10", "texstudio": "70", "lyx": "60", "winword": "90"}
paste_pause_time_dict = {"standard": "0", "texstudio": "100", "lyx": "20", "winword": "20"} 
# winword (a.k.a. Microsoft Word) pause times may need some tweaking, 
# people are probably better off just using the native Dragon commands in winword.

def text_manipulation_copy(application):
    """ the wait time can also be modified up or down further by going into context.read_selected_without_altering_clipboard 
    and changing the sleep time which is apparently slightly different than the pause time.
    the sleep time is set to a positive number, so can be reduced
    here I am using "wait time" to mean the sum of the sleep and pause time right after pressing control c """
    # double hashmarks below indicate an alternative to using the functions in lib.context
    ## previous_item_on_the_clipboard = pyperclip.paste()
    ## Key("c-c").execute()
    ## Pause(copy_pause_time_dict[application]).execute()
    ## selected_text = pyperclip.paste()
    
    err, selected_text = context.read_selected_without_altering_clipboard(same_is_okay=True, pause_time=copy_pause_time_dict[application])
    if err != 0:
        # I'm not discriminating between err = 1 and err = 2
        print("failed to copy text")
        return
    return selected_text
    ## pyperclip.copy(previous_item_on_the_clipboard) 

def text_manipulation_paste(text, application):
    context.paste_string_without_altering_clipboard(text, pause_time=copy_pause_time_dict[application])

def select_text_and_return_it(direction, number_of_lines_to_search, application):
    if direction == "left":
        if number_of_lines_to_search > 0:
            Key("s-up:%d" %number_of_lines_to_search).execute()
        Key("s-home").execute()
    if direction == "right":    
        if number_of_lines_to_search > 0:
            Key("s-down:%d" %number_of_lines_to_search).execute()
        Key("s-end").execute()
        
    selected_text = text_manipulation_copy(application)
    if selected_text == None:
        # failed to copy
        return 
    if selected_text == "":
        print("no text to select")
        return 
    
    return selected_text

def deal_with_phrase_not_found(selected_text, application, direction):
        # Approach 1: unselect text by pressing left and then right, works in Tex studio
        if application == "texstudio":
            Key("left, right").execute() # unselect text
            if direction == "right":
                Key("left:%d" %len(selected_text)).execute()
        # Approach 2: unselect text by pressing opposite arrow key, does not work in Tex studio
        else:
            if direction == "left":
                Key("right").execute()
            if direction == "right":
                Key("left").execute()

def deal_with_up_down_directions(direction, number_of_lines_to_search):
    # note that zero is the default number of lines to search, so if you change that you will may want to change this
    if number_of_lines_to_search == 0 and (direction == "up" or direction == "down"):
        # if the user says sauce (meeting up) or dunce (meaning down), set default number of lines to 3
        number_of_lines_to_search = 3
    if direction == "up":
        direction = "left"
    if direction == "down":    
        direction = "right"
    return (number_of_lines_to_search, direction)
        
def replace_phrase_with_phrase(text, replaced_phrase, replacement_phrase, direction, occurrence_number, dictation_versus_character):
    match_index = get_start_end_position(text, replaced_phrase, direction, occurrence_number, dictation_versus_character)
    if match_index:
        left_index, right_index = match_index
    else:
        return
    return text[: left_index] + replacement_phrase + text[right_index:] 
    

def copypaste_replace_phrase_with_phrase(replaced_phrase, replacement_phrase, direction, number_of_lines_to_search, occurrence_number, dictation_versus_character):
    if direction == "up" or direction == "down":
        # "up" and "down" get treated just as the "left" and "right" 
        # except that the default number of lines to search get set to three instead of zero
        number_of_lines_to_search, direction = deal_with_up_down_directions(direction, number_of_lines_to_search)
    application = get_application()
    selected_text = select_text_and_return_it(direction, number_of_lines_to_search, application)
    if not selected_text:
        return 
    replaced_phrase = str(replaced_phrase)
    replacement_phrase = str(replacement_phrase) 
    new_text = replace_phrase_with_phrase(selected_text, replaced_phrase, replacement_phrase, direction, occurrence_number, dictation_versus_character)
    if not new_text:
        # replaced_phrase not found
        deal_with_phrase_not_found(selected_text, application, direction)
        return
    
    text_manipulation_paste(new_text, application)
    if number_of_lines_to_search < 20: 
        # only put the cursor back in the right spot if the number of lines to search is fairly small
        if direction == "right":
            offset = len(new_text)
            Key("left:%d" %offset).execute()
    
def remove_phrase_from_text(text, phrase, direction, occurrence_number, dictation_versus_character):
    match_index = get_start_end_position(text, phrase, direction, occurrence_number, dictation_versus_character)
    if match_index:
        left_index, right_index = match_index
    else:
        return
        
    # if the "phrase" is punctuation, just remove it, but otherwise remove an extra space adjacent to the phrase
    if dictation_versus_character == "character":
        return text[: left_index] + text[right_index:] 
    else:
        if left_index == 0:
            # the phrase is at the beginning of the line
            return text[right_index:]  # todo: consider removing extra space
        elif text[left_index - 1] == " ":
            return text[: left_index - 1] + text[right_index:]
        else:
            return text[: left_index] + text[right_index:]

def copypaste_remove_phrase_from_text(phrase, direction, number_of_lines_to_search, occurrence_number, dictation_versus_character):
    if direction == "up" or direction == "down":
        number_of_lines_to_search, direction = deal_with_up_down_directions(direction, number_of_lines_to_search)
    application = get_application()
    selected_text = select_text_and_return_it(direction, number_of_lines_to_search, application)
    if not selected_text:
        return 
    phrase = str(phrase)
    new_text = remove_phrase_from_text(selected_text, phrase, direction, occurrence_number, dictation_versus_character)
    if not new_text:
        # phrase not found
        deal_with_phrase_not_found(selected_text, application, direction)
        return 
    
    text_manipulation_paste(new_text, application)

    if direction == "right":
        offset = len(new_text)
        Key("left:%d" %offset).execute()

    
def delete_until_phrase(text, phrase, direction, before_after, occurrence_number, dictation_versus_character):
    match_index = get_start_end_position(text, phrase, direction, occurrence_number, dictation_versus_character)
    if match_index:
        left_index, right_index = match_index
    else:
        return
    # the spacing below may need to be tweaked
    if direction == "left":
        if before_after == "before":
            # if text[-1] == " ":
            #     return text[: left_index] + " "
                return text[: left_index]

        else: # todo: handle before-and-after defaults better
            if text[-1] == " ":
                return text[: right_index] + " "
            else:
                return text[: right_index]
    if direction == "right":
        if before_after == "after":
            return text[right_index :]
        else:
            if text[0] == " ":
                return " " + text[left_index :]
            else:
                return text[left_index :]

def copypaste_delete_until_phrase(direction, phrase, number_of_lines_to_search, before_after, occurrence_number, dictation_versus_character):
    if direction == "up" or direction == "down":
        number_of_lines_to_search, direction = deal_with_up_down_directions(direction, number_of_lines_to_search)
    application = get_application()  
    if not before_after:
        # default to delete all the way through the phrase not just up until it
        if direction == "left":
            before_after = "before"
        if direction == "right":
            before_after = "after"

    selected_text = select_text_and_return_it(direction, number_of_lines_to_search, application)
    if not selected_text:
        return 
    phrase = str(phrase)
    new_text = delete_until_phrase(selected_text, phrase, direction, before_after, occurrence_number, dictation_versus_character)
        
    if new_text is None: 
        # do NOT use `if not new_text` because that will pick up the case where new_text="" which
        # occurs if the phrase is at the beginning of the line
        # phrase not found
        # deal_with_phrase_not_found(selected_text, temp_for_previous_clipboard_item, application, direction)
        deal_with_phrase_not_found(selected_text, application, direction)
        return

    if new_text == "":
        # phrase is at the beginning of the line
        Key("del").execute()
        return
    else:
        text_manipulation_paste(new_text, application)

        if direction == "right":
            offset = len(new_text)
            Key("left:%d" %offset).execute()

    

def move_until_phrase(direction, before_after, phrase, number_of_lines_to_search, occurrence_number, dictation_versus_character):
    if direction == "up" or direction == "down":
        number_of_lines_to_search, direction = deal_with_up_down_directions(direction, number_of_lines_to_search)
    application = get_application()
    if not before_after:
          # default to whatever is closest to the cursor
        if direction  == "left":
            before_after = "after"
        if direction == "right":
            before_after = "before"
    
    selected_text = select_text_and_return_it(direction, number_of_lines_to_search, application)
    if not selected_text:
        return
    phrase = str(phrase)
    match_index = get_start_end_position(selected_text, phrase, direction, occurrence_number, dictation_versus_character)
    if match_index:
        left_index, right_index = match_index
    else:
        # phrase not found
        deal_with_phrase_not_found(selected_text, application, direction)
        return
              
    if application == "texstudio":
    # Approach 1: Unselect text by pressing left and then right. A little slower but works in Texstudio
        Key("left, right").execute() # unselect text
        if direction == "left":
            # cursor is at the left side of the previously selected text
            if before_after == "before":
                selected_text_to_the_left_of_phrase = selected_text[:left_index]
                multiline_offset_correction = selected_text_to_the_left_of_phrase.count("\r\n")
                offset = left_index - multiline_offset_correction
            if before_after == "after":
                selected_text_to_the_left_of_phrase = selected_text[:right_index]
                multiline_offset_correction = selected_text_to_the_left_of_phrase.count("\r\n")
                offset = right_index - multiline_offset_correction
            Key("right:%d" %offset).execute()

        if direction == "right":
            # cursor is at the left side of the previously selected text
            if before_after == "before":
                selected_text_to_the_right_of_phrase = selected_text[left_index :]    
            if before_after == "after":
                selected_text_to_the_right_of_phrase = selected_text[right_index :]
            multiline_offset_correction = selected_text_to_the_right_of_phrase.count("\r\n")
            if before_after  == "before":
                offset = len(selected_text) - left_index - multiline_offset_correction
            if before_after == "after":
                offset = len(selected_text) - right_index - multiline_offset_correction
            Key("left:%d" %offset).execute()
    else:
        # Approach 2: unselect using arrow keys rather than pasting over the existing text. (a little faster) does not work texstudio
        if right_index < round(len(selected_text))/2:
            # it's faster to approach phrase from the left
            Key("left").execute() # unselect text and place cursor on the left side of selection 
            if before_after  == "before":
                offset_correction = selected_text[: left_index].count("\r\n")
                offset = left_index - offset_correction
            if before_after  == "after":
                offset_correction = selected_text[: right_index].count("\r\n")
                offset = right_index - offset_correction
            Key("right:%d" %offset).execute()
        else:
            # it's faster to approach phrase from the right
            Key("right").execute() # unselect text and place cursor on the right side of selection
            if before_after  == "before":
                offset_correction = selected_text[left_index :].count("\r\n")
                offset = len(selected_text) - left_index - offset_correction
            if before_after  == "after":
                offset_correction = selected_text[right_index :].count("\r\n")
                offset = len(selected_text) - right_index - offset_correction
            Key("left:%d" %offset).execute()
    
            
    
def select_phrase(phrase, direction, number_of_lines_to_search, occurrence_number, dictation_versus_character):
    if direction == "up" or direction == "down":
        number_of_lines_to_search, direction = deal_with_up_down_directions(direction, number_of_lines_to_search)
    application = get_application()
    selected_text = select_text_and_return_it(direction, number_of_lines_to_search, application)
    if not selected_text:
        return 
    phrase = str(phrase)
    match_index = get_start_end_position(selected_text, phrase, direction, occurrence_number, dictation_versus_character)
    if match_index:
        left_index, right_index = match_index
    else:
        # phrase not found
        deal_with_phrase_not_found(selected_text, application, direction)
        return
    
        
    # Approach 1: paste the selected text over itself rather than simply unselecting. A little slower but works Texstudio
    # todo: change this so that it unselects by pressing left and then right rather than pasting over the top
    if application == "texstudio":
        text_manipulation_paste(selected_text, application) # yes, this is kind of redundant but it gets the proper pause time
        multiline_movement_correction = selected_text[right_index :].count("\r\n")
        movement_offset = len(selected_text) - right_index - multiline_movement_correction
        Key("left:%d" %movement_offset).execute()
        multiline_selection_correction = selected_text[left_index : right_index].count("\r\n")
        selection_offset = len(selected_text[left_index : right_index]) - multiline_selection_correction
        Key("s-left:%d" %selection_offset).execute()

    # Approach 2: unselect using arrow keys rather than pasting over the existing text. (a little faster) does not work texstudio
    else:
        if right_index < round(len(selected_text))/2:
            # it's faster to approach phrase from the left
            Key("left").execute() # unselect text and place cursor on the left side of selection 
            multiline_movement_offset_correction = selected_text[: left_index].count("\r\n")
            movement_offset = left_index - multiline_movement_offset_correction
            # move to the left side of the phrase
            Key("right:%d" %movement_offset).execute()
            # select phrase
            multiline_selection_offset_correction = selected_text[left_index : right_index].count("\r\n")
            selection_offset = len(phrase) - multiline_selection_offset_correction
            Key("s-right:%d" %selection_offset).execute()
        else:
            # it's faster to approach phrase from the right
            Key("right").execute() # unselect text and place cursor on the right side of selection
            multiline_movement_offset_correction = selected_text[left_index :].count("\r\n")
            movement_offset = len(selected_text) -  left_index - multiline_movement_offset_correction
            # move to the left side of the phrase
            Key("left:%d" %movement_offset).execute()
            # select phrase
            multiline_selection_offset_correction = selected_text[left_index : right_index].count("\r\n")
            selection_offset = len(phrase) - multiline_selection_offset_correction
            Key("s-right:%d" %selection_offset).execute()
    


def select_until_phrase(direction, phrase, before_after, number_of_lines_to_search, occurrence_number, dictation_versus_character):
    if direction == "up" or direction == "down":
        number_of_lines_to_search, direction = deal_with_up_down_directions(direction, number_of_lines_to_search)
    application = get_application()  
    if not before_after:
    # default to select all the way through the phrase not just up until it
        if direction == "left":
            before_after = "before"
        if direction == "right":
            before_after = "after"
    
    selected_text = select_text_and_return_it(direction, number_of_lines_to_search, application)
    if not selected_text:
        return 
    phrase = str(phrase)
    match_index = get_start_end_position(selected_text, phrase, direction, occurrence_number, dictation_versus_character)
    if match_index:
        left_index, right_index = match_index
    else:
        # phrase not found
        deal_with_phrase_not_found(selected_text, application, direction)
        return
    
    # Approach 1: paste the selected text over itself rather than simply unselecting. A little slower but works Texstudio
    # todo: change this so that it unselects by pressing left and then right rather than pasting over the top
    if application == "texstudio":
        text_manipulation_paste(selected_text, application) # yes, this is kind of redundant but it gets the proper pause time
        if direction == "left":
            if before_after == "before": 
                selected_text_to_the_right_of_phrase = selected_text[left_index :]    
                multiline_offset_correction = selected_text_to_the_right_of_phrase.count("\r\n")
                offset = len(selected_text) - left_index - multiline_offset_correction
                
            if before_after == "after":
                selected_text_to_the_right_of_phrase = selected_text[right_index :]
                multiline_offset_correction = selected_text_to_the_right_of_phrase.count("\r\n")
                offset = len(selected_text) - right_index - multiline_offset_correction
            
            Key("s-left:%d" %offset).execute()
        if direction == "right":
            multiline_movement_correction = selected_text.count("\r\n")
            movement_offset = len(selected_text) - multiline_movement_correction
            
            if before_after == "before":
                multiline_selection_correction = selected_text[: left_index].count("\r\n")
                selection_offset = left_index - multiline_movement_correction
            if before_after == "after":
                multiline_selection_correction = selected_text[: right_index].count("\r\n")
                selection_offset = right_index

            # move cursor to original position
            Key("left:%d" %movement_offset).execute()
            # select text
            Key("s-right:%d" %selection_offset).execute()
    
    # Approach 2: unselect using arrow keys rather than pasting over the existing text. (a little faster) does not work texstudio
    else:
        if direction == "left":
            Key("right").execute() # unselect text and move to left side of selection
            if before_after == "before":
                multiline_correction = selected_text[left_index :].count("\r\n")
                offset = len(selected_text) - left_index - multiline_correction
            if before_after == "after":
                multiline_correction = selected_text[right_index :].count("\r\n")
                offset = len(selected_text) - right_index - multiline_correction
            Key("s-left:%d" %offset).execute()
        if direction == "right": 
            Key("left").execute() # unselect text and move to the right side of selection
            if before_after == "before":
                multiline_correction = selected_text[: left_index].count("\r\n")
                offset = left_index - multiline_correction
            if before_after == "after":
                multiline_correction = selected_text[: right_index].count("\r\n")
                offset = right_index - multiline_correction
            Key("s-right:%d" %offset).execute()