Add checks to run on different env e.g. GTK<3.18 or without xdotool v0.99
authorAdrian Iain Lam <adrianiainlam@users.noreply.github.com>
Sun, 17 Sep 2017 21:20:09 +0000 (05:20 +0800)
committerAdrian Iain Lam <adrianiainlam@users.noreply.github.com>
Sun, 17 Sep 2017 21:20:09 +0000 (05:20 +0800)
 - Scroll lock hidden if GTK < 3.18
 - drop-down menu hidden (except quit) if xdotool not found / not executable
 - custom xdotool path can be specified

Add version number to prepare for future .deb packaging plan

Fix drop-down menu creation to not create unnecessary items
(e.g. those not listed in ORDER)

Move blocks of code around for better encapsulation

README.md
indicator-keyboard-led.py

index e8f123c..c7f7e56 100644 (file)
--- a/README.md
+++ b/README.md
@@ -33,8 +33,8 @@ distributions. To install the rest, run:
 
 **Note**: [`gdk_keymap_get_scroll_lock_state ()`][gtkdoc-scroll] is only
 available since GTK+ 3.18. The earliest Ubuntu release that supports
-GTK+ 3.18 is 16.04 (Xenial). This means that if you use an older version
-you will have to ignore Scroll lock functionality (See [Examples](#examples)).
+GTK+ 3.18 is 16.04 (Xenial). This means that if you use an older version,
+scroll lock functionality will be disabled.
 
 [gtkdoc-scroll]: https://developer.gnome.org/gdk3/stable/gdk3-Keyboard-Handling.html#gdk-keymap-get-scroll-lock-state
 
@@ -66,7 +66,7 @@ Order changed to Caps Num Scroll.
 `python3 indicator-keyboard-led.py --order CNS`
 
 ![indicator NC][sc5]  
-Hide Scroll lock. `python3 indicator-keyboard-led.py --order NC`
+Hide Scroll lock, also the default appearance if Scroll lock is not supported. `python3 indicator-keyboard-led.py --order NC`
 
 ![indicator C short][sc6]  
 Show Caps lock only, short appearance.
@@ -84,7 +84,7 @@ Default appearance in a French locale.
 [sc6]: screenshots/sc6.png
 [sc7]: screenshots/sc7.png
 
-## Known bugs
+## Known bugs / Troubleshooting
 
 ### Pressing Scroll Lock does nothing
 
@@ -119,15 +119,19 @@ modified. [Original source][origsrc] by dm+ on PCLinuxOS-Forums.
 [origsrc]: http://www.pclinuxos.com/forum/index.php/topic,125690.msg1052201.html?PHPSESSID=2qsv83lve6dgd0ivq14bfcjc30#msg1052201
 [quotesrc]: http://askubuntu.com/a/597757/274080
 
-### X11Keymap has no attribute 'get_scroll_lock_state'
+### Scroll lock does not appear on indicator
 
-If running the script gives the error message
+Your installed GTK+ version is probably older than 3.18, which
+does not support `get_scroll_lock_state`. Please consider upgrading
+your system if you really want Scroll lock functionality.
 
-    AttributeError: 'gtk.gdk.X11Keymap' object has no attribute 'get_scroll_lock_state'
+If your installed GTK+ is 3.18+ then please file a bug report.
 
-then your installed GTK+ version is probably older than 3.18, which
-does not support `get_scroll_lock_state`. Either hide all Scroll Lock
-functionality (See [Examples](#examples)), or upgrade your system.
+### Drop-down menu only has "Quit", the clickable locks do not appear
+
+Please verify that xdotool is installed in your PATH with the executable
+bit set. If you provided a custom path to xdotool please verify that it
+is correct and is an executable regular file.
 
 ## Localization
 
index 86aa543..7f78bc6 100755 (executable)
@@ -2,7 +2,7 @@
 # -*- coding: utf-8 -*-
 #
 # indicator-keyboard-led - simulate keyboard lock keys LED
-# Copyright (c) 2016 Adrian I Lam <adrianiainlam@gmail.com>
+# Copyright (c) 2017 Adrian I Lam <me@adrianiainlam.tk>
 #
 # Permission is hereby granted, free of charge, to any person obtaining a
 # copy of this software and associated documentation files (the "Software"),
 
 import signal
 import subprocess
-from os import path
+import os
 import argparse
-import sys
+import shutil
 import gi
 gi.require_version('Gdk', '3.0')
 gi.require_version('Gtk', '3.0')
 gi.require_version('AppIndicator3', '0.1')
 from gi.repository import Gdk, Gtk, AppIndicator3
 
-SCRIPT_DIR = path.dirname(path.realpath(__file__))
+APP_NAME = 'indicator-keyboard-led'
+APP_VERSION = '1.1'
+
+SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
 import gettext
-t = gettext.translation('default', path.join(SCRIPT_DIR, 'locale'))
+t = gettext.translation('default', os.path.join(SCRIPT_DIR, 'locale'))
 _ = t.gettext
 
 class IndicatorKeyboardLED:
-    locks = { 'N': _('Num'), 'C': _('Caps'), 'S': _('Scroll') }
-    
-    def __init__(self, short=False, order='NCS'):
+    def __init__(self, short=False, order='NCS', xdotool=None):
+        self.validate_order(order)
+
         self.indicator = AppIndicator3.Indicator.new(
-            'indicator-keyboard-led',
-            path.join(SCRIPT_DIR, 'icon.svg'),
+            APP_NAME, os.path.join(SCRIPT_DIR, 'icon.svg'),
             AppIndicator3.IndicatorCategory.APPLICATION_STATUS)
         self.indicator.set_status(AppIndicator3.IndicatorStatus.ACTIVE)
 
         if short:
             self.locks = { 'N': _('N'), 'C': _('C'), 'S': _('S') }
+        else:
+            self.locks = { 'N': _('Num'), 'C': _('Caps'), 'S': _('Scroll') }
 
         keymap = Gdk.Keymap.get_default()
         keymap.connect('state-changed', self.update_indicator, order)
         self.update_indicator(keymap, order)
+        self.create_menu(xdotool, order)
 
+    def create_menu(self, xdotool, order):
         menu = Gtk.Menu()
-        items = {
-            'N': Gtk.MenuItem.new_with_label(_('Num Lock')),
-            'C': Gtk.MenuItem.new_with_label(_('Caps Lock')),
-            'S': Gtk.MenuItem.new_with_label(_('Scroll Lock'))
-        }
-        items['N'].connect('activate', self.send_keypress, 'Num_Lock')
-        items['C'].connect('activate', self.send_keypress, 'Caps_Lock')
-        items['S'].connect('activate', self.send_keypress, 'Scroll_Lock')
+        xdotool = xdotool or shutil.which('xdotool')
+        if xdotool and os.access(xdotool, os.X_OK) and os.path.isfile(xdotool):
+            def send_keypress(menuitem, keystroke):
+                subprocess.call([xdotool, 'key', keystroke])
+            def new_menu_item(itemtype):
+                if itemtype == 'N':
+                    item = Gtk.MenuItem.new_with_label(_('Num Lock'))
+                    item.connect('activate', send_keypress, 'Num_Lock')
+                elif itemtype == 'C':
+                    item = Gtk.MenuItem.new_with_label(_('Caps Lock'))
+                    item.connect('activate', send_keypress, 'Caps_Lock')
+                elif itemtype == 'S':
+                    item = Gtk.MenuItem.new_with_label(_('Scroll Lock'))
+                    item.connect('activate', send_keypress, 'Scroll_Lock')
+                else:
+                    raise ValueError('Invalid itemtype')
+                return item
 
-        for i in order:
-            menu.append(items[i])
+            for i in order:
+                menu.append(new_menu_item(i))
+            menu.append(Gtk.SeparatorMenuItem())
 
         quit_item = Gtk.MenuItem.new_with_label(_('Quit'))
-        menu.append(Gtk.SeparatorMenuItem())
         menu.append(quit_item)
         quit_item.connect('activate', Gtk.main_quit)
 
@@ -94,17 +109,19 @@ class IndicatorKeyboardLED:
             labels += [('⚫' if state else '⚪') + self.locks[i]]
         self.indicator.set_label(' '.join(labels), '')
 
-    def send_keypress(self, menuitem, keystroke):
-        subprocess.call(['xdotool', 'key', keystroke])
+    def validate_order(self, order):
+        order = order.upper()
+        for i in order:
+            if i not in ['N', 'C', 'S']:
+                raise ValueError('Illegal character in ORDER. '
+                                 '(Choices: [N, C, S])')
+        if len(order) != len(set(order)):
+            raise ValueError('Repeated character in ORDER. '
+                             'Please specify each lock at most once.')
+        if 'S' in order and Gtk.check_version(3, 18, 0) is not None:
+            # Silently drop Scroll lock if GTK <= 3.18
+            order.remove('S')
 
-def validate_order(args):
-    args.order = args.order.upper()
-    for i in args.order:
-        if i not in ['N', 'C', 'S']:
-            sys.exit('Illegal character in ORDER. (Choices: [N, C, S])')
-    if len(args.order) != len(set(args.order)):
-        sys.exit('Repeated character in ORDER. '
-                 'Please specify each lock at most once.')
 
 if __name__ == '__main__':
     signal.signal(signal.SIGINT, lambda signum, frame: Gtk.main_quit())
@@ -117,8 +134,12 @@ if __name__ == '__main__':
     parser.add_argument('-o', '--order', required=False, default='NCS',
         help='specify the order of the locks displayed, e.g. CSN for '
              '⚫Caps ⚫Scroll ⚫Num, or NC for ⚫Num ⚫Caps without Scroll lock')
+    parser.add_argument('-x', '--xdotool', required=False,
+        help='provide full path to xdotool executable, '
+             'e.g. "/usr/bin/xdotool"; '
+             'if not specified, search in PATH')
     args = parser.parse_args()
-    validate_order(args)
 
-    IndicatorKeyboardLED(short=args.short, order=args.order)
+    IndicatorKeyboardLED(short=args.short, order=args.order,
+                         xdotool=args.xdotool)
     Gtk.main()