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, _): '''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(r'\[\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()