extension.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. /* extension.js
  2. * Copyright (C) 2025 kosmospredanie, shyzus, Shinigaminai
  3. *
  4. * This program is free software: you can redistribute it and/or modify
  5. * it under the terms of the GNU General Public License as published by
  6. * the Free Software Foundation, either version 3 of the License, or
  7. * (at your option) any later version.
  8. *
  9. * This program is distributed in the hope that it will be useful,
  10. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. * GNU General Public License for more details.
  13. *
  14. * You should have received a copy of the GNU General Public License
  15. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  16. */
  17. import Gio from 'gi://Gio';
  18. import { Extension } from 'resource:///org/gnome/shell/extensions/extension.js';
  19. import * as Main from 'resource:///org/gnome/shell/ui/main.js';
  20. import * as SystemActions from 'resource:///org/gnome/shell/misc/systemActions.js';
  21. import * as Rotator from './rotator.js'
  22. import { Orientation } from './orientation.js';
  23. import { ManualOrientationIndicator } from './manualOrientationIndicator.js';
  24. import { SensorProxy } from './sensorProxy.js';
  25. const ORIENTATION_LOCK_SCHEMA = 'org.gnome.settings-daemon.peripherals.touchscreen';
  26. const ORIENTATION_LOCK_KEY = 'orientation-lock';
  27. const A11Y_APPLICATIONS_SCHEMA = 'org.gnome.desktop.a11y.applications';
  28. const SHOW_KEYBOARD = 'screen-keyboard-enabled';
  29. const PERIPHERAL_TOUCHPAD_SCHEMA = 'org.gnome.desktop.peripherals.touchpad';
  30. const TOUCHPAD_EVENTS = 'send-events';
  31. export default class ScreenAutoRotateExtension extends Extension {
  32. enable() {
  33. this._settings = this.getSettings();
  34. this._system_actions = SystemActions.getDefault();
  35. this._system_actions_backup = null;
  36. this._override_system_actions();
  37. this._a11yApplicationsSettings = new Gio.Settings({ schema_id: A11Y_APPLICATIONS_SCHEMA });
  38. this._peripheralTouchpadSettings = new Gio.Settings({ schema_id: PERIPHERAL_TOUCHPAD_SCHEMA });
  39. this._orientation_settings = new Gio.Settings({ schema_id: ORIENTATION_LOCK_SCHEMA });
  40. this._orientation_settings_handler = this._orientation_settings.connect('changed::' + ORIENTATION_LOCK_KEY, this._orientation_lock_changed.bind(this));
  41. this._sensor_proxy = new SensorProxy(this.rotate_to.bind(this));
  42. this._state = false;
  43. let locked = this._orientation_settings.get_boolean(ORIENTATION_LOCK_KEY);
  44. if (!locked) {
  45. this.toggle_rotation_lock()
  46. }
  47. this._manual_flip_settings_handler = this._settings.connect('changed::manual-flip', (settings, key) => {
  48. if (settings.get_boolean(key)) {
  49. this._add_manual_flip();
  50. } else {
  51. this._remove_manual_flip();
  52. }
  53. });
  54. this._hide_lock_rotate_settings_handler = this._settings.connect('changed::hide-lock-rotate', (settings, key) => {
  55. this._set_hide_lock_rotate(settings.get_boolean(key));
  56. });
  57. if (this._settings.get_boolean('manual-flip')) {
  58. this._add_manual_flip();
  59. } else {
  60. this._remove_manual_flip();
  61. }
  62. /* Timeout needed due to unknown race condition causing 'Auto Rotate'
  63. * Quick Toggle to be undefined for a brief moment.
  64. */
  65. this._timeoutId = setTimeout(() => {
  66. this._set_hide_lock_rotate(this._settings.get_boolean('hide-lock-rotate'));
  67. // Rotate once on start up to the orientation detected by the claimed accelerometer
  68. this.rotate_to(this._sensor_proxy.get_accelerometer_orientation());
  69. }, 1000);
  70. }
  71. toggle_rotation_lock() {
  72. if (this._state) {
  73. this._a11yApplicationsSettings.set_boolean(SHOW_KEYBOARD, this._originala11yKeyboardSetting);
  74. this._originala11yKeyboardSetting = null;
  75. this._sensor_proxy.disable();
  76. this._state = false;
  77. } else {
  78. this._originala11yKeyboardSetting = this._a11yApplicationsSettings.get_boolean(SHOW_KEYBOARD);
  79. this._sensor_proxy.enable();
  80. this._state = true;
  81. }
  82. }
  83. _set_hide_lock_rotate(state) {
  84. const autoRotateIndicator = Main.panel.statusArea.quickSettings._autoRotate;
  85. if (state) {
  86. Main.panel.statusArea.quickSettings._indicators.remove_child(autoRotateIndicator);
  87. Main.panel.statusArea.quickSettings.menu._grid.remove_child(autoRotateIndicator.quickSettingsItems[0]);
  88. } else {
  89. Main.panel.statusArea.quickSettings._indicators.add_child(autoRotateIndicator);
  90. Main.panel.statusArea.quickSettings._addItemsBefore(
  91. autoRotateIndicator.quickSettingsItems,
  92. Main.panel.statusArea.quickSettings._rfkill.quickSettingsItems[0]
  93. );
  94. }
  95. }
  96. _add_manual_flip() {
  97. this.flipIndicator = new ManualOrientationIndicator(this);
  98. Main.panel.statusArea.quickSettings.addExternalIndicator(this.flipIndicator);
  99. }
  100. _remove_manual_flip() {
  101. if (this.flipIndicator !== null && this.flipIndicator !== undefined) {
  102. this.flipIndicator.destroy();
  103. this.flipIndicator = null;
  104. }
  105. }
  106. _override_system_actions() {
  107. this._system_actions_backup = {
  108. '_updateOrientationLock': this._system_actions._updateOrientationLock
  109. };
  110. this._system_actions._updateOrientationLock = function() {
  111. this._actions.get('lock-orientation').available = true;
  112. this.notify('can-lock-orientation');
  113. };
  114. this._system_actions._updateOrientationLock();
  115. }
  116. _restore_system_actions() {
  117. if (this._system_actions_backup === null) return;
  118. this._system_actions._updateOrientationLock = this._system_actions_backup['_updateOrientationLock'];
  119. this._system_actions._updateOrientationLock();
  120. this._system_actions_backup = null;
  121. }
  122. _orientation_lock_changed() {
  123. let locked = this._orientation_settings.get_boolean(ORIENTATION_LOCK_KEY);
  124. if (this._state === locked) {
  125. this.toggle_rotation_lock();
  126. }
  127. }
  128. _handle_osk(target) {
  129. const landscapeOsk = this._settings.get_boolean('landscape-osk');
  130. const portraitRightOsk = this._settings.get_boolean('portrait-right-osk');
  131. const portraitLeftOsk = this._settings.get_boolean('portrait-left-osk');
  132. const landscapeFlippedOsk = this._settings.get_boolean('landscape-flipped-osk');
  133. switch (target) {
  134. case 0:
  135. this._a11yApplicationsSettings.set_boolean(SHOW_KEYBOARD, landscapeOsk);
  136. break;
  137. case 1:
  138. this._a11yApplicationsSettings.set_boolean(SHOW_KEYBOARD, portraitLeftOsk);
  139. break;
  140. case 2:
  141. this._a11yApplicationsSettings.set_boolean(SHOW_KEYBOARD, landscapeFlippedOsk);
  142. break;
  143. case 3:
  144. this._a11yApplicationsSettings.set_boolean(SHOW_KEYBOARD, portraitRightOsk);
  145. break;
  146. }
  147. }
  148. _handle_dor_touchpad(target) {
  149. const dorLandscape = this._settings.get_boolean('dor-touchpad-landscape');
  150. const dorPortraitRight = this._settings.get_boolean('dor-touchpad-portrait-right');
  151. const dorPortraitLeft = this._settings.get_boolean('dor-touchpad-portrait-left');
  152. const dorLandscapeFlipped = this._settings.get_boolean('dor-touchpad-landscape-flipped');
  153. let setting_value = undefined;
  154. switch (target) {
  155. case 0:
  156. if (dorLandscape) {
  157. setting_value = "disabled";
  158. } else {
  159. setting_value = "enabled";
  160. }
  161. this._peripheralTouchpadSettings.set_string(TOUCHPAD_EVENTS, setting_value);
  162. break;
  163. case 1:
  164. if (dorPortraitLeft) {
  165. setting_value = "disabled";
  166. } else {
  167. setting_value = "enabled";
  168. }
  169. this._peripheralTouchpadSettings.set_string(TOUCHPAD_EVENTS, setting_value);
  170. break;
  171. case 2:
  172. if (dorLandscapeFlipped) {
  173. setting_value = "disabled";
  174. } else {
  175. setting_value = "enabled";
  176. }
  177. this._peripheralTouchpadSettings.set_string(TOUCHPAD_EVENTS, setting_value);
  178. break;
  179. case 3:
  180. if (dorPortraitRight) {
  181. setting_value = "disabled";
  182. } else {
  183. setting_value = "enabled";
  184. }
  185. this._peripheralTouchpadSettings.set_string(TOUCHPAD_EVENTS, setting_value);
  186. break;
  187. }
  188. }
  189. rotate_to(orientation) {
  190. const sensor = Orientation[orientation];
  191. const invert_horizontal_direction = this._settings.get_boolean('invert-horizontal-rotation-direction');
  192. const invert_vertical_direction = this._settings.get_boolean('invert-vertical-rotation-direction');
  193. const offset = this._settings.get_int('orientation-offset');
  194. let target = sensor; // Default to sensor output.
  195. switch (sensor) {
  196. case 0:
  197. // sensor reports landscape
  198. if (invert_horizontal_direction) {
  199. target = 2;
  200. }
  201. break;
  202. case 1:
  203. // sensor reports portrait
  204. if (invert_vertical_direction) {
  205. target = 3;
  206. }
  207. break;
  208. case 2:
  209. // sensor reports landscape-inverted
  210. if (invert_horizontal_direction) {
  211. target = 0;
  212. }
  213. break;
  214. case 3:
  215. // sensor reports portrait-inverted
  216. if (invert_vertical_direction) {
  217. target = 1;
  218. }
  219. break;
  220. }
  221. target = (target + offset) % 4;
  222. if (this._settings.get_boolean('debug-logging')) {
  223. console.log(`sensor=${Orientation[orientation]}`);
  224. console.log(`offset=${offset}`);
  225. console.log(`target=${target}`);
  226. }
  227. Rotator.rotate_to(target);
  228. this._handle_osk(target);
  229. this._handle_dor_touchpad(target);
  230. }
  231. disable() {
  232. /*
  233. Comment for unlock-dialog usage:
  234. The unlock-dialog session-mode is useful for this extension as it allows
  235. the user to rotate their screen or lock rotation after their device may
  236. have auto-locked. This provides the ability to log back in regardless of
  237. the orientation of the device in tablet mode.
  238. */
  239. this._settings.disconnect(this._manual_flip_settings_handler);
  240. this._settings.disconnect(this._hide_lock_rotate_settings_handler);
  241. this._settings = null;
  242. clearTimeout(this._timeoutId);
  243. this._timeoutId = null;
  244. this._remove_manual_flip();
  245. this._sensor_proxy.destroy();
  246. this._orientation_settings.disconnect(this._orientation_settings_handler);
  247. this._orientation_settings = null;
  248. this._a11yApplicationsSettings = null;
  249. this._restore_system_actions();
  250. }
  251. }