Skip to content

Commit

Permalink
Merge branch 'tray_icons'
Browse files Browse the repository at this point in the history
Add support for several system tray icon gui toolkits: increase the number of compatible desktop environments.
  • Loading branch information
j4321 committed Oct 3, 2017
2 parents da8fc79 + dc9b249 commit 3d7e901
Show file tree
Hide file tree
Showing 20 changed files with 744 additions and 309 deletions.
27 changes: 15 additions & 12 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,6 @@ for each mailbox is stored in an encrypted file using a master password.
CheckMails is designed for Linux. It is written in Python 3 and relies upon
Tk GUI toolkit.

Warning
-------

CheckMails is a system tray unread mail checker, so it does not work on
desktop environments with no system tray (like Unity). Furthermore, it relies
on tktray which is an old library, so some desktop environment system trays
are not fully compatible with it (e.g. in KDE/plasma).

Install
-------
Expand All @@ -30,9 +23,9 @@ Install
- Ubuntu

CheckMails is available in the PPA `ppa:j-4321-i/ppa`.

::

$ sudo add-apt-repository ppa:j-4321-i/ppa
$ sudo apt-get update
$ sudo apt-get install checkmails
Expand All @@ -41,12 +34,17 @@ Install

First, install the missing dependencies among:
- Tkinter (Python wrapper for Tk)
- Tktray https://code.google.com/archive/p/tktray/downloads
- libnotify and a notification server if your desktop environment does not provide one.
(see https://wiki.archlinux.org/index.php/Desktop_notifications for more details)
- PyCrypto https://pypi.python.org/pypi/pycrypto
- Pillow https://pypi.python.org/pypi/Pillow

You also need to have at least one of the following GUI toolkits for the system tray icon:
- Tktray https://code.google.com/archive/p/tktray/downloads
- PyGTK http://www.pygtk.org/downloads.html
- PyQt5, PyQt4 or PySide


For instance, in Ubuntu/Debian you will need to install the following packages:
python3-tk, tk-tktray, libnotify and the notification server of your choice,
python3-crypto, python3-pil
Expand All @@ -64,10 +62,15 @@ it from the command line with `checkmails`. In this last case, you will see
the messages printed every time a process is lauched or finished and when
an error is encountered. Therefore you can check that everything works fine.

Troubleshooting
---------------

If there is a black border around the system tray icon, you can try to
restart the panel (e.g. xfce4-panel), it should disappear (at least in xfce).
Several gui toolkits are available to display the system tray icon, so if the
icon does not behave properly, try to change toolkit, they are not all fully
compatible with every desktop environment.

If there is a problem with the font of the number of unread mails, try to change
the font from the settings.

If you encounter bugs or if you have suggestions, please open an issue on
`GitHub <https://github.com/j4321/CheckMails/issues>`__ or write me an email
Expand Down
6 changes: 6 additions & 0 deletions changelog
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ Copyright 2016 Juliette Monsel <[email protected]>
Changelog
---------

- Version 1.2.0
* Fix font settings
* Add support for several system tray icon gui toolkits:
increase the number of compatible desktop environments
* Improve style consistency

- Version 1.1.5
* Add log file ~/.checkmails/checkmails.log
* Fix setup.py so that install works
Expand Down
1 change: 0 additions & 1 deletion checkmailslib/about.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ class About(Toplevel):
def __init__(self, master):
""" créer le Toplevel 'À propos de CheckMails' """
Toplevel.__init__(self, master)

self.title(_("About CheckMails"))
self.image = PhotoImage(file=ICON_48, master=self)
Label(self, image=self.image).grid(row=0, columnspan=2, pady=10)
Expand Down
141 changes: 80 additions & 61 deletions checkmailslib/check.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@
from tkinter.messagebox import showerror, askokcancel
from tkinter.ttk import Entry, Label, Button, Style
from PIL import Image, ImageDraw, ImageFont
from checkmailslib.tktray import Icon
from checkmailslib.constants import IMAGE, ICON, IMAGE2, save_config, ICON_48
from checkmailslib.constants import encrypt, decrypt, LOCAL_PATH, CONFIG, internet_on
from checkmailslib.trayicon import TrayIcon
from checkmailslib.constants import IMAGE, ICON, IMAGE2, save_config, FONTSIZE,\
encrypt, decrypt, LOCAL_PATH, CONFIG, internet_on, TTF_FONTS, ICON_48
from checkmailslib.manager import Manager
from checkmailslib.config import Config
from checkmailslib.about import About
Expand All @@ -54,33 +54,41 @@ def __init__(self):
# icon that will show up in the taskbar for every toplevel
self.im_icon = PhotoImage(master=self, file=ICON_48)
self.iconphoto(True, self.im_icon)

# system tray icon
self.img = PhotoImage(file=IMAGE)
self.icon = Icon(self, image=self.img)
self.icon.menu.add_command(label=_("Check"), command=self.check_mails)
self.icon.menu.add_command(label=_("Reconnect"),
self.icon = TrayIcon(IMAGE)
self.icon.add_menu_item(label=_("Details"), command=self.display)
self.icon.add_menu_item(label=_("Check"), command=self.check_mails)
self.icon.add_menu_item(label=_("Reconnect"),
command=self.reconnect)
self.icon.menu.add_command(label=_("Suspend"), command=self.start_stop)
self.icon.menu.add_separator()
self.icon.menu.add_command(label=_("Change password"),
self.icon.add_menu_item(label=_("Suspend"), command=self.start_stop)
self.icon.add_menu_separator()
self.icon.add_menu_item(label=_("Change password"),
command=self.change_password)
self.icon.menu.add_command(label=_("Reset password"),
self.icon.add_menu_item(label=_("Reset password"),
command=self.reset_password)
self.icon.menu.add_separator()
self.icon.menu.add_command(label=_("Manage mailboxes"),
self.icon.add_menu_separator()
self.icon.add_menu_item(label=_("Manage mailboxes"),
command=self.manage_mailboxes)
self.icon.menu.add_command(label=_("Preferences"), command=self.config)
self.icon.menu.add_separator()
self.icon.menu.add_command(label=_("Check for updates"),
self.icon.add_menu_item(label=_("Preferences"), command=self.config)
self.icon.add_menu_separator()
self.icon.add_menu_item(label=_("Check for updates"),
command=lambda: UpdateChecker(self, True))
self.icon.menu.add_command(label=_("About"),
self.icon.add_menu_item(label=_("About"),
command=lambda: About(self))
self.icon.menu.add_separator()
self.icon.menu.add_command(label=_("Quit"), command=self.quit)
self.icon.bind('<Button-1>', self.display)
self.icon.add_menu_separator()
self.icon.add_menu_item(label=_("Quit"), command=self.quit)
self.icon.loop(self)
self.icon.bind_left_click(self.display)

self.style = Style(self)
self.style.theme_use('clam')
bg = self.cget("background")
self.style.configure("TLabel", background=bg)
self.style.configure("TFrame", background=bg)
self.style.configure("TButton", background=bg)
self.style.configure("TCheckbutton", background=bg)
self.style.configure("TMenubutton", background=bg)
self.style.map('TCheckbutton',
indicatorbackground=[('pressed', '#dcdad5'),
('!disabled', 'alternate', 'white'),
Expand Down Expand Up @@ -118,37 +126,43 @@ def __init__(self):
if CONFIG.getboolean("General", "check_update"):
UpdateChecker(self)

# replace Ctrl+A binding by select all for all entries
self.bind_class("TEntry", "<Control-a>", self.select_all_entry)

def select_all_entry(self, event):
event.widget.selection_range(0, "end")

def report_callback_exception(self, *args):
"""Log exceptions."""
err = "".join(traceback.format_exception(*args))
logging.error(err)

def start_stop(self):
"""Suspend checks."""
if self.icon.menu.entrycget(2, "label") == _("Suspend"):
self.icon.after_cancel(self.check_id)
self.icon.after_cancel(self.timer_id)
self.icon.after_cancel(self.notif_id)
self.icon.after_cancel(self.internet_id)
self.img.configure(file=IMAGE)
self.icon.menu.entryconfigure(2, label=_("Restart"))
self.icon.menu.entryconfigure(0, state="disabled")
self.icon.menu.entryconfigure(1, state="disabled")
if self.icon.get_item_label(3) == _("Suspend"):
self.after_cancel(self.check_id)
self.after_cancel(self.timer_id)
self.after_cancel(self.notif_id)
self.after_cancel(self.internet_id)
self.icon.change_icon(IMAGE, "checkmails suspended")
self.icon.set_item_label(3, _("Restart"))
self.icon.disable_item(1)
self.icon.disable_item(2)
else:
self.icon.menu.entryconfigure(2, label=_("Suspend"))
self.icon.menu.entryconfigure(0, state="normal")
self.icon.menu.entryconfigure(1, state="normal")
self.icon.set_item_label(3, _("Suspend"))
self.icon.enable_item(1)
self.icon.enable_item(2)
self.reconnect()

def reconnect(self):
self.icon.after_cancel(self.check_id)
self.after_cancel(self.check_id)
self.nb_unread = {box: 0 for box in self.info_conn}
for box in self.boxes:
self.logout(box, True, True)
self.check_id = self.icon.after(20000, self.launch_check, False)
self.check_id = self.after(20000, self.launch_check, False)

def display(self, event):
if self.icon.menu.entrycget(2, "label") == _("Suspend"):
def display(self):
if self.icon.get_item_label(3) == _("Suspend"):
notif = self.notif
if not notif:
notif = _("Checking...")
Expand All @@ -165,9 +179,9 @@ def reset_conn(self):
self.threads_connect = {}
self.threads_reconnect = {}
self.threads_check = {}
self.icon.after_cancel(self.timer_id)
self.icon.after_cancel(self.check_id)
self.icon.after_cancel(self.notif_id)
self.after_cancel(self.timer_id)
self.after_cancel(self.check_id)
self.after_cancel(self.notif_id)
self.get_info_conn()

def get_info_conn(self):
Expand All @@ -192,36 +206,37 @@ def get_info_conn(self):
if not self.info_conn:
self.notif = _("No active mailbox")
run(["notify-send", "-i", IMAGE2, _("No active mailbox"), _("Use the mailbox manager to configure a mailbox.")])
elif self.icon.menu.entrycget(2, "label") == _("Suspend"):
elif self.icon.get_item_label(3) == _("Suspend"):
self.notif = ""
for box in self.info_conn:
self.connect(box)
self.icon.after_cancel(self.check_id)
self.after_cancel(self.check_id)
self.check_id = self.after(20000, self.launch_check, False)

def change_icon(self, nbmail):
"""Display the number of unread mails nbmail in the system tray icon."""
nb = "%i" % nbmail
im = Image.open(IMAGE)
draw = ImageDraw.Draw(im)
font_path = CONFIG.get("General", "font")
W, H = im.size
draw = ImageDraw.Draw(im)
font_path = TTF_FONTS[CONFIG.get("General", "font")]
try:
font = ImageFont.truetype(font_path, 10)
font = ImageFont.truetype(font_path, FONTSIZE)
w, h = draw.textsize(nb, font=font)
draw.text(((W - w) / 2, (H - h) / 2), nb, fill=(255, 0, 0), font=font)
draw.text(((W - w) / 2, (H - h) / 2), nb, fill=(255, 0, 0),
font=font)
except OSError:
w, h = draw.textsize(nb)
draw.text(((W - w) / 2, (H - h) / 2), nb, fill=(255, 0, 0))
im.save(ICON)
self.img.configure(file=ICON)
self.icon.change_icon(ICON, "checkmails %s" % nb)

def config(self):
"""Open config dialog to set times and language."""
Config(self)
self.time = CONFIG.getint("General", "time")
self.timeout = CONFIG.getint("General", "timeout")
if self.icon.menu.entrycget(2, "label") == _("Suspend"):
if self.icon.get_item_label(3) == _("Suspend"):
self.check_mails(False)

def manage_mailboxes(self):
Expand Down Expand Up @@ -299,10 +314,10 @@ def connect_mailbox(self, box):
run(["notify-send", "-i", "dialog-error", _("Error"),
_("No internet connection.")])
# cancel everything
self.icon.after_cancel(self.check_id)
self.icon.after_cancel(self.timer_id)
self.icon.after_cancel(self.notif_id)
self.icon.after_cancel(self.internet_id)
self.after_cancel(self.check_id)
self.after_cancel(self.timer_id)
self.after_cancel(self.notif_id)
self.after_cancel(self.internet_id)
# periodically checks if the internet connection is turned on
self.internet_id = self.after(self.timeout, self.test_connection)
else:
Expand All @@ -324,7 +339,7 @@ def test_connection(self):
if internet_on():
self.reset_conn()
else:
self.internet_id = self.icon.after(self.timeout, self.test_connection)
self.internet_id = self.after(self.timeout, self.test_connection)

def logout_mailbox(self, box, reconnect):
"""
Expand Down Expand Up @@ -373,8 +388,8 @@ def launch_check(self, force_notify=False):
b = [self.threads_connect[box].isAlive() for box in self.threads_connect]
if len(b) < len(self.info_conn) or True in b:
logging.info("Waiting for connexion ...")
self.icon.after_cancel(self.check_id)
self.check_id = self.icon.after(20000, self.launch_check, force_notify)
self.after_cancel(self.check_id)
self.check_id = self.after(20000, self.launch_check, force_notify)
else:
logging.info("Launching check")
self.check_mails(force_notify)
Expand Down Expand Up @@ -417,17 +432,17 @@ def check_mails(self, force_notify=True):
display a notification even if there is no unread mail.
"""
self.notif = _("Checking...") + "\n"
self.icon.after_cancel(self.timer_id)
self.icon.after_cancel(self.notif_id)
self.after_cancel(self.timer_id)
self.after_cancel(self.notif_id)
for box, mail in self.boxes.items():
if not self.threads_connect[box].isAlive() and (box not in self.threads_check or not self.threads_check[box].isAlive()):
self.threads_check[box] = Thread(target=self.check_mailbox,
name='check_' + box,
daemon=True,
args=(box,))
self.threads_check[box].start()
self.notif_id = self.icon.after(20000, self.notify_unread_mails, force_notify)
self.timer_id = self.icon.after(self.time, self.check_mails, False)
self.notif_id = self.after(20000, self.notify_unread_mails, force_notify)
self.timer_id = self.after(self.time, self.check_mails, False)

def notify_unread_mails(self, force_notify=True):
"""
Expand All @@ -438,11 +453,14 @@ def notify_unread_mails(self, force_notify=True):
"""
b = [self.threads_check[box].isAlive() for box in self.threads_check]
if len(b) < len(self.info_conn) or True in b:
self.notif_id = self.icon.after(20000, self.notify_unread_mails, force_notify)
self.notif_id = self.after(20000, self.notify_unread_mails, force_notify)
else:
if self.notif != _("Checking...") + "\n":
self.notif = self.notif[:-2].split("\n")[1]
run(["notify-send", "-i", IMAGE2, _("Unread mails"), self.notif])
try:
self.notif = self.notif[:-2].split("\n")[1]
run(["notify-send", "-i", IMAGE2, _("Unread mails"), self.notif])
except IndexError:
run(["notify-send", "-i", IMAGE2, _("Unread mails"), self.notif])
elif force_notify:
run(["notify-send", "-i", IMAGE2, _("Unread mails"), _("No unread mail")])
self.notif = _("No unread mail")
Expand All @@ -458,6 +476,7 @@ def quit(self):
for box in self.info_conn:
self.logout(box)
try:
self.after_cancel(self.loop_id)
self.destroy()
except TclError:
# depending on the pending processes when the app is destroyed
Expand Down
Loading

0 comments on commit 3d7e901

Please sign in to comment.