topBarSearchEntry.js 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. import * as Main from 'resource:///org/gnome/shell/ui/main.js';
  2. import St from 'gi://St';
  3. import {Button as PanelButton} from 'resource:///org/gnome/shell/ui/panelMenu.js';
  4. import Clutter from 'gi://Clutter';
  5. import {gnomeExecutables, autocomplete, clear} from './autocomplete.js';
  6. import {launchApp} from './utils.js';
  7. export class TopBarSearchEntry {
  8. _searchButton;
  9. _searchContainer;
  10. _searchEntry;
  11. _searchSuggestion;
  12. _alive;
  13. constructor(settings) {
  14. this._alive = true;
  15. this._searchButton = new PanelButton(0.0, 'searchEntry', false);
  16. this._searchContainer = new St.Bin({
  17. x_expand: true,
  18. });
  19. this._searchEntry = new St.Entry({
  20. style_class: 'custom-search-entry',
  21. can_focus: true,
  22. hint_text: 'Type to search…',
  23. track_hover: true,
  24. x_expand: true,
  25. });
  26. this._searchSuggestion = new St.Label({
  27. style_class: 'suggestion',
  28. text: '',
  29. x_expand: true,
  30. });
  31. this._searchEntry.clutter_text.connect('notify::mapped', actor => {
  32. if (actor.mapped)
  33. actor.grab_key_focus();
  34. });
  35. this._searchContainer.add_child(this._searchSuggestion);
  36. this._searchContainer.add_child(this._searchEntry);
  37. this._searchEntry.clutter_text.connect('text-changed', actor => {
  38. let current = actor.get_text();
  39. if (current.length > 1 && this._searchSuggestion) {
  40. let matches = autocomplete(current);
  41. if (matches.length > 0 && this._searchEntry) {
  42. let match = matches[0];
  43. const ct = this._searchEntry.get_clutter_text();
  44. const layout = ct.get_layout();
  45. const [textW] = layout.get_pixel_size();
  46. const themeNode = this._searchEntry.get_theme_node();
  47. const leftPad = themeNode.get_padding(St.Side.LEFT);
  48. const x = leftPad + textW;
  49. this._searchSuggestion.set_style(`color: rgba(255,255,255,0.35); margin-left: ${x + 4}px;`);
  50. this._searchSuggestion.set_text(match.slice(current.length));
  51. } else {
  52. this._searchSuggestion.set_text('');
  53. }
  54. } else if (this._searchSuggestion) {
  55. this._searchSuggestion.set_text('');
  56. }
  57. });
  58. let completeText = () => {
  59. const typed = this._searchEntry?.get_text();
  60. const ct = this._searchEntry?.get_clutter_text();
  61. if (!typed || !ct)
  62. return;
  63. let full = typed + this._searchSuggestion?.get_text();
  64. this._searchEntry?.set_text(full);
  65. ct.set_cursor_position(full.length);
  66. this._searchSuggestion?.set_text('');
  67. };
  68. this._searchEntry.clutter_text.connect('key-press-event', (ct, event) => {
  69. const key = event.get_key_symbol();
  70. if (key === Clutter.KEY_KP_Right || key === Clutter.KEY_Right) {
  71. // Only accept if cursor is at end and a suggestion exists
  72. const typed = this._searchEntry?.get_text();
  73. if (typed) {
  74. completeText();
  75. return Clutter.EVENT_STOP;
  76. }
  77. }
  78. return Clutter.EVENT_PROPAGATE;
  79. });
  80. this._searchEntry.connect('captured-event', (actor, event) => {
  81. if (event.type() !== Clutter.EventType.KEY_PRESS)
  82. return Clutter.EVENT_PROPAGATE;
  83. const sym = event.get_key_symbol();
  84. if (sym === Clutter.KEY_Tab || sym === Clutter.KEY_ISO_Left_Tab) {
  85. completeText();
  86. return Clutter.EVENT_STOP;
  87. }
  88. return Clutter.EVENT_PROPAGATE;
  89. });
  90. this._searchEntry.clutter_text.connect('activate', actor => {
  91. let query = actor.get_text().trim().toLowerCase();
  92. if (query === '') {
  93. this.destroy();
  94. } else if (gnomeExecutables?.get(query)) {
  95. gnomeExecutables.get(query)?.launch([], null);
  96. this.destroy();
  97. } else if (launchApp([query])) {
  98. this.destroy();
  99. } else {
  100. actor.set_text('');
  101. this._searchEntry?.set_style('border: 2px solid red;');
  102. }
  103. });
  104. this._searchButton.add_child(this._searchContainer);
  105. let positionInt = settings.get_int('search-entry-position');
  106. let position;
  107. if (positionInt === 0)
  108. position = 'left';
  109. else if (positionInt === 1)
  110. position = 'center';
  111. else
  112. position = 'right';
  113. Main.panel.addToStatusArea('SearchEntry', this._searchButton, 0, position);
  114. }
  115. isAlive() {
  116. return this._alive;
  117. }
  118. destroy() {
  119. this._alive = false;
  120. if (this._searchButton) {
  121. this._searchButton?.destroy();
  122. this._searchContainer = undefined;
  123. this._searchEntry = undefined;
  124. this._searchButton = undefined;
  125. this._searchSuggestion = undefined;
  126. }
  127. clear();
  128. }
  129. }