panel.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559
  1. import St from 'gi://St';
  2. import GLib from 'gi://GLib';
  3. import Meta from 'gi://Meta';
  4. import * as Main from 'resource:///org/gnome/shell/ui/main.js';
  5. import { PaintSignals } from '../conveniences/paint_signals.js';
  6. import { Pipeline } from '../conveniences/pipeline.js';
  7. import { DummyPipeline } from '../conveniences/dummy_pipeline.js';
  8. const DASH_TO_PANEL_UUID = 'dash-to-panel@jderose9.github.com';
  9. const PANEL_STYLES = [
  10. "transparent-panel",
  11. "light-panel",
  12. "dark-panel",
  13. "contrasted-panel"
  14. ];
  15. export const PanelBlur = class PanelBlur {
  16. constructor(connections, settings, effects_manager) {
  17. this.connections = connections;
  18. this.window_signal_ids = new Map();
  19. this.settings = settings;
  20. this.effects_manager = effects_manager;
  21. this.actors_list = [];
  22. this.enabled = false;
  23. }
  24. enable() {
  25. this._log("blurring top panel");
  26. // check for panels when Dash to Panel is activated
  27. this.connections.connect(
  28. Main.extensionManager,
  29. 'extension-state-changed',
  30. (_, extension) => {
  31. if (extension.uuid === DASH_TO_PANEL_UUID
  32. && extension.state === 1
  33. ) {
  34. this.connections.connect(
  35. global.dashToPanel,
  36. 'panels-created',
  37. _ => this.blur_dtp_panels()
  38. );
  39. this.blur_existing_panels();
  40. }
  41. }
  42. );
  43. this.blur_existing_panels();
  44. // connect to overview being opened/closed, and dynamically show or not
  45. // the blur when a window is near a panel
  46. this.connect_to_windows_and_overview();
  47. // connect to workareas change
  48. this.connections.connect(global.display, 'workareas-changed',
  49. _ => this.reset()
  50. );
  51. this.enabled = true;
  52. }
  53. reset() {
  54. this._log("resetting...");
  55. this.disable();
  56. setTimeout(_ => this.enable(), 1);
  57. }
  58. /// Check for already existing panels and blur them if they are not already
  59. blur_existing_panels() {
  60. // check if dash-to-panel is present
  61. if (global.dashToPanel) {
  62. // blur already existing ones
  63. if (global.dashToPanel.panels)
  64. this.blur_dtp_panels();
  65. } else {
  66. // if no dash-to-panel, blur the main and only panel
  67. this.maybe_blur_panel(Main.panel);
  68. }
  69. }
  70. blur_dtp_panels() {
  71. // Defer the blurring to the next idle cycle.
  72. // This is crucial to ensure the panel actors have been allocated their
  73. // final size and position by the compositor, avoiding race conditions
  74. // during extension startup.
  75. GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => {
  76. if (!global.dashToPanel?.panels) {
  77. return GLib.SOURCE_REMOVE;
  78. }
  79. this._log("Blurring Dash to Panel panels after idle.");
  80. // blur every panel found
  81. global.dashToPanel.panels.forEach(p => {
  82. this.maybe_blur_panel(p.panel);
  83. });
  84. // if main panel is not included in the previous panels, blur it
  85. if (
  86. !global.dashToPanel.panels
  87. .map(p => p.panel)
  88. .includes(Main.panel)
  89. &&
  90. this.settings.dash_to_panel.BLUR_ORIGINAL_PANEL
  91. )
  92. this.maybe_blur_panel(Main.panel);
  93. return GLib.SOURCE_REMOVE;
  94. });
  95. };
  96. /// Blur a panel only if it is not already blurred (contained in the list)
  97. maybe_blur_panel(panel) {
  98. // check if the panel is contained in the list
  99. let actors = this.actors_list.find(
  100. actors => actors.widgets.panel == panel
  101. );
  102. if (!actors)
  103. // if the actors is not blurred, blur it
  104. this.blur_panel(panel);
  105. }
  106. /// Blur a panel
  107. blur_panel(panel) {
  108. let panel_box = panel.get_parent();
  109. let is_dtp_panel = false;
  110. if (!panel_box.name) {
  111. is_dtp_panel = true;
  112. panel_box = panel_box.get_parent();
  113. }
  114. let monitor = Main.layoutManager.findMonitorForActor(panel);
  115. if (!monitor)
  116. return;
  117. let background_group = new Meta.BackgroundGroup(
  118. { name: 'bms-panel-backgroundgroup', width: 0, height: 0 }
  119. );
  120. let background, bg_manager;
  121. let static_blur = this.settings.panel.STATIC_BLUR;
  122. if (static_blur) {
  123. let bg_manager_list = [];
  124. const pipeline = new Pipeline(
  125. this.effects_manager,
  126. global.blur_my_shell._pipelines_manager,
  127. this.settings.panel.PIPELINE
  128. );
  129. background = pipeline.create_background_with_effects(
  130. monitor.index, bg_manager_list,
  131. background_group, 'bms-panel-blurred-widget'
  132. );
  133. bg_manager = bg_manager_list[0];
  134. }
  135. else {
  136. const pipeline = new DummyPipeline(this.effects_manager, this.settings.panel);
  137. [background, bg_manager] = pipeline.create_background_with_effect(
  138. background_group, 'bms-panel-blurred-widget'
  139. );
  140. let paint_signals = new PaintSignals(this.connections);
  141. // HACK
  142. //
  143. //`Shell.BlurEffect` does not repaint when shadows are under it. [1]
  144. //
  145. // This does not entirely fix this bug (shadows caused by windows
  146. // still cause artifacts), but it prevents the shadows of the panel
  147. // buttons to cause artifacts on the panel itself
  148. //
  149. // [1]: https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/2857
  150. {
  151. if (this.settings.HACKS_LEVEL === 1) {
  152. this._log("panel hack level 1");
  153. paint_signals.disconnect_all();
  154. paint_signals.connect(background, pipeline.effect);
  155. } else {
  156. paint_signals.disconnect_all();
  157. }
  158. }
  159. }
  160. // insert the background group to the panel box
  161. panel_box.insert_child_at_index(background_group, 0);
  162. // the object that is used to remembering each elements that is linked to the blur effect
  163. let actors = {
  164. widgets: { panel, panel_box, background, background_group },
  165. static_blur,
  166. monitor,
  167. bg_manager,
  168. is_dtp_panel
  169. };
  170. this.actors_list.push(actors);
  171. // update the size of the actor
  172. this.update_size(actors);
  173. // connect to panel, panel_box and its parent position or size change
  174. // this should fire update_size every time one of its params change
  175. this.connections.connect(
  176. panel,
  177. 'notify::position',
  178. _ => this.update_size(actors)
  179. );
  180. this.connections.connect(
  181. panel_box,
  182. ['notify::size', 'notify::position'],
  183. _ => this.update_size(actors)
  184. );
  185. this.connections.connect(
  186. panel_box.get_parent(),
  187. 'notify::position',
  188. _ => this.update_size(actors)
  189. );
  190. // connect to the panel getting destroyed
  191. this.connections.connect(
  192. panel,
  193. 'destroy',
  194. _ => this.destroy_blur(actors, true)
  195. );
  196. }
  197. update_size(actors) {
  198. let panel = actors.widgets.panel;
  199. let panel_box = actors.widgets.panel_box;
  200. let background = actors.widgets.background;
  201. let [width, height] = panel_box.get_size();
  202. // if static blur, need to clip the background
  203. if (actors.static_blur) {
  204. let monitor = Main.layoutManager.findMonitorForActor(panel);
  205. if (!monitor)
  206. return;
  207. // an alternative to panel.get_transformed_position, because it
  208. // sometimes yields NaN (probably when the actor is not fully
  209. // positionned yet)
  210. let [p_x, p_y] = panel_box.get_position();
  211. let [p_p_x, p_p_y] = panel_box.get_parent().get_position();
  212. let x = p_x + p_p_x - monitor.x + (width - panel.width) / 2;
  213. let y = p_y + p_p_y - monitor.y + (height - panel.height) / 2;
  214. background.set_clip(x, y, panel.width, panel.height);
  215. background.x = (width - panel.width) / 2 - x;
  216. background.y = .5 + (height - panel.height) / 2 - y;
  217. } else {
  218. background.x = panel.x;
  219. background.y = panel.y;
  220. background.width = panel.width;
  221. background.height = panel.height;
  222. }
  223. // update the monitor panel is on
  224. actors.monitor = Main.layoutManager.findMonitorForActor(panel);
  225. }
  226. /// Connect when overview if opened/closed to hide/show the blur accordingly
  227. ///
  228. /// If HIDETOPBAR is set, we need just to hide the blur when showing appgrid
  229. /// (so no shadow is cropped)
  230. connect_to_overview() {
  231. // may be called when panel blur is disabled, if hidetopbar
  232. // compatibility is toggled on/off
  233. // if this is the case, do nothing as only the panel blur interfers with
  234. // hidetopbar
  235. if (
  236. this.settings.panel.BLUR &&
  237. this.settings.panel.UNBLUR_IN_OVERVIEW
  238. ) {
  239. if (!this.settings.hidetopbar.COMPATIBILITY) {
  240. this.connections.connect(
  241. Main.overview, 'showing', _ => this.hide()
  242. );
  243. this.connections.connect(
  244. Main.overview, 'hidden', _ => this.show()
  245. );
  246. } else {
  247. let appDisplay = Main.overview._overview._controls._appDisplay;
  248. this.connections.connect(
  249. appDisplay, 'show', _ => this.hide()
  250. );
  251. this.connections.connect(
  252. appDisplay, 'hide', _ => this.show()
  253. );
  254. this.connections.connect(
  255. Main.overview, 'hidden', _ => this.show()
  256. );
  257. }
  258. }
  259. }
  260. /// Connect to windows disable transparency when a window is too close
  261. connect_to_windows() {
  262. if (
  263. this.settings.panel.OVERRIDE_BACKGROUND_DYNAMICALLY
  264. ) {
  265. // connect to overview opening/closing
  266. this.connections.connect(Main.overview, ['showing', 'hiding'],
  267. _ => this.update_visibility()
  268. );
  269. // connect to session mode update
  270. this.connections.connect(Main.sessionMode, 'updated',
  271. _ => this.update_visibility()
  272. );
  273. // manage already-existing windows
  274. for (const meta_window_actor of global.get_window_actors()) {
  275. this.on_window_actor_added(
  276. meta_window_actor.get_parent(), meta_window_actor
  277. );
  278. }
  279. // manage windows at their creation/removal
  280. this.connections.connect(global.window_group, 'child-added',
  281. this.on_window_actor_added.bind(this)
  282. );
  283. this.connections.connect(global.window_group, 'child-removed',
  284. this.on_window_actor_removed.bind(this)
  285. );
  286. // connect to a workspace change
  287. this.connections.connect(global.window_manager, 'switch-workspace',
  288. _ => this.update_visibility()
  289. );
  290. // perform early update
  291. this.update_visibility();
  292. } else {
  293. // reset transparency for every panels
  294. this.actors_list.forEach(
  295. actors => this.set_should_override_panel(actors, true)
  296. );
  297. }
  298. }
  299. /// An helper to connect to both the windows and overview signals.
  300. /// This is the only function that should be directly called, to prevent
  301. /// inconsistencies with signals not being disconnected.
  302. connect_to_windows_and_overview() {
  303. this.disconnect_from_windows_and_overview();
  304. this.connect_to_overview();
  305. this.connect_to_windows();
  306. }
  307. /// Disconnect all the connections created by connect_to_windows
  308. disconnect_from_windows_and_overview() {
  309. // disconnect the connections to actors
  310. for (const actor of [
  311. Main.overview, Main.sessionMode,
  312. global.window_group, global.window_manager,
  313. Main.overview._overview._controls._appDisplay
  314. ]) {
  315. this.connections.disconnect_all_for(actor);
  316. }
  317. // disconnect the connections from windows
  318. for (const [actor, ids] of this.window_signal_ids) {
  319. for (const id of ids) {
  320. actor.disconnect(id);
  321. }
  322. }
  323. this.window_signal_ids = new Map();
  324. }
  325. /// Update the css classname of the panel for light theme
  326. update_light_text_classname(disable = false) {
  327. if (this.settings.panel.FORCE_LIGHT_TEXT && !disable)
  328. Main.panel.add_style_class_name("panel-light-text");
  329. else
  330. Main.panel.remove_style_class_name("panel-light-text");
  331. }
  332. /// Callback when a new window is added
  333. on_window_actor_added(container, meta_window_actor) {
  334. this.window_signal_ids.set(meta_window_actor, [
  335. meta_window_actor.connect('notify::allocation',
  336. _ => this.update_visibility()
  337. ),
  338. meta_window_actor.connect('notify::visible',
  339. _ => this.update_visibility()
  340. )
  341. ]);
  342. this.update_visibility();
  343. }
  344. /// Callback when a window is removed
  345. on_window_actor_removed(container, meta_window_actor) {
  346. for (const signalId of this.window_signal_ids.get(meta_window_actor)) {
  347. meta_window_actor.disconnect(signalId);
  348. }
  349. this.window_signal_ids.delete(meta_window_actor);
  350. this.update_visibility();
  351. }
  352. /// Update the visibility of the blur effect
  353. update_visibility() {
  354. if (
  355. Main.panel.has_style_pseudo_class('overview')
  356. || !Main.sessionMode.hasWindows
  357. ) {
  358. this.actors_list.forEach(
  359. actors => this.set_should_override_panel(actors, true)
  360. );
  361. return;
  362. }
  363. if (!Main.layoutManager.primaryMonitor)
  364. return;
  365. // get all the windows in the active workspace that are visible
  366. const workspace = global.workspace_manager.get_active_workspace();
  367. const windows = workspace.list_windows().filter(meta_window =>
  368. meta_window.showing_on_its_workspace()
  369. && !meta_window.is_hidden()
  370. && meta_window.get_window_type() !== Meta.WindowType.DESKTOP
  371. // exclude Desktop Icons NG
  372. && meta_window.get_gtk_application_id() !== "com.rastersoft.ding"
  373. && meta_window.get_gtk_application_id() !== "com.desktop.ding"
  374. );
  375. // check if at least one window is near enough to each panel and act
  376. // accordingly
  377. const scale = St.ThemeContext.get_for_stage(global.stage).scale_factor;
  378. this.actors_list
  379. // do not apply for dtp panels, as it would only cause bugs and it
  380. // can be done from its preferences anyway
  381. .filter(actors => !actors.is_dtp_panel)
  382. .forEach(actors => {
  383. let panel = actors.widgets.panel;
  384. let panel_top = panel.get_transformed_position()[1];
  385. let panel_bottom = panel_top + panel.get_height();
  386. // check if at least a window is near enough the panel
  387. let window_overlap_panel = false;
  388. windows.forEach(meta_window => {
  389. let window_monitor_i = meta_window.get_monitor();
  390. let same_monitor = actors.monitor.index == window_monitor_i;
  391. let window_vertical_pos = meta_window.get_frame_rect().y;
  392. let window_vertical_bottom = window_vertical_pos + meta_window.get_frame_rect().height;
  393. // if so, and if in the same monitor, then it overlaps
  394. if (same_monitor
  395. &&
  396. // check if panel is on top
  397. ((panel_top === 0 && window_vertical_pos < panel_bottom + 5 * scale) ||
  398. // check if panel is at the bottom
  399. (panel_top > 0 && window_vertical_bottom > panel_top - 5 * scale))
  400. )
  401. window_overlap_panel = true;
  402. });
  403. // if no window overlaps, then the panel is transparent
  404. this.set_should_override_panel(
  405. actors, !window_overlap_panel
  406. );
  407. });
  408. }
  409. /// Choose wether or not the panel background should be overriden, in
  410. /// respect to its argument and the `override-background` setting.
  411. set_should_override_panel(actors, should_override) {
  412. let panel = actors.widgets.panel;
  413. PANEL_STYLES.forEach(style => panel.remove_style_class_name(style));
  414. if (
  415. this.settings.panel.OVERRIDE_BACKGROUND
  416. &&
  417. should_override
  418. ) {
  419. panel.add_style_class_name(
  420. PANEL_STYLES[this.settings.panel.STYLE_PANEL]
  421. );
  422. }
  423. // update the classname if the panel to have or have not light text
  424. this.update_light_text_classname(!should_override);
  425. }
  426. update_pipeline() {
  427. this.actors_list.forEach(actors =>
  428. actors.bg_manager._bms_pipeline.change_pipeline_to(
  429. this.settings.panel.PIPELINE
  430. )
  431. );
  432. }
  433. show() {
  434. this.actors_list.forEach(actors => {
  435. actors.widgets.background.show();
  436. });
  437. }
  438. hide() {
  439. this.actors_list.forEach(actors => {
  440. actors.widgets.background.hide();
  441. });
  442. }
  443. // IMPORTANT: do never call this in a mutable `this.actors_list.forEach`
  444. destroy_blur(actors, panel_already_destroyed) {
  445. this.set_should_override_panel(actors, false);
  446. actors.bg_manager._bms_pipeline.destroy();
  447. if (panel_already_destroyed)
  448. actors.bg_manager.backgroundActor = null;
  449. actors.bg_manager.destroy();
  450. if (!panel_already_destroyed) {
  451. actors.widgets.panel_box.remove_child(actors.widgets.background_group);
  452. actors.widgets.background_group.destroy_all_children();
  453. actors.widgets.background_group.destroy();
  454. }
  455. let index = this.actors_list.indexOf(actors);
  456. if (index >= 0)
  457. this.actors_list.splice(index, 1);
  458. }
  459. disable() {
  460. this._log("removing blur from top panel");
  461. this.disconnect_from_windows_and_overview();
  462. this.update_light_text_classname(true);
  463. const immutable_actors_list = [...this.actors_list];
  464. immutable_actors_list.forEach(actors => this.destroy_blur(actors, false));
  465. this.actors_list = [];
  466. this.connections.disconnect_all();
  467. this.enabled = false;
  468. }
  469. _log(str) {
  470. if (this.settings.DEBUG)
  471. console.log(`[Blur my Shell > panel] ${str}`);
  472. }
  473. _warn(str) {
  474. console.warn(`[Blur my Shell > panel] ${str}`);
  475. }
  476. };