使用 DBus 将面板 Applet 迁移到 Gnome Shell。






4.75/5 (5投票s)
本文介绍了一种方法,可以在 Linux 上的 Gnome Shell 中复现面板小程序(菜单和与应用程序的交互)的行为,适用于 Mono 应用程序,借助 DBus。

引言
Gnome Shell 是一种用户界面技术,目前在 Linux Gnome 桌面上越来越普及。它有很多优点,但也存在一些缺点,特别是如果您在之前的 Gnome 版本下开发过一些应用程序。最显著的变化之一是对小程序面板(类似于 Windows 中的状态图标)缺乏向后兼容性。本文提供了一种将基于小程序面板的应用程序迁移到 Gnome Shell 技术的方法。应用程序本身基于 Mono/C#,但该思路可以应用于其他语言。
背景
Mono 作为一种在 Linux 上开发 C# 应用程序的便捷方式被引入。它工作良好,并已集成到一些主要的 Linux 发行版中。通过 Gtk# 与 Gnome 集成,并提供一组与 Windows 对应功能相似的功能。状态图标是其中之一,在 Gnome 环境中称为面板小程序。虽然其用途可能存在争议,但它是一种控制应用程序并集成到用户桌面的便捷方式。
我最近升级到了最新的 Ubuntu Oneiric Ocelot,其中包含了 Gnome 3 和 Gnome Shell。Gnome 2 已不再受官方支持,并引入了一些重大更改,尤其是小程序面板。我过去实现的一些依赖状态图标来向用户报告信息或提供菜单的应用程序,在新发行版上不再像以前那样正常工作。然后我开始考虑一种替代方案。
正确的方法可能是遵循新的 Gnome 理念,从 Shell 的角度重构应用程序。我也可以回到 Gnome 2 并保持现状,因为我深知升级迟早会发生。鉴于我已经在某些应用程序上投入了大量时间,并且希望保留 Gnome Shell 的优势,我需要一个快速的解决方案。这时 Gnome Shell 扩展就派上用场了。
Gnome Shell 扩展是一种钩入桌面实现并修改其行为的方式。它基于 JavaScript,并通过库的帮助,允许在桌面上放置控件或操作。其中一个集成库是 DBus
。
DBus
是一个提供进程间通信的框架。它支持基于封送对象的事件和方法调用。它通常是大多数 Linux 发行版的默认组件,并拥有广泛的语言库(Perl, JavaScript, C, C#, python,...)。这个通信层是本文解释的解决方案的粘合剂。
实现
代码包含两个部分:Shell 扩展和 DBus
监听器。
Shell 扩展在状态栏显示一个图标,其中包含可用菜单。当用户单击菜单项时,它还会向 DBus
监听器发送事件。最后,它还可以响应 DBus
监听器引发的事件,并向用户显示消息。如果 Shell 扩展初始化时 DBus
监听器不可用,则提供一个菜单项来刷新菜单。
DBus
监听器应用程序将在总线上提供支持扩展所需的各种对象。有一个用于初始化总线的按钮,一个用于向扩展发送时间的按钮,以及一个用于显示扩展活动的文本控件。
DBus 集成
DBus
是本文的核心。与之相关的命名空间是
using NDesk.DBus;
using org.freedesktop.DBus;
在 MonoDevelop 中无法直接获得这些引用,需要从以下位置手动链接:
- /usr/lib/mono/gac/NDesk.DBus/1.0.0.0__f6716e4f9b2ed099/NDesk.DBus.dll
- /usr/lib/mono/gac/NDesk.DBus.GLib/1.0.0.0__f6716e4f9b2ed099/NDesk.DBus.GLib.dll
对于不同的发行版,路径可能需要调整。这些库也包含在演示二进制文件中,应该与较新的 Mono 版本兼容。
如果库不在 Mono 的 GAC 中,可能需要使用以下命令从存储库注册它们:
apt-get install libndesk-dbus-glib1.0-cil libndesk-dbus1.0-cil
DBus
通过封送对象和接口进行工作。因此,总线上提供的类定义如下:
[Interface("com.cadexis.dbusmenusample")]
public class NotificationObject : MarshalByRefObject
{
...
}
Interface
属性用于唯一地命名 Bus
上的对象。名称可以是任何内容,但通常的约定是使用反向的域名,因为这些名称足够唯一,可以与任何应用程序共享对象,并且在按字母顺序排序时逻辑分组,以方便发现。
该类还继承自 MarshalByRefObject
,以方便将类的基本类型在进程之间传递。
为扩展提供菜单项列表的方法
从扩展调用方法与从主应用程序调用方法没有太大区别,并且类中的实现看起来会相同。
public Dictionary<string,string> GetMenu(string text)
{
//Initialize the object that will be returned
Dictionary<string,string> retDic = new Dictionary<string, string>();
//Log the message on the application that some consumer used the method
LogMessage(string.Format("GetMenu is called from {0}",text));
//Populate the list of items (name and unique Id)
retDic.Add("MenuItem1","ID1");
retDic.Add("MenuItem2","ID2");
retDic.Add("MenuItem3","ID3");
//Return the list of items
return retDic;
}
该方法接受一个 string
参数并返回一个 Dictionary
的 string
s。此参数用作修饰符,基于调用它的应用程序,以返回足够的信息用于扩展菜单。名称将显示为菜单项,ID 将传递回主应用程序以处理数据。
单击菜单项时提交操作的方法
这只是类中的另一个方法,它将从扩展调用,但在主应用程序中执行。
public void SendMessage(string text)
{
Console.WriteLine(text);
}
在这种情况下,除了记录文本之外,没有做其他事情。为了根据参数执行特定操作,可以使其更复杂。
应用程序的回调
扩展可以对主 applicationEvent
触发的事件做出响应。首先,需要定义一个委托来指定回调的签名。它接受一个 string
作为参数。然后必须在类上定义一个 public
事件,该事件将在扩展中与之关联。最后,必须通过某些逻辑引发该事件。这可以通过从主应用程序调用 ChangeStatus
方法来实现。
public delegate void StatusChangedHandler(string param);
[Interface("com.cadexis.centerdocbus")]
public class NotificationObject : MarshalByRefObject
{
//Handler to notify the extension of an event
public event StatusChangedHandler StatusChanged;
//Method to indicate to the application that the status has changed
//and raise the event for the extension to react
public void ChangeStatus(string status)
{
if (StatusChanged!=null)
{
SendMessage(string.Format("Changing status to {0} ",status));
StatusChanged(status);
}
else
{
SendMessage("No delegate");
}
}
//Application Log
public void SendMessage(string text)
{
Console.WriteLine(text);
}
...
}
现在该类已完成,可以集成到称为 DBus
Listener 的扩展和主应用程序中。
DBus 监听器
这是一个基本的桌面应用程序,它实例化 NotificationObject
类并处理来自扩展的通知。
为了将 Bus
特定的信息与主应用程序隔离,创建了一个名为 DBusMonitor
的类。
//Imports of the DBus namespaces
using NDesk.DBus;
using org.freedesktop.DBus;
//Definition of the delegate to log information on the application
public delegate void Logger(string msg);
//Wrapper class for the NotificationObject class
public class DBusMonitor
{
//DBus session that is used for the application
Bus bus;
//Marshalled object that will live on the bus for remote access by the extension
NotificationObject notify;
//Logger method to allow a hook from the UI application
Logger log =null;
//DBus path of the NotificationObject
ObjectPath path = new ObjectPath ("/com/cadexis/dbusmenusample");
//DBus session name
string busName = "com.cadexis.dbusmenusample";
//Constructor
public DBusMonitor (Logger log_)
{
//Set the logger of the instance
log = log_;
//Initializes a DBus session
BusG.Init();
//Set the current session of the bus for future use
bus = Bus.Session;
}
public void InitObjects()
{
//Check that the object is not already instantiated
if (notify==null)
{
//Check that the current bus session is
//owned by the current application
if (bus.RequestName (busName) == RequestNameReply.PrimaryOwner)
{
//Create a new instance of the NotificationObject,
//with the associated logger delegate
notify = new NotificationObject (log);
//Register the object on the bus so that it can
//be accessible from other applications,
//including Gnome extensions.
//The object will be available through the path value.
bus.Register(path, notify);
}
}
}
...
}
Logger
委托是一种方便的方式,可以将信息传递回 UI。如果未设置,输出将发送到控制台。它作为构造函数的参数传递。
busName
值应与 NotificationObject
接口中指定的值相同 (com.cadexis.centerdocbus)。
bus
会话中的对象路径应唯一。由于只有一个对象将被注册,因此选择了与 busName
相同的值,将点替换为斜杠,使其看起来像一个路径。
BusG.Init()
是将应用程序与 DBus
集成的部分。它实例化了一些 static
变量,这些变量可以在之后使用。在这种情况下,DBus.Session
是将用于注册 NotificationObject
的实例。这是通过 Register(string objectPath,object marshalByRefObject)
方法完成的。
此类中还添加了一个测试方法来模拟应用程序状态的更改,该事件应流向扩展。
public void SendTest()
{
try
{
//Get the notification object instance that is on the bus
NotificationObject not = bus.GetObject<notificationobject /> (busName, path);
//Raised the StatusChanged event
not.ChangeStatus("SERVER_EVENT");
}
catch (Exception ex)
{
Log("Cannot instantiate the remote object"+ex.Message);
}
}
通过使用类中已有的 notify
变量可以避免 try catch
,但这只是为了演示如何访问总线上的对象。这是通过使用 GetObject
方法实现的。
在应用程序代码本身,与 DBusMonitor
包装器的集成非常简单:
- 通过提供一个
Logger
方法,该方法将作为实例一起传递: - 在应用程序启动时实例化
DBusMonitor
: - 在单击“Initialize DBus Object”按钮时调用
InitializeBus
方法: - 在单击“Send Message”按钮时调用
SendTest
方法:
class MainClass
{
static DBusMonitor dbmon;
public static void Main (string[] args)
{
Application.Init ();
//Init of the DBusMonitor to send and receive the events
dbmon = new DBusMonitor(LogMsg);
//Init the layout (buttons and textview
...
}
//Initialize the objects on the bus
static void OnButtonListenClicked(object obj, EventArgs args)
{
LogMsg("Start Listening");
dbmon.InitializeBus();
}
//Send a message on the bus for the clients
static void OnButtonSendClicked (object sender, System.EventArgs e)
{
LogMsg("Send Message");
dbmon.SendTest();
}
//Logs messages in the textview
static void LogMsg(string msg)
{
textview1.Buffer.InsertAtCursor(string.Format
("{0}: {1}{2}",DateTime.Now.ToString(),msg,Environment.NewLine));
}
}
Shell 扩展基础
扩展是集成到 Gnome 桌面的部分。
扩展特定于安装在机器上的 Shell 版本。因此,创建兼容扩展的最佳方法是使用以下命令:
gnome-shell-extension-tool --create-extension
Name: DBbusMenuSample
Description: Gnome extension with Dbus integration
Uuid: dbusmenusample@cadexis.com
此命令将在 ~/.local/share/gnome-shell/extensions/dbusmenusample@cadexis.com/ 目录中创建三个文件。
- metadata.json:此文件包含扩展的描述以及扩展的目标 gnome shell 版本。
- stylesheet.css:这是扩展的样式,用于特定自定义。
- extension.js:这是包含代码逻辑的文件。
extension.js 具有默认实现,通常是在单击顶部栏中显示的新图标时,在屏幕中间显示“Hello World”。
扩展需要三个方法:
init
:在扩展首次加载时调用。disable
:用户可以按需禁用或启用扩展,在扩展被停用时调用此方法。enable
:在扩展加载时或用户激活扩展时调用。
为了激活此新扩展,需要重新启动 Shell。可以通过按 alt+F2 并输入 r 命令(r=Restart)来完成。
如果在 Shell 重新启动后未显示新图标,可以使用另一个工具来调查任何问题,即 Looking Glass。可以通过 alt+F2 并输入 lg 命令(lg=Looking Glass)来访问它。这将显示遇到的错误以及有关 Shell 的其他信息。
状态栏在代码中通过 const Main = imports.ui.main;
进行引用。Main
通过 panel
属性公开了栏的各个部分(_rightbox
用于左侧图标,_centerbox
用于中间图标,_leftbox
用于右侧图标,_menus
用于附加到面板的菜单)。
Shell 扩展逻辑
扩展仍将保持默认行为,因此通过将默认的 extension.js 替换为本文提供的文件,就可以实现与 DBus
的集成。
首先,扩展的代码需要从 JavaScript 的角度复制 NotificationObject
对象。
由于大部分逻辑实际上是在应用程序端执行的,因此在扩展端只需要签名。
name
:此部分对应 C# 中的接口名称。methods
:此部分列出了可通过此对象在总线上使用的所有方法签名。signals
:与方法类似,但用于事件。
对象的完整定义将是:
//Remote object definition
const NotificationObjectInterface = {
name: 'com.cadexis.dbusmenusample',
methods: [
{ //method to get the current status, getting a string and passing a string
name: 'Status',
inSignature: 's',
outSignature: 's'
},
{ //method to send a message, passing a string as argument
name: 'SendMessage',
inSignature: 's',
outSignature: ''
},
{ //method to retrieve the menu items
name: 'GetMenu',
inSignature: 's',
outSignature: 'a{ss}'
},
],
signals: [
{ //event raised when the status on the application is changed.
name: 'StatusChanged',
inSignature: 's'
}
]
};
需要注意的一些事项。
DBus
命名空间可以通过语句const DBus = imports.dbus
; 导入。- 对象名称为
NotificationObjectInterface
,而不是 C# 中的NotificationObject
。这是DBus
正确解析对象和方法的一种方式,因为 JavaScript 不是强类型语言。这种命名“怪癖”也用于方法,其中“Remote
”一词被添加到名称中,用于从扩展调用方法。 - 方法或信号中的每个对象都由一个必需的名称以及可选的
inSignature
和outSignature
(分别用于输入和输出签名)定义。 - 签名定义使用 DBus API 签名,它使用字符定义参数类型,例如“
s
”表示string
参数。 a{ss}
是一个dictionary
的签名,其键和值均为string
s,它将包含菜单项定义作为GetMenu
方法的输出。
然后,可以在 init
方法中完成对象与扩展的集成:
function init()
{
//Create the remote object, based on the correct path and bus name
let NotificationProxy = DBus.makeProxyClass(NotificationObjectInterface);
dbusNotify = new NotificationProxy(DBus.session,
'com.cadexis.dbusmenusample',
'/com/cadexis/dbusmenusample');
//Set the delegate to the StatusChanged Event
dbusNotify.connect("StatusChanged",_statusChanged);
//Create the icon button
dbusMenuButton = new AppMenu(dbusNotify);
this.enable();
}
使用相同的总线名称 (com.cadexis.dbusmenusample) 和对象路径 (/com/cadexis/dbusmenusample) 来检索应用程序在总线上注册的对象。通过 connect
方法监视 StatusChanged
事件,当事件从 DBus
Listener 触发时,将调用 _statusChanged
方法。
AppMenu
是一个包含 UI 代码的类。它公开了一个 actor
属性(菜单栏中显示的图标)和一个 menu
(通过 DBus
公开的动作列表)。它继承自 SystemStatusButton
类。
AppMenu.prototype = {
__proto__: PanelMenu.SystemStatusButton.prototype,
_init: function(notify) {
//Save the remote object as reference
this._notify = dbusNotify;
//Create the StatusButton on the Panel Menu, using the system-run icon
PanelMenu.SystemStatusButton.prototype._init.call(this, 'system-run');
},
...
};
enable()
方法将图标(称为 actor)集成到栏的右侧部分以及菜单。
function enable() {
//Add the appMenu object on the right box
Main.panel._rightBox.insert_actor(dbusMenuButton.actor, 1);
Main.panel._rightBox.child_set(dbusMenuButton.actor, { y_fill : true } );
//Add the appMenu item container to the panel
Main.panel._menus.addMenu(dbusMenuButton.menu);
//Async callback to retrieve the content of the appMenu items
dbusNotify.GetMenuRemote('INIT', _refreshMenuList);
}
GetMenuRemote
调用 NotificationObject
上的 GetMenu
,并通过 _refreshMenuList
方法填充菜单项。
menuitem
中的每个元素都继承自 PopupMenu.PopupBaseMenuItem
,公开一个 actor(一个带有项目文本的标签),并在 actor 激活时使用项目 ID 调用 _notifyApp
。
AppMenuItem.prototype = {
__proto__: PopupMenu.PopupBaseMenuItem.prototype,
_init: function (lblText,lblId,appMenu, notify, params) {
PopupMenu.PopupBaseMenuItem.prototype._init.call(this, params);
this.label = new St.Label({ text: lblText });
this.addActor(this.label);
this._notify=notify;
this._text = lblText;
this._idTxt = lblId;
this._appMenu = appMenu;
},
activate: function (event) {
//This allows to refresh the menu item list when the AppMenuRefresh is clicked.
if ("AppMenuRefresh"==this._idTxt)
{
this._appMenu.handleEvent(this._idTxt);
}
else
{
this._notifyApp(this._idTxt);
}
//This will close the menu after it the menu item is closed
PopupMenu.PopupBaseMenuItem.prototype.activate.call(this, event);
},
_notifyApp: function (id)
{
this._notify.SendMessageRemote('MenuClick:'+ id);
}
};
disable
方法只需调用 AppMenu
类的 destroy
方法,即可完成扩展。
关注点
总的来说,这只是一个 DBus
用法的示例,可以根据不同的应用程序需求进行不同程度的复杂化。
引入一个两部分应用程序而不是单个语言应用程序可能看起来很笨拙,并且在各种系统和发行版之间保持一致性会更困难。
然而,这也提供了一个机会,使应用程序更加模块化,并开始将某些功能暴露给外部进程,反之亦然。
有用链接
历史
- 2011-10-29 - 首次发布