Fork me on GitHub
6/11/2006

Создание апплета GNOME: часть вторая (из трех)

Продолжу рассказ о создании апплета к панели GNOME. В прошлый раз я написал "костяк" апплета, который ничего не делает. Сегодня на его основе напишу апплет, который уже чем-то будет полезен.

GConf

Напомню, что конечная цель - создание апплета для переключения прокси (вкл/выкл). Для этих целей есть диалог "Параметры прокси-серверов" (Система->Параметры->Сервис Прокси), но им не удобно пользоваться, поскольку нужно совершать много лишних действий.

Сам диалог настройки ничего не делает, он лишь изменяет значение соответствующего параметра в GConf (/system/proxy/mode), а программы, использующие прокси, следят за значением этого параметра. Поэтому, для того чтобы включить или выключить прокси, апплету достаточно изменить нужное значение в GConf.

У GConf есть как положительные, так и отрицательные стороны. Из положительных хочу отметить такие приятные вещи как:

  • информирование всех "подписанных" на параметр программ об изменении его значения (внешне это выглядит так: открываете диалог "Параметры прокси-серверов", меняете при помощи Python значение нужного параметра и переключатель в диалоге сам "прыгает" на нужную позицию)
  • глобальность изменения: не нужно думать о том, что нужно еще проставить значение переменной http_proxy для wget - GConf делает это автоматически. Ну а то, что все GNOME-программы берут настройки о использовании/не использовании прокси из GConf, я думаю, понятно без объяснений.

Из отрицательных я выделяю:

  • gconfd "гадит" в журналы. Мне не нравится, что gconfd пишет в общесистемный журнал (/var/log/messages) на русском языке.
  • несогласованность настроек различных программ. Например, Gajim и Firefox используют собственные настройки соединений. И если для Gajim можно найти оправдание (поддержка нескольких аккаунтов), то для Firefox я не вижу причин игнорировать GConf. Хотя для Gajim адекватен был бы выбор между "использовать глобальные настройки Gnome" и "использовать отдельные настройки для соединений".
  • невозможность (или просто я не знаю о таком) изменить настройки только для отдельных приложений

Прежде чем писать код, советую "поиграться" в интерактивной сессии (в качестве индикатора о смене настроек можно использовать диалог "Параметры прокси-сервера", либо редактор `gconf-editor`, но на мой взгляд, диалог прокси более показателен)

>>> import gconf
>>> gc = gconf.client_get_default()
>>> gc.get_string('/system/proxy/mode')
'none'

>>> gc.set_string('/system/proxy/mode', 'manual')
True
>>> gc.get_string()
'manual'

Что происходит, если попытаться получить значение несуществующего ключа, или извлечь значение не того типа, предлагаю изучить самостоятельно, вооружившись gconf-editor и Python.

Что касается возможных значений ключа '/system/proxy/mode', то допустимые значения здесь таковы: none, manual, auto. Если значение не входит в список разрешенных, оно интерпретируется как none.

Последняя оговорка и можно приступать к написанию кода: для того, чтобы GConf сообщал об изменении того или иного ключа, нужно "подгрузить" одну из веток GConf для "прослушки" и добавить callback-функцию на изменение нужного ключа.

В коде изложено всё вышесказанное:

class ProxyGconfClient(object):
    """Get/set proxy states"""
    proxy_dir = "/system/proxy"

    proxy_key = "/system/proxy/mode"
    on_state = 'manual'
    off_state = 'none'

    def __init__(self, callback=None):
        """
        GConf client for getting/setting proxy states

        @param callback: callback function. Executing
            when proxy state changed. It calls with params:
              * client - GConf client
              * cnxn_id - connection ID
              * entry - changed entry
              * params - additional params
        @type callback: callable
        """
        if callback is None:
            callback = lambda client, cnxn_id, entry, params: None
        # make connection to GConfD
        self.client = gconf.client_get_default()
        # add proxy_dir for inspection, without preload
        self.client.add_dir(self.proxy_dir,
                            gconf.CLIENT_PRELOAD_NONE)
        # add callback for notifying about changes
        self.client.notify_add(self.proxy_key,
                               callback)

    def get_state(self):
        """Returns state of proxy"""
        return self.client.get_string(self.proxy_key)

    def set_state(self, value):
        """

        Set state of proxy

        @param value: state of proxy, may be
          * 'none' - direct connection, proxy off
          * 'manual' - manual settings, proxy on
          * 'auto' - auto settings, proxy on 
          if value neither 'manual', no 'auto', it means
          direct connection, i.e. proxy off.
        @raise RuntimeError: cannot set value to GConf's key
        """
        if not self.client.set_string(self.proxy_key,
                                      value):
            raise RuntimeError("Unable to change key %s" % \
                               self.proxy_key)

    def on(self):
        """Turn proxy on (i.e. set proxy mode 'manual')"""
        self.set_state(self.on_state)

    def is_on(self):
        """Is proxy on? (i.e. proxy in 'manual' mode)"""
        return self.get_state() == self.on_state

    def off(self):
        """Turn proxy off (i.e. set direct connection)"""

        self.set_state(self.off_state)

Заполнение скелета

Собственно для реализации работающего апплета почти всё готово: скелет апплета, объект-переключатель -

class ProxyGnomeApplet(GnomeAppletSkeleton):

    def after_init(self):
        self.proxy = ProxyGconfClient(callback=self._cb_proxy_change)
	self.proxy_state = self.proxy.get_state()
        self.button_actions[1] = self.switch_proxy
        self.label.set_text(self.proxy_state)

    def _cb_proxy_change(self, client, cnxn_id, entry, params):
        """Callback for changing proxy"""
        self.proxy_state = self.proxy.get_state()
        self.label.set_text(self.proxy_state)

    def on_enter(self, widget, event):
        info = "Proxy mode: %s" % self.proxy_state
        self.tooltips.set_tip(self.ev_box, info)

    def switch_proxy(self):
        if self.proxy.is_on():
            self.proxy.off()
        else:
            self.proxy.on()

Поясняю написанное:

Во-первых, напоминаю, что after_init специально создавался в скелете для переопределения в потомках, так что это правильное место для добавления прокси-переключателя (атрибут proxy), определения действия на левую кнопку мыши (button_actions[1]) и установки начального текста для label.

Во-вторых, в качестве callback-функции, которая выполняется при смене состояния ключа GConf, я использую _cb_proxy_change (сигнатура этой функции такова: GConf-клиент, идентификатор соединения, измененный ключ, дополнительные параметры). По идее, идеологически более правильно здесь использовать конструкцию entry.get_value().get_string(), но мне не нравится такой стиль записи, он не Pythonic. Поэтому я использую информацию от объекта прокси-переключателя.

Далее, переопределенная callback-функция on_enter, теперь она показывает состояние прокси, а не просто "Привет мир".

Ну и последний метод - switch_proxy - выполняется по нажатию левой кнопки, переключает состояние прокси.

Действия по регистрации этого, уже работающего, апплета абсолютно аналогичны таковым для скелета, так что я не описываю их. Рабочий код можете взять отсюда.

Фактически, апплет уже функционирует, однако выглядит он не притязательно. В следующей части буду "шлифовать" внешний вид.

Комментарии

Все статьи