diff --git a/chrome/content/zotero/elements/noteEditor.js b/chrome/content/zotero/elements/noteEditor.js
index bafe54dffc..3a2ee687f4 100644
--- a/chrome/content/zotero/elements/noteEditor.js
+++ b/chrome/content/zotero/elements/noteEditor.js
@@ -89,6 +89,33 @@
'zotero/item': event.dataTransfer.getData('zotero/item')
}, this._iframe.contentWindow);
}, true);
+ // Same workaround for paste: the iframe's clipboardData.getData()
+ // returns empty for non-standard MIME types (zotero/annotation,
+ // zotero/item), so read them from the system clipboard here and
+ // proxy into the iframe scope.
+ this._iframe.contentWindow.addEventListener('paste', (_event) => {
+ let read = (flavor) => {
+ try {
+ let xfer = Components.classes['@mozilla.org/widget/transferable;1']
+ .createInstance(Components.interfaces.nsITransferable);
+ xfer.init(null);
+ xfer.addDataFlavor(flavor);
+ Components.classes['@mozilla.org/widget/clipboard;1']
+ .getService(Components.interfaces.nsIClipboard)
+ .getData(xfer, Components.interfaces.nsIClipboard.kGlobalClipboard);
+ let str = {};
+ xfer.getTransferData(flavor, str, {});
+ return str.value.QueryInterface(Components.interfaces.nsISupportsString).data;
+ }
+ catch (e) {
+ return '';
+ }
+ };
+ this._iframe.contentWindow.wrappedJSObject.pastedData = Components.utils.cloneInto({
+ 'zotero/annotation': read('zotero/annotation'),
+ 'zotero/item': read('zotero/item')
+ }, this._iframe.contentWindow);
+ }, true);
this._iframe.docShell.windowDraggingAllowed = true;
this._initialized = true;
});
diff --git a/chrome/content/zotero/fileInterface.js b/chrome/content/zotero/fileInterface.js
index 857e9cfa09..18862bbfae 100644
--- a/chrome/content/zotero/fileInterface.js
+++ b/chrome/content/zotero/fileInterface.js
@@ -248,7 +248,7 @@ var Zotero_File_Interface = new function () {
*/
this.writeToClipboard = function (content) {
if (!content) return;
- let { text, html } = content;
+ let { text, html, annotations } = content;
let transferable = Components.classes['@mozilla.org/widget/transferable;1']
.createInstance(Components.interfaces.nsITransferable);
@@ -270,6 +270,16 @@ var Zotero_File_Interface = new function () {
transferable.addDataFlavor('text/plain');
transferable.setTransferData('text/plain', str, text.length * 2);
}
+ // Rich annotation payload for the note editor's paste handler; without
+ // it, pasted annotations land as plain text with unresolved {citation}
+ // placeholders.
+ if (annotations) {
+ let str = Components.classes['@mozilla.org/supports-string;1']
+ .createInstance(Components.interfaces.nsISupportsString);
+ str.data = annotations;
+ transferable.addDataFlavor('zotero/annotation');
+ transferable.setTransferData('zotero/annotation', str, annotations.length * 2);
+ }
clipboardService.setData(transferable, null, Components.interfaces.nsIClipboard.kGlobalClipboard);
};
diff --git a/chrome/content/zotero/preferences/preferences_advanced.xhtml b/chrome/content/zotero/preferences/preferences_advanced.xhtml
index cc0901cfcc..f97a178e97 100644
--- a/chrome/content/zotero/preferences/preferences_advanced.xhtml
+++ b/chrome/content/zotero/preferences/preferences_advanced.xhtml
@@ -168,16 +168,16 @@
- &zotero.preferences.keys.copySelectedItemCitationsToClipboard;
+
-
- &zotero.preferences.keys.copySelectedItemsToClipboard;
+
-
&zotero.preferences.keys.toggleAllRead;
diff --git a/chrome/content/zotero/preferences/preferences_export.jsx b/chrome/content/zotero/preferences/preferences_export.jsx
index 2a1b315f06..040f46638e 100644
--- a/chrome/content/zotero/preferences/preferences_export.jsx
+++ b/chrome/content/zotero/preferences/preferences_export.jsx
@@ -505,22 +505,20 @@ Zotero_Preferences.Export = {
updateQuickCopyInstructions: function () {
- var prefix = Zotero.isMac ? Zotero.getString('general.keys.cmdShift') : Zotero.getString('general.keys.ctrlShift');
-
- var key = Zotero.Prefs.get('keys.copySelectedItemsToClipboard');
- var str = Zotero.getString('zotero.preferences.export.quickCopy.instructions', prefix + key);
- var instr = document.getElementById('quickCopy-instructions');
- while (instr.hasChildNodes()) {
- instr.removeChild(instr.firstChild);
- }
- instr.appendChild(document.createTextNode(str));
-
- key = Zotero.Prefs.get('keys.copySelectedItemCitationsToClipboard');
- str = Zotero.getString('zotero.preferences.export.quickCopy.citationInstructions', prefix + key);
- instr = document.getElementById('quickCopy-citationInstructions');
- while (instr.hasChildNodes()) {
- instr.removeChild(instr.firstChild);
- }
- instr.appendChild(document.createTextNode(str));
+ var shiftPrefix = Zotero.getString("command-or-control") + "+" + Zotero.getString("general-key-shift") + "+";
+
+ document.l10n.setAttributes(
+ document.getElementById('quickCopy-instructions'),
+ 'preferences-quickCopy-instructions',
+ {
+ bibShortcut: shiftPrefix + Zotero.Prefs.get('keys.copyAsBibliography'),
+ exportShortcut: shiftPrefix + Zotero.Prefs.get('keys.copyAsExport')
+ }
+ );
+ document.l10n.setAttributes(
+ document.getElementById('quickCopy-citationInstructions'),
+ 'preferences-quickCopy-citationInstructions',
+ { citationShortcut: Zotero.getString("command-or-control") + "+C" }
+ );
}
};
diff --git a/chrome/content/zotero/standalone/standalone.js b/chrome/content/zotero/standalone/standalone.js
index dfff443f2c..e4dad3c461 100644
--- a/chrome/content/zotero/standalone/standalone.js
+++ b/chrome/content/zotero/standalone/standalone.js
@@ -64,7 +64,7 @@ const ZoteroStandalone = new function () {
setTimeout(async () => {
// Item and other things might not be loaded yet when reopening tabs
await Zotero.Schema.schemaUpdatePromise;
- this.updateQuickCopyOptions();
+ ZoteroPane.updateQuickCopyMenu();
}, 0);
// "library", "reader" or "note"
let type = extraData[ids[0]].type;
@@ -95,12 +95,10 @@ const ZoteroStandalone = new function () {
return Zotero.initializationPromise;
})
.then(async function () {
- document.getElementById('key_copyCitation')
- .setAttribute('key', Zotero.Keys.getKeyForCommand('copySelectedItemCitationsToClipboard'));
document.getElementById('key_copyBibliography')
- .setAttribute('key', Zotero.Keys.getKeyForCommand('copySelectedItemsToClipboard'));
+ .setAttribute('key', Zotero.Keys.getKeyForCommand('copyAsBibliography'));
document.getElementById('key_copyExport')
- .setAttribute('key', Zotero.Keys.getKeyForCommand('copySelectedItemsToClipboardAsExport'));
+ .setAttribute('key', Zotero.Keys.getKeyForCommand('copyAsExport'));
document.getElementById('key_showTabsMenu')
.setAttribute('key', Zotero.Keys.getKeyForCommand('showTabsMenu'));
// Force menu to update with shortcut key at startup -- as of fx128, this is necessary
@@ -236,9 +234,10 @@ const ZoteroStandalone = new function () {
};
this.onEditMenuOpen = function (event) {
- this.updateQuickCopyOptions();
// goUpdateGlobalEditMenuItems(true) is necessary to update Edit menu when contenteditable is focused
+ // must happen before updateQuickCopyMenu so it knows the state of standard copy command
window.goUpdateGlobalEditMenuItems(true);
+ ZoteroPane.updateQuickCopyMenu();
this.onUpdateCustomMenus(event, 'edit');
};
@@ -337,53 +336,18 @@ const ZoteroStandalone = new function () {
};
- this.updateQuickCopyOptions = function () {
- var selected = [];
-
- let win = Zotero.getMainWindow();
- if (win) {
- try {
- selected = win.ZoteroPane.getSelectedItems();
- }
- catch (e) {
- }
- win.ZoteroPane.updateQuickCopyCommands(selected);
+ /**
+ * Handle consolidated Edit > Copy menu clicked.
+ * Native cmd_copy when text is selected, otherwise delegate to copySelectedItemsToClipboard
+ * that handles copying of items/notes/annotation in library and reader tabs.
+ */
+ this.handleCopyMenuCommand = function () {
+ if (ZoteroPane.classifyCopyContext() === 'text') {
+ // eslint-disable-next-line no-undef
+ goDoCommand('cmd_copy');
}
-
- var exportingNotes = selected.every(item => item.isNote() || item.isAttachment());
- var exportingAnnotations = selected.every(item => item.isAnnotation());
-
- var copyCitation = document.getElementById('menu_copyCitation');
- var copyBibliography = document.getElementById('menu_copyBibliography');
- var copyExport = document.getElementById('menu_copyExport');
- var copyNote = document.getElementById('menu_copyNote');
- var copyAnnotation = document.getElementById('menu_copyAnnotation');
-
- copyCitation.hidden = !selected.length || exportingNotes || exportingAnnotations;
- copyBibliography.hidden = !selected.length || exportingNotes || exportingAnnotations;
- copyExport.hidden = !selected.length || exportingNotes || exportingAnnotations;
- copyNote.hidden = !selected.length || !exportingNotes;
- copyAnnotation.hidden = !selected.length || !exportingAnnotations;
- document.l10n.setAttributes(copyAnnotation, "menu-edit-copy-annotation", { count: selected.length });
-
- // Label the export menu with the selected translator name
- var exportFormat = Zotero.QuickCopy.getFormat({ mode: 'export' });
- if (exportFormat.id) {
- try {
- let translator = Zotero.Translators.get(exportFormat.id);
- if (translator) {
- copyExport.label = Zotero.getString('quickCopy.copyAs', translator.label);
- }
- else {
- copyExport.hidden = true;
- }
- }
- catch (e) {
- if (!(e instanceof Zotero.Exception.UnloadedDataException && e.dataType == 'translators')) {
- Zotero.logError(e);
- }
- copyExport.hidden = true;
- }
+ else {
+ ZoteroPane.copySelectedItemsToClipboard(true, 'bibliography');
}
};
diff --git a/chrome/content/zotero/xpcom/prefs.js b/chrome/content/zotero/xpcom/prefs.js
index 9a2540a1c3..f297ae03aa 100644
--- a/chrome/content/zotero/xpcom/prefs.js
+++ b/chrome/content/zotero/xpcom/prefs.js
@@ -46,7 +46,7 @@ Zotero.Prefs = new function () {
// Process pref version updates
var fromVersion = this.get('prefVersion');
- var toVersion = 23;
+ var toVersion = 24;
if (!fromVersion) {
this.set('prefVersion', toVersion);
}
@@ -261,6 +261,21 @@ Zotero.Prefs = new function () {
this.clear('export.quickCopy.locale');
}
break;
+
+ case 24:
+ // Separate Cmd+Shift+A shortcut to copy citation is removed - now handled with Cmd/Ctrl + C
+ this.clear('keys.copySelectedItemCitationsToClipboard');
+ // Rename copySelectedItemsToClipboard to copyAsBibliography for clarity
+ if (this.prefHasUserValue("keys.copySelectedItemsToClipboard")) {
+ this.set("keys.copyAsBibliography", this.get("keys.copySelectedItemsToClipboard"));
+ this.clear("keys.copySelectedItemsToClipboard");
+ }
+ // Rename copySelectedItemsToClipboardAsExport to copyAsExport for consistency with the above
+ if (this.prefHasUserValue("keys.copySelectedItemsToClipboardAsExport")) {
+ this.set("keys.copyAsExport", this.get("keys.copySelectedItemsToClipboardAsExport"));
+ this.clear("keys.copySelectedItemsToClipboardAsExport");
+ }
+ break;
}
}
this.set('prefVersion', toVersion);
diff --git a/chrome/content/zotero/xpcom/reader.js b/chrome/content/zotero/xpcom/reader.js
index 7c48ae65bb..39d8c67d77 100644
--- a/chrome/content/zotero/xpcom/reader.js
+++ b/chrome/content/zotero/xpcom/reader.js
@@ -110,6 +110,40 @@ class ReaderInstance {
return state ? JSON.parse(JSON.stringify(state)) : undefined;
}
+ /**
+ * Keys of the annotations currently selected in the reader.
+ *
+ * @returns {String[]}
+ */
+ getSelectedAnnotationIDs() {
+ let ids = this._internalReader?._state?.selectedAnnotationIDs;
+ return ids ? Array.from(ids) : [];
+ }
+
+ /**
+ * What is currently selected in the reader's view(s), used to predict what
+ * a copy action will produce so the Edit menu and Cmd+C handler can label
+ * and behave accordingly.
+ *
+ * @returns {'annotation' | 'text' | null}
+ */
+ getSelectionType() {
+ if (!this._internalReader) return null;
+
+ if (this.getSelectedAnnotationIDs().length > 0) {
+ return 'annotation';
+ }
+
+ let view = this._internalReader._lastViewPrimary ? this._internalReader._primaryView : this._internalReader._secondaryView;
+ if (!view || !view._iframeWindow) return null;
+ let sel = view._iframeWindow.getSelection();
+ if (sel && !sel.isCollapsed && String(sel).length > 0) {
+ return 'text';
+ }
+
+ return null;
+ }
+
async migrateMendeleyColors(libraryID, annotations) {
let colorMap = new Map();
colorMap.set('#fff5ad', '#ffd400');
diff --git a/chrome/content/zotero/xpcom/utilities_internal.js b/chrome/content/zotero/xpcom/utilities_internal.js
index 6c04b30902..5e419fee2f 100644
--- a/chrome/content/zotero/xpcom/utilities_internal.js
+++ b/chrome/content/zotero/xpcom/utilities_internal.js
@@ -3425,7 +3425,11 @@ Zotero.Utilities.Internal.onDragItems = function (event, itemIDs, dragImage = ev
// Get Quick Copy format for current URL (set via /ping from connector)
// and the items being dragged. getContentFromItems handles the rest
// (annotation wrapping, note-citation reformatting, regular-item filter).
- let format = Zotero.QuickCopy.getFormat({ items });
+ // Shift-Drag means the user wants a citation cluster, so use bibliography
+ // mode then regardless of the pref
+ let format = event.shiftKey
+ ? Zotero.QuickCopy.getFormat({ mode: 'bibliography', items })
+ : Zotero.QuickCopy.getFormat({ items });
Zotero.debug("Dragging with format " + JSON.stringify(format));
try {
diff --git a/chrome/content/zotero/zoteroPane.js b/chrome/content/zotero/zoteroPane.js
index 1dba92c990..e446147285 100644
--- a/chrome/content/zotero/zoteroPane.js
+++ b/chrome/content/zotero/zoteroPane.js
@@ -1361,11 +1361,10 @@ var ZoteroPane = new function () {
break;
// Handled by s in standalone.js, pointing to s in zoteroPane.xul,
- // which are enabled or disabled by this.updateQuickCopyCommands(), called by
+ // which are enabled or disabled by this.updateQuickCopyMenu(), called by
// this.itemSelected()
- case 'copySelectedItemCitationsToClipboard':
- case 'copySelectedItemsToClipboard':
- case 'copySelectedItemsToClipboardAsExport':
+ case 'copyAsBibliography':
+ case 'copyAsExport':
return;
default:
@@ -1901,10 +1900,9 @@ var ZoteroPane = new function () {
this.itemPane.editable = this.collectionsView.editable;
this.itemPane.updateItemPaneButtons(selectedItems);
- // Tab selection observer in standalone.js makes sure that
- // updateQuickCopyCommands is called
+ // Keep the copy command + menu state current as the library selection changes
if (Zotero_Tabs.selectedType == 'library') {
- this.updateQuickCopyCommands(selectedItems);
+ this.updateQuickCopyMenu();
}
// Check if selection has actually changed. The onselect event that calls this
@@ -1996,29 +1994,65 @@ var ZoteroPane = new function () {
};
/**
- * Update the elements that control the shortcut keys and the enabled state of the
- * "Copy Citation"/"Copy Bibliography"/"Copy as"/"Copy Note" menu options. When disabled, the shortcuts are
- * still caught in handleKeyPress so that we can show an alert about not having references selected.
+ * Refresh the Edit-menu copy entries to match the current context.
+ * Sets `menu_copy` item's label and disabled state (Cmd/Ctrl+C), as well as
+ * Copy Bibliography and Export as ... disabled state.
*/
- this.updateQuickCopyCommands = function (selectedItems) {
- let canCopyBibliography = false;
- let canCopyExport = false;
- // If all items are notes/attachments and at least one note is not empty
- if (selectedItems.every(item => item.isNote() || item.isAttachment())) {
- if (selectedItems.some(item => item.note)) {
- canCopyBibliography = true;
- canCopyExport = true;
- }
- }
- else {
- canCopyBibliography = selectedItems.some(item => item.isRegularItem() || item.isAnnotation());
- canCopyExport = selectedItems.length > 0;
+ this.updateQuickCopyMenu = function () {
+ let selected = this.getSelectedItems();
+
+ // Consolidated Copy entry: reflect what Cmd+C would do in this context
+ let copyItem = document.getElementById('menu_copy');
+ let action = this.classifyCopyContext();
+ copyItem.disabled = false;
+ let count = selected.length;
+ document.l10n.setAttributes(copyItem, `menu-edit-copy-${action}`, { count });
+ // Default Copy -- mirror cmd_copy's enabled state
+ if (action === "text") {
+ copyItem.disabled = document.getElementById('cmd_copy').hasAttribute('disabled');
}
- document.getElementById('cmd_zotero_copyCitation').setAttribute('disabled', !canCopyBibliography);
- document.getElementById('cmd_zotero_copyBibliography').setAttribute('disabled', !canCopyBibliography);
- document.getElementById('cmd_zotero_copyExport').setAttribute('disabled', !canCopyExport);
- document.getElementById('cmd_zotero_copyAnnotation').setAttribute('disabled', !canCopyBibliography);
+ // Copy Bibliography / Copy as ...:
+ // - Library tab: only available when the selection contains regular items.
+ // - Reader tab: available when the open item has a parent regular item
+ // to act on.
+ let hideBibAndExport;
+ if (Zotero_Tabs.selectedType === 'reader') {
+ let reader = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID);
+ let hasMetadata = Zotero.Items.get(reader.itemID).parentItem;
+ hideBibAndExport = !hasMetadata;
+ }
+ else {
+ let exportingNotes = selected.every(item => item.isNote() || item.isAttachment());
+ let exportingAnnotations = selected.every(item => item.isAnnotation());
+ hideBibAndExport = !selected.length || exportingNotes || exportingAnnotations;
+ }
+ let copyBibliography = document.getElementById('menu_copyBibliography');
+ let copyExport = document.getElementById('menu_copyExport');
+ copyBibliography.hidden = hideBibAndExport;
+ copyExport.hidden = hideBibAndExport;
+ document.getElementById('cmd_zotero_copyBibliography').setAttribute('disabled', hideBibAndExport);
+ document.getElementById('cmd_zotero_copyExport').setAttribute('disabled', hideBibAndExport);
+
+ // Label "Copy as ..." with the selected export translator's name
+ let exportFormat = Zotero.QuickCopy.getFormat({ mode: 'export' });
+ if (exportFormat.id) {
+ try {
+ let translator = Zotero.Translators.get(exportFormat.id);
+ if (translator) {
+ copyExport.label = Zotero.getString('quickCopy.copyAs', translator.label);
+ }
+ else {
+ copyExport.hidden = true;
+ }
+ }
+ catch (e) {
+ if (!(e instanceof Zotero.Exception.UnloadedDataException && e.dataType == 'translators')) {
+ Zotero.logError(e);
+ }
+ copyExport.hidden = true;
+ }
+ }
};
@@ -2698,26 +2732,95 @@ var ZoteroPane = new function () {
this.copySelectedItemsToClipboard = function (asCitations, mode = 'bibliography') {
- let itemIDs = this.getSelectedItems(true);
- // Get selected item IDs in the item tree order
- itemIDs = this.getSortedItems(true).filter(id => itemIDs.includes(id));
- let items = Zotero.Items.get(itemIDs);
+ let items;
+ // "Copy Bibliography" / "Copy as ..." in the reader tab always act
+ // on the parent item being read -- annotation selection is for the
+ // smart-copy (Cmd+C / asCitations=true) path only.
+ if (!asCitations && Zotero_Tabs.selectedType === 'reader') {
+ let reader = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID);
+ let item = Zotero.Items.get(reader.itemID);
+ if (item.parentItem) item = item.parentItem;
+ items = [item];
+ }
+ else {
+ let itemIDs = this.getSelectedItems(true);
+ // Get selected item IDs in the item tree order
+ itemIDs = this.getSortedItems(true).filter(id => itemIDs.includes(id));
+ items = Zotero.Items.get(itemIDs);
+ }
if (!items.length) return;
let format = Zotero.QuickCopy.getFormat({ mode, items });
- // asCitations only applies to bibliography mode
- if (asCitations && format.mode !== 'bibliography') return;
-
- let content = Zotero.QuickCopy.getContentFromItems(items, format, { asCitations });
+ // asCitations only applies to bibliography mode; for notes/annotations
+ // the note format is used and asCitations is ignored
+ let content = Zotero.QuickCopy.getContentFromItems(items, format, {
+ asCitations: asCitations && format.mode === 'bibliography'
+ });
if (!content) {
Services.prompt.alert(null, "", Zotero.getString("fileInterface.noReferencesError"));
return;
}
+ // When copying annotations, also set the zotero/annotation flavor so
+ // the note editor's paste handler can render them with linked
+ // citations (matches the rich path used by drag-and-drop from the
+ // reader). Shape mirrors reader.js _getAnnotation -- the note
+ // editor's paste plugin expects `id` (not `key`) and normalized tags.
+ if (items.every(item => item.isAnnotation())) {
+ content.annotations = JSON.stringify(items.map((annotation) => {
+ let json = Zotero.Annotations.toJSONSync(annotation);
+ json.id = annotation.key;
+ delete json.key;
+ json.attachmentItemID = annotation.parentItemID;
+ json.tags = json.tags || [];
+ return json;
+ }));
+ }
Zotero_File_Interface.writeToClipboard(content);
}
-
-
+
+
+ /**
+ * Classify what Cmd/Ctrl+C in the current context should do
+ *
+ * @returns {'text' | 'citation' | 'note' | 'annotation'}
+ * - 'citation' if we should quick-copy as an in-text citation
+ * - 'note'/'annotation' if we should quick-copy via the note format
+ * - 'text' - default, copy should copy text
+ */
+ this.classifyCopyContext = function () {
+ let tab = Zotero_Tabs.getTabInfo();
+
+ if (tab.type === 'reader') {
+ let reader = Zotero.Reader.getByTabID(tab.id);
+ // Quick-copy only applies when the reader is focused
+ if (!reader?._iframeWindow?.document.hasFocus()) return 'text';
+ let hasMetadata = Zotero.Items.get(reader.itemID).parentItem;
+ let defaultMode = hasMetadata ? "citation" : "text";
+ // if nothing is selected in the reader, Copy should copy the citation if top level item exists
+ return reader.getSelectionType() || defaultMode;
+ }
+
+ if (tab.type === 'library') {
+ // Quick-copy only applies when the items tree owns focus
+ if (!this.itemsView.domEl.contains(document.activeElement)) {
+ return 'text';
+ }
+ let items = this.getSelectedItems();
+ if (!items.length) return 'text';
+ if (items.every(item => item.isAnnotation())) return 'annotation';
+ if (items.every(item => item.isNote() || item.isAttachment())) {
+ return items.some(item => item.note) ? 'note' : 'text';
+ }
+ if (items.some(item => item.isRegularItem() || item.isAnnotation())) {
+ return 'citation';
+ }
+ }
+
+ return 'text';
+ };
+
+
this.clearQuicksearch = async function (skipSearchRun) {
var search = document.getElementById('zotero-tb-search');
if (search.searchTextbox.value !== '') {
@@ -3089,6 +3192,8 @@ var ZoteroPane = new function () {
* Return an array of Item objects for selected items
*
* If asIDs is true, return an array of itemIDs instead
+ *
+ * For the reader tab, returns the selected annotation item(s) in sidebar, if any.
*/
this.getSelectedItems = function (asIDs) {
switch (Zotero_Tabs.selectedType) {
@@ -3104,6 +3209,13 @@ var ZoteroPane = new function () {
if (item.parentItem) {
item = item.parentItem;
}
+ // Return annotations selected in reader's sidebar if any
+ let selectedAnnotations = reader.getSelectedAnnotationIDs()
+ .map(id => Zotero.Items.getByLibraryAndKey(item.libraryID, id))
+ .filter(Boolean);
+ if (selectedAnnotations.length) {
+ return asIDs ? selectedAnnotations.map(item => item.id) : selectedAnnotations;
+ }
return asIDs ? [item.id] : [item];
}
return [];
diff --git a/chrome/content/zotero/zoteroPane.xhtml b/chrome/content/zotero/zoteroPane.xhtml
index 111b3fcafc..e57f760f97 100644
--- a/chrome/content/zotero/zoteroPane.xhtml
+++ b/chrome/content/zotero/zoteroPane.xhtml
@@ -103,18 +103,12 @@
-
-
@@ -169,9 +163,13 @@
key="&importCmd.key;"
command="cmd_zotero_importFromClipboard"
modifiers="accel shift alt"/>
-
+
+
@@ -376,13 +374,9 @@
key="key_cut"
command="cmd_cut" data-l10n-id="text-action-cut"/>
-
+ key="key_smartCopy"
+ oncommand="ZoteroStandalone.handleCopyMenuCommand();"
+ data-l10n-id="menu-edit-copy-text"/>
-
-
diff --git a/chrome/locale/en-US/zotero/preferences.ftl b/chrome/locale/en-US/zotero/preferences.ftl
index 2733bc0f6f..9b13faff35 100644
--- a/chrome/locale/en-US/zotero/preferences.ftl
+++ b/chrome/locale/en-US/zotero/preferences.ftl
@@ -1,6 +1,9 @@
preferences-window =
.title = { -app-name } Settings
+preferences-keys-copy-as-bibliography = Copy as Bibliography
+preferences-keys-copy-as-export = Copy in Export Format
+
preferences-appearance-title = Appearance and Language
preferences-auto-recognize-files =
@@ -49,6 +52,9 @@ preferences-locate-library-lookup-intro = Library Lookup can find a resource onl
preferences-locate-resolver = Resolver:
preferences-locate-base-url = Base URL:
+preferences-quickCopy-instructions = Quick Copy allows you to quickly export items in a given format. You can copy selected items to the clipboard in the selected bibliography style or export format by pressing { $bibShortcut } or { $exportShortcut } respectively. You can also drag items directly into a text box in another program.
+preferences-quickCopy-citationInstructions = You can also copy citations or footnotes in selected citation style by pressing { $citationShortcut } or holding down Shift before dragging items.
+
preferences-quickCopy-bibliographyFormat = Bibliography Style:
preferences-quickCopy-exportFormat = Export Format:
preferences-quickCopy-preferredFormatOnDrag = Format to use on drag:
diff --git a/chrome/locale/en-US/zotero/zotero.ftl b/chrome/locale/en-US/zotero/zotero.ftl
index df65a30cbf..37549c49dc 100644
--- a/chrome/locale/en-US/zotero/zotero.ftl
+++ b/chrome/locale/en-US/zotero/zotero.ftl
@@ -150,11 +150,20 @@ menu-view-note-tab-font-size =
menu-show-tabs-menu =
.label = Show Tabs Menu
+menu-edit-copy-citation =
+ .label = Copy Citation
+menu-edit-copy-note =
+ .label = { $count ->
+ [one] Copy Note
+ *[other] Copy { $count } Notes
+ }
menu-edit-copy-annotation =
.label = { $count ->
[one] Copy Annotation
- *[other] Copy { $count } Annotations
+ *[other] Copy { $count } Annotations
}
+menu-edit-copy-text =
+ .label = Copy
main-window-command =
.label = Library
diff --git a/chrome/locale/en-US/zotero/zotero.properties b/chrome/locale/en-US/zotero/zotero.properties
index 65f6fc7ce0..f0c778d06f 100644
--- a/chrome/locale/en-US/zotero/zotero.properties
+++ b/chrome/locale/en-US/zotero/zotero.properties
@@ -729,8 +729,6 @@ zotero.preferences.search.clearNonLinkedURLs = Clear All Except Web Links
zotero.preferences.search.indexUnindexed = Index Unindexed Items
zotero.preferences.export.quickCopy.citationStyles = Citation Styles
zotero.preferences.export.quickCopy.exportFormats = Export Formats
-zotero.preferences.export.quickCopy.instructions = Quick Copy allows you to quickly export items in a given format. You can copy selected items to the clipboard by pressing %S or drag items directly into a text box in another program.
-zotero.preferences.export.quickCopy.citationInstructions = For citation styles, you can copy citations or footnotes by pressing %S or holding down Shift before dragging items.
zotero.preferences.export.quickCopy.note.htmlOptions.label = Rich Text/HTML
zotero.preferences.wordProcessors.installationSuccess = Installation was successful.
diff --git a/defaults/preferences/zotero.js b/defaults/preferences/zotero.js
index 265b923521..2b6923edf7 100644
--- a/defaults/preferences/zotero.js
+++ b/defaults/preferences/zotero.js
@@ -99,9 +99,8 @@ pref("extensions.zotero.keys.newItem", "N");
pref("extensions.zotero.keys.newNote", "O");
pref("extensions.zotero.keys.library", "L");
pref("extensions.zotero.keys.quicksearch", "K");
-pref("extensions.zotero.keys.copySelectedItemCitationsToClipboard", "A");
-pref("extensions.zotero.keys.copySelectedItemsToClipboard", "C");
-pref("extensions.zotero.keys.copySelectedItemsToClipboardAsExport", "E");
+pref("extensions.zotero.keys.copyAsBibliography", "C");
+pref("extensions.zotero.keys.copyAsExport", "E");
pref("extensions.zotero.keys.sync", "Y");
pref("extensions.zotero.keys.toggleAllRead", "R");
pref("extensions.zotero.keys.toggleRead", "`");
diff --git a/test/tests/fileInterfaceTest.js b/test/tests/fileInterfaceTest.js
index 8d5ee62735..a571930791 100644
--- a/test/tests/fileInterfaceTest.js
+++ b/test/tests/fileInterfaceTest.js
@@ -306,7 +306,7 @@ describe("Zotero_File_Interface", function () {
});
});
- describe("#exportItemsToClipboard()", function () {
+ describe("Copy citations to clipboard", function () {
var noteItem, itemOne, itemTwo;
var styleID = 'http://www.zotero.org/styles/chicago-notes-bibliography';
diff --git a/test/tests/quickCopyTest.js b/test/tests/quickCopyTest.js
index 966566a883..ec3cf79740 100644
--- a/test/tests/quickCopyTest.js
+++ b/test/tests/quickCopyTest.js
@@ -349,4 +349,153 @@ describe("Zotero.QuickCopy", function () {
let content = Zotero.QuickCopy.getContentFromItems([item], format, { asCitations: true });
assert.equal(content.text, '《新型数据财产的行为主义保护:基于财产权理论的分析》。');
});
-})
+});
+
+
+describe("Smart copy", function () {
+ var win, zp, doc;
+ var regularItem, attachment, annotation, note;
+ var clipboardService;
+ const SMART_COPY_BIB = {
+ mode: 'bibliography',
+ id: 'http://www.zotero.org/styles/apa',
+ contentType: '',
+ locale: ''
+ };
+
+ before(async function () {
+ win = await loadZoteroPane();
+ zp = win.ZoteroPane;
+ doc = win.document;
+ clipboardService = Components.classes["@mozilla.org/widget/clipboard;1"]
+ .getService(Components.interfaces.nsIClipboard);
+
+ await Zotero.Styles.init();
+ await Zotero.Translators.init();
+
+ // Preload Note Markdown / Note HTML translators
+ for (let id of [
+ Zotero.Translators.TRANSLATOR_ID_NOTE_MARKDOWN,
+ Zotero.Translators.TRANSLATOR_ID_NOTE_HTML,
+ ]) {
+ let translator = Zotero.Translators.get(id);
+ translator.cacheCode = true;
+ await Zotero.Translators.getCodeForTranslator(translator);
+ }
+
+ regularItem = createUnsavedDataObject('item', {
+ itemType: 'journalArticle',
+ title: 'My Smart Copy Paper'
+ });
+ regularItem.setField('date', '2020');
+ regularItem.setCreators([
+ { firstName: 'Jane', lastName: 'Smartcopy', creatorType: 'author' }
+ ]);
+ await regularItem.saveTx();
+
+ attachment = await importFileAttachment('test.pdf', { parentItemID: regularItem.id });
+ annotation = await createAnnotation('highlight', attachment);
+
+ note = createUnsavedDataObject('item', { itemType: 'note' });
+ note.setNote('Test note content
');
+ await note.saveTx();
+ });
+
+ // The test runner has a root-level afterEach (test/content/runtests.js)
+ // that clears any user-set pref after every test. Re-pin the bibliography
+ // style here before each test.
+ beforeEach(function () {
+ Zotero.Prefs.set('export.quickCopy.bibliographySetting', JSON.stringify(SMART_COPY_BIB));
+ });
+
+ after(async function () {
+ win.Zotero_Tabs.closeAll();
+ win.close();
+ });
+
+ function getClipboardText() {
+ let transferable = Components.classes["@mozilla.org/widget/transferable;1"]
+ .createInstance(Components.interfaces.nsITransferable);
+ transferable.init(null);
+ transferable.addDataFlavor('text/plain');
+
+ clipboardService.getData(transferable, Components.interfaces.nsIClipboard.kGlobalClipboard);
+ let str = {};
+ transferable.getTransferData('text/plain', str, {});
+ return str.value.QueryInterface(Components.interfaces.nsISupportsString).data;
+ }
+
+ function clearClipboard() {
+ clipboardService.emptyClipboard(Components.interfaces.nsIClipboard.kGlobalClipboard);
+ }
+
+ describe("Library tab", function () {
+ beforeEach(async function () {
+ await selectLibrary(win);
+ doc.getElementById('item-tree-main').focus();
+ });
+
+ it("should copy citation for a regular item", async function () {
+ await zp.itemsView.selectItem(regularItem.id);
+ clearClipboard();
+ doc.getElementById('key_smartCopy').doCommand();
+ assert.equal(getClipboardText(), '(Smartcopy, 2020)');
+ });
+
+ it("should copy annotation content when an annotation is selected", async function () {
+ await zp.itemsView.selectItem(annotation.id);
+ clearClipboard();
+ doc.getElementById('key_smartCopy').doCommand();
+ assert.include(getClipboardText(), annotation.annotationText);
+ });
+
+ it("should copy note content when a note is selected", async function () {
+ await zp.itemsView.selectItem(note.id);
+ clearClipboard();
+ doc.getElementById('key_smartCopy').doCommand();
+ assert.include(getClipboardText(), 'Test note content');
+ });
+ });
+
+ describe("Reader tab", function () {
+ var reader;
+ var hasFocusStub;
+
+ before(async function () {
+ reader = await Zotero.Reader.open(attachment.id);
+ await reader._initPromise;
+ await reader._internalReader._primaryView.initializedPromise;
+ while (!reader._iframeWindow) {
+ await Zotero.Promise.delay(50);
+ }
+ // Pretend that the reader is focused
+ hasFocusStub = sinon.stub(reader._iframeWindow.document, 'hasFocus').returns(true);
+ });
+
+ beforeEach(async function () {
+ await win.Zotero_Tabs.select(reader.tabID);
+ });
+
+ after(function () {
+ hasFocusStub.restore();
+ });
+
+ it("should copy parent citation when nothing is selected in the reader", function () {
+ clearClipboard();
+ doc.getElementById('key_smartCopy').doCommand();
+ assert.equal(getClipboardText(), '(Smartcopy, 2020)');
+ });
+
+ it("should copy selected annotation", function () {
+ let stub = sinon.stub(reader, 'getSelectedAnnotationIDs').returns([annotation.key]);
+ try {
+ clearClipboard();
+ doc.getElementById('key_smartCopy').doCommand();
+ assert.include(getClipboardText(), annotation.annotationText);
+ }
+ finally {
+ stub.restore();
+ }
+ });
+ });
+});