How can I write a dynamically updated panel app / indicator?
Solution 1:
Since what seems to be the occasion to ask this question already has an answer, I am answering this question as an extended explanation on how it was done (in python
)
Basic static indicator
Since Ubuntu Mate, from 15,10, supports indicators, there is not much difference between writing an indicator and a panel app for Mate. Therefore, this link is a good starting point for a basic indicator in python
, using the AppIndicator3
API. The link is a nice start, but does not provide any information on how to show text on the indicator, let alone how to update the text (or icon). Nevertheless, with a few additions, this leads to a basic "frame" of an indicator as below. It will show an icon, a text label and a menu:
#!/usr/bin/env python3
import signal
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('AppIndicator3', '0.1')
from gi.repository import Gtk, AppIndicator3
class Indicator():
def __init__(self):
self.app = 'test123'
iconpath = "/opt/abouttime/icon/indicator_icon.png"
self.indicator = AppIndicator3.Indicator.new(
self.app, iconpath,
AppIndicator3.IndicatorCategory.OTHER)
self.indicator.set_status(AppIndicator3.IndicatorStatus.ACTIVE)
self.indicator.set_menu(self.create_menu())
self.indicator.set_label("1 Monkey", self.app)
def create_menu(self):
menu = Gtk.Menu()
# menu item 1
item_1 = Gtk.MenuItem('Menu item')
# item_about.connect('activate', self.about)
menu.append(item_1)
# separator
menu_sep = Gtk.SeparatorMenuItem()
menu.append(menu_sep)
# quit
item_quit = Gtk.MenuItem('Quit')
item_quit.connect('activate', self.stop)
menu.append(item_quit)
menu.show_all()
return menu
def stop(self, source):
Gtk.main_quit()
Indicator()
signal.signal(signal.SIGINT, signal.SIG_DFL)
Gtk.main()
In the line AppIndicator3.IndicatorCategory.OTHER
, the category is defined, as explaned in this (partially outdated) link. Setting the right category is important, a.o. to put the indicator in an appropriate position in the panel.
The main challenge; how to update the indicator text and/or icon
The real challenge is not how to write a basic indicator, but how to periodically update the text and/or icon of your indicator, since you want to have it show the (textual) time. To make the indicator work properly, we cannot simply use threading
to start a second process to periodically update the interface. Well, actually we can, but on a longer run, it will lead to conflicts, as I found out.
Here is where GObject
comes in, to, as it is put in this (also outdated) link:
call gobject.threads_init()
at applicaiton initialization. Then you launch your threads normally, but make sure the threads never do any GUI tasks directly. Instead, you use gobject.idle_add
to schedule GUI task to executed in the main thread
When we replace gobject.threads_init()
by GObject.threads_init()
and gobject.idle_add
by GObject.idle_add()
, we pretty much have the updated version of how to run threads in a Gtk
application. A simplified example, showing an increasing number of Monkeys:
#!/usr/bin/env python3
import signal
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('AppIndicator3', '0.1')
from gi.repository import Gtk, AppIndicator3, GObject
import time
from threading import Thread
class Indicator():
def __init__(self):
self.app = 'test123'
iconpath = "/opt/abouttime/icon/indicator_icon.png"
self.indicator = AppIndicator3.Indicator.new(
self.app, iconpath,
AppIndicator3.IndicatorCategory.OTHER)
self.indicator.set_status(AppIndicator3.IndicatorStatus.ACTIVE)
self.indicator.set_menu(self.create_menu())
self.indicator.set_label("1 Monkey", self.app)
# the thread:
self.update = Thread(target=self.show_seconds)
# daemonize the thread to make the indicator stopable
self.update.setDaemon(True)
self.update.start()
def create_menu(self):
menu = Gtk.Menu()
# menu item 1
item_1 = Gtk.MenuItem('Menu item')
# item_about.connect('activate', self.about)
menu.append(item_1)
# separator
menu_sep = Gtk.SeparatorMenuItem()
menu.append(menu_sep)
# quit
item_quit = Gtk.MenuItem('Quit')
item_quit.connect('activate', self.stop)
menu.append(item_quit)
menu.show_all()
return menu
def show_seconds(self):
t = 2
while True:
time.sleep(1)
mention = str(t)+" Monkeys"
# apply the interface update using GObject.idle_add()
GObject.idle_add(
self.indicator.set_label,
mention, self.app,
priority=GObject.PRIORITY_DEFAULT
)
t += 1
def stop(self, source):
Gtk.main_quit()
Indicator()
# this is where we call GObject.threads_init()
GObject.threads_init()
signal.signal(signal.SIGINT, signal.SIG_DFL)
Gtk.main()
That's the principle. In the actual indicator in this answer, both the loop time and the indicator text were determined by a secondary module, imported in the script, but the main idea is the same.