indicatorStatusIcon.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592
  1. // This file is part of the AppIndicator/KStatusNotifierItem GNOME Shell extension
  2. //
  3. // This program is free software; you can redistribute it and/or
  4. // modify it under the terms of the GNU General Public License
  5. // as published by the Free Software Foundation; either version 2
  6. // of the License, or (at your option) any later version.
  7. //
  8. // This program is distributed in the hope that it will be useful,
  9. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. // GNU General Public License for more details.
  12. //
  13. // You should have received a copy of the GNU General Public License
  14. // along with this program; if not, write to the Free Software
  15. // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  16. import Clutter from 'gi://Clutter';
  17. import Gio from 'gi://Gio';
  18. import GObject from 'gi://GObject';
  19. import St from 'gi://St';
  20. import * as AppDisplay from 'resource:///org/gnome/shell/ui/appDisplay.js';
  21. import * as Main from 'resource:///org/gnome/shell/ui/main.js';
  22. import * as Panel from 'resource:///org/gnome/shell/ui/panel.js';
  23. import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js';
  24. import * as AppIndicator from './appIndicator.js';
  25. import * as PromiseUtils from './promiseUtils.js';
  26. import * as SettingsManager from './settingsManager.js';
  27. import * as Util from './util.js';
  28. import * as DBusMenu from './dbusMenu.js';
  29. const DEFAULT_ICON_SIZE = Panel.PANEL_ICON_SIZE || 16;
  30. export function addIconToPanel(statusIcon) {
  31. if (!(statusIcon instanceof BaseStatusIcon))
  32. throw TypeError(`Unexpected icon type: ${statusIcon}`);
  33. const settings = SettingsManager.getDefaultGSettings();
  34. const indicatorId = `appindicator-${statusIcon.uniqueId}`;
  35. const currentIcon = Main.panel.statusArea[indicatorId];
  36. if (currentIcon) {
  37. if (currentIcon !== statusIcon)
  38. currentIcon.destroy();
  39. Main.panel.statusArea[indicatorId] = null;
  40. }
  41. Main.panel.addToStatusArea(indicatorId, statusIcon, 1,
  42. settings.get_string('tray-pos'));
  43. Util.connectSmart(settings, 'changed::tray-pos', statusIcon, () =>
  44. addIconToPanel(statusIcon));
  45. }
  46. export function getTrayIcons() {
  47. return Object.values(Main.panel.statusArea).filter(
  48. i => i instanceof IndicatorStatusTrayIcon);
  49. }
  50. export function getAppIndicatorIcons() {
  51. return Object.values(Main.panel.statusArea).filter(
  52. i => i instanceof IndicatorStatusIcon);
  53. }
  54. export const BaseStatusIcon = GObject.registerClass(
  55. class IndicatorBaseStatusIcon extends PanelMenu.Button {
  56. _init(menuAlignment, nameText, iconActor, dontCreateMenu) {
  57. super._init(menuAlignment, nameText, dontCreateMenu);
  58. const settings = SettingsManager.getDefaultGSettings();
  59. Util.connectSmart(settings, 'changed::icon-opacity', this, this._updateOpacity);
  60. this.connect('notify::hover', () => this._onHoverChanged());
  61. if (!super._onDestroy)
  62. this.connect('destroy', () => this._onDestroy());
  63. this._box = new St.BoxLayout({style_class: 'panel-status-indicators-box'});
  64. this.add_child(this._box);
  65. this._setIconActor(iconActor);
  66. this._showIfReady();
  67. }
  68. _setIconActor(icon) {
  69. if (!(icon instanceof Clutter.Actor))
  70. throw new Error(`${icon} is not a valid actor`);
  71. if (this._icon && this._icon !== icon)
  72. this._icon.destroy();
  73. this._icon = icon;
  74. this._updateEffects();
  75. this._monitorIconEffects();
  76. if (this._icon) {
  77. this._box.add_child(this._icon);
  78. const id = this._icon.connect('destroy', () => {
  79. this._icon.disconnect(id);
  80. this._icon = null;
  81. this._monitorIconEffects();
  82. });
  83. }
  84. }
  85. _onDestroy() {
  86. if (this._icon)
  87. this._icon.destroy();
  88. if (super._onDestroy)
  89. super._onDestroy();
  90. }
  91. isReady() {
  92. throw new GObject.NotImplementedError('isReady() in %s'.format(this.constructor.name));
  93. }
  94. get icon() {
  95. return this._icon;
  96. }
  97. get uniqueId() {
  98. throw new GObject.NotImplementedError('uniqueId in %s'.format(this.constructor.name));
  99. }
  100. _showIfReady() {
  101. this.visible = this.isReady();
  102. }
  103. _onHoverChanged() {
  104. if (this.hover) {
  105. this.opacity = 255;
  106. if (this._icon)
  107. this._icon.remove_effect_by_name('desaturate');
  108. } else {
  109. this._updateEffects();
  110. }
  111. }
  112. _updateOpacity() {
  113. const settings = SettingsManager.getDefaultGSettings();
  114. const userValue = settings.get_user_value('icon-opacity');
  115. if (userValue)
  116. this.opacity = userValue.unpack();
  117. else
  118. this.opacity = 255;
  119. }
  120. _updateEffects() {
  121. this._updateOpacity();
  122. if (this._icon) {
  123. this._updateSaturation();
  124. this._updateBrightnessContrast();
  125. }
  126. }
  127. _monitorIconEffects() {
  128. const settings = SettingsManager.getDefaultGSettings();
  129. const monitoring = !!this._iconSaturationIds;
  130. if (!this._icon && monitoring) {
  131. Util.disconnectSmart(settings, this, this._iconSaturationIds);
  132. delete this._iconSaturationIds;
  133. Util.disconnectSmart(settings, this, this._iconBrightnessIds);
  134. delete this._iconBrightnessIds;
  135. Util.disconnectSmart(settings, this, this._iconContrastIds);
  136. delete this._iconContrastIds;
  137. } else if (this._icon && !monitoring) {
  138. this._iconSaturationIds =
  139. Util.connectSmart(settings, 'changed::icon-saturation', this,
  140. this._updateSaturation);
  141. this._iconBrightnessIds =
  142. Util.connectSmart(settings, 'changed::icon-brightness', this,
  143. this._updateBrightnessContrast);
  144. this._iconContrastIds =
  145. Util.connectSmart(settings, 'changed::icon-contrast', this,
  146. this._updateBrightnessContrast);
  147. }
  148. }
  149. _updateSaturation() {
  150. const settings = SettingsManager.getDefaultGSettings();
  151. const desaturationValue = settings.get_double('icon-saturation');
  152. let desaturateEffect = this._icon.get_effect('desaturate');
  153. if (desaturationValue > 0) {
  154. if (!desaturateEffect) {
  155. desaturateEffect = new Clutter.DesaturateEffect();
  156. this._icon.add_effect_with_name('desaturate', desaturateEffect);
  157. }
  158. desaturateEffect.set_factor(desaturationValue);
  159. } else if (desaturateEffect) {
  160. this._icon.remove_effect(desaturateEffect);
  161. }
  162. }
  163. _updateBrightnessContrast() {
  164. const settings = SettingsManager.getDefaultGSettings();
  165. const brightnessValue = settings.get_double('icon-brightness');
  166. const contrastValue = settings.get_double('icon-contrast');
  167. let brightnessContrastEffect = this._icon.get_effect('brightness-contrast');
  168. if (brightnessValue !== 0 | contrastValue !== 0) {
  169. if (!brightnessContrastEffect) {
  170. brightnessContrastEffect = new Clutter.BrightnessContrastEffect();
  171. this._icon.add_effect_with_name('brightness-contrast', brightnessContrastEffect);
  172. }
  173. brightnessContrastEffect.set_brightness(brightnessValue);
  174. brightnessContrastEffect.set_contrast(contrastValue);
  175. } else if (brightnessContrastEffect) {
  176. this._icon.remove_effect(brightnessContrastEffect);
  177. }
  178. }
  179. });
  180. /*
  181. * IndicatorStatusIcon implements an icon in the system status area
  182. */
  183. export const IndicatorStatusIcon = GObject.registerClass(
  184. class IndicatorStatusIcon extends BaseStatusIcon {
  185. _init(indicator) {
  186. super._init(0.5, indicator.accessibleName,
  187. new AppIndicator.IconActor(indicator, DEFAULT_ICON_SIZE));
  188. this._indicator = indicator;
  189. this._lastClickTime = -1;
  190. this._lastClickX = -1;
  191. this._lastClickY = -1;
  192. this._box.add_style_class_name('appindicator-box');
  193. Util.connectSmart(this._indicator, 'ready', this, this._showIfReady);
  194. Util.connectSmart(this._indicator, 'menu', this, this._updateMenu);
  195. Util.connectSmart(this._indicator, 'label', this, this._updateLabel);
  196. Util.connectSmart(this._indicator, 'status', this, this._updateStatus);
  197. Util.connectSmart(this._indicator, 'reset', this, () => {
  198. this._updateStatus();
  199. this._updateLabel();
  200. });
  201. Util.connectSmart(this._indicator, 'accessible-name', this, () =>
  202. this.set_accessible_name(this._indicator.accessibleName));
  203. Util.connectSmart(this._indicator, 'destroy', this, () => this.destroy());
  204. this.connect('notify::visible', () => this._updateMenu());
  205. this._showIfReady();
  206. }
  207. _onDestroy() {
  208. if (this._menuClient) {
  209. this._menuClient.disconnect(this._menuReadyId);
  210. this._menuClient.destroy();
  211. this._menuClient = null;
  212. }
  213. super._onDestroy();
  214. }
  215. get uniqueId() {
  216. return this._indicator.uniqueId;
  217. }
  218. isReady() {
  219. return this._indicator && this._indicator.isReady;
  220. }
  221. _updateLabel() {
  222. const {label} = this._indicator;
  223. if (label) {
  224. if (!this._label || !this._labelBin) {
  225. this._labelBin = new St.Bin({
  226. yAlign: Clutter.ActorAlign.CENTER,
  227. });
  228. this._label = new St.Label();
  229. this._labelBin.add_actor(this._label);
  230. this._box.add_actor(this._labelBin);
  231. }
  232. this._label.set_text(label);
  233. if (!this._box.contains(this._labelBin))
  234. this._box.add_actor(this._labelBin); // FIXME: why is it suddenly necessary?
  235. } else if (this._label) {
  236. this._labelBin.destroy_all_children();
  237. this._box.remove_actor(this._labelBin);
  238. this._labelBin.destroy();
  239. delete this._labelBin;
  240. delete this._label;
  241. }
  242. }
  243. _updateStatus() {
  244. const wasVisible = this.visible;
  245. this.visible = this._indicator.status !== AppIndicator.SNIStatus.PASSIVE;
  246. if (this.visible !== wasVisible)
  247. this._indicator.checkAlive().catch(logError);
  248. }
  249. _updateMenu() {
  250. if (this._menuClient) {
  251. this._menuClient.disconnect(this._menuReadyId);
  252. this._menuClient.destroy();
  253. this._menuClient = null;
  254. this.menu.removeAll();
  255. }
  256. if (this.visible && this._indicator.menuPath) {
  257. this._menuClient = new DBusMenu.Client(this._indicator.busName,
  258. this._indicator.menuPath, this._indicator);
  259. if (this._menuClient.isReady)
  260. this._menuClient.attachToMenu(this.menu);
  261. this._menuReadyId = this._menuClient.connect('ready-changed', () => {
  262. if (this._menuClient.isReady)
  263. this._menuClient.attachToMenu(this.menu);
  264. else
  265. this._updateMenu();
  266. });
  267. }
  268. }
  269. _showIfReady() {
  270. if (!this.isReady())
  271. return;
  272. this._updateLabel();
  273. this._updateStatus();
  274. this._updateMenu();
  275. }
  276. _updateClickCount(event) {
  277. const [x, y] = event.get_coords();
  278. const time = event.get_time();
  279. const {doubleClickDistance, doubleClickTime} =
  280. Clutter.Settings.get_default();
  281. if (time > (this._lastClickTime + doubleClickTime) ||
  282. (Math.abs(x - this._lastClickX) > doubleClickDistance) ||
  283. (Math.abs(y - this._lastClickY) > doubleClickDistance))
  284. this._clickCount = 0;
  285. this._lastClickTime = time;
  286. this._lastClickX = x;
  287. this._lastClickY = y;
  288. this._clickCount = (this._clickCount % 2) + 1;
  289. return this._clickCount;
  290. }
  291. _maybeHandleDoubleClick(event) {
  292. if (this._indicator.supportsActivation === false)
  293. return Clutter.EVENT_PROPAGATE;
  294. if (event.get_button() !== Clutter.BUTTON_PRIMARY)
  295. return Clutter.EVENT_PROPAGATE;
  296. if (this._updateClickCount(event) === 2) {
  297. this._indicator.open(...event.get_coords(), event.get_time());
  298. return Clutter.EVENT_STOP;
  299. }
  300. return Clutter.EVENT_PROPAGATE;
  301. }
  302. async _waitForDoubleClick() {
  303. const {doubleClickTime} = Clutter.Settings.get_default();
  304. this._waitDoubleClickPromise = new PromiseUtils.TimeoutPromise(
  305. doubleClickTime);
  306. try {
  307. await this._waitDoubleClickPromise;
  308. this.menu.toggle();
  309. } catch (e) {
  310. if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
  311. throw e;
  312. } finally {
  313. delete this._waitDoubleClickPromise;
  314. }
  315. }
  316. vfunc_event(event) {
  317. if (this.menu.numMenuItems && event.type() === Clutter.EventType.TOUCH_BEGIN)
  318. this.menu.toggle();
  319. return Clutter.EVENT_PROPAGATE;
  320. }
  321. vfunc_button_press_event(event) {
  322. if (this._waitDoubleClickPromise)
  323. this._waitDoubleClickPromise.cancel();
  324. // if middle mouse button clicked send SecondaryActivate dbus event and do not show appindicator menu
  325. if (event.get_button() === Clutter.BUTTON_MIDDLE) {
  326. if (Main.panel.menuManager.activeMenu)
  327. Main.panel.menuManager._closeMenu(true, Main.panel.menuManager.activeMenu);
  328. this._indicator.secondaryActivate(event.get_time(), ...event.get_coords());
  329. return Clutter.EVENT_STOP;
  330. }
  331. if (event.get_button() === Clutter.BUTTON_SECONDARY) {
  332. this.menu.toggle();
  333. return Clutter.EVENT_PROPAGATE;
  334. }
  335. const doubleClickHandled = this._maybeHandleDoubleClick(event);
  336. if (doubleClickHandled === Clutter.EVENT_PROPAGATE &&
  337. event.get_button() === Clutter.BUTTON_PRIMARY &&
  338. this.menu.numMenuItems) {
  339. if (this._indicator.supportsActivation !== false)
  340. this._waitForDoubleClick().catch(logError);
  341. else
  342. this.menu.toggle();
  343. }
  344. return Clutter.EVENT_PROPAGATE;
  345. }
  346. vfunc_button_release_event(event) {
  347. if (!this._indicator.supportsActivation)
  348. return this._maybeHandleDoubleClick(event);
  349. return Clutter.EVENT_PROPAGATE;
  350. }
  351. vfunc_scroll_event(event) {
  352. // Since Clutter 1.10, clutter will always send a smooth scrolling event
  353. // with explicit deltas, no matter what input device is used
  354. // In fact, for every scroll there will be a smooth and non-smooth scroll
  355. // event, and we can choose which one we interpret.
  356. if (event.get_scroll_direction() === Clutter.ScrollDirection.SMOOTH) {
  357. const [dx, dy] = event.get_scroll_delta();
  358. this._indicator.scroll(dx, dy);
  359. return Clutter.EVENT_STOP;
  360. }
  361. return Clutter.EVENT_PROPAGATE;
  362. }
  363. });
  364. export const IndicatorStatusTrayIcon = GObject.registerClass(
  365. class IndicatorTrayIcon extends BaseStatusIcon {
  366. _init(icon) {
  367. super._init(0.5, icon.wm_class, icon, {dontCreateMenu: true});
  368. Util.Logger.debug(`Adding legacy tray icon ${this.uniqueId}`);
  369. this._box.add_style_class_name('appindicator-trayicons-box');
  370. this.add_style_class_name('appindicator-icon');
  371. this.add_style_class_name('tray-icon');
  372. this.connect('button-press-event', (_actor, _event) => {
  373. this.add_style_pseudo_class('active');
  374. return Clutter.EVENT_PROPAGATE;
  375. });
  376. this.connect('button-release-event', (_actor, event) => {
  377. this._icon.click(event);
  378. this.remove_style_pseudo_class('active');
  379. return Clutter.EVENT_PROPAGATE;
  380. });
  381. this.connect('key-press-event', (_actor, event) => {
  382. this.add_style_pseudo_class('active');
  383. this._icon.click(event);
  384. return Clutter.EVENT_PROPAGATE;
  385. });
  386. this.connect('key-release-event', (_actor, event) => {
  387. this._icon.click(event);
  388. this.remove_style_pseudo_class('active');
  389. return Clutter.EVENT_PROPAGATE;
  390. });
  391. Util.connectSmart(this._icon, 'destroy', this, () => {
  392. icon.clear_effects();
  393. this.destroy();
  394. });
  395. const settings = SettingsManager.getDefaultGSettings();
  396. Util.connectSmart(settings, 'changed::icon-size', this, this._updateIconSize);
  397. const themeContext = St.ThemeContext.get_for_stage(global.stage);
  398. Util.connectSmart(themeContext, 'notify::scale-factor', this, () =>
  399. this._updateIconSize());
  400. this._updateIconSize();
  401. }
  402. _onDestroy() {
  403. Util.Logger.debug(`Destroying legacy tray icon ${this.uniqueId}`);
  404. if (this._waitDoubleClickPromise)
  405. this._waitDoubleClickPromise.cancel();
  406. super._onDestroy();
  407. }
  408. isReady() {
  409. return !!this._icon;
  410. }
  411. get uniqueId() {
  412. return `legacy:${this._icon.wm_class}:${this._icon.pid}`;
  413. }
  414. vfunc_navigate_focus(from, direction) {
  415. this.grab_key_focus();
  416. return super.vfunc_navigate_focus(from, direction);
  417. }
  418. _getSimulatedButtonEvent(touchEvent) {
  419. const event = Clutter.Event.new(Clutter.EventType.BUTTON_RELEASE);
  420. event.set_button(1);
  421. event.set_time(touchEvent.get_time());
  422. event.set_flags(touchEvent.get_flags());
  423. event.set_stage(global.stage);
  424. event.set_source(touchEvent.get_source());
  425. event.set_coords(...touchEvent.get_coords());
  426. event.set_state(touchEvent.get_state());
  427. return event;
  428. }
  429. vfunc_touch_event(event) {
  430. // Under X11 we rely on emulated pointer events
  431. if (!imports.gi.Meta.is_wayland_compositor())
  432. return Clutter.EVENT_PROPAGATE;
  433. const slot = event.get_event_sequence().get_slot();
  434. if (!this._touchPressSlot &&
  435. event.get_type() === Clutter.EventType.TOUCH_BEGIN) {
  436. this.add_style_pseudo_class('active');
  437. this._touchButtonEvent = this._getSimulatedButtonEvent(event);
  438. this._touchPressSlot = slot;
  439. this._touchDelayPromise = new PromiseUtils.TimeoutPromise(
  440. AppDisplay.MENU_POPUP_TIMEOUT);
  441. this._touchDelayPromise.then(() => {
  442. delete this._touchDelayPromise;
  443. delete this._touchPressSlot;
  444. this._touchButtonEvent.set_button(3);
  445. this._icon.click(this._touchButtonEvent);
  446. this.remove_style_pseudo_class('active');
  447. });
  448. } else if (event.get_type() === Clutter.EventType.TOUCH_END &&
  449. this._touchPressSlot === slot) {
  450. delete this._touchPressSlot;
  451. delete this._touchButtonEvent;
  452. if (this._touchDelayPromise) {
  453. this._touchDelayPromise.cancel();
  454. delete this._touchDelayPromise;
  455. }
  456. this._icon.click(this._getSimulatedButtonEvent(event));
  457. this.remove_style_pseudo_class('active');
  458. } else if (event.get_type() === Clutter.EventType.TOUCH_UPDATE &&
  459. this._touchPressSlot === slot) {
  460. this.add_style_pseudo_class('active');
  461. this._touchButtonEvent = this._getSimulatedButtonEvent(event);
  462. }
  463. return Clutter.EVENT_PROPAGATE;
  464. }
  465. vfunc_leave_event(event) {
  466. this.remove_style_pseudo_class('active');
  467. if (this._touchDelayPromise) {
  468. this._touchDelayPromise.cancel();
  469. delete this._touchDelayPromise;
  470. }
  471. return super.vfunc_leave_event(event);
  472. }
  473. _updateIconSize() {
  474. const settings = SettingsManager.getDefaultGSettings();
  475. const {scaleFactor} = St.ThemeContext.get_for_stage(global.stage);
  476. let iconSize = settings.get_int('icon-size');
  477. if (iconSize <= 0)
  478. iconSize = DEFAULT_ICON_SIZE;
  479. this.height = -1;
  480. this._icon.set({
  481. width: iconSize * scaleFactor,
  482. height: iconSize * scaleFactor,
  483. xAlign: Clutter.ActorAlign.CENTER,
  484. yAlign: Clutter.ActorAlign.CENTER,
  485. });
  486. }
  487. });