import re import sys import tempfile import json import datetime import tkinter import tkinter.filedialog import tkinter.messagebox import tkinter.simpledialog class Text(tkinter.Text): """ Tkinter Text Widget Override that allows to highlight words with regex. """ def __init__(self, *args, **kwargs): tkinter.Text.__init__(self, *args, **kwargs) def highlight(self, pattern, tag, start="1.0", stop="end", regex=True): """ Highlight text matched by "pattern" and apply it the tag "tag" Specify "start" and "stop" in order to restrict the search and regex=False if pattern is not a regex. """ start = self.index(start) stop = self.index(stop) self.mark_set("matchStart", start) self.mark_set("matchEnd", start) self.mark_set("searchLimit", stop) occurrences = tkinter.IntVar() while True: index = self.search( pattern, "matchEnd", "searchLimit", count=occurrences, regexp=regex ) if index == "": break self.mark_set("matchStart", index) self.mark_set("matchEnd", "%s+%sc" % (index, occurrences.get())) self.tag_add(tag, "matchStart", "matchEnd") class Application(tkinter.Frame): """Application class""" def __init__(self): tkinter.Frame.__init__(self, window) window.iconify() #Vars self.temp = tempfile.TemporaryFile(mode="w+t") self.text = tkinter.StringVar() self.file = "" self.opzionifile = { "parent": window, "filetypes": [("text files", ".txt")], "defaultextension": ".txt", "initialfile": "file.txt" } self.opzionichat = { "font": ("Monaco", 13), "borderwidth": 2, "highlightthickness": 0 } #pref management try: self.prefs = json.load(open("prefs.json")) except FileNotFoundError: tkinter.messagebox.showwarning( "Dropchat", "Preferences file non found!", detail="Default preferences rebuilt.") self.build_prefs() #Gestione della window window.geometry(self.prefs["geometry"]) window.configure(background="#a8a8a8") window.columnconfigure(1, weight=1) window.rowconfigure(0, weight=1) window.protocol('WM_DELETE_WINDOW', self.close) window.deiconify() self.grid() #Inizzializzazione self.set_title() self.widgets() self.read() #Gestione delle ultime conversazioni try: self.delete_not_found() self.file = self.prefs["chat"][-1] except IndexError: response = tkinter.messagebox.askquestion( "Dropchat", "No conversations found.", detail="Make a new one?") if response == "yes": self.new_file() #Loop principale self.loop() #Widget della window def widgets(self): """ Draw window widgets. """ self.menubar = tkinter.Menu(window) self.chat = Text(window, width=100, height=30, relief="sunken", insertbackground="#fff", **self.opzionichat) self.sidebar = tkinter.Listbox( window, width=20, borderwidth=0, background="#dce0e8") self.textbox = tkinter.Entry( window, textvariable=self.text, **self.opzionichat) self.chat.grid(column=1, row=0, sticky="nswe", pady=(6, 3), padx=6) self.textbox.grid(column=1, row=1, sticky="nswe", pady=(3, 6), padx=6) self.sidebar.grid(column=0, row=0, sticky="nswe", rowspan=2) #Bindings self.chat.bind("", "break") self.sidebar.bind("", self.switch_file) self.sidebar.bind("", self.show_menu) self.textbox.bind("", self.write) #Menu bar self.menubar = tkinter.Menu(self) menu = tkinter.Menu(self.menubar) #Menu 1 self.menubar.add_cascade(label="Conversation", menu=menu) menu.add_command(label="New file", command=self.new_file) menu.add_command(label="Open file...", command=self.open_file) #Menu 2 menu = tkinter.Menu(self.menubar) self.menubar.add_cascade(label="Profile", menu=menu) menu.add_command( label="User name", command=lambda: self.edit_prefs( "username", tkinter.simpledialog.askstring( "User name", "Insert your name:", initialvalue=self.prefs["username"], parent=window))) #Menu 3 menu = tkinter.Menu(self.menubar) self.menubar.add_cascade(label="Edit", menu=menu) menu.add_command(label="Cut", command=lambda: window.focus_get().event_generate("<>")) menu.add_command(label="Copy", command=lambda: window.focus_get().event_generate("<>")) menu.add_command(label="Paste", command=lambda: window.focus_get().event_generate("<>")) window.config(menu=self.menubar) #Context menu 1 self.menu1 = tkinter.Menu(self) self.menu1.add_command(label="Open", command=self.switch_file) self.menu1.add_command(label="Delete", command=self.delete_file) self.menu1.add_separator() self.menu1.add_command(label="New file", command=self.new_file) self.menu1.add_command(label="Reload", command=lambda: self.sidebar.delete(0, "end")) #Contextmenu 2 self.menu2 = tkinter.Menu(self) self.menu2.add_command(label="New file", command=self.new_file) self.menu2.add_command(label="Reload", command=lambda: self.sidebar.delete(0, "end")) def read(self): """ Reads the current file from the buffer and writes it into the chat. """ self.update() self.temp.seek(0) self.chat.delete(0.0, "end") self.chat.insert("end", self.temp.read()) self.highlight() def write(self, event): """Write a message in the current file""" if self.file != "": with open(self.file, "a") as file: if self.text.get() != "": row = "[%s] %s: %s\n" % ( datetime.datetime.now().strftime("%d-%m-%y %H:%M"), self.prefs["username"], self.text.get()) self.text.set("") file.write(row) def update(self): """Copy file into the buffer""" self.temp.seek(0) self.temp.truncate() if self.file != "": with open(self.file) as text: self.temp.write(text.read()) def set_title(self): """Update window's title to match current conversation""" file = re.search("([^/]*)$", self.file) if self.file == "": window.title("Dropchat") else: window.title("Dropchat - " + file.group(0)) def highlight(self): """ Highlight semantic parts in the text chat. (Date, username and others) """ self.chat.tag_configure("date", foreground="#005d8f") self.chat.tag_configure("name", foreground="#648f00") self.chat.tag_configure("other", foreground="#de7a31") self.chat.highlight("\[\d+-\d+-\d+ \d+:\d+\]", "data") for name in re.findall(" ([a-zA-Z]+): ", self.chat.get(0.0, "end")): if name == self.prefs["username"]: self.chat.highlight(name, "name") else: self.chat.highlight(name, "other") def show_menu(self, event): """Manages the opening and positioning of context menu""" self.sidebar.index = self.sidebar.nearest(event.y) if self.sidebar.index < 0: self.menu2.post(event.x_root, event.y_root) return self.sidebar.activate(self.sidebar.index) _, offset, _, height = self.sidebar.bbox(self.sidebar.index) if event.y > height + offset + self.sidebar.size(): self.menu2.post(event.x_root, event.y_root) else: self.menu1.post(event.x_root, event.y_root) def recent_files(self): """Inserts recently opened files into the sidebar""" for x, file in enumerate(self.prefs["chat"]): file = " - " + re.search("([^/]*)$", file).group(0) if file not in self.sidebar.get(x): self.sidebar.insert(x, file) def delete_file(self): """Removes the currently selected file from the sidebar""" if self.prefs["Chat"][self.sidebar.index] == self.file: self.file = "" del self.prefs["Chat"][self.sidebar.index] json.dump(self.prefs, open("prefs.json", "w")) self.read() self.set_title() def switch_file(self, _=None): """Opens the currently selected file from the sidebar in the chat""" file = self.prefs["chat"][int(self.sidebar.curselection()[0])] if self.file != file: self.file = file self.read() self.set_title() def open_file(self): """Selects a new file and opens it in the chat.""" if sys.platform == "darwin": file = tkinter.filedialog.askopenfilename( title="Choose a file...", message="Open a conversation.", **self.opzionifile) else: file = tkinter.filedialog.askopenfilename( title="Choose a file...", **self.opzionifile) if file not in self.prefs["Chat"] and file != "": self.prefs["chat"] += file, json.dump(self.prefs, open("prefs.json", "w")) self.file = file self.read() self.set_title() def new_file(self): """Creates a new file and opens it in the chat""" if sys.platform == "darwin": file = tkinter.filedialog.asksaveasfilename( title="Create a file...", message="Select the name of the new conversation.", **self.opzionifile) else: file = tkinter.filedialog.asksaveasfilename( title="Creates a new file...", **self.opzionifile) if file != "": self.file = file open(self.file, "w").close() if self.file not in self.prefs["chat"]: self.prefs["chat"] += self.file, json.dump(self.prefs, open("prefs.json", "w")) self.read() self.set_title() def delete_not_found(self): """Removes files that are no longer available""" for x, file in enumerate(self.prefs["chat"]): try: open(file) self.file = file except FileNotFoundError: del self.prefs["Chat"][x] continue json.dump(self.prefs, open("prefs.json", "w")) def build_prefs(self): """Rebuild default preferences""" default = { "username": "user", "chat": ["chat.txt"], "geometry": "800x500+500+250" } json.dump(default, open("prefs.json", "w")) self.prefs = json.load(open("prefs.json")) def edit_prefs(self, key, pref): """Changing a preference and saves it into an external file.""" if pref != "": self.prefs[key] = pref json.dump(self.prefs, open("prefs.json", "w")) def close(self): """ Save the geometry of the window for the next boot, closes and deletes the cache, closes the window and exits. """ self.edit_prefs("geometry", window.geometry()) self.temp.close() window.destroy() def loop(self): """ Main loop Update recent files and current covnersation. """ self.recent_files() self.read() self.after(200, self.loop) window = tkinter.Tk() app = Application() app.mainloop()