tooltip.js 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. // SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect
  2. //
  3. // SPDX-License-Identifier: GPL-2.0-or-later
  4. import Clutter from 'gi://Clutter';
  5. import Gio from 'gi://Gio';
  6. import GLib from 'gi://GLib';
  7. import Pango from 'gi://Pango';
  8. import St from 'gi://St';
  9. import * as Main from 'resource:///org/gnome/shell/ui/main.js';
  10. import {HAS_ST_ORIENTATION} from './utils.js';
  11. /**
  12. * An StTooltip for ClutterActors
  13. *
  14. * Adapted from: https://github.com/RaphaelRochet/applications-overview-tooltip
  15. * See also: https://github.com/GNOME/gtk/blob/master/gtk/gtktooltip.c
  16. */
  17. export let TOOLTIP_BROWSE_ID = 0;
  18. export let TOOLTIP_BROWSE_MODE = false;
  19. export default class Tooltip {
  20. constructor(params) {
  21. Object.assign(this, params);
  22. this._bin = null;
  23. this._hoverTimeoutId = 0;
  24. this._showing = false;
  25. this._destroyId = this.parent.connect(
  26. 'destroy',
  27. this.destroy.bind(this)
  28. );
  29. this._hoverId = this.parent.connect(
  30. 'notify::hover',
  31. this._onHover.bind(this)
  32. );
  33. this._buttonPressEventId = this.parent.connect(
  34. 'button-press-event',
  35. this._hide.bind(this)
  36. );
  37. }
  38. get custom() {
  39. if (this._custom === undefined)
  40. this._custom = null;
  41. return this._custom;
  42. }
  43. set custom(actor) {
  44. this._custom = actor;
  45. this._markup = null;
  46. this._text = null;
  47. if (this._showing)
  48. this._show();
  49. }
  50. get gicon() {
  51. if (this._gicon === undefined)
  52. this._gicon = null;
  53. return this._gicon;
  54. }
  55. set gicon(gicon) {
  56. this._gicon = gicon;
  57. if (this._showing)
  58. this._show();
  59. }
  60. get icon() {
  61. return (this.gicon) ? this.gicon.name : null;
  62. }
  63. set icon(icon_name) {
  64. if (!icon_name)
  65. this.gicon = null;
  66. else
  67. this.gicon = new Gio.ThemedIcon({name: icon_name});
  68. }
  69. get markup() {
  70. if (this._markup === undefined)
  71. this._markup = null;
  72. return this._markup;
  73. }
  74. set markup(text) {
  75. this._markup = text;
  76. this._text = null;
  77. if (this._showing)
  78. this._show();
  79. }
  80. get text() {
  81. if (this._text === undefined)
  82. this._text = null;
  83. return this._text;
  84. }
  85. set text(text) {
  86. this._markup = null;
  87. this._text = text;
  88. if (this._showing)
  89. this._show();
  90. }
  91. get x_offset() {
  92. if (this._x_offset === undefined)
  93. this._x_offset = 0;
  94. return this._x_offset;
  95. }
  96. set x_offset(offset) {
  97. this._x_offset = (Number.isInteger(offset)) ? offset : 0;
  98. }
  99. get y_offset() {
  100. if (this._y_offset === undefined)
  101. this._y_offset = 0;
  102. return this._y_offset;
  103. }
  104. set y_offset(offset) {
  105. this._y_offset = (Number.isInteger(offset)) ? offset : 0;
  106. }
  107. _show() {
  108. if (this.text === null && this.markup === null)
  109. return this._hide();
  110. if (this._bin === null) {
  111. this._bin = new St.Bin({
  112. style_class: 'osd-window gsconnect-tooltip',
  113. opacity: 232,
  114. });
  115. if (this.custom) {
  116. this._bin.child = this.custom;
  117. } else {
  118. if (HAS_ST_ORIENTATION) {
  119. // GNOME 48
  120. this._bin.child = new St.BoxLayout(
  121. {orientation: Clutter.Orientation.HORIZONTAL}
  122. );
  123. } else {
  124. // GNOME 46/47
  125. this._bin.child = new St.BoxLayout({vertical: false});
  126. }
  127. if (this.gicon) {
  128. this._bin.child.icon = new St.Icon({
  129. gicon: this.gicon,
  130. y_align: St.Align.START,
  131. });
  132. this._bin.child.icon.set_y_align(Clutter.ActorAlign.START);
  133. this._bin.child.add_child(this._bin.child.icon);
  134. }
  135. this.label = new St.Label({text: this.markup || this.text});
  136. this.label.clutter_text.line_wrap = true;
  137. this.label.clutter_text.line_wrap_mode = Pango.WrapMode.WORD;
  138. this.label.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
  139. this.label.clutter_text.use_markup = (this.markup);
  140. this._bin.child.add_child(this.label);
  141. }
  142. Main.layoutManager.addTopChrome(this._bin);
  143. } else if (this.custom) {
  144. this._bin.child = this.custom;
  145. } else {
  146. if (this._bin.child.icon)
  147. this._bin.child.icon.destroy();
  148. if (this.gicon) {
  149. this._bin.child.icon = new St.Icon({gicon: this.gicon});
  150. this._bin.child.insert_child_at_index(this._bin.child.icon, 0);
  151. }
  152. this.label.clutter_text.text = this.markup || this.text;
  153. this.label.clutter_text.use_markup = (this.markup);
  154. }
  155. // Position tooltip
  156. let [x, y] = this.parent.get_transformed_position();
  157. x = (x + (this.parent.width / 2)) - Math.round(this._bin.width / 2);
  158. x += this.x_offset;
  159. y += this.y_offset;
  160. // Show tooltip
  161. if (this._showing) {
  162. this._bin.ease({
  163. x: x,
  164. y: y,
  165. time: 0.15,
  166. transition: Clutter.AnimationMode.EASE_OUT_QUAD,
  167. });
  168. } else {
  169. this._bin.set_position(x, y);
  170. this._bin.ease({
  171. opacity: 232,
  172. time: 0.15,
  173. transition: Clutter.AnimationMode.EASE_OUT_QUAD,
  174. });
  175. this._showing = true;
  176. }
  177. // Enable browse mode
  178. TOOLTIP_BROWSE_MODE = true;
  179. if (TOOLTIP_BROWSE_ID) {
  180. GLib.source_remove(TOOLTIP_BROWSE_ID);
  181. TOOLTIP_BROWSE_ID = 0;
  182. }
  183. if (this._hoverTimeoutId) {
  184. GLib.source_remove(this._hoverTimeoutId);
  185. this._hoverTimeoutId = 0;
  186. }
  187. }
  188. _hide() {
  189. if (this._bin) {
  190. this._bin.ease({
  191. opacity: 0,
  192. time: 0.10,
  193. transition: Clutter.AnimationMode.EASE_OUT_QUAD,
  194. onComplete: () => {
  195. Main.layoutManager.removeChrome(this._bin);
  196. if (this.custom)
  197. this._bin.remove_child(this.custom);
  198. this._bin.destroy();
  199. this._bin = null;
  200. },
  201. });
  202. }
  203. TOOLTIP_BROWSE_ID = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 500, () => {
  204. TOOLTIP_BROWSE_MODE = false;
  205. TOOLTIP_BROWSE_ID = 0;
  206. return false;
  207. });
  208. if (this._hoverTimeoutId) {
  209. GLib.source_remove(this._hoverTimeoutId);
  210. this._hoverTimeoutId = 0;
  211. }
  212. this._showing = false;
  213. this._hoverTimeoutId = 0;
  214. }
  215. _onHover() {
  216. if (this.parent.hover) {
  217. if (!this._hoverTimeoutId) {
  218. if (this._showing) {
  219. this._show();
  220. } else {
  221. this._hoverTimeoutId = GLib.timeout_add(
  222. GLib.PRIORITY_DEFAULT,
  223. (TOOLTIP_BROWSE_MODE) ? 60 : 500,
  224. () => {
  225. this._show();
  226. this._hoverTimeoutId = 0;
  227. return false;
  228. }
  229. );
  230. }
  231. }
  232. } else {
  233. this._hide();
  234. }
  235. }
  236. destroy() {
  237. this.parent.disconnect(this._destroyId);
  238. this.parent.disconnect(this._hoverId);
  239. this.parent.disconnect(this._buttonPressEventId);
  240. if (this.custom)
  241. this.custom.destroy();
  242. if (this._bin) {
  243. Main.layoutManager.removeChrome(this._bin);
  244. this._bin.destroy();
  245. }
  246. if (TOOLTIP_BROWSE_ID) {
  247. GLib.source_remove(TOOLTIP_BROWSE_ID);
  248. TOOLTIP_BROWSE_ID = 0;
  249. }
  250. if (this._hoverTimeoutId) {
  251. GLib.source_remove(this._hoverTimeoutId);
  252. this._hoverTimeoutId = 0;
  253. }
  254. }
  255. }