class SettingsEditor(Frame): def __init__(self, master, app_main, **kwargs): super().__init__(master, **kwargs) self.app = app_main self.create_settings_ui() def create_settings_ui(self): self.rowconfigure(0, weight=1) self.columnconfigure(0, weight=1) self.frm_settings = Frame(self) # self.frm_settings.rowconfigure(2, weight=1) # self.frm_settings.columnconfigure(0, weight=1) is_valid_req_delay = self.register(functools.partial(_is_number, min=0)) is_valid_duration = self.register( functools.partial(_is_number, min=0, max=20)) is_valid_history_retention = self.register( functools.partial(_is_number, min=0, max=100)) is_valid_max_conns = self.register( functools.partial(_is_number, min=1, max=20, integer=True)) is_valid_num_workers = self.register( functools.partial(_is_number, min=0, max=os.cpu_count() or 8, integer=True)) self.frm_settings.grid(padx=10, pady=10, sticky='nsew') frm_basic = LabelFrame(self.frm_settings, text='Basic') frm_basic.grid(padx=5, pady=5, sticky='nsew', row=0, column=0, ipadx=5) lbl = Label(frm_basic, text='League:') lbl.grid(row=0, column=0, padx=5, pady=3, sticky='w') self.cmb_league = Combobox(frm_basic, state=READONLY, values=leagueOptions) self.cmb_league.grid(row=0, column=1, pady=3, sticky='nsew') # lbl = Label(frm_basic, text='Minimum request delay(s):') # lbl.grid(row=1, column=0, padx=5, pady=3, sticky='w') # self.entry_req_delay = Entry(frm_basic, validate='all', validatecommand=(is_valid_req_delay, '%P')) # self.entry_req_delay.grid(row=1, column=1, pady=3, sticky='nsew') # lbl = Label(frm_basic, text='Scan mode:') # lbl.grid(row=2, column=0, padx=5, pady=3, sticky='w') # self.cmb_scan_mode = Combobox(frm_basic, state=READONLY, values=scanModeOptions) # self.cmb_scan_mode.grid(row=2, column=1, pady=3, sticky='nsew') lbl = Label(frm_basic, text='Notification duration(s):') lbl.grid(row=3, column=0, padx=5, pady=3, sticky='w') self.entry_notification_duration = Entry( frm_basic, validate='all', validatecommand=(is_valid_duration, '%P')) self.entry_notification_duration.grid(row=3, column=1, pady=3, sticky='nsew') frm = LabelFrame(self.frm_settings, text='Advanced') frm.grid(pady=5, sticky='nsew', row=0, column=1, ipadx=5) lbl = Label(frm, text='Scan mode:') lbl.grid(row=0, column=0, padx=5, pady=3, sticky='w') self.cmb_scan_mode = Combobox(frm, state=READONLY, values=scanModeOptions) self.cmb_scan_mode.grid(row=0, column=1, pady=3, sticky='nsew') lbl = Label(frm, text='Min. request delay:') lbl.grid(row=1, column=0, padx=5, pady=3, sticky='w') self.entry_req_delay = Entry(frm, validate='all', validatecommand=(is_valid_req_delay, '%P')) self.entry_req_delay.grid(row=1, column=1, pady=3, sticky='nsew') lbl = Label(frm, text='(seconds)') lbl.grid(row=1, column=2, padx=(5, 0), pady=3, sticky='w') lbl = Label(frm, text='Max connections:') lbl.grid(row=2, column=0, padx=5, pady=3, sticky='w') self.entry_max_conns = Entry(frm, validate='all', validatecommand=(is_valid_max_conns, '%P')) self.entry_max_conns.grid(row=2, column=1, pady=3, sticky='nsew') lbl = Label(frm, text='Parsers #:') lbl.grid(row=3, column=0, padx=5, pady=3, sticky='w') self.entry_num_workers = Entry(frm, validate='all', validatecommand=(is_valid_num_workers, '%P')) self.entry_num_workers.grid(row=3, column=1, pady=3, sticky='nsew') lbl = Label(frm, text='(0 = Auto)') lbl.grid(row=3, column=2, padx=(5, 0), pady=3, sticky='w') lbl = Label(frm, text='History retention:') lbl.grid(row=4, column=0, padx=5, pady=3, sticky='w') self.entry_history_retention = Entry( frm, validate='all', validatecommand=(is_valid_history_retention, '%P')) self.entry_history_retention.grid(row=4, column=1, pady=3, sticky='nsew') lbl = Label(frm, text='(days)') lbl.grid(row=4, column=2, padx=(5, 0), pady=3, sticky='w') frm = Frame(frm_basic) frm.grid(row=4, column=0) self.var_notify = BooleanVar() self.var_notify.trace_variable( 'w', lambda a, b, c: self._on_notify_option_change()) self.cb_notifications = Checkbutton(frm, text='Growl notifications', variable=self.var_notify) self.cb_notifications.grid(row=0, column=0, padx=5, pady=3, sticky='w') self.var_notify_copy = BooleanVar() self.cb_notify_copy = Checkbutton(frm, text='Copy message', variable=self.var_notify_copy) self.cb_notify_copy.grid(row=1, column=0, padx=5, pady=3, sticky='w') self.var_notify_play_sound = BooleanVar() self.cb_notify_play_sound = Checkbutton( frm, text='Play sound', variable=self.var_notify_play_sound) self.cb_notify_play_sound.grid(row=2, column=0, padx=5, pady=3, sticky='w') frm_btns = Frame(self.frm_settings) frm_btns.grid(row=2, columnspan=3, pady=(20, 5), sticky='w') self.btn_apply = Button(frm_btns, text='Apply', command=self.applyChanges) self.btn_apply.grid(row=0, column=0, padx=5) self.btn_reload = Button(frm_btns, text='Reload', command=self.loadSettings) self.btn_reload.grid(row=0, column=1) def _on_notify_option_change(self): state = NORMAL if self.var_notify.get() else DISABLED self.cb_notify_copy.config(state=state) self.cb_notify_play_sound.config(state=state) def applyChanges(self): cfg = AppConfiguration() cfg.league = self.cmb_league.get() or leagueOptions[0] cfg.notify = self.var_notify.get() cfg.notify_copy_msg = self.var_notify_copy.get() cfg.notify_play_sound = self.var_notify_play_sound.get() cfg.notification_duration = float( self.entry_notification_duration.get() or 4) cfg.request_delay = float(self.entry_req_delay.get() or 0.7) cfg.scan_mode = self.cmb_scan_mode.get() or scanModeOptions[0] cfg.history_retention = int(self.entry_history_retention.get() or 1) cfg.max_conns = int(self.entry_max_conns.get() or 8) cfg.num_workers = int(self.entry_num_workers.get() or 0) cfg.smooth_delay = config.smooth_delay self.app.update_configuration(cfg) def loadSettings(self): self.cmb_league.set(config.league) self.cmb_scan_mode.set(config.scan_mode) self.entry_notification_duration.delete(0, END) self.entry_notification_duration.insert(0, config.notification_duration) self.var_notify.set(config.notify) self.var_notify_copy.set(config.notify_copy_msg) self.var_notify_play_sound.set(config.notify_play_sound) self.entry_req_delay.delete(0, END) self.entry_req_delay.insert(0, config.request_delay) self.entry_history_retention.delete(0, END) self.entry_history_retention.insert(0, config.history_retention) self.entry_max_conns.delete(0, END) self.entry_max_conns.insert(0, config.max_conns) self.entry_num_workers.delete(0, END) self.entry_num_workers.insert(0, config.num_workers)
class PricesEditor(Frame): def __init__(self, master, **kwargs): super().__init__(master, **kwargs) self.create_prices_ui() self.initial_values = {} self.table_modified = False def create_prices_ui(self): self.rowconfigure(0, weight=1) self.columnconfigure(0, weight=1) self.frm_prices = Frame(self) self.frm_prices.rowconfigure(2, weight=1) self.frm_prices.columnconfigure(0, weight=1) self.frm_prices.grid(padx=10, pady=10, sticky='nsew') # Button Frame frm_btns = Frame(self.frm_prices, relief=SOLID, borderwidth=2) frm_btns.grid(row=0, column=0, pady=(0, 5), sticky='nsew') frm_btns.columnconfigure(10, weight=1) # self.entry_currency = \ # Combobox_Autocomplete(frm, list_of_items=['one of many currencies'], startswith_match=False) self.search_var = StringVar() self.entry_search = PlaceholderEntry(frm_btns, 'Search..', style='Default.TEntry', textvariable=self.search_var) self.search_var.trace_variable( 'w', lambda a, b, c: self.tree.search(self.entry_search.get_value())) self.entry_search.bind( '<Return>', lambda event: self.tree.search(self.entry_search.get_value(), find_next=True)) self.btn_apply = Button(frm_btns, text='Apply', command=self.applyChanges) self.btn_reload = Button( frm_btns, text='Reload', command=lambda: self.loadPrices(force_reload=True)) self.entry_search.grid(row=2, column=0, pady=5, padx=5) self.btn_apply.grid(row=2, column=2, pady=5) # frm.columnconfigure(3, weight=1) self.btn_reload.grid(row=2, column=3, sticky='e', pady=5) self.var_advanced = BooleanVar(False) self.var_advanced.trace_variable( 'w', lambda a, b, c: self._on_view_option_change()) self.cb_advanced = Checkbutton(frm_btns, text='Advanced', variable=self.var_advanced) self.cb_advanced.grid(row=2, column=10, sticky='e', padx=10) frm_border = Frame(self.frm_prices, relief=SOLID, borderwidth=2) frm_border.grid(row=2, column=0, sticky='nsew') frm_border.rowconfigure(2, weight=1) frm_border.columnconfigure(0, weight=1) # Tree Frame self.frm_tree = Frame(frm_border) self.frm_tree.grid(row=2, column=0, sticky='nsew', padx=5, pady=(0, 0)) self.frm_tree.rowconfigure(0, weight=1) self.frm_tree.columnconfigure(0, weight=1) self.tree = EditableTreeview(self.frm_tree, on_cell_update=self.onCellUpdate) scrly = AutoScrollbar(self.frm_tree, command=self.tree.yview) scrlx = AutoScrollbar(self.frm_tree, command=self.tree.xview, orient=HORIZONTAL) self.tree.config(yscrollcommand=scrly.set, xscrollcommand=scrlx.set) self.tree.grid(row=0, column=0, sticky='nsew') scrly.grid(row=0, column=1, sticky='nsew') scrlx.grid(row=1, column=0, sticky='nsew') # Button Frame frm = Frame(frm_border) #, relief=SOLID, borderwidth=1) # frm = Frame(self.frm_prices) frm.grid(row=0, column=0, sticky='nsew') # self.entry_currency = \ # Combobox_Autocomplete(frm, list_of_items=['one of many currencies'], startswith_match=False) lbl = Label(frm, text='Item value threshold:') lbl.grid(row=0, column=0, padx=5, pady=5, sticky='w') self.var_threshold = StringVar() self.entry_threshold = TooltipEntry(frm, textvariable=self.var_threshold) self.entry_threshold.bind( '<FocusOut>', lambda event: self._validate_threshold_entry()) self.entry_threshold.grid(row=0, column=1, padx=5, pady=5) self.var_threshold.trace( 'w', lambda a, b, c: self.on_entry_change(self.entry_threshold)) lbl = Label(frm, text='Budget:') lbl.grid(row=0, column=2, padx=5, pady=5) self.var_budget = StringVar() self.entry_budget = TooltipEntry(frm, textvariable=self.var_budget) self.entry_budget.bind('<FocusOut>', lambda event: self._validate_budget_entry()) self.entry_budget.grid(row=0, column=3, padx=5, pady=5) self.var_budget.trace( 'w', lambda a, b, c: self.on_entry_change(self.entry_budget)) lbl = Label(frm, text='Minimum price:') lbl.grid(row=0, column=4, padx=5, pady=5) self.var_min_price = StringVar() self.entry_min_price = TooltipEntry(frm, textvariable=self.var_min_price) self.entry_min_price.bind( '<FocusOut>', lambda event: self._validate_min_price_entry()) self.entry_min_price.grid(row=0, column=5, padx=5, pady=5) self.var_min_price.trace( 'w', lambda a, b, c: self.on_entry_change(self.entry_min_price)) lbl = Label(frm, text='Default filter override:') lbl.grid(row=0, column=6, padx=5, pady=5) self.lbl_fprice_override = lbl self.var_fprice_override = StringVar() self.entry_fprice_override = TooltipEntry( frm, textvariable=self.var_fprice_override) self.entry_fprice_override.bind( '<FocusOut>', lambda event: self._validate_fprice_override_entry()) self.entry_fprice_override.grid(row=0, column=7, padx=5, pady=5) self.var_fprice_override.trace( 'w', lambda a, b, c: self.on_entry_change(self.entry_fprice_override)) # Advanced lbl = Label(frm, text='Default item value override:') lbl.grid(row=1, column=0, padx=5, pady=(2, 5), sticky='w') self.lbl_price_override = lbl self.var_price_override = StringVar() self.entry_price_override = TooltipEntry( frm, textvariable=self.var_price_override) self.entry_price_override.bind( '<FocusOut>', lambda event: self._validate_price_override_entry()) self.entry_price_override.grid(row=1, column=1, padx=5, pady=(2, 5)) self.var_price_override.trace( 'w', lambda a, b, c: self.on_entry_change(self.entry_price_override)) # Confidence Level lbl = Label(frm, text="Confidence level:") lbl.grid(row=1, column=2, padx=5, pady=(2, 5), sticky='w') self.lbl_confidence_lvl = lbl self.var_confidence_lvl = IntVar() self.entry_confidence_lvl = ConfidenceScale( frm, variable=self.var_confidence_lvl) self.entry_confidence_lvl.grid(row=1, column=3, padx=5, pady=(2, 5)) self.var_confidence_lvl.trace( 'w', lambda a, b, c: self.on_entry_change(self.entry_confidence_lvl)) self.var_5l_filters = BooleanVar(False) self.cb_5l_filters = VarCheckbutton(frm, text='Enable 5L filters', variable=self.var_5l_filters) self.cb_5l_filters.var = self.var_5l_filters self.cb_5l_filters.grid(row=1, column=4, padx=5, pady=(2, 5), columnspan=1) self.var_5l_filters.trace_variable( 'w', lambda a, b, c: self.on_entry_change(self.cb_5l_filters)) # Tree Config tree = self.tree def init_tree_column(col): col_name = pricesColumns[0] if col == '#0' else col tree.heading(col, text=PricesColumn[col_name].value, anchor=W, command=lambda col=col: tree.sort_col(col)) tree.column(col, width=140, stretch=False) # self.tree['columns'] = ('ID', 'Item Price', 'Override', 'Filter Price', 'Filter Override', 'Effective Filter Price', 'Filter State Override', '') self.tree['columns'] = pricesColumns[1:] self.tree.register_column( PricesColumn.Override.name, ColEntry(TooltipEntry(self.tree), func_validate=_validate_price_override)) self.tree.register_column( PricesColumn.FilterOverride.name, ColEntry(TooltipEntry(self.tree), func_validate=_validate_price_override)) self.tree.register_column( PricesColumn.FilterStateOverride.name, ColEntry(Combobox(self.tree, values=filterStateOptions, state=READONLY), accept_events=('<<ComboboxSelected>>', '<Return>'))) for col in (('#0', ) + tree['columns']): init_tree_column(col) tree.heading('#0', anchor=CENTER) tree.column('#0', width=200, stretch=False) tree.column(PricesColumn.Filler.name, stretch=True) tree.heading(PricesColumn.ItemPrice.name, command=lambda col=PricesColumn.ItemPrice.name: tree. sort_col(col, key=self._price_key)) tree.heading(PricesColumn.Override.name, command=lambda col=PricesColumn.Override.name: tree. sort_col(col, key=self._price_key)) tree.heading(PricesColumn.FilterOverride.name, command=lambda col=PricesColumn.FilterOverride.name: tree. sort_col(col, key=self._price_key)) tree.heading(PricesColumn.FilterPrice.name, command=lambda col=PricesColumn.FilterPrice.name: tree. sort_col(col, key=self._rate_key, default=0)) tree.heading(PricesColumn.EffectiveFilterPrice.name, command=lambda col=PricesColumn.EffectiveFilterPrice.name: tree.sort_col(col, key=self._rate_key, default=0)) self.bvar_modified = BooleanVar() self.bvar_modified.trace('w', lambda a, b, c: self._updateApplyState()) self.bvar_modified.set(False) self.var_advanced.set(False) def _rate_key(self, key): if key == 'N/A': return 0 return float(key) def _price_key(self, key): if key == '': return None # this means it will be ignored while sorting try: return cm.compilePrice(key, base_price=0) except Exception: return 0 def on_entry_change(self, entry): val = entry.get() if self.initial_values[entry] != val: self.bvar_modified.set(True) # def on_price_entry_focusout(self, widget): # valid = _validate_price(widget, accept_empty=False) # if valid and not self.bvar_modified.get() and self.initial_values[widget] != widget.get(): # self.bvar_modified.set(True) # return valid # # def on_override_entry_focusout(self, widget): # valid = _validate_price_override(widget, accept_empty=False) # if valid and not self.bvar_modified.get() and self.initial_values[widget] != widget.get(): # self.bvar_modified.set(True) # return valid def _validate_threshold_entry(self): return _validate_price(self.entry_threshold, accept_empty=False) def _validate_budget_entry(self): return _validate_price(self.entry_budget, accept_empty=True) def _validate_min_price_entry(self): return _validate_price(self.entry_min_price, accept_empty=True) def _validate_price_override_entry(self): return _validate_price_override(self.entry_price_override, accept_empty=False) def _validate_fprice_override_entry(self): return _validate_price_override(self.entry_fprice_override, accept_empty=False) def _update_modified(self): modified = any(entry.get() != self.initial_values[entry] for entry in self.initial_values) or self.table_modified self.bvar_modified.set(modified) def _updateApplyState(self): if self.bvar_modified.get(): self.btn_apply.config(state=NORMAL) else: self.btn_apply.config(state=DISABLED) def _validateForm(self): if not self._validate_threshold_entry(): return False if not self._validate_budget_entry(): return False if not self._validate_min_price_entry(): return False if not self._validate_price_override_entry(): return False if not self._validate_fprice_override_entry(): return False return True def applyChanges(self, event=None): if not self.bvar_modified.get() or not fm.initialized: return if not self._validateForm(): return price_threshold = self.entry_threshold.get() default_price_override = self.entry_price_override.get() default_fprice_override = self.entry_fprice_override.get() budget = self.entry_budget.get() min_price = self.entry_min_price.get() confidence_lvl = self.entry_confidence_lvl.get( ) or fm.DEFAULT_CONFIDENCE_LEVEL enable_5l_filters = self.var_5l_filters.get() price_overrides = {} filter_price_overrides = {} filter_state_overrides = {} for iid in self.tree.get_children(): id = self.tree.set(iid, PricesColumn.ID.name) iprice = self.tree.set(iid, PricesColumn.Override.name) if iprice: price_overrides[id] = iprice fprice = self.tree.set(iid, PricesColumn.FilterOverride.name) if fprice: filter_price_overrides[id] = fprice fstate = self.tree.set(iid, PricesColumn.FilterStateOverride.name) try: filter_state_overrides[id] = FilterStateOption[fstate].value except KeyError: pass ids = set([ self.tree.set(iid, PricesColumn.ID.name) for iid in self.tree.get_children() ]) # preserve unhandled ids configuration for key in (set(fm.price_overrides) - ids): price_overrides[key] = fm.price_overrides[key] for key in (set(fm.filter_price_overrides) - ids): filter_price_overrides[key] = fm.filter_price_overrides[key] for key in (set(fm.filter_state_overrides) - ids): filter_state_overrides[key] = fm.filter_state_overrides[key] try: fm.updateConfig(default_price_override, default_fprice_override, price_threshold, budget, min_price, price_overrides, filter_price_overrides, filter_state_overrides, int(confidence_lvl), enable_5l_filters) except AppException as e: messagebox.showerror( 'Validation error', 'Failed to update configuration:\n{}'.format(e), parent=self.winfo_toplevel()) except Exception as e: logexception() messagebox.showerror( 'Update error', 'Failed to apply changes, unexpected error:\n{}'.format(e), parent=self.winfo_toplevel()) else: # SHOULD always work since config is valid, main console will report any failures # background thread because schema validating takes a bit of time threading.Thread(target=fm.compileFilters).start() self._initFormState() def loadPrices(self, force_reload=False): if not cm.initialized or not fm.initialized: return if not force_reload: self._update_modified() # in case of reverted changes if self.bvar_modified.get(): # dont interrupt user changes return tree = self.tree tree.clear() table = {} for fltr in fm.autoFilters: # effective_rate = cm.crates.get(curr, '') # if effective_rate != '': # effective_rate = round(effective_rate, 3) fid = fltr.id fstate_override = fm.filter_state_overrides.get(fid, '') try: fstate_override = FilterStateOption(fstate_override).name except ValueError: fstate_override = '' table[fid] = (fltr.title, fid, fm.item_prices[fid], fm.price_overrides.get(fid, ''), _to_display_rate( fm.compiled_item_prices.get(fid, 'N/A')), fm.filter_price_overrides.get(fid, ''), _to_display_rate( fm.compiled_filter_prices.get(fid, 'N/A')), fstate_override) for fid in table: tree.insert('', END, '', text=table[fid][0], values=table[fid][1:]) # tree.sort_by('#0', descending=True) tree.sort_col('#0', reverse=False) self._initFormState() # def onItemPriceUpdate(self, iid, col, old, new): # print('IPrice update: iid {}, col {}'.format(iid, col)) def onCellUpdate(self, iid, col, old, new): if old != new: self.table_modified = True self.bvar_modified.set(True) # self._update_modified() def _initFormState(self): self.table_modified = False self.initial_values[self.entry_threshold] = fm.price_threshold self.initial_values[self.entry_budget] = fm.budget self.initial_values[self.entry_min_price] = fm.default_min_price self.initial_values[ self.entry_price_override] = fm.default_price_override self.initial_values[ self.entry_fprice_override] = fm.default_fprice_override self.initial_values[self.entry_confidence_lvl] = fm.confidence_level self.initial_values[self.cb_5l_filters] = fm.enable_5l_filters self.var_threshold.set(fm.price_threshold) self.var_budget.set(fm.budget) self.var_min_price.set(fm.default_min_price) self.var_price_override.set(fm.default_price_override) self.var_fprice_override.set(fm.default_fprice_override) self.var_confidence_lvl.set(fm.confidence_level) self.var_5l_filters.set(fm.enable_5l_filters) self.bvar_modified.set(False) def _on_view_option_change(self): advanced_widgets = [ self.entry_price_override, self.lbl_price_override, self.lbl_confidence_lvl, self.entry_confidence_lvl, self.cb_5l_filters ] if not self.var_advanced.get(): for w in advanced_widgets: w.grid_remove() self.tree.config(displaycolumn=[ PricesColumn.FilterPrice.name, PricesColumn.FilterOverride. name, PricesColumn.EffectiveFilterPrice.name, PricesColumn.Filler.name ]) else: for w in advanced_widgets: w.grid() self.tree.config(displaycolumn='#all') self.tree.on_entry_close()