如何用Python编写Unity系统指示器?

背景:

C原始代码:(正常工作)

我已经在C语言中找到了一个工作的示例,可以参考我的其他问题:How to develop a System Indicator for Unity?">如何为Unity开发系统指示器?。然而,indicator-sysmonitor已经以Python开发,并且许多其他应用程序指示器也是如此。我不喜欢开发人员被迫将他们的项目转换为C语言,或者编写一个Python-C代理,如果他们想要在登录/锁定/ubiquity屏幕上显示指示器。相反,直接从Python创建indicator-sysmonitor将是最好的解决方案(没有变通方法),并且它将成为当前使用appindicator的所有Python项目的通用解决方案。
Python代码:(我尝试将C代码转换为Python的失败尝试)
我正在努力将它移植到Python中。这是我的当前代码,但它不起作用。它确实为菜单和操作创建了DBus对象。它在XFCE指示器插件中列出。但是在面板上没有显示。
/usr/lib/indicator-test/indicator-test-service #!/usr/bin/python2
import os import sys
import gi from gi.repository import Gio, GLib
APPLICATION_ID = 'local.sneetsher.indicator.test' DBUS_MENU_PATH = '/local/sneetsher/indicator/test/desktop' DBUS_ACTION_PATH = '/local/sneetsher/indicator/test'
def callback(): print ok
def quit_callback(notification, loop): global connection global exported_action_group_id global exported_menu_model_id
connection.unexport_action_group (exported_action_group_id) connection.unexport_menu_model (exported_menu_model_id)
loop.quit()
def cancel (notification, action, data): if action == "cancel": print "取消" else: print "不应该发生这种情况(取消)!"
def bus_acquired(bus, name): # 菜单 submenu = Gio.Menu() submenu.append("显示", "show") item = Gio.MenuItem.new(None, "_header") item.set_attribute([("x-canonical-type","s","com.canonical.indicator.root")]) item.set_submenu(submenu) menu = Gio.Menu() menu.append_item (item)
actions = Gio.SimpleActionGroup.new() action1 = Gio.SimpleAction.new("_header", None) actions.insert(action1) action2 = Gio.SimpleAction.new('show', None) actions.insert(action2) action2.connect("activate",callback)
global connection connection = bus
global exported_action_group_id exported_action_group_id = connection.export_action_group(DBUS_ACTION_PATH, actions)
global exported_menu_model_id exported_menu_model_id = connection.export_menu_model(DBUS_MENU_PATH, menu)
def setup (): #总线连接 Gio.bus_own_name(Gio.BusType.SESSION, APPLICATION_ID, 0, bus_acquired, None, None)
if __name__ == '__main__':
connection = None exported_menu_model_id = 0 exported_action_group_id = 0 password = ""
loop = GLib.MainLoop() setup ()
loop.run() local.sneetsher.indicator.test
[Indicator Service] Name=indicator-test ObjectPath=/local/sneetsher/indicator/test
[desktop] ObjectPath=/local/sneetsher/indicator/test/desktop [desktop_greeter] ObjectPath=/local/sneetsher/indicator/test/desktop [desktop_lockscreen] ObjectPath=/local/sneetsher/indicator/test/desktop local.sneetsher.indicator.test.service [D-BUS Service] Name=local.sneetsher.indicator.test Exec=/usr/lib/indicator-test/indicator-test-service 90_unity-greeter.gschema.override [com.canonical.unity-greeter] indicators=['ug-accessibility', 'com.canonical.indicator.keyboard', 'com.canonical.indicator.session', 'com.canonical.indicator.datetime', 'com.canonical.indicator.power', 'com.canonical.indicator.sound', 'local.sneetsher.indicator.test', 'application']
问题:
我预计的原因是,我没有像原始的C代码那样创建菜单结构或其元数据(伪项目,如_header)。
有人能将这个系统指示器代码在C中移植到Python并使其正常工作吗?

2你能把问题具体些吗?"帮我修复它"并附加很多代码让我有点犹豫。你知道出了什么问题吗?如果没有具体的错误回溯,我们可能无法提供帮助。 - don.joey
1@don.joey,是的,我看到了,我已经提供了所有细节来创建完整的系统指示器。你可以跳过所有那些配置文件。所以直接尝试编译tests/indicator-test-service.c并直接运行它。使用d-feet检查它的DBus菜单结构。然后编写一个Python代码或修改我的代码来创建相同的DBus菜单结构。如果有任何问题,请随时询问,甚至是按照步骤进行测试。我在寻找GMenu和GSimpleActionGroup的清晰完整的Python文档方面遇到了困难,只有C文档是完整的,Python只是Glib Introspection的绑定。 - user.dz
2@user.dz 你在GitHub上有C代码吗? - Sergiy Kolodyazhnyy
1@Serg,是的,可以在这里的“注意事项”部分找到:https://askubuntu.com/a/752750/26246 - user.dz
1我可以问一下为什么你需要用Python来做,如果你已经有一个在C语言中运行良好的解决方案? - Elder Geek
3@ElderGeek,我的主要原因是indicator-sysmonitor已经以Python开发,就像许多其他应用程序指示器一样。我不喜欢开发人员被迫将他们的项目转换为C语言或编写一个Python-C代理,如果他们想在登录/锁定/ubiquity屏幕上显示指示器。相反,直接从Python创建一个系统指示器会是最好的解决方案(没有变通方法,并且适用于当前使用appindicator的所有Python项目)。 - user.dz
1明白了。我还是习惯用 indicator-multiload,但我得去看看 indicator-sysmonitor。我可以理解在登录界面、锁屏界面和Ubiquity界面上显示这些信息的价值。非常感兴趣地观察中... :-) - Elder Geek
8所以,基本上,你希望我们将你的C代码转换成Python并创建一个可工作的端口...而你不想自己尝试转换它?不确定这个问题是否会在这里得到答案,因为你基本上是想雇佣一位高度专业化的Python程序员来编写一个高度专业化的软件/代码来处理你的指标。系统指标与应用指标完全不同。 - Thomas Ward
@ThomasWard,是的,但实际上我尝试过多次进行端口移植,但都失败了(问题中包含了我其中一次尝试的完整代码)。我同意,看来我对于一个C-Python-DBUS-GLib天使的梦想走得太远了:)。Gnome只维护C文档(我也错过了绑定文档,而且我的编程基础有些薄弱)。另一方面,Ubuntu没有记录系统指示器API,似乎他们不希望太多垃圾泄漏到面板中(所以他们将appindicators囚禁在indicator-application中)。我喜欢挑战,也许有赏金猎人能超越我。 - user.dz
如果你的问题是关于代码的,那么这个问题是否更适合像 Stack Overflow 这样的平台呢?因为它涉及到 C-Python,并不特定于 Ubuntu。 - spark
1@spark 实际上这是针对Ubuntu的,指示器是Canonical为Unity桌面环境发明的。由于它的复杂性,我会在悬赏结束后尝试在SO上重新提出另一个问题。谢谢。 - user.dz
6这个问题非常特定于Ubuntu和在Ubuntu上的开发。这不是一些普通的“hello world”东西,需要对Ubuntu的组织方式有所了解,并且需要使用特定API进行编程。关于这个问题更适合在SO上讨论的评论并没有帮助。 - Sergiy Kolodyazhnyy
6由于Ubuntu放弃了Unity,那么这个问题还有意义吗? - beruic
这个问题可以合理地提高赏金限额,从500提高到5000。 - WinEunuuchs2Unix
@beruic 自2010年以来,我们一直听说Wayland的承诺,但它直到去年才出现,然后又消失了(或被搁置)在Ubuntu 18.04 LTS中,该版本将包括Unity 7.5和Gnome桌面。这并不意味着Unity不会消亡,只是说它还没有完全消失。 - WinEunuuchs2Unix
@Thomas 你让我笑了,因为那正是我想的一样。虽然听起来很有趣。原因有两个。首先,如果他做的C语言程序很好用,为什么还要用Python写一个呢?其次,唯一的理由就是你想学习Python,在这种情况下,你应该寻求帮助或建议,而不是说“有人能不能做一个……”回答问题的人,如果我会Python但不懂C语言怎么办? - An0n
@user.dz - 你想用Python写这个吗?有些系统不允许使用Python,或者已禁用了Python。写一个shell脚本会不会更有效呢? - dschinn1001
@WinEunuuchs2Unix - 你对Linux用户的友善,...与你的悬赏率不相称。 - dschinn1001
@user.dz 你能把这个放到一个git仓库里吗?我很愿意帮助你... - Joshua Besneatte
2个回答

我刚刚上传了一个从 @user.dz的C示例移植过来的原始“工作”Python示例。这是源代码仓库: 我会随着进展不断更新,但欢迎任何贡献。
谢谢提供有用的信息!
主脚本的移植源代码。注意:这只是初始副本,链接可能在将来失效。要获取完整的软件包和最新更新,请点击上面的链接。
#!/usr/bin/python3
import sys
import os
import logging
import logging.handlers
logger = logging.getLogger('LoginHelper')
logger.setLevel(logging.DEBUG)
handler = logging.handlers.SysLogHandler(address = '/dev/log')
logger.addHandler(handler)
logger.debug("Login-Helper: Start")
os.environ["DISPLAY"] = ":0"

import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from gi.repository import Gio
from gi.repository import GLib


class LoginHelperIndicator():

  def __init__(self, dbus_name, dbus_path):
    self.dbus_name = dbus_name
    self.dbus_path = dbus_path
    self.actions = Gio.SimpleActionGroup()
    self.menu = Gio.Menu()
    self.actions_export_id = 0
    self.menu_export_id = 0



  def activate_about (self, action, parameter):
    # gtk_show_about_dialog(NULL,
    #                       "program-name", PROJECT_NAME,
    #                       "title", "About " PROJECT_NAME,
    #                       "version", PROJECT_VERSION_MAJOR "." PROJECT_VERSION_MINOR,
    #                       "license_type", GTK_LICENSE_GPL_3_0,
    #                       "wrap_license", TRUE,
    #                       "website", "https://github.com/sneetsher/mysystemindicator",
    #                       "website_label", "https://github.com/sneetsher/mysystemindicator",
    #                       "logo_icon_name", "indicator-" SHORT_NAME,
    #                       NULL);
    # g_message ("showing about dialog");
    pass

  def activate_private (self, action, parameter):
    #g_message ("clicked private menu entry");
    pass

  def activate_exit (self, action, parameter):
    #g_message ("exit the program");
    Gtk.main_quit()

  def on_bus_acquired (self, connection, name):
    logger.debug ('Bus acquired: ' + str(connection))
    error = None
    item = Gio.MenuItem()
    submenu = Gio.Menu()

    action_state = GLib.Variant("a{sv}", {
      'label': GLib.Variant("s", "Login-helper"),
      'icon':  GLib.Variant("s", "indicator-loginhelper"),
      'accessible-desc': GLib.Variant("s", "Login Helper indicator")
    })
    self.actions.add_action(Gio.SimpleAction.new_stateful("_header", None, action_state))
    about_action = Gio.SimpleAction.new("about", None)
    about_action.connect("activate", self.activate_about)
    self.actions.add_action(about_action)
    private_action = Gio.SimpleAction.new("private", None)
    private_action.connect("activate", self.activate_private)
    self.actions.add_action(private_action)
    exit_action = Gio.SimpleAction.new("exit", None)
    exit_action.connect("activate", self.activate_exit)
    self.actions.add_action(exit_action)

    submenu.append("About", "indicator.about")
    submenu.append("Exit", "indicator.exit")
    item = Gio.MenuItem().new(None, "indicator._header")
    item.set_attribute_value("x-canonical-type", GLib.Variant("s", "com.canonical.indicator.root"))
    item.set_submenu(submenu)

    self.menu = Gio.Menu.new()
    self.menu.append_item(item)

    self.actions_export_id = connection.export_action_group(self.dbus_path, self.actions)
    if self.actions_export_id == 0:
      #g_warning ("cannot export action group: %s", error->message);
      #g_error_free (error);
      return

    self.menu_export_id = connection.export_menu_model(self.dbus_path + "/greeter", self.menu)
    if self.menu_export_id == 0:
      #g_warning ("cannot export menu: %s", error->message);
      #g_error_free (error);
      return


  def on_name_lost (self, connection, name):
    if self.actions_export_id:
      connection.unexport_action_group(self.actions_export_id)

    if (self.menu_export_id):
      connection.unexport_menu_model(self.menu_export_id)


if __name__ == '__main__':

  logger.debug('Login-Helper: Initializing Login Helper Indicator')

  res_gtk = Gtk.init(sys.argv)
  indicator = LoginHelperIndicator('com.canonical.indicator.loginhelper', '/com/canonical/indicator/loginhelper')

  logger.debug ('Login-Helper: Res_gtk: ' + str(res_gtk))
  res_own = Gio.bus_own_name(Gio.BusType.SESSION,
                indicator.dbus_name,
                Gio.BusNameOwnerFlags.NONE,
                indicator.on_bus_acquired,
                None,
                indicator.on_name_lost)
  logger.debug ('Login-Helper: Res_own: ' + str(res_own))

  Gtk.main()

  exit(0)

非常感谢Marto的分享。我已经尝试了几次,但发现很难,因为没有清晰的文档(混合了旧的/遗留的文档)。我无法取得任何突破。我需要好好复习一下,以理解如何通过DBus传递菜单。 - user.dz
1我不得不硬编码DISPLAY环境变量,因为在指示器激活时它是空的。我没有找到更好的解决办法。如果你能想到什么尝试的方法,我会很感激。 - Marto
1关于通过DBus传递菜单,它与C代码非常相似。我认为主要区别在于Python库是面向对象的,所以您需要相应地调整代码。 - Marto
谢谢你的提示。关于DISPLAY,还要导入Gdk,在Gtk.init()之后调用Gdk.get_display(),它相当于gdk_display_get_default(),我在C、Python和JavaScript中测试过,一段时间前也适用于X11和Wayland。 - user.dz

系统指示器服务

嗯,实际上比我预想的简单。它没有特定的API。因为它只是一个通过DBus导出相应GMenu的GSimpleActionGroup,然后使用相同名称的声明文件放置在/usr/share/unity/indicators/中告知Unity其存在。不需要任何其他库。

这里有一个非常简单的C语言示例:

libindicator源代码中获取tests/indicator-test-service.c的副本。

apt-get source libindicator
cp libindicator-*/tests/indicator-test-service.c .
cp libindicator-*/tests/com.canonical.indicator.test*

indicator-test-service.c没有更改

**#include <gio/gio.h>

typedef struct
{
  GSimpleActionGroup *actions;
  GMenu *menu;

  guint actions_export_id;
  guint menu_export_id;
} IndicatorTestService;

static void
bus_acquired (GDBusConnection *connection,
              const gchar     *name,
              gpointer         user_data)
{
  IndicatorTestService *indicator = user_data;
  GError *error = NULL;

  indicator->actions_export_id = g_dbus_connection_export_action_group (connection,
                                                                        "/com/canonical/indicator/test",
                                                                        G_ACTION_GROUP (indicator->actions),
                                                                        &error);
  if (indicator->actions_export_id == 0)
    {
      g_warning ("cannot export action group: %s", error->message);
      g_error_free (error);
      return;
    }

  indicator->menu_export_id = g_dbus_connection_export_menu_model (connection,
                                                                   "/com/canonical/indicator/test/desktop",
                                                                   G_MENU_MODEL (indicator->menu),
                                                                   &error);
  if (indicator->menu_export_id == 0)
    {
      g_warning ("cannot export menu: %s", error->message);
      g_error_free (error);
      return;
    }
}

static void
name_lost (GDBusConnection *connection,
           const gchar     *name,
           gpointer         user_data)
{
  IndicatorTestService *indicator = user_data;

  if (indicator->actions_export_id)
    g_dbus_connection_unexport_action_group (connection, indicator->actions_export_id);

  if (indicator->menu_export_id)
    g_dbus_connection_unexport_menu_model (connection, indicator->menu_export_id);
}

static void
activate_show (GSimpleAction *action,
               GVariant      *parameter,
               gpointer       user_data)
{
  g_message ("showing");
}

int
main (int argc, char **argv)
{
  IndicatorTestService indicator = { 0 };
  GMenuItem *item;
  GMenu *submenu;
  GActionEntry entries[] = {
    { "_header", NULL, NULL, "{'label': <'Test'>,"
                             " 'icon': <'indicator-test'>,"
                             " 'accessible-desc': <'Test indicator'> }", NULL },
    { "show", activate_show, NULL, NULL, NULL }
  };
  GMainLoop *loop;

  indicator.actions = g_simple_action_group_new ();
  g_simple_action_group_add_entries (indicator.actions, entries, G_N_ELEMENTS (entries), NULL);

  submenu = g_menu_new ();
  g_menu_append (submenu, "Show", "indicator.show");
  item = g_menu_item_new (NULL, "indicator._header");
  g_menu_item_set_attribute (item, "x-canonical-type", "s", "com.canonical.indicator.root");
  g_menu_item_set_submenu (item, G_MENU_MODEL (submenu));
  indicator.menu = g_menu_new ();
  g_menu_append_item (indicator.menu, item);

  g_bus_own_name (G_BUS_TYPE_SESSION,
                  "com.canonical.indicator.test",
                  G_BUS_NAME_OWNER_FLAGS_NONE,
                  bus_acquired,
                  NULL,
                  name_lost,
                  &indicator,
                  NULL);

  loop = g_main_loop_new (NULL, FALSE);
  g_main_loop_run (loop);

  g_object_unref (submenu);
  g_object_unref (item);
  g_object_unref (indicator.actions);
  g_object_unref (indicator.menu);
  g_object_unref (loop);

  return 0;
}**

com.canonical.indicator.test 修改以添加锁定和登录界面模式

[Indicator Service]
Name=indicator-test
ObjectPath=/com/canonical/indicator/test

[desktop]
ObjectPath=/com/canonical/indicator/test/desktop

[desktop_greeter]
ObjectPath=/com/canonical/indicator/test/desktop

[desktop_lockscreen]
ObjectPath=/com/canonical/indicator/test/desktop

com.canonical.indicator.test.service 将文件名中的 .in 后缀删除,并更改可执行路径

[D-BUS Service]
Name=com.canonical.indicator.test
Exec=/usr/lib/x86_64-linux-gnu/indicator-test/indicator-test-service

Compile it

gcc -o indicator-test-service indicator-test-service.c pkg-config --cflags --libs gtk+-3.0

手动安装

sudo su
mkdir /usr/lib/x86_64-linux-gnu/indicator-test/
cp indicator-test-service /usr/lib/x86_64-linux-gnu/indicator-test/
cp com.canonical.indicator.test /usr/share/unity/indicators/
cp com.canonical.indicator.test.service /usr/share/dbus-1/services/

配置Greeter,覆盖默认的指示器列表

90_unity-greeter.gschema.override

com.canonical.unity-greeter]
indicators=['ug-accessibility', 'com.canonical.indicator.keyboard', 'com.canonical.indicator.session', 'com.canonical.indicator.datetime', 'com.canonical.indicator.power', 'com.canonical.indicator.sound', 'com.canonical.indicator.test', 'application']

安装

cp 90_unity-greeter.gschema.override /usr/share/glib-2.0/schemas/
glib-compile-schemas /usr/share/glib-2.0/schemas/

测试

sudo service lightdm restart

注意事项 如果您希望用户随时能够关闭应用程序,DBus服务可能会带来麻烦。最好使用自动启动功能,就像默认的指示器一样。
我已经在这里上传了准备好的文件。

https://github.com/sneetsher/mysystemindicator_minimum

这里是一个修改过的副本。

https://github.com/sneetsher/mysystemindicator

我尝试了不同模式下的不同菜单。它可以快速安装和测试。

这似乎太简单了,可以轻松移植到任何支持GIO Gnome库(包括DBus)的其他语言中。由于我正在寻找Python,我以后可能会添加它。

参考资料: libindicator README:指示器服务文件格式 https://bazaar.launchpad.net/~indicator-applet-developers/libindicator/trunk.14.04/view/head:/README


欢迎来到AskUbuntu。这是我在这里的回答的副本https://askubuntu.com/a/752750/26246,已经在问题中链接了。它是用C语言编写的。但问题是关于Python版本/移植。Marto已经发布了一个可行的解决方案,请查看被接受的答案。 - user.dz
@user.dz 对于C语言有自己的想法,是我理解错了。 - DCM
1没问题,@DCM,祝你有个愉快的一天。 - user.dz