def style_for_warning_entry() -> None: """ Функция создания стиля для пустого Entry :return: """ style = Style() try: style.element_create('plain.field', 'from', 'clam') except TclError as error: if str(error) == 'Duplicate element plain.field': pass style.layout('Warning.TEntry', [('Entry.plain.field', { 'children': [('Entry.background', { 'children': [('Entry.padding', { 'children': [('Entry.textarea', { 'sticky': 'nswe' })], 'sticky': 'nswe' })], 'sticky': 'nswe' })], 'border': '2', 'sticky': 'nswe' })]) style.configure('Warning.TEntry', fieldbackground='#FFA3AD')
def __init__(self): Tk.__init__(self) self.title("BSE Kafka Tool") self.set_window_center(900, 500) self.resizable(True, True) self.win_success = None # Treeview style style = Style() style.element_create("Custom.Treeheading.border", "from", "default") style.layout("Custom.Treeview.Heading", [ ("Custom.Treeheading.cell", { 'sticky': 'nswe' }), ("Custom.Treeheading.border", { 'sticky': 'nswe', 'children': [("Custom.Treeheading.padding", { 'sticky': 'nswe', 'children': [("Custom.Treeheading.text", { 'sticky': 'we' })] })] }), ]) style.configure("Custom.Treeview.Heading", background="grey", foreground="white", relief="flat") configuration_window.ConfigurationWindow()
def __init__(self): global root self.master = Toplevel(root) self.master.withdraw() self.master.protocol('WM_DELETE_WINDOW', root.destroy) self.master.iconbitmap(imgdir) self.master.geometry("400x150") self.master.resizable(False, False) self.master.title("Adb & Fastboot Installer - By @Pato05") estyle = Style() estyle.element_create("plain.field", "from", "clam") estyle.layout("White.TEntry", [('Entry.plain.field', {'children': [( 'Entry.background', {'children': [( 'Entry.padding', {'children': [( 'Entry.textarea', {'sticky': 'nswe'})], 'sticky': 'nswe'})], 'sticky': 'nswe'})], 'border': '4', 'sticky': 'nswe'})]) estyle.configure("White.TEntry", background="white", foreground="black", fieldbackground="white") window = Frame(self.master, relief=FLAT) window.pack(padx=10, pady=5, fill=BOTH) Label(window, text='Installation path:').pack(fill=X) self.syswide = IntVar() self.instpath = StringVar() self.e = Entry(window, state='readonly', textvariable=self.instpath, style='White.TEntry') self.e.pack(fill=X) self.toggleroot() Label(window, text='Options:').pack(pady=(10, 0), fill=X) inst = Checkbutton(window, text="Install Adb and Fastboot system-wide?", variable=self.syswide, command=self.toggleroot) inst.pack(fill=X) self.path = IntVar(window, value=1) Checkbutton(window, text="Put Adb and Fastboot in PATH?", variable=self.path).pack(fill=X) Button(window, text='Install', command=self.install).pack(anchor='se') self.master.deiconify()
Frame with Image replace the image with your own image, if the screen size is large enough, about 1000 wide by 350 high. ''' from tkinter import Tk, PhotoImage from tkinter.ttk import Frame, Style, Label, Entry from PIL import Image, ImageTk root = Tk() s = Style() im = Image.open('../images/BountyMoorea.jpg') # change to your own file tkim = ImageTk.PhotoImage(im) width,height = im.size s.element_create("ship", "image", tkim) s.layout("ship", [("ship", {"sticky": "nsew"})]) fr=Frame(root, style="ship", height=height,width=width) fr.grid(column=0, row=1, sticky='nsew') il = Label(fr, text= 'Label') il.place(relx=0.1, rely=0.1, anchor='center') # using place to position widget en = Entry(fr, width=15) en.place(relx=0.1, rely=0.15, anchor='center') root.mainloop()
class App: def __init__(self): self.root = Tk() self.s = Style() self.fr = Frame(self.root) self.fr.grid(sticky='nsew') self.img = PhotoImage("frameBorder", data=""" R0lGODlhPwA/APcAAAAAAJWVlby8vMTExAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAP8ALAAAAAA/AD8A AAj/AP8JHEiwoMGDCBMqXMiwocOHECNKnEixokWGATJq3Mixo8ePF/9pDLlw48SRJB2iVBkgpcSM LF2ebIlRJkWaCmHafIkTYc+dEH8aFAq0IVGBAnQWjRhAwMGkS086NQg1KtOpBatafdj06dGtPrES 1AoWo9iBZMvmPIv0q1qCXam6fSswbta5dO2OxftWL1q+av22pVuS7b+0hAsKPgy47GLEiQc+bgx2 cuSwXi8ftKxZsWHIlzl3lvyZ8lbRo0WWTk06M2vVrlmjHj27c23Nt0Ovfp07cu/EvwkH7zvZ9NKM pY0XRf40qXKbyA0zfi6TOUIBznE3ld5WaV7rCbGbNQysETtD7M5XLi9v3iH6j/Djy4/OXSH69PPz e0Qf8r7//wAG+J9LAxRo4IEIJqjggq81CFZAADs= """) self.img1 = PhotoImage("frameFocusBorder", data=""" R0lGODlhPwA/APcAAAAAAP9jR5WVlby8vMTExAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAP8ALAAAAAA/AD8A AAj/AP8JHEiwoMGDCBMqXMiwocOHECNKnEixokWGAjJq3Mixo8ePF/9pDECypMmTKFOm3DhxZICQ C0tqhJiRJMyHJDM6rPnyJs4AOjHa9AkxJ0YBPYkWDZoQqdKJQBc6fRoxKsIBNalKBDrgINakWnEK 6Grwa9iqY71OPeuQq1qwbGOmLbs2rlyyBc3aZeiWLty9B/vmrQs48NzBfwsTFExQr2LDeBsTfjyQ 8UDHlBcflpyYsmWBmDML/PwvtGjSpjOjnqx682XWnl2Dhv14defaskvTVmxbdMHevivnTh078uvb vIfvLgw8+L/mwaH7ln5aOXLm1p2Pzq6demvjs6/vaQWqfLld8uB1m4+L3ivW9WHRp1c/tHb7q+/r A845dv5snsyRl1tj7/Ek3kX8ZTSAf5ctyJFKEEao0kYLMpiXgx9lqOGG/VmIH4YchvhRhSFVaOKJ KKaoIok3EeDiizDGKOOMNGpno2IBAQA7 """) self.s.element_create("RoundedFrame", "image", "frameBorder", ("focus", "frameFocusBorder"), border=16, sticky="nsew") self.s.layout("RoundedFrame", [("RoundedFrame", {"sticky": "nsew"})]) self.build() def build(self): self.rbs = [] self.rbs1 = [] self.lF0 = lF0 = LabelFrame(self.fr, text='Widget and Themes') lF0.grid(row=0, column=0, sticky='nw') self.fr1 = fr1 = Frame(lF0) fr1.grid(row=0, column=0, sticky='nw') # create check box to select reverse selection order self.lF12 = lF12 = LabelFrame(fr1, text='Select Widget before Theme') lF12.grid(row=0, column=0, sticky='nw') self.ord = ord = IntVar() ord.set(0) cbut3 = Checkbutton(lF12, text='Reverse selection order', variable=ord, command=self.selord) cbut3.grid(row=0, column=0, padx=5, pady=5) cbut3.state(['!selected']) # create a Combobox to choose widgets widget_sel = ['Button', 'Checkbutton', 'Combobox', 'Entry', 'Frame', 'Label', 'LabelFrame', 'Menubutton', 'Notebook', 'PanedWindow', 'Progressbar', 'Radiobutton', 'Scale', 'Scrollbar', 'Separator', 'Sizegrip', 'Treeview'] ord = self.ord self.lf6 = LabelFrame(self.fr1, text='Select Widget', style="RoundedFrame", padding=(10,1,10,10)) self.lf6.grid(row=1, column=0, sticky='nw') self.lf6.state([("focus" if self.ord.get() == 0 else "!focus")]) self.widget_value = StringVar() self.cb = Combobox(self.lf6, values=widget_sel, textvariable=self.widget_value, state= ('disabled' if self.ord.get()==1 else 'active')) self.cb.grid(row=0, column=0, padx=5, pady=5, sticky='nw') self.cb.bind('<<ComboboxSelected>>', self.enabled) # create a Radio Buttons to choose orientation fr2 = Frame(self.lF0) fr2.grid(row=0, column=1, sticky='nw') self.lF5 = lF5 = LabelFrame(fr2, style="RoundedFrame", padding=(10,1,10,10), text='Orientation of \nProgressbar \nScale \nScrollbar') lF5.grid(row=0, column=0, padx=5, pady=5, sticky='nw') self.orient = StringVar() orientT = ['Horizontal', 'Vertical'] for ix, val in enumerate(orientT): rb = Radiobutton(lF5, text=val, value=val, command=self.orient_command, variable=self.orient, state='disabled') rb.grid(row=ix, column=0, sticky='w') self.rbs.append(rb) # create Radio Buttons to choose themes themes = {"alt": "alt - standard", "clam": "clam - standard", "classic": "classic - standard", "default": "default - standard"} self.lF1 = LabelFrame(self.fr1, text='Select Theme', style="RoundedFrame", padding=(10,1,10,10)) self.lF1.grid(row=2, column=0, sticky='n') self.theme_value = StringVar() for ix, val in enumerate(themes): rb1 = Radiobutton(self.lF1, text=themes[val], value=val, state='disabled', variable=self.theme_value, command=self.theme_command) rb1.grid(row=ix, column=0, padx=10, sticky='nw') self.rbs1.append(rb1) def enabled(self, event): # from widget selection self.lf6.state(["!focus"]) if self.ord.get() == 0: if self.widget_value.get() in ('Progressbar', 'Scale', 'Scrollbar'): self.lF5.state(["focus"]) for opt in self.rbs: opt.state(['!disabled']) for opt in self.rbs1: opt.state(['disabled']) else: for opt in self.rbs1: opt.state(['!disabled']) self.lF1.state(["focus"]) self.lF1['text'] = 'Select Theme' self.theme_value.set(None) if self.ord.get() == 1: self.lf6['text'] = 'Widget' if self.widget_value.get() in ('Progressbar', 'Scale', 'Scrollbar'): self.lF5.state(["focus"]) for opt in self.rbs: opt.state(['!disabled']) def orient_command(self): # from orient selection self.lF5.state(["!focus"]) if self.ord.get() == 0: try: for opt in self.rbs1: opt.state(['!disabled']) self.lF1.state(["focus"]) self.theme_value.set(None) self.lF1['text'] = 'Select Theme' except (NameError, AttributeError): pass def theme_command(self): # from theme selection self.s.theme_use(self.theme_value.get()) self.lF1.state(["!focus"]) if self.ord.get() == 0: self.lF1['text'] = 'Theme' if self.ord.get() == 1: self.cb.state(['!disabled']) self.lF1['text'] = 'Theme' self.lf6.state(["focus"]) def selord(self): # from select ord if self.ord.get() == 0: self.lf6.state(["focus"]) self.lF12['text'] = 'Select widget before theme' self.theme_value.set(None) self.orient.set(None) self.cb.set('') self.lF1.state(["!focus"]) self.lF5.state(["!focus"]) if self.ord.get() == 1: self.lF12['text'] = 'Select theme before widget' self.cb.state(['disabled']) for opt in self.rbs1: opt.state(['!disabled']) self.lF1.state(["focus"]) self.theme_value.set(None) self.orient.set(None) self.cb.set('') self.lf6.state(["!focus"]) self.lF5.state(["!focus"])
fPH71Xeef/kFyB93/sln4EP2Ebjegg31B5+CEDLUIH4PVqiQhOABqKFCF6qn 34cHcfjffCQaFOJtGaZYkIkUuljQigXK+CKCE3po40A0trgjjDru+EGPI/6I Y4co7kikkAMBmaSNSzL5gZNSDjkghkXaaGIBHjwpY4gThJeljFt2WSWYMQpZ 5pguUnClehS4tuMEDARQgH8FBMBBBExGwIGdAxywXAUBKHCZkAIoEEAFp33W QGl47ZgBAwZEwKigE1SQgAUCUDCXiwtQIIAFCTQwgaCrZeCABAzIleIGHDD/ oIAHGUznmXABGMABT4xpmBYBHGgAKGq1ZbppThgAG8EEAW61KwYMSOBAApdy pNp/BkhAAQLcEqCTt+ACJW645I5rLrgEeOsTBtwiQIEElRZg61sTNBBethSw CwEA/Pbr778ABywwABBAgAAG7xpAq6mGUUTdAPZ6YIACsRKAAbvtZqzxxhxn jDG3ybbKFHf36ZVYpuE5oIGhHMTqcqswvyxzzDS/HDMHEiiggQMLDxCZXh8k BnEBCQTggAUGGKCB0ktr0PTTTEfttNRQT22ABR4EkEABDXgnGUEn31ZABglE EEAAWaeN9tpqt832221HEEECW6M3wc+Hga3SBgtMODBABw00UEEBgxdO+OGG J4744oZzXUEDHQxwN7F5G7QRdXxPoPkAnHfu+eeghw665n1vIKhJBQUEADs=""") style = Style() # img1 is frameFocusBorder, img2 is frameBorder - cross reference style.element_create("RoundedFrame", "image", "frameBorder", ("focus", "frameFocusBorder"), border=16, sticky="nsew") style.layout("RoundedFrame", [("RoundedFrame", {"sticky": "nsew"})]) style.configure("TEntry", borderwidth=0) # general handle widget class Entry frame = Frame(style="RoundedFrame", padding=10) frame.pack(fill='x') frame2 = Frame(style="RoundedFrame", padding=10) frame2.pack(fill='both', expand=1) entry = Entry(frame, text='Test') entry.pack(fill='x') entry.bind("<FocusIn>", lambda evt: frame.state(["focus"])) entry.bind("<FocusOut>", lambda evt: frame.state(["!focus"]))
cKxRC4ngeILiH8Qkk0cCAUzSDZWpzbLEE1EwggcYqWCj2DNADFDAAQUgIAAAEFDDJmPYqNJF F1s4cscTmCDjDTjdSPOHBQggUAEQDAgggTWDPoYMJkFoUdRmddyyjWLeULMMMcAsIw0x4wkM IME1g25zyxpHxFYUHmyIggw4H4ojITnfiLMNMAkcAAub4BQjihRdDGTJHmvc4Qo1wD6Imje6 eILbj+BQ4wqu5Q3ECSJ0FOKKMtv4mBg33Pw4zjbKuBIIE1xYpIkhdQQiyi7OtAucj6dt48wu otQhBRa6VvSJIRwhIkotvgRTzMUYZ6xxMcj4QkspeKDxxRhEmUfIHWjAgQcijEDissuXvCyz zH7Q8YQURxDhUsn/bCInR3AELfTQZBRt9BBJkCGFFVhMwTNBlnBCSCGEIJQQIAklZMXWRBAR RRRWENHwRQEBADs=""" # the images are combined gif data s1 = PhotoImage("search1", data=data, format="gif -index 0") s2 = PhotoImage("search2", data=data, format="gif -index 1") style = Style() style.theme_use('default') style.element_create("Search.field", "image", "search1", ("focus", "search2"), border=[22, 7, 14], sticky="ew") style.layout("Search.entry", [ ("Search.field", {"sticky": "nswe", "border": 1, "children": [("Entry.padding", {"sticky": "nswe", "children": [("Entry.textarea", {"sticky": "nswe"})] })] })] ) style.configure("Search.entry", background="#b2b2b2", font=font.nametofont("TkDefaultFont")) root.configure(background="#b2b2b2") e1 = Entry(style="Search.entry", width=20)
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
class App(Tk): def __init__(self): Tk.__init__(self, className=cst.APP_NAME) self.protocol("WM_DELETE_WINDOW", self.quit) self.withdraw() logging.info('Starting %s', cst.APP_NAME) self.im_icon = PhotoImage(master=self, file=cst.IM_ICON_48) self.iconphoto(True, self.im_icon) # --- style self.style = Style(self) self.style.theme_use("clam") self.style.configure("TScale", sliderlength=20) self.style.map("TCombobox", fieldbackground=[('readonly', 'white')], selectbackground=[('readonly', 'white')], selectforeground=[('readonly', 'black')]) self.style.configure("title.TLabel", font="TkDefaultFont 9 bold") self.style.configure("white.TLabel", background="white") self.style.map("white.TLabel", background=[("active", "white")]) self.style.configure('heading.TLabel', relief='ridge', borderwidth=1, padding=(10, 4)) self.style.configure('manager.TButton', padding=0) self.style.map('manager.Treeview', background=[], foreground=[]) self.style.layout( 'no_edit.TEntry', [('Entry.padding', { 'children': [('Entry.textarea', { 'sticky': 'nswe' })], 'sticky': 'nswe' })]) self.style.configure('no_edit.TEntry', background='white', padding=[4, 0]) self.style.configure('manager.TEntry', padding=[2, 1]) self.style.layout('manager.Treeview.Row', [('Treeitem.row', { 'sticky': 'nswe' }), ('Treeitem.image', { 'side': 'right', 'sticky': 'e' })]) self.style.layout('manager.Treeview.Item', [('Treeitem.padding', { 'children': [('Checkbutton.indicator', { 'side': 'left', 'sticky': '' }), ('Treeitem.text', { 'side': 'left', 'sticky': '' })], 'sticky': 'nswe' })]) self._im_trough = tkPhotoImage(name='trough-scrollbar-vert', width=15, height=15, master=self) bg = CONFIG.get("Widget", 'background', fallback='gray10') widget_bg = (0, 0, 0) widget_fg = (255, 255, 255) vmax = self.winfo_rgb('white')[0] color = tuple(int(val / vmax * 255) for val in widget_bg) active_bg = cst.active_color(color) active_bg2 = cst.active_color(cst.active_color(color, 'RGB')) slider_vert_insens = Image.new('RGBA', (13, 28), widget_bg) 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.put(" ".join(["{" + " ".join([bg] * 15) + "}"] * 15)) self._im_slider_vert_active = PhotoImage(slider_vert_active, name='slider-vert-active', master=self) self._im_slider_vert = PhotoImage(slider_vert, name='slider-vert', master=self) self._im_slider_vert_prelight = PhotoImage(slider_vert_prelight, name='slider-vert-prelight', master=self) self._im_slider_vert_insens = PhotoImage(slider_vert_insens, name='slider-vert-insens', master=self) self.style.element_create('widget.Vertical.Scrollbar.trough', 'image', 'trough-scrollbar-vert') self.style.element_create( 'widget.Vertical.Scrollbar.thumb', 'image', 'slider-vert', ('pressed', '!disabled', 'slider-vert-active'), ('active', '!disabled', 'slider-vert-prelight'), ('disabled', 'slider-vert-insens'), border=6, sticky='ns') self.style.layout('widget.Vertical.TScrollbar', [ ('widget.Vertical.Scrollbar.trough', { 'children': [('widget.Vertical.Scrollbar.thumb', { 'expand': '1' })], 'sticky': 'ns' }) ]) hide = Image.new('RGBA', (12, 12), active_bg2) hide_active = Image.new('RGBA', (12, 12), widget_fg) hide_pressed = Image.new('RGBA', (12, 12), (150, 0, 0)) toggle_open = Image.new('RGBA', (9, 9), widget_fg) toggle_open_active = Image.new('RGBA', (9, 9), active_bg2) toggle_close = Image.new('RGBA', (9, 9), widget_fg) toggle_close_active = Image.new('RGBA', (9, 9), active_bg2) self._im_hide = PhotoImage(hide, master=self) self._im_hide_active = PhotoImage(hide_active, master=self) self._im_hide_pressed = PhotoImage(hide_pressed, master=self) self._im_open = PhotoImage(toggle_open, master=self) self._im_open_active = PhotoImage(toggle_open_active, master=self) self._im_close = PhotoImage(toggle_close, master=self) self._im_close_active = PhotoImage(toggle_close_active, master=self) self.style.element_create( "toggle", "image", self._im_close, ("!hover", "selected", "!disabled", self._im_open), ("hover", "!selected", "!disabled", self._im_close_active), ("hover", "selected", "!disabled", self._im_open_active), border=2, sticky='') self.style.layout('Toggle', [('Toggle.border', { 'children': [('Toggle.padding', { 'children': [('Toggle.toggle', { 'sticky': 'nswe' })], 'sticky': 'nswe' })], 'sticky': 'nswe' })]) self.style.configure('widget.close.TButton', background=bg, relief='flat', image=self._im_hide, padding=0) self.style.map('widget.close.TButton', background=[], relief=[], image=[('active', '!pressed', self._im_hide_active), ('active', 'pressed', self._im_hide_pressed)]) self.option_add('*Toplevel.background', self.style.lookup('TFrame', 'background')) self.option_add('*{app_name}.background'.format(app_name=cst.APP_NAME), self.style.lookup('TFrame', 'background')) self.widget_style_init() # --- tray icon menu self.icon = TrayIcon(cst.ICON) self.menu_widgets = SubMenu(parent=self.icon.menu) self.menu_categories = SubMenu(parent=self.menu_widgets) self.menu_categories.add_command(label=_('Hide all'), command=self.hide_all_cats) self.menu_categories.add_command(label=_('Show all'), command=self.hide_all_cats) self.menu_categories.add_separator() self.menu_feeds = SubMenu(parent=self.menu_widgets) self.menu_feeds.add_command(label=_('Hide all'), command=self.hide_all_feeds) self.menu_feeds.add_command(label=_('Show all'), command=self.show_all_feeds) self.menu_feeds.add_separator() self.menu_widgets.add_command(label=_('Hide all'), command=self.hide_all) self.menu_widgets.add_command(label=_('Show all'), command=self.show_all) self.menu_widgets.add_separator() self.menu_widgets.add_cascade(label=_('Categories'), menu=self.menu_categories) self.menu_widgets.add_cascade(label=_('Feeds'), menu=self.menu_feeds) self.icon.menu.add_cascade(label=_('Widgets'), menu=self.menu_widgets) self.icon.menu.add_command(label=_('Add feed'), command=self.add) self.icon.menu.add_command(label=_('Update feeds'), command=self.feed_update) self.icon.menu.add_command(label=_('Manage feeds'), command=self.feed_manage) self.icon.menu.add_command(label=_("Suspend"), command=self.start_stop) self.icon.menu.add_separator() self.icon.menu.add_command(label=_('Settings'), command=self.settings) self.icon.menu.add_command(label=_("Check for updates"), command=lambda: UpdateChecker(self, True)) self.icon.menu.add_command(label=_("Help"), command=lambda: Help(self)) self.icon.menu.add_command(label=_("About"), command=lambda: About(self)) self.icon.menu.add_command(label=_('Quit'), command=self.quit) self.icon.loop(self) self._notify_no_internet = True self._internet_id = "" self._update_id = "" self._check_add_id = "" self._check_end_update_id = "" self._check_result_update_id = {} self._check_result_init_id = {} self.queues = {} self.threads = {} # --- category widgets self.cat_widgets = {} self.cat_widgets['All'] = CatWidget(self, 'All') self.cat_widgets['All'].event_generate('<Configure>') self.menu_widgets.add_checkbutton(label=_('Latests'), command=self.toggle_latests_widget) cst.add_trace(self.cat_widgets['All'].variable, 'write', self.latests_widget_trace) self.cat_widgets['All'].variable.set( LATESTS.getboolean('All', 'visible')) cats = LATESTS.sections() cats.remove('All') for category in cats: self.cat_widgets[category] = CatWidget(self, category) self.cat_widgets[category].event_generate('<Configure>') self.menu_categories.add_checkbutton( label=category, command=lambda c=category: self.toggle_category_widget(c)) cst.add_trace(self.cat_widgets[category].variable, 'write', lambda *args, c=category: self.cat_widget_trace(c)) self.cat_widgets[category].variable.set( LATESTS.getboolean(category, 'visible')) # --- feed widgets self.feed_widgets = {} for title in FEEDS.sections(): self._check_result_update_id[title] = '' self._check_result_init_id[title] = '' self.queues[title] = Queue(1) self.threads[title] = None self.menu_feeds.add_checkbutton( label=title, command=lambda t=title: self.toggle_feed_widget(t)) self.feed_widgets[title] = FeedWidget(self, title) cst.add_trace(self.feed_widgets[title].variable, 'write', lambda *args, t=title: self.feed_widget_trace(t)) self.feed_widgets[title].variable.set( FEEDS.getboolean(title, 'visible', fallback=True)) self.feed_init() # --- check for updates if CONFIG.getboolean("General", "check_update"): UpdateChecker(self) self.bind_class('TEntry', '<Control-a>', self.entry_select_all) def widget_style_init(self): """Init widgets style.""" bg = CONFIG.get('Widget', 'background', fallback='gray10') feed_bg = CONFIG.get('Widget', 'feed_background', fallback='gray20') fg = CONFIG.get('Widget', 'foreground') vmax = self.winfo_rgb('white')[0] widget_bg = tuple(int(val / vmax * 255) for val in self.winfo_rgb(bg)) widget_fg = tuple(int(val / vmax * 255) for val in self.winfo_rgb(fg)) active_bg = cst.active_color(widget_bg) active_bg2 = cst.active_color(cst.active_color(widget_bg, 'RGB')) slider_alpha = Image.open(cst.IM_SCROLL_ALPHA) slider_vert_insens = Image.new('RGBA', (13, 28), widget_bg) slider_vert = Image.new('RGBA', (13, 28), active_bg) slider_vert.putalpha(slider_alpha) slider_vert_active = Image.new('RGBA', (13, 28), widget_fg) slider_vert_active.putalpha(slider_alpha) slider_vert_prelight = Image.new('RGBA', (13, 28), active_bg2) slider_vert_prelight.putalpha(slider_alpha) self._im_slider_vert_active.paste(slider_vert_active) self._im_slider_vert.paste(slider_vert) self._im_slider_vert_prelight.paste(slider_vert_prelight) self._im_slider_vert_insens.paste(slider_vert_insens) self._im_trough.put(" ".join(["{" + " ".join([bg] * 15) + "}"] * 15)) hide_alpha = Image.open(cst.IM_HIDE_ALPHA) hide = Image.new('RGBA', (12, 12), active_bg) hide.putalpha(hide_alpha) hide_active = Image.new('RGBA', (12, 12), active_bg2) hide_active.putalpha(hide_alpha) hide_pressed = Image.new('RGBA', (12, 12), widget_fg) hide_pressed.putalpha(hide_alpha) toggle_open_alpha = Image.open(cst.IM_OPENED_ALPHA) toggle_open = Image.new('RGBA', (9, 9), widget_fg) toggle_open.putalpha(toggle_open_alpha) toggle_open_active = Image.new('RGBA', (9, 9), active_bg2) toggle_open_active.putalpha(toggle_open_alpha) toggle_close_alpha = Image.open(cst.IM_CLOSED_ALPHA) toggle_close = Image.new('RGBA', (9, 9), widget_fg) toggle_close.putalpha(toggle_close_alpha) toggle_close_active = Image.new('RGBA', (9, 9), active_bg2) toggle_close_active.putalpha(toggle_close_alpha) self._im_hide.paste(hide) self._im_hide_active.paste(hide_active) self._im_hide_pressed.paste(hide_pressed) self._im_open.paste(toggle_open) self._im_open_active.paste(toggle_open_active) self._im_close.paste(toggle_close) self._im_close_active.paste(toggle_close_active) self.style.configure('widget.TFrame', background=bg) self.style.configure('widget.close.TButton', background=bg) # relief='flat', image=self._im_hide, padding=0) # self.style.map('widget.close.TButton', background=[], relief=[], # image=[('active', '!pressed', self._im_hide_active), # ('active', 'pressed', self._im_hide_pressed)]) self.style.configure('widget.interior.TFrame', background=feed_bg) self.style.configure('widget.TSizegrip', background=bg) self.style.configure('widget.Horizontal.TSeparator', background=bg) self.style.configure('widget.TLabel', background=bg, foreground=fg, font=CONFIG.get('Widget', 'font')) self.style.configure('widget.title.TLabel', background=bg, foreground=fg, font=CONFIG.get('Widget', 'font_title')) self.style.configure('widget.TButton', background=bg, foreground=fg, padding=1, relief='flat') self.style.map('widget.TButton', background=[('disabled', active_bg), ('pressed', fg), ('active', active_bg)], foreground=[('pressed', bg)]) # relief=[('pressed', 'sunken')]) # bordercolor=[('pressed', active_bg)], # darkcolor=[('pressed', bg)], # lightcolor=[('pressed', fg)]) self.update_idletasks() def hide_all(self): """Withdraw all widgets.""" for widget in self.cat_widgets.values(): widget.withdraw() for widget in self.feed_widgets.values(): widget.withdraw() def show_all(self): """Deiconify all widgets.""" for widget in self.cat_widgets.values(): widget.deiconify() for widget in self.feed_widgets.values(): widget.deiconify() def hide_all_feeds(self): """Withdraw all feed widgets.""" for widget in self.feed_widgets.values(): widget.withdraw() def show_all_feeds(self): """Deiconify all feed widgets.""" for widget in self.feed_widgets.values(): widget.deiconify() def hide_all_cats(self): """Withdraw all category widgets.""" for cat, widget in self.cat_widgets.items(): if cat != 'All': widget.withdraw() def show_all_cats(self): """Deiconify all category widgets.""" for cat, widget in self.cat_widgets.items(): if cat != 'All': widget.deiconify() def start_stop(self): """Suspend / restart update checks.""" if self.icon.menu.get_item_label(4) == _("Suspend"): after_ids = [ self._update_id, self._check_add_id, self._internet_id, self._check_end_update_id, self._update_id ] after_ids.extend(self._check_result_update_id.values()) after_ids.extend(self._check_result_init_id.values()) for after_id in after_ids: try: self.after_cancel(after_id) except ValueError: pass self.icon.menu.set_item_label(4, _("Restart")) self.icon.menu.disable_item(1) self.icon.menu.disable_item(2) self.icon.change_icon(cst.ICON_DISABLED, 'feedagregator suspended') else: self.icon.menu.set_item_label(4, _("Suspend")) self.icon.menu.enable_item(1) self.icon.menu.enable_item(2) self.icon.change_icon(cst.ICON, 'feedagregator') for widget in self.feed_widgets.values(): widget.clear() self.feed_init() @staticmethod def entry_select_all(event): event.widget.selection_clear() event.widget.selection_range(0, 'end') def test_connection(self): """ Launch update check if there is an internet connection otherwise check again for an internet connection after 30s. """ if cst.internet_on(): logging.info('Connected to Internet') self._notify_no_internet = True for widget in self.feed_widgets.values(): widget.clear() self.feed_init() else: self._internet_id = self.after(30000, self.test_connection) def quit(self): for after_id in self.tk.call('after', 'info'): try: self.after_cancel(after_id) except ValueError: pass for thread in self.threads.values(): try: thread.terminate() except AttributeError: pass for title, widget in self.feed_widgets.items(): FEEDS.set(title, 'visible', str(widget.variable.get())) for cat, widget in self.cat_widgets.items(): LATESTS.set(cat, 'visible', str(widget.variable.get())) try: self.destroy() except TclError: logging.error("Error on quit") self.after(500, self.quit) def feed_widget_trace(self, title): value = self.feed_widgets[title].variable.get() self.menu_feeds.set_item_value(title, value) FEEDS.set(title, 'visible', str(value)) cst.save_feeds() def cat_widget_trace(self, category): value = self.cat_widgets[category].variable.get() self.menu_categories.set_item_value(category, value) LATESTS.set(category, 'visible', str(value)) cst.save_latests() def latests_widget_trace(self, *args): value = self.cat_widgets['All'].variable.get() self.menu_widgets.set_item_value(_('Latests'), value) LATESTS.set('All', 'visible', str(value)) cst.save_latests() def toggle_category_widget(self, category): value = self.menu_categories.get_item_value(category) if value: self.cat_widgets[category].deiconify() else: self.cat_widgets[category].withdraw() self.update_idletasks() def toggle_latests_widget(self): value = self.menu_widgets.get_item_value(_('Latests')) if value: self.cat_widgets['All'].deiconify() else: self.cat_widgets['All'].withdraw() self.update_idletasks() def toggle_feed_widget(self, title): value = self.menu_feeds.get_item_value(title) if value: self.feed_widgets[title].deiconify() else: self.feed_widgets[title].withdraw() self.update_idletasks() def report_callback_exception(self, *args): """Log exceptions.""" err = "".join(traceback.format_exception(*args)) logging.error(err) showerror(_("Error"), str(args[1]), err, True) def settings(self): update_delay = CONFIG.get('General', 'update_delay') splash_supp = CONFIG.get('General', 'splash_supported', fallback=True) dialog = Config(self) self.wait_window(dialog) cst.save_config() self.widget_style_init() splash_change = splash_supp != CONFIG.get('General', 'splash_supported') for widget in self.cat_widgets.values(): widget.update_style() if splash_change: widget.update_position() for widget in self.feed_widgets.values(): widget.update_style() if splash_change: widget.update_position() if update_delay != CONFIG.get('General', 'update_delay'): self.feed_update() def add(self): dialog = Add(self) self.wait_window(dialog) url = dialog.url self.feed_add(url) def category_remove(self, category): self.cat_widgets[category].destroy() del self.cat_widgets[category] self.menu_categories.delete(category) LATESTS.remove_section(category) cst.save_feeds() cst.save_latests() @staticmethod def feed_get_info(url, queue, mode='latest'): feed = feedparser.parse(url) feed_title = feed['feed'].get('title', '') entries = feed['entries'] today = datetime.now().strftime('%Y-%m-%d %H:%M') if entries: entry_title = entries[0].get('title', '') summary = entries[0].get('summary', '') link = entries[0].get('link', '') latest = """<p id=title>{}</p>\n{}""".format(entry_title, summary) if 'updated' in entries[0]: updated = entries[0].get('updated') else: updated = entries[0].get('published', today) updated = dateutil.parser.parse( updated, tzinfos=cst.TZINFOS).strftime('%Y-%m-%d %H:%M') else: entry_title = "" summary = "" link = "" latest = "" updated = today if mode == 'all': data = [] for entry in entries: title = entry.get('title', '') summary = entry.get('summary', '') if 'updated' in entry: date = entry.get('updated') else: date = entry.get('published', today) date = dateutil.parser.parse( date, tzinfos=cst.TZINFOS).strftime('%Y-%m-%d %H:%M') link = entry.get('link', '') data.append((title, date, summary, link)) queue.put((feed_title, latest, updated, data)) else: queue.put( (feed_title, latest, updated, entry_title, summary, link)) def _check_result_add(self, thread, queue, url, manager_queue=None): if thread.is_alive(): self._check_add_id = self.after(1000, self._check_result_add, thread, queue, url, manager_queue) else: title, latest, date, data = queue.get(False) if title: try: # check if feed's title already exists FEEDS.add_section(title) except configparser.DuplicateSectionError: i = 2 duplicate = True while duplicate: # increment i until title~#i does not already exist try: FEEDS.add_section("{}~#{}".format(title, i)) except configparser.DuplicateSectionError: i += 1 else: duplicate = False name = "{}~#{}".format(title, i) else: name = title if manager_queue is not None: manager_queue.put(name) logging.info("Added feed '%s' %s", name, url) if CONFIG.getboolean("General", "notifications", fallback=True): run([ "notify-send", "-i", cst.IM_ICON_SVG, name, cst.html2text(latest) ]) self.cat_widgets['All'].entry_add(name, date, latest, url) filename = cst.new_data_file() cst.save_data(filename, latest, data) FEEDS.set(name, 'url', url) FEEDS.set(name, 'updated', date) FEEDS.set(name, 'data', filename) FEEDS.set(name, 'visible', 'True') FEEDS.set(name, 'geometry', '') FEEDS.set(name, 'position', 'normal') FEEDS.set(name, 'category', '') FEEDS.set(name, 'sort_is_reversed', 'False') FEEDS.set(name, 'active', 'True') cst.save_feeds() self.queues[name] = queue self.feed_widgets[name] = FeedWidget(self, name) self.menu_feeds.add_checkbutton( label=name, command=lambda: self.toggle_feed_widget(name)) cst.add_trace(self.feed_widgets[name].variable, 'write', lambda *args: self.feed_widget_trace(name)) self.feed_widgets[name].variable.set(True) for entry_title, date, summary, link in data: self.feed_widgets[name].entry_add(entry_title, date, summary, link, -1) else: if manager_queue is not None: manager_queue.put('') if cst.internet_on(): logging.error('%s is not a valid feed.', url) showerror(_('Error'), _('{url} is not a valid feed.').format(url=url)) else: logging.warning('No Internet connection.') showerror(_('Error'), _('No Internet connection.')) def feed_add(self, url, manager=False): """ Add feed with given url. manager: whether this command is run from the feed manager. """ if url: queue = Queue(1) manager_queue = Queue(1) if manager else None thread = Process(target=self.feed_get_info, args=(url, queue, 'all'), daemon=True) thread.start() self._check_result_add(thread, queue, url, manager_queue) if manager: return manager_queue def feed_set_active(self, title, active): FEEDS.set(title, 'active', str(active)) cst.save_feeds() cat = FEEDS.get(title, 'category', fallback='') if active: self.menu_feeds.enable_item(title) if FEEDS.getboolean(title, 'visible'): self.feed_widgets[title].deiconify() if cat != '': self.cat_widgets[cat].show_feed(title) self.cat_widgets['All'].show_feed(title) self._feed_update(title) else: self.menu_feeds.disable_item(title) self.feed_widgets[title].withdraw() if cat != '': self.cat_widgets[cat].hide_feed(title) self.cat_widgets['All'].hide_feed(title) def feed_change_cat(self, title, old_cat, new_cat): if old_cat != new_cat: FEEDS.set(title, 'category', new_cat) if old_cat != '': self.cat_widgets[old_cat].remove_feed(title) if new_cat != '': if new_cat not in LATESTS.sections(): LATESTS.add_section(new_cat) LATESTS.set(new_cat, 'visible', 'True') LATESTS.set(new_cat, 'geometry', '') LATESTS.set(new_cat, 'position', 'normal') LATESTS.set(new_cat, 'sort_order', 'A-Z') self.cat_widgets[new_cat] = CatWidget(self, new_cat) self.cat_widgets[new_cat].event_generate('<Configure>') self.menu_categories.add_checkbutton( label=new_cat, command=lambda: self.toggle_category_widget(new_cat)) cst.add_trace(self.cat_widgets[new_cat].variable, 'write', lambda *args: self.cat_widget_trace(new_cat)) self.cat_widgets[new_cat].variable.set(True) else: try: filename = FEEDS.get(title, 'data') latest = cst.feed_get_latest(filename) except (configparser.NoOptionError, pickle.UnpicklingError): latest = '' self.cat_widgets[new_cat].entry_add( title, FEEDS.get(title, 'updated'), latest, FEEDS.get(title, 'url')) def feed_rename(self, old_name, new_name): options = { opt: FEEDS.get(old_name, opt) for opt in FEEDS.options(old_name) } FEEDS.remove_section(old_name) try: # check if feed's title already exists FEEDS.add_section(new_name) except configparser.DuplicateSectionError: i = 2 duplicate = True while duplicate: # increment i until new_name~#i does not already exist try: FEEDS.add_section("{}~#{}".format(new_name, i)) except configparser.DuplicateSectionError: i += 1 else: duplicate = False name = "{}~#{}".format(new_name, i) else: name = new_name logging.info("Renamed feed '%s' to '%s'", old_name, name) for opt, val in options.items(): FEEDS.set(name, opt, val) self._check_result_init_id[name] = self._check_result_init_id.pop( old_name, '') self._check_result_update_id[name] = self._check_result_update_id.pop( old_name, '') self.threads[name] = self.threads.pop(old_name, None) self.queues[name] = self.queues.pop(old_name) self.feed_widgets[name] = self.feed_widgets.pop(old_name) self.feed_widgets[name].rename_feed(name) self.cat_widgets['All'].rename_feed(old_name, name) category = FEEDS.get(name, 'category', fallback='') if category != '': self.cat_widgets[category].rename_feed(old_name, name) self.menu_feeds.delete(old_name) self.menu_feeds.add_checkbutton( label=name, command=lambda: self.toggle_feed_widget(name)) trace_info = cst.info_trace(self.feed_widgets[name].variable) if trace_info: cst.remove_trace(self.feed_widgets[name].variable, 'write', trace_info[0][1]) cst.add_trace(self.feed_widgets[name].variable, 'write', lambda *args: self.feed_widget_trace(name)) self.menu_feeds.set_item_value(name, self.feed_widgets[name].variable.get()) cst.save_feeds() return name def feed_remove(self, title): self.feed_widgets[title].destroy() del self.queues[title] try: del self.threads[title] except KeyError: pass del self.feed_widgets[title] try: del self._check_result_init_id[title] except KeyError: pass try: del self._check_result_update_id[title] except KeyError: pass try: os.remove(os.path.join(cst.PATH_DATA, FEEDS.get(title, 'data'))) except FileNotFoundError: pass self.menu_feeds.delete(title) logging.info("Removed feed '%s' %s", title, FEEDS.get(title, 'url')) category = FEEDS.get(title, 'category', fallback='') self.cat_widgets['All'].remove_feed(title) if category != '': self.cat_widgets[category].remove_feed(title) FEEDS.remove_section(title) def feed_manage(self): dialog = Manager(self) self.wait_window(dialog) self.update_idletasks() cst.save_latests() if dialog.change_made: cst.save_feeds() self.feed_update() def feed_init(self): """Update feeds.""" for title in FEEDS.sections(): if FEEDS.getboolean(title, 'active', fallback=True): logging.info("Updating feed '%s'", title) self.threads[title] = Process(target=self.feed_get_info, args=(FEEDS.get(title, 'url'), self.queues[title], 'all'), daemon=True) self.threads[title].start() self._check_result_init(title) self._check_end_update_id = self.after(2000, self._check_end_update) def _check_result_init(self, title): if self.threads[title].is_alive(): self._check_result_init_id[title] = self.after( 1000, self._check_result_init, title) else: t, latest, updated, data = self.queues[title].get() if not t: if cst.internet_on(): run([ "notify-send", "-i", "dialog-error", _("Error"), _('{url} is not a valid feed.').format( url=FEEDS.get(title, 'url')) ]) logging.error('%s is not a valid feed.', FEEDS.get(title, 'url')) else: if self._notify_no_internet: run([ "notify-send", "-i", "dialog-error", _("Error"), _('No Internet connection.') ]) logging.warning('No Internet connection') self._notify_no_internet = False self._internet_id = self.after(30000, self.test_connection) after_ids = [ self._update_id, self._check_add_id, self._check_end_update_id, self._update_id ] after_ids.extend(self._check_result_update_id.values()) after_ids.extend(self._check_result_init_id.values()) for after_id in after_ids: try: self.after_cancel(after_id) except ValueError: pass else: date = datetime.strptime(updated, '%Y-%m-%d %H:%M') if (date > datetime.strptime(FEEDS.get(title, 'updated'), '%Y-%m-%d %H:%M') or not FEEDS.has_option(title, 'data')): if CONFIG.getboolean("General", "notifications", fallback=True): run([ "notify-send", "-i", cst.IM_ICON_SVG, title, cst.html2text(latest) ]) FEEDS.set(title, 'updated', updated) category = FEEDS.get(title, 'category', fallback='') self.cat_widgets['All'].update_display( title, latest, updated) if category != '': self.cat_widgets[category].update_display( title, latest, updated) logging.info("Updated feed '%s'", title) self.feed_widgets[title].clear() for entry_title, date, summary, link in data: self.feed_widgets[title].entry_add( entry_title, date, summary, link, -1) logging.info("Populated widget for feed '%s'", title) self.feed_widgets[title].event_generate('<Configure>') self.feed_widgets[title].sort_by_date() try: filename = FEEDS.get(title, 'data') except configparser.NoOptionError: filename = cst.new_data_file() FEEDS.set(title, 'data', filename) cst.save_feeds() cst.save_data(filename, latest, data) else: logging.info("Feed '%s' is up-to-date", title) def _feed_update(self, title): """Update feed with given title.""" logging.info("Updating feed '%s'", title) self.threads[title] = Process(target=self.feed_get_info, args=(FEEDS.get(title, 'url'), self.queues[title]), daemon=True) self.threads[title].start() self._check_result_update(title) def feed_update(self): """Update all feeds.""" try: self.after_cancel(self._update_id) except ValueError: pass for thread in self.threads.values(): try: thread.terminate() except AttributeError: pass self.threads.clear() for title in FEEDS.sections(): if FEEDS.getboolean(title, 'active', fallback=True): self._feed_update(title) self._check_end_update_id = self.after(2000, self._check_end_update) def _check_result_update(self, title): if self.threads[title].is_alive(): self._check_result_update_id[title] = self.after( 1000, self._check_result_update, title) else: t, latest, updated, entry_title, summary, link = self.queues[ title].get(False) if not t: if cst.internet_on(): run([ "notify-send", "-i", "dialog-error", _("Error"), _('{url} is not a valid feed.').format( url=FEEDS.get(title, 'url')) ]) logging.error('%s is not a valid feed.', FEEDS.get(title, 'url')) else: if self._notify_no_internet: run([ "notify-send", "-i", "dialog-error", _("Error"), _('No Internet connection.') ]) logging.warning('No Internet connection') self._notify_no_internet = False self._internet_id = self.after(30000, self.test_connection) after_ids = [ self._update_id, self._check_add_id, self._check_end_update_id, self._update_id ] after_ids.extend(self._check_result_update_id.values()) after_ids.extend(self._check_result_init_id.values()) for after_id in after_ids: try: self.after_cancel(after_id) except ValueError: pass else: date = datetime.strptime(updated, '%Y-%m-%d %H:%M') if date > datetime.strptime(FEEDS.get(title, 'updated'), '%Y-%m-%d %H:%M'): logging.info("Updated feed '%s'", title) if CONFIG.getboolean("General", "notifications", fallback=True): run([ "notify-send", "-i", cst.IM_ICON_SVG, title, cst.html2text(latest) ]) FEEDS.set(title, 'updated', updated) category = FEEDS.get(title, 'category', fallback='') self.cat_widgets['All'].update_display( title, latest, updated) if category != '': self.cat_widgets[category].update_display( title, latest, updated) self.feed_widgets[title].entry_add(entry_title, updated, summary, link, 0) self.feed_widgets[title].sort_by_date() try: filename = FEEDS.get(title, 'data') old, data = cst.load_data(filename) except pickle.UnpicklingError: cst.save_data(filename, latest, [(entry_title, updated, summary, link)]) except configparser.NoOptionError: filename = cst.new_data_file() FEEDS.set(title, 'data', filename) cst.save_data(filename, latest, [(entry_title, updated, summary, link)]) else: data.insert(0, (entry_title, updated, summary, link)) cst.save_data(filename, latest, data) else: logging.info("Feed '%s' is up-to-date", title) def _check_end_update(self): b = [t.is_alive() for t in self.threads.values() if t is not None] if sum(b): self._check_end_update_id = self.after(1000, self._check_end_update) else: cst.save_feeds() for widget in self.cat_widgets.values(): widget.sort() self._update_id = self.after( CONFIG.getint("General", "update_delay"), self.feed_update)