Этот цикл посвящен теме создания апплетов для панели GNOME. Если кратко, апплет - это маленькое приложение, которое встраивается в панель и либо информирует о чем-либо (например, погоде, или о состоянии батареи), либо выполняет какие-либо одноэтапные действия (запускает поиск, изменяет громкость и т.д.).
Я буду создавать простой апплет для включения/выключения прокси в GNOME.
Прежде чем начать, стоит упомянуть один документ, который описывает создание апплета средствами Python и PyGTK: это GNOME applets with Python. Однако, на мой взгляд, у него есть ряд недостатков, которые и побудили меня осветить эту тему по-своему.
Итак, приступим.
В первой части я буду создавать скелет апплета и регистрировать его в GNOME, во второй буду писать функциональную часть, а в третьей - заниматься "полировкой" и украшательствами.
Скелет апплета
Перво-наперво, выделю то, что необходимо для функционирования любого апплета, вне зависимости от его природы:
-
некий виджет-контейнер (например,
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
Ух. С первым этапом закончил. Костяк апплета сделан. Осталось дело за малым. Запустить и посмотреть, что же получилось :)
Запуск апплета в отдельном окне
Для начала нужно отладить апплет, для этого пишу код, позволяющий запускать апплет в отдельном окне:
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)
. Этим собственно и достигается, что апплет запускается в отдельном окне.
Как выглядит костяк апплета, можно увидеть на скриншотах:
Скелет апплета, в оконном режиме, вместе с всплывающей подсказкой
Скелет апплета, в оконном режиме, вместе с контекстным меню
Скелет апплета, в оконном режиме, вместе с диалогом “О программе”
Регистрация апплета в панели 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 правильно "скормили", то апплет-скелет появляется в списке кандидатов на добавление. Если и все остальное сделали верно, то добавление пройдет гладко. И Вы получите примерно такой результат:
Стоит отметить, что контекстное меню в "режиме окна" и в
"режиме панели" отличаются - для панели появляются
дополнительные пункты меню.
На сегодня, я думаю, достаточно. Полный код примера Вы можете взять
здесь.
В следующий раз скелет будет обрастать мясом ;)