66
77import javafx .beans .binding .Bindings ;
88import javafx .beans .binding .BooleanBinding ;
9+ import javafx .beans .property .BooleanProperty ;
10+ import javafx .collections .ListChangeListener ;
911import javafx .css .PseudoClass ;
1012import javafx .fxml .FXML ;
1113import javafx .geometry .Insets ;
14+ import javafx .scene .Cursor ;
1215import javafx .scene .Node ;
1316import javafx .scene .control .Button ;
1417import javafx .scene .control .ButtonType ;
3538import org .jabref .gui .util .BaseDialog ;
3639import org .jabref .gui .util .NoSelectionModel ;
3740import org .jabref .gui .util .ViewModelListCellFactory ;
41+ import org .jabref .logic .importer .PagedSearchBasedFetcher ;
3842import org .jabref .logic .importer .ParserResult ;
43+ import org .jabref .logic .importer .SearchBasedFetcher ;
3944import org .jabref .logic .l10n .Localization ;
4045import org .jabref .logic .shared .DatabaseLocation ;
4146import org .jabref .logic .util .BackgroundTask ;
5358import org .fxmisc .richtext .CodeArea ;
5459
5560public class ImportEntriesDialog extends BaseDialog <Boolean > {
56-
61+ @ FXML private HBox paginationBox ;
62+ @ FXML private Label pageNumberLabel ;
5763 @ FXML private CheckListView <BibEntry > entriesListView ;
5864 @ FXML private ComboBox <BibDatabaseContext > libraryListView ;
5965 @ FXML private ButtonType importButton ;
@@ -64,10 +70,15 @@ public class ImportEntriesDialog extends BaseDialog<Boolean> {
6470 @ FXML private CheckBox showEntryInformation ;
6571 @ FXML private CodeArea bibTeXData ;
6672 @ FXML private VBox bibTeXDataBox ;
73+ @ FXML private Button nextPageButton ;
74+ @ FXML private Button prevPageButton ;
75+ @ FXML private Label statusLabel ;
6776
6877 private final BackgroundTask <ParserResult > task ;
6978 private final BibDatabaseContext database ;
7079 private ImportEntriesViewModel viewModel ;
80+ private final Optional <SearchBasedFetcher > searchBasedFetcher ;
81+ private final Optional <String > query ;
7182
7283 @ Inject private TaskExecutor taskExecutor ;
7384 @ Inject private DialogService dialogService ;
@@ -78,42 +89,61 @@ public class ImportEntriesDialog extends BaseDialog<Boolean> {
7889 @ Inject private FileUpdateMonitor fileUpdateMonitor ;
7990
8091 /**
81- * Imports the given entries into the given database. The entries are provided using the BackgroundTask
92+ * Creates an import dialog for entries from file sources.
93+ * This constructor is used for importing entries from local files, BibTeX files,
94+ * or other file-based sources that don't require pagination or search functionality.
8295 *
8396 * @param database the database to import into
8497 * @param task the task executed for parsing the selected files(s).
8598 */
8699 public ImportEntriesDialog (BibDatabaseContext database , BackgroundTask <ParserResult > task ) {
87100 this .database = database ;
88101 this .task = task ;
89- ViewLoader .view (this )
90- .load ()
91- .setAsDialogPane (this );
102+ this .searchBasedFetcher = Optional .empty ();
103+ this .query = Optional .empty ();
92104
93- BooleanBinding booleanBind = Bindings .isEmpty (entriesListView .getCheckModel ().getCheckedItems ());
94- Button btn = (Button ) this .getDialogPane ().lookupButton (importButton );
95- btn .disableProperty ().bind (booleanBind );
96-
97- downloadLinkedOnlineFiles .setSelected (preferences .getFilePreferences ().shouldDownloadLinkedFiles ());
105+ initializeDialog ();
106+ }
98107
99- setResultConverter (button -> {
100- if (button == importButton ) {
101- viewModel .importEntries (entriesListView .getCheckModel ().getCheckedItems (), downloadLinkedOnlineFiles .isSelected ());
102- } else {
103- dialogService .notify (Localization .lang ("Import canceled" ));
104- }
108+ /**
109+ * Creates an import dialog for entries from web-based search sources.
110+ * This constructor is used for importing entries that support pagination and require search queries.
111+ *
112+ * @param database database where the imported entries will be added
113+ * @param task task that handles parsing and loading entries from the search results
114+ * @param fetcher the search-based fetcher implementation used to retrieve entries from the web source
115+ * @param query the search string used to find relevant entries
116+ */
117+ public ImportEntriesDialog (BibDatabaseContext database , BackgroundTask <ParserResult > task , SearchBasedFetcher fetcher , String query ) {
118+ this .database = database ;
119+ this .task = task ;
120+ this .searchBasedFetcher = Optional .of (fetcher );
121+ this .query = Optional .of (query );
105122
106- return false ;
107- });
123+ initializeDialog ();
108124 }
109125
110126 @ FXML
111127 private void initialize () {
112- viewModel = new ImportEntriesViewModel (task , taskExecutor , database , dialogService , undoManager , preferences , stateManager , entryTypesManager , fileUpdateMonitor );
128+ viewModel = new ImportEntriesViewModel (task , taskExecutor , database , dialogService , undoManager , preferences , stateManager , entryTypesManager , fileUpdateMonitor , searchBasedFetcher , query );
113129 Label placeholder = new Label ();
114130 placeholder .textProperty ().bind (viewModel .messageProperty ());
115131 entriesListView .setPlaceholder (placeholder );
116132 entriesListView .setItems (viewModel .getEntries ());
133+ entriesListView .getCheckModel ().getCheckedItems ().addListener ((ListChangeListener <BibEntry >) change -> {
134+ while (change .next ()) {
135+ if (change .wasAdded ()) {
136+ for (BibEntry entry : change .getAddedSubList ()) {
137+ viewModel .getCheckedEntries ().add (entry );
138+ }
139+ }
140+ if (change .wasRemoved ()) {
141+ for (BibEntry entry : change .getRemoved ()) {
142+ viewModel .getCheckedEntries ().remove (entry );
143+ }
144+ }
145+ }
146+ });
117147
118148 libraryListView .setEditable (false );
119149 libraryListView .getItems ().addAll (stateManager .getOpenDatabases ());
@@ -180,14 +210,144 @@ private void initialize() {
180210 .withPseudoClass (entrySelected , entriesListView ::getItemBooleanProperty )
181211 .install (entriesListView );
182212
183- selectedItems .textProperty ().bind (Bindings .size (entriesListView . getCheckModel (). getCheckedItems ()).asString ());
184- totalItems .textProperty ().bind (Bindings .size (entriesListView . getItems ()).asString ());
213+ selectedItems .textProperty ().bind (Bindings .size (viewModel . getCheckedEntries ()).asString ());
214+ totalItems .textProperty ().bind (Bindings .size (viewModel . getAllEntries ()).asString ());
185215 entriesListView .setSelectionModel (new NoSelectionModel <>());
186216 initBibTeX ();
217+ if (searchBasedFetcher .isPresent ()) {
218+ updatePageUI ();
219+ setupPaginationBindings ();
220+ }
221+ }
222+
223+ private void initializeDialog () {
224+ ViewLoader .view (this )
225+ .load ()
226+ .setAsDialogPane (this );
227+
228+ paginationBox .setVisible (searchBasedFetcher .isPresent ());
229+ paginationBox .setManaged (searchBasedFetcher .isPresent ());
230+
231+ BooleanBinding booleanBind = Bindings .isEmpty (entriesListView .getCheckModel ().getCheckedItems ());
232+ Button btn = (Button ) this .getDialogPane ().lookupButton (importButton );
233+ btn .disableProperty ().bind (booleanBind );
234+
235+ downloadLinkedOnlineFiles .setSelected (preferences .getFilePreferences ().shouldDownloadLinkedFiles ());
236+
237+ setResultConverter (button -> {
238+ if (button == importButton ) {
239+ viewModel .importEntries (viewModel .getCheckedEntries ().stream ().toList (), downloadLinkedOnlineFiles .isSelected ());
240+ } else {
241+ dialogService .notify (Localization .lang ("Import canceled" ));
242+ }
243+
244+ return false ;
245+ });
246+ }
247+
248+ private void setupPaginationBindings () {
249+ BooleanProperty loading = viewModel .loadingProperty ();
250+ BooleanProperty initialLoadComplete = viewModel .initialLoadCompleteProperty ();
251+
252+ BooleanBinding isOnLastPage = Bindings .createBooleanBinding (() -> {
253+ int currentPage = viewModel .currentPageProperty ().get ();
254+ int totalPages = viewModel .totalPagesProperty ().get ();
255+ return currentPage >= totalPages - 1 ;
256+ }, viewModel .currentPageProperty (), viewModel .totalPagesProperty ());
257+
258+ BooleanBinding isPagedFetcher = Bindings .createBooleanBinding (() ->
259+ searchBasedFetcher .isPresent () && searchBasedFetcher .get () instanceof PagedSearchBasedFetcher
260+ );
261+
262+ // Disable: during loading OR when on the last page for non-paged fetchers
263+ // OR when the initial load is not complete for paged fetchers
264+ nextPageButton .disableProperty ().bind (
265+ loading .or (isOnLastPage .and (isPagedFetcher .not ()))
266+ .or (isPagedFetcher .and (initialLoadComplete .not ()))
267+ );
268+ prevPageButton .disableProperty ().bind (loading .or (viewModel .currentPageProperty ().isEqualTo (0 )));
269+
270+ prevPageButton .textProperty ().bind (
271+ Bindings .when (loading )
272+ .then ("< " + Localization .lang ("Loading..." ))
273+ .otherwise ("< " + Localization .lang ("Previous" ))
274+ );
275+
276+ nextPageButton .textProperty ().bind (
277+ Bindings .when (loading )
278+ .then (Localization .lang ("Loading..." ) + " >" )
279+ .otherwise (
280+ Bindings .when (initialLoadComplete .not ().and (isPagedFetcher ))
281+ .then (Localization .lang ("Loading initial entries..." ))
282+ .otherwise (
283+ Bindings .when (isOnLastPage )
284+ .then (
285+ Bindings .when (isPagedFetcher )
286+ .then (Localization .lang ("Load More" ) + " >>" )
287+ .otherwise (Localization .lang ("No more entries" ))
288+ )
289+ .otherwise (Localization .lang ("Next" ) + " >" )
290+ )
291+ )
292+ );
293+
294+ statusLabel .textProperty ().bind (
295+ Bindings .when (loading )
296+ .then (Localization .lang ("Fetching more entries..." ))
297+ .otherwise (
298+ Bindings .when (initialLoadComplete .not ().and (isPagedFetcher ))
299+ .then (Localization .lang ("Loading initial results..." ))
300+ .otherwise (
301+ Bindings .when (isOnLastPage )
302+ .then (
303+ Bindings .when (isPagedFetcher )
304+ .then (Localization .lang ("Click 'Load More' to fetch additional entries" ))
305+ .otherwise (Bindings .createStringBinding (() -> {
306+ int totalEntries = viewModel .getAllEntries ().size ();
307+ return totalEntries > 0 ?
308+ Localization .lang ("All %0 entries loaded" , String .valueOf (totalEntries )) :
309+ Localization .lang ("No entries available" );
310+ }, viewModel .getAllEntries ()))
311+ )
312+ .otherwise ("" )
313+ )
314+ )
315+ );
316+
317+ loading .addListener ((_ , _ , newVal ) -> {
318+ getDialogPane ().getScene ().setCursor (newVal ? Cursor .WAIT : Cursor .DEFAULT );
319+ });
320+
321+ isOnLastPage .addListener ((_ , oldVal , newVal ) -> {
322+ if (newVal && !oldVal ) {
323+ statusLabel .getStyleClass ().add ("info-message" );
324+ } else if (!newVal && oldVal ) {
325+ statusLabel .getStyleClass ().remove ("info-message" );
326+ }
327+ });
328+ }
329+
330+ private void updatePageUI () {
331+ pageNumberLabel .textProperty ().bind (Bindings .createStringBinding (() -> {
332+ int totalPages = viewModel .totalPagesProperty ().get ();
333+ int currentPage = viewModel .currentPageProperty ().get () + 1 ;
334+ if (totalPages != 0 ) {
335+ return Localization .lang ("%0 of %1" , currentPage , totalPages );
336+ }
337+ return "" ;
338+ }, viewModel .currentPageProperty (), viewModel .totalPagesProperty ()));
339+
340+ viewModel .getAllEntries ().addListener ((ListChangeListener <BibEntry >) change -> {
341+ while (change .next ()) {
342+ if (change .wasAdded () || change .wasRemoved ()) {
343+ viewModel .updateTotalPages ();
344+ }
345+ }
346+ });
187347 }
188348
189349 private void displayBibTeX (BibEntry entry , String bibTeX ) {
190- if (entriesListView . getCheckModel ().isChecked (entry )) {
350+ if (viewModel . getCheckedEntries ().contains (entry )) {
191351 bibTeXData .clear ();
192352 bibTeXData .appendText (bibTeX );
193353 bibTeXData .moveTo (0 );
@@ -208,14 +368,16 @@ private void initBibTeX() {
208368 }
209369
210370 public void unselectAll () {
211- entriesListView .getCheckModel ().clearChecks ();
371+ viewModel .getCheckedEntries ().clear ();
372+ entriesListView .getItems ().forEach (entry -> entriesListView .getCheckModel ().clearCheck (entry ));
212373 }
213374
214375 public void selectAllNewEntries () {
215376 unselectAll ();
216- for (BibEntry entry : entriesListView . getItems ()) {
377+ for (BibEntry entry : viewModel . getAllEntries ()) {
217378 if (!viewModel .hasDuplicate (entry )) {
218379 entriesListView .getCheckModel ().check (entry );
380+ viewModel .getCheckedEntries ().add (entry );
219381 displayBibTeX (entry , viewModel .getSourceString (entry ));
220382 }
221383 }
@@ -224,5 +386,40 @@ public void selectAllNewEntries() {
224386 public void selectAllEntries () {
225387 unselectAll ();
226388 entriesListView .getCheckModel ().checkAll ();
389+ viewModel .getCheckedEntries ().addAll (viewModel .getAllEntries ());
390+ }
391+
392+ private boolean isOnLastPageAndPagedFetcher () {
393+ if (searchBasedFetcher .isEmpty () || !(searchBasedFetcher .get () instanceof PagedSearchBasedFetcher )) {
394+ return false ;
395+ }
396+
397+ int currentPage = viewModel .currentPageProperty ().get ();
398+ int totalPages = viewModel .totalPagesProperty ().get ();
399+ return currentPage >= totalPages - 1 ;
400+ }
401+
402+ @ FXML
403+ private void onPrevPage () {
404+ viewModel .goToPrevPage ();
405+ restoreCheckedEntries ();
406+ }
407+
408+ @ FXML
409+ private void onNextPage () {
410+ if (isOnLastPageAndPagedFetcher ()) {
411+ viewModel .fetchMoreEntries ();
412+ } else {
413+ viewModel .goToNextPage ();
414+ }
415+ restoreCheckedEntries ();
416+ }
417+
418+ private void restoreCheckedEntries () {
419+ for (BibEntry entry : viewModel .getEntries ()) {
420+ if (viewModel .getCheckedEntries ().contains (entry )) {
421+ entriesListView .getCheckModel ().check (entry );
422+ }
423+ }
227424 }
228425}
0 commit comments