diff --git a/README.rst b/README.rst index 4fc78b63..9a26a27d 100644 --- a/README.rst +++ b/README.rst @@ -8,7 +8,7 @@ :alt: Get it on GNOME Extensions The most popular, reliable and feature-rich clipboard manager for GNOME with -over **1M** downloads. +over **2M** downloads. |Screenshot| diff --git a/clipboard-indicator.pot b/clipboard-indicator.pot index b10fb338..ad0783f7 100644 --- a/clipboard-indicator.pot +++ b/clipboard-indicator.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-21 16:41+0300\n" +"POT-Creation-Date: 2026-01-23 20:53+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,214 +17,246 @@ msgstr "" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" -#: extension.js:108 +#: extension.js:113 msgid "Text will be here" msgstr "" -#: extension.js:181 +#: extension.js:187 msgid "Type here to search..." msgstr "" -#: extension.js:250 prefs.js:220 +#: extension.js:256 prefs.js:257 msgid "Private mode" msgstr "" -#: extension.js:264 prefs.js:222 +#: extension.js:270 prefs.js:259 msgid "Clear history" msgstr "" -#: extension.js:276 +#: extension.js:315 msgid "Settings" msgstr "" -#: extension.js:299 +#: extension.js:338 msgid "Clipboard is empty" msgstr "" -#: extension.js:571 +#: extension.js:626 msgid "Clear all?" msgstr "" -#: extension.js:572 +#: extension.js:627 msgid "Are you sure you want to delete all clipboard items?" msgstr "" -#: extension.js:573 +#: extension.js:628 msgid "This operation cannot be undone." msgstr "" -#: extension.js:575 +#: extension.js:630 msgid "Clear" msgstr "" -#: extension.js:575 extension.js:740 +#: extension.js:630 extension.js:801 msgid "Cancel" msgstr "" -#: extension.js:588 +#: extension.js:645 msgid "Clipboard history cleared" msgstr "" -#: extension.js:739 +#: extension.js:648 +msgid "Clipboard history cleared automatically" +msgstr "" + +#: extension.js:800 msgid "Copied to clipboard" msgstr "" -#: prefs.js:30 +#: prefs.js:31 msgid "History Size" msgstr "" -#: prefs.js:39 +#: prefs.js:40 msgid "Preview Size (characters)" msgstr "" -#: prefs.js:48 +#: prefs.js:49 msgid "Max cache file size (MB)" msgstr "" -#: prefs.js:57 +#: prefs.js:58 msgid "Number of characters in top bar" msgstr "" -#: prefs.js:66 +#: prefs.js:67 msgid "What to show in top bar" msgstr "" -#: prefs.js:71 +#: prefs.js:72 msgid "Remove down arrow in top bar" msgstr "" -#: prefs.js:75 +#: prefs.js:76 msgid "Cache only pinned items" msgstr "" -#: prefs.js:79 +#: prefs.js:80 msgid "Show notification on copy" msgstr "" -#: prefs.js:83 +#: prefs.js:84 msgid "Show notification on cycle" msgstr "" -#: prefs.js:87 +#: prefs.js:88 msgid "Show confirmation on Clear History" msgstr "" -#: prefs.js:91 +#: prefs.js:92 msgid "Remove whitespace around text" msgstr "" -#: prefs.js:95 +#: prefs.js:96 msgid "Move item to the top after selection" msgstr "" -#: prefs.js:99 +#: prefs.js:100 msgid "Keep selected entry after Clear History" msgstr "" -#: prefs.js:103 +#: prefs.js:104 msgid "Show paste buttons" msgstr "" -#: prefs.js:104 +#: prefs.js:105 msgid "Adds a paste button to each entry that lets you paste it directly" msgstr "" -#: prefs.js:108 +#: prefs.js:109 msgid "Place the pinned section on the bottom" msgstr "" -#: prefs.js:109 +#: prefs.js:110 msgid "Requires restarting the extension" msgstr "" -#: prefs.js:113 +#: prefs.js:114 msgid "Clear clipboard history on system reboot" msgstr "" -#: prefs.js:117 +#: prefs.js:118 msgid "Paste on select" msgstr "" -#: prefs.js:121 +#: prefs.js:122 msgid "Cache images" msgstr "" -#: prefs.js:126 +#: prefs.js:127 msgid "Excluded Apps" msgstr "" -#: prefs.js:127 +#: prefs.js:128 msgid "Content copied will not be saved while these apps are in focus" msgstr "" -#: prefs.js:146 +#: prefs.js:139 +msgid "Case-sensitive search" +msgstr "" + +#: prefs.js:143 +msgid "Regular expression matching in search" +msgstr "" + +#: prefs.js:156 +msgid "Clear clipboard history on interval" +msgstr "" + +#: prefs.js:160 +msgid "History clear interval (in minutes)" +msgstr "" + +#: prefs.js:172 msgid "UI" msgstr "" -#: prefs.js:147 +#: prefs.js:173 msgid "Behavior" msgstr "" -#: prefs.js:148 +#: prefs.js:174 msgid "Exclusion" msgstr "" -#: prefs.js:149 +#: prefs.js:175 msgid "Limits" msgstr "" -#: prefs.js:150 +#: prefs.js:176 msgid "Topbar" msgstr "" -#: prefs.js:151 +#: prefs.js:177 msgid "Notifications" msgstr "" -#: prefs.js:152 +#: prefs.js:178 msgid "Shortcuts" msgstr "" -#: prefs.js:207 +#: prefs.js:179 +msgid "Search" +msgstr "" + +#: prefs.js:244 msgid "Icon" msgstr "" -#: prefs.js:208 +#: prefs.js:245 msgid "Clipboard Content" msgstr "" -#: prefs.js:209 +#: prefs.js:246 msgid "Both" msgstr "" -#: prefs.js:210 +#: prefs.js:247 msgid "Neither" msgstr "" -#: prefs.js:221 +#: prefs.js:258 msgid "Toggle the menu" msgstr "" -#: prefs.js:223 +#: prefs.js:260 msgid "Previous entry" msgstr "" -#: prefs.js:224 +#: prefs.js:261 msgid "Next entry" msgstr "" -#: prefs.js:229 +#: prefs.js:266 msgid "Enable shortcuts" msgstr "" -#: prefs.js:254 +#: prefs.js:291 msgid "Disabled" msgstr "" -#: prefs.js:263 +#: prefs.js:300 msgid "Enter shortcut" msgstr "" -#: prefs.js:345 +#: prefs.js:387 msgid "Window class name, e.g. \"KeePassXC\"" msgstr "" + +#: prefs.js:399 +msgid "Choose from installed applications" +msgstr "" + +#: prefs.js:416 +msgid "Search applications..." +msgstr "" diff --git a/extension.js b/extension.js index 27383b11..f1963462 100644 --- a/extension.js +++ b/extension.js @@ -1,1430 +1,1524 @@ -import Clutter from 'gi://Clutter'; -import GObject from 'gi://GObject'; -import Meta from 'gi://Meta'; -import Shell from 'gi://Shell'; -import St from 'gi://St'; - -import * as AnimationUtils from 'resource:///org/gnome/shell/misc/animationUtils.js'; -import * as MessageTray from 'resource:///org/gnome/shell/ui/messageTray.js'; -import * as Main from 'resource:///org/gnome/shell/ui/main.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 { Registry, ClipboardEntry } from './registry.js'; -import { DialogManager } from './confirmDialog.js'; -import { PrefsFields } from './constants.js'; -import { Keyboard } from './keyboard.js'; - -const CLIPBOARD_TYPE = St.ClipboardType.CLIPBOARD; - -const INDICATOR_ICON = 'edit-paste-symbolic'; - -let DELAYED_SELECTION_TIMEOUT = 750; -let MAX_REGISTRY_LENGTH = 15; -let MAX_ENTRY_LENGTH = 50; -let CACHE_ONLY_FAVORITE = false; -let DELETE_ENABLED = true; -let MOVE_ITEM_FIRST = false; -let ENABLE_KEYBINDING = true; -let PRIVATEMODE = false; -let NOTIFY_ON_COPY = true; -let NOTIFY_ON_CYCLE = true; -let CONFIRM_ON_CLEAR = true; -let MAX_TOPBAR_LENGTH = 15; -let TOPBAR_DISPLAY_MODE = 1; //0 - only icon, 1 - only clipboard content, 2 - both, 3 - neither -let CLEAR_ON_BOOT = false; -let PASTE_ON_SELECT = false; -let DISABLE_DOWN_ARROW = false; -let STRIP_TEXT = false; -let KEEP_SELECTED_ON_CLEAR = false; -let PASTE_BUTTON = true; -let PINNED_ON_BOTTOM = false; -let CACHE_IMAGES = true; -let EXCLUDED_APPS = []; -let CLEAR_HISTORY_ON_INTERVAL = false; -let CLEAR_HISTORY_INTERVAL = 60; -let NEXT_HISTORY_CLEAR = -1; -let CASE_SENSITIVE_SEARCH = false; -let REGEX_SEARCH = false; - -export default class ClipboardIndicatorExtension extends Extension { - enable () { - this.clipboardIndicator = new ClipboardIndicator({ - clipboard: St.Clipboard.get_default(), - settings: this.getSettings(), - openSettings: this.openPreferences, - uuid: this.uuid - }); - - Main.panel.addToStatusArea('clipboardIndicator', this.clipboardIndicator, 1); - } - - disable () { - this.clipboardIndicator.destroy(); - this.clipboardIndicator = null; - EXCLUDED_APPS = []; - } -} - -const ClipboardIndicator = GObject.registerClass({ - GTypeName: 'ClipboardIndicator' -}, class ClipboardIndicator extends PanelMenu.Button { - #refreshInProgress = false; - - destroy () { - this._disconnectSettings(); - this._unbindShortcuts(); - this._disconnectSelectionListener(); - this._clearDelayedSelectionTimeout(); - this.#clearTimeouts(); - this.dialogManager.destroy(); - this.keyboard.destroy(); - - super.destroy(); - } - - _init (extension) { - super._init(0.0, "ClipboardIndicator"); - this.extension = extension; - this.registry = new Registry(extension); - this.keyboard = new Keyboard(); - this._settingsChangedId = null; - this._selectionOwnerChangedId = null; - this._historyLabel = null; - this._buttonText = null; - this._disableDownArrow = null; - - this._shortcutsBindingIds = []; - this.clipItemsRadioGroup = []; - - let hbox = new St.BoxLayout({ - style_class: 'panel-status-menu-box clipboard-indicator-hbox' - }); - - this.hbox = hbox; - - this.icon = new St.Icon({ - icon_name: INDICATOR_ICON, - style_class: 'system-status-icon clipboard-indicator-icon' - }); - - this._buttonText = new St.Label({ - text: _('Text will be here'), - y_align: Clutter.ActorAlign.CENTER - }); - - this._buttonImgPreview = new St.Bin({ - style_class: 'clipboard-indicator-topbar-preview' - }); - - hbox.add_child(this.icon); - hbox.add_child(this._buttonText); - hbox.add_child(this._buttonImgPreview); - this._downArrow = PopupMenu.arrowIcon(St.Side.BOTTOM); - hbox.add_child(this._downArrow); - this.add_child(hbox); - this._createHistoryLabel(); - this._loadSettings(); - - if (CLEAR_ON_BOOT) this.registry.clearCacheFolder(); - - this.dialogManager = new DialogManager(); - this._buildMenu().then(() => { - this._updateTopbarLayout(); - this._setupListener(); - this._setupHistoryIntervalClearing(); - }); - } - - #updateIndicatorContent(entry) { - if (this.preventIndicatorUpdate || (TOPBAR_DISPLAY_MODE !== 1 && TOPBAR_DISPLAY_MODE !== 2)) { - return; - } - - if (!entry || PRIVATEMODE) { - this._buttonImgPreview.destroy_all_children(); - this._buttonText.set_text("...") - } else { - if (entry.isText()) { - this._buttonText.set_text(this._truncate(entry.getStringValue(), MAX_TOPBAR_LENGTH)); - this._buttonImgPreview.destroy_all_children(); - } - else if (entry.isImage()) { - this._buttonText.set_text(''); - this._buttonImgPreview.destroy_all_children(); - this.registry.getEntryAsImage(entry).then(img => { - img.add_style_class_name('clipboard-indicator-img-preview'); - img.y_align = Clutter.ActorAlign.CENTER; - - // icon only renders properly in setTimeout for some arcane reason - this._imagePreviewTimeout = setTimeout(() => { - this._buttonImgPreview.set_child(img); - }, 0); - }); - } - } - } - - async _buildMenu () { - let that = this; - const clipHistory = await this._getCache(); - let lastIdx = clipHistory.length - 1; - let clipItemsArr = that.clipItemsRadioGroup; - - /* This create the search entry, which is add to a menuItem. - The searchEntry is connected to the function for research. - The menu itself is connected to some shitty hack in order to - grab the focus of the keyboard. */ - that._entryItem = new PopupMenu.PopupBaseMenuItem({ - reactive: false, - can_focus: false - }); - that.searchEntry = new St.Entry({ - name: 'searchEntry', - style_class: 'search-entry', - can_focus: true, - hint_text: _('Type here to search...'), - track_hover: true, - x_expand: true, - y_expand: true, - primary_icon: new St.Icon({ icon_name: 'edit-find-symbolic' }) - }); - - that.searchEntry.get_clutter_text().connect( - 'text-changed', - that._onSearchTextChanged.bind(that) - ); - - that._entryItem.add_child(that.searchEntry); - - that.menu.connect('open-state-changed', (self, open) => { - this._setFocusOnOpenTimeout = setTimeout(() => { - if (open) { - if (this.clipItemsRadioGroup.length > 0) { - that.searchEntry.set_text(''); - global.stage.set_key_focus(that.searchEntry); - } - else { - global.stage.set_key_focus(that.privateModeMenuItem); - } - } - }, 50); - }); - - // Create menu sections for items - // Favorites - that.favoritesSection = new PopupMenu.PopupMenuSection(); - - that.scrollViewFavoritesMenuSection = new PopupMenu.PopupMenuSection(); - this.favoritesScrollView = new St.ScrollView({ - style_class: 'ci-history-menu-section', - overlay_scrollbars: true - }); - this.favoritesScrollView.add_child(that.favoritesSection.actor); - - that.scrollViewFavoritesMenuSection.actor.add_child(this.favoritesScrollView); - this.favoritesSeparator = new PopupMenu.PopupSeparatorMenuItem(); - - // History - that.historySection = new PopupMenu.PopupMenuSection(); - - that.scrollViewMenuSection = new PopupMenu.PopupMenuSection(); - this.historyScrollView = new St.ScrollView({ - style_class: 'ci-main-menu-section ci-history-menu-section', - overlay_scrollbars: true - }); - this.historyScrollView.add_child(that.historySection.actor); - - that.scrollViewMenuSection.actor.add_child(this.historyScrollView); - - // Add separator - this.historySeparator = new PopupMenu.PopupSeparatorMenuItem(); - - // Add sections ordered according to settings - if (PINNED_ON_BOTTOM) { - that.menu.addMenuItem(that.scrollViewMenuSection); - that.menu.addMenuItem(that.scrollViewFavoritesMenuSection); - } - else { - that.menu.addMenuItem(that.scrollViewFavoritesMenuSection); - that.menu.addMenuItem(that.scrollViewMenuSection); - } - - // Private mode switch - that.privateModeMenuItem = new PopupMenu.PopupSwitchMenuItem( - _("Private mode"), PRIVATEMODE, { reactive: true }); - that.privateModeMenuItem.connect('toggled', - that._onPrivateModeSwitch.bind(that)); - that.privateModeMenuItem.insert_child_at_index( - new St.Icon({ - icon_name: 'security-medium-symbolic', - style_class: 'clipboard-menu-icon', - y_align: Clutter.ActorAlign.CENTER - }), - 0 - ); - that.menu.addMenuItem(that.privateModeMenuItem); - - // Add 'Clear' button which removes all items from cache - this.clearMenuItem = new PopupMenu.PopupMenuItem(_('Clear history')); - this.clearMenuItem.insert_child_at_index( - new St.Icon({ - icon_name: 'user-trash-symbolic', - style_class: 'clipboard-menu-icon', - y_align: Clutter.ActorAlign.CENTER - }), - 0 - ); - - let timerBox = new St.BoxLayout({ - x_align: Clutter.ActorAlign.END, - x_expand: true - }); - - this.timerLabel = new St.Label({ - text: '', - style: 'font-family: monospace;', - x_align: Clutter.ActorAlign.END, - x_expand: true - }); - - this.resetTimerButton = new St.Button({ - style_class: 'ci-action-btn', - can_focus: true, - child: new St.Icon({ - icon_name: 'view-refresh-symbolic', - style_class: 'system-status-icon', - icon_size: 14 - }), - x_align: Clutter.ActorAlign.END, - y_align: Clutter.ActorAlign.CENTER, - }); - - this.resetTimerButton.connect('clicked', () => { - this._scheduleNextHistoryClear(); - }); - - timerBox.add_child(this.timerLabel); - timerBox.add_child(this.resetTimerButton); - this.clearMenuItem.add_child(timerBox); - - this.clearMenuItem.connect('activate', that._removeAll.bind(that)); - - // Add 'Settings' menu item to open settings - this.settingsMenuItem = new PopupMenu.PopupMenuItem(_('Settings')); - this.settingsMenuItem.insert_child_at_index( - new St.Icon({ - icon_name: 'preferences-system-symbolic', - style_class: 'clipboard-menu-icon', - y_align: Clutter.ActorAlign.CENTER - }), - 0 - ); - that.menu.addMenuItem(this.settingsMenuItem); - this.settingsMenuItem.connect('activate', that._openSettings.bind(that)); - - // Empty state section - this.emptyStateSection = new St.BoxLayout({ - style_class: 'clipboard-indicator-empty-state', - vertical: true - }); - this.emptyStateSection.add_child(new St.Icon({ - icon_name: INDICATOR_ICON, - style_class: 'system-status-icon clipboard-indicator-icon', - x_align: Clutter.ActorAlign.CENTER - })); - this.emptyStateSection.add_child(new St.Label({ - text: _('Clipboard is empty'), - x_align: Clutter.ActorAlign.CENTER - })); - - // Add cached items - clipHistory.forEach(entry => this._addEntry(entry)); - - if (lastIdx >= 0) { - that._selectMenuItem(clipItemsArr[lastIdx]); - } - - this.#showElements(); - } - - #hideElements() { - if (this.menu.box.contains(this._entryItem)) this.menu.box.remove_child(this._entryItem); - if (this.menu.box.contains(this.favoritesSeparator)) this.menu.box.remove_child(this.favoritesSeparator); - if (this.menu.box.contains(this.historySeparator)) this.menu.box.remove_child(this.historySeparator); - if (this.menu.box.contains(this.clearMenuItem)) this.menu.box.remove_child(this.clearMenuItem); - if (this.menu.box.contains(this.emptyStateSection)) this.menu.box.remove_child(this.emptyStateSection); - } - - #showElements() { - if (this.clipItemsRadioGroup.length > 0) { - if (this.menu.box.contains(this._entryItem) === false) { - this.menu.box.insert_child_at_index(this._entryItem, 0); - } - if (this.menu.box.contains(this.clearMenuItem) === false) { - this.menu.box.insert_child_below(this.clearMenuItem, this.settingsMenuItem); - } - if (this.menu.box.contains(this.emptyStateSection) === true) { - this.menu.box.remove_child(this.emptyStateSection); - } - - if (this.favoritesSection._getMenuItems().length > 0) { - if (this.menu.box.contains(this.favoritesSeparator) === false) { - this.menu.box.insert_child_above(this.favoritesSeparator, this.scrollViewFavoritesMenuSection.actor); - } - } - else if (this.menu.box.contains(this.favoritesSeparator) === true) { - this.menu.box.remove_child(this.favoritesSeparator); - } - - if (this.historySection._getMenuItems().length > 0) { - if (this.menu.box.contains(this.historySeparator) === false) { - this.menu.box.insert_child_above(this.historySeparator, this.scrollViewMenuSection.actor); - } - } - else if (this.menu.box.contains(this.historySeparator) === true) { - this.menu.box.remove_child(this.historySeparator); - } - } - else if (this.menu.box.contains(this.emptyStateSection) === false) { - this.#renderEmptyState(); - } - } - - #renderEmptyState () { - this.#hideElements(); - this.menu.box.insert_child_at_index(this.emptyStateSection, 0); - } - - /* When text change, this function will check, for each item of the - historySection and favoritesSestion, if it should be visible or not (based on words contained - in the clipContents attribute of the item). It doesn't destroy or create - items. It the entry is empty, the section is restored with all items - set as visible. */ - _onSearchTextChanged () { - - // Text to be searched converted to lowercase if search is case insensitive - let searchedText = this.searchEntry.get_text(); - if (!CASE_SENSITIVE_SEARCH) searchedText = searchedText.toLowerCase(); - - if(searchedText === '') { - this._getAllIMenuItems().forEach(function(mItem){ - mItem.actor.visible = true; - }); - } - else { - this._getAllIMenuItems().forEach(function(mItem){ - // Clip content converted to lowercase if search is case insensitive - let text = mItem.clipContents; - if (!CASE_SENSITIVE_SEARCH) text = text.toLowerCase(); - - let isMatching = false; - if (REGEX_SEARCH){ - /* Regex flags: - - 'm' for multiline matching (when multiline content is copied) - - 'i' for case insensitive matching when search is not set to case sensitive - */ - let text_regex = new RegExp(searchedText, 'm' + (CASE_SENSITIVE_SEARCH ? '' : 'i')); - isMatching = text_regex.test(text); - }else{ - isMatching = text.indexOf(searchedText) >= 0; - } - mItem.actor.visible = isMatching - }); - } - } - - _truncate (string, length) { - let shortened = string.replace(/\s+/g, ' '); - - let chars = [...shortened] - if (chars.length > length) - shortened = chars.slice(0, length - 1).join('') + '...'; - - return shortened; - } - - _setEntryLabel (menuItem) { - const { entry } = menuItem; - if (entry.isText()) { - menuItem.label.set_text(this._truncate(entry.getStringValue(), MAX_ENTRY_LENGTH)); - } - else if (entry.isImage()) { - this.registry.getEntryAsImage(entry).then(img => { - img.add_style_class_name('clipboard-menu-img-preview'); - if (menuItem.previewImage) { - menuItem.remove_child(menuItem.previewImage); - } - menuItem.previewImage = img; - menuItem.insert_child_below(img, menuItem.label); - }); - } - } - - _findNextMenuItem (currentMenutItem) { - let currentIndex = this.clipItemsRadioGroup.indexOf(currentMenutItem); - - // for only one item - if(this.clipItemsRadioGroup.length === 1) { - return null; - } - - // when focus is in middle of the displayed list - for (let i = currentIndex - 1; i >= 0; i--) { - let menuItem = this.clipItemsRadioGroup[i]; - if (menuItem.actor.visible) { - return menuItem; - } - } - - // when focus is at the last element of the displayed list - let beforeMenuItem = this.clipItemsRadioGroup[currentIndex + 1]; - if(beforeMenuItem.actor.visible){ - return beforeMenuItem; - } - - return null; - } - - #selectNextMenuItem (menuItem) { - let nextMenuItem = this._findNextMenuItem(menuItem); - - if (nextMenuItem) { - nextMenuItem.actor.grab_key_focus(); - } else { - this.privateModeMenuItem.actor.grab_key_focus(); - } - } - - _addEntry (entry, autoSelect, autoSetClip) { - let menuItem = new PopupMenu.PopupMenuItem(''); - - menuItem.menu = this.menu; - menuItem.entry = entry; - menuItem.clipContents = entry.getStringValue(); - menuItem.radioGroup = this.clipItemsRadioGroup; - menuItem.buttonPressId = menuItem.connect('activate', - autoSet => this._onMenuItemSelectedAndMenuClose(menuItem, autoSet)); - menuItem.connect('key-focus-in', () => { - const viewToScroll = menuItem.entry.isFavorite() ? - this.favoritesScrollView : this.historyScrollView; - AnimationUtils.ensureActorVisibleInScrollView(viewToScroll, menuItem); - }); - menuItem.actor.connect('key-press-event', (actor, event) => { - switch (event.get_key_symbol()) { - case Clutter.KEY_Delete: - this.#selectNextMenuItem(menuItem); - this._removeEntry(menuItem, 'delete'); - break; - case Clutter.KEY_p: - this.#selectNextMenuItem(menuItem); - this._favoriteToggle(menuItem); - break; - case Clutter.KEY_v: - this.#pasteItem(menuItem); - break; - case Clutter.KEY_KP_Enter: - case Clutter.KEY_Return: - if (PASTE_ON_SELECT) { - this.#pasteItem(menuItem); - } - this._onMenuItemSelectedAndMenuClose(menuItem, true); - break; - } - }) - - this._setEntryLabel(menuItem); - this.clipItemsRadioGroup.push(menuItem); - - // Favorite button - let iconfav = new St.Icon({ - icon_name: 'view-pin-symbolic', - style_class: 'system-status-icon' - }); - - let icofavBtn = new St.Button({ - style_class: 'ci-pin-btn ci-action-btn', - can_focus: true, - child: iconfav, - x_align: Clutter.ActorAlign.END, - x_expand: true, - y_expand: true - }); - - menuItem.actor.add_child(icofavBtn); - menuItem.icofavBtn = icofavBtn; - menuItem.favoritePressId = icofavBtn.connect('clicked', - () => this._favoriteToggle(menuItem) - ); - - // Paste button - menuItem.pasteBtn = new St.Button({ - style_class: 'ci-action-btn', - can_focus: true, - child: new St.Icon({ - icon_name: 'edit-paste-symbolic', - style_class: 'system-status-icon' - }), - x_align: Clutter.ActorAlign.END, - x_expand: false, - y_expand: true, - visible: PASTE_BUTTON - }); - - menuItem.pasteBtn.connect('clicked', - () => this.#pasteItem(menuItem) - ); - - menuItem.actor.add_child(menuItem.pasteBtn); - - // Delete button - let icon = new St.Icon({ - icon_name: 'edit-delete-symbolic', //'mail-attachment-symbolic', - style_class: 'system-status-icon' - }); - - let 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); - menuItem.icoBtn = icoBtn; - menuItem.deletePressId = icoBtn.connect('clicked', - () => this._removeEntry(menuItem, 'delete') - ); - - if (entry.isFavorite()) { - this.favoritesSection.addMenuItem(menuItem, 0); - } else { - this.historySection.addMenuItem(menuItem, 0); - } - - if (autoSelect === true) { - this._selectMenuItem(menuItem, autoSetClip); - } - else { - menuItem.setOrnament(PopupMenu.Ornament.NONE); - } - - this.#showElements(); - } - - _favoriteToggle (menuItem) { - menuItem.entry.favorite = menuItem.entry.isFavorite() ? false : true; - this._moveItemFirst(menuItem); - this._updateCache(); - this.#showElements(); - } - - _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."); - - this.dialogManager.open(title, message, sub_message, _("Clear"), _("Cancel"), () => { - this._clearHistory(); - } - ); - } - - _clearHistory (invokedAutomatically = false) { - // Don't remove pinned items - this.historySection._getMenuItems().forEach(mItem => { - if (KEEP_SELECTED_ON_CLEAR === false || !mItem.currentlySelected) { - this._removeEntry(mItem, 'delete'); - } - }); - - if (!invokedAutomatically) { - this._showNotification(_("Clipboard history cleared")); - } - else { - this._showNotification(_("Clipboard history cleared automatically")); - } - } - - _removeAll () { - if (PRIVATEMODE) return; - var that = this; - - if (CONFIRM_ON_CLEAR) { - that._confirmRemoveAll(); - } else { - that._clearHistory(); - } - } - - _removeEntry (menuItem, event) { - let itemIdx = this.clipItemsRadioGroup.indexOf(menuItem); - - if(event === 'delete' && menuItem.currentlySelected) { - this.#clearClipboard(); - } - - menuItem.destroy(); - this.clipItemsRadioGroup.splice(itemIdx,1); - - if (menuItem.entry.isImage()) { - this.registry.deleteEntryFile(menuItem.entry); - } - - this._updateCache(); - this.#showElements(); - } - - _removeOldestEntries () { - let that = this; - - let clipItemsRadioGroupNoFavorite = that.clipItemsRadioGroup.filter( - item => item.entry.isFavorite() === false); - - const origSize = clipItemsRadioGroupNoFavorite.length; - - while (clipItemsRadioGroupNoFavorite.length > MAX_REGISTRY_LENGTH) { - let oldestNoFavorite = clipItemsRadioGroupNoFavorite.shift(); - that._removeEntry(oldestNoFavorite); - - clipItemsRadioGroupNoFavorite = that.clipItemsRadioGroup.filter( - item => item.entry.isFavorite() === false); - } - - if (clipItemsRadioGroupNoFavorite.length < origSize) { - that._updateCache(); - } - } - - _onMenuItemSelected (menuItem, autoSet) { - for (let otherMenuItem of menuItem.radioGroup) { - let clipContents = menuItem.clipContents; - - if (otherMenuItem === menuItem && clipContents) { - menuItem.setOrnament(PopupMenu.Ornament.DOT); - menuItem.currentlySelected = true; - if (autoSet !== false) - this.#updateClipboard(menuItem.entry); - } - else { - otherMenuItem.setOrnament(PopupMenu.Ornament.NONE); - otherMenuItem.currentlySelected = false; - } - } - } - - _selectMenuItem (menuItem, autoSet) { - this._onMenuItemSelected(menuItem, autoSet); - this.#updateIndicatorContent(menuItem.entry); - } - - _onMenuItemSelectedAndMenuClose (menuItem, autoSet) { - for (let otherMenuItem of menuItem.radioGroup) { - let clipContents = menuItem.clipContents; - - if (menuItem === otherMenuItem && clipContents) { - menuItem.setOrnament(PopupMenu.Ornament.DOT); - menuItem.currentlySelected = true; - if (autoSet !== false) - this.#updateClipboard(menuItem.entry); - } - else { - otherMenuItem.setOrnament(PopupMenu.Ornament.NONE); - otherMenuItem.currentlySelected = false; - } - } - - menuItem.menu.close(); - } - - _getCache () { - return this.registry.read(); - } - - #addToCache (entry) { - const entries = this.clipItemsRadioGroup - .map(menuItem => menuItem.entry) - .filter(entry => CACHE_ONLY_FAVORITE == false || entry.isFavorite()) - .concat([entry]); - this.registry.write(entries); - } - - _updateCache () { - const entries = this.clipItemsRadioGroup - .map(menuItem => menuItem.entry) - .filter(entry => CACHE_ONLY_FAVORITE == false || entry.isFavorite()); - - this.registry.write(entries); - } - - async _onSelectionChange (selection, selectionType, selectionSource) { - if (selectionType === Meta.SelectionType.SELECTION_CLIPBOARD) { - this._refreshIndicator(); - } - } - - async _refreshIndicator () { - if (PRIVATEMODE) return; // Private mode, do not. - - const focussedWindow = Shell.Global.get().display.focusWindow; - const wmClass = focussedWindow?.get_wm_class(); - - if (wmClass && EXCLUDED_APPS.includes(wmClass)) return; // Excluded app, do not. - - if (this.#refreshInProgress) return; - this.#refreshInProgress = true; - - try { - const result = await this.#getClipboardContent(); - - if (result) { - for (let menuItem of this.clipItemsRadioGroup) { - if (menuItem.entry.equals(result)) { - this._selectMenuItem(menuItem, false); - - if (!menuItem.entry.isFavorite() && MOVE_ITEM_FIRST) { - this._moveItemFirst(menuItem); - } - - return; - } - } - - this.#addToCache(result); - this._addEntry(result, true, false); - this._removeOldestEntries(); - if (NOTIFY_ON_COPY) { - this._showNotification(_("Copied to clipboard"), notif => { - notif.addAction(_('Cancel'), this._cancelNotification); - }); - } - } - } - catch (e) { - console.error('Clipboard Indicator: Failed to refresh indicator'); - console.error(e); - } - finally { - this.#refreshInProgress = false; - } - } - - _moveItemFirst (item) { - this._removeEntry(item); - this._addEntry(item.entry, item.currentlySelected, false); - this._updateCache(); - } - - _findItem (text) { - return this.clipItemsRadioGroup.filter( - item => item.clipContents === text)[0]; - } - - _getCurrentlySelectedItem () { - return this.clipItemsRadioGroup.find(item => item.currentlySelected); - } - - _getAllIMenuItems () { - return this.historySection._getMenuItems().concat(this.favoritesSection._getMenuItems()); - } - - _setupListener () { - const metaDisplay = Shell.Global.get().get_display(); - const selection = metaDisplay.get_selection(); - this._setupSelectionTracking(selection); - } - - _setupSelectionTracking (selection) { - this.selection = selection; - this._selectionOwnerChangedId = selection.connect('owner-changed', (selection, selectionType, selectionSource) => { - this._onSelectionChange(selection, selectionType, selectionSource); - }); - } - - _setupHistoryIntervalClearing() { - this._fetchSettings(); - - if (this._intervalSettingChangedId) { - this.extension.settings.disconnect(this._intervalSettingChangedId); - this._intervalSettingChangedId = null; - } - if (this._intervalToggleChangedId) { - this.extension.settings.disconnect(this._intervalToggleChangedId); - this._intervalToggleChangedId = null; - } - if (this._historyClearTimeoutId) { - clearTimeout(this._historyClearTimeoutId); - this._historyClearTimeoutId = null; - } - - this._intervalSettingChangedId = this.extension.settings.connect( - `changed::${PrefsFields.CLEAR_HISTORY_INTERVAL}`, - this._onHistoryIntervalClearSettingsChanged.bind(this) - ); - this._intervalToggleChangedId = this.extension.settings.connect( - `changed::${PrefsFields.CLEAR_HISTORY_ON_INTERVAL}`, - this._onHistoryIntervalClearSettingsChanged.bind(this) - ); - - - - if (!CLEAR_HISTORY_ON_INTERVAL) { - this._updateIntervalTimer(); - return; - } - - const currentTime = Math.ceil(new Date().getTime() / 1000); - - if (NEXT_HISTORY_CLEAR === -1) { //new timer - this._scheduleNextHistoryClear(); - } - else if (NEXT_HISTORY_CLEAR < currentTime) { //timer expired - this._clearHistory(true); - this._scheduleNextHistoryClear(); - } - else { //timer already set, but not expired - const timeoutMs = (NEXT_HISTORY_CLEAR - currentTime) * 1000; - this._historyClearTimeoutId = setTimeout(() => { - this._clearHistory(true); - this._scheduleNextHistoryClear(); - }, timeoutMs); - this._timerIntervalId = setInterval(() => { - this._updateIntervalTimer(); - }, 1000); - } - } - - _onHistoryIntervalClearSettingsChanged(_settings, key) { - this._fetchSettings(); - if (key === PrefsFields.CLEAR_HISTORY_INTERVAL) { - this._scheduleNextHistoryClear(); - } - else if (key === PrefsFields.CLEAR_HISTORY_ON_INTERVAL) { - if (CLEAR_HISTORY_ON_INTERVAL) { - this._resetHistoryClearTimer(); - this._setupHistoryIntervalClearing(); - } else { - this._resetHistoryClearTimer(); - } - } - } - - _scheduleNextHistoryClear() { - this._fetchSettings(); - - clearInterval(this._timerIntervalId); - if (this._historyClearTimeoutId) { - clearTimeout(this._historyClearTimeoutId); - this._historyClearTimeoutId = null; - } - - if(!CLEAR_HISTORY_ON_INTERVAL) { - this._resetHistoryClearTimer(); - return; - } - - const currentTime = Math.ceil(new Date().getTime() / 1000); - NEXT_HISTORY_CLEAR = currentTime + CLEAR_HISTORY_INTERVAL * 60; - const timeoutMs = (NEXT_HISTORY_CLEAR - currentTime) * 1000; - - this.extension.settings.set_int(PrefsFields.NEXT_HISTORY_CLEAR, NEXT_HISTORY_CLEAR); - - this._updateIntervalTimer(); - this._timerIntervalId = setInterval(() => { - this._updateIntervalTimer(); - }, 1000); - - this._historyClearTimeoutId = setTimeout(() => { - this._clearHistory(true); - this._scheduleNextHistoryClear(); - }, timeoutMs); - } - - _resetHistoryClearTimer() { - //basically just reset and stop the timer - if (this._historyClearTimeoutId) { - clearTimeout(this._historyClearTimeoutId); - this._historyClearTimeoutId = null; - } - clearInterval(this._timerIntervalId); - this._timerIntervalId = null; - this._updateIntervalTimer(); - this.extension.settings.set_int(PrefsFields.NEXT_HISTORY_CLEAR, -1); - } - - _updateIntervalTimer() { - this._fetchSettings(); - this.resetTimerButton.visible = CLEAR_HISTORY_ON_INTERVAL; - this.timerLabel.visible = CLEAR_HISTORY_ON_INTERVAL; - if (!CLEAR_HISTORY_ON_INTERVAL) return; - - - let currentTime = Math.ceil(new Date().getTime() / 1000); - let timeLeft = NEXT_HISTORY_CLEAR - currentTime; - - if (timeLeft <= 0) { - this.timerLabel.set_text(''); - return; - } - - let hours = Math.floor(timeLeft / 3600); - let minutes = Math.floor((timeLeft % 3600) / 60); - let seconds = Math.floor(timeLeft % 60); - - let formattedTime = ''; - if (hours > 0) { - formattedTime += `${hours}h `; - } - if (minutes > 0) { - formattedTime += `${minutes}m `; - } - formattedTime += `${seconds}s`; - this.timerLabel.set_text(formattedTime); - } - - _openSettings () { - this.extension.openSettings(); - } - - _initNotifSource () { - if (!this._notifSource) { - this._notifSource = new MessageTray.Source({ - title: 'Clipboard Indicator', - 'icon-name': INDICATOR_ICON - }); - - this._notifSource.connect('destroy', () => { - this._notifSource = null; - }); - - Main.messageTray.add(this._notifSource); - } - } - - _cancelNotification () { - if (this.clipItemsRadioGroup.length >= 2) { - let clipSecond = this.clipItemsRadioGroup.length - 2; - let previousClip = this.clipItemsRadioGroup[clipSecond]; - this.#updateClipboard(previousClip.entry); - previousClip.setOrnament(PopupMenu.Ornament.DOT); - previousClip.icoBtn.visible = false; - previousClip.currentlySelected = true; - } else { - this.#clearClipboard(); - } - let clipFirst = this.clipItemsRadioGroup.length - 1; - this._removeEntry(this.clipItemsRadioGroup[clipFirst]); - } - - _showNotification (message, transformFn) { - const dndOn = () => - !Main.panel.statusArea.dateMenu._indicator._settings.get_boolean( - 'show-banners', - ); - if (PRIVATEMODE || dndOn()) { - return; - } - - let notification = null; - - this._initNotifSource(); - - if (this._notifSource.count === 0) { - notification = new MessageTray.Notification({ - source: this._notifSource, - body: message, - 'is-transient': true - }); - } - else { - notification = this._notifSource.notifications[0]; - notification.body = message; - notification.clearActions(); - } - - if (typeof transformFn === 'function') { - transformFn(notification); - } - - this._notifSource.addNotification(notification); - } - - _createHistoryLabel () { - this._historyLabel = new St.Label({ - style_class: 'ci-notification-label', - text: '' - }); - - global.stage.add_child(this._historyLabel); - - this._historyLabel.hide(); - } - - togglePrivateMode () { - this.privateModeMenuItem.toggle(); - } - - _onPrivateModeSwitch () { - let that = this; - PRIVATEMODE = this.privateModeMenuItem.state; - // We hide the history in private ModeTypee because it will be out of sync (selected item will not reflect clipboard) - this.scrollViewMenuSection.actor.visible = !PRIVATEMODE; - this.scrollViewFavoritesMenuSection.actor.visible = !PRIVATEMODE; - // If we get out of private mode then we restore the clipboard to old state - if (!PRIVATEMODE) { - let selectList = this.clipItemsRadioGroup.filter((item) => !!item.currentlySelected); - - if (selectList.length) { - this._selectMenuItem(selectList[0]); - } else { - // Nothing to return to, let's empty it instead - this.#clearClipboard(); - } - - this.#getClipboardContent().then(entry => { - if (!entry) return; - this.#updateIndicatorContent(entry); - }).catch(e => console.error(e)); - - this.hbox.remove_style_class_name('private-mode'); - this.#showElements(); - } else { - this.hbox.add_style_class_name('private-mode'); - this.#updateIndicatorContent(null); - this.#hideElements(); - } - } - - _loadSettings () { - this._settingsChangedId = this.extension.settings.connect('changed', - this._onSettingsChange.bind(this)); - - this._fetchSettings(); - - if (ENABLE_KEYBINDING) - this._bindShortcuts(); - } - - _fetchSettings () { - const { settings } = this.extension; - MAX_REGISTRY_LENGTH = settings.get_int(PrefsFields.HISTORY_SIZE); - MAX_ENTRY_LENGTH = settings.get_int(PrefsFields.PREVIEW_SIZE); - CACHE_ONLY_FAVORITE = settings.get_boolean(PrefsFields.CACHE_ONLY_FAVORITE); - DELETE_ENABLED = settings.get_boolean(PrefsFields.DELETE); - MOVE_ITEM_FIRST = settings.get_boolean(PrefsFields.MOVE_ITEM_FIRST); - NOTIFY_ON_COPY = settings.get_boolean(PrefsFields.NOTIFY_ON_COPY); - NOTIFY_ON_CYCLE = settings.get_boolean(PrefsFields.NOTIFY_ON_CYCLE); - CONFIRM_ON_CLEAR = settings.get_boolean(PrefsFields.CONFIRM_ON_CLEAR); - ENABLE_KEYBINDING = settings.get_boolean(PrefsFields.ENABLE_KEYBINDING); - MAX_TOPBAR_LENGTH = settings.get_int(PrefsFields.TOPBAR_PREVIEW_SIZE); - TOPBAR_DISPLAY_MODE = settings.get_int(PrefsFields.TOPBAR_DISPLAY_MODE_ID); - CLEAR_ON_BOOT = settings.get_boolean(PrefsFields.CLEAR_ON_BOOT); - PASTE_ON_SELECT = settings.get_boolean(PrefsFields.PASTE_ON_SELECT); - DISABLE_DOWN_ARROW = settings.get_boolean(PrefsFields.DISABLE_DOWN_ARROW); - STRIP_TEXT = settings.get_boolean(PrefsFields.STRIP_TEXT); - KEEP_SELECTED_ON_CLEAR = settings.get_boolean(PrefsFields.KEEP_SELECTED_ON_CLEAR); - PASTE_BUTTON = settings.get_boolean(PrefsFields.PASTE_BUTTON); - PINNED_ON_BOTTOM = settings.get_boolean(PrefsFields.PINNED_ON_BOTTOM); - CACHE_IMAGES = settings.get_boolean(PrefsFields.CACHE_IMAGES); - EXCLUDED_APPS = settings.get_strv(PrefsFields.EXCLUDED_APPS); - CLEAR_HISTORY_ON_INTERVAL = settings.get_boolean(PrefsFields.CLEAR_HISTORY_ON_INTERVAL); - CLEAR_HISTORY_INTERVAL = settings.get_int(PrefsFields.CLEAR_HISTORY_INTERVAL); - NEXT_HISTORY_CLEAR = settings.get_int(PrefsFields.NEXT_HISTORY_CLEAR); - CASE_SENSITIVE_SEARCH = settings.get_boolean(PrefsFields.CASE_SENSITIVE_SEARCH); - REGEX_SEARCH = settings.get_boolean(PrefsFields.REGEX_SEARCH); - } - - async _onSettingsChange () { - try { - var that = this; - - // Load the settings into variables - that._fetchSettings(); - - // Remove old entries in case the registry size changed - that._removeOldestEntries(); - - // Re-set menu-items lables in case preview size changed - this._getAllIMenuItems().forEach(function (mItem) { - that._setEntryLabel(mItem); - mItem.pasteBtn.visible = PASTE_BUTTON; - }); - - //update topbar - this._updateTopbarLayout(); - that.#updateIndicatorContent(await this.#getClipboardContent()); - - // Bind or unbind shortcuts - if (ENABLE_KEYBINDING) - that._bindShortcuts(); - else - that._unbindShortcuts(); - } catch (e) { - console.error('Clipboard Indicator: Failed to update registry'); - console.error(e); - } - } - - _bindShortcuts () { - this._unbindShortcuts(); - this._bindShortcut(PrefsFields.BINDING_CLEAR_HISTORY, this._removeAll); - this._bindShortcut(PrefsFields.BINDING_PREV_ENTRY, this._previousEntry); - this._bindShortcut(PrefsFields.BINDING_NEXT_ENTRY, this._nextEntry); - this._bindShortcut(PrefsFields.BINDING_TOGGLE_MENU, this._toggleMenu); - this._bindShortcut(PrefsFields.BINDING_PRIVATE_MODE, this.togglePrivateMode); - } - - _unbindShortcuts () { - this._shortcutsBindingIds.forEach( - (id) => Main.wm.removeKeybinding(id) - ); - - this._shortcutsBindingIds = []; - } - - _bindShortcut (name, cb) { - var ModeType = Shell.hasOwnProperty('ActionMode') ? - Shell.ActionMode : Shell.KeyBindingMode; - - Main.wm.addKeybinding( - name, - this.extension.settings, - Meta.KeyBindingFlags.NONE, - ModeType.ALL, - cb.bind(this) - ); - - this._shortcutsBindingIds.push(name); - } - - _updateTopbarLayout () { - if(TOPBAR_DISPLAY_MODE === 0){ - this.icon.visible = true; - this._buttonText.visible = false; - this._buttonImgPreview.visible = false; - this.show(); - } - if(TOPBAR_DISPLAY_MODE === 1){ - this.icon.visible = false; - this._buttonText.visible = true; - this._buttonImgPreview.visible = true; - this.show(); - } - if(TOPBAR_DISPLAY_MODE === 2){ - this.icon.visible = true; - this._buttonText.visible = true; - this._buttonImgPreview.visible = true; - this.show(); - } - if (TOPBAR_DISPLAY_MODE === 3) { - this.hide(); - } - if(!DISABLE_DOWN_ARROW) { - this._downArrow.visible = true; - } else { - this._downArrow.visible = false; - } - } - - _disconnectSettings () { - if (!this._settingsChangedId) - return; - - this.extension.settings.disconnect(this._settingsChangedId); - this._settingsChangedId = null; - - if (this._intervalSettingChangedId) { - this.extension.settings.disconnect(this._intervalSettingChangedId); - this._intervalSettingChangedId = null; - } - - if (this._intervalToggleChangedId) { - this.extension.settings.disconnect(this._intervalToggleChangedId); - this._intervalToggleChangedId = null; - } - - if (this._historyClearTimeoutId) { - clearTimeout(this._historyClearTimeoutId); - this._historyClearTimeoutId = null; - } - } - - _disconnectSelectionListener () { - if (!this._selectionOwnerChangedId) - return; - - this.selection.disconnect(this._selectionOwnerChangedId); - } - - _clearDelayedSelectionTimeout () { - if (this._delayedSelectionTimeoutId) { - clearInterval(this._delayedSelectionTimeoutId); - } - } - - _selectEntryWithDelay (entry) { - let that = this; - that._selectMenuItem(entry, false); - - that._delayedSelectionTimeoutId = setTimeout(function () { - that._selectMenuItem(entry); //select the item - that._delayedSelectionTimeoutId = null; - }, DELAYED_SELECTION_TIMEOUT); - } - - _previousEntry () { - if (PRIVATEMODE) return; - let that = this; - - that._clearDelayedSelectionTimeout(); - - this._getAllIMenuItems().some(function (mItem, i, menuItems){ - if (mItem.currentlySelected) { - i--; //get the previous index - if (i < 0) i = menuItems.length - 1; //cycle if out of bound - let index = i + 1; //index to be displayed - - if(NOTIFY_ON_CYCLE) { - that._showNotification(index + ' / ' + menuItems.length + ': ' + menuItems[i].entry.getStringValue()); - } - if (MOVE_ITEM_FIRST) { - that._selectEntryWithDelay(menuItems[i]); - } - else { - that._selectMenuItem(menuItems[i]); - } - return true; - } - return false; - }); - } - - _nextEntry () { - if (PRIVATEMODE) return; - let that = this; - - that._clearDelayedSelectionTimeout(); - - this._getAllIMenuItems().some(function (mItem, i, menuItems){ - if (mItem.currentlySelected) { - i++; //get the next index - if (i === menuItems.length) i = 0; //cycle if out of bound - let index = i + 1; //index to be displayed - - if(NOTIFY_ON_CYCLE) { - that._showNotification(index + ' / ' + menuItems.length + ': ' + menuItems[i].entry.getStringValue()); - } - if (MOVE_ITEM_FIRST) { - that._selectEntryWithDelay(menuItems[i]); - } - else { - that._selectMenuItem(menuItems[i]); - } - return true; - } - return false; - }); - } - - _toggleMenu () { - this.menu.toggle(); - } - - #pasteItem (menuItem) { - this.menu.close(); - const currentlySelected = this._getCurrentlySelectedItem(); - this.preventIndicatorUpdate = true; - this.#updateClipboard(menuItem.entry); - this._pastingKeypressTimeout = setTimeout(() => { - if (this.keyboard.purpose === Clutter.InputContentPurpose.TERMINAL) { - this.keyboard.press(Clutter.KEY_Control_L); - this.keyboard.press(Clutter.KEY_Shift_L); - this.keyboard.press(Clutter.KEY_Insert); - this.keyboard.release(Clutter.KEY_Insert); - this.keyboard.release(Clutter.KEY_Shift_L); - this.keyboard.release(Clutter.KEY_Control_L); - } - else { - this.keyboard.press(Clutter.KEY_Shift_L); - this.keyboard.press(Clutter.KEY_Insert); - this.keyboard.release(Clutter.KEY_Insert); - this.keyboard.release(Clutter.KEY_Shift_L); - } - - this._pastingResetTimeout = setTimeout(() => { - this.preventIndicatorUpdate = false; - this.#updateClipboard(currentlySelected.entry); - }, 50); - }, 50); - } - - #clearTimeouts () { - if (this._imagePreviewTimeout) clearTimeout(this._imagePreviewTimeout); - if (this._setFocusOnOpenTimeout) clearTimeout(this._setFocusOnOpenTimeout); - if (this._pastingKeypressTimeout) clearTimeout(this._pastingKeypressTimeout); - if (this._pastingResetTimeout) clearTimeout(this._pastingResetTimeout); - if (this._historyClearTimeoutId) clearTimeout(this._historyClearTimeoutId); - if (this._timerIntervalId) clearInterval(this._timerIntervalId); - } - - #clearClipboard () { - this.extension.clipboard.set_text(CLIPBOARD_TYPE, ""); - this.#updateIndicatorContent(null); - } - - #updateClipboard (entry) { - this.extension.clipboard.set_content(CLIPBOARD_TYPE, entry.mimetype(), entry.asBytes()); - this.#updateIndicatorContent(entry); - } - - async #getClipboardContent () { - const mimetypes = [ - "text/plain;charset=utf-8", - "UTF8_STRING", - "text/plain", - "STRING", - 'image/gif', - 'image/png', - 'image/jpg', - 'image/jpeg', - 'image/webp', - 'image/svg+xml', - 'text/html', - ]; - - for (let type of mimetypes) { - let result = await new Promise(resolve => this.extension.clipboard.get_content(CLIPBOARD_TYPE, type, (clipBoard, bytes) => { - if (bytes === null || bytes.get_size() === 0) { - resolve(null); - return; - } - - // HACK: workaround for GNOME 2nd+ copy mangling mimetypes https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/8233 - // In theory GNOME or XWayland should auto-convert this back to UTF8_STRING for legacy apps when it's needed https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/5300 - if (type === "UTF8_STRING") { - type = "text/plain;charset=utf-8"; - } - - const entry = new ClipboardEntry(type, bytes.get_data(), false); - if (CACHE_IMAGES && entry.isImage()) { - this.registry.writeEntryFile(entry); - } - resolve(entry); - })); - - if (result) { - if (!CACHE_IMAGES && result.isImage()) { - return null; - } - else { - return result; - } - } - } - - return null; - } -}); +import Clutter from 'gi://Clutter'; +import GObject from 'gi://GObject'; +import Gio from 'gi://Gio'; +import Meta from 'gi://Meta'; +import Shell from 'gi://Shell'; +import St from 'gi://St'; + +import * as AnimationUtils from 'resource:///org/gnome/shell/misc/animationUtils.js'; +import * as MessageTray from 'resource:///org/gnome/shell/ui/messageTray.js'; +import * as Main from 'resource:///org/gnome/shell/ui/main.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 { Registry, ClipboardEntry } from './registry.js'; +import { DialogManager } from './confirmDialog.js'; +import { PrefsFields } from './constants.js'; +import { Keyboard } from './keyboard.js'; + +const CLIPBOARD_TYPE = St.ClipboardType.CLIPBOARD; + +const INDICATOR_ICON = 'edit-paste-symbolic'; +const INDICATOR_ICON_FULL = 'edit-pasted-symbolic'; + +const ext = Extension.lookupByURL(import.meta.url); + +let DELAYED_SELECTION_TIMEOUT = 750; +let MAX_REGISTRY_LENGTH = 15; +let MAX_ENTRY_LENGTH = 50; +let CACHE_ONLY_FAVORITE = false; +let DELETE_ENABLED = true; +let MOVE_ITEM_FIRST = false; +let ENABLE_KEYBINDING = true; +let PRIVATEMODE = false; +let NOTIFY_ON_COPY = true; +let NOTIFY_ON_CYCLE = true; +let CONFIRM_ON_CLEAR = true; +let MAX_TOPBAR_LENGTH = 15; +let TOPBAR_DISPLAY_MODE = 1; //0 - only icon, 1 - only clipboard content, 2 - both, 3 - neither +let CLEAR_ON_BOOT = false; +let PASTE_ON_SELECT = false; +let DISABLE_DOWN_ARROW = false; +let STRIP_TEXT = false; +let KEEP_SELECTED_ON_CLEAR = false; +let PASTE_BUTTON = true; +let PINNED_ON_BOTTOM = false; +let CACHE_IMAGES = true; +let EXCLUDED_APPS = []; +let CLEAR_HISTORY_ON_INTERVAL = false; +let CLEAR_HISTORY_INTERVAL = 60; +let NEXT_HISTORY_CLEAR = -1; +let CASE_SENSITIVE_SEARCH = false; +let REGEX_SEARCH = false; + +export default class ClipboardIndicatorExtension extends Extension { + enable () { + this.clipboardIndicator = new ClipboardIndicator({ + clipboard: St.Clipboard.get_default(), + settings: this.getSettings(), + openSettings: this.openPreferences, + uuid: this.uuid + }); + + Main.panel.addToStatusArea('clipboardIndicator', this.clipboardIndicator, 1); + } + + disable () { + this.clipboardIndicator.destroy(); + this.clipboardIndicator = null; + EXCLUDED_APPS = []; + } +} + +const ClipboardIndicator = GObject.registerClass({ + GTypeName: 'ClipboardIndicator' +}, class ClipboardIndicator extends PanelMenu.Button { + #refreshInProgress = false; + + destroy () { + this._disconnectSettings(); + this._unbindShortcuts(); + this._disconnectSelectionListener(); + this._clearDelayedSelectionTimeout(); + this.#clearTimeouts(); + this.dialogManager.destroy(); + this.keyboard.destroy(); + if (this._iconThemeChangedId) { + this._interfaceSettings.disconnect(this._iconThemeChangedId); + + this._iconThemeChangedId = 0; + } + super.destroy(); + } + + _init (extension) { + super._init(0.0, "ClipboardIndicator"); + this.extension = extension; + this.registry = new Registry(extension); + this.keyboard = new Keyboard(); + this._settingsChangedId = null; + this._selectionOwnerChangedId = null; + this._historyLabel = null; + this._buttonText = null; + this._disableDownArrow = null; + + this._shortcutsBindingIds = []; + this.clipItemsRadioGroup = []; + + let hbox = new St.BoxLayout({ + style_class: 'panel-status-menu-box clipboard-indicator-hbox' + }); + + this.hbox = hbox; + + this.icon = new St.Icon({ + icon_name: INDICATOR_ICON, + style_class: 'system-status-icon clipboard-indicator-icon' + }); + + this._interfaceSettings = new Gio.Settings({ + schema_id: 'org.gnome.desktop.interface' + }); + + this._iconThemeChangedId = this._interfaceSettings.connect('changed::icon-theme', () => { + this.#updateIndicatorContent( + this._getCurrentlySelectedItem()?.entry ?? null + ); + }); + + this._buttonText = new St.Label({ + text: _('Text will be here'), + y_align: Clutter.ActorAlign.CENTER + }); + + + this._buttonImgPreview = new St.Bin({ + style_class: 'clipboard-indicator-topbar-preview' + }); + + hbox.add_child(this.icon); + hbox.add_child(this._buttonText); + hbox.add_child(this._buttonImgPreview); + this._downArrow = PopupMenu.arrowIcon(St.Side.BOTTOM); + hbox.add_child(this._downArrow); + this.add_child(hbox); + this._createHistoryLabel(); + this._loadSettings(); + + if (CLEAR_ON_BOOT) this.registry.clearCacheFolder(); + + this.dialogManager = new DialogManager(); + this._buildMenu().then(async() => { + this._updateTopbarLayout(); + this._setupListener(); + this._setupHistoryIntervalClearing(); + const entry = await this.#getClipboardContent(); + this.#updateIndicatorContent(entry); + }); + } + + _getIconThemeName() { + const ext = Extension.lookupByURL(import.meta.url); + const settings = new Gio.Settings({ + schema_id: 'org.gnome.desktop.interface' + }); + return settings.get_string('icon-theme'); + } + + _getExtIcon(iconName) { + const themeName = this._getIconThemeName(); + const ext = Extension.lookupByURL(import.meta.url); + let relativePath; + + if (themeName.startsWith('Adwaita')) { + relativePath = `icons/Adwaita/${iconName}.svg`; + } else if (themeName.startsWith('Flat')) { + relativePath = `icons/Flat/${iconName}.svg`; + } else if (themeName.startsWith('Yaru')) { + relativePath = `icons/Yaru/${iconName}.svg`; + } else { + return null; + } + const file = ext.dir.resolve_relative_path(relativePath); + if (!file.query_exists(null)) { + return null; + } + return new Gio.FileIcon({ file }); + } + + #updateIndicatorContent(entry) { + if (this.preventIndicatorUpdate || !this.icon) return; + const showTopbarContent = (TOPBAR_DISPLAY_MODE === 1 || TOPBAR_DISPLAY_MODE === 2); + if (showTopbarContent) { + this._buttonImgPreview.destroy_all_children(); + this._buttonText.set_text(entry ? this._truncate(entry.getStringValue(), MAX_TOPBAR_LENGTH) : "..."); + } + + const isYaru = this._getIconThemeName().startsWith('Yaru'); + + if (!entry || PRIVATEMODE) { + if (isYaru) { + const gicon = this._getExtIcon(INDICATOR_ICON_FULL); + if (gicon) { + this.icon.set_icon_name(null); + this.icon.set_gicon(gicon); + } else { + this.icon.set_gicon(null); + this.icon.set_icon_name(INDICATOR_ICON); + } + } else { + this.icon.set_gicon(null); + this.icon.set_icon_name(INDICATOR_ICON); + } + } else { + if (isYaru) { + this.icon.set_gicon(null); + this.icon.set_icon_name(INDICATOR_ICON); + } else { + const gicon = this._getExtIcon(INDICATOR_ICON_FULL); + if (gicon) { + this.icon.set_icon_name(null); + this.icon.set_gicon(gicon); + } else { + this.icon.set_gicon(null); + this.icon.set_icon_name(INDICATOR_ICON); + } + } + if (entry.isText()) { + this._buttonText.set_text(this._truncate(entry.getStringValue(), MAX_TOPBAR_LENGTH)); + this._buttonImgPreview.destroy_all_children(); + } + else if (entry.isImage()) { + this._buttonText.set_text(''); + this._buttonImgPreview.destroy_all_children(); + this.registry.getEntryAsImage(entry).then(img => { + img.add_style_class_name('clipboard-indicator-img-preview'); + img.y_align = Clutter.ActorAlign.CENTER; + + // icon only renders properly in setTimeout for some arcane reason + this._imagePreviewTimeout = setTimeout(() => { + this._buttonImgPreview.set_child(img); + }, 0); + }); + } + } + } + + async _buildMenu () { + let that = this; + const clipHistory = await this._getCache(); + let lastIdx = clipHistory.length - 1; + let clipItemsArr = that.clipItemsRadioGroup; + + /* This create the search entry, which is add to a menuItem. + The searchEntry is connected to the function for research. + The menu itself is connected to some shitty hack in order to + grab the focus of the keyboard. */ + that._entryItem = new PopupMenu.PopupBaseMenuItem({ + reactive: false, + can_focus: false + }); + that.searchEntry = new St.Entry({ + name: 'searchEntry', + style_class: 'popup-menu-search-entry', + can_focus: true, + hint_text: _('Type here to search...'), + track_hover: true, + x_expand: true, + y_expand: true, + primary_icon: new St.Icon({ icon_name: 'edit-find-symbolic' }) + }); + + that.searchEntry.get_clutter_text().connect( + 'text-changed', + that._onSearchTextChanged.bind(that) + ); + + that._entryItem.add_child(that.searchEntry); + + that.menu.connect('open-state-changed', (self, open) => { + this._setFocusOnOpenTimeout = setTimeout(() => { + if (open) { + if (this.clipItemsRadioGroup.length > 0) { + that.searchEntry.set_text(''); + global.stage.set_key_focus(that.searchEntry); + } + else { + global.stage.set_key_focus(that.privateModeMenuItem); + } + } + }, 50); + }); + + // Create menu sections for items + // Favorites + that.favoritesSection = new PopupMenu.PopupMenuSection(); + + that.scrollViewFavoritesMenuSection = new PopupMenu.PopupMenuSection(); + this.favoritesScrollView = new St.ScrollView({ + style_class: 'ci-history-menu-section', + overlay_scrollbars: true + }); + this.favoritesScrollView.add_child(that.favoritesSection.actor); + + that.scrollViewFavoritesMenuSection.actor.add_child(this.favoritesScrollView); + this.favoritesSeparator = new PopupMenu.PopupSeparatorMenuItem(); + + // History + that.historySection = new PopupMenu.PopupMenuSection(); + + that.scrollViewMenuSection = new PopupMenu.PopupMenuSection(); + this.historyScrollView = new St.ScrollView({ + style_class: 'ci-main-menu-section ci-history-menu-section', + overlay_scrollbars: true + }); + this.historyScrollView.add_child(that.historySection.actor); + + that.scrollViewMenuSection.actor.add_child(this.historyScrollView); + + // Add separator + this.historySeparator = new PopupMenu.PopupSeparatorMenuItem(); + + // Add sections ordered according to settings + if (PINNED_ON_BOTTOM) { + that.menu.addMenuItem(that.scrollViewMenuSection); + that.menu.addMenuItem(that.scrollViewFavoritesMenuSection); + } + else { + that.menu.addMenuItem(that.scrollViewFavoritesMenuSection); + that.menu.addMenuItem(that.scrollViewMenuSection); + } + + // Private mode switch + that.privateModeMenuItem = new PopupMenu.PopupSwitchMenuItem( + _("Private mode"), PRIVATEMODE, { reactive: true }); + that.privateModeMenuItem.connect('toggled', + that._onPrivateModeSwitch.bind(that)); + that.privateModeMenuItem.insert_child_at_index( + new St.Icon({ + icon_name: 'security-medium-symbolic', + style_class: 'clipboard-menu-icon', + y_align: Clutter.ActorAlign.CENTER + }), + 0 + ); + that.menu.addMenuItem(that.privateModeMenuItem); + + // Add 'Clear' button which removes all items from cache + this.clearMenuItem = new PopupMenu.PopupMenuItem(_('Clear history')); + this.clearMenuItem.insert_child_at_index( + new St.Icon({ + icon_name: 'user-trash-symbolic', + style_class: 'clipboard-menu-icon', + y_align: Clutter.ActorAlign.CENTER + }), + 0 + ); + + let timerBox = new St.BoxLayout({ + x_align: Clutter.ActorAlign.END, + x_expand: true + }); + + this.timerLabel = new St.Label({ + + text: '', + style: 'font-family: monospace;', + x_align: Clutter.ActorAlign.END, + x_expand: true + }); + + this.resetTimerButton = new St.Button({ + style_class: 'ci-action-btn', + can_focus: true, + child: new St.Icon({ + icon_name: 'view-refresh-symbolic', + style_class: 'system-status-icon', + icon_size: 14 + }), + x_align: Clutter.ActorAlign.END, + y_align: Clutter.ActorAlign.CENTER, + }); + + this.resetTimerButton.connect('clicked', () => { + this._scheduleNextHistoryClear(); + }); + + timerBox.add_child(this.timerLabel); + timerBox.add_child(this.resetTimerButton); + this.clearMenuItem.add_child(timerBox); + + this.clearMenuItem.connect('activate', that._removeAll.bind(that)); + + // Add 'Settings' menu item to open settings + this.settingsMenuItem = new PopupMenu.PopupMenuItem(_('Settings')); + this.settingsMenuItem.insert_child_at_index( + new St.Icon({ + icon_name: 'preferences-system-symbolic', + style_class: 'clipboard-menu-icon', + y_align: Clutter.ActorAlign.CENTER + }), + 0 + ); + that.menu.addMenuItem(this.settingsMenuItem); + this.settingsMenuItem.connect('activate', that._openSettings.bind(that)); + + // Empty state section + this.emptyStateSection = new St.BoxLayout({ + style_class: 'clipboard-indicator-empty-state', + vertical: true + }); + let emptyIcon = null; + if (this._getIconThemeName().startsWith('Yaru')) { + const gicon = this._getExtIcon(INDICATOR_ICON_FULL); + if (gicon) { + emptyIcon = new St.Icon({ gicon, style_class: 'system-status-icon clipboard-indicator-icon', x_align: Clutter.ActorAlign.CENTER }); + } else { + emptyIcon = new St.Icon({ icon_name: INDICATOR_ICON, style_class: 'system-status-icon clipboard-indicator-icon', x_align: Clutter.ActorAlign.CENTER }); + } + } else { + emptyIcon = new St.Icon({ icon_name: INDICATOR_ICON, style_class: 'system-status-icon clipboard-indicator-icon', x_align: Clutter.ActorAlign.CENTER }); + } + this.emptyStateSection.add_child(emptyIcon); + this.emptyStateSection.add_child(new St.Label({ + text: _('Clipboard is empty'), + x_align: Clutter.ActorAlign.CENTER + })); + + // Add cached items + clipHistory.forEach(entry => this._addEntry(entry)); + + if (lastIdx >= 0) { + that._selectMenuItem(clipItemsArr[lastIdx]); + } + + this.#showElements(); + } + + #hideElements() { + if (this.menu.box.contains(this._entryItem)) this.menu.box.remove_child(this._entryItem); + if (this.menu.box.contains(this.favoritesSeparator)) this.menu.box.remove_child(this.favoritesSeparator); + if (this.menu.box.contains(this.historySeparator)) this.menu.box.remove_child(this.historySeparator); + if (this.menu.box.contains(this.clearMenuItem)) this.menu.box.remove_child(this.clearMenuItem); + if (this.menu.box.contains(this.emptyStateSection)) this.menu.box.remove_child(this.emptyStateSection); + } + + #showElements() { + if (this.clipItemsRadioGroup.length > 0) { + if (this.menu.box.contains(this._entryItem) === false) { + this.menu.box.insert_child_at_index(this._entryItem, 0); + } + if (this.menu.box.contains(this.clearMenuItem) === false) { + this.menu.box.insert_child_below(this.clearMenuItem, this.settingsMenuItem); + } + if (this.menu.box.contains(this.emptyStateSection) === true) { + this.menu.box.remove_child(this.emptyStateSection); + } + + if (this.favoritesSection._getMenuItems().length > 0) { + if (this.menu.box.contains(this.favoritesSeparator) === false) { + this.menu.box.insert_child_above(this.favoritesSeparator, this.scrollViewFavoritesMenuSection.actor); + } + } + else if (this.menu.box.contains(this.favoritesSeparator) === true) { + this.menu.box.remove_child(this.favoritesSeparator); + } + + if (this.historySection._getMenuItems().length > 0) { + if (this.menu.box.contains(this.historySeparator) === false) { + this.menu.box.insert_child_above(this.historySeparator, this.scrollViewMenuSection.actor); + } + } + else if (this.menu.box.contains(this.historySeparator) === true) { + this.menu.box.remove_child(this.historySeparator); + } + } + else if (this.menu.box.contains(this.emptyStateSection) === false) { + this.#renderEmptyState(); + } + } + + #renderEmptyState () { + this.#hideElements(); + this.menu.box.insert_child_at_index(this.emptyStateSection, 0); + } + + /* When text change, this function will check, for each item of the + historySection and favoritesSestion, if it should be visible or not (based on words contained + in the clipContents attribute of the item). It doesn't destroy or create + items. It the entry is empty, the section is restored with all items + set as visible. */ + _onSearchTextChanged () { + + // Text to be searched converted to lowercase if search is case insensitive + let searchedText = this.searchEntry.get_text(); + if (!CASE_SENSITIVE_SEARCH) searchedText = searchedText.toLowerCase(); + + if(searchedText === '') { + this._getAllIMenuItems().forEach(function(mItem){ + mItem.actor.visible = true; + }); + } + else { + this._getAllIMenuItems().forEach(function(mItem){ + // Clip content converted to lowercase if search is case insensitive + let text = mItem.clipContents; + if (!CASE_SENSITIVE_SEARCH) text = text.toLowerCase(); + + let isMatching = false; + if (REGEX_SEARCH){ + /* Regex flags: + - 'm' for multiline matching (when multiline content is copied) + - 'i' for case insensitive matching when search is not set to case sensitive + */ + let text_regex = new RegExp(searchedText, 'm' + (CASE_SENSITIVE_SEARCH ? '' : 'i')); + isMatching = text_regex.test(text); + }else{ + isMatching = text.indexOf(searchedText) >= 0; + } + mItem.actor.visible = isMatching + }); + } + } + + _truncate (string, length) { + let shortened = string.replace(/\s+/g, ' '); + + let chars = [...shortened] + if (chars.length > length) + shortened = chars.slice(0, length - 1).join('') + '...'; + + return shortened; + } + + _setEntryLabel (menuItem) { + const { entry } = menuItem; + if (entry.isText()) { + + menuItem.label.set_text(this._truncate(entry.getStringValue(), MAX_ENTRY_LENGTH)); + } + else if (entry.isImage()) { + this.registry.getEntryAsImage(entry).then(img => { + img.add_style_class_name('clipboard-menu-img-preview'); + if (menuItem.previewImage) { + menuItem.remove_child(menuItem.previewImage); + } + menuItem.previewImage = img; + menuItem.insert_child_below(img, menuItem.label); + }); + } + } + + _findNextMenuItem (currentMenutItem) { + let currentIndex = this.clipItemsRadioGroup.indexOf(currentMenutItem); + + // for only one item + if(this.clipItemsRadioGroup.length === 1) { + return null; + } + + // when focus is in middle of the displayed list + for (let i = currentIndex - 1; i >= 0; i--) { + let menuItem = this.clipItemsRadioGroup[i]; + if (menuItem.actor.visible) { + return menuItem; + } + } + + // when focus is at the last element of the displayed list + let beforeMenuItem = this.clipItemsRadioGroup[currentIndex + 1]; + if(beforeMenuItem.actor.visible){ + + return beforeMenuItem; + } + + return null; + } + + #selectNextMenuItem (menuItem) { + let nextMenuItem = this._findNextMenuItem(menuItem); + + if (nextMenuItem) { + nextMenuItem.actor.grab_key_focus(); + } else { + this.privateModeMenuItem.actor.grab_key_focus(); + } + } + + _addEntry (entry, autoSelect, autoSetClip) { + let menuItem = new PopupMenu.PopupMenuItem(''); + + menuItem.menu = this.menu; + menuItem.entry = entry; + menuItem.clipContents = entry.getStringValue(); + menuItem.radioGroup = this.clipItemsRadioGroup; + menuItem.buttonPressId = menuItem.connect('activate', + autoSet => this._onMenuItemSelectedAndMenuClose(menuItem, autoSet)); + menuItem.connect('key-focus-in', () => { + const viewToScroll = menuItem.entry.isFavorite() ? + this.favoritesScrollView : this.historyScrollView; + AnimationUtils.ensureActorVisibleInScrollView(viewToScroll, menuItem); + }); + menuItem.actor.connect('key-press-event', (actor, event) => { + switch (event.get_key_symbol()) { + case Clutter.KEY_Delete: + this.#selectNextMenuItem(menuItem); + this._removeEntry(menuItem, 'delete'); + break; + case Clutter.KEY_p: + this.#selectNextMenuItem(menuItem); + this._favoriteToggle(menuItem); + break; + case Clutter.KEY_v: + this.#pasteItem(menuItem); + break; + case Clutter.KEY_KP_Enter: + case Clutter.KEY_Return: + if (PASTE_ON_SELECT) { + this.#pasteItem(menuItem); + } + this._onMenuItemSelectedAndMenuClose(menuItem, true); + break; + } + }) + + this._setEntryLabel(menuItem); + this.clipItemsRadioGroup.push(menuItem); + + // Favorite button + let iconfav = new St.Icon({ + icon_name: 'view-pin-symbolic', + style_class: 'system-status-icon' + }); + + let icofavBtn = new St.Button({ + style_class: 'ci-pin-btn ci-action-btn', + can_focus: true, + child: iconfav, + x_align: Clutter.ActorAlign.END, + x_expand: true, + y_expand: true + }); + + menuItem.actor.add_child(icofavBtn); + menuItem.icofavBtn = icofavBtn; + menuItem.favoritePressId = icofavBtn.connect('clicked', + () => this._favoriteToggle(menuItem) + ); + + // Paste button + menuItem.pasteBtn = new St.Button({ + style_class: 'ci-action-btn', + can_focus: true, + child: new St.Icon({ + icon_name: 'edit-paste-symbolic', + style_class: 'system-status-icon' + }), + x_align: Clutter.ActorAlign.END, + x_expand: false, + y_expand: true, + visible: PASTE_BUTTON + }); + + menuItem.pasteBtn.connect('clicked', + () => this.#pasteItem(menuItem) + ); + + menuItem.actor.add_child(menuItem.pasteBtn); + + // Delete button + let icon = new St.Icon({ + icon_name: 'edit-delete-symbolic', //'mail-attachment-symbolic', + style_class: 'system-status-icon' + }); + + let 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); + menuItem.icoBtn = icoBtn; + menuItem.deletePressId = icoBtn.connect('clicked', + () => this._removeEntry(menuItem, 'delete') + ); + + if (entry.isFavorite()) { + this.favoritesSection.addMenuItem(menuItem, 0); + } else { + this.historySection.addMenuItem(menuItem, 0); + } + + if (autoSelect === true) { + this._selectMenuItem(menuItem, autoSetClip); + } + else { + menuItem.setOrnament(PopupMenu.Ornament.NONE); + } + + this.#showElements(); + } + + _favoriteToggle (menuItem) { + menuItem.entry.favorite = menuItem.entry.isFavorite() ? false : true; + + this._moveItemFirst(menuItem); + this._updateCache(); + this.#showElements(); + } + + _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."); + + this.dialogManager.open(title, message, sub_message, _("Clear"), _("Cancel"), () => { + this._clearHistory(); + } + ); + } + + _clearHistory (invokedAutomatically = false) { + // Don't remove pinned items + this.historySection._getMenuItems().forEach(mItem => { + if (KEEP_SELECTED_ON_CLEAR === false || !mItem.currentlySelected) { + this._removeEntry(mItem, 'delete'); + } + }); + + if (!invokedAutomatically) { + this._showNotification(_("Clipboard history cleared")); + } + else { + this._showNotification(_("Clipboard history cleared automatically")); + } + } + + _removeAll () { + if (PRIVATEMODE) return; + var that = this; + + if (CONFIRM_ON_CLEAR) { + that._confirmRemoveAll(); + } else { + that._clearHistory(); + } + } + + _removeEntry (menuItem, event) { + let itemIdx = this.clipItemsRadioGroup.indexOf(menuItem); + + if(event === 'delete' && menuItem.currentlySelected) { + this.#clearClipboard(); + } + + menuItem.destroy(); + this.clipItemsRadioGroup.splice(itemIdx,1); + + if (menuItem.entry.isImage()) { + this.registry.deleteEntryFile(menuItem.entry); + } + + this._updateCache(); + this.#showElements(); + } + + _removeOldestEntries () { + let that = this; + + let clipItemsRadioGroupNoFavorite = that.clipItemsRadioGroup.filter( + item => item.entry.isFavorite() === false); + + const origSize = clipItemsRadioGroupNoFavorite.length; + + while (clipItemsRadioGroupNoFavorite.length > MAX_REGISTRY_LENGTH) { + let oldestNoFavorite = clipItemsRadioGroupNoFavorite.shift(); + that._removeEntry(oldestNoFavorite); + + clipItemsRadioGroupNoFavorite = that.clipItemsRadioGroup.filter( + item => item.entry.isFavorite() === false); + } + + if (clipItemsRadioGroupNoFavorite.length < origSize) { + that._updateCache(); + } + } + + _onMenuItemSelected (menuItem, autoSet) { + for (let otherMenuItem of menuItem.radioGroup) { + let clipContents = menuItem.clipContents; + + if (otherMenuItem === menuItem && clipContents) { + menuItem.setOrnament(PopupMenu.Ornament.DOT); + menuItem.currentlySelected = true; + if (autoSet !== false) + this.#updateClipboard(menuItem.entry); + } + else { + otherMenuItem.setOrnament(PopupMenu.Ornament.NONE); + otherMenuItem.currentlySelected = false; + } + } + } + + _selectMenuItem (menuItem, autoSet) { + this._onMenuItemSelected(menuItem, autoSet); + this.#updateIndicatorContent(menuItem.entry); + } + + _onMenuItemSelectedAndMenuClose (menuItem, autoSet) { + for (let otherMenuItem of menuItem.radioGroup) { + let clipContents = menuItem.clipContents; + + if (menuItem === otherMenuItem && clipContents) { + menuItem.setOrnament(PopupMenu.Ornament.DOT); + menuItem.currentlySelected = true; + if (autoSet !== false) + this.#updateClipboard(menuItem.entry); + } + else { + otherMenuItem.setOrnament(PopupMenu.Ornament.NONE); + otherMenuItem.currentlySelected = false; + } + } + + menuItem.menu.close(); + } + + _getCache () { + return this.registry.read(); + } + + #addToCache (entry) { + const entries = this.clipItemsRadioGroup + .map(menuItem => menuItem.entry) + .filter(entry => CACHE_ONLY_FAVORITE == false || entry.isFavorite()) + .concat([entry]); + this.registry.write(entries); + } + + _updateCache () { + const entries = this.clipItemsRadioGroup + .map(menuItem => menuItem.entry) + .filter(entry => CACHE_ONLY_FAVORITE == false || entry.isFavorite()); + + this.registry.write(entries); + } + + async _onSelectionChange (selection, selectionType, selectionSource) { + if (selectionType === Meta.SelectionType.SELECTION_CLIPBOARD) { + this._refreshIndicator(); + } + } + + async _refreshIndicator () { + if (PRIVATEMODE) return; // Private mode, do not. + + const focussedWindow = Shell.Global.get().display.focusWindow; + const wmClass = focussedWindow?.get_wm_class(); + + if (wmClass && EXCLUDED_APPS.includes(wmClass)) return; // Excluded app, do not. + + if (this.#refreshInProgress) return; + this.#refreshInProgress = true; + + try { + const result = await this.#getClipboardContent(); + + if (result) { + for (let menuItem of this.clipItemsRadioGroup) { + if (menuItem.entry.equals(result)) { + this._selectMenuItem(menuItem, false); + + if (!menuItem.entry.isFavorite() && MOVE_ITEM_FIRST) { + this._moveItemFirst(menuItem); + } + + return; + } + } + + this.#addToCache(result); + this._addEntry(result, true, false); + this._removeOldestEntries(); + if (NOTIFY_ON_COPY) { + this._showNotification(_("Copied to clipboard"), notif => { + notif.addAction(_('Cancel'), this._cancelNotification); + }); + } + } + } + catch (e) { + console.error('Clipboard Indicator: Failed to refresh indicator'); + console.error(e); + } + finally { + this.#refreshInProgress = false; + } + } + + _moveItemFirst (item) { + this._removeEntry(item); + this._addEntry(item.entry, item.currentlySelected, false); + this._updateCache(); + } + + _findItem (text) { + return this.clipItemsRadioGroup.filter( + item => item.clipContents === text)[0]; + } + + _getCurrentlySelectedItem () { + return this.clipItemsRadioGroup.find(item => item.currentlySelected); + } + + _getAllIMenuItems () { + return this.historySection._getMenuItems().concat(this.favoritesSection._getMenuItems()); + } + + _setupListener () { + const metaDisplay = Shell.Global.get().get_display(); + const selection = metaDisplay.get_selection(); + this._setupSelectionTracking(selection); + } + + _setupSelectionTracking (selection) { + this.selection = selection; + this._selectionOwnerChangedId = selection.connect('owner-changed', (selection, selectionType, selectionSource) => { + this._onSelectionChange(selection, selectionType, selectionSource); + }); + } + + _setupHistoryIntervalClearing() { + this._fetchSettings(); + + if (this._intervalSettingChangedId) { + this.extension.settings.disconnect(this._intervalSettingChangedId); + this._intervalSettingChangedId = null; + } + if (this._intervalToggleChangedId) { + this.extension.settings.disconnect(this._intervalToggleChangedId); + this._intervalToggleChangedId = null; + } + if (this._historyClearTimeoutId) { + clearTimeout(this._historyClearTimeoutId); + this._historyClearTimeoutId = null; + } + + this._intervalSettingChangedId = this.extension.settings.connect( + `changed::${PrefsFields.CLEAR_HISTORY_INTERVAL}`, + this._onHistoryIntervalClearSettingsChanged.bind(this) + ); + this._intervalToggleChangedId = this.extension.settings.connect( + `changed::${PrefsFields.CLEAR_HISTORY_ON_INTERVAL}`, + this._onHistoryIntervalClearSettingsChanged.bind(this) + ); + + + if (!CLEAR_HISTORY_ON_INTERVAL) { + this._updateIntervalTimer(); + return; + } + + const currentTime = Math.ceil(new Date().getTime() / 1000); + + if (NEXT_HISTORY_CLEAR === -1) { //new timer + this._scheduleNextHistoryClear(); + } + else if (NEXT_HISTORY_CLEAR < currentTime) { //timer expired + this._clearHistory(true); + this._scheduleNextHistoryClear(); + } + else { //timer already set, but not expired + const timeoutMs = (NEXT_HISTORY_CLEAR - currentTime) * 1000; + this._historyClearTimeoutId = setTimeout(() => { + + this._clearHistory(true); + this._scheduleNextHistoryClear(); + }, timeoutMs); + this._timerIntervalId = setInterval(() => { + this._updateIntervalTimer(); + }, 1000); + } + } + + _onHistoryIntervalClearSettingsChanged(_settings, key) { + this._fetchSettings(); + if (key === PrefsFields.CLEAR_HISTORY_INTERVAL) { + this._scheduleNextHistoryClear(); + } + else if (key === PrefsFields.CLEAR_HISTORY_ON_INTERVAL) { + if (CLEAR_HISTORY_ON_INTERVAL) { + this._resetHistoryClearTimer(); + this._setupHistoryIntervalClearing(); + } else { + this._resetHistoryClearTimer(); + } + } + } + + _scheduleNextHistoryClear() { + this._fetchSettings(); + + clearInterval(this._timerIntervalId); + if (this._historyClearTimeoutId) { + clearTimeout(this._historyClearTimeoutId); + this._historyClearTimeoutId = null; + } + + if(!CLEAR_HISTORY_ON_INTERVAL) { + this._resetHistoryClearTimer(); + return; + } + + const currentTime = Math.ceil(new Date().getTime() / 1000); + NEXT_HISTORY_CLEAR = currentTime + CLEAR_HISTORY_INTERVAL * 60; + const timeoutMs = (NEXT_HISTORY_CLEAR - currentTime) * 1000; + + this.extension.settings.set_int(PrefsFields.NEXT_HISTORY_CLEAR, NEXT_HISTORY_CLEAR); + + this._updateIntervalTimer(); + this._timerIntervalId = setInterval(() => { + this._updateIntervalTimer(); + }, 1000); + + this._historyClearTimeoutId = setTimeout(() => { + this._clearHistory(true); + this._scheduleNextHistoryClear(); + }, timeoutMs); + } + + _resetHistoryClearTimer() { + //basically just reset and stop the timer + if (this._historyClearTimeoutId) { + clearTimeout(this._historyClearTimeoutId); + this._historyClearTimeoutId = null; + } + clearInterval(this._timerIntervalId); + this._timerIntervalId = null; + this._updateIntervalTimer(); + this.extension.settings.set_int(PrefsFields.NEXT_HISTORY_CLEAR, -1); + } + + _updateIntervalTimer() { + this._fetchSettings(); + this.resetTimerButton.visible = CLEAR_HISTORY_ON_INTERVAL; + this.timerLabel.visible = CLEAR_HISTORY_ON_INTERVAL; + if (!CLEAR_HISTORY_ON_INTERVAL) return; + + + let currentTime = Math.ceil(new Date().getTime() / 1000); + let timeLeft = NEXT_HISTORY_CLEAR - currentTime; + + if (timeLeft <= 0) { + this.timerLabel.set_text(''); + return; + } + + let hours = Math.floor(timeLeft / 3600); + let minutes = Math.floor((timeLeft % 3600) / 60); + + let seconds = Math.floor(timeLeft % 60); + + let formattedTime = ''; + if (hours > 0) { + formattedTime += `${hours}h `; + } + if (minutes > 0) { + formattedTime += `${minutes}m `; + } + formattedTime += `${seconds}s`; + this.timerLabel.set_text(formattedTime); + } + + _openSettings () { + this.extension.openSettings(); + } + + _initNotifSource () { + if (!this._notifSource) { + this._notifSource = new MessageTray.Source({ + title: 'Clipboard Indicator', + 'icon-name': INDICATOR_ICON + }); + + this._notifSource.connect('destroy', () => { + this._notifSource = null; + }); + + Main.messageTray.add(this._notifSource); + } + } + + _cancelNotification () { + if (this.clipItemsRadioGroup.length >= 2) { + let clipSecond = this.clipItemsRadioGroup.length - 2; + let previousClip = this.clipItemsRadioGroup[clipSecond]; + this.#updateClipboard(previousClip.entry); + previousClip.setOrnament(PopupMenu.Ornament.DOT); + previousClip.icoBtn.visible = false; + previousClip.currentlySelected = true; + } else { + this.#clearClipboard(); + } + let clipFirst = this.clipItemsRadioGroup.length - 1; + this._removeEntry(this.clipItemsRadioGroup[clipFirst]); + } + + _showNotification (message, transformFn) { + const dndOn = () => + !Main.panel.statusArea.dateMenu._indicator._settings.get_boolean( + 'show-banners', + ); + if (PRIVATEMODE || dndOn()) { + return; + } + + let notification = null; + + this._initNotifSource(); + + if (this._notifSource.count === 0) { + notification = new MessageTray.Notification({ + source: this._notifSource, + body: message, + 'is-transient': true + }); + } + else { + notification = this._notifSource.notifications[0]; + notification.body = message; + notification.clearActions(); + } + + if (typeof transformFn === 'function') { + transformFn(notification); + } + + this._notifSource.addNotification(notification); + } + + _createHistoryLabel () { + this._historyLabel = new St.Label({ + style_class: 'ci-notification-label', + text: '' + }); + + global.stage.add_child(this._historyLabel); + + this._historyLabel.hide(); + } + + togglePrivateMode () { + this.privateModeMenuItem.toggle(); + } + + _onPrivateModeSwitch () { + let that = this; + PRIVATEMODE = this.privateModeMenuItem.state; + // We hide the history in private ModeTypee because it will be out of sync (selected item will not reflect clipboard) + this.scrollViewMenuSection.actor.visible = !PRIVATEMODE; + this.scrollViewFavoritesMenuSection.actor.visible = !PRIVATEMODE; + // If we get out of private mode then we restore the clipboard to old state + if (!PRIVATEMODE) { + let selectList = this.clipItemsRadioGroup.filter((item) => !!item.currentlySelected); + + if (selectList.length) { + this._selectMenuItem(selectList[0]); + } else { + // Nothing to return to, let's empty it instead + this.#clearClipboard(); + } + + this.#getClipboardContent().then(entry => { + if (!entry) return; + this.#updateIndicatorContent(entry); + }).catch(e => console.error(e)); + + this.hbox.remove_style_class_name('private-mode'); + this.#showElements(); + } else { + this.hbox.add_style_class_name('private-mode'); + this.#updateIndicatorContent(null); + this.#hideElements(); + } + } + + _loadSettings () { + this._settingsChangedId = this.extension.settings.connect('changed', + this._onSettingsChange.bind(this)); + + this._fetchSettings(); + + if (ENABLE_KEYBINDING) + this._bindShortcuts(); + } + + _fetchSettings () { + const { settings } = this.extension; + MAX_REGISTRY_LENGTH = settings.get_int(PrefsFields.HISTORY_SIZE); + MAX_ENTRY_LENGTH = settings.get_int(PrefsFields.PREVIEW_SIZE); + CACHE_ONLY_FAVORITE = settings.get_boolean(PrefsFields.CACHE_ONLY_FAVORITE); + DELETE_ENABLED = settings.get_boolean(PrefsFields.DELETE); + MOVE_ITEM_FIRST = settings.get_boolean(PrefsFields.MOVE_ITEM_FIRST); + NOTIFY_ON_COPY = settings.get_boolean(PrefsFields.NOTIFY_ON_COPY); + NOTIFY_ON_CYCLE = settings.get_boolean(PrefsFields.NOTIFY_ON_CYCLE); + CONFIRM_ON_CLEAR = settings.get_boolean(PrefsFields.CONFIRM_ON_CLEAR); + ENABLE_KEYBINDING = settings.get_boolean(PrefsFields.ENABLE_KEYBINDING); + MAX_TOPBAR_LENGTH = settings.get_int(PrefsFields.TOPBAR_PREVIEW_SIZE); + TOPBAR_DISPLAY_MODE = settings.get_int(PrefsFields.TOPBAR_DISPLAY_MODE_ID); + CLEAR_ON_BOOT = settings.get_boolean(PrefsFields.CLEAR_ON_BOOT); + PASTE_ON_SELECT = settings.get_boolean(PrefsFields.PASTE_ON_SELECT); + DISABLE_DOWN_ARROW = settings.get_boolean(PrefsFields.DISABLE_DOWN_ARROW); + STRIP_TEXT = settings.get_boolean(PrefsFields.STRIP_TEXT); + KEEP_SELECTED_ON_CLEAR = settings.get_boolean(PrefsFields.KEEP_SELECTED_ON_CLEAR); + PASTE_BUTTON = settings.get_boolean(PrefsFields.PASTE_BUTTON); + PINNED_ON_BOTTOM = settings.get_boolean(PrefsFields.PINNED_ON_BOTTOM); + CACHE_IMAGES = settings.get_boolean(PrefsFields.CACHE_IMAGES); + EXCLUDED_APPS = settings.get_strv(PrefsFields.EXCLUDED_APPS); + CLEAR_HISTORY_ON_INTERVAL = settings.get_boolean(PrefsFields.CLEAR_HISTORY_ON_INTERVAL); + CLEAR_HISTORY_INTERVAL = settings.get_int(PrefsFields.CLEAR_HISTORY_INTERVAL); + NEXT_HISTORY_CLEAR = settings.get_int(PrefsFields.NEXT_HISTORY_CLEAR); + CASE_SENSITIVE_SEARCH = settings.get_boolean(PrefsFields.CASE_SENSITIVE_SEARCH); + REGEX_SEARCH = settings.get_boolean(PrefsFields.REGEX_SEARCH); + } + + async _onSettingsChange () { + try { + var that = this; + + // Load the settings into variables + that._fetchSettings(); + + // Remove old entries in case the registry size changed + that._removeOldestEntries(); + + // Re-set menu-items lables in case preview size changed + this._getAllIMenuItems().forEach(function (mItem) { + that._setEntryLabel(mItem); + mItem.pasteBtn.visible = PASTE_BUTTON; + }); + + //update topbar + this._updateTopbarLayout(); + that.#updateIndicatorContent(await this.#getClipboardContent()); + + // Bind or unbind shortcuts + if (ENABLE_KEYBINDING) + that._bindShortcuts(); + else + that._unbindShortcuts(); + } catch (e) { + console.error('Clipboard Indicator: Failed to update registry'); + console.error(e); + } + } + + _bindShortcuts () { + this._unbindShortcuts(); + this._bindShortcut(PrefsFields.BINDING_CLEAR_HISTORY, this._removeAll); + this._bindShortcut(PrefsFields.BINDING_PREV_ENTRY, this._previousEntry); + this._bindShortcut(PrefsFields.BINDING_NEXT_ENTRY, this._nextEntry); + this._bindShortcut(PrefsFields.BINDING_TOGGLE_MENU, this._toggleMenu); + + this._bindShortcut(PrefsFields.BINDING_PRIVATE_MODE, this.togglePrivateMode); + } + + _unbindShortcuts () { + this._shortcutsBindingIds.forEach( + (id) => Main.wm.removeKeybinding(id) + ); + + this._shortcutsBindingIds = []; + } + + _bindShortcut (name, cb) { + var ModeType = Shell.hasOwnProperty('ActionMode') ? + Shell.ActionMode : Shell.KeyBindingMode; + + Main.wm.addKeybinding( + name, + this.extension.settings, + Meta.KeyBindingFlags.NONE, + ModeType.ALL, + cb.bind(this) + ); + + this._shortcutsBindingIds.push(name); + } + + _updateTopbarLayout () { + if(TOPBAR_DISPLAY_MODE === 0){ + this.icon.visible = true; + this._buttonText.visible = false; + this._buttonImgPreview.visible = false; + this.show(); + } + if(TOPBAR_DISPLAY_MODE === 1){ + this.icon.visible = false; + this._buttonText.visible = true; + this._buttonImgPreview.visible = true; + this.show(); + } + if(TOPBAR_DISPLAY_MODE === 2){ + this.icon.visible = true; + this._buttonText.visible = true; + this._buttonImgPreview.visible = true; + this.show(); + } + if (TOPBAR_DISPLAY_MODE === 3) { + this.hide(); + } + if(!DISABLE_DOWN_ARROW) { + this._downArrow.visible = true; + } else { + this._downArrow.visible = false; + } + } + + _disconnectSettings () { + if (!this._settingsChangedId) + return; + + this.extension.settings.disconnect(this._settingsChangedId); + this._settingsChangedId = null; + + if (this._intervalSettingChangedId) { + this.extension.settings.disconnect(this._intervalSettingChangedId); + this._intervalSettingChangedId = null; + } + + if (this._intervalToggleChangedId) { + this.extension.settings.disconnect(this._intervalToggleChangedId); + this._intervalToggleChangedId = null; + } + + if (this._historyClearTimeoutId) { + clearTimeout(this._historyClearTimeoutId); + this._historyClearTimeoutId = null; + } + } + + _disconnectSelectionListener () { + if (!this._selectionOwnerChangedId) + return; + + this.selection.disconnect(this._selectionOwnerChangedId); + } + + _clearDelayedSelectionTimeout () { + if (this._delayedSelectionTimeoutId) { + clearInterval(this._delayedSelectionTimeoutId); + } + } + + _selectEntryWithDelay (entry) { + let that = this; + that._selectMenuItem(entry, false); + + that._delayedSelectionTimeoutId = setTimeout(function () { + that._selectMenuItem(entry); //select the item + that._delayedSelectionTimeoutId = null; + }, DELAYED_SELECTION_TIMEOUT); + } + + _previousEntry () { + if (PRIVATEMODE) return; + let that = this; + + that._clearDelayedSelectionTimeout(); + + this._getAllIMenuItems().some(function (mItem, i, menuItems){ + if (mItem.currentlySelected) { + i--; //get the previous index + if (i < 0) i = menuItems.length - 1; //cycle if out of bound + let index = i + 1; //index to be displayed + + if(NOTIFY_ON_CYCLE) { + that._showNotification(index + ' / ' + menuItems.length + ': ' + menuItems[i].entry.getStringValue()); + + } + if (MOVE_ITEM_FIRST) { + that._selectEntryWithDelay(menuItems[i]); + } + else { + that._selectMenuItem(menuItems[i]); + } + return true; + } + return false; + }); + } + + _nextEntry () { + if (PRIVATEMODE) return; + let that = this; + + that._clearDelayedSelectionTimeout(); + + this._getAllIMenuItems().some(function (mItem, i, menuItems){ + if (mItem.currentlySelected) { + i++; //get the next index + if (i === menuItems.length) i = 0; //cycle if out of bound + let index = i + 1; //index to be displayed + + if(NOTIFY_ON_CYCLE) { + that._showNotification(index + ' / ' + menuItems.length + ': ' + menuItems[i].entry.getStringValue()); + } + if (MOVE_ITEM_FIRST) { + that._selectEntryWithDelay(menuItems[i]); + } + else { + that._selectMenuItem(menuItems[i]); + } + return true; + } + return false; + }); + } + + _toggleMenu () { + this.menu.toggle(); + } + + #pasteItem (menuItem) { + this.menu.close(); + const currentlySelected = this._getCurrentlySelectedItem(); + this.preventIndicatorUpdate = true; + this.#updateClipboard(menuItem.entry); + this._pastingKeypressTimeout = setTimeout(() => { + if (this.keyboard.purpose === Clutter.InputContentPurpose.TERMINAL) { + this.keyboard.press(Clutter.KEY_Control_L); + this.keyboard.press(Clutter.KEY_Shift_L); + this.keyboard.press(Clutter.KEY_Insert); + this.keyboard.release(Clutter.KEY_Insert); + this.keyboard.release(Clutter.KEY_Shift_L); + this.keyboard.release(Clutter.KEY_Control_L); + } + else { + this.keyboard.press(Clutter.KEY_Shift_L); + this.keyboard.press(Clutter.KEY_Insert); + this.keyboard.release(Clutter.KEY_Insert); + this.keyboard.release(Clutter.KEY_Shift_L); + } + + this._pastingResetTimeout = setTimeout(() => { + this.preventIndicatorUpdate = false; + this.#updateClipboard(currentlySelected.entry); + }, 50); + }, 50); + } + + #clearTimeouts () { + if (this._imagePreviewTimeout) clearTimeout(this._imagePreviewTimeout); + if (this._setFocusOnOpenTimeout) clearTimeout(this._setFocusOnOpenTimeout); + if (this._pastingKeypressTimeout) clearTimeout(this._pastingKeypressTimeout); + if (this._pastingResetTimeout) clearTimeout(this._pastingResetTimeout); + if (this._historyClearTimeoutId) clearTimeout(this._historyClearTimeoutId); + if (this._timerIntervalId) clearInterval(this._timerIntervalId); + } + + #clearClipboard () { + this.extension.clipboard.set_text(CLIPBOARD_TYPE, ""); + this.#updateIndicatorContent(null); + } + + #updateClipboard (entry) { + this.extension.clipboard.set_content(CLIPBOARD_TYPE, entry.mimetype(), entry.asBytes()); + this.#updateIndicatorContent(entry); + } + + + async #getClipboardContent () { + const mimetypes = [ + "text/plain;charset=utf-8", + "UTF8_STRING", + "text/plain", + "STRING", + 'image/gif', + 'image/png', + 'image/jpg', + 'image/jpeg', + 'image/webp', + 'image/svg+xml', + 'text/html', + ]; + + for (let type of mimetypes) { + let result = await new Promise(resolve => this.extension.clipboard.get_content(CLIPBOARD_TYPE, type, (clipBoard, bytes) => { + if (bytes === null || bytes.get_size() === 0) { + resolve(null); + return; + } + + // HACK: workaround for GNOME 2nd+ copy mangling mimetypes https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/8233 + // In theory GNOME or XWayland should auto-convert this back to UTF8_STRING for legacy apps when it's needed https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/5300 + if (type === "UTF8_STRING") { + type = "text/plain;charset=utf-8"; + } + + const entry = new ClipboardEntry(type, bytes.get_data(), false); + if (CACHE_IMAGES && entry.isImage()) { + this.registry.writeEntryFile(entry); + } + resolve(entry); + })); + + if (result) { + if (!CACHE_IMAGES && result.isImage()) { + return null; + } + else { + return result; + } + } + } + + return null; + } +}); diff --git a/icons/Adwaita/edit-pasted-symbolic.svg b/icons/Adwaita/edit-pasted-symbolic.svg new file mode 100644 index 00000000..26e2a2f4 --- /dev/null +++ b/icons/Adwaita/edit-pasted-symbolic.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/icons/Adwaita/edit-pasted-symbolic.svg.license b/icons/Adwaita/edit-pasted-symbolic.svg.license new file mode 100644 index 00000000..3a52cd55 --- /dev/null +++ b/icons/Adwaita/edit-pasted-symbolic.svg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2010-2023 The GNOME Project +SPDX-FileCopyrightText: 2026 IlCraccatore2011 +SPDX-License-Identifier: LGPL-3.0-or-later diff --git a/icons/Flat/edit-pasted-symbolic.svg b/icons/Flat/edit-pasted-symbolic.svg new file mode 100644 index 00000000..1e96d961 --- /dev/null +++ b/icons/Flat/edit-pasted-symbolic.svg @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/icons/Flat/edit-pasted-symbolic.svg.license b/icons/Flat/edit-pasted-symbolic.svg.license new file mode 100644 index 00000000..3a52cd55 --- /dev/null +++ b/icons/Flat/edit-pasted-symbolic.svg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2010-2023 The GNOME Project +SPDX-FileCopyrightText: 2026 IlCraccatore2011 +SPDX-License-Identifier: LGPL-3.0-or-later diff --git a/icons/Yaru/edit-pasted-symbolic.svg b/icons/Yaru/edit-pasted-symbolic.svg new file mode 100644 index 00000000..8915e05e --- /dev/null +++ b/icons/Yaru/edit-pasted-symbolic.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/Yaru/edit-pasted-symbolic.svg.license b/icons/Yaru/edit-pasted-symbolic.svg.license new file mode 100644 index 00000000..3a52cd55 --- /dev/null +++ b/icons/Yaru/edit-pasted-symbolic.svg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2010-2023 The GNOME Project +SPDX-FileCopyrightText: 2026 IlCraccatore2011 +SPDX-License-Identifier: LGPL-3.0-or-later diff --git a/locale/de/LC_MESSAGES/clipboard-indicator.mo b/locale/de/LC_MESSAGES/clipboard-indicator.mo index ebceb2e1..b9907b9e 100644 Binary files a/locale/de/LC_MESSAGES/clipboard-indicator.mo and b/locale/de/LC_MESSAGES/clipboard-indicator.mo differ diff --git a/locale/de/LC_MESSAGES/clipboard-indicator.po b/locale/de/LC_MESSAGES/clipboard-indicator.po index 91dd148e..9c0f346e 100644 --- a/locale/de/LC_MESSAGES/clipboard-indicator.po +++ b/locale/de/LC_MESSAGES/clipboard-indicator.po @@ -1,238 +1,261 @@ -# German translation for gnome-shell-extension-clipboard-indicator. -# Copyright (C) 2014-2015, Yotam Bar-On -# This file is distributed under the same license as the gnome-shell-extension-clipboard-indicator package. -# Jens Lody , 2015. +# German translations for PACKAGE package. +# Copyright (C) 2026 THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# Il Craccatore 2011, 2026. # msgid "" msgstr "" -"Project-Id-Version: \n" +"Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-21 16:41+0300\n" -"PO-Revision-Date: 2024-04-23 18:10+0200\n" -"Last-Translator: Norman Henges \n" -"Language-Team: Deutsch \n" +"POT-Creation-Date: 2026-01-23 20:53+0100\n" +"PO-Revision-Date: 2026-01-24 15:31+0100\n" +"Last-Translator: IlCraccatore2011\n" "Language: de\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -"X-Generator: Poedit 3.4.2\n" -#: extension.js:108 +#: extension.js:113 msgid "Text will be here" -msgstr "Text wird hier angezeigt" +msgstr "Der Text wird hier angezeigt" -#: extension.js:181 +#: extension.js:187 msgid "Type here to search..." -msgstr "Text eingeben, um zu suchen…" +msgstr "Hier tippen, um zu suchen..." -#: extension.js:250 prefs.js:220 +#: extension.js:256 prefs.js:257 msgid "Private mode" msgstr "Privater Modus" -#: extension.js:264 prefs.js:222 +#: extension.js:270 prefs.js:259 msgid "Clear history" msgstr "Verlauf löschen" -#: extension.js:276 +#: extension.js:315 msgid "Settings" msgstr "Einstellungen" -#: extension.js:299 +#: extension.js:338 msgid "Clipboard is empty" -msgstr "Zwischenablage ist leer" +msgstr "Die Zwischenablage ist leer" -#: extension.js:571 +#: extension.js:626 msgid "Clear all?" msgstr "Alles löschen?" -#: extension.js:572 +#: extension.js:627 msgid "Are you sure you want to delete all clipboard items?" -msgstr "Soll die Zwischenablage wirklich geleert werden?" +msgstr "Möchten Sie wirklich alle Einträge der Zwischenablage löschen?" -#: extension.js:573 +#: extension.js:628 msgid "This operation cannot be undone." -msgstr "Diese Aktion kann nicht rückgängig gemacht werden." +msgstr "Dieser Vorgang kann nicht rückgängig gemacht werden." -#: extension.js:575 +#: extension.js:630 msgid "Clear" msgstr "Löschen" -#: extension.js:575 extension.js:740 +#: extension.js:630 extension.js:801 msgid "Cancel" msgstr "Abbrechen" -#: extension.js:588 +#: extension.js:645 msgid "Clipboard history cleared" -msgstr "Verlauf der Zwischenablage gelöscht" +msgstr "Der Verlauf der Zwischenablage wurde gelöscht" -#: extension.js:739 +#: extension.js:648 +msgid "Clipboard history cleared automatically" +msgstr "Der Verlauf der Zwischenablage wurde automatisch gelöscht" + +#: extension.js:800 msgid "Copied to clipboard" -msgstr "In Zwischenablage kopiert" +msgstr "In die Zwischenablage kopiert" -#: prefs.js:30 +#: prefs.js:31 msgid "History Size" -msgstr "Größe des Verlaufs" +msgstr "Verlaufsgröße" -#: prefs.js:39 +#: prefs.js:40 msgid "Preview Size (characters)" msgstr "Vorschaugröße (Zeichen)" -#: prefs.js:48 +#: prefs.js:49 msgid "Max cache file size (MB)" -msgstr "Maximale Größe der Cachedatei (MB)" +msgstr "Maximale Cache-Dateigröße (MB)" -#: prefs.js:57 +#: prefs.js:58 msgid "Number of characters in top bar" -msgstr "Anzahl der Zeichen in oberer Leiste" +msgstr "Anzahl der Zeichen in der oberen Leiste" -#: prefs.js:66 +#: prefs.js:67 msgid "What to show in top bar" -msgstr "Folgendes in oberer Leiste anzeigen" +msgstr "In der oberen Leiste anzeigen" -#: prefs.js:71 +#: prefs.js:72 msgid "Remove down arrow in top bar" -msgstr "Pfeil in oberer Leiste ausblenden" +msgstr "Pfeil nach unten in der oberen Leiste entfernen" -#: prefs.js:75 +#: prefs.js:76 msgid "Cache only pinned items" -msgstr "Nur Favoriten im Cache behalten" +msgstr "Nur angeheftete Elemente zwischenspeichern" -#: prefs.js:79 +#: prefs.js:80 msgid "Show notification on copy" msgstr "Benachrichtigung beim Kopieren anzeigen" -#: prefs.js:83 -#, fuzzy +#: prefs.js:84 msgid "Show notification on cycle" -msgstr "Benachrichtigung beim Kopieren anzeigen" +msgstr "Benachrichtigung beim Durchblättern anzeigen" -#: prefs.js:87 +#: prefs.js:88 msgid "Show confirmation on Clear History" msgstr "Bestätigung beim Löschen des Verlaufs anzeigen" -#: prefs.js:91 +#: prefs.js:92 msgid "Remove whitespace around text" -msgstr "Leerzeichen vor und nach dem Text entfernen" +msgstr "Leerzeichen um den Text entfernen" -#: prefs.js:95 +#: prefs.js:96 msgid "Move item to the top after selection" -msgstr "Eintrag nach Auswahl nach oben verschieben" +msgstr "Element nach der Auswahl nach oben verschieben" -#: prefs.js:99 +#: prefs.js:100 msgid "Keep selected entry after Clear History" -msgstr "Gewählten Eintrag beim Löschen des Verlaufs beibehalten" +msgstr "Ausgewählten Eintrag nach dem Löschen des Verlaufs beibehalten" -#: prefs.js:103 +#: prefs.js:104 msgid "Show paste buttons" -msgstr "Schaltflächen zum Einfügen anzeigen" +msgstr "Einfügeschaltflächen anzeigen" -#: prefs.js:104 +#: prefs.js:105 msgid "Adds a paste button to each entry that lets you paste it directly" -msgstr "Fügt jedem Eintrag eine Schaltfläche zum direkten Einfügen hinzu" +msgstr "Fügt jedem Eintrag eine Einfügeschaltfläche hinzu, mit der er direkt eingefügt werden kann" -#: prefs.js:108 +#: prefs.js:109 msgid "Place the pinned section on the bottom" -msgstr "Favoriten am Ende der Liste anzeigen" +msgstr "Angehefteten Bereich unten platzieren" -#: prefs.js:109 +#: prefs.js:110 msgid "Requires restarting the extension" -msgstr "Erfordert Neustart der Erweiterung" +msgstr "Erfordert einen Neustart der Erweiterung" -#: prefs.js:113 +#: prefs.js:114 msgid "Clear clipboard history on system reboot" -msgstr "Verlauf beim Neustart des Systems löschen" +msgstr "Verlauf der Zwischenablage beim Systemneustart löschen" -#: prefs.js:117 +#: prefs.js:118 msgid "Paste on select" -msgstr "" +msgstr "Beim Auswählen einfügen" -#: prefs.js:121 +#: prefs.js:122 msgid "Cache images" -msgstr "" +msgstr "Bilder im Verlauf speichern" -#: prefs.js:126 +#: prefs.js:127 msgid "Excluded Apps" -msgstr "" +msgstr "Ausgeschlossene Anwendungen" -#: prefs.js:127 +#: prefs.js:128 msgid "Content copied will not be saved while these apps are in focus" -msgstr "" +msgstr "Kopierte Inhalte werden nicht gespeichert, solange diese Anwendungen aktiv sind" + +#: prefs.js:139 +msgid "Case-sensitive search" +msgstr "Groß-/Kleinschreibung beachten" + +#: prefs.js:143 +msgid "Regular expression matching in search" +msgstr "Suche mit regulären Ausdrücken" + +#: prefs.js:156 +msgid "Clear clipboard history on interval" +msgstr "Verlauf der Zwischenablage in regelmäßigen Abständen löschen" -#: prefs.js:146 +#: prefs.js:160 +msgid "History clear interval (in minutes)" +msgstr "Intervall zum Löschen des Verlaufs (in Minuten)" + +#: prefs.js:172 msgid "UI" -msgstr "Oberfläche" +msgstr "Benutzeroberfläche" -#: prefs.js:147 +#: prefs.js:173 msgid "Behavior" msgstr "Verhalten" -#: prefs.js:148 +#: prefs.js:174 msgid "Exclusion" -msgstr "" +msgstr "Ausschluss" -#: prefs.js:149 +#: prefs.js:175 msgid "Limits" -msgstr "Grenzwerte" +msgstr "Grenzen" -#: prefs.js:150 +#: prefs.js:176 msgid "Topbar" msgstr "Obere Leiste" -#: prefs.js:151 +#: prefs.js:177 msgid "Notifications" msgstr "Benachrichtigungen" -#: prefs.js:152 +#: prefs.js:178 msgid "Shortcuts" -msgstr "Tastenkombinationen" +msgstr "Tastenkürzel" -#: prefs.js:207 +#: prefs.js:179 +msgid "Search" +msgstr "Suche" + +#: prefs.js:244 msgid "Icon" msgstr "Symbol" -#: prefs.js:208 +#: prefs.js:245 msgid "Clipboard Content" -msgstr "Inhalt der Zwischenablage" +msgstr "Zwischenablageinhalt" -#: prefs.js:209 +#: prefs.js:246 msgid "Both" -msgstr "Beides" +msgstr "Beide" -#: prefs.js:210 +#: prefs.js:247 msgid "Neither" -msgstr "Nichts" +msgstr "Keines" -#: prefs.js:221 +#: prefs.js:258 msgid "Toggle the menu" -msgstr "Menü öffnen/schließen" +msgstr "Menü ein-/ausblenden" -#: prefs.js:223 +#: prefs.js:260 msgid "Previous entry" msgstr "Vorheriger Eintrag" -#: prefs.js:224 +#: prefs.js:261 msgid "Next entry" msgstr "Nächster Eintrag" -#: prefs.js:229 +#: prefs.js:266 msgid "Enable shortcuts" -msgstr "Tastenkombinationen aktivieren" +msgstr "Tastenkürzel aktivieren" -#: prefs.js:254 +#: prefs.js:291 msgid "Disabled" msgstr "Deaktiviert" -#: prefs.js:263 +#: prefs.js:300 msgid "Enter shortcut" -msgstr "Tastenkombination eingeben" +msgstr "Tastenkürzel eingeben" -#: prefs.js:345 +#: prefs.js:387 msgid "Window class name, e.g. \"KeePassXC\"" -msgstr "" +msgstr "Fensterklassenname, z. B. \"KeePassXC\"" -#~ msgid "Refresh Interval (ms)" -#~ msgstr "Aktualisierungsintervall (ms)" +#: prefs.js:399 +msgid "Choose from installed applications" +msgstr "Aus installierten Anwendungen auswählen" -#~ msgid "Enable Deletion" -#~ msgstr "Löschen ermöglichen" +#: prefs.js:416 +msgid "Search applications..." +msgstr "Anwendungen suchen..." diff --git a/locale/es/LC_MESSAGES/clipboard-indicator.mo b/locale/es/LC_MESSAGES/clipboard-indicator.mo index 8bd949c9..70f43c32 100644 Binary files a/locale/es/LC_MESSAGES/clipboard-indicator.mo and b/locale/es/LC_MESSAGES/clipboard-indicator.mo differ diff --git a/locale/es/LC_MESSAGES/clipboard-indicator.po b/locale/es/LC_MESSAGES/clipboard-indicator.po index c69caf41..6639ba05 100644 --- a/locale/es/LC_MESSAGES/clipboard-indicator.po +++ b/locale/es/LC_MESSAGES/clipboard-indicator.po @@ -1,240 +1,261 @@ -# Spanish translation for gnome-shell-extension-clipboard-indicator. -# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the gnome-shell-extension-clipboard-indicator package. -# Javier Junquera , 2016. +# Spanish translations for PACKAGE package. +# Copyright (C) 2026 THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# Il Craccatore 2011, 2026. # msgid "" msgstr "" -"Project-Id-Version: gnome-shell-extension-clipboard-indicator\n" +"Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-21 16:41+0300\n" -"PO-Revision-Date: 2018-03-10 09:43-0600\n" -"Last-Translator: Adolfo Jayme Barrientos \n" -"Language-Team: \n" +"POT-Creation-Date: 2026-01-23 20:53+0100\n" +"PO-Revision-Date: 2026-01-24 15:28+0100\n" +"Last-Translator: IlCraccatore2011\n" "Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -"X-Generator: Poedit 2.0.6\n" -#: extension.js:108 +#: extension.js:113 msgid "Text will be here" msgstr "El texto aparecerá aquí" -#: extension.js:181 +#: extension.js:187 msgid "Type here to search..." -msgstr "Escriba aquí para buscar..." +msgstr "Escribe aquí para buscar..." -#: extension.js:250 prefs.js:220 +#: extension.js:256 prefs.js:257 msgid "Private mode" msgstr "Modo privado" -#: extension.js:264 prefs.js:222 +#: extension.js:270 prefs.js:259 msgid "Clear history" -msgstr "Vaciar historial" +msgstr "Borrar historial" -#: extension.js:276 +#: extension.js:315 msgid "Settings" msgstr "Configuración" -#: extension.js:299 +#: extension.js:338 msgid "Clipboard is empty" msgstr "El portapapeles está vacío" -#: extension.js:571 +#: extension.js:626 msgid "Clear all?" msgstr "¿Borrar todo?" -#: extension.js:572 +#: extension.js:627 msgid "Are you sure you want to delete all clipboard items?" -msgstr "¿Está seguro de que quiere borrar todos los ítems del portapapeles?" +msgstr "¿Seguro que quieres eliminar todos los elementos del portapapeles?" -#: extension.js:573 +#: extension.js:628 msgid "This operation cannot be undone." -msgstr "Esta operación no puede ser deshecha" +msgstr "Esta operación no se puede deshacer." -#: extension.js:575 +#: extension.js:630 msgid "Clear" msgstr "Borrar" -#: extension.js:575 extension.js:740 +#: extension.js:630 extension.js:801 msgid "Cancel" msgstr "Cancelar" -#: extension.js:588 +#: extension.js:645 msgid "Clipboard history cleared" -msgstr "Historial del portapapeles vaciado" +msgstr "El historial del portapapeles se ha borrado" -#: extension.js:739 +#: extension.js:648 +msgid "Clipboard history cleared automatically" +msgstr "El historial del portapapeles se ha borrado automáticamente" + +#: extension.js:800 msgid "Copied to clipboard" -msgstr "Copiado en el portapapeles" +msgstr "Copiado al portapapeles" -#: prefs.js:30 +#: prefs.js:31 msgid "History Size" msgstr "Tamaño del historial" -#: prefs.js:39 +#: prefs.js:40 msgid "Preview Size (characters)" -msgstr "Tamaño de previsualización (caracteres)" +msgstr "Tamaño de la vista previa (caracteres)" -#: prefs.js:48 +#: prefs.js:49 msgid "Max cache file size (MB)" -msgstr "Tamaño máximo de cache (MB)" +msgstr "Tamaño máximo del archivo de caché (MB)" -#: prefs.js:57 +#: prefs.js:58 msgid "Number of characters in top bar" msgstr "Número de caracteres en la barra superior" -#: prefs.js:66 +#: prefs.js:67 msgid "What to show in top bar" msgstr "Qué mostrar en la barra superior" -#: prefs.js:71 +#: prefs.js:72 msgid "Remove down arrow in top bar" -msgstr "Quitar la flecha hacia abajo de la barra de arriba" +msgstr "Quitar la flecha hacia abajo de la barra superior" -#: prefs.js:75 +#: prefs.js:76 msgid "Cache only pinned items" -msgstr "Cachear sólo los items anclados" +msgstr "Guardar en caché solo los elementos fijados" -#: prefs.js:79 +#: prefs.js:80 msgid "Show notification on copy" -msgstr "Mostrar una notificación al copiar" +msgstr "Mostrar notificación al copiar" -#: prefs.js:83 -#, fuzzy +#: prefs.js:84 msgid "Show notification on cycle" -msgstr "Mostrar una notificación al copiar" +msgstr "Mostrar notificación al recorrer el historial" -#: prefs.js:87 +#: prefs.js:88 msgid "Show confirmation on Clear History" -msgstr "Mostrar una confirmación al borrar el historial" +msgstr "Mostrar confirmación al borrar el historial" -#: prefs.js:91 +#: prefs.js:92 msgid "Remove whitespace around text" msgstr "Eliminar espacios en blanco alrededor del texto" -#: prefs.js:95 +#: prefs.js:96 msgid "Move item to the top after selection" -msgstr "Mover el ítem hacia arriba despues de seleccionarlo" +msgstr "Mover el elemento arriba después de seleccionarlo" -#: prefs.js:99 +#: prefs.js:100 msgid "Keep selected entry after Clear History" -msgstr "Mantener la entrada seleccionada después de limpiar el historial" +msgstr "Mantener la entrada seleccionada tras borrar el historial" -#: prefs.js:103 +#: prefs.js:104 msgid "Show paste buttons" -msgstr "Mostrar botones de pegar" +msgstr "Mostrar botones de pegado" -#: prefs.js:104 +#: prefs.js:105 msgid "Adds a paste button to each entry that lets you paste it directly" -msgstr "" -"Añade un botón de pegar a cada entrada, que te permite pegarlo directamente" +msgstr "Añade un botón de pegado a cada entrada que permite pegarla directamente" -#: prefs.js:108 +#: prefs.js:109 msgid "Place the pinned section on the bottom" -msgstr "Situar la sección anclada abajo" +msgstr "Colocar la sección fijada en la parte inferior" -#: prefs.js:109 +#: prefs.js:110 msgid "Requires restarting the extension" msgstr "Requiere reiniciar la extensión" -#: prefs.js:113 -#, fuzzy +#: prefs.js:114 msgid "Clear clipboard history on system reboot" -msgstr "Historial del portapapeles vaciado" +msgstr "Borrar el historial del portapapeles al reiniciar el sistema" -#: prefs.js:117 +#: prefs.js:118 msgid "Paste on select" -msgstr "" +msgstr "Pegar al seleccionar" -#: prefs.js:121 +#: prefs.js:122 msgid "Cache images" -msgstr "" +msgstr "Guardar imágenes en el historial" -#: prefs.js:126 +#: prefs.js:127 msgid "Excluded Apps" -msgstr "" +msgstr "Aplicaciones excluidas" -#: prefs.js:127 +#: prefs.js:128 msgid "Content copied will not be saved while these apps are in focus" -msgstr "" +msgstr "El contenido copiado no se guardará mientras estas aplicaciones estén activas" + +#: prefs.js:139 +msgid "Case-sensitive search" +msgstr "Búsqueda sensible a mayúsculas y minúsculas" + +#: prefs.js:143 +msgid "Regular expression matching in search" +msgstr "Búsqueda con expresiones regulares" + +#: prefs.js:156 +msgid "Clear clipboard history on interval" +msgstr "Borrar el historial del portapapeles periódicamente" + +#: prefs.js:160 +msgid "History clear interval (in minutes)" +msgstr "Intervalo de borrado del historial (en minutos)" -#: prefs.js:146 +#: prefs.js:172 msgid "UI" -msgstr "Interfaz de Usuario" +msgstr "Interfaz" -#: prefs.js:147 +#: prefs.js:173 msgid "Behavior" -msgstr "" +msgstr "Comportamiento" -#: prefs.js:148 +#: prefs.js:174 msgid "Exclusion" -msgstr "" +msgstr "Exclusión" -#: prefs.js:149 +#: prefs.js:175 msgid "Limits" -msgstr "Limites" +msgstr "Límites" -#: prefs.js:150 +#: prefs.js:176 msgid "Topbar" msgstr "Barra superior" -#: prefs.js:151 +#: prefs.js:177 msgid "Notifications" msgstr "Notificaciones" -#: prefs.js:152 +#: prefs.js:178 msgid "Shortcuts" -msgstr "Atajos de teclado" +msgstr "Atajos" + +#: prefs.js:179 +msgid "Search" +msgstr "Búsqueda" -#: prefs.js:207 +#: prefs.js:244 msgid "Icon" msgstr "Icono" -#: prefs.js:208 +#: prefs.js:245 msgid "Clipboard Content" msgstr "Contenido del portapapeles" -#: prefs.js:209 +#: prefs.js:246 msgid "Both" msgstr "Ambos" -#: prefs.js:210 +#: prefs.js:247 msgid "Neither" -msgstr "" +msgstr "Ninguno" -#: prefs.js:221 +#: prefs.js:258 msgid "Toggle the menu" -msgstr "Alternar menú" +msgstr "Mostrar/ocultar el menú" -#: prefs.js:223 +#: prefs.js:260 msgid "Previous entry" msgstr "Entrada anterior" -#: prefs.js:224 +#: prefs.js:261 msgid "Next entry" -msgstr "Entrada siguiente" +msgstr "Siguiente entrada" -#: prefs.js:229 +#: prefs.js:266 msgid "Enable shortcuts" -msgstr "Habilitar atajos de teclado" +msgstr "Habilitar atajos" -#: prefs.js:254 +#: prefs.js:291 msgid "Disabled" -msgstr "Deshabilitado" +msgstr "Desactivado" -#: prefs.js:263 +#: prefs.js:300 msgid "Enter shortcut" -msgstr "Introduce atajo de teclado" +msgstr "Introducir atajo" -#: prefs.js:345 +#: prefs.js:387 msgid "Window class name, e.g. \"KeePassXC\"" -msgstr "" +msgstr "Nombre de la clase de ventana, p. ej. \"KeePassXC\"" -#~ msgid "Refresh Interval (ms)" -#~ msgstr "Intervalo de actualización (ms)" +#: prefs.js:399 +msgid "Choose from installed applications" +msgstr "Elegir entre las aplicaciones instaladas" -#~ msgid "Disable cache file" -#~ msgstr "Desactivar archivo de antememoria" +#: prefs.js:416 +msgid "Search applications..." +msgstr "Buscar aplicaciones..." diff --git a/locale/fr_FR/LC_MESSAGES/clipboard-indicator.mo b/locale/fr_FR/LC_MESSAGES/clipboard-indicator.mo index f8ac1d8f..f7f8b3d0 100644 Binary files a/locale/fr_FR/LC_MESSAGES/clipboard-indicator.mo and b/locale/fr_FR/LC_MESSAGES/clipboard-indicator.mo differ diff --git a/locale/fr_FR/LC_MESSAGES/clipboard-indicator.po b/locale/fr_FR/LC_MESSAGES/clipboard-indicator.po index 38fd0b13..d87812cc 100644 --- a/locale/fr_FR/LC_MESSAGES/clipboard-indicator.po +++ b/locale/fr_FR/LC_MESSAGES/clipboard-indicator.po @@ -1,17 +1,20 @@ +# French translations for PACKAGE package. +# Copyright (C) 2026 THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# Il Craccatore 2011, 2026. +# msgid "" msgstr "" -"Project-Id-Version: unnamed project\n" +"Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-05-23 17:07+0200\n" -"PO-Revision-Date: 2025-05-23 17:15+0200\n" -"Last-Translator: p-sage <>\n" -"Language-Team: French\n" +"POT-Creation-Date: 2026-01-23 20:53+0100\n" +"PO-Revision-Date: 2026-01-24 15:22+0100\n" +"Last-Translator: IlCraccatore2011\n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" -"X-Generator: Poedit 3.6\n" #: extension.js:108 msgid "Text will be here" @@ -61,6 +64,10 @@ msgstr "Annuler" msgid "Clipboard history cleared" msgstr "L'historique du presse-papiers a été effacé" +#: extension.js:648 +msgid "Clipboard history cleared automatically" +msgstr "L'historique du presse-papiers a été effacé automatiquement" + #: extension.js:739 msgid "Copied to clipboard" msgstr "Copié dans le presse-papiers" @@ -157,6 +164,22 @@ msgstr "" "Le contenu copié ne sera pas enregistré tant que ces applications sont " "actives" +#: prefs.js:139 +msgid "Case-sensitive search" +msgstr "Recherche sensible à la casse" + +#: prefs.js:143 +msgid "Regular expression matching in search" +msgstr "Recherche avec expressions régulières" + +#: prefs.js:156 +msgid "Clear clipboard history on interval" +msgstr "Effacer l'historique du presse-papiers à intervalles réguliers" + +#: prefs.js:160 +msgid "History clear interval (in minutes)" +msgstr "Intervalle d'effacement de l'historique (en minutes)" + #: prefs.js:146 msgid "UI" msgstr "Interface" @@ -185,6 +208,10 @@ msgstr "Notifications" msgid "Shortcuts" msgstr "Raccourcis" +#: prefs.js:179 +msgid "Search" +msgstr "Recherche" + #: prefs.js:207 msgid "Icon" msgstr "Icône" @@ -228,3 +255,11 @@ msgstr "Entrer un raccourci" #: prefs.js:345 msgid "Window class name, e.g. \"KeePassXC\"" msgstr "Classe de fenêtre (ex. : « KeePassXC »)" + +#: prefs.js:399 +msgid "Choose from installed applications" +msgstr "Choisir parmi les applications installées" + +#: prefs.js:416 +msgid "Search applications..." +msgstr "Rechercher des applications…" diff --git a/locale/it/LC_MESSAGES/clipboard-indicator.mo b/locale/it/LC_MESSAGES/clipboard-indicator.mo index b83b301d..28c2f5ef 100644 Binary files a/locale/it/LC_MESSAGES/clipboard-indicator.mo and b/locale/it/LC_MESSAGES/clipboard-indicator.mo differ diff --git a/locale/it/LC_MESSAGES/clipboard-indicator.po b/locale/it/LC_MESSAGES/clipboard-indicator.po index b9efd1f1..0271a1fc 100644 --- a/locale/it/LC_MESSAGES/clipboard-indicator.po +++ b/locale/it/LC_MESSAGES/clipboard-indicator.po @@ -1,14 +1,14 @@ # Italian translations for PACKAGE package. -# Copyright (C) 2025 THE PACKAGE'S COPYRIGHT HOLDER +# Copyright (C) 2026 THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. -# Il Craccatore 2011, 2025. +# Il Craccatore 2011, 2026. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-05-23 11:43+0300\n" -"PO-Revision-Date: 2025-05-23 11:43+0300\n" +"POT-Creation-Date: 2026-01-23 21:05+0100\n" +"PO-Revision-Date: 2026-01-23 21:05+0100\n" "Last-Translator: Il Craccatore 2011\n" "Language-Team: none\n" "Language: it\n" @@ -17,214 +17,247 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: extension.js:108 +#: extension.js:113 msgid "Text will be here" msgstr "Il testo sarà qui" -#: extension.js:181 +#: extension.js:187 msgid "Type here to search..." msgstr "Digita qui per cercare..." -#: extension.js:250 prefs.js:220 +#: extension.js:256 prefs.js:257 msgid "Private mode" msgstr "Modalità privata" -#: extension.js:264 prefs.js:222 +#: extension.js:270 prefs.js:259 msgid "Clear history" -msgstr "Cancella la cronologia" +msgstr "Cancella cronologia" -#: extension.js:276 +#: extension.js:315 msgid "Settings" msgstr "Impostazioni" -#: extension.js:299 +#: extension.js:338 msgid "Clipboard is empty" msgstr "Appunti vuoti" -#: extension.js:571 +#: extension.js:626 msgid "Clear all?" msgstr "Cancellare tutto?" -#: extension.js:572 +#: extension.js:627 msgid "Are you sure you want to delete all clipboard items?" -msgstr "Sei sicuro di voler eliminare tutti gli elementi negli appunti?" +msgstr "Sei sicuro di voler eliminare tutti gli elementi degli appunti?" -#: extension.js:573 +#: extension.js:628 msgid "This operation cannot be undone." msgstr "Questa operazione non può essere annullata." -#: extension.js:575 +#: extension.js:630 msgid "Clear" msgstr "Cancella" -#: extension.js:575 extension.js:740 +#: extension.js:630 extension.js:801 msgid "Cancel" msgstr "Annulla" -#: extension.js:588 +#: extension.js:645 msgid "Clipboard history cleared" msgstr "Cronologia cancellata" -#: extension.js:739 +#: extension.js:648 +msgid "Clipboard history cleared automatically" +msgstr "Cronologia cancellata automaticamente" + +#: extension.js:800 msgid "Copied to clipboard" msgstr "Copiato negli appunti" -#: prefs.js:30 +#: prefs.js:31 msgid "History Size" -msgstr "Dimensioni della cronologia" +msgstr "Dimensione della cronologia" -#: prefs.js:39 +#: prefs.js:40 msgid "Preview Size (characters)" msgstr "Dimensione anteprima (caratteri)" -#: prefs.js:48 +#: prefs.js:49 msgid "Max cache file size (MB)" -msgstr "Dimensione massima della cache (MB)" +msgstr "Dimensione massima file cache (MB)" -#: prefs.js:57 +#: prefs.js:58 msgid "Number of characters in top bar" msgstr "Numero di caratteri nella barra superiore" -#: prefs.js:66 +#: prefs.js:67 msgid "What to show in top bar" msgstr "Cosa mostrare nella barra superiore" -#: prefs.js:71 +#: prefs.js:72 msgid "Remove down arrow in top bar" -msgstr "Rimuove la freccia verso il basso nella barra superiore" +msgstr "Rimuovi la freccia verso il basso nella barra superiore" -#: prefs.js:75 +#: prefs.js:76 msgid "Cache only pinned items" msgstr "Memorizza solo gli elementi fissati" -#: prefs.js:79 +#: prefs.js:80 msgid "Show notification on copy" msgstr "Mostra notifica quando si copia" -#: prefs.js:83 +#: prefs.js:84 msgid "Show notification on cycle" msgstr "Mostra notifica per voce già copiata" -#: prefs.js:87 +#: prefs.js:88 msgid "Show confirmation on Clear History" msgstr "Mostra conferma per cancellare la cronologia" -#: prefs.js:91 +#: prefs.js:92 msgid "Remove whitespace around text" msgstr "Rimuovi spazi bianchi attorno al testo" -#: prefs.js:95 +#: prefs.js:96 msgid "Move item to the top after selection" msgstr "Sposta l'elemento in cima dopo la selezione" -#: prefs.js:99 +#: prefs.js:100 msgid "Keep selected entry after Clear History" msgstr "Mantieni l'elemento selezionato dopo aver cancellato la cronologia" -#: prefs.js:103 +#: prefs.js:104 msgid "Show paste buttons" msgstr "Mostra i pulsanti di incolla" -#: prefs.js:104 +#: prefs.js:105 msgid "Adds a paste button to each entry that lets you paste it directly" msgstr "Aggiunge a ogni voce un pulsante Incolla per copiarla direttamente" -#: prefs.js:108 +#: prefs.js:109 msgid "Place the pinned section on the bottom" msgstr "Posiziona la sezione fissata in fondo" -#: prefs.js:109 +#: prefs.js:110 msgid "Requires restarting the extension" msgstr "Richiede il riavvio dell'estensione" -#: prefs.js:113 +#: prefs.js:114 msgid "Clear clipboard history on system reboot" msgstr "Cancella la cronologia al riavvio del sistema" -#: prefs.js:117 +#: prefs.js:118 msgid "Paste on select" msgstr "Incolla alla selezione" -#: prefs.js:121 +#: prefs.js:122 msgid "Cache images" msgstr "Copia le immagini" -#: prefs.js:126 +#: prefs.js:127 msgid "Excluded Apps" msgstr "App escluse" -#: prefs.js:127 +#: prefs.js:128 msgid "Content copied will not be saved while these apps are in focus" -msgstr "Il contenuto copiato non verrà salvato mentre queste app sono in primo piano" +msgstr "Il contenuto copiato non verrà salvato mentre queste applicazioni sono in primo piano" + +#: prefs.js:139 +msgid "Case-sensitive search" +msgstr "Ricerca con distinzione tra maiuscole e minuscole" -#: prefs.js:146 +#: prefs.js:143 +msgid "Regular expression matching in search" +msgstr "Corrispondenza con espressioni regolari nella ricerca" + +#: prefs.js:156 +msgid "Clear clipboard history on interval" +msgstr "Cancella la cronologia degli appunti a intervalli" + +#: prefs.js:160 +msgid "History clear interval (in minutes)" +msgstr "Intervallo di cancellazione della cronologia (in minuti)" + +#: prefs.js:172 msgid "UI" msgstr "Interfaccia" -#: prefs.js:147 +#: prefs.js:173 msgid "Behavior" msgstr "Comportamento" -#: prefs.js:148 +#: prefs.js:174 msgid "Exclusion" msgstr "Esclusione" -#: prefs.js:149 +#: prefs.js:175 msgid "Limits" msgstr "Limiti" -#: prefs.js:150 +#: prefs.js:176 msgid "Topbar" msgstr "Barra superiore" -#: prefs.js:151 +#: prefs.js:177 msgid "Notifications" msgstr "Notifiche" -#: prefs.js:152 +#: prefs.js:178 msgid "Shortcuts" msgstr "Scorciatoie" -#: prefs.js:207 +#: prefs.js:179 +msgid "Search" +msgstr "Ricerca" + +#: prefs.js:244 msgid "Icon" msgstr "Icona" -#: prefs.js:208 +#: prefs.js:245 msgid "Clipboard Content" msgstr "Contenuto appunti" -#: prefs.js:209 +#: prefs.js:246 msgid "Both" msgstr "Entrambi" -#: prefs.js:210 +#: prefs.js:247 msgid "Neither" msgstr "Nessuno" -#: prefs.js:221 +#: prefs.js:258 msgid "Toggle the menu" msgstr "Mostra/nascondi il menu" -#: prefs.js:223 +#: prefs.js:260 msgid "Previous entry" msgstr "Voce precedente" -#: prefs.js:224 +#: prefs.js:261 msgid "Next entry" msgstr "Voce successiva" -#: prefs.js:229 +#: prefs.js:266 msgid "Enable shortcuts" msgstr "Abilita scorciatoie" -#: prefs.js:254 +#: prefs.js:291 msgid "Disabled" msgstr "Disabilitato" -#: prefs.js:263 +#: prefs.js:300 msgid "Enter shortcut" msgstr "Inserisci scorciatoia" -#: prefs.js:345 +#: prefs.js:387 msgid "Window class name, e.g. \"KeePassXC\"" -msgstr "Nome classe della finestra, ad esempio \"KeePassXC\"" +msgstr "Nome della classe della finestra, ad es. \"KeePassXC\"" + +#: prefs.js:399 +msgid "Choose from installed applications" +msgstr "Scegli dalle applicazioni installate" + +#: prefs.js:416 +msgid "Search applications..." +msgstr "Cerca applicazioni..." + diff --git a/locale/ru/LC_MESSAGES/clipboard-indicator.mo b/locale/ru/LC_MESSAGES/clipboard-indicator.mo index 9af9b83f..0b215f4b 100644 Binary files a/locale/ru/LC_MESSAGES/clipboard-indicator.mo and b/locale/ru/LC_MESSAGES/clipboard-indicator.mo differ diff --git a/locale/ru/LC_MESSAGES/clipboard-indicator.po b/locale/ru/LC_MESSAGES/clipboard-indicator.po index dfdf420c..9639fd36 100644 --- a/locale/ru/LC_MESSAGES/clipboard-indicator.po +++ b/locale/ru/LC_MESSAGES/clipboard-indicator.po @@ -1,238 +1,262 @@ -# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# Russian translations for PACKAGE package. +# Copyright (C) 2026 THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. -# Dragonic , 2017. +# Il Craccatore 2011, 2026. # -# SPDX-FileCopyrightText: 2025 Mariya Shikunova msgid "" msgstr "" -"Project-Id-Version: \n" +"Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-21 16:41+0300\n" -"PO-Revision-Date: 2025-05-15 18:37+0300\n" -"Last-Translator: Mariya Shikunova \n" -"Language-Team: Basealt Translation Team\n" +"POT-Creation-Date: 2026-01-23 20:53+0100\n" +"PO-Revision-Date: 2026-01-24 15:35+0100\n" +"Last-Translator: IlCraccatore2011\n" "Language: ru\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " -"|| n%100>=20) ? 1 : 2);\n" -"X-Generator: Poedit 3.6\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" -#: extension.js:108 +#: extension.js:113 msgid "Text will be here" msgstr "Текст будет здесь" -#: extension.js:181 +#: extension.js:187 msgid "Type here to search..." -msgstr "Введите здесь для поиска..." +msgstr "Введите текст для поиска..." -#: extension.js:250 prefs.js:220 +#: extension.js:256 prefs.js:257 msgid "Private mode" msgstr "Приватный режим" -#: extension.js:264 prefs.js:222 +#: extension.js:270 prefs.js:259 msgid "Clear history" msgstr "Очистить историю" -#: extension.js:276 +#: extension.js:315 msgid "Settings" msgstr "Настройки" -#: extension.js:299 +#: extension.js:338 msgid "Clipboard is empty" msgstr "Буфер обмена пуст" -#: extension.js:571 +#: extension.js:626 msgid "Clear all?" msgstr "Очистить всё?" -#: extension.js:572 +#: extension.js:627 msgid "Are you sure you want to delete all clipboard items?" msgstr "Вы уверены, что хотите удалить все элементы буфера обмена?" -#: extension.js:573 +#: extension.js:628 msgid "This operation cannot be undone." msgstr "Эту операцию нельзя отменить." -#: extension.js:575 +#: extension.js:630 msgid "Clear" msgstr "Очистить" -#: extension.js:575 extension.js:740 +#: extension.js:630 extension.js:801 msgid "Cancel" -msgstr "Отменить" +msgstr "Отмена" -#: extension.js:588 +#: extension.js:645 msgid "Clipboard history cleared" msgstr "История буфера обмена очищена" -#: extension.js:739 +#: extension.js:648 +msgid "Clipboard history cleared automatically" +msgstr "История буфера обмена была очищена автоматически" + +#: extension.js:800 msgid "Copied to clipboard" msgstr "Скопировано в буфер обмена" -#: prefs.js:30 +#: prefs.js:31 msgid "History Size" msgstr "Размер истории" -#: prefs.js:39 +#: prefs.js:40 msgid "Preview Size (characters)" -msgstr "Длина строки в меню (символов)" +msgstr "Размер предпросмотра (символы)" -#: prefs.js:48 +#: prefs.js:49 msgid "Max cache file size (MB)" -msgstr "Максимальный размер кэша (мб)" +msgstr "Максимальный размер файла кэша (МБ)" -#: prefs.js:57 +#: prefs.js:58 msgid "Number of characters in top bar" msgstr "Количество символов в верхней панели" -#: prefs.js:66 +#: prefs.js:67 msgid "What to show in top bar" msgstr "Что показывать в верхней панели" -#: prefs.js:71 +#: prefs.js:72 msgid "Remove down arrow in top bar" msgstr "Убрать стрелку вниз в верхней панели" -#: prefs.js:75 +#: prefs.js:76 msgid "Cache only pinned items" -msgstr "Кэшировать только избранное" +msgstr "Кэшировать только закреплённые элементы" -#: prefs.js:79 +#: prefs.js:80 msgid "Show notification on copy" msgstr "Показывать уведомление при копировании" -#: prefs.js:83 +#: prefs.js:84 msgid "Show notification on cycle" -msgstr "Показывать уведомление при циклическом переключении" +msgstr "Показывать уведомление при переключении" -#: prefs.js:87 +#: prefs.js:88 msgid "Show confirmation on Clear History" -msgstr "Показывать уведомление при очистке истории" +msgstr "Показывать подтверждение при очистке истории" -#: prefs.js:91 +#: prefs.js:92 msgid "Remove whitespace around text" -msgstr "Удалить пробелы в тексте" +msgstr "Удалять пробелы вокруг текста" -#: prefs.js:95 +#: prefs.js:96 msgid "Move item to the top after selection" -msgstr "Переместить запись в начало после выделения" +msgstr "Перемещать элемент наверх после выбора" -#: prefs.js:99 +#: prefs.js:100 msgid "Keep selected entry after Clear History" -msgstr "Не очищать выбранную запись после очитски истории" +msgstr "Сохранять выбранный элемент после очистки истории" -#: prefs.js:103 +#: prefs.js:104 msgid "Show paste buttons" msgstr "Показывать кнопки вставки" -#: prefs.js:104 +#: prefs.js:105 msgid "Adds a paste button to each entry that lets you paste it directly" -msgstr "" -"Добавить кнопку вставки на каждую запись, позволяющая вставить напрямую" +msgstr "Добавляет кнопку вставки к каждому элементу для прямой вставки" -#: prefs.js:108 +#: prefs.js:109 msgid "Place the pinned section on the bottom" -msgstr "Переместить избранный элемент в конец истории" +msgstr "Разместить закреплённый раздел внизу" -#: prefs.js:109 +#: prefs.js:110 msgid "Requires restarting the extension" -msgstr "Требует перезагрузки расширения" +msgstr "Требуется перезапуск расширения" -#: prefs.js:113 +#: prefs.js:114 msgid "Clear clipboard history on system reboot" -msgstr "История буфера обмена очищена" +msgstr "Очищать историю буфера обмена при перезагрузке системы" -#: prefs.js:117 +#: prefs.js:118 msgid "Paste on select" -msgstr "Вставка при выделении" +msgstr "Вставлять при выборе" -#: prefs.js:121 +#: prefs.js:122 msgid "Cache images" -msgstr "Кеш изображений" +msgstr "Сохранять изображения в истории" -#: prefs.js:126 +#: prefs.js:127 msgid "Excluded Apps" msgstr "Исключённые приложения" -#: prefs.js:127 +#: prefs.js:128 msgid "Content copied will not be saved while these apps are in focus" -msgstr "" -"Скопированное содержимое не будет сохранено, пока эти приложения находятся в " -"фокусе" +msgstr "Скопированное содержимое не будет сохраняться, пока эти приложения активны" + +#: prefs.js:139 +msgid "Case-sensitive search" +msgstr "Поиск с учётом регистра" + +#: prefs.js:143 +msgid "Regular expression matching in search" +msgstr "Поиск с использованием регулярных выражений" + +#: prefs.js:156 +msgid "Clear clipboard history on interval" +msgstr "Очищать историю буфера обмена по интервалу" -#: prefs.js:146 +#: prefs.js:160 +msgid "History clear interval (in minutes)" +msgstr "Интервал очистки истории (в минутах)" + +#: prefs.js:172 msgid "UI" msgstr "Интерфейс" -#: prefs.js:147 +#: prefs.js:173 msgid "Behavior" msgstr "Поведение" -#: prefs.js:148 +#: prefs.js:174 msgid "Exclusion" -msgstr "Исключение" +msgstr "Исключения" -#: prefs.js:149 +#: prefs.js:175 msgid "Limits" msgstr "Ограничения" -#: prefs.js:150 +#: prefs.js:176 msgid "Topbar" msgstr "Верхняя панель" -#: prefs.js:151 +#: prefs.js:177 msgid "Notifications" msgstr "Уведомления" -#: prefs.js:152 +#: prefs.js:178 msgid "Shortcuts" -msgstr "Горячие клавиши" +msgstr "Сочетания клавиш" + +#: prefs.js:179 +msgid "Search" +msgstr "Поиск" -#: prefs.js:207 +#: prefs.js:244 msgid "Icon" msgstr "Значок" -#: prefs.js:208 +#: prefs.js:245 msgid "Clipboard Content" msgstr "Содержимое буфера обмена" -#: prefs.js:209 +#: prefs.js:246 msgid "Both" msgstr "Оба" -#: prefs.js:210 +#: prefs.js:247 msgid "Neither" msgstr "Ничего" -#: prefs.js:221 +#: prefs.js:258 msgid "Toggle the menu" -msgstr "Показать меню" +msgstr "Показать/скрыть меню" -#: prefs.js:223 +#: prefs.js:260 msgid "Previous entry" -msgstr "Предыдущая запись" +msgstr "Предыдущий элемент" -#: prefs.js:224 +#: prefs.js:261 msgid "Next entry" -msgstr "Следующая запись" +msgstr "Следующий элемент" -#: prefs.js:229 +#: prefs.js:266 msgid "Enable shortcuts" -msgstr "Горячие клавиши" +msgstr "Включить сочетания клавиш" -#: prefs.js:254 +#: prefs.js:291 msgid "Disabled" -msgstr "Выключено" +msgstr "Отключено" -#: prefs.js:263 +#: prefs.js:300 msgid "Enter shortcut" -msgstr "Ввести горячие клавиши" +msgstr "Введите сочетание клавиш" -#: prefs.js:345 +#: prefs.js:387 msgid "Window class name, e.g. \"KeePassXC\"" -msgstr "" +msgstr "Имя класса окна, например \"KeePassXC\"" + +#: prefs.js:399 +msgid "Choose from installed applications" +msgstr "Выбрать из установленных приложений" -#~ msgid "Refresh Interval (ms)" -#~ msgstr "Интервал обновления (мс)" +#: prefs.js:416 +msgid "Search applications..." +msgstr "Поиск приложений..." diff --git a/metadata.json b/metadata.json index e6b908b2..59cb2339 100644 --- a/metadata.json +++ b/metadata.json @@ -7,7 +7,7 @@ ], "uuid": "clipboard-indicator@tudmotu.com", "name": "Clipboard Indicator", - "description": "The most popular clipboard manager for GNOME, with over 1M downloads", + "description": "The most popular clipboard manager for GNOME, with over 2M downloads", "url": "https://github.com/Tudmotu/gnome-shell-extension-clipboard-indicator", "gettext-domain": "clipboard-indicator", "settings-schema": "org.gnome.shell.extensions.clipboard-indicator"