def open_folder(self, folder=None): if self.net_hand.is_connected: self.back.config(state='disabled') else: self.back.config(state='normal') location = "" if folder: location = folder else: location = filedialog.askdirectory() if location != "": #clear text and delete current radio buttons self.workspace.open_directory(location) # clear text and delete current radio buttons self.code.text.delete("1.0", END) # folder = os.listdir(location) # for item in folder: # item_path = location+ "/" + item # # condition so that folders that start with "." are not displayed # if os.path.isfile(item_path) or not item.startswith("."): # Radiobutton(self.radio_frame, text = item, variable=self.current_file_name, command=self.open_item, value=item_path, indicator=0).pack(fill = 'x', ipady = 0) split = str(location).split("/") index = -1 folder_name = split[index] while folder_name == "": index -= 1 folder_name = split[index] self.directory.config(text="Current Folder:\n" + folder_name) # clear text and delete current radio buttons self.code.text.delete("1.0", END) self.radio_frame.destroy() self.radio_frame = Frame(self.files, width=self.files.cget("width")) self.radio_frame.pack(fill="both", expand=True) self.options = FilesFrame(self.radio_frame, window=self) self.options.populate(self.workspace) self.reset_terminal()
def display_luggage(self, luggage): """Update the Frame to display the given (open) Luggage """ self.luggage = luggage self.frame_secrets = SecretsFrame(pyluggage_config=self.pyluggage_config, secrets=luggage.load_secrets()) self.frame_secrets.secrets_changed.connect(self._update_secrets) self.frame_files = FilesFrame(pyluggage_config=self.pyluggage_config, luggage=self.luggage) self.frame_config = EditConfigFrame(pyluggage_config=self.pyluggage_config, luggage=self.luggage) self.tab_contents.setIconSize(QSize(50,50)) self.tab_contents.addTab(self.frame_secrets, QIcon(":/pyluggage/img/secret_50.png"), _translate("Secrets")) self.tab_contents.addTab(self.frame_files, QIcon(":/pyluggage/img/secret_files_50.png"), _translate("Files")) self.tab_contents.addTab(self.frame_config, QIcon(":pyluggage/img/config.png"), _translate("Configuration")) self.update_controls() self.frame_secrets.lineedit_filter.setFocus()
class LuggageFrame(QFrame, Ui_LuggageFrame): # Emitted when the current luggage is closed luggage_closed = QtCore.pyqtSignal() # Emitted when the user requests help help_needed = QtCore.pyqtSignal() # Luggage being managed by this frame. Can be None before opening luggage = None # Tabs managed by this frame frame_secrets = None frame_files = None def __init__(self, pyluggage_config): QFrame.__init__(self) self.setupUi(self) self.update_controls() self.luggage = None self.pyluggage_config = pyluggage_config self.button_close.clicked.connect(self.close_luggage) self.button_help.clicked.connect(self.help_needed.emit) def display_luggage(self, luggage): """Update the Frame to display the given (open) Luggage """ self.luggage = luggage self.frame_secrets = SecretsFrame(pyluggage_config=self.pyluggage_config, secrets=luggage.load_secrets()) self.frame_secrets.secrets_changed.connect(self._update_secrets) self.frame_files = FilesFrame(pyluggage_config=self.pyluggage_config, luggage=self.luggage) self.frame_config = EditConfigFrame(pyluggage_config=self.pyluggage_config, luggage=self.luggage) self.tab_contents.setIconSize(QSize(50,50)) self.tab_contents.addTab(self.frame_secrets, QIcon(":/pyluggage/img/secret_50.png"), _translate("Secrets")) self.tab_contents.addTab(self.frame_files, QIcon(":/pyluggage/img/secret_files_50.png"), _translate("Files")) self.tab_contents.addTab(self.frame_config, QIcon(":pyluggage/img/config.png"), _translate("Configuration")) self.update_controls() self.frame_secrets.lineedit_filter.setFocus() def close_luggage(self): """Update the frame so that it does not reference the luggage and emit the luggage_closed signal """ if self.luggage is not None: if be_verbose: print "[LuggageFrame] Closing luggage..." # Close the luggage, possibly optimizing its file usage if self.luggage.get_db_usage_ratio() < self.pyluggage_config.db_compaction_efficiency_threshold: CompactLuggageWorker.run_optimization_with_progress_dialog(parent=self, luggage=self.luggage) self.luggage = None self.frame_files.hide() self.frame_secrets.hide() self.frame_config.hide() for _ in range(3): self.tab_contents.removeTab(0) self.frame_secrets = None self.frame_files = None self.frame_config = None self.luggage_closed.emit() def update_controls(self): if self.luggage is not None: self.label_luggage_name.setText(os.path.basename(self.luggage.luggage_path)) self.tab_contents.setFocus() def _update_secrets(self): """Called when the list of secrets is modified. Make the luggage store the new list """ self.luggage.save_secrets(self.frame_secrets.secret_list)
class Window: """ This class handles all display aspects of Jum.py. """ # tk root root = Tk() # menu bar menu_bar = Menu() # file sub-menu in the menu bar menu_file = Menu(tearoff=False) # connections sub-menu in the menu bar menu_connections = Menu(tearoff=False) # frames for UX top_frame = Frame(root) bottom_frame = Frame(root) files = Frame(top_frame) location = Frame(files) radio_frame = Frame(files) directory = Label(location) back = Button(location) # functional frames code = CodeFrame(top_frame) terminal = Text(bottom_frame) # other variables current_file_name = StringVar() current_file = None old_text = "" # the workspace used by the program workspace: Workspace = None def __init__(self): self.net_hand = NetworkHandler(self.parse_message) #self.cursor_thread_run = True #self.cursor_thread = Thread(target=self.track_cursor) #self.cursor_thread.setDaemon(True) #self.u2_pos = None self.names = {} self.cursor_colors = ['red', 'green', 'blue', 'yellow', 'cyan'] self.autosave_thread = Thread(target=self.autosave_thread) self.autosave_thread.setDaemon(True) current_terminal_buffer_column = 0 current_terminal_buffer_line = 0 self.log = logging.getLogger('jumpy') self.mac = hex(uuid.getnode()) self.is_host = False self.have_perms = False self.mac_name = dict() self.workspace = Workspace() self.create() def create(self) -> None: """ Creates the window. """ self.root.title("jum.py") self.root.bind('<Key>', self.handle_event) self.root.bind('<Button-1>', self.handle_event) # menu bar self.menu_bar.add_cascade(label='File', menu=self.menu_file) self.menu_bar.add_cascade(label='Connections', menu=self.menu_connections) # file sub-menu self.menu_file.add_command(label="Open", command=self.open_folder) self.menu_file.add_command(label="Save", command=self.save_file) self.menu_file.add_command(label="Help", command=self.open_help) # connections sub-menu # self.menu_connections.add_command(label='Connect', command=self.net_hand.establish_connection) def create(): if self.workspace.is_active: val = simpledialog.askstring("Lobby name", "Please name your lobby") username = simpledialog.askstring("Prompt", "Please input a username") self.mac_name.update({self.mac: username}) self.net_hand.join_lobby(val) self.is_host = True self.have_perms = True self.net_hand.establish_connection() self.back.config(state='disabled') else: messagebox.showerror("jumpy", "no active workspace") def join(): self.workspace.use_temp_workspace() self.open_folder(self.workspace.directory) self.code.text.config(state='disabled') val = simpledialog.askstring( "Lobby name", "Please input the lobby you want to join.") username = simpledialog.askstring("Prompt", "Please input a username") self.mac_name.update({self.mac: username}) self.net_hand.join_lobby(val) self.net_hand.establish_connection() self.is_host = False self.have_perms = False dprj = DataPacketRequestJoin() dprj.set_name(self.mac_name.get(self.mac)) self.net_hand.send_packet(dprj) def disconnect(): self.net_hand.close_lobby() self.back.config(state='normal') self.menu_connections.add_command(label='Disconnect', command=disconnect) self.menu_connections.add_command(label='Create lobby', command=create) self.menu_connections.add_command(label='Join lobby', command=join) # add menubar to root self.root.config(menu=self.menu_bar) # terminal default self.terminal.insert("1.0", "Console:\n>>>") self.current_terminal_buffer_column = 3 self.current_terminal_buffer_line = 2 # text default self.old_text = self.code.text.get("1.0", END) self.directory.config(width=20, text="Current Folder:\nNone") self.back.config(text="cd ..\\", command=self.previous_dir) # visual effects self.files.config(width=200, bg='light grey') self.terminal.config(height=10, borderwidth=5) # visual packs self.root.geometry("900x600") self.top_frame.pack(side="top", fill='both', expand=True) self.bottom_frame.pack(side="bottom", fill='both', expand=True) self.files.pack(side="left", fill='both') self.location.pack(side="top", fill='x') self.directory.pack(side="left", fill='x', expand=True) self.back.pack(side="right", fill='x', expand=True) self.code.pack(side="right", fill='both', expand=True) self.terminal.pack(fill='both', expand=True) def show(self) -> None: """ Shows the window. """ # self.autosave_thread.start() # TODO: fix for better placing #self.cursor_thread.start() self.root.mainloop() def previous_dir(self): if self.workspace.directory != "C:/" and self.workspace.directory: split = self.workspace.directory.split("/") new_dir = "/".join(split[0:-1]) if (new_dir == "C:"): new_dir += "/" self.open_folder(new_dir) def open_help(self): webbrowser.open_new( "https://docs.google.com/document/d/13AHTV3BVfS3ELmaW2cqfzgJ9YmOeBkkkMqViyU-0WDM/edit?usp=sharing" ) # TODO for folders with alot of files add a scrollbar def open_folder(self, folder=None): if self.net_hand.is_connected: self.back.config(state='disabled') else: self.back.config(state='normal') location = "" if folder: location = folder else: location = filedialog.askdirectory() if location != "": #clear text and delete current radio buttons self.workspace.open_directory(location) # clear text and delete current radio buttons self.code.text.delete("1.0", END) # folder = os.listdir(location) # for item in folder: # item_path = location+ "/" + item # # condition so that folders that start with "." are not displayed # if os.path.isfile(item_path) or not item.startswith("."): # Radiobutton(self.radio_frame, text = item, variable=self.current_file_name, command=self.open_item, value=item_path, indicator=0).pack(fill = 'x', ipady = 0) split = str(location).split("/") index = -1 folder_name = split[index] while folder_name == "": index -= 1 folder_name = split[index] self.directory.config(text="Current Folder:\n" + folder_name) # clear text and delete current radio buttons self.code.text.delete("1.0", END) self.radio_frame.destroy() self.radio_frame = Frame(self.files, width=self.files.cget("width")) self.radio_frame.pack(fill="both", expand=True) self.options = FilesFrame(self.radio_frame, window=self) self.options.populate(self.workspace) self.reset_terminal() # starts cursor tracking thread # TODO: uncomment # TODO add functionality to clicking on folders (change current folder to that folder, have a back button to go to original folder) (chad doesn't think this is needed anymore) def open_item(self): if os.path.isfile(self.current_file_name.get()): self.code.text.delete("1.0", END) file = open(self.current_file_name.get(), "r") self.current_file = file try: self.code.text.insert(1.0, file.read()) self.syntax_highlighting() self.old_text = self.code.text.get("1.0", END) except: self.code.text.insert(1.0, "Can not interperate this file") file.close() else: self.open_folder(self.current_file_name.get()) name = self.current_file_name.get().split("/")[-1] self.directory.config(text="Current Folder:\n" + name) def save_file(self) -> None: f = filedialog.asksaveasfilename(defaultextension=".py") to_save_file = open(f, 'w') to_save_file.write(self.code.text.get("1.0", END)) to_save_file.close() def update_text(self, action: Action, position: int, character: str): self.log.debug( 'updating text with action: \'{}\', position: \'{}\', character: \'{}\'' .format(action, position, repr(character))) text_current = self.code.text.get("1.0", END) text_new = text_current[1:position + 1] + character + text_current[position + 1:] self.log.debug( f"current text:{repr(text_current)} \n updated text {repr(text_new)}" ) self.code.text.delete("1.0", END) self.code.text.insert("1.0", text_new) # n = 1 # if action == Action.ADD: # # TODO: fix# # # # text_new = character # if text_new == "\n": # n+=1 # self.log.debug("%d.%d"%(n,position)) # #self.text.insert("%d.%d"%(n,position), text_new) # elif action == Action.REMOVE: # # TODO: implement # pass def set_text(self, new_text: str): """ Sets the text on the Text object directly. Author: Chad Args: new_text: string Returns: """ self.code.text.delete("1.0", END) self.code.text.insert("1.0", new_text) def handle_event(self, event): """ Interpret keypresses on the local machine and send them off to be processed as a data packet. Keeps track of one-edit lag. TODO: Don't interpret all keypress as somthing to be sent e.g. don't send _alt_ Authors: Chad, Ben Args: event: str unused? Returns: Interactions: sends DataPacketDocumentEdit """ # if self.net_hand.is_connected: # new_text = self.code.text.get("1.0", END) # packet = DataPacketDocumentEdit(old_text=self.old_text, new_text=new_text) # if packet.character == '' or new_text == self.old_text: # return # else: # self.net_hand.send_packet(packet) # self.syntax_highlighting() # self.old_text = self.code.text.get("1.0", END) if event.widget == self.terminal: # handle terminal event cursor_line, cursor_column = [ int(x) for x in self.terminal.index(INSERT).split('.') ] if event.char == '\r': command = self.terminal.get( str(self.current_terminal_buffer_line) + "." + str(self.current_terminal_buffer_column), END).strip("\n ").split(" ") print(command) if command[0] != "": if self.workspace.directory: os.chdir(self.workspace.directory) if "cd" in command: if not self.net_hand.is_connected: if len(command) >= 2: try: os.chdir(self.workspace.directory + "/" + " ".join(command[1::]).strip( '\'\"')) self.workspace.open_directory( os.getcwd().replace("\\", "/")) self.open_folder( self.workspace.directory) return except: self.current_terminal_buffer_line += 1 self.terminal.insert( END, "'" + " ".join( command[1::]).strip('\'\"') + "' does not exist as a subdirectory\n" ) else: os.chdir("C:/") self.workspace.open_directory(os.getcwd()) self.open_folder("C:/") return else: self.terminal.insert( END, "Can not change directories while in workspace.\n" ) self.current_terminal_buffer_line += 1 else: error = self.run_command(" ".join(command)) if error: self.terminal.insert(END, error) self.current_terminal_buffer_line += 1 else: self.terminal.insert( END, "Open a directory before using the console.\n") self.current_terminal_buffer_line += 1 if self.workspace.directory: self.terminal.insert(END, self.workspace.directory + ">") self.current_terminal_buffer_column = len( self.workspace.directory) + 1 else: self.terminal.insert(END, ">>>") self.terminal.see(END) self.current_terminal_buffer_line += 1 return if event.char == '\x03': self.reset_terminal() if cursor_column < self.current_terminal_buffer_column or cursor_line < self.current_terminal_buffer_line: if event.char == '\x08': self.terminal.insert(END, ">") self.terminal.mark_set( "insert", "%d.%d" % (self.current_terminal_buffer_line, self.current_terminal_buffer_column)) elif event.widget == self.code.text: # handle text event if self.net_hand.is_connected and self.current_file_name.get( ) != "None": to_send = DataPacketDocumentEdit() to_send.set_document( self.current_file_name.get().split('/')[-1]) to_send.set_text(self.code.text.get("1.0", END)) self.net_hand.send_packet(to_send) # send a DataPacketCursorUpdate position = self.code.text.index(INSERT) dpcu = DataPacketCursorUpdate() dpcu.set_position(position) dpcu.set_document(self.current_file_name.get().split('/')[-1]) self.net_hand.send_packet(dpcu) self.syntax_highlighting() # # TODO: chad thinks that this is the answere to hash mis-match sleep(0.1) def syntax_highlighting(self, lang='python'): """ Highlights key elements of syntax with a color as defined in the language's SyntaxHandler. Only 'python' is currently implemented, but more can easily be added in the future. Author: Ben Args: lang: string, which language to use Returns: TODO: fix so keywords inside another keyword aren't highlighted TODO: make so that it doesn't trigger after every character TODO: run on seperate thread at interval or trigger (perhaps at spacebar? would reduce work) """ for tag in self.code.text.tag_names(): self.code.text.tag_delete(tag) if lang == 'python': SyntaxHandler = Syntax() syntax_dict = SyntaxHandler.get_color_dict() for kw in SyntaxHandler.get_keywords(): idx = '1.0' color = syntax_dict[kw] self.code.text.tag_config(color, foreground=color) # search_term =#rf'\\y{kw}\\y' # ' '+ kw + ' ' while idx: idx = self.code.text.search('\\y' + kw + '\\y', idx, nocase=1, stopindex=END, regexp=True) if idx: # self.log.debug(idx) nums = idx.split('.') nums = [int(x) for x in nums] # self.log.debug(f"{left} { right}") lastidx = '%s+%dc' % (idx, len(kw)) self.code.text.tag_add(color, idx, lastidx) idx = lastidx self.code.text.tag_config("comments", foreground="olive drab") idx = '1.0' while idx != '': idx = self.code.text.search('#', idx, nocase=1, stopindex=END) #self.log.debug(idx) if idx == '': #self.log.debug(idx) continue #self.log.debug(idx) endl = self.code.text.search('\n', idx, stopindex=END) if endl == "": endl = END self.code.text.tag_add("comments", idx, endl) idx = endl def reset_terminal(self): self.terminal.delete("1.0", END) self.terminal.insert(END, "Console:\n") if self.workspace.directory: self.terminal.insert(END, self.workspace.directory + ">") self.current_terminal_buffer_column = len( self.workspace.directory) + 1 else: self.terminal.insert(END, ">>>") self.current_terminal_buffer_column = 3 self.current_terminal_buffer_line = 2 def run_command(self, command): try: process = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE) while True: output = process.stdout.readline() if output == bytes('', "utf-8") and process.poll() == 0: break if output: self.terminal.insert(END, output.strip() + bytes("\n", "utf-8")) self.current_terminal_buffer_line += 1 self.terminal.insert(END, "\n") self.current_terminal_buffer_line += 1 except: return "'" + command + "' is not a valid command\n" def parse_message(self, packet_str: DataPacket): data_dict = json.loads(packet_str) packet_name = data_dict.get('packet-name') if data_dict.get('mac-addr') == self.mac: self.log.debug('received packet from self, ignoring...') else: self.log.debug('Received a \'{}\''.format(packet_name)) print(data_dict) if packet_name == 'DataPacket': self.log.debug('Received a DataPacket') elif packet_name == 'DataPacketDocumentEdit': self.log.debug('Received a DataPacketDocumentEdit') cursor_index = self.code.text.index(INSERT) packet: DataPacketDocumentEdit = DataPacketDocumentEdit() packet.parse_json(packet_str) self.workspace.apply_data_packet_document_edit(packet) current_doc = self.current_file_name.get().split('/')[-1] if packet.get_document() == current_doc: self.code.text.delete("1.0", END) self.code.text.insert(END, packet.get_text()) self.syntax_highlighting() self.code.text.mark_set(INSERT, cursor_index) elif packet_name == 'DataPacketCursorUpdate': u2_pos = data_dict.get('position') name = data_dict.get('mac-addr') self.cursor_update(u2_pos, str(name)) elif packet_name == 'DataPacketRequestJoin': packet: DataPacketRequestJoin = DataPacketRequestJoin() packet.parse_json(packet_str) if self.is_host: result = messagebox.askyesno( "jumpy request", "Allow \'{}\' to join the lobby?".format( data_dict.get(DataPacketRequestJoin.KEY_NAME))) dprr = DataPacketRequestResponse() dprr.set_target_mac(packet.get_mac_addr()) dprr.set_can_join(result) self.net_hand.send_packet(dprr) if result: sleep(3) name_broadcast = DataPacketNameBroadcast() name_broadcast.set_name(self.mac_name.get(self.mac)) self.net_hand.send_packet(name_broadcast) to_send = self.workspace.get_save_dump() for packet in to_send: self.net_hand.send_packet(packet) elif packet_name == 'DataPacketRequestResponse': packet: DataPacketRequestResponse = DataPacketRequestResponse() packet.parse_json(packet_str) if packet.get_target_mac() == DataPacket.get_mac_addr_static(): self.log.debug('Received a DataPacketRequestResponse') can_join = packet.get_can_join() # todo: fix if can_join: self.log.debug('allowed into the lobby') self.workspace.use_temp_workspace() self.have_perms = True messagebox.showinfo( "jumpy", "You have been accepted into the lobby!") else: self.log.debug('rejected from the lobby') self.have_perms = False messagebox.showerror( "jumpy", "You have NOT been accepted into the lobby...") self.net_hand.close_connection() name_broadcast = DataPacketNameBroadcast() name_broadcast.set_name(self.mac_name.get(self.mac)) self.net_hand.send_packet(name_broadcast) elif packet_name == 'DataPacketSaveDump': packet: DataPacketSaveDump = DataPacketSaveDump() packet.parse_json(packet_str) self.workspace.apply_data_packet_save_dump(packet) if self.workspace.new_file_added: if len(self.workspace.files) == packet.get_workspace_size( ): self.log.debug( 'received whole workspace, setting code.text state to normal' ) self.code.text.config(state='normal') self.open_folder(self.workspace.directory) elif packet_name == 'DataPacketSaveRequest': to_send = self.workspace.get_save_dump_from_document( data_dict.get('document')) self.net_hand.send_packet(to_send) elif packet_name == 'DataPacketNameBroadcast': packet = DataPacketNameBroadcast() packet.parse_json(packet_str) self.log.debug('mac_name updating {} to {}'.format( packet.get_mac_addr(), packet.get_name())) self.mac_name.update( {packet.get_mac_addr(): packet.get_name()}) else: self.log.warning( 'Unknown packet type: \'{}\''.format(packet_name)) return False def get_words(self): """ Gets all words (definition: seperated by a space character) in the Text object. Author: Ben Args: Returns: words: list a list a words in the Text object """ words = self.code.text.get("1.0", END).split(" ") return words def cursor_update(self, pos, name): if name not in self.names: self.names[name] = self.cursor_colors.pop() color = self.names[name] print(color) self.code.text.tag_remove(color, "1.0", END) curs = self.code.text.tag_config(color, background=color) pos_int = [int(x) for x in pos.split(".")] end_pos = f'{pos_int[0]}.{pos_int[1]+1}' self.code.text.tag_add(color, pos, end_pos) def autosave_thread(self): while True: sleep(10) if self.is_host: self.log.debug("autosaving...") self.autosave() else: pass def autosave(self): if self.is_host: to_send = self.workspace.get_save_dump() for packet in to_send: self.net_hand.send_packet(packet)