diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css index 56b88ff15..986535cef 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 a3a730176..5c56c9c42 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 000000000..043f3caf1 --- /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 e9edef4f1..6d3b04196 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) {