summaryrefslogtreecommitdiffstats
path: root/js/ui/ibusCandidatePopup.js
diff options
context:
space:
mode:
Diffstat (limited to 'js/ui/ibusCandidatePopup.js')
-rw-r--r--js/ui/ibusCandidatePopup.js359
1 files changed, 359 insertions, 0 deletions
diff --git a/js/ui/ibusCandidatePopup.js b/js/ui/ibusCandidatePopup.js
new file mode 100644
index 0000000..268b324
--- /dev/null
+++ b/js/ui/ibusCandidatePopup.js
@@ -0,0 +1,359 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported CandidatePopup */
+
+const { Clutter, GObject, IBus, St } = imports.gi;
+
+const BoxPointer = imports.ui.boxpointer;
+const Main = imports.ui.main;
+
+var MAX_CANDIDATES_PER_PAGE = 16;
+
+var DEFAULT_INDEX_LABELS = [
+ '1', '2', '3', '4', '5', '6', '7', '8', '9', '0',
+ 'a', 'b', 'c', 'd', 'e', 'f',
+];
+
+var CandidateArea = GObject.registerClass({
+ Signals: {
+ 'candidate-clicked': {
+ param_types: [
+ GObject.TYPE_UINT, GObject.TYPE_UINT, Clutter.ModifierType.$gtype,
+ ],
+ },
+ 'cursor-down': {},
+ 'cursor-up': {},
+ 'next-page': {},
+ 'previous-page': {},
+ },
+}, class CandidateArea extends St.BoxLayout {
+ _init() {
+ super._init({
+ vertical: true,
+ reactive: true,
+ visible: false,
+ });
+ this._candidateBoxes = [];
+ for (let i = 0; i < MAX_CANDIDATES_PER_PAGE; ++i) {
+ const box = new St.BoxLayout({
+ style_class: 'candidate-box',
+ reactive: true,
+ track_hover: true,
+ });
+ box._indexLabel = new St.Label({ style_class: 'candidate-index' });
+ box._candidateLabel = new St.Label({ style_class: 'candidate-label' });
+ box.add_child(box._indexLabel);
+ box.add_child(box._candidateLabel);
+ this._candidateBoxes.push(box);
+ this.add(box);
+
+ let j = i;
+ box.connect('button-release-event', (actor, event) => {
+ this.emit('candidate-clicked', j, event.get_button(), event.get_state());
+ return Clutter.EVENT_PROPAGATE;
+ });
+ }
+
+ this._buttonBox = new St.BoxLayout({ style_class: 'candidate-page-button-box' });
+
+ this._previousButton = new St.Button({
+ style_class: 'candidate-page-button candidate-page-button-previous button',
+ x_expand: true,
+ });
+ this._buttonBox.add_child(this._previousButton);
+
+ this._nextButton = new St.Button({
+ style_class: 'candidate-page-button candidate-page-button-next button',
+ x_expand: true,
+ });
+ this._buttonBox.add_child(this._nextButton);
+
+ this.add(this._buttonBox);
+
+ this._previousButton.connect('button-press-event', () => {
+ this.emit('previous-page');
+ return Clutter.EVENT_STOP;
+ });
+ this._previousButton.connect('touch-event', (actor, event) => {
+ if (event.type() === Clutter.EventType.TOUCH_BEGIN) {
+ this.emit('previous-page');
+ return Clutter.EVENT_STOP;
+ }
+ return Clutter.EVENT_PROPAGATE;
+ });
+ this._nextButton.connect('button-press-event', () => {
+ this.emit('next-page');
+ return Clutter.EVENT_STOP;
+ });
+ this._nextButton.connect('touch-event', (actor, event) => {
+ if (event.type() === Clutter.EventType.TOUCH_BEGIN) {
+ this.emit('next-page');
+ return Clutter.EVENT_STOP;
+ }
+ return Clutter.EVENT_PROPAGATE;
+ });
+
+ this._orientation = -1;
+ this._cursorPosition = 0;
+ }
+
+ vfunc_scroll_event(scrollEvent) {
+ switch (scrollEvent.direction) {
+ case Clutter.ScrollDirection.UP:
+ this.emit('cursor-up');
+ break;
+ case Clutter.ScrollDirection.DOWN:
+ this.emit('cursor-down');
+ break;
+ }
+ return Clutter.EVENT_PROPAGATE;
+ }
+
+ setOrientation(orientation) {
+ if (this._orientation == orientation)
+ return;
+
+ this._orientation = orientation;
+
+ if (this._orientation == IBus.Orientation.HORIZONTAL) {
+ this.vertical = false;
+ this.remove_style_class_name('vertical');
+ this.add_style_class_name('horizontal');
+ this._previousButton.icon_name = 'go-previous-symbolic';
+ this._nextButton.icon_name = 'go-next-symbolic';
+ } else { // VERTICAL || SYSTEM
+ this.vertical = true;
+ this.add_style_class_name('vertical');
+ this.remove_style_class_name('horizontal');
+ this._previousButton.icon_name = 'go-up-symbolic';
+ this._nextButton.icon_name = 'go-down-symbolic';
+ }
+ }
+
+ setCandidates(indexes, candidates, cursorPosition, cursorVisible) {
+ for (let i = 0; i < MAX_CANDIDATES_PER_PAGE; ++i) {
+ let visible = i < candidates.length;
+ let box = this._candidateBoxes[i];
+ box.visible = visible;
+
+ if (!visible)
+ continue;
+
+ box._indexLabel.text = indexes && indexes[i] ? indexes[i] : DEFAULT_INDEX_LABELS[i];
+ box._candidateLabel.text = candidates[i];
+ }
+
+ this._candidateBoxes[this._cursorPosition].remove_style_pseudo_class('selected');
+ this._cursorPosition = cursorPosition;
+ if (cursorVisible)
+ this._candidateBoxes[cursorPosition].add_style_pseudo_class('selected');
+ }
+
+ updateButtons(wrapsAround, page, nPages) {
+ if (nPages < 2) {
+ this._buttonBox.hide();
+ return;
+ }
+ this._buttonBox.show();
+ this._previousButton.reactive = wrapsAround || page > 0;
+ this._nextButton.reactive = wrapsAround || page < nPages - 1;
+ }
+});
+
+var CandidatePopup = GObject.registerClass(
+class IbusCandidatePopup extends BoxPointer.BoxPointer {
+ _init() {
+ super._init(St.Side.TOP);
+ this.visible = false;
+ this.style_class = 'candidate-popup-boxpointer';
+
+ this._dummyCursor = new Clutter.Actor({ opacity: 0 });
+ Main.layoutManager.uiGroup.add_actor(this._dummyCursor);
+
+ Main.layoutManager.addTopChrome(this);
+
+ const box = new St.BoxLayout({
+ style_class: 'candidate-popup-content',
+ vertical: true,
+ });
+ this.bin.set_child(box);
+
+ this._preeditText = new St.Label({
+ style_class: 'candidate-popup-text',
+ visible: false,
+ });
+ box.add(this._preeditText);
+
+ this._auxText = new St.Label({
+ style_class: 'candidate-popup-text',
+ visible: false,
+ });
+ box.add(this._auxText);
+
+ this._candidateArea = new CandidateArea();
+ box.add(this._candidateArea);
+
+ this._candidateArea.connect('previous-page', () => {
+ this._panelService.page_up();
+ });
+ this._candidateArea.connect('next-page', () => {
+ this._panelService.page_down();
+ });
+
+ this._candidateArea.connect('cursor-up', () => {
+ this._panelService.cursor_up();
+ });
+ this._candidateArea.connect('cursor-down', () => {
+ this._panelService.cursor_down();
+ });
+
+ this._candidateArea.connect('candidate-clicked', (area, index, button, state) => {
+ this._panelService.candidate_clicked(index, button, state);
+ });
+
+ this._panelService = null;
+ }
+
+ setPanelService(panelService) {
+ this._panelService = panelService;
+ if (!panelService)
+ return;
+
+ panelService.connect('set-cursor-location', (ps, x, y, w, h) => {
+ this._setDummyCursorGeometry(x, y, w, h);
+ });
+ try {
+ panelService.connect('set-cursor-location-relative', (ps, x, y, w, h) => {
+ if (!global.display.focus_window)
+ return;
+ let window = global.display.focus_window.get_compositor_private();
+ this._setDummyCursorGeometry(window.x + x, window.y + y, w, h);
+ });
+ } catch (e) {
+ // Only recent IBus versions have support for this signal
+ // which is used for wayland clients. In order to work
+ // with older IBus versions we can silently ignore the
+ // signal's absence.
+ }
+ panelService.connect('update-preedit-text', (ps, text, cursorPosition, visible) => {
+ this._preeditText.visible = visible;
+ this._updateVisibility();
+
+ this._preeditText.text = text.get_text();
+
+ let attrs = text.get_attributes();
+ if (attrs) {
+ this._setTextAttributes(this._preeditText.clutter_text,
+ attrs);
+ }
+ });
+ panelService.connect('show-preedit-text', () => {
+ this._preeditText.show();
+ this._updateVisibility();
+ });
+ panelService.connect('hide-preedit-text', () => {
+ this._preeditText.hide();
+ this._updateVisibility();
+ });
+ panelService.connect('update-auxiliary-text', (_ps, text, visible) => {
+ this._auxText.visible = visible;
+ this._updateVisibility();
+
+ this._auxText.text = text.get_text();
+ });
+ panelService.connect('show-auxiliary-text', () => {
+ this._auxText.show();
+ this._updateVisibility();
+ });
+ panelService.connect('hide-auxiliary-text', () => {
+ this._auxText.hide();
+ this._updateVisibility();
+ });
+ panelService.connect('update-lookup-table', (_ps, lookupTable, visible) => {
+ this._candidateArea.visible = visible;
+ this._updateVisibility();
+
+ let nCandidates = lookupTable.get_number_of_candidates();
+ let cursorPos = lookupTable.get_cursor_pos();
+ let pageSize = lookupTable.get_page_size();
+ let nPages = Math.ceil(nCandidates / pageSize);
+ let page = cursorPos == 0 ? 0 : Math.floor(cursorPos / pageSize);
+ let startIndex = page * pageSize;
+ let endIndex = Math.min((page + 1) * pageSize, nCandidates);
+
+ let indexes = [];
+ let indexLabel;
+ for (let i = 0; (indexLabel = lookupTable.get_label(i)); ++i)
+ indexes.push(indexLabel.get_text());
+
+ Main.keyboard.resetSuggestions();
+ Main.keyboard.setSuggestionsVisible(visible);
+
+ let candidates = [];
+ for (let i = startIndex; i < endIndex; ++i) {
+ candidates.push(lookupTable.get_candidate(i).get_text());
+
+ Main.keyboard.addSuggestion(lookupTable.get_candidate(i).get_text(), () => {
+ let index = i;
+ this._panelService.candidate_clicked(index, 1, 0);
+ });
+ }
+
+ this._candidateArea.setCandidates(indexes,
+ candidates,
+ cursorPos % pageSize,
+ lookupTable.is_cursor_visible());
+ this._candidateArea.setOrientation(lookupTable.get_orientation());
+ this._candidateArea.updateButtons(lookupTable.is_round(), page, nPages);
+ });
+ panelService.connect('show-lookup-table', () => {
+ Main.keyboard.setSuggestionsVisible(true);
+ this._candidateArea.show();
+ this._updateVisibility();
+ });
+ panelService.connect('hide-lookup-table', () => {
+ Main.keyboard.setSuggestionsVisible(false);
+ this._candidateArea.hide();
+ this._updateVisibility();
+ });
+ panelService.connect('focus-out', () => {
+ this.close(BoxPointer.PopupAnimation.NONE);
+ Main.keyboard.resetSuggestions();
+ });
+ }
+
+ _setDummyCursorGeometry(x, y, w, h) {
+ this._dummyCursor.set_position(Math.round(x), Math.round(y));
+ this._dummyCursor.set_size(Math.round(w), Math.round(h));
+
+ if (this.visible)
+ this.setPosition(this._dummyCursor, 0);
+ }
+
+ _updateVisibility() {
+ let isVisible = !Main.keyboard.visible &&
+ (this._preeditText.visible ||
+ this._auxText.visible ||
+ this._candidateArea.visible);
+
+ if (isVisible) {
+ this.setPosition(this._dummyCursor, 0);
+ this.open(BoxPointer.PopupAnimation.NONE);
+ // We shouldn't be above some components like the screenshot UI,
+ // so don't raise to the top.
+ // The on-screen keyboard is expected to be above any entries,
+ // so just above the keyboard gets us to the right layer.
+ const { keyboardBox } = Main.layoutManager;
+ this.get_parent().set_child_above_sibling(this, keyboardBox);
+ } else {
+ this.close(BoxPointer.PopupAnimation.NONE);
+ }
+ }
+
+ _setTextAttributes(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());
+ }
+ }
+});