8423ba44fe
These traditionally got the various ClutterEvent subtype structs as their argument, so it was not allowed to use ClutterEvent generic getter methods in these vfuncs. These methods used direct access to struct fields instead. This got spoiled with the move to make ClutterEvent opaque types, since these are no longer public structs so GNOME Shell most silently failed to fetch the expected values from event fields. But since they are not ClutterEvents either, the getters could not be used on them. Mutter is changing so that these vmethods all contain an alias to the one and only Clutter.Event type, thus lifting those barriers, and making it possible to use the ClutterEvent methods in these vfuncs. Closes: https://gitlab.gnome.org/GNOME/mutter/-/issues/2950 Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/2872>
346 lines
12 KiB
JavaScript
346 lines
12 KiB
JavaScript
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
|
|
|
import Clutter from 'gi://Clutter';
|
|
import GObject from 'gi://GObject';
|
|
import IBus from 'gi://IBus';
|
|
import St from 'gi://St';
|
|
|
|
import * as BoxPointer from './boxpointer.js';
|
|
import * as Main from './main.js';
|
|
|
|
const MAX_CANDIDATES_PER_PAGE = 16;
|
|
|
|
const DEFAULT_INDEX_LABELS = [
|
|
'1', '2', '3', '4', '5', '6', '7', '8', '9', '0',
|
|
'a', 'b', 'c', 'd', 'e', 'f',
|
|
];
|
|
|
|
const 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(event) {
|
|
switch (event.get_scroll_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;
|
|
}
|
|
});
|
|
|
|
export const 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());
|
|
}
|
|
}
|
|
});
|