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:

enter image description here

#!/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:

enter image description here

#!/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.