#!/usr/bin/env python3 # -*- coding: utf-8 -*- # #Copyright 2018 Sodium "natoriusushio" Chloride # #Released under the MIT license # #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), #to deal in the Software without restriction, including without limitation #the rights to use, copy, modify, merge, publish, distribute, sublicense, #and/or sell copies of the Software, and to permit persons to whom #the Software is furnished to do so, subject to the following conditions: # #The above copyright notice and this permission notice shall be included # in all copies or substantial portions of the Software. # #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, #EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES #OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. #IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, #DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, #TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE #OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #https://opensource.org/licenses/mit-license.php #http://sourceforge.jp/projects/opensource/wiki/licenses%2FMIT_license import ctypes import datetime import infi.systray import json import keyboard import romkan import socket import threading import time import tkinter import win32api import win32gui import win32process # ==== definition area ==== # == variable == abort = False autophagy = False backspaceisreleased = False destroy = False end = False firstloop = True chkfghWnd = 0 boxname = "" converted = "" chkfgWndt = "" source = "" systray = None # == exception handling == exception = [",", "|", ";", " "] comma = ["、", ",", ","] semicolon = [";", ";"] space = [" ", " "] verticalbar =["|", "¦", "∥", "|", "¦", "‖"] bracket = ["[", "]", "(", ")"] # == win32api == # pywin32 # https://github.com/mhammond/pywin32 # http://timgolden.me.uk/pywin32-docs/index.html def attach_thread_input(current, target, boolean): win32process.AttachThreadInput(current, target, boolean) def client_to_screen(target, client_x, client_y): coordinates = (client_x, client_y) screen_x, screen_y = win32gui.ClientToScreen(target, coordinates) return screen_x, screen_y def find_window(classname, windowname): result = win32gui.FindWindow(classname, windowname) return result def get_caret_pos(): result = win32gui.GetCaretPos() return result def get_current_thread_id(): result = win32api.GetCurrentThreadId() return result def get_foreground_window(): result = win32gui.GetForegroundWindow() return result def get_system_metrics(): screen_x = win32api.GetSystemMetrics(0) screen_y = win32api.GetSystemMetrics(1) return screen_x, screen_y def get_window_rect(hWnd): topleft_x, topleft_y, bottomright_x, bottomright_y \ = win32gui.GetWindowRect(hWnd) return topleft_x, topleft_y, bottomright_x, bottomright_y def get_window_text(hWnd): result = win32gui.GetWindowText(hWnd) return result def get_window_thread_process_id(hWnd): targetthreadid, targetprocessid = win32process.GetWindowThreadProcessId( hWnd) return targetthreadid, targetprocessid def set_foreground_window(hWnd): win32gui.SetForegroundWindow(hWnd) # == function == def apoptosis(boolean): global destroy destroy = boolean global end end = boolean global autophagy autophagy = boolean # keyboard # https://github.com/boppreh/keyboard def terminator(): keyboard.add_hotkey("ctrl+alt+del", apoptosis, args=[True]) def backspace_is_released(boolean): global backspaceisreleased backspaceisreleased = boolean def add_hotkey_backspace_is_released(): keyboard.add_hotkey("backspace", backspace_is_released, args=[True], trigger_on_release=True) def get_convwindow_size(): # convwindow = window for converting topleft_x =0 topleft_y =0 bottomright_x =144 bottomright_y =21 return bottomright_x, bottomright_y # I referred to these topics for writing the function below. # https://stackoverflow.com/questions/19724360/python-get-caret-position # http://timgolden.me.uk/python/win32_how_do_i/find-the-screen-resolution.html def get_coordinates(): targetwindow = get_foreground_window() targetthreadid, targetprocessid = get_window_thread_process_id( targetwindow) currentthreadid = get_current_thread_id() try: attach_thread_input(currentthreadid, targetthreadid, True) clientcaret_x, clientcaret_y = get_caret_pos() finally: attach_thread_input(currentthreadid, targetthreadid, False) if (clientcaret_x, clientcaret_y) == (None, None): screen_x, screen_y = get_system_metrics() convwin_x, convwin_y = get_convwindow_size() screencaret_x = (screen_x - convwin_x) // 2 screencaret_y = 2 * (screen_y - convwin_y) // 3 else: if (clientcaret_x, clientcaret_y) == (0, 0): # topleft=tpl, bottomright=btmr tpl_x, tpl_y, btmr_x, btmr_y = get_window_rect(targetwindow) convwin_x, convwin_y = get_convwindow_size() tmp_x = (btmr_x - tpl_x - convwin_x) // 2 tmp_y = 2 * (btmr_y - tpl_y - convwin_x) // 3 screencaret_x = tpl_x + tmp_x screencaret_y = tpl_y + tmp_y else: screencaret_x, screencaret_y = client_to_screen( targetwindow, clientcaret_x, clientcaret_y) return screencaret_x, screencaret_y def get_source(caretpositionglobal_x, caretpositionglobal_y): global destroy destroy = False label = "conv" identifier = datetime.datetime.now().strftime("%f") global boxname boxname = label+identifier root = tkinter.Tk() root.title(boxname) root.attributes("-topmost", True) root.overrideredirect(1) coordinate = "+%s+%s" % (caretpositionglobal_x, caretpositionglobal_y) root.geometry(coordinate) getentry = tkinter.Entry() getentry.config(background="azure") def get_entry(event): keyboard.release("enter") global source source = getentry.get() global destroy destroy = True def terminate(event): keyboard.release("\\") keyboard.release("ctrl") global end end = True fgw = get_foreground_window() fgwt = get_window_text(fgw) global destroy destroy = True getentry.pack() getentry.focus_set() root.bind("", get_entry) root.bind("", terminate) while destroy == False: time.sleep(0.001) # for reducing CPU usage root.update() fgw = get_foreground_window() fgwt = get_window_text(fgw) if fgwt == boxname: pass else: target = find_window("TkTopLevel", boxname) set_foreground_window(target) destroy = False getentry.destroy() root.destroy() # romkan # https://github.com/soimort/python-romkan # I referred to this topic for writing the function below. # https://masatoi.github.io/2017/11/19/skk-client def convert_romaji_to_kanji(source): skkreq = "1" skksource = "" skkspacer = " " skkhost = "127.0.0.1" skkport = 1178 skkencoding = "euc_jp" hiraganasource = romkan.to_hiragana(source) katakanasource = romkan.to_katakana(source) skksource = skkreq + hiraganasource + skkspacer client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect((skkhost, skkport)) client.send(skksource.encode(skkencoding)) result = client.recv(1024) client.shutdown(socket.SHUT_RDWR) client.close() if result.decode(skkencoding) == "4\n": result = [] else: result = result.decode(skkencoding) result = result.replace("1/", "[\"") result = result.replace("/\n", "\"]") result = result.replace("/", "\", \"") result = json.loads(result) if hiraganasource not in result: try: result.insert(0, hiraganasource) except: print("不正な文字列です") if katakanasource not in result: try: result.append(katakanasource) except: print("不正な文字列です") if source not in result: try: result.append(source) except: print("不正な文字列です") if source in bracket: for en, ja in zip(["(", ")", "[", "]"], ["「", "」", "「", "」"]): if source is en: if ja not in result: result.append(ja) return result def select_word_from_list(result, screencaret_x, screencaret_y): global destroy destroy = False counts = len(result) label = "conv" identifier = datetime.datetime.now().strftime("%f") global boxname boxname = label+identifier root = tkinter.Tk() root.title(boxname) root.attributes("-topmost", True) root.overrideredirect(1) coordinates = "+%s+%s" % (screencaret_x, screencaret_y) root.geometry(coordinates) selected = tkinter.StringVar() selected.set("") entry = tkinter.Entry(root, textvariable = selected) entry.pack() listbox = tkinter.Listbox(root) listbox.pack() listbox.focus_set() listbox.insert(tkinter.END, " ===== 変換候補 ===== ") for item in result: listbox.insert(tkinter.END, item) selected.set(listbox.get(1)) def select_next(event): keyboard.release("space") nowselected = listbox.index("active") if nowselected < counts: listbox.see(nowselected+1) listbox.activate(nowselected+1) selected.set(listbox.get(nowselected+1)) else: listbox.activate(1) listbox.see(1) selected.set(listbox.get(1)) def select_prev(event): keyboard.release("space") keyboard.release("shift") nowselected = listbox.index("active") if nowselected > 1: listbox.see(nowselected-1) listbox.activate(nowselected-1) selected.set(listbox.get(nowselected-1)) else: listbox.activate(counts) listbox.see(counts) selected.set(listbox.get(counts)) def get_entry(event): keyboard.release("enter") nowselected = listbox.index("active") global converted if nowselected > 0: converted = listbox.get("active") else: converted = listbox.get(1) global destroy destroy = True def terminate(event): keyboard.release("\\") keyboard.release("ctrl") global end end = True global destroy destroy = True listbox.bind("", select_next) listbox.bind("", select_prev) listbox.bind("", get_entry) listbox.bind("", terminate) while destroy == False: time.sleep(0.001) # for reducing CPU usage root.update() fgw = get_foreground_window() fgwt = get_window_text(fgw) if fgwt == boxname: pass else: target = find_window("TkTopLevel", boxname) set_foreground_window(target) destroy = False listbox.destroy() root.destroy() def threading_start(threaddef, threadname, targetname): threadname = threading.Thread(target=threaddef, name=targetname) threadname.start() def write_word(word): time.sleep(0.001) keyboard.write(word) def release_keys(): keyboard.release("backspace") keyboard.release("space") keyboard.release("shift") keyboard.release("ctrl") keyboard.release("enter") def unhook_all(): keyboard.unhook_all() #def send_key(key): # keyboard.send(key) def send_backspace(): keyboard.send("backspace") def send_enter(): keyboard.send("enter") def send_plus(): ctypes.windll.user32.keybd_event(0x6B, 0, 0, 0) ctypes.windll.user32.keybd_event(0x6B, 0, 0x0002, 0) def sleep_1ms(): time.sleep(0.001) def sleep_5cs(): time.sleep(0.05) def replace_verticalbar(source): result = source.replace("|", "|") return result def define_systray(): global systray systray = infi.systray.SysTrayIcon("icon.ico", "変換中") def start_systray(): systray.start() def shutdown_systray(): systray.shutdown() # ==== algorithm area ==== # something like pseudocode oriented programming style, "pops" ;-) print("hi") #print("Select window, then press shift.") # for testing #keyboard.wait("shift") # for testing define_systray() start_systray() #chkfghWnd = get_foreground_window() # check foreground handle of window #chkfgWndt= get_window_text(chkfghWnd) # check foreground window text if "conv" in chkfgWndt: print("Another process is already running!") print("Program will be aborted.") end = True abort = True while end == False: fghWnd = 0 # foreground handle of window screencaret_x = 0 screencaret_y = 0 backspaceisreleased = False split = False thread1 = None thread2 = None source = "" converted = "" optionlist = [] release_keys() unhook_all() add_hotkey_backspace_is_released() # for avoiding a confliction between this program # and the ctrl+alt+del security option window terminator() if firstloop == False: sleep_1ms() fghWnd = get_foreground_window() sleep_1ms() screencaret_x, screencaret_y = get_coordinates() sleep_1ms() thread1 = get_source(screencaret_x, screencaret_y) sleep_1ms() threading_start(thread1, "sourcethread", "sourcethread") if end == True: break sleep_1ms() set_foreground_window(fghWnd) quotation = "\"" leftbracket = "[" rightbracket = "]" sourcegen = source.strip() sourcegen = sourcegen.replace(" ", "\", \"") sourcegen = leftbracket + quotation + sourcegen + quotation + rightbracket sourcegen = json.loads(sourcegen) print(str(sourcegen)) sourcenew = [] for item in sourcegen: if item is not "": sourcenew.append(item) sourcenum = len(sourcenew) print(str(sourcenew)) sourcecounter = 0 while sourcecounter < sourcenum: sourcecounter = sourcecounter + 1 sourcecounter = 0 while sourcecounter < sourcenum: sleep_1ms() set_foreground_window(fghWnd) if source is not " ": source = sourcenew[sourcecounter] if len(source) == 1: if source in exception: for src, alt in zip([",", "|", ";"], [comma, verticalbar, semicolon]): if source is src: optionlist = alt else: optionlist = convert_romaji_to_kanji(source) else: if "|" in source: source = replace_verticalbar(source) optionlist = convert_romaji_to_kanji(source) sleep_1ms() set_foreground_window(fghWnd) sleep_1ms() screencaret_x, screencaret_y = get_coordinates() sleep_1ms() thread2 = select_word_from_list(optionlist, screencaret_x, screencaret_y) sleep_1ms() threading_start(thread2, "resultthread", "resultthread") if end == True: break sleep_1ms() set_foreground_window(fghWnd) if converted is not ";": split = False while split == False: if ";" in converted: # for SKKDict with annotation converted = converted[:-1] else: split = True if converted is not "+": write_word(converted) else: # because keyboard.send("+") replies ";" on ROS's japanese env send_plus() sourcecounter = sourcecounter + 1 if source is " ": optionlist = space sleep_1ms() thread3 = select_word_from_list(optionlist, screencaret_x, screencaret_y) sleep_1ms() threading_start(thread3, "resultthread", "resultthread") sleep_1ms() set_foreground_window(fghWnd) sleep_1ms() write_word(converted) if len(converted) == 0: if backspaceisreleased == True: send_backspace() else: send_enter() firstloop = False if abort == False: set_foreground_window(fghWnd) release_keys() unhook_all() sleep_5cs() shutdown_systray() sleep_5cs() if autophagy == False: print("bye") else: print("adieu")