| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352 |
- import Clutter from 'gi://Clutter';
- import GObject from 'gi://GObject';
- import GLib from 'gi://GLib';
- import Meta from 'gi://Meta';
- import Shell from 'gi://Shell';
- import St from 'gi://St';
- import * as Main from 'resource:///org/gnome/shell/ui/main.js';
- import * as MessageTray from 'resource:///org/gnome/shell/ui/messageTray.js';
- import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js';
- import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
- import {
- Extension,
- gettext as _,
- } from 'resource:///org/gnome/shell/extensions/extension.js';
- import { ensureActorVisibleInScrollView } from 'resource:///org/gnome/shell/misc/animationUtils.js';
- import * as Store from './store.js';
- import * as DS from './dataStructures.js';
- import { openConfirmDialog } from './confirmDialog.js';
- import SettingsFields from './settingsFields.js';
- const Clipboard = St.Clipboard.get_default();
- const VirtualKeyboard = (() => {
- let VirtualKeyboard;
- return () => {
- if (!VirtualKeyboard) {
- VirtualKeyboard = Clutter.get_default_backend()
- .get_default_seat()
- .create_virtual_device(Clutter.InputDeviceType.KEYBOARD_DEVICE);
- }
- return VirtualKeyboard;
- };
- })();
- const SETTING_KEY_CLEAR_HISTORY = 'clear-history';
- const SETTING_KEY_PREV_ENTRY = 'prev-entry';
- const SETTING_KEY_NEXT_ENTRY = 'next-entry';
- const SETTING_KEY_TOGGLE_MENU = 'toggle-menu';
- const SETTING_KEY_PRIVATE_MODE = 'toggle-private-mode';
- const INDICATOR_ICON = 'edit-paste-symbolic';
- const PAGE_SIZE = 50;
- const MAX_VISIBLE_CHARS = 200;
- let MAX_REGISTRY_LENGTH;
- let MAX_BYTES;
- let WINDOW_WIDTH_PERCENTAGE;
- let CACHE_ONLY_FAVORITES;
- let MOVE_ITEM_FIRST;
- let ENABLE_KEYBINDING;
- let PRIVATE_MODE;
- let NOTIFY_ON_COPY;
- let CONFIRM_ON_CLEAR;
- let MAX_TOPBAR_LENGTH;
- let TOPBAR_DISPLAY_MODE; // 0 - only icon, 1 - only clipboard content, 2 - both, 3 - none
- let DISABLE_DOWN_ARROW;
- let STRIP_TEXT;
- let PASTE_ON_SELECTION;
- let PROCESS_PRIMARY_SELECTION;
- let IGNORE_PASSWORD_MIMES;
- class ClipboardIndicator extends PanelMenu.Button {
- _init(extension) {
- super._init(0, extension.indicatorName, false);
- this.extension = extension;
- this.settings = extension.getSettings();
- this._shortcutsBindingIds = [];
- const hbox = new St.BoxLayout({
- style_class: 'panel-status-menu-box clipboard-indicator-hbox',
- });
- this.icon = new St.Icon({
- icon_name: INDICATOR_ICON,
- style_class: 'system-status-icon clipboard-indicator-icon',
- });
- hbox.add_child(this.icon);
- this._buttonText = new St.Label({
- text: '',
- y_align: Clutter.ActorAlign.CENTER,
- });
- hbox.add_child(this._buttonText);
- this._downArrow = PopupMenu.arrowIcon(St.Side.BOTTOM);
- hbox.add_child(this._downArrow);
- this.add_child(hbox);
- this._fetchSettings();
- this._buildMenu();
- this._updateTopbarLayout();
- }
- destroy() {
- this._disconnectSettings();
- this._unbindShortcuts();
- this._disconnectSelectionListener();
- if (this._searchFocusHackCallbackId) {
- GLib.Source.source_remove(this._searchFocusHackCallbackId);
- this._searchFocusHackCallbackId = undefined;
- }
- if (this._pasteHackCallbackId) {
- GLib.Source.source_remove(this._pasteHackCallbackId);
- this._pasteHackCallbackId = undefined;
- }
- super.destroy();
- }
- _buildMenu() {
- this.searchEntry = new St.Entry({
- name: 'searchEntry',
- style_class: 'search-entry ci-history-search-entry',
- can_focus: true,
- hint_text: _('Search clipboard history…'),
- track_hover: true,
- x_expand: true,
- y_expand: true,
- });
- const entryItem = new PopupMenu.PopupBaseMenuItem({
- style_class: 'ci-history-search-section',
- reactive: false,
- can_focus: false,
- });
- entryItem.add_child(this.searchEntry);
- this.menu.addMenuItem(entryItem);
- this.menu.connect('open-state-changed', (self, open) => {
- if (open) {
- this._setMenuWidth();
- this.searchEntry.set_text('');
- this._searchFocusHackCallbackId = GLib.timeout_add(
- GLib.PRIORITY_DEFAULT,
- 1,
- () => {
- global.stage.set_key_focus(this.searchEntry);
- this._searchFocusHackCallbackId = undefined;
- return false;
- },
- );
- }
- });
- // Create menu sections for items
- // Favorites
- this.favoritesSection = new PopupMenu.PopupMenuSection();
- this.scrollViewFavoritesMenuSection = new PopupMenu.PopupMenuSection();
- const favoritesScrollView = new St.ScrollView({
- style_class: 'ci-history-menu-section',
- overlay_scrollbars: true,
- });
- favoritesScrollView.add_child(this.favoritesSection.actor);
- this.scrollViewFavoritesMenuSection.actor.add_child(favoritesScrollView);
- this.menu.addMenuItem(this.scrollViewFavoritesMenuSection);
- this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
- // History
- this.historySection = new PopupMenu.PopupMenuSection();
- this.scrollViewMenuSection = new PopupMenu.PopupMenuSection();
- this.historyScrollView = new St.ScrollView({
- style_class: 'ci-history-menu-section',
- overlay_scrollbars: true,
- });
- this.historyScrollView.add_child(this.historySection.actor);
- this.scrollViewMenuSection.actor.add_child(this.historyScrollView);
- this.menu.addMenuItem(this.scrollViewMenuSection);
- this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
- const actionsSection = new PopupMenu.PopupMenuSection();
- const actionsBox = new St.BoxLayout({
- style_class: 'ci-history-actions-section',
- vertical: false,
- });
- actionsSection.actor.add_child(actionsBox);
- this.menu.addMenuItem(actionsSection);
- const prevPage = new PopupMenu.PopupBaseMenuItem();
- prevPage.add_child(
- new St.Icon({
- icon_name: 'go-previous-symbolic',
- style_class: 'popup-menu-icon',
- }),
- );
- prevPage.connect('activate', this._navigatePrevPage.bind(this));
- actionsBox.add_child(prevPage);
- const nextPage = new PopupMenu.PopupBaseMenuItem();
- nextPage.add_child(
- new St.Icon({
- icon_name: 'go-next-symbolic',
- style_class: 'popup-menu-icon',
- }),
- );
- nextPage.connect('activate', this._navigateNextPage.bind(this));
- actionsBox.add_child(nextPage);
- actionsBox.add_child(new St.BoxLayout({ x_expand: true }));
- this.privateModeMenuItem = new PopupMenu.PopupSwitchMenuItem(
- _('Private mode'),
- PRIVATE_MODE,
- { reactive: true },
- );
- this.privateModeMenuItem.connect('toggled', () => {
- this.settings.set_boolean(
- SettingsFields.PRIVATE_MODE,
- this.privateModeMenuItem.state,
- );
- });
- actionsBox.add_child(this.privateModeMenuItem);
- this._updatePrivateModeState();
- const clearMenuItem = new PopupMenu.PopupBaseMenuItem();
- clearMenuItem.add_child(
- new St.Icon({
- icon_name: 'edit-delete-symbolic',
- style_class: 'popup-menu-icon',
- }),
- );
- actionsBox.add_child(clearMenuItem);
- const settingsMenuItem = new PopupMenu.PopupBaseMenuItem();
- settingsMenuItem.add_child(
- new St.Icon({
- icon_name: 'emblem-system-symbolic',
- style_class: 'popup-menu-icon',
- }),
- );
- settingsMenuItem.connect('activate', this._openSettings.bind(this));
- actionsBox.add_child(settingsMenuItem);
- if (ENABLE_KEYBINDING) {
- this._bindShortcuts();
- }
- this.menu.actor.connect('key-press-event', (_, event) =>
- this._handleGlobalKeyEvent(event),
- );
- Store.buildClipboardStateFromLog(
- (entries, favoriteEntries, nextId, nextDiskId) => {
- /**
- * This field stores the number of items in the historySection to avoid calling _getMenuItems
- * since that method is slow.
- */
- this.activeHistoryMenuItems = 0;
- /**
- * These two IDs are extremely important: making a mistake with either one breaks the
- * extension. Both IDs are globally unique within compaction intervals. The normal ID is
- * *always* present and valid -- it allows us to build an inverted index so we can find
- * previously copied items in O(1) time. The Disk ID is only present when we cache all
- * entries. This additional complexity is needed to know what the ID of an item is on disk as
- * compared to in memory when we're only caching favorites.
- */
- this.nextId = nextId;
- this.nextDiskId = nextDiskId || nextId;
- /**
- * DS.LinkedList is the actual clipboard history and source of truth. Never use historySection
- * or favoritesSection as the source of truth as these may get outdated during pagination.
- *
- * Entries *may* have a menuItem attached, meaning they are currently visible. On the other
- * hand, menu items must always have an entry attached.
- */
- this.entries = entries;
- this.favoriteEntries = favoriteEntries;
- this.currentlySelectedEntry = entries.last();
- this._restoreFavoritedEntries();
- this._maybeRestoreMenuPages();
- this._settingsChangedId = this.settings.connect(
- 'changed',
- this._onSettingsChange.bind(this),
- );
- this.searchEntry
- .get_clutter_text()
- .connect('text-changed', this._onSearchTextChanged.bind(this));
- clearMenuItem.connect('activate', this._removeAll.bind(this));
- this._setupSelectionChangeListener();
- },
- );
- }
- _setMenuWidth() {
- const display = global.display;
- const screen_width = display.get_monitor_geometry(
- display.get_primary_monitor(),
- ).width;
- this.menu.actor.width = screen_width * (WINDOW_WIDTH_PERCENTAGE / 100);
- }
- _handleGlobalKeyEvent(event) {
- this._handleCtrlSelectKeyEvent(event);
- this._handleSettingsKeyEvent(event);
- this._handleNavigationKeyEvent(event);
- this._handleFocusSearchKeyEvent(event);
- }
- _handleCtrlSelectKeyEvent(event) {
- if (!event.has_control_modifier()) {
- return;
- }
- const index = parseInt(event.get_key_unicode()); // Starts at 1
- if (isNaN(index) || index <= 0) {
- return;
- }
- const items =
- event.get_state() === 68 // Ctrl + Super
- ? this.favoritesSection._getMenuItems()
- : this.historySection._getMenuItems();
- if (index > items.length) {
- return;
- }
- this._onMenuItemSelectedAndMenuClose(items[index - 1]);
- }
- _handleSettingsKeyEvent(event) {
- if (event.get_state() !== 12 || event.get_key_unicode() !== 's') {
- return;
- }
- this._openSettings();
- }
- _handleNavigationKeyEvent(event) {
- if (!event.has_control_modifier()) {
- return;
- }
- if (event.get_key_unicode() === 'n') {
- this._navigateNextPage();
- } else if (event.get_key_unicode() === 'p') {
- this._navigatePrevPage();
- }
- }
- _handleFocusSearchKeyEvent(event) {
- if (event.get_key_unicode() !== '/') {
- return;
- }
- global.stage.set_key_focus(this.searchEntry);
- }
- _addEntry(entry, selectEntry, updateClipboard, insertIndex) {
- if (!entry.favorite && this.activeHistoryMenuItems >= PAGE_SIZE) {
- const items = this.historySection._getMenuItems();
- const item = items[items.length - 1];
- this._rewriteMenuItem(item, entry);
- this.historySection.moveMenuItem(item, 0);
- if (selectEntry) {
- this._selectEntry(entry, updateClipboard);
- }
- return;
- }
- const menuItem = new PopupMenu.PopupMenuItem('', { hover: false });
- menuItem.setOrnament(PopupMenu.Ornament.NONE);
- menuItem.entry = entry;
- entry.menuItem = menuItem;
- menuItem.connect(
- 'activate',
- this._onMenuItemSelectedAndMenuClose.bind(this),
- );
- menuItem.connect('key-press-event', (_, event) =>
- this._handleMenuItemKeyEvent(event, menuItem),
- );
- this._setEntryLabel(menuItem);
- // Favorite button
- const icon_name = entry.favorite
- ? 'starred-symbolic'
- : 'non-starred-symbolic';
- const iconfav = new St.Icon({
- icon_name: icon_name,
- style_class: 'system-status-icon',
- });
- const icofavBtn = new St.Button({
- style_class: 'ci-action-btn',
- can_focus: true,
- child: iconfav,
- x_align: Clutter.ActorAlign.END,
- x_expand: true,
- y_expand: true,
- });
- menuItem.actor.add_child(icofavBtn);
- icofavBtn.connect('clicked', () => {
- this._favoriteToggle(menuItem);
- });
- // Delete button
- const icon = new St.Icon({
- icon_name: 'edit-delete-symbolic',
- style_class: 'system-status-icon',
- });
- const icoBtn = new St.Button({
- style_class: 'ci-action-btn',
- can_focus: true,
- child: icon,
- x_align: Clutter.ActorAlign.END,
- x_expand: false,
- y_expand: true,
- });
- menuItem.actor.add_child(icoBtn);
- icoBtn.connect('clicked', () => {
- this._deleteEntryAndRestoreLatest(menuItem.entry);
- });
- menuItem.connect('destroy', () => {
- delete menuItem.entry.menuItem;
- if (!menuItem.entry.favorite) {
- this.activeHistoryMenuItems--;
- }
- });
- menuItem.connect('key-focus-in', () => {
- if (!menuItem.entry.favorite) {
- ensureActorVisibleInScrollView(this.historyScrollView, menuItem);
- }
- });
- if (entry.favorite) {
- this.favoritesSection.addMenuItem(menuItem, insertIndex);
- } else {
- this.historySection.addMenuItem(menuItem, insertIndex);
- this.activeHistoryMenuItems++;
- }
- if (selectEntry) {
- this._selectEntry(entry, updateClipboard);
- }
- }
- _handleMenuItemKeyEvent(event, menuItem) {
- if (event.get_key_unicode() === 'f') {
- this._favoriteToggle(menuItem);
- }
- if (event.get_key_code() === 119) {
- const next = menuItem.entry.prev || menuItem.entry.next;
- if (next?.menuItem) {
- global.stage.set_key_focus(next.menuItem);
- }
- this._deleteEntryAndRestoreLatest(menuItem.entry);
- }
- }
- _updateButtonText(entry) {
- if (
- !(TOPBAR_DISPLAY_MODE === 1 || TOPBAR_DISPLAY_MODE === 2) ||
- (entry && entry.type !== DS.TYPE_TEXT)
- ) {
- return;
- }
- if (PRIVATE_MODE) {
- this._buttonText.set_text('…');
- } else if (entry) {
- this._buttonText.set_text(this._truncated(entry.text, MAX_TOPBAR_LENGTH));
- } else {
- this._buttonText.set_text('');
- }
- }
- _setEntryLabel(menuItem) {
- const entry = menuItem.entry;
- if (entry.type === DS.TYPE_TEXT) {
- menuItem.label.set_text(this._truncated(entry.text, MAX_VISIBLE_CHARS));
- } else {
- throw new TypeError('Unknown type: ' + entry.type);
- }
- }
- _favoriteToggle(menuItem) {
- const entry = menuItem.entry;
- const wasSelected = this.currentlySelectedEntry?.id === entry.id;
- // Move to front (end of list)
- (entry.favorite ? this.entries : this.favoriteEntries).append(entry);
- this._removeEntry(entry);
- entry.favorite = !entry.favorite;
- this._addEntry(entry, wasSelected, false, 0);
- this._maybeRestoreMenuPages();
- global.stage.set_key_focus(entry.menuItem);
- if (CACHE_ONLY_FAVORITES && !entry.favorite) {
- if (entry.diskId) {
- Store.deleteTextEntry(entry.diskId, true);
- delete entry.diskId;
- }
- return;
- }
- if (entry.diskId) {
- Store.updateFavoriteStatus(entry.diskId, entry.favorite);
- } else {
- entry.diskId = this.nextDiskId++;
- Store.storeTextEntry(entry.text);
- Store.updateFavoriteStatus(entry.diskId, true);
- }
- }
- _removeAll() {
- if (CONFIRM_ON_CLEAR) {
- this._confirmRemoveAll();
- } else {
- this._clearHistory();
- }
- }
- _confirmRemoveAll() {
- const title = _('Clear all?');
- const message = _('Are you sure you want to delete all clipboard items?');
- const sub_message = _('This operation cannot be undone.');
- openConfirmDialog(
- title,
- message,
- sub_message,
- _('Clear'),
- _('Cancel'),
- () => {
- this._clearHistory();
- },
- );
- }
- _clearHistory() {
- if (this.currentlySelectedEntry && !this.currentlySelectedEntry.favorite) {
- this._resetSelectedMenuItem(true);
- }
- // Favorites aren't touched when clearing history
- this.entries = new DS.LinkedList();
- this.historySection.removeAll();
- Store.resetDatabase(this._currentStateBuilder.bind(this));
- }
- _removeEntry(entry, fullyDelete, humanGenerated) {
- if (fullyDelete) {
- entry.detach();
- if (entry.diskId) {
- Store.deleteTextEntry(entry.diskId, entry.favorite);
- }
- }
- if (entry.id === this.currentlySelectedEntry?.id) {
- this._resetSelectedMenuItem(humanGenerated);
- }
- entry.menuItem?.destroy();
- if (fullyDelete) {
- this._maybeRestoreMenuPages();
- }
- }
- _pruneOldestEntries() {
- let entry = this.entries.head;
- while (
- entry &&
- (this.entries.length > MAX_REGISTRY_LENGTH ||
- this.entries.bytes > MAX_BYTES)
- ) {
- const next = entry.next;
- this._removeEntry(entry, true);
- entry = next;
- }
- Store.maybePerformLogCompaction(this._currentStateBuilder.bind(this));
- }
- _selectEntry(entry, updateClipboard, triggerPaste) {
- this.currentlySelectedEntry?.menuItem?.setOrnament(PopupMenu.Ornament.NONE);
- this.currentlySelectedEntry = entry;
- entry.menuItem?.setOrnament(PopupMenu.Ornament.DOT);
- this._updateButtonText(entry);
- if (updateClipboard !== false) {
- if (entry.type === DS.TYPE_TEXT) {
- this._setClipboardText(entry.text);
- } else {
- throw new TypeError('Unknown type: ' + entry.type);
- }
- if (PASTE_ON_SELECTION && triggerPaste) {
- this._triggerPasteHack();
- }
- }
- }
- _setClipboardText(text) {
- if (this._debouncing !== undefined) {
- this._debouncing++;
- }
- Clipboard.set_text(St.ClipboardType.CLIPBOARD, text);
- Clipboard.set_text(St.ClipboardType.PRIMARY, text);
- }
- _triggerPasteHack() {
- this._pasteHackCallbackId = GLib.timeout_add(
- GLib.PRIORITY_DEFAULT,
- 1, // Just post to the end of the event loop
- () => {
- const SHIFT_L = 42;
- const INSERT = 110;
- const eventTime = Clutter.get_current_event_time() * 1000;
- VirtualKeyboard().notify_key(
- eventTime,
- SHIFT_L,
- Clutter.KeyState.PRESSED,
- );
- VirtualKeyboard().notify_key(
- eventTime,
- INSERT,
- Clutter.KeyState.PRESSED,
- );
- VirtualKeyboard().notify_key(
- eventTime,
- INSERT,
- Clutter.KeyState.RELEASED,
- );
- VirtualKeyboard().notify_key(
- eventTime,
- SHIFT_L,
- Clutter.KeyState.RELEASED,
- );
- this._pasteHackCallbackId = undefined;
- return false;
- },
- );
- }
- _onMenuItemSelectedAndMenuClose(menuItem) {
- this._moveEntryFirst(menuItem.entry);
- this._selectEntry(menuItem.entry, true, true);
- this.menu.close();
- }
- _resetSelectedMenuItem(resetClipboard) {
- this.currentlySelectedEntry = undefined;
- this._updateButtonText();
- if (resetClipboard) {
- this._setClipboardText('');
- }
- }
- _restoreFavoritedEntries() {
- for (let entry = this.favoriteEntries.last(); entry; entry = entry.prev) {
- this._addEntry(entry);
- }
- }
- _maybeRestoreMenuPages() {
- if (this.activeHistoryMenuItems > 0) {
- return;
- }
- for (
- let entry = this.entries.last();
- entry && this.activeHistoryMenuItems < PAGE_SIZE;
- entry = entry.prev
- ) {
- this._addEntry(entry, this.currentlySelectedEntry === entry);
- }
- }
- /**
- * Our pagination implementation is purposefully "broken." The idea is simply to do no unnecessary
- * work. As a consequence, if a user navigates to some page and then starts copying/moving items,
- * those items will appear on the currently visible page even though they don't belong there. This
- * could kind of be considered a feature since it means you can go back to some cluster of copied
- * items and start copying stuff from the same cluster and have it all show up together.
- *
- * Note that over time (as the user copies items), the page reclamation process will morph the
- * current page into the first page. This is the only way to make the user-visible state match our
- * backing store after changing pages.
- *
- * Also note that the use of `last` and `next` is correct. Menu items are ordered from latest to
- * oldest whereas `entries` is ordered from oldest to latest.
- */
- _navigatePrevPage() {
- if (this.searchEntryFront) {
- this.populateSearchResults(this.searchEntry.get_text(), false);
- return;
- }
- const items = this.historySection._getMenuItems();
- if (items.length === 0) {
- return;
- }
- const start = items[0].entry;
- for (
- let entry = start.nextCyclic(), i = items.length - 1;
- entry !== start && i >= 0;
- entry = entry.nextCyclic()
- ) {
- this._rewriteMenuItem(items[i--], entry);
- }
- }
- _navigateNextPage() {
- if (this.searchEntryFront) {
- this.populateSearchResults(this.searchEntry.get_text(), true);
- return;
- }
- const items = this.historySection._getMenuItems();
- if (items.length === 0) {
- return;
- }
- const start = items[items.length - 1].entry;
- for (
- let entry = start.prevCyclic(), i = 0;
- entry !== start && i < items.length;
- entry = entry.prevCyclic()
- ) {
- this._rewriteMenuItem(items[i++], entry);
- }
- }
- _rewriteMenuItem(item, entry) {
- if (item.entry.id === this.currentlySelectedEntry?.id) {
- item.setOrnament(PopupMenu.Ornament.NONE);
- }
- item.entry = entry;
- entry.menuItem = item;
- this._setEntryLabel(item);
- if (entry.id === this.currentlySelectedEntry?.id) {
- item.setOrnament(PopupMenu.Ornament.DOT);
- }
- }
- _onSearchTextChanged() {
- const query = this.searchEntry.get_text();
- if (!query) {
- this.historySection.removeAll();
- this.favoritesSection.removeAll();
- this.searchEntryFront = this.searchEntryBack = undefined;
- this._restoreFavoritedEntries();
- this._maybeRestoreMenuPages();
- return;
- }
- this.searchEntryFront = this.searchEntryBack = this.entries.last();
- this.populateSearchResults(query);
- }
- populateSearchResults(query, forward) {
- if (!this.searchEntryFront) {
- return;
- }
- this.historySection.removeAll();
- this.favoritesSection.removeAll();
- if (typeof forward !== 'boolean') {
- forward = true;
- }
- query = query.toLowerCase();
- let searchExp;
- try {
- searchExp = new RegExp(query, 'i');
- } catch {}
- const start = forward ? this.searchEntryFront : this.searchEntryBack;
- let entry = start;
- while (this.activeHistoryMenuItems < PAGE_SIZE) {
- if (entry.type === DS.TYPE_TEXT) {
- let match = entry.text.toLowerCase().indexOf(query);
- if (searchExp && match < 0) {
- match = entry.text.search(searchExp);
- }
- if (match >= 0) {
- this._addEntry(
- entry,
- entry === this.currentlySelectedEntry,
- false,
- forward ? undefined : 0,
- );
- entry.menuItem.label.set_text(
- this._truncated(
- entry.text,
- match - 40,
- match + MAX_VISIBLE_CHARS - 40,
- ),
- );
- }
- } else {
- throw new TypeError('Unknown type: ' + entry.type);
- }
- entry = forward ? entry.prevCyclic() : entry.nextCyclic();
- if (entry === start) {
- break;
- }
- }
- if (forward) {
- this.searchEntryBack = this.searchEntryFront.nextCyclic();
- this.searchEntryFront = entry;
- } else {
- this.searchEntryFront = this.searchEntryBack.prevCyclic();
- this.searchEntryBack = entry;
- }
- }
- _shouldAbortClipboardQuery(kind) {
- if (PRIVATE_MODE) {
- return true;
- }
- if (
- IGNORE_PASSWORD_MIMES &&
- Clipboard.get_mimetypes(kind).includes(
- // Note that we should check for the value "secret" but there don't appear to be any other
- // values so it's not worth the trouble right now.
- 'x-kde-passwordManagerHint',
- )
- ) {
- console.log(this.uuid, 'Ignoring password entry.');
- return true;
- }
- return false;
- }
- _queryClipboard() {
- if (this._shouldAbortClipboardQuery(St.Clipboard.CLIPBOARD)) {
- return;
- }
- Clipboard.get_text(St.ClipboardType.CLIPBOARD, (_, text) => {
- this._processClipboardContent(text, true);
- });
- }
- _queryPrimaryClipboard() {
- if (this._shouldAbortClipboardQuery(St.Clipboard.PRIMARY)) {
- return;
- }
- Clipboard.get_text(St.ClipboardType.PRIMARY, (_, text) => {
- const last = this.entries.last();
- text = this._processClipboardContent(text, false);
- if (
- last &&
- text &&
- text.length !== last.text.length &&
- (text.endsWith(last.text) ||
- text.startsWith(last.text) ||
- last.text.endsWith(text) ||
- last.text.startsWith(text))
- ) {
- this._removeEntry(last, true);
- }
- });
- }
- _processClipboardContent(text, selectEntry) {
- if (this._debouncing > 0) {
- this._debouncing--;
- return;
- }
- if (STRIP_TEXT && text) {
- text = text.trim();
- }
- if (!text) {
- return;
- }
- let entry =
- this.entries.findTextItem(text) ||
- this.favoriteEntries.findTextItem(text);
- if (entry) {
- const isFirst =
- entry === this.entries.last() || entry === this.favoriteEntries.last();
- if (!isFirst) {
- this._moveEntryFirst(entry);
- }
- if (selectEntry && (!isFirst || entry !== this.currentlySelectedEntry)) {
- this._selectEntry(entry, false);
- }
- } else {
- entry = new DS.LLNode();
- entry.id = this.nextId++;
- entry.diskId = CACHE_ONLY_FAVORITES ? undefined : this.nextDiskId++;
- entry.type = DS.TYPE_TEXT;
- entry.text = text;
- entry.favorite = false;
- this.entries.append(entry);
- this._addEntry(entry, selectEntry, false, 0);
- if (!CACHE_ONLY_FAVORITES) {
- Store.storeTextEntry(text);
- }
- this._pruneOldestEntries();
- }
- if (NOTIFY_ON_COPY) {
- this._showNotification(_('Copied to clipboard'), null, (notif) => {
- notif.addAction(_('Cancel'), () =>
- this._deleteEntryAndRestoreLatest(this.currentlySelectedEntry),
- );
- });
- }
- return text;
- }
- _moveEntryFirst(entry) {
- if (!MOVE_ITEM_FIRST) {
- return;
- }
- let menu;
- let entries;
- if (entry.favorite) {
- menu = this.favoritesSection;
- entries = this.favoriteEntries;
- } else {
- menu = this.historySection;
- entries = this.entries;
- }
- if (entry.menuItem) {
- menu.moveMenuItem(entry.menuItem, 0);
- } else {
- this._addEntry(entry, false, false, 0);
- }
- entries.append(entry);
- if (entry.diskId) {
- Store.moveEntryToEnd(entry.diskId);
- }
- }
- _currentStateBuilder() {
- const state = [];
- this.nextDiskId = 1;
- for (const entry of this.favoriteEntries) {
- entry.diskId = this.nextDiskId++;
- state.push(entry);
- }
- for (const entry of this.entries) {
- if (CACHE_ONLY_FAVORITES) {
- delete entry.diskId;
- } else {
- entry.diskId = this.nextDiskId++;
- state.push(entry);
- }
- }
- return state;
- }
- _setupSelectionChangeListener() {
- this._debouncing = 0;
- this.selection = Shell.Global.get().get_display().get_selection();
- this._selectionOwnerChangedId = this.selection.connect(
- 'owner-changed',
- (_, selectionType) => {
- if (selectionType === Meta.SelectionType.SELECTION_CLIPBOARD) {
- this._queryClipboard();
- } else if (
- PROCESS_PRIMARY_SELECTION &&
- selectionType === Meta.SelectionType.SELECTION_PRIMARY
- ) {
- this._queryPrimaryClipboard();
- }
- },
- );
- }
- _disconnectSelectionListener() {
- if (!this._selectionOwnerChangedId) {
- return;
- }
- this.selection.disconnect(this._selectionOwnerChangedId);
- this.selection = undefined;
- this._selectionOwnerChangedId = undefined;
- }
- _deleteEntryAndRestoreLatest(entry) {
- this._removeEntry(entry, true, true);
- if (!this.currentlySelectedEntry) {
- const nextEntry = this.entries.last();
- if (nextEntry) {
- this._selectEntry(nextEntry, true);
- }
- }
- }
- _initNotifSource() {
- if (this._notifSource) {
- return;
- }
- this._notifSource = new MessageTray.Source({
- title: this.extension.indicatorName,
- iconName: INDICATOR_ICON,
- });
- this._notifSource.connect('destroy', () => {
- this._notifSource = undefined;
- });
- Main.messageTray.add(this._notifSource);
- }
- _showNotification(title, message, transformFn) {
- const dndOn = () =>
- !Main.panel.statusArea.dateMenu._indicator._settings.get_boolean(
- 'show-banners',
- );
- if (PRIVATE_MODE || dndOn()) {
- return;
- }
- this._initNotifSource();
- let notification;
- if (this._notifSource.count === 0) {
- notification = new MessageTray.Notification({
- source: this._notifSource,
- title,
- body: message,
- isTransient: true,
- });
- } else {
- notification = this._notifSource.notifications[0];
- notification.set({
- title,
- body: message,
- });
- notification.clearActions();
- }
- if (typeof transformFn === 'function') {
- transformFn(notification);
- }
- this._notifSource.addNotification(notification);
- }
- _updatePrivateModeState() {
- // We hide the history in private mode because it will be out of sync
- // (selected item will not reflect clipboard)
- this.scrollViewMenuSection.actor.visible = !PRIVATE_MODE;
- this.scrollViewFavoritesMenuSection.actor.visible = !PRIVATE_MODE;
- if (PRIVATE_MODE) {
- this.icon.add_style_class_name('private-mode');
- this._updateButtonText();
- } else {
- this.icon.remove_style_class_name('private-mode');
- if (this.currentlySelectedEntry) {
- this._selectEntry(this.currentlySelectedEntry, true);
- } else {
- this._resetSelectedMenuItem(true);
- }
- }
- }
- _fetchSettings() {
- MAX_REGISTRY_LENGTH = this.settings.get_int(SettingsFields.HISTORY_SIZE);
- MAX_BYTES =
- (1 << 20) * this.settings.get_int(SettingsFields.CACHE_FILE_SIZE);
- WINDOW_WIDTH_PERCENTAGE = this.settings.get_int(
- SettingsFields.WINDOW_WIDTH_PERCENTAGE,
- );
- CACHE_ONLY_FAVORITES = this.settings.get_boolean(
- SettingsFields.CACHE_ONLY_FAVORITES,
- );
- MOVE_ITEM_FIRST = this.settings.get_boolean(SettingsFields.MOVE_ITEM_FIRST);
- NOTIFY_ON_COPY = this.settings.get_boolean(SettingsFields.NOTIFY_ON_COPY);
- CONFIRM_ON_CLEAR = this.settings.get_boolean(
- SettingsFields.CONFIRM_ON_CLEAR,
- );
- ENABLE_KEYBINDING = this.settings.get_boolean(
- SettingsFields.ENABLE_KEYBINDING,
- );
- MAX_TOPBAR_LENGTH = this.settings.get_int(
- SettingsFields.TOPBAR_PREVIEW_SIZE,
- );
- TOPBAR_DISPLAY_MODE = this.settings.get_int(
- SettingsFields.TOPBAR_DISPLAY_MODE_ID,
- );
- DISABLE_DOWN_ARROW = this.settings.get_boolean(
- SettingsFields.DISABLE_DOWN_ARROW,
- );
- STRIP_TEXT = this.settings.get_boolean(SettingsFields.STRIP_TEXT);
- PRIVATE_MODE = this.settings.get_boolean(SettingsFields.PRIVATE_MODE);
- PASTE_ON_SELECTION = this.settings.get_boolean(
- SettingsFields.PASTE_ON_SELECTION,
- );
- PROCESS_PRIMARY_SELECTION = this.settings.get_boolean(
- SettingsFields.PROCESS_PRIMARY_SELECTION,
- );
- IGNORE_PASSWORD_MIMES = this.settings.get_boolean(
- SettingsFields.IGNORE_PASSWORD_MIMES,
- );
- }
- _onSettingsChange() {
- const prevCacheOnlyFavorites = CACHE_ONLY_FAVORITES;
- const prevPrivateMode = PRIVATE_MODE;
- this._fetchSettings();
- if (
- prevCacheOnlyFavorites !== undefined &&
- CACHE_ONLY_FAVORITES !== prevCacheOnlyFavorites
- ) {
- if (CACHE_ONLY_FAVORITES) {
- Store.resetDatabase(this._currentStateBuilder.bind(this));
- } else {
- for (const entry of this.entries) {
- entry.diskId = this.nextDiskId++;
- Store.storeTextEntry(entry.text);
- }
- }
- }
- if (prevPrivateMode !== undefined && PRIVATE_MODE !== prevPrivateMode) {
- this._updatePrivateModeState();
- }
- // Remove old entries in case the registry size changed
- this._pruneOldestEntries();
- // Re-set menu-items labels in case preview size changed
- const resetLabel = (item) => this._setEntryLabel(item);
- this.favoritesSection._getMenuItems().forEach(resetLabel);
- this.historySection._getMenuItems().forEach(resetLabel);
- this._updateTopbarLayout();
- if (this.currentlySelectedEntry) {
- this._updateButtonText(this.currentlySelectedEntry);
- }
- this._setMenuWidth();
- if (ENABLE_KEYBINDING) {
- this._bindShortcuts();
- } else {
- this._unbindShortcuts();
- }
- }
- _bindShortcuts() {
- this._unbindShortcuts();
- this._bindShortcut(SETTING_KEY_CLEAR_HISTORY, () => {
- if (this.entries) {
- this._removeAll();
- }
- });
- this._bindShortcut(SETTING_KEY_PREV_ENTRY, () => {
- if (this.entries) {
- this._previousEntry();
- }
- });
- this._bindShortcut(SETTING_KEY_NEXT_ENTRY, () => {
- if (this.entries) {
- this._nextEntry();
- }
- });
- this._bindShortcut(SETTING_KEY_TOGGLE_MENU, () => this.menu.toggle());
- this._bindShortcut(SETTING_KEY_PRIVATE_MODE, () =>
- this.privateModeMenuItem.toggle(),
- );
- }
- _unbindShortcuts() {
- this._shortcutsBindingIds.forEach((id) => Main.wm.removeKeybinding(id));
- this._shortcutsBindingIds = [];
- }
- _bindShortcut(name, cb) {
- const ModeType = Shell.hasOwnProperty('ActionMode')
- ? Shell.ActionMode
- : Shell.KeyBindingMode;
- Main.wm.addKeybinding(
- name,
- this.settings,
- Meta.KeyBindingFlags.NONE,
- ModeType.ALL,
- cb.bind(this),
- );
- this._shortcutsBindingIds.push(name);
- }
- _updateTopbarLayout() {
- if (TOPBAR_DISPLAY_MODE === 3) {
- this.icon.visible = false;
- this._buttonText.visible = false;
- this._style_class = this.style_class;
- this.style_class = '';
- } else if (this._style_class) {
- this.style_class = this._style_class;
- }
- if (TOPBAR_DISPLAY_MODE === 0) {
- this.icon.visible = true;
- this._buttonText.visible = false;
- }
- if (TOPBAR_DISPLAY_MODE === 1) {
- this.icon.visible = false;
- this._buttonText.visible = true;
- }
- if (TOPBAR_DISPLAY_MODE === 2) {
- this.icon.visible = true;
- this._buttonText.visible = true;
- }
- this._downArrow.visible = !DISABLE_DOWN_ARROW;
- }
- _disconnectSettings() {
- if (!this._settingsChangedId) {
- return;
- }
- this.settings.disconnect(this._settingsChangedId);
- this._settingsChangedId = undefined;
- }
- _openSettings() {
- this.extension.openPreferences();
- this.menu.close();
- }
- _previousEntry() {
- this._selectNextPrevEntry(
- this.currentlySelectedEntry.nextCyclic() || this.entries.head,
- );
- }
- _nextEntry() {
- this._selectNextPrevEntry(
- this.currentlySelectedEntry.prevCyclic() || this.entries.last(),
- );
- }
- _selectNextPrevEntry(entry) {
- if (!entry) {
- return;
- }
- this._selectEntry(entry, true);
- if (entry.type === DS.TYPE_TEXT) {
- this._showNotification(_('Copied'), entry.text);
- }
- }
- _truncated(s, start, end) {
- if (start < 0) {
- start = 0;
- }
- if (!end) {
- end = start;
- start = 0;
- }
- if (end > s.length) {
- end = s.length;
- }
- const includesStart = start === 0;
- const includesEnd = end === s.length;
- const isMiddle = !includesStart && !includesEnd;
- const length = end - start;
- const overflow = s.length > length;
- // Reduce regex search space. If the string is mostly whitespace,
- // we might end up removing too many characters, but oh well.
- s = s.substring(start, end + 100);
- // Remove new lines and extra spaces so the text fits nicely on one line
- s = s.replace(/\s+/g, ' ').trim();
- if (includesStart && overflow) {
- s = s.substring(0, length - 1) + '…';
- }
- if (includesEnd && overflow) {
- s = '…' + s.substring(1, length);
- }
- if (isMiddle) {
- s = '…' + s.substring(1, length - 1) + '…';
- }
- return s;
- }
- }
- const ClipboardIndicatorObj = GObject.registerClass(ClipboardIndicator);
- export default class ClipboardHistoryExtension extends Extension {
- enable() {
- this.indicatorName = `${this.metadata.name} Indicator`;
- Store.init(this.uuid);
- this.clipboardIndicator = new ClipboardIndicatorObj(this);
- Main.panel.addToStatusArea(this.indicatorName, this.clipboardIndicator, 1);
- }
- disable() {
- this.clipboardIndicator.destroy();
- this.clipboardIndicator = undefined;
- Store.destroy();
- }
- }
|