prefs.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  1. import GObject from 'gi://GObject';
  2. import Gtk from 'gi://Gtk';
  3. import Gio from 'gi://Gio';
  4. import Adw from 'gi://Adw';
  5. import {
  6. ExtensionPreferences,
  7. gettext as _,
  8. } from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js';
  9. import Fields from './settingsFields.js';
  10. export default class ClipboardHistoryPrefs extends ExtensionPreferences {
  11. // fillPreferencesWindow() is passed a Adw.PreferencesWindow,
  12. // we need to wrap our widget in a Adw.PreferencesPage and Adw.PreferencesGroup
  13. // ourselves.
  14. // It would be great to port the preferences to standard Adw widgets.
  15. // https://gjs.guide/extensions/development/preferences.html#prefs-js
  16. fillPreferencesWindow(window) {
  17. const settings = this.getSettings();
  18. const main = new Gtk.Grid({
  19. margin_top: 10,
  20. margin_bottom: 10,
  21. margin_start: 10,
  22. margin_end: 10,
  23. row_spacing: 12,
  24. column_spacing: 18,
  25. column_homogeneous: false,
  26. row_homogeneous: false,
  27. });
  28. const field_size = new Gtk.SpinButton({
  29. adjustment: new Gtk.Adjustment({
  30. lower: 1,
  31. upper: 100_000,
  32. step_increment: 100,
  33. }),
  34. });
  35. const window_width_percentage = new Gtk.SpinButton({
  36. adjustment: new Gtk.Adjustment({
  37. lower: 0,
  38. upper: 100,
  39. step_increment: 5,
  40. }),
  41. });
  42. const field_cache_size = new Gtk.SpinButton({
  43. adjustment: new Gtk.Adjustment({
  44. lower: 1,
  45. upper: 1024,
  46. step_increment: 5,
  47. }),
  48. });
  49. const field_topbar_preview_size = new Gtk.SpinButton({
  50. adjustment: new Gtk.Adjustment({
  51. lower: 1,
  52. upper: 100,
  53. step_increment: 10,
  54. }),
  55. });
  56. const field_display_mode = new Gtk.ComboBox({
  57. model: this._create_display_mode_options(),
  58. });
  59. const rendererText = new Gtk.CellRendererText();
  60. field_display_mode.pack_start(rendererText, false);
  61. field_display_mode.add_attribute(rendererText, 'text', 0);
  62. const field_disable_down_arrow = new Gtk.Switch();
  63. const field_cache_disable = new Gtk.Switch();
  64. const field_notification_toggle = new Gtk.Switch();
  65. const field_confirm_clear_toggle = new Gtk.Switch();
  66. const field_strip_text = new Gtk.Switch();
  67. const field_paste_on_selection = new Gtk.Switch();
  68. const field_process_primary_selection = new Gtk.Switch();
  69. const field_ignore_password_mimes = new Gtk.Switch();
  70. const field_move_item_first = new Gtk.Switch();
  71. const field_keybinding = createKeybindingWidget(settings);
  72. addKeybinding(
  73. field_keybinding.model,
  74. settings,
  75. 'toggle-menu',
  76. _('Toggle the menu'),
  77. );
  78. addKeybinding(
  79. field_keybinding.model,
  80. settings,
  81. 'clear-history',
  82. _('Clear history'),
  83. );
  84. addKeybinding(
  85. field_keybinding.model,
  86. settings,
  87. 'prev-entry',
  88. _('Previous entry'),
  89. );
  90. addKeybinding(
  91. field_keybinding.model,
  92. settings,
  93. 'next-entry',
  94. _('Next entry'),
  95. );
  96. addKeybinding(
  97. field_keybinding.model,
  98. settings,
  99. 'toggle-private-mode',
  100. _('Toggle private mode'),
  101. );
  102. const field_keybinding_activation = new Gtk.Switch();
  103. field_keybinding_activation.connect('notify::active', (widget) => {
  104. field_keybinding.set_sensitive(widget.active);
  105. });
  106. const sizeLabel = new Gtk.Label({
  107. label: _('Max number of items'),
  108. hexpand: true,
  109. halign: Gtk.Align.START,
  110. });
  111. const windowWidthPercentageLabel = new Gtk.Label({
  112. label: _('Window width (%)'),
  113. hexpand: true,
  114. halign: Gtk.Align.START,
  115. });
  116. const cacheSizeLabel = new Gtk.Label({
  117. label: _('Max clipboard history size (MiB)'),
  118. hexpand: true,
  119. halign: Gtk.Align.START,
  120. });
  121. const cacheDisableLabel = new Gtk.Label({
  122. label: _('Only save favorites to disk'),
  123. hexpand: true,
  124. halign: Gtk.Align.START,
  125. });
  126. const notificationLabel = new Gtk.Label({
  127. label: _('Show notification on copy'),
  128. hexpand: true,
  129. halign: Gtk.Align.START,
  130. });
  131. const confirmClearLabel = new Gtk.Label({
  132. label: _('Ask for confirmation before clearing history'),
  133. hexpand: true,
  134. halign: Gtk.Align.START,
  135. });
  136. const moveFirstLabel = new Gtk.Label({
  137. label: _('Move previously copied items to the top'),
  138. hexpand: true,
  139. halign: Gtk.Align.START,
  140. });
  141. const keybindingLabel = new Gtk.Label({
  142. label: _('Keyboard shortcuts'),
  143. hexpand: true,
  144. halign: Gtk.Align.START,
  145. });
  146. const topbarPreviewLabel = new Gtk.Label({
  147. label: _('Number of characters in status bar'),
  148. hexpand: true,
  149. halign: Gtk.Align.START,
  150. });
  151. const displayModeLabel = new Gtk.Label({
  152. label: _('What to show in status bar'),
  153. hexpand: true,
  154. halign: Gtk.Align.START,
  155. });
  156. const disableDownArrowLabel = new Gtk.Label({
  157. label: _('Remove down arrow in status bar'),
  158. hexpand: true,
  159. halign: Gtk.Align.START,
  160. });
  161. const stripTextLabel = new Gtk.Label({
  162. label: _('Remove whitespace around text'),
  163. hexpand: true,
  164. halign: Gtk.Align.START,
  165. });
  166. const pasteOnSelectionLabel = new Gtk.Label({
  167. label: _('Paste on selection'),
  168. hexpand: true,
  169. halign: Gtk.Align.START,
  170. });
  171. const processPrimarySelection = new Gtk.Label({
  172. label: _('Save selected text to history'),
  173. hexpand: true,
  174. halign: Gtk.Align.START,
  175. });
  176. const ignorePasswordMimes = new Gtk.Label({
  177. label: _('Try to avoid copying passwords (known potentially buggy)'),
  178. hexpand: true,
  179. halign: Gtk.Align.START,
  180. });
  181. const addRow = ((main) => {
  182. let row = 0;
  183. return (label, input) => {
  184. let inputWidget = input;
  185. if (input instanceof Gtk.Switch) {
  186. inputWidget = new Gtk.Box({
  187. orientation: Gtk.Orientation.HORIZONTAL,
  188. });
  189. inputWidget.append(input);
  190. }
  191. if (label) {
  192. main.attach(label, 0, row, 1, 1);
  193. main.attach(inputWidget, 1, row, 1, 1);
  194. } else {
  195. main.attach(inputWidget, 0, row, 2, 1);
  196. }
  197. row++;
  198. };
  199. })(main);
  200. addRow(windowWidthPercentageLabel, window_width_percentage);
  201. addRow(sizeLabel, field_size);
  202. addRow(cacheSizeLabel, field_cache_size);
  203. addRow(cacheDisableLabel, field_cache_disable);
  204. addRow(moveFirstLabel, field_move_item_first);
  205. addRow(stripTextLabel, field_strip_text);
  206. addRow(pasteOnSelectionLabel, field_paste_on_selection);
  207. addRow(processPrimarySelection, field_process_primary_selection);
  208. addRow(ignorePasswordMimes, field_ignore_password_mimes);
  209. addRow(displayModeLabel, field_display_mode);
  210. addRow(disableDownArrowLabel, field_disable_down_arrow);
  211. addRow(topbarPreviewLabel, field_topbar_preview_size);
  212. addRow(notificationLabel, field_notification_toggle);
  213. addRow(confirmClearLabel, field_confirm_clear_toggle);
  214. addRow(keybindingLabel, field_keybinding_activation);
  215. addRow(null, field_keybinding);
  216. settings.bind(
  217. Fields.HISTORY_SIZE,
  218. field_size,
  219. 'value',
  220. Gio.SettingsBindFlags.DEFAULT,
  221. );
  222. settings.bind(
  223. Fields.WINDOW_WIDTH_PERCENTAGE,
  224. window_width_percentage,
  225. 'value',
  226. Gio.SettingsBindFlags.DEFAULT,
  227. );
  228. settings.bind(
  229. Fields.CACHE_FILE_SIZE,
  230. field_cache_size,
  231. 'value',
  232. Gio.SettingsBindFlags.DEFAULT,
  233. );
  234. settings.bind(
  235. Fields.CACHE_ONLY_FAVORITES,
  236. field_cache_disable,
  237. 'active',
  238. Gio.SettingsBindFlags.DEFAULT,
  239. );
  240. settings.bind(
  241. Fields.NOTIFY_ON_COPY,
  242. field_notification_toggle,
  243. 'active',
  244. Gio.SettingsBindFlags.DEFAULT,
  245. );
  246. settings.bind(
  247. Fields.CONFIRM_ON_CLEAR,
  248. field_confirm_clear_toggle,
  249. 'active',
  250. Gio.SettingsBindFlags.DEFAULT,
  251. );
  252. settings.bind(
  253. Fields.MOVE_ITEM_FIRST,
  254. field_move_item_first,
  255. 'active',
  256. Gio.SettingsBindFlags.DEFAULT,
  257. );
  258. settings.bind(
  259. Fields.TOPBAR_DISPLAY_MODE_ID,
  260. field_display_mode,
  261. 'active',
  262. Gio.SettingsBindFlags.DEFAULT,
  263. );
  264. settings.bind(
  265. Fields.DISABLE_DOWN_ARROW,
  266. field_disable_down_arrow,
  267. 'active',
  268. Gio.SettingsBindFlags.DEFAULT,
  269. );
  270. settings.bind(
  271. Fields.TOPBAR_PREVIEW_SIZE,
  272. field_topbar_preview_size,
  273. 'value',
  274. Gio.SettingsBindFlags.DEFAULT,
  275. );
  276. settings.bind(
  277. Fields.STRIP_TEXT,
  278. field_strip_text,
  279. 'active',
  280. Gio.SettingsBindFlags.DEFAULT,
  281. );
  282. settings.bind(
  283. Fields.PASTE_ON_SELECTION,
  284. field_paste_on_selection,
  285. 'active',
  286. Gio.SettingsBindFlags.DEFAULT,
  287. );
  288. settings.bind(
  289. Fields.PROCESS_PRIMARY_SELECTION,
  290. field_process_primary_selection,
  291. 'active',
  292. Gio.SettingsBindFlags.DEFAULT,
  293. );
  294. settings.bind(
  295. Fields.IGNORE_PASSWORD_MIMES,
  296. field_ignore_password_mimes,
  297. 'active',
  298. Gio.SettingsBindFlags.DEFAULT,
  299. );
  300. settings.bind(
  301. Fields.ENABLE_KEYBINDING,
  302. field_keybinding_activation,
  303. 'active',
  304. Gio.SettingsBindFlags.DEFAULT,
  305. );
  306. const group = new Adw.PreferencesGroup();
  307. group.add(main);
  308. const page = new Adw.PreferencesPage();
  309. page.add(group);
  310. window.add(page);
  311. }
  312. _create_display_mode_options() {
  313. const options = [
  314. { name: _('Icon') },
  315. { name: _('Clipboard contents') },
  316. { name: _('Both') },
  317. { name: _('Neither') },
  318. ];
  319. const liststore = new Gtk.ListStore();
  320. liststore.set_column_types([GObject.TYPE_STRING]);
  321. for (let i = 0; i < options.length; i++) {
  322. const option = options[i];
  323. const iter = liststore.append();
  324. liststore.set(iter, [0], [option.name]);
  325. }
  326. return liststore;
  327. }
  328. }
  329. //binding widgets
  330. //////////////////////////////////
  331. const COLUMN_ID = 0;
  332. const COLUMN_DESCRIPTION = 1;
  333. const COLUMN_KEY = 2;
  334. const COLUMN_MODS = 3;
  335. function addKeybinding(model, settings, id, description) {
  336. // Get the current accelerator.
  337. const accelerator = settings.get_strv(id)[0];
  338. let key, mods;
  339. if (accelerator == null) {
  340. [key, mods] = [0, 0];
  341. } else {
  342. [, key, mods] = Gtk.accelerator_parse(settings.get_strv(id)[0]);
  343. }
  344. // Add a row for the keybinding.
  345. const row = model.insert(100); // Erm...
  346. model.set(
  347. row,
  348. [COLUMN_ID, COLUMN_DESCRIPTION, COLUMN_KEY, COLUMN_MODS],
  349. [id, description, key, mods],
  350. );
  351. }
  352. function createKeybindingWidget(Settings) {
  353. const model = new Gtk.ListStore();
  354. model.set_column_types([
  355. GObject.TYPE_STRING, // COLUMN_ID
  356. GObject.TYPE_STRING, // COLUMN_DESCRIPTION
  357. GObject.TYPE_INT, // COLUMN_KEY
  358. GObject.TYPE_INT,
  359. ]); // COLUMN_MODS
  360. const treeView = new Gtk.TreeView();
  361. treeView.model = model;
  362. treeView.headers_visible = false;
  363. let column, renderer;
  364. // Description column.
  365. renderer = new Gtk.CellRendererText();
  366. column = new Gtk.TreeViewColumn();
  367. column.expand = true;
  368. column.pack_start(renderer, true);
  369. column.add_attribute(renderer, 'text', COLUMN_DESCRIPTION);
  370. treeView.append_column(column);
  371. // Key binding column.
  372. renderer = new Gtk.CellRendererAccel();
  373. renderer.accel_mode = Gtk.CellRendererAccelMode.GTK;
  374. renderer.editable = true;
  375. renderer.connect(
  376. 'accel-edited',
  377. function (renderer, path, key, mods, hwCode) {
  378. const [ok, iter] = model.get_iter_from_string(path);
  379. if (!ok) {
  380. return;
  381. }
  382. // Update the UI.
  383. model.set(iter, [COLUMN_KEY, COLUMN_MODS], [key, mods]);
  384. // Update the stored setting.
  385. const id = model.get_value(iter, COLUMN_ID);
  386. const accelString = Gtk.accelerator_name(key, mods);
  387. Settings.set_strv(id, [accelString]);
  388. },
  389. );
  390. renderer.connect('accel-cleared', function (renderer, path) {
  391. const [ok, iter] = model.get_iter_from_string(path);
  392. if (!ok) {
  393. return;
  394. }
  395. // Update the UI.
  396. model.set(iter, [COLUMN_KEY, COLUMN_MODS], [0, 0]);
  397. // Update the stored setting.
  398. const id = model.get_value(iter, COLUMN_ID);
  399. Settings.set_strv(id, []);
  400. });
  401. column = new Gtk.TreeViewColumn();
  402. column.pack_end(renderer, false);
  403. column.add_attribute(renderer, 'accel-key', COLUMN_KEY);
  404. column.add_attribute(renderer, 'accel-mods', COLUMN_MODS);
  405. treeView.append_column(column);
  406. return treeView;
  407. }