diff --git a/Main.py b/Main.py index 7a596a1..5a35fc9 100644 --- a/Main.py +++ b/Main.py @@ -8,10 +8,22 @@ from os.path import exists from os import listdir from tkcalendar import DateEntry +from PIL import Image, ImageTk +import platform +import ast # safeguard for the treeview automated string conversion problem PREFIX = '<@!PREFIX>' +def setIcon(object): + """ + This function sets the icon of a tkinter window (passed in as `object`) to + the CFM icon. + """ + im = Image.open('assets/CFM.ico') + photo = ImageTk.PhotoImage(im) + object.wm_iconphoto(True, photo) + # change to desired resolution def set_resolution(window, width, height): @@ -136,11 +148,34 @@ def __init__(self, parent, controller): for keyword, text in columns.items(): self.treeview.heading(keyword, text=text, anchor='center') self.treeview.bind('', lambda event: self.deselect()) - # I commented out the following line because it was raising errors with selecting a conversation: - # self.treeview.bind('', lambda event: self.show_statistics()) - # sets a conversation to current conversation on - self.treeview.bind('<>', self.set_current_conversation) - + self.treeview.bind('', lambda event: self.show_statistics()) + + # *Ordered* list of columns (so we can display them in a fixed order) + self.columns = [ + 'name', + 'pep', + 'type', + 'msg', + 'call', + 'photos', + 'gifs', + 'videos', + 'files' + ] + self.column_titles = columns + + # filter columns + self.filter_columns = { + 'name': '', + 'pep': set(), + 'msg': -1, + 'call': -1, + 'photos': -1, + 'gifs': -1, + 'videos': -1, + 'files': -1 + } + # show frame title ttk.Label( self.main, text=f'{self.module.TITLE_NUMBER_OF_MSGS}: ', foreground='#ffffff', background='#232323', @@ -166,6 +201,19 @@ def __init__(self, parent, controller): command=self.search ).pack(side='top', pady=10) + # show filter button + ttk.Button( + self.nav, text=self.module.TITLE_FILTER, padding=5, command=lambda: FilterPopup(self.controller, self.columns, self.column_titles, self.filter_columns, lambda : self.filter_treeview()) + ).pack(side='top', pady=10) + + # clear selection button + ttk.Button( + self.nav, + text="Clear Filters", + padding=5, + command=self.deselect + ).pack(side='top', pady=10) + # show exit button ttk.Button( self.nav, image=self.controller.ICON_EXIT, text=self.module.TITLE_EXIT, compound='left', padding=5, @@ -253,6 +301,43 @@ def sort_treeview(self, column, order, bias): # Reverse the order for the next sort self.treeview.heading(column, command=lambda: self.sort_treeview(column, not order, bias)) + def filter_treeview(self): + """ + This function filters out rows based on criterias selected by the user. + Example: Show messages with more than 10 photos, but less than 50. + """ + # Retrieve all the rows in the treeview + children = self.treeview.get_children('') + + column_headers = self.treeview['columns'] + + filtered = [] + + for child in children: + row_content = self.treeview.item(child)['values'] + row_dict = {column_header: value for column_header, value in zip(column_headers, row_content)} + + keepRow = True + # Check if user wants to filter by name + if self.filter_columns['name'] != '' and row_dict['name'] != self.filter_columns['name']: + keepRow = False + + # Check if user wants to filter by participants + if self.filter_columns['pep'] != {''} and not self.filter_columns['pep'].issubset(ast.literal_eval(row_dict['pep'])): + keepRow = False + + for column_name in ['msg', 'call', 'photos', 'gifs', 'videos', 'files']: + # Check if user wants to filter by messages + if self.filter_columns[column_name] != (-1, -1): + min, max = self.filter_columns[column_name] + if (min != -1 and row_dict[column_name] < min) or (max != -1 and row_dict[column_name] > max): + keepRow = False + if (keepRow): + filtered.append(child) + # + # Set the selection to the filtered list + self.treeview.selection_set(filtered) + # invoked on double left click on any treeview listing def show_statistics(self): try: @@ -710,11 +795,13 @@ def __init__(self, controller, selection): self.focus_set() self.grab_set() + title, people, room, all_msgs, all_chars, calltime, sent_msgs, start_date, total_photos, total_gifs, total_videos, total_files, first_five_messages = self.controller.extract_data( title, people, room, all_msgs, all_chars, calltime, sent_msgs, start_date, total_photos, total_gifs, total_videos, total_files, first_five_messages = self.controller.extract_data( selection) # resize the window to fit all data if the conversation is a group chat if room == self.module.TITLE_GROUP_CHAT: set_resolution(self, 800, 1200) # enlarge to contain first five messages + set_resolution(self, 800, 1200) # enlarge to contain first five messages # display popup title ttk.Label(self, text=f'{self.module.TITLE_MSG_STATS}:').pack(side='top', pady=16) # show conversation title and type @@ -789,6 +876,171 @@ def __init__(self, controller, selection): + # box to contain first five messages: + ttk.Label( + self, text="First 5 Messages:" + ).pack(side='top', pady=5) + + messages_frame = ttk.Frame(self) # Frame to hold Listbox and Scrollbar for messages + messages_frame.pack(side='top', fill='both', expand=True) + + messages_scrollbar = ttk.Scrollbar(messages_frame) + messages_scrollbar.pack(side='right', fill='y') + + messages_listbox = tk.Listbox(messages_frame, width=50, height=1, yscrollcommand=messages_scrollbar.set) + messages_listbox.pack(side='left', fill='both', expand=True) + messages_scrollbar.config(command=messages_listbox.yview) + + for sender_name, content in first_five_messages: + messages_listbox.insert('end', f"{sender_name}: {content}") + + # add close button to close statistics popup + ttk.Button(self, text="Close", command=self.destroy).pack(side='bottom', pady=10) + +class FilterPopup(tk.Toplevel): + """ + This class implements the filter popup + """ + def __init__(self, controller, columns, column_titles, filter_columns, apply_callback): + tk.Toplevel.__init__(self) + self.controller = controller + self.module = self.controller.lang_mdl + set_resolution(self, 800, 600) + + self.columns = columns + print(self.columns) + self.column_titles = column_titles + + self.title(self.module.TITLE_FILTER) + self.focus_set() + self.grab_set() + + self.filter_entries = {} # Dictionary to store filter entry widgets + self.filter_columns = filter_columns + + self.apply_callback = apply_callback + + # Create filter GUI elements + ttk.Label(self, text="Filter by:", foreground='#000000', background='#ffffff', font=('Arial', 15)).pack(side='top', pady=10) + + + self.canvas = tk.Canvas(self, borderwidth=0) + # place a frame on the canvas, this frame will hold the child widgets + self.viewPort = tk.Frame(self.canvas) + # place a scrollbar on self + self.vsb = tk.Scrollbar(self, orient="vertical", command=self.canvas.yview) + # attach scrollbar action to scroll of canvas + self.canvas.configure(yscrollcommand=self.vsb.set) + + # pack scrollbar to right of self + self.vsb.pack(side="right", fill="y") + # pack canvas to left of self and expand to fil + self.canvas.pack(side="left", fill="both", expand=True) + # add view port frame to canvas + self.canvas_window = self.canvas.create_window((4,4), window=self.viewPort, anchor="nw", + tags="self.viewPort") + + # bind an event whenever the size of the viewPort frame changes. + self.viewPort.bind("", self.onFrameConfigure) + # bind an event whenever the size of the canvas frame changes. + self.canvas.bind("", self.onCanvasConfigure) + # bind wheel events when the cursor enters the control + self.viewPort.bind('', self.onEnter) + # unbind wheel events when the cursorl leaves the control + self.viewPort.bind('', self.onLeave) + + # perform an initial stretch on render, otherwise the scroll region + # has a tiny border until the first resize + self.onFrameConfigure(None) + for column in columns: + label_width = len(self.column_titles[column]) + 2 + if column in ['name', 'pep', 'type']: + label = ttk.Label(self.viewPort, text=self.column_titles[column], foreground='#000000', background='#ffffff', anchor="center", width = label_width) + label.pack(side='top', pady=5) # Fill the label horizontally + entry = ttk.Entry(self.viewPort, width=30) + entry.pack(side='top', pady=5) # Fill the entry horizontally + self.filter_entries[column] = entry + else: # For numerical fields + + label = ttk.Label(self.viewPort, text=f"{self.column_titles[column]}:", foreground='#000000', background='#ffffff', anchor="center", width = label_width) + label.pack(side='top', pady=5) # Fill the label horizontally + + label_frame = ttk.Frame(self.viewPort) + label_frame.pack(side='top', pady=5) # Fill and expand the label frame horizontally + container_frame = ttk.Frame(label_frame) + container_frame.pack() + + min_label = ttk.Label(container_frame, text="Min:") + min_label.grid(row=0, column=0, padx=(0, 5)) + + min_entry = ttk.Entry(container_frame, width=5) + min_entry.grid(row=0, column=1, padx=(0, 5)) + + max_label = ttk.Label(container_frame, text="Max:") + max_label.grid(row=0, column=2, padx=(0, 5)) + + max_entry = ttk.Entry(container_frame, width=5) + max_entry.grid(row=0, column=3, padx=(0, 5)) + + container_frame.grid_columnconfigure(0, weight=1) + self.filter_entries[column] = (min_entry,max_entry) + + apply_button = ttk.Button(self.viewPort, text="Apply Filters", command=self.apply_filters) + apply_button.pack(fill=tk.X, pady=10) # Fill the button horizontally + + def onFrameConfigure(self, event): + '''Reset the scroll region to encompass the inner frame''' + self.canvas.configure(scrollregion=self.canvas.bbox("all")) + + def onCanvasConfigure(self, event): + '''Reset the canvas window to encompass inner frame when required''' + canvas_width = event.width + self.canvas.itemconfig(self.canvas_window, width = canvas_width) + + def onMouseWheel(self, event): + # cross platform scroll wheel event + if platform.system() == 'Windows': + self.canvas.yview_scroll(int(-1* (event.delta/120)), "units") + elif platform.system() == 'Darwin': + self.canvas.yview_scroll(int(-1 * event.delta), "units") + else: + if event.num == 4: + self.canvas.yview_scroll( -1, "units" ) + elif event.num == 5: + self.canvas.yview_scroll( 1, "units" ) + + def onEnter(self, event): + # bind wheel events when the cursor enters the control + if platform.system() == 'Linux': + self.canvas.bind_all("", self.onMouseWheel) + self.canvas.bind_all("", self.onMouseWheel) + else: + self.canvas.bind_all("", self.onMouseWheel) + + def onLeave(self, event): + # unbind wheel events when the cursorl leaves the control + if platform.system() == 'Linux': + self.canvas.unbind_all("") + self.canvas.unbind_all("") + else: + self.canvas.unbind_all("") + + + def apply_filters(self): + for column, entry in self.filter_entries.items(): + if column in ['name', 'type']: + self.filter_columns[column] = entry.get() + elif column == 'pep': + participant_names = [participant.strip() for participant in entry.get().split(',')] + self.filter_columns[column] = set(participant_names) + else: # For numerical fields + min_val = entry[0].get() + max_val = entry[1].get() + self.filter_columns[column] = (int(min_val) if min_val else -1, int(max_val) if max_val else -1) + self.apply_callback() + self.destroy() + + if __name__ == '__main__': MasterWindow().mainloop() diff --git a/langs/Deutsch.py b/langs/Deutsch.py index 1db7829..323de34 100644 --- a/langs/Deutsch.py +++ b/langs/Deutsch.py @@ -14,6 +14,7 @@ TITLE_HOME = 'Heim' TITLE_UPLOAD_MESSAGES = 'Nachrichten hochladen' TITLE_SEARCH = 'Suchen' +TITLE_FILTER = 'Filter' TITLE_EXIT = 'Ausfahrt' TITLE_SETTINGS = 'Einstellungen' TITLE_PROFILE = 'Mein Profil' diff --git a/langs/English.py b/langs/English.py index 9d3cc50..8d33bb8 100644 --- a/langs/English.py +++ b/langs/English.py @@ -14,6 +14,7 @@ TITLE_HOME = 'Home' TITLE_UPLOAD_MESSAGES = 'Upload messages' TITLE_SEARCH = 'Search' +TITLE_FILTER = 'Filter' TITLE_EXIT = 'Exit' TITLE_SETTINGS = 'Settings' TITLE_PROFILE = 'My profile' diff --git "a/langs/Espa\303\261ol.py" "b/langs/Espa\303\261ol.py" index 6700c40..abcc06f 100644 --- "a/langs/Espa\303\261ol.py" +++ "b/langs/Espa\303\261ol.py" @@ -14,6 +14,7 @@ TITLE_HOME = 'Inicio' TITLE_UPLOAD_MESSAGES = 'Subir mensajes' TITLE_SEARCH = 'Buscar' +TITLE_FILTER = 'Filtro' TITLE_EXIT = 'Salir' TITLE_SETTINGS = 'Ajustes' TITLE_PROFILE = 'Mi perfil' diff --git a/langs/Francais.py b/langs/Francais.py index 874c814..5577a74 100644 --- a/langs/Francais.py +++ b/langs/Francais.py @@ -14,6 +14,7 @@ TITLE_HOME = 'Accueil' TITLE_UPLOAD_MESSAGES = 'Télécharger les messages' TITLE_SEARCH = 'Recherche' +TITLE_FILTER = 'Filtre' TITLE_EXIT = 'Quitter' TITLE_SETTINGS = 'Paramètres' TITLE_PROFILE = 'Mon profil' diff --git a/langs/Hindi.py b/langs/Hindi.py index 6cdad8f..3140015 100644 --- a/langs/Hindi.py +++ b/langs/Hindi.py @@ -14,6 +14,7 @@ TITLE_HOME = 'होम' TITLE_UPLOAD_MESSAGES = 'संदेश अपलोड करें' TITLE_SEARCH = 'खोजें' +TITLE_FILTER = 'फ़िल्टर' TITLE_EXIT = 'बाहर निकलें' TITLE_SETTINGS = 'सेटिंग्स' TITLE_PROFILE = 'मेरी प्रोफ़ाइल' diff --git a/langs/Marathi.py b/langs/Marathi.py index c857b74..bf9f611 100644 --- a/langs/Marathi.py +++ b/langs/Marathi.py @@ -14,6 +14,7 @@ TITLE_HOME = 'मुख्यपृष्ठ' TITLE_UPLOAD_MESSAGES = 'संदेश अपलोड करा' TITLE_SEARCH = 'शोधा' +TITLE_FILTER = 'फिल्टर' TITLE_EXIT = 'बाहेर पडा' TITLE_SETTINGS = 'सेटिंग्ज' TITLE_PROFILE = 'माझे प्रोफाइल' diff --git a/langs/Nederlands.py b/langs/Nederlands.py index 634445c..c1ddfa8 100644 --- a/langs/Nederlands.py +++ b/langs/Nederlands.py @@ -14,6 +14,7 @@ TITLE_HOME = 'Home' TITLE_UPLOAD_MESSAGES = 'Berichten uploaden' TITLE_SEARCH = 'Zoeken' +TITLE_FILTER = 'Filter' TITLE_EXIT = 'Verlaat' TITLE_SETTINGS = 'Instellingen' TITLE_PROFILE = 'Mijn profiel' diff --git a/langs/Polski.py b/langs/Polski.py index 42c2cba..8d64862 100644 --- a/langs/Polski.py +++ b/langs/Polski.py @@ -14,6 +14,7 @@ TITLE_HOME = 'Strona główna' TITLE_UPLOAD_MESSAGES = 'Załaduj wiadomości' TITLE_SEARCH = 'Szukaj' +TITLE_FILTER = 'Filtr' TITLE_EXIT = 'Wyjście' TITLE_SETTINGS = 'Ustawienia' TITLE_PROFILE = 'Mój profil' diff --git a/langs/Slovensky.py b/langs/Slovensky.py index 6d3c2d0..6836c23 100644 --- a/langs/Slovensky.py +++ b/langs/Slovensky.py @@ -14,6 +14,7 @@ TITLE_HOME = 'Domov' TITLE_UPLOAD_MESSAGES = 'Nahrať správy' TITLE_SEARCH = 'Hľadať' +TITLE_FILTER = 'Filter' TITLE_EXIT = 'Ukončiť' TITLE_SETTINGS = 'Nastavenia' TITLE_PROFILE = 'Môj profil' diff --git a/langs/Tagalog.py b/langs/Tagalog.py index 961a11b..96b48fe 100644 --- a/langs/Tagalog.py +++ b/langs/Tagalog.py @@ -14,6 +14,7 @@ TITLE_HOME = 'Bahay' TITLE_UPLOAD_MESSAGES = 'i-upload ang iyong mga mensahe' TITLE_SEARCH = 'Paghanap' +TITLE_FILTER = 'Salain' TITLE_EXIT = 'Labasan' TITLE_SETTINGS = 'Mga setting' TITLE_PROFILE = 'Aking profile' diff --git "a/langs/\316\225\316\273\316\273\316\267\316\275\316\271\316\272\316\254.py" "b/langs/\316\225\316\273\316\273\316\267\316\275\316\271\316\272\316\254.py" index a4e92da..815afe9 100644 --- "a/langs/\316\225\316\273\316\273\316\267\316\275\316\271\316\272\316\254.py" +++ "b/langs/\316\225\316\273\316\273\316\267\316\275\316\271\316\272\316\254.py" @@ -14,6 +14,7 @@ TITLE_HOME = 'Αρχική' TITLE_UPLOAD_MESSAGES = 'Ανέβασμα μηνυμάτων' TITLE_SEARCH = 'Αναζήτηση' +TITLE_FILTER = 'Φίλτρο' TITLE_EXIT = 'Έξοδος' TITLE_SETTINGS = 'Ρυθμίσεις' TITLE_PROFILE = 'Το προφίλ μου' diff --git "a/langs/\330\247\331\204\330\271\330\261\330\250\331\212\330\251.py" "b/langs/\330\247\331\204\330\271\330\261\330\250\331\212\330\251.py" index 18c7250..f2b1b02 100644 --- "a/langs/\330\247\331\204\330\271\330\261\330\250\331\212\330\251.py" +++ "b/langs/\330\247\331\204\330\271\330\261\330\250\331\212\330\251.py" @@ -14,6 +14,7 @@ TITLE_HOME = 'الصفحة الرئيسية' TITLE_UPLOAD_MESSAGES = 'تحميل الرسائل' TITLE_SEARCH = 'بحث' +TITLE_FILTER = 'فلتر' TITLE_EXIT = 'خروج' TITLE_SETTINGS = 'الإعدادات' TITLE_PROFILE = 'الملف الخاص'