1135 lines
39 KiB
JavaScript
1135 lines
39 KiB
JavaScript
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 CONFIRM_ON_CLEAR = true;
|
|
let MAX_TOPBAR_LENGTH = 15;
|
|
let TOPBAR_DISPLAY_MODE = 1; //0 - only icon, 1 - only clipboard content, 2 - both
|
|
let DISABLE_DOWN_ARROW = false;
|
|
let STRIP_TEXT = false;
|
|
let KEEP_SELECTED_ON_CLEAR = false;
|
|
let PASTE_BUTTON = true;
|
|
let PINNED_ON_BOTTOM = 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;
|
|
}
|
|
}
|
|
|
|
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(this._downArrow);
|
|
this.add_child(hbox);
|
|
this._createHistoryLabel();
|
|
this._loadSettings();
|
|
this.dialogManager = new DialogManager();
|
|
this._buildMenu().then(() => {
|
|
this._updateTopbarLayout();
|
|
this._setupListener();
|
|
});
|
|
}
|
|
|
|
#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(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_actor(that.favoritesSection.actor);
|
|
|
|
that.scrollViewFavoritesMenuSection.actor.add_actor(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_actor(that.historySection.actor);
|
|
|
|
that.scrollViewMenuSection.actor.add_actor(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
|
|
);
|
|
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 () {
|
|
let searchedText = this.searchEntry.get_text().toLowerCase();
|
|
|
|
if(searchedText === '') {
|
|
this._getAllIMenuItems().forEach(function(mItem){
|
|
mItem.actor.visible = true;
|
|
});
|
|
}
|
|
else {
|
|
this._getAllIMenuItems().forEach(function(mItem){
|
|
let text = mItem.clipContents.toLowerCase();
|
|
let 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) => {
|
|
if(event.get_key_symbol() === Clutter.KEY_Delete) {
|
|
this.#selectNextMenuItem(menuItem);
|
|
this._removeEntry(menuItem, 'delete');
|
|
}
|
|
else if (event.get_key_symbol() === Clutter.KEY_p) {
|
|
this.#selectNextMenuItem(menuItem);
|
|
this._favoriteToggle(menuItem);
|
|
}
|
|
else if (event.get_key_symbol() === Clutter.KEY_v) {
|
|
this.#pasteItem(menuItem);
|
|
}
|
|
})
|
|
|
|
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 () {
|
|
// Don't remove pinned items
|
|
this.historySection._getMenuItems().forEach(mItem => {
|
|
if (KEEP_SELECTED_ON_CLEAR === false || !mItem.currentlySelected) {
|
|
this._removeEntry(mItem, 'delete');
|
|
}
|
|
});
|
|
this._showNotification(_("Clipboard history cleared"));
|
|
}
|
|
|
|
_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.
|
|
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);
|
|
});
|
|
}
|
|
|
|
_openSettings () {
|
|
this.extension.openSettings();
|
|
}
|
|
|
|
_initNotifSource () {
|
|
if (!this._notifSource) {
|
|
this._notifSource = new MessageTray.Source('ClipboardIndicator',
|
|
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(this._notifSource, message);
|
|
}
|
|
else {
|
|
notification = this._notifSource.notifications[0];
|
|
notification.update(message, '', { clear: true });
|
|
}
|
|
|
|
if (typeof transformFn === 'function') {
|
|
transformFn(notification);
|
|
}
|
|
|
|
notification.setTransient(true);
|
|
this._notifSource.showNotification(notification);
|
|
}
|
|
|
|
_createHistoryLabel () {
|
|
this._historyLabel = new St.Label({
|
|
style_class: 'ci-notification-label',
|
|
text: ''
|
|
});
|
|
|
|
global.stage.add_actor(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);
|
|
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);
|
|
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);
|
|
|
|
}
|
|
|
|
async _onSettingsChange () {
|
|
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();
|
|
}
|
|
|
|
_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;
|
|
}
|
|
if(TOPBAR_DISPLAY_MODE === 1){
|
|
this.icon.visible = false;
|
|
this._buttonText.visible = true;
|
|
}
|
|
if(TOPBAR_DISPLAY_MODE === 2){
|
|
this.icon.visible = true;
|
|
this._buttonText.visible = true;
|
|
}
|
|
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;
|
|
}
|
|
|
|
_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
|
|
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
|
|
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);
|
|
}
|
|
|
|
#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',
|
|
'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;
|
|
}
|
|
|
|
const entry = new ClipboardEntry(type, bytes.get_data(), false);
|
|
if (entry.isImage()) {
|
|
this.registry.writeEntryFile(entry);
|
|
}
|
|
resolve(entry);
|
|
}));
|
|
|
|
if (result) return result;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
});
|