gnome-shell/js/ui/ibusCandidatePopup.js
Carlos Garnacho 237ba24dbe ibusCandidatePopup: Set OSK candidates list visibility correctly
Right now, we don't pay much attention to visibility hints from IBus
about the candidates list for the OSK suggestions bar. Since some
IMs rely on this visibility being honored, do that.

To fix this, the visibility hints for the lookup table are now
propagated to the keyboard, so the Suggestions actor hides its
internal candidate buttons. Since the Suggestions actor gets its
minimum height from CSS, this does not result in OSK size jumps.

Closes: https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/5601
Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/2347>
2022-06-29 13:44:36 +00:00

339 lines
12 KiB
JavaScript

// -*- 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('clicked', () => {
this.emit('previous-page');
});
this._nextButton.connect('clicked', () => {
this.emit('next-page');
});
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.addChrome(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);
this.get_parent().set_child_above_sibling(this, null);
} 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());
}
}
});