tooltip.js 7.9 KB

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