# # Copyright (C) 2020 IBM. All Rights Reserved. # # See LICENSE.txt file in the root directory # of this source tree for licensing information. # import os import re import tkinter as tk from tkinter import ttk, messagebox from tkinter.font import Font from typing import List from PIL import Image, ImageTk from clai.emulator.emulator_docker_bridge import EmulatorDockerBridge from clai.emulator.log_window import LogWindow from clai.emulator.toggled_frame import ToggledFrame from clai.emulator.emulator_presenter import EmulatorPresenter # pylint: disable=too-many-instance-attributes,protected-access,attribute-defined-outside-init,too-many-public-methods from clai.server.command_runner.clai_last_info_command_runner import InfoDebug class ClaiEmulator: def __init__(self, emulator_docker_bridge: EmulatorDockerBridge): self.presenter = EmulatorPresenter(emulator_docker_bridge, self.on_skills_ready, self.on_server_running, self.on_server_stopped) self.log_window = None def launch(self): self.root = tk.Tk() self.root.wait_visibility(self.root) self.title_font = Font(size=16, weight='bold') self.bold_font = Font(weight='bold') self.root.geometry("900x600") style = ttk.Style(self.root) style.configure("TLable", bg="black") self.add_toolbar(self.root) self.add_send_command_box(self.root) self.add_list_commands(self.root) self.root.protocol("WM_DELETE_WINDOW", lambda root=self.root: self.on_closing(root)) self.root.createcommand('exit', lambda root=self.root: self.on_closing(root)) self.root.mainloop() def on_closing(self, root): if messagebox.askokcancel("Quit", "Do you want to quit?"): self.presenter.stop_server() root.destroy() def on_skills_ready(self, skills_as_array: List[str]): self.selected_skills_dropmenu.configure(state="active") self.selected_skills_dropmenu["menu"].delete(0, 'end') for skill in skills_as_array[1:-1]: name_skill, active = self.clear_text(skill) self.selected_skills_dropmenu["menu"].add_command( label=name_skill, command=tk._setit(self.selected_skills, name_skill)) if active: self.presenter.current_active_skill = self.extract_skill_name(name_skill)[0] self.selected_skills.set(name_skill) def on_server_running(self): self.run_button.configure(image=self.stop_image) self.loading_text.set("Starting CLAI. It will take a while") self.__listen_messages() def on_server_stopped(self): self.run_button.configure(image=self.run_image) # pylint: disable=unused-argument def on_skill_selected(self, *args): self.loading_text.set("") print(f"new skills {self.selected_skills.get()}") skill_name = self.extract_skill_name(self.selected_skills.get())[0] self.presenter.select_skill(skill_name) def add_detail_label(self, parent: ttk.Frame, title: str, text_value: str, side: str): row = ttk.Frame(parent) title_label = ttk.Label(parent, text=title, font=self.bold_font, anchor='n', padding=(10, 0, 0, 0)) title_label.pack(side=side, fill=tk.BOTH) row_value = ttk.Label(parent, text=text_value, wraplength=250, anchor='nw') if side == tk.LEFT: row_value.pack(side=side, fill=tk.BOTH, expand=True) else: row_value.pack(side=side, fill=tk.BOTH, before=title_label) row.pack(side=side, fill=tk.BOTH, expand=True) def add_row(self, response: str, info: InfoDebug): response = self.clean_message(response) toggled_frame = ToggledFrame(self.frame, text=response, relief=tk.RAISED, borderwidth=1) toggled_frame.pack(fill="x", expand=1, pady=2, padx=2, anchor="n") first_row = ttk.Frame(toggled_frame.sub_frame) first_row.pack(fill="x", expand=True) self.add_detail_label(first_row, "Original:", info.command, side=tk.LEFT) self.add_detail_label(first_row, "Id:", info.command_id, side=tk.RIGHT) second_row = ttk.Frame(toggled_frame.sub_frame) second_row.pack(fill="x", expand=True) self.add_detail_label(second_row, 'Description:', self.remove_emoji(info.action_suggested.description), tk.LEFT) self.add_detail_label(second_row, 'Confidence:', f'{info.action_suggested.confidence}', tk.RIGHT) self.add_detail_label(second_row, 'Force:', f'{info.action_suggested.execute}', tk.RIGHT) third_row = ttk.Frame(toggled_frame.sub_frame) third_row.pack(fill="x", expand=True) self.add_detail_label(third_row, 'Agent:', text_value=info.action_suggested.agent_owner, side=tk.LEFT) self.add_detail_label(third_row, 'Sugestion:', text_value=info.action_suggested.suggested_command, side=tk.RIGHT) self.add_detail_label(third_row, 'Applied:', text_value=f'{info.already_processed}', side=tk.RIGHT) fourth_row = ttk.Frame(toggled_frame.sub_frame) fourth_row.pack(fill="x", expand=True) process_text = ','.join(list(map(lambda process: process.name, info.processes.last_processes))) self.add_detail_label(fourth_row, 'Processes:', text_value=f'{process_text}', side=tk.LEFT) post_title_frame = ttk.Frame(toggled_frame.sub_frame) post_title_frame.pack(fill='x', expand=True) ttk.Label(post_title_frame, text=f'Post execution', anchor='center', font=self.title_font). \ pack(side=tk.LEFT, padx=10, fill='x', expand=True) fifth_row = ttk.Frame(toggled_frame.sub_frame) fifth_row.pack(fill="x", expand=True) post_description = "" post_confidence = 0 if info.action_post_suggested: post_description = info.action_post_suggested.description post_confidence = info.action_post_suggested.confidence self.add_detail_label(fifth_row, 'Description:', text_value=f'{self.remove_emoji(post_description)}', side=tk.LEFT) self.add_detail_label(fifth_row, 'Confidence:', text_value=f'{post_confidence}', side=tk.RIGHT) self.root.after(100, self.__scroll_down_after_create) def __scroll_down_after_create(self): self.canvas.yview_moveto(1.0) def add_send_command_box(self, root): button_bar_frame = tk.Frame(root, bd=1, relief=tk.RAISED) send_button = tk.Button(button_bar_frame, padx=10, text=u"\u2713", command=self.on_send_click) send_button.pack(side=tk.RIGHT, padx=5) self.text_input = tk.StringVar() send_edit_text = tk.Entry(button_bar_frame, textvariable=self.text_input) send_edit_text.bind('<Return>', self.on_enter) send_edit_text.pack(side=tk.LEFT, fill=tk.X, expand=True) button_bar_frame.pack(side=tk.BOTTOM, pady=2, fill=tk.X) def add_toolbar(self, root): toolbar = tk.Frame(root, bd=1, relief=tk.RAISED) self.add_play_button(toolbar) self.add_refresh_button(toolbar) self.add_log_button(toolbar) self.add_skills_selector(root, toolbar) self.add_loading_progress(toolbar) toolbar.pack(side=tk.TOP, fill=tk.X) def add_skills_selector(self, root, toolbar): label = ttk.Label(toolbar, text=u'Skills') label.pack(side=tk.LEFT, padx=2) self.selected_skills = tk.StringVar(root) self.selected_skills.set("") self.selected_skills.trace("w", self.on_skill_selected) self.selected_skills_dropmenu = tk.OptionMenu(toolbar, self.selected_skills, []) self.selected_skills_dropmenu.configure(state="disabled") self.selected_skills_dropmenu.pack(side=tk.LEFT, padx=2) def add_play_button(self, toolbar): path = os.path.dirname(os.path.abspath(__file__)) self.run_image = tk.PhotoImage(file=f"{path}/run.gif") self.stop_image = tk.PhotoImage(file=f"{path}/stop.gif") self.run_button = ttk.Button(toolbar, image=self.run_image, command=self.on_run_click) self.run_button.pack(side=tk.LEFT, padx=2, pady=2) def add_refresh_button(self, toolbar): path = os.path.dirname(os.path.abspath(__file__)) # self.refresh_image = tk.PhotoImage(file=f"{path}/refresh.png") temp_image = Image.open(f"{path}/refresh.png") self.refresh_image = ImageTk.PhotoImage(temp_image) refresh_button = ttk.Button(toolbar, image=self.refresh_image, command=self.on_refresh_click) refresh_button.pack(side=tk.LEFT, padx=2, pady=2) def add_log_button(self, toolbar): path = os.path.dirname(os.path.abspath(__file__)) # self.log_image = tk.PhotoImage(file=f"{path}/log.png") temp_image = Image.open(f"{path}/log.png") self.log_image = ImageTk.PhotoImage(temp_image) log_button = ttk.Button(toolbar, image=self.log_image, command=self.open_log_window) log_button.pack(side=tk.LEFT, padx=2, pady=2) def add_loading_progress(self, toolbar): self.loading_text = tk.StringVar() loading_label = ttk.Label(toolbar, textvariable=self.loading_text) loading_label.pack(side=tk.LEFT, padx=2) def open_log_window(self): if not self.log_window: self.log_window = LogWindow(self.root, self.presenter) # pylint: disable=unused-argument def on_enter(self, event): self.send_command(self.text_input.get()) def on_send_click(self): self.send_command(self.text_input.get()) @staticmethod def clean_message(text: str): text = text[text.find('\n'):] text = re.sub('[[0-9;]*m', '', text) return text[:text.find(']0;') - 3] def send_command(self, command): self.presenter.send_message(command) self.text_input.set("") def on_run_click(self): if not self.presenter.server_running: self.presenter.run_server() else: self.presenter.stop_server() def on_refresh_click(self): self.presenter.refresh_files() @staticmethod def on_configure(canvas): canvas.configure(scrollregion=canvas.bbox("all")) def canvas_resize(self, event, canvas): canvas_width = event.width canvas.itemconfig(self.canvas_frame, width=canvas_width) def add_list_commands(self, root): canvas = tk.Canvas(root, borderwidth=0) self.frame = tk.Frame(canvas) scrollbar = tk.Scrollbar(root, orient="vertical", command=canvas.yview) canvas.configure(yscrollcommand=scrollbar.set) scrollbar.pack(side="right", fill="y") canvas.pack(fill="both", expand=True) self.canvas_frame = canvas.create_window((4, 4), window=self.frame, anchor="nw") canvas.bind('<Configure>', lambda event, canvas=canvas: self.canvas_resize(event, canvas)) self.frame.bind("<Configure>", lambda event, canvas=canvas: self.on_configure(canvas)) self.frame.bind_all("<MouseWheel>", lambda event, canvas=canvas: canvas.yview_scroll(-1 * event.delta, "units")) self.canvas = canvas @staticmethod def clear_text(skill): active = '☑' in skill skill_without_tick = skill.replace('☑\x1b[32m ', '').replace('\x1b[0m', '').replace('◻', '').strip() return skill_without_tick, active @staticmethod def extract_skill_name(skill): installed = '(Installed)' in skill return skill.replace('(Installed)', '').replace('(Not Installed)', '').strip(), installed @staticmethod def remove_emoji(description): if not description: return '' char_list = [description[j] for j in range(len(description)) if ord(description[j]) in range(65536)] description = '' for char in char_list: description = description + char return description def __listen_messages(self): self.presenter.retrieve_messages(self.add_row) self.root.after(100, self.__listen_messages)