def popupMenu(self, event): colname = self.model.columnNames[self.table.currentcol] collabel = self.model.columnlabels[colname] currcol = self.table.currentcol popupmenu = Menu(self, tearoff=0) # noinspection PyUnusedLocal,PyShadowingNames def popupFocusOut(event): popupmenu.unpost() popupmenu.add_command(label="Sort by " + collabel, command=lambda: self.table.sortTable(currcol)) popupmenu.add_command(label="Sort by " + collabel + ' (descending)', command=lambda: self.table.sortTable(currcol, reverse=1)) popupmenu.bind("<FocusOut>", popupFocusOut) # self.bind("<Button-3>", popupFocusOut) popupmenu.focus_set() popupmenu.post(event.x_root, event.y_root) return popupmenu
class FlugerTable(Table): """Custom table class inherits from Table. You can then override required methods""" def __init__(self, parent=None, **kwargs): Table.__init__(self, parent, **kwargs) self.popupmenu = Menu() def handle_left_click(self, event): """Example - override left click""" Table.handle_left_click(self, event) def popupMenu(self, event, rows=None, cols=None, outside=None): """Custom right click menu""" self.popupmenu = Menu(self, tearoff=0) def popupFocusOut(self, event): self.popupmenu.unpost() # self.app is a reference to the parent app self.popupmenu.add_command(label='do stuff', command=self.app.stuff) self.popupmenu.bind("<FocusOut>", self.popupFocusOut) self.popupmenu.focus_set() self.popupmenu.post(event.x_root, event.y_root) return self.popupmenu
def popupMenu(self, event, rows=None, cols=None, outside=None): """Add left and right click behaviour for canvas, should not have to override this function, it will take its values from defined dicts in constructor""" defaultactions = { "Set Fill Color": lambda: self.setcellColor(rows, cols, key='bg'), "Set Text Color": lambda: self.setcellColor(rows, cols, key='fg'), "Copy": lambda: self.copyCell(rows, cols), "Paste": lambda: self.pasteCell(rows, cols), "Fill Down": lambda: self.fillDown(rows, cols), "Fill Right": lambda: self.fillAcross(cols, rows), "Add Row(s)": lambda: self.addRows(), "Delete Row(s)": lambda: self.deleteRow(), "View Record": lambda: self.getRecordInfo(row), "Clear Data": lambda: self.deleteCells(rows, cols), "Select All": self.select_All, "Auto Fit Columns": self.autoResizeColumns, "Filter Records": self.showFilteringBar, "New": self.new, "Load": self.load, "Save": self.save, "Import text": self.importTable, "Export csv": self.exportTable, "Plot Selected": self.plotSelected, "Plot Options": self.plotSetup, "Export Table": self.exportTable, "Preferences": self.showtablePrefs, "Formulae->Value": lambda: self.convertFormulae(rows, cols) } main = [ "Set Fill Color", "Set Text Color", "Copy", "Paste", "Fill Down", "Fill Right", "Clear Data" ] general = [ "Select All", "Auto Fit Columns", "Filter Records", "Preferences" ] def createSubMenu(parent, label, commands): menu = Menu(parent, tearoff=0) popupmenu.add_cascade(label=label, menu=menu) for action in commands: menu.add_command(label=action, command=defaultactions[action]) return menu def add_commands(fieldtype): """Add commands to popup menu for column type and specific cell""" functions = self.columnactions[fieldtype] for f in functions.keys(): func = getattr(self, functions[f]) popupmenu.add_command(label=f, command=lambda: func(row, col)) return popupmenu = Menu(self, tearoff=0) def popupFocusOut(event): popupmenu.unpost() if outside == None: # if outside table, just show general items row = self.get_row_clicked(event) col = self.get_col_clicked(event) coltype = self.model.getColumnType(col) def add_defaultcommands(): """now add general actions for all cells""" for action in main: if action == 'Fill Down' and (rows == None or len(rows) <= 1): continue if action == 'Fill Right' and (cols == None or len(cols) <= 1): continue else: popupmenu.add_command(label=action, command=defaultactions[action]) return if coltype in self.columnactions: add_commands(coltype) add_defaultcommands() for action in general: popupmenu.add_command(label=action, command=defaultactions[action]) popupmenu.add_separator() # createSubMenu(popupmenu, 'File', filecommands) # createSubMenu(popupmenu, 'Plot', plotcommands) popupmenu.bind("<FocusOut>", popupFocusOut) popupmenu.focus_set() popupmenu.post(event.x_root, event.y_root) return popupmenu
charABCOrder.set(0) # Add options to the mainFileDropDown Cascade fileDropDown.add_command(label="Open", command=palEdit.openFileCmd) fileDropDown.add_separator() fileDropDown.add_command(label="Quit", command=root.quit) # Add options to the Options Cascade optionDropDown.add_checkbutton(label="Show Characters Alphabetically", onvalue=1, offvalue=0, variable=charABCOrder, command=updateCharList) # Bind MouseOver events fileDropDown.bind("<<MenuSelect>>", mOver_File) optionDropDown.bind("<<MenuSelect>>", mOver_Options) ####################### ##### MAIN WINDOW ##### ####################### # Add in buttons to perform tasks buttonTestCalc = Button(root, text="Update Frame Color", command=updateFrameColor) buttonTestCalc.pack() palFrame = Frame(height=100, width=100, bd=1, relief=tk.SUNKEN, bg="red") palFrame.pack()
class MyFirstGUI: fileData = RejectingDict() last_statusbar_value = '' def __init__(self, master): self.master = master master.title("Сертификаты из ТТН") master.geometry("300x100") master.grid_columnconfigure(index=0, minsize=350, weight=5) #master.grid_columnconfigure(index = 1, weight = 0) menu = Menu(master, tearoff=0) #need to store submenu to be able to address it from others subs self.submenu2 = Menu(master, tearoff=0) self.submenu3 = Menu(master, tearoff=0) self.submenu4 = Menu(master, tearoff=0) self.menubar = Menu(menu, tearoff=0) menu.add_command(label="Open", command=self.open) menu.add_separator() menu.add_command(label="Exit", command=master.destroy) #cascade index = 0 self.menubar.add_cascade(label="File", menu=menu, state="normal") self.submenu2.add_command(label="Copies", command=self.copies, state="disabled") #cascade index = 1 self.menubar.add_cascade(label="Operations", menu=self.submenu2) self.submenu4.add_command(label="Options", command=self.options) #cascade index = 2 self.menubar.add_cascade(label="Settings", menu=self.submenu4) self.commandAbout = self.submenu3.add_command(label="About", command=self.about) #cascade index = 3 self.menubar.add_cascade(label="Help", menu=self.submenu3) self.lbMain = Listbox( master, selectmode='extended', state="disabled", width=50, #width in characters height=5 #number of lines ) self.scrollbar = Scrollbar(master, orient='vertical') self.lbMain.config(yscrollcommand=self.scrollbar.set) self.scrollbar.config(command=self.lbMain.yview) self.scrollbar.grid( row=0, column=0, ipady=0, sticky='e' # just to the right side East ) self.lbMain.grid( row=0, column=0, sticky='n, s, e, w' #to all sides ) # status bar status_frame = Frame(master, height=10) self.status = Label(status_frame, text="this is the status bar") self.status.pack(fill="both", expand=True) status_frame.grid(row=1, column=0) self.lbMain.bind('<<ListboxSelect>>', self.on_lbSelect) self.submenu3.bind('<<MenuSelect>>', self.about_status) self.submenu3.bind('<Enter>', self.about_status) self.submenu4.bind('<<MenuSelect>>', self.options_status) self.submenu4.bind('<Enter>', self.options_status) self.submenu3.bind('<Leave>', self.status_leave) self.submenu4.bind('<Leave>', self.status_leave) master.config(menu=self.menubar) def about_status(self, event): self.last_statusbar_value = self.status['text'] self.status['text'] = 'About' print(event.widget) def options_status(self, event): self.last_statusbar_value = self.status['text'] self.status['text'] = 'Options' print(event.widget) def status_leave(self, event): self.status['text'] = self.last_statusbar_value def about(self): messagebox.showinfo( title="About", message="Program reads TTNs from Excel files " + "and prints either quality certificates Consignee-wise or prepares" + " PDFs with complacency certificates on the same basis") def options(self): pass def open(self): print("Opening files") filename = Dialog.askopenfilename( initialdir=os.path.dirname(__file__), title="Файлы с ТТН", #need to leave comma to build 1-x tuple filetypes=(("Excel files", "*.xls"), ), multiple=True) if len(filename) == 0: return for nm in filename: try: ob = TTNReader(nm) self.fileData[ob.TTN_data["TTN_Number"]] = ob #filling in the listbox if self.lbMain.size() == 0: self.lbMain.config(state='normal') self.lbMain.insert('end', str(ob.TTN_data["TTN_Number"])) except: continue def on_lbSelect(self, evt): if len(self.lbMain.curselection()) != 0: self.submenu2.entryconfig('Copies', state="normal") else: self.submenu2.entryconfig('Copies', state="disabled") def copies(self): """ Prepares PDFs with inscriptions """ for ttn in self.lbMain.curselection(): print(self.fileData[int(self.lbMain.get(ttn))].TTN_data["path"])
def popupMenu(self, event, rows=None, cols=None, outside=None): """Add left and right click behaviour for canvas, should not have to override this function, it will take its values from defined dicts in constructor""" defaultactions = {"Set Fill Color": lambda: self.setcellColor(rows, cols, key='bg'), "Set Text Color": lambda: self.setcellColor(rows, cols, key='fg'), "Copy": lambda: self.copyCell(rows, cols), "Paste": lambda: self.pasteCell(rows, cols), "Fill Down": lambda: self.fillDown(rows, cols), "Fill Right": lambda: self.fillAcross(cols, rows), "Add Row(s)": lambda: self.addRows(), "Delete Row(s)": lambda: self.deleteRow(), "View Record": lambda: self.getRecordInfo(row), "Clear Data": lambda: self.deleteCells(rows, cols), "Select": lambda: self.handle_double_click(event), "Select All": self.select_All, "Auto Fit Columns": self.autoResizeColumns, "Filter Records": self.showFilteringBar, "New": self.new, "Load": self.load, "Save": self.save, "Import text": self.importTable, "Export csv": self.exportTable, "Plot Selected": self.plotSelected, "Plot Options": self.plotSetup, "Export Table": self.exportTable, "Preferences": self.showtablePrefs, "Formulae->Value": lambda: self.convertFormulae(rows, cols)} main = ["Select", "Set Fill Color", "Set Text Color"] general = ["Filter Records"] popupmenu = Menu(self, tearoff=0) # noinspection PyUnusedLocal,PyShadowingNames def popupFocusOut(event): popupmenu.unpost() if outside is None: # if outside table, just show general items row = self.get_row_clicked(event) col = self.get_col_clicked(event) coltype = self.model.getColumnType(col) # noinspection PyShadowingNames def add_defaultcommands(): """now add general actions for all cells""" for action in main: if action == 'Fill Down' and (rows is None or len(rows) <= 1): continue if action == 'Fill Right' and (cols is None or len(cols) <= 1): continue else: popupmenu.add_command(label=action, command=defaultactions[action]) return add_defaultcommands() for action in general: popupmenu.add_command(label=action, command=defaultactions[action]) popupmenu.bind("<FocusOut>", popupFocusOut) popupmenu.focus_set() popupmenu.post(event.x_root, event.y_root) return popupmenu
# Config to set mainMenuBar as the menu bar mainMenuBar = Menu(root) root.config(menu=mainMenuBar) # Create object for the File dropdown bar, tearoff=0 removes the dashes at the top of the cascade menu mainFileDropDown = Menu(mainMenuBar, tearoff=0) # Add the File cascade to the mainMenuBar mainMenuBar.add_cascade(label="File", menu=mainFileDropDown) # Add Options to the mainFileDropDown Cascade mainFileDropDown.add_command(label="Open", command=palette_editor.openFileCmd) mainFileDropDown.add_separator() mainFileDropDown.add_command(label="Quit", command=root.quit) # Bind MouseOver events mainFileDropDown.bind("<<MenuSelect>>", mOver_File) # Add in buttons to perform tasks buttonTestCalc = Button(root, text="Test Calc", command=palette_editor.testClasses) buttonTestCalc.pack() ##### STATUS BAR GUI ##### statusBar = Label(root, text="", bd=1, relief=tk.SUNKEN, anchor=tk.W) statusBar.pack(side=tk.BOTTOM, fill=tk.X) # Run window infinitely until close root.mainloop()
class EventScheduler(Tk): def __init__(self): Tk.__init__(self, className='Scheduler') logging.info('Start') self.protocol("WM_DELETE_WINDOW", self.hide) self._visible = BooleanVar(self, False) self.withdraw() self.icon_img = PhotoImage(master=self, file=ICON48) self.iconphoto(True, self.icon_img) # --- systray icon self.icon = TrayIcon(ICON, fallback_icon_path=ICON_FALLBACK) # --- menu self.menu_widgets = SubMenu(parent=self.icon.menu) self.menu_eyes = Eyes(self.icon.menu, self) self.icon.menu.add_checkbutton(label=_('Manager'), command=self.display_hide) self.icon.menu.add_cascade(label=_('Widgets'), menu=self.menu_widgets) self.icon.menu.add_cascade(label=_("Eyes' rest"), menu=self.menu_eyes) self.icon.menu.add_command(label=_('Settings'), command=self.settings) self.icon.menu.add_separator() self.icon.menu.add_command(label=_('About'), command=lambda: About(self)) self.icon.menu.add_command(label=_('Quit'), command=self.exit) self.icon.bind_left_click(lambda: self.display_hide(toggle=True)) add_trace(self._visible, 'write', self._visibility_trace) self.menu = Menu(self, tearoff=False) self.menu.add_command(label=_('Edit'), command=self._edit_menu) self.menu.add_command(label=_('Delete'), command=self._delete_menu) self.right_click_iid = None self.menu_task = Menu(self.menu, tearoff=False) self._task_var = StringVar(self) menu_in_progress = Menu(self.menu_task, tearoff=False) for i in range(0, 110, 10): prog = '{}%'.format(i) menu_in_progress.add_radiobutton(label=prog, value=prog, variable=self._task_var, command=self._set_progress) for state in ['Pending', 'Completed', 'Cancelled']: self.menu_task.add_radiobutton(label=_(state), value=state, variable=self._task_var, command=self._set_progress) self._img_dot = tkPhotoImage(master=self) self.menu_task.insert_cascade(1, menu=menu_in_progress, compound='left', label=_('In Progress'), image=self._img_dot) self.title('Scheduler') self.rowconfigure(1, weight=1) self.columnconfigure(0, weight=1) self.scheduler = BackgroundScheduler(coalesce=False, misfire_grace_time=86400) self.scheduler.add_jobstore('sqlalchemy', url='sqlite:///%s' % JOBSTORE) self.scheduler.add_jobstore('memory', alias='memo') # --- style self.style = Style(self) self.style.theme_use("clam") self.style.configure('title.TLabel', font='TkdefaultFont 10 bold') self.style.configure('title.TCheckbutton', font='TkdefaultFont 10 bold') self.style.configure('subtitle.TLabel', font='TkdefaultFont 9 bold') self.style.configure('white.TLabel', background='white') self.style.configure('border.TFrame', background='white', border=1, relief='sunken') self.style.configure("Treeview.Heading", font="TkDefaultFont") bgc = self.style.lookup("TButton", "background") fgc = self.style.lookup("TButton", "foreground") bga = self.style.lookup("TButton", "background", ("active", )) self.style.map('TCombobox', fieldbackground=[('readonly', 'white'), ('readonly', 'focus', 'white')], background=[("disabled", "active", "readonly", bgc), ("!disabled", "active", "readonly", bga)], foreground=[('readonly', '!disabled', fgc), ('readonly', '!disabled', 'focus', fgc), ('readonly', 'disabled', 'gray40'), ('readonly', 'disabled', 'focus', 'gray40') ], arrowcolor=[("disabled", "gray40")]) self.style.configure('menu.TCombobox', foreground=fgc, background=bgc, fieldbackground=bgc) self.style.map('menu.TCombobox', fieldbackground=[('readonly', bgc), ('readonly', 'focus', bgc)], background=[("disabled", "active", "readonly", bgc), ("!disabled", "active", "readonly", bga)], foreground=[('readonly', '!disabled', fgc), ('readonly', '!disabled', 'focus', fgc), ('readonly', 'disabled', 'gray40'), ('readonly', 'disabled', 'focus', 'gray40') ], arrowcolor=[("disabled", "gray40")]) self.style.map('DateEntry', arrowcolor=[("disabled", "gray40")]) self.style.configure('cal.TFrame', background='#424242') self.style.configure('month.TLabel', background='#424242', foreground='white') self.style.configure('R.TButton', background='#424242', arrowcolor='white', bordercolor='#424242', lightcolor='#424242', darkcolor='#424242') self.style.configure('L.TButton', background='#424242', arrowcolor='white', bordercolor='#424242', lightcolor='#424242', darkcolor='#424242') active_bg = self.style.lookup('TEntry', 'selectbackground', ('focus', )) self.style.map('R.TButton', background=[('active', active_bg)], bordercolor=[('active', active_bg)], darkcolor=[('active', active_bg)], lightcolor=[('active', active_bg)]) self.style.map('L.TButton', background=[('active', active_bg)], bordercolor=[('active', active_bg)], darkcolor=[('active', active_bg)], lightcolor=[('active', active_bg)]) self.style.configure('txt.TFrame', background='white') self.style.layout('down.TButton', [('down.TButton.downarrow', { 'side': 'right', 'sticky': 'ns' })]) self.style.map('TRadiobutton', indicatorforeground=[('disabled', 'gray40')]) self.style.map('TCheckbutton', indicatorforeground=[('disabled', 'gray40')], indicatorbackground=[ ('pressed', '#dcdad5'), ('!disabled', 'alternate', 'white'), ('disabled', 'alternate', '#a0a0a0'), ('disabled', '#dcdad5') ]) self.style.map('down.TButton', arrowcolor=[("disabled", "gray40")]) self.style.map('TMenubutton', arrowcolor=[('disabled', self.style.lookup('TMenubutton', 'foreground', ['disabled']))]) bg = self.style.lookup('TFrame', 'background', default='#ececec') self.configure(bg=bg) self.option_add('*Toplevel.background', bg) self.option_add('*Menu.background', bg) self.option_add('*Menu.tearOff', False) # toggle text self._open_image = PhotoImage(name='img_opened', file=IM_OPENED, master=self) self._closed_image = PhotoImage(name='img_closed', file=IM_CLOSED, master=self) self._open_image_sel = PhotoImage(name='img_opened_sel', file=IM_OPENED_SEL, master=self) self._closed_image_sel = PhotoImage(name='img_closed_sel', file=IM_CLOSED_SEL, master=self) self.style.element_create( "toggle", "image", "img_closed", ("selected", "!disabled", "img_opened"), ("active", "!selected", "!disabled", "img_closed_sel"), ("active", "selected", "!disabled", "img_opened_sel"), border=2, sticky='') self.style.map('Toggle', background=[]) self.style.layout('Toggle', [('Toggle.border', { 'children': [('Toggle.padding', { 'children': [('Toggle.toggle', { 'sticky': 'nswe' })], 'sticky': 'nswe' })], 'sticky': 'nswe' })]) # toggle sound self._im_sound = PhotoImage(master=self, file=IM_SOUND) self._im_mute = PhotoImage(master=self, file=IM_MUTE) self._im_sound_dis = PhotoImage(master=self, file=IM_SOUND_DIS) self._im_mute_dis = PhotoImage(master=self, file=IM_MUTE_DIS) self.style.element_create( 'mute', 'image', self._im_sound, ('selected', '!disabled', self._im_mute), ('selected', 'disabled', self._im_mute_dis), ('!selected', 'disabled', self._im_sound_dis), border=2, sticky='') self.style.layout('Mute', [('Mute.border', { 'children': [('Mute.padding', { 'children': [('Mute.mute', { 'sticky': 'nswe' })], 'sticky': 'nswe' })], 'sticky': 'nswe' })]) self.style.configure('Mute', relief='raised') # widget scrollbar self._im_trough = {} self._im_slider_vert = {} self._im_slider_vert_prelight = {} self._im_slider_vert_active = {} self._slider_alpha = Image.open(IM_SCROLL_ALPHA) for widget in ['Events', 'Tasks']: bg = CONFIG.get(widget, 'background', fallback='gray10') fg = CONFIG.get(widget, 'foreground') widget_bg = self.winfo_rgb(bg) widget_fg = tuple( round(c * 255 / 65535) for c in self.winfo_rgb(fg)) active_bg = active_color(*widget_bg) active_bg2 = active_color(*active_color(*widget_bg, 'RGB')) slider_vert = Image.new('RGBA', (13, 28), active_bg) slider_vert_active = Image.new('RGBA', (13, 28), widget_fg) slider_vert_prelight = Image.new('RGBA', (13, 28), active_bg2) self._im_trough[widget] = tkPhotoImage(width=15, height=15, master=self) self._im_trough[widget].put(" ".join( ["{" + " ".join([bg] * 15) + "}"] * 15)) self._im_slider_vert_active[widget] = PhotoImage( slider_vert_active, master=self) self._im_slider_vert[widget] = PhotoImage(slider_vert, master=self) self._im_slider_vert_prelight[widget] = PhotoImage( slider_vert_prelight, master=self) self.style.element_create('%s.Vertical.Scrollbar.trough' % widget, 'image', self._im_trough[widget]) self.style.element_create( '%s.Vertical.Scrollbar.thumb' % widget, 'image', self._im_slider_vert[widget], ('pressed', '!disabled', self._im_slider_vert_active[widget]), ('active', '!disabled', self._im_slider_vert_prelight[widget]), border=6, sticky='ns') self.style.layout( '%s.Vertical.TScrollbar' % widget, [('%s.Vertical.Scrollbar.trough' % widget, { 'children': [('%s.Vertical.Scrollbar.thumb' % widget, { 'expand': '1' })], 'sticky': 'ns' })]) # --- tree columns = { _('Summary'): ({ 'stretch': True, 'width': 300 }, lambda: self._sort_by_desc(_('Summary'), False)), _('Place'): ({ 'stretch': True, 'width': 200 }, lambda: self._sort_by_desc(_('Place'), False)), _('Start'): ({ 'stretch': False, 'width': 150 }, lambda: self._sort_by_date(_('Start'), False)), _('End'): ({ 'stretch': False, 'width': 150 }, lambda: self._sort_by_date(_("End"), False)), _('Category'): ({ 'stretch': False, 'width': 100 }, lambda: self._sort_by_desc(_('Category'), False)) } self.tree = Treeview(self, show="headings", columns=list(columns)) for label, (col_prop, cmd) in columns.items(): self.tree.column(label, **col_prop) self.tree.heading(label, text=label, anchor="w", command=cmd) self.tree.tag_configure('0', background='#ececec') self.tree.tag_configure('1', background='white') self.tree.tag_configure('outdated', foreground='red') scroll = AutoScrollbar(self, orient='vertical', command=self.tree.yview) self.tree.configure(yscrollcommand=scroll.set) # --- toolbar toolbar = Frame(self) self.img_plus = PhotoImage(master=self, file=IM_ADD) Button(toolbar, image=self.img_plus, padding=1, command=self.add).pack(side="left", padx=4) Label(toolbar, text=_("Filter by")).pack(side="left", padx=4) # --- TODO: add filter by start date (after date) self.filter_col = Combobox( toolbar, state="readonly", # values=("",) + self.tree.cget('columns')[1:], values=("", _("Category")), exportselection=False) self.filter_col.pack(side="left", padx=4) self.filter_val = Combobox(toolbar, state="readonly", exportselection=False) self.filter_val.pack(side="left", padx=4) Button(toolbar, text=_('Delete All Outdated'), padding=1, command=self.delete_outdated_events).pack(side="right", padx=4) # --- grid toolbar.grid(row=0, columnspan=2, sticky='we', pady=4) self.tree.grid(row=1, column=0, sticky='eswn') scroll.grid(row=1, column=1, sticky='ns') # --- restore data data = {} self.events = {} self.nb = 0 try: with open(DATA_PATH, 'rb') as file: dp = Unpickler(file) data = dp.load() except Exception: l = [ f for f in os.listdir(os.path.dirname(BACKUP_PATH)) if f.startswith('data.backup') ] if l: l.sort(key=lambda x: int(x[11:])) shutil.copy(os.path.join(os.path.dirname(BACKUP_PATH), l[-1]), DATA_PATH) with open(DATA_PATH, 'rb') as file: dp = Unpickler(file) data = dp.load() self.nb = len(data) backup() now = datetime.now() for i, prop in enumerate(data): iid = str(i) self.events[iid] = Event(self.scheduler, iid=iid, **prop) self.tree.insert('', 'end', iid, values=self.events[str(i)].values()) tags = [str(self.tree.index(iid) % 2)] self.tree.item(iid, tags=tags) if not prop['Repeat']: for rid, d in list(prop['Reminders'].items()): if d < now: del self.events[iid]['Reminders'][rid] self.after_id = self.after(15 * 60 * 1000, self.check_outdated) # --- bindings self.bind_class("TCombobox", "<<ComboboxSelected>>", self.clear_selection, add=True) self.bind_class("TCombobox", "<Control-a>", self.select_all) self.bind_class("TEntry", "<Control-a>", self.select_all) self.tree.bind('<3>', self._post_menu) self.tree.bind('<1>', self._select) self.tree.bind('<Double-1>', self._edit_on_click) self.menu.bind('<FocusOut>', lambda e: self.menu.unpost()) self.filter_col.bind("<<ComboboxSelected>>", self.update_filter_val) self.filter_val.bind("<<ComboboxSelected>>", self.apply_filter) # --- widgets self.widgets = {} prop = { op: CONFIG.get('Calendar', op) for op in CONFIG.options('Calendar') } self.widgets['Calendar'] = CalendarWidget(self, locale=CONFIG.get( 'General', 'locale'), **prop) self.widgets['Events'] = EventWidget(self) self.widgets['Tasks'] = TaskWidget(self) self.widgets['Timer'] = Timer(self) self.widgets['Pomodoro'] = Pomodoro(self) self._setup_style() for item, widget in self.widgets.items(): self.menu_widgets.add_checkbutton( label=_(item), command=lambda i=item: self.display_hide_widget(i)) self.menu_widgets.set_item_value(_(item), widget.variable.get()) add_trace(widget.variable, 'write', lambda *args, i=item: self._menu_widgets_trace(i)) self.icon.loop(self) self.tk.eval(""" apply {name { set newmap {} foreach {opt lst} [ttk::style map $name] { if {($opt eq "-foreground") || ($opt eq "-background")} { set newlst {} foreach {st val} $lst { if {($st eq "disabled") || ($st eq "selected")} { lappend newlst $st $val } } if {$newlst ne {}} { lappend newmap $opt $newlst } } else { lappend newmap $opt $lst } } ttk::style map $name {*}$newmap }} Treeview """) # react to scheduler --update-date in command line signal.signal(signal.SIGUSR1, self.update_date) # update selected date in calendar and event list every day self.scheduler.add_job(self.update_date, CronTrigger(hour=0, minute=0, second=1), jobstore='memo') self.scheduler.start() def _setup_style(self): # scrollbars for widget in ['Events', 'Tasks']: bg = CONFIG.get(widget, 'background', fallback='gray10') fg = CONFIG.get(widget, 'foreground', fallback='white') widget_bg = self.winfo_rgb(bg) widget_fg = tuple( round(c * 255 / 65535) for c in self.winfo_rgb(fg)) active_bg = active_color(*widget_bg) active_bg2 = active_color(*active_color(*widget_bg, 'RGB')) slider_vert = Image.new('RGBA', (13, 28), active_bg) slider_vert.putalpha(self._slider_alpha) slider_vert_active = Image.new('RGBA', (13, 28), widget_fg) slider_vert_active.putalpha(self._slider_alpha) slider_vert_prelight = Image.new('RGBA', (13, 28), active_bg2) slider_vert_prelight.putalpha(self._slider_alpha) self._im_trough[widget].put(" ".join( ["{" + " ".join([bg] * 15) + "}"] * 15)) self._im_slider_vert_active[widget].paste(slider_vert_active) self._im_slider_vert[widget].paste(slider_vert) self._im_slider_vert_prelight[widget].paste(slider_vert_prelight) for widget in self.widgets.values(): widget.update_style() def report_callback_exception(self, *args): err = ''.join(traceback.format_exception(*args)) logging.error(err) showerror('Exception', str(args[1]), err, parent=self) def save(self): logging.info('Save event database') data = [ev.to_dict() for ev in self.events.values()] with open(DATA_PATH, 'wb') as file: pick = Pickler(file) pick.dump(data) def update_date(self, *args): """Update Calendar's selected day and Events' list.""" self.widgets['Calendar'].update_date() self.widgets['Events'].display_evts() self.update_idletasks() # --- bindings def _select(self, event): if not self.tree.identify_row(event.y): self.tree.selection_remove(*self.tree.selection()) def _edit_on_click(self, event): sel = self.tree.selection() if sel: sel = sel[0] self.edit(sel) # --- class bindings @staticmethod def clear_selection(event): combo = event.widget combo.selection_clear() @staticmethod def select_all(event): event.widget.selection_range(0, "end") return "break" # --- show / hide def _menu_widgets_trace(self, item): self.menu_widgets.set_item_value(_(item), self.widgets[item].variable.get()) def display_hide_widget(self, item): value = self.menu_widgets.get_item_value(_(item)) if value: self.widgets[item].show() else: self.widgets[item].hide() def hide(self): self._visible.set(False) self.withdraw() self.save() def show(self): self._visible.set(True) self.deiconify() def _visibility_trace(self, *args): self.icon.menu.set_item_value(_('Manager'), self._visible.get()) def display_hide(self, toggle=False): value = self.icon.menu.get_item_value(_('Manager')) if toggle: value = not value self.icon.menu.set_item_value(_('Manager'), value) self._visible.set(value) if not value: self.withdraw() self.save() else: self.deiconify() # --- event management def event_add(self, event): self.nb += 1 iid = str(self.nb) self.events[iid] = event self.tree.insert('', 'end', iid, values=event.values()) self.tree.item(iid, tags=str(self.tree.index(iid) % 2)) self.widgets['Calendar'].add_event(event) self.widgets['Events'].display_evts() self.widgets['Tasks'].display_tasks() self.save() def event_configure(self, iid): self.tree.item(iid, values=self.events[iid].values()) self.widgets['Calendar'].add_event(self.events[iid]) self.widgets['Events'].display_evts() self.widgets['Tasks'].display_tasks() self.save() def add(self, date=None): iid = str(self.nb + 1) if date is not None: event = Event(self.scheduler, iid=iid, Start=date) else: event = Event(self.scheduler, iid=iid) Form(self, event, new=True) def delete(self, iid): index = self.tree.index(iid) self.tree.delete(iid) for k, item in enumerate(self.tree.get_children('')[index:]): tags = [ t for t in self.tree.item(item, 'tags') if t not in ['1', '0'] ] tags.append(str((index + k) % 2)) self.tree.item(item, tags=tags) self.events[iid].reminder_remove_all() self.widgets['Calendar'].remove_event(self.events[iid]) del (self.events[iid]) self.widgets['Events'].display_evts() self.widgets['Tasks'].display_tasks() self.save() def edit(self, iid): self.widgets['Calendar'].remove_event(self.events[iid]) Form(self, self.events[iid]) def check_outdated(self): """Check for outdated events every 15 min.""" now = datetime.now() for iid, event in self.events.items(): if not event['Repeat'] and event['Start'] < now: tags = list(self.tree.item(iid, 'tags')) if 'outdated' not in tags: tags.append('outdated') self.tree.item(iid, tags=tags) self.after_id = self.after(15 * 60 * 1000, self.check_outdated) def delete_outdated_events(self): now = datetime.now() outdated = [] for iid, prop in self.events.items(): if prop['End'] < now: if not prop['Repeat']: outdated.append(iid) elif prop['Repeat']['Limit'] != 'always': end = prop['End'] enddate = datetime.fromordinal( prop['Repeat']['EndDate'].toordinal()) enddate.replace(hour=end.hour, minute=end.minute) if enddate < now: outdated.append(iid) for item in outdated: self.delete(item) logging.info('Deleted outdated events') def refresh_reminders(self): """ Reschedule all reminders. Required when APScheduler is updated. """ for event in self.events.values(): reminders = [date for date in event['Reminders'].values()] event.reminder_remove_all() for date in reminders: event.reminder_add(date) logging.info('Refreshed reminders') # --- sorting def _move_item(self, item, index): self.tree.move(item, "", index) tags = [t for t in self.tree.item(item, 'tags') if t not in ['1', '0']] tags.append(str(index % 2)) self.tree.item(item, tags=tags) @staticmethod def to_datetime(date): date_format = get_date_format("short", CONFIG.get("General", "locale")).pattern dayfirst = date_format.startswith("d") yearfirst = date_format.startswith("y") return parse(date, dayfirst=dayfirst, yearfirst=yearfirst) def _sort_by_date(self, col, reverse): l = [(self.to_datetime(self.tree.set(k, col)), k) for k in self.tree.get_children('')] l.sort(reverse=reverse) # rearrange items in sorted positions for index, (val, k) in enumerate(l): self._move_item(k, index) # reverse sort next time self.tree.heading(col, command=lambda: self._sort_by_date(col, not reverse)) def _sort_by_desc(self, col, reverse): l = [(self.tree.set(k, col), k) for k in self.tree.get_children('')] l.sort(reverse=reverse, key=lambda x: x[0].lower()) # rearrange items in sorted positions for index, (val, k) in enumerate(l): self._move_item(k, index) # reverse sort next time self.tree.heading(col, command=lambda: self._sort_by_desc(col, not reverse)) # --- filter def update_filter_val(self, event): col = self.filter_col.get() self.filter_val.set("") if col: l = set() for k in self.events: l.add(self.tree.set(k, col)) self.filter_val.configure(values=tuple(l)) else: self.filter_val.configure(values=[]) self.apply_filter(event) def apply_filter(self, event): col = self.filter_col.get() val = self.filter_val.get() items = list(self.events.keys()) if not col: for item in items: self._move_item(item, int(item)) else: i = 0 for item in items: if self.tree.set(item, col) == val: self._move_item(item, i) i += 1 else: self.tree.detach(item) # --- manager's menu def _post_menu(self, event): self.right_click_iid = self.tree.identify_row(event.y) self.tree.selection_remove(*self.tree.selection()) self.tree.selection_add(self.right_click_iid) if self.right_click_iid: try: self.menu.delete(_('Progress')) except TclError: pass state = self.events[self.right_click_iid]['Task'] if state: self._task_var.set(state) if '%' in state: self._img_dot = PhotoImage(master=self, file=IM_DOT) else: self._img_dot = tkPhotoImage(master=self) self.menu_task.entryconfigure(1, image=self._img_dot) self.menu.insert_cascade(0, menu=self.menu_task, label=_('Progress')) self.menu.tk_popup(event.x_root, event.y_root) def _delete_menu(self): if self.right_click_iid: self.delete(self.right_click_iid) def _edit_menu(self): if self.right_click_iid: self.edit(self.right_click_iid) def _set_progress(self): if self.right_click_iid: self.events[self.right_click_iid]['Task'] = self._task_var.get() self.widgets['Tasks'].display_tasks() if '%' in self._task_var.get(): self._img_dot = PhotoImage(master=self, file=IM_DOT) else: self._img_dot = tkPhotoImage(master=self) self.menu_task.entryconfigure(1, image=self._img_dot) # --- icon menu def exit(self): self.save() rep = self.widgets['Pomodoro'].stop(self.widgets['Pomodoro'].on) if not rep: return self.menu_eyes.quit() self.after_cancel(self.after_id) try: self.scheduler.shutdown() except SchedulerNotRunningError: pass self.destroy() def settings(self): splash_supp = CONFIG.get('General', 'splash_supported', fallback=True) dialog = Settings(self) self.wait_window(dialog) self._setup_style() if splash_supp != CONFIG.get('General', 'splash_supported'): for widget in self.widgets.values(): widget.update_position() # --- week schedule def get_next_week_events(self): """Return events scheduled for the next 7 days """ locale = CONFIG.get("General", "locale") next_ev = {} today = datetime.now().date() for d in range(7): day = today + timedelta(days=d) evts = self.widgets['Calendar'].get_events(day) if evts: evts = [self.events[iid] for iid in evts] evts.sort(key=lambda ev: ev.get_start_time()) desc = [] for ev in evts: if ev["WholeDay"]: date = "" else: date = "%s - %s " % ( format_time(ev['Start'], locale=locale), format_time(ev['End'], locale=locale)) place = "(%s)" % ev['Place'] if place == "()": place = "" desc.append(("%s%s %s\n" % (date, ev['Summary'], place), ev['Description'])) next_ev[day.strftime('%A')] = desc return next_ev # --- tasks def get_tasks(self): # TODO: find events with repetition in the week # TODO: better handling of events on several days tasks = [] for event in self.events.values(): if event['Task']: tasks.append(event) return tasks