context-aware consolidated quick-copy command

Menu > Copy and Cmd/Ctrl + C are now one entry point
for context-aware copying of selected items.

Updated behavior:
- when a regular item is selected, copy the citation
- when a note or annotation is selected, copy them
via quickCopy in note format
- when a reader tab is open and nothing is selected,
copy the citation of the top-level item
- when a reader tab is open and an annotation is selected,
copy the annotation
- when a reader tab is open and text is selected,
select the text - no changes here.

Depending on which items are selected, Menu > Copy
command labels will be updated to Menu > Copy Citation or
Menu > Copy Note, or Menu > Copy Annotations. Added
helpers to the reader and ZoteroPane to indicate what
Cmd/Ctrl+C is supposed to do based on the selection
(classifyCopyContext), so we can determine what label to use.

Removed old Cmd-Shift-A "Copy Citation" shortcut.

Updated preferences Export panel text, renamed preferences
to "copyAsBibliography" and "copyAsExport" for clarity.
This commit is contained in:
Bogdan Abaev 2026-05-25 14:51:31 -07:00
parent ac6c26096e
commit 99dbdfa9ac
16 changed files with 458 additions and 149 deletions

View file

@ -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;
});

View file

@ -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);
};

View file

@ -168,16 +168,16 @@
<html:label class="modifier"/>
<html:input type="text" id="textbox-quicksearch" maxlength="1" size="1" preference="extensions.zotero.keys.quicksearch"/>
<html:label for="textbox-copySelectedItemCitationsToClipboard">&zotero.preferences.keys.copySelectedItemCitationsToClipboard;</html:label>
<html:label for="textbox-copyAsBibliography" data-l10n-id="preferences-keys-copy-as-bibliography"/>
<html:label class="modifier"/>
<html:input type="text" id="textbox-copySelectedItemCitationsToClipboard" maxlength="1" size="1"
preference="extensions.zotero.keys.copySelectedItemCitationsToClipboard"
<html:input type="text" id="textbox-copyAsBibliography" maxlength="1" size="1"
preference="extensions.zotero.keys.copyAsBibliography"
oninput="if (Zotero_Preferences.Export) { Zotero_Preferences.Export.updateQuickCopyInstructions(); }"/>
<html:label for="textbox-copySelectedItemsToClipboard">&zotero.preferences.keys.copySelectedItemsToClipboard;</html:label>
<html:label for="textbox-copyAsExport" data-l10n-id="preferences-keys-copy-as-export"/>
<html:label class="modifier"/>
<html:input type="text" id="textbox-copySelectedItemsToClipboard" maxlength="1" size="1"
preference="extensions.zotero.keys.copySelectedItemsToClipboard"
<html:input type="text" id="textbox-copyAsExport" maxlength="1" size="1"
preference="extensions.zotero.keys.copyAsExport"
oninput="if (Zotero_Preferences.Export) { Zotero_Preferences.Export.updateQuickCopyInstructions(); }"/>
<html:label for="textbox-toggleAllRead">&zotero.preferences.keys.toggleAllRead;</html:label>

View file

@ -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" }
);
}
};

View file

@ -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');
}
};

View file

@ -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);

View file

@ -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');

View file

@ -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 {

View file

@ -1361,11 +1361,10 @@ var ZoteroPane = new function () {
break;
// Handled by <key>s in standalone.js, pointing to <command>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 <command> 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 [];

View file

@ -103,18 +103,12 @@
<command id="cmd_zotero_importFromClipboard" oncommand="Zotero_File_Interface.importFromClipboard();"/>
<command id="cmd_zotero_exportLibrary" oncommand="Zotero_File_Interface.exportFile();"/>
<command id="cmd_zotero_advancedSearch" oncommand="ZoteroPane_Local.openAdvancedSearchWindow();"/>
<command id="cmd_zotero_copyCitation"
oncommand="ZoteroPane_Local.copySelectedItemsToClipboard(true, 'bibliography');"
disabled="true"/>
<command id="cmd_zotero_copyBibliography"
oncommand="ZoteroPane_Local.copySelectedItemsToClipboard(false, 'bibliography');"
disabled="true"/>
<command id="cmd_zotero_copyExport"
oncommand="ZoteroPane_Local.copySelectedItemsToClipboard(false, 'export');"
disabled="true"/>
<command id="cmd_zotero_copyAnnotation"
oncommand="ZoteroPane_Local.copySelectedItemsToClipboard();"
disabled="true"/>
<command id="cmd_zotero_createTimeline" oncommand="Zotero_Timeline_Interface.loadTimeline();"/>
<command id="cmd_zotero_rtfScan" oncommand="window.openDialog('chrome://zotero/content/rtfScan.xhtml', 'rtfScan', 'chrome,centerscreen')"/>
<command id="cmd_zotero_newCollection" oncommand="ZoteroPane_Local.newCollection(ZoteroPane_Local.getSelectedCollection()?.key)"/>
@ -169,9 +163,13 @@
key="&importCmd.key;"
command="cmd_zotero_importFromClipboard"
modifiers="accel shift alt"/>
<key id="key_copyCitation"
command="cmd_zotero_copyCitation"
modifiers="accel shift"/>
<!-- Use inline `oncommand`, not a shared <command>: routing through a
<command> consumes the keypress so the reader iframe never gets
its native copy event. menu_copy duplicates the handler call. -->
<key id="key_smartCopy"
key="C"
modifiers="accel"
oncommand="ZoteroStandalone.handleCopyMenuCommand();"/>
<key id="key_copyBibliography"
command="cmd_zotero_copyBibliography"
modifiers="accel shift"/>
@ -376,13 +374,9 @@
key="key_cut"
command="cmd_cut" data-l10n-id="text-action-cut"/>
<menuitem id="menu_copy"
key="key_copy"
command="cmd_copy" data-l10n-id="text-action-copy"/>
<menuitem id="menu_copyCitation"
label="&copyCitationCmd.label;"
command="cmd_zotero_copyCitation"
key="key_copyCitation"
hidden="true"/>
key="key_smartCopy"
oncommand="ZoteroStandalone.handleCopyMenuCommand();"
data-l10n-id="menu-edit-copy-text"/>
<menuitem id="menu_copyBibliography"
label="&copyBibliographyCmd.label;"
command="cmd_zotero_copyBibliography"
@ -393,16 +387,6 @@
key="key_copyExport"
command="cmd_zotero_copyExport"
hidden="true"/>
<menuitem id="menu_copyNote"
label="&copyNoteCmd.label;"
command="cmd_zotero_copyBibliography"
key="key_copyBibliography"
hidden="true"/>
<menuitem id="menu_copyAnnotation"
data-l10n-id="menu-edit-copy-annotation"
command="cmd_zotero_copyAnnotation"
key="key_copyBibliography"
hidden="true"/>
<menuitem id="menu_paste"
key="key_paste"
command="cmd_paste" data-l10n-id="text-action-paste"/>

View file

@ -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:

View file

@ -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

View file

@ -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.

View file

@ -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", "`");

View file

@ -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';

View file

@ -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('<p>Test note content</p>');
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();
}
});
});
});