From df8ba1d495c9e9d046fac95787770b374d40b4aa Mon Sep 17 00:00:00 2001 From: Theo Kroening Date: Tue, 16 Apr 2024 11:59:28 -0400 Subject: [PATCH 1/9] Fixed icon-related crashes on Linux --- Main.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/Main.py b/Main.py index 604ebae..a1f9146 100644 --- a/Main.py +++ b/Main.py @@ -8,10 +8,20 @@ from os.path import exists from os import listdir from tkcalendar import DateEntry +from PIL import Image, ImageTk # 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): @@ -287,7 +297,7 @@ def __init__(self, *args, **kwargs): # global window customization self.title('Counter for Messenger') - self.iconbitmap('assets/CFM.ico') + setIcon(self) # frame container setup self.container = tk.Frame(self) @@ -447,7 +457,7 @@ def __init__(self, controller): # profile window customization self.title(self.module.TITLE_PROFILE) - self.iconbitmap('assets/CFM.ico') + setIcon(self) self.focus_set() self.grab_set() @@ -501,7 +511,7 @@ def __init__(self, controller): # settings window customization self.title(self.module.TITLE_SETTINGS) - self.iconbitmap('assets/CFM.ico') + setIcon(self) self.focus_set() self.grab_set() @@ -607,7 +617,7 @@ def __init__(self, controller, chat_total, treeview): # loading window customization self.title(f'{self.module.TITLE_LOADING}...') - self.iconbitmap('assets/CFM.ico') + setIcon(self) self.resizable(False, False) self.focus_set() self.grab_set() @@ -678,7 +688,7 @@ def __init__(self, controller, selection): # statistics window customization self.title(self.module.TITLE_STATISTICS) - self.iconbitmap('assets/CFM.ico') + setIcon(self) self.focus_set() self.grab_set() From 1d600a64a99f3bad9e0cd67b10662c10293d8738 Mon Sep 17 00:00:00 2001 From: Theo Kroening Date: Sun, 21 Apr 2024 23:35:41 -0400 Subject: [PATCH 2/9] Preliminary implementation of new sorting feature - Adds new "Multi-sort" popup that lets you customize the sort. - Also adds a new sorting function that lets you sort based on any arbitrary combination of columns. - Tested on example given in the original issue (sort by chat type then number of messages) and on single column sorts. - Only supports English for now. --- Main.py | 405 ++++++++++++++++++++++++++++++++++++++++++++++- langs/English.py | 1 + 2 files changed, 399 insertions(+), 7 deletions(-) diff --git a/Main.py b/Main.py index a1f9146..8f4e966 100644 --- a/Main.py +++ b/Main.py @@ -9,6 +9,7 @@ from os import listdir from tkcalendar import DateEntry from PIL import Image, ImageTk +from functools import cmp_to_key # safeguard for the treeview automated string conversion problem PREFIX = '<@!PREFIX>' @@ -148,6 +149,31 @@ def __init__(self, parent, controller): self.treeview.bind('', lambda event: self.deselect()) self.treeview.bind('', lambda event: self.show_statistics()) + # Ordered list of columns to sort by + self.sort_columns = [] + + # Store the biases for each column in one place + self.column_biases = { + "name": "stringwise", + "pep": "numberwise", + "type": "stringwise", + "msg": "numberwise", + "call": "numberwise", + "photos": "numberwise", + "gifs": "numberwise", + "videos": "numberwise", + "files": "numberwise", + } + + """ + Store whether each column is reversed. We only need this if we are doing + a multi-sort. + """ + self.columns_reversed = dict() + + # Add the ability to select multiple columns + self.select_multiple_columns = False + # show frame title ttk.Label( self.main, text=f'{self.module.TITLE_NUMBER_OF_MSGS}: ', foreground='#ffffff', background='#232323', @@ -173,6 +199,20 @@ def __init__(self, parent, controller): command=self.search ).pack(side='top', pady=10) + + # Multi-sort button opens the sort-editor UI + ttk.Button( + self.nav, text=self.module.TITLE_MULTI_SORT, padding=5, + command = lambda : + MultiSortPopup( + self.controller, + self.column_biases, + self.columns_reversed, + self.sort_columns, + lambda : self.apply_multi_sort() + ) + ).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, @@ -222,16 +262,27 @@ def upload_data(self): conversations = len(listdir(self.controller.get_directory())) LoadingPopup(self.controller, conversations, self.treeview) - # enable column sorting on treeview - self.treeview.heading('msg', command=lambda col='msg': self.sort_treeview(col, False, 'numberwise')) - self.treeview.heading('name', command=lambda col='name': self.sort_treeview(col, False, 'stringwise')) - self.treeview.heading('type', command=lambda col='type': self.sort_treeview(col, False, 'stringwise')) - self.treeview.heading('call', command=lambda col='call': self.sort_treeview(col, False, 'numberwise')) - self.treeview.heading('photos', command=lambda col='photos': self.sort_treeview(col, False, 'numberwise')) + # bind general purpose handler to column clicks + self.treeview.heading('msg', command=lambda col='msg': self.click_column(col, False, 'numberwise')) + self.treeview.heading('name', command=lambda col='name': self.click_column(col, False, 'stringwise')) + self.treeview.heading('type', command=lambda col='type': self.click_column(col, False, 'stringwise')) + self.treeview.heading('call', command=lambda col='call': self.click_column(col, False, 'numberwise')) + self.treeview.heading('photos', command=lambda col='photos': self.click_column(col, False, 'numberwise')) + except FileNotFoundError: print('>MainPage/upload_data THROWS FileNotFoundError, NOTIFY OP IF UNEXPECTED') - # invoked by pressing the column headers + + def click_column(self, col, order, bias): + """ + Clicking a column (and therefore doing a sort on it) discards multi-sort + info + """ + self.sort_columns.clear() + self.columns_reversed.clear() + + self.sort_treeview(col, order, bias) + def sort_treeview(self, column, order, bias): # Cache the get_children call children = self.treeview.get_children('') @@ -251,6 +302,72 @@ 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 apply_multi_sort(self): + """ + This function sorts the rows based on the ordering stored in + `self.sort_columns`. + + To achieve this, the `compare` function tries to break ties based + each successive column in the ordering, before giving up and declaring + a tie. + """ + def compare(a, b, ordering): + # a and b are *rows* of data (dictionaries) + if ordering == []: + # We have nothing left to break ties on + return 0 + + # We will try to break the tie on this column + column_name = ordering[0] + bias = self.column_biases[column_name] + reverse_multiplier = -1 if self.columns_reversed[column_name] else 1 + + # Retrieve the appropriate column from each row + a_value = a[column_name] + b_value = b[column_name] + + if bias == "stringwise": + if a_value < b_value: return -1 * reverse_multiplier + elif a_value > b_value: return 1 * reverse_multiplier + elif bias == "numberwise": + a_value, b_value = int(a_value), int(b_value) + if a_value < b_value: return -1 * reverse_multiplier + elif a_value > b_value: return 1 * reverse_multiplier + else: + raise Exception(f"Undefined bias '{bias}'") + + """ + If we made it here, then the values are equal when compared + on the current column value. + + We continue with the next column in the ordering. + """ + return compare(a, b, ordering[1:]) + + def compare_wrapper(a, b): + (_, a) = a + (_, b) = b + + return compare(a, b, self.sort_columns) + + # Retrieve all of the rows of the dataset + children = self.treeview.get_children('') + rows = [ + (k, + { + column_name: self.treeview.set(k, column_name) + for column_name in self.column_biases + } + ) + for k in children + ] + + rows.sort(key = cmp_to_key(compare_wrapper)) + + for (idx, (k, _)) in enumerate(rows): + self.treeview.move(k, '', idx) + + # invoked on double left click on any treeview listing def show_statistics(self): try: @@ -748,6 +865,280 @@ def __init__(self, controller, selection): listbox.insert('end', f'{self.module.TITLE_PER_MONTH} - {all_msgs / (sec_since_start / (30 * 86400)):.2f}') listbox.insert('end', f'{self.module.TITLE_PER_YEAR} - {all_msgs / (sec_since_start / (365 * 86400)):.2f}') +class MultiSortPopup(tk.Toplevel): + """ + This class implements the sort-editor popup + """ + def __init__(self, controller, column_biases, columns_reversed, sort_columns, apply_callback): + tk.Toplevel.__init__(self) + self.controller = controller + self.module = self.controller.lang_mdl + + # This popup is bigger to make room for all the additional buttons + set_resolution(self, 600, 600) + + """ + bind sort info so we can mutate it later - it's important we don't break + these aliases + """ + self.column_biases = column_biases + self.columns_reversed = columns_reversed + self.sort_columns = sort_columns + + # We can call this function to apply the sort + self.apply_callback = apply_callback + + # Temporary ordering - we will keep this in sync with a listbox + self.temp_ordering = [] + self.temp_reversed = dict() + + # Profile window customization + self.title("Configure Multi-Sort") + setIcon(self) + self.focus_set() + self.grab_set() + + # Show 'My data' header + ttk.Label( + self, text=f'Multi-Sort', font=('Ariel', 24) + ).pack(side='top', pady=20) + + """ + Listbox Frame + """ + listbox_frame = tk.Frame(self) + listbox_frame.pack() + # Listbox of "available" columns + tk.Label(listbox_frame, text="Available Columns").grid(row=0, column=0) + self.available_listbox = tk.Listbox(listbox_frame) + self.available_listbox.grid(row=1, column=0) + + # Fill the listbox with all the columns + for column_name in self.column_biases: + self.available_listbox.insert("end", column_name) + + # Listbox to configure sort_order (empty to begin with) + tk.Label(listbox_frame, text="Sort Order").grid(row=0, column=1) + self.sort_order_listbox = tk.Listbox(listbox_frame) + self.sort_order_listbox.grid(row=1, column=1) + + # "Add" and "Remove" buttons + tk.Button( + listbox_frame, + text="Add", + command = lambda : self.add_clicked() + ).grid(row=2,column=0) + + tk.Button( + listbox_frame, + text="Remove", + command = lambda : self.remove_clicked() + ).grid(row=3,column=0) + + # "Move up" and "Move down" buttons + tk.Button( + listbox_frame, + text="Move up", + command = lambda : self.move_up_clicked() + ).grid(row=2, column=1) + + tk.Button( + listbox_frame, + text="Move down", + command = lambda : self.move_down_clicked() + ).grid(row=3, column=1) + + # "Reverse" button + tk.Button( + listbox_frame, + text="Reverse", + command = lambda : self.reverse_clicked() + ).grid(row=4,column=0) + + # "Clear" button + tk.Button( + listbox_frame, + text="Clear", + command = lambda : self.clear() + ).grid(row=4,column=1) + + # "Apply" button + tk.Button( + self, + text="Apply", + command = lambda : self.apply() + ).pack() + + def get_entry_text(self, column_name): + """ + Helper function that returns a string representation of `column_name` + for display in the "Sort order" column. + """ + if self.temp_reversed[column_name]: + return f"{column_name} (reversed)" + else: + return column_name + + def add_to_sort(self, column_name): + """ + Add a column to the ordering + """ + # Add to the temporary ordering + self.temp_ordering.append(column_name) + # Add to listbox + self.sort_order_listbox.insert(tk.END, column_name) + # Not reversed by default + self.temp_reversed[column_name] = False + + def remove_from_sort(self, column_name): + """ + Remove a column from the ordering + """ + # Retrieve the index + idx = self.temp_ordering.index(column_name) + + # Delete from the temporary ordering + self.temp_ordering.pop(idx) + + # Delete from the listbox + self.sort_order_listbox.delete(idx) + + # Delete reversed info + del self.temp_reversed[column_name] + + def move_up(self, column_name): + """ + Move a column up in the sort ordering + """ + # Retrieve the index + idx = self.temp_ordering.index(column_name) + new_idx = max(0, idx - 1) + + # Move in the temporary ordering + self.temp_ordering.pop(idx) + self.temp_ordering.insert(new_idx, column_name) + + # Move in the listbox + self.sort_order_listbox.delete(idx) + text = self.get_entry_text(column_name) + self.sort_order_listbox.insert(new_idx, text) + + def move_down(self, column_name): + """ + Move a column down in the sort ordering + """ + # Retrieve the index + idx = self.temp_ordering.index(column_name) + new_idx = min(len(self.temp_ordering) - 1, idx + 1) + + # Move in the temporary ordering + self.temp_ordering.pop(idx) + self.temp_ordering.insert(new_idx, column_name) + + # Move in the listbox + self.sort_order_listbox.delete(idx) + text = self.get_entry_text(column_name) + self.sort_order_listbox.insert(new_idx, text) + + def reverse(self, column_name): + """ + Specify that a column should be sorted in reverse order. + """ + # Reverse in the dictionary + self.temp_reversed[column_name] = not self.temp_reversed[column_name] + + # Retrieve the index + idx = self.temp_ordering.index(column_name) + + # Delete current entry in listbox + self.sort_order_listbox.delete(idx) + + # Rewrite entry + text = self.get_entry_text(column_name) + self.sort_order_listbox.insert(idx, text) + + def clear(self): + """ + Reset the sort ordering column + """ + # Reset state + self.temp_ordering = [] + self.temp_reversed = dict() + + # Clear listbox + self.sort_order_listbox.delete(0, tk.END) + + def apply(self): + """ + Sort according to the ordering the user builds and close this popup. + """ + # Overwrite old state + self.sort_columns.clear() + self.columns_reversed.clear() + + # Write new state + self.sort_columns.extend(self.temp_ordering) + self.columns_reversed.update(self.temp_reversed) + + # Call the callback + self.apply_callback() + + # Close the window + self.destroy() + + def add_clicked(self): + """ + "Add" button clicked + """ + column_name = self.available_listbox.get(tk.ANCHOR) + + if column_name != "" and column_name not in self.temp_ordering: + self.add_to_sort(column_name) + + def remove_clicked(self): + """ + "Remove" button clicked + """ + column_name = self.available_listbox.get(tk.ANCHOR) + + if column_name != "" and column_name in self.temp_ordering: + self.remove_from_sort(column_name) + + def move_up_clicked(self): + """ + "Move up" button clicked + """ + idx = self.sort_order_listbox.curselection() + if len(idx) <= 0: return + (idx,) = idx + + column_name = self.temp_ordering[idx] + + if column_name != "": + self.move_up(column_name) + + def move_down_clicked(self): + """ + "Move down" button clicked + """ + idx = self.sort_order_listbox.curselection() + if len(idx) <= 0: return + (idx,) = idx + + column_name = self.temp_ordering[idx] + + if column_name != "": + self.move_down(column_name) + + def reverse_clicked(self): + """ + "Reverse" button clicked + """ + column_name = self.available_listbox.get(tk.ANCHOR) + + if column_name != "" and column_name in self.temp_ordering: + self.reverse(column_name) + if __name__ == '__main__': MasterWindow().mainloop() diff --git a/langs/English.py b/langs/English.py index 9d3cc50..c6a2376 100644 --- a/langs/English.py +++ b/langs/English.py @@ -44,3 +44,4 @@ TITLE_PER_YEAR = 'Year' TITLE_FROM = 'From' TITLE_TO = 'To' +TITLE_MULTI_SORT = 'Multi-Sort' From 8365e96a2b00b3fec2886345164d91148322533f Mon Sep 17 00:00:00 2001 From: Theo Kroening Date: Mon, 22 Apr 2024 13:41:15 -0400 Subject: [PATCH 3/9] Improved multi-sort UI - Listboxes now use full column titles rather than abbreviations - Listboxes fill the width of the window to accomodate larger titles --- Main.py | 69 ++++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 53 insertions(+), 16 deletions(-) diff --git a/Main.py b/Main.py index 8f4e966..35c3b9b 100644 --- a/Main.py +++ b/Main.py @@ -149,6 +149,20 @@ def __init__(self, parent, controller): self.treeview.bind('', lambda event: self.deselect()) 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 + # Ordered list of columns to sort by self.sort_columns = [] @@ -206,7 +220,8 @@ def __init__(self, parent, controller): command = lambda : MultiSortPopup( self.controller, - self.column_biases, + self.columns[:], # Pass a copy + {**self.column_titles}, # Pass a copy self.columns_reversed, self.sort_columns, lambda : self.apply_multi_sort() @@ -869,19 +884,21 @@ class MultiSortPopup(tk.Toplevel): """ This class implements the sort-editor popup """ - def __init__(self, controller, column_biases, columns_reversed, sort_columns, apply_callback): + def __init__(self, controller, columns, column_titles, columns_reversed, sort_columns, apply_callback): tk.Toplevel.__init__(self) self.controller = controller self.module = self.controller.lang_mdl # This popup is bigger to make room for all the additional buttons - set_resolution(self, 600, 600) + set_resolution(self, 800, 600) + + self.columns = columns + self.column_titles = column_titles """ bind sort info so we can mutate it later - it's important we don't break these aliases """ - self.column_biases = column_biases self.columns_reversed = columns_reversed self.sort_columns = sort_columns @@ -907,20 +924,26 @@ def __init__(self, controller, column_biases, columns_reversed, sort_columns, ap Listbox Frame """ listbox_frame = tk.Frame(self) - listbox_frame.pack() + listbox_frame.pack(fill=tk.X, padx=50) + + # Have columns fill width + listbox_frame.grid_columnconfigure(0, weight=1) + listbox_frame.grid_columnconfigure(1, weight=1) + # Listbox of "available" columns tk.Label(listbox_frame, text="Available Columns").grid(row=0, column=0) self.available_listbox = tk.Listbox(listbox_frame) - self.available_listbox.grid(row=1, column=0) + self.available_listbox.grid(row=1, column=0, sticky='nesw') # Fill the listbox with all the columns - for column_name in self.column_biases: - self.available_listbox.insert("end", column_name) + for column_name in self.columns: + column_title = self.column_titles[column_name] + self.available_listbox.insert(tk.END, column_title) # Listbox to configure sort_order (empty to begin with) tk.Label(listbox_frame, text="Sort Order").grid(row=0, column=1) self.sort_order_listbox = tk.Listbox(listbox_frame) - self.sort_order_listbox.grid(row=1, column=1) + self.sort_order_listbox.grid(row=1, column=1, columnspan=1, sticky='nesw') # "Add" and "Remove" buttons tk.Button( @@ -974,10 +997,12 @@ def get_entry_text(self, column_name): Helper function that returns a string representation of `column_name` for display in the "Sort order" column. """ + text = self.column_titles[column_name] + if self.temp_reversed[column_name]: - return f"{column_name} (reversed)" + return f"{text} (reversed)" else: - return column_name + return text def add_to_sort(self, column_name): """ @@ -985,11 +1010,14 @@ def add_to_sort(self, column_name): """ # Add to the temporary ordering self.temp_ordering.append(column_name) - # Add to listbox - self.sort_order_listbox.insert(tk.END, column_name) + # Not reversed by default self.temp_reversed[column_name] = False + # Add to listbox + text = self.get_entry_text(column_name) + self.sort_order_listbox.insert(tk.END, text) + def remove_from_sort(self, column_name): """ Remove a column from the ordering @@ -1090,7 +1118,10 @@ def add_clicked(self): """ "Add" button clicked """ - column_name = self.available_listbox.get(tk.ANCHOR) + idx = self.available_listbox.curselection() + if len(idx) <= 0: return + (idx,) = idx + column_name = self.columns[idx] if column_name != "" and column_name not in self.temp_ordering: self.add_to_sort(column_name) @@ -1099,7 +1130,10 @@ def remove_clicked(self): """ "Remove" button clicked """ - column_name = self.available_listbox.get(tk.ANCHOR) + idx = self.available_listbox.curselection() + if len(idx) <= 0: return + (idx,) = idx + column_name = self.columns[idx] if column_name != "" and column_name in self.temp_ordering: self.remove_from_sort(column_name) @@ -1134,7 +1168,10 @@ def reverse_clicked(self): """ "Reverse" button clicked """ - column_name = self.available_listbox.get(tk.ANCHOR) + idx = self.available_listbox.curselection() + if len(idx) <= 0: return + (idx,) = idx + column_name = self.columns[idx] if column_name != "" and column_name in self.temp_ordering: self.reverse(column_name) From 81ad43ec460cfe3832d841a314af040fb6470a27 Mon Sep 17 00:00:00 2001 From: Theo Kroening Date: Mon, 22 Apr 2024 13:57:19 -0400 Subject: [PATCH 4/9] Multi-Sort popup restores sort order - Only restores sort order if one is stored in MainPage - Still gets overwritten if the user does a one-column sort --- Main.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/Main.py b/Main.py index 35c3b9b..06ce97d 100644 --- a/Main.py +++ b/Main.py @@ -896,18 +896,23 @@ def __init__(self, controller, columns, column_titles, columns_reversed, sort_co self.column_titles = column_titles """ - bind sort info so we can mutate it later - it's important we don't break + Bind sort info so we can mutate it later - it's important we don't break these aliases """ - self.columns_reversed = columns_reversed self.sort_columns = sort_columns + self.columns_reversed = columns_reversed # We can call this function to apply the sort self.apply_callback = apply_callback - # Temporary ordering - we will keep this in sync with a listbox - self.temp_ordering = [] - self.temp_reversed = dict() + """ + Temporary ordering - we will keep this in sync with a listbox. + + We initialize the ordering with the ordering from the MainPage, if one + exists. + """ + self.temp_ordering = self.sort_columns[:] + self.temp_reversed = {**self.columns_reversed} # Profile window customization self.title("Configure Multi-Sort") @@ -945,6 +950,11 @@ def __init__(self, controller, columns, column_titles, columns_reversed, sort_co self.sort_order_listbox = tk.Listbox(listbox_frame) self.sort_order_listbox.grid(row=1, column=1, columnspan=1, sticky='nesw') + # Fill the listbox with restored columns + for column_name in self.temp_ordering: + text = self.get_entry_text(column_name) + self.sort_order_listbox.insert(tk.END, text) + # "Add" and "Remove" buttons tk.Button( listbox_frame, From fa7b1bb50d17f91fc6ccae7f658bf867cea4191d Mon Sep 17 00:00:00 2001 From: Theo Kroening Date: Mon, 22 Apr 2024 15:42:17 -0400 Subject: [PATCH 5/9] UX improvement for Multi-Sort popup The popup now tries to read selections from both listboxes when the "reverse" and "remove" buttons are clicked --- Main.py | 67 ++++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 54 insertions(+), 13 deletions(-) diff --git a/Main.py b/Main.py index 06ce97d..7a1201e 100644 --- a/Main.py +++ b/Main.py @@ -934,7 +934,7 @@ def __init__(self, controller, columns, column_titles, columns_reversed, sort_co # Have columns fill width listbox_frame.grid_columnconfigure(0, weight=1) listbox_frame.grid_columnconfigure(1, weight=1) - + # Listbox of "available" columns tk.Label(listbox_frame, text="Available Columns").grid(row=0, column=0) self.available_listbox = tk.Listbox(listbox_frame) @@ -1087,14 +1087,14 @@ def reverse(self, column_name): # Retrieve the index idx = self.temp_ordering.index(column_name) - + # Delete current entry in listbox self.sort_order_listbox.delete(idx) # Rewrite entry text = self.get_entry_text(column_name) self.sort_order_listbox.insert(idx, text) - + def clear(self): """ Reset the sort ordering column @@ -1110,7 +1110,7 @@ def apply(self): """ Sort according to the ordering the user builds and close this popup. """ - # Overwrite old state + # Overwrite old state self.sort_columns.clear() self.columns_reversed.clear() @@ -1132,18 +1132,40 @@ def add_clicked(self): if len(idx) <= 0: return (idx,) = idx column_name = self.columns[idx] - + if column_name != "" and column_name not in self.temp_ordering: self.add_to_sort(column_name) def remove_clicked(self): """ "Remove" button clicked + + Try to read a selection from both listboxes, giving priority to the left + one. """ - idx = self.available_listbox.curselection() - if len(idx) <= 0: return - (idx,) = idx - column_name = self.columns[idx] + idx = None + column_name = None + left_idx = self.available_listbox.curselection() + right_idx = self.sort_order_listbox.curselection() + if len(left_idx) > 0: + (idx,) = left_idx + + """ + In this case, `idx` corresponds to an index in the list of available + columns. + """ + column_name = self.columns[idx] + elif len(right_idx) > 0: + (idx,) = right_idx + + """ + In this case, `idx` corresponds to an index in the temporary + ordering. + """ + column_name = self.temp_ordering[idx] + else: + # Give up + return if column_name != "" and column_name in self.temp_ordering: self.remove_from_sort(column_name) @@ -1178,10 +1200,29 @@ def reverse_clicked(self): """ "Reverse" button clicked """ - idx = self.available_listbox.curselection() - if len(idx) <= 0: return - (idx,) = idx - column_name = self.columns[idx] + idx = None + column_name = None + left_idx = self.available_listbox.curselection() + right_idx = self.sort_order_listbox.curselection() + if len(left_idx) > 0: + (idx,) = left_idx + + """ + In this case, `idx` corresponds to an index in the list of available + columns. + """ + column_name = self.columns[idx] + elif len(right_idx) > 0: + (idx,) = right_idx + + """ + In this case, `idx` corresponds to an index in the temporary + ordering. + """ + column_name = self.temp_ordering[idx] + else: + # Give up + return if column_name != "" and column_name in self.temp_ordering: self.reverse(column_name) From a70ffac068974018faedbe2a2e439b2a1ccab7e8 Mon Sep 17 00:00:00 2001 From: Theo Kroening Date: Mon, 22 Apr 2024 16:43:30 -0400 Subject: [PATCH 6/9] Added localization support to Multi-Sort feature - Added translations for all the included languages. These translations were generated using an LLM (Google Gemini). --- Main.py | 28 +++++++++++-------- langs/Deutsch.py | 14 +++++++++- langs/English.py | 10 +++++++ "langs/Espa\303\261ol.py" | 12 ++++++++ langs/Francais.py | 12 ++++++++ langs/Hindi.py | 14 +++++++++- langs/Marathi.py | 12 ++++++++ langs/Nederlands.py | 14 +++++++++- langs/Polski.py | 12 ++++++++ langs/Slovensky.py | 12 ++++++++ langs/Tagalog.py | 12 ++++++++ ...16\267\316\275\316\271\316\272\316\254.py" | 12 ++++++++ ...30\271\330\261\330\250\331\212\330\251.py" | 12 ++++++++ 13 files changed, 162 insertions(+), 14 deletions(-) diff --git a/Main.py b/Main.py index 7a1201e..556ed80 100644 --- a/Main.py +++ b/Main.py @@ -915,14 +915,14 @@ def __init__(self, controller, columns, column_titles, columns_reversed, sort_co self.temp_reversed = {**self.columns_reversed} # Profile window customization - self.title("Configure Multi-Sort") + self.title(self.module.TITLE_CONFIGURE_MULTI_SORT) setIcon(self) self.focus_set() self.grab_set() # Show 'My data' header ttk.Label( - self, text=f'Multi-Sort', font=('Ariel', 24) + self, text=self.module.TITLE_MULTI_SORT, font=('Ariel', 24) ).pack(side='top', pady=20) """ @@ -936,7 +936,10 @@ def __init__(self, controller, columns, column_titles, columns_reversed, sort_co listbox_frame.grid_columnconfigure(1, weight=1) # Listbox of "available" columns - tk.Label(listbox_frame, text="Available Columns").grid(row=0, column=0) + tk.Label( + listbox_frame, + text=self.module.TITLE_AVAILABLE_COLUMNS + ).grid(row=0, column=0) self.available_listbox = tk.Listbox(listbox_frame) self.available_listbox.grid(row=1, column=0, sticky='nesw') @@ -946,7 +949,10 @@ def __init__(self, controller, columns, column_titles, columns_reversed, sort_co self.available_listbox.insert(tk.END, column_title) # Listbox to configure sort_order (empty to begin with) - tk.Label(listbox_frame, text="Sort Order").grid(row=0, column=1) + tk.Label( + listbox_frame, + text=self.module.TITLE_SORT_ORDER + ).grid(row=0, column=1) self.sort_order_listbox = tk.Listbox(listbox_frame) self.sort_order_listbox.grid(row=1, column=1, columnspan=1, sticky='nesw') @@ -958,47 +964,47 @@ def __init__(self, controller, columns, column_titles, columns_reversed, sort_co # "Add" and "Remove" buttons tk.Button( listbox_frame, - text="Add", + text=self.module.TITLE_ADD, command = lambda : self.add_clicked() ).grid(row=2,column=0) tk.Button( listbox_frame, - text="Remove", + text=self.module.TITLE_REMOVE, command = lambda : self.remove_clicked() ).grid(row=3,column=0) # "Move up" and "Move down" buttons tk.Button( listbox_frame, - text="Move up", + text=self.module.TITLE_MOVE_UP, command = lambda : self.move_up_clicked() ).grid(row=2, column=1) tk.Button( listbox_frame, - text="Move down", + text=self.module.TITLE_MOVE_DOWN, command = lambda : self.move_down_clicked() ).grid(row=3, column=1) # "Reverse" button tk.Button( listbox_frame, - text="Reverse", + text=self.module.TITLE_REVERSE, command = lambda : self.reverse_clicked() ).grid(row=4,column=0) # "Clear" button tk.Button( listbox_frame, - text="Clear", + text=self.module.TITLE_CLEAR, command = lambda : self.clear() ).grid(row=4,column=1) # "Apply" button tk.Button( self, - text="Apply", + text=self.module.TITLE_APPLY, command = lambda : self.apply() ).pack() diff --git a/langs/Deutsch.py b/langs/Deutsch.py index 1db7829..399fcc3 100644 --- a/langs/Deutsch.py +++ b/langs/Deutsch.py @@ -43,4 +43,16 @@ TITLE_PER_MONTH = 'Monat' TITLE_PER_YEAR = 'Jahr' TITLE_FROM = 'Von' -TITLE_TO = 'Zu' \ No newline at end of file +TITLE_TO = 'Zu' +# For the multi-sort popup: NEEDS REVIEW +TITLE_MULTI_SORT = 'Mehrfachsortierung' +TITLE_CONFIGURE_MULTI_SORT = 'Mehrfachsortierung: Einstellungen' +TITLE_AVAILABLE_COLUMNS = 'Verfügbare Spalten' +TITLE_SORT_ORDER = 'Sortierreihenfolge' +TITLE_ADD = 'Hinzufügen' +TITLE_REMOVE = 'Entfernen' +TITLE_REVERSE = 'Umkehren' +TITLE_MOVE_UP = 'Nach oben' +TITLE_MOVE_DOWN = 'Nach unten' +TITLE_CLEAR = 'Zurücksetzen' +TITLE_APPLY = 'Anwenden' diff --git a/langs/English.py b/langs/English.py index c6a2376..0997000 100644 --- a/langs/English.py +++ b/langs/English.py @@ -45,3 +45,13 @@ TITLE_FROM = 'From' TITLE_TO = 'To' TITLE_MULTI_SORT = 'Multi-Sort' +TITLE_CONFIGURE_MULTI_SORT = 'Configure Multi-Sort' +TITLE_AVAILABLE_COLUMNS = 'Available Columns' +TITLE_SORT_ORDER = 'Sort Order' +TITLE_ADD = 'Add' +TITLE_REMOVE = 'Remove' +TITLE_REVERSE = 'Reverse' +TITLE_MOVE_UP = 'Move up' +TITLE_MOVE_DOWN = 'Move down' +TITLE_CLEAR = 'Clear' +TITLE_APPLY = 'Apply' diff --git "a/langs/Espa\303\261ol.py" "b/langs/Espa\303\261ol.py" index 6700c40..bdbb4db 100644 --- "a/langs/Espa\303\261ol.py" +++ "b/langs/Espa\303\261ol.py" @@ -44,3 +44,15 @@ TITLE_PER_YEAR = 'Año' TITLE_FROM = 'De' TITLE_TO = 'A' +# For the multi-sort popup: NEEDS REVIEW +TITLE_MULTI_SORT = 'Clasificación múltiple' +TITLE_CONFIGURE_MULTI_SORT = 'Configurar clasificación múltiple' +TITLE_AVAILABLE_COLUMNS = 'Columnas disponibles' +TITLE_SORT_ORDER = 'Orden de clasificación' +TITLE_ADD = 'Agregar' +TITLE_REMOVE = 'Eliminar' +TITLE_REVERSE = 'Retroceder' +TITLE_MOVE_UP = 'Mover fila hacia arriba' +TITLE_MOVE_DOWN = 'Mover fila hacia abajo' +TITLE_CLEAR = 'Limpiar' +TITLE_APPLY = 'Aplicar' diff --git a/langs/Francais.py b/langs/Francais.py index 874c814..41cc9f7 100644 --- a/langs/Francais.py +++ b/langs/Francais.py @@ -44,3 +44,15 @@ TITLE_PER_YEAR = 'An' #R EVIEW NEEDED TITLE_FROM = 'De' TITLE_TO = 'À' +# For the multi-sort popup: NEEDS REVIEW +TITLE_MULTI_SORT = 'Tri multi-critères' +TITLE_CONFIGURE_MULTI_SORT = 'Configurer le tri multi-critères' +TITLE_AVAILABLE_COLUMNS = 'Colonnes disponibles' +TITLE_SORT_ORDER = 'Ordre de tri' +TITLE_ADD = 'Ajouter' +TITLE_REMOVE = 'Supprimer' +TITLE_REVERSE = 'Inverser' +TITLE_MOVE_UP = 'Remonter' +TITLE_MOVE_DOWN = 'Descendre' +TITLE_CLEAR = 'Effacer' +TITLE_APPLY = 'Appliquer' diff --git a/langs/Hindi.py b/langs/Hindi.py index 6cdad8f..abb2e39 100644 --- a/langs/Hindi.py +++ b/langs/Hindi.py @@ -43,4 +43,16 @@ TITLE_PER_MONTH = 'महीने' # REVIEW NEEDED TITLE_PER_YEAR = 'वर्ष' # REVIEW NEEDED TITLE_FROM = 'से' -TITLE_TO = 'तक' \ No newline at end of file +TITLE_TO = 'तक' +# For the multi-sort popup: NEEDS REVIEW +TITLE_MULTI_SORT = 'मल्टी-सॉर्ट' +TITLE_CONFIGURE_MULTI_SORT = 'मल्टी-सॉर्ट को कॉन्फ़िगर करें' +TITLE_AVAILABLE_COLUMNS = 'उपलब्ध कॉलम' +TITLE_SORT_ORDER = 'सॉर्ट क्रम' +TITLE_ADD = 'जोड़ें' +TITLE_REMOVE = 'हटाएं' +TITLE_REVERSE = 'उल्टा करें' +TITLE_MOVE_UP = 'ऊपर ले जाएं' +TITLE_MOVE_DOWN = 'नीचे ले जाएं' +TITLE_CLEAR = 'हटाएं' +TITLE_APPLY = 'लागू करें' diff --git a/langs/Marathi.py b/langs/Marathi.py index c857b74..a362be4 100644 --- a/langs/Marathi.py +++ b/langs/Marathi.py @@ -44,3 +44,15 @@ TITLE_PER_YEAR = 'वर्ष' TITLE_FROM = 'पासून' TITLE_TO = 'पर्यंत' +# For the multi-sort popup: NEEDS REVIEW +TITLE_MULTI_SORT = 'मल्टी-सॉर्ट' +TITLE_CONFIGURE_MULTI_SORT = 'मल्टी-सॉर्ट कॉन्फिगर करा' +TITLE_AVAILABLE_COLUMNS = 'उपलब्ध स्तंभ' +TITLE_SORT_ORDER = 'सॉर्ट क्रम' +TITLE_ADD = 'जोडा' +TITLE_REMOVE = 'काढा' +TITLE_REVERSE = 'उलटे करा' +TITLE_MOVE_UP = 'वर सरका' +TITLE_MOVE_DOWN = 'खाली सरका' +TITLE_CLEAR = 'हटा' +TITLE_APPLY = 'लागू करा' diff --git a/langs/Nederlands.py b/langs/Nederlands.py index 634445c..5b0103b 100644 --- a/langs/Nederlands.py +++ b/langs/Nederlands.py @@ -43,4 +43,16 @@ TITLE_PER_MONTH = 'Maand' TITLE_PER_YEAR = 'Jaar' TITLE_FROM = 'Van' -TITLE_TO = 'Naar' \ No newline at end of file +TITLE_TO = 'Naar' +# For the multi-sort popup: NEEDS REVIEW +TITLE_MULTI_SORT = 'Multi-sorteren' +TITLE_CONFIGURE_MULTI_SORT = 'Multi-sorteren configureren' +TITLE_AVAILABLE_COLUMNS = 'Beschikbare kolommen' +TITLE_SORT_ORDER = 'Sorteervolgorde' +TITLE_ADD = 'Toevoegen' +TITLE_REMOVE = 'Verwijderen' +TITLE_REVERSE = 'Omkeren' +TITLE_MOVE_UP = 'Omhoog verplaatsen' +TITLE_MOVE_DOWN = 'Omlaag verplaatsen' +TITLE_CLEAR = 'Wissen' +TITLE_APPLY = 'Toepassen' diff --git a/langs/Polski.py b/langs/Polski.py index 42c2cba..0f6e9cd 100644 --- a/langs/Polski.py +++ b/langs/Polski.py @@ -45,3 +45,15 @@ TITLE_PER_YEAR = 'Rocznie' TITLE_FROM = 'Od' TITLE_TO = 'Do' +# For the multi-sort popup: NEEDS REVIEW +TITLE_MULTI_SORT = 'Wielokrotne sortowanie' +TITLE_CONFIGURE_MULTI_SORT = 'Konfiguruj wielokrotne sortowanie' +TITLE_AVAILABLE_COLUMNS = 'Dostępne kolumny' +TITLE_SORT_ORDER = 'Kolejność sortowania' +TITLE_ADD = 'Dodaj' +TITLE_REMOVE = 'Usuń' +TITLE_REVERSE = 'Odwróć' +TITLE_MOVE_UP = 'Przesuń w górę' +TITLE_MOVE_DOWN = 'Przesuń w dół' +TITLE_CLEAR = 'Wyczyść' +TITLE_APPLY = 'Zastosuj' diff --git a/langs/Slovensky.py b/langs/Slovensky.py index 6d3c2d0..fac0484 100644 --- a/langs/Slovensky.py +++ b/langs/Slovensky.py @@ -44,3 +44,15 @@ TITLE_PER_YEAR = 'Rok' TITLE_FROM = 'Od' TITLE_TO = 'Do' +# For the multi-sort popup: NEEDS REVIEW +TITLE_MULTI_SORT = 'Viacnásobné zoradenie' +TITLE_CONFIGURE_MULTI_SORT = 'Konfigurácia viacnásobného zoradenia' +TITLE_AVAILABLE_COLUMNS = 'Dostupné stĺpce' +TITLE_SORT_ORDER = 'Poradie zoradenia' +TITLE_ADD = 'Pridať' +TITLE_REMOVE = 'Odstrániť' +TITLE_REVERSE = 'Obrátiť' +TITLE_MOVE_UP = 'Posunúť hore' +TITLE_MOVE_DOWN = 'Posunúť dole' +TITLE_CLEAR = 'Vymazať' +TITLE_APPLY = 'Použiť' diff --git a/langs/Tagalog.py b/langs/Tagalog.py index 961a11b..737b0c3 100644 --- a/langs/Tagalog.py +++ b/langs/Tagalog.py @@ -44,3 +44,15 @@ TITLE_PER_YEAR = 'Bawat taon' TITLE_FROM = 'Mula' TITLE_TO = 'Hanggang' +# For the multi-sort popup: NEEDS REVIEW +TITLE_MULTI_SORT = 'Multi-Uri' +TITLE_CONFIGURE_MULTI_SORT = 'I-configure ang Multi-Uri' +TITLE_AVAILABLE_COLUMNS = 'Magagamit na Mga Hanay' +TITLE_SORT_ORDER = 'Ayusin ang Order' +TITLE_ADD = 'Magdagdag' +TITLE_REMOVE = 'Tanggalin' +TITLE_REVERSE = 'Baligtarin' +TITLE_MOVE_UP = 'Ilipat Pataas' +TITLE_MOVE_DOWN = 'Ilipat Pababa' +TITLE_CLEAR = 'Linisin' +TITLE_APPLY = 'Ilapat' 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..2b433cb 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" @@ -44,3 +44,15 @@ TITLE_PER_YEAR = 'Ετος' # REVIEW NEEDED TITLE_FROM = 'Από' TITLE_TO = 'Προς' +# For the multi-sort popup: NEEDS REVIEW +TITLE_MULTI_SORT = 'Πολλαπλή Διαλογή' +TITLE_CONFIGURE_MULTI_SORT = 'Ρύθμιση Πολλαπλής Διαλογής' +TITLE_AVAILABLE_COLUMNS = 'Διαθέσιμες Στήλες' +TITLE_SORT_ORDER = 'Σειρά Διαλογής' +TITLE_ADD = 'Προσθήκη' +TITLE_REMOVE = 'Αφαίρεση' +TITLE_REVERSE = 'Αντιστροφή' +TITLE_MOVE_UP = 'Μετακίνηση Πάνω' +TITLE_MOVE_DOWN = 'Μετακίνηση Κάτω' +TITLE_CLEAR = 'Εκκαθάριση' +TITLE_APPLY = 'Εφαρμογή' 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..c505297 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" @@ -44,3 +44,15 @@ TITLE_PER_YEAR = 'السنة' TITLE_FROM = 'من' TITLE_TO = 'إلى' +# For the multi-sort popup: NEEDS REVIEW +TITLE_MULTI_SORT = 'فرز متعدد' +TITLE_CONFIGURE_MULTI_SORT = 'تكوين الفرز المتعدد' +TITLE_AVAILABLE_COLUMNS = 'الأعمدة المتاحة' +TITLE_SORT_ORDER = 'ترتيب الفرز' +TITLE_ADD = 'إضافة' +TITLE_REMOVE = 'إزالة' +TITLE_REVERSE = 'عكس' +TITLE_MOVE_UP = 'تحريك لأعلى' +TITLE_MOVE_DOWN = 'تحريك لأسفل' +TITLE_CLEAR = 'مسح' +TITLE_APPLY = 'تطبيق' From fd620ce007d83ae4b4c74e18c21e6995f4972063 Mon Sep 17 00:00:00 2001 From: Theo Kroening Date: Mon, 22 Apr 2024 16:47:04 -0400 Subject: [PATCH 7/9] Updated setIcon to match codebase style --- Main.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Main.py b/Main.py index 556ed80..99d88b2 100644 --- a/Main.py +++ b/Main.py @@ -14,7 +14,7 @@ # safeguard for the treeview automated string conversion problem PREFIX = '<@!PREFIX>' -def setIcon(object): +def set_icon(object): """ This function sets the icon of a tkinter window (passed in as `object`) to the CFM icon. @@ -429,7 +429,7 @@ def __init__(self, *args, **kwargs): # global window customization self.title('Counter for Messenger') - setIcon(self) + set_icon(self) # frame container setup self.container = tk.Frame(self) @@ -589,7 +589,7 @@ def __init__(self, controller): # profile window customization self.title(self.module.TITLE_PROFILE) - setIcon(self) + set_icon(self) self.focus_set() self.grab_set() @@ -643,7 +643,7 @@ def __init__(self, controller): # settings window customization self.title(self.module.TITLE_SETTINGS) - setIcon(self) + set_icon(self) self.focus_set() self.grab_set() @@ -749,7 +749,7 @@ def __init__(self, controller, chat_total, treeview): # loading window customization self.title(f'{self.module.TITLE_LOADING}...') - setIcon(self) + set_icon(self) self.resizable(False, False) self.focus_set() self.grab_set() @@ -820,7 +820,7 @@ def __init__(self, controller, selection): # statistics window customization self.title(self.module.TITLE_STATISTICS) - setIcon(self) + set_icon(self) self.focus_set() self.grab_set() @@ -914,9 +914,9 @@ def __init__(self, controller, columns, column_titles, columns_reversed, sort_co self.temp_ordering = self.sort_columns[:] self.temp_reversed = {**self.columns_reversed} - # Profile window customization + # Window customization self.title(self.module.TITLE_CONFIGURE_MULTI_SORT) - setIcon(self) + set_icon(self) self.focus_set() self.grab_set() From ffd5075ab7b42ea0d6e19e8a403335d270db2511 Mon Sep 17 00:00:00 2001 From: Theo Kroening Date: Mon, 22 Apr 2024 18:34:26 -0400 Subject: [PATCH 8/9] Removed unused flag --- Main.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/Main.py b/Main.py index 99d88b2..f8b4c84 100644 --- a/Main.py +++ b/Main.py @@ -185,9 +185,6 @@ def __init__(self, parent, controller): """ self.columns_reversed = dict() - # Add the ability to select multiple columns - self.select_multiple_columns = False - # show frame title ttk.Label( self.main, text=f'{self.module.TITLE_NUMBER_OF_MSGS}: ', foreground='#ffffff', background='#232323', From c48725e930958f9a6a63b188073e31cc1d2b643f Mon Sep 17 00:00:00 2001 From: Theo Kroening Date: Mon, 22 Apr 2024 18:39:53 -0400 Subject: [PATCH 9/9] Fixed erroneous comments --- Main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Main.py b/Main.py index f8b4c84..0063a9c 100644 --- a/Main.py +++ b/Main.py @@ -917,7 +917,7 @@ def __init__(self, controller, columns, column_titles, columns_reversed, sort_co self.focus_set() self.grab_set() - # Show 'My data' header + # Show header ttk.Label( self, text=self.module.TITLE_MULTI_SORT, font=('Ariel', 24) ).pack(side='top', pady=20) @@ -1008,7 +1008,7 @@ def __init__(self, controller, columns, column_titles, columns_reversed, sort_co def get_entry_text(self, column_name): """ Helper function that returns a string representation of `column_name` - for display in the "Sort order" column. + for display in the listboxes. """ text = self.column_titles[column_name]