Что дальше
Дальше встает вопрос о распространении апплета. Напомню, какие файлы нужны для работы апплета:
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
Создание апплета GNOME
Напомню, что конечная цель - создание апплета для переключения прокси (вкл/выкл). Для этих целей есть диалог "Параметры прокси-серверов" (Система->Параметры->Сервис Прокси), но им не удобно пользоваться, поскольку нужно совершать много лишних действий.
Сам диалог настройки ничего не делает, он лишь изменяет значение соответствующего параметра в 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)
Img0.shtml
![](paneled-addto.png)
Скелет апплета, в панельном режиме, добавление апплета на панель
Многоязычное окружение
Для "апплета на коленке" допустимо, чтобы он работал только на языке создателя. Если же есть желание показать апплет хотя бы одному другу, то резонно задуматься о работе в многоязычном окружении. Стандартный инструмент для таких вещей - 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).
Регистрация апплета в панели GNOME
Если выше был обычный Python-код, с некоторой PyGTK-спецификой, то сейчас будет сплошная магия ;) Это, кстати, одна из слабых сторон GNOME-Python - отсутствие систематической документации (для gnomeapplet вообще никакой документации нет, за исключением пары примеров и вышеупомянутой "методички"). К примеру, при регистрации апплета вызывается функция applet_bonobo_factory, однако нигде не упоминается, какие параметры в нее передаются. Чтобы узнать это, нужно лезть в исходные тексты. Я, конечно, понимаю, что "Use code, Luke!", но все же качество документации по PyGTK в целом хромает (например, сплошь и рядом в документации рекомендуются методы, которые уже пару версий назад как уже объявлены устаревшими).
Общая идеология регистрации апплета такова:
описываем мета-информацию в специальном .server файле
в апплете вызываем специальный интерфейс
Вначале закончу дело с кодом апплета:
def run_in_panel(): gnomeapplet.bonobo_factory("OAFIID:GNOME_AppletSkeleton_Factory", GnomeAppletSkeleton.__gtype__, "Applet skeleton", "0", applet_factory)
это и есть вызов "специального интерфейса". Параметры такие: IID (уникальный идентификатор сервиса в GNOME), тип (это остатки C-природы GTK, тип GObject), имя, версия, callback-функция.
Теперь, что касается "описания мета-информации". Пишем следующий XML (он для всех апплетов будет идентичным, специфичные для моего апплета данные я выделил полужирным):
<oaf_info>
<oaf_server iid="OAFIID:GNOME_AppletSkeleton_Factory" type="exe" location="/usr/local/lib/pygnomeapplet/applet_skeleton.py">
<oaf_attribute name="repo_ids" type="stringv"> <item value="IDL:Bonobo/GenericFactory:1.0" /> <item value="IDL:Bonobo/Unknown:1.0" />
</oaf_attribute> <oaf_attribute name="name" type="string" value="Applet skeleton factory" /> <oaf_attribute name="name-ru" type="string" value="Фабрика скелета апплета" />
<oaf_attribute name="description" type="string" value="Factory of simple applet skeleton" /> <oaf_attribute name="description-ru" type="string" value="Фабрика скелета простейшего апплета" />
</oaf_server> <oaf_server iid="OAFIID:GNOME_AppletSkeleton" type="factory" location="OAFIID:GNOME_AppletSkeleton_Factory"> <oaf_attribute name="repo_ids" type="stringv">
<item value="IDL:GNOME/Vertigo/PanelAppletShell:1.0" /> <item value="IDL:Bonobo/Control:1.0" /> <item value="IDL:Bonobo/Unknown:1.0" />
</oaf_attribute> <oaf_attribute name="name" type="string" value="Applet skeleton" /> <oaf_attribute name="name-ru" type="string" value="Скелет апплета" />
<oaf_attribute name="description" type="string" value="Simple applet skeleton, do nothing" /> <oaf_attribute name="description-ru" type="string" value=" Скелет простого апплета, ни делает ни чего" />
<oaf_attribute name="panel:category" type="string" value="Accessories" /> <oaf_attribute name="panel:icon" type="string" value="gnome-panel.png" />
</oaf_server> </oaf_info>
Так, что тут: два раздела, фабрика и сам апплет. Для каждого определены IID, у фабрики IID должен совпадать с тем, что указали в вызове bonobo_factory в апплете. Дополнительно отмечу, что тут же можно задавать переводы названия/описания апплета (в данном случае будет на русском, если у Вас русская локаль и на английском во всех остальных случаях). Называем этот файл GNOME_AppletSkeleton.server и "скармливаем" его Bonobo Activation Server. Существует несколько вариантов этого "действа":
Поместить .server в каталог /usr/lib/bonobo/servers
Изменить /etc/bonobo-activation/bonobo-activation-config.xml (там есть несколько примеров), добавить нужный путь (скажем, /usr/local/lib/bonobo/servers) и положить .server туда
В переменную BONOBO_ACTIVATION_PATH добавить каталог, где лежит .server.
Мне наиболее правильным показался второй вариант, я его и использовал.
После этого скрещиваем пальцы и пытаемся добавить апплет на панель. Если .server правильно "скормили", то апплет-скелет появляется в списке кандидатов на добавление. Если и все остальное сделали верно, то добавление пройдет гладко. И Вы получите примерно такой результат:
Рисунок.
![](paneled-tooltip.png)
Скелет апплета, в панельном режиме, вместе с всплывающей подсказкой
![](paneled-ppmenu.png)
Скелет апплета, в панельном режиме, вместе с контекстным меню
Стоит отметить, что контекстное меню в "режиме окна" и в "режиме панели" отличаются - для панели появляются дополнительные пункты меню.
На сегодня, я думаю, достаточно. Полный код примера Вы можете взять . В следующий раз скелет будет обрастать мясом ;)
Результат
Пробуем собрать всё воедино и посмотреть, что получилось. Если я нигде не ошибся и вы нигде не опечатались, то результат должен быть таков:
Рисунок.Диалог добавления апплета на панель
![](applet_tooltip_on.png)
Апплет на панели, всплывающая подсказка, прокси включен
![](applet_tooltip_off.png)
Апплет на панели, всплывающая подсказка, прокси выключен
Рисунок. Диалог “О программе” апплета
Скелет апплета
Перво-наперво, выделю то, что необходимо для функционирования любого апплета, вне зависимости от его природы:
некий виджет-контейнер (например, HBox)
некий "полезный" виджет (у меня это будет Label)
всплывающая подсказка
некое действие по левой кнопки мыши
контекстное меню
возможность запуска как отдельного приложения (для отладки)
регистрация в GNOME как апплета к панели
Займемся реализацией:
import sys import gtk import gtk.gdk import gnome.ui import gnomeapplet
class GnomeAppletSkeleton(gnomeapplet.Applet): """Simple applet skeleton"""
def __init__(self, applet): """Create applet"""
self.applet = applet self.__init_core_widgets() self.init_additional_widgets() self.init_ppmenu() self.__connect_events() self.applet.connect("destroy", self._cleanup) self.after_init() self.applet.show_all()
Прежде чем приступить к пояснениям, скажу о конвенции насчет имен методов. Если имя метод начинается с двух подчеркиваний, то перегружать (переопределять) такой метод нежелательно. Если же имя метода начинается с буквы, то такой метод можно практически безболезненно перегружать. Но все же, если Вы будете писать свой апплет, то все же гляньте код соответствующего метода GnomeAppletSkeleton прежде чем перегружать его.
Итак, первым делом инициализирую ключевые виджеты, без которых не обойдется ни один апплет:
def __init_core_widgets(self): """Create internal widgets""" self.tooltips = gtk.Tooltips() self.hbox = gtk.HBox() self.ev_box = gtk.EventBox() self.applet.add(self.hbox) self.hbox.add(self.ev_box)
Поскольку апплет - безоконный виджет (у него нет окна), то для того, чтобы была возможность реагировать на события, я помещаю EventBox в него. А уж все дополнительные виджеты (в моем случае это будет только Label) добавляются к ev_box.
def init_additional_widgets(self): """Create additional widgets""" self.label = gtk.Label("Dummy") self.ev_box.add(self.label)
Далее, указываю необходимую информацию для контекстного меню (popup menu):
def init_ppmenu(self): """Create popup menu""" self.ppmenu_xml = """ <popup name="button3">
<menuitem name="About Item" verb="About" stockid="gtk-about" /> </popup> """
self.ppmenu_verbs = [ ("About", self.on_ppm_about), ]
Заметьте, что в XML-описании пункта меню "О программе" нет собственно названия пункта, а лишь его StockID. Это сделано по той простой причине, что пункт меню "О программе" стандартен для большинства приложений и в случае указания StockID Вы получаете:
стандартную иконку для данного пункта (причем, с изменением темы оформления GNOME эта иконка может меняться)
стандартное название пункта меню, причем автоматически переведенное на нужный язык
Каждый пункт меню имеет "глагол"-действие, который ставится ему в соответствие. self.ppmenu_verbs же задает соответствие между "глаголом"-действием и callback-функцией.
Следующий шаг по созданию апплета - "соединение" callback-функций и событий:
def __connect_events(self): """Connect applet's events to callbacks""" self.ev_box.connect("button-press-event", self.on_button) self.ev_box.connect("enter-notify-event", self.on_enter) self.button_actions = { 1: lambda: None, 2: lambda: None, 3: self._show_ppmenu, }
Еще раз отмечу, что апплет - безоконный виджет, поэтому все события генерирует ev_box. В данном случае, я соединил события "нажатие на кнопку" с callback-функцией self.on_button и событие "попадание курсора в область виджета" с callback-функцией self.on_enter. Здесь же при помощи словаря self.button_actions задал соответствие между кнопками мыши и функциями-действиями. Стоит заметить, что callback-функции, соединенные с событиями, должны быть определенной сигнатуры (об этом чуть позже), а функции-действия не должны принимать ни один параметр.
Следующий по порядку вызов - это метод after_init. В скелете он пустой, предназначен специально для переопределения в потомках.
С этапами создания апплета вроде завершил, остались callback-функции… Я не буду пересказывать PyGTK reference, лишь перечислю типы callback-функций и их сигнатуры, которые встречаются у меня:
callback-функция на событие destroy апплета. Сигнатура function(event). Реализация - _cleanup
callback-функция на события ev_box. Сигнатура function(widget, event). Реализации - on_enter, on_button
callback-функция на пункт меню. Сигнатура function(event, data=None). Реализация - on_ppm_about
функция-действие (мое название) на нажатие одной из кнопок мыши. Сигнатура function(). Реализация - _show_ppmenu.
Содержимое callback-функции _cleanup не буду приводить - оно слишком тривиально (удаляется объект self.applet) для того, чтобы занимать место, а кому интересно - гляньте в полном исходном тексте апплета. Что касается остальных callback-функций, я их приведу и прокомментирую, поскольку они все же представляют интерес. def on_button(self, widget, event): """Action on pressing button in applet"""
if event.type == gtk.gdk.BUTTON_PRESS: self.button_actions[event.button]()
Callback-функция on_button вызывается при нажатии любой кнопки мыши внутри виджета. И внутри этой функции я, во-первых, убеждаюсь, что присоединили к правильному событию (нажатию на клавишу), а, во-вторых, вызываю нужную функцию-действие, выбирая (в event.button хранится номер кнопки, нажатие на которую и вызвало появление данного события) из ранее описанного словаря self.button_actions. Для кнопок 1 и 2 у меня пустые действия, для 3 - контекстное меню. Показ контекстного меню - специальный метод класса Applet - setup_menu. Первый аргумент - XML-описание меню, второй - "глаголы"-действия, третий - пользовательские данные (передаются третьим параметром в callback-функцию).
def _show_ppmenu(self): """Show popup menu""" self.applet.setup_menu(self.ppmenu_xml, self.ppmenu_verbs, None)
Что касается события "попадание курсора в область виджета", то на него я реагировать буду так: показывать какую-нибудь простенькую подсказку, ради разнообразия сделав ее динамической.
def on_enter(self, widget, event): """Action on entering""" info = "Hey, it just skeletonnAnd on_enter event time is %d" % event.time self.tooltips.set_tip(self.ev_box, info)
И последняя callback-функция - на вызов пункта меню "О программе". Здесь я воспользуюсь стандартным диалогом из модуля gnome.ui:
def on_ppm_about(self, event, data=None): """Action on choosing 'about' in popup menu""" gnome.ui.About("GNOME Applet Skeleton", "0.1", "GNU General Public License v.2", "Simple skeleton for Python powered GNOME applet", ["Pythy <the.pythy@gmail.com>",] ).show()
Класс-костяк апплета написан, теперь нужно описать его "фабрику":
def applet_factory(applet, iid): GnomeAppletSkeleton(applet, iid) return True
Ух. С первым этапом закончил. Костяк апплета сделан. Осталось дело за малым. Запустить и посмотреть, что же получилось :)
Этот цикл посвящен теме создания
, http://gorod-omsk.ru/blog/pythy/
Этот цикл посвящен теме создания апплетов для панели GNOME. Если кратко, апплет - это маленькое приложение, которое встраивается в панель и либо информирует о чем-либо (например, погоде, или о состоянии батареи), либо выполняет какие-либо одноэтапные действия (запускает поиск, изменяет громкость и т.д.).
Я буду создавать простой апплет для включения/выключения прокси в GNOME.
Прежде чем начать, стоит упомянуть один документ, который описывает создание апплета средствами Python и PyGTK: это GNOME applets with Python. Однако, на мой взгляд, у него есть ряд недостатков, которые и побудили меня осветить эту тему по-своему.
Итак, приступим.
В первой части я буду создавать скелет апплета и регистрировать его в GNOME, во второй буду писать функциональную часть, а в третьей - заниматься "полировкой" и украшательствами.
который ничего не делает. Сегодня
, http://gorod-omsk.ru/blog/pythy/
Создание апплета GNOMEЧасть 3, заключительная
, http://gorod-omsk.ru/blog/pythy/
Заканчиваем с апплетом к 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.
Заполнение скелета
Собственно для реализации работающего апплета почти всё готово: скелет апплета, объект-переключатель -
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 - выполняется по нажатию левой кнопки, переключает состояние прокси.
Действия по регистрации этого, уже работающего, апплета абсолютно аналогичны таковым для скелета, так что я не описываю их. Рабочий код можете взять отсюда.
Фактически, апплет уже функционирует, однако выглядит он не притязательно. В следующей части буду "шлифовать" внешний вид.
Запуск апплета в отдельном окне
Для начала нужно отладить апплет, для этого пишу код, позволяющий запускать апплет в отдельном окне:
def run_in_window(): main_window = gtk.Window(gtk.WINDOW_TOPLEVEL) main_window.set_title("GNOME Applet Skeleton") main_window.connect("destroy", gtk.main_quit) app = gnomeapplet.Applet() applet_factory(app, None) app.reparent(main_window) main_window.show_all() gtk.main() sys.exit()
def main(args): if len(args) == 2 and args[1] == "run-in-window": run_in_window() else: run_in_panel()
if __name__ == '__main__': main(sys.argv)
Небольшой комментарий по коду: если файл запускается как скрипт, то выполняется функция main, в ней, в зависимости от того, передан ли аргумент run-in-window, апплет запускается либо в окне (функция run_in_window), либо в панели (run_in_panel). Про функцию run_in_panel чуть ниже, а в run_in_window стоит обратить внимание на строчку app.reparent(main_window). Этим собственно и достигается, что апплет запускается в отдельном окне.
Как выглядит костяк апплета, можно увидеть на скриншотах:
![](windowed-tooltip.png)
Скелет апплета, в оконном режиме, вместе с всплывающей подсказкой
![](windowed-ppmenu.png)
Скелет апплета, в оконном режиме, вместе с контекстным меню
![](windowed-about.png)
Скелет апплета, в оконном режиме, вместе с диалогом “О программе”