如何将键盘修饰符状态小程序添加到Unity面板?

我是一个使用KDE的用户,正在考虑转移到Unity。由于手动残疾,我使用粘滞键,在KDE中,我有一个在系统面板中显示活动修饰键的小程序。我记得Gnome也有这个功能,Windows和OS X也有。
如何在Unity面板中添加键盘修饰键状态小程序?
澄清一下:我已经启用了粘滞键。我想知道如何添加一个小程序,用于指示修饰键的状态。这个指示器将显示Shift键、Alt键、Tux键和Ctrl键是否按下。这个小程序存在于所有主要的桌面环境(KDE、Windows、Mac OSX和Gnome)中。它对于桌面的可访问性是必需的。
这是键盘修饰符状态小程序的图像,位于键盘布局指示器小程序旁边。从左到右表示的修饰符有ShiftCtrlAlt我不知道这个Tux/WinNumLockCapsLock。可以看到NumLock键处于激活状态。

enter image description here


嗯...在这方面似乎存在着一个很大的差距...不过,当按下修饰键时会有一个蜂鸣声。 - Wilf
@wilf 如果我没记错的话,在Unity(和Gnome 3一样)中有一个辅助功能图标,但没有一个“额外”的图标来告知用户状态。 - Braiam
indicator-keylock是一个选择吗?还可以参考:http://askubuntu.com/a/37998/ 和 http://www.omgubuntu.co.uk/2010/09/indicator-keylock-ubuntu - 目前无法测试是否可以扩展到ALT SHIFT和CTRL键。 - Takkat
5@Takkat:感谢您的建议。indicator-keylock只显示键盘本身上传统具有状态指示器的那些键的状态:CapsLockScrollLockNumLock。我需要一个指示器来显示标准修饰键的状态:ShiftCtrlTuxAlt。所有主要的桌面环境(KDE、Windows、Mac OSX)都有这个指示器小程序可用。 - dotancohen
1жҲ‘зӣёдҝЎдҪ жүҖжҢҮзҡ„KDEе·Ҙе…·еҢ…еҗҚз§°жҳҜplasma-widget-kbstateпјҢеңЁиҪҜ件дёӯеҝғиҝӣиЎҢеҝ«йҖҹжҗңзҙўзЎ®е®һжІЎжңүжүҫеҲ°д»»дҪ•зӯүж•Ҳз»“жһңгҖӮ - Daniel W.
@datancohen 请告诉我们这个方法是否适用于您,并且您使用的是哪个版本,以便我们能够为所有可能遇到此问题的人提供最佳帮助。 - Elder Geek
你能告诉我在KDE中你使用的是什么软件吗?我正在尝试使用KDE,但找不到这样的指示软件。 - shengy
2@shengy:我正在使用KB State小部件。如果你在Kubuntu上,可以通过sudo apt-get install plasma-widget-kbstate来安装它。 - dotancohen
3个回答

这是Unity中的一个突出问题:

下面的代码已经更新,现在可以使用图标来显示状态。但有时可能会变慢,因为我必须更新硬盘上的图标文件,然后重新加载它。(请参阅关于此问题/限制的说明libappindicator

一个精心打包的版本已经在webupd8 ppa上提供(感谢Alin Andrei /Andrew/)

sudo add-apt-repository ppa:nilarimogard/webupd8
sudo apt-get update
sudo apt-get install indicator-xkbmod

参考:Ubuntu的键盘修饰符状态指示器:Xkbmod Indicator

原始答案:

这并不是一个权威的问题答案。可以将其视为一种变通方法。希望有人能提供更复杂的解决方案。

这是一个简单的Unity键盘修饰符指示器的原型。

从左到右的图标:Shift、Caps Lock、Ctrl、Alt、Super、AltGr锁定(小圆圈表示锁定状态)

screenshot of unity-xkbmod prototype

源文件 unity-xkbmod.c:

/*
 * unity-xkbmod.c
 *
 * Copyright 2014 Sneetsher <sneetsher@localhost>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301, USA.
 *
 *
 */

#include <string.h>

#include <X11/XKBlib.h>

#include <glib/gprintf.h>
#include <gtk/gtk.h>
#include <libappindicator/app-indicator.h>

//callback data structure
typedef struct _AppData {
  Display *_display;
  int *_deviceId;
  AppIndicator *indicator;
} AppData;

//menu ui
static GtkActionEntry entries[] = {
  { "Quit",     "application-exit", "_Quit", "<control>Q",
    "Exit the application", G_CALLBACK (gtk_main_quit) },
};

static guint n_entries = G_N_ELEMENTS (entries);

static const gchar *ui_info =
"<ui>"
"  <popup name='IndicatorPopup'>"
"    <menuitem action='Quit' />"
"  </popup>"
"</ui>";

//callback function, get xkb state, update indicator label (icon have limitation)
static gboolean update_xkb_state (gpointer data)
{
  //get xkb state
  XkbStateRec xkbState;
  XkbGetState(((AppData*) data)->_display, *(((AppData*) data)->_deviceId), &xkbState);

  //construct label
  GString *label = g_string_new("");

  register int i;
  unsigned bit;

  //loop taken from xkbwatch source
  for (i = XkbNumModifiers - 1, bit = 0x80; i >= 0; i--, bit >>= 1)
  {
    //printf("base%d %s  ", i, (xkbState.base_mods & bit) ? "on " : "off");
    //printf("latched%d %s  ", i, (xkbState.latched_mods & bit) ? "on " : "off");
    //printf("locked%d %s  ", i, (xkbState.locked_mods & bit) ? "on " : "off");
    //printf("effective%d %s  ", i, (xkbState.mods & bit) ? "on " : "off");
    //printf("compat%d %s\n", i, (xkbState.compat_state & bit) ? "on " : "off");

    //todo: change constant with xkb modifier constant (defined in the headers)
    // show effective modifier stat
    switch (i)
    {
      case 7:
        g_string_prepend (label,  ((xkbState.mods & bit) ? "⎇" : ""));
        break;
      case 6:
        g_string_prepend (label,  ((xkbState.mods & bit) ? "⌘" : ""));
        break;
      case 5:
        g_string_prepend (label,  ((xkbState.mods & bit) ? "5" : ""));
        break;
      case 4:
        g_string_prepend (label,  ((xkbState.mods & bit) ? "①" : ""));
        break;
      case 3:
        g_string_prepend (label,  ((xkbState.mods & bit) ? "⌥" : ""));
        break;
      case 2:
        g_string_prepend (label,  ((xkbState.mods & bit) ? "⋀" : ""));
        break;
      case 1:
        g_string_prepend (label,  ((xkbState.mods & bit) ? "⇬" : ""));
        break;
      case 0:
        g_string_prepend (label,  ((xkbState.mods & bit) ? "⇧" : ""));
        break;
      default:
        break;
    };

    // show if modifier is locked
    g_string_prepend (label,  ((xkbState.locked_mods & bit) ? " ˳" : " "));
  }

  //g_string_prepend (label,  "");
  app_indicator_set_label (((AppData*) data)->indicator, label->str, NULL);

  //g_free(label);
  return TRUE;
}


int main (int argc, char **argv)
{
  AppData appdata;
  Display *_display;
  int _deviceId;

  char* displayName = strdup("");
  int eventCode;
  int errorReturn;
  int major = XkbMajorVersion;
  int minor = XkbMinorVersion;;
  int reasonReturn;


  AppIndicator *indicator;
  GtkWidget *indicator_menu;
  GtkUIManager *uim;
  GtkActionGroup *action_group;
  GError *error = NULL;

  gtk_init (&argc, &argv);


  XkbIgnoreExtension(False);

  g_printf("Xkb client lib ver: %d.%d\n" , major , minor );
  _display = XkbOpenDisplay(displayName, &eventCode, &errorReturn,
                            &major, &minor, &reasonReturn);
  g_printf("Xkb server lib ver: %d.%d\n" , major , minor );

  if (reasonReturn != XkbOD_Success)
  {
    g_printf("Unable to open display!\n");
    return 1;
  }

  XkbDescRec* kbdDescPtr = XkbAllocKeyboard();
  if (kbdDescPtr == NULL)
  {
    g_printf ("Failed to get keyboard description.\n");
    return 2;
  }
  kbdDescPtr->dpy = _display;
  _deviceId = kbdDescPtr->device_spec;

  /*
  //no need for event listener, used gtk_timeout timer
  XkbSelectEventDetails(_display, XkbUseCoreKbd, XkbStateNotify,
                     XkbAllStateComponentsMask, XkbGroupStateMask);
  */


  action_group = gtk_action_group_new ("AppActions");
  gtk_action_group_add_actions (action_group, entries, n_entries, NULL);

  indicator = app_indicator_new_with_path (
                                        "Simple XKB Modifier Indicator",
                                        "icon",
                                        APP_INDICATOR_CATEGORY_HARDWARE,
                                        g_get_current_dir());

  uim = gtk_ui_manager_new ();
  gtk_ui_manager_insert_action_group (uim, action_group, 0);
  if (!gtk_ui_manager_add_ui_from_string (uim, ui_info, -1, &error))
  {
    g_printf ("Failed to build menus: %s\n", error->message);
    g_error_free (error);
    error = NULL;
    return 3;
  }

  indicator_menu = gtk_ui_manager_get_widget (uim, "/ui/IndicatorPopup");
  app_indicator_set_menu (indicator, GTK_MENU (indicator_menu));
  app_indicator_set_status (indicator, APP_INDICATOR_STATUS_ACTIVE);

  //app_indicator_set_label (indicator, " ⇧ ⋀ ⌥ ⎇  ⌘ ", NULL);
  //symbols: shift U21E7 ctrl U22C0 alt/altgr U2325 U2387  cmd U2318
  //from font: DejaVu Sans

  appdata._display = _display;
  appdata._deviceId = &_deviceId;
  appdata.indicator = indicator;
  gtk_timeout_add (120, (GtkFunction) update_xkb_state, &appdata);
  //nice for realtime tasks, to replace gtk_timeout
  //gtk_idle_add ((GtkFunction) idle_func, &appdata);

  gtk_main ();

  XCloseDisplay (_display);
  return 0;
}

安装所需的头文件/库:(不确定是否漏掉了什么)
sudo apt-get install libx11-dev libappindicator-dev libgtk2.0-dev
编译:
gcc -Wall unity-xkbmod.c -o unity-xkbmod `pkg-config --cflags --libs appindicator-0.1` -lX11
运行:
./unity-xkbmod

注意:

  • libappindicator 用于Unity指示器的库缺少一个重要功能,这使得将其他桌面环境的指示器移植变得容易。 请参见Bug #812067 API所需功能:pixbuf图标设置支持

    没有这个功能,假设我们需要(Shift、Ctrl、Alt、AltGr、Super)与粘滞键处于活动状态;对于每个按键,我们有3种主要状态(关闭、开启/锁定、锁定)。因此,应该生成3^5个组合图标。(正常情况下只需要3x5个单独的图标)

    这就是为什么我使用了来自DejaVu Sans字体的符号作为指示器标签。

  • 要放置一个图标,请将其放在相同的文件夹中,并将其命名为icon.*。接受的格式:png、svg、ico、xpm等...

    如果您不喜欢任何图标,只需创建一个1x1像素的图像即可。

参考资料:


好的,谢谢!我会试一试然后再回复你。干得好,Sneetsher! - dotancohen
你这个讨厌鬼! - dotancohen
是的,谢谢@dotancohen。所以至少你可以使用Unity,也许没有固定位置的图标会有点丑陋? - user.dz
1我不介意丑,你应该看看我的照片!我已经在Ubuntu错误跟踪器上提到了这个问题。 - dotancohen
@dotancohen,我更新了我的回答以反映当前状态。现在它可以显示指示图标(虽然速度较慢,但似乎正常工作)。 - user.dz
1Sneetsher,我想再次感谢你。我现在已经转移到了Unity,如果没有这个非常棒的应用程序,这个转移是不可能的。我也从代码中学到了很多东西。谢谢! - dotancohen
@dotancohen,很高兴再次收到你的来信,并且知道它对你和其他用户有所帮助。我可以问一下你使用的是哪个版本吗?也就是说,你是如何安装的?是从哪个ppa或者源仓库获取的呢? - user.dz
我刚刚在这个答案上编译了源代码。事实上,webupd8 PPA版本在我的15.10系统上无法正常工作。它可以成功安装,但无法响应键盘事件。我尝试向Launchpad上的软件包提交错误报告,但我不知道如何将错误报告分配给PPA中的特定软件包。 - dotancohen
@dotancohen 你能试试这个ppa(我的每日构建)https://launchpad.net/~sneetsher/+archive/ubuntu/one吗?如果不起作用,请在github上报告。如果你遇到任何问题,请告诉我。 - user.dz
在发布这个问题时,我看到了你的答案并尝试了indicator-xkbmod,无论是从你的PPA还是从WebUpd8 PPA安装的版本都没有起作用,只会持续显示一个错误的、无关的图标(来自qtox)。有什么想法为什么它在16.04上不起作用? - Byte Commander

另一种解决方案可能并非完美,但对某些人来说可能很有用,因为它可以实现与KDE中类似的完整功能,比如点击即可激活一个模块。
安装kbstate小程序。
sudo apt-get install plasma-widget-kbstate kde-workspace-bin
plasma-windowed播放器中运行它。
  • 常规窗口。
    plasma-windowed kbstate
    

    在Xubuntu中的plasma-widget-kbstate的截图

  • 无边框窗口。
    plasma-windowed --noborder kbstate
    

    在Unity中无边框的plasma-widget-kbstate的截图

我没有太多时间来玩,但是 wmctrl 可能有助于在启动时定位、调整大小并使其置于顶层。

参考:如何启动 KDE plasmoid 和 kickstart 菜单的命令


我在搜索Ubuntu粘滞键监视器时找到了一些可能对你有帮助的东西:http://code.google.com/p/key-mon/

show case screenshot

试着运行

key-mon --sticky以支持粘滞键功能。

参考:http://code.google.com/p/key-mon/

请注意,通过软件中心提供的版本是1.6-0ubuntu1。该版本发布于2011年6月,不支持--sticky开关。如果指示器看起来与上述相同,则表示您使用的是旧版本。请尝试最新版本http://code.google.com/p/key-mon/,截至本文撰写时,最新版本为keymon_1.17-1_all.deb,大小为229 KB,发布于2014年1月3日。已测试并确认支持--sticky开关。


1不错的发现,然而 key-mon 显示的是哪个按钮被按下而不是修饰键的状态。区别在于 key-mon 的显示在按钮被按下后1秒钟后返回到未按下的状态。键盘修饰键状态小程序会在下一次按键发生时将显示返回到未按下的状态,从而禁用“按下”状态。 - dotancohen
重新编辑的答案。 - Elder Geek
对于之前的混淆,我很抱歉。我已经(再次)编辑了答案,以提供您所需的准确内容。 - Elder Geek
1谢谢Elder Geek。--sticky选项似乎通过观察其他键的状态来猜测修饰键的状态,而不是像适当的键盘修饰符状态小部件一样查询正确的接口。这可以通过两次按下修饰键来看到:第一次启用小部件中的指示器,但第二次不会释放它。因此,当它实际上没有激活时,状态被错误地报告为处于活动状态。这个相关的错误部分解决了这个问题,我将在那里填写详细信息和其他错误。谢谢。 - dotancohen
1@dotancohen 我根据你的说法进行了测试,即“键盘修饰符状态小程序在下一次按键时会将显示恢复为未按下状态,从而禁用‘按下’状态。”我从未想过有人会故意连续两次按下修饰键。对于这个疏忽,我向你道歉。 - Elder Geek
谢谢你,Elder Geek。我在key-mon的错误跟踪器上提到了问题,但在有可行解决方案之前,我将保留这个问题未回答。谢谢! - dotancohen
@dotancohen 不用谢。感谢您澄清您要寻找的是对key-mon进行代码修改以实现锁定功能,还是寻找另一种可以实现该功能的替代程序。希望key-mon项目成员能够提供更多帮助。如果没有成功,也许您可以通过提供自己的补丁来帮助他们?祝一切顺利。 - Elder Geek