Fork me on GitHub
26/11/2006

Создание апплета GNOME: заключительная часть

Заканчиваем с апплетом к GNOME. В первых двух частях работали над структурой апплета и его наполнением, сегодня завершающий этап: "отшлифовка" внешнего вида и подготовка к многоязычному окружению.

Внешний вид

Общая структура апплета аналогична таковой во второй части: наследуемся от ProxyGnomeApplet:

class ProxySwitcherGnomeApplet(ProxyGnomeApplet)

И переопределяем нужные методы. Во-первых, роль главного виджета играет gtk.Image, а не gtk.Label как в "модельном" апплете.

def init_additional_widgets(self):
    """Create additional widgets"""
    self._init_pixbufs()
    self.image = gtk.Image()
    self.ev_box.add(self.image)

gtk.Image это некий "контейнер" изображения. Его можно "заполнить" данными из различных источников, мы будем использовать пиксельный буфер (pixbuf). Само изображение (как пиксельный буфер) будем брать из значка "Прокси" текущей темы. Если в текущей теме не будет такой иконки - будем использовать тему Tango.

def _init_pixbufs(self):
    """Init pixbufs from current theme, or from Tango, if 'proxy' icon not in current theme"""
    self.pixbufs = {}
    self.theme = self._get_theme()
    try:
        self._reload_pixbufs()
    except gobject.GError:
        self.theme = self._get_theme('Tango')
        self._reload_pixbufs()

Мы будем использовать словарь pixbufs для хранения пиксельных буферов для включенного и выключенного состояния прокси. В первой строке мы инициализируем этот словарь. Во второй - получаем текущую тему. Потом пробуем подгрузить значок "Прокси" из текущей темы (self._reload_pixbufs()), если такого значка нет (исключение gobject.GError), то используем тему Tango и уже с нее загружаем значок.

Название текущей темы берем из GConf, ключ /desktop/gnome/interface/icon_theme:

def _get_theme(self, name=None):
    """Return a theme by name, or current one if name is None"""
    if name is None:
        name = gconf.client_get_default().get_string('/desktop/gnome/interface/icon_theme')
    theme = gtk.IconTheme()
    theme.set_custom_theme(name)
    return theme

В методе _reload_pixbufs мы решаем сразу несколько задач:

  • подгружаем значок для включенного прокси (как пиксельный буфер) из текущей темы

  • на основе полученного изображения формируем пиксельный буфер для выключенного прокси

def _reload_pixbufs(self, size=None):
       """Reload pixbufs from current theme for specified size, or for panel's size if size is None"""
       if size is None:
           size = self.applet.get_size()
       pixbuf = self.theme.load_icon('proxy', size, gtk.ICON_LOOKUP_FORCE_SVG)
       faded_pixbuf = gtk.gdk.Pixbuf(pixbuf.get_colorspace(),
                   pixbuf.get_has_alpha(),
                   pixbuf.get_bits_per_sample(),
                   pixbuf.get_width(),
                   pixbuf.get_height())
       pixbuf.saturate_and_pixelate(faded_pixbuf, 1, True)
       self.pixbufs[True] = pixbuf
       self.pixbufs[False] = faded_pixbuf

И вот в этом коде четко проявляется, что PyGTK - лишь "прослойка" между Python и C-библиотекой GTK: для того чтобы получить "затемненный" пиксельный буфер (faded_pixbuf) нужно воспользоваться методом saturate_and_pixelate объекта gtk.gdk.Pixbuf, причем метод ничего не возвращает, а "затемненный" пиксельный буфер должен быть передан первым параметром. Что еще более не типично для Python - он обязательно должен быть типа gtk.gdk.Pixbuf. Т.е. нельзя, скажем, инициализировав новый пиксельный буфер значением None, передать его методу saturate_and_pixelate - будет ошибка несовпадения типов. Еще один момент - объект gtk.gdk.Pixbuf не получиться скопировать при помощи copy.deepcopy() - опять таки по причине C-природы GTK. Поэтому приходится абсолютно неестественно для Python создавать новый пиксельный буфер, передавая конструктору gtk.gdk.Pixbuf параметры исходного пиксельного буфера. И уже этот, новый пиксельный буфер, "отдавать" saturate_and_pixelate.

Все остальное в этом методе достаточно просто: в самом начале, если не передан параметр size, то берем размер панели, на которую помещается данный апплет (self.applet.get_size()); а в самом конце сохраняем полученные пиксельные буферы в словарь pixbufs.

Теперь нужно переопределить методы after_init где инициализируется начальное состояние апплета и callback-функции _cb_proxy_change на переключение прокси и on_enter на попадание курсора мыши внутрь апплета. Ну и неплохо было бы изменить диалог "О программе", переопределив on_ppm_about.

def after_init(self):
    """Init additional attributes of applet"""
    self.proxy = ProxyGconfClient(callback=self._cb_proxy_change)
    self.proxy_state = self.proxy.get_state()
    self.proxy_is_on = self.proxy.is_on()
    self.set_visual_state(self.proxy_state, self.proxy_is_on)
    self.button_actions[1] = self.switch_proxy

Метод after_init повторяет таковой у класса ProxyGnomeApplet за небольшими изменениями: дополнительно в атрибут proxy_is_on записываем данные, включен ли прокси; визуальное состояние апплета устанавливается методом set_visual_state.

def set_visual_state(self, state, is_on):
    """Set overall visual state for corresponding proxy's state"""
    msg_on_state = u"Proxy is on"
    msg_off_state = u"Proxy is off"
    mode = u"mode: %s" % state
    variant = (is_on and msg_on_state) or msg_off_state
    self.info = u"%s (%s)" % (variant, mode)
    self._set_image(is_on)

def _set_image(self, kind):
    """Set image for specified state"""
    self.image.set_from_pixbuf(self.pixbufs[kind])

Здесь код незамысловат: в начале формируются строки всплывающей подсказки, в зависимости от значения параметра is_on выбирается текст "Proxy is on" или "Proxy is off". Дополнительно, в скобках отображается режим (параметр state). Последняя строка - установка соответствующего значка (метод _set_image). Ну а в методе _set_image - заполнение контейнера gtk.Image данными из пиксельного буфера. Какой пиксельный буфер (из двух, что хранятся в self.pixbufs) использовать определяет переданный параметр kind.

Следующая пара методов, который нужно переопределить, это _cb_proxy_change и on_enter - callback-функции на переключение прокси и на попадание указателя мыши в апплет. Тут очень просто и понятно:

def _cb_proxy_change(self, client, cnxn_id, entry, params):
    """Callback for changing proxy, change visual state of applet"""
    self.proxy_state = self.proxy.get_state()
    self.proxy_is_on = self.proxy.is_on()
    self.set_visual_state(self.proxy_state, self.proxy_is_on)

def on_enter(self, widget, event):
    """Callback for 'on-enter' event, show tooltip"""
    self.tooltips.set_tip(self.ev_box, self.info)

И последний метод, это показ диалога "О программе". Здесь мы используем стандартный виджет gnome.ui.About. Параметры конструктора у него такие: имя приложения, версия, лицензия, краткое описание, список авторов, список авторов документации, переводчики, логотип. Версия, лицензия и автор у нас указаны в начале файла, в "магических" переменных __version__, __license__ и __author__. В качестве логотипа используем все тот же значок "Прокси" из текущей темы, только бОльшего размера (80 пикселов). Все остальное понятно из кода:

def on_ppm_about(self, event, data=None):
    """Callback for pop-up menu item 'About', show About dialog"""
    pixbuf_logo = self.theme.load_icon('proxy', 80, gtk.ICON_LOOKUP_FORCE_SVG)
    msg_applet_name = u"Proxy switcher"
    msg_applet_description = u"Applet for turning proxy on/off"
    gnome.ui.About(msg_applet_name, __version__, __license__,
                   msg_applet_description,
                   [__author__,],   # programming
                    None,   # documentation
                    None,   # translating
                    pixbuf_logo,
                   ).show()

С внешним видом вроде бы все.

Здесь я намеренно опустил некоторые вещи, чтобы не перегружать код непринципиальными моментами:

  • Реакция апплета на изменение ориентации панели - сигнал change-orient
  • Реакция апплета на изменение размера панели - сигнал change-size
  • Реакция апплета на изменение фона панели - сигнал change-background

Примеры callback-функций на эти сигналы (а они идентичны у большинства апплетов) можно найти при помощи Google Codesearch: например, для change-orient.

Многоязычное окружение

Для "апплета на коленке" допустимо, чтобы он работал только на языке создателя. Если же есть желание показать апплет хотя бы одному другу, то резонно задуматься о работе в многоязычном окружении. Стандартный инструмент для таких вещей - GNU gettext. В Python есть его поддержка. Во время разработки программы особо ничего не меняется, лишь у каждой строки, которую нужно перевести, появляется "обертка" _().

В нашем случае, нужно переводить: всплывающие подсказки (в методе set_visual_state) и название программы, ее описание (в методе on_ppm_about). Делаем:

import gettext
gettext.install('proxyswitcher', unicode=True)

[...]
    def set_visual_state(self, state, is_on):
        """Set overall visual state for corresponding proxy's state"""
        msg_on_state = _(u"Proxy is on")
        msg_off_state = _(u"Proxy is off")
        mode = _(u"mode: %s") % state
        variant = (is_on and msg_on_state) or msg_off_state
        self.info = u"%s (%s)" % (variant, mode)
        self._set_image(is_on)

    def on_ppm_about(self, event, data=None):
        """Callback for pop-up menu item 'About', show About dialog"""

        pixbuf_logo = self.theme.load_icon('proxy', 80, gtk.ICON_LOOKUP_FORCE_SVG)
        msg_applet_name = _("Proxy switcher")
        msg_applet_description = _("Applet for turning proxy on/off")
        gnome.ui.About(msg_applet_name, __version__, __license__,
                       msg_applet_description,
                       [__author__,],   # programming
                        None,   # documentation
                        None,   # translating
                        pixbuf_logo,
                       ).show()

Первым параметром в gettext.install идет название домена переводов - обычно совпадает с именем программы.

Отмечу один момент: в случае использования подстановок в строки стоит избегать конструкций _(u"some string %s with subst" % value) по той причине, что при извлечении строк для перевода, будет извлечена строка u"some string %s with subst", а во время работы программы будет искаться перевод для строки с уже подставленным значением value, поэтому лучше вынести операцию подстановки значения "за скобки", т.е. _(u"some string %s with subs") % value.

Теперь нужно извлечь строки, которые нужно перевести. И здесь есть некоторая неопределенность. В том смысле, что нет только одного способа выполнить эту операцию. Можно использовать "канонические" инструменты GNU gettext, можно использовать инструменты Python. Я приведу пример использования Python-инструментов - не только в силу специфики блога, но и по причине лучшей переносимости. Итак, делаем:

pythy@axcel:~/blog/gnome-applet/gnomeapplet_03$ pygettext -v -o po/proxyswitcher.pot proxyswitcher.py
Working on proxyswitcher.py

pot-файл - это шаблон, так что копируем его в файл перевода po:

pythy@axcel:~/blog/gnome-applet/gnomeapplet_03$ cp po/proxyswitcher.pot po/ru/proxyswitcher.po

Теперь редактируем параметры перевода (в файле перевода есть "рыба") и собственно сам перевод. Далее, "компилируем" в mo-файл. В качестве "компилятора" msgfmt.py (для Debian и Ubuntu он доступен в пакете python2.4-doc):

pythy@axcel:~/blog/gnome-applet/gnomeapplet_03$ msgfmt.py -o po/ru/proxyswitcher.mo po/ru/proxyswitcher.po

"Инструментальный" этап завершен. Теперь появляется резонный вопрос: как использовать данный перевод? И здесь опять неоднозначность: Python по умолчанию ищет переводы в /usr/share/locale, однако есть возможность указать другое место, где искать переводы - передать имя каталога вторым параметром в gettext.install:

import gettext
gettext.install('proxyswitcher', os.path.dirname(os.path.abspath(__file__)), unicode=True)

в данном примере переводы ищутся в той же директории, где и располагается программа. Однако стоит предостеречь о том, что в данном каталоге нужно положить файл перевода не просто так, а по соглашению <targetdir>/<lang>/LC_MESSAGES/<domain>.mo, где <targetdir> - каталог, где ищутся переводы (текущий каталог), <lang> - двухбуквенный код языка (ru), <domain> - домен перевода (proxyswitcher). И вот по вот этому соглашению, кладем файл proxyswitcher.mo в каталог ru/LC_MESSAGES/proxyswitcher.mo. Еще один момент, на который стоит обратить внимание - все строки unicode, и в gettext.install тоже указываем опцию unicode, иначе строка будет подставлена "как есть", без перекодировки (т.е. если я создавал файл перевода в коировке UTF-8, а пользователь запустит программу в KOI8-R окружении, то подставлены будут переведенные строки в UTF-8).

Результат

Пробуем собрать всё воедино и посмотреть, что получилось. Если я нигде не ошибся и вы нигде не опечатались, то результат должен быть таков:

Dialog 'add to panel' Dialog 'add to panel' Dialog 'add to panel' Dialog 'add to panel'

Что дальше

Дальше встает вопрос о распространении апплета. Напомню, какие файлы нужны для работы апплета:

  • GNOME_ProxySwitcher.server - описание нашего апплета для Bonobo, должен лежать либо в /usr/lib/bonobo/servers, либо в одном из каталогов, упомянутых в /etc/bonobo-activation/bonobo-activation-config.xml (по умолчанию всё закомментировано).
  • applet_skeleton.py, applet_example.py, proxyswitcher.py - собственно код апплета, исполняемый файл (proxyswitcher.py) должен лежать в месте, указанном в GNOME_ProxySwitcher.server; остальные - в PYTHONPATH (наиболее простой вариант - в том же месте, что и proxyswitcher.py)
  • файлы переводов proxyswitcher.mo в каталоге /usr/share/locale, либо указанном в proxyswitcher.py

Если апплет пакетировать в deb/rpm, то проблем не возникает - Bonobo-файл и файл переводов кладутся в нужные места, а код апплета, скажем, в /usr/lib/gnome-applets/proxyswitcher. Если не пакетировать, то ситуация такая: пользовательское ПО по стандарту FHS должно помещаться либо в /opt, либо в /usr/local. В /opt обычно ставят ПО сторонних вендоров, так что правильное место для нашего апплета - это /usr/local. Так что нужно: написать шаблоны Bonobo-файла и proxyswitcher.py и в зависимости от переданного значения ключа --prefix при установке заполнять шаблоны необходимыми данными.

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

Думаю, что решению этих вопросов будет посвящена еще одна статья. Код, описанный в статье, как всегда, можно получить на code.google.com

Комментарии

Все статьи