Beispiel #1
0
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')
Beispiel #2
0
    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()
Beispiel #5
0
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"])
Beispiel #6
0
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)
Beispiel #8
0
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
Beispiel #9
0
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)