[gnome-shell] ibusCandidatePopup: A candidate popup for IBus input methods



commit 04074f883f44cd9c73c5c0e0ae1be04d76fc5ff9
Author: Rui Matos <tiagomatos gmail com>
Date:   Tue May 29 19:30:46 2012 +0200

    ibusCandidatePopup: A candidate popup for IBus input methods
    
    This is an implementation of the org.freedesktop.IBus.Panel API which
    shows a shell style popup (BoxPointer) when using an IBus input
    method.
    
    Based on code from the ibus-gjs project[1].
    
    [1] https://github.com/fujiwarat/ibus-gjs
    
    https://bugzilla.gnome.org/show_bug.cgi?id=641531

 data/theme/gnome-shell.css  |   14 +++
 js/Makefile.am              |    1 +
 js/ui/ibusCandidatePopup.js |  228 +++++++++++++++++++++++++++++++++++++++++++
 js/ui/status/keyboard.js    |   39 +++++++-
 4 files changed, 279 insertions(+), 3 deletions(-)
---
diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css
index 56b88ff..986535c 100644
--- a/data/theme/gnome-shell.css
+++ b/data/theme/gnome-shell.css
@@ -2006,3 +2006,17 @@ StScrollBar StButton#vhandle:hover
     -arrow-rise: 10px;
     -boxpointer-gap: 5px;
 }
+
+/* IBus Candidate Popup */
+.candidate-index {
+    padding: 0.5em 0.5em 0.5em 0.5em;
+}
+
+.candidate-label {
+    padding: 0.5em 0.5em 0.5em 0.5em;
+}
+
+.candidate-label:selected {
+    border-radius: 4px;
+    background-color: rgba(255,255,255,0.33);
+}
diff --git a/js/Makefile.am b/js/Makefile.am
index a3a7301..5c56c9c 100644
--- a/js/Makefile.am
+++ b/js/Makefile.am
@@ -51,6 +51,7 @@ nobase_dist_js_DATA = 	\
 	ui/extensionSystem.js	\
 	ui/extensionDownloader.js \
 	ui/flashspot.js		\
+	ui/ibusCandidatePopup.js\
 	ui/iconGrid.js		\
 	ui/keyboard.js		\
 	ui/keyringPrompt.js	\
diff --git a/js/ui/ibusCandidatePopup.js b/js/ui/ibusCandidatePopup.js
new file mode 100644
index 0000000..043f3ca
--- /dev/null
+++ b/js/ui/ibusCandidatePopup.js
@@ -0,0 +1,228 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+const Clutter = imports.gi.Clutter;
+const IBus = imports.gi.IBus;
+const Lang = imports.lang;
+const St = imports.gi.St;
+
+const BoxPointer = imports.ui.boxpointer;
+const Main = imports.ui.main;
+const PopupMenu = imports.ui.popupMenu;
+
+const MAX_CANDIDATES_PER_PAGE = 16;
+
+const CandidateArea = new Lang.Class({
+    Name: 'CandidateArea',
+    Extends: PopupMenu.PopupBaseMenuItem,
+
+    _init: function() {
+        this.parent({ reactive: false });
+
+        // St.Table exhibits some sizing problems so let's go with a
+        // clutter layout manager for now.
+        this._table = new Clutter.Actor();
+        this.addActor(this._table);
+
+        this._tableLayout = new Clutter.TableLayout();
+        this._table.set_layout_manager(this._tableLayout);
+
+        this._indexLabels = [];
+        this._candidateLabels = [];
+        for (let i = 0; i < MAX_CANDIDATES_PER_PAGE; ++i) {
+            this._indexLabels.push(new St.Label({ style_class: 'candidate-index' }));
+            this._candidateLabels.push(new St.Label({ style_class: 'candidate-label' }));
+        }
+
+        this._orientation = -1;
+        this._cursorPosition = 0;
+    },
+
+    _setOrientation: function(orientation) {
+        if (this._orientation == orientation)
+            return;
+
+        this._orientation = orientation;
+
+        this._table.remove_all_children();
+
+        if (this._orientation == IBus.Orientation.HORIZONTAL)
+            for (let i = 0; i < MAX_CANDIDATES_PER_PAGE; ++i) {
+                this._tableLayout.pack(this._indexLabels[i], i*2, 0);
+                this._tableLayout.pack(this._candidateLabels[i], i*2 + 1, 0);
+            }
+        else                    // VERTICAL || SYSTEM
+            for (let i = 0; i < MAX_CANDIDATES_PER_PAGE; ++i) {
+                this._tableLayout.pack(this._indexLabels[i], 0, i);
+                this._tableLayout.pack(this._candidateLabels[i], 1, i);
+            }
+    },
+
+    setCandidates: function(indexes, candidates, orientation, cursorPosition, cursorVisible) {
+        this._setOrientation(orientation);
+
+        for (let i = 0; i < MAX_CANDIDATES_PER_PAGE; ++i) {
+            let visible = i < candidates.length;
+            this._indexLabels[i].visible = visible;
+            this._candidateLabels[i].visible = visible;
+
+            if (!visible)
+                continue;
+
+            this._indexLabels[i].text = ((indexes && indexes[i]) ? indexes[i] : '%x.'.format(i + 1));
+            this._candidateLabels[i].text = candidates[i];
+        }
+
+        this._candidateLabels[this._cursorPosition].remove_style_pseudo_class('selected');
+        this._cursorPosition = cursorPosition;
+        if (cursorVisible)
+            this._candidateLabels[cursorPosition].add_style_pseudo_class('selected');
+    },
+});
+
+const CandidatePopup = new Lang.Class({
+    Name: 'CandidatePopup',
+    Extends: PopupMenu.PopupMenu,
+
+    _init: function() {
+        this._cursor = new St.Bin({ opacity: 0 });
+        Main.uiGroup.add_actor(this._cursor);
+
+        this.parent(this._cursor, 0, St.Side.TOP);
+        this.actor.hide();
+        Main.uiGroup.add_actor(this.actor);
+
+        this._preeditTextItem = new PopupMenu.PopupMenuItem('', { reactive: false });
+        this._preeditTextItem.actor.hide();
+        this.addMenuItem(this._preeditTextItem);
+
+        this._auxTextItem = new PopupMenu.PopupMenuItem('', { reactive: false });
+        this._auxTextItem.actor.hide();
+        this.addMenuItem(this._auxTextItem);
+
+        this.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
+
+        this._lookupTableItem = new CandidateArea();
+        this._lookupTableItem.actor.hide();
+        this.addMenuItem(this._lookupTableItem);
+
+        this._panelService = null;
+    },
+
+    setPanelService: function(panelService) {
+        this._panelService = panelService;
+        if (!panelService)
+            return;
+
+        panelService.connect('set-cursor-location',
+                             Lang.bind(this, function(ps, x, y, w, h) {
+                                 this._cursor.set_position(x, y);
+                                 this._cursor.set_size(w, h);
+                             }));
+        panelService.connect('update-preedit-text',
+                             Lang.bind(this, function(ps, text, cursorPosition, visible) {
+                                 if (visible)
+                                     this._preeditTextItem.actor.show();
+                                 else
+                                     this._preeditTextItem.actor.hide();
+                                 this._updateVisibility();
+
+                                 this._preeditTextItem.actor.label_actor.text = text.get_text();
+
+                                 let attrs = text.get_attributes();
+                                 if (attrs)
+                                     this._setTextAttributes(this._preeditTextItem.actor.label_actor.clutter_text,
+                                                             attrs);
+                             }));
+        panelService.connect('show-preedit-text',
+                             Lang.bind(this, function(ps) {
+                                 this._preeditTextItem.actor.show();
+                                 this._updateVisibility();
+                             }));
+        panelService.connect('hide-preedit-text',
+                             Lang.bind(this, function(ps) {
+                                 this._preeditTextItem.actor.hide();
+                                 this._updateVisibility();
+                             }));
+        panelService.connect('update-auxiliary-text',
+                             Lang.bind(this, function(ps, text, visible) {
+                                 if (visible)
+                                     this._auxTextItem.actor.show();
+                                 else
+                                     this._auxTextItem.actor.hide();
+                                 this._updateVisibility();
+
+                                 this._auxTextItem.actor.label_actor.text = text.get_text();
+                             }));
+        panelService.connect('show-auxiliary-text',
+                             Lang.bind(this, function(ps) {
+                                 this._auxTextItem.actor.show();
+                                 this._updateVisibility();
+                             }));
+        panelService.connect('hide-auxiliary-text',
+                             Lang.bind(this, function(ps) {
+                                 this._auxTextItem.actor.hide();
+                                 this._updateVisibility();
+                             }));
+        panelService.connect('update-lookup-table',
+                             Lang.bind(this, function(ps, lookupTable, visible) {
+                                 if (visible)
+                                     this._lookupTableItem.actor.show();
+                                 else
+                                     this._lookupTableItem.actor.hide();
+                                 this._updateVisibility();
+
+                                 let cursorPos = lookupTable.get_cursor_pos();
+                                 let pageSize = lookupTable.get_page_size();
+                                 let page = ((cursorPos == 0) ? 0 : Math.floor(cursorPos / pageSize));
+                                 let startIndex = page * pageSize;
+                                 let endIndex = Math.min((page + 1) * pageSize,
+                                                         lookupTable.get_number_of_candidates());
+                                 let indexes = [];
+                                 let indexLabel;
+                                 for (let i = 0; indexLabel = lookupTable.get_label(i); ++i)
+                                      indexes.push(indexLabel.get_text());
+
+                                 let candidates = [];
+                                 for (let i = startIndex; i < endIndex; ++i)
+                                     candidates.push(lookupTable.get_candidate(i).get_text());
+
+                                 this._lookupTableItem.setCandidates(indexes,
+                                                                     candidates,
+                                                                     lookupTable.get_orientation(),
+                                                                     cursorPos % pageSize,
+                                                                     lookupTable.is_cursor_visible());
+                             }));
+        panelService.connect('show-lookup-table',
+                             Lang.bind(this, function(ps) {
+                                 this._lookupTableItem.actor.show();
+                                 this._updateVisibility();
+                             }));
+        panelService.connect('hide-lookup-table',
+                             Lang.bind(this, function(ps) {
+                                 this._lookupTableItem.actor.hide();
+                                 this._updateVisibility();
+                             }));
+        panelService.connect('focus-out',
+                             Lang.bind(this, function(ps) {
+                                 this.close(BoxPointer.PopupAnimation.NONE);
+                             }));
+    },
+
+    _updateVisibility: function() {
+        let isVisible = (this._preeditTextItem.actor.visible ||
+                         this._auxTextItem.actor.visible ||
+                         this._lookupTableItem.actor.visible);
+
+        if (isVisible)
+            this.open(BoxPointer.PopupAnimation.NONE);
+        else
+            this.close(BoxPointer.PopupAnimation.NONE);
+    },
+
+    _setTextAttributes: function(clutterText, ibusAttrList) {
+        let attr;
+        for (let i = 0; attr = ibusAttrList.get(i); ++i)
+            if (attr.get_attr_type() == IBus.AttrType.BACKGROUND)
+                clutterText.set_selection(attr.get_start_index(), attr.get_end_index());
+    }
+});
diff --git a/js/ui/status/keyboard.js b/js/ui/status/keyboard.js
index e9edef4..6d3b041 100644
--- a/js/ui/status/keyboard.js
+++ b/js/ui/status/keyboard.js
@@ -14,6 +14,7 @@ try {
     var IBus = imports.gi.IBus;
     if (!('new_async' in IBus.Bus))
         throw "IBus version is too old";
+    const IBusCandidatePopup = imports.ui.ibusCandidatePopup;
 } catch (e) {
     var IBus = null;
     log(e);
@@ -41,8 +42,10 @@ const IBusManager = new Lang.Class({
         IBus.init();
 
         this._readyCallback = readyCallback;
+        this._candidatePopup = new IBusCandidatePopup.CandidatePopup();
 
         this._ibus = null;
+        this._panelService = null;
         this._engines = {};
         this._ready = false;
 
@@ -53,10 +56,14 @@ const IBusManager = new Lang.Class({
     },
 
     _clear: function() {
+        if (this._panelService)
+            this._panelService.destroy();
         if (this._ibus)
             this._ibus.destroy();
 
         this._ibus = null;
+        this._panelService = null;
+        this._candidatePopup.setPanelService(null);
         this._engines = {};
         this._ready = false;
     },
@@ -68,6 +75,10 @@ const IBusManager = new Lang.Class({
 
     _onConnected: function() {
         this._ibus.list_engines_async(-1, null, Lang.bind(this, this._initEngines));
+        this._ibus.request_name_async(IBus.SERVICE_PANEL,
+                                      IBus.BusNameFlag.REPLACE_EXISTING,
+                                      -1, null,
+                                      Lang.bind(this, this._initPanelService));
         this._ibus.connect('disconnected', Lang.bind(this, this._clear));
     },
 
@@ -78,12 +89,34 @@ const IBusManager = new Lang.Class({
                 let name = enginesList[i].get_name();
                 this._engines[name] = enginesList[i];
             }
-            this._ready = true;
-            if (this._readyCallback)
-                this._readyCallback();
         } else {
             this._clear();
+            return;
+        }
+
+        this._updateReadiness();
+    },
+
+    _initPanelService: function(ibus, result) {
+        let success = this._ibus.request_name_async_finish(result);
+        if (success) {
+            this._panelService = new IBus.PanelService({ connection: this._ibus.get_connection(),
+                                                         object_path: IBus.PATH_PANEL });
+            this._candidatePopup.setPanelService(this._panelService);
+        } else {
+            this._clear();
+            return;
         }
+
+        this._updateReadiness();
+    },
+
+    _updateReadiness: function() {
+        this._ready = (Object.keys(this._engines).length > 0 &&
+                       this._panelService != null);
+
+        if (this._ready && this._readyCallback)
+            this._readyCallback();
     },
 
     getEngineDesc: function(id) {



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]