Skip to content

Conversation

w0nderfu11
Copy link

@w0nderfu11 w0nderfu11 commented Aug 20, 2025

Extend the Entry Editor context menu to handle entries with multiple linked files. This improves UX when managing more than one file per entry and aligns behavior across single- and multi-file scenarios.

Changes:

  • Made general actions in StandardActions enum for multi-file items
  • Extract selection predicates into SelectionChecks and reuse for actions
  • Introduce SingleSelectionMenuBuilder and MultiSelectionMenuBuilder (pattern strategy fix)
  • Extend ContextMenuFactory to delegate to builder based on selection
  • Refactor ContextAction: unified enablement by bindings
  • Remove CopyMultipleFilesAction and merged into CopySingleFileAction
  • Wire LinkedFilesEditor to new ContextMenuFactory; keep double-click behavior
  • Update localization: add pluralized keys, remove obsolete ones; localization tests pass
  • Add/update unit tests: ContextActionTest, ContextMenuFactoryTest, SingleSelectionMenuBuilderTest,
    MultiSelectionMenuBuilderTest, SelectionChecksTest
  • Guard LinkedFileViewModel to avoid a JavaFX crash

Fixes #12567
Keywords: context menu, linked files, multi-selection, UI

Steps to test

Mandatory checks

  • I own the copyright of the code submitted and I license it under the MIT license
  • Change in CHANGELOG.md described in a way that is understandable for the average user (if change is visible to the user)
  • Tests created for changes (if applicable)
  • Manually tested changed features in running JabRef (always required)
  • Screenshots added in PR description (if change is visible to the user)
  • Checked documentation (developer & user docs): information is available and up to date. If not, I created an issue at https://github.com/JabRef/user-documentation/issues/ (or submitted a PR)
image

Extend the Entry Editor context menu to handle entries with multiple
linked files. This improves UX when managing more than one file per
entry and aligns behavior across single- and multi-file scenarios.

Changes:
- Add plural actions to StandardActions enum
- Rewrite ContextAction.execute() for multi-file cases
- Extend ContextMenuFactory to build multi-file items
- Rework MultiContextAction to operate on selections
- Introduce CopyMultipleFilesAction (new class)
- Update/add localization keys; tests pass
- Add unit tests: ContextActionTest, ContextMenuFactoryTest,
  MultiContextActionTest, CopyMultipleFilesActionTest
- Guard LinkedFileViewModel to avoid a JavaFX crash

Fixes JabRef#12567
Keywords: context menu, linked files, multi-selection, UI
@w0nderfu11
Copy link
Author

image i set up checkstyle with your guide but it doesn`t show me some problems in test classes, only in main module)

@w0nderfu11 w0nderfu11 marked this pull request as ready for review August 20, 2025 14:08
@w0nderfu11
Copy link
Author

Hi @koppor, all checks have passed ✅ Could you please review and approve when you have time?
Thanks a lot!

@subhramit
Copy link
Member

subhramit commented Aug 20, 2025

i set up checkstyle with your guide but it doesn`t show me some problems in test classes, only in main module)

image

Check this setting on the right (screenshot taken from here)

@w0nderfu11
Copy link
Author

i set up checkstyle with your guide but it doesn`t show me some problems in test classes, only in main module)

image Check this setting on the right (screenshot taken from [here](https://devdocs.jabref.org/getting-into-the-code/guidelines-for-setting-up-a-local-workspace/intellij-13-code-style.html#put-jabrefs-checkstyle-configuration-in-place))

thank you anyway, but all check are done succesefully, so i wait for review

Copy link
Member

@calixtus calixtus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your PR so far.
Some remarks made.

I noticed the Strategy Pattern was somewhat broken there (not by you). Please fix it.
grafik


OPEN_DATABASE_FOLDER(Localization.lang("Reveal in file explorer")),
OPEN_FOLDER(Localization.lang("Open folder"), Localization.lang("Open folder"), IconTheme.JabRefIcons.FOLDER, KeyBinding.OPEN_FOLDER),
OPEN_FOLDERS(Localization.lang("Open folders"), Localization.lang("Open folders"), IconTheme.JabRefIcons.FOLDER, KeyBinding.OPEN_FOLDER),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The task description suggests renaming the commands instead of adding new ones.
grafik

Adding new StandardActions just with a little nuance in the name is increasing maintenance debt with no real benefit for the user. Please use the existing commands, just with different behaviour for multiple files.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I deleted additional commands, using only existing (except REMOVE_LINKS, it was implemented not by me so decided to let it go), fixed enum and localisation, implemented with Strategy pattern also

@calixtus
Copy link
Member

I mean reuse existing commands with changed text: eg 'Download file(s)'

@w0nderfu11
Copy link
Author

Thx for your feedback, i will research the problems you marked and try to solve, if it`s possible, thx a lot

w0nderfu11 and others added 2 commits September 1, 2025 16:50
…sActionTest.java

Co-authored-by: Carl Christian Snethlage <[email protected]>
…menu

- Introduce ContextMenuBuilder + SingleSelectionMenuBuilder + MultiSelectionMenuBuilder
- Extract shared checks/openContainingFolders into SelectionChecks
- ContextMenuFactory delegates to strategies; no more branching by selection size
- LinkedFilesEditor initializes ContextMenuFactory once and just requests menus on right-click
- Replace plural menu commands with single StandardActions; multi-selection handled inside builders
- Fix NPE in ContextAction executable binding by removing null observables and binding menu disable state properly
- Remove obsolete MultiContextAction and its tests

Follow-ups:
- Convert hardcoded labels to i18n keys (Download file(s), Open folder(s), etc.)
- Consider removing *_FILES actions from enum if unused elsewhere
- Re-add unit tests around builders/factory (TestFX/JUnit5) once API stabilized
@w0nderfu11
Copy link
Author

w0nderfu11 commented Sep 2, 2025

Thanks for your PR so far. Some remarks made.

I noticed the Strategy Pattern was somewhat broken there (not by you). Please fix it. grafik

ContextMenuFactory now delegates to ContextMenuBuilder strategies (SingleSelectionMenuBuilder / MultiSelectionMenuBuilder) with shared checks in SelectionChecks.

@w0nderfu11
Copy link
Author

All the review has been done, i will test it manually and do unit test, after make a PR if all is OK

Comment on lines +55 to +57
assertEquals(false,
builder.supports(FXCollections.observableArrayList()),
"Empty selection should not be supported");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The assertion should use assertFalse instead of assertEquals for better readability and to follow the special instruction on using plain JUnit assert.

Comment on lines +61 to +63
assertEquals(false,
builder.supports(single),
"Single selection should not be supported");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The assertion should use assertFalse instead of assertEquals for better readability and to follow the special instruction on using plain JUnit assert.

Comment on lines +55 to +57
assertEquals(false,
builder.supports(FXCollections.observableArrayList()),
"Empty selection should not be supported");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The use of assertEquals with boolean values is discouraged. Instead, assertFalse should be used for better readability and consistency with JUnit practices.

Comment on lines +67 to +69
assertEquals(true,
builder.supports(multiple),
"Multiple selection should be supported");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The assertion should use assertTrue instead of assertEquals for better readability and to follow the special instruction on using plain JUnit assert.

Comment on lines +67 to +69
assertEquals(true,
builder.supports(multiple),
"Multiple selection should be supported");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The use of assertEquals with boolean values is discouraged. Instead, assertTrue should be used for better readability and consistency with JUnit practices.

Comment on lines +123 to +124
assertEquals(false, itemsA.get(1).isDisable(),
"OPEN_FOLDER should be enabled when at least one local existing file is present");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The assertion should use assertFalse instead of assertEquals for better readability and to follow the special instruction on using plain JUnit assert.

Comment on lines +123 to +124
assertEquals(false, itemsA.get(1).isDisable(),
"OPEN_FOLDER should be enabled when at least one local existing file is present");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The use of assertEquals with boolean values is discouraged. Instead, assertFalse should be used for better readability and consistency with JUnit practices.

Comment on lines +131 to +132
assertEquals(true, itemsB.get(1).isDisable(),
"OPEN_FOLDER should be disabled when there are no local existing files");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The assertion should use assertTrue instead of assertEquals for better readability and to follow the special instruction on using plain JUnit assert.

Comment on lines +131 to +132
assertEquals(true, itemsB.get(1).isDisable(),
"OPEN_FOLDER should be disabled when there are no local existing files");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The use of assertEquals with boolean values is discouraged. Instead, assertTrue should be used for better readability and consistency with JUnit practices.

@w0nderfu11
Copy link
Author

w0nderfu11 commented Sep 29, 2025

@subhramit @calixtus i changed interface to class, fixed few moments (cause i did test inside GUI really long time ago), deleted variable we discussed, i let .max() to be cause all logic works correctly. Video is too big, so i prepared google drive link for you:

https://drive.google.com/file/d/14NRhxLdCbD-H9YNAt7p7ycETdTHrQBS_/view?usp=sharing

I had lags (caused not by JabRef app), so some moments caused by it (you will see in video)

P.S. bot now suggest to switch for assertTrue/False, but ihad it before, funny, i will ignore it if you don`t mind

in my point of view all is ok and usable enough, logic is correct, i tried to optimize all i could (i mean you Set for folders etc.)

@calixtus
Copy link
Member

Tragbot only offers suggestions. Just comment under it and close.

@calixtus
Copy link
Member

Comment in github about tragbot, not in code

@w0nderfu11
Copy link
Author

Comment in github about tragbot, not in code

its not in code i mean (i dont remember i did in ode, could be wrong)

w8iting for feedback

@subhramit
Copy link
Member

@subhramit @calixtus i changed interface to class, fixed few moments (cause i did test inside GUI really long time ago), deleted variable we discussed, i let .max() to be cause all logic works correctly. Video is too big, so i prepared google drive link for you:

https://drive.google.com/file/d/14NRhxLdCbD-H9YNAt7p7ycETdTHrQBS_/view?usp=sharing

I had lags (caused not by JabRef app), so some moments caused by it (you will see in video)

P.S. bot now suggest to switch for assertTrue/False, but ihad it before, funny, i will ignore it if you don`t mind

in my point of view all is ok and usable enough, logic is correct, i tried to optimize all i could (i mean you Set for folders etc.)

2:32 onwards, when doing "copy linked file(s) to folder", only one file is copied out of all selected. Why?

Comment on lines +35 to +36
public ContextMenu createMenuForSelection(ObservableList<LinkedFileViewModel> selection) {
Objects.requireNonNull(selection, "selection must not be null");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use jspecify annotations.
This has been mentioned in comment #13726 (comment) and demonstrated in commit c327bd8.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@subhramit will fix, ty

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static java.util.Objects.requireNonNull;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Use jspecify annotations
  2. Don't import static.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will fix, ty

menuItems.add(buildCopyToFolderItem(actionFactory, selection));
SelectionChecks checks = new SelectionChecks(databaseContext, preferences);

menuItems.add(batchCommandItem(actionFactory, StandardActions.OPEN_FILE, selection, _ -> true));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain this change to me? You changed this::isLocalAndExists to _ -> true??

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@subhramit it worked that for selected files (only LOCAL!) it was working correctly, i deleted predicate cause we can open all files, URL files and Local files, so now it works correctly

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, for the sake of clean and readable, code maybe just add a dummy method alwaysEnabled to SelectionChecks.

import static java.util.Objects.requireNonNull;

Logger LOGGER = LoggerFactory.getLogger(SelectionChecks.class);
public record SelectionChecks(BibDatabaseContext databaseContext, GuiPreferences preferences) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What was the rationale behind converting this to a record?
You are still using the methods here in the multiselectionmenubuilder class only. Why this additional layer of abstraction?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@subhramit

I CAN MERGE theese methods in multiselection menu builder if you want, i guess it will reduce code and abstraction as you said. IT was planned to use in other class for Single too but now there is no need.

Comment on lines -256 to +271
switch (keyBinding.get()) {
case DELETE_ENTRY:
deleteAttachedFilesWithConfirmation();
event.consume();
break;
default:
// Pass other keys to children
if (keyBinding.get() == KeyBinding.DELETE_ENTRY) {
deleteAttachedFilesWithConfirmation();
event.consume();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was the default case handling not needed anymore?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@subhramit there are 1 switch statement (1 case and default empty), i replaced it for the same exectly but with if statement which is more readable as for me

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We kept this deliberatly a switch to keep it consistent with keybindings check in other ui components.

@w0nderfu11
Copy link
Author

w0nderfu11 commented Sep 29, 2025

@subhramit @calixtus i changed interface to class, fixed few moments (cause i did test inside GUI really long time ago), deleted variable we discussed, i let .max() to be cause all logic works correctly. Video is too big, so i prepared google drive link for you:
https://drive.google.com/file/d/14NRhxLdCbD-H9YNAt7p7ycETdTHrQBS_/view?usp=sharing
I had lags (caused not by JabRef app), so some moments caused by it (you will see in video)
P.S. bot now suggest to switch for assertTrue/False, but ihad it before, funny, i will ignore it if you don`t mind
in my point of view all is ok and usable enough, logic is correct, i tried to optimize all i could (i mean you Set for folders etc.)

2:32 onwards, when doing "copy linked file(s) to folder", only one file is copied out of all selected. Why?

@subhramit this is case with 1 online file and 1 local file, copied only 1 local file, guess this is all explanation

i mean the online file is not downloaded

I need to change only things above right? Dont wanna spam with changes so lets make a deal what changes we need

@subhramit
Copy link
Member

subhramit commented Sep 29, 2025

@subhramit @calixtus i changed interface to class, fixed few moments (cause i did test inside GUI really long time ago), deleted variable we discussed, i let .max() to be cause all logic works correctly. Video is too big, so i prepared google drive link for you:
https://drive.google.com/file/d/14NRhxLdCbD-H9YNAt7p7ycETdTHrQBS_/view?usp=sharing
I had lags (caused not by JabRef app), so some moments caused by it (you will see in video)
P.S. bot now suggest to switch for assertTrue/False, but ihad it before, funny, i will ignore it if you don`t mind
in my point of view all is ok and usable enough, logic is correct, i tried to optimize all i could (i mean you Set for folders etc.)

2:32 onwards, when doing "copy linked file(s) to folder", only one file is copied out of all selected. Why?

@subhramit this is case with 1 online file and 1 local file, copied only 1 local file, guess this is all explanation

i mean the online file is not downloaded

I need to change only things above right? Dont wanna spam with changes so lets make a deal what changes we need

In the issue, the requirement comment says:

All files are copied to the selected destination folder

So this is wrong/misleading. The option doesn't say "Copy downloaded file(s) to to folder".
We have to make sure all the files selected are copied.

@w0nderfu11
Copy link
Author

@subhramit @calixtus i changed interface to class, fixed few moments (cause i did test inside GUI really long time ago), deleted variable we discussed, i let .max() to be cause all logic works correctly. Video is too big, so i prepared google drive link for you:
https://drive.google.com/file/d/14NRhxLdCbD-H9YNAt7p7ycETdTHrQBS_/view?usp=sharing
I had lags (caused not by JabRef app), so some moments caused by it (you will see in video)
P.S. bot now suggest to switch for assertTrue/False, but ihad it before, funny, i will ignore it if you don`t mind
in my point of view all is ok and usable enough, logic is correct, i tried to optimize all i could (i mean you Set for folders etc.)

2:32 onwards, when doing "copy linked file(s) to folder", only one file is copied out of all selected. Why?

@subhramit this is case with 1 online file and 1 local file, copied only 1 local file, guess this is all explanation
i mean the online file is not downloaded
I need to change only things above right? Dont wanna spam with changes so lets make a deal what changes we need

In the issue, the requirement comment says:

All files are copied to the selected destination folder

So this is wrong/misleading. The option doesn't say "Copy downloaded file(s) to to folder". We have to make sure all the files selected are copied.

I will do it available only for local tho

@subhramit
Copy link
Member

I will do it available only for local tho

I didn't get you, could you elaborate what you mean

@koppor
Copy link
Member

koppor commented Sep 29, 2025

This PR seems to be more hard than thought.

org.jabref.gui.copyfiles.CopyLinkedFilesAction#execute contains code which should be in logic. The interface of notifcation should be used, not the dialog - no showErrorAndWait.

I forgot the requirement that files should be moved -- e.g., when splitting a library into sub libraries.

Not sure if this is really good first issue.

@w0nderfu11
Copy link
Author

w0nderfu11 commented Sep 30, 2025

@subhramit in case you mentioned 1 file is in browser, thats why it isnt copied. In case of 1 local file (on SSD for example) and 1 file with link (as in example - NASA pdf file) selected only local can be copied. I will change the functionality, it will be available IF SELECTED ONLY LOCAL FILES on SSD (or HDD) or ther (simply to say - storage). IS THAT OK?

@koppor i will try to do it anyway, doesn`t matter first issue or not BUT! Could you please give a bit of explanation about you said splitting libraries

@subhramit @calixtus @koppor i will do changes we discussed. I really ask you to do after MY MAJOR CHANGES WILL BE PUSHED FULL REVIEW, to not extend work with it cause it took much more time i though and you i guess.

So would be nice if i push major changes we discussed above, you all left comments what we fix and after i will try to do it once (maybe will be not 1 fix but anyway it will reduce our time! It`s a proposition)

What guys do you think?

@calixtus calixtus added the dev: no-bot-comments If set, there should be no comments from our bots label Oct 3, 2025
@calixtus
Copy link
Member

calixtus commented Oct 3, 2025

Hi, i think the next iteration will probably be the last one. I just made some quick refactorings to ContextMenuFactoryTest to make it a bit more readable. It was just like a wall that hit me will all the declarations in the single tests.

Please apply the suggested changes. After that, we will finish this PR and get it merged. Most is already good and you did a great job in this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
component: entry-editor dev: no-bot-comments If set, there should be no comments from our bots
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Have the context menu of linked files working for multipe selected files.
5 participants