def getPermutation(self, n: int, k: int) -> str: factorial = [1, 1] # list where each element is factorial of index # figure out what number factorial is just above OR EQUAL TO k b = 1 while factorial[b] < k: b += 1 factorial.append(factorial[b - 1] * b) answer = ''.join([str(i) for i in range(1, n - b + 1)]) remaining = OrderedSet(i for i in range(n - b + 1, n + 1)) while b > 0: # figure out which of the b digits to move here next_digit_indexindex = k // factorial[b - 1] oneless = not k % factorial[b - 1] if oneless: next_digit_indexindex -= 1 # do the swap, keeping the tail in increasing order digit = remaining[next_digit_indexindex] remaining.remove(digit) answer += str(digit) # update k because we have already calculated part of the permutation k -= next_digit_indexindex * (factorial[b - 1]) # on to the next digit b -= 1 return answer
class Node: def __init__(self, x: int, y: int, width: int): self.x = x self.y = y self.width = width self.__neighbors = OrderedSet() self.__conn_ins = [] self.__edge_cost = {} def add_edge(self, node: "Node", delay: int = 0, force_connect: bool = False): if not force_connect: assert self.width == node.width if node not in self.__neighbors: self.__neighbors.add(node) node.__conn_ins.append(self) self.__edge_cost[node] = delay def remove_edge(self, node: "Node"): if node in self.__neighbors: self.__edge_cost.pop(node) self.__neighbors.remove(node) # remove the incoming connections as well node.__conn_ins.remove(self) def get_edge_cost(self, node: "Node") -> int: if node not in self.__edge_cost: return MAX_DEFAULT_DELAY else: return self.__edge_cost[node] def get_conn_in(self) -> List["Node"]: return self.__conn_ins def __iter__(self) -> Iterator["Node"]: return iter(self.__neighbors) def __len__(self): return len(self.__neighbors) @abstractmethod def __repr__(self): pass @abstractmethod def node_str(self): pass def clear(self): self.__neighbors.clear() self.__edge_cost.clear() self.__conn_ins.clear() def __contains__(self, item): return item in self.__neighbors def __hash__(self): return hash(self.width) ^ hash(self.x) ^ hash(self.y)
class Routes: def __init__(self): self.routes = {} self.route_number = 0 self.route_list = OrderedSet() self.route = '' @property def _get_route_list(self): return self.route_list @property def _get_routes(self): return self.routes def _add(self, point): self.route_list.add(point) self.route = '-'.join(self.route_list) def _remove(self, point): self.route_list.remove(point) def _set_route(self): def _route_cost(_set, cost=0): _set = list(_set) for i in range(len(_set) - 1): if any(_set[i + 1] == k for k in nodes[_set[i]].keys()): cost += nodes[_set[i]][_set[i + 1]] return cost if len(self.route) > 1: result = self.route if result not in self.routes.values(): self.route_number += 1 cost = _route_cost(self.route_list) self.routes[self.route_number] = result, cost self.route_list = self.route_list[:-1] self.route = '' @staticmethod def cheapest_route(routes): cost = tuple() if routes: values = routes.values() value_iter = iter(values) cost = next(value_iter) c = cost[1] for i in routes.keys(): if c > routes[i][1]: c = routes[i][1] cost[0] = routes.get(i)[0] return f'cheapest route is: {cost}' return f'There is not routes...' def __str__(self): return str(self.routes)
def test_remove(): set1 = OrderedSet('abracadabra') set1.remove('a') set1.remove('b') assert set1 == OrderedSet('rcd') assert set1[0] == 'r' assert set1[1] == 'c' assert set1[2] == 'd' assert set1.index('r') == 0 assert set1.index('c') == 1 assert set1.index('d') == 2 assert 'a' not in set1 assert 'b' not in set1 assert 'r' in set1 # Make sure we can .discard() something that's already gone, plus # something that was never there set1.discard('a') set1.discard('a')
def test_remove_error(): # If we .remove() an element that's not there, we get a KeyError set1 = OrderedSet('abracadabra') with pytest.raises(KeyError): set1.remove('z')
from ordered_set import OrderedSet from ..utils import SimpleEnumMeta class Color(metaclass=SimpleEnumMeta): BLACK = 'BLACK' BLUE = 'BLUE' RED = 'RED' YELLOW = 'YELLOW' ALL = 'ALL' all_colors = OrderedSet(Color) all_colors.remove(Color.ALL) class ColorSet(set): '''Set that contains values of the Color Enum.Color. Convert string to the proper color on addition or update if necessary. ''' def __init__(self, colors=None): super() if colors is None: colors = [] self.update(colors)
class CategorizedListbox(Frame): #AKA CLB def __init__(self, parent, listview): Frame.__init__(self, parent) self.root = parent #creates the list that will house all controlled ModlistListboxes self.modlists = [] self.selected_modlists = OrderedSet([]) self.listview = listview self.current_index = 0 self.grid_columnconfigure(0, weight=1) #checks for whether a pre-made CLB was input if len(self.modlists) <= 0: #Creates the default category 'Mods' when a new CLB is created self.insert(0, 'Mods') else: #Populates CLB with modlists given self.modlists = modlists def split(self, name, modlist_index, first, Last=None): '''Creates a new mod listbox with the mods of its original mod listbox based on the given indices, or an empty mod listbox''' modlist = self.modlists[modlist_index] if Last is None: pass def load(self, named_modlists): for i in range(len(self.modlists)): self.delete(0, force_delete=True) for named_modlist in named_modlists: self.insert(END, named_modlist[0], named_modlist[1]) def get_mod_count(self, colors): n = 0 for modlist in self.modlists: for mod in modlist.modlabel_list: if mod.color not in colors: n += 1 ## n += len(modlist.modlabel_list) return 'Total Modcount: ' + str(n) def insert(self, index, name, modlist_info=None, is_collapsed=False): '''Create and insert a new modlist at the given modlist index''' modlist = ModlistListbox(self, self.listview, name) #if modlist info given, populate modlistbox with mods if modlist_info is not None and modlist_info != []: for i in range(len(modlist_info)): modlist.insert(i, modlist_info[i]) #collapse mods if necessary if is_collapsed: modlist.force_collapse() #check last index values and set index accordingly if index == END or index >= len(self.modlists): index = len(self.modlists) #insert modlist into modlists list self.modlists.insert(index, modlist) #move modlists down if after inserted modlist if len(self.modlists) > index: for x in range(index, len(self.modlists)): self.modlists[x].grid(row=x, column=0, sticky='nsew') else: modlist.grid(row=len(self.mod_list), column=0, sticky='nsew') #Set modlist name label size self.update() def insert_input(self, index): if index == END: index = len(self.modlists) name = askstring('New Category at Index ' + str(index + 1), 'Name of new category:') if name is not None and name != '': self.insert(index, name) def merge_up(self, index): '''Merge the modlist at the given index with the modlist above it''' msgBox = messagebox.askquestion('Merge Categories Confirmation', 'Merge "' + self.modlists[index].name + '" into "' + self.modlists[index - 1].name + '"?', icon='warning') if msgBox == 'yes': #populate list of mod info lists to add to and from, then get name l1 = self.modlists[index - 1].get_all_info() l2 = self.modlists[index].get_all_info() l_name = self.modlists[index - 1].get_name() #insert new merged modlist self.insert(index - 1, l_name, l1 + l2) #delete both previous modlists for x in range(2): self.delete(index) def merge_down(self, index): '''Merge the modlist at the given index with the modlist below it''' msgBox = messagebox.askquestion('Merge Categories Confirmation', 'Merge "' + self.modlists[index].name + '" into "' + self.modlists[index + 1].name + '"?', icon='warning') if msgBox == 'yes': #populate list of mod info lists to add to and from, then get name l1 = self.modlists[index + 1].get_all_info() l2 = self.modlists[index].get_all_info() l_name = self.modlists[index + 1].get_name() #insert new merged modlist self.insert(index, l_name, l1 + l2) #delete both previous modlists for x in range(2): self.delete(index + 1) def delete_selected(self): '''delete all selected modlists''' if len(self.selected_modlists) == len(self.modlists): messagebox.showinfo('Selection Size Too Large', 'You cannot delete all the categories in the ' 'list. There must always be at least one.', icon='warning') elif len(self.selected_modlists) > 0: msgBox = messagebox.askquestion('Removing Selected Categories', 'Remove all selected categories ' 'and their contents?', icon='warning') if msgBox == 'yes': for x in range(len(self.selected_modlists)): self.delete(self.modlists.index(self.selected_modlists[0])) def delete_confirm(self, index): '''Add a confirmation to delete commands''' msgBox = messagebox.askquestion('Removing Category', 'Remove the "' + self.modlists[index].name + '" Category and its contents?', icon='warning') if msgBox == 'yes': self.delete(index) def delete(self, index, force_delete=False): '''Delete a modlist at the given index''' if not force_delete and len(self.modlists) == 1: messagebox.showinfo( 'Prohibited Action', 'You must always have at least one category in the list.') else: if index == END: index = len(self.mod_list) if index < len(self.modlists) - 1: for x in range(index, len(self.modlists) - 1): self.modlists[x + 1].grid(row=x, column=0, sticky='nsew') if self.modlists[index] in self.selected_modlists: self.selected_modlists.remove(self.modlists[index]) self.modlists[index].grid_forget() self.modlists[index].destroy() del self.modlists[index] def delete_mod(self, modlist_index, mod_index): '''Delete a mod at the given indices''' mod = self.modlists[modlist_index].modlabel_list[mod_index] msgBox = messagebox.askquestion('Removing Mod', 'Remove "{}"?'.format( mod.get_info()[1]), icon='warning') if msgBox == 'yes': self.modlists[modlist_index].delete(mod_index) def delete_selected_mod(self, event=None): selection_exists = False for modlist in self.modlists: if len(modlist.selected_modlabel_list) > 0: selection_exists = True if selection_exists: msgBox = messagebox.askquestion( 'Removing Selected', 'Remove selected mods from the list?', icon='warning') if msgBox == 'yes': for modlist in self.modlists: modlist.delete_selected() def delete_all_mods(self): msgBox = messagebox.askquestion('Removing All', 'Remove all mods from the list?', icon='warning') if msgBox == 'yes': for modlist in self.modlists: modlist.delete_all() def delete_all_cat(self, modlist_index): '''Delete all mods in a category at the given index''' modlist = self.modlists[modlist_index] msgBox = messagebox.askquestion('Removing All', 'Remove all mods from the "' + modlist.name + '" Category?', icon='warning') if msgBox == 'yes': modlist.delete_all() def collapse_all(self): '''Collapses all mod listboxes''' for mod in self.modlists: if not mod.is_collapsed: mod.force_collapse() def expand_all(self): for mod in self.modlists: if mod.is_collapsed: mod.force_expand() def get_info(self): '''Gets a list of lists of mods throughout ALL modlists''' list = [] for modlist in self.modlists: list.append(modlist.get_all_info()) return list def get_all_info(self): '''Gets a list of lists of all modlists''' return self.modlists def rename(self, index): '''Rename a category at the given index by remaking the category''' name = askstring('Rename Category at Index ' + str(index + 1), 'New name of category:') if name is not None and name != '': data = self.modlists[index].get_all_info() is_collapsed = self.modlists[index].is_collapsed self.delete(index, force_delete=True) self.insert(index, name, data) self.modlists[index].forceSelectTop() if is_collapsed: self.modlists[index].force_collapse() #====passed or modified modlist functions==== def onShiftClickEvent(self, event): if len(self.selected_modlists) > 0: #set original index to start multi-selection from origin = self.modlists.index(self.selected_modlists[-1]) for x in range(len(self.modlists)): #checks every modlist for a valid multi-selection activation if self.modlists[x].is_top_entered: #checks whether the index of the target modlists is above #or below origin, then multi-selects accordingly if (x - origin) > 0: for y in range(origin, x + 1): self.selected_modlists.append(self.modlists[y]) self.modlists[y].forceSelectTop() elif (x - origin) < 0: for y in range(x, origin): self.selected_modlists.append(self.modlists[y]) self.modlists[y].forceSelectTop() else: for modlist in self.modlists: modlist.onShiftClickEvent(event) def dragSelection(self, event): '''Moves selected mods depending on mouse movement, and moves mods into and out of categories they are moved into and out of''' for modlist in self.modlists: modlist.dragSelection(event) def moveInto(self, direction, modlist): '''Depending on the direction, move the selected mods from the modlist into the modlist below or above it''' modlist_index = self.modlists.index(modlist) if direction == -1 and modlist_index != 0: #Move up for mod in sorted(modlist.selected_modlabel_list, key=lambda x: x.get_index()): self.modlists[modlist_index - 1].insert(END, mod.get_info()) #messy code to make the mod in the new category selected new_upper_mod = self.modlists[modlist_index - 1].modlabel_list[-1] new_upper_mod.force_select() self.modlists[modlist_index - 1].selected_modlabel_list.append(new_upper_mod) selected_list_len = len(modlist.selected_modlabel_list) for i in range(selected_list_len): modlist.delete(0) modlist.selected_modlabel_list.clear() self.modlists[modlist_index - 1].force_expand() elif direction == 1 and modlist_index != len(self.modlists) - 1: #Move down for mod in sorted(modlist.selected_modlabel_list, key=lambda x: x.get_index(), reverse=True): self.modlists[modlist_index + 1].insert(0, mod.get_info()) #messy code to make the mod in the new category selected new_lower_mod = self.modlists[modlist_index + 1].modlabel_list[0] new_lower_mod.force_select() self.modlists[modlist_index + 1].selected_modlabel_list.append(new_lower_mod) selected_list_len = len(modlist.selected_modlabel_list) for i in range(selected_list_len): modlist.delete(END) modlist.selected_modlabel_list.clear() self.modlists[modlist_index + 1].force_expand() def moveSelectionUp(self, event=None): focused_widget = self.master.master.focus_get() if event is not None and type(focused_widget) in [ Entry, Text ] and focused_widget.cget('state') == 'normal': return else: top_selected = False if len(self.selected_modlists) > 0: for modlist in self.modlists: if modlist.is_top_selected: top_selected = True if top_selected: sorted_selected_modlists = sorted( self.selected_modlists, key=lambda x: self.modlists.index(x)) if sorted_selected_modlists[-1] == self.modlists[0]: return for modlist in sorted_selected_modlists: modlist_index = self.modlists.index(modlist) list_to_move = self.modlists[modlist_index - 1].get_all_info() list_to_move_name = self.modlists[modlist_index - 1].get_name() list_to_move_is_collapsed = self.modlists[modlist_index - 1].is_collapsed self.delete(modlist_index - 1) self.insert(modlist_index, list_to_move_name, list_to_move, list_to_move_is_collapsed) #Collapse the category moved if it was collapsed if list_to_move_is_collapsed: self.modlists[modlist_index].force_collapse() else: for modlist in self.modlists: n = 0 n = modlist.moveSelectionUp() if n == -1: self.moveInto(n, modlist) def moveSelectionDown(self, event=None): focused_widget = self.master.master.focus_get() if event is not None and type(focused_widget) in [ Entry, Text ] and focused_widget.cget('state') == 'normal': return else: top_selected = False if len(self.selected_modlists) > 0: for modlist in self.modlists: if modlist.is_top_selected: top_selected = True if top_selected: sorted_selected_modlists = sorted( self.selected_modlists, key=lambda x: self.modlists.index(x)) if sorted_selected_modlists[-1] == self.modlists[-1]: return for modlist in sorted_selected_modlists: modlist_index = self.modlists.index(modlist) list_to_move = self.modlists[modlist_index + 1].get_all_info() list_to_move_name = self.modlists[modlist_index + 1].get_name() list_to_move_is_collapsed = self.modlists[modlist_index + 1].is_collapsed self.delete(self.modlists.index(modlist) + 1) self.insert(modlist_index, list_to_move_name, list_to_move, list_to_move_is_collapsed) #Collapse the category moved if it was collapsed if list_to_move_is_collapsed: self.modlists[modlist_index].force_collapse() else: for modlist in self.modlists: n = 0 n = modlist.moveSelectionDown() if n == 1: self.moveInto(n, modlist) return def onClickEvent(self, event): '''When the player clicks, control whether categories should be selected''' deselect_others = True #if clicked mod is already part of selection, prevents the deselection of other mods for x in range(len(self.modlists)): if self.modlists[x].is_top_entered and self.modlists[ x].is_top_selected: deselect_others = False if deselect_others: for x in range(len(self.modlists)): #Controls the selection of category names modlist = self.modlists[x] modlist.selectTop() if modlist.is_top_selected and modlist not in self.selected_modlists: self.current_index = x self.selected_modlists.append(modlist) elif not modlist.is_top_selected and modlist in self.selected_modlists: self.selected_modlists.remove(modlist) for modlist in self.modlists: modlist.onClickEvent(event) def selectAll(self): for modlist in self.modlists: modlist.forceDeselectTop() modlist.selectAll() self.selected_modlists.clear() def insert_mod(self, modlist_index, mod_index): self.modlists[modlist_index].insertInput(mod_index) def insert_custom_mod(self, modlist_index, mod_index): self.modlists[modlist_index].insertCustomInput(mod_index) def batch_insert_mod(self, modlist_index, mod_index): l = [] LinkGrabber(self, l, nexus=True) if len(l) == 1 and l[0] == False: messagebox.showinfo( 'No Valid Data Found', 'Either none of the ' 'links provided were valid Nexus mod links, ' 'or the Nexus web server is currently unava' 'ilable.') else: for info in reversed(l): self.modlists[modlist_index].insert(mod_index, info) def move_mod_to(self, modlist_index, target_modlist): modlist = self.modlists[modlist_index] for mod in sorted(modlist.selected_modlabel_list, key=lambda x: x.get_index()): target_modlist.insert(END, mod.get_info()) modlist.delete_selected() def rightClickMenu(self, event, rc_menu): ## #Select proper categories ## for i in self.modlists: ## i.selectTop() self.onClickEvent(event) #Initialize submenus colors_menu = Menu(self.master.master, tearoff=0) remove_menu = Menu(self.master.master, tearoff=0) merge_menu = Menu(self.master.master, tearoff=0) select_menu = Menu(self.master.master, tearoff=0) links_menu = Menu(self.master.master, tearoff=0) move_menu = Menu(self.master.master, tearoff=0) #Get clicked indices and modlist modlist_index = self.dynamic_nearest() mod_index = self._get_clicked_mod_index(modlist_index) modlist = self.modlists[modlist_index] #General modlist commands rc_menu.add_command( label='Insert Nexus Mod Here...', command=lambda: self.insert_mod(modlist_index, mod_index)) rc_menu.add_command( label='Insert Multiple Nexus Mods Here...', command=lambda: self.batch_insert_mod(modlist_index, mod_index)) rc_menu.add_command( label='Insert Non-Nexus Mod Here...', command=lambda: self.insert_custom_mod(modlist_index, mod_index)) y = self._get_clicked_cat_index(modlist_index) rc_menu.add_command(label='Insert Category Here...', command=lambda: self.insert_input(y)) rc_menu.add_command(label='Insert Category At End...', command=lambda: self.insert_input(END)) #Move options rc_menu.add_separator() rc_menu.add_cascade(label='Move Selected Mods To...', menu=move_menu) if len(modlist.modlabel_list) > 0 and \ len(modlist.selected_modlabel_list) > 0: for ml in self.modlists: move_menu.add_command(label=ml.name, command=lambda ml=ml: self.move_mod_to( \ modlist_index,ml)) if ml == modlist: move_menu.entryconfig(ml.name, state='disabled') #Color options if len(modlist.modlabel_list) > 0: rc_menu.add_separator() rc_menu.add_cascade(label="Change Selected Mods' Color To...", menu=colors_menu) colors_menu.add_command(label='Default', command=lambda: \ self.update_selected_colors('#383838')) colors_menu.add_command(label='Red', command=lambda: \ self.update_selected_colors('red')) colors_menu.add_command(label='Blue', command=lambda: \ self.update_selected_colors('blue')) colors_menu.add_command(label='Green', command=lambda: \ self.update_selected_colors('green')) colors_menu.add_command(label='Yellow', command=lambda: \ self.update_selected_colors('yellow')) rc_menu.add_separator() #incompatibilities commands rc_menu.add_command(label='Manage Incompatibilities...', command=lambda: \ self.manage_incomp(modlist_index, mod_index)) if len(self.modlists[modlist_index].modlabel_list[mod_index]. conflicts) > 0: rc_menu.add_command(label='View Conflicts', command=lambda: self.view_conflicts( \ modlist_index, mod_index)) rc_menu.add_separator() rc_menu.add_command(label='Rename Category', command=lambda: self.rename(y)) #Link options rc_menu.add_separator() rc_menu.add_command( label='Copy Mod Link', command=lambda: self.copyURL(modlist_index, mod_index)) rc_menu.add_cascade(label='Open Links...', menu=links_menu) links_menu.add_command(label='Open Selected Mod Links', command=self.open_selected) links_menu.add_command(label='Open All Mod Links in Category Here', command=lambda x=modlist_index: self.openAll(x)) #Selection options rc_menu.add_separator() rc_menu.add_cascade(label='Select...', menu=select_menu) select_menu.add_command( label='Select Here', command=lambda: modlist.rightClickSelect(mod_index)) select_menu.add_command(label='Select All Mods in Category Here', command=modlist.selectAll) select_menu.add_command(label='Select All Mods', command=self.selectAll) #Merge options rc_menu.add_separator() rc_menu.add_cascade(label='Merge Category...', menu=merge_menu) merge_menu.add_command(label='Merge Category Here Into Upper', command=lambda: self.merge_up(modlist_index)) merge_menu.add_command(label='Merge Category Here Into Lower', command=lambda: self.merge_down(modlist_index)) if modlist_index == 0: merge_menu.entryconfig('Merge Category Here Into Upper', state='disabled') if modlist_index == len(self.modlists) - 1: merge_menu.entryconfig('Merge Category Here Into Lower', state='disabled') #Removal options rc_menu.add_separator() rc_menu.add_cascade(label='Remove...', menu=remove_menu) remove_menu.add_command( label='Remove Mod Here', command=lambda: self.delete_mod(modlist_index, mod_index)) remove_menu.add_command(label='Remove Selected Mods', command=self.delete_selected_mod) remove_menu.add_command( label='Remove All In Category', command=lambda: self.delete_all_cat(modlist_index)) remove_menu.add_command( label='Remove Category Here', command=lambda: self.delete_confirm(modlist_index)) remove_menu.add_command(label='Remove Selected Categories', command=lambda: self.delete_selected()) remove_menu.add_command(label='Remove All Mods', command=self.delete_all_mods) #Disables the appropriate menu options if len(modlist.modlabel_list) == 0: remove_menu.entryconfig('Remove Mod Here', state='disabled') remove_menu.entryconfig('Remove All In Category', state='disabled') select_menu.entryconfig('Select Here', state='disabled') select_menu.entryconfig('Select All Mods in Category Here', state='disabled') links_menu.entryconfig('Open All Mod Links in Category Here', state='disabled') if len(self.selected_modlists) == 0: remove_menu.entryconfig('Remove Selected Categories', state='disabled') #Selects and deselects appropriate mods and categories i = 0 for modlist in self.modlists: i += len(modlist.selected_modlabel_list) ## modlist.onClickEvent(event) modlist.rightClickMenu(event, rc_menu) if i == 0: links_menu.entryconfig('Open Selected Mod Links', state='disabled') def view_conflicts(self, modlist_index, mod_index): conflicts = self.modlists[modlist_index].modlabel_list[ mod_index].conflicts ConflictListbox(self, conflicts) def copyURL(self, modlist_index, mod_index): self.master.master.clipboard_clear() self.master.master.clipboard_append( self.modlists[modlist_index].modlabel_list[mod_index].get_info() [0]) def openAll(self, modlist_index): msgBox = messagebox.askquestion('Opening All Mod Links', 'Open all mod links in the "' + self.modlists[modlist_index].name + '" category in your default browser?', icon='warning') if msgBox == 'yes': self.modlists[modlist_index].open_all_links() def open_selected(self): for modlist in self.modlists: modlist.open_selected_links() def update_color(self, modlist_index, mod_index, color, state='normal'): '''Update a single mod's label color''' self.modlists[modlist_index].modlabel_list[ \ mod_index].update_color(color,state) def update_selected_colors(self, color, state='normal'): for modlist in self.modlists: for mod in modlist.selected_modlabel_list: mod.update_color(color, state) def manage_incomp(self, modlist_index, mod_index): l = self.modlists[modlist_index].modlabel_list[ mod_index].incompatibilities IncompatibilityManager(self, l) def _get_clicked_mod_index(self, modlist_index): '''return updated index if mouse is below the last mod in a given list''' modlist = self.modlists[modlist_index] mod_index = modlist.nearest() if modlist.listview: height = modlist.listview_height else: height = modlist.defaultview_height mouse_y = modlist.mlb_frame.winfo_pointery( ) - modlist.mlb_frame.winfo_rooty() if len(modlist.modlabel_list) > 1 and (height * mod_index + height) < mouse_y: return mod_index + 1 else: return mod_index def _get_clicked_cat_index(self, modlist_index): '''return updated index if mouse is below the last category''' modlist = self.modlists[modlist_index] height = modlist.winfo_height() mouse_y = self.winfo_pointery() - self.winfo_rooty() if (modlist.winfo_y() + modlist.winfo_height()) < mouse_y: return modlist_index + 1 else: return modlist_index def onDoubleClickEvent(self, event): for modlist in self.modlists: modlist.onDoubleClickEvent(event) def toggle_view(self): for modlist in self.modlists: modlist.toggle_view() def dynamic_nearest(self): '''get index of mod listbox nearest to the mouse y position. designed to work with widgets of variable sizes''' index = 0 current_nearest_index = 0 #get the absolute position of the mouse in relation to the ModlistListbox position mouse_y = self.winfo_pointery() - self.winfo_rooty() if len(self.modlists) > 1: #initialize y_maps, a list of 2-lengthed lists that store the #start and end y values of each modlist y_maps = [] for i in range(len(self.modlists)): #populate heights modlist = self.modlists[i] #Set y-extending values if i == 0: base = 0 else: base = y_maps[i - 1][1] if modlist.listview: mod_height = modlist.listview_height * len( modlist.modlabel_list) else: mod_height = modlist.defaultview_height * len( modlist.modlabel_list) #set start and end values if modlist.is_collapsed: y_maps.append([base, base + modlist.name_height]) else: y_maps.append( [base, base + modlist.name_height + mod_height]) for i in range(len(y_maps)): #find the index within the y mappings if y_maps[i][0] <= mouse_y < y_maps[i][1]: index = i return index
class VcpStorageStorable(metaclass=ABCMeta): """ Methods that modify the instance. This list is used to generate wrapper methods automatically in the FallbackVcpStorageStorable classes """ WRITE_METHODS = ('add_name', 'add_names', 'remove_name', 'remove_names', 'clear_names') def __init__(self, *args, **kwargs): instance_parent = None if not isinstance(self, HierarchicalMixin): instance_parent = kwargs.pop('instance_parent', None) super().__init__(*args, **kwargs) if not isinstance(self, HierarchicalMixin): self.instance_parent = instance_parent self._initialize() def _initialize(self): self._names = OrderedSet() @abstractmethod def vcp_storage_key(self) -> T_VcpStorageKey: pass @abstractmethod def vcp_storage_key_name(self) -> str: pass # Name(s) @property def has_name(self) -> bool: return len(self._names) > 0 @property def name(self) -> T_VcpStorageName: if self.has_name: return str(self._names[0]) return f"0x{self.vcp_storage_key():X}" @property def names(self) -> Iterable[T_VcpStorageName]: return OrderedSet(self._names) def add_name(self, new_name: T_VcpStorageName) -> None: if isinstance(new_name, int): raise ValueError(f"new_name={new_name} cannot be an integer") self._names.add(new_name) from .storage import VcpStorage if isinstance(self.instance_parent, VcpStorage): self.instance_parent[new_name] = self.vcp_storage_key() def add_names(self, *names: Iterable[T_VcpStorageName]) -> None: for name in names: self.add_name(name) def remove_name(self, name: T_VcpStorageName) -> None: if isinstance(name, int): raise ValueError(f"name={name} cannot be an integer") if name not in self._names: return self._names.remove(name) from .storage import VcpStorage if isinstance(self.instance_parent, VcpStorage): del self.instance_parent[name] def remove_names(self, *names: Iterable[T_VcpStorageName]) -> None: for name in names: self.remove_name(name) def clear_names(self) -> None: self.remove_names(*list(self._names)) # Comparison, etc def __eq__( self, other: Union['VcpStorageStorable', T_VcpStorageIdentifier]) -> bool: if isinstance(other, self.__class__): return other is self if isinstance(other, int): return other == self.vcp_storage_key() if isinstance(other, str): from .storage import VcpStorage key = VcpStorage.standardise_identifier(other) for nm in self.names: nm_key = VcpStorage.standardise_identifier(nm) if key == nm_key: return True return False return False def __hash__(self) -> int: return self.vcp_storage_key() # Copying def copy_storable(self, other: 'VcpStorageStorable') -> None: assert self.vcp_storage_key() == other.vcp_storage_key() assert self.__class__ is other.__class__ self.add_names(*other.names) # Conversion def asdict(self, include_key=False) -> Dict[str, Any]: d = {} if self.has_name: d['name'] = self.name if len(self._names) > 1: aliases = [] for nm in self._names[1:]: aliases.append(str(nm)) d['aliases'] = aliases storage_key_nm = self.vcp_storage_key_name() for attr_nm in self.__dict__: if attr_nm[0] != '_' and attr_nm != storage_key_nm: attr = getattr(self, attr_nm) if attr is not None: d[attr_nm] = attr if include_key: d[storage_key_nm] = self.vcp_storage_key() return d def _fromdict(self, data: Dict) -> None: self.clear_names() if 'name' in data: self.add_name(data['name']) if 'aliases' in data: self.add_names(*data['aliases']) storage_key_nm = self.vcp_storage_key_name() for attr_nm in self.__dict__: if attr_nm[0] != '_' and attr_nm != storage_key_nm: attr = data.get(attr_nm, None) if attr is not None: setattr(self, attr_nm, attr) # Printing def __str__(self) -> str: return self.name
class ModlistListbox(Frame): def __init__(self, parent, listview=False, name='Mods'): Frame.__init__(self, parent, bg='#2d2d2d') self.mod_list = [] self.modlabel_list = [] self.selected_modlabel_list = OrderedSet([]) self.listview = listview self.listview_height = 29 self.defaultview_height = 68 self.name_height = 35 self.current_index = None #category selection variables self.is_top_entered = False self.is_top_selected = False #collapse variable self.is_collapsed = False #enter variable self.is_entered_all = False #create mod listbox frame self.mlb_frame = Frame(self) #naming initalization self.name = name self.name_label = TagLabel(self, bordercolor='#666666', size=12, font=('roboto', 16, 'bold'), text=self.name, bg='#444444', fg='#f0f0f0', borderwidth=4, relief='flat') #limit the minimum size of name label if self.name_label.label.winfo_reqwidth() < 205: self.name_label.label.configure(width=15) #enter and leave events self.name_label.bind('<Enter>', self.on_enter_top) self.name_label.bind('<Leave>', self.on_leave_top) self.bind('<Enter>', self.on_enter_all) self.bind('<Leave>', self.on_leave_all) #lay out the name frame and mod listbox frame self.name_label.pack(side='top', fill='y', anchor='sw', padx=10) self.mlb_frame.pack(side='bottom', fill='both', expand=True) self.mlb_frame.grid_columnconfigure(0, weight=1) self.update_idletasks() def update_name(self, name): self.name = name ## self.name_label.update_text(name) #resizing a pre-existing TagLabel isn't working when changing text #so just make a new one 4Head new_name = TagLabel(self, bordercolor='#666666', size=12, font=('roboto', 16, 'bold'), text=name, bg='#444444', fg='#f0f0f0', borderwidth=4, relief='flat') self.name_label.pack_forget() self.name_label.destroy() self.name_label = new_name self.name_label.pack(side='top', fill='y', anchor='sw', padx=10) if self.name_label.label.winfo_reqwidth() < 205: self.name_label.label.configure(width=15) def get_name(self): return self.name def toggle_collapse(self): '''Collapses or expands the mod listbox''' if len(self.modlabel_list) > 0: if self.is_collapsed: self.force_expand() else: self.force_collapse() self.update_idletasks() def force_collapse(self): if len(self.modlabel_list) > 0: for mod in self.modlabel_list: mod.grid_remove() self.mlb_frame.configure(height=1) self.is_collapsed = True self.name_label.configure(bg='white') def force_expand(self): if len(self.modlabel_list) > 0: for mod in self.modlabel_list: mod.grid() self.is_collapsed = False self.name_label.configure(bg='#666666') def insert(self, index, info): '''inserts a mod at the given index, and with the given info''' if index == END or index > len(self.mod_list): index = len(self.mod_list) mod_label = ModLabel(self.mlb_frame, info=info, index=index, listview=self.listview) #Try to update the new indices. Fails if from an older mod try: mod_label.update_color(info[6]) mod_label.incompatibilities = info[7] except IndexError: pass self.mod_list.insert(index, info) self.modlabel_list.insert(index, mod_label) if len(self.mod_list) > index: for x in range(index, len(self.mod_list)): self.modlabel_list[x].update_index(x) self.modlabel_list[x].grid(row=x, column=0, sticky='nsew') else: mod_label.grid(row=len(self.mod_list), column=0, sticky='nsew') if self.is_collapsed: self.force_expand() def delete(self, index): '''Delete a mod at the given index''' if index == END: index = len(self.mod_list) - 1 if index < len(self.mod_list) - 1: for x in range(index, len(self.mod_list) - 1): self.modlabel_list[x + 1].grid(row=x, column=0, sticky='nsew') self.modlabel_list[x + 1].update_index(x) if self.modlabel_list[index] in self.selected_modlabel_list: self.selected_modlabel_list.remove(self.modlabel_list[index]) self.modlabel_list[index].grid_forget() self.modlabel_list[index].destroy() del self.mod_list[index] del self.modlabel_list[index] if len(self.modlabel_list) == 0: self.mlb_frame.configure(height=1) if self.is_collapsed: self.force_expand() #Quick fix for a category not properly expanding if last mod deleted self.is_collapsed = False self.name_label.configure(bg='#666666') def delete_selected(self): for x in range(len(self.selected_modlabel_list)): self.delete(self.selected_modlabel_list[0].get_index()) def delete_all_confirm(self): msgBox = messagebox.askquestion('Removing All', 'Remove all mods from the ' + self.name + ' Category?', icon='warning') if msgBox == 'yes': self.delete_all() def delete_all(self): '''deletes all mods from the list''' for x in range(len(self.modlabel_list)): self.delete(0) #Quick fix for a category not properly expanding if last mod deleted self.is_collapsed = False self.name_label.configure(bg='#666666') def open_link(self, modlabel): webbrowser.open_new(modlabel.get_info()[0]) def open_selected_links(self): for mod in sorted(self.selected_modlabel_list, key=lambda x: x.get_index()): self.open_link(mod) def open_all_links(self): for mod in self.modlabel_list: self.open_link(mod) def get_size(self): return len(self.modlabel_list) def get_all_info(self): '''return a list of lists of all mod info''' l = [] for mod in self.modlabel_list: l.append(mod.get_info()) return l def get_info(self, index): '''return a list of the mod info at the given index''' return self.modlabel_list[index].get_info() def get_height(self): return len(self.modlabel_list) def toggle_view(self): for mod in self.modlabel_list: mod.toggle_view() self.listview = not self.listview def force_listview(self): for mod in self.modlabel_list: mod.display_listview() self.listview = True def force_defaultview(self): for mod in self.modlabel_list: mod.display_default() self.listview = False def on_enter_top(self, event): '''event to determine if the list's category title is focused''' self.is_top_entered = True def on_leave_top(self, event): self.is_top_entered = False def on_enter_all(self, event): self.is_entered_all = True def on_leave_all(self, event): self.is_entered_all = False #====Right Click Menu Functionality def rightClickMenu(self, event, rc_menu): for mod in self.modlabel_list: if mod.is_focused: #If the mod clicked is not selected, select it if mod not in self.selected_modlabel_list: self.onClickEvent(event) ## rc_menu.add_command(label='Select', ## command=lambda mod=mod: self.rightClickSelect(mod)) ## rc_menu.add_command(label='Select All Mods in "'+self.name+'" Category', ## command=self.selectAll) ## rc_menu.add_separator() ## rc_menu.add_separator() ## rc_menu.add_command(label='Remove', ## command=lambda mod=mod: self.delete(mod.get_index())) ## if len(self.selected_modlabel_list) > 0: ## rc_menu.add_command(label='Remove Selected', ## command=self.delete_selected) ## if len(self.modlabel_list) > 0: ## rc_menu.add_command(label='Remove All Mods In "'+self.name+'" Category', ## command=self.delete_all_confirm) def insertInput(self, index): url = askstring('New Mod at Index ' + str(index + 1), 'Input Nexus URL') info = None if (url is not None): info = ParseURL.parse_nexus_url(url) if info is not None: self.insert(index, info) def insertCustomInput(self, index): info = [] CM = CustomModMessageBox(self, 'New Mod at Index ' + str(index + 1), info) if info != []: self.insert(index, info) #====Selection Functionality==== def onClickEvent(self, event): deselect_others = True self._check_descs() if len(self.modlabel_list) > 0: #if clicked mod is already part of selection, prevents the deselection of other mods for x in range(len(self.modlabel_list)): if self.modlabel_list[ x].is_index_focused and self.modlabel_list[ x].is_selected: deselect_others = False self.current_index = x if deselect_others: #checks every modlabel to see if any are selected for x in range(len(self.modlabel_list)): self.modlabel_list[x].select() #code for multi-selection capabilities if self.modlabel_list[x].is_selected and self.modlabel_list[ x] not in self.selected_modlabel_list: #adds selected modlabels to the selected modlabels list self.current_index = x self.selected_modlabel_list.append( self.modlabel_list[x]) elif not self.modlabel_list[ x].is_selected and self.modlabel_list[ x] in self.selected_modlabel_list: #removes deselected modlabels from the selected modlabels list self.selected_modlabel_list.remove( self.modlabel_list[x]) def _check_descs(self): '''checks whether mouse is over a mod description or not''' #Get widget type under mouse x, y = self.winfo_pointerxy() widget = self.winfo_containing(x, y) for mod in self.modlabel_list: if widget is not mod.description: mod.disable_desc_edit() else: mod.enable_desc_edit() def onDoubleClickEvent(self, event): if self.is_top_entered and len(self.modlabel_list) > 0: self.toggle_collapse() def onShiftClickEvent(self, event): #code for multi-selection if len(self.selected_modlabel_list) > 0: #set original index to start multi-selection from origin = self.selected_modlabel_list[-1].get_index() for x in range(len(self.modlabel_list)): # checks every modlabel for a valid multi-selection activation if self.modlabel_list[x].is_index_focused: #checks whether the index of the target modlabel is above #or below origin, then multi-selects accordingly if (x - origin) > 0: for y in range(origin, x + 1): self.selected_modlabel_list.append( self.modlabel_list[y]) self.modlabel_list[y].force_select() elif (x - origin) < 0: for y in range(x, origin): self.selected_modlabel_list.append( self.modlabel_list[y]) self.modlabel_list[y].force_select() def rightClickSelect(self, mod_index): mod = self.modlabel_list[mod_index] self.deselectAll() mod.force_select() self.selected_modlabel_list.append(mod) def selectAll(self): '''Selects all the mods''' if len(self.modlabel_list) > 0: for mod in self.modlabel_list: self.selected_modlabel_list.append(mod) mod.force_select() def deselectAll(self): if len(self.selected_modlabel_list) > 0: for mod in self.selected_modlabel_list: mod.force_deselect() self.selected_modlabel_list.clear() def selectTop(self): if self.is_top_entered: self.forceSelectTop() else: self.forceDeselectTop() def forceSelectTop(self): self.is_top_selected = True self.name_label.label.configure(bg='#f0f0f0', fg='#444444') def forceDeselectTop(self): self.is_top_selected = False self.name_label.label.configure(bg='#444444', fg='#f0f0f0') def is_top_selected(self): return self.is_top_selected #====Drag and Drop Functionality==== def moveSelectionUp(self): '''Goes through the selected mods and moves them up by 1''' if len(self.selected_modlabel_list) > 0: for mod in self.selected_modlabel_list: #if mod at upper limit, don't move anything if mod.get_index() == 0: return -1 #sorts selected mods using the index as the key, then iterates for mod in sorted(self.selected_modlabel_list, key=lambda x: x.get_index()): modtomove_data = self.modlabel_list[mod.get_index() - 1].get_info() modtomove_index = mod.get_index() - 1 self.delete(modtomove_index) self.insert(modtomove_index + 1, modtomove_data) def moveSelectionDown(self): '''Goes through the selected mods and moves them down by 1''' if len(self.selected_modlabel_list) > 0: for mod in self.selected_modlabel_list: #if mod at lower limit, don't move anything if mod.get_index() == len(self.modlabel_list) - 1: return 1 #sorts selected mods using the index as the key, then iterates for mod in sorted(self.selected_modlabel_list, key=lambda x: x.get_index(), reverse=True): modtomove_data = self.modlabel_list[mod.get_index() + 1].get_info() modtomove_index = mod.get_index() + 1 self.delete(modtomove_index) self.insert(modtomove_index - 1, modtomove_data) def dragSelection(self, event): '''Moves all selected ModLabels up or down depending on mouse movement while the mouse is held''' i = self.nearest() if self.current_index is not None: if i < self.current_index and i != -1: self.moveSelectionUp() self.current_index = i elif i > self.current_index and i != len(self.modlabel_list): self.moveSelectionDown() self.current_index = i return i def nearest(self): '''get index of ModLabel item nearest to the mouse y position using hardcoded ModLabel heights''' index = 0 current_nearest_index = 0 #get the correct height of the ModLabels if self.listview: height = self.listview_height else: height = self.defaultview_height #get the absolute position of the mouse in relation to the ModlistListbox position mouse_y = self.mlb_frame.winfo_pointery() - self.mlb_frame.winfo_rooty( ) if len(self.modlabel_list) > 0: current_index = 0 distance_from_current = abs((height / 2) - mouse_y) for i in range(len(self.modlabel_list)): distance_from_next = abs((i * height + (height / 2)) - mouse_y) if distance_from_current > distance_from_next: distance_from_current = distance_from_next current_index = i #if going beyond the list, return an index beyond the list to signify it moving into a different category ## if(current_index = index = current_index return index #====Code that doesn't f****n' work==== '''NOTES: This nearest() is based off of each widget's actual position on the board. It bugs out when a mod is